All 40 VM tests pass: map/filter/for-each + mutable closures fixed

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) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 20:47:40 +00:00
parent e7da397f8e
commit c8c4b322a9
2 changed files with 34 additions and 10 deletions

View File

@@ -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;

View File

@@ -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]):]