← board

Erlang frontend — scoping only

Reframed under the esoteric-frontend-probe category (2026-07-05)

The full-language scoping below still stands, but a skeleton-only pass (lexer/parser for a trivial subset, lowering onto existing IR, no scheduler/ GC work) is separately in scope as a bug-probe against shared internals — not to make Erlang usable. See [[feature-esoteric-frontend-probes]] for the category rule.

Motivation

User chose Erlang over Zig as the next frontend to scope (not necessarily build first — Rust is already in progress).

Correcting the original rationale (2026-07-05): the first cut of this ticket justified the choice as "Zig risks determinism, Erlang doesn't." That doesn't hold up — see [[feature-zig-frontend]]'s corrected "Why parked" section. Recursion/Turing-completeness in a comptime interpreter is a termination risk (bounded by a step quota), not a determinism one, and it can't touch the compiler's own self-host fixedpoint gate since it only runs while compiling Zig source. Meanwhile Erlang's actor model — preemptive scheduling, message ordering across processes — is itself a classically nondeterministic runtime execution model. If determinism were the metric, Zig would win, not lose. Determinism is not why Erlang is worth scoping.

The real reason: Zig and Erlang are hard in genuinely different domains. Zig's cost is concentrated in one place (a comptime interpreter/CTFE engine) that is pervasive but at least conceptually contained. Erlang's cost is spread across the runtime — but the memory-management part of that cost is smaller than first estimated (see "GC gap sizing, corrected" below); the scheduler is where the real open work is. Not "syntax sugar and hashing" either way — the syntax (pattern matching, immutability, tuples/atoms/maps) is the easy part, same as Zig's type theory was the easy part of Zig — but the remaining hard part is now sized as one genuinely novel piece (the scheduler), not two.

This ticket is scoping only — same posture as the original Rust/Zig umbrellas before any code: map Erlang's constructs onto existing/needed IR, identify what's free vs. what needs new shared machinery, before committing to a build.

GC gap sizing, corrected (2026-07-05)

Original cut of this ticket claimed per-process GC was "plausibly the single largest item in this whole ticket, bigger than the scheduler itself," reasoning by analogy to BEAM's own implementation (per-process generational copying GC). That analogy doesn't transfer cleanly and overstated the gap:

Net effect on where the hard part sits: moved from "two roughly-equal unknowns (scheduler + GC)" to "one clearer unknown (the scheduler), one probably-tractable-via-reuse item (memory management)." This makes Erlang more realistic than the original sizing suggested, not less — flag this explicitly so it isn't rediscovered the hard way later.

Known hard parts, unscoped (fill in when picked up)

Explicit early non-goals (proposed — confirm when scoping starts)

Acceptance (for this scoping ticket only)

A gap-map table (like [[feature-zig-frontend]]'s "AST/IR gap map") showing what lowers onto existing IR for free vs. what needs new shared machinery, plus an honest sizing of the scheduler gap specifically (the one part with no close existing analogue) and a validated (not assumed) answer on whether refcounting extends cleanly to Erlang terms or hits a real wall (closures/ETS lifetime, refcount-traffic cost). No code required to close this ticket — only a scoping doc, same bar as the original Rust/Zig umbrellas before work started.

Log

Skeleton probe done (2026-07-07, Track Z session)

The esoteric-probe pass (NOT the full-language scoping, which stands unchanged below/above): compiler/elexer.inc + compiler/eparser.inc (new), isErl + .erl dispatch in compiler.pas, one-line var in defs.inc.

The probe shape, and why it mattered: multi-clause functions with pattern dispatch — fact(0) -> 1; fact(N) -> N * fact(N-1). compiles to ONE proc whose body chains per-clause conditions (literal patterns = equality tests on the parameter, variable patterns = clause-local binds, when guards ANDed on, fall-off-the-end = loud runtime function_clause error + halt 1). Dispatch-on-argument-value is an AST shape no other frontend generates. Verdict: shared AST/IR handled it cleanly — fact/fib recursion, 4-clause guard dispatch, div/rem all correct first run once two frontend-local bugs were fixed:

Subset: module/export attributes swallowed, integer-only arithmetic (+ - * div rem; / errors, pointing at div), == /= < =< > >=, single-assignment enforced (rebinding = compile error), calls ≤4 params, io:format with ~p/~w/~B and trailing ~n. No processes/messages/atoms/ tuples/lists — the full-language scoping below remains the ticket's actual subject.

Probe closed per the umbrella (acceptance (b)); scoping ticket itself stays open/parked as before — moved back to backlog.