MEP 4. Type System
| Field | Value |
|---|---|
| MEP | 4 |
| Title | Type System |
| Author | Mochi core |
| Status | Informational |
| Type | Informational |
| Created | 2026-05-08 |
Abstract
Mochi has a small structural type system with nominal records and unions. This MEP documents the type kinds present in the implementation today, the equality and unification rules used by the checker, and the numeric tower that drives binary operator typing.
Motivation
The type system is the contract between the checker and the runtime. Misunderstandings about whether int and int64 are equal, or whether any is a top type or a bottom type, produce soundness gaps that are hard to track down later. This MEP documents the kinds and their relationships so future changes are deliberate.
Specification
The Type interface
types/check.go:12-14. Every type kind implements:
type Type interface {
String() string
}
There is no equality method on the interface. Equality is computed externally by equalTypes in types/infer.go and structural unification is done by unify in types/check.go:150-387. The fallthrough case in equalTypes is reflect.DeepEqual, with explicit carve-outs for Int/Int64 interop, union/struct member matching, and option types. unify uses a different rule for AnyType: see below.
Kinds present today
Listed in source order at types/check.go:16-140.
Primitive kinds:
IntType. Default integer width. Backed by Goint64at runtime but the surface type isint.Int64Type. Distinguished only where the runtime needs it, for example the result type ofnow(). Unifies withIntTypefor arithmetic.FloatType. IEEE 754 double precision.BigIntType. Arbitrary precision integer.BigRatType. Arbitrary precision rational.StringType. UTF-8 byte sequence.BoolType.trueorfalse.VoidType. Result type of statement-only operations likeprint.
Structural kinds:
ListType{Elem Type}. Homogeneous ordered sequence.MapType{Key Type, Value Type}. Hash map keyed byKey.OptionType{Elem Type}. Declared but not produced by inference today. See MEP 10.GroupType{Key Type, Elem Type}. Internal type carried by theinto Nameclause of agroup byquery.StructType{Name, Fields, Order, Methods}. Nominal record.Orderpreserves the declaration sequence so output and JSON encoding are deterministic.UnionType{Name, Variants}. Nominal sum of struct-shaped variants.Variantsis a map but the iteration order should be stable: the variant order from the declaration is captured when the type decl is processed.
Function kind:
FuncType{Params, Return, Pure, Variadic}. ThePureflag is computed but not enforced anywhere yet (tracked in #21188). TheVariadicflag is exercised by builtins likeconcatandrange.
Polymorphism kinds:
TypeVar{Name}. Carried by generic function declarations. Not yet used for full inference. See MEP 12.
Top kind:
AnyType{}. Unifies with everything in both directions. This is the primary unsoundness escape hatch and is documented in detail in MEP 10 and MEP 11.
What is missing
The list of kinds the language does not have today:
- No nominal alias kind.
type Id = intis collapsed at resolve time to its target. - No tuple kind. A heterogeneous fixed-length sequence has no surface type.
- No record subtype kind separate from
StructType. Width subtyping is decided by name plus field set, not by an explicit row variable. - No effect kind. The
Pureflag is on the function type, not a separate kind. - No reference kind. Mutability is a property of the binding (
varversuslet), not of the type. - No existential or universal kind beyond
TypeVarfor generics. - No first-class type kind. Types cannot be passed as values.
Typing judgement
We use the standard form Γ ⊢ e : τ where Γ is the lexical environment. Mochi's Γ carries:
- Variable bindings with mutability flag (
Env.SetVar). - Type bindings (
Env.SetType). - Function bindings (
Env.SetFunc). - Stream and agent bindings.
Environments are linked. A child environment delegates lookups to its parent and shadows on write. See types/env.go for the API.
Equality and unification
The exact rules in equalTypes (types/infer.go):
IntTypeandInt64Typeare equal to each other. The code has explicit carve-outs soint64result types (for example fromnow()) unify cleanly withintvariables.- Two list types are equal iff their element types are equal.
- Two map types are equal iff key and value types are equal.
- Two function types are equal iff arity, parameter types, return type, and
Variadicflag all match. - Two struct types are equal by
reflect.DeepEqual; in practice names are unique so this reduces to name equality. - Two union types are equal by name.
- A
StructTypeis equal to aUnionTypewhen the struct is one of the union's variants. This is what allowsmatcharms typed as variant structs to satisfy a union binding. AnyTypeis equal only to anotherAnyTypeinequalTypes. It isunifythat acceptsAnyTypeon either side as a match; that is the unsoundness escape hatch.
Unification propagates substitutions for TypeVar on top of the equality rules. See types/check.go:150-387.
Numeric tower
Listed from narrowest to widest, but the relations are not a clean total order:
int ⊂ int64 ⊂ bigint
int ⊂ float
int ⊂ bigrat
bigint ⊂ bigrat
float ⊄ bigrat (mixed becomes bigrat in practice today)
The actual rules used by inferBinaryType:
- Integer divide (
/between twoints) returnsint. - Either side
bigratwidens tobigrat. - Either side
int64and the other sideintorint64returnsint64. - Either side
floatand both sides numeric returnsfloat. - Either side
bigintreturnsbigint. - Two ints return
int. - String plus string returns
string. - List plus list returns the same list type if elements agree, or
[any]otherwise. - Otherwise
any.
The + rule on lists silently widens to [any] on element type mismatch. This is a soundness gap tracked in #21187.
Rationale
Keeping String() as the only required method on Type makes the kind table easy to extend. Equality and unification are external because they need to be customisable for variance and substitution.
AnyType exists because the alternative is making every builtin generic, which we cannot do until parametric polymorphism (MEP 12) lands. AnyType is a debt; it is not a design choice.
The numeric tower is conservative on purpose. We prefer to widen to bigrat when in doubt rather than silently lose precision.
Backwards Compatibility
Informational. No backward compatibility implications.
Reference Implementation
types/check.go:12-14:Typeinterface.types/check.go:16-140: kind definitions.types/check.go:150-387:unify.types/infer.go:equalTypesand operator typing.types/env.go: environment API.
Pinned decisions
Reading a missing map key
Once MEP 10 A2 lands, m[k] for an absent key will infer to OptionType{V}. Callers must unwrap the option before using the value at type V.
Three options were considered:
- Type
m[k]asV?. Picked. - Require an explicit default,
m[k] ?? default, with a checker error otherwise. - Raise a typed runtime panic.
Option 1 wins because it reuses the same OptionType shape that MEP 10 A2 gives null and MEP 10 C1 gives the T? shorthand. Option 2 needs a new operator; option 3 forces in m guards at every call site.
Today the runtime returns the zero value of V. For non-zero-bearing types this is a progress violation. The current behaviour is pinned by tests/types/valid/missing_map_key.mochi; the future tightening is a deliberate breaking change tracked in #21186 alongside MEP 10 A2.
Open Questions
- Tuple kind. No surface type for heterogeneous fixed-length sequences. Needed once query result tuples are generalised.
- Option kind.
OptionTypeexists but is not produced by inference today. Tied to thenullsoundness gap in MEP 10. - Numeric ordering.
floatandbigratmix awkwardly. No decision yet on whether to widen tobigrator reject the mix.
References
Copyright
This document is placed in the public domain.