Skip to main content

MEP-49 research note 06, Type-system lowering for MEP-49

Author: research pass for MEP-49 (Mochi to Swift transpiler). Date: 2026-05-23 (GMT+7).

This note specifies, type-by-type, how Mochi's type system lowers to Swift 6.0 types. It is the load-bearing reference for 05-codegen-design's codegen pass.

1. Primitive types

Mochi typeSwift typeBoxed (for any)Notes
intInt64AnyHashableAlways 64-bit signed, never Int.
floatDoubleAnyHashableIEEE 754 double precision.
boolBoolAnyHashable
stringStringAnyHashableUTF-8 native (Swift 5.7+).
bytesData or [UInt8]AnyHashableData for I/O, [UInt8] in core.
charCharacterAnyHashableExtended grapheme cluster.
unitVoidn/aOr ().
nullnil literaln/aUsed with Optional only.

Why Int64, not Int. Swift's Int is platform-pointer-width (32-bit on watchOS Series 3 and below, 64-bit elsewhere). Mochi int is fixed 64-bit. Always emit Int64 to keep semantics stable across platforms.

Why Double. Mochi float is 64-bit IEEE 754. Swift Double matches. Swift's Float80 is x86-only; Mochi never uses it.

Why Data vs [UInt8]. Data is Foundation.Data, heap-allocated with copy-on-write, optimised for I/O (URLSession, FileHandle). [UInt8] is the stdlib array, also COW. Mochi codegen picks Data for I/O boundaries and [UInt8] for in-memory manipulation.

2. Boxing and AnyHashable

Swift uses AnyHashable for type-erased hashable values. Mochi heterogeneous containers (e.g., list<any>) lower to [AnyHashable]. Any is reserved for non-hashable ([Any] works for the equivalent without dictionary-key use).

Boxing into Any is implicit in Swift. Unboxing requires conditional cast: if let x = a as? Int64 { ... }.

Mochi pattern matching on any lowers to a Swift switch with type patterns:

switch boxed {
case let i as Int64: ...
case let s as String: ...
default: ...
}

3. Reified generics

Swift generics are reified (the compiler specialises per type at WMO). Unlike JVM erasure, the runtime type is preserved. Mochi's generic functions lower directly:

fun first<T>(xs: list<T>): T { ... }

becomes:

public func first<T>(_ xs: [T]) -> T { ... }

No type erasure, no JVM-style synthetic accessors, no reflection boundary. The Swift compiler may specialise first<Int64> and first<String> separately under -O -whole-module-optimization, producing efficient monomorphic code; outside WMO it keeps a generic dispatch path.

Mochi's monomorphisation pass (shared with MEP-45/46/47/48) may pre-specialise hot paths in the codegen output, emitting both func firstInt64(_ xs: [Int64]) -> Int64 and the generic first<T> for cold paths.

4. Collections

MochiSwiftSource
list<T>[T] (Array<T>)stdlib
map<K,V>[K: V] (Dictionary<K,V>)stdlib
set<T>Set<T>stdlib
ordered_map<K,V>OrderedDictionary<K,V>apple/swift-collections
ordered_set<T>OrderedSet<T>apple/swift-collections
deque<T>Deque<T>apple/swift-collections
heap<T>Heap<T>apple/swift-collections

Hashable requirement. Swift Dictionary and Set require K: Hashable. Mochi's static check enforces this at the Mochi level. The codegen pass emits : Hashable constraints on generic type parameters.

Copy-on-write. Swift Array, Dictionary, Set, and the swift- collections variants are all COW. Mochi value semantics work naturally without explicit copy.

Why OrderedDictionary for group_by. Mochi's group_by produces output ordered by first-occurrence key. Swift Dictionary is unordered. OrderedDictionary from apple/swift-collections (1.1.x) gives insertion-ordered iteration with O(1) lookup, matching vm3's behavior.

5. Records

Mochi records lower to Swift struct:

record Point { x: int, y: int }

becomes:

@frozen public struct Point: Sendable, Hashable, Codable {
public var x: Int64
public var y: Int64
public init(x: Int64, y: Int64) {
self.x = x
self.y = y
}
}

Why @frozen. Tells the Swift compiler the struct layout is ABI-stable, enabling efficient cross-module access without indirect field offsets. Mochi codegen marks every record @frozen because Mochi records' field set is part of the type identity.

Why three protocol conformances by default. Sendable because Mochi records cross actor boundaries freely (all fields must be Sendable, enforced by Swift). Hashable because Mochi records can be set elements and dictionary keys. Codable because Mochi records serialise to/from JSON via the runtime.

with expressions. Mochi point with x: 10 lowers to:

