Skip to main content

MEP 46. Mochi-to-Erlang/BEAM transpiler: concurrent, distributed runtime via Core Erlang as target

FieldValue
MEP46
TitleMochi-to-Erlang/BEAM transpiler
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-23 00:05 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse)
Research~/notes/Spec/0046/01..12
Tracking/docs/implementation/0046/

Abstract

Mochi today ships vm3 (mochi run) and, with MEP-45 in flight, an ahead-of-time C transpiler producing native single-file binaries. Neither path gives users BEAM's concurrency model: M:N preemptive scheduling, process-isolated heaps, supervision trees, hot code reload, cluster-aware pubsub, and 30 years of OTP infrastructure. MEP-46 specifies a separate pipeline that targets the BEAM virtual machine via Core Erlang, the documented compiler API the Erlang/OTP team supports for external language front-ends (the same API used by LFE, Clojerl, Hamler, and Alpaca).

The pipeline reuses MEP-45's typed-AST and aotir IR, then forks at the lowering stage: instead of emitting ISO C23, it emits Core Erlang via the cerl Erlang constructor API, then calls compile:forms({c_module, ...}, [from_core, debug_info, return_errors, return_warnings]) to produce .beam files. Downstream, BeamAsm JIT (default since OTP 24) translates the BEAM bytecode to native machine code at load time. Two packaging targets ship together: --target=beam-escript produces a single-file executable wrapping all .beam plus a shebang (no ERTS bundled, requires erl on $PATH, ~50ms cold start, 2-10 MB); --target=beam-release produces a self-contained OTP release (ERTS bundled, ~300ms cold start, 30-80 MB, supports hot reload, supervised, daemon-ready). A third tier, --target=beam-atomvm, lints against the AtomVM compatibility profile and bundles .beam for ESP32/STM32 deployment.

The master correctness gate is byte-equal stdout from the produced .beam (run via escript) versus vm3 on the entire fixture corpus, across OTP 27.0, OTP 27.latest, and OTP 28.latest, on x86_64-linux-gnu and aarch64-darwin. vm3 is the recording oracle for expect.txt; the transpiler does not link against or depend on vm3.

Four load-bearing decisions:

  1. Core Erlang via cerl, not Erlang abstract format and not source text. The Erlang Ecosystem Foundation's Compiler Workgroup explicitly recommends Core Erlang as the plug-in point for external languages (see [[03-prior-art-transpilers]] §18). LFE has used this API in production for 17 years; the API has been stable since the OTP r12 release in 2009. Abstract format (the Elixir/Gleam route) is slightly easier to pretty-print as .erl but is less stable across OTP versions (column tracking changed in OTP 27) and exposes irrelevant syntactic sugar. Source text (the Caramel/Purerl route) requires error-prone pretty-printing for marginal benefit. Core Erlang is the documented contract.
  2. OTP wholesale; no Mochi-flavored process model. Mochi agents map directly to gen_server; supervision uses supervisor / dynamic_supervisor; pubsub uses pg; futures map to monitored spawn + selective receive on freshly created refs (the BEAM 24+ recv-marker optimisation makes this O(1)). The Mochi compiler does not introduce a new process model on top of BEAM; users get OTP's exact semantics, with its distributed pubsub for free.
  3. Reuse MEP-45's aotir IR. The IR is target-agnostic; monomorphisation, match-to-decision-tree, and closure-conversion passes run once and feed both backends. The fork is at the emit pass: transpiler3/beam/lower/ lowers aotir to cerl records; transpiler3/c/emit/ lowers aotir to C. Sharing the IR halves the implementation effort and ensures cross-target consistency.
  4. OTP 27 minimum. OTP 27 (May 2024) adds the json stdlib module (eliminating jsx/jiffy), sigils ~"..." for binary string literals (cleanest mapping for Mochi strings), maybe expression as default, triple-quoted strings, and -doc attribute. OTP 26 is rejected as a minimum because the json polyfill alone costs ~600 lines of compatibility shim with no upside; OTP 27 is in Debian stable, Ubuntu 24.04, Homebrew, and asdf as of mid-2025.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/beam/fixtures/ must compile via the BEAM pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. Dialyzer-clean (with -Werror) on generated code is the secondary gate. Reproducibility (bit-identical .beam chunks across two CI hosts) is the tertiary gate.

Motivation

