Skip to main content

MEP 53. Mochi-to-Rust transpiler

FieldValue
MEP53
TitleMochi-to-Rust transpiler
AuthorMochi core
StatusActive
TypeStandards Track
Created2026-05-29 14:55 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse), MEP-46 (BEAM transpiler, IR reuse), MEP-47 (JVM transpiler, IR reuse), MEP-48 (.NET transpiler, IR reuse), MEP-56 (Ruby transpiler, multi-backend pattern)
Research/docs/research/0053/
Tracking/docs/implementation/0053/

Abstract

Mochi today ships vm3 (mochi run), a C transpiler producing native single-file binaries (MEP-45), a BEAM transpiler producing supervised concurrent runtimes (MEP-46), a JVM transpiler producing Maven-Central-interoperable jars (MEP-47), a .NET transpiler producing NuGet-interoperable assemblies (MEP-48), a Swift transpiler producing Apple-ecosystem binaries (MEP-49), a Kotlin transpiler producing JVM and Android binaries (MEP-50), a Python transpiler (MEP-51), a TypeScript / JavaScript transpiler (MEP-52), and a Ruby transpiler (MEP-56). None target the Rust ecosystem directly: 140,000+ crates on crates.io, the most popular non-C systems language since 2016, the canonical 2024-2026 target for WASI Preview 2 and Wasm Component Model, the canonical embedded systems language (Embedded Rust working group, Drogue IoT, Tock OS), and the dominant language in modern systems programming research. MEP-53 specifies a tenth transpiler pipeline that lowers a type-checked Mochi program to Rust source emitted via a structural rtree AST, then drives cargo to produce a statically linked native binary, a publishable crate, a wasm32-wasip1 module, or a no_std + alloc embedded build.

The pipeline reuses MEP-45's typed-AST and aotir IR, plus the monomorphisation, match-to-decision-tree, and closure-conversion passes shared with MEP-46, MEP-47, MEP-48, and MEP-56. It forks at the emit stage: instead of emitting ISO C23 (MEP-45), Core Erlang via cerl (MEP-46), Java source via JavaPoet (MEP-47), C# source via Roslyn (MEP-48), Swift source via SwiftSyntax (MEP-49), Kotlin source via KotlinPoet (MEP-50), Python source (MEP-51), TypeScript source (MEP-52), or Ruby source via the Ruby rtree (MEP-56), it emits Rust source as rtree.SourceFile trees, then renders to disk with 4-space indent and stable item ordering.

Five packaging targets ship together: TargetNativeExecutable (default for mochi build --lang=rust) produces a cargo-built release binary on the host triple; TargetRustSource writes the rendered .rs plus runtime references without invoking cargo; TargetLinuxStaticX64 and TargetLinuxStaticArm64 produce statically linked musl binaries via cargo zigbuild; TargetWasm32WASI produces a .wasm module runnable under wasmtime via the wasm32-wasip1 target (Rust 1.78+ renamed wasm32-wasi to wasm32-wasip1); TargetRustCrate exports the full Cargo crate (Cargo.toml + src/ + sidecar cffi/ + build.rs) into the output directory without invoking cargo, used for publish-ready crate inspection.

The intended master correctness gate is byte-equal stdout from the produced binary versus vm3 on the entire fixture corpus, across stable Rust 1.95 (the active stable as of 2026-05) on aarch64-apple-darwin (host), cargo-zigbuild on x86_64-unknown-linux-musl and aarch64-unknown-linux-musl, and wasm32-wasip1 under wasmtime 26. As of 2026-05-29 every phase is LANDED on the host triple; cross-architecture and wasm coverage is asserted by phases 17 (wasm) and the cross-build flags inside phases 0-16; the embedded cargo check gate is asserted by phase 18. See /docs/implementation/0053/ for the live status. vm3 is the recording oracle for expect.txt; the transpiler does not link against or depend on vm3.

