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:
@@ -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;
|
||||
|
||||
@@ -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]):]
|
||||
|
||||
Reference in New Issue
Block a user