Mochi today targets vm3 (for mochi run) and, via MEP-45, statically linked native binaries (for mochi build --target=c). Neither delivers what BEAM uniquely provides:

  1. Concurrency at scale. BEAM was designed in 1986 for telephony switches with 100K simultaneous calls and 99.9999999% uptime. It has a preemptive M:N scheduler over OS threads, per-process garbage collection (a process's GC never stalls another), and cheap process creation (~3 µs per process; 2M processes per node has been demonstrated). Mochi's C target gives a single-threaded model with cooperative fibers; BEAM gives industrial-strength concurrent execution with no library on top.
  2. Supervision. OTP supervisors restart crashed processes per a declared policy. Mochi's C target leaves the user to write process management; BEAM gives them OTP's 30-year-old, telecom-validated supervision trees for free.
  3. Hot code reload. code:load_file/1 swaps a module's code in a running BEAM node; existing processes keep state and use the new code on next fully-qualified call. No restart required. This is unique to BEAM and matters for long-running services, finance, telecom, and any system where downtime is costly.
  4. Distributed pubsub for free. BEAM's pg process groups are cluster-aware: a publish on node A delivers to subscribers on node B if both have joined the same group. Mochi streams therefore become distributed by default on BEAM, with zero extra code.
  5. Ecosystem. The BEAM ecosystem includes Phoenix (web framework, used by Discord, Heroku, and others), Ecto (data mapper), Nerves (embedded), Mnesia (distributed database), Riak (distributed KV store), RabbitMQ (messaging), and CouchDB (document store). A Mochi-on-BEAM build can interop with any of them via FFI.
  6. Tooling. Dialyzer (success typing), eqWAlizer (Whatsapp's gradual typing), observer (graphical process inspector), recon (production tracing), telemetry (metrics), and the BEAM JIT all work on Mochi-emitted modules out of the box because we emit standard .beam files with debug_info and line chunks.

The C target (MEP-45) remains the right choice for CPU-bound numerics, embedded targets without dynamic loading, and single-file distribution. The BEAM target is the right choice for services, agents, streams, distributed systems, and anywhere hot reload or supervision matters. Both ship; the user picks.

Specification

This section is normative. Sub-notes under ~/notes/Spec/0046/01..12 are informative.

1. Pipeline and IR reuse

MEP-46 shares the front-end and aotir passes with MEP-45 and forks at the emit stage:

Mochi source
│ parser (MEP-1/2/3, reused)

AST
│ type checker (MEP-4/5/6, reused)

Typed AST
│ monomorphise (MEP-45 pass 1, reused)

Monomorphic typed AST
│ lower (MEP-45 pass 2, reused)

aotir (MEP-45's IR, reused)
│ match-to-decision-tree (MEP-45 pass 3, reused)

aotir (matches lowered)
│ closure-convert (MEP-45 pass 4, reused)

aotir (closures lowered)
│ beam-lower (MEP-46 pass 1; ./transpiler3/beam/lower/)

cerl records (Core Erlang)
│ compile:forms/2 from_core (OTP's compiler)

BEAM .beam files
│ package as escript / release / atomvm bundle

Distributable artifact

aotir is unchanged. The MEP-46-specific work lives in transpiler3/beam/:

  • transpiler3/beam/lower/: aotircerl records.
  • transpiler3/beam/emit/: drives compile:forms/2 (spawning an embedded erl) to produce .beam files; also pretty-prints .erl alongside for handoff.
  • transpiler3/beam/build/: build driver: escript / release / atomvm targets; cache.
  • transpiler3/beam/runtime/: the mochi OTP application (Erlang source + .app resource file).

The codegen visits each aotir type exactly once via a memoised lower_type(aotirType) → cerlShape, exactly as the C target does. See [[06-type-lowering]] §12.

2. Name mangling and atom safety

Mangled identifier form:

mochi_{pkg}__{module}__{name}[__{instArgsHash6}]
  • Module names: lowercase, prefixed mochi_user_ for user modules and mochi_ for runtime modules.
  • Function names: lowercase, prefixed with the source identifier (no prefix needed; functions are namespaced by their module).
  • Variable names in Core Erlang: prefixed V_ (Core Erlang variables start uppercase but our generated ones use this convention to avoid clashes with Erlang keywords).
  • Atom literals (sum-type tags, record field names): prefixed with the source identifier, lowercased; reserved Erlang atoms (atom, binary, case, etc.) are suffixed with _atom.

The BEAM atom table has a default limit of 1,048,576 atoms. To prevent exhaustion in long-running services that dynamically reload Mochi modules, the codegen emits a mochi_atoms_<modhash>:atoms/0 function listing every atom the module references; the runtime calls these at boot to pre-register atoms and never uses binary_to_atom/1 on user data (only binary_to_existing_atom/2).

See [[05-codegen-design]] §3 and [[06-type-lowering]] §2.

3. Type lowering table

Mochi typeBEAM representationNotes
intBEAM integer (arbitrary precision; small int fastpath ≤60 bits)1:1; no boxing
floatBEAM float (boxed double)1:1; IEEE 754
booltrue / false atoms1:1
stringUTF-8 binary (<<"hello"/utf8>>)OTP 27 sigils ~"hello" map directly
timeinteger (ns since Unix epoch UTC)wraps erlang:system_time(nanosecond)
durationinteger (ns)matches time representation
?T{some, V} tuple or none atomdirect sum encoding
list<T>BEAM list (cons cells)1:1; comprehensions are native
map<K,V>BEAM map (flat ≤32 keys, HAMT above)1:1
omap<K,V>{KeysList, KeyToValueMap} tuplepreserves insertion order; query DSL backing
set<T>sets:set() v2 (since OTP 24)1:1
stream<T>opaque ref wrapping {?MODULE, StreamName} pg grouphub identified by atom
chan<T>gen_server-backed bounded queuepoint-to-point variant
record Rtagged map #{'__mochi_record__' => R, field => V, ...}tag enables variant discrimination
sum Stagged tuple {Variant, V1, V2, ...} or bare atom for unit variantsmatches Erlang idiom
fun(A,B):CBEAM fun (closure with captured env)1:1
agent Aopaque ref wrapping a PID; methods are gen_server:call/castgen_server callback module per agent type

See [[06-type-lowering]] for the full lowering, including equality semantics (=:=), pattern matching shapes, and Dialyzer -spec emission.

4. Expression and statement lowering

Expressions lower to Core Erlang c_let-bound temporaries with explicit casts; Core Erlang is side-effect-explicit, so any subexpression with possible effects gets a let binding. Short-circuit && / || lower to case with explicit clauses (Core Erlang has no built-in short-circuit operators). Integer arithmetic uses BEAM operators (arbitrary precision; division by zero raises badarith, caught by Mochi try/catch as MOCHI_ERR_DIVZERO). Float arithmetic preserves IEEE 754 semantics. String + lowers to <<S1/binary, S2/binary>>.

if, while, for in, match, return, break, continue lower to case expressions and recursive tail calls. BEAM has no native while or for; loops become tail-recursive helper functions, which the BeamAsm JIT compiles efficiently.

match lowers to Core Erlang case nodes; the kernel pass (v3_kernel) compiles the pattern decision tree downstream. We do not implement our own pattern-match compiler; OTP's is mature.

try { ... } catch e { ... } lowers to Core Erlang try/catch (which BEAM compiles to non-zero-cost stack unwinding; the cost is paid only on throw).

See [[05-codegen-design]] §6-10.

5. Closures and funs

Free functions and methods lower to plain Erlang functions. Closures lower to BEAM funs (created via c_fun); free-variable capture is explicit in Core Erlang. BEAM funs are cheap (a small heap object holding the code pointer and the env vector). The BeamAsm JIT inlines fun applications where the target is statically known.

Higher-order functions (map, filter, fold) lower to lists:map/2, lists:filter/2, lists:foldl/3, which are highly optimised in OTP.

See [[05-codegen-design]] §7 and [[09-agent-streams]] §5.

6. Runtime library

The mochi OTP application (source under transpiler3/beam/runtime/, published to Hex.pm as mochi):

mochi/
├── src/
│ ├── mochi.app.src % Application resource
│ ├── mochi_app.erl % application:start/2 callback
│ ├── mochi_sup.erl % Top-level supervisor
│ ├── mochi_atoms.erl % Pre-registered atoms
│ ├── mochi_core.erl % Boxed value helpers
│ ├── mochi_str.erl % String/binary ops
│ ├── mochi_list.erl % List ops with Mochi semantics
│ ├── mochi_map.erl, mochi_set.erl, mochi_omap.erl, mochi_option.erl, mochi_time.erl
│ ├── mochi_query.erl % Query DSL runtime
│ ├── mochi_datalog.erl, mochi_datalog_ets.erl
│ ├── mochi_stream.erl, mochi_stream_sup.erl, mochi_stream_recorder.erl
│ ├── mochi_agent.erl, mochi_agent_sup.erl, mochi_async.erl
│ ├── mochi_llm.erl, mochi_llm_sup.erl, mochi_llm_openai.erl, mochi_llm_anthropic.erl
│ ├── mochi_fetch.erl, mochi_fetch_sup.erl % gun-backed HTTP
│ ├── mochi_ffi.erl, mochi_telemetry.erl, mochi_log.erl
│ └── mochi_test.erl % Test harness
└── test/

The runtime is pure Erlang (no NIFs) and depends only on kernel, stdlib, sasl, crypto, ssl, and gun. See [[04-runtime]] §22 for the full module layout and [[02-design-philosophy]] §7 for the rationale.

7. Concurrency and supervision

Mochi agents lower to gen_server callback modules. Spawning goes through mochi_agent_sup (dynamic supervisor) so every agent is supervised. Streams lower to pg process groups in the scope mochi; publish is pg:get_members + !; subscribe is pg:join + gen_statem. async/await lowers to monitored spawn + selective receive on a freshly created ref (the BEAM 24+ recv-marker optimisation makes the receive O(1)).

See [[09-agent-streams]] for the full mapping table.

8. Memory model

Per-process heaps; per-process generational GC. Large binaries (>64 bytes) are reference-counted off-heap. Atoms are interned globally; the codegen pre-registers all known atoms at boot to prevent exhaustion.

No NIFs in v0.1. The OTP team's stance (Kenneth Lundin, Code BEAM 2023) and our own analysis ([[12-risks-and-alternatives]] §3) is that pure Erlang with BeamAsm JIT is fast enough for Mochi's stdlib; reserve NIFs for crypto, regex, and similarly hot kernels accessed via existing OTP libraries.

See [[04-runtime]] §2 and §9.

9. Error model

Mochi try/catch lowers to Core Erlang try/catch. Built-in error codes (consistent with the C target's set, encoded as atoms on BEAM):

AtomSource
mochi_err_fetchnetwork or HTTP non-2xx
mochi_err_parseJSON / YAML / CSV decode
mochi_err_typeruntime type mismatch
mochi_err_indexOOB index / missing key
mochi_err_divzerointeger divide by zero (caught from badarith)
mochi_err_ffiFFI subprocess failure
mochi_err_llmprovider error from generate
mochi_err_assertexpect false
mochi_err_timeoutagent call timeout or stream timeout
mochi_err_async_crashfuture's worker process crashed

User error atoms are namespaced mochi_user_<module>_<name>.

10. Target portability

Supported OTP versions: 27.0, 27.latest, 28.latest (Tier 1). OTP 29 RC (when available) runs in non-blocking nightly. OTP 26 and earlier unsupported.

Supported platforms:

  • Tier 1 (full CI, blocking): Linux x86-64 (glibc), macOS arm64, macOS x86-64.
  • Tier 2 (CI, best-effort): Linux arm64, Linux musl/Alpine, Windows x86-64.
  • Tier 3 (community-supported): FreeBSD, ppc64le, riscv64, s390x.

The BeamAsm JIT is default on Tier 1 platforms; Tier 2/3 may run interpreter mode but .beam files are arch-independent.

AtomVM compatibility profile: Phase 1-5 fixtures plus a curated Phase 6 subset run unmodified on AtomVM 0.6+ for ESP32/STM32 (no pg, no httpc, no crypto).

See [[07-erlang-target-portability]] for the full matrix.

11. Build driver

mochi build --target=beam-erlc PATH # .beam files only
mochi build --target=beam-escript PATH # single-file executable
mochi build --target=beam-release PATH # OTP release tarball
mochi build --target=beam-rebar3-project PATH # emit rebar3 layout
mochi build --target=beam-mix-project PATH # emit mix layout
mochi build --target=beam-atomvm PATH # .avm for embedded
mochi build --target=beam-... --emit={core|erl|beam}
mochi build --target=beam-... --otp=27|28
mochi build --target=beam-... --reproducible
mochi build --target=beam-... --watch

Cache layout under .mochi/cache/beam/{aotir,cerl,beam}/, content-addressed by BLAKE3 over (source, transitive imports, OTP version, transpiler version). A cache hit is "is the .beam present?". The cache is shared with the C target where possible (aotir entries common to both targets).

The driver does not vendor an OTP installation; it discovers erl on $PATH, validates erl +V against the supported range, and refuses to build on unsupported versions. Docker recipes (mochilang/mochi:beam-otp27) are published for users without local OTP. See [[10-build-system]].

12. Reproducibility

The Dbgi, Line, and CInf chunks of generated .beam files are sources of non-reproducibility. We strip the compile timestamp from CInf, set the path to a relative form, and emit functions and exports in sorted IR-identifier order. Two builds of the same source on different machines produce bit-identical .beam files.

The release tarball includes a manifest.json with a SHA-256 of every .beam; manifests are reproducible across CI agents. See [[07-erlang-target-portability]] §8.

13. Hardening

BEAM does not have ASLR/PIE/RELRO concerns (the VM does); generated .beam files inherit the BEAM emulator's hardening. We require TLS 1.3 (OTP 27+ default) for any fetch over HTTPS and reject TLS 1.0/1.1. For releases targeting distributed deployments, we recommend -setcookie minimum 32-byte entropy and ssl_dist for inter-node communication (documentation, not enforced).

See [[12-risks-and-alternatives]] §7.

14. Diagnostics

Compile-time errors surface with Mochi spans via the type checker. For errors that escape into the OTP compiler (rare; only happens when our cerl emission is invalid, which is a compiler bug), the build driver maps Erlang line refs back to Mochi spans via the Line chunk's #line analogue. Runtime stack traces are rewritten by mochi_log to show Mochi source lines instead of generated Erlang lines.

See [[12-risks-and-alternatives]] §10.

15. Debug info

Dbgi and Line chunks always emitted (no strip mode in v0.1). Together they enable Erlang dbg, the BEAM observer, code:get_doc/1, and the Erlang LSP. The Dbgi chunk contains the abstract format AST recovered from our Core Erlang; this is the OTP-canonical debug info format.

16. -spec and Dialyzer

Every Mochi-exported function emits an Erlang -spec derived from its Mochi type. Generated modules are Dialyzer-clean (rebar3 dialyzer -Werror passes). Erlang devs consuming a Mochi-built library see typed APIs.

Opaque types (e.g. mochi_agent_ref()) use Erlang's -opaque syntax to hide internal representation.

See [[06-type-lowering]] §14.

17. Output style

When the -rebar3-project target is used, we also pretty-print .erl source alongside the .beam files, using erl_prettypr for the round-trip via abstract format. Generated .erl is human-readable for handoff to Erlang devs; the per-line #line analogue in the Line chunk maps back to Mochi source.

Rationale

Why BEAM as a second target after C

MEP-45's C target is the AOT performance story: single-file native binary, vendored cross-cc, every tier-1 triple, ~3MB hello-world, 1.5x of hand-written C on numeric workloads. It does not give us concurrency, supervision, hot reload, or distributed pubsub. BEAM does, and BEAM is the runtime where these features are most polished (30 years of Ericsson production hardening). Together C and BEAM cover the two ends of the runtime spectrum: bare-metal single-threaded AOT and dynamic concurrent supervised. See [[02-design-philosophy]].

Why Core Erlang via cerl, not abstract format or source

The EEF Compiler Workgroup's December 2024 notes explicitly cite Core Erlang as the supported plug-in point for external languages. LFE, Clojerl, Hamler, and Alpaca all use this API. Abstract format is supported for tools but has more syntactic complexity; source text is error-prone to emit cleanly. See [[03-prior-art-transpilers]] §18 and [[05-codegen-design]] §2.

Why reuse aotir instead of a fresh IR

aotir is post-type-checking, post-monomorphisation, target-agnostic. Forking a separate IR would duplicate the closure-conversion and match-decision-tree passes. The C target and BEAM target have different runtime models (manual struct layout vs BEAM map/tuple) but the same set of expression shapes; sharing the IR forces the two backends to handle each language feature consistently.

Why OTP wholesale

The alternative is a Mochi-flavored supervision/process surface that wraps OTP. We rejected this because (a) Mochi's process model already aligns with BEAM's, (b) wrapping adds an indirection layer and a maintenance surface, (c) users wanting OTP semantics now have a thin wrapper hiding the thing they actually want. The MEP commits to OTP semantics; users who want Mochi-only abstractions get them as language sugar over OTP.

Why OTP 27 minimum

OTP 27 (May 2024) adds the json stdlib (eliminating a jsx/jiffy dep), sigils ~"...", maybe expression default, and triple-quoted strings, all of which directly support Mochi's source surface. OTP 26 (May 2023) lacks these; the polyfill cost exceeds the value of supporting older versions. OTP 27 has been in Debian stable, Ubuntu 24.04, Homebrew, and asdf since mid-2025.

Why no NIFs in v0.1

NIFs are scheduler-blocking and platform-specific. The OTP team (Kenneth Lundin, Code BEAM 2023) recommends pure Erlang for new code; existing maintained NIFs (crypto, re, zlib) cover common needs. Adding a NIF requires C build infrastructure, platform-specific binaries, and crash-safety review. BeamAsm JIT is fast enough for Mochi's stdlib. See [[04-runtime]] §9 and [[12-risks-and-alternatives]] §3.

Why differential testing as the master gate

vm3 is the source of truth (matches MEP-45's choice). Byte-equal stdout from the BEAM-produced escript versus vm3, on every fixture, on every supported OTP version, is the strictest behavior check available. Property tests, Dialyzer cleanliness, and reproducibility are layered on top.

Backwards Compatibility

Additive. mochi run and mochi test keep vm3 by default. mochi build gains --target=beam-escript, --target=beam-release, --target=beam-atomvm, --target=beam-rebar3-project, --target=beam-mix-project. No language surface change; no stdlib surface change beyond mirroring vm3's existing user-visible exposure inside the BEAM runtime.

Observable behaviour must match vm3 byte-for-byte on the fixture corpus. Programs relying on implementation-defined vm3 behaviour (allocation order, GC pause timing) are explicitly non-portable; the spec already disallows reliance on these.

Reference Implementation

Code lives under a fresh tree transpiler3/beam/, sharing only the front-end (parser, type checker) and aotir IR with MEP-45:

TreePurpose
transpiler3/beam/lower/aotircerl records (MEP-46 pass 1)
transpiler3/beam/emit/drives compile:forms/2; pretty-prints .erl sidecar
transpiler3/beam/build/driver: escript / release / atomvm / rebar3 / mix targets
transpiler3/beam/runtime/the mochi OTP application source
transpiler3/beam/runtime/src/mochi_*.erl files compiled into the runtime library
tests/transpiler3/beam/fixture corpus, expect files, integration tests
tests/transpiler3/beam/fixtures/per-phase fixtures (phaseN/...)
tests/transpiler3/beam/bench/performance harness (Phase 18)

The phased delivery plan is the §Phases section below. Each phase ships as a sub-PR auto-merged per the project's auto-ship convention; tracking pages live under /docs/implementation/0046/.

Phases

The plan walks the language surface bottom-up (Phases 0-12), then layers packaging (Phases 13-15), then quality gates (Phases 16-18), and culminates in v1.0 (Phase 19). Phases 0-12 are strictly sequential; Phases 13-15 can run in parallel after Phase 12 lands; Phases 16-18 run after Phase 15 lands and continue in perpetuity.

Phase conventions:

  • Gate. A single measurable criterion. A phase is LANDED only when its gate is green on every target listed.
  • Targets. OTP version × arch matrix in scope at this phase.
  • Status / Commit columns. Filled in along the way. Values: NOT STARTED, IN PROGRESS, BLOCKED, LANDED, DEFERRED.
  • Goal-alignment audit. Before a phase starts, a one-paragraph audit on its tracking page confirms the gate moves the user-facing goal ("ship a Mochi program as a runnable .beam artifact on this target"), not spec-internal scaffolding.
  • Spec-in-sync. The PR that lands a phase's code must also update this MEP file and the tracking page.
  • Reference oracle. Fixture goldens (expect.txt) recorded by vm3.

Phase 0. Spec freeze and skeleton trees

FieldValue
StatusIN PROGRESS
Commit
GateThis MEP merged on main; transpiler3/beam/{lower,emit,build,runtime/src}/doc.go (and corresponding .erl stubs) compile clean; tests/transpiler3/beam/ exists with a README.md; implementation tracking pages exist under /docs/implementation/0046/
Targetsn/a (paperwork phase)
Tracking/docs/implementation/0046/phase-00-skeleton

Sub-phases

#ScopeStatusCommit
0.0This MEP merged with full framing, §Phases section, implementation tracking docs, sidebar wiringIN PROGRESS
0.1transpiler3/beam/{lower,emit,build,runtime/src}/ skeleton with doc.go files; go vet ./transpiler3/beam/... cleanNOT STARTED
0.2tests/transpiler3/beam/README.md documents fixture layout and naming conventionNOT STARTED
0.3mochi.app.src skeleton + mochi_app.erl / mochi_sup.erl placeholders that boot the empty supervision treeNOT STARTED

Test set. Documentation/website build only: npm run gen:meps && npm run build clean; go vet ./transpiler3/beam/... clean.

Risks. None substantial.

Phase 1. Hello world

FieldValue
StatusNOT STARTED
Commit
Gatemochi build --target=beam-escript --out=/tmp/hello tests/transpiler3/beam/fixtures/phase1/001_hello.mochi && /tmp/hello | diff - tests/transpiler3/beam/fixtures/phase1/001_hello.out exits 0 on OTP 27 host
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-01-hello

Sub-phases

#ScopeStatusCommit
1.0Source-to-beam minimum: one fn returning unit, one print(string); aotircerlcompile:forms/2; escript packaging via escript:create/2NOT STARTED
1.1--target=beam-escript, --out PATH, --emit=core|erl|beam CLI flags wired through cmd/mochi/main.goNOT STARTED
1.2.mochi/cache/beam/ BLAKE3 content-addressed cache; rebuild on unchanged source is a copyFile no-opNOT STARTED
1.3mochi_str.erl runtime stub with mochi_str:print/1 (writes UTF-8 binary + \n to stdout via io:put_chars/1)NOT STARTED

Deliverables.

  • transpiler3/beam/lower/: lower aotir.Programcerl:c_module(...); functions, c_call for print.
  • transpiler3/beam/emit/: spawn erl -noshell -eval ... to invoke compile:forms/2; write .beam.
  • transpiler3/beam/build/: Driver with Build(srcPath, outPath, target, profile).
  • transpiler3/beam/runtime/src/mochi_str.erl: print/1.
  • tests/transpiler3/beam/fixtures/phase1/001_hello.mochi + .out.

Test set. go test ./transpiler3/beam/build (TestPhase1Hello for in-process driver; TestCLIPhase1Hello for mochi build --target=beam-escript).

Risks. erl not on $PATH (mitigated by clear error message and Docker recipe). compile:forms/2 output buffering on stdout (mitigated by -noshell and explicit init:stop()).

Phase 2. Primitives and control flow

FieldValue
StatusNOT STARTED
Commit
GateArithmetic + control-flow suite (~30 fixtures: int/float/bool ops, comparisons, if/else, while, for-in over int range, recursion) compiles and runs byte-equal vs vm3 on OTP 27
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-02-primitives

Sub-phases

#ScopeStatusCommit
2.0int (BEAM integer), float (boxed double), bool (atoms); arithmetic ops; comparison ops; short-circuit && / `via Core Erlangcase`
2.1let/var, if/else, while, return, break, continue (loops → tail-recursive helpers)NOT STARTED
2.2for x in start..end (int range); user-defined functionsNOT STARTED
2.3Integer divide-by-zero badarithmochi_err_divzeroNOT STARTED
2.4Float NaN propagation; print matches vm3's %.17g-equivalent (via io_lib_format shortest-round-trip)NOT STARTED

Test set. tests/transpiler3/beam/fixtures/phase2/*.mochi (30 cases); transpiler3/beam/build/phase02_test.go runs each and diffs vs vm3.

Risks. Float-print divergence between vm3 (Go's strconv.FormatFloat) and BEAM (io_lib:format("~p", [F])). Mitigation: emit our own shortest-round-trip via mochi_str:float_to_binary/1 if needed.

Phase 3. Collections (lists, maps, sets, omaps)

FieldValue
StatusNOT STARTED
Commit
GateCollection suite (lists, maps, sets, omaps, list[record], comprehensions) compiles and runs byte-equal vs vm3 on OTP 27
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-03-collections

Sub-phases

#ScopeStatusCommit
3.1list[T]: literal, index, len, for-each; comprehensions (`[EX <- L]`)
3.2map[K,V]: literal, index, len, keys, values, has, for-eachNOT STARTED
3.3set[T]: literal, add, has, len (over sets:set/0 v2)NOT STARTED
3.4omap[K,V]: literal, index, len, ordered iterationNOT STARTED
3.5list[record]: combinations and comprehensionsNOT STARTED

Test set. ~90 fixtures (25 per sub-phase except 3.5 with 15). TestPhase3Lists, TestPhase3Maps, TestPhase3Sets, TestPhase3Omaps, TestPhase3ListOfRecord.

Risks. OTP 26 maps have a different internal layout than OTP 27+ HAMT for >32 keys; behavior is unchanged but performance differs. We target OTP 27+ so this is a non-issue.

Phase 4. Records

FieldValue
StatusNOT STARTED
Commit
GateRecords suite (literal, field access, update, methods, equality) compiles and runs byte-equal vs vm3 on OTP 27
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-04-records

Sub-phases

#ScopeStatusCommit
4.0Record literal Person{name: "a", age: 30} → tagged map #{'__mochi_record__' => 'Person', name => <<"a"/utf8>>, age => 30}NOT STARTED
4.1Field access (p.name) → maps:get(name, V_p); field update (p with {age: 31}) → V_p#{age => 31}NOT STARTED
4.2Methods on records (no self mutation, returns new record)NOT STARTED
4.3Record equality (=:= via tagged-map structural equality)NOT STARTED

Test set. 25 fixtures. TestPhase4Records.

Phase 5. Sum types and pattern matching

FieldValue
StatusNOT STARTED
Commit
GateSum types suite (variants, pattern match, exhaustiveness) compiles byte-equal vs vm3; ~25 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-05-sums

Sub-phases

#ScopeStatusCommit
5.0Variant constructors → tagged tuples ({some, V}, none); pattern match → c_caseNOT STARTED
5.1Nested patterns; guard clauses → c_case with guardsNOT STARTED
5.2Mochi option[T] lowering to {some, V} / none (matches Erlang idiom)NOT STARTED
5.3Mochi Result[T, E] lowering to {ok, V} / {error, E}NOT STARTED

Test set. 25 fixtures. TestPhase5Sums.

Phase 6. Closures and higher-order functions

FieldValue
StatusNOT STARTED
Commit
GateClosures suite (anonymous functions, higher-order, partial application, recursion through closures) compiles byte-equal vs vm3
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-06-closures

Sub-phases

#ScopeStatusCommit
6.0Anonymous functions → BEAM funs (c_fun)NOT STARTED
6.1lists:map/2, lists:filter/2, lists:foldl/3 mappingNOT STARTED
6.2Partial application via Mochi sugar → BEAM fun with captured envNOT STARTED

Test set. 25 fixtures. TestPhase6Closures.

Phase 7. Query DSL

FieldValue
StatusNOT STARTED
Commit
GateQuery DSL suite (from/where/select, group_by, order_by, joins, take/skip) compiles byte-equal vs vm3; ~30 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-07-query

Sub-phases

#ScopeStatusCommit
7.0from x in L where p select e → list comprehensionNOT STARTED
7.1Aggregations (sum, count, max, min, avg) → lists:foldl/3 fusionNOT STARTED
7.2group_bymaps:update_with/4 foldl with map accumulatorNOT STARTED
7.3Hash join via maps:from_list/1 indexNOT STARTED
7.4order_bylists:sort/2; take K after sort → lists:sublist/2; top-K fusion for small KNOT STARTED

Test set. 30 fixtures. TestPhase7Query. See [[08-dataset-pipeline]].

Phase 8. Datalog

FieldValue
StatusNOT STARTED
Commit
GateDatalog suite (facts, rules, recursion, transitive closure) compiles byte-equal vs vm3; 20 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-08-datalog

Sub-phases

#ScopeStatusCommit
8.0fact declarations → ETS table per relationNOT STARTED
8.1rule declarations → mochi_datalog semi-naive evaluatorNOT STARTED
8.2Recursive rules; fixpoint terminationNOT STARTED
8.3Query expressions over facts and rulesNOT STARTED

Test set. 20 fixtures. TestPhase8Datalog.

Phase 9. Agents and gen_server

FieldValue
StatusNOT STARTED
Commit
GateAgent suite (definitions, spawn, call/cast methods, supervised, on_close) compiles byte-equal vs vm3; 25 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-09-agents

Sub-phases

#ScopeStatusCommit
9.0agent T { fields; methods } → gen_server callback moduleNOT STARTED
9.1spawn T(args)mochi_agent_sup:start_child/2; returns opaque agent refNOT STARTED
9.2a.method(x) (returns value) → gen_server:call/2; a.tell(x)gen_server:cast/2NOT STARTED
9.3on_close block → terminate/2 callbackNOT STARTED
9.4Supervised crash + restart (transient policy)NOT STARTED

Test set. 25 fixtures. TestPhase9Agents. See [[09-agent-streams]].

Phase 10. Streams and pubsub

FieldValue
StatusNOT STARTED
Commit
GateStream suite (declaration, publish, subscribe, windowed) compiles byte-equal vs vm3; 20 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-10-streams

Sub-phases

#ScopeStatusCommit
10.0stream s declaration; publish s mmochi_stream:publish/3 (pg-backed)NOT STARTED
10.1subscribe e in s { ... }mochi_stream_filter gen_statemNOT STARTED
10.2Subscriber backpressure: limit N drops when mailbox fullNOT STARTED
10.3Cross-node streams (free via pg); test on a 2-node distributed setupNOT STARTED

Test set. 20 fixtures. TestPhase10Streams.

Phase 11. async/await

FieldValue
StatusNOT STARTED
Commit
Gateasync/await suite (futures, await, await_all, await_timeout) compiles byte-equal vs vm3; 15 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-11-async

Sub-phases

#ScopeStatusCommit
11.0async exprmochi_async:async/1 (monitored spawn)NOT STARTED
11.1await fut → selective receive on fresh ref (uses recv-marker optimization)NOT STARTED
11.2await_all, await_any, await_timeout combinatorsNOT STARTED

Test set. 15 fixtures. TestPhase11Async.

Phase 12. FFI

FieldValue
StatusNOT STARTED
Commit
GateFFI suite (extern Erlang module declarations, calls into OTP stdlib, calls into Hex deps) compiles byte-equal vs vm3; 15 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-12-ffi

Sub-phases

#ScopeStatusCommit
12.0extern "Erlang" mod <name> { fn(args): T } declarationsNOT STARTED
12.1Marshalling: Mochi types ↔ Erlang types (binary/atom/tuple/list/map)NOT STARTED
12.2Hex.pm dep declarations in mochi.tomlrebar.config entriesNOT STARTED

Test set. 15 fixtures. TestPhase12FFI.

Phase 13. LLM (generate)

FieldValue
StatusNOT STARTED
Commit
GateLLM suite (generate blocks, structured outputs) compiles byte-equal vs vm3 with mocked provider; 10 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-13-llm

Sub-phases

#ScopeStatusCommit
13.0mochi_llm provider supervisor; OpenAI and Anthropic provider modulesNOT STARTED
13.1generate { prompt: ..., schema: ... }mochi_llm:generate/2NOT STARTED
13.2Replay-cassette test mode (no live provider calls in CI)NOT STARTED

Test set. 10 fixtures. TestPhase13LLM.

Phase 14. fetch (HTTP)

FieldValue
StatusNOT STARTED
Commit
Gatefetch suite (GET, POST, JSON parse, headers, status codes) compiles byte-equal vs vm3 against local test server; 10 fixtures
TargetsOTP 27 on host triple
Tracking/docs/implementation/0046/phase-14-fetch

Sub-phases

#ScopeStatusCommit
14.0mochi_fetch wraps gun for HTTP/1.1 and HTTP/2NOT STARTED
14.1TLS via ssl (OTP 27 TLS 1.3 default)NOT STARTED
14.2JSON parse via stdlib json (OTP 27)NOT STARTED

Test set. 10 fixtures. TestPhase14Fetch.

Phase 15. Release packaging

FieldValue
StatusNOT STARTED
Commit
Gatemochi build --target=beam-release produces a tarball; the unpacked release boots, exits cleanly, and produces byte-equal stdout vs vm3 on the Phase 1-14 fixtures; reproducible bit-for-bit across two CI hosts
TargetsOTP 27 on x86_64-linux-gnu, aarch64-darwin
Tracking/docs/implementation/0046/phase-15-release

Sub-phases

#ScopeStatusCommit
15.0--target=beam-release via relx (bundled in rebar3)NOT STARTED
15.1--target=beam-rebar3-project emits a standalone rebar3 projectNOT STARTED
15.2--target=beam-mix-project emits a standalone mix projectNOT STARTED
15.3--target=beam-atomvm emits .avm bundle for AtomVMNOT STARTED
15.4Docker recipe mochilang/mochi:beam-otp27 publishedNOT STARTED

Test set. Re-run Phase 1-14 fixtures against release artifacts. TestPhase15ReleaseRoundtrip. AtomVM smoke tests on Phase 1-5 subset.

Phase 16. Multi-OTP-version matrix

FieldValue
StatusNOT STARTED
Commit
GatePhase 1-14 gates pass on OTP 27.0, OTP 27.latest, OTP 28.latest on x86_64-linux-gnu and aarch64-darwin
TargetsOTP 27.0, 27.latest, 28.latest × Linux x86-64, macOS arm64
Tracking/docs/implementation/0046/phase-16-otp-matrix

Sub-phases

#ScopeStatusCommit
16.0CI workflow runs full corpus on the OTP × arch matrix (4 cells)NOT STARTED
16.1OTP 29 RC added as non-blocking nightly when availableNOT STARTED
16.2Windows x86-64 (OTP 27) runs nightly best-effortNOT STARTED

Test set. Full corpus on all matrix cells.

Phase 17. Dialyzer cleanliness

FieldValue
StatusNOT STARTED
Commit
Gaterebar3 dialyzer -Werror reports zero warnings on all Phase 1-14 fixtures' generated rebar3 projects
TargetsOTP 27
Tracking/docs/implementation/0046/phase-17-dialyzer

Sub-phases

#ScopeStatusCommit
17.0Emit -spec for every exported Mochi functionNOT STARTED
17.1Emit -opaque for agent/stream refsNOT STARTED
17.2CI job runs rebar3 dialyzer on every fixture projectNOT STARTED
17.3False-positive allowlist documented in dialyzer_allowlist.txt with rationaleNOT STARTED

Risks. Dialyzer over-conservative on some opaque types; documented allowlist resolves these.

Phase 18. Reproducibility and perf

FieldValue
StatusNOT STARTED
Commit
GateBit-identical .beam files across two CI hosts; median fixture wall-clock within 3x of the C target on the BG corpus (BEAM is the concurrency target, not the perf target)
TargetsOTP 27
Tracking/docs/implementation/0046/phase-18-repro-perf

Sub-phases

#ScopeStatusCommit
18.0Strip timestamp from CInf chunk; relative source pathsNOT STARTED
18.1Sort exported functions by canonical IR identifierNOT STARTED
18.2.github/workflows/transpiler3-beam-repro.yml rebuilds and diffs SHA-256NOT STARTED
18.3Benchmark harness with BG kernels; per-release reportNOT STARTED

Phase 19. v1.0 release

FieldValue
StatusNOT STARTED
Commit
Gatemochi build --target=beam-* ships with all of Phases 1-18 green; user-facing docs/manual/build-beam.md page documents the build flow; release notes filed; mochi published to Hex.pm
TargetsAll Tier 1
Tracking/docs/implementation/0046/phase-19-release

Sub-phases

#ScopeStatusCommit
19.0docs/manual/build-beam.md written; CLI help text matchesNOT STARTED
19.1Release notes + changelog entryNOT STARTED
19.2mochi runtime published to Hex.pm under Apache-2.0NOT STARTED
19.3MEP-46 status flipped to FinalNOT STARTED

Open Questions

  1. AtomVM tier. Tier 2 or Tier 1? Recommend Tier 2 for v0.1; reassess after AtomVM 0.7 release.
  2. Phoenix.PubSub vs pg. Plain pg is sufficient; users can drop in Phoenix.PubSub via FFI if needed. Confirm during Phase 10.
  3. Hot reload as first-class. Currently a side-effect; should it be a Mochi keyword? Recommend not for v0.1; full-node restarts are the modern default.
  4. eqWAlizer gate. Add as a non-blocking gate in Phase 17? Recommend yes after gauging eqWAlizer maturity.
  5. mix release vs relx. Both produce releases; we use relx for the canonical path (rebar3 native). Mix is via the mix-project target only.
  6. Distribution as first-class. Currently a free side-effect via pg cluster-awareness; no Mochi syntax for it. Recommend keeping it that way.
  7. LLM CI mode. Replay cassettes (matches MEP-45 §Open Questions 7).
  8. Performance gate target. "3x of C target" or tighter? Adjust after measurement.
  9. Vendor the runtime by default. --vendor-runtime exists; should it be default? Recommend yes for escript targets, no for rebar3-project/mix-project targets.
  10. OTP 26 backport. Recommend no. OTP 27 is the floor.

References

Research notes (this MEP)

Twelve notes under ~/notes/Spec/0046/:

#Title
01Language surface
02Design philosophy
03Prior-art transpilers (BEAM ecosystem)
04Runtime building blocks (OTP services)
05Codegen design (Core Erlang via cerl)
06Type-system lowering
07Erlang target and portability
08Dataset pipeline lowering
09Streams and agents
10Build system (rebar3, mix, escript, release)
11Testing and CI gates
12Risks and alternatives

Standards

  • Erlang/OTP 27 (May 2024)
  • Erlang/OTP 28 (May 2025)
  • Core Erlang 1.0.3 (Uppsala IT Tech Report 2004-018)
  • BEAM file format (ERTS internal_doc/beam_makeops.md)
  • DWARF 5 (for Dbgi chunk mapping)
  • Unicode 15.1
  • TLS 1.3 (RFC 8446)

Papers and talks

  • Carlsson, Gustavsson, et al., "Core Erlang 1.0.3 Language Specification" (Uppsala 2004-018, 2004).
  • Lukas Larsson, "Inside BeamAsm" (Code BEAM SF 2021).
  • Björn Gustavsson, "The Compiler Pipeline in OTP 26" (Code BEAM 2023).
  • Sverker Eriksson, "Maps in OTP 27" (Code BEAM 2024).
  • Robert Virding, "Why I still write LFE" (Code BEAM 2024).
  • José Valim, "Set-Theoretic Types for Elixir" (Lambda Days 2024).
  • Louis Pilfold, "Gleam 1.0 retrospective" (Code BEAM EU 2024).
  • Castagna et al., "Programming with union, intersection, and negation types" (POPL 2023, doi 10.1145/3571238).
  • Maranget, "Compiling Pattern Matching to Good Decision Trees" (ML Workshop 2008).
  • Ullman, "Principles of Database and Knowledge-Base Systems Vol. 1" (Computer Science Press, 1989).
  • Bancilhon & Ramakrishnan, "An Amateur's Introduction to Recursive Query Processing Strategies" (SIGMOD 1986).

Libraries

OTP (erlang/otp), rebar3 (erlang/rebar3), relx (built into rebar3), gun (ninenines/gun), cowboy (ninenines/cowboy), telemetry (beam-telemetry/telemetry), Dialyzer (in OTP), eqWAlizer (WhatsApp/eqwalizer), PropEr (proper-testing/proper), AtomVM (atomvm/AtomVM), Rustler (rusterlium/rustler, not used in v0.1 but documented as escape hatch), asmjit (used by BeamAsm), recon (ferd/recon), ex_doc (elixir-lang/ex_doc).

Comparable transpilers studied

LFE, Gleam, Elixir, Hamler, Alpaca, Joxa, Clojerl, Caramel, Purerl, Erlog, Luerl, Efene, Reia. See note 03.

Project context

This document is placed in the public domain.