Skip to main content

Phase 3. Closed type-mapping table

FieldValue
MEPMEP-73 §Phases
StatusLANDED
Started2026-05-29 21:23 (GMT+7)
Landed2026-05-29 21:30 (GMT+7)
Tracking issue(pending)
Tracking PR(pending)
Commit(pending)

Gate

package3/rust/typemap/ exposes a single Map(t rustdoc.Type, dir Direction) (*Mapping, errors.SkipReason, string) entry point. The function returns a structured Mapping describing the Mochi-side type and its FFI representation when a closed-table rule applies, or an errors.SkipReason plus a free-text detail when none exists. The contract is "closed": every rustdoc-types Type variant either has a documented rule or a documented refusal reason. There is no silent fallthrough that emits an approximated mapping.

The 53 test functions across kind_test.go, mapping_test.go, and map_test.go pin the closed table. The full package3/rust/... suite (6 packages: errors, build, semver, sparse, rustdoc, typemap) is green.

Closed-table rules

Primitives

RustMochi KindMochi renderingFFI repr
boolKindBoolboolbool
i8, i16, i32KindIntintint32_t
i64, i128, isizeKindInt64int64int64_t
u8KindBytebyteuint8_t
u16, u32KindUIntuintuint32_t
u64, u128, usizeKindUInt64uint64uint64_t
f32KindFloatfloatfloat
f64KindFloat64float64double
charKindCharcharuint32_t (codepoint)
strKindStringstringMochiString
(), never, !KindUnitunitvoid

The i128/u128 folding into 64-bit kinds is provisional, gated by a Phase 4 wrapper rule that emits a guard rejecting values outside [i64::MIN, i64::MAX] (resp. u64). Phase 4 may promote i128/u128 to a MochiBigInt slot if the polish lands; this phase only commits to the lower-bound mapping so the table is closed.

Strings

std::string::String, alloc::string::String, and any &str (immutable borrow of the str primitive) all map to KindString with FFIRepr = "MochiString". The bridge always copies the bytes; nothing crosses the FFI as a borrow.

Collections

RustMochi
Vec<u8>, &[u8], [u8; N]bytes
Vec<T>, &[T], [T; N]list[T]
HashMap<K, V>, BTreeMap<K, V>map[K]V
HashSet<T>, BTreeSet<T>list[T] (Mochi has no set primitive)

Path matching strips the std::, alloc::, core:: qualifiers via LastSegment, so a Vec<i64> referenced as std::vec::Vec<i64> or alloc::vec::Vec<i64> produces the same KindList. The Vec<u8> and [u8] specialisations to bytes are detected after the element mapping resolves to KindByte; this keeps the rule local to mapPath / mapSlice / mapArray rather than a separate path table.

Algebraic types

  • Option<T> (any ::option::Option path) → KindOption with Elem = T.
  • Result<T, E>KindResult with OK = T, Err = E.
  • (T1, T2, ...)KindTuple with Fields = [T1, T2, ...].
  • () (empty tuple) → KindUnit. The rustdoc encoding {"tuple": []} is detected by checking for a non-nil empty Tuple slice before the main dispatch, because Type.Kind() reports the empty-slice form as "empty".

Smart pointers

Box<T>, Rc<T>, and Arc<T> are transparent: their inner T is the result of Map. The wrapper layer (Phase 4) decides whether to clone (Rc/Arc) or deref-move (Box) at the FFI boundary; the surface mapping only sees the inner type.

User-defined paths

Any resolved_path that does not match a known std container falls through to KindStruct with the PathID and PathName filled from the rustdoc PathType. Phase 4 (wrapper synth) resolves whether the path points at a struct, enum, or opaque type via the ApiSurface.Structs / Enums slices captured in Phase 2.

Borrowed references

FormDirection InDirection Out
&Trecurse into Trecurse into T if lifetime is 'static, else SkipLifetime
&'static Trecurse into Trecurse into T
&mut TSkipUnknown (no v1 mapping)SkipUnknown

The &mut T refusal uses SkipUnknown because the enum has no dedicated SkipMutBorrow variant; the detail string carries the explanation. A future enum bump will promote this to a named reason without changing the rule.

Refusal classes

