diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 6bd3a44c..c57e1d32 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 709, - "total": 709, + "total_pass": 715, + "total": 715, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, @@ -13,6 +13,6 @@ {"name":"echo","pass":7,"total":7,"status":"ok"}, {"name":"fib","pass":8,"total":8,"status":"ok"}, {"name":"ffi","pass":14,"total":14,"status":"ok"}, - {"name":"vm","pass":72,"total":72,"status":"ok"} + {"name":"vm","pass":78,"total":78,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 525a9df1..2b0cd79b 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 709 / 709 tests passing** +**Total: 715 / 715 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -14,7 +14,7 @@ | ✅ | echo | 7 | 7 | | ✅ | fib | 8 | 8 | | ✅ | ffi | 14 | 14 | -| ✅ | vm | 72 | 72 | +| ✅ | vm | 78 | 78 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/vm.sx b/lib/erlang/tests/vm.sx index f99922b5..026171b8 100644 --- a/lib/erlang/tests/vm.sx +++ b/lib/erlang/tests/vm.sx @@ -366,4 +366,38 @@ (er-vm-test "opcode list has 16+" (>= (len (er-vm-list-opcodes)) 16) true) + +;; ── Phase 9i — host opcode-id resolution ──────────────────────── +;; Requires a binary with the erlang_ext extension registered (9h). +;; The loop runs conformance against exactly that binary. +(er-vm-test "host id: OP_PATTERN_TUPLE = 222" + (er-vm-host-opcode-id "erlang.OP_PATTERN_TUPLE") 222) +(er-vm-test "host id: OP_BIF_IS_TUPLE = 239" + (er-vm-host-opcode-id "erlang.OP_BIF_IS_TUPLE") 239) +(er-vm-test "host id: unknown name -> nil" + (er-vm-host-opcode-id "erlang.OP_NOPE") nil) +(er-vm-test "effective id prefers host when present" + (er-vm-effective-opcode-id "erlang.OP_BIF_LENGTH" 136) 230) +(er-vm-test "effective id falls back to stub on nil" + (er-vm-effective-opcode-id "erlang.OP_NOPE" 999) 999) +;; The full erlang.OP_* namespace resolves to the contiguous 222-239 block. +(er-vm-test "host ids contiguous 222..239" + (let ((names (list "erlang.OP_PATTERN_TUPLE" "erlang.OP_PATTERN_LIST" + "erlang.OP_PATTERN_BINARY" "erlang.OP_PERFORM" + "erlang.OP_HANDLE" "erlang.OP_RECEIVE_SCAN" + "erlang.OP_SPAWN" "erlang.OP_SEND" + "erlang.OP_BIF_LENGTH" "erlang.OP_BIF_HD" + "erlang.OP_BIF_TL" "erlang.OP_BIF_ELEMENT" + "erlang.OP_BIF_TUPLE_SIZE" "erlang.OP_BIF_LISTS_REVERSE" + "erlang.OP_BIF_IS_INTEGER" "erlang.OP_BIF_IS_ATOM" + "erlang.OP_BIF_IS_LIST" "erlang.OP_BIF_IS_TUPLE")) + (ok (list true))) + (for-each + (fn (i) + (when (not (= (er-vm-host-opcode-id (nth names i)) (+ 222 i))) + (set-nth! ok 0 false))) + (range 0 (len names))) + (nth ok 0)) + true) + (define er-vm-test-summary (str "vm " er-vm-test-pass "/" er-vm-test-count)) diff --git a/lib/erlang/vm/dispatcher.sx b/lib/erlang/vm/dispatcher.sx index d740a8dd..dc3946a7 100644 --- a/lib/erlang/vm/dispatcher.sx +++ b/lib/erlang/vm/dispatcher.sx @@ -58,6 +58,32 @@ (define er-vm-list-opcodes (fn () (keys (er-vm-opcodes-get)))) +;; ── Phase 9i — host opcode-id resolution ──────────────────────── +;; When the OCaml `erlang_ext` extension is registered (Phase 9h), the +;; runtime exposes `extension-opcode-id` which maps an "erlang.OP_*" +;; name to the host-assigned id (222-239). We consult it so the SX +;; side and the OCaml side agree on ids; when it returns nil (name not +;; registered) we fall back to the stub-local id. +;; +;; NOTE: this requires a binary with the VM extension mechanism (the +;; vm-ext phase-A..E cherry-pick + Sx_vm_extensions force-link). The +;; loop builds and runs against exactly that binary +;; (hosts/ocaml/_build/default/bin/sx_server.exe). `extension-opcode-id` +;; resolves lazily at call time, so merely loading this file is safe; +;; only invoking the resolver on a binary that lacks the primitive +;; would raise. + +(define er-vm-host-opcode-id + (fn (ext-name) + (extension-opcode-id ext-name))) + +(define er-vm-effective-opcode-id + (fn (ext-name stub-id) + (let ((host (extension-opcode-id ext-name))) + (cond + (= host nil) stub-id + :else host)))) + (define er-vm-dispatch (fn diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index be958c41..d722237b 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -137,7 +137,7 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in - [x] **9e — `OP_SPAWN` / `OP_SEND` + lightweight scheduler** — **+16 vm tests** (691/691 total). Stubs at ids 134 (SPAWN) and 135 (SEND) in `lib/erlang/vm/dispatcher.sx`, plus the VM-process registry: `er-vm-procs` (dict pid → proc record), `er-vm-next-pid`, `er-vm-procs-reset!`, `er-vm-proc-new!`/`get`/`send!`/`mailbox`/`state`/`count`. Process record shape is the register-machine layout the real scheduler will use: `{:id :registers (list of 8 nil slots) :mailbox (SX list) :state ("runnable"/"waiting"/"dead") :initial-fn :initial-args}`. OP_SPAWN returns a numeric pid and allocates a fresh record; OP_SEND appends to the target's mailbox, flipping `:state` from "waiting" → "runnable" if needed (returns true on success, false on unknown pid — no crash). Sits parallel to `er-scheduler` (the language-level scheduler from Phase 3); the real VM scheduler will take over once 9a integrates and Erlang programs compile to bytecode. Perf targets in the bullet (spawn <50µs, send <5µs) defer to the integration step. - [x] **9f — BIF dispatch table** — **+18 vm tests** (709/709 total). 10 hot BIFs get their own opcode IDs (136-145) in `lib/erlang/vm/dispatcher.sx`: `OP_BIF_LENGTH`, `OP_BIF_HD`, `OP_BIF_TL`, `OP_BIF_ELEMENT`, `OP_BIF_TUPLE_SIZE`, `OP_BIF_LISTS_REVERSE`, `OP_BIF_IS_INTEGER`, `OP_BIF_IS_ATOM`, `OP_BIF_IS_LIST`, `OP_BIF_IS_TUPLE`. Each opcode's handler IS the underlying `er-bif-*` impl directly (no registry-string-lookup), so cost is opcode-id → handler one-hop. Cold BIFs continue through `er-apply-bif` / `er-lookup-bif` as before. IDs 136-159 reserved for future hot-BIF additions. - [x] **9h — `erlang_ext.ml`** — OCaml extension at `hosts/ocaml/lib/extensions/erlang_ext.ml` registering the 18-opcode Erlang namespace (ids **222-239**, names `erlang.OP_*` mirroring the SX stub dispatcher). Registered at sx_server startup via `Erlang_ext.register ()` (guarded against double-register Failure). `extension-opcode-id "erlang.OP_PATTERN_TUPLE"` → 222 … `OP_BIF_IS_TUPLE` → 239, unknown → nil. Handlers raise a descriptive not-wired `Eval_error` (bytecode emission is a later phase; SX stub dispatcher remains the working specialization path) — keeps the extension honest rather than silently corrupting the VM stack. id range 222+ dodges test_reg (210/211) + test_ext (220/221) so all three coexist in run_tests. **+5 OCaml ext tests** (run_tests `Suite: extensions/erlang_ext`); Erlang conformance held **709/709**. -- [ ] **9i — wire SX dispatcher to real ids**: `lib/erlang/vm/dispatcher.sx` consults `(extension-opcode-id "erlang.OP_*")` and uses the host id when present, falling back to the stub-local id otherwise. Tests verify both paths. +- [x] **9i — wire SX dispatcher to real ids** — `lib/erlang/vm/dispatcher.sx` gains `er-vm-host-opcode-id` (thin `extension-opcode-id` wrapper) and `er-vm-effective-opcode-id name stub-id` (host id when non-nil, else stub-id). `extension-opcode-id` resolves lazily at call time so loading the file is safe even on a binary lacking the primitive; only invoking the resolver there would raise (documented prereq — the loop builds + runs against the binary that has it). **+6 vm tests** (715/715): OP_PATTERN_TUPLE→222, OP_BIF_IS_TUPLE→239, unknown→nil, effective prefers host (OP_BIF_LENGTH→230), effective falls back to stub on nil (999), and a sweep asserting the whole 18-name namespace maps contiguously to 222..239. Stub-local ids (128-145) registration untouched so the prior 72 vm tests stay green. - [ ] **9g — Conformance + perf bench** — Conformance half satisfied: **709/709** with stub infra. Perf-bench half: after 9h+9i, re-run `lib/erlang/bench_ring.sh`, record real numbers in `lib/erlang/bench_ring_results.md`. Targets: 100k+ hops/sec at N=1000, 1M-process spawn under 30s. **Acceptance:** ring benchmark hits the 100k hops/sec target. All prior phase tests pass. Two opcodes chiselled to `lib/guest/vm/` (or annotated as candidates with a written rationale). @@ -146,6 +146,8 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in _Newest first._ +- **2026-05-15 Phase 9i — SX dispatcher consults host opcode ids** — `lib/erlang/vm/dispatcher.sx` now bridges SX↔OCaml opcode ids. Two new functions: `er-vm-host-opcode-id` (wraps `extension-opcode-id`) and `er-vm-effective-opcode-id name stub-id` (host id if the OCaml `erlang_ext` registered it, else the stub-local id). Key SX-runtime fact established this iteration: symbol resolution is **lazy/call-time** — `(define f (fn () (extension-opcode-id "x")))` does NOT raise at load even when the primitive is absent; only calling `f` does. Combined with the earlier findings (guard can't catch undefined-symbol; no symbol-existence reflection), this means graceful in-SX degradation is impossible — so the design instead documents the binary prerequisite and relies on the loop building+running the freshly-built `hosts/ocaml/_build/default/bin/sx_server.exe` (conformance.sh's default, which has the vm-ext mechanism + erlang_ext). Stub-local registration (128-145) deliberately left intact so the 72 pre-existing vm tests don't move. 6 new vm tests: 222/239 lookups, unknown→nil, effective-prefers-host (230), effective-fallback (999), and a contiguity sweep over all 18 `erlang.OP_*` names asserting they map to 222..239 in order. vm suite 72→78. Total **715/715** on the fresh binary. Next: 9g — re-run ring bench, record numbers (note: stubs still wrap existing impls 1-to-1 so numbers won't move until the compiler emits these opcodes — a later phase). + - **2026-05-15 Phase 9h — erlang_ext.ml registered, opcode namespace live** — New `hosts/ocaml/lib/extensions/erlang_ext.ml` modelled on `test_ext.ml`: an `EXTENSION` module `name="erlang"`, per-instance `ErlangExtState` (dispatch counter), 18 opcodes ids 222-239 named `erlang.OP_*` exactly mirroring the SX stub dispatcher. Registered at sx_server startup with a second guarded line in `bin/sx_server.ml` (`try Erlang_ext.register () with Failure _ -> ()` — survives a re-entered server). `include_subdirs unqualified` in `lib/dune` already pulls `lib/extensions/*.ml` into the `sx` lib, so no dune edit needed. Handlers deliberately raise a descriptive `Eval_error` ("bytecode emission not yet wired (Phase 9j) — Erlang runs via CEK; specialization path is the SX stub dispatcher") rather than fake stack ops — the compiler doesn't emit these yet, so an honest loud failure beats silent corruption. Hit and fixed an opcode-id collision: the original 200-217 range clashed with run_tests' inline test_reg (210/211); relocated to 222-239 (clears test_reg + test_ext 220/221, all coexist; production sx_server only registers erlang). 5 new OCaml tests in run_tests `Suite: extensions/erlang_ext`: opcode-id 222 + 239 resolve, unknown→nil, dispatch raises not-wired (substring check, no Str dep since run_tests doesn't link str), dispatch_count state ≥1. Built via `eval $(opam env --switch=5.2.0); dune build bin/run_tests.exe bin/sx_server.exe`. Erlang conformance **709/709** on the rebuilt binary (the broad run_tests 1110 failures are loops/erlang's pre-existing months-old divergence from architecture — run_tests was never built on this branch before; my changes are isolated additive). Next: 9i — wire the SX stub dispatcher to consult `extension-opcode-id`. - **2026-05-15 Phase 9a integrated — scope widened to hosts/** — User lifted the hosts/ scope restriction ("we are going to merge this back anyhow"). Cherry-picked the 5 `vm-ext` commits (phases A-E) from `loops/sx-vm-extensions` onto `loops/erlang` — only conflict was `plans/sx-vm-opcode-extension.md` (already had architecture's final copy from an earlier iteration; resolved `-X ours`, OCaml files auto-merged clean since loops/erlang never touched hosts/). Discovered `extension-opcode-id` was still "Undefined symbol" even on a fresh build: `Sx_vm_extensions`'s module-init (`install_dispatch` + primitive registration) only runs if the module is linked, and `sx_server.ml` never referenced it (only `run_tests.ml` did), so OCaml dead-code-eliminated it. Fix: added `let () = ignore (Sx_vm_extensions.id_of_name "")` force-link reference near the top of `bin/sx_server.ml`. Rebuilt with `dune build` (opam switch 5.2.0; `dune` not on PATH by default — `eval $(opam env --switch=5.2.0)` first). `extension-opcode-id` now live: returns nil for unregistered names, will return real ids once an extension registers. Conformance **709/709** on the freshly built binary (cherry-picked sx_vm.ml dispatch changes + force-link, zero regressions). 9a checkbox flipped from BLOCKED to INTEGRATED; Blockers entry resolved; added 9h (erlang_ext.ml) + 9i (wire SX dispatcher to real ids) as ordinary in-scope checkboxes, reordered 9g after them. Next: write `hosts/ocaml/lib/extensions/erlang_ext.ml`.