Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
7.9 KiB
7.9 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
<<...>>— not yet parsed (deferred to Phase 6) - 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 - 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
Phase 3 — processes + mailboxes + receive (THE SHOWCASE)
- Scheduler in
runtime.sx: runnable queue, pid counter, per-process state record spawn/1,spawn/3,self/0!(send),receive ... endwith selective pattern matchingreceive ... after Ms -> ...timeout clause (use SX timer primitive)exit/1, basic process termination- Classic programs in
lib/erlang/tests/programs/:ring.erl— N processes in a ring, pass a token around M timesping_pong.erl— two processes exchanging messagesbank.erl— account server (deposit/withdraw/balance)echo.erl— minimal serverfib_server.erl— compute fib on request
lib/erlang/conformance.sh+ runner,scoreboard.json+scoreboard.md- Target: 5/5 classic programs + 1M-process ring benchmark runs
Phase 4 — links, monitors, exit signals
link/1,unlink/1,monitor/2,demonitor/1- Exit-signal propagation; trap_exit flag
try/catch/of/end
Phase 5 — modules + OTP-lite
-module(M).loading,M:F(...)calls across modulesgen_serverbehaviour (the big OTP win)supervisor(simple one-for-one)- Registered processes:
register/2,whereis/1
Phase 6 — the rest
- List comprehensions
[X*2 || X <- L] - Binary pattern matching
<<A:8, B:16>> - ETS-lite (in-memory tables via SX dicts)
- More BIFs — target 200+ test corpus green
Progress log
Newest first.
- 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)