Skip to main content

Phase 5. Mochi extern-fn emitter

FieldValue
MEPMEP-73 §Phases
StatusLANDED
Started2026-05-29 22:00 (GMT+7)
Landed2026-05-29 22:03 (GMT+7)
Tracking issue(pending)
Tracking PR(pending)
Commit(pending)

Gate

package3/rust/emit.Emit(*wrapper.Crate) Files lowers a Phase 4 wrapper.Crate into a pair of Mochi source files. The output is byte-deterministic; tests run Emit five times and assert equality.

The two-file split keeps the FFI surface separate from the API the user imports:

  • <crate>_extern.mochi carries extern fun <mangled_symbol>(...): <type> lines (one per SynthFn, in walker order) plus extern type <Name> lines for every user-defined struct / enum / handle that appears anywhere in the surface (deduplicated and sorted). These bind directly to the wrapper crate's #[no_mangle] symbols emitted in Phase 4.
  • <crate>.mochi carries fun <short>(...): <type> { return <mangled_symbol>(<args>) } re-exports, one per SynthFn. <short> is the last ::-separated segment of the function's upstream rustdoc path, so callers write hex.encode(buf) rather than mochi_hex_encode(buf) (Phase 6's import rust "<crate>@<semver>" as <alias> grammar treats the alias module as the import target).

Mochi type rendering

MochiRender(typemap.Mapping) string is the only place where Phase 3's closed-table Kind set meets Mochi syntax. The mapping is:

typemap KindMochi syntax
KindUnitunit
KindBoolbool
KindInt, KindInt64, KindUInt, KindUInt64, KindByte, KindCharint
KindFloat, KindFloat64float
KindStringstring
KindBytesbytes
KindList<T>list<T>
KindMap<K, V>map<K, V>
KindOption<T>Option<T>
KindResult<T, E>Result<T, E>
KindTuple(T1, T2, ...)(T1, T2, ...)
KindStruct / KindEnum / KindHandlelast ::-segment of the rustdoc path

All sized integer widths collapse to int; both f32 and f64 collapse to float. Mochi's int is 64-bit, so any precision narrowing happens on the C-ABI side (the wrapper crate truncates / extends on the way through #[no_mangle]), not at the Mochi surface.

The angle-bracket generics syntax (list<T>, map<K, V>, Option<T>, Result<T, E>) matches examples/v0.6/extern.mochi's pl.DataFrame(data: map<string, list<any>>): DataFrame. Tuples use parens: (int, string).

Extern type collection

collectTypeNames(*wrapper.Crate) walks every parameter and return mapping in walker order and unions the user-defined struct / enum / handle names found by typeNames(typemap.Mapping). The result is deduplicated via a map[string]struct{} and sorted via sort.Strings before being emitted. The sort keeps the diff stable when only function order changes between rustdoc rebuilds.

typeNames only walks the typemap recursion shapes the closed table allows: List/Option dive into Elem; Map dives into Key and Value; Result dives into OK and Err; Tuple dives into Fields. Scalar kinds (KindInt, KindString, KindBytes, etc.) and primitives contribute nothing.

In the hex-like fixture, only FromHexError (reached through Result<Vec<u8>, FromHexError> on decode) surfaces as an extern type. Vec<u8> collapses to bytes and contributes nothing.

Alias module shape

For each SynthFn, the alias module emits:

fun <short>(<params>): <return> {
return <mangled>(<args>)
}

<short> strips the upstream path qualifier (hex::encode -> encode); <params> is the same name: type list as the extern declaration; <args> is the positional name list (no name munging - parameter names match between the extern decl and the alias body).

Unit-return functions drop the : <return> clause from both declarations and use a statement-call body (no return keyword): fun tick() { mochi_demo_tick() }. The extern declaration similarly drops the colon-prefix return suffix: extern fun mochi_demo_tick().

Hex-like fixture output

The hex crate's three functions render as:

// extern bindings for hex 0.4.3
// generated by mochi/package3/rust/emit; do not edit.

extern type FromHexError

extern fun mochi_hex_encode(data: bytes): string

extern fun mochi_hex_decode(input: string): Result<bytes, FromHexError>

extern fun mochi_hex_to_upper_hex(value: int): int
// alias module for hex 0.4.3
// generated by mochi/package3/rust/emit; do not edit.

fun encode(data: bytes): string {
return mochi_hex_encode(data)
}

fun decode(input: string): Result<bytes, FromHexError> {
return mochi_hex_decode(input)
}

fun to_upper_hex(value: int): int {
return mochi_hex_to_upper_hex(value)
}

Determinism

Emit is byte-deterministic: the header lines (crate name + version + provenance comment) are static, walker order is preserved from Phase 2, the sorted collectTypeNames set is stable, and there is no map iteration in user-visible output paths. The test suite verifies this with five repeated calls.

Files changed

FilePurpose
package3/rust/emit/render.goMochiRender(typemap.Mapping) string, typeNames(typemap.Mapping) []string, lastSegment(string) string
package3/rust/emit/emit.goFiles struct, Emit(*wrapper.Crate) Files, emitExtern, emitAlias, emitExternFn, emitAliasFn, collectTypeNames, shortName
package3/rust/emit/emit_test.goheader presence, extern fun signature shape, extern type declaration order, alias fn re-export shape, byte determinism, empty-crate shape, void-return shape, multi-param shape, MochiRender per-kind coverage, lastSegment / shortName mangling
website/docs/implementation/0073/phase-05-extern-emit.mdthis page
website/docs/implementation/0073/index.mdmark Phase 4 LANDED with commit SHA; mark Phase 5 LANDED; advance roadmap

Test set

  • All package3/rust/emit/... unit tests (15 functions).
  • Full go test ./package3/rust/... regression across all 8 packages (build, emit, errors, rustdoc, semver, sparse, typemap, wrapper).

Closeout notes

Phase 5 introduces no external dependencies. The emitter is pure (a *wrapper.Crate in, two strings out), so it can be folded into Phase 7's build orchestration without an I/O wrapper.

Phase 6 (import rust "<crate>@<semver>" as <alias> grammar) consumes the alias module file as the import target. The two-file split means the parser only ever sees Mochi syntax (no extern fun leakage into user space); the FFI declarations are loaded transitively by the alias module's own import "./<crate>_extern.mochi"-equivalent, which Phase 7 wires up.

The extern type declarations carry no constructor or field exposure, matching examples/v0.6/extern.mochi's extern type socket convention. Opaque-handle methods will be added in Phase 12 (monomorphisation) where impl-block lowering happens.