erlang: pattern matching + case (+21 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user