erlang: list comprehensions (+12 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:
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))))
|
||||
|
||||
@@ -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 `<<A:8, B:16>>`
|
||||
- [ ] 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.
|
||||
|
||||
Reference in New Issue
Block a user