From 7f4fb9c3edb1ed82341ce9dbe4eef2f99a4332e7 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 18:08:48 +0000 Subject: [PATCH] erlang: guard BIFs + call dispatch (+20 tests) --- lib/erlang/tests/eval.sx | 36 ++++++++++++ lib/erlang/transpile.sx | 117 +++++++++++++++++++++++++++++++++++++++ plans/erlang-on-sx.md | 3 +- 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index fd349610..1f8e50c4 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -174,6 +174,42 @@ (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") + (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 be8d478a..a5eb4b92 100644 --- a/lib/erlang/transpile.sx +++ b/lib/erlang/transpile.sx @@ -96,6 +96,7 @@ (= ty "block") (er-eval-body (get node :exprs) env) (= ty "if") (er-eval-if node env) (= ty "case") (er-eval-case node env) + (= ty "call") (er-eval-call node env) (= ty "match") (er-eval-match node env) :else (error (str "Erlang eval: unsupported node type '" ty "'")))))) @@ -439,3 +440,119 @@ (er-truthy? (er-eval-expr (nth conj i) env)) (er-eval-guard-conj-iter conj (+ i 1) env) false)))) + +;; ── function calls ─────────────────────────────────────────────── +(define + er-eval-call + (fn + (node env) + (let + ((fun (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"))))) + +(define + er-eval-args + (fn + (args env) + (let + ((out (list))) + (for-each + (fn (i) (append! out (er-eval-expr (nth args i) env))) + (range 0 (len args))) + out))) + +(define + er-eval-local-call + (fn + (name args env) + (let + ((vs (er-eval-args args env))) + (er-apply-bif name vs)))) + +(define + er-eval-remote-call + (fn + (mod name args env) + (error + (str "Erlang: undefined function '" mod ":" name "/" (len args) "'")))) + +;; ── BIFs ───────────────────────────────────────────────────────── +(define + er-apply-bif + (fn + (name vs) + (cond + (= name "is_integer") (er-bif-is-integer vs) + (= name "is_atom") (er-bif-is-atom vs) + (= name "is_list") (er-bif-is-list vs) + (= name "is_tuple") (er-bif-is-tuple vs) + (= name "is_number") (er-bif-is-number vs) + (= name "is_float") (er-bif-is-float vs) + (= name "is_boolean") (er-bif-is-boolean vs) + :else (error + (str "Erlang: undefined function '" name "/" (len vs) "'"))))) + +(define + er-bif-arg1 + (fn + (vs name) + (if + (= (len vs) 1) + (nth vs 0) + (error (str "Erlang: " name ": wrong arity"))))) + +(define + er-bif-is-integer + (fn + (vs) + (let + ((v (er-bif-arg1 vs "is_integer"))) + (er-bool (and (= (type-of v) "number") (integer? v)))))) + +(define + er-bif-is-atom + (fn (vs) (er-bool (er-atom? (er-bif-arg1 vs "is_atom"))))) + +(define + er-bif-is-list + (fn + (vs) + (let + ((v (er-bif-arg1 vs "is_list"))) + (er-bool (or (er-nil? v) (er-cons? v)))))) + +(define + er-bif-is-tuple + (fn (vs) (er-bool (er-tuple? (er-bif-arg1 vs "is_tuple"))))) + +(define + er-bif-is-number + (fn + (vs) + (er-bool (= (type-of (er-bif-arg1 vs "is_number")) "number")))) + +(define + er-bif-is-float + (fn + (vs) + (let + ((v (er-bif-arg1 vs "is_float"))) + (er-bool (and (= (type-of v) "number") (not (integer? v))))))) + +(define + er-bif-is-boolean + (fn + (vs) + (let + ((v (er-bif-arg1 vs "is_boolean"))) + (er-bool + (or (er-is-atom-named? v "true") (er-is-atom-named? v "false")))))) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 1b4bc452..5b4d80e9 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -59,7 +59,7 @@ Core mapping: ### Phase 2 — sequential eval + pattern matching + BIFs - [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 -- [ ] Guards: `is_integer`, `is_atom`, `is_list`, `is_tuple`, comparisons, arithmetic +- [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` @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **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. - **parser green** — `lib/erlang/parser.sx` + `parser-core.sx` + `parser-expr.sx` + `parser-module.sx`. 52/52 in `tests/parse.sx`. Covers literals, tuples, lists (incl. `[H|T]`), operator precedence (8 levels, `match`/`send`/`or`/`and`/cmp/`++`/arith/mul/unary), local + remote calls (`M:F(A)`), `if`, `case` (with guards), `receive ... after ... end`, `begin..end` blocks, anonymous `fun`, `try..of..catch..after..end` with `Class:Pattern` catch clauses. Module-level: `-module(M).`, `-export([...]).`, multi-clause functions with guards. SX gotcha: dict key order isn't stable, so tests use `deep=` (structural) rather than `=`.