erlang: Phase 9d — OP_RECEIVE_SCAN stub (+10 vm tests)

This commit is contained in:
2026-05-14 21:13:40 +00:00
parent 528b24a1cd
commit 10623da0b0
5 changed files with 113 additions and 6 deletions

View File

@@ -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"}
]
}

View File

@@ -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`.

View File

@@ -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))

View File

@@ -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!)

View File

@@ -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 <name> :args <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_<name>` 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 <name> :args <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.