diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index 1f8e50c4..846abcfc 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -210,6 +210,81 @@ (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)) diff --git a/lib/erlang/transpile.sx b/lib/erlang/transpile.sx index a5eb4b92..7d879f4f 100644 --- a/lib/erlang/transpile.sx +++ b/lib/erlang/transpile.sx @@ -97,6 +97,7 @@ (= ty "if") (er-eval-if node env) (= ty "case") (er-eval-case node env) (= ty "call") (er-eval-call node env) + (= ty "fun") (er-eval-fun node env) (= ty "match") (er-eval-match node env) :else (error (str "Erlang eval: unsupported node type '" ty "'")))))) @@ -447,17 +448,22 @@ (fn (node env) (let - ((fun (get node :fun)) (args (get node :args))) + ((fun-node (get node :fun)) (args (get node :args))) (cond - (= (get fun :type) "atom") - (er-eval-local-call (get fun :value) args env) - (= (get fun :type) "remote") - (er-eval-remote-call - (get (get fun :mod) :value) - (get (get fun :fun) :value) - args - env) - :else (error "Erlang: unsupported call target"))))) + (= (get fun-node :type) "atom") + (er-apply-bif (get fun-node :value) (er-eval-args args env)) + (= (get fun-node :type) "remote") + (er-apply-remote-bif + (get (get fun-node :mod) :value) + (get (get fun-node :fun) :value) + (er-eval-args args env)) + :else + (let + ((fv (er-eval-expr fun-node env))) + (if + (er-fun? fv) + (er-apply-fun fv (er-eval-args args env)) + (error "Erlang: not a function"))))))) (define er-eval-args @@ -470,22 +476,46 @@ (range 0 (len args))) out))) +;; ── fun values ─────────────────────────────────────────────────── (define - er-eval-local-call - (fn - (name args env) - (let - ((vs (er-eval-args args env))) - (er-apply-bif name vs)))) + er-mk-fun + (fn (clauses env) {:env env :clauses clauses :tag "fun"})) +(define er-fun? (fn (v) (er-is-tagged? v "fun"))) (define - er-eval-remote-call + er-eval-fun + (fn (node env) (er-mk-fun (get node :clauses) env))) + +(define + er-apply-fun (fn - (mod name args env) - (error - (str "Erlang: undefined function '" mod ":" name "/" (len args) "'")))) + (fv vs) + (er-apply-fun-clauses (get fv :clauses) vs (get fv :env) 0))) + +(define + er-apply-fun-clauses + (fn + (clauses vs closure-env i) + (if + (>= i (len clauses)) + (error "Erlang: function_clause: no matching fun clause") + (let + ((c (nth clauses i)) + (ps (get c :patterns)) + (call-env (er-env-copy closure-env))) + (if + (not (= (len ps) (len vs))) + (er-apply-fun-clauses clauses vs closure-env (+ i 1)) + (if + (and + (er-match-all ps vs 0 call-env) + (er-eval-guards (get c :guards) call-env)) + (er-eval-body (get c :body) call-env) + (er-apply-fun-clauses clauses vs closure-env (+ i 1)))))))) ;; ── BIFs ───────────────────────────────────────────────────────── +(define er-atom-ok (er-mk-atom "ok")) + (define er-apply-bif (fn @@ -498,9 +528,47 @@ (= name "is_number") (er-bif-is-number vs) (= name "is_float") (er-bif-is-float vs) (= name "is_boolean") (er-bif-is-boolean vs) + (= name "length") (er-bif-length vs) + (= name "hd") (er-bif-hd vs) + (= name "tl") (er-bif-tl vs) + (= name "element") (er-bif-element vs) + (= name "tuple_size") (er-bif-tuple-size vs) + (= name "atom_to_list") (er-bif-atom-to-list vs) + (= name "list_to_atom") (er-bif-list-to-atom vs) :else (error (str "Erlang: undefined function '" name "/" (len vs) "'"))))) +(define + er-apply-remote-bif + (fn + (mod name vs) + (cond + (= mod "lists") (er-apply-lists-bif name vs) + (= mod "io") (er-apply-io-bif name vs) + (= mod "erlang") (er-apply-bif name vs) + :else (error + (str "Erlang: undefined module '" mod "'"))))) + +(define + er-apply-lists-bif + (fn + (name vs) + (cond + (= name "reverse") (er-bif-lists-reverse vs) + (= name "map") (er-bif-lists-map vs) + (= name "foldl") (er-bif-lists-foldl vs) + :else (error + (str "Erlang: undefined 'lists:" name "/" (len vs) "'"))))) + +(define + er-apply-io-bif + (fn + (name vs) + (cond + (= name "format") (er-bif-io-format vs) + :else (error + (str "Erlang: undefined 'io:" name "/" (len vs) "'"))))) + (define er-bif-arg1 (fn @@ -556,3 +624,275 @@ ((v (er-bif-arg1 vs "is_boolean"))) (er-bool (or (er-is-atom-named? v "true") (er-is-atom-named? v "false")))))) + +;; ── list / tuple BIFs ──────────────────────────────────────────── +(define er-bif-length (fn (vs) (er-list-length (er-bif-arg1 vs "length")))) + +(define + er-list-length + (fn + (v) + (cond + (er-nil? v) 0 + (er-cons? v) (+ 1 (er-list-length (get v :tail))) + :else (error "Erlang: length: not a proper list")))) + +(define + er-bif-hd + (fn + (vs) + (let + ((v (er-bif-arg1 vs "hd"))) + (if + (er-cons? v) + (get v :head) + (error "Erlang: hd: empty list or non-list"))))) + +(define + er-bif-tl + (fn + (vs) + (let + ((v (er-bif-arg1 vs "tl"))) + (if + (er-cons? v) + (get v :tail) + (error "Erlang: tl: empty list or non-list"))))) + +(define + er-bif-element + (fn + (vs) + (if + (not (= (len vs) 2)) + (error "Erlang: element: arity") + (let + ((i (nth vs 0)) (t (nth vs 1))) + (if + (and (= (type-of i) "number") (er-tuple? t)) + (let + ((elems (get t :elements))) + (if + (and (>= i 1) (<= i (len elems))) + (nth elems (- i 1)) + (error "Erlang: element: badarg (index out of range)"))) + (error "Erlang: element: badarg")))))) + +(define + er-bif-tuple-size + (fn + (vs) + (let + ((v (er-bif-arg1 vs "tuple_size"))) + (if + (er-tuple? v) + (len (get v :elements)) + (error "Erlang: tuple_size: not a tuple"))))) + +(define + er-bif-atom-to-list + (fn + (vs) + (let + ((v (er-bif-arg1 vs "atom_to_list"))) + (if + (er-atom? v) + (get v :name) + (error "Erlang: atom_to_list: not an atom"))))) + +(define + er-bif-list-to-atom + (fn + (vs) + (let + ((v (er-bif-arg1 vs "list_to_atom"))) + (if + (= (type-of v) "string") + (er-mk-atom v) + (error "Erlang: list_to_atom: not a string"))))) + +;; ── lists module ───────────────────────────────────────────────── +(define + er-bif-lists-reverse + (fn + (vs) + (er-list-reverse-iter (er-bif-arg1 vs "lists:reverse") (er-mk-nil)))) + +(define + er-list-reverse-iter + (fn + (v acc) + (cond + (er-nil? v) acc + (er-cons? v) + (er-list-reverse-iter (get v :tail) (er-mk-cons (get v :head) acc)) + :else (error "Erlang: lists:reverse: not a list")))) + +(define + er-bif-lists-map + (fn + (vs) + (if + (not (= (len vs) 2)) + (error "Erlang: lists:map: arity") + (er-list-reverse-iter + (er-map-iter (nth vs 0) (nth vs 1) (er-mk-nil)) + (er-mk-nil))))) + +(define + er-map-iter + (fn + (f lst acc) + (cond + (er-nil? lst) acc + (er-cons? lst) + (er-map-iter + f + (get lst :tail) + (er-mk-cons (er-apply-fun f (list (get lst :head))) acc)) + :else (error "Erlang: lists:map: not a list")))) + +(define + er-bif-lists-foldl + (fn + (vs) + (if + (not (= (len vs) 3)) + (error "Erlang: lists:foldl: arity") + (er-foldl-iter (nth vs 0) (nth vs 1) (nth vs 2))))) + +(define + er-foldl-iter + (fn + (f acc lst) + (cond + (er-nil? lst) acc + (er-cons? lst) + (er-foldl-iter + f + (er-apply-fun f (list (get lst :head) acc)) + (get lst :tail)) + :else (error "Erlang: lists:foldl: not a list")))) + +;; ── io module ──────────────────────────────────────────────────── +(define er-io-buffer (list "")) +(define er-io-flush! (fn () (set-nth! er-io-buffer 0 ""))) +(define er-io-buffer-content (fn () (nth er-io-buffer 0))) + +(define + er-bif-io-format + (fn + (vs) + (let + ((s + (cond + (= (len vs) 1) (er-format-string (nth vs 0) (list)) + (= (len vs) 2) + (er-format-string (nth vs 0) (er-list-to-sx-list (nth vs 1))) + :else (error "Erlang: io:format: arity")))) + (set-nth! er-io-buffer 0 (str (nth er-io-buffer 0) s)) + er-atom-ok))) + +(define + er-list-to-sx-list + (fn + (lst) + (let + ((out (list))) + (er-list-to-sx-collect lst out) + out))) + +(define + er-list-to-sx-collect + (fn + (lst out) + (cond + (er-nil? lst) nil + (er-cons? lst) + (do + (append! out (get lst :head)) + (er-list-to-sx-collect (get lst :tail) out)) + :else (error "Erlang: expected proper list")))) + +;; ── format string rendering (~n, ~~, ~p, ~w, ~s) ──────────────── +(define + er-format-string + (fn (fmt args) (er-format-walk fmt 0 args 0 ""))) + +(define + er-format-walk + (fn + (fmt i args ai out) + (if + (>= i (len fmt)) + out + (let + ((c (char-at fmt i))) + (cond + (and (= c "~") (< (+ i 1) (len fmt))) + (let + ((d (char-at fmt (+ i 1)))) + (cond + (= d "n") + (er-format-walk fmt (+ i 2) args ai (str out "\n")) + (= d "~") (er-format-walk fmt (+ i 2) args ai (str out "~")) + (or (= d "p") (= d "w") (= d "s")) + (er-format-walk + fmt + (+ i 2) + args + (+ ai 1) + (str out (er-format-value (nth args ai)))) + :else (er-format-walk + fmt + (+ i 2) + args + ai + (str out "~" d)))) + :else (er-format-walk fmt (+ i 1) args ai (str out c))))))) + +(define + er-format-value + (fn + (v) + (cond + (= (type-of v) "number") (str v) + (= (type-of v) "string") (str "\"" v "\"") + (er-atom? v) (get v :name) + (er-nil? v) "[]" + (er-cons? v) (str "[" (er-format-list-elems v) "]") + (er-tuple? v) (str "{" (er-format-tuple-elems (get v :elements)) "}") + (er-fun? v) "#Fun" + :else (str v)))) + +(define + er-format-list-elems + (fn + (v) + (cond + (er-nil? v) "" + (and (er-cons? v) (er-nil? (get v :tail))) + (er-format-value (get v :head)) + (er-cons? v) + (str + (er-format-value (get v :head)) + "," + (er-format-list-elems (get v :tail))) + :else (str "|" (er-format-value v))))) + +(define + er-format-tuple-elems + (fn + (elems) + (if + (= (len elems) 0) + "" + (let + ((out (list (er-format-value (nth elems 0))))) + (for-each + (fn + (i) + (append! out ",") + (append! out (er-format-value (nth elems i)))) + (range 1 (len elems))) + (reduce str "" out))))) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 5b4d80e9..b77076c6 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -60,8 +60,8 @@ Core mapping: - [x] `erlang-eval-ast`: evaluate sequential expressions — **54/54 tests** - [x] Pattern matching (atoms, numbers, vars, tuples, lists, `[H|T]`, underscore, bound-var re-match) — **21 new eval tests**; `case ... of ... end` wired - [x] Guards: `is_integer`, `is_atom`, `is_list`, `is_tuple`, comparisons, arithmetic — **20 new eval tests**; local-call dispatch wired -- [ ] BIFs: `length/1`, `hd/1`, `tl/1`, `element/2`, `tuple_size/1`, `atom_to_list/1`, `list_to_atom/1`, `lists:map/2`, `lists:foldl/3`, `lists:reverse/1`, `io:format/1-2` -- [ ] 30+ tests in `lib/erlang/tests/eval.sx` +- [x] BIFs: `length/1`, `hd/1`, `tl/1`, `element/2`, `tuple_size/1`, `atom_to_list/1`, `list_to_atom/1`, `lists:map/2`, `lists:foldl/3`, `lists:reverse/1`, `io:format/1-2` — **35 new eval tests**; funs + closures wired +- [x] 30+ tests in `lib/erlang/tests/eval.sx` — **130 tests green** ### Phase 3 — processes + mailboxes + receive (THE SHOWCASE) - [ ] Scheduler in `runtime.sx`: runnable queue, pid counter, per-process state record @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **2026-04-24 core BIFs + funs green** — Phase 2 complete. Added to `lib/erlang/transpile.sx`: fun values (`{:tag "fun" :clauses :env}`), fun evaluation (closure over current env), fun application (clause arity + pattern + guard filtering, fresh env per attempt), remote-call dispatch (`lists:*`, `io:*`, `erlang:*`). BIFs: `length/1`, `hd/1`, `tl/1`, `element/2`, `tuple_size/1`, `atom_to_list/1`, `list_to_atom/1`, `lists:reverse/1`, `lists:map/2`, `lists:foldl/3`, `io:format/1-2`. `io:format` writes to a capture buffer (`er-io-buffer`, `er-io-flush!`, `er-io-buffer-content`) and returns `ok` — supports `~n`, `~p`/`~w`/`~s`, `~~`. 35 new eval tests. Total eval 130/130; erlang suite 244/244. **Phase 2 complete — Phase 3 (processes, scheduler, receive) is next.** - **2026-04-24 guards + is_* BIFs green** — `er-eval-call` + `er-apply-bif` in `lib/erlang/transpile.sx` wire local function calls to a BIF dispatcher. Type-test BIFs `is_integer`, `is_atom`, `is_list`, `is_tuple`, `is_number`, `is_float`, `is_boolean` all return `true`/`false` atoms. Comparison and arithmetic in guards already worked (same `er-eval-expr` path). 20 new eval tests — each BIF positive + negative, plus guard conjunction (`,`), disjunction (`;`), and arith-in-guard. Total eval 95/95; erlang suite 209/209. - **2026-04-24 pattern matching green** — `er-match!` in `lib/erlang/transpile.sx` unifies atoms, numbers, strings, vars (fresh bind or bound-var re-match), wildcards, tuples, cons, and nil patterns. `case ... of ... [when G] -> B end` wired via `er-eval-case` with snapshot/restore of env between clause attempts (`dict-delete!`-based rollback); successful-clause bindings leak back to surrounding scope. 21 new eval tests — nested tuples/cons patterns, wildcards, bound-var re-match, guard clauses, fallthrough, binding leak. Total eval 75/75; erlang suite 189/189. - **2026-04-24 eval (sequential) green** — `lib/erlang/transpile.sx` (tree-walking interpreter) + `lib/erlang/tests/eval.sx`. 54/54 tests covering literals, arithmetic, comparison, logical (incl. short-circuit `andalso`/`orelse`), tuples, lists with `++`, `begin..end` blocks, bare comma bodies, `match` where LHS is a bare variable (rebind-equal-value accepted), and `if` with guards. Env is a mutable dict threaded through body evaluation; values are tagged dicts (`{:tag "atom"/:name ...}`, `{:tag "nil"}`, `{:tag "cons" :head :tail}`, `{:tag "tuple" :elements}`). Numbers pass through as SX numbers. Gotcha: SX's `parse-number` coerces `"1.0"` → integer `1`, so `=:=` can't distinguish `1` from `1.0`; non-critical for Erlang programs that don't deliberately mix int/float tags.