VM upvalue support: closures capture variables from enclosing scopes

Compiler (compiler.sx):
- Function scopes marked is-function=true; let scopes share parent frame
- scope-resolve only creates upvalue captures at function boundaries
- Let scope locals use parent's slot numbering (same frame)
- OP_CLOSURE emits upvalue descriptors: (is_local, index) per capture

VM (sx_vm.ml):
- upvalue_cell type: shared mutable reference to captured value
- OP_UPVALUE_GET/SET: read/write from closure's upvalue array
- OP_CLOSURE: reads upvalue descriptors, creates cells from
  enclosing frame's locals (is_local=1) or upvalues (is_local=0)
- vm_closure carries live env_ref (not snapshot)
- vm_call falls back to CEK for Lambda/Component/Island values

Verified: (let ((x 10)) (let ((add-x (fn (y) (+ x y)))) (add-x 5)))
  Compiles to: CONST 10, LOC_SET #0, CLOSURE [UV_GET#0 LOC_GET#0 CPRIM+ RET]
  with upvalue descriptor: is_local=1 index=0
  VM executes → 15 ✓

Auto-compile: 6/117 functions compile (up from 3). Disabled until
compiler handles all features — fallback can't reconstruct closure
scope for variables like nav-state bound in caller's let*.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 20:13:17 +00:00
parent a62b7c8a5e
commit 1bb40415a8
4 changed files with 135 additions and 64 deletions

View File

@@ -820,18 +820,21 @@ let dispatch env cmd =
match result with
| Dict d when Hashtbl.mem d "bytecode" ->
let code = Sx_vm.code_from_value result in
let globals_snapshot = Hashtbl.copy env.bindings in
Hashtbl.iter (fun k v ->
Hashtbl.replace globals_snapshot k v) lam.l_closure.bindings;
(* VM closure with CEK fallback on error *)
(* Live env reference — NOT a snapshot. Functions see
current bindings, including later-defined functions. *)
let live_env = env.bindings in
(* Original lambda for CEK fallback *)
let orig_lambda = Lambda lam in
let fn = NativeFn ("vm:" ^ name, fun args ->
try
Sx_vm.execute_closure
{ Sx_vm.code; name = lam.l_name } args globals_snapshot
with _ ->
(* Fall back to CEK machine *)
Sx_ref.cek_call orig_lambda (List args)) in
Sx_vm.call_closure
{ Sx_vm.code; upvalues = [||]; name = lam.l_name;
env_ref = live_env }
args live_env
with
| _ ->
(* Any VM error — fall back to CEK *)
Sx_ref.eval_expr (List (orig_lambda :: args)) (Env env)) in
Hashtbl.replace env.bindings name fn;
incr count
| _ -> incr failed