diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 47655456..2013020d 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,7 +1,7 @@ { "language": "erlang", - "total_pass": 656, - "total": 656, + "total_pass": 665, + "total": 665, "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":19,"total":19,"status":"ok"} + {"name":"vm","pass":28,"total":28,"status":"ok"} ] } diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index 263b695e..8eddb989 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,6 +1,6 @@ # Erlang-on-SX Scoreboard -**Total: 656 / 656 tests passing** +**Total: 665 / 665 tests passing** | | Suite | Pass | Total | |---|---|---|---| @@ -14,7 +14,7 @@ | ✅ | echo | 7 | 7 | | ✅ | fib | 8 | 8 | | ✅ | ffi | 14 | 14 | -| ✅ | vm | 19 | 19 | +| ✅ | vm | 28 | 28 | Generated by `lib/erlang/conformance.sh`. diff --git a/lib/erlang/tests/vm.sx b/lib/erlang/tests/vm.sx index 372edc1a..0f989091 100644 --- a/lib/erlang/tests/vm.sx +++ b/lib/erlang/tests/vm.sx @@ -134,4 +134,60 @@ (string-contains? (str (nth er-vm-err-caught 0)) "unknown opcode") true) + +;; ── Phase 9c — OP_PERFORM / OP_HANDLE ─────────────────────────── +(er-vm-test "perform opcode by id" + (get (er-vm-lookup-opcode-by-id 131) :name) "OP_PERFORM") +(er-vm-test "handle opcode by id" + (get (er-vm-lookup-opcode-by-id 132) :name) "OP_HANDLE") + +(define er-vm-pf-caught (list nil)) +(guard (c (:else (set-nth! er-vm-pf-caught 0 c))) + (er-vm-dispatch 131 (list "yield" (list 42)))) +(er-vm-test "perform raises tagged" + (get (nth er-vm-pf-caught 0) :tag) "vm-effect") +(er-vm-test "perform effect name" + (get (nth er-vm-pf-caught 0) :effect) "yield") +(er-vm-test "perform args carried" + (nth (get (nth er-vm-pf-caught 0) :args) 0) 42) + +(er-vm-test "handle catches matching effect" + (er-vm-dispatch 132 + (list + (fn () (er-vm-dispatch 131 (list "yield" (list 7)))) + "yield" + (fn (args) (+ (nth args 0) 100)))) + 107) + +(er-vm-test "handle no-effect returns thunk result" + (er-vm-dispatch 132 + (list + (fn () 99) + "yield" + (fn (args) "handler ran"))) + 99) + +(define er-vm-rt-caught (list nil)) +(guard (c (:else (set-nth! er-vm-rt-caught 0 c))) + (er-vm-dispatch 132 + (list + (fn () (er-vm-dispatch 131 (list "other" (list)))) + "yield" + (fn (args) "wrong")))) +(er-vm-test "handle rethrows non-matching" + (get (nth er-vm-rt-caught 0) :effect) "other") + +(er-vm-test "nested handles separate effect names" + (er-vm-dispatch 132 + (list + (fn () + (er-vm-dispatch 132 + (list + (fn () (er-vm-dispatch 131 (list "b" (list 5)))) + "a" + (fn (args) "inner-handled")))) + "b" + (fn (args) (+ (nth args 0) 1000)))) + 1005) + (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 8de1280f..dbec056b 100644 --- a/lib/erlang/vm/dispatcher.sx +++ b/lib/erlang/vm/dispatcher.sx @@ -80,6 +80,44 @@ (error (str "Erlang VM: unknown opcode name '" name "'")) ((get entry :handler) operands))))) +;; ── Phase 9c — effect opcodes (perform / handle) ──────────────── +;; Stub algebraic-effects-style operators. OP_PERFORM raises a tagged +;; exception; OP_HANDLE wraps a thunk in `guard` and catches matching +;; effects, passing the args to the handler. The real specialization +;; (constant-time effect dispatch, single-shot vs multi-shot continuations) +;; lands when 9a integrates. + +(define er-vm-effect-marker? + (fn (c effect-name) + (and (= (type-of c) "dict") + (= (get c :tag) "vm-effect") + (= (get c :effect) effect-name)))) + +(define er-vm-op-perform + (fn (operands) + (raise {:tag "vm-effect" :effect (nth operands 0) :args (nth operands 1)}))) + +(define er-vm-op-handle + (fn (operands) + (let ((thunk (nth operands 0)) + (effect-name (nth operands 1)) + (handler (nth operands 2)) + (result (list nil)) + (caught (list false)) + (rethrow (list nil))) + (guard + (c + (:else + (cond + (er-vm-effect-marker? c effect-name) + (do (set-nth! caught 0 true) + (set-nth! result 0 (handler (get c :args)))) + :else (set-nth! rethrow 0 c)))) + (set-nth! result 0 (thunk))) + (cond + (not (= (nth rethrow 0) nil)) (raise (nth rethrow 0)) + :else (nth result 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 @@ -118,6 +156,8 @@ (nth operands 0) (nth operands 1) (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-mk-atom "ok"))) (er-vm-register-erlang-opcodes!) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 98148605..2c6f3b9b 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -132,7 +132,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. -- [ ] **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). +- [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). - [ ] **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. @@ -144,6 +144,8 @@ Replace today's hardcoded BIF dispatch (`er-apply-bif`/`er-apply-remote-bif` in _Newest first._ +- **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. - **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.