Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 10s
1131 lines
46 KiB
Plaintext
1131 lines
46 KiB
Plaintext
;; Erlang evaluator tests — sequential expressions.
|
|
|
|
(define er-eval-test-count 0)
|
|
(define er-eval-test-pass 0)
|
|
(define er-eval-test-fails (list))
|
|
|
|
(define
|
|
eev-deep=
|
|
(fn
|
|
(a b)
|
|
(cond
|
|
(and (= (type-of a) "dict") (= (type-of b) "dict"))
|
|
(let
|
|
((ka (sort (keys a))) (kb (sort (keys b))))
|
|
(and (= ka kb) (every? (fn (k) (eev-deep= (get a k) (get b k))) ka)))
|
|
(and (= (type-of a) "list") (= (type-of b) "list"))
|
|
(and
|
|
(= (len a) (len b))
|
|
(every? (fn (i) (eev-deep= (nth a i) (nth b i))) (range 0 (len a))))
|
|
:else (= a b))))
|
|
|
|
(define
|
|
er-eval-test
|
|
(fn
|
|
(name actual expected)
|
|
(set! er-eval-test-count (+ er-eval-test-count 1))
|
|
(if
|
|
(eev-deep= actual expected)
|
|
(set! er-eval-test-pass (+ er-eval-test-pass 1))
|
|
(append! er-eval-test-fails {:actual actual :expected expected :name name}))))
|
|
|
|
(define ev erlang-eval-ast)
|
|
(define nm (fn (v) (get v :name)))
|
|
|
|
;; ── literals ──────────────────────────────────────────────────────
|
|
(er-eval-test "int" (ev "42") 42)
|
|
(er-eval-test "zero" (ev "0") 0)
|
|
(er-eval-test "float" (ev "3.14") 3.14)
|
|
(er-eval-test "string" (ev "\"hi\"") "hi")
|
|
(er-eval-test "atom" (nm (ev "ok")) "ok")
|
|
(er-eval-test "atom true" (nm (ev "true")) "true")
|
|
(er-eval-test "atom false" (nm (ev "false")) "false")
|
|
|
|
;; ── arithmetic ────────────────────────────────────────────────────
|
|
(er-eval-test "add" (ev "1 + 2") 3)
|
|
(er-eval-test "sub" (ev "5 - 3") 2)
|
|
(er-eval-test "mul" (ev "4 * 3") 12)
|
|
(er-eval-test "div-real" (ev "10 / 4") 2.5)
|
|
(er-eval-test "div-int" (ev "10 div 3") 3)
|
|
(er-eval-test "rem" (ev "10 rem 3") 1)
|
|
(er-eval-test "div-neg" (ev "-10 div 3") -3)
|
|
(er-eval-test "precedence" (ev "1 + 2 * 3") 7)
|
|
(er-eval-test "parens" (ev "(1 + 2) * 3") 9)
|
|
(er-eval-test "unary-neg" (ev "-(1 + 2)") -3)
|
|
(er-eval-test "unary-neg int" (ev "-7") -7)
|
|
|
|
;; ── comparison ────────────────────────────────────────────────────
|
|
(er-eval-test "lt true" (nm (ev "1 < 2")) "true")
|
|
(er-eval-test "gt false" (nm (ev "1 > 2")) "false")
|
|
(er-eval-test "le equal" (nm (ev "2 =< 2")) "true")
|
|
(er-eval-test "ge equal" (nm (ev "2 >= 2")) "true")
|
|
(er-eval-test "eq" (nm (ev "2 == 2")) "true")
|
|
(er-eval-test "neq" (nm (ev "1 /= 2")) "true")
|
|
(er-eval-test "exact-eq same" (nm (ev "1 =:= 1")) "true")
|
|
(er-eval-test "exact-neq int" (nm (ev "1 =:= 2")) "false")
|
|
(er-eval-test "=/= true" (nm (ev "1 =/= 2")) "true")
|
|
(er-eval-test "atom-eq" (nm (ev "ok == ok")) "true")
|
|
(er-eval-test "atom-neq" (nm (ev "ok == error")) "false")
|
|
|
|
;; ── logical ───────────────────────────────────────────────────────
|
|
(er-eval-test "and tt" (nm (ev "true and true")) "true")
|
|
(er-eval-test "and tf" (nm (ev "true and false")) "false")
|
|
(er-eval-test "or tf" (nm (ev "true or false")) "true")
|
|
(er-eval-test
|
|
"andalso short"
|
|
(nm (ev "false andalso Neverref"))
|
|
"false")
|
|
(er-eval-test
|
|
"orelse short"
|
|
(nm (ev "true orelse Neverref"))
|
|
"true")
|
|
(er-eval-test "not true" (nm (ev "not true")) "false")
|
|
(er-eval-test "not false" (nm (ev "not false")) "true")
|
|
|
|
;; ── tuples & lists ────────────────────────────────────────────────
|
|
(er-eval-test "tuple tag" (get (ev "{1, 2, 3}") :tag) "tuple")
|
|
(er-eval-test "tuple len" (len (get (ev "{1, 2, 3}") :elements)) 3)
|
|
(er-eval-test "tuple elem" (nth (get (ev "{10, 20}") :elements) 1) 20)
|
|
(er-eval-test "empty tuple" (len (get (ev "{}") :elements)) 0)
|
|
(er-eval-test "nested tuple"
|
|
(nm (nth (get (ev "{ok, error}") :elements) 0)) "ok")
|
|
(er-eval-test "nil list" (get (ev "[]") :tag) "nil")
|
|
(er-eval-test "list head" (get (ev "[1, 2, 3]") :head) 1)
|
|
(er-eval-test
|
|
"list tail tail head"
|
|
(get (get (get (ev "[1, 2, 3]") :tail) :tail) :head)
|
|
3)
|
|
|
|
;; ── list ops ──────────────────────────────────────────────────────
|
|
(er-eval-test "++ head" (get (ev "[1, 2] ++ [3]") :head) 1)
|
|
(er-eval-test "++ last"
|
|
(get (get (get (ev "[1, 2] ++ [3]") :tail) :tail) :head) 3)
|
|
|
|
;; ── block ─────────────────────────────────────────────────────────
|
|
(er-eval-test "block last wins" (ev "begin 1, 2, 3 end") 3)
|
|
(er-eval-test "bare body" (ev "1, 2, 99") 99)
|
|
|
|
;; ── match + var ───────────────────────────────────────────────────
|
|
(er-eval-test "match bind-and-use" (ev "X = 5, X + 1") 6)
|
|
(er-eval-test "match sequential" (ev "X = 1, Y = 2, X + Y") 3)
|
|
(er-eval-test
|
|
"rebind equal ok"
|
|
(ev "X = 5, X = 5, X") 5)
|
|
|
|
;; ── if ────────────────────────────────────────────────────────────
|
|
(er-eval-test "if picks first" (ev "if true -> 1; true -> 2 end") 1)
|
|
(er-eval-test
|
|
"if picks second"
|
|
(nm (ev "if 1 > 2 -> bad; true -> good end"))
|
|
"good")
|
|
(er-eval-test
|
|
"if with guard"
|
|
(ev "X = 5, if X > 0 -> 1; true -> 0 end")
|
|
1)
|
|
|
|
;; ── pattern matching ─────────────────────────────────────────────
|
|
(er-eval-test "match atom literal" (nm (ev "ok = ok, done")) "done")
|
|
(er-eval-test "match int literal" (ev "5 = 5, 42") 42)
|
|
(er-eval-test "match tuple bind"
|
|
(ev "{ok, V} = {ok, 99}, V") 99)
|
|
(er-eval-test "match tuple nested"
|
|
(ev "{A, {B, C}} = {1, {2, 3}}, A + B + C") 6)
|
|
(er-eval-test "match cons head"
|
|
(ev "[H|T] = [1, 2, 3], H") 1)
|
|
(er-eval-test "match cons tail head"
|
|
(ev "[_, H|_] = [1, 2, 3], H") 2)
|
|
(er-eval-test "match nil"
|
|
(ev "[] = [], 7") 7)
|
|
(er-eval-test "match wildcard always"
|
|
(ev "_ = 42, 7") 7)
|
|
(er-eval-test "match var reuse equal"
|
|
(ev "X = 5, X = 5, X") 5)
|
|
|
|
;; ── case ─────────────────────────────────────────────────────────
|
|
(er-eval-test "case bind" (ev "case 5 of N -> N end") 5)
|
|
(er-eval-test "case tuple"
|
|
(ev "case {ok, 42} of {ok, V} -> V end") 42)
|
|
(er-eval-test "case cons"
|
|
(ev "case [1, 2, 3] of [H|_] -> H end") 1)
|
|
(er-eval-test "case fallthrough"
|
|
(ev "case error of ok -> 1; error -> 2 end") 2)
|
|
(er-eval-test "case wildcard"
|
|
(nm (ev "case x of ok -> ok; _ -> err end"))
|
|
"err")
|
|
(er-eval-test "case guard"
|
|
(ev "case 5 of N when N > 0 -> pos; _ -> neg end")
|
|
(er-mk-atom "pos"))
|
|
(er-eval-test "case guard fallthrough"
|
|
(ev "case -3 of N when N > 0 -> pos; _ -> neg end")
|
|
(er-mk-atom "neg"))
|
|
(er-eval-test "case bound re-match"
|
|
(ev "X = 5, case 5 of X -> same; _ -> diff end")
|
|
(er-mk-atom "same"))
|
|
(er-eval-test "case bound re-match fail"
|
|
(ev "X = 5, case 6 of X -> same; _ -> diff end")
|
|
(er-mk-atom "diff"))
|
|
(er-eval-test "case nested tuple"
|
|
(ev "case {ok, {value, 42}} of {ok, {value, V}} -> V end")
|
|
42)
|
|
(er-eval-test "case multi-clause"
|
|
(ev "case 2 of 1 -> one; 2 -> two; _ -> other end")
|
|
(er-mk-atom "two"))
|
|
(er-eval-test "case leak binding"
|
|
(ev "case {ok, 7} of {ok, X} -> X end + 1")
|
|
8)
|
|
|
|
;; ── guard BIFs (is_*) ────────────────────────────────────────────
|
|
(er-eval-test "is_integer 42" (nm (ev "is_integer(42)")) "true")
|
|
(er-eval-test "is_integer ok" (nm (ev "is_integer(ok)")) "false")
|
|
(er-eval-test "is_atom ok" (nm (ev "is_atom(ok)")) "true")
|
|
(er-eval-test "is_atom int" (nm (ev "is_atom(42)")) "false")
|
|
(er-eval-test "is_list cons" (nm (ev "is_list([1,2])")) "true")
|
|
(er-eval-test "is_list nil" (nm (ev "is_list([])")) "true")
|
|
(er-eval-test "is_list tuple" (nm (ev "is_list({1,2})")) "false")
|
|
(er-eval-test "is_tuple tuple" (nm (ev "is_tuple({ok,1})")) "true")
|
|
(er-eval-test "is_tuple list" (nm (ev "is_tuple([1])")) "false")
|
|
(er-eval-test "is_number int" (nm (ev "is_number(42)")) "true")
|
|
(er-eval-test "is_number atom" (nm (ev "is_number(foo)")) "false")
|
|
(er-eval-test "is_boolean true" (nm (ev "is_boolean(true)")) "true")
|
|
(er-eval-test "is_boolean false" (nm (ev "is_boolean(false)")) "true")
|
|
(er-eval-test "is_boolean atom" (nm (ev "is_boolean(foo)")) "false")
|
|
|
|
;; ── guard BIFs wired into case / if ─────────────────────────────
|
|
(er-eval-test "guard is_integer pick"
|
|
(nm (ev "case 5 of N when is_integer(N) -> int; _ -> other end"))
|
|
"int")
|
|
(er-eval-test "guard is_integer reject"
|
|
(nm (ev "case foo of N when is_integer(N) -> int; _ -> other end"))
|
|
"other")
|
|
(er-eval-test "guard is_atom"
|
|
(nm (ev "case foo of X when is_atom(X) -> atom_yes; _ -> no end"))
|
|
"atom_yes")
|
|
(er-eval-test "guard conjunction"
|
|
(nm (ev "case 5 of N when is_integer(N), N > 0 -> pos; _ -> np end"))
|
|
"pos")
|
|
(er-eval-test "guard disjunction (if)"
|
|
(nm (ev "X = foo, if is_integer(X); is_atom(X) -> yes; true -> no end"))
|
|
"yes")
|
|
(er-eval-test "guard arith"
|
|
(nm (ev "case 3 of N when N * 2 > 5 -> big; _ -> small end"))
|
|
"big")
|
|
|
|
;; ── BIFs: list + tuple ──────────────────────────────────────────
|
|
(er-eval-test "length empty" (ev "length([])") 0)
|
|
(er-eval-test "length 3" (ev "length([a, b, c])") 3)
|
|
(er-eval-test "length cons chain" (ev "length([1 | [2 | [3 | []]]])") 3)
|
|
(er-eval-test "hd" (ev "hd([10, 20, 30])") 10)
|
|
(er-eval-test "hd atom"
|
|
(nm (ev "hd([ok, err])")) "ok")
|
|
(er-eval-test "tl head"
|
|
(get (ev "tl([1, 2, 3])") :head) 2)
|
|
(er-eval-test "tl of single" (get (ev "tl([1])") :tag) "nil")
|
|
(er-eval-test "element 1" (nm (ev "element(1, {ok, value})")) "ok")
|
|
(er-eval-test "element 2" (ev "element(2, {ok, 42})") 42)
|
|
(er-eval-test "element 3"
|
|
(nm (ev "element(3, {a, b, c, d})")) "c")
|
|
(er-eval-test "tuple_size 2" (ev "tuple_size({a, b})") 2)
|
|
(er-eval-test "tuple_size 0" (ev "tuple_size({})") 0)
|
|
|
|
;; ── BIFs: atom / list conversions ───────────────────────────────
|
|
(er-eval-test "atom_to_list" (ev "atom_to_list(hello)") "hello")
|
|
(er-eval-test "list_to_atom roundtrip"
|
|
(nm (ev "list_to_atom(atom_to_list(foo))")) "foo")
|
|
(er-eval-test "list_to_atom fresh"
|
|
(nm (ev "list_to_atom(\"bar\")")) "bar")
|
|
|
|
;; ── lists module ────────────────────────────────────────────────
|
|
(er-eval-test "lists:reverse empty"
|
|
(get (ev "lists:reverse([])") :tag) "nil")
|
|
(er-eval-test "lists:reverse 3"
|
|
(ev "hd(lists:reverse([1, 2, 3]))") 3)
|
|
(er-eval-test "lists:reverse full"
|
|
(ev "lists:foldl(fun (X, Acc) -> Acc + X end, 0, lists:reverse([1, 2, 3]))") 6)
|
|
|
|
;; ── funs + lists:map / lists:foldl ──────────────────────────────
|
|
(er-eval-test "fun call" (ev "F = fun (X) -> X + 1 end, F(10)") 11)
|
|
(er-eval-test "fun two-arg"
|
|
(ev "F = fun (X, Y) -> X * Y end, F(3, 4)") 12)
|
|
(er-eval-test "fun closure"
|
|
(ev "N = 100, F = fun (X) -> X + N end, F(5)") 105)
|
|
(er-eval-test "fun clauses"
|
|
(ev "F = fun (0) -> zero; (N) -> N end, element(1, {F(0), F(7)})")
|
|
(er-mk-atom "zero"))
|
|
(er-eval-test "fun multi-clause second"
|
|
(ev "F = fun (0) -> 0; (N) -> N * 2 end, F(5)") 10)
|
|
(er-eval-test "lists:map empty"
|
|
(get (ev "lists:map(fun (X) -> X end, [])") :tag) "nil")
|
|
(er-eval-test "lists:map double"
|
|
(ev "hd(lists:map(fun (X) -> X * 2 end, [1, 2, 3]))") 2)
|
|
(er-eval-test "lists:map sum-length"
|
|
(ev "length(lists:map(fun (X) -> X end, [a, b, c, d]))") 4)
|
|
(er-eval-test "lists:foldl sum"
|
|
(ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [1, 2, 3, 4, 5])") 15)
|
|
(er-eval-test "lists:foldl product"
|
|
(ev "lists:foldl(fun (X, Acc) -> X * Acc end, 1, [1, 2, 3, 4])") 24)
|
|
(er-eval-test "lists:foldl as reverse"
|
|
(ev "hd(lists:foldl(fun (X, Acc) -> [X | Acc] end, [], [1, 2, 3]))") 3)
|
|
|
|
;; ── io:format (via capture buffer) ──────────────────────────────
|
|
(er-eval-test "io:format plain"
|
|
(do (er-io-flush!) (ev "io:format(\"hello~n\")") (er-io-buffer-content))
|
|
"hello\n")
|
|
(er-eval-test "io:format args"
|
|
(do (er-io-flush!) (ev "io:format(\"x=~p y=~p~n\", [42, hello])") (er-io-buffer-content))
|
|
"x=42 y=hello\n")
|
|
(er-eval-test "io:format returns ok"
|
|
(nm (do (er-io-flush!) (ev "io:format(\"~n\")"))) "ok")
|
|
(er-eval-test "io:format tuple"
|
|
(do (er-io-flush!) (ev "io:format(\"~p\", [{ok, 1}])") (er-io-buffer-content))
|
|
"{ok,1}")
|
|
(er-eval-test "io:format list"
|
|
(do (er-io-flush!) (ev "io:format(\"~p\", [[1,2,3]])") (er-io-buffer-content))
|
|
"[1,2,3]")
|
|
(er-eval-test "io:format escape"
|
|
(do (er-io-flush!) (ev "io:format(\"50~~\")") (er-io-buffer-content))
|
|
"50~")
|
|
|
|
;; ── processes: self/0, spawn/1, is_pid ──────────────────────────
|
|
(er-eval-test "self tag"
|
|
(get (ev "self()") :tag) "pid")
|
|
(er-eval-test "is_pid self"
|
|
(nm (ev "is_pid(self())")) "true")
|
|
(er-eval-test "is_pid number"
|
|
(nm (ev "is_pid(42)")) "false")
|
|
(er-eval-test "is_pid atom"
|
|
(nm (ev "is_pid(ok)")) "false")
|
|
(er-eval-test "self equals self"
|
|
(nm (ev "Pid = self(), Pid =:= Pid")) "true")
|
|
(er-eval-test "self =:= self expr"
|
|
(nm (ev "self() == self()")) "true")
|
|
(er-eval-test "spawn returns pid"
|
|
(get (ev "spawn(fun () -> ok end)") :tag) "pid")
|
|
(er-eval-test "is_pid spawn"
|
|
(nm (ev "is_pid(spawn(fun () -> ok end))")) "true")
|
|
(er-eval-test "spawn new pid distinct"
|
|
(nm (ev "P1 = self(), P2 = spawn(fun () -> ok end), P1 =:= P2"))
|
|
"false")
|
|
(er-eval-test "two spawns distinct"
|
|
(nm (ev "P1 = spawn(fun () -> ok end), P2 = spawn(fun () -> ok end), P1 =:= P2"))
|
|
"false")
|
|
(er-eval-test "spawn then drain io"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "spawn(fun () -> io:format(\"child~n\") end), io:format(\"parent~n\")")
|
|
(er-io-buffer-content))
|
|
"parent\nchild\n")
|
|
(er-eval-test "multiple spawn ordering"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "spawn(fun () -> io:format(\"a~n\") end), spawn(fun () -> io:format(\"b~n\") end), io:format(\"main~n\")")
|
|
(er-io-buffer-content))
|
|
"main\na\nb\n")
|
|
(er-eval-test "child self is its own pid"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "P = spawn(fun () -> io:format(\"~p\", [is_pid(self())]) end), io:format(\"~p;\", [is_pid(P)])")
|
|
(er-io-buffer-content))
|
|
"true;true")
|
|
|
|
;; ── ! (send) + receive ──────────────────────────────────────────
|
|
(er-eval-test "self-send + receive"
|
|
(nm (ev "Me = self(), Me ! hello, receive Msg -> Msg end")) "hello")
|
|
(er-eval-test "send returns msg"
|
|
(nm (ev "Me = self(), Msg = Me ! ok, Me ! x, receive _ -> Msg end")) "ok")
|
|
(er-eval-test "receive int"
|
|
(ev "Me = self(), Me ! 42, receive N -> N + 1 end") 43)
|
|
(er-eval-test "receive with pattern"
|
|
(ev "Me = self(), Me ! {ok, 7}, receive {ok, V} -> V * 2 end") 14)
|
|
(er-eval-test "receive with guard"
|
|
(ev "Me = self(), Me ! 5, receive N when N > 0 -> positive end")
|
|
(er-mk-atom "positive"))
|
|
(er-eval-test "receive skips non-match"
|
|
(nm (ev "Me = self(), Me ! wrong, Me ! right, receive right -> ok end"))
|
|
"ok")
|
|
(er-eval-test "receive selective leaves others"
|
|
(nm (ev "Me = self(), Me ! a, Me ! b, receive b -> got_b end"))
|
|
"got_b")
|
|
(er-eval-test "two receives consume both"
|
|
(ev "Me = self(), Me ! 1, Me ! 2, X = receive A -> A end, Y = receive B -> B end, X + Y") 3)
|
|
|
|
;; ── spawn + send + receive (real process communication) ─────────
|
|
(er-eval-test "spawn sends back"
|
|
(nm
|
|
(ev "Me = self(), spawn(fun () -> Me ! pong end), receive pong -> got_pong end"))
|
|
"got_pong")
|
|
(er-eval-test "ping-pong"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "Me = self(), Child = spawn(fun () -> receive {ping, From} -> From ! pong end end), Child ! {ping, Me}, receive pong -> io:format(\"pong~n\") end")
|
|
(er-io-buffer-content))
|
|
"pong\n")
|
|
(er-eval-test "echo server"
|
|
(ev "Me = self(), Echo = spawn(fun () -> receive {From, Msg} -> From ! Msg end end), Echo ! {Me, 99}, receive R -> R end") 99)
|
|
|
|
;; ── receive with multiple clauses ────────────────────────────────
|
|
(er-eval-test "receive multi-clause"
|
|
(nm (ev "Me = self(), Me ! foo, receive ok -> a; foo -> b; bar -> c end"))
|
|
"b")
|
|
(er-eval-test "receive nested tuple"
|
|
(ev "Me = self(), Me ! {result, {ok, 42}}, receive {result, {ok, V}} -> V end") 42)
|
|
|
|
;; ── receive ... after ... ───────────────────────────────────────
|
|
(er-eval-test "after 0 empty mailbox"
|
|
(nm (ev "receive _ -> got after 0 -> timeout end"))
|
|
"timeout")
|
|
(er-eval-test "after 0 match wins"
|
|
(nm (ev "Me = self(), Me ! ok, receive ok -> got after 0 -> timeout end"))
|
|
"got")
|
|
(er-eval-test "after 0 non-match fires timeout"
|
|
(nm (ev "Me = self(), Me ! wrong, receive right -> got after 0 -> timeout end"))
|
|
"timeout")
|
|
(er-eval-test "after 0 leaves non-match"
|
|
(ev "Me = self(), Me ! wrong, receive right -> got after 0 -> to end, receive X -> X end")
|
|
(er-mk-atom "wrong"))
|
|
(er-eval-test "after Ms no sender — timeout fires"
|
|
(nm (ev "receive _ -> got after 100 -> timed_out end"))
|
|
"timed_out")
|
|
(er-eval-test "after Ms with sender — match wins"
|
|
(nm (ev "Me = self(), spawn(fun () -> Me ! hi end), receive hi -> got after 100 -> to end"))
|
|
"got")
|
|
(er-eval-test "after Ms computed"
|
|
(nm (ev "Ms = 50, receive _ -> got after Ms -> done end"))
|
|
"done")
|
|
(er-eval-test "after 0 body side effect"
|
|
(do (er-io-flush!)
|
|
(ev "receive _ -> ok after 0 -> io:format(\"to~n\") end")
|
|
(er-io-buffer-content))
|
|
"to\n")
|
|
(er-eval-test "after zero poll selective"
|
|
(ev "Me = self(), Me ! first, Me ! second, X = receive second -> got_second after 0 -> to end, Y = receive first -> got_first after 0 -> to end, {X, Y}")
|
|
(er-mk-tuple (list (er-mk-atom "got_second") (er-mk-atom "got_first"))))
|
|
|
|
;; ── exit/1 + process termination ─────────────────────────────────
|
|
(er-eval-test "exit normal returns nil" (ev "exit(normal)") nil)
|
|
(er-eval-test "exit normal reason"
|
|
(do (ev "exit(normal)") (nm (er-last-main-exit-reason))) "normal")
|
|
(er-eval-test "exit bye reason"
|
|
(do (ev "exit(bye)") (nm (er-last-main-exit-reason))) "bye")
|
|
(er-eval-test "exit tuple reason"
|
|
(do (ev "exit({shutdown, crash})")
|
|
(get (er-last-main-exit-reason) :tag))
|
|
"tuple")
|
|
(er-eval-test "normal completion reason"
|
|
(do (ev "42") (nm (er-last-main-exit-reason))) "normal")
|
|
(er-eval-test "exit aborts subsequent"
|
|
(do (er-io-flush!) (ev "io:format(\"a~n\"), exit(bye), io:format(\"b~n\")") (er-io-buffer-content))
|
|
"a\n")
|
|
(er-eval-test "child exit doesn't kill parent"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "spawn(fun () -> io:format(\"before~n\"), exit(quit), io:format(\"after~n\") end), io:format(\"main~n\")")
|
|
(er-io-buffer-content))
|
|
"main\nbefore\n")
|
|
(er-eval-test "child exit reason recorded on child"
|
|
(do
|
|
(er-io-flush!)
|
|
(ev "P = spawn(fun () -> exit(child_bye) end), io:format(\"~p\", [is_pid(P)])")
|
|
(er-io-buffer-content))
|
|
"true")
|
|
(er-eval-test "exit inside fn chain"
|
|
(do (ev "F = fun () -> exit(from_fn) end, F()")
|
|
(nm (er-last-main-exit-reason)))
|
|
"from_fn")
|
|
|
|
;; ── refs / link / monitor ──────────────────────────────────────
|
|
(er-eval-test "make_ref tag"
|
|
(get (ev "make_ref()") :tag) "ref")
|
|
(er-eval-test "is_reference fresh"
|
|
(nm (ev "R = make_ref(), is_reference(R)")) "true")
|
|
(er-eval-test "is_reference pid"
|
|
(nm (ev "is_reference(self())")) "false")
|
|
(er-eval-test "is_reference number"
|
|
(nm (ev "is_reference(42)")) "false")
|
|
(er-eval-test "make_ref distinct"
|
|
(nm (ev "R1 = make_ref(), R2 = make_ref(), R1 =:= R2")) "false")
|
|
(er-eval-test "make_ref same id eq"
|
|
(nm (ev "R = make_ref(), R =:= R")) "true")
|
|
|
|
(er-eval-test "link returns true"
|
|
(nm (ev "P = spawn(fun () -> ok end), link(P)")) "true")
|
|
(er-eval-test "self link returns true"
|
|
(nm (ev "link(self())")) "true")
|
|
(er-eval-test "unlink returns true"
|
|
(nm (ev "P = spawn(fun () -> ok end), link(P), unlink(P)")) "true")
|
|
(er-eval-test "unlink without link"
|
|
(nm (ev "P = spawn(fun () -> ok end), unlink(P)")) "true")
|
|
|
|
(er-eval-test "monitor returns ref"
|
|
(get (ev "P = spawn(fun () -> ok end), monitor(process, P)") :tag)
|
|
"ref")
|
|
(er-eval-test "monitor refs distinct"
|
|
(nm (ev "P = spawn(fun () -> ok end), R1 = monitor(process, P), R2 = monitor(process, P), R1 =:= R2"))
|
|
"false")
|
|
(er-eval-test "demonitor returns true"
|
|
(nm (ev "P = spawn(fun () -> ok end), R = monitor(process, P), demonitor(R)"))
|
|
"true")
|
|
|
|
;; Bidirectional link recorded on both sides.
|
|
(er-eval-test "link bidirectional"
|
|
(do
|
|
(ev "P = spawn(fun () -> receive forever -> ok end end), link(P)")
|
|
;; After eval, check links on main + child via accessors.
|
|
(and
|
|
(= (len (er-proc-field (er-mk-pid 0) :links)) 1)
|
|
(= (len (er-proc-field (er-mk-pid 1) :links)) 1)))
|
|
true)
|
|
|
|
;; unlink clears both sides.
|
|
(er-eval-test "unlink clears both"
|
|
(do
|
|
(ev "P = spawn(fun () -> receive forever -> ok end end), link(P), unlink(P)")
|
|
(and
|
|
(= (len (er-proc-field (er-mk-pid 0) :links)) 0)
|
|
(= (len (er-proc-field (er-mk-pid 1) :links)) 0)))
|
|
true)
|
|
|
|
;; monitor adds entries to both lists.
|
|
(er-eval-test "monitor records both sides"
|
|
(do
|
|
(ev "P = spawn(fun () -> receive forever -> ok end end), monitor(process, P)")
|
|
(and
|
|
(= (len (er-proc-field (er-mk-pid 0) :monitors)) 1)
|
|
(= (len (er-proc-field (er-mk-pid 1) :monitored-by)) 1)))
|
|
true)
|
|
|
|
;; demonitor clears both lists.
|
|
(er-eval-test "demonitor clears both"
|
|
(do
|
|
(ev "P = spawn(fun () -> receive forever -> ok end end), R = monitor(process, P), demonitor(R)")
|
|
(and
|
|
(= (len (er-proc-field (er-mk-pid 0) :monitors)) 0)
|
|
(= (len (er-proc-field (er-mk-pid 1) :monitored-by)) 0)))
|
|
true)
|
|
|
|
;; ── exit-signal propagation + trap_exit ────────────────────────
|
|
(er-eval-test "process_flag default false"
|
|
(nm (ev "process_flag(trap_exit, true)")) "false")
|
|
(er-eval-test "process_flag returns prev"
|
|
(nm (ev "process_flag(trap_exit, true), process_flag(trap_exit, false)"))
|
|
"true")
|
|
|
|
;; Monitor fires on normal exit.
|
|
(er-eval-test "monitor DOWN normal"
|
|
(nm (ev "P = spawn(fun () -> ok end), monitor(process, P), receive {'DOWN', _, process, _, R} -> R end"))
|
|
"normal")
|
|
|
|
;; Monitor fires on abnormal exit.
|
|
(er-eval-test "monitor DOWN abnormal"
|
|
(nm (ev "P = spawn(fun () -> exit(boom) end), monitor(process, P), receive {'DOWN', _, process, _, R} -> R end"))
|
|
"boom")
|
|
|
|
;; Monitor's ref appears in DOWN message.
|
|
(er-eval-test "monitor DOWN ref matches"
|
|
(nm (ev "P = spawn(fun () -> exit(bye) end), Ref = monitor(process, P), receive {'DOWN', Ref, process, _, _} -> ok_match end"))
|
|
"ok_match")
|
|
|
|
;; Two monitors -> both fire.
|
|
(er-eval-test "two monitors both fire"
|
|
(ev "P = spawn(fun () -> exit(crash) end), monitor(process, P), monitor(process, P), receive {'DOWN', _, _, _, _} -> ok end, receive {'DOWN', _, _, _, _} -> 2 end")
|
|
2)
|
|
|
|
;; trap_exit + link + abnormal exit -> {'EXIT', From, Reason} message.
|
|
(er-eval-test "trap_exit catches abnormal"
|
|
(nm (ev "process_flag(trap_exit, true), P = spawn(fun () -> exit(boom) end), link(P), receive {'EXIT', _, R} -> R end"))
|
|
"boom")
|
|
|
|
;; trap_exit + link + normal exit -> {'EXIT', From, normal}.
|
|
(er-eval-test "trap_exit catches normal"
|
|
(nm (ev "process_flag(trap_exit, true), P = spawn(fun () -> ok end), link(P), receive {'EXIT', _, R} -> R end"))
|
|
"normal")
|
|
|
|
;; Cascade exit: A links B, B dies abnormally, A dies with same reason.
|
|
(er-eval-test "cascade reason"
|
|
(do
|
|
(ev "A = spawn(fun () -> B = spawn(fun () -> exit(crash) end), link(B), receive forever -> ok end end), receive after 0 -> ok end")
|
|
(nm (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
"crash")
|
|
|
|
;; Normal exit doesn't cascade (without trap_exit) — A's body returns
|
|
;; "survived" via the `after` clause and A dies normally.
|
|
(er-eval-test "normal exit no cascade"
|
|
(do
|
|
(ev "A = spawn(fun () -> B = spawn(fun () -> ok end), link(B), receive {'EXIT', _, _} -> got_exit after 50 -> survived end end), receive after 0 -> ok end")
|
|
(list
|
|
(nm (er-proc-field (er-mk-pid 1) :exit-reason))
|
|
(nm (er-proc-field (er-mk-pid 1) :exit-result))))
|
|
(list "normal" "survived"))
|
|
|
|
;; Monitor without trap_exit: monitored proc abnormal doesn't kill the monitor.
|
|
(er-eval-test "monitor doesn't cascade"
|
|
(nm (ev "P = spawn(fun () -> exit(boom) end), monitor(process, P), receive {'DOWN', _, _, _, _} -> alive end"))
|
|
"alive")
|
|
|
|
;; ── try / catch / of / after ─────────────────────────────────
|
|
(er-eval-test "try plain"
|
|
(ev "try 1 + 2 catch _ -> oops end") 3)
|
|
|
|
(er-eval-test "try throw caught"
|
|
(nm (ev "try throw(boom) catch throw:X -> X end")) "boom")
|
|
(er-eval-test "try error caught"
|
|
(nm (ev "try error(crash) catch error:X -> X end")) "crash")
|
|
(er-eval-test "try exit caught"
|
|
(nm (ev "try exit(quit) catch exit:X -> X end")) "quit")
|
|
|
|
(er-eval-test "default class is throw"
|
|
(nm (ev "try throw(bye) catch X -> X end")) "bye")
|
|
(er-eval-test "default class doesn't catch error"
|
|
(do
|
|
(ev "P = spawn(fun () -> try error(crash) catch X -> X end end), receive after 0 -> ok end")
|
|
(nm (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
"crash")
|
|
|
|
;; of clauses
|
|
(er-eval-test "try of single"
|
|
(ev "try 42 of N -> N * 2 catch _ -> 0 end") 84)
|
|
(er-eval-test "try of multi"
|
|
(nm (ev "try ok of ok -> matched; _ -> nope catch _ -> oops end"))
|
|
"matched")
|
|
(er-eval-test "try of fallthrough"
|
|
(nm (ev "try x of ok -> a; error -> b; _ -> default catch _ -> oops end"))
|
|
"default")
|
|
(er-eval-test "try of with guard"
|
|
(nm (ev "try 5 of N when N > 0 -> pos; _ -> nonneg catch _ -> oops end"))
|
|
"pos")
|
|
|
|
;; after clause
|
|
(er-eval-test "after on success"
|
|
(do (er-io-flush!)
|
|
(ev "try 7 after io:format(\"a\") end")
|
|
(er-io-buffer-content))
|
|
"a")
|
|
(er-eval-test "after on caught"
|
|
(do (er-io-flush!)
|
|
(ev "try throw(b) catch throw:_ -> caught after io:format(\"x\") end")
|
|
(er-io-buffer-content))
|
|
"x")
|
|
(er-eval-test "after returns body value"
|
|
(ev "try 99 after 0 end") 99)
|
|
(er-eval-test "try preserves catch result"
|
|
(nm (ev "try throw(x) catch throw:_ -> recovered after 0 end"))
|
|
"recovered")
|
|
|
|
;; nested try
|
|
(er-eval-test "try nested catch outer"
|
|
(nm (ev "try (try throw(inner) catch error:_ -> bad end) catch throw:X -> X end"))
|
|
"inner")
|
|
(er-eval-test "try nested catch inner"
|
|
(nm (ev "try (try throw(inner) catch throw:X -> X end) catch _ -> outer end"))
|
|
"inner")
|
|
|
|
;; class re-raise on no-match
|
|
(er-eval-test "throw without catch-throw escapes"
|
|
(do
|
|
(ev "P = spawn(fun () -> try throw(bye) catch error:_ -> nope end end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(and (er-tuple? reason) (nm (nth (get reason :elements) 0)))))
|
|
"nocatch")
|
|
|
|
;; multi-clause catch
|
|
(er-eval-test "multi-clause catch picks throw"
|
|
(nm (ev "try throw(a) catch error:X -> e; throw:X -> t; exit:X -> x end"))
|
|
"t")
|
|
(er-eval-test "multi-clause catch picks exit"
|
|
(nm (ev "try exit(a) catch error:X -> e; throw:X -> t; exit:X -> x end"))
|
|
"x")
|
|
|
|
;; ── modules: -module(M)., M:F/N cross-module calls ─────────────
|
|
(er-eval-test "load module returns name"
|
|
(nm (erlang-load-module "-module(m1). foo() -> 42."))
|
|
"m1")
|
|
|
|
(er-eval-test "cross-module zero-arity"
|
|
(do
|
|
(erlang-load-module "-module(m2). val() -> 7.")
|
|
(ev "m2:val()"))
|
|
7)
|
|
|
|
(er-eval-test "cross-module n-ary"
|
|
(do
|
|
(erlang-load-module "-module(m3). add(X, Y) -> X + Y.")
|
|
(ev "m3:add(3, 4)"))
|
|
7)
|
|
|
|
(er-eval-test "module recursive fn"
|
|
(do
|
|
(erlang-load-module "-module(m4). fact(0) -> 1; fact(N) -> N * fact(N-1).")
|
|
(ev "m4:fact(6)"))
|
|
720)
|
|
|
|
(er-eval-test "module sibling calls"
|
|
(do
|
|
(erlang-load-module "-module(m5). a(X) -> b(X) + 1. b(X) -> X * 10.")
|
|
(ev "m5:a(5)"))
|
|
51)
|
|
|
|
(er-eval-test "module multi-arity"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(m6). f(X) -> X. f(X, Y) -> X + Y. f(X, Y, Z) -> X * Y + Z.")
|
|
(ev "{m6:f(1), m6:f(2, 3), m6:f(2, 3, 4)}"))
|
|
(er-mk-tuple (list 1 5 10)))
|
|
|
|
(er-eval-test "module pattern match clauses"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(m7). check(0) -> zero; check(N) when N > 0 -> pos; check(_) -> neg.")
|
|
(nm (ev "m7:check(-3)")))
|
|
"neg")
|
|
|
|
(er-eval-test "cross-module call within module"
|
|
(do
|
|
(erlang-load-module "-module(util1). dbl(X) -> X * 2.")
|
|
(erlang-load-module "-module(util2). quad(X) -> util1:dbl(X) * 2.")
|
|
(ev "util2:quad(5)"))
|
|
20)
|
|
|
|
(er-eval-test "module undefined fn raises"
|
|
(do
|
|
(erlang-load-module "-module(m8). foo() -> 1.")
|
|
(er-io-flush!)
|
|
(ev "P = spawn(fun () -> m8:bar() end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(and (er-tuple? reason) (nm (nth (get reason :elements) 0)))))
|
|
"undef")
|
|
|
|
(er-eval-test "module function used in spawn"
|
|
(do
|
|
(erlang-load-module "-module(m9). work(P) -> P ! done.")
|
|
(ev "Me = self(), spawn(fun () -> m9:work(Me) end), receive done -> ok end"))
|
|
(er-mk-atom "ok"))
|
|
|
|
;; ── gen_server (OTP-lite) ──────────────────────────────────────
|
|
(do
|
|
(er-load-gen-server!)
|
|
(erlang-load-module
|
|
"-module(ctr).
|
|
init(N) -> {ok, N}.
|
|
handle_call(get, _F, S) -> {reply, S, S}.
|
|
handle_call({set, V}, _F, _S) -> {reply, ok, V}.
|
|
handle_call({add, K}, _F, S) -> {reply, S + K, S + K}.
|
|
handle_cast(inc, S) -> {noreply, S + 1}.
|
|
handle_cast(dec, S) -> {noreply, S - 1}.
|
|
handle_cast({add, K}, S) -> {noreply, S + K}.
|
|
handle_info(_M, S) -> {noreply, S}.")
|
|
nil)
|
|
|
|
(er-eval-test "gen_server start + call get"
|
|
(ev "P = gen_server:start_link(ctr, 10), gen_server:call(P, get)")
|
|
10)
|
|
|
|
(er-eval-test "gen_server cast then call"
|
|
(ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, inc), gen_server:cast(P, inc), gen_server:cast(P, inc), gen_server:call(P, get)")
|
|
3)
|
|
|
|
(er-eval-test "gen_server call returns reply"
|
|
(ev "P = gen_server:start_link(ctr, 5), gen_server:call(P, {add, 7})")
|
|
12)
|
|
|
|
(er-eval-test "gen_server state mutation"
|
|
(ev "P = gen_server:start_link(ctr, 5), gen_server:call(P, {set, 99}), gen_server:call(P, get)")
|
|
99)
|
|
|
|
(er-eval-test "gen_server stop returns ok"
|
|
(nm (ev "P = gen_server:start_link(ctr, 0), gen_server:stop(P)"))
|
|
"ok")
|
|
|
|
(er-eval-test "gen_server cast returns ok immediately"
|
|
(nm (ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, inc)"))
|
|
"ok")
|
|
|
|
(er-eval-test "gen_server multi-state mutations"
|
|
(ev "P = gen_server:start_link(ctr, 0), gen_server:cast(P, {add, 100}), gen_server:cast(P, dec), gen_server:cast(P, dec), gen_server:call(P, get)")
|
|
98)
|
|
|
|
;; Stack server — exercises a different state shape.
|
|
(do
|
|
(erlang-load-module
|
|
"-module(stk).
|
|
init(_) -> {ok, []}.
|
|
handle_call(pop, _F, []) -> {reply, empty, []};
|
|
handle_call(pop, _F, [H | T]) -> {reply, {ok, H}, T};
|
|
handle_call(peek, _F, []) -> {reply, empty, []};
|
|
handle_call(peek, _F, [H | T]) -> {reply, {ok, H}, [H | T]};
|
|
handle_call(size, _F, S) -> {reply, length(S), S}.
|
|
handle_cast({push, V}, S) -> {noreply, [V | S]}.
|
|
handle_info(_M, S) -> {noreply, S}.")
|
|
nil)
|
|
|
|
(er-eval-test "stack push/pop"
|
|
(ev "P = gen_server:start_link(stk, ignored), gen_server:cast(P, {push, 1}), gen_server:cast(P, {push, 2}), gen_server:cast(P, {push, 3}), gen_server:call(P, size)")
|
|
3)
|
|
|
|
(er-eval-test "stack lifo"
|
|
(ev "P = gen_server:start_link(stk, ignored), gen_server:cast(P, {push, 1}), gen_server:cast(P, {push, 2}), gen_server:cast(P, {push, 3}), {ok, V} = gen_server:call(P, pop), V")
|
|
3)
|
|
|
|
(er-eval-test "stack empty pop"
|
|
(nm (ev "P = gen_server:start_link(stk, ignored), gen_server:call(P, pop)"))
|
|
"empty")
|
|
|
|
;; ── supervisor (one-for-one) ────────────────────────────────────
|
|
(do
|
|
(er-load-supervisor!)
|
|
(erlang-load-module
|
|
"-module(echoer).
|
|
start() -> spawn(fun () -> echoer:loop() end).
|
|
loop() ->
|
|
receive
|
|
{ping, From} -> From ! pong, echoer:loop();
|
|
die -> exit(killed)
|
|
end.")
|
|
nil)
|
|
|
|
(er-eval-test "sup starts children"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup1). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.")
|
|
(ev "Sup = supervisor:start_link(sup1, []), receive after 5 -> ok end, length(supervisor:which_children(Sup))"))
|
|
1)
|
|
|
|
(er-eval-test "sup multiple children"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup2).
|
|
init(_) -> {ok, [
|
|
{w1, fun () -> echoer:start() end},
|
|
{w2, fun () -> echoer:start() end},
|
|
{w3, fun () -> echoer:start() end}
|
|
]}.")
|
|
(ev "Sup = supervisor:start_link(sup2, []), receive after 5 -> ok end, length(supervisor:which_children(Sup))"))
|
|
3)
|
|
|
|
(er-eval-test "sup child responds"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup3). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.")
|
|
(nm (ev "Sup = supervisor:start_link(sup3, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! {ping, self()}, receive pong -> ok end")))
|
|
"ok")
|
|
|
|
(er-eval-test "sup restarts on exit"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup4). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.")
|
|
(nm
|
|
(ev "Sup = supervisor:start_link(sup4, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, P2} | _] = supervisor:which_children(Sup), P1 =/= P2")))
|
|
"true")
|
|
|
|
(er-eval-test "sup restarted child works"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup5). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.")
|
|
(nm
|
|
(ev "Sup = supervisor:start_link(sup5, []), receive after 5 -> ok end, [{_, _, P1} | _] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, P2} | _] = supervisor:which_children(Sup), P2 ! {ping, self()}, receive pong -> ok end")))
|
|
"ok")
|
|
|
|
(er-eval-test "sup one-for-one isolates failures"
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup6).
|
|
init(_) -> {ok, [
|
|
{w1, fun () -> echoer:start() end},
|
|
{w2, fun () -> echoer:start() end}
|
|
]}.")
|
|
(nm
|
|
(ev "Sup = supervisor:start_link(sup6, []), receive after 5 -> ok end, [{_, _, P1}, {_, _, P2}] = supervisor:which_children(Sup), P1 ! die, receive after 5 -> ok end, [{_, _, _NewP1}, {_, _, P2Again}] = supervisor:which_children(Sup), P2 =:= P2Again")))
|
|
"true")
|
|
|
|
(er-eval-test "sup stop"
|
|
(nm
|
|
(do
|
|
(erlang-load-module
|
|
"-module(sup7). init(_) -> {ok, [{w1, fun () -> echoer:start() end}]}.")
|
|
(ev "Sup = supervisor:start_link(sup7, []), receive after 5 -> ok end, supervisor:stop(Sup)")))
|
|
"ok")
|
|
|
|
;; ── register / whereis / registered ─────────────────────────────
|
|
(er-eval-test "register returns true"
|
|
(nm (ev "register(me, self())")) "true")
|
|
|
|
(er-eval-test "whereis registered self"
|
|
(nm (ev "register(me, self()), Pid = whereis(me), if Pid =:= self() -> matched; true -> nope end"))
|
|
"matched")
|
|
|
|
(er-eval-test "whereis undefined"
|
|
(nm (ev "whereis(no_such)")) "undefined")
|
|
|
|
(er-eval-test "send via registered atom"
|
|
(nm (ev "register(srv, self()), srv ! hello, receive M -> M end"))
|
|
"hello")
|
|
|
|
(er-eval-test "send to spawned registered"
|
|
(nm
|
|
(ev "Me = self(), P = spawn(fun () -> receive {From, X} -> From ! {got, X} end end), register(child, P), child ! {Me, payload}, receive {got, V} -> V end"))
|
|
"payload")
|
|
|
|
(er-eval-test "unregister returns true"
|
|
(nm (ev "register(a, self()), unregister(a)")) "true")
|
|
|
|
(er-eval-test "unregister then whereis"
|
|
(nm (ev "register(a, self()), unregister(a), whereis(a)"))
|
|
"undefined")
|
|
|
|
(er-eval-test "registered/0 lists names"
|
|
(ev "register(a, self()), register(b, self()), register(c, self()), length(registered())")
|
|
3)
|
|
|
|
(er-eval-test "register dup raises"
|
|
(do
|
|
(ev "P = spawn(fun () -> register(d, self()), register(d, self()) end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(nm (if (er-atom? reason) reason (nth (get reason :elements) 0)))))
|
|
"badarg")
|
|
|
|
(er-eval-test "unregister missing raises"
|
|
(do
|
|
(ev "P = spawn(fun () -> unregister(no_such) end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(nm (if (er-atom? reason) reason (nth (get reason :elements) 0)))))
|
|
"badarg")
|
|
|
|
(er-eval-test "dead process auto-unregisters"
|
|
;; Register a child while it's alive (still in receive). Send `die` so
|
|
;; it exits. After scheduler drains, whereis should return undefined.
|
|
(nm
|
|
(ev "P = spawn(fun () -> receive die -> exit(killed) end end), register(was_alive, P), P ! die, receive after 5 -> ok end, whereis(was_alive)"))
|
|
"undefined")
|
|
|
|
(er-eval-test "send to unregistered name raises"
|
|
(do
|
|
(ev "P = spawn(fun () -> no_such ! oops end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(nm (if (er-atom? reason) reason (nth (get reason :elements) 0)))))
|
|
"badarg")
|
|
|
|
;; ── list comprehensions ───────────────────────────────────────
|
|
(er-eval-test "lc map double"
|
|
(ev "hd([X * 2 || X <- [1, 2, 3]])") 2)
|
|
(er-eval-test "lc map sum"
|
|
(ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X * 2 || X <- [1, 2, 3]])")
|
|
12)
|
|
(er-eval-test "lc length"
|
|
(ev "length([X || X <- [1, 2, 3, 4, 5]])") 5)
|
|
(er-eval-test "lc filter sum"
|
|
(ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 0])")
|
|
6)
|
|
(er-eval-test "lc filter only"
|
|
(ev "length([X || X <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], X > 5])")
|
|
5)
|
|
(er-eval-test "lc empty source"
|
|
(get (ev "[X || X <- []]") :tag) "nil")
|
|
(er-eval-test "lc all filtered"
|
|
(get (ev "[X || X <- [1, 2, 3], X > 100]") :tag) "nil")
|
|
(er-eval-test "lc cartesian length"
|
|
(ev "length([{X, Y} || X <- [1, 2, 3], Y <- [a, b]])")
|
|
6)
|
|
(er-eval-test "lc pattern match"
|
|
(ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [V || {ok, V} <- [{ok, 1}, {error, x}, {ok, 2}, {ok, 3}]])")
|
|
6)
|
|
(er-eval-test "lc nested generators"
|
|
(ev "length([{X, Y} || X <- [1, 2, 3], Y <- [10, 20, 30], X + Y > 12])")
|
|
7)
|
|
(er-eval-test "lc squares"
|
|
(ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X*X || X <- [1, 2, 3, 4, 5]])")
|
|
55)
|
|
;; First {ok, X} tuple: head of [{ok,a}, {ok,b}] is {ok, a}.
|
|
(er-eval-test "lc tuple capture"
|
|
(nm (nth (get (get (ev "[{ok, X} || X <- [a, b]]") :head) :elements) 0))
|
|
"ok")
|
|
|
|
;; ── binary literals / patterns ────────────────────────────────
|
|
(er-eval-test "binary tag"
|
|
(get (ev "<<>>") :tag) "binary")
|
|
(er-eval-test "is_binary empty" (nm (ev "is_binary(<<>>)")) "true")
|
|
(er-eval-test "is_binary 3 bytes"
|
|
(nm (ev "is_binary(<<1, 2, 3>>)")) "true")
|
|
(er-eval-test "is_binary list" (nm (ev "is_binary([1, 2])")) "false")
|
|
(er-eval-test "byte_size 0" (ev "byte_size(<<>>)") 0)
|
|
(er-eval-test "byte_size 3" (ev "byte_size(<<1, 2, 3>>)") 3)
|
|
(er-eval-test "byte_size 16-bit" (ev "byte_size(<<256:16>>)") 2)
|
|
(er-eval-test "byte_size 32-bit" (ev "byte_size(<<999999:32>>)") 4)
|
|
|
|
;; Match
|
|
(er-eval-test "match single byte"
|
|
(ev "<<X>> = <<7>>, X") 7)
|
|
(er-eval-test "match X:8"
|
|
(ev "<<X:8>> = <<200>>, X") 200)
|
|
(er-eval-test "match 16-bit decode"
|
|
(ev "<<X:16>> = <<1, 0>>, X") 256)
|
|
(er-eval-test "match 16-bit hi byte"
|
|
(ev "<<X:16>> = <<2, 1>>, X") 513)
|
|
(er-eval-test "match A:8 B:16"
|
|
(ev "<<A:8, B:16>> = <<1, 0, 2>>, A + B") 3)
|
|
(er-eval-test "match three 8-bit"
|
|
(ev "<<A, B, C>> = <<1, 2, 3>>, A + B + C") 6)
|
|
|
|
;; Tail binary
|
|
(er-eval-test "tail rest size"
|
|
(ev "<<_:8, Rest/binary>> = <<1, 2, 3, 4>>, byte_size(Rest)") 3)
|
|
(er-eval-test "tail rest content"
|
|
(ev "<<_:8, Rest/binary>> = <<1, 2, 3, 4>>, <<X:8, _/binary>> = Rest, X") 2)
|
|
|
|
;; Match failure
|
|
(er-eval-test "size mismatch fails"
|
|
(do
|
|
(ev "P = spawn(fun () -> <<X:8, Y:8>> = <<1>>, ok end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(cond
|
|
(er-tuple? reason) (nm (nth (get reason :elements) 0))
|
|
(er-atom? reason) (get reason :name)
|
|
:else nil)))
|
|
"badmatch")
|
|
|
|
;; Equality
|
|
(er-eval-test "binary =:= self"
|
|
(nm (ev "B = <<1, 2, 3>>, B =:= B")) "true")
|
|
(er-eval-test "binary =:= same"
|
|
(nm (ev "<<1, 2>> =:= <<1, 2>>")) "true")
|
|
(er-eval-test "binary =/= different"
|
|
(nm (ev "<<1, 2>> =:= <<1, 3>>")) "false")
|
|
|
|
;; Construction with computed value
|
|
(er-eval-test "build with var"
|
|
(ev "X = 42, byte_size(<<X>>)") 1)
|
|
(er-eval-test "build with size var"
|
|
(ev "X = 7, byte_size(<<X:16>>)") 2)
|
|
|
|
;; ── ETS-lite ──────────────────────────────────────────────────
|
|
(er-eval-test "ets:new returns name"
|
|
(nm (ev "ets:new(t1, [set])")) "t1")
|
|
(er-eval-test "ets:insert returns true"
|
|
(nm (ev "T = ets:new(t2, [set]), ets:insert(T, {foo, 1})")) "true")
|
|
(er-eval-test "ets:lookup hit"
|
|
(ev "T = ets:new(t3, [set]), ets:insert(T, {foo, 42}), [{foo, V}] = ets:lookup(T, foo), V")
|
|
42)
|
|
(er-eval-test "ets:lookup miss returns []"
|
|
(get (ev "T = ets:new(t4, [set]), ets:lookup(T, no_such)") :tag) "nil")
|
|
(er-eval-test "ets:insert replaces (set semantics)"
|
|
(ev "T = ets:new(t5, [set]), ets:insert(T, {x, 1}), ets:insert(T, {x, 2}), ets:insert(T, {x, 3}), [{x, V}] = ets:lookup(T, x), V")
|
|
3)
|
|
(er-eval-test "ets:info size grows"
|
|
(ev "T = ets:new(t6, [set]), ets:insert(T, {a, 1}), ets:insert(T, {b, 2}), ets:insert(T, {c, 3}), ets:info(T, size)")
|
|
3)
|
|
(er-eval-test "ets:info size after delete"
|
|
(ev "T = ets:new(t7, [set]), ets:insert(T, {a, 1}), ets:insert(T, {b, 2}), ets:delete(T, a), ets:info(T, size)")
|
|
1)
|
|
(er-eval-test "ets:tab2list length"
|
|
(ev "T = ets:new(t8, [set]), ets:insert(T, {a, 1}), ets:insert(T, {b, 2}), ets:insert(T, {c, 3}), length(ets:tab2list(T))")
|
|
3)
|
|
(er-eval-test "ets:delete table returns true"
|
|
(nm (ev "T = ets:new(t9, [set]), ets:delete(T)")) "true")
|
|
(er-eval-test "ets:lookup after table delete"
|
|
(do
|
|
(ev "P = spawn(fun () -> T = ets:new(t10, [set]), ets:delete(T), ets:lookup(T, x) end), receive after 0 -> ok end")
|
|
(let ((reason (er-proc-field (er-mk-pid 1) :exit-reason)))
|
|
(cond
|
|
(er-atom? reason) (get reason :name)
|
|
:else (nm reason))))
|
|
"badarg")
|
|
|
|
;; Sum a column via lookup chain.
|
|
(er-eval-test "ets aggregate"
|
|
(ev "T = ets:new(t11, [set]), ets:insert(T, {a, 10}), ets:insert(T, {b, 20}), ets:insert(T, {c, 30}), [{a, A}] = ets:lookup(T, a), [{b, B}] = ets:lookup(T, b), [{c, C}] = ets:lookup(T, c), A + B + C")
|
|
60)
|
|
|
|
;; Tuple key (non-atom).
|
|
(er-eval-test "ets tuple key"
|
|
(nm
|
|
(ev "T = ets:new(t12, [set]), ets:insert(T, {{x, 1}, hello}), [{{x, 1}, V}] = ets:lookup(T, {x, 1}), V"))
|
|
"hello")
|
|
|
|
;; Tables are independent.
|
|
(er-eval-test "ets two tables independent"
|
|
(ev "T1 = ets:new(t13, [set]), T2 = ets:new(t14, [set]), ets:insert(T1, {x, 1}), ets:insert(T2, {x, 99}), [{x, A}] = ets:lookup(T1, x), [{x, B}] = ets:lookup(T2, x), A + B")
|
|
100)
|
|
|
|
;; ── more BIFs ─────────────────────────────────────────────────
|
|
(er-eval-test "abs neg" (ev "abs(-7)") 7)
|
|
(er-eval-test "abs pos" (ev "abs(42)") 42)
|
|
(er-eval-test "abs zero" (ev "abs(0)") 0)
|
|
|
|
(er-eval-test "min" (ev "min(3, 5)") 3)
|
|
(er-eval-test "min equal" (ev "min(7, 7)") 7)
|
|
(er-eval-test "max" (ev "max(3, 5)") 5)
|
|
(er-eval-test "max neg" (ev "max(-10, -2)") -2)
|
|
|
|
(er-eval-test "tuple_to_list head"
|
|
(nm (ev "hd(tuple_to_list({a, b, c}))")) "a")
|
|
(er-eval-test "tuple_to_list len"
|
|
(ev "length(tuple_to_list({1, 2, 3, 4, 5}))") 5)
|
|
(er-eval-test "list_to_tuple roundtrip"
|
|
(ev "tuple_size(list_to_tuple([10, 20, 30]))") 3)
|
|
|
|
(er-eval-test "integer_to_list" (ev "integer_to_list(42)") "42")
|
|
(er-eval-test "integer_to_list neg" (ev "integer_to_list(-99)") "-99")
|
|
(er-eval-test "list_to_integer" (ev "list_to_integer(\"123\")") 123)
|
|
(er-eval-test "list_to_integer roundtrip"
|
|
(ev "list_to_integer(integer_to_list(7))") 7)
|
|
|
|
(er-eval-test "is_function fun"
|
|
(nm (ev "F = fun (X) -> X end, is_function(F)")) "true")
|
|
(er-eval-test "is_function not"
|
|
(nm (ev "is_function(42)")) "false")
|
|
(er-eval-test "is_function arity match"
|
|
(nm (ev "F = fun (X, Y) -> X + Y end, is_function(F, 2)")) "true")
|
|
(er-eval-test "is_function arity mismatch"
|
|
(nm (ev "F = fun (X) -> X end, is_function(F, 5)")) "false")
|
|
|
|
;; lists module
|
|
(er-eval-test "lists:seq 1..5"
|
|
(ev "length(lists:seq(1, 5))") 5)
|
|
(er-eval-test "lists:seq head"
|
|
(ev "hd(lists:seq(10, 20))") 10)
|
|
(er-eval-test "lists:seq sum"
|
|
(ev "lists:sum(lists:seq(1, 100))") 5050)
|
|
(er-eval-test "lists:seq with step"
|
|
(ev "length(lists:seq(0, 20, 2))") 11)
|
|
(er-eval-test "lists:seq empty"
|
|
(get (ev "lists:seq(5, 1)") :tag) "nil")
|
|
|
|
(er-eval-test "lists:sum empty" (ev "lists:sum([])") 0)
|
|
(er-eval-test "lists:sum 5"
|
|
(ev "lists:sum([1, 2, 3, 4, 5])") 15)
|
|
|
|
(er-eval-test "lists:nth 1" (ev "lists:nth(1, [10, 20, 30])") 10)
|
|
(er-eval-test "lists:nth mid"
|
|
(nm (ev "lists:nth(2, [a, b, c])")) "b")
|
|
(er-eval-test "lists:last"
|
|
(nm (ev "lists:last([a, b, c, d])")) "d")
|
|
(er-eval-test "lists:last single" (ev "lists:last([42])") 42)
|
|
|
|
(er-eval-test "lists:member yes"
|
|
(nm (ev "lists:member(3, [1, 2, 3, 4])")) "true")
|
|
(er-eval-test "lists:member no"
|
|
(nm (ev "lists:member(99, [1, 2, 3])")) "false")
|
|
|
|
(er-eval-test "lists:append"
|
|
(ev "length(lists:append([1, 2], [3, 4, 5]))") 5)
|
|
|
|
(er-eval-test "lists:filter"
|
|
(ev "length(lists:filter(fun (X) -> X > 2 end, [1, 2, 3, 4, 5]))") 3)
|
|
(er-eval-test "lists:filter sum"
|
|
(ev "lists:sum(lists:filter(fun (X) -> X rem 2 =:= 0 end, lists:seq(1, 20)))") 110)
|
|
|
|
(er-eval-test "lists:any false"
|
|
(nm (ev "lists:any(fun (X) -> X > 100 end, [1, 2, 3])")) "false")
|
|
(er-eval-test "lists:any true"
|
|
(nm (ev "lists:any(fun (X) -> X > 2 end, [1, 2, 3])")) "true")
|
|
(er-eval-test "lists:all true"
|
|
(nm (ev "lists:all(fun (X) -> X > 0 end, [1, 2, 3])")) "true")
|
|
(er-eval-test "lists:all false"
|
|
(nm (ev "lists:all(fun (X) -> X > 1 end, [1, 2, 3])")) "false")
|
|
|
|
(er-eval-test "lists:duplicate len"
|
|
(ev "length(lists:duplicate(5, foo))") 5)
|
|
(er-eval-test "lists:duplicate val"
|
|
(nm (ev "hd(lists:duplicate(3, marker))")) "marker")
|
|
|
|
(define
|
|
er-eval-test-summary
|
|
(str "eval " er-eval-test-pass "/" er-eval-test-count))
|