Skip to main content

MEP 50. Mochi-to-Kotlin transpiler: Kotlin Multiplatform via Gradle, coroutine + Channel agents, Android .aab, K/Native single binaries

FieldValue
MEP50
TitleMochi-to-Kotlin transpiler
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-23 10:30 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse), MEP-46 (BEAM transpiler, IR reuse), MEP-47 (JVM transpiler, IR reuse but distinct from MEP-50), MEP-48 (.NET transpiler, IR reuse), MEP-49 (Swift transpiler, IR reuse)
Research~/notes/Spec/0050/01..12
Tracking/docs/implementation/0050/

Abstract

Mochi today ships vm3 (mochi run), an ahead-of-time C transpiler producing native single-file binaries (MEP-45), an Erlang/BEAM transpiler producing supervised concurrent runtimes (MEP-46), a JVM transpiler that emits Java bytecode directly via ASM and reaches Maven Central and Project Loom (MEP-47), a .NET transpiler that emits C# source and reaches NuGet and NativeAOT (MEP-48), and a Swift transpiler that emits Swift 6.0 source and reaches the Apple App Store, server-side Swift, and Static Linux SDK binaries (MEP-49). None of these paths produces a first-class Android application, a Jetpack Compose UI, a Kotlin Multiplatform iOS / desktop binary built from shared sources, or a Kotlin/Wasm browser bundle. The Android ecosystem (3.6B active devices) requires Kotlin or Java compiled by Google's Android Gradle Plugin, signed via apksigner, and distributed through Google Play Console as an .aab Android App Bundle. Kotlin Multiplatform reached Stable in 2023-11 and is now the load-bearing surface for cross-platform mobile, desktop, web, and server code from a single Kotlin codebase. MEP-50 specifies a sixth transpiler pipeline that targets Kotlin 2.1's K2 compiler, packages output through Gradle and the Kotlin Multiplatform Gradle Plugin, and reaches every KMP target: JVM, Android, Kotlin/Native (iOS, macOS, Linux, Windows), Kotlin/JS (browser, Node.js), and Kotlin/Wasm.

The pipeline reuses MEP-45's typed-AST and aotir IR, plus the monomorphisation, match-to-decision-tree, closure-conversion, exhaustiveness-check, and sendability-inference passes shared with MEP-46, MEP-47, MEP-48, and MEP-49. It forks at the emit stage: instead of emitting ISO C23 (MEP-45), Core Erlang via cerl (MEP-46), JVM bytecode via ASM (MEP-47), C# source via Roslyn SyntaxFactory (MEP-48), or Swift 6.0 source via a Mochi-side syntax tree (MEP-49), it emits Kotlin 2.1 source text via KotlinPoet (Square's KotlinPoet 1.18+, the canonical Kotlin-source emitter), optionally piped through ktfmt or ktlint for canonical layout. Eleven packaging targets ship together: --target=kotlin-jvm produces a .jar runnable on any Java 17+ runtime (~3-8 MB with kotlin-stdlib included); --target=kotlin-android produces a signed .aab Android App Bundle (or .apk debug build) via AGP 8.7+, ready for Play Console upload; --target=kotlin-ios-arm64, --target=kotlin-macos-arm64, --target=kotlin-linux-x64, --target=kotlin-linux-arm64, and --target=kotlin-windows-x64 produce Kotlin/Native single binaries (~5-15 MB, statically linked against the K/Native runtime); --target=kotlin-js-browser and --target=kotlin-js-node produce ES-module JavaScript bundles; --target=kotlin-wasm-js produces a Wasm GC bundle (shipped as Alpha with a stability caveat); --target=kotlin-kmp-library produces a multi-target KMP library consumable from another Kotlin project. One codegen pass feeds all KMP targets via per-target source sets (commonMain, jvmMain, androidMain, iosMain, macosMain, linuxMain, mingwMain, jsMain, wasmJsMain).

The master correctness gate is byte-equal stdout from the produced binary (executable Kotlin target run directly, Android app run in the Android emulator with stdout captured via instrumented test, JS bundle run via Node.js, Wasm bundle run via wasmtime or Node.js with Wasm GC support) versus vm3 on the entire fixture corpus, across Kotlin 2.1.0 and Kotlin 2.1.20, on x86_64-linux-gnu, aarch64-linux-gnu, aarch64-darwin, and x86_64-windows. vm3 is the recording oracle for expect.txt; the transpiler does not link against or depend on vm3.