extension Point {
public func with(x: Int64? = nil, y: Int64? = nil) -> Point {
Point(x: x ?? self.x, y: y ?? self.y)
}
}

This per-field-optional pattern keeps the call site readable: point.with(x: 10). The codegen pass emits one with method per record.

Memberwise initializer. Swift's compiler auto-generates a memberwise init for struct. Mochi codegen relies on this and doesn't emit one explicitly except when public ABI demands (@frozen public struct requires an explicit init for stable ABI under Library Evolution).

6. Sum types

Mochi unions lower to Swift enum with associated values:

union Tree { Leaf | Branch(left: Tree, value: int, right: Tree) }

becomes:

public indirect enum Tree: Sendable, Hashable, Codable {
case leaf
case branch(left: Tree, value: Int64, right: Tree)
}

indirect. Required when the enum is recursive. Mochi codegen detects recursion and emits indirect. The cost is one heap allocation per recursive case (Swift boxes the payload).

Lowercased cases. Swift convention is lowerCamelCase for enum cases. Mochi Leafcase leaf. Case-name mangling table is emitted alongside for FFI back to Mochi.

Pattern matching. Mochi match lowers to Swift switch:

switch tree {
case .leaf:
return 0
case let .branch(left, value, right):
return f(left) + value + f(right)
}

Swift switch is exhaustive. Mochi's exhaustiveness check is the same check; the Swift compiler will repeat it.

Guards. Mochi when clauses → Swift where:

case let .branch(_, value, _) where value > 0:
...

7. Option and Result

MochiSwift
option<T>T? (Optional<T>)
result<T, E>Result<T, E>

Both are built-in Swift types. No codegen work beyond direct lowering.

Option pattern. Mochi if-let some(x) = opt then ... lowers to Swift if let x = opt { ... }. Mochi some(x)Optional.some(x), Mochi nonenil.

Result pattern. Mochi match r { ok(x) -> ...; err(e) -> ... } lowers to:

switch r {
case let .success(x): ...
case let .failure(e): ...
}

Optional chaining. Mochi ?. lowers to Swift ?. directly.

8. Function and closure types

Mochi fun(int, int) -> int → Swift (Int64, Int64) -> Int64.

@Sendable. Closures crossing actor boundaries must be @Sendable. The codegen pass annotates based on Mochi's purity analysis: pure closures get @Sendable, closures that capture mutable state don't.

@escaping. Closures stored in struct fields or otherwise outliving the call must be @escaping. Mochi's escape analysis identifies these.

Captures. Mochi closures capture by value (Swift default for let bindings) or by reference (for actor-state). Capture lists like [x = x, weak self] are emitted when needed.

Async closures. Mochi async fun(T) -> R → Swift (T) async -> R or @Sendable (T) async -> R.

Throwing closures. Typed throws: (T) throws(E) -> R.

9. Generics and constraints

Mochi constraintSwift constraint
T: EqT: Equatable
T: OrdT: Comparable
T: HashT: Hashable
T: ShowT: CustomStringConvertible
T: Copyimplicit (Swift values are Copyable unless ~Copyable)
T: SendableT: Sendable

Multiple constraints. Mochi T: Eq + Hash → Swift T: Hashable (Hashable: Equatable implies Eq + Hash).

Where clauses. Mochi where T: Foo, U: Bar → Swift where T: Foo, U: Bar.

Associated types. Mochi protocols with associated types lower to Swift protocols with associatedtype.

10. Protocols (Mochi traits)

Mochi traits lower to Swift protocol. Default implementations become protocol extensions:

trait Show { fun show(self): string }

becomes:

public protocol Show {
func show() -> String
}

extension Show {
public func showDefault() -> String { String(describing: self) }
}

Protocol existentials. Swift 5.7+ requires any Protocol for existential types. Mochi Show as a value type lowers to any Show.

Opaque return types. Mochi -> some Show → Swift -> some Show.

Conformance synthesis. : Hashable, Codable, Sendable is often synthesised; Mochi codegen relies on this when all fields conform.

11. Non-copyable and non-escapable types

MochiSwift
linear T (planned)~Copyable T
borrowed T (planned)~Escapable T

Mochi v1 surface does not expose linear types but the codegen pass must handle them when added in v2. ~Copyable types must be either consumed or borrowed; the Swift compiler enforces this.

12. Strings

Mochi string → Swift String. Mochi assumes UTF-8; Swift's String is UTF-8-native since 5.7 (SE-0335). No encoding conversion at boundaries.

Substrings. Substring is a view into a String. Mochi slicing emits Substring for chains, converting to String only at the end via String(substr).

StringBuilder pattern. Mochi string_builder → Swift appending. For hot loops, Mochi codegen uses var s = "" plus s.append(...) or String(reserveCapacity:).

