;; 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~") (define er-eval-test-summary (str "eval " er-eval-test-pass "/" er-eval-test-count))