Five load-bearing decisions:

  1. Rust source via a structural rtree AST, not raw string emission. The default emit path constructs rtree.SourceFile trees, then calls SourceFile.RustSource() to render. The structural representation gives free indentation handling, avoids whitespace bugs (a class of issue that plagues string-template-based emitters), supports peephole optimisation passes (notably the colour pass that decides where clone() is required), and produces debuggable output that a Rustacean can read line-by-line. #![allow(unused, non_snake_case, non_camel_case_types)] is emitted on every file so generated names that don't match Rust naming convention (e.g., synthesised parameter slots __a0, __a1) compile without warnings.

  2. Single-thread runtime (Rc<RefCell<...>>), not Arc<Mutex<...>>. Mochi channels lower to Rc<RefCell<VecDeque<T>>>, streams to Rc<RefCell<Vec<Rc<RefCell<VecDeque<T>>>>>>. There is no use of std::sync::{Arc, Mutex, RwLock} in the runtime or emitted code. Rationale: Mochi's async lowers to immediate evaluation on the same thread under the Rust target (no green-thread runtime, no tokio); concurrency primitives (Thread::new, OS threads, work-stealing pools) are explicitly out of scope. This keeps the runtime small (under 800 LOC), reproducibly compilable on no_std + alloc once the std-requiring modules are gated off, and avoids forcing the user to pay for a multi-thread synchronisation tax they did not opt into. Users who need real OS threads can call into std::thread directly via FFI.

  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 nine backends. The fork is at the emit pass: transpiler3/rust/lower/ lowers aotir to Rust-source structural nodes. Sharing the IR keeps the targets semantically aligned and amortises pass-implementation work. Rust-specific lowerings (e.g., Box<dyn Fn(...)> for closures, Rc<RefCell<...>> for shared mutable state, Clone derivation for sum-type variants) are localised to the Rust lower pass.

  4. Reproducible builds via SOURCE_DATE_EPOCH=0 + RUSTFLAGS=-C strip=symbols. Identical inputs produce byte-identical outputs across Driver.Build invocations. Cargo respects SOURCE_DATE_EPOCH for build timestamps; RUSTFLAGS=-C strip=symbols strips symbol tables that carry path-dependent timestamps and build IDs. On macOS the gate is platform-skipped: the Mach-O LC_UUID load command is randomised per link by ld64 and cannot be neutralised from rustc-side flags. The Linux gate is enforced.

  5. Stdlib as fat runtime; mochi-runtime as thin runtime. The mochi-runtime crate provides only what stdlib does not: io::print_{str,i64,f64,bool} (deterministic float formatting), conv::{int_to_float,float_to_int,str_to_int,int_to_str}, strings::{len,index,contains,cat,substring,reverse} (UTF-8 char-aware, alloc-only), chan::Chan (single-thread queue), stream::Stream + subscribe + subscribe_limit (single-thread broadcast), panic::{raise,catch,silence_hook} (panic-as-error-code over panic::catch_unwind), fetch::get (HTTP/1.1 GET over std::net::TcpStream), json::decode (90-LOC object decoder returning HashMap<String, String>), llm::call (cassette replay over MOCHI_LLM_CASSETTE_DIR), and check::{div_i64,mod_i64,list_index} (runtime-checked arithmetic and indexing). Everything else (file I/O via std::fs, time via std::time, env via std::env, format via the std::fmt machinery) goes through stdlib directly. tokio, reqwest, serde, serde_json, and sha2 are explicitly rejected as runtime deps; the embedded feature gates io / chan / stream / panic / fetch / json / llm / check behind #[cfg(feature = "std")] so the crate compiles under no_std + alloc on bare-metal targets.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/rust/fixtures/ must compile via the Rust pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. cargo check clean on emitted code is the secondary gate. cargo zigbuild clean producing a static musl binary (for the Linux targets), wasmtime run clean producing matching stdout (for the wasm target), cargo publish --dry-run --no-verify --allow-dirty clean (for the publish gate, opt-in under MOCHI_RUN_PUBLISH_DRYRUN=1), SHA-256-byte-equal output across two builds with Deterministic=true (for the reproducibility gate), and cargo check --no-default-features --features embedded clean (for the embedded gate) are the tertiary gates.

Motivation

