From 10623da0b0a17b3331dbb5948e8a551ea859e4a3 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 14 May 2026 21:13:40 +0000 Subject: [PATCH] =?UTF-8?q?erlang:=20Phase=209d=20=E2=80=94=20OP=5FRECEIVE?= =?UTF-8?q?=5FSCAN=20stub=20(+10=20vm=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/erlang/scoreboard.json | 6 ++-- lib/erlang/scoreboard.md | 4 +-- lib/erlang/tests/vm.sx | 61 +++++++++++++++++++++++++++++++++++++ lib/erlang/vm/dispatcher.sx | 44 ++++++++++++++++++++++++++ plans/erlang-on-sx.md | 4 ++- 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 2013020d..8633a8f1 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 665, - "total": 665, + "total_pass": 675, + "total": 675, "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":28,"total":28,"status":"ok"} + {"name":"vm","pass":38,"total":38,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 8eddb989..16fda7c7 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 665 / 665 tests passing** +**Total: 675 / 675 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -14,7 +14,7 @@ | ✅ | echo | 7 | 7 | | ✅ | fib | 8 | 8 | | ✅ | ffi | 14 | 14 | -| ✅ | vm | 28 | 28 | +| ✅ | vm | 38 | 38 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/vm.sx b/lib/erlang/tests/vm.sx index 0f989091..fa76d6eb 100644 --- a/lib/erlang/tests/vm.sx +++ b/lib/erlang/tests/vm.sx @@ -190,4 +190,65 @@ (fn (args) (+ (nth args 0) 1000)))) 1005) + +;; ── Phase 9d — OP_RECEIVE_SCAN ────────────────────────────────── +(er-vm-test "receive-scan opcode by id" + (get (er-vm-lookup-opcode-by-id 133) :name) "OP_RECEIVE_SCAN") + +;; Pattern: receive {ok, X} -> X end against mailbox [{error, 1}, {ok, 42}, foo] +(define er-vm-r1-env (er-env-new)) +(define er-vm-r1-clauses + (list + {:pattern {:type "tuple" + :elements (list + {:type "atom" :value "ok"} + {:type "var" :name "X"})} + :guards (list) + :body (list {:type "var" :name "X"})})) +(define er-vm-r1-mbox + (list + (er-mk-tuple (list (er-mk-atom "error") 1)) + (er-mk-tuple (list (er-mk-atom "ok") 42)) + (er-mk-atom "foo"))) + +(define er-vm-r1-result + (er-vm-dispatch 133 (list er-vm-r1-clauses er-vm-r1-mbox er-vm-r1-env))) +(er-vm-test "scan finds match" + (get er-vm-r1-result :matched) true) +(er-vm-test "scan reports correct index" + (get er-vm-r1-result :index) 1) +(er-vm-test "scan binds var" + (get er-vm-r1-env "X") 42) +(er-vm-test "scan leaves body unevaluated" + (= (get er-vm-r1-result :body) nil) false) + +;; No match case +(define er-vm-r2-env (er-env-new)) +(define er-vm-r2-mbox (list (er-mk-atom "nope") 99)) +(define er-vm-r2-result + (er-vm-dispatch 133 (list er-vm-r1-clauses er-vm-r2-mbox er-vm-r2-env))) +(er-vm-test "scan no-match" + (get er-vm-r2-result :matched) false) +(er-vm-test "scan no-match leaves env clean" + (dict-has? er-vm-r2-env "X") false) + +;; Empty mailbox +(define er-vm-r3-result + (er-vm-dispatch 133 (list er-vm-r1-clauses (list) (er-env-new)))) +(er-vm-test "scan empty mailbox" + (get er-vm-r3-result :matched) false) + +;; First-match wins (arrival order) +(define er-vm-r4-env (er-env-new)) +(define er-vm-r4-mbox + (list + (er-mk-tuple (list (er-mk-atom "ok") 1)) + (er-mk-tuple (list (er-mk-atom "ok") 2)))) +(define er-vm-r4-result + (er-vm-dispatch 133 (list er-vm-r1-clauses er-vm-r4-mbox er-vm-r4-env))) +(er-vm-test "scan first-match wins (index 0)" + (get er-vm-r4-result :index) 0) +(er-vm-test "scan binds first match's var" + (get er-vm-r4-env "X") 1) + (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 dbec056b..8a38290f 100644 --- a/lib/erlang/vm/dispatcher.sx +++ b/lib/erlang/vm/dispatcher.sx @@ -118,6 +118,49 @@ (not (= (nth rethrow 0) nil)) (raise (nth rethrow 0)) :else (nth result 0))))) +;; ── Phase 9d — receive scan opcode ──────────────────────────── +;; Selective receive primitive. Scans a mailbox value-list in arrival +;; order; for each value, tries each clause's pattern (binding into +;; env on success); on match returns `{:matched true :index N :body B}` +;; — the caller decides what to do with the index (queue-delete) and +;; the body (eval in the now-mutated env). On miss returns +;; `{:matched false}`, the caller arranges suspension (via OP_PERFORM). +;; +;; Operands: (clauses mbox-list env) +;; clauses — list of {:pattern :guards :body} dicts +;; mbox-list — SX list of message values +;; env — env dict (mutated on match) + +(define er-vm-receive-try-clauses + (fn (clauses msg env i) + (cond + (>= i (len clauses)) {:matched false} + :else + (let ((c (nth clauses i)) (snap (er-env-copy env))) + (cond + (and + (er-match! (get c :pattern) msg env) + (er-eval-guards (get c :guards) env)) + {:matched true :body (get c :body)} + :else + (do (er-env-restore! env snap) + (er-vm-receive-try-clauses clauses msg env (+ i 1)))))))) + +(define er-vm-receive-scan-loop + (fn (clauses mbox env i) + (cond + (>= i (len mbox)) {:matched false} + :else + (let ((msg (nth mbox i)) + (cr (er-vm-receive-try-clauses clauses msg env 0))) + (cond + (get cr :matched) {:matched true :index i :body (get cr :body)} + :else (er-vm-receive-scan-loop clauses mbox env (+ i 1))))))) + +(define er-vm-op-receive-scan + (fn (operands) + (er-vm-receive-scan-loop (nth operands 0) (nth operands 1) (nth operands 2) 0))) + ;; ── 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 @@ -158,6 +201,7 @@ (nth operands 2)))) (er-vm-register-opcode! 131 "OP_PERFORM" er-vm-op-perform) (er-vm-register-opcode! 132 "OP_HANDLE" er-vm-op-handle) + (er-vm-register-opcode! 133 "OP_RECEIVE_SCAN" er-vm-op-receive-scan) (er-mk-atom "ok"))) (er-vm-register-erlang-opcodes!) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 2c6f3b9b..1f0058bc 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -133,7 +133,7 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in - [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. - [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. - [x] **9c — `OP_PERFORM` / `OP_HANDLE`** — **+9 vm tests** (665/665 total). Stubs in `lib/erlang/vm/dispatcher.sx`: `OP_PERFORM` (id 131) raises `{:tag "vm-effect" :effect :args }`; `OP_HANDLE` (id 132) wraps a thunk in `guard`, catches matching effects (by `:effect` name), passes args to the handler, returns the handler's result. Non-matching effects rethrow to outer handlers (verified by a nested-handle test). Pure Erlang `receive` interface unchanged; this is the substrate for the eventual call/cc-free implementation when 9a integrates. Candidate for chiselling (Scheme call/cc, OCaml 5 effects, miniKanren all want the same shape). -- [ ] **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). +- [x] **9d — `OP_RECEIVE_SCAN`** — **+10 vm tests** (675/675 total). Stub at id 133 in `lib/erlang/vm/dispatcher.sx`. Operand contract: `(clauses mbox-list env)` where each clause is `{:pattern :guards :body}`, mbox-list is a plain SX list (not a queue — caller does queue→list before invoking and queue-delete after). Walks mbox in arrival order; tries each clause per message; first match returns `{:matched true :index N :body B}` (env mutated with bindings, body NOT evaluated — caller chooses when); no match returns `{:matched false}`. Pure pattern scan; suspension is the caller's job (compose with OP_PERFORM "receive-suspend" once 9a integrates). The real opcode will skip the AST walk by JIT-compiling each clause's match expr; this stub re-uses `er-match!` for correctness. - [ ] **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. - [ ] **9f — BIF dispatch table**: `OP_BIF_` for hot BIFs (`length/1`, `hd/1`, `tl/1`, `element/2`, `lists:reverse/1`, etc.) — direct dispatch, no registry lookup. Cold BIFs continue through the general dispatch path. - [ ] **9g — Conformance + perf bench**: full Phase 1-8 conformance must pass on the new VM. Ring benchmark target: **100k+ hops/sec at N=1000** (current ~30/sec → ~3000× speedup target). 1M-process spawn target: **under 30 seconds** (current ~9h extrapolation → ~1000× speedup target). Document achieved numbers in `lib/erlang/bench_ring_results.md`. @@ -144,6 +144,8 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in _Newest first._ +- **2026-05-14 Phase 9d — OP_RECEIVE_SCAN stub green** — Selective-receive primitive at opcode id 133 in `lib/erlang/vm/dispatcher.sx`. Operand contract: `(clauses mbox-list env)` — clauses are AST dicts (`{:pattern :guards :body}`), mbox-list is a plain SX list (queue → list is the caller's job), env is the binding target. Internal helpers `er-vm-receive-try-clauses` (per-message clause walker with env snapshot/restore on failure) and `er-vm-receive-scan-loop` (mailbox walker, arrival order). Match returns `{:matched true :index N :body B}` so the caller can queue-delete at N and then evaluate B in the now-mutated env; miss returns `{:matched false}` so the caller can suspend via OP_PERFORM "receive-suspend". Mirrors the existing `er-try-receive-loop` in `transpile.sx` but doesn't reach into the scheduler — purely VM-level. 10 new tests in `tests/vm.sx`: opcode registered, scan finds match at correct index, scan binds var, body left unevaluated, no-match leaves env untouched, empty mailbox, first-match wins (arrival order — verified by two `{ok, _}` msgs and binding the FIRST value). vm suite 28 → 38. Total **675/675** (+10 vm). When 9a integrates and the real OP_RECEIVE_SCAN compiles clauses into a register-machine match, the existing `er-eval-receive-loop` becomes a one-line dispatch wrapper. + - **2026-05-14 Phase 9c — OP_PERFORM / OP_HANDLE stubs green** — Two new opcodes in `lib/erlang/vm/dispatcher.sx`: id 131 `OP_PERFORM` raises `{:tag "vm-effect" :effect :args }`; id 132 `OP_HANDLE` wraps a thunk in SX `guard`, catches matching effects by `:effect` name, passes the `:args` list to the handler fn, returns the handler's result. New helper `er-vm-effect-marker?` predicates on the dict shape. Non-matching effects rethrow via a small box+rethrow dance (caught with `:else` first, decision deferred to a post-guard cond — re-raise outside the guard's scope so it propagates to outer handlers cleanly). 9 new tests in `tests/vm.sx`: opcode registered for each id; OP_PERFORM raises with correct tag/effect/args; OP_HANDLE catches matching effect; OP_HANDLE returns thunk result when no effect performed; OP_HANDLE rethrows non-matching effect to outer; nested OP_HANDLE blocks separate by effect name (inner handles "a", outer handles "b", performing "b" bypasses inner). vm suite grew 19 → 28 tests. Total **665/665** (+9 vm). Underlying call/cc + raise/guard machinery used by Erlang `receive` is unchanged; this is the shape for the eventual specialization when 9a integrates. Candidate for chiselling to `lib/guest/vm/effects.sx` — Scheme call/cc, OCaml 5 effects, miniKanren all want the same shape. - **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.