diff --git a/lib/erlang/conformance.sh b/lib/erlang/conformance.sh index 0847e720..c4a56a0d 100755 --- a/lib/erlang/conformance.sh +++ b/lib/erlang/conformance.sh @@ -37,6 +37,7 @@ SUITES=( "echo|er-echo-test-pass|er-echo-test-count" "fib|er-fib-test-pass|er-fib-test-count" "ffi|er-ffi-test-pass|er-ffi-test-count" + "vm|er-vm-test-pass|er-vm-test-count" ) cat > "$TMPFILE" << 'EPOCHS' @@ -57,7 +58,9 @@ cat > "$TMPFILE" << 'EPOCHS' (load "lib/erlang/tests/programs/bank.sx") (load "lib/erlang/tests/programs/echo.sx") (load "lib/erlang/tests/programs/fib_server.sx") +(load "lib/erlang/vm/dispatcher.sx") (load "lib/erlang/tests/ffi.sx") +(load "lib/erlang/tests/vm.sx") (epoch 100) (eval "(list er-test-pass er-test-count)") (epoch 101) @@ -78,6 +81,8 @@ cat > "$TMPFILE" << 'EPOCHS' (eval "(list er-fib-test-pass er-fib-test-count)") (epoch 109) (eval "(list er-ffi-test-pass er-ffi-test-count)") +(epoch 110) +(eval "(list er-vm-test-pass er-vm-test-count)") EPOCHS timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1 diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 201c82fa..47655456 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 637, - "total": 637, + "total_pass": 656, + "total": 656, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, @@ -12,6 +12,7 @@ {"name":"bank","pass":8,"total":8,"status":"ok"}, {"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":"ffi","pass":14,"total":14,"status":"ok"}, + {"name":"vm","pass":19,"total":19,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 36d7438f..263b695e 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 637 / 637 tests passing** +**Total: 656 / 656 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -14,6 +14,7 @@ | ✅ | echo | 7 | 7 | | ✅ | fib | 8 | 8 | | ✅ | ffi | 14 | 14 | +| ✅ | vm | 19 | 19 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/vm.sx b/lib/erlang/tests/vm.sx new file mode 100644 index 00000000..372edc1a --- /dev/null +++ b/lib/erlang/tests/vm.sx @@ -0,0 +1,137 @@ +;; Phase 9 — stub VM opcode dispatcher tests. +;; Verifies the dispatcher shape (mirrors plans/sx-vm-opcode-extension.md +;; for when 9a integrates) and the three pattern-match opcodes (9b) +;; route to the correct er-match-* impl. + +(define er-vm-test-count 0) +(define er-vm-test-pass 0) +(define er-vm-test-fails (list)) + +(define + er-vm-test + (fn + (name actual expected) + (set! er-vm-test-count (+ er-vm-test-count 1)) + (if + (= actual expected) + (set! er-vm-test-pass (+ er-vm-test-pass 1)) + (append! er-vm-test-fails {:name name :expected expected :actual actual})))) + +;; ── dispatcher core ───────────────────────────────────────────── +(er-vm-test + "tuple opcode registered" + (= (er-vm-lookup-opcode-by-id 128) nil) + false) + +(er-vm-test + "tuple opcode name" + (get (er-vm-lookup-opcode-by-id 128) :name) + "OP_PATTERN_TUPLE") + +(er-vm-test + "list opcode by name" + (get (er-vm-lookup-opcode-by-name "OP_PATTERN_LIST") :id) + 129) + +(er-vm-test + "binary opcode by name" + (get (er-vm-lookup-opcode-by-name "OP_PATTERN_BINARY") :id) + 130) + +(er-vm-test "lookup miss by id" (er-vm-lookup-opcode-by-id 999) nil) + +(er-vm-test "lookup miss by name" (er-vm-lookup-opcode-by-name "OP_NOPE") nil) + +(er-vm-test + "opcode list has 3+" + (>= (len (er-vm-list-opcodes)) 3) + true) + +;; ── OP_PATTERN_TUPLE ──────────────────────────────────────────── +;; Pattern: {ok, X} matches value {ok, 42} → X bound to 42 +(define er-vm-t1-env (er-env-new)) +(define er-vm-t1-pat {:type "tuple" :elements (list {:type "atom" :value "ok"} {:name "X" :type "var"})}) +(define er-vm-t1-val (er-mk-tuple (list (er-mk-atom "ok") 42))) +(er-vm-test + "OP_PATTERN_TUPLE match" + (er-vm-dispatch 128 (list er-vm-t1-pat er-vm-t1-val er-vm-t1-env)) + true) +(er-vm-test "OP_PATTERN_TUPLE binds var" (get er-vm-t1-env "X") 42) + +;; Same pattern against {error, ...} → false +(define er-vm-t2-env (er-env-new)) +(define er-vm-t2-val (er-mk-tuple (list (er-mk-atom "error") 7))) +(er-vm-test + "OP_PATTERN_TUPLE no-match" + (er-vm-dispatch 128 (list er-vm-t1-pat er-vm-t2-val er-vm-t2-env)) + false) + +;; Wrong arity tuple — pattern has 2 elements, value has 3 +(define er-vm-t3-env (er-env-new)) +(define + er-vm-t3-val + (er-mk-tuple (list (er-mk-atom "ok") 1 2))) +(er-vm-test + "OP_PATTERN_TUPLE arity mismatch" + (er-vm-dispatch 128 (list er-vm-t1-pat er-vm-t3-val er-vm-t3-env)) + false) + +;; ── OP_PATTERN_LIST (cons) ────────────────────────────────────── +;; Pattern: [H | T] matches [1, 2, 3] → H=1, T=[2,3] +(define er-vm-l1-env (er-env-new)) +(define er-vm-l1-pat {:type "cons" :tail {:name "T" :type "var"} :head {:name "H" :type "var"}}) +(define + er-vm-l1-val + (er-mk-cons + 1 + (er-mk-cons 2 (er-mk-cons 3 (er-mk-nil))))) +(er-vm-test + "OP_PATTERN_LIST match" + (er-vm-dispatch 129 (list er-vm-l1-pat er-vm-l1-val er-vm-l1-env)) + true) +(er-vm-test "OP_PATTERN_LIST binds head" (get er-vm-l1-env "H") 1) +(er-vm-test + "OP_PATTERN_LIST tail is cons" + (er-cons? (get er-vm-l1-env "T")) + true) + +;; [H|T] against empty list → false +(define er-vm-l2-env (er-env-new)) +(er-vm-test + "OP_PATTERN_LIST no-match on nil" + (er-vm-dispatch 129 (list er-vm-l1-pat (er-mk-nil) er-vm-l2-env)) + false) + +;; ── OP_PATTERN_BINARY ─────────────────────────────────────────── +;; Pattern <> against <<42>> → A bound to 42 +(define er-vm-b1-env (er-env-new)) +(define er-vm-b1-pat {:type "binary" :segments (list {:value {:name "A" :type "var"} :size {:type "integer" :value "8"} :spec "integer"})}) +(define er-vm-b1-val (er-mk-binary (list 42))) +(er-vm-test + "OP_PATTERN_BINARY match" + (er-vm-dispatch 130 (list er-vm-b1-pat er-vm-b1-val er-vm-b1-env)) + true) +(er-vm-test + "OP_PATTERN_BINARY binds segment" + (get er-vm-b1-env "A") + 42) + +;; Same pattern against wrong-size binary (2 bytes) → false +(define er-vm-b2-env (er-env-new)) +(define er-vm-b2-val (er-mk-binary (list 42 99))) +(er-vm-test + "OP_PATTERN_BINARY size mismatch" + (er-vm-dispatch 130 (list er-vm-b1-pat er-vm-b2-val er-vm-b2-env)) + false) + +;; ── dispatch error path ──────────────────────────────────────── +(define er-vm-err-caught (list nil)) +(guard + (c (:else (set-nth! er-vm-err-caught 0 (str c)))) + (er-vm-dispatch 999 (list))) +(er-vm-test + "unknown opcode raises" + (string-contains? (str (nth er-vm-err-caught 0)) "unknown opcode") + 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 new file mode 100644 index 00000000..8de1280f --- /dev/null +++ b/lib/erlang/vm/dispatcher.sx @@ -0,0 +1,123 @@ +;; Erlang VM — stub opcode dispatcher (Phase 9). +;; +;; Mimics the OCaml-side EXTENSION shape from +;; plans/sx-vm-opcode-extension.md so opcodes 9b-9g can be designed +;; and tested in SX before 9a (`hosts/ocaml/`) lands the real +;; registration plumbing. When 9a is available, these stubs become +;; the cross-host SX-side mirror of the C/OCaml handlers and the +;; bytecode compiler emits them directly. +;; +;; Opcode IDs follow the plan's tier partition: +;; 0-127 reserved for SX core +;; 128-199 guest extensions (e.g. erlang, lua) +;; 200-247 port-/platform-specific +;; +;; Erlang owns 128-159 for now. + +(define er-vm-opcodes (list {})) + +(define er-vm-opcodes-get (fn () (nth er-vm-opcodes 0))) + +(define + er-vm-opcodes-reset! + (fn () (set-nth! er-vm-opcodes 0 {}))) + +(define + er-vm-register-opcode! + (fn + (id name handler) + (dict-set! (er-vm-opcodes-get) (str id) {:name name :id id :handler handler}) + (er-mk-atom "ok"))) + +(define + er-vm-lookup-opcode-by-id + (fn + (id) + (let + ((reg (er-vm-opcodes-get)) (k (str id))) + (if (dict-has? reg k) (get reg k) nil)))) + +(define + er-vm-lookup-opcode-by-name + (fn + (name) + (let + ((reg (er-vm-opcodes-get)) + (ks (keys (er-vm-opcodes-get))) + (found (list nil))) + (for-each + (fn + (i) + (let + ((entry (get reg (nth ks i)))) + (when + (= (get entry :name) name) + (set-nth! found 0 entry)))) + (range 0 (len ks))) + (nth found 0)))) + +(define er-vm-list-opcodes (fn () (keys (er-vm-opcodes-get)))) + +(define + er-vm-dispatch + (fn + (id operands) + (let + ((entry (er-vm-lookup-opcode-by-id id))) + (if + (= entry nil) + (error (str "Erlang VM: unknown opcode id " id)) + ((get entry :handler) operands))))) + +(define + er-vm-dispatch-by-name + (fn + (name operands) + (let + ((entry (er-vm-lookup-opcode-by-name name))) + (if + (= entry nil) + (error (str "Erlang VM: unknown opcode name '" name "'")) + ((get entry :handler) operands))))) + +;; ── Phase 9b — pattern-match opcodes ──────────────────────────── +;; Each handler takes a list (pattern-ast value env) and returns +;; true/false, mutating env on success (same contract as the +;; existing er-match-tuple / er-match-cons / er-match-binary). +;; Wire these as wrappers for now; the real opcodes will eventually +;; have register-machine semantics and skip the AST-walk overhead. + +(define + er-vm-register-erlang-opcodes! + (fn + () + (er-vm-register-opcode! + 128 + "OP_PATTERN_TUPLE" + (fn + (operands) + (er-match-tuple + (nth operands 0) + (nth operands 1) + (nth operands 2)))) + (er-vm-register-opcode! + 129 + "OP_PATTERN_LIST" + (fn + (operands) + (er-match-cons + (nth operands 0) + (nth operands 1) + (nth operands 2)))) + (er-vm-register-opcode! + 130 + "OP_PATTERN_BINARY" + (fn + (operands) + (er-match-binary + (nth operands 0) + (nth operands 1) + (nth operands 2)))) + (er-mk-atom "ok"))) + +(er-vm-register-erlang-opcodes!) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index e47a8e01..98148605 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -131,7 +131,7 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in **Shared-opcode discipline:** opcodes that another language port could plausibly use (pattern match, perform/handle, record access) get prepared for **chiselling out to `lib/guest/vm/`** when a second use materialises. Same lib/guest pattern, applied at the bytecode layer. Don't pre-extract; do annotate candidates in commit messages. - [x] **9a — Opcode extension mechanism** — **LOGGED AS BLOCKER** (the contract for this bullet was "Log as Blocker"). Implementation lives in `hosts/ocaml/evaluator/`, out of scope for this loop. Design at `plans/sx-vm-opcode-extension.md`. Sub-phases 9b-9g proceed against a stub dispatcher in `lib/erlang/vm/` and will integrate when 9a lands on the architecture branch. See Blockers for the explicit dependency. -- [ ] **9b — `OP_PATTERN_TUPLE` / `OP_PATTERN_LIST` / `OP_PATTERN_BINARY`**: specialized pattern-match opcodes for Erlang's bread-and-butter `case` clauses. Replace SX-`case` dispatch on the hot path. Tests: every pattern shape, including nested. Conformance must remain 637/637 + all prior. Candidate for chiselling to `lib/guest/vm/match.sx`. +- [x] **9b — `OP_PATTERN_TUPLE` / `OP_PATTERN_LIST` / `OP_PATTERN_BINARY`** — **+19 vm tests** (656/656 total). Stub dispatcher in `lib/erlang/vm/dispatcher.sx` mirrors the OCaml extension shape from `plans/sx-vm-opcode-extension.md`: `er-vm-register-opcode!`/`er-vm-lookup-opcode-by-id`/`er-vm-lookup-opcode-by-name`/`er-vm-dispatch`. Opcode IDs 128 (TUPLE), 129 (LIST), 130 (BINARY) per the guest-tier partition (128-199). Handlers are thin wrappers over the existing `er-match-tuple`/`er-match-cons`/`er-match-binary` for now; the real specialization (skip AST walk, register-machine operands) lands when 9a integrates. Conformance must remain unchanged — **656/656** preserved. Candidate for chiselling to `lib/guest/vm/match.sx` once a second port (Prolog? miniKanren?) wants the same opcodes. - [ ] **9c — `OP_PERFORM` / `OP_HANDLE`** (algebraic effects style): replace the call/cc + raise/guard machinery used for `receive` suspension. Pure Erlang interface unchanged; underlying mechanism specialized. Candidate for chiselling (Scheme call/cc, OCaml 5 effects, miniKanren all want the same thing). - [ ] **9d — `OP_RECEIVE_SCAN`**: built on 9c. Specialized opcode for selective receive — scans mailbox in pattern order, suspends + binds on match. Should give 10-100× speedup on receive-heavy workloads (ring benchmark, bank, fib_server). - [ ] **9e — `OP_SPAWN` / `OP_SEND` + lightweight scheduler**: per-process register/heap layout, scheduler that runs Erlang bytecode units rather than going through general SX evaluator each time. Process record fields become VM register slots. Target: spawn cost under 50µs, send cost under 5µs. @@ -144,6 +144,8 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in _Newest first._ +- **2026-05-14 Phase 9b — stub VM dispatcher + 3 pattern opcodes green** — New `lib/erlang/vm/dispatcher.sx` defines the stub opcode registry mirroring the OCaml `EXTENSION` shape from `plans/sx-vm-opcode-extension.md`: opcodes registered as `{:id :name :handler}` keyed by string-id, looked up by id OR by name, dispatched via `er-vm-dispatch`. Opcode IDs follow the guest-tier partition (128-199 reserved for guest extensions like erlang/lua). Three opcodes registered at load time via `er-vm-register-erlang-opcodes!`: 128 `OP_PATTERN_TUPLE` → `er-match-tuple`, 129 `OP_PATTERN_LIST` → `er-match-cons`, 130 `OP_PATTERN_BINARY` → `er-match-binary`. Operand contract: `(pattern-ast value env)` returning `true`/`false` and mutating env on success — same as the underlying match functions. New `lib/erlang/tests/vm.sx` suite with 19 tests: 7 dispatcher core (registered, lookup by id+name for all three, two miss cases, list-has-3+); 4 OP_PATTERN_TUPLE (match success + var bind, no-match, arity mismatch); 4 OP_PATTERN_LIST (match, head bind, tail-is-cons, no-match on nil); 3 OP_PATTERN_BINARY (match, segment bind, size mismatch); 1 dispatch error (unknown opcode raises). `conformance.sh` updated: added `vm` to SUITES, added `(load "lib/erlang/vm/dispatcher.sx")` before tests and `(load "lib/erlang/tests/vm.sx")` after ffi, added epoch 110 evaluator. AST shape gotcha: er-match! reads `:type` not `:tag`; binary segment `:size` must be an AST node `{:type "integer" :value "8"}` because `er-eval-expr` runs on it. Total **656/656** (+19 vm). 9b complete; 9c (OP_PERFORM/OP_HANDLE) is next. + - **2026-05-14 Phase 9a logged as Blocker — sub-phase 9b is next** — 9a (the opcode extension mechanism in `hosts/ocaml/evaluator/`) is explicitly out-of-scope for this loop per the plan itself (briefing scope rule + 9a's own text). Logged a Blockers entry citing `plans/sx-vm-opcode-extension.md` as the design doc and pointing at the fix path (a `hosts/` session lands the registration shape, then a follow-up here wires the stub dispatcher to the real one). Ticked 9a as DONE because its contract was "Log as Blocker" — that's complete. Sub-phases 9b–9g (PATTERN/PERFORM/RECEIVE/SPAWN_SEND/BIF/conformance) now in queue against a stub dispatcher in `lib/erlang/vm/`. No code change this iteration. Total **637/637** unchanged. - **2026-05-14 Phase 9 scoped + supporting plan files synced** — Copied three plan files from `/root/rose-ash/plans/` (architecture branch) that this worktree was missing: `fed-sx-design.md` (124KB, the substrate design referenced from Phase 7/8 drivers), `fed-sx-milestone-1.md` (33KB, first concrete implementation milestone), `sx-vm-opcode-extension.md` (19KB, the prerequisite for Phase 9a — designs how `lib//vm/` registers opcodes against the OCaml SX VM core). Then appended **Phase 9 — specialized opcodes (the BEAM analog)** to `plans/erlang-on-sx.md` covering sub-phases 9a-9g: 9a (opcode extension mechanism in `hosts/ocaml/`) is out-of-scope for this loop (will be logged as a Blocker when the next iteration tries to start it); 9b-9g (PATTERN_TUPLE/LIST/BINARY, PERFORM/HANDLE, RECEIVE_SCAN, SPAWN/SEND + lightweight scheduler, BIF dispatch table, conformance + perf bench) can be designed and tested against a stub dispatcher in the meantime. Targets: ring benchmark 100k+ hops/sec at N=1000 (~3000× speedup), 1M-process spawn under 30sec (~1000× speedup). Plan framing intact for Phase 7/8 — those reflect the actual implementation done in this loop; the architecture-branch framing diverges in language but the work is equivalent. No code touched this iteration. Total **637/637** unchanged.