Five load-bearing decisions:

  1. Kotlin 2.1 floor with the K2 compiler. Kotlin 2.0 (released 2024-05-21) was the first release where the K2 frontend (a complete rewrite of the legacy K1 frontend) reached stable status; Kotlin 2.1 (released 2024-11-27) added multidollar string interpolation, smart-cast improvements for sealed when expressions, and refined KMP source-set defaults. MEP-50 emits exclusively against Kotlin 2.1+ language mode. This locks in static guarantees that Mochi's type system already proves at the Mochi level (exhaustive when over sealed interfaces, sendability of channel payloads via Sendable markers, nominal error types via sealed hierarchies) and prevents the codegen pass from emitting code that compiles under K1 but breaks under K2. Floor-ing at 2.1 drops Kotlin 1.9 (last K1-frontend release, end-of-life by 2026 H2). Kotlin 1.9 is rejected because the K2 compiler unlocks the smart-cast improvements that Mochi's lowered code relies on; backporting via flags creates a dual codepath. See 02-design-philosophy §3, 12-risks-and-alternatives §R1, §A2.

  2. Kotlin Multiplatform full, not JVM-only. MEP-47 already owns the JVM target via direct bytecode emission (skip kotlinc, skip javac, ASM-direct). MEP-50's reason to exist is the KMP surface: a single Mochi codebase compiles to JVM, Android, Kotlin/Native (iOS arm64, macOS arm64, Linux x64/arm64, Windows x64), Kotlin/JS (browser, Node.js), and Kotlin/Wasm (Wasm GC). Excluding any of these targets would force users with a Mochi backend and a KMP-mobile frontend to switch Mochi targets per platform, breaking the single-language story KMP was designed to enable. The cost is a 5-OS-by-3-arch CI matrix plus the KMP plugin's gradle plumbing, comparable to MEP-49 (Swift) and MEP-48 (.NET). On the JVM target the two flows (MEP-47 direct bytecode vs MEP-50 source-to-kotlinc) produce different bytecode; users can pick either via the --target=kotlin-jvm vs --target=jvm distinction. The cross-target differential gate (TestCrossTargetDifferential) verifies both produce byte-equal stdout on every shared fixture. See 02-design-philosophy §4, 07-kotlin-target-portability §1-9.

  3. Mochi agents as custom actor class wrapping Channel<Message> plus structured concurrency. kotlinx.coroutines (1.10+) provides Channel<T> (a bounded or unbounded coroutine-safe queue), CoroutineScope, SupervisorJob (a job whose children failures do not cancel siblings), and Dispatchers.Default (a shared worker pool sized to CPU count). Mochi agents lower one-to-one: an agent declaration becomes a public class with a private val mailbox: Channel<Message> and a private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default); the mailbox loop is a scope.launch { for (msg in mailbox) handle(msg) } started in init; cast(msg) is mailbox.trySend(msg) from a non-suspending function or mailbox.send(msg) from a suspend function; call(req) -> reply uses a one-shot CompletableDeferred<Reply> carried as a field on the message data class. The kotlinx.coroutines actor { } builder is explicitly not used because it has been deprecated in the ROADMAP since 2017 (kotlinx.coroutines issue #87) and pending replacement since 1.7; the custom-class shape is what the kotlinx.coroutines maintainers recommend. No GCD, no Reactor, no Akka-Kotlin, no third-party actor framework. See 02-design-philosophy §5, 09-agent-streams §1-§12.

  4. Reuse MEP-45's aotir IR plus all shared passes. The IR is target-agnostic; monomorphisation, match-to-decision-tree, closure-conversion, exhaustiveness checking, and sendability inference all run once and feed six backends. The fork is at the emit pass: transpiler3/kotlin/lower/ lowers aotir to Kotlin-source structural nodes; transpiler3/c/emit/ lowers aotir to C; transpiler3/beam/lower/ to Core Erlang; transpiler3/jvm/lower/ to JVM bytecode; transpiler3/dotnet/lower/ to C# source; transpiler3/swift/lower/ to Swift source. Sharing the IR keeps the six targets semantically aligned and amortises pass-implementation work. See 05-codegen-design §5.

  5. kotlinx. family as fat runtime; MochiRuntime as thin KMP library.* The runtime library MochiRuntime (Apache-2.0, ~6000 LOC) provides only what the kotlinx.* package family does not: a Mochi-typed function-interface zoo (handled by Kotlin function types directly), Mochi-shaped collection helpers re-exporting PersistentList / PersistentMap / PersistentSet from kotlinx.collections.immutable, a small Datalog engine, AI / FFI dispatch tables, agent supervisor, ZonedDateTime helpers above kotlinx.datetime.Instant (since Foundation-style date manipulation lives in kotlinx.datetime), and a JSONValue sealed-interface ADT for dynamic JSON over kotlinx.serialization.json.JsonElement. Everything else (HTTP via Ktor client, file I/O via okio or platform stdlib, regex via Kotlin Regex, locale, dates via kotlinx.datetime) goes through Kotlin first-party libraries directly. Square Retrofit, OkHttp, Moshi, Gson, RxKotlin are explicitly rejected as runtime deps. See 04-runtime §1-§20, 02-design-philosophy §8 and §13.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/kotlin/fixtures/ must compile via the Kotlin pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. kotlinc-clean (with -Werror -Xexplicit-api=strict -language-version=2.1 -api-version=2.1) on generated code is the secondary gate. ktfmt fixed-point (running format twice produces no diff) is the tertiary gate. Android App Bundle validation via bundletool validate is the quaternary gate. Kotlin/Native single-binary build is the quinary gate. Reproducibility (bit-identical .jar and .aab across two CI hosts) is the senary gate. Google Play Console pre-launch validation (via gcloud firebase test android run) is the septenary gate.

Motivation

Mochi today targets vm3 (for mochi run), MEP-45 (C, single-binary AOT), MEP-46 (BEAM, supervision and hot reload), MEP-47 (JVM, Maven Central via direct bytecode and Loom), MEP-48 (.NET, NuGet and NativeAOT), and MEP-49 (Swift, App Store and Static Linux SDK). None deliver the Android platform, the Kotlin Multiplatform iOS/desktop shared-codebase story, Jetpack Compose UI, Kotlin/JS (the canonical Kotlin browser target), or Kotlin/Wasm:

  1. Google Play and Android. Android apps require Kotlin or Java compiled by the Android Gradle Plugin, dex'd by D8/R8, signed by apksigner, and distributed as an .aab Android App Bundle through Google Play Console. Sideloading exists but Google Play is the primary distribution channel (3.6B active Android devices, 96% of Android app installs in non-China markets). MEP-47 (JVM bytecode) cannot produce an .aab: the JVM-bytecode target emits .class files that lack the Android-specific annotations, resource-merging metadata, manifest synthesis, and lifecycle-aware code that AGP injects. MEP-48 (.NET) reaches Android via .NET MAUI but ships the Mono runtime (~30-50 MB footprint, JIT-incompatible with Google Play's app-size guidance). MEP-49 (Swift) does not target Android at all (Swift on Android is unsupported by Apple). MEP-50 produces idiomatic Kotlin indistinguishable from hand-written Android code after ktfmt, sized at 3-8 MB after R8 minification, free of any embedded runtime evaluator, ready for Play Console upload.

  2. Kotlin Multiplatform: one codebase, every target. KMP reached Stable in 2023-11 (Kotlin 1.9.20 release notes; further refined in 2.0 and 2.1). The promise is: write shared logic in commonMain Kotlin, expose it via expect/actual boundary declarations, compile to JVM bytecode, Android dex, Kotlin/Native machine code, Kotlin/JS, or Kotlin/Wasm from one source tree. Mochi's aotir IR is already target-agnostic; emitting Kotlin source feeds every KMP target without a per-target Mochi codegen pass. A Mochi user with iOS + Android + web + server backend has one Mochi source tree producing five binaries, all bit-equal in semantics.

  3. Jetpack Compose Multiplatform. Google's Jetpack Compose (canonical Android UI since 2021) and JetBrains' Compose Multiplatform 1.7+ (Android + iOS + macOS + Linux + Windows + web, since 2024) are the declarative-UI story for Kotlin. While MEP-50 v1 does not expose Compose as Mochi language surface (it remains a v2 candidate), Mochi-emitted Kotlin code is consumable from Compose projects: users import the Mochi-generated KMP module and call Mochi functions from their @Composable views.

  4. Server-side Kotlin via Ktor and Spring. Ktor 3.0 (released 2024-10) is JetBrains' coroutine-native Kotlin web framework, used in production at JetBrains, Adobe, and a number of Fortune 500 backends. Spring Boot 3.4 (2024-11) supports Kotlin 2.1 as a first-class language. A Mochi user with a Kotlin Android frontend and a Mochi backend can now keep one language across the stack, with Ktor as the HTTP layer.

  5. Kotlin/Native single binaries. Kotlin/Native (LLVM-backed, post-1.9 with the new memory model as default) produces statically linked single binaries comparable to Go or Rust output. Targets: iosArm64, iosSimulatorArm64, macosArm64, macosX64, linuxArm64, linuxX64, mingwX64 (Windows), watchosArm64, tvosArm64. Mochi --target=kotlin-linux-x64 produces a single-binary Linux executable; --target=kotlin-ios-arm64 produces a Kotlin/Native iOS framework consumable from Swift / Objective-C. The 2021-era K/Native freeze model is not supported (incompatible with Mochi's shared-state agent model); the new memory model (default since Kotlin 1.9) is required.

  6. Kotlin/JS and Kotlin/Wasm. Kotlin/JS (IR backend, since Kotlin 1.9; legacy backend removed) produces ES module JavaScript ready for npm publish or direct browser load. Kotlin/Wasm (Wasm GC, marked Beta in 2.1.20) produces a Wasm module that runs on browsers with Wasm GC enabled (Chrome 119+, Firefox 120+, Safari 18.2+) plus Node.js with the experimental flag. Mochi reaches the browser, Node.js, and the modern Wasm GC target through one codegen pass.

  7. JetBrains tooling. IntelliJ IDEA (the dominant Kotlin IDE), Android Studio (built on IntelliJ Platform), and Fleet provide first-class Kotlin support: K2-aware refactoring, inline hints, multiplatform navigation, KMP wizards. Mochi-emitted Kotlin opens cleanly in these tools, with full IDE inference and debugger support, because the emitted source is structurally indistinguishable from hand-written Kotlin.

  8. Sealed interfaces (since Kotlin 1.7) and the K2 smart-cast improvements (Kotlin 2.0+). Mochi sum types map directly to Kotlin sealed interfaces with data class and data object variants. K2's smart-cast improvements make exhaustive when over sealed hierarchies a compile-time guarantee identical to Mochi's match-exhaustiveness check. The two type systems align; the Kotlin compiler becomes a second-line check on Mochi's correctness.

  9. kotlinx.coroutines: the canonical async surface. kotlinx.coroutines 1.10+ provides structured concurrency (parent-child Job hierarchies, automatic cancellation propagation), Flow<T> (a cold async sequence with backpressure), Channel<T> (a coroutine-safe queue with bounded / unbounded / conflated / rendezvous policies), SharedFlow<T> and StateFlow<T> (hot variants), plus dispatchers (Dispatchers.Main for UI, Dispatchers.IO for blocking I/O, Dispatchers.Default for CPU). Mochi's agent, stream, async, and parallel primitives lower one-to-one. Compare Swift actor + AsyncStream (similar but Apple-only), .NET IAsyncEnumerable (similar but more verbose), BEAM GenStage (different paradigm). Kotlin's syntax and semantics are the closest to Mochi's own after Swift.

  10. Maven Central and Gradle. Kotlin libraries publish to Maven Central (the canonical Java/Kotlin package registry), with Gradle 8.11+ as the canonical build tool. Mochi's runtime library MochiRuntime publishes as a KMP artifact (with -jvm, -android, -iosarm64, -iossimulatorarm64, -macosarm64, -linuxx64, -linuxarm64, -mingwx64, -js, -wasm-js per-target classifiers) so any KMP project can depend on it via implementation("dev.mochilang:mochi-runtime:1.0.0").

The C target (MEP-45) remains the right choice for embedded targets, single-file distribution, and minimal runtime footprint. The BEAM target (MEP-46) remains the right choice for hot-reload services, distributed pubsub, and OTP supervision. The JVM target (MEP-47) remains the right choice for Maven Central interop without kotlinc, Loom concurrency without a coroutine library, and direct-bytecode JIT performance. The .NET target (MEP-48) remains the right choice for NuGet, value-type structs without ARC, and Windows-enterprise integration. The Swift target (MEP-49) remains the right choice for Apple platforms (iOS, macOS, watchOS, tvOS, visionOS), App Store distribution, and FoundationModels access. The Kotlin target is the right choice for Android, Kotlin Multiplatform iOS/desktop shared-codebase, Jetpack Compose UI, Ktor server, and Kotlin/Wasm browser. All six ship; the user picks per workload.

Specification

This section is normative. Sub-notes under ~/notes/Spec/0050/01..12 are informative.

1. Pipeline and IR reuse

MEP-50 shares the front-end and aotir passes with MEP-45, MEP-46, MEP-47, MEP-48, and MEP-49 and forks at the emit stage:

Mochi source
| parser (MEP-1/2/3, reused)
v
typed AST
| type checker (MEP-4/5, reused)
v
aotir IR
| monomorphisation pass (shared)
| closure conversion pass (shared)
| match decision tree (shared)
| exhaustiveness check (shared)
| sendability inference (shared)
v
Kotlin source codegen pass (transpiler3/kotlin/lower/)
| emit Kotlin 2.1 source via KotlinPoet (Square 1.18+)
| optional: ktfmt format --in-place
v
.kt files under out/src/<sourceSet>Main/kotlin/<package>/
| emit settings.gradle.kts at out/
| emit build.gradle.kts at out/
| emit gradle/libs.versions.toml at out/
| emit gradle/wrapper/gradle-wrapper.properties at out/
v
./gradlew build (KMP, JVM, JS, Wasm targets)
./gradlew bundleRelease (Android .aab via AGP 8.7+)
./gradlew linkReleaseExecutable<TGT> (K/Native single binaries)
./gradlew jsBrowserProductionWebpack (Kotlin/JS)
./gradlew wasmJsBrowserDistribution (Kotlin/Wasm)
| apksigner / jarsigner (Android, JVM signing)
| bundletool validate (Android .aab)
v
.jar / .aab / .apk / K/Native binary / .js / .wasm / KMP library

Both mochi run (vm3) and mochi build (transpiler3) share the same parser, type system, and IR.

2. Build driver UX

mochi build --target=<TGT> <input.mochi> [-o <output>]

Targets:

  • kotlin-jvm: produces a runnable .jar for the JVM (kotlin-stdlib bundled or referenced).
  • kotlin-android: produces a signed .aab Android App Bundle via AGP 8.7+ plus bundletool validate. Also produces .apk debug builds via the --apk flag.
  • kotlin-ios-arm64: produces a Kotlin/Native iOS framework consumable from Swift (XCFramework via the KMP plugin's XCFramework task).
  • kotlin-ios-simulator-arm64: produces a Kotlin/Native iOS simulator framework for Apple Silicon hosts.
  • kotlin-macos-arm64, kotlin-macos-x64: produces a Kotlin/Native macOS single binary.
  • kotlin-linux-x64, kotlin-linux-arm64: produces a Kotlin/Native Linux single binary, statically linked against the K/Native runtime.
  • kotlin-windows-x64 (mingwX64): produces a Kotlin/Native Windows .exe.
  • kotlin-js-browser, kotlin-js-node: produces ES module JavaScript bundles via the Kotlin/JS IR backend.
  • kotlin-wasm-js: produces a Wasm GC bundle (Alpha caveat documented in README).
  • kotlin-kmp-library: produces a multi-target KMP library consumable from another Kotlin project, with all per-target classifier artefacts.
  • kotlin-source: emits Kotlin source plus Gradle files without building (for downstream IntelliJ / Android Studio integration).

The driver dispatches to Gradle: ./gradlew build for the default JVM target, ./gradlew bundleRelease for Android, ./gradlew linkReleaseExecutable<TGT> for K/Native, ./gradlew jsBrowserProductionWebpack for Kotlin/JS, ./gradlew wasmJsBrowserDistribution for Kotlin/Wasm.

3. Name mangling

Mochi names to Kotlin names:

  • Module path mochilang/aiops/Pipeline to Kotlin package mochi.user.aiops.pipeline (lowercase per Kotlin package convention; configurable via --kotlin-package-prefix, default mochi.user).
  • Mochi function process_batch to Kotlin processBatch (lowerCamelCase).
  • Mochi type User_Record to Kotlin UserRecord (PascalCase).
  • Mochi sum variant OK to Kotlin Ok (PascalCase data class or data object).
  • Mochi reserved-word collisions: class, interface, object, fun, val, var, package, import, return, if, else, when, for, while, do, try, catch, finally, throw, null, true, false, this, super, Nothing, Any, Unit are escaped with backticks (`class`) or a _ suffix.

4. Type lowering

Per 06-type-lowering:

  • int to Long (Kotlin Int is 32-bit; we pick Long for Mochi int to match the 64-bit semantic of vm3).
  • float to Double (IEEE 754 64-bit).
  • bool to Boolean.
  • string to String (UTF-16 internally on JVM/Android/Native/JS/Wasm, with UTF-8 conversion at I/O boundaries).
  • bytes to ByteArray in core, kotlinx.io.bytestring.ByteString at I/O boundaries.
  • list<T> to kotlin.collections.List<T> (read-only) backed by ArrayList.
  • map<K, V> to MutableMap<K, V> backed by LinkedHashMap (insertion-ordered, the Kotlin / JVM default).
  • set<T> to MutableSet<T> backed by LinkedHashSet (insertion-ordered).
  • ordered_map<K, V> to LinkedHashMap<K, V> (already insertion-ordered) or PersistentMap<K, V> from kotlinx.collections.immutable when immutability is requested.
  • record { ... } to data class ...(val ...). Kotlin data classes synthesise equals, hashCode, toString, copy, and componentN.
  • sum type T = A | B to sealed interface T with data class and data object variants. sealed interface (Kotlin 1.7+) is preferred over sealed class so variants can also implement other interfaces.
  • option<T> to Kotlin nullable T? (the idiomatic Kotlin optional, built into the type system).
  • result<T, E> to a custom sealed MochiResult<T, E> (Kotlin's stdlib kotlin.Result<T> is invariant in T, lacks an E parameter, and was originally an internal type; we emit our own).
  • fun(T) -> R to Kotlin function type (T) -> R or suspend (T) -> R when concurrency colouring applies.
  • agent to a custom actor class wrapping Channel<Message>.
  • stream<T> to kotlinx.coroutines.flow.Flow<T>. Cold flow by default; hot variants via SharedFlow / StateFlow.
  • time to kotlin.time.Instant (stable since Kotlin 2.1; falls back to kotlinx.datetime.Instant on pre-2.1 modules).
  • duration to kotlin.time.Duration (stable since Kotlin 1.6).

5. Expression lowering

Per 05-codegen-design §13-§22:

  • Mochi literals to Kotlin literals; Kotlin integer literals default to Int, so the codegen emits explicit L suffix (42L) or Long(...) calls.
  • Mochi index expressions to Kotlin subscripts (list[i], map[k]).
  • Mochi match to Kotlin when with is patterns and where-style guards via secondary if. Exhaustiveness is verified by the Kotlin compiler when when is used as an expression over a sealed hierarchy.
  • Mochi if-let to Kotlin let { ... } chained off the nullable receiver.
  • Mochi closure literal fn x => x + 1 to Kotlin lambda { x: Long -> x + 1 }.
  • Mochi method call obj.method(args) to Kotlin method call (extension functions for record types when methods are defined outside the record).
  • Mochi with expression to Kotlin data class .copy(field = value).

6. Closures and concurrency colouring

Per 05-codegen-design §12, §17 and 09-agent-streams:

  • Mochi closures lower to Kotlin lambdas. Captures are by reference for var, by value for val; the JVM emits a synthetic class per closure.
  • Suspending closures (closures that call suspend functions) are marked suspend (...) -> R.
  • Mochi async functions (any function transitively reachable from an await or agent call) lower to suspend fun. Sync functions stay as plain fun. Sendability of captured state is checked by Kotlin's coroutine machinery at runtime; Mochi's sendability inference at the IR level prevents non-Sendable captures from reaching cross-coroutine boundaries.

7. Runtime library

Per 04-runtime:

  • MochiRuntime KMP Gradle module (Apache-2.0, ~6000 LOC).
  • Re-exports kotlinx.coroutines, kotlinx.serialization, kotlinx.collections.immutable, kotlinx.datetime, kotlinx.io.
  • Adds Mochi-specific: agent supervisor (MochiRuntime.Supervisor), Datalog evaluator (MochiRuntime.Datalog), ZonedDateTime helpers, JSONValue sealed-interface ADT over kotlinx.serialization.json.JsonElement, query DSL extensions.
  • Targets Kotlin 2.1 minimum; published to Maven Central as dev.mochilang:mochi-runtime with all KMP target classifiers.
  • Explicit-API mode (-Xexplicit-api=strict): every public API has an explicit visibility modifier and an explicit return type, required for KMP library ABI stability.

8. Concurrency

Per 09-agent-streams:

  • Mochi agent to a custom Kotlin class. The class holds a private val mailbox: Channel<Message> (capacity Channel.UNLIMITED by default, matching Mochi's unbounded-mailbox semantic; bounded(N) qualifier in Mochi source lowers to Channel(N)).
  • The class also holds a private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default).
  • The mailbox loop is scope.launch { for (msg in mailbox) when (msg) { ... } } started in init.
  • cast(.msg) to mailbox.trySend(msg) from a non-suspending function, mailbox.send(msg) from a suspend fun.
  • call(.req) -> Reply to a suspend fun that constructs a CompletableDeferred<Reply>, attaches it to the message, sends, and awaits the deferred.
  • spawn f() to scope.launch { f() } inside the enclosing CoroutineScope; top-level spawn uses GlobalScope.launch { ... } only for the program entry.
  • Streams: Flow<T> for cold streams (flow { emit(...) } builder, consume via .collect { ... }). Hot variants via SharedFlow<T> / StateFlow<T>.
  • Parallel blocks: coroutineScope { launch { ... }; launch { ... } } or withTimeout(d) { ... } for time-bounded parallel work.
  • Cancellation: cooperative via ensureActive() / yield() at every loop back-edge in long-running blocks; cancellation propagates structurally via the SupervisorJob tree.

9. Memory model

  • JVM and Android: HotSpot / ART tracing GC.
  • Kotlin/Native: new memory model (concurrent mark-and-sweep, default since Kotlin 1.9). The 2021-era freeze-based memory model is not supported; the transpiler refuses to emit code targeting it. Configured via the absence of kotlin.native.binary.memoryModel=experimental (the new model is unconditionally default since Kotlin 1.9).
  • Kotlin/JS: JavaScript engine GC (V8, SpiderMonkey, JSC).
  • Kotlin/Wasm: Wasm GC.
  • No tracing GC required on the Mochi side; Kotlin's runtime handles allocation.

10. Error model

Per 06-type-lowering §7, 02-design-philosophy §11:

  • Mochi Result<T, E> to custom sealed MochiResult<T, E> with Ok(value: T) and Err(error: E) variants.
  • Mochi throwing function fun parse() -> AST throws ParseError to Kotlin fun parse(): MochiResult<AST, ParseError> (no checked exceptions; Kotlin does not have them and we do not emulate them).
  • Mochi try / catch (planned) to Kotlin when (val r = expr()) { is MochiResult.Ok -> ...; is MochiResult.Err -> ... }.
  • Mochi panic to Kotlin error(msg) which throws IllegalStateException.

11. Portability

Per 07-kotlin-target-portability:

  • JVM: Java 17 LTS minimum (Kotlin bytecode target 17). Java 21 LTS preferred but not required.
  • Android: minSdk 24 (Android 7.0 Nougat, 2016-08), targetSdk 35 (Android 15, 2024-10). AGP 8.7+, Kotlin Android Plugin 2.1+.
  • Kotlin/Native: 2.1+, new memory model. Targets: iosArm64, iosSimulatorArm64, iosX64 (Rosetta only), macosArm64, macosX64, linuxArm64, linuxX64, mingwX64, watchosArm64, tvosArm64.
  • Kotlin/JS: 2.1+, IR backend only. Targets: browser and nodejs. Module system: ES modules.
  • Kotlin/Wasm: 2.1+, marked Beta in 2.1.20 (the Wasm GC target). Shipped as Alpha with a clear caveat.

12. Build system

Per 10-build-system:

  • Gradle Kotlin DSL (build.gradle.kts), never Groovy.
  • settings.gradle.kts declares the KMP module structure.
  • gradle/libs.versions.toml is the version catalog with pinned versions of Kotlin, AGP, kotlinx.coroutines, etc.
  • Gradle wrapper bundled; gradle/wrapper/gradle-wrapper.properties pinned to a specific Gradle version (8.11.1 as of MEP-50 v1).
  • Android Gradle Plugin (AGP): 8.7+ for AAPT2, Compose 1.7 / 1.8 alpha targeting.
  • iOS framework export: XCFramework task on the Kotlin KMP plugin (preferred for Swift Package Manager consumption); CocoaPods integration available via the cocoapods block.
  • Compose Multiplatform: org.jetbrains.compose Gradle plugin 1.7.3+; reserved for UI-heavy fixtures, not in core lowering.

13. Reproducibility

Per 11-testing-gates §10:

  • Pinned Kotlin toolchain (Gradle Kotlin DSL kotlin("multiplatform") version "2.1.0").
  • ktfmt applied deterministically.
  • No BuildConfig.TIMESTAMP or __DATE__-style stamps in emitted code.
  • jarsigner / apksigner timestamping excluded from byte-equality check (timestamping stamps Unix epoch); unsigned .jar and .aab are bit-identical.
  • gradle/libs.versions.toml and gradle.lockfile checked in.

14. Hardening

Per 11-testing-gates §14:

  • kotlinc with -Werror -Xexplicit-api=strict -language-version=2.1 -api-version=2.1.
  • No unsafeCast<T>() or Unsafe-style constructs in user-facing code.
  • Android: minimum proguard/R8 rules emitted; sandbox via Android permissions manifest.
  • TLS pinning available via Ktor client HttpsURLConnection interceptor; manifest-driven.

15. Diagnostics

  • Mochi emits Kotlin source with @Suppress annotations only where absolutely necessary; tracked in SUPPRESSIONS.md.
  • Source maps: .mochi.map sidecar per generated .kt for debugger integration.
  • JVM debug info via -Xdebug and -Xemit-jvm-type-annotations.

16. FFI

Per 01-language-surface §10, 06-type-lowering §16:

  • JVM: Mochi extern fun foo(...) -> ... from a Java module to a Kotlin external fun foo(...): ... plus a JNI binding via System.loadLibrary.
  • Kotlin/Native: Mochi extern fun foo(...) -> ... from a C module to a Kotlin/Native cinterop declaration generated via the KMP plugin's cinterops block (.def file emitted per Mochi import "c/<library>").
  • Kotlin/JS: Mochi extern fun foo(...) -> ... to a Kotlin/JS external fun foo(...) plus a @JsModule annotation.
  • Mochi-side exports: Kotlin @JvmStatic for JVM, @CName("mochi_foo") for K/Native, @JsExport for K/JS.

17. Output style

  • 4-space indentation (Kotlin convention).
  • K&R brace style.
  • Explicit public modifiers on all public APIs (required by -Xexplicit-api=strict).
  • Explicit type annotations on public function signatures (Library Evolution requirement).
  • Sorted imports, sorted map keys in literals (for determinism).
  • Trailing commas on multi-line argument and parameter lists (Kotlin 1.4+ feature).

Rationale

The five load-bearing decisions (§Abstract) flow from a single observation: Android requires Kotlin, and Kotlin Multiplatform plus the K2 compiler now make a single Kotlin codebase reach every modern client and server platform. Lower Mochi to Kotlin 2.1 and the two type systems align; the Kotlin compiler becomes a second-line check on Mochi's correctness.

The choice of Channel + CoroutineScope over the deprecated actor { } builder is forced by the kotlinx.coroutines maintainers' own ROADMAP: the builder has been pending replacement since 2017, and modern code uses the custom-class shape that we emit.

The choice of all KMP targets (not just JVM and Android) follows from the user-facing goal: Mochi should let a single team ship the same backend logic to Android, iOS, web, server, and desktop from one source. JVM-only would re-introduce the multi-language stack KMP was designed to eliminate, and MEP-47 already covers the JVM-bytecode story.

The choice of KotlinPoet-shaped emission (Square's canonical Kotlin-source emitter) over raw string templates is forced by codegen reproducibility: hand-rolled string templates drift, ktfmt alone cannot fix structural bugs. KotlinPoet gives us correctness with a familiar Gradle-side dependency.

The thin runtime (MochiRuntime) is the same philosophy that drove MEP-47 (thin dev.mochi.runtime), MEP-48 (thin Mochi.Runtime), and MEP-49 (thin MochiRuntime SwiftPM package): the platform's first-party libraries are the canonical surface; Mochi adds only what is missing.

Backwards Compatibility

MEP-50 is purely additive. mochi run keeps the vm3 path; existing transpiler3 targets (C, BEAM, JVM, .NET, Swift) are untouched. No Mochi language surface changes. The Kotlin target lands as a new transpiler3/kotlin/ subtree and new tests/transpiler3/kotlin/ fixture corpus. Phase gates ensure no cross-target regression. On the JVM, MEP-47 (direct bytecode) and MEP-50 (kotlin-jvm via kotlinc) coexist; users pick per workload.

Reference Implementation

Implementation lives under:

  • transpiler3/kotlin/aotir/: Mochi aotir IR consumer (target-agnostic).
  • transpiler3/kotlin/lower/: aotir to Kotlin source codegen pass.
  • transpiler3/kotlin/emit/: file writer, build.gradle.kts / settings.gradle.kts / libs.versions.toml emitters, KotlinPoet invocation, ktfmt invocation.
  • transpiler3/kotlin/build/: driver for ./gradlew build, ./gradlew bundleRelease, ./gradlew linkReleaseExecutable<TGT>, bundletool validate.
  • runtime/kotlin/MochiRuntime/: KMP Gradle module source.
  • tests/transpiler3/kotlin/: fixture corpus and gate tests.

The transpiler3/kotlin codegen pass is approximately 4000 LOC of Go (a Mochi-side syntax tree model plus KotlinPoet driver). The runtime library MochiRuntime is approximately 6000 LOC of Kotlin across ~30 files. The fixture corpus targets ~400 fixtures by Phase 18 completion.

Phase plan

Eighteen phases mirroring MEP-45 / MEP-46 / MEP-47 / MEP-48 / MEP-49:

PhaseNameSurface
1Hello worldbasic print, let, int
2Scalarsint/float/bool/string ops, comparisons
3.1Listslist literal, index, len, for-each
3.2Mapsmap literal, index, len, keys, values, has
3.3Setsset literal, add, has, len
3.4Lists of recordslist[record], comprehensions over records
4Recordsrecords, methods, equality, copy
5Sum typessum types, pattern matching, exhaustiveness
6Closures and higher-orderclosures, higher-order, suspend colouring
7Query DSLfrom/where/select, group_by, order_by, joins
8Datalogfacts, rules, recursion
9Agentsactor pattern, spawn, call, cast
10StreamsFlow, AsyncSequence, await foreach
11Async colouringsuspend colouring, structured concurrency
12FFIJNI (JVM), cinterop (K/Native), external (K/JS)
13LLMgenerate (provider-pluggable)
14Fetchfetch (Ktor client, against local test server)
15Android App Bundle.aab/.apk bundle, signing, AGP 8.7+ build
16Reproducible buildbyte-identical .jar and .aab
17Kotlin/Native single binariesK/Native single binary, Linux/macOS/Windows
18Play Console validationGoogle Play Console pre-launch validation

Per 11-testing-gates §18, a phase lands only when all gates are green: per-phase fixture corpus, kotlinc clean, ktfmt fixed-point, cross-target differential where applicable, Android App Bundle validation for Phase 15+, K/Native single-binary gate for Phase 17, Play Console pre-launch validation for Phase 18.

Dependencies

  • MEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match): Mochi front-end.
  • MEP-45 (C transpiler): aotir IR plus shared passes.
  • MEP-46 (BEAM transpiler): shared IR confirmation, cross-target gates.
  • MEP-47 (JVM transpiler): shared IR confirmation; MEP-47 emits JVM bytecode directly (skip kotlinc), MEP-50 emits Kotlin source then routes through kotlinc. Both produce JVM .jar artefacts; the cross-target differential gate verifies byte-equal stdout.
  • MEP-48 (.NET transpiler): shared IR confirmation, prior art for C#-source emission via Roslyn SyntaxFactory.
  • MEP-49 (Swift transpiler): shared IR confirmation, prior art for Swift-source emission via a Mochi-side syntax tree.

External:

  • Kotlin toolchain 2.1+ (JetBrains-shipped via Gradle Kotlin plugin).
  • Gradle 8.11+ (bundled via the Gradle wrapper).
  • Android Gradle Plugin 8.7+ (for kotlin-android target).
  • Android SDK platform-35, build-tools-35.0.0, NDK r26+ (for kotlin-android target).
  • KotlinPoet 1.18+ (Square's Kotlin-source emitter, runtime dep of the transpiler).
  • ktfmt 0.49+ (Facebook's deterministic Kotlin formatter, optional dep of the transpiler).
  • bundletool (Google, for .aab validation).
  • apksigner, jarsigner, zipalign (Android SDK build-tools).
  • kotlinx.coroutines 1.10+, kotlinx.serialization 1.7+, kotlinx.collections.immutable 0.3.8+, kotlinx.datetime 0.6+ (runtime deps).
  • Ktor 3.0+ (client, for fetch).

Open questions

Per 12-risks-and-alternatives §3:

  • Q1: Jetpack Compose Multiplatform lowering. v2.
  • Q2: SwiftUI interop from Kotlin/Native iOS frameworks. v2.
  • Q3: Embedded Kotlin / Kotlin/Native stripped-down runtime. v2.
  • Q4: Kotlin/Wasm WASI target (alongside Wasm GC). v2 when toolchain matures.
  • Q5: Kotlin/Native watchOS / tvOS coverage. v2.
  • Q6: Distributed agents over kotlinx.coroutines Channel<T> networking. v2.
  • Q7: Google Play Console release automation via Gradle Play Publisher plugin. v2.

Security considerations

  • Android: apksigner v3 / v4 mandatory for Play Store distribution; minimum target API 31 for new apps (Google Play 2024 policy, MEP-50 targets API 35 to stay current).
  • Android manifest: deny-by-default permissions; runtime permission prompts emitted via Mochi's permission DSL.
  • JVM: jarsigner with SHA-256 mandatory; reproducible build excludes signature.
  • TLS pinning available via Ktor client HttpsURLConnection interceptor.
  • FFI requires manifest entry; reflection-bypass via kotlin-reflect rejected at runtime (kotlin-reflect is not a runtime dep of MochiRuntime).
  • K/Native: no Unsafe constructs in user-facing code; runtime library MochiRuntime.Unsafe namespace contains pointer manipulations under @OptIn(ExperimentalForeignApi::class).
  • Kotlin/JS and Kotlin/Wasm: emitted code has no eval or Function constructor usage; CSP-strict by default.
  • Play Console pre-launch validation runs every fixture through Google's automated security scanner; failures block release.

References

Research notes

Twelve research notes elaborate the design:

  • 01-language-surface: Mochi sub-languages mapped onto Kotlin 2.1 lowering obligations.
  • 02-design-philosophy: Why Kotlin, why 2.1 floor, why Channel + CoroutineScope (not the deprecated actor { }), why all KMP targets.
  • 03-prior-art-transpilers: J2K (IntelliJ Java-to-Kotlin), ts2kt (TypeScript-to-Kotlin definition files), KotlinPoet (Square's source emitter), the Compose UI compiler, the J2CL story.
  • 04-runtime: kotlin stdlib, kotlinx.coroutines, kotlinx.serialization, kotlinx.collections.immutable, kotlinx.datetime, MochiRuntime layout.
  • 05-codegen-design: Mochi-side syntax tree emission via KotlinPoet, ktfmt integration, aotir IR reuse.
  • 06-type-lowering: Type-by-type mapping to Kotlin 2.1 (Long, data class, sealed interface, custom actor class, Flow, kotlinx.serialization Codable).
  • 07-kotlin-target-portability: KMP target matrix (JVM, Android, K/Native, K/JS, K/Wasm), Static Linux SDK comparison, Play Console packaging.
  • 08-dataset-pipeline: Query DSL lowering via Kotlin Sequence plus Flow plus kotlinx.collections.immutable, Datalog engine.
  • 09-agent-streams: Mochi agents as a custom actor class wrapping Channel<Message>, Flow streams, structured concurrency via CoroutineScope(SupervisorJob() + Dispatchers.Default).
  • 10-build-system: Gradle Kotlin DSL, KGP, AGP, Maven Central publish, version catalog, Gradle wrapper.
  • 11-testing-gates: Per-phase Go test gates, Kotlin version matrix, Play Console validation, K/Native single-binary gate.
  • 12-risks-and-alternatives: Risk register, Java-source emission and Scala interop and Groovy DSL rejected and why.

The 18-phase delivery plan walks from hello-world through scalars, collections, records, sums, closures, queries, datalog, agents, streams, async, FFI, LLM, fetch, then Android App Bundle, reproducibility, Kotlin/Native single binaries, and Play Console validation. Each phase is gated against vm3.