erlang: core BIFs + funs, Phase 2 complete (+35 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 18:43:25 +00:00
parent 7f4fb9c3ed
commit 1dc96c814e
3 changed files with 438 additions and 22 deletions

View File

@@ -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))

View File

@@ -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)))))

View File

@@ -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.