Phase 5. Sum types + pattern matching
| Field | Value |
|---|---|
| MEP | MEP-54 |
| Status | LANDED |
| Started | 2026-05-28 (GMT+7) |
| Landed | 2026-05-28 (GMT+7) |
| Tracking PR | #22522 |
| Commit | 52e07d4998 |
Gate
20 new fixtures cover the sum-type surface across four axes: basic shape (sum_basic, sum_int_field, sum_two_arms, sum_three_variants), field types (sum_bool_field, sum_string_field, sum_mixed_field_types, sum_two_fields), variants (sum_no_fields, sum_two_unions), match (sum_default_arm, sum_eq_self, sum_match_print_sum, sum_nested_call, sum_use_binding, sum_arith_arm), state (sum_let_var, sum_var_assign, sum_in_if, sum_loop_match). Run: go test ./transpiler3/go/build/... -run TestPhase1Hello/sum_.
Lowering decisions
Mochi type T = A {...} | B {...} lowers to a single Go struct per union: type T struct { Tag uint8; A_Field1 int64; A_Field2 string; B_Field1 bool; ... }. The Tag discriminates variants (assigned in declaration order starting from 0); per-variant fields are flattened with a Variant_Field prefix so all variants share one struct. One constructor T_A(args...) T and T_B(args...) T is emitted per variant, taking the variant's source-order arguments and returning a T with the right Tag and Variant_* fields populated.
VariantLit lowers to a call to the constructor: T_A(1, "x"). VariantFieldAccess lowers to a SelectorExpr on the flattened name: tmp.A_Field1. UnionVarRef is plumbed through lowerExpr for uses outside MatchStmt.
MatchStmt lowers to a Go switch on tmp.Tag: tmp := target; switch tmp.Tag { case 0: <A arm>; case 1: <B arm> }. Each arm establishes a per-binding local: b := tmp.A_Field1 so the arm body can reference b without going through the discriminated struct. The tmp local is generated by freshName("__m") so nested matches do not collide. The default arm (when present) lowers to default: on the same switch.
Choosing the flattened-struct encoding over Go's interface-based sum (one interface + per-variant struct type implementing it) was a deliberate Phase 5 decision: the flattened form has zero allocation overhead per VariantLit (no boxing), and the Tag discriminator is a single byte that fits in the struct alignment slack. The downside is one struct that holds fields for every variant; with the all-scalar-field constraint inherited from Phase 4, the struct stays small (a few words).
TypeUnion is handled in letTypeText so var x T = T_A(...) gets the right annotation.
Files changed
| File | Purpose |
|---|---|
transpiler3/go/lower/lower.go | lowerUnionDecl -> struct + Tag + variant ctors |
transpiler3/go/lower/expr.go | VariantLit, VariantFieldAccess, UnionVarRef |
transpiler3/go/lower/stmt.go | MatchStmt -> tmp := target; switch tmp.Tag { case N: <arm> } |
transpiler3/go/lower/types.go | TypeUnion in letTypeText |
transpiler3/go/lower/names.go | freshName("__m") for per-match temp locals |
tests/transpiler3/go/fixtures/sum_*/ | 20 fixtures |
Test set
- 20
TestPhase1Hello/sum_*subtests covering basic shape, field types, variants, match arms, and state mutations.
Closeout notes
The flattened struct encoding was the right call: every Phase 5 fixture compiles to a single allocation per VariantLit and zero allocations per match. Encoding the variant Tag as uint8 rather than int saves a word on every union value with up to 255 variants (the upstream type-checker caps this); switching to int16 later would be backward-compatible because the Go switch arms compare against integer literals.