Skip to main content

Phase 5. Sum types and pattern matching

FieldValue
MEPMEP-49 §Phases · Phase 5
StatusLANDED
Started2026-05-28 13:40 (GMT+7)
Landed2026-05-28 13:40 (GMT+7)
Tracking issue
Tracking PR

Gate

TestPhase5Sums: 25 fixtures green on Swift 6.0 and 6.1, linux-x64. TestSwiftcClean remains green (exhaustiveness warnings become errors).

Goal-alignment audit

Sum types are the primary abstraction over option<T>, result<T,E>, and user-defined variants. Swift enum with associated values is the natural and idiomatic target -- it gives compile-time exhaustiveness checking via the Swift compiler's switch exhaustiveness analysis, readable generated code, and full Sendable / Hashable / Codable support when all associated values conform.

Sub-phases

#ScopeStatusCommit
5.0type Shape = Circle(r: float) | Rect(w: float, h: float)public enum Shape with associated valuesNOT STARTED
5.1match → Swift switch with exhaustive case pattern; indirect for recursive variantsNOT STARTED
5.2option<T>T?; some(x)x; nonenil; match opt { some(v) => ..., none => ... }NOT STARTED
5.3result<T,E>Result<T,E>; ok(v).success(v); err(e).failure(e)NOT STARTED
5.4Sendable, Hashable, Codable conformances synthesised by Swift for all enum variantsNOT STARTED

Sub-phase 5.0 -- Enum declaration

Decisions made (5.0)

public enum: every Mochi union type lowers to a public enum. Swift enums with associated values are the canonical sum type representation. Unlike the .NET target which needs an abstract record hierarchy, Swift enums have built-in discriminant + associated data.

