# 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/**` and `plans/erlang-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, `lib/js/**`, `lib/hyperscript/**`, `lib/lua/**`, `lib/prolog/**`, `lib/forth/**`, `lib/haskell/**`, `lib/stdlib.sx`, or `lib/` root. Erlang primitives go in `lib/erlang/runtime.sx`. - **SX files:** use `sx-tree` MCP tools only. - **Commits:** one feature per commit. Keep `## Progress log` updated 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 pid - **`send`** = append to target mailbox; if target is blocked on receive, resume its continuation - **`receive`** = selective — scan mailbox for first matching clause; if none, `perform` a suspend with the receive pattern; scheduler resumes when a matching message arrives - **Pattern matching** = SX `case` on 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 - [x] Tokenizer: atoms (bare + single-quoted), variables (Uppercase/`_`-prefixed), numbers (int, float, `16#HEX`), strings `"..."`, chars `$c`, punct `( ) { } [ ] , ; . : :: ->` — **62/62 tests** - [x] Parser: module declarations, `-module`/`-export`/`-import` attributes, function clauses with head patterns + guards + body — **52/52 tests** - [x] Expressions: literals, vars, calls, tuples `{...}`, lists `[...|...]`, `if`, `case`, `receive`, `fun`, `try/catch`, operators, precedence - [ ] Binaries `<<...>>` — not yet parsed (deferred to Phase 6) - [x] Unit tests in `lib/erlang/tests/parse.sx` ### Phase 2 — sequential eval + pattern matching + BIFs - [x] `erlang-eval-ast`: evaluate sequential expressions — **54/54 tests** - [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 - [ ] 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 ... end` with selective pattern matching - [ ] `receive ... 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 times - [ ] `ping_pong.erl` — two processes exchanging messages - [ ] `bank.erl` — account server (deposit/withdraw/balance) - [ ] `echo.erl` — minimal server - [ ] `fib_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 modules - [ ] `gen_server` behaviour (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 `<>` - [ ] 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!` 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. - **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. ## Blockers - _(none yet)_