Skip to main content

01. Language surface

This note enumerates the Mochi surface forms the Rust transpiler must accept and the Rust shape each lowers to. The exhaustive table is in MEP-53 §3; this note frames the categories and calls out the cases that drove specific Rust-target decisions.

Scope

Mochi's surface is roughly: scalars, control flow, records, sum types, closures, lists / maps / sets, query DSL, Datalog, agents, channels, streams, async, try / catch / panic, FFI, fetch, JSON, LLM generate. Each maps to a Rust idiom, but several mappings have non-trivial trade-offs.

Scalars and control flow

inti64, floatf64, boolbool, stringString. Arithmetic uses native ops; integer division and modulo route through mochi_runtime::check::div_i64 / mod_i64 so that vm3's "panic code 5 on zero divisor" semantics are preserved exactly (Rust's built-in / and % already panic on zero for ints, but the panic message format differs from vm3's; routing through the runtime gives uniform panic codes).

for i in lo..hi uses Rust's exclusive .. range, which matches Mochi's exclusive range exactly. for x in xs lowers to for x in xs.iter().cloned(), with the clone gated by the codegen-design colour pass.

Records and sum types

record and anonymous type X = { ... } both lower to:

#[derive(Clone, Debug, PartialEq, Default)]
struct Foo { /* ... */ }

The four derives are load-bearing: Clone for functional update semantics, Debug for print(foo) (lowered to print_str(format!("{:?}", foo))), PartialEq for ==, Default for spawn AgentType() zero-value construction.

type T = A | B lowers to a tagged enum:

#[derive(Clone, Debug, PartialEq)]
enum T { A { /* ... */ }, B { /* ... */ } }

Self-referential variants get Box-wrapped at the recursive position. The match-to-decision-tree pass (Maranget 2008) is reused from the C target via clower.

Closures

Box<dyn Fn(...)> is the lowering target. Generic impl Fn cannot be stored in struct fields, returned from functions with multiple bodies, or kept in homogeneous lists, all of which Mochi's source language permits. Box<dyn Fn> accepts a runtime indirection cost for surface compatibility.

Captures lower to explicit move clauses with clone() calls computed by the closure-conversion pass. Recursive closures use a Rc<RefCell<Option<Box<dyn Fn>>>> Y-combinator trampoline.

FnMut is detected at lower time and switches the box type to Box<dyn FnMut> when the closure body mutates a captured &mut binding.

Collections

Vec<T> for lists, HashMap<K, V> for maps, HashSet<T> for sets. Iteration order of HashMap and HashSet is unspecified by Rust stdlib; Mochi's omap (insertion-ordered map) is lowered via BTreeMap<K, V> when the lower pass detects an ordered-iteration requirement (e.g., printing the map, or keys(m) consumption).

Concurrency primitives

Channels (Rc<RefCell<VecDeque<T>>>), streams (Rc<RefCell<Vec<Rc<RefCell<VecDeque<T>>>>>>), and agents (plain structs) are all single-thread. No Arc, no Mutex, no Thread::spawn. Mochi's async expr lowers to expr (immediate evaluation); await fut lowers to fut (identity). The async colouring is a typecheck-time pass with no runtime effect.

Rationale: Mochi's source language does not expose a thread-spawn primitive at user level. Forcing every Mochi program through Arc<Mutex<...>> for the channel and stream primitives would pay a synchronisation tax that the source language does not require. Users who need real OS threads can call into std::thread via FFI.

try / catch / panic

try { ... } catch e { ... } lowers to match mochi_runtime::panic::catch(|| { ... }) { Some(code) => ..., None => {} }. The runtime's catch wraps panic::catch_unwind with panic::AssertUnwindSafe and downcasts the payload to extract an i64 code. Stdlib panics (out-of-bounds, divide-by-zero) are message-string-mapped to canonical codes (4, 5). User panics via panic(code) lower to panic::panic_any(code).

FFI

extern fn name(a: T): U from "header.h" lowers to a Rust extern "C" block plus a sidecar cffi/ directory carrying the user-supplied C source and a build.rs that runs cc-rs. String arguments round-trip through CString::new(s).unwrap().as_ptr().

Fetch + JSON + LLM

fetch <url> / httpGet(url)mochi_runtime::fetch::get(url) (HTTP/1.1 over std::net::TcpStream, no TLS).

json_decode(s)mochi_runtime::json::decode(s) (top-level object decoder returning HashMap<String, String> with non-string values string-coerced).

generate <provider> { prompt: p, model: m }mochi_runtime::llm::call(provider, prompt) (cassette replay over MOCHI_LLM_CASSETTE_DIR).

Cross-references