Skip to main content

MEP-49 research note 05, Codegen design: choosing an IR layer for Mochi to Swift

Author: research pass for MEP-49 (Mochi to Swift transpiler). Date: 2026-05-23 10:10 (GMT+7). Target toolchain: Swift 6.0 floor (typed throws, complete strict concurrency, region-based isolation, ~Copyable, ~Escapable), forward compatible to Swift 6.x point releases. All platforms the official Swift toolchain supports: Apple (macOS 13+, iOS 16+, tvOS 16+, watchOS 9+, visionOS 1+), Linux (Ubuntu 22.04 / 24.04, Amazon Linux 2, Debian 12, Fedora 39, RHEL UBI 9), Windows 10/11 (swift.org distribution), Android (community toolchain), and WASI (SwiftWasm).

1. IR layer decision: SwiftSyntax over raw string concatenation

The first decision for Mochi to Swift code generation is the layer at which we serialise the program tree. Two practical options exist on the Swift platform in 2026: emit Swift source text by string concatenation, or build a SyntaxTree via Apple's official apple/swift-syntax package and serialise that tree. We choose the SwiftSyntax route, with one caveat (§7) about how the Go-side codegen pass actually links against it.

Three things tip the balance toward SwiftSyntax.

  1. Indentation correctness. Swift is brace-delimited and not indentation-sensitive at the parser level, but the official style (Apple's "Swift API Design Guidelines" plus the community-maintained swift-format rules) is rigid: 4-space indent, brace on the same line as the declaration. A string-concatenation emitter has to track an indent counter by hand; this is the most common source of cosmetic drift between machines or between toolchain versions. SwiftSyntax pretty-prints deterministically from the tree shape.

  2. Syntax validity guarantee. Every Syntax node in the swift-syntax tree carries a typed parent and typed children; constructing a FunctionDeclSyntax requires a parameter clause of the right type, a return type clause of the right type, and a body. The compiler-style invariants (a where clause cannot appear without a generic parameter clause, a throws clause cannot appear after a rethrows clause) are checked at the builder level, so the worst that happens at runtime is a panic in our codegen, not malformed text reaching swiftc.

  3. Lossless round-trip. SwiftSyntax preserves trivia (comments, whitespace) and is the same library swiftc uses for its own front end since SE-0354 (the regex literal work) and the Swift macro infrastructure (SE-0382, SE-0389, SE-0397). A Mochi codegen pass that emits a SwiftSyntax tree can be teed off to a Swift macro pipeline later (v2; see §29).

The single trade-off is the dependency footprint. swift-syntax compiles to roughly 10 MB of products on Apple silicon Release builds (split between SwiftSyntax, SwiftParser, SwiftSyntaxBuilder, SwiftBasicFormat, SwiftOperators, SwiftDiagnostics, SwiftSyntaxMacros). Cold compile takes 60-90 seconds on an M2 Pro; that compile happens once per CI worker per swift-syntax revision. This is why §7 prescribes a Go-native shadow tree rather than CGO-binding the Swift library.

2. No direct SIL emit

Swift Intermediate Language (SIL) is not a public IR. SIL is generated by swiftc internally between the typed AST and LLVM IR; its on-disk form (.sil) is documented under swift/docs/SIL.rst in the swiftlang/swift repository, and the -emit-sil and -emit-silgen flags exist for compiler-engineer debugging, but there is no stable Swift package, no stable Go binding, no stable textual format guarantee across compiler releases. A transpiler that wrote .sil would be relinked to every six-month Swift release.

Mochi never touches SIL. We emit Swift source text and let swiftc generate SIL, optimise it, and lower to LLVM IR.

Contrast with the other transpiler3 targets:

  • MEP-47 (JVM): ClassFile API (JEP 484, GA in JDK 24) is an officially stable, in-stdlib bytecode emitter. JVM bytecode has decades of stability and a published specification (JVMS).
  • MEP-48 (CLR): Roslyn SyntaxFactory for source emission; System.Reflection.Emit for IL fallback. Both are public APIs with documented stability.
  • MEP-49 (Swift): No equivalent stable IR below source. SIL is internal-only.

The lesson: a transpiler that targets Swift has exactly one stable input contract, the Swift source language as defined by the latest Swift Language Reference. Everything below that contract is private.

3. No bytecode

Swift has no JVM-style bytecode. Compilation goes Swift source -> typed AST -> SIL (raw, then canonical, then optimised) -> LLVM IR -> native object. There is no portable "Swift bytecode" artifact that runs on a Swift VM, because there is no Swift VM. Apple's toolchain ships native .o files (Mach-O on Apple platforms, ELF on Linux, COFF on Windows, Wasm on WASI) and links them with the Swift runtime (libswiftCore, libBlocksRuntime, the Concurrency runtime in libswift_Concurrency).

Practical consequences for Mochi:

  • One artifact per platform/architecture triple. A Mochi project shipped for macOS arm64 + iOS arm64 + Linux x86_64 produces three distinct binaries; cross-compilation is supported via swift build --triple (SE-0387, Swift SDKs / SE-0410 swift-sdk bundle format).
  • No "Swift JAR" equivalent. Mochi's package output is an .xcframework for Apple platforms (multi-arch fat-bundle), a .so or static .a for Linux, a .dll/.lib pair for Windows, or an .ipa for iOS distribution. The MEP-49 build system (§10 of the umbrella spec) drives swift build for the simple case and xcodebuild -archive or xcrun for the iOS case.
  • No JIT. Swift is AOT-only since v1. There is no Swift equivalent of HotSpot or RyuJIT. This simplifies the codegen story (we never emit a hot-path IL fallback the way MEP-48 might), at the price of slower edit-compile-run loops than the JVM target.

4. Pipeline diagram

+---------------------+
| Mochi source |
| *.mochi files |
+----------+----------+
|
v
+---------------------+
| parse, type check |
| (shared front end) |
+----------+----------+
|
v
+---------------------+
| aotir IR |
| (target-agnostic) |
+----------+----------+
|
v
+---------------------+ +-------------------------+
| monomorphisation | | shared with |
| pass (shared) |--->| MEP-45 / MEP-46 / |
+----------+----------+ | MEP-47 / MEP-48 |
| +-------------------------+
v
+---------------------+
| closure conversion |
| pass (shared) |
+----------+----------+
|
v
+---------------------+ [MEP-49 begins here]
| Swift codegen pass |
| ~4000 LOC Go |
+----------+----------+
|
v
+---------------------+
| SwiftSyntax tree |
| (Go shadow tree) |
+----------+----------+
|
v
+---------------------+
| pretty-print |
| canonical .swift |
+----------+----------+
|
v
+---------------------+
| swift-format |
| --in-place |
+----------+----------+
|
v
+---------------------+
| swiftc / swift |
| build (SwiftPM) |
+----------+----------+
|
v
+---------------------+
| .o / .dylib / |
| executable / .ipa |
+---------------------+

The boxes above the "MEP-49 begins here" line are shared with the other transpiler3 targets. The boxes below are Swift-specific. The total Swift-specific code budget is roughly 4000 lines of Go for the codegen pass plus 600 lines for the package/file layout writer and 400 lines for the swift-format integration shim.

5. aotir IR reuse

The aotir IR designed for MEP-45 (Mochi to C) is target-agnostic by construction. It is a typed, monomorphised, closure-converted representation of Mochi programs with explicit lifetimes for stack allocation. Three properties make it reusable for Swift:

  • No assumption of C calling conventions. aotir uses an abstract Call opcode with named arguments; the target backend maps to func invocation in Swift, INVOKEVIRTUAL on JVM, callvirt on CLR, or C ABI on the C target.
  • No assumption of manual memory management. aotir carries a per-allocation lifetime annotation (stack, arena, heap). The C target reads stack and emits stack variables, arena and emits arena allocations, heap and emits malloc. The Swift target reads all three and emits Swift values (stack becomes a local let, arena becomes a value-typed struct on the stack, heap becomes a reference-typed class or an Array/Dictionary that ARC manages).
  • No assumption of nominal vs structural typing. aotir tracks whether a type is nominal (Mochi record Foo { ... }) or structural (Mochi tuple (int, string)). Swift has both: nominal types become structs, structural types become Swift tuples ((Int, String)).

The MEP-49 Swift codegen pass is roughly 4000 LOC in Go:

  • ~1200 LOC: SwiftSyntax tree builder (one Go type per node kind we emit, plus serialisation).
  • ~1100 LOC: aotir-to-SwiftSyntax lowering rules (one function per aotir opcode family).
  • ~500 LOC: name mangling and module layout.
  • ~400 LOC: closure-to-Swift-closure ABI selection (@escaping, @Sendable, inout).
  • ~300 LOC: actor/agent lowering.
  • ~250 LOC: error type lowering (typed throws SE-0413).
  • ~250 LOC: deterministic ordering pass (§27).

This matches the budget MEP-47 reports for its Java codegen (~3800 LOC) and is well within the size a single contributor can hold in their head.

6. Why emit Swift source, not LLVM IR directly

Two reasonable alternatives to "emit Swift source" exist for a Swift-targeting transpiler:

  • Emit LLVM IR directly, link with libswiftCore and the concurrency runtime, skip swiftc.
  • Emit SIL textually and pipe through swiftc -emit-irgen-after-sil to recover the rest.

Both are rejected. Five reasons to stay at the Swift source layer:

  1. Debuggability. A user staring at "what did Mochi produce from this union declaration" can open the generated .swift file in Xcode, set a breakpoint, and step through. With LLVM IR the user would need lldb against the unstripped binary and the .dSYM plus a working DWARF expression evaluator. The cognitive load is multiple orders of magnitude higher.

  2. Reviewability. Mochi's golden test corpus (see MEP-49 §11 of the umbrella) checks the emitted Swift into git. A reviewer can read Sources/MochiUser/Foo.swift and tell whether the output is sensible. Reviewing the LLVM IR equivalent is not realistic.

  3. Library Evolution. Swift's ABI stability story (SE-0260 Library Evolution, SE-0258 Property Wrappers, SE-0297 actor isolation for ABI) operates at the source declaration level via the @frozen, @usableFromInline, @inlinable, and @_alwaysEmitIntoClient attributes. Library Evolution attribute semantics are baked into swiftc's ABI computation; bypassing swiftc would mean re-implementing Library Evolution lowering ourselves.

  4. Xcode integration. Source-level Swift drops straight into an Xcode project. Generated .swift files appear in the Project Navigator, get indexed by SourceKit, support Quick Help, support code-completion-on-Mochi-generated-API, and benefit from Xcode's incremental rebuild dependency graph. LLVM IR products do not.

  5. ARC and Sendable checking. The Swift compiler runs ownership checking (SE-0377 borrowing/consuming parameters, SE-0390 noncopyable types, SE-0427 noncopyable generics) and strict concurrency checking (SE-0337 strict concurrency, SE-0414 region based isolation) at the source-to-SIL boundary. Generating LLVM IR by hand skips those checks. Mochi's whole point is to give users a safer source language than what they would write; if our generated Swift type-checks and Sendable-checks clean, we know we have not introduced data races. If we skip the checker we are on our own.

Swift source is the contract. Everything below is swiftc's job.

7. Codegen pass implementation language

Go, consistent with the other transpiler3 targets. Three options were considered for how Go talks to the Swift syntax tree:

  • Option A: CGo binding to libSwiftSyntax. Build apple/swift-syntax as a static C-callable library via a thin wrapper. Rejected: swift-syntax has no C ABI; exposing one would require writing an @_cdecl shim layer covering every node type, which is roughly the same amount of code as just building a shadow tree directly.

  • Option B: JSON Schema serialisation. Generate a Go data structure mirroring SwiftSyntax's CodeGeneration/Sources/SyntaxSupport/*.swift description files (the same files swift-syntax uses to generate its own Swift types). Serialise the Go tree to JSON, ship to a sidecar Swift process that deserialises and pretty-prints.

  • Option C: Go-native shadow tree. Build a Go data structure that mirrors SwiftSyntax's tree shape, with one Go type per node kind. Render to canonical Swift source text directly from Go, with no Swift process in the loop at build time.

Option C wins for Mochi. The reasons:

  • Mochi's pre-built binary distribution must be a single static Go executable; we do not want a Swift toolchain dependency just to compile Mochi itself.
  • The set of node kinds Mochi emits is a strict subset of SwiftSyntax (roughly 60 node kinds out of 230+). The shadow tree is small.
  • Canonical pretty-printing is a deterministic walk over the tree with fixed indent/brace rules. About 600 LOC.
  • We still shell out to swift-format post-emit (§8) for belt-and-braces formatting compliance.

The Go package path is github.com/mochilang/mochi/transpiler3/swift/sxtree. "sxtree" stands for Swift syntax tree. Each node looks roughly like:

type FunctionDecl struct {
Attributes []Attribute
Modifiers []Modifier
Name Identifier
Generics *GenericParameterClause
Params ParameterClause
Effects EffectSpecifiers // async/throws
ReturnType *Type
WhereClause *GenericWhereClause
Body *CodeBlock
}

Serialisation is a func (n *FunctionDecl) Render(w *Writer) method that emits canonical Swift text. The whole tree implements a single Node interface with Render and Kind methods.

8. swift-format integration

After Mochi writes a .swift file, the codegen pipeline shells out to swift-format format --in-place (or --mode format on older toolchains; we detect the toolchain version at the start of the build and pick the right invocation). swift-format has shipped with the Swift toolchain since the Swift 5.8 release in March 2023 (prior to that it was a separate apple/swift-format repository); on Swift 6.0 it is bundled and discoverable via swift-format --version.

Mochi ships a .swift-format config file at the package root:

{
"version": 1,
"indentation": { "spaces": 4 },
"lineLength": 100,
"respectsExistingLineBreaks": true,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"prioritizeKeepingFunctionOutputTogether": true,
"indentConditionalCompilationBlocks": true,
"rules": {
"AlwaysUseLowerCamelCase": true,
"NoBlockComments": false,
"OrderedImports": true,
"UseShorthandTypeNames": true
}
}

Three reasons we run swift-format even though our pretty-printer already produces canonical text:

  • Belt and braces. If a future change in our Go pretty-printer introduces a regression (extra blank line, missing space after comma), swift-format catches and fixes it before the file is committed to the golden corpus.
  • Community alignment. swift-format's rules align with the Apple-shepherded Swift style. Reviewers reading Mochi-generated Swift get exactly the look they expect from hand-written Swift.
  • Configurable. Users with strong opinions can override the .swift-format config in their project; Mochi respects the user's config and re-runs the formatter, so the user-facing source matches their house style.

We do not depend on swift-format for correctness, only for cosmetics. If swift-format is unavailable on the build host, the build emits a warning and proceeds with our pretty-printer's output.

9. Name mangling

Mochi names map to Swift names by a deterministic rule.

  • Module prefix. Every Mochi module m.n.p becomes the Swift module name Mochi_m_n_p (with _ separator). Public symbols are referenced as Mochi_m_n_p.Symbol. The user can override the prefix via a swift_module = "Foo" package directive in the Mochi build manifest.

  • Reserved word handling. Swift reserved words (class, struct, enum, func, var, let, if, else, for, ...) collide rarely because Mochi's own grammar avoids most of them. The few that do clash get wrapped in backticks: Mochi class field becomes Swift `class` (Swift permits any reserved word as an identifier when backticked, SE-0231 for raw identifiers via backticks predates Swift 5).

  • Stdlib name collisions. Some Mochi types collide with Swift stdlib types: Type, Any, String, Array. We never reuse these names directly for Mochi-generated types; instead we prefix with the module: a Mochi record Type { ... } in module geom becomes public struct Mochi_geom_Type { ... }. The user can opt out with an attribute @swift_name("Type") on the declaration, in which case Mochi emits Mochi.geom.Type via a nested type to avoid the top-level collision.

  • Operator characters. Mochi identifiers permitting ?, !, ' (prime) are escaped: foo? becomes fooOpt, foo! becomes fooBang, foo' becomes fooPrime. The escape table is in the type-lowering note (06-type-lowering §3).

  • Monomorphisation suffix. Specialised instances get a six-hex suffix derived from BLAKE3 over the instantiation arguments: map$__inst_a1b2c3. This matches the convention MEP-47 uses on the JVM target.

Two emitted Swift identifiers never collide across modules or generic specialisations. The mangling table is reversible via a sidecar .mangle.json file shipped alongside the generated Package.swift.

10. Source layout

Default layout: one .swift file per Mochi module, named after the module with _ separators flattened to directory separators. A Mochi module geom.shapes.circle produces Sources/Mochi_geom_shapes_circle/Mochi_geom_shapes_circle.swift.

Optional layout: one .swift file per Mochi top-level declaration, behind a --swift-split-by-decl flag. The per-declaration mode is IDE-friendly (Xcode loves small files, SourceKit indexes them faster), at the cost of more file-system churn during incremental builds.

A Package.swift is emitted at the project root:

// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "MochiOut",
platforms: [.macOS(.v13), .iOS(.v16)],
products: [
.library(name: "MochiOut", targets: ["MochiOut"])
],
targets: [
.target(name: "MochiOut",
path: "Sources/MochiOut",
swiftSettings: [.unsafeFlags(["-strict-concurrency=complete"])])
]
)

The swift-tools-version line pins to 6.0 to match the language floor. If the user targets a higher version, Mochi re-emits with the appropriate tools-version comment. The platforms list reflects the deployment targets declared in the Mochi manifest.

The generated Sources/ tree is laid out per SwiftPM convention: one directory per target, source files at the top level of the directory or under Sources/<Target>/<SubModule>/ for nested modules. Tests go in Tests/MochiOutTests/. A Resources/ sibling directory holds embedded resources (Mochi @embed files become Resource.process(...) calls in the target spec).

11. Monomorphisation

Swift generics are reified, not erased. A func foo<T>(x: T) -> T keeps the type parameter at runtime via a witness table. This differs from JVM erasure (<T> -> Object) and is closer to C++ templates (one instantiation per type argument). The Swift compiler performs whole-module specialisation when invoked with -O -whole-module-optimization (the default for Release configurations); under -Onone (the default for Debug) generics go through the witness-table indirection.

Mochi's shared monomorphisation pass produces specialised non-generic types where the call graph shows a hot, narrow instantiation set, and leaves generic declarations alone where the instantiation set is wide or unknown at codegen time. Two emit modes per generic Mochi function:

  • #[mono=specialise]: emit one concrete Swift func per instantiation, mangled name carries the type suffix. Inlining hint via @inlinable so cross-module callers also inline.
@inlinable
public func foo_Int(x: Int) -> Int { x &+ 1 }

@inlinable
public func foo_String(x: String) -> String { x + "!" }
  • #[mono=generic]: emit one generic Swift func with the Mochi type parameters mapped one-to-one to Swift type parameters.
@inlinable
public func foo<T: MochiAddable>(x: T) -> T {
x.adding(.one)
}

The default policy: specialise when the instantiation set has 4 or fewer elements and at least one of them is a struct (Swift's struct calling convention benefits the most from specialisation), generic otherwise. The heuristic mirrors what swiftc -O does internally; we do it at codegen so users on -Onone see the same performance.

12. Closure conversion

Mochi closures lower to Swift closures with explicit ABI annotations.

A simple non-capturing closure:

let inc: (Int) -> Int = { x in x &+ 1 }

A capturing closure where the capture is by value:

let bump: (Int) -> Int = { [base] x in x &+ base }

A capturing closure where the capture is by reference (Mochi &x or a captured var):

let observe: (Int) -> Void = { [counter] x in counter.value += x }
// counter is a class instance; .value mutation is the reference path

A closure crossing an actor boundary:

let action: @Sendable (Int) -> Void = { x in
print("got \(x)")
}

The closure-conversion pass annotates each closure with three bits: escapes (@escaping), sendable (@Sendable), and capture mode (by-value vs by-reference). The emitter picks the right Swift syntax. For typed-throws closures (SE-0413):

let parse: (String) throws(ParseError) -> Int = { s in
guard let n = Int(s) else { throw .badNumber(s) }
return n
}

Captured vars are reified to a heap class with a single mutable field, mirroring what swiftc would do for an escaping closure that captures a mutable. We emit the box explicitly so the user can see the heap allocation in the generated code and reason about performance.

13. Sum-type lowering

Mochi union (algebraic data type) maps to Swift enum with associated values. Recursive variants use indirect case.

Mochi:

union Tree {
Leaf(int)
Node(Tree, Tree)
}

Swift:

public enum Tree {
case leaf(Int)
indirect case node(Tree, Tree)
}

indirect case was finalised long before Swift 6.0 (SE-0036 era). The indirect keyword on a case introduces an automatic box so the enum's storage is finite.

Mochi match lowers to Swift switch:

func depth(_ t: Tree) -> Int {
switch t {
case .leaf: return 0
case .node(let l, let r): return 1 + max(depth(l), depth(r))
}
}

The Swift compiler enforces exhaustiveness; if the Mochi match is non-exhaustive, the type checker has already rejected it. We never emit default: arms for fully-covered matches because exhaustive arms compile to a dense jump table while a default arm forces a guarded comparison ladder.

For variants with payload labels (Mochi named-field variants), Swift enums accept labelled associated values (SE-0155):

public enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}

The labels round-trip through case .circle(let radius): patterns, so Mochi destructuring with named fields stays readable.

14. Record lowering

Mochi record maps to Swift struct. For public records crossing ABI boundaries we add @frozen so the layout becomes part of the ABI (SE-0260 Library Evolution; @frozen opts the struct out of resilience and locks the field layout).

Mochi:

record Point { x: float, y: float }

Swift:

@frozen public struct Point: Equatable, Hashable, Sendable {
public var x: Double
public var y: Double

@inlinable
public init(x: Double, y: Double) {
self.x = x
self.y = y
}
}

Mochi with-expressions (functional update) lower to a with method that returns a new copy:

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

The optional parameter pattern lets the user write p.with(x: 3) and leave y unchanged. The closure-counter form (positional with) is preserved via overloads when the Mochi program uses it.

Sendable conformance is synthesised when every stored field's type is Sendable; this lets the record cross actor boundaries without runtime checks (SE-0302 strict concurrency synthesised conformances).

15. Function lowering

Mochi fun -> Swift func. Three orthogonal axes:

  • Effects. Mochi tracks pure / impure / async / fails. Pure Mochi fun lowers to a plain Swift func with no async, no throws. Impure Mochi fun lowers to a plain Swift func (Swift has no "impure" marker). Async Mochi fun lowers to a Swift async function (SE-0296). Failing Mochi fun lowers to a Swift typed-throws function (SE-0413, finalised in Swift 6.0).

  • Return type. Mochi return types map one-to-one to Swift return types via the type-lowering note (06-type-lowering).

  • Generics. See §11.

A Mochi function that is async-and-throws:

public func fetch(_ url: URL) async throws(NetworkError) -> Data {
try await session.data(from: url)
}

The typed-throws form replaces the untyped throws we would have written before Swift 6.0. throws(MochiError) produces a more precise diagnostic on the caller side and avoids existential boxing of the error.

Mochi @main-style entry points (the user's fun main()) lower to:

@main
public enum MochiMain {
public static func main() async throws {
try await Mochi_user.main()
}
}

The trampoline exists so the user's Mochi main can be async without forcing the Swift @main declaration onto a Mochi-side declaration the user did not write.

16. Method lowering

Mochi methods on records can lower to Swift in two ways: inline in the struct definition, or in an extension. We default to extension-grouped methods because it lets us emit one logical unit per Mochi declaration without rewriting the struct each time:

@frozen public struct Point {
public var x: Double
public var y: Double
}

extension Point {
@inlinable
public func magnitude() -> Double {
(x * x + y * y).squareRoot()
}
}

extension Point {
@inlinable
public func translated(by d: Point) -> Point {
Point(x: x + d.x, y: y + d.y)
}
}

Each extension block contains a single method, keyed by source position in the original Mochi file. This shape interacts well with Xcode's "Jump to Definition", which prefers small extensions.

For protocols (Mochi trait), the lowering uses Swift protocol declarations with default implementations in an extension:

public protocol Shape {
func area() -> Double
}

extension Shape {
public func areaDescription() -> String {
"area=\(area())"
}
}

Mochi trait default-method bodies map to the extension's default implementation slot. Concrete types adopting the trait get extension Concrete: Shape { ... } with the required method.

17. Closure ABI

Swift closures have a documented runtime representation: a two-word { function pointer, context pointer } pair. When the closure does not escape its caller, the context pointer can be stack-allocated; when it does escape, the context lives on the heap and is retained/released via ARC.

The Mochi closure-conversion pass picks the ABI annotation at emit time:

  • Non-escaping closure: default. Used for forEach-style inline callbacks. No @escaping annotation.
public func forEach(_ body: (Int) -> Void) {
for x in storage { body(x) }
}
  • Escaping closure: when the closure is stored in a field or passed to an API that retains it.
public func register(_ handler: @escaping (Event) -> Void)
  • Sendable closure: when the closure crosses an actor boundary (the standard library defines @Sendable closures as those that are safe to share across isolation domains; SE-0302).
public func dispatch(_ work: @Sendable @escaping () async -> Void)

The pass also picks the capture mode. Mochi &x (by-reference) becomes a closure capture of a heap-allocated reference type holding the mutable field; the closure list has no annotation because Swift's default is by-reference for vars and by-value for lets. Mochi by-value captures become [x] capture lists.

18. Datalog lowering

Mochi's Datalog-flavoured rules ([[../0030]] umbrella) lower to Swift in two pieces: facts as values, rules as recursive functions with memoisation.

A fact is a struct conforming to Hashable:

public struct ParentFact: Hashable, Sendable {
public let parent: String
public let child: String
}

The fact base is a typed dictionary keyed by predicate:

public final class FactBase {
public var parent: Set<ParentFact> = []
public var ancestor: Set<AncestorFact> = []
}

A rule lowers to a function that walks the fact base and emits derived facts; the bottom-up semi-naive evaluator runs to fixpoint:

public func deriveAncestor(_ db: FactBase) {
var delta: Set<AncestorFact> = []
repeat {
delta.removeAll()
for p in db.parent {
let fact = AncestorFact(ancestor: p.parent, descendant: p.child)
if !db.ancestor.contains(fact) { delta.insert(fact) }
}
for a in db.ancestor {
for p in db.parent where p.child == a.ancestor {
let fact = AncestorFact(ancestor: p.parent, descendant: a.descendant)
if !db.ancestor.contains(fact) { delta.insert(fact) }
}
}
db.ancestor.formUnion(delta)
} while !delta.isEmpty
}

The memoisation cache uses OrderedDictionary from apple/swift-collections to preserve insertion order for deterministic output (matches vm3's expected ordering). Caches are keyed by the rule's argument tuple; cache eviction is not performed (Datalog programs are finite and small in practice).

19. Query DSL lowering

Mochi's from / where / select / group_by / order_by query expressions lower to chained Swift Sequence operations. Each clause becomes one combinator call.

Mochi:

from u in users
where u.age >= 18
order_by u.name
select u.name

Swift:

users
.lazy
.filter { $0.age >= 18 }
.sorted { $0.name < $1.name }
.map { $0.name }

.lazy avoids materialising intermediate arrays. .sorted cannot be lazy (it has to see every element to sort), so the sort fuses the laziness off; the final .map runs eagerly. For group_by, Mochi uses OrderedDictionary from swift-collections:

import OrderedCollections

let byCountry: OrderedDictionary<String, [User]> = users.reduce(into: [:]) {
$0[$1.country, default: []].append($1)
}

The OrderedDictionary preserves insertion order, matching vm3's expectation for group_by results. A plain [String: [User]] would hash-shuffle the keys and produce non-deterministic golden output across machines.

For join-style queries Mochi lowers to nested .flatMap calls when the join is small; for larger joins the pass emits an explicit hash-join (a build phase populating an OrderedDictionary<JoinKey, [Right]> followed by a probe phase walking the left side).

20. Agent lowering

Mochi agent Foo { ... } lowers to a Swift actor with an AsyncStream-backed mailbox.

Mochi:

agent Counter {
state count: int = 0
fun bump() { count = count + 1 }
fun get() -> int { count }
}

Swift:

public actor Counter {
private var count: Int = 0

public func bump() async {
count &+= 1
}

public func get() async -> Int {
count
}
}

For agents that need explicit message-passing (Mochi send and receive), the pass adds an AsyncStream<Message> mailbox and a private runMailboxLoop():

public actor Worker {
public enum Message: Sendable {
case bump
case get(CheckedContinuation<Int, Never>)
}

private var count = 0
private let inbox: AsyncStream<Message>
private let send: AsyncStream<Message>.Continuation

public init() {
var c: AsyncStream<Message>.Continuation!
self.inbox = AsyncStream { c = $0 }
self.send = c
Task { await self.runMailboxLoop() }
}

private func runMailboxLoop() async {
for await msg in inbox {
switch msg {
case .bump: count &+= 1
case .get(let cont): cont.resume(returning: count)
}
}
}
}

Actor isolation (SE-0306 actors, SE-0414 region-based isolation in Swift 6.0) ensures the mailbox is touched by exactly one task at a time. The mailbox Continuation is Sendable, so external code can send messages from any context.

21. Stream lowering

Mochi streams (stream Foo { ... }) lower to Swift AsyncSequence. The default backing is AsyncStream<Element> (SE-0314), which is non-throwing and unicast; for throwing streams we use AsyncThrowingStream<Element, Error>.

Mochi await foreach x in stream { ... }:

for await x in stream {
process(x)
}

Mochi await foreach x in stream { ... } with a throwing stream:

for try await x in stream {
try process(x)
}

Producing a stream from a callback-based source:

public func ticks(every interval: Duration) -> AsyncStream<Date> {
AsyncStream { continuation in
let task = Task {
while !Task.isCancelled {
try? await Task.sleep(for: interval)
continuation.yield(Date())
}
continuation.finish()
}
continuation.onTermination = { _ in task.cancel() }
}
}

The Mochi front end tracks whether a stream is finite or infinite; infinite streams get a Task.isCancelled check in the producer loop. Stream cancellation propagates via Swift's cooperative cancellation model (SE-0304 structured concurrency).

22. Error lowering

Mochi Result<T, E> lowers to Swift Result<T, E> (stdlib type since Swift 5.0):

public func parse(_ s: String) -> Result<Int, ParseError> {
guard let n = Int(s) else { return .failure(.badNumber(s)) }
return .success(n)
}

Mochi try / catch (planned for the language; not in v1) lowers to Swift do / catch with typed throws (SE-0413, finalised in Swift 6.0):

do {
let n = try parse(input)
use(n)
} catch let e as ParseError {
handle(e)
}

Mochi panic (unrecoverable error) lowers to fatalError:

fatalError("Mochi panic: \(reason)")

Mochi's error hierarchy lowers to a Swift enum conforming to Error:

public enum MochiError: Error {
case division
case overflow
case ioError(String)
}

Typed throws on Mochi functions become typed throws on Swift functions:

public func divide(_ a: Int, _ b: Int) throws(MochiError) -> Int {
if b == 0 { throw .division }
return a / b
}

The Swift 6.0 typed-throws diagnostic is much more precise than the pre-6.0 untyped form; callers learn which errors a function can produce without reading the body.

23. FFI lowering

Mochi extern fun foo(...) -> ... interoperates with three foreign worlds: C, Objective-C, and other Swift modules.

C interop. Mochi declares the C module via an extern module directive; codegen emits:

  • A module.modulemap file describing the C headers.
  • A Swift import line.
  • Direct call sites using the imported C symbols.
// module.modulemap
module CMochiSqrt {
header "sqrt.h"
export *
}
import CMochiSqrt

@inlinable
public func sqrtF(_ x: Double) -> Double {
cSqrt(x)
}

For symbols Mochi wants to call without a header (private link), codegen uses @_silgen_name:

@_silgen_name("my_c_function")
internal func my_c_function(_ x: Int32) -> Int32

The @_silgen_name attribute is technically underscored (private to the standard library); we use it sparingly and prefer the module.modulemap route. The trade-off is documented in the FFI note.

Objective-C interop. On Apple platforms, Mochi types annotated @objc_compatible lower to Swift @objc classes inheriting from NSObject. This is opt-in; the default Mochi-to-Swift lowering uses value types.

Swift-to-Swift interop. Mochi import other_module (where other_module is itself a Mochi module) lowers to a Swift import Mochi_other_module. Cross-module calls go through the public symbol surface emitted by §10's package layout.

24. String lowering

Mochi string lowers to Swift String. Swift's String has been UTF-8-backed since Swift 5.7 (SE-0335 implicit existential opening landed in the same release; the UTF-8 backing was an implementation change documented in swift/docs/StringDesign.md). No encoding conversion is needed at boundaries.

let greeting: String = "Hello, " + name

Mochi string concatenation lowers to Swift + for two operands and to string interpolation for three or more, on the principle that interpolation is more efficient than chained concatenation:

let s = "Hello, \(name) from \(city)!"

For byte-oriented operations Mochi bytes lowers to Swift Data (Foundation) or [UInt8] (no Foundation). The default is [UInt8] to keep Foundation off the dependency list; the user can opt into Data for serialisation contexts via a package flag.

Mochi string slicing lowers to String.Index-based slicing:

let prefix = s[s.startIndex..<s.index(s.startIndex, offsetBy: 5)]

We do not emit integer-indexed slicing because Swift's String does not provide it (random access by codepoint is not O(1)). The type-lowering note has the full mapping table.

25. JSON lowering

Mochi's JSON support has two modes: typed (Codable) and dynamic (JSONValue).

Typed mode. Mochi record types annotated as JSON-serialisable lower to Swift structs conforming to Codable:

public struct User: Codable, Sendable {
public let id: Int
public let name: String
public let tags: [String]
}

Encoding and decoding use the stdlib JSONEncoder and JSONDecoder:

let data = try JSONEncoder().encode(user)
let u = try JSONDecoder().decode(User.self, from: data)

Dynamic mode. Mochi json literal type (where the schema is not known at compile time) lowers to a custom JSONValue enum:

public enum JSONValue: Sendable, Codable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([JSONValue])
case object([String: JSONValue])
}

The enum implements its own init(from: Decoder) and encode(to: Encoder) so it round-trips through JSONEncoder and JSONDecoder without JSONSerialization. The dynamic mode is deliberately Foundation-free at the type level, which lets Mochi ship on platforms without Foundation (an embedded Linux build with a stripped runtime).

For Mochi json literal expressions (compile-time JSON literals), the codegen pass elaborates them into nested .object([...]) and .array([...]) constructors at emit time so there is no runtime parse.

26. Generated code style

The Apple-shepherded Swift style applies:

  • Indent: 4 spaces, never tabs. Matches the swift-format default. K&R brace style ({ on the same line as the declaration); Allman is not used in Swift idiom.
  • Trailing commas: allowed in argument lists, array literals, dictionary literals, generic clauses, and parameter lists per SE-0084 trailing commas; we emit them for multi-line literals to minimise diff noise on append.
  • Modifiers: explicit public on every exported declaration. internal is the Swift default and we omit it. private is scoped narrower than fileprivate; we use private for per-declaration helpers, fileprivate only when sharing across declarations in the same file.
  • Type annotations: explicit on every public declaration's signature, omitted on locals where inference is unambiguous (Library Evolution requires the signature to be ABI-stable; type inference defeats this).
  • let over var: every binding that does not need mutation is emitted as let. The aotir lifetime annotation drives this.
  • Self qualification: self. is emitted only inside closures that capture self and in initialisers where it disambiguates parameter from field.
  • Operator alignment: Mochi infix operators map to Swift's precedence groups via the infix operator declaration mechanism. Default precedence groups (AdditionPrecedence, etc.) cover the common cases; rare ones get a custom group.

A representative emitted file:

// Auto-generated by Mochi 0.x from geom/shapes.mochi
// Do not edit; re-run `mochi build --target swift` to regenerate.

import Foundation
import OrderedCollections

@frozen public struct Circle: Equatable, Hashable, Sendable {
public var radius: Double

@inlinable
public init(radius: Double) {
self.radius = radius
}
}

extension Circle {
@inlinable
public func area() -> Double {
.pi * radius * radius
}
}

27. Deterministic output

Byte-identical Swift source across machines, across runs, across operating systems. This is a hard requirement for golden tests and for the issue-per-PR review workflow. The deterministic-ordering pass enforces:

  • Imports sorted alphabetically. Foundation before OrderedCollections before SwiftCollections. The swift-format OrderedImports rule re-checks this.
  • Top-level declarations ordered by source position in the Mochi file. A Mochi declaration's line/column in the input controls its position in the output, not the order of insertion into the codegen tree.
  • Dictionary literal entries sorted by key. For [String: T] literals where Mochi's source did not specify an order, keys sort lexicographically. Where order matters (OrderedDictionary), Mochi preserves the user-given order.
  • Set literal entries sorted by canonical hash. Same rule.
  • Stable extension order. Methods on a record emit in source position order; one extension per method (§16).
  • Stable closure naming. Anonymous closures get a name derived from BLAKE3 over their captured-variable list and body fingerprint, so two structurally identical closures in the same file always produce the same Swift symbol.
  • No timestamps in the output. The Auto-generated by Mochi comment carries the Mochi version, not the build time. A reproducible build produces a byte-identical artifact regardless of when it ran.

This determinism contract is verified by the MEP-49 gate test TestSwiftDeterminism, which compiles the same Mochi corpus twice and cmps the outputs.

28. Source maps

Mochi-to-Swift line maps are emitted as a sidecar .mochi.map file per generated .swift. The format mirrors the Source Map v3 specification (originally a Chrome/Firefox JS source-map format, since adopted by TypeScript, Dart, Kotlin/JS, and others):

{
"version": 3,
"file": "Mochi_geom_shapes.swift",
"sources": ["geom/shapes.mochi"],
"names": ["Circle", "area", "radius"],
"mappings": "AAAA,SAAS;EACP,..."
}

The map lets debugger UIs (Xcode's lldb, VS Code's Swift extension, IntelliJ's Swift plugin) attribute Swift line numbers back to Mochi line numbers when stepping. The map is loaded by Mochi's own debugger adaptor (a DAP server living in transpiler3/swift/dap) which translates breakpoint requests from .mochi coordinates into the .swift coordinates the underlying Swift debugger understands.

Caveats:

  • LLDB's first-class line table maps .swift to native; the Mochi map gives us the second hop. The two hops are fused by the DAP adaptor.
  • DWARF support for Mochi source files (via a custom DWARF producer) is an alternative path explored in 10-build-system; the sidecar map is simpler for v1.
  • The .mochi.map file is checked into git alongside the generated .swift so reviewers can audit the position mapping.

29. v1 vs v2 scope

v1 ships:

  • Pure Swift source emission via the Go shadow tree (§7).
  • swift-format --in-place post-processing (§8).
  • swiftc / swift build driving the compile (§4).
  • Source maps as sidecar .mochi.map files (§28).
  • Single Package.swift at the package root (§10).
  • All target platforms via swift build --triple (§3).

v1 does not ship:

  • SwiftSyntax round-trip parsing (the Go shadow tree is write-only; we never re-parse our own output).
  • Swift Macro emission. Swift macros (SE-0382, SE-0389, SE-0397) require a swift-syntax host at build time, which would force Mochi users to install a Swift toolchain just to consume Mochi output. We avoid that. If a Mochi user needs macros, they can write a Swift macro target that consumes the Mochi-generated module.
  • DWARF-level source mapping (sidecar JSON instead, §28).
  • Library Evolution .swiftinterface emission (the Mochi-generated module is opt-in resilient via @frozen annotations; an explicit .swiftinterface would need a second pass through swiftc -emit-module-interface).

v2 ships, opt-in:

  • SwiftSyntax integration as an alternative codegen mode. Users who have a Swift toolchain installed can flip --swift-codegen =swift-syntax and get the same output, generated via the authoritative SwiftSyntax tree. The benefit is round-trip fidelity for tools that consume Mochi-generated Swift and want to re-serialise.
  • Swift Macro emission for Mochi @derive annotations (the user writes @derive(Equatable, Hashable) in Mochi; v2 emits a Swift macro invocation that calls into a Mochi-shipped macro library).
  • .swiftinterface emission for ABI-stable distribution of Mochi-generated frameworks.
  • Direct .xcframework packaging for iOS distribution (currently this is a manual xcodebuild step driven by the user's project configuration).

The split keeps v1 small (no Swift-toolchain dependency at Mochi build time) and keeps v2 ambitious without making v1 impossible.

Sources