Skip to main content

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:

  1. parser.Parse -- identical to every other Mochi backend.
  2. types.Check -- shared type-checker; Swift backend adds no new types.
  3. aotir.Lower -- the shared IR from MEP-45; runs monomorphisation, closure-conversion, and match-to-decision-tree.
  4. colour.Analyse -- Swift-specific async colouring pass. Builds a ColourMap (map[string]Colour) and seeds it from every function whose body contains an AsyncExpr or AwaitExpr AST node. The pass then propagates Red (async) colour to a fixpoint using slices.ContainsFunc over the static call graph.
  5. lower.Lower (swift) -- converts aotir nodes to sxtree nodes (a Swift-shaped AST). This is where agent declarations become ClassDecl, async expressions become Task<T, Never> { body }, and await expressions become await fut.value.
  6. emit.Emit -- renders sxtree nodes to .swift source files.
  7. swift build / xcodebuild subprocess -- invokes the Swift toolchain with a generated Package.swift that declares a ProductReference to MochiRuntime.

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 callSwift runtime functionImplementation
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 the MochiRuntime package.
  • Info.plist -- a minimal iOS 18+ application property list with BundleIdentifier, 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 "Swift public final class", matching the actual ClassDecl node in sxtree/nodes.go.
  • Streams: changed from "Swift AsyncStream<T>" to "synchronous MochiStream<T>/MochiSub<T>", matching Stream.swift.
  • File I/O (Phase 12): changed from "C FFI / module maps" to "Foundation FileManager + FileHandle", matching FileIO.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/*.go files 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 functionPhaseFixturesCoverage
TestPhase1Hello15hello world; print; basic arithmetic
TestPhase2Scalars220int/float/bool/string; NaN/Inf; divzero; casts; control flow
TestPhase3Collections313list/map/set literals and operations; for-in
TestPhase4Records414record types; field access; record update
TestPhase5Sums54sealed enum variants; switch; option/result
TestPhase6Closures66lambdas; partial apply; higher-order builtins
TestPhase7Query710from/where/select; group-by; sort/take/skip
TestPhase8Datalog86facts; recursive rules; negation-as-failure
TestPhase9Agents915final class agents; intents; accumulator; flag; arithmetic
TestPhase10Streams1012MochiStream; emit; subscribe; recv; count; chain
TestPhase11Async1112colour pass; Task; await; await_all; compose chains
TestPhase12FileIO1210readFile; writeFile; appendFile; lines
TestPhase13LLM138cassette playback; DJB2 key lookup
TestPhase14Fetch1410mochiHttpGet; file:// URLs; newline trimming
TestPhase15IOS1510project.yml structure; Info.plist keys
TestPhase16Repro167SHA-256 bit-identical (linux-x64 only)
TestPhase17StaticLinux177musl binary; runtime execution; golden output
TestPhase18Appstore1810.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 nodeSwift lowering
AgentDeclpublic final class with var fields and func intents
SpawnExprMochiAgentType(args...) initializer
AsyncExprTask<ReturnType, Never> { body }
AwaitExprawait fut.value (in Red async context)
StreamMakeExprMochiStream<T>(capacity: cap)
SumTypeDeclSwift enum with associated value cases
MatchExprSwift switch with case let .variant(...) arms
RecordDeclSwift struct with var properties
LLMExprmochiLLMGenerate(provider, model, prompt)
ReadFileExprmochiReadFile(path)
WriteFileStmtmochiWriteFile(path, content)
FetchExprmochiHttpGet(url)
PrintStmtprint(expr) (Swift stdlib)
IntLitInt64(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.