From 193b0c04be2744fc4b8b153d48d4597b54eab2f6 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 06:19:14 +0000 Subject: [PATCH] erlang: list comprehensions (+12 tests) --- lib/erlang/parser-expr.sx | 52 ++++++++++++++++++++++-- lib/erlang/scoreboard.json | 6 +-- lib/erlang/scoreboard.md | 4 +- lib/erlang/tests/eval.sx | 35 ++++++++++++++++ lib/erlang/transpile.sx | 82 ++++++++++++++++++++++++++++++++++++++ plans/erlang-on-sx.md | 3 +- 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/lib/erlang/parser-expr.sx b/lib/erlang/parser-expr.sx index afdf6094..9bfbca9d 100644 --- a/lib/erlang/parser-expr.sx +++ b/lib/erlang/parser-expr.sx @@ -281,12 +281,56 @@ (fn (st) (er-expect! st "punct" "[") - (if + (cond (er-is? st "punct" "]") (do (er-advance! st) {:type "nil"}) - (let - ((elems (list (er-parse-expr-prec st 0)))) - (er-parse-list-tail st elems))))) + :else (let + ((first (er-parse-expr-prec st 0))) + (cond + (er-is? st "punct" "||") (er-parse-list-comp st first) + :else (er-parse-list-tail st (list first))))))) + +(define + er-parse-list-comp + (fn + (st head) + (er-advance! st) + (let + ((quals (list (er-parse-lc-qualifier st)))) + (er-parse-list-comp-tail st head quals)))) + +(define + er-parse-list-comp-tail + (fn + (st head quals) + (cond + (er-is? st "punct" ",") + (do + (er-advance! st) + (append! quals (er-parse-lc-qualifier st)) + (er-parse-list-comp-tail st head quals)) + (er-is? st "punct" "]") + (do (er-advance! st) {:head head :qualifiers quals :type "lc"}) + :else (error + (str + "Erlang parse: expected ',' or ']' in list comprehension, got '" + (er-cur-value st) + "'"))))) + +(define + er-parse-lc-qualifier + (fn + (st) + (let + ((e (er-parse-expr-prec st 0))) + (cond + (er-is? st "punct" "<-") + (do + (er-advance! st) + (let + ((source (er-parse-expr-prec st 0))) + {:kind "gen" :pattern e :source source})) + :else {:kind "filter" :expr e})))) (define er-parse-list-tail diff --git a/lib/erlang/scoreboard.json b/lib/erlang/scoreboard.json index 7496762a..f6264ec7 100644 --- a/lib/erlang/scoreboard.json +++ b/lib/erlang/scoreboard.json @@ -1,11 +1,11 @@ { "language": "erlang", - "total_pass": 444, - "total": 444, + "total_pass": 456, + "total": 456, "suites": [ {"name":"tokenize","pass":62,"total":62,"status":"ok"}, {"name":"parse","pass":52,"total":52,"status":"ok"}, - {"name":"eval","pass":260,"total":260,"status":"ok"}, + {"name":"eval","pass":272,"total":272,"status":"ok"}, {"name":"runtime","pass":39,"total":39,"status":"ok"}, {"name":"ring","pass":4,"total":4,"status":"ok"}, {"name":"ping-pong","pass":4,"total":4,"status":"ok"}, diff --git a/lib/erlang/scoreboard.md b/lib/erlang/scoreboard.md index ddb76d40..613c5d7f 100644 --- a/lib/erlang/scoreboard.md +++ b/lib/erlang/scoreboard.md @@ -1,12 +1,12 @@ # Erlang-on-SX Scoreboard -**Total: 444 / 444 tests passing** +**Total: 456 / 456 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | tokenize | 62 | 62 | | ✅ | parse | 52 | 52 | -| ✅ | eval | 260 | 260 | +| ✅ | eval | 272 | 272 | | ✅ | runtime | 39 | 39 | | ✅ | ring | 4 | 4 | | ✅ | ping-pong | 4 | 4 | diff --git a/lib/erlang/tests/eval.sx b/lib/erlang/tests/eval.sx index 39a3f440..a832e652 100644 --- a/lib/erlang/tests/eval.sx +++ b/lib/erlang/tests/eval.sx @@ -902,6 +902,41 @@ (nm (if (er-atom? reason) reason (nth (get reason :elements) 0))))) "badarg") +;; ── list comprehensions ─────────────────────────────────────── +(er-eval-test "lc map double" + (ev "hd([X * 2 || X <- [1, 2, 3]])") 2) +(er-eval-test "lc map sum" + (ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X * 2 || X <- [1, 2, 3]])") + 12) +(er-eval-test "lc length" + (ev "length([X || X <- [1, 2, 3, 4, 5]])") 5) +(er-eval-test "lc filter sum" + (ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 0])") + 6) +(er-eval-test "lc filter only" + (ev "length([X || X <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], X > 5])") + 5) +(er-eval-test "lc empty source" + (get (ev "[X || X <- []]") :tag) "nil") +(er-eval-test "lc all filtered" + (get (ev "[X || X <- [1, 2, 3], X > 100]") :tag) "nil") +(er-eval-test "lc cartesian length" + (ev "length([{X, Y} || X <- [1, 2, 3], Y <- [a, b]])") + 6) +(er-eval-test "lc pattern match" + (ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [V || {ok, V} <- [{ok, 1}, {error, x}, {ok, 2}, {ok, 3}]])") + 6) +(er-eval-test "lc nested generators" + (ev "length([{X, Y} || X <- [1, 2, 3], Y <- [10, 20, 30], X + Y > 12])") + 7) +(er-eval-test "lc squares" + (ev "lists:foldl(fun (X, Acc) -> X + Acc end, 0, [X*X || X <- [1, 2, 3, 4, 5]])") + 55) +;; First {ok, X} tuple: head of [{ok,a}, {ok,b}] is {ok, a}. +(er-eval-test "lc tuple capture" + (nm (nth (get (get (ev "[{ok, X} || X <- [a, b]]") :head) :elements) 0)) + "ok") + (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 5ec4ec2b..a167db0f 100644 --- a/lib/erlang/transpile.sx +++ b/lib/erlang/transpile.sx @@ -123,6 +123,7 @@ (= ty "send") (er-eval-send node env) (= ty "receive") (er-eval-receive node env) (= ty "try") (er-eval-try node env) + (= ty "lc") (er-eval-lc node env) (= ty "match") (er-eval-match node env) :else (error (str "Erlang eval: unsupported node type '" ty "'")))))) @@ -1281,3 +1282,84 @@ (do (er-env-restore! env snap) (er-eval-of-clauses clauses subject env (+ i 1)))))))) + +;; ── list comprehensions ───────────────────────────────────────── +;; `[E || Pat <- Source, FilterExpr, ...]`. Walk qualifiers in order: +;; generators iterate their source list and bind the pattern (with +;; env snapshot/restore so each iteration starts from the same +;; baseline); filters skip when falsy. At the end of the qualifier +;; chain, evaluate `head` and append to the accumulator. Build the +;; final cons chain in O(n) with a single right-fold. +(define + er-eval-lc + (fn + (node env) + (let + ((acc (list))) + (er-lc-walk (get node :qualifiers) 0 (get node :head) env acc) + (er-list-from-sx-list acc)))) + +(define + er-lc-walk + (fn + (quals i head env acc) + (if + (>= i (len quals)) + (append! acc (er-eval-expr head env)) + (let + ((q (nth quals i))) + (cond + (= (get q :kind) "gen") + (let + ((src (er-eval-expr (get q :source) env))) + (er-lc-iter-gen + src + (get q :pattern) + quals + i + head + env + acc)) + (= (get q :kind) "filter") + (when + (er-truthy? (er-eval-expr (get q :expr) env)) + (er-lc-walk quals (+ i 1) head env acc)) + :else (error "Erlang LC: unknown qualifier")))))) + +(define + er-lc-iter-gen + (fn + (src pat quals i head env acc) + (cond + (er-nil? src) nil + (er-cons? src) + (let + ((snap (er-env-copy env))) + (when + (er-match! pat (get src :head) env) + (er-lc-walk quals (+ i 1) head env acc)) + (er-env-restore! env snap) + (er-lc-iter-gen + (get src :tail) + pat + quals + i + head + env + acc)) + :else (error "Erlang LC: generator source is not a list")))) + +(define + er-list-from-sx-list + (fn + (xs) + (let + ((acc (list (er-mk-nil)))) + (for-each + (fn + (i) + (let + ((j (- (- (len xs) 1) i))) + (set-nth! acc 0 (er-mk-cons (nth xs j) (nth acc 0))))) + (range 0 (len xs))) + (nth acc 0)))) diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 90fb76e8..a70ddb2a 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -90,7 +90,7 @@ Core mapping: - [x] Registered processes: `register/2`, `whereis/1` — **12 new eval tests**; `unregister/1`, `registered/0`, `Name ! Msg` via registered atom; auto-unregister on death ### Phase 6 — the rest -- [ ] List comprehensions `[X*2 || X <- L]` +- [x] List comprehensions `[X*2 || X <- L]` — **12 new eval tests**; generators, filters, multiple generators (cartesian), pattern-matching gens (`{ok, V} <- ...`) - [ ] Binary pattern matching `<>` - [ ] ETS-lite (in-memory tables via SX dicts) - [ ] More BIFs — target 200+ test corpus green @@ -99,6 +99,7 @@ Core mapping: _Newest first._ +- **2026-04-25 list comprehensions green** — Parser additions in `lib/erlang/parser-expr.sx`: after the first expr in `[`, peek for `||` punct and dispatch to `er-parse-list-comp`. Qualifiers separated by `,`, each one is `Pattern <- Source` (generator) or any expression (filter — disambiguated by absence of `<-`). AST: `{:type "lc" :head E :qualifiers [...]}` with each qualifier `{:kind "gen"/"filter" ...}`. Evaluator (`er-eval-lc` in transpile.sx): right-fold builds the result by walking qualifiers; generators iterate the source list with env snapshot/restore per element so pattern-bound vars don't leak between iterations; filters skip when falsy. Pattern-matching generators are silently skipped on no-match (e.g. `[V || {ok, V} <- ...]`). 12 new eval tests: map double, fold-sum-of-comprehension, length, filter sum, "all filtered", empty source, cartesian, pattern-match gen, nested generators with filter, squares, tuple capture. Total suite 456/456. - **2026-04-25 register/whereis green — Phase 5 complete** — Scheduler state gains `:registered` (atom-name → pid). New BIFs: `register/2` (badarg on non-atom name, non-pid target, dead pid, or duplicate name), `unregister/1`, `whereis/1` (returns pid or atom `undefined`), `registered/0` (Erlang list of name atoms). `er-eval-send` for `Name ! Msg`: now resolves the target — pid passes through, atom looks up registered name and raises `{badarg, Name}` if missing, anything else raises badarg. Process death (in `er-sched-step!`) calls `er-unregister-pid!` to drop any registered name before `er-propagate-exit!` so monitor `{'DOWN'}` messages see the cleared registry. 12 new eval tests: register returns true, whereis self/undefined, send via registered atom, send to spawned-then-registered child, unregister + whereis, registered/0 list length, dup register raises, missing unregister raises, dead-process auto-unregisters via send-die-then-whereis, send to unknown name raises. Total suite 444/444. **Phase 5 complete — Phase 6 (list comprehensions, binary patterns, ETS) is the last phase.** - **2026-04-25 supervisor (one-for-one) green** — `er-supervisor-source` in `lib/erlang/runtime.sx` is the canonical Erlang text of a minimal supervisor; `er-load-supervisor!` registers it. Implements `start_link(Mod, Args)` (sup process traps exits, calls `Mod:init/1` to get child-spec list, runs `start_child/1` for each which links the spawned pid back to itself), `which_children/1`, `stop/1`. Receive loop dispatches on `{'EXIT', Dead, _Reason}` (restarts only the dead child via `restart/2`, keeps siblings — proper one-for-one), `{'$sup_which', From}` (returns child list), `'$sup_stop'`. Child specs are `{Id, StartFn}` where `StartFn/0` returns the new child's pid. 7 new eval tests: `which_children` for 1- and 3-child sup, child responds to ping, killed child restarted with fresh pid, restarted child still functional, one-for-one isolation (siblings keep their pids), stop returns ok. Total suite 432/432. - **2026-04-25 gen_server (OTP-lite) green** — `er-gen-server-source` in `lib/erlang/runtime.sx` is the canonical Erlang text of the behaviour; `er-load-gen-server!` registers it in the user-module table. Implements `start_link/2`, `call/2` (sync via `make_ref` + selective `receive {Ref, Reply}`), `cast/2` (async fire-and-forget returning `ok`), `stop/1`, and the receive loop dispatching `{'$gen_call', {From, Ref}, Req}` → `Mod:handle_call/3`, `{'$gen_cast', Msg}` → `Mod:handle_cast/2`, anything else → `Mod:handle_info/2`. handle_call reply tuples supported: `{reply, R, S}`, `{noreply, S}`, `{stop, R, Reply, S}`. handle_cast/info: `{noreply, S}`, `{stop, R, S}`. `Mod:F` and `M:F` where `M` is a runtime variable now work via new `er-resolve-call-name` (was bug: passed unevaluated AST node `:value` to remote dispatch). 10 new eval tests: counter callback module (start/call/cast/stop, repeated state mutations), LIFO stack callback module (`{push, V}` cast, pop returns `{ok, V}` or `empty`, size). Total suite 425/425.