308 lines
10 KiB
Plaintext
308 lines
10 KiB
Plaintext
;; 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 <<A:8>> 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)
|
|
|
|
(define er-vm-test-summary (str "vm " er-vm-test-pass "/" er-vm-test-count))
|