May 2026 (v0.16.0)
v0.16.0 completes the Mochi-to-Swift transpiler. Every phase of MEP-49 is now landed and the MEP is marked Final. You can compile a Mochi source file to a runnable Swift binary on macOS 15, an iOS app bundle for the App Store, or a fully static single-file binary on Linux -- all without writing a line of Swift or touching Xcode configuration.
mochi build --target=swift-macos hello.mochi -o out/
./out/hello
The pipeline goes: parser.Parse -> types.Check ->
aotir.Lower -> colour.Analyse (async colouring pass) ->
lower.Lower (swift) -> sxtree nodes ->
emit.Emit (writes .swift files) -> swift build / xcodebuild
subprocess -> binary or .app bundle. No Xcode project files or
Swift Package Manager manifests need to be written by hand;
Driver.Build generates everything from the Mochi source alone.
1. Swift transpiler: MEP-49 Final
MEP-49 covers 18 phases from hello-world through scalars, collections,
records, sum types, closures, queries, Datalog, final class agents,
synchronous broadcast streams, async/await colouring, file I/O, LLM
cassette generation, HTTP fetch, iOS project generation, reproducible
builds, static Linux musl binaries, and macOS .app bundle creation
with App Store validation infrastructure. All 18 phases are now LANDED.
1.1 Pipeline and packaging (Phases 0-1)
The transpiler pipeline lives under transpiler3/swift/. The entry
point is transpiler3/swift/build/Driver.Build, which accepts a
DriverConfig struct specifying the target (swift-macos,
swift-ios, swift-static-linux, swift-appstore), output path,
and optional determinism flag.
The pipeline stages are:
parser.Parse-- identical to every other Mochi backend.types.Check-- shared type-checker; Swift backend adds no new types.aotir.Lower-- the shared IR from MEP-45; runs monomorphisation, closure-conversion, and match-to-decision-tree.colour.Analyse-- Swift-specific async colouring pass. Builds aColourMap(map[string]Colour) and seeds it from every function whose body contains anAsyncExprorAwaitExprAST node. The pass then propagates Red (async) colour to a fixpoint usingslices.ContainsFuncover the static call graph.lower.Lower(swift) -- convertsaotirnodes tosxtreenodes (a Swift-shaped AST). This is where agent declarations becomeClassDecl, async expressions becomeTask<T, Never> { body }, and await expressions becomeawait fut.value.emit.Emit-- renderssxtreenodes to.swiftsource files.swift build/xcodebuildsubprocess -- invokes the Swift toolchain with a generatedPackage.swiftthat declares aProductReferencetoMochiRuntime.
The emitted Swift module for user code links against MochiRuntime,
a Swift package under transpiler3/swift/runtime/. MochiRuntime
provides the stream, async, file I/O, LLM, fetch, and format runtime
functions. A @main entry point is emitted automatically from
top-level statements in the source file.
MochiRuntime Swift package. The runtime targets Swift 6.0+,
macOS 15+, iOS 18+. It contains Stream.swift (broadcast streams),
Async.swift (await-all helper), FileIO.swift (file primitives),
LLM.swift (cassette-backed generation), and Fetch.swift (HTTP
GET). No third-party Swift dependencies are required.
1.2 Scalars and control flow (Phases 1-2)
Integers lower to Swift Int64. Every integer literal carries the
explicit type annotation in declarations. Divide-by-zero is caught at
runtime via a guard that calls fatalError("MOCHI_ERR_DIVZERO").
Floats lower to Swift Double. print(3.14) uses Swift's default
Double string conversion, which matches vm3's strconv.FormatFloat
output for normal values. Edge cases -- NaN, +Inf, -Inf -- match
the BEAM and JVM backends exactly.
Booleans lower to Swift Bool. print(true) emits "true"
(lowercase) to match vm3 via String(describing: v).
Strings lower to Swift String (Unicode scalars). Concatenation
uses +. Interpolation uses \(expr).
Control flow. if/else -> Swift if/else. while -> Swift
while. for x in range(lo, hi) -> for x in lo..<hi. for x in collection -> Swift for x in collection. break/continue/return
lower directly.
1.3 Collections (Phase 3)
list<T> lowers to Swift [T]. List literals [1, 2, 3] lower
to [1, 2, 3] (Swift array literal). len(xs) -> xs.count.
append(xs, x) returns a new array with x appended. xs[i] ->
xs[Int(i)].
map<K,V> lowers to Swift [(K, V)] backed by a scan helper that
preserves insertion order, matching vm3 semantics. m[k] panics on
missing keys. has(m, k) does a linear scan.
set<T> lowers to Swift Set<T> for scalar element types.
add(s, v) returns a new Set. has(s, v) -> s.contains(v).
1.4 Records (Phase 4)
Records lower to Swift struct types with var stored properties.
A Mochi record type Point { x: int, y: int } emits:
struct MochiPoint {
var x: Int64
var y: Int64
}
Field access is direct property access. Record update {...p, x: 10}
lowers to a var copy = p; copy.x = 10; return copy pattern.
1.5 Sum types and pattern matching (Phase 5)
Sum types lower to Swift enum with associated values:
type Shape = Circle(r: float) | Rect(w: float, h: float)
emits:
enum MochiShape {
case circle(r: Double)
case rect(w: Double, h: Double)
}
Pattern matching. match x { arm => expr, ... } lowers to Swift
switch x with case let .variant(...) arms. Swift's exhaustiveness
checker validates the match at compile time.
option[T] lowers to Swift Optional<T>. result[T,E] lowers
to Swift Result<T, E>.
1.6 Closures and higher-order functions (Phase 6)
Closures lower to Swift closures { (params) -> ReturnType in body }.
Partial application add(5, _) lowers to { b in add(5, b) }.
Higher-order builtins map, filter, reduce use Swift's native
Array.map, Array.filter, Array.reduce.
1.7 Query DSL (Phase 7)
from x in xs where pred select proj lowers to Swift method chains:
xs.filter({ x in pred }).map({ x in proj })
Group-by and aggregations use Swift's Dictionary(grouping:by:) and
reduce variants. The query DSL targets in-memory collections; SQL and
PLINQ equivalents are not wired for this backend.
1.8 Datalog (Phase 8)
Facts, rules, and recursive queries compile to MochiDatalog, a
semi-naive fixpoint evaluator in MochiRuntime. Each relation is
backed by a Set<[String]> for membership and an Array<[String]> for
the delta list per round. Negation-as-failure is supported when the
negation is stratifiable.
1.9 Agents (final class, not actor) (Phase 9)
This is the most important architectural decision in MEP-49. Mochi
agent declarations lower to Swift public final class, not Swift
actor. The choice is deliberate: actor-isolated methods cannot be
called from synchronous contexts without await, and Mochi's current
VM model treats agent calls as synchronous sends. Using a final class
with a private mutable state field avoids actor-boundary friction for
the majority of Mochi programs that do not need true data-race isolation.
A Mochi counter agent:
agent Counter(count: int) {
intent get(): int { return count }
intent increment() { count = count + 1 }
}
emits:
public final class MochiCounter {
var count: Int64
public init(count: Int64) {
self.count = count
}
public func get() -> Int64 {
return count
}
public func increment() {
count = count + 1
}
}
spawn AgentType(args...) lowers to MochiAgentType(args...).
__self->field rewrite pass. The shared aotir IR represents
agent field access as __self->fieldName for compatibility with the
C backend. The Swift lowering pass (rewriteAgentSelfRefs) strips
these to bare field names before emitting Swift, so __self->count
becomes count in the method body.
The phase-9 gate covers 15 fixtures: agents with a single intent, multiple intents, accumulators, flags, and arithmetic.
1.10 Synchronous broadcast streams (Phase 10)
Mochi stream<T> lowers to MochiStream<T>, a synchronous broadcast
stream class in MochiRuntime, not to Swift's AsyncStream<T>. The
class holds a list of MochiSub<T> subscriber handles. emit(stream, value) appends the value to each subscriber's buffer. recv(sub)
reads the next value from the subscriber's buffer by advancing a read
index.
public final class MochiStream<T> {
private var subscribers: [MochiSub<T>] = []
public init(capacity: Int64) {}
public func emit(_ value: T) {
for sub in subscribers { sub.buffer.append(value) }
}
public func subscribe() -> MochiSub<T> { ... }
}
public final class MochiSub<T> {
var buffer: [T] = []
private var readIndex: Int = 0
public func recv() -> T {
let v = buffer[readIndex]; readIndex += 1; return v
}
}
The synchronous model matches the BEAM backend's mailbox semantics and
avoids imposing Swift's structured concurrency requirements on Mochi
programs that don't opt into async. The phase-10 gate covers 12
fixtures.
1.11 Async/await and colour colouring (Phase 11)
Async colouring pass. colour.Analyse in
transpiler3/swift/colour/colour.go builds a ColourMap seeded from
all AsyncExpr and AwaitExpr nodes in the AST. It then propagates
Red (async) colour through the call graph to a fixpoint. Functions that
transitively call an async expression become async in the emitted
Swift. Functions that do not remain synchronous and require no async
keyword or await call sites.
async expr -> Task<ReturnType, Never> { body } in Swift.
await fut -> await fut.value in Swift.
await_all(futures) -> mochiAwaitAll(futures) in Swift, which
is implemented as a sequential for task in tasks { results.append(await task.value) } loop inside an async function -- giving deterministic
ordering regardless of completion order.
The phase-11 gate covers 12 fixtures: compose chains, parallel invocations, counter tasks, negation, and triple-compose.
1.12 File I/O (Phase 12)
File I/O is backed by Foundation's FileManager and FileHandle.
| Mochi call | Swift runtime function | Implementation |
|---|---|---|
read_file(path) | mochiReadFile(path) | String(contentsOfFile:) |
write_file(path, content) | mochiWriteFile(path, content) | atomic Data.write(to:options:.atomic) |
append_file(path, content) | mochiAppendFile(path, content) | FileHandle.seekToEndOfFile + write |
lines(path) | mochiLines(path) | read + split on \n + strip trailing empty |
All four functions live in transpiler3/swift/runtime/Sources/MochiRuntime/FileIO.swift.
The phase-12 gate covers 10 fixtures. Note that phases 12 and 14 both
use /tmp/mochi_swift_*.txt scratch paths and must not be run with a
combined regex that causes Go test parallelism between them; each phase
runs cleanly in isolation.
1.13 LLM generation with cassette playback (Phase 13)
generate provider { model: "...", prompt: "..." } lowers to
mochiLLMGenerate(provider, model, prompt) in Swift. In cassette mode
(environment variable MOCHI_LLM_CASSETTE_DIR set), the function looks
up a pre-recorded response file instead of calling an LLM API:
public func mochiLLMGenerate(
_ provider: String, _ model: String, _ prompt: String
) -> String {
if let cassetteDir = ProcessInfo.processInfo
.environment["MOCHI_LLM_CASSETTE_DIR"] {
let key = mochiDJB2Key(provider, model, prompt)
let path = "\(cassetteDir)/\(key).txt"
if let content = try? String(contentsOfFile: path, encoding: .utf8) {
return content.trimmingCharacters(in: .newlines)
}
return ""
}
return ""
}
DJB2 XOR hash. The cassette key is the DJB2 XOR hash of the
concatenated string "provider\0model\0prompt", using the same
algorithm as the BEAM backend (h=5381, h=(h&*33)^UInt64(byte) per
byte). This means cassette files are interchangeable between backends:
a cassette recorded against the BEAM backend plays back identically on
the Swift backend for the same provider/model/prompt triple.
The phase-13 gate covers 8 fixture directories (each a subdirectory
with a cassette/<hash>.txt file, a .mochi source, and a .out
golden output).
1.14 HTTP fetch (Phase 14)
fetch url into var lowers to mochiHttpGet(url) in Swift:
public func mochiHttpGet(_ urlString: String) -> String {
guard let url = URL(string: urlString) else { return "" }
guard let data = try? Data(contentsOf: url) else { return "" }
return (String(data: data, encoding: .utf8) ?? "")
.trimmingCharacters(in: .newlines)
}
The implementation uses Data(contentsOf:) -- synchronous Foundation
I/O -- rather than URLSession async tasks, to keep the call site colour-
neutral. This means fetch works in both synchronous and async Mochi
programs without the colour pass needing to mark it Red.
Tests use file:// URLs backed by fixture text files, making the test
suite hermetic with no outbound network requirements. The phase-14 gate
covers 10 fixtures.
1.15 iOS project generation (Phase 15)
GenerateIOSProject(cfg, srcDir, outDir) generates two files:
project.yml-- a XcodeGen project spec referencing the Mochi transpiled Swift sources and theMochiRuntimepackage.Info.plist-- a minimal iOS 18+ application property list withBundleIdentifier,BundleDisplayName,BundleVersion,MinimumOSVersion="18.0", and required device capabilities.
XcodeGenAvailable() checks that xcodegen is on $PATH. If the
tool is absent, the test is skipped. Building and archiving via
xcodebuild is scaffolded in build/ios.go (IOSSimulatorAvailable,
BuildIOSSimulator) but is deferred to a future sub-phase because
it requires a full Xcode installation with iOS simulators present.
The phase-15 gate validates that project.yml and Info.plist are
produced with the correct key/value structure for each of its 10 fixtures.
No binary build is attempted.
1.16 Reproducible builds (Phase 16)
Three Swift Package Manager and linker flags make binary output bit- identical across machines:
SWIFTPM_DETERMINISTIC_BUILD=1-- instructs SwiftPM to produce deterministic object files.SOURCE_DATE_EPOCH=0-- strips embedded timestamps from Swift metadata sections.-Xlinker -no_uuid-- suppresses the Mach-O UUID on macOS.
TestPhase16Repro builds each fixture twice in separate temp
directories and SHA-256 hashes the resulting binary. The two hashes
must match exactly.
macOS skip. Despite all three flags, macOS Mach-O binaries embed
a non-deterministic UUID at link time that survives -Xlinker -no_uuid
on certain XCode/ld versions. The MEP-49 spec explicitly gates phase
16 as a linux-x64 requirement. TestPhase16Repro skips automatically
on runtime.GOOS == "darwin" with an explanatory message; CI on
linux-x64 sees the full SHA-256 comparison. The macOS linker issue is
tracked as deferred work in the phase-16 doc.
1.17 Static Linux musl binary (Phase 17)
--target=swift-static-linux invokes swift build with the Swift
Static Linux SDK, producing a musl-linked single-file binary with no
shared library dependencies.
Two SDK triples are defined in build/sdk.go:
const (
SDKTripleX64 = "x86_64-swift-linux-musl"
SDKTripleArm64 = "aarch64-swift-linux-musl"
)
StaticLinuxSDKAvailable(triple) runs swift sdk list and searches
its output for the triple string. If the SDK is absent, the test is
skipped. The phase-17 gate builds 7 fixtures against the static SDK
and runs each binary, comparing stdout against the .out golden file.
1.18 macOS .app bundle and App Store infrastructure (Phase 18)
CreateMacOSAppBundle(cfg, binaryPath, outDir) creates a standard
macOS application bundle:
AppName.app/
Contents/
MacOS/<binary> -- copied from the built Swift binary
Info.plist -- generated with BundleId, Version, etc.
CodesignAvailable() checks for codesign on $PATH. AltoolAvailable()
checks for xcrun altool. Neither is required to generate the bundle
structure; codesign and notarisation are wired as post-build steps
that run only when the tools are available and the appropriate environment
variables (MOCHI_CODESIGN_IDENTITY, MOCHI_APPLE_ID) are set.
The phase-18 gate verifies that the bundle directory structure and
Info.plist contents are correct for each of its 10 fixtures. No
codesign or upload is attempted in CI.
2. Implementation doc audit
All 10 phase implementation docs (website/docs/implementation/0049/phase-09-agents.md
through phase-18-appstore.md) were audited against the real source
code after all phases landed. The docs were originally written as
aspirational specs before implementation and contained a number of
inaccuracies that are now corrected:
- Agents: changed from "Swift
actor+AsyncStream<T>mailbox" to "Swiftpublic final class", matching the actualClassDeclnode insxtree/nodes.go. - Streams: changed from "Swift
AsyncStream<T>" to "synchronousMochiStream<T>/MochiSub<T>", matchingStream.swift. - File I/O (Phase 12): changed from "C FFI / module maps" to
"Foundation
FileManager+FileHandle", matchingFileIO.swift. - LLM cassette: DJB2 XOR hash algorithm documented precisely, with the note that it matches the BEAM backend's cassette key format.
- 14 non-existent
lower/*.gofiles removed from "Files changed" tables across all 10 docs. - 7 non-existent runtime Swift files removed from doc tables.
- Fixture counts corrected from estimated values to actual directory counts for all 10 phases.
- All sub-phase statuses updated from "NOT STARTED" to their correct LANDED or DEFERRED state.
3. Test corpus
| Test function | Phase | Fixtures | Coverage |
|---|---|---|---|
TestPhase1Hello | 1 | 5 | hello world; print; basic arithmetic |
TestPhase2Scalars | 2 | 20 | int/float/bool/string; NaN/Inf; divzero; casts; control flow |
TestPhase3Collections | 3 | 13 | list/map/set literals and operations; for-in |
TestPhase4Records | 4 | 14 | record types; field access; record update |
TestPhase5Sums | 5 | 4 | sealed enum variants; switch; option/result |
TestPhase6Closures | 6 | 6 | lambdas; partial apply; higher-order builtins |
TestPhase7Query | 7 | 10 | from/where/select; group-by; sort/take/skip |
TestPhase8Datalog | 8 | 6 | facts; recursive rules; negation-as-failure |
TestPhase9Agents | 9 | 15 | final class agents; intents; accumulator; flag; arithmetic |
TestPhase10Streams | 10 | 12 | MochiStream; emit; subscribe; recv; count; chain |
TestPhase11Async | 11 | 12 | colour pass; Task; await; await_all; compose chains |
TestPhase12FileIO | 12 | 10 | readFile; writeFile; appendFile; lines |
TestPhase13LLM | 13 | 8 | cassette playback; DJB2 key lookup |
TestPhase14Fetch | 14 | 10 | mochiHttpGet; file:// URLs; newline trimming |
TestPhase15IOS | 15 | 10 | project.yml structure; Info.plist keys |
TestPhase16Repro | 16 | 7 | SHA-256 bit-identical (linux-x64 only) |
TestPhase17StaticLinux | 17 | 7 | musl binary; runtime execution; golden output |
TestPhase18Appstore | 18 | 10 | .app bundle structure; Info.plist; codesign hooks |
Total Swift test fixtures: 171. All tests pass on macOS 15.5 with Swift 6.0 toolchain. The reproducibility gate passes on linux-x64 with the Swift Static Linux SDK installed.
4. New compiler pipeline files
transpiler3/swift/
colour/colour.go -- async colouring pass (ColourMap)
sxtree/nodes.go -- ~30 Swift AST node types
lower/lower.go -- aotir -> sxtree (all lowering)
emit/emit.go -- sxtree -> .swift source text
build/build.go -- Driver.Build; target constants
build/ios.go -- iOS project generation (project.yml + Info.plist)
build/macos.go -- macOS .app bundle creation
build/sdk.go -- static Linux SDK availability check
build/deterministic.go -- sha256File helper for phase-16 gate
runtime/Sources/MochiRuntime/
Async.swift -- mochiAwaitAll
Fetch.swift -- mochiHttpGet
FileIO.swift -- mochiReadFile/Write/Append/Lines
LLM.swift -- mochiLLMGenerate with DJB2 cassette
Stream.swift -- MochiStream<T> + MochiSub<T>
5. aotir IR lowering table
MEP-49 added no new IR nodes to the shared aotir package.
| aotir node | Swift lowering |
|---|---|
AgentDecl | public final class with var fields and func intents |
SpawnExpr | MochiAgentType(args...) initializer |
AsyncExpr | Task<ReturnType, Never> { body } |
AwaitExpr | await fut.value (in Red async context) |
StreamMakeExpr | MochiStream<T>(capacity: cap) |
SumTypeDecl | Swift enum with associated value cases |
MatchExpr | Swift switch with case let .variant(...) arms |
RecordDecl | Swift struct with var properties |
LLMExpr | mochiLLMGenerate(provider, model, prompt) |
ReadFileExpr | mochiReadFile(path) |
WriteFileStmt | mochiWriteFile(path, content) |
FetchExpr | mochiHttpGet(url) |
PrintStmt | print(expr) (Swift stdlib) |
IntLit | Int64(literal) |
6. Compatibility
v0.16.0 is additive. All existing Mochi programs continue to run under
vm3 with mochi run. The following build targets are unchanged:
mochi build --target=c-aot(MEP-45)mochi build --target=beam-escript(MEP-46)mochi build --target=jvm-uberjar(MEP-47)mochi build --target=dotnet-fx-dependent(MEP-48, v0.15.0)
mochi build --target=swift-macos is the new entry point for Swift
output on macOS. --target=swift-static-linux produces musl-linked
binaries for Linux server deployment.
Swift 6.0 is the minimum supported Swift version. macOS 15 and iOS 18 are the minimum supported Apple platform versions. Earlier Apple platforms are not supported.
7. Upgrade
curl -fsSL https://get.mochi-lang.dev | sh
mochi --version # 0.16.0
Or, with Docker:
docker pull ghcr.io/mochilang/mochi:0.16.0
Or, from source:
git pull && make build
Pre-built binaries for all five tier-1 triples (linux/amd64, linux/arm64, darwin/arm64, windows/amd64, darwin/amd64) are available on the GitHub release page.