rustdoc kindReasonDetail
raw_pointerSkipRawPointerrequires unsafe capability opt-in
genericSkipGenericunresolved generic; declare under [rust.monomorphise]
dyn_traitSkipDynTraitdyn Trait has no Mochi surface
impl_traitSkipImplTraitimpl Trait return position requires explicit monomorphisation
qualified_pathSkipQualifiedPath<T as Trait>::Item not mappable
function_pointerSkipUnknownfunction pointer types are not mapped in v1
inferSkipUnknowninference placeholder forbidden in public sig
patSkipUnknownpattern types are unstable rustc feature
Cow<T>SkipCownot directly mappable; pass owned type
OsString/OsStr/PathBuf/Path/CString/CStrSkipOsStringplatform-specific encoding
Pin<T>SkipPinrequires custom lifetime contracts
unknown rustdoc-types variantSkipUnknowndetail names the variant tag

Composition propagation

Skip propagation is bottom-up: Vec<dyn Trait> returns SkipDynTrait from the inner mapping; (i32, *const u8) returns SkipRawPointer. Top-level callers see the most specific reason from the deepest unmappable subterm. The detail string is prefixed with "tuple field N: " etc. so the error surface tells the user which subterm failed.

Files changed

FilePurpose
package3/rust/typemap/kind.goKind enum (21 variants + KindInvalid), Direction enum + String() methods
package3/rust/typemap/mapping.goMapping struct, MochiType(), FFIRepr(), IsScalar()
package3/rust/typemap/map.goMap(t, dir) dispatch, primitive table, path table, LastSegment, all sub-maps
package3/rust/typemap/kind_test.goKind and Direction rendering
package3/rust/typemap/mapping_test.goMochiType / FFIRepr / IsScalar coverage for every Kind
package3/rust/typemap/map_test.go42 dispatch tests covering every rule plus refusal classes and skip propagation
website/docs/implementation/0073/phase-03-type-mapping.mdthis page

Test set

  • All package3/rust/typemap/... unit tests (53 functions: 2 in kind_test, 9 in mapping_test, 42 in map_test).
  • Full go test ./package3/rust/... regression across all 6 packages.

Lowering decisions

Why u8 maps to byte and not int

The Mochi byte type is a distinct primitive used by I/O, hashing, and binary protocols. Mapping every u8 to int would erase semantics at the surface: callers passing Vec<u8> would lose the bytes literal form. The byte mapping survives into the Vec<u8> → bytes and &[u8] → bytes specialisations, which the wrapper layer relies on to skip the per-element FFI marshalling cost.

Why Box, Rc, Arc are transparent at the surface

Smart pointers carry no value-type information at the Mochi surface; they only affect ownership and refcount handling, both of which are FFI-layer concerns. Folding them into the inner type lets Mochi callers see the underlying value type while the wrapper layer (Phase 4) emits the correct clone/move on each crossing. This matches research note 05 §"Smart pointer folding" and avoids exposing Rc<T> vs Arc<T> distinctions in import-site signatures.

Why HashSet/BTreeSet map to list

Mochi has no first-class set type. Mapping HashSet<T> to list[T] preserves the element type and lets callers use list-style iteration. The wrapper layer materialises the set as a Vec<T> on entry and rebuilds the set on call; the order is not preserved, but HashSet callers do not expect order anyway. A future Mochi set primitive can promote this without changing the SkipReason taxonomy.

Why empty tuple is unit, but Type.Kind() reports "empty"

The rustdoc-types JSON encoding for () is {"tuple": []}. rustdoc.Type.Kind() uses len(t.Tuple) > 0 as the discriminator, so the empty form falls through to "empty". The fix lives in typemap.Map: a pre-dispatch check for t.Tuple != nil && len(t.Tuple) == 0 produces a KindUnit Mapping. The rustdoc parser is intentionally not changed; downstream consumers can rely on Kind() == "empty" continuing to mean "no payload at all" for genuine empty Type values.

Why &mut T uses SkipUnknown

The errors.SkipReason enum (Phase 0) does not yet carry a SkipMutBorrow variant. Adding one mid-stream would break the fixture-count golden files in earlier phases. The Phase 4 wrapper synth will batch enum additions, at which point &mut T will get a named reason; today it surfaces as SkipUnknown with a clear Detail string.

Closeout notes

Phase 3 introduces no new external dependencies; the package depends only on mochi/package3/rust/errors and mochi/package3/rust/rustdoc. The closed table is the single source of truth consumed by Phase 4 (wrapper synth), Phase 5 (extern-fn emitter), and Phase 12 (monomorphisation). A regression in any future rustdoc-types schema bump that adds a new Type variant will produce a SkipUnknown skip rather than a crash, exactly as Phase 2's Type.Unknown catch-all guarantees.

Phase 4 will consume Mapping.FFIRepr() to emit the extern-C wrapper function signatures, and Mapping.MochiType() to populate the SkipReport detail strings when a containing function fails to lower.