Associated value labels: Mochi Circle(r: float) → Swift case circle(r: Double). Labels are preserved (Swift associated values support labels). The label is lowercase (matching Mochi's snake_case) to keep the case name and label consistent.

Case name: Mochi variant names are PascalCase (Circle). Swift enum case names are lowerCamelCase (circle). The lowerer converts: Circle.circle, Rect.rect. This matches Swift API Design Guidelines.

// Mochi: type Shape = Circle(r: float) | Rect(w: float, h: float)
public enum Shape: Sendable, Hashable, Codable {
case circle(r: Double)
case rect(w: Double, h: Double)
}

Nullary variants: Mochi type Color = Red | Green | Blue → enum cases with no associated values:

public enum Color: Sendable, Hashable, Codable {
case red
case green
case blue
}

indirect for recursive variants: Mochi type List<T> = Nil | Cons(head: T, tail: List<T>) requires indirect because Cons contains the enum itself. The lowerer marks the whole enum indirect:

public indirect enum MochiList<T>: Sendable, Hashable, Codable
where T: Sendable & Hashable & Codable {
case `nil`
case cons(head: T, tail: MochiList<T>)
}

nil keyword conflict: Mochi Nil variant cannot be named nil in Swift (reserved keyword). The lowerer uses a backtick escape: case `nil`.

One file per union type: same as records. Each Mochi union type gets its own .swift file.

Sub-phase 5.1 -- Match to switch

Decisions made (5.1)

Swift switch is exhaustive by default: the Swift compiler requires all cases to be covered (or a default arm). Since Mochi's match is always exhaustive, the lowerer never emits a default arm unless the Mochi source has a wildcard _ arm.

Pattern lowering:

// Mochi:
// match shape {
// Circle(r) => 3.14159 * r * r,
// Rect(w, h) => w * h
// }

let area: Double
switch shape {
case .circle(let r):
area = 3.14159 * r * r
case .rect(let w, let h):
area = w * h
}

Switch as expression: Swift switch is a statement by default (not an expression). For Mochi match used as an expression, the lowerer assigns to a temp variable:

let __area: Double
switch shape {
case .circle(let r): __area = 3.14159 * r * r
case .rect(let w, let h): __area = w * h
}
let area = __area

In Swift 5.9+ (if and switch expressions via SE-0380), the lowerer can emit switch as an expression directly. The lowerer targets Swift 6.0 (which includes SE-0380), so the expression form is preferred when the switch result is used immediately:

let area = switch shape {
case .circle(let r): 3.14159 * r * r
case .rect(let w, let h): w * h
}

Guard clauses: Mochi Circle(r) if r > 0.0 => ...case .circle(let r) where r > 0.0:.

Nested patterns: Pair(Some(x), _)case .pair(.some(let x), _):.

Wildcard: Mochi _ =>default:.

Sub-phase 5.2 -- option → T?

Decisions made (5.2)

T? not Option<T>: Mochi option<T> maps to Swift Optional<T> (written T?). Swift Optional is a stdlib enum with .some(T) and .none cases. This avoids a custom Option type in MochiRuntime and integrates with Swift's ??, if let, guard let, optional chaining, and map/flatMap on optionals.

some(x)Optional.some(x) → just x in context: in Swift, wrapping a value in Optional is implicit when the type context is T?. So some(x) in Mochi lowers to just x when assigned to a T? variable. When a none is needed, the lowerer emits Optional<T>.none or nil depending on context.

match opt { some(v) => f(v), none => g() }: lowered to:

switch opt {
case .some(let v): f(v)
case .none: g()
}

Or with the if let idiom when the none branch is trivial:

if let v = opt {
f(v)
} else {
g()
}

The lowerer prefers switch for exhaustive multi-arm matches and if let for simple some/none pairs.

opt ?? default: Mochi opt.get_or(default)opt ?? default.

opt.map(f): → opt.map(f). Swift Optional.map applies f to the wrapped value if present.

opt.flat_map(f): → opt.flatMap(f).

Optional chaining: user?.address?.city in Mochi → user?.address?.city in Swift. The lowerer recognises optional field chains and emits optional chaining directly.

Sub-phase 5.3 -- result<T,E> → Result<T,E>

Decisions made (5.3)

Swift stdlib Result<T,E>: Mochi result<T,E> maps to Swift's Result<Success, Failure> where Failure: Error. ok(v).success(v). err(e).failure(e).

Error type: Mochi error types in result<T,E> are Mochi union types or record types. They are lowered to Swift types that also conform to Error. The lowerer adds Error conformance to the error enum/struct when it appears as the E in result<T,E>:

// Mochi: type MyError = NotFound | Unauthorized
public enum MyError: Error, Sendable, Hashable, Codable {
case notFound
case unauthorized
}

match res { ok(v) => ..., err(e) => ... }:

switch res {
case .success(let v): ...
case .failure(let e): ...
}

result.map(f): → res.map(f).

result.flat_map(f): → res.flatMap(f).

Typed throws bridge (Phase 11): result<T,E> and throws(E) are interconvertible. Result.get() throws the error; Result(catching:) wraps a throwing call. The bridge is fully established in Phase 11.

Sub-phase 5.4 -- Synthesised conformances

Decisions made (5.4)

Sendable: Swift synthesises Sendable for enums when all associated values are Sendable. Since all Mochi types are Sendable by construction (records, scalars, collections), the synthesis always succeeds.

Hashable: synthesised when all associated values are Hashable. Same reasoning.

Codable: synthesised when all associated values are Codable. Nullary cases encode as a string of the case name. Associated-value cases encode as an object { "type": "circle", "r": 1.5 } by default. Custom encoding keys can be specified in Phase 14 (JSON customisation).

CustomStringConvertible: NOT synthesised by Swift for enums with associated values. MochiRuntime.print uses String(reflecting:) for enum values in Phase 5. A proper description implementation is deferred.

Files changed

FilePurpose
transpiler3/swift/lower/lower.goUnionDeclpublic enum; variant lowering; indirect detection
transpiler3/swift/lower/match.goMatchStmt/MatchExprswitch statement/expression; pattern deconstruction
transpiler3/swift/lower/option.gooption<T>T?; some/none literal lowering; optional chain lowering
transpiler3/swift/lower/result.goresult<T,E>Result<T,E>; ok/err lowering; Error conformance injection
transpiler3/swift/build/phase05_test.goTestPhase5Sums: 25 fixtures
tests/transpiler3/swift/fixtures/phase05-sums/25 fixture directories

Test set

  • TestPhase5Sums -- 25 fixtures covering: sum_basic, sum_nullary, sum_recursive, sum_generic, sum_nested_match, sum_guard, sum_wildcard, sum_as_expression, sum_in_list, sum_in_map, sum_codable, sum_hashable, sum_multi_variant, option_some_none, option_map, option_flat_map, option_chain, option_get_or, option_in_list, result_ok_err, result_map, result_flat_map, result_error_type, result_typed_throws_bridge, sum_string_result.

Deferred work

  • CustomStringConvertible for enums. Deferred (low priority; String(reflecting:) is a working fallback).
  • Codable custom encoding keys for sum types. Deferred to Phase 14.
  • Pattern matching on string literals in match. Deferred to Phase 2 extension.
  • Non-copyable (~Copyable) enum variants for unique-ownership types. Out of v1 scope.