Skip to main content

06. Type-system lowering

This note maps each Mochi type to its Rust lowering and frames the trade-offs in the non-obvious cases.

Scalar types

MochiRustNote
inti64Mochi's int is bounded 64-bit signed.
floatf64IEEE-754 double precision.
boolboolNative Rust bool.
stringStringUTF-8, char-aware via mochi_runtime::strings.

i64 not isize: Mochi int is portable, must behave identically on 32-bit and 64-bit hosts. isize would vary.

String not &str: Mochi strings are owned values that can be returned from functions, stored in records, mutated via concatenation. &str would force lifetime annotations throughout the emitted code, which the colour pass would have to navigate. Owned String is simpler; the colour pass handles clones.

Collection types

MochiRustNote
list<T>Vec<T>Native Rust vec.
map<K, V>HashMap<K, V>Iteration order unspecified.
omap<K, V>BTreeMap<K, V>Sorted iteration order matches vm3's order-preserving iteration.
set<T>HashSet<T>Iteration order unspecified.
oset<T>BTreeSet<T>Sorted iteration order.

Mochi distinguishes map (unordered) from omap (insertion-ordered, but vm3 actually iterates in insertion order via a slice + map combo). The Rust lowering uses BTreeMap for omap, which gives sorted order, not insertion order. This is a small semantic deviation: Mochi-on-Rust output for keys(omap) is sorted by key, while Mochi-on-vm3 is in insertion order. For most Mochi programs (where insertion order matches sort order by construction), this is invisible; for programs that insert in non-sort order, the outputs diverge.

The deviation is acceptable for MEP-53 because: (1) no existing fixture exercises the insertion-order-non-equals-sort-order case, (2) the C target (MEP-45) has the same deviation, (3) emitting an indexmap-backed map would add a dep that the embedded gate would have to drop.

Record and sum types

record User { id: int, name: string } and anonymous type Pair = { a: int, b: int }:

#[derive(Clone, Debug, PartialEq, Default)]
struct User { id: i64, name: String }

The four derives are load-bearing (see language-surface). Hash is not auto-derived because Mochi records can contain f64 which is not Hash (NaN != NaN). When a record needs to be a HashMap key, the lower pass emits #[derive(Hash, Eq)] additionally and rejects f64 fields at typecheck.

type Shape = Circle { r: float } | Rect { w: float, h: float } | Empty:

#[derive(Clone, Debug, PartialEq)]
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
Empty,
}

Self-referential variants get Box-wrapped at the recursive position:

type List = Cons(int, List) | Nil
enum List {
Cons(i64, Box<List>),
Nil,
}

The Box is required because Rust enums must be sized at compile time; enum E { V(E), N } would have infinite size. The Box adds one heap allocation per recursive node, which is acceptable for the Mochi semantic model.

Function types

fun(T) -> U:

Box<dyn Fn(T) -> U>

When the lower pass detects an FnMut requirement (closure body mutates a captured &mut binding), the box switches to Box<dyn FnMut>. When it detects an FnOnce requirement (move-out of a captured value inside the body), the box switches to Box<dyn FnOnce>. The detection is conservative: false positives push toward FnMut / FnOnce when Fn would have sufficed, which the user sees as a slightly stricter call-site shape but never as a compile failure.

The box is heap-allocated; calls go through a vtable dispatch. The colour pass mitigates the heap cost for capture by eliding .clone() of captured values when they're Copy.

Concurrency types

MochiRustNote
chan<T>mochi_runtime::chan::Chan<T>Wraps Rc<RefCell<VecDeque<T>>>.
stream<T>mochi_runtime::stream::Stream<T>Wraps Rc<RefCell<Vec<Rc<RefCell<VecDeque<T>>>>>>.
Sub<T>mochi_runtime::stream::Sub<T>Wraps Rc<RefCell<VecDeque<T>>>.
agent A { ... }struct A { ... } impl A { fn new() -> Self ... }Plain struct.
async fun(): Tfun(): TAsync coloring is typecheck-only; no runtime effect.
await futfutIdentity.

Rc<RefCell> not Arc<Mutex>: see agent-streams for the single-thread rationale.

Error types

panic does not have a user-visible type; it's a control-flow effect. try { ... } catch e { ... } binds e: i64 from the panic payload.

There is no Mochi Result<T, E> type. Mochi programs that want explicit error returns use sum types (type Result = Ok(T) | Err(E)) and pattern-match.

Reference / pointer types

Mochi has no pointer type, no reference type at the surface level. All values are owned. The Rust target follows: no &T or &mut T in the emitted public surface, only as implementation details of method signatures (e.g., &self in impl methods).

The runtime crate uses &str and &[T] in some signatures (e.g., mochi_runtime::strings::index(s: impl AsRef<str>, i: i64)); this is a Rust-side ergonomics choice that doesn't leak through to the Mochi-visible API.

Cross-references