;; 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) ;; ── 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) ;; ── 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) ;; ── Phase 9e — OP_SPAWN / OP_SEND ─────────────────────────────── (er-vm-procs-reset!) (er-vm-test "spawn opcode by id" (get (er-vm-lookup-opcode-by-id 134) :name) "OP_SPAWN") (er-vm-test "send opcode by id" (get (er-vm-lookup-opcode-by-id 135) :name) "OP_SEND") (define er-vm-fn (fn () "body")) (define er-vm-p1 (er-vm-dispatch 134 (list er-vm-fn (list)))) (define er-vm-p2 (er-vm-dispatch 134 (list er-vm-fn (list "arg")))) (er-vm-test "spawn returns pid 0 first" er-vm-p1 0) (er-vm-test "spawn returns pid 1 second" er-vm-p2 1) (er-vm-test "proc count is 2" (er-vm-proc-count) 2) (er-vm-test "spawned proc state runnable" (er-vm-proc-state er-vm-p1) "runnable") (er-vm-test "spawned proc mailbox empty" (len (er-vm-proc-mailbox er-vm-p1)) 0) (er-vm-test "spawned proc has 8 registers" (len (get (er-vm-proc-get er-vm-p1) :registers)) 8) ;; OP_SEND appends to target's mailbox, preserves arrival order. (er-vm-test "send returns true on valid pid" (er-vm-dispatch 135 (list er-vm-p1 "msg1")) true) (er-vm-dispatch 135 (list er-vm-p1 "msg2") ) (er-vm-dispatch 135 (list er-vm-p1 "msg3")) (er-vm-test "mailbox length after 3 sends" (len (er-vm-proc-mailbox er-vm-p1)) 3) (er-vm-test "mailbox preserves order — first" (nth (er-vm-proc-mailbox er-vm-p1) 0) "msg1") (er-vm-test "mailbox preserves order — last" (nth (er-vm-proc-mailbox er-vm-p1) 2) "msg3") ;; send to nonexistent pid returns false (doesn't crash) (er-vm-test "send to unknown pid is false" (er-vm-dispatch 135 (list 99999 "x")) false) ;; Isolation: msgs to p1 don't appear in p2's mailbox (er-vm-test "isolation — p2 mailbox empty" (len (er-vm-proc-mailbox er-vm-p2)) 0) ;; reset clears (er-vm-procs-reset!) (er-vm-test "reset clears procs" (er-vm-proc-count) 0) (er-vm-test "reset resets pid counter" (er-vm-dispatch 134 (list er-vm-fn (list))) 0) ;; ── Phase 9f — hot-BIF dispatch table ─────────────────────────── ;; Each opcode skips the registry lookup and calls the underlying ;; er-bif-* directly. Verify each returns the same result as going ;; through er-apply-bif. (er-vm-test "BIF_LENGTH opcode by id" (get (er-vm-lookup-opcode-by-id 136) :name) "OP_BIF_LENGTH") (er-vm-test "BIF_LENGTH on 3-cons" (er-vm-dispatch 136 (list (er-mk-cons 1 (er-mk-cons 2 (er-mk-cons 3 (er-mk-nil)))))) 3) (er-vm-test "BIF_HD on cons" (er-vm-dispatch 137 (list (er-mk-cons 99 (er-mk-nil)))) 99) (er-vm-test "BIF_TL is cons" (er-cons? (er-vm-dispatch 138 (list (er-mk-cons 1 (er-mk-cons 2 (er-mk-nil)))))) true) (er-vm-test "BIF_ELEMENT pulls index" (er-vm-dispatch 139 (list 2 (er-mk-tuple (list "a" "b" "c")))) "b") (er-vm-test "BIF_TUPLE_SIZE on 4-tuple" (er-vm-dispatch 140 (list (er-mk-tuple (list 1 2 3 4)))) 4) (er-vm-test "BIF_LISTS_REVERSE preserves elements" (er-list-length (er-vm-dispatch 141 (list (er-mk-cons 1 (er-mk-cons 2 (er-mk-cons 3 (er-mk-nil))))))) 3) (er-vm-test "BIF_LISTS_REVERSE actually reverses" (get (er-vm-dispatch 141 (list (er-mk-cons 1 (er-mk-cons 2 (er-mk-cons 3 (er-mk-nil)))))) :head) 3) (er-vm-test "BIF_IS_INTEGER true on int" (get (er-vm-dispatch 142 (list 42)) :name) "true") (er-vm-test "BIF_IS_INTEGER false on float" (get (er-vm-dispatch 142 (list 3.14)) :name) "false") (er-vm-test "BIF_IS_ATOM true" (get (er-vm-dispatch 143 (list (er-mk-atom "ok"))) :name) "true") (er-vm-test "BIF_IS_ATOM false on int" (get (er-vm-dispatch 143 (list 7)) :name) "false") (er-vm-test "BIF_IS_LIST true on cons" (get (er-vm-dispatch 144 (list (er-mk-cons 1 (er-mk-nil)))) :name) "true") (er-vm-test "BIF_IS_LIST true on nil" (get (er-vm-dispatch 144 (list (er-mk-nil))) :name) "true") (er-vm-test "BIF_IS_LIST false on tuple" (get (er-vm-dispatch 144 (list (er-mk-tuple (list)))) :name) "false") (er-vm-test "BIF_IS_TUPLE true" (get (er-vm-dispatch 145 (list (er-mk-tuple (list 1)))) :name) "true") (er-vm-test "BIF_IS_TUPLE false on int" (get (er-vm-dispatch 145 (list 5)) :name) "false") ;; Sanity: total opcode count grew (3 patterns + perform + handle + ;; receive-scan + spawn + send + 10 hot-BIFs = 16+ registered). (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))