Mochi today targets vm3 (mochi run), the C target (MEP-45), the BEAM target (MEP-46), the JVM target (MEP-47), the .NET target (MEP-48), the Swift target (MEP-49), the Kotlin target (MEP-50), the Python target (MEP-51), the TypeScript target (MEP-52), and the Ruby target (MEP-56). None deliver what Rust uniquely provides:

  1. crates.io and the Rust ecosystem. As of 2026-05, crates.io hosts 140,000+ crates spanning systems (tokio, rayon, hyper, axum, salvo), embedded (embassy, rtic, hal-stm32f4, esp-idf-svc), webassembly (wasm-bindgen, walrus, wasm-tools), CLI (clap, console, dialoguer), parsing (nom, winnow, chumsky), serialisation (serde, postcard, rkyv), DB (sqlx, sea-orm, diesel), GUI (egui, iced, druid, slint), GPU (wgpu, naga, ash), and AI / ML (candle, burn, smartcore). A Mochi program needing zero-copy parsing of a custom binary format, GPU-accelerated linear algebra, or embedded RTOS scheduling can import an existing crate.

  2. WebAssembly and WASI Preview 2. Rust is the canonical source language for WASI components (April 2024 GA), the Wasm Component Model, and the Bytecode Alliance toolchain (cargo component, wit-bindgen). The wasm32-wasip1 target shipped in Rust 1.78 (May 2024, renamed from wasm32-wasi) and is stable as of 2026-05. Mochi-on-Rust drops into any wasmtime / wasmer / WasmEdge host with no extra glue.

  3. Embedded and no_std. The Rust embedded working group has reached production maturity (Tock OS, Drogue IoT, embassy 1.0 in May 2025, RTIC 2.0 in October 2024). #![no_std] + extern crate alloc; lets a single library compile against bare-metal targets where libc / fs / net are unavailable. MEP-53's embedded feature gates the std-requiring runtime modules so the conv + strings subset of the Mochi runtime compiles into any embedded program.

  4. Cargo as a packaging system. Cargo provides build, test, doc, publish, and dep resolution in one tool, with reproducible builds (since 1.65 via --locked and Cargo.lock plus SOURCE_DATE_EPOCH), a deeply integrated workspace model, and direct crates.io integration via cargo publish. MEP-53's TargetRustCrate produces a publish-ready crate so a Mochi program can be uploaded to crates.io and consumed by Rust users via cargo add mochi-emitted-crate.

  5. Reproducible binaries. The Rust toolchain is the leading 2026 implementation of bit-reproducible release binaries (see reproducible-builds.org's 2025 status report; Rust 1.84 GA'd reproducible-by-default flags). MEP-53 wires SOURCE_DATE_EPOCH=0 + RUSTFLAGS=-C strip=symbols + cargo build --locked into Driver.Build so the same input produces the same SHA-256 across machines and time.

  6. Static linking by default. Cargo's release profile with cargo zigbuild --target x86_64-unknown-linux-musl produces statically linked single-file binaries that run on any musl-or-glibc Linux without ABI dance. This matches the C target's distribution shape (MEP-45) but without manually picking a libc.

  7. FFI without the FFI tax. Rust's extern "C" plus cc build script integration provides clean, statically linked FFI with no runtime cost. MEP-53's cffi/ sidecar compiles user-supplied C alongside the emitted Rust crate.

The C target remains the right choice for minimal binary size and bare-metal targets without alloc. The BEAM target remains the right choice for hot-reload services and OTP supervision. The JVM target remains the right choice for Maven Central interop and Android. The .NET target remains the right choice for NuGet interop and Windows enterprise. The Swift target remains the right choice for the Apple ecosystem. The Kotlin target remains the right choice for Android and Kotlin Multiplatform. The Python target remains the right choice for PyPI and data-science workflows. The TypeScript target remains the right choice for npm and web. The Ruby target remains the right choice for RubyGems and Rails. The Rust target is the right choice for crates.io interop, WebAssembly distribution, embedded systems work, reproducibly-built binaries, and static-musl Linux deployments.

Specification

This section is normative.

1. Pipeline and IR reuse

The Rust pipeline reuses MEP-45's aotir IR. The emit stage forks: transpiler3/rust/lower/Lower(prog, fileBase, moduleName) consumes *aotir.Program and returns *rtree.SourceFile. transpiler3/rust/colour/Colour(sf) runs a borrow-and-clone colouring pass that decides where .clone() is inserted for shared ownership. transpiler3/rust/emit/Emit(sf, workDir) writes the rendered source to disk. The driver at transpiler3/rust/build/Driver.Build(src, out, target) glues parse → typecheck → clower.Lower → rust/lower.Lower → colour → emit → cargo build.

2. Toolchain detection

build.resolveCargo() resolves cargo in order:

  1. $MOCHI_CARGO (env override)
  2. ~/.cargo/bin/cargo (rustup default install)
  3. exec.LookPath("cargo")

The minimum supported Rust version is 1.78 (wasm32-wasip1 rename); the production gate runs on Rust 1.95 (stable as of 2026-05). For cross builds, cargo zigbuild is required (gated by exec.LookPath("cargo-zigbuild")) and wasmtime is required for the wasm gate.

