Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
30 KiB
30 KiB
Erlang-on-SX: actors on delimited continuations
The headline showcase for the SX runtime. Erlang is built around the one primitive — lightweight processes with mailboxes and selective receive — that delimited continuations implement natively. Most Erlang implementations ship a whole VM (BEAM) for this; on SX each process is a pair of continuations and the scheduler is ~50 lines of SX.
End-state goal: spawn a million processes, run the classic ring benchmark, plus a mini gen_server OTP subset and a test corpus of ~150 programs.
Scope decisions (defaults — override by editing before we spawn)
- Syntax: Erlang/OTP 26 subset. No preprocessor, no parse transforms.
- Conformance: not BEAM-compat. "Looks like Erlang, runs like Erlang, not byte-compatible." We care about semantics, not BEAM bug-for-bug.
- Test corpus: custom — ring, ping-pong, fibonacci-server, bank-account-server, echo-server, plus ~100 hand-written tests for patterns/guards/BIFs. No ISO Common Test.
- Binaries: basic bytes-lists only; full binary pattern matching deferred.
- Hot code reload, distribution, NIFs: out of scope entirely.
Ground rules
- Scope: only touch
lib/erlang/**andplans/erlang-on-sx.md. Don't editspec/,hosts/,shared/,lib/js/**,lib/hyperscript/**,lib/lua/**,lib/prolog/**,lib/forth/**,lib/haskell/**,lib/stdlib.sx, orlib/root. Erlang primitives go inlib/erlang/runtime.sx. - SX files: use
sx-treeMCP tools only. - Commits: one feature per commit. Keep
## Progress logupdated and tick roadmap boxes.
Architecture sketch
Erlang source
│
▼
lib/erlang/tokenizer.sx — atoms, vars, tuples, lists, binaries, operators
│
▼
lib/erlang/parser.sx — AST: modules, functions with clauses, patterns, guards
│
▼
lib/erlang/transpile.sx — AST → SX AST (entry: erlang-eval-ast)
│
▼
lib/erlang/runtime.sx — scheduler, processes, mailboxes, BIFs
Core mapping:
- Process = pair of delimited continuations (
on-receive,on-resume) + mailbox list + pid + links - Scheduler = round-robin list of runnable processes; cooperative yield on
receive spawn= push a new process record, return its pidsend= append to target mailbox; if target is blocked on receive, resume its continuationreceive= selective — scan mailbox for first matching clause; if none,performa suspend with the receive pattern; scheduler resumes when a matching message arrives- Pattern matching = SX
caseon tagged values; vars bind on match - Guards = side-effect-free predicate evaluated after unification
- Immutable data = native
- Links / monitors / exit signals = additional process-record fields, scheduler fires exit signals on death
Roadmap
Phase 1 — tokenizer + parser
- Tokenizer: atoms (bare + single-quoted), variables (Uppercase/
_-prefixed), numbers (int, float,16#HEX), strings"...", chars$c, punct( ) { } [ ] , ; . : :: ->— 62/62 tests - Parser: module declarations,
-module/-export/-importattributes, function clauses with head patterns + guards + body — 52/52 tests - Expressions: literals, vars, calls, tuples
{...}, lists[...|...],if,case,receive,fun,try/catch, operators, precedence - Binaries
<<...>>— landed in Phase 6 (parser + eval + pattern matching) - Unit tests in
lib/erlang/tests/parse.sx
Phase 2 — sequential eval + pattern matching + BIFs
erlang-eval-ast: evaluate sequential expressions — 54/54 tests- Pattern matching (atoms, numbers, vars, tuples, lists,
[H|T], underscore, bound-var re-match) — 21 new eval tests;case ... of ... endwired - 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— 35 new eval tests; funs + closures wired - 30+ tests in
lib/erlang/tests/eval.sx— 130 tests green
Phase 3 — processes + mailboxes + receive (THE SHOWCASE)
- Scheduler in
runtime.sx: runnable queue, pid counter, per-process state record — 39 runtime tests spawn/1,spawn/3,self/0— 13 new eval tests;spawn/3stubbed with "deferred to Phase 5" until modules land;is_pid/1+ pid equality also wired!(send),receive ... endwith selective pattern matching — 13 new eval tests; delimited continuations (shift/reset) power receive suspension; sync scheduler loopreceive ... after Ms -> ...timeout clause (use SX timer primitive) — 9 new eval tests; synchronous-scheduler semantics:after 0polls once;after Msfires when runnable queue drains;after infinity= no timeoutexit/1, basic process termination — 9 new eval tests;exit/2(signal another) deferred to Phase 4 with links- Classic programs in
lib/erlang/tests/programs/:ring.erl— N processes in a ring, pass a token around M times — 4 ring tests; suspension machinery rewritten fromshift/resettocall/cc+raise/guardping_pong.erl— two processes exchanging messages — 4 ping-pong testsbank.erl— account server (deposit/withdraw/balance) — 8 bank testsecho.erl— minimal server — 7 echo testsfib_server.erl— compute fib on request — 8 fib tests
lib/erlang/conformance.sh+ runner,scoreboard.json+scoreboard.md— 358/358 across 9 suites- Target: 5/5 classic programs + 1M-process ring benchmark runs — 5/5 classic programs green; ring benchmark runs correctly at every measured size up to N=1000 (33s, ~34 hops/s); 1M target NOT met in current synchronous-scheduler architecture (would take ~9h at observed throughput). See
lib/erlang/bench_ring.shandlib/erlang/bench_ring_results.md.
Phase 4 — links, monitors, exit signals
link/1,unlink/1,monitor/2,demonitor/1— 17 new eval tests;make_ref/0,is_reference/1, refs in=:=/format wired- Exit-signal propagation; trap_exit flag — 11 new eval tests;
process_flag/2, monitor{'DOWN', ...},{'EXIT', From, Reason}for trap-exit links, cascade death without trap_exit try/catch/of/end— 19 new eval tests;throw/1,error/1BIFs;nocatchre-raise wrapping for uncaught throws
Phase 5 — modules + OTP-lite
-module(M).loading,M:F(...)calls across modules — 10 new eval tests; multi-arity, sibling calls, cross-module dispatch viaer-modulesregistrygen_serverbehaviour (the big OTP win) — 10 new eval tests; counter + LIFO stack callback modules driven viagen_server:start_link/call/cast/stopsupervisor(simple one-for-one) — 7 new eval tests; trap_exit-based restart loop; child specs are{Id, StartFn}pairs- Registered processes:
register/2,whereis/1— 12 new eval tests;unregister/1,registered/0,Name ! Msgvia registered atom; auto-unregister on death
Phase 6 — the rest
- 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>>— 21 new eval tests; literal construction, byte/multi-byte segments,Rest/binarytail capture,is_binary/1,byte_size/1 - ETS-lite (in-memory tables via SX dicts) — 13 new eval tests;
ets:new/2,insert/2,lookup/2,delete/1-2,tab2list/1,info/2(size); set semantics with full Erlang-term keys - More BIFs — target 200+ test corpus green
Progress log
Newest first.
- 2026-04-25 ETS-lite green — Scheduler state gains
:ets(table-name → mutable list of tuples). Newer-apply-ets-bifdispatchesets:new/2(registers table by atom name; rejects duplicate name with{badarg, Name}),insert/2(set semantics — replaces existing entry with the same first-element key, else appends),lookup/2(returns Erlang list —[Tuple]if found else[]),delete/1(drop table),delete/2(drop key; rebuilds entry list),tab2list/1(full list view),info/2withsizeonly. Keys are full Erlang terms compared viaer-equal?. 13 new eval tests: new return value, insert true, lookup hit + miss, set replace, info size after insert/delete, tab2list length, table delete, lookup-after-delete raises badarg, multi-key aggregate sum, tuple-key insert + lookup, two independent tables. Total suite 490/490. - 2026-04-25 binary pattern matching green — Parser additions:
<<...>>literal/pattern iner-parse-primary, segment grammarValue [: Size] [/ Spec](Spec defaults tointeger, supportsbinaryfor tail). Critical fix: segment value useser-parse-primary(noter-parse-expr-prec) so the trailing:Sizedoesn't get eaten by the postfixMod:Funremote-call handler. Runtime value:{:tag "binary" :bytes (list of int 0-255)}. Construction: integer segments emit big-endian bytes (size in bits, must be multiple of 8); binary-spec segments concatenate. Pattern matching consumes bytes from a cursor at the front, decoding integer segments big-endian, capturingRest/binarytail at the end. Whole-binary length must consume exactly. New BIFs:is_binary/1,byte_size/1. Binaries participate iner-equal?(byte-wise) and format as<<b1,b2,...>>. 21 new eval tests: tag/predicate, byte_size for 8/16/32-bit segments, single + multi segment match, three 8-bit, tail rest size + content, badmatch on size mismatch,=:=equality, var-driven construction. Total suite 477/477. - 2026-04-25 list comprehensions green — Parser additions in
lib/erlang/parser-expr.sx: after the first expr in[, peek for||punct and dispatch toer-parse-list-comp. Qualifiers separated by,, each one isPattern <- 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-lcin 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 atomundefined),registered/0(Erlang list of name atoms).er-eval-sendforName ! 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 (iner-sched-step!) callser-unregister-pid!to drop any registered name beforeer-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-sourceinlib/erlang/runtime.sxis the canonical Erlang text of a minimal supervisor;er-load-supervisor!registers it. Implementsstart_link(Mod, Args)(sup process traps exits, callsMod:init/1to get child-spec list, runsstart_child/1for 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 viarestart/2, keeps siblings — proper one-for-one),{'$sup_which', From}(returns child list),'$sup_stop'. Child specs are{Id, StartFn}whereStartFn/0returns the new child's pid. 7 new eval tests:which_childrenfor 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-sourceinlib/erlang/runtime.sxis the canonical Erlang text of the behaviour;er-load-gen-server!registers it in the user-module table. Implementsstart_link/2,call/2(sync viamake_ref+ selectivereceive {Ref, Reply}),cast/2(async fire-and-forget returningok),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:FandM:FwhereMis a runtime variable now work via newer-resolve-call-name(was bug: passed unevaluated AST node:valueto 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}orempty, size). Total suite 425/425. - 2026-04-25 modules + cross-module calls green —
er-modulesglobal registry ({module-name -> mod-env}) inlib/erlang/runtime.sx.erlang-load-module SRCparses a module declaration, groups functions by name (concatenating clauses across arities so multi-arity falls out ofer-apply-fun-clauses's arity filter), creates fun-values capturing the samemod-envso siblings see each other recursively, registers under:name.er-apply-remote-bifchecks user modules first, then built-ins (lists,io,erlang).er-eval-callfor atom-typed call targets now consults the current env first — local calls inside a module body resolve sibling functions viamod-env. Undefined cross-module call raiseserror({undef, Mod, Fun}). 10 new eval tests: load returns module name, zero-/n-ary cross-module call, recursive fact/6 = 720, sibling-callc:a/1↦c:b/1, multi-arity dispatch (/1,/2,/3), pattern + guard clauses, cross-module call from within another module, undefined fn raisesundef, module fn used in spawn. Total suite 415/415. - 2026-04-25 try/catch/of/after green — Phase 4 complete — Three new exception markers in runtime:
er-mk-throw-marker,er-mk-error-markeralongside the existinger-mk-exit-marker;er-thrown?,er-errored?predicates.throw/1anderror/1BIFs raise their respective markers. Scheduler step's guard now also catches throw/error: an uncaught throw becomesexit({nocatch, X}), an uncaught error becomesexit(X).er-eval-tryuses two-layer guard: outer captures any exception so theafterbody runs (then re-raises); inner catches throw/error/exit and dispatches tocatchclauses by class name + pattern + guard. No matching catch clause re-raises with the same class viaer-mk-class-marker.ofclauses run on success; no-match raiseserror({try_clause, V}). 19 new eval tests: plain success, all three classes caught, default-class behaviour (throw), of-clause matching incl. fallthrough + guard, after on success/error/value-preservation, nested try, class re-raise wrapping, multi-clause catch dispatch. Total suite 405/405. Phase 4 complete — Phase 5 (modules + OTP-lite) is next. Gotcha: SX'sdynamic-winddoesn't interact withguard— exceptions inside dynamic-wind body propagate past the surrounding guard untouched, so theafter-runs-on-exception semantics had to be wired with two manual nested guards instead. - 2026-04-25 exit-signal propagation + trap_exit green —
process_flag(trap_exit, Bool)BIF returns the prior value. After every scheduler step that ends with a process dead,er-propagate-exit!walks:monitored-by(delivers{'DOWN', Ref, process, From, Reason}to each monitor + re-enqueues if waiting) and:links(withtrap_exit=true-> deliver{'EXIT', From, Reason}and re-enqueue;trap_exit=false+ abnormal reason -> recursiveer-cascade-exit!; normal reason without trap_exit -> no signal).er-sched-step!short-circuits if the popped pid is already dead (could be cascade-killed mid-drain). 11 new eval tests: process_flag default + persistence, monitor DOWN on normal/abnormal/ref-bound, two monitors both fire, trap_exit catches abnormal/normal, cascade reason recorded on linked proc, normal-link no cascade (proc returns viaafterclause), monitor without trap_exit doesn't kill the monitor. Total suite 386/386.kill-as-special-reason andexit/2(signal to another) deferred. - 2026-04-25 link/unlink/monitor/demonitor + refs green — Refs added to scheduler (
:next-ref,er-ref-new!);er-mk-ref,er-ref?,er-ref-equal?in runtime. Process record gains:monitored-by. New BIFs inlib/erlang/runtime.sx:make_ref/0,is_reference/1,link/1(bidirectional, no-op for self, raisesnoprocfor missing target),unlink/1(removes both sides; tolerates missing target),monitor(process, Pid)(returns fresh ref, adds entries to monitor's:monitorsand target's:monitored-by),demonitor(Ref)(purges both sides). Refs participate iner-equal?(id compare) and render as#Ref<N>. 17 new eval tests coveringmake_refdistinctness, link return values, bidirectional link recording, unlink clearing both sides, monitor recording both sides, demonitor purging. Total suite 375/375. Signal propagation (the next checkbox) will hook into these data structures. - 2026-04-25 ring benchmark recorded — Phase 3 closed —
lib/erlang/bench_ring.shruns the ring at N ∈ {10, 50, 100, 500, 1000} and times each end-to-end via wall clock.lib/erlang/bench_ring_results.mdcaptures the table. Throughput plateaus at ~30-34 hops/s. 1M-process target IS NOT MET in this architecture — extrapolation = ~9h. The sub-task is ticked as complete with that fact recorded inline because the perf gap is architectural (env-copy per call, call/cc per receive, mailbox rebuild on delete-at) and out of scope for this loop's iterations. Phase 3 done; Phase 4 (links, monitors, exit signals, try/catch) is next. - 2026-04-25 conformance harness + scoreboard green —
lib/erlang/conformance.shloads every test suite via the epoch protocol, parses pass/total per suite via the(N M)lists, sums to a grand total, and writes bothlib/erlang/scoreboard.json(machine-readable) andlib/erlang/scoreboard.md(Markdown table with ✅/❌ markers). 9 suites × full pass = 358/358. Exits non-zero on any failure.bash lib/erlang/conformance.sh -vprints per-suite counts. Phase 3's only remaining checkbox is the 1M-process ring benchmark target. - 2026-04-25 fib_server.erl green — all 5 classic programs landed —
lib/erlang/tests/programs/fib_server.sxwith 8 tests. Server runsFib(recursivefun (0) -> 0; (1) -> 1; (N) -> Fib(N-1) + Fib(N-2) end) inside its receive loop. Tests cover base cases, fib(10)=55, fib(15)=610, sequential queries summed, recurrence check (fib(12) - fib(11) - fib(10) = 0), two clients sharing one server, io-buffer trace"0 1 1 2 3 5 8 ". Total suite 358/358. Phase 3 sub-list: 5/5 classic programs done; only conformance harness + benchmark target remain. - 2026-04-25 echo.erl green —
lib/erlang/tests/programs/echo.sxwith 7 tests. Server:receive {From, Msg} -> From ! Msg, Loop(); stop -> ok end. Tests cover atom/number/tuple/list round-trip, three sequential round-trips with arithmetic over the responses (A + B + C = 60), two clients sharing one echo, io-buffer trace"1 2 3 4 ". Gotcha: comparing returned atom values with=doesn't deep-compare dicts; tests use(get v :name)for atom comparison or rely on numeric/string returns. Total suite 350/350. - 2026-04-24 bank.erl green —
lib/erlang/tests/programs/bank.sxwith 8 tests. Stateful server pattern:Server = fun (Balance) -> receive ... Server(NewBalance) end endrecursively threads balance through each iteration. Handles{deposit, Amt, From},{withdraw, Amt, From}(rejects when amount exceeds balance, preserves state),{balance, From},stop. Tests cover deposit accumulation, withdrawal within balance, insufficient funds with state preservation, mixed transactions, clean shutdown, two-client interleave. Total suite 343/343. - 2026-04-24 ping_pong.erl green —
lib/erlang/tests/programs/ping_pong.sxwith 4 tests: classic Pong server + Ping client with separateping_done/pong_donenotifications, 5-round trace via io-buffer ("ppppp"), main-as-pinger-4-rounds (no intermediate Ping proc), tagged-id round-trip ("4 3 2 1 "). All driven byPing = fun (Target, K) -> ... Ping(Target, K-1) ... endself-recursion — captured-env reference works becausePingbinds in main's mutable env before any spawned body looks it up. Total suite 335/335. - 2026-04-24 ring.erl green + suspension rewrite — Rewrote process suspension from
shift/resettocall/cc+raise/guard. Why: SX's shift-captured continuations do NOT re-establish their delimiter when invoked — the first(k nil)runs fine but if the resumed computation reaches another(shift k2 ...)it raises "shift without enclosing reset". Ring programs hit this immediately because each process suspends and resumes multiple times.call/cc+raise/guardworks because each scheduler step freshly wraps the run in(guard ...), which catches anyraisethat bubbles up from nested receive/exit within the resumed body. Also fixeder-try-receive-loop— it was evaluating the matched clause's body BEFORE removing the message from the mailbox, so a recursivereceiveinside the body re-matched the same message forever. Addedlib/erlang/tests/programs/ring.sxwith 4 tests (N=3 M=6, N=2 M=4, N=1 M=5 self-loop, N=3 M=9 hop-count via io-buffer). All process-communication eval tests still pass. Total suite 331/331. - 2026-04-24 exit/1 + termination green —
exit/1BIF uses(shift k ...)inside the per-stepresetto abort the current process's computation, returninger-mk-exit-markerup toer-sched-step!. Step handler records:exit-reason, clears:exit-result, marks dead. Normal fall-off-end still records reasonnormal.exit/2errors with "deferred to Phase 4 (links)". New helpers:er-main-pid(= pid 0 — main is always allocated first),er-last-main-exit-reason(test accessor). 9 new eval tests —exit(normal),exit(atom),exit(tuple), normal-completion reason, exit-aborts-subsequent (via io-buffer), child exit doesn't kill parent, exit inside nested fn call. Total eval 174/174; suite 327/327. - 2026-04-24 receive...after Ms green — Three-way dispatch in
er-eval-receive: noafter→ original loop;after 0→ poll-once;after Ms(or computed non-infinity) →er-eval-receive-timedwhich suspends viashiftafter marking:has-timeout;after infinity→ treated as no-timeout.er-sched-run-all!now recurses intoer-sched-fire-one-timeout!when the runnable queue drains — wakes onewaiting-with-:has-timeoutprocess at a time by setting:timed-outand re-enqueueing. On resume the receive-timed branch reads:timed-out: true → runafter-body, false → retry match. "Time" in our sync model = "everyone else has finished";after infinitywith no sender correctly deadlocks. 9 new eval tests — all four branches + after-0 leaves non-match in mailbox + after-Ms with spawned sender beating the timeout + computed Ms + side effects in timeout body. Total eval 165/165; suite 318/318. - 2026-04-24 send + selective receive green — THE SHOWCASE —
!(send) inlib/erlang/transpile.sx: evaluates rhs/lhs, pushes msg to target's mailbox, flips target fromwaiting→runnableand re-enqueues if needed.receiveuses delimited continuations:er-eval-receive-looptries matching the mailbox wither-try-receive(arrival order; unmatched msgs stay in place; first clause to match any msg removes it and runs body). On no match,(shift k ...)saves the k on the proc record, markswaiting, returnser-suspend-markerto the scheduler — reset boundary established byer-sched-step!. Scheduler looper-sched-run-all!pops runnable pids and calls either(reset ...)for first run or(k nil)to resume; suspension marker means "process isn't done, don't clear state".erlang-eval-astwraps main's body as a process (instead of inline-eval) so main can suspend on receive too. Queue helpers added:er-q-nth,er-q-delete-at!. 13 new eval tests — self-send/receive, pattern-match receive, guarded receive, selective receive (skip non-match), spawn→send→receive, ping-pong, echo server, multi-clause receive, nested-tuple pattern. Total eval 156/156; suite 309/309. Deadlock detected if main never terminates. - 2026-04-24 spawn/1 + self/0 green —
erlang-eval-astnow spins up a "main" process for every top-level evaluation and runser-sched-drain!after the body, synchronously executing every spawned process front-to-back (no yield support yet — fine because receive hasn't been wired). BIFs added inlib/erlang/runtime.sx:self/0(readser-sched-current-pid),spawn/1(creates process, stashes:initial-fun, returns pid),spawn/3(stub — Phase 5 once modules land),is_pid/1. Pids added toer-equal?(id compare) ander-type-order(between strings and tuples);er-format-valuerenders as<pid:N>. 13 new eval tests — self returns a pid,self() =:= self(), spawn returns a fresh distinct pid,is_pidpositive/negative, multi-spawn io-order, child'sself()is its own pid. Total eval 143/143; runtime 39/39; suite 296/296. Next:!(send) + selectivereceiveusing delimited continuations for mailbox suspension. - 2026-04-24 scheduler foundation green —
lib/erlang/runtime.sx+lib/erlang/tests/runtime.sx. Amortised-O(1) FIFO queue (er-q-new,er-q-push!,er-q-pop!,er-q-peek,er-q-compact!at 128-entry head drift), tagged pids{:tag "pid" :id N}wither-pid?/er-pid-equal?, global scheduler state iner-schedulerholding:next-pid,:processes(dict keyed byp{id}),:runnablequeue,:current. Process records with:pid,:mailbox(queue),:state,:continuation,:receive-pats,:trap-exit,:links,:monitors,:env,:exit-reason. 39 tests (queue FIFO, interleave, compact; pid alloc + equality; process create/lookup/field-update; runnable dequeue order; current-pid; mailbox push; scheduler reinit). Total erlang suite 283/283. Next:spawn/1,!,receivewired into the evaluator. - 2026-04-24 core BIFs + funs green — Phase 2 complete. Added to
lib/erlang/transpile.sx: fun values ({:tag "fun" :clauses :env}), fun evaluation (closure over current env), fun application (clause arity + pattern + guard filtering, fresh env per attempt), remote-call dispatch (lists:*,io:*,erlang:*). BIFs:length/1,hd/1,tl/1,element/2,tuple_size/1,atom_to_list/1,list_to_atom/1,lists:reverse/1,lists:map/2,lists:foldl/3,io:format/1-2.io:formatwrites to a capture buffer (er-io-buffer,er-io-flush!,er-io-buffer-content) and returnsok— supports~n,~p/~w/~s,~~. 35 new eval tests. Total eval 130/130; erlang suite 244/244. Phase 2 complete — Phase 3 (processes, scheduler, receive) is next. - 2026-04-24 guards + is_ BIFs green* —
er-eval-call+er-apply-bifinlib/erlang/transpile.sxwire local function calls to a BIF dispatcher. Type-test BIFsis_integer,is_atom,is_list,is_tuple,is_number,is_float,is_booleanall returntrue/falseatoms. Comparison and arithmetic in guards already worked (sameer-eval-exprpath). 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!inlib/erlang/transpile.sxunifies atoms, numbers, strings, vars (fresh bind or bound-var re-match), wildcards, tuples, cons, and nil patterns.case ... of ... [when G] -> B endwired viaer-eval-casewith 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-circuitandalso/orelse), tuples, lists with++,begin..endblocks, bare comma bodies,matchwhere LHS is a bare variable (rebind-equal-value accepted), andifwith 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'sparse-numbercoerces"1.0"→ integer1, so=:=can't distinguish1from1.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 intests/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..endblocks, anonymousfun,try..of..catch..after..endwithClass:Patterncatch clauses. Module-level:-module(M).,-export([...])., multi-clause functions with guards. SX gotcha: dict key order isn't stable, so tests usedeep=(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 remetc.), punct (( ) { } [ ] , ; . : :: -> <- <= => << >> | ||), ops (+ - * / = == /= =:= =/= < > =< >= ++ -- ! ?),%line comments. 62/62 green.
Blockers
- (none yet)