Compile to Erlang Abstract Format in-process via compile:forms/2 - #82
Merged
Conversation
…le:forms/2 Replace the textual Core Erlang round trip (core_printer print -> core_scan/core_parse re-parse -> undocumented from_core entry) with a direct lowering of the backend CModule AST to Erlang Abstract Format terms, compiled in-process by compile:forms/2. - backend/eaf.gleam: the new CModule -> abstract-forms translator. EAF nodes are plain tuples, so they are built as Gleam tuple literals coerced through an identity FFI. The two Core-isms Erlang lacks are lowered here: every binder is alpha-renamed to a unique <legalized>@<counter> name (Core shadowing -> Erlang single-assignment), and letrec lowers to a named fun (F = fun F(...) -> ... end; a multi-def group lowers to one tag-dispatching named fun). Value-list lets become match sequences; primop 'build_stacktrace' vanishes (catch C:R:S already binds the built stacktrace); each top-level function is annotated with a distinct synthetic line. - twocore_codegen_ffi.erl: compile_core (scan/parse/from_core) is replaced by compile_forms (compile:forms/2 + the same flat '<loc>: <message>' diagnostic normalization), plus forms_to_erl (erl_pp dump) and the id/1 coercion helper. The indent-stripping workaround for the 85 MB .core text transient is gone with the text. - build_beam.gleam: the stage API now takes the CModule (compile_module / compile_and_load / link_beam) or raw forms (compile_forms); the BuildError contract is unchanged. - core_erlang/core_printer docs: the printer is demoted to an inspection-only surface (to-core/emit dumps); its output is never re-parsed.
The whole-program linker consumed the generated module as .core text
(core_scan/core_parse). It now takes the same abstract forms the build
compiles and recovers the #c_module{} through the compiler's own
to_core pass in-process, stripping the auto-added module_info/0,1
defs/exports to restore the merge pipeline's invariant (assemble emits
the merged pair itself). The discovered-closure acquisition (resident
.beam debug_info core_v1, compile:file to_core fallback), reachability,
mangle, DCE, D3a self-check and the deterministic from_core compile of
the MERGED cerl are unchanged.
link_manifest: the frozen acquisition rule for the generated module is
renamed GeneratedCoreText -> GeneratedAbstractForms.
…akes .ir - pipeline: ir_to_lowered_core/ir_to_core+core_to_beam are replaced by ir_to_lowered_cmod/ir_to_cmod + cmod_to_beam - the compile path hands the backend AST straight to build_beam (abstract forms + compile:forms/2), with no printed text in between. ir_to_core remains as the textual inspection surface backing the to-core dump. chunks_to_beams compiles each chunk CModule directly. - cli: to-beam/build now compiles .ir (there is no .core text left to compile; the verb runs the fail-closed Safe binding); a new to-erl verb dumps the generated module as Erlang source via erl_pp over the exact forms compile:forms/2 consumes; to-beam-wasm and the --link path go through the CModule seam. - embed: unchanged behavior (compiles via the updated chunk path); doc references updated.
Update every test that compiled printed .core text to hand the backend CModule (or its abstract forms, for the linker) to the new API: - build_beam_test: the hand-written .core string fixtures become hand-built CModule fixtures exercising the same shapes (guards, hot-replace, error paths - the broken-input cases now fail in erl_lint/compile:forms instead of the scanner, same typed CompileFailed contract). - beam_link_test: the synthetic generated modules (fun-capture, D3a erlang:apply, missing closure, mangle collision, on_load, malformed) are built as CModules and lowered with eaf.module_forms; the malformed-input case is now an unbound variable caught by the linker's to_core acquisition. - everywhere else: mechanical moves from ir_to_core + compile_and_load(from_string(core)) to ir_to_cmod + compile_and_load(cmod) (and cmod_to_beam), keeping the text-shape assertions on the printed .core dump where they existed. - cli_test: to-beam is driven with .ir input; a to-erl test asserts the erl_pp dump.
alii
marked this pull request as ready for review
July 19, 2026 15:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the textual Core Erlang backend with Erlang Abstract Format (EAF) generation compiled in-process via
compile:forms/2. The compiler now builds abstract forms as in-memory Erlang terms and hands them straight to the compiler — no Core Erlang text is ever printed, re-scanned, or re-parsed.Why
cerlhas been undocumented since ~OTP 16.debug_info/abstract_codechunk → Dialyzer, cover, and decompilation work on 2core output.Annoon every node → real stack traces.Precedent: Elixir compiles to EAF +
:compile.forms; Gleam abandoned Core Erlang (and is now exploring EAF in gleam-lang/gleam#5990); LFE moved off Core.Design notes
src/twocore/backend/eaf.gleam: IR → EAF forms. EAF tuples are plain tuples, so they're constructed directly from Gleam.CModule/forms seam;to-erladded for debugging (pretty-prints forms viaerl_pp); linker acquires generated modules from abstract forms.Test status
Full suite green: 2212 passed, 0 failures, including the WASM spec-suite capstones (tail calls 47734/683/0 headline, cross-module funcref elem init) and differential combos (OptNone ≡ Baseline ≡ Aggressive).
Open questions
to-erloutput is for debugging only — happy to drop it if unwanted.🤖 Generated with Claude Code