Regex literals. Mochi regex /.../ → Swift /.../ literal. Requires Swift 5.7+. The Swift Regex is type-safe with capture groups as tuple elements.

13. Dates and times

MochiSwift
instantDate (Foundation)
durationTimeInterval (typealias for Double)
zoned_datetimeMochiRuntime.ZonedDateTime (custom)
local_dateMochiRuntime.LocalDate (custom)

Why custom ZonedDateTime. Foundation Date is wall-clock only; it doesn't carry a time zone. Mochi requires zoned datetime serialisation. The runtime ships a ZonedDateTime struct with a Date plus a TimeZone, Codable to RFC 3339.

14. JSON values

Mochi JSON literal → MochiRuntime.JSONValue enum:

public indirect enum JSONValue: Sendable, Hashable, Codable {
case null
case bool(Bool)
case int(Int64)
case double(Double)
case string(String)
case array([JSONValue])
case object(OrderedDictionary<String, JSONValue>)
}

OrderedDictionary for objects. JSON object key order is preserved (matches vm3 behavior).

Codable conformance. Custom encode/decode using SingleValueContainer to dispatch by JSON node type.

15. Logic terms (Datalog)

Mochi facts and rules lower to:

public struct Predicate: Hashable, Sendable {
public let name: String
public let arity: Int
}

public struct Fact: Hashable, Sendable {
public let predicate: Predicate
public let args: [Term]
}

public indirect enum Term: Hashable, Sendable {
case constant(JSONValue)
case variable(String)
case functor(name: String, args: [Term])
}

Rule evaluation uses tabled SLG resolution implemented in MochiRuntime. See 01-language-surface §8.

16. Boundary types (FFI)

Mochi externSwift attribute
extern fun foo() -> ... from C@_silgen_name("foo") func foo()
Mochi function exported to C@_cdecl("mochi_foo") public func mochiFoo() -> ...
C structSwift struct matching C layout
C unionManual via UnsafeMutableRawPointer
C function pointer@convention(c) (T) -> R

Module map. For C library imports, Mochi emits a module.modulemap declaring the C module. Swift import CLibrary then sees the C symbols. The Mochi codegen pass writes the module.modulemap to out/Sources/<Module>.modulemap.

Bridging headers. Apple-platform-only mechanism. Mochi v1 uses module maps consistently (Linux/Windows compatible).

17. Sendability inference

Mochi codegen infers Sendable conformance:

  • All primitive types: Sendable.
  • Records where all fields are Sendable: Sendable.
  • Enums where all associated values are Sendable: Sendable.
  • Closures with no captures or only Sendable captures: @Sendable.
  • Actor types: Sendable by definition.
  • Classes: Sendable only if all stored properties are Sendable and immutable (let bindings) or protected by a lock.

The codegen pass emits explicit : Sendable on every type that should be sendable. The Swift compiler verifies the conformance at build time; a Mochi codegen bug that misses a non-Sendable field fails the build, not just runtime.

18. Library Evolution mode

For Mochi.Runtime and other publicly versioned packages, Mochi codegen emits with -enable-library-evolution. This forces:

  • All public types @frozen or non-frozen explicitly.
  • All public functions have explicit return types (no some).
  • Public stored properties have backing computed-property trampolines (compiler-handled).

Mochi.Runtime targets stable ABI across Swift minor versions. User code (Mochi → Swift output for an application) does not use Library Evolution; it's a per-package decision in Package.swift.

19. Type-erasure helpers

Where Mochi needs runtime polymorphism (heterogeneous lists), Swift provides AnyHashable, AnySequence, AnyIterator, etc. Mochi codegen prefers any Protocol over type-erased wrappers when Swift 5.7+ existential types suffice.

For protocols with Self requirements or associated types, an existential is invalid; Mochi codegen falls back to a type-erased wrapper.

20. Summary table

Mochi kindSwift kindSendableCopyableABI-frozen by default
primitiveInt64/Double/Bool/String/Datayesyesyes
record@frozen structyesyesyes
sumenumyesyesyes
traitprotocoln/an/ayes
generic fungeneric funcn/an/an/a
closure(T) -> R, +@Sendable as neededinferredyesn/a
collectionArray/Dictionary/Set/Ordered* (COW)yesyesyes
optionalOptionalinferredyesyes
resultResult<T, E>inferredyesyes
agentactoryesno (reference)no
streamAsyncSequence (AsyncStream)yesyesn/a
extern fun@_silgen_name / @_cdecl funcn/an/an/a
linear (v2)~Copyable structyesnoyes

Cross-references: 01-language-surface for the Mochi side; 05-codegen-design for the emit-pass mechanics; 09-agent-streams for actor/AsyncStream details; 11-testing-gates for the type-mapping regression gate.