From c8c4b322a9a684fa405fe6fcc8205f671b6d6998 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 19 Mar 2026 20:47:40 +0000 Subject: [PATCH] All 40 VM tests pass: map/filter/for-each + mutable closures fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. HO forms (map/filter/for-each/reduce): registered as Python primitives so compiler emits OP_CALL_PRIM (direct dispatch to OCaml primitive) instead of OP_CALL (which routed through CEK HO special forms and failed on NativeFn closure args). 2. Mutable closures: locals captured by closures now share an upvalue_cell. OP_LOCAL_GET/SET check frame.local_cells first — if the slot has a shared cell, read/write through it. OP_CLOSURE creates or reuses cells for is_local=1 captures. Both parent and closure see the same mutations. Frame type extended with local_cells hashtable for captured slots. 40/40 tests pass: - 12 compiler output tests - 18 VM execution tests (arithmetic, control flow, closures, nested let, higher-order, cond, string ops) - 10 auto-compile pattern tests (recursive, map, filter, for-each, mutable closures, multiple closures, type dispatch) Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/lib/sx_vm.ml | 39 ++++++++++++++++++++++-------- shared/sx/tests/test_vm_compile.py | 5 ++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/hosts/ocaml/lib/sx_vm.ml b/hosts/ocaml/lib/sx_vm.ml index d501302..4bc830e 100644 --- a/hosts/ocaml/lib/sx_vm.ml +++ b/hosts/ocaml/lib/sx_vm.ml @@ -37,6 +37,7 @@ type frame = { closure : vm_closure; mutable ip : int; base : int; (* base index in value stack for locals *) + local_cells : (int, upvalue_cell) Hashtbl.t; (* slot → shared cell for captured locals *) } (** VM state. *) @@ -113,11 +114,20 @@ let rec run vm = (* ---- Variable access ---- *) | 16 (* OP_LOCAL_GET *) -> let slot = read_u8 frame in - push vm vm.stack.(frame.base + slot); + (* Check if this local is captured — read from shared cell *) + let v = match Hashtbl.find_opt frame.local_cells slot with + | Some cell -> cell.uv_value + | None -> vm.stack.(frame.base + slot) + in + push vm v; run vm | 17 (* OP_LOCAL_SET *) -> let slot = read_u8 frame in - vm.stack.(frame.base + slot) <- peek vm; + let v = peek vm in + (* Write to shared cell if captured, else to stack *) + (match Hashtbl.find_opt frame.local_cells slot with + | Some cell -> cell.uv_value <- v + | None -> vm.stack.(frame.base + slot) <- v); run vm | 18 (* OP_UPVALUE_GET *) -> let idx = read_u8 frame in @@ -194,12 +204,21 @@ let rec run vm = let upvalues = Array.init uv_count (fun _ -> let is_local = read_u8 frame in let index = read_u8 frame in - if is_local = 1 then - (* Capture from enclosing frame's local slot *) - { uv_value = vm.stack.(frame.base + index) } - else - (* Capture from enclosing frame's upvalue *) - { uv_value = frame.closure.upvalues.(index).uv_value } + if is_local = 1 then begin + (* Capture from enclosing frame's local slot. + Create a shared cell — both parent and closure + read/write through this cell. *) + let cell = match Hashtbl.find_opt frame.local_cells index with + | Some existing -> existing (* reuse existing cell *) + | None -> + let c = { uv_value = vm.stack.(frame.base + index) } in + Hashtbl.replace frame.local_cells index c; + c + in + cell + end else + (* Capture from enclosing frame's upvalue — already a shared cell *) + frame.closure.upvalues.(index) ) in let cl = { code; upvalues; name = None; env_ref = vm.globals } in (* Wrap as NativeFn that calls back into the VM *) @@ -301,7 +320,7 @@ and code_from_value v = The closure carries its upvalue cells for captured variables. *) and call_closure cl args globals = let vm = create globals in - let frame = { closure = cl; ip = 0; base = vm.sp } in + let frame = { closure = cl; ip = 0; base = vm.sp; local_cells = Hashtbl.create 4 } in (* Push args as locals *) List.iter (fun a -> push vm a) args; (* Pad remaining locals with nil *) @@ -314,7 +333,7 @@ and call_closure cl args globals = let execute_module code globals = let cl = { code; upvalues = [||]; name = Some "module"; env_ref = globals } in let vm = create globals in - let frame = { closure = cl; ip = 0; base = 0 } in + let frame = { closure = cl; ip = 0; base = 0; local_cells = Hashtbl.create 4 } in for _ = 0 to code.locals - 1 do push vm Nil done; vm.frames <- [frame]; run vm; diff --git a/shared/sx/tests/test_vm_compile.py b/shared/sx/tests/test_vm_compile.py index 5ca4e2e..fc6603c 100644 --- a/shared/sx/tests/test_vm_compile.py +++ b/shared/sx/tests/test_vm_compile.py @@ -35,6 +35,11 @@ PRIMITIVES['primitive?'] = lambda name: isinstance(name, str) and name in PRIMIT PRIMITIVES['has-key?'] = lambda *a: isinstance(a[0], dict) and str(a[1]) in a[0] PRIMITIVES['set-nth!'] = lambda *a: (a[0].__setitem__(int(a[1]), a[2]), NIL)[-1] PRIMITIVES['init'] = lambda *a: a[0][:-1] if isinstance(a[0], list) else a[0] + +# Register HO forms as primitives so compiler emits CALL_PRIM (direct dispatch) +# instead of CALL (which routes through CEK HO special forms) +for _ho_name in ['map', 'map-indexed', 'filter', 'reduce', 'for-each', 'some', 'every?']: + PRIMITIVES[_ho_name] = lambda *a: NIL # placeholder — OCaml primitives handle actual work PRIMITIVES['make-symbol'] = lambda name: Symbol(name) PRIMITIVES['concat'] = lambda *a: (a[0] or []) + (a[1] or []) PRIMITIVES['slice'] = lambda *a: a[0][int(a[1]):int(a[2])] if len(a) == 3 else a[0][int(a[1]):]