3. Surface-syntax lowering

Mochi constructRust lowering
let x = elet x = e;
var x = elet mut x = e;
if/elif/elseif cond { ... } else if cond { ... } else { ... }
while c { ... }while c { ... }
for i in lo..hi { ... }for i in lo..hi { ... } (Rust exclusive range matches Mochi)
for x in xs { ... }for x in xs.iter().cloned() { ... } (clone gated by colour pass)
fun f(a: T): U { ... }fn f(a: T) -> U { ... } (free function, module-level)
let f = fun(a: T): U => bodylet f: Box<dyn Fn(T) -> U> = Box::new(move |a| { body }); with explicit move-capture clause computed by the closure-conversion pass
match e { Variant(x) => arm }match e { UnionName::Variant(x) => { arm } }
type T = A | B#[derive(Clone, Debug, PartialEq)] enum T { A(f1, ...), B(f1, ...) }
record User { id: int }#[derive(Clone, Debug, PartialEq, Default)] struct User { id: i64 }
type Pair = { a: int, b: int } (anonymous record)identical to record form, both lower through lowerStruct
Stream s = make_stream(N)let s = mochi_runtime::stream::Stream::make(N);
subscribe(s)mochi_runtime::stream::subscribe(&s) (returns Sub<T>)
subscribe_limit(s, N)mochi_runtime::stream::subscribe_limit(&s, N) (currently unbounded; symbol reserved)
emit(s, v)s.emit(v);
recv_sub(sub)sub.recv()
Channel make_chan(N)mochi_runtime::chan::Chan::make(N)
chan <- vchan.send(v);
<- chanchan.recv()
agent A { state ... on Msg ... }#[derive(Clone)] struct A { field: T, ... } impl A { fn new() -> Self { Self::default() } fn intent(&mut self, ...) { ... } }
spawn AgentType()AgentType::new() (immediate value; no thread)
a.intent(arg)a.intent(arg)
async exprexpr (immediate evaluation; no thread, no future)
await futfut (identity; async colouring is a typecheck-time pass with no runtime effect)
try { ... } catch e { ... }match mochi_runtime::panic::catch(|| { ... }) { Some(__code) => { let e = __code; ... }, None => {} }
panic(code)mochi_runtime::panic::raise(code)
break / continuebreak / continue
from x in xs where p select edesugared by clower into iterator chain; final shape is xs.iter().filter(|x| p).map(|x| e).collect::<Vec<_>>() (colour pass decides whether iter or into_iter)
... order by k skip s take t.sorted_by_key(|x| k).skip(s).take(t)
Datalog query parent(_, Y)evaluated at compile-time via semi-naive fixpoint in transpiler3/rust/lower/datalog.go; emitted as a frozen Vec literal of pre-computed result tuples
min(xs) / max(xs) / sum(xs).iter().min().unwrap() / .max().unwrap() / .sum()
in(x, xs)xs.contains(&x)
map(xs, f) / filter(xs, p) / reduce(xs, acc, f)xs.iter().map(|x| f(x.clone())).collect() / .filter(|x| p(x.clone())) / .fold(acc, |a, x| f(a, x.clone()))
len(xs) / len(s) / len(m) / len(set).len() as i64 (Vec, String char count via mochi_runtime::strings::len, HashMap, HashSet)
keys(m) / values(m)m.keys().cloned().collect::<Vec<_>>() / m.values().cloned().collect::<Vec<_>>()
append(xs, v){ let mut __t = xs.clone(); __t.push(v); __t }
slice(xs, lo, hi) / xs[lo:hi]xs.get(lo..hi).unwrap_or(&[]).to_vec()
sort(xs){ let mut __t = xs.clone(); __t.sort(); __t }
abs(n) / floor(f) / ceil(f)n.abs() / f.floor() / f.ceil()
str(v)v.to_string() (via mochi_runtime::conv::int_to_str for i64)
upper(s) / lower(s)s.to_uppercase() / s.to_lowercase()
index(s, sub) / contains(s, sub)s.find(sub).map(|i| i as i64).unwrap_or(-1) / mochi_runtime::strings::contains(s, sub)
substring(s, lo, hi)mochi_runtime::strings::substring(s, lo, hi) (char-aware)
reverse(s) / split(s, sep) / join(xs, sep)mochi_runtime::strings::reverse(s) / s.split(sep).map(String::from).collect::<Vec<_>>() / xs.join(sep)
Set literal set{1, 2} / add(s, x) / has(s, x)HashSet::from([1, 2]) / { let mut __t = s.clone(); __t.insert(x); __t } / s.contains(&x)
Map literal / m[k] / has(m, k)HashMap::from([(k, v), ...]) / m.get(&k).cloned().unwrap_or_default() / m.contains_key(&k)
readFile(p)std::fs::read_to_string(p).unwrap_or_default()
lines(p)std::fs::read_to_string(p).unwrap_or_default().lines().map(String::from).collect::<Vec<_>>()
writeFile(p, s) / appendFile(p, s)std::fs::write(p, s) / opened with OpenOptions::new().append(true)
json_decode(s)mochi_runtime::json::decode(s) (returns HashMap<String, String>)
fetch <url> / httpGet(url)mochi_runtime::fetch::get(url) (HTTP/1.1 over plain TcpStream; no TLS)
generate <provider> { prompt: p, model: m }mochi_runtime::llm::call(provider, prompt) (cassette replay keyed by SHA-256 of provider:prompt)
(int)f / int(x)mochi_runtime::conv::float_to_int(f) (truncates toward zero)
a / b integer dividemochi_runtime::check::div_i64(a, b) (panics with code 5 on zero)
a % b integer modulomochi_runtime::check::mod_i64(a, b)
xs[i] indexingmochi_runtime::check::list_index(&xs, i) (panics with code 4 on out-of-range)
!b boolean negation!b
Bareword identifier collisions (fn, let, mut, mod, ...)suffix with _ (e.g., fn_) to avoid Rust keyword clash

4. Runtime crate

mochi-runtime (Apache-2.0, ~700 LOC of Rust across src/lib.rs only — single-file crate) exports:

  • mochi_runtime::io::{print_str, print_i64, print_f64, print_bool} (deterministic float formatting: NaN, ±Inf, integer-valued floats render as the integer, otherwise default Display)
  • mochi_runtime::conv::{int_to_float, float_to_int, str_to_int, int_to_str} (alloc-only, no_std-compatible)
  • mochi_runtime::strings::{len, index, contains, cat, substring, reverse} (UTF-8 char-aware, alloc-only)
  • mochi_runtime::chan::Chan<T> with make(cap) / send(v) / recv() over Rc<RefCell<VecDeque<T>>>
  • mochi_runtime::stream::Stream<T> with make(cap) / emit(v) over Rc<RefCell<Vec<Rc<RefCell<VecDeque<T>>>>>>; subscribe(&s) -> Sub<T>; subscribe_limit(&s, _) -> Sub<T> (currently unbounded)
  • mochi_runtime::panic::{raise(code: i64) -> !, catch(F) -> Option<i64>, silence_hook()} (panic-as-error-code via panic::catch_unwind; silence_hook suppresses the default panic stderr message exactly once via Once)
  • mochi_runtime::fetch::get(url) -> String (HTTP/1.1 GET over std::net::TcpStream; supports plain http:// URLs only, no TLS; status >= 400 raises panic code 98; supports Transfer-Encoding: chunked)
  • mochi_runtime::json::decode(input) -> HashMap<String, String> (top-level object, non-string values coerced to their string representation, null becomes "", panic code 97 on malformed input)
  • mochi_runtime::llm::call(provider, prompt) -> String (cassette replay over $MOCHI_LLM_CASSETTE_DIR/{sha256_hex(provider:prompt)}.txt, panic code 99 on missing cassette or env var)
  • mochi_runtime::check::{div_i64(a, b), mod_i64(a, b), list_index(&xs, i)} (runtime-checked arithmetic and indexing; raises panic code 5 on division by zero, code 4 on out-of-range index)

The embedded feature gates io, chan, stream, panic, fetch, json, llm, and check behind #[cfg(feature = "std")], exposing only conv and strings (alloc-only). Default features include std so non-embedded users see no change.

5. Build targets

TargetSwitch caseOutput layout
TargetNativeExecutablerust<out>/<name> (cargo-built release binary)
TargetRustSourcerust-source<workdir>/src/main.rs (emit only, no cargo)
TargetLinuxStaticX64rust-linux-x64<out>/<name> (cargo zigbuild --target x86_64-unknown-linux-musl)
TargetLinuxStaticArm64rust-linux-arm64<out>/<name> (cargo zigbuild --target aarch64-unknown-linux-musl)
TargetWasm32WASIrust-wasm32-wasip1<out>/<name>.wasm (cargo build --target wasm32-wasip1)
TargetRustCraterust-crate<out>/Cargo.toml + <out>/src/ + <out>/cffi/ + <out>/build.rs (no cargo invocation; for publish-ready inspection)

Build flags include --locked (with Cargo.lock in the crate) and, when Driver.Deterministic=true, SOURCE_DATE_EPOCH=0 plus RUSTFLAGS=-C strip=symbols. Cache directory defaults to ~/.cache/mochi/rust/ and is content-addressed by SHA-256 of the workspace path; Driver.NoCache=true skips the cache.

Phases

See /docs/implementation/0053/ for the per-phase tracking matrix. Nineteen phases cover skeleton (0), language surface (1-11), advanced runtime (12-14), and packaging / reproducibility / cross-platform (15-18).

A phase is LANDED only when its gate is green on every Rust runtime listed for it in §6 below.

6. Target matrix

The matrix below is the intended correctness gate, not the current CI matrix. As of 2026-05-29, every phase is LANDED on stable Rust 1.95 (aarch64-apple-darwin, local). Cross-architecture coverage (cargo-zigbuild musl x64 / arm64) is exercised in the build path for phase 15 (publish) but not gated per phase in CI; that lands under sub-phase 16.1. wasm32-wasip1 is gated by phase 17. Embedded (cargo check --no-default-features --features embedded) is gated by phase 18. JRuby-equivalent alternative implementations (Polonius, gccrs, mrustc) are out of scope. See /docs/implementation/0053/ for the live runtime-matrix status.

Phase scopeRust 1.95 darwin-arm64musl x64musl arm64wasm32-wasip1embedded (no_std + alloc)
Scalars / arithmeticLANDED (local)required (16.1)required (16.1)LANDED (phase 17)LANDED (phase 18 via conv/strings)
Lists / maps / setsLANDED (local)requiredrequiredLANDED (phase 17)n/a (HashMap requires std)
Records / sums (structs/enums)LANDED (local)requiredrequiredLANDED (phase 17)n/a (no Debug/Default without std)
Closures (Box<dyn Fn>)LANDED (local)requiredrequiredLANDED (phase 17)n/a (Box requires alloc; dyn requires unstable for no_std)
Queries / DatalogLANDED (local)requiredrequiredLANDED (phase 17)n/a
Channels (single-thread Rc)LANDED (local)requiredrequiredLANDED (phase 17)n/a (Rc requires alloc; but mochi_runtime::chan gated by std for VecDeque)
StreamsLANDED (local)requiredrequiredLANDED (phase 17)n/a
Async (immediate eval)LANDED (local)requiredrequiredLANDED (phase 17)n/a
AgentsLANDED (local)requiredrequiredLANDED (phase 17)n/a
Try / catch / panicLANDED (local)requiredrequiredLANDED (phase 17)n/a (panic::catch_unwind requires std)
FFI (cc-rs sidecar)LANDED (local)requiredrequiredn/a (no cc on wasm32)n/a
LLM (cassette)LANDED (local)requiredrequiredLANDED (phase 17)n/a
Fetch + JSONLANDED (local)requiredrequiredn/a (no TcpStream on wasm32-wasip1)n/a
Publish-ready crateLANDED (local)n/a (same artefact)n/an/an/a
ReproducibilityLANDED (linux) / skipped (darwin LC_UUID)LANDEDLANDEDLANDEDLANDED

musl x64 and musl arm64 use cargo zigbuild so the host toolchain does not need to install separate linkers. wasm32-wasip1 requires the rustup target plus wasmtime on PATH (skipped if either is missing). Embedded requires only the default rustup install (no extra target).

Toolchain handling currently auto-detects cargo via MOCHI_CARGO, ~/.cargo/bin/cargo, then PATH. cargo-zigbuild is detected via PATH only; wasmtime is detected via PATH only. Container-based detection of cross toolchains is a future sub-phase.

Alternatives considered

  1. Emit via syn (proc-macro AST) instead of an in-tree rtree. Rejected: syn is a procedural-macro IR with TokenStream-based rendering; using it from a Go program would require either a Rust-side helper binary or quote-style token emission from Go, neither of which buys anything over the structural rtree. The rtree is also reused by Ruby (MEP-56) and would be reused by future targets.
  2. Use Arc<Mutex<...>> for channels and streams to enable real OS-thread concurrency. Rejected: forces every Mochi program through a synchronisation cost that the source language does not require; Mochi's async is colouring-only and there is no thread-spawn primitive at the source level. Users who need real threads can call into std::thread via FFI.
  3. Lower closures to generic impl Fn(T) -> U instead of Box<dyn Fn(T) -> U>. Rejected: generic closure types cannot be stored as struct fields, returned from functions with multiple type-incompatible bodies, or kept in a homogeneous list. Box<dyn Fn> accepts the runtime indirection cost in exchange for surface compatibility with the source language.
  4. Use tokio for async / streams. Rejected: tokio is a heavyweight (~500K LOC across the runtime + ecosystem) async runtime; pulling it in for a transpiler whose source-language async is immediate-eval would be a substantial reverse subsidy. tokio remains available as a user-imported dep.
  5. Use reqwest for httpGet. Rejected: reqwest pulls in tokio (or async-h1), rustls or native-tls, hyper, http, mime, and ~50 transitive deps for what Mochi exposes as a one-call API. The 90-LOC hand-rolled HTTP/1.1 client in mochi_runtime::fetch keeps the runtime crate small enough to compile under embedded (when gated off via std).
  6. Use serde_json for json_decode. Rejected: serde / serde_json pulls in a meaningful amount of machinery (derives, monomorphisation, generic deserialisers) for what Mochi exposes as a "decode top-level object into HashMap<String, String>" API. The 90-LOC hand-rolled JSON decoder is faster to compile and easier to audit.
  7. Use sha2 for the LLM cassette key. Rejected: sha2 pulls in cpufeatures, block-buffer, crypto-common, and digest for a single SHA-256 hash. The inlined SHA-256 in mochi_runtime::llm adds ~80 LOC and zero deps.
  8. Bundle the runtime as a single inlined file per emission instead of an external crate. Rejected: makes generated code untraceable under cargo doc and breaks cargo publish. Shipping mochi-runtime as a real crate lets users cargo add mochi-runtime from any Rust program and lets the LLM cassette / fetch / panic modules be tested in isolation.

Risks

  1. macOS LC_UUID non-determinism. The Mach-O LC_UUID load command is randomised per link by Apple's ld64. Cannot be controlled from rustc-side flags. Phase 16 platform-skips the macOS reproducibility gate and asserts only the Linux equivalent. Mitigation: document the skip prominently; users who need bit-reproducible darwin binaries are directed to lld -no-uuid or a post-link patcher.
  2. Generic closure storage gap. Box<dyn Fn(T) -> U> cannot capture by &mut without further nesting (Box<dyn FnMut>), and cannot capture !Send types in some embedded targets. Phase 6 detects FnMut requirements and switches the box type; embedded use of closures is documented as out-of-scope.
  3. wasm32-wasip1 + std::net::TcpStream is a non-runner. wasm32-wasip1 has no TCP socket support; mochi_runtime::fetch::get panics at link time when targeted to wasm. Phase 17 fixtures avoid fetch; phase 14 fixtures are not exercised under wasm.
  4. cc-rs requires a C compiler on PATH. FFI fixtures (phase 12) gate on a cc detection at the start of Driver.Build. On systems without a C toolchain, FFI fixtures are skipped rather than failed.
  5. Cassette drift. LLM cassettes (phase 13) are keyed by SHA-256 of provider:prompt; any change to the prompt invalidates the cassette. Mitigation: fixtures pin the exact prompt string and commit the cassette file alongside the .mochi source.
  6. Embedded feature surface gap. The embedded feature exposes only conv and strings. Mochi programs using collections, channels, streams, or panic do not compile under embedded. Mitigation: the feature is documented as a "Mochi subset" target; phase 18 fixtures stay within conv + strings.

Acknowledgements

This MEP builds on MEP-45 (C transpiler) for the aotir IR and clower pipeline, on MEP-46 / MEP-47 / MEP-48 / MEP-49 / MEP-50 / MEP-51 / MEP-52 / MEP-56 for the multi-backend lowering pattern, on Rust's stable 1.78+ wasm32-wasip1 target naming, on the cargo zigbuild project for cross-musl compilation, on cc-rs for FFI sidecar compilation, and on the Rust embedded WG's no_std + extern crate alloc convention that makes the embedded feature gate clean.