vm-ext: phase E — JIT skips lambdas containing extension opcodes
Adds Sx_vm.bytecode_uses_extension_opcodes — an operand-aware bytecode scanner that walks past CONST u16, CALL_PRIM u16+u8, and CLOSURE u16+dynamic upvalue descriptors so operand bytes that happen to be ≥200 don't false-positive as extension opcodes. jit_compile_lambda calls the scanner on the inner closure's bytecode. On hit it returns None — the lambda then runs through CEK interpretation. The VM's dispatch fallthrough still routes the extension opcodes themselves through the registry; this change just prevents the JIT from claiming code it has no plan for. Tests: 7 new foundation cases — pure core eligible, head/middle/ post-CLOSURE detection, CONST + CALL_PRIM + CLOSURE-descriptor false- positive avoidance. +7 pass vs Phase D baseline, no regressions across 11 conformance suites. Loop complete: acceptance criteria 1-4 met. Hand-off to the Erlang loop — lib/erlang/vm/dispatcher.sx's Phase 9b stub can now be replaced with a real hosts/ocaml/lib/extensions/erlang.ml consumer.
This commit is contained in:
@@ -1597,7 +1597,92 @@ let run_foundation_tests () =
|
|||||||
| other ->
|
| other ->
|
||||||
incr fail_count;
|
incr fail_count;
|
||||||
Printf.printf " FAIL: invocation_count: %s\n"
|
Printf.printf " FAIL: invocation_count: %s\n"
|
||||||
(match other with Some n -> string_of_int n | None -> "None"))
|
(match other with Some n -> string_of_int n | None -> "None"));
|
||||||
|
|
||||||
|
Printf.printf "\nSuite: jit extension-opcode awareness\n";
|
||||||
|
let scan = Sx_vm.bytecode_uses_extension_opcodes in
|
||||||
|
let no_consts = [||] in
|
||||||
|
|
||||||
|
(* Pure core ops: scan reports false. *)
|
||||||
|
(* OP_TRUE OP_RETURN *)
|
||||||
|
if not (scan [| 3; 50 |] no_consts) then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: pure core bytecode is JIT-eligible\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: pure core bytecode flagged as extension\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* Extension opcode anywhere → true. *)
|
||||||
|
if scan [| 220; 50 |] no_consts then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: extension opcode detected at head\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: extension opcode at head missed\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* Mixed: core + extension → true. *)
|
||||||
|
if scan [| 3; 220; 50 |] no_consts then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: extension opcode detected after core ops\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: extension opcode after core ops missed\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* Operand bytes ≥200 must NOT trigger. CONST u16 with index 220
|
||||||
|
into a synthetic constant pool — the operand is 220 (lo) 0 (hi),
|
||||||
|
not an opcode. The pool entry at 220 is irrelevant for the scan. *)
|
||||||
|
let big_consts = Array.make 256 Nil in
|
||||||
|
if not (scan [| 1; 220; 0; 50 |] big_consts) then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: CONST operand ≥200 not a false positive\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: CONST operand ≥200 false-positives as ext op\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* CALL_PRIM has 3 operand bytes (u16 + u8); all ≥200 should not
|
||||||
|
trigger. *)
|
||||||
|
if not (scan [| 52; 220; 200; 200; 50 |] big_consts) then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: CALL_PRIM operands ≥200 not a false positive\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: CALL_PRIM operands ≥200 false-positive\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* CLOSURE with upvalue descriptors: scan must skip the 2 + 2*n
|
||||||
|
dynamic operand bytes. Build a synthetic constant pool with a
|
||||||
|
Dict at index 0 declaring upvalue-count 1, descriptors that are
|
||||||
|
≥200 — the scan should skip them and not trigger.
|
||||||
|
|
||||||
|
Bytecode layout: CLOSURE 0 0 desc_is_local desc_index RETURN
|
||||||
|
op lo hi 210 220 50
|
||||||
|
With upvalue-count = 1, scan must advance past the 2-byte CLOSURE
|
||||||
|
operand AND the 2 descriptor bytes (210, 220), landing on RETURN. *)
|
||||||
|
let cl_consts = Array.make 1 Nil in
|
||||||
|
let dict = Hashtbl.create 1 in
|
||||||
|
Hashtbl.replace dict "upvalue-count" (Integer 1);
|
||||||
|
cl_consts.(0) <- Dict dict;
|
||||||
|
if not (scan [| 51; 0; 0; 210; 220; 50 |] cl_consts) then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: CLOSURE upvalue descriptors ≥200 skipped\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: CLOSURE upvalue descriptors false-positive\n"
|
||||||
|
end;
|
||||||
|
|
||||||
|
(* Sanity: opcode after CLOSURE+descriptors that IS an extension
|
||||||
|
opcode triggers correctly. *)
|
||||||
|
if scan [| 51; 0; 0; 210; 220; 221; 50 |] cl_consts then begin
|
||||||
|
incr pass_count;
|
||||||
|
Printf.printf " PASS: extension opcode after CLOSURE detected\n"
|
||||||
|
end else begin
|
||||||
|
incr fail_count;
|
||||||
|
Printf.printf " FAIL: extension opcode after CLOSURE missed\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
(* ====================================================================== *)
|
(* ====================================================================== *)
|
||||||
|
|||||||
@@ -1056,6 +1056,62 @@ let _jit_is_broken_name n =
|
|||||||
|| n = "hs-repeat-while" || n = "hs-repeat-until"
|
|| n = "hs-repeat-while" || n = "hs-repeat-until"
|
||||||
|| n = "hs-for-each" || n = "hs-put!"
|
|| n = "hs-for-each" || n = "hs-put!"
|
||||||
|
|
||||||
|
(** Scan bytecode for any extension opcode (≥ 200, the registry's
|
||||||
|
[Sx_vm_extensions.extension_min]). Walks operand bytes correctly
|
||||||
|
so values that happen to be ≥200 (e.g. a CONST u16 index pointing
|
||||||
|
into a large pool) do not trigger false positives. CLOSURE's
|
||||||
|
dynamic upvalue descriptors are read from the constant pool entry
|
||||||
|
at the same index it pushes.
|
||||||
|
|
||||||
|
Used by [jit_compile_lambda] (Phase E of the opcode-extension
|
||||||
|
plan): a lambda whose compiled body contains any extension opcode
|
||||||
|
is routed through interpretation rather than JIT. Extensions
|
||||||
|
interpret their opcodes via the registry; the JIT does not
|
||||||
|
currently know how to compile them.
|
||||||
|
|
||||||
|
Operand-size logic mirrors [opcode_operand_size] (which is defined
|
||||||
|
later, in the disassembly section); inlined here so this helper can
|
||||||
|
sit before [jit_compile_lambda] in the file. *)
|
||||||
|
let bytecode_uses_extension_opcodes (bc : int array) (consts : value array) =
|
||||||
|
let core_operand_size = function
|
||||||
|
| 1 | 20 | 21 | 64 | 65 | 128 -> 2 (* u16 *)
|
||||||
|
| 16 | 17 | 18 | 19 | 48 | 49 | 144 -> 1 (* u8 *)
|
||||||
|
| 32 | 33 | 34 | 35 -> 2 (* i16 *)
|
||||||
|
| 52 -> 3 (* CALL_PRIM: u16 + u8 *)
|
||||||
|
| _ -> 0
|
||||||
|
in
|
||||||
|
let len = Array.length bc in
|
||||||
|
let ip = ref 0 in
|
||||||
|
let found = ref false in
|
||||||
|
while not !found && !ip < len do
|
||||||
|
let op = bc.(!ip) in
|
||||||
|
if op >= 200 then found := true
|
||||||
|
else begin
|
||||||
|
ip := !ip + 1;
|
||||||
|
let extra = match op with
|
||||||
|
| 51 (* CLOSURE *) when !ip + 1 < len ->
|
||||||
|
let lo = bc.(!ip) in
|
||||||
|
let hi = bc.(!ip + 1) in
|
||||||
|
let idx = lo lor (hi lsl 8) in
|
||||||
|
let uv_count =
|
||||||
|
if idx < Array.length consts then
|
||||||
|
(match consts.(idx) with
|
||||||
|
| Dict d ->
|
||||||
|
(match Hashtbl.find_opt d "upvalue-count" with
|
||||||
|
| Some (Integer n) -> n
|
||||||
|
| Some (Number n) -> int_of_float n
|
||||||
|
| _ -> 0)
|
||||||
|
| _ -> 0)
|
||||||
|
else 0
|
||||||
|
in
|
||||||
|
2 + uv_count * 2
|
||||||
|
| _ -> core_operand_size op
|
||||||
|
in
|
||||||
|
ip := !ip + extra
|
||||||
|
end
|
||||||
|
done;
|
||||||
|
!found
|
||||||
|
|
||||||
let jit_compile_lambda (l : lambda) globals =
|
let jit_compile_lambda (l : lambda) globals =
|
||||||
let fn_name = match l.l_name with Some n -> n | None -> "<anon>" in
|
let fn_name = match l.l_name with Some n -> n | None -> "<anon>" in
|
||||||
if !_jit_compiling then (
|
if !_jit_compiling then (
|
||||||
@@ -1118,8 +1174,18 @@ let jit_compile_lambda (l : lambda) globals =
|
|||||||
if idx < Array.length outer_code.vc_constants then
|
if idx < Array.length outer_code.vc_constants then
|
||||||
let inner_val = outer_code.vc_constants.(idx) in
|
let inner_val = outer_code.vc_constants.(idx) in
|
||||||
let code = code_from_value inner_val in
|
let code = code_from_value inner_val in
|
||||||
Some { vm_code = code; vm_upvalues = [||];
|
(* Phase E: if the inner lambda's bytecode contains any
|
||||||
vm_name = l.l_name; vm_env_ref = effective_globals; vm_closure_env = Some l.l_closure }
|
extension opcode (≥200), skip JIT and let the lambda run
|
||||||
|
interpreted via CEK. Extension opcodes dispatch correctly
|
||||||
|
through the VM's registry fallthrough, but the JIT has no
|
||||||
|
knowledge of them and shouldn't claim ownership. *)
|
||||||
|
if bytecode_uses_extension_opcodes code.vc_bytecode code.vc_constants then begin
|
||||||
|
Printf.eprintf "[jit] SKIP %s: bytecode uses extension opcodes (interpret-only in v1)\n%!"
|
||||||
|
fn_name;
|
||||||
|
None
|
||||||
|
end else
|
||||||
|
Some { vm_code = code; vm_upvalues = [||];
|
||||||
|
vm_name = l.l_name; vm_env_ref = effective_globals; vm_closure_env = Some l.l_closure }
|
||||||
else begin
|
else begin
|
||||||
Printf.eprintf "[jit] FAIL %s: closure index %d out of bounds (pool=%d)\n%!"
|
Printf.eprintf "[jit] FAIL %s: closure index %d out of bounds (pool=%d)\n%!"
|
||||||
fn_name idx (Array.length outer_code.vc_constants);
|
fn_name idx (Array.length outer_code.vc_constants);
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ future language port that wants performance-critical opcodes.
|
|||||||
Reference: `plans/erlang-on-sx.md` Phase 9, `plans/fed-sx-design.md` §17.5,
|
Reference: `plans/erlang-on-sx.md` Phase 9, `plans/fed-sx-design.md` §17.5,
|
||||||
`hosts/ocaml/lib/sx_vm.ml` (current VM).
|
`hosts/ocaml/lib/sx_vm.ml` (current VM).
|
||||||
|
|
||||||
Status: **in progress** on `loops/sx-vm-extensions`.
|
Status: **complete** on `loops/sx-vm-extensions` (Phases A-E landed
|
||||||
|
2026-05-14 / 2026-05-15). Ready for first real consumer
|
||||||
|
(`hosts/ocaml/lib/extensions/erlang.ml`, replacing the Phase 9b stub
|
||||||
|
dispatcher in `lib/erlang/vm/dispatcher.sx`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -317,9 +320,9 @@ The JIT (lazy lambda compilation) currently compiles based on opcode ranges.
|
|||||||
Extension opcodes (≥200) should fall through to interpretation, not be
|
Extension opcodes (≥200) should fall through to interpretation, not be
|
||||||
JIT-compiled in v1.
|
JIT-compiled in v1.
|
||||||
|
|
||||||
- [ ] Mark extension opcodes as "interpret only" in the JIT pre-analysis.
|
- [x] Mark extension opcodes as "interpret only" in the JIT pre-analysis.
|
||||||
- [ ] Lambda containing only core opcodes JIT-compiles as before.
|
- [x] Lambda containing only core opcodes JIT-compiles as before.
|
||||||
- [ ] Lambda containing any extension opcode runs interpreted.
|
- [x] Lambda containing any extension opcode runs interpreted.
|
||||||
|
|
||||||
JITing extension opcodes is a follow-up project; v1 keeps the JIT scope
|
JITing extension opcodes is a follow-up project; v1 keeps the JIT scope
|
||||||
unchanged and just makes it correctly route mixed bytecode.
|
unchanged and just makes it correctly route mixed bytecode.
|
||||||
@@ -457,6 +460,29 @@ familiarity.
|
|||||||
|
|
||||||
Newest first.
|
Newest first.
|
||||||
|
|
||||||
|
- **2026-05-15** — Phase E done. Loop complete (acceptance criteria
|
||||||
|
1-4 all met). New `Sx_vm.bytecode_uses_extension_opcodes` walks
|
||||||
|
bytecode operand-aware (CONST u16 indices, CALL_PRIM u16+u8,
|
||||||
|
CLOSURE u16+dynamic upvalue descriptors) so values that happen to
|
||||||
|
be ≥200 don't false-positive as extension opcodes. Wired into
|
||||||
|
`jit_compile_lambda`: when the inner closure's bytecode contains
|
||||||
|
any extension opcode, JIT returns None and the lambda runs
|
||||||
|
interpreted via CEK (the dispatch fallthrough still routes
|
||||||
|
extension opcodes through the registry — this just prevents the
|
||||||
|
JIT from claiming ownership of code it can't optimise). 7 new
|
||||||
|
foundation tests (`jit extension-opcode awareness` suite): pure
|
||||||
|
core eligible, head/middle/post-CLOSURE detection, CONST + CALL_PRIM
|
||||||
|
+ CLOSURE-descriptor false-positive avoidance. +7 pass vs Phase D
|
||||||
|
baseline (4833 vs 4826), 1111 pre-existing failures unchanged.
|
||||||
|
Conformance suites green: erlang 530/530, haskell 285/285, datalog
|
||||||
|
276/276, prolog 590/590, smalltalk 847/847, common-lisp 487/487,
|
||||||
|
apl 562/562, js 148/148, forth 632/638 (pre-existing), tcl 3/4
|
||||||
|
(pre-existing), ocaml-on-sx unit 607/607.
|
||||||
|
|
||||||
|
Loop done. Hand-off: the Erlang loop's Phase 9b stub dispatcher in
|
||||||
|
`lib/erlang/vm/dispatcher.sx` can now be replaced with a real
|
||||||
|
`hosts/ocaml/lib/extensions/erlang.ml` consumer.
|
||||||
|
|
||||||
- **2026-05-15** — Phase D done. New `hosts/ocaml/lib/extensions/` subtree
|
- **2026-05-15** — Phase D done. New `hosts/ocaml/lib/extensions/` subtree
|
||||||
wired into the `sx` library via `(include_subdirs unqualified)`.
|
wired into the `sx` library via `(include_subdirs unqualified)`.
|
||||||
`extensions/test_ext.ml` is the canonical worked example: two
|
`extensions/test_ext.ml` is the canonical worked example: two
|
||||||
|
|||||||
Reference in New Issue
Block a user