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.
-
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-formatrules) 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. -
Syntax validity guarantee. Every
Syntaxnode in theswift-syntaxtree carries a typed parent and typed children; constructing aFunctionDeclSyntaxrequires a parameter clause of the right type, a return type clause of the right type, and a body. The compiler-style invariants (awhereclause cannot appear without a generic parameter clause, athrowsclause cannot appear after arethrowsclause) are checked at the builder level, so the worst that happens at runtime is a panic in our codegen, not malformed text reachingswiftc. -
Lossless round-trip. SwiftSyntax preserves trivia (comments, whitespace) and is the same library
swiftcuses 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
SyntaxFactoryfor source emission;System.Reflection.Emitfor 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
.xcframeworkfor Apple platforms (multi-arch fat-bundle), a.soor static.afor Linux, a.dll/.libpair for Windows, or an.ipafor iOS distribution. The MEP-49 build system (§10 of the umbrella spec) drivesswift buildfor the simple case andxcodebuild -archiveorxcrunfor 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
Callopcode with named arguments; the target backend maps tofuncinvocation in Swift,INVOKEVIRTUALon JVM,callvirton CLR, or C ABI on the C target. - No assumption of manual memory management. aotir carries a
per-allocation
lifetimeannotation (stack,arena,heap). The C target readsstackand emits stack variables,arenaand emits arena allocations,heapand emitsmalloc. The Swift target reads all three and emits Swift values (stackbecomes a locallet,arenabecomes a value-typed struct on the stack,heapbecomes a reference-typed class or anArray/Dictionarythat 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 becomestructs, 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
libswiftCoreand the concurrency runtime, skipswiftc. - Emit SIL textually and pipe through
swiftc -emit-irgen-after-silto recover the rest.
Both are rejected. Five reasons to stay at the Swift source layer:
-
Debuggability. A user staring at "what did Mochi produce from this
uniondeclaration" can open the generated.swiftfile in Xcode, set a breakpoint, and step through. With LLVM IR the user would needlldbagainst the unstripped binary and the.dSYMplus a working DWARF expression evaluator. The cognitive load is multiple orders of magnitude higher. -
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.swiftand tell whether the output is sensible. Reviewing the LLVM IR equivalent is not realistic. -
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@_alwaysEmitIntoClientattributes. Library Evolution attribute semantics are baked intoswiftc's ABI computation; bypassingswiftcwould mean re-implementing Library Evolution lowering ourselves. -
Xcode integration. Source-level Swift drops straight into an Xcode project. Generated
.swiftfiles 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. -
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-syntaxas a static C-callable library via a thin wrapper. Rejected: swift-syntax has no C ABI; exposing one would require writing an@_cdeclshim 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/*.swiftdescription files (the same filesswift-syntaxuses 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-formatpost-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-formatcatches 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-formatconfig 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.pbecomes the Swift module nameMochi_m_n_p(with_separator). Public symbols are referenced asMochi_m_n_p.Symbol. The user can override the prefix via aswift_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: Mochiclassfield 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 Mochirecord Type { ... }in modulegeombecomespublic struct Mochi_geom_Type { ... }. The user can opt out with an attribute@swift_name("Type")on the declaration, in which case Mochi emitsMochi.geom.Typevia a nested type to avoid the top-level collision. -
Operator characters. Mochi identifiers permitting
?,!,'(prime) are escaped:foo?becomesfooOpt,foo!becomesfooBang,foo'becomesfooPrime. 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 Swiftfuncper instantiation, mangled name carries the type suffix. Inlining hint via@inlinableso 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 Swiftfuncwith 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
funlowers to a plain Swiftfuncwith noasync, nothrows. Impure Mochifunlowers to a plain Swiftfunc(Swift has no "impure" marker). Async Mochifunlowers to a Swiftasyncfunction (SE-0296). Failing Mochifunlowers 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@escapingannotation.
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
@Sendableclosures 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.modulemapfile 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-formatdefault. 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
publicon every exported declaration.internalis the Swift default and we omit it.privateis scoped narrower thanfileprivate; we useprivatefor per-declaration helpers,fileprivateonly when sharing across declarations in the same file. - Type annotations: explicit on every
publicdeclaration's signature, omitted on locals where inference is unambiguous (Library Evolution requires the signature to be ABI-stable; type inference defeats this). letovervar: every binding that does not need mutation is emitted aslet. The aotir lifetime annotation drives this.- Self qualification:
self.is emitted only inside closures that captureselfand in initialisers where it disambiguates parameter from field. - Operator alignment: Mochi infix operators map to Swift's
precedence groups via the
infix operatordeclaration 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.
FoundationbeforeOrderedCollectionsbeforeSwiftCollections. Theswift-formatOrderedImportsrule 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 Mochicomment 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
.swiftto 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.mapfile is checked into git alongside the generated.swiftso 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-placepost-processing (§8).swiftc/swift builddriving the compile (§4).- Source maps as sidecar
.mochi.mapfiles (§28). - Single
Package.swiftat 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
.swiftinterfaceemission (the Mochi-generated module is opt-in resilient via@frozenannotations; an explicit.swiftinterfacewould need a second pass throughswiftc -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-syntaxand 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
@deriveannotations (the user writes@derive(Equatable, Hashable)in Mochi; v2 emits a Swift macro invocation that calls into a Mochi-shipped macro library). .swiftinterfaceemission for ABI-stable distribution of Mochi-generated frameworks.- Direct
.xcframeworkpackaging for iOS distribution (currently this is a manualxcodebuildstep 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
- Swift 6.0 release notes, swift.org: https://www.swift.org/blog/swift-6/
- Swift Language Reference, current: https://docs.swift.org/swift-book/
- SE-0260 Library Evolution: https://github.com/apple/swift-evolution/blob/main/proposals/0260-library-evolution.md
- SE-0296 async/await: https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
- SE-0302 Sendable and @Sendable closures: https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
- SE-0304 Structured Concurrency: https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md
- SE-0306 Actors: https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md
- SE-0314 AsyncStream and AsyncThrowingStream: https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md
- SE-0335 Existential
any: https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md - SE-0337 Strict Concurrency: https://github.com/apple/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md
- SE-0354 Regex literals: https://github.com/apple/swift-evolution/blob/main/proposals/0354-regex-literals.md
- SE-0377 Borrowing and consuming parameters: https://github.com/apple/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md
- SE-0382 Expression Macros: https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md
- SE-0387 Cross-compilation destinations: https://github.com/apple/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md
- SE-0389 Attached Macros: https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md
- SE-0390 Noncopyable structs and enums: https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md
- SE-0397 Freestanding Declaration Macros: https://github.com/apple/swift-evolution/blob/main/proposals/0397-freestanding-declaration-macros.md
- SE-0410 Atomics: https://github.com/apple/swift-evolution/blob/main/proposals/0410-atomics.md
- SE-0413 Typed Throws: https://github.com/apple/swift-evolution/blob/main/proposals/0413-typed-throws.md
- SE-0414 Region-Based Isolation: https://github.com/apple/swift-evolution/blob/main/proposals/0414-region-based-isolation.md
- SE-0427 Noncopyable Generics: https://github.com/apple/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md
- apple/swift-syntax repository: https://github.com/apple/swift-syntax
- apple/swift-format repository: https://github.com/apple/swift-format
- apple/swift-collections (OrderedDictionary): https://github.com/apple/swift-collections
- SwiftPM Package.swift reference: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
- Swift ABI Stability manifesto: https://www.swift.org/blog/abi-stability-and-more/
- SIL.rst (Swift Intermediate Language docs, swiftlang/swift): https://github.com/swiftlang/swift/blob/main/docs/SIL.rst
- Source Map Revision 3 Proposal: https://sourcemaps.info/spec.html