erlang: pattern matching + case (+21 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 17:36:44 +00:00
parent efbab24cb2
commit 4965be71ca
3 changed files with 170 additions and 15 deletions

View File

@@ -123,6 +123,57 @@
(ev "X = 5, if X > 0 -> 1; true -> 0 end") (ev "X = 5, if X > 0 -> 1; true -> 0 end")
1) 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)
(define (define
er-eval-test-summary er-eval-test-summary
(str "eval " er-eval-test-pass "/" er-eval-test-count)) (str "eval " er-eval-test-pass "/" er-eval-test-count))

View File

@@ -95,6 +95,7 @@
(= ty "unop") (er-eval-unop node env) (= ty "unop") (er-eval-unop node env)
(= ty "block") (er-eval-body (get node :exprs) env) (= ty "block") (er-eval-body (get node :exprs) env)
(= ty "if") (er-eval-if node env) (= ty "if") (er-eval-if node env)
(= ty "case") (er-eval-case node env)
(= ty "match") (er-eval-match node env) (= ty "match") (er-eval-match node env)
:else (error (str "Erlang eval: unsupported node type '" ty "'")))))) :else (error (str "Erlang eval: unsupported node type '" ty "'"))))))
@@ -130,7 +131,7 @@
(er-eval-expr (get node :head) env) (er-eval-expr (get node :head) env)
(er-eval-expr (get node :tail) env)))) (er-eval-expr (get node :tail) env))))
;; ── match (bare-var LHS only; full pattern matching comes next) ──── ;; ── match expression ─────────────────────────────────────────────
(define (define
er-eval-match er-eval-match
(fn (fn
@@ -138,20 +139,122 @@
(let (let
((lhs (get node :lhs)) ((lhs (get node :lhs))
(rhs-val (er-eval-expr (get node :rhs) env))) (rhs-val (er-eval-expr (get node :rhs) env)))
(if
(er-match! lhs rhs-val env)
rhs-val
(error "Erlang: badmatch")))))
;; ── pattern matching ─────────────────────────────────────────────
;; Unifies PAT against VAL, binding fresh vars into ENV.
;; Returns true on success, false otherwise. On failure ENV may hold
;; partial bindings — callers trying multiple clauses must snapshot
;; ENV and restore it between attempts.
(define
er-match!
(fn
(pat val env)
(let
((ty (get pat :type)))
(cond (cond
(= (get lhs :type) "var") (= ty "var") (er-match-var pat val env)
(let (= ty "integer")
((name (get lhs :name))) (and (= (type-of val) "number") (= (parse-number (get pat :value)) val))
(cond (= ty "float")
(= name "_") rhs-val (and (= (type-of val) "number") (= (parse-number (get pat :value)) val))
(dict-has? env name) (= ty "atom") (and (er-atom? val) (= (get val :name) (get pat :value)))
(if (= ty "string")
(er-equal? (get env name) rhs-val) (and (= (type-of val) "string") (= val (get pat :value)))
rhs-val (= ty "nil") (er-nil? val)
(error "Erlang: badmatch (rebind mismatch)")) (= ty "tuple") (er-match-tuple pat val env)
:else (do (er-env-bind! env name rhs-val) rhs-val))) (= ty "cons") (er-match-cons pat val env)
:else (error :else (error (str "Erlang match: unsupported pattern type '" ty "'"))))))
"Erlang: pattern matching not yet supported (next Phase 2 step)")))))
(define
er-match-var
(fn
(pat val env)
(let
((name (get pat :name)))
(cond
(= name "_") true
(dict-has? env name) (er-equal? (get env name) val)
:else (do (er-env-bind! env name val) true)))))
(define
er-match-tuple
(fn
(pat val env)
(and
(er-tuple? val)
(let
((ps (get pat :elements)) (vs (get val :elements)))
(if (not (= (len ps) (len vs))) false (er-match-all ps vs 0 env))))))
(define
er-match-all
(fn
(ps vs i env)
(if
(>= i (len ps))
true
(if
(er-match! (nth ps i) (nth vs i) env)
(er-match-all ps vs (+ i 1) env)
false))))
(define
er-match-cons
(fn
(pat val env)
(and
(er-cons? val)
(and
(er-match! (get pat :head) (get val :head) env)
(er-match! (get pat :tail) (get val :tail) env)))))
;; ── env snapshot / restore ────────────────────────────────────────
(define
er-env-copy
(fn
(env)
(let
((out {}))
(for-each (fn (k) (dict-set! out k (get env k))) (keys env))
out)))
(define
er-env-restore!
(fn
(env snap)
(for-each (fn (k) (dict-delete! env k)) (keys env))
(for-each (fn (k) (dict-set! env k (get snap k))) (keys snap))))
;; ── case ─────────────────────────────────────────────────────────
(define
er-eval-case
(fn
(node env)
(let
((subject (er-eval-expr (get node :expr) env)))
(er-eval-case-clauses (get node :clauses) 0 subject env))))
(define
er-eval-case-clauses
(fn
(clauses i subject env)
(if
(>= i (len clauses))
(error "Erlang: case_clause: no matching clause")
(let
((c (nth clauses i)) (snap (er-env-copy env)))
(if
(and
(er-match! (get c :pattern) subject env)
(er-eval-guards (get c :guards) env))
(er-eval-body (get c :body) env)
(do
(er-env-restore! env snap)
(er-eval-case-clauses clauses (+ i 1) subject env)))))))
;; ── operators ───────────────────────────────────────────────────── ;; ── operators ─────────────────────────────────────────────────────
(define (define

View File

@@ -58,7 +58,7 @@ Core mapping:
### Phase 2 — sequential eval + pattern matching + BIFs ### Phase 2 — sequential eval + pattern matching + BIFs
- [x] `erlang-eval-ast`: evaluate sequential expressions — **54/54 tests** - [x] `erlang-eval-ast`: evaluate sequential expressions — **54/54 tests**
- [ ] Pattern matching (atoms, numbers, vars, tuples, lists, `[H|T]`, underscore, bound-var re-match) - [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 - [ ] Guards: `is_integer`, `is_atom`, `is_list`, `is_tuple`, comparisons, arithmetic
- [ ] 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` - [ ] 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` - [ ] 30+ tests in `lib/erlang/tests/eval.sx`
@@ -99,6 +99,7 @@ Core mapping:
_Newest first._ _Newest first._
- **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. - **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 `=`. - **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 `=`.
- **tokenizer green** — `lib/erlang/tokenizer.sx` + `lib/erlang/tests/tokenize.sx`. Covers atoms (bare, quoted, `node@host`), variables, integers (incl. `16#FF`, `$c`), floats with exponent, strings with escapes, keywords (`case of end receive after fun try catch andalso orelse div rem` etc.), punct (`( ) { } [ ] , ; . : :: -> <- <= => << >> | ||`), ops (`+ - * / = == /= =:= =/= < > =< >= ++ -- ! ?`), `%` line comments. 62/62 green. - **tokenizer green** — `lib/erlang/tokenizer.sx` + `lib/erlang/tests/tokenize.sx`. Covers atoms (bare, quoted, `node@host`), variables, integers (incl. `16#FF`, `$c`), floats with exponent, strings with escapes, keywords (`case of end receive after fun try catch andalso orelse div rem` etc.), punct (`( ) { } [ ] , ; . : :: -> <- <= => << >> | ||`), ops (`+ - * / = == /= =:= =/= < > =< >= ++ -- ! ?`), `%` line comments. 62/62 green.