Skip to main content

MEP 47. Mochi-to-JVM transpiler: Maven Central ecosystem, Loom-native agents, GraalVM native-image AOT

FieldValue
MEP47
TitleMochi-to-JVM transpiler
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-23 01:14 (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)
Research~/notes/Spec/0047/01..12
Tracking/docs/implementation/0047/

Abstract

Mochi today ships vm3 (mochi run), an ahead-of-time C transpiler producing native single-file binaries (MEP-45), and an Erlang/BEAM transpiler producing supervised concurrent runtimes (MEP-46). None of these paths gives users the JVM ecosystem: 10+ million artefacts on Maven Central, Project Loom virtual threads, the C2 JIT, GraalVM ahead-of-time native-image, JDK FFI to java.util.* / java.net.* / java.time.* / java.security.*, Android via D8/R8, Spring/Helidon/Vert.x/Quarkus integration, and 30 years of HotSpot performance engineering. MEP-47 specifies a third transpiler pipeline that targets the Java Virtual Machine via Java source emitted through javax.tools.JavaCompiler, with a java.lang.classfile (ClassFile API, JEP 484) direct-bytecode fallback for hot lowerings.

The pipeline reuses MEP-45's typed-AST and aotir IR, plus the monomorphisation, match-to-decision-tree, and closure-conversion passes shared with MEP-46. It forks at the emit stage: instead of emitting ISO C23 (MEP-45) or Core Erlang via cerl (MEP-46), it emits Java source as text strings (via Palantir JavaPoet 0.9.0+ as the structural builder, see [[05-codegen-design]] §4), feeds them to an in-process javax.tools.JavaCompiler instance, and collects the resulting .class files. Four packaging targets ship together: --target=jvm-uberjar produces a single-file fat jar runnable via java -jar (~5-20 MB, ~500 ms cold start, requires JDK on $PATH); --target=jvm-jlink produces a custom runtime image bundling a minimal JDK (~50 MB, ~200 ms cold start, self-contained); --target=jvm-native produces a GraalVM native-image binary (~10-25 MB, ~50 ms cold start, no JDK required); --target=jvm-jpackage produces a platform installer (.deb, .dmg, .msi, .app). A fifth secondary target --target=jvm-android ships a .aar library suitable for inclusion in Android projects (preview, MEP-47.1 sub-MEP).

The master correctness gate is byte-equal stdout from the produced .class (run via java -jar or native-image) versus vm3 on the entire fixture corpus, across JDK 21 LTS (Temurin) and JDK 25 LTS (Temurin), 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. Java source via javax.tools.JavaCompiler, with ClassFile API fallback. The default emit path produces Java source, then invokes the JDK-bundled javax.tools.JavaCompiler in-process to generate .class files. Java source carries no version drag (every JDK ships with its own javac), gives us free static typing checks (catching transpiler bugs at compile time), produces debuggable output (a developer can read the emitted Java), and exploits the entire javac feature set including record patterns, sealed interfaces, and pattern matching for switch (JEP 440 + JEP 441 in JDK 21). For hot lowerings where source is too verbose or too slow (lambda call sites, monomorphic numeric loops), the ClassFile API (JEP 484, GA in JDK 24, with ASM 9.7 as the JDK 21 fallback) emits bytecode directly. See [[05-codegen-design]] §1-§4. Kotlin/Scala/Groovy as IR are rejected: see [[02-design-philosophy]] §9 and [[12-risks-and-alternatives]] §A1-A2.

  2. JDK 21 LTS minimum, JDK 25 LTS preferred. JDK 21 (September 2023) is the floor: virtual threads (JEP 444), sealed classes (JEP 409), records (JEP 395), record patterns (JEP 440), and pattern matching for switch (JEP 441) are all final. JDK 25 (September 2025) adds AOT class loading (JEP 514), AOT method profiling (JEP 515), generational Shenandoah (JEP 519), compact source files (JEP 512), and final scoped values (JEP 506). The build emits with --release 21 for source compatibility; CI runs the full matrix on both. JDK 17 is rejected: no virtual threads, no record patterns. JDK 8/11 are rejected as outside support. See [[02-design-philosophy]] §2 and [[07-jvm-target-portability]] §1.

  3. Loom-native agents and streams; no async/await keywords, no actor framework. Mochi agents lower one-to-one to virtual threads (JEP 444) with LinkedTransferQueue<Object> mailboxes. Streams lower to java.util.concurrent.Flow.Publisher<T> (JSR 266, JDK 9+). spawn is Thread.ofVirtual().start(); await is CompletableFuture.get(). No Akka, no Pekko, no Reactor, no RxJava as defaults. The runtime exposes adapter helpers for users who want those libraries. See [[09-agent-streams]] §1-§6, [[02-design-philosophy]] §10, [[12-risks-and-alternatives]] §A10-A11.

  4. Reuse MEP-45's aotir IR. The IR is target-agnostic; monomorphisation, match-to-decision-tree, and closure-conversion passes run once and feed three backends. The fork is at the emit pass: transpiler3/jvm/lower/ lowers aotir to Java-source structural nodes; transpiler3/beam/lower/ lowers aotir to cerl records; transpiler3/c/emit/ lowers aotir to C. Sharing the IR keeps the three targets semantically aligned and amortises pass-implementation work across all three. See [[05-codegen-design]] §5.

  5. JDK as fat runtime; dev.mochi.runtime as thin runtime. The runtime library dev.mochi.runtime (Apache-2.0, ~5000 LOC) provides only what the JDK does not: a Mochi-typed function-interface zoo (Fn1<A,R>, Fn2<A,B,R>, ...), Mochi-shaped collection helpers (Lists.concat, Maps.put immutable variants), a small Datalog engine, JFR event definitions, the AI / FFI dispatch tables, and the agent/stream support classes. Everything else (HTTP, JSON, time, regex, locale, file I/O) goes through the JDK directly. Jackson 2.18 LTS, snakeyaml-engine 2.10+, and bouncycastle are the only vendored Maven Central deps. Spring, Guava, Apache Commons, and Netty are explicitly rejected as runtime deps. See [[04-runtime]] §1-§17, [[02-design-philosophy]] §4 and §7.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/jvm/fixtures/ must compile via the JVM pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. javac-clean (with -Xlint:all -Werror) on generated code is the secondary gate. GraalVM native-image build (with auto-generated reachability metadata) is the tertiary gate. Reproducibility (bit-identical jar across two CI hosts) is the quaternary gate.

Motivation

Mochi today targets vm3 (for mochi run), the C target (for static native binaries via MEP-45), and the BEAM target (for supervised concurrent runtimes via MEP-46). None deliver what the JVM uniquely provides:

  1. Maven Central, the largest software-component repository in existence. As of 2026-05, Maven Central hosts more than 10 million unique artefacts across 700,000+ groupIds, with Sonatype's annual ecosystem report documenting more than 1 trillion artefact downloads in the prior twelve months. A Mochi program that needs PDF generation, Excel parsing, BLE communication, Kubernetes API access, OAuth, AWS S3, GCP BigQuery, Azure Cosmos, or any of thousands of other capabilities, can import an existing Maven Central artefact, with a SHA-256 pin and a transitive-dep lockfile. The C ecosystem has Conan/vcpkg; BEAM has Hex.pm; but neither matches Maven Central's breadth.

  2. Project Loom virtual threads, GA since JDK 21. Loom (JEP 444, finalised 2023-09-19) makes blocking I/O free: a virtual thread allocates a few hundred bytes, can park on BlockingQueue or Socket.read without pinning a carrier OS thread, and the JVM scheduler multiplexes thousands of virtual threads onto a small carrier pool. Mochi agents map one-to-one. The JDK 24 fix for synchronized pinning (JEP 491, shipped 2025-03) removed the last common pinning pitfall. For Mochi's concurrency model (mailboxes + non-blocking message passing + structured concurrency), Loom is exactly the runtime we want. The C target has no equivalent; the BEAM target has BEAM's preemptive scheduler (a different but also-good model).

  3. HotSpot JIT plus GraalVM native-image AOT. Mochi programs running long enough to JIT-warm reach C-comparable performance on numeric and collection-heavy workloads, with the C2 JIT inlining lambdas, monomorphising calls, eliding bounds checks, and auto-vectorising loops. For short-lived programs, GraalVM native-image (or its successor Project Leyden, see JEPs 483/514/515) produces ahead-of-time binaries with sub-50ms cold start and ~10 MB footprint, competitive with MEP-45 C output. The user picks per workload.

  4. JDK is one of the best-engineered runtime libraries on the planet. java.time (JSR 310) is the canonical date-time library copied by Kotlin, Scala, and a dozen other languages. java.net.http is HTTP/2 capable. java.nio.file handles cross-platform paths correctly. java.util.concurrent is the reference design for lock-free data structures. java.security covers TLS, X.509, JWT, AEAD ciphers, message digests. java.util.stream is a fluent collection-processing API. Mochi FFI to any of these is direct: import "java/time/Instant" lowers to import java.time.Instant. The C target ships hand-written equivalents (date-parsing in C is painful); the BEAM target uses OTP's libraries (excellent but smaller). The JVM target gets the JDK for free.

  5. Tooling. IntelliJ IDEA, Eclipse, NetBeans, VS Code Java extension, JFR, JConsole, VisualVM, Maven, Gradle, Bazel, jacoco, ErrorProne, SpotBugs, NullAway, async-profiler, OpenAPI generator, AssertJ, JUnit, TestNG, Mockito all work on Mochi-emitted classes because we emit standard JVMS-compliant .class files with SourceFile, LineNumberTable, and SourceDebugExtension (JSR 45) attributes.

  6. Android. The Android Runtime (ART) accepts JDK 17 bytecode after dex translation by D8/R8. A Mochi-on-JVM program (without virtual threads, which ART does not implement) can ship as a .aar library or, with --target=jvm-android, as a top-level Android app target. This is a preview target (MEP-47.1) but the codegen path is the same.

  7. Polyglot interop on GraalVM. The GraalVM polyglot runtime can call between Mochi (compiled to bytecode), JavaScript (via GraalJS), Python (via GraalPy), Ruby (via TruffleRuby), and R (via FastR). A Mochi data-pipeline that needs to call into a Python statistics library or a JavaScript template engine can do so without crossing a process boundary. (Note: GraalVM ownership shifted in 2025; see [[12-risks-and-alternatives]] §R5.)

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 is the right choice for Maven Central interop, Loom concurrency, JIT performance, GraalVM AOT, JDK FFI, Android, and enterprise integration. All four ship; the user picks per workload.

Specification

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

1. Pipeline and IR reuse

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

Mochi source
| parser (MEP-1/2/3, reused)
v
AST
| type checker (MEP-4/5/6, reused)
v
Typed AST
| monomorphise (MEP-45 pass 1, reused)
v
Monomorphic typed AST
| lower (MEP-45 pass 2, reused)
v
aotir (MEP-45's IR, reused)
| match-to-decision-tree (MEP-45 pass 3, reused)
v
aotir (matches lowered)
| closure-convert (MEP-45 pass 4, reused)
v
aotir (closures lowered)
| jvm-lower (MEP-47 pass 1; ./transpiler3/jvm/lower/)
v
Java-source AST (JavaPoet structural nodes; or ClassFile API records for hot paths)
| emit (./transpiler3/jvm/emit/)
v
Java source text (or .class bytes directly for hot paths)
| javax.tools.JavaCompiler in-process
v
JVM .class files
| package as uberjar / jlink / native-image / jpackage / aar
v
Distributable artefact

aotir is unchanged. The MEP-47-specific work lives in transpiler3/jvm/:

  • transpiler3/jvm/lower/: aotir -> Java-source AST (JavaPoet nodes).
  • transpiler3/jvm/emit/: pretty-prints to Java source text; drives javax.tools.JavaCompiler to .class.
  • transpiler3/jvm/classfile/: optional ClassFile API direct emission for hot lowerings (numeric loops, lambda call sites).
  • transpiler3/jvm/build/: build driver: uberjar / jlink / native-image / jpackage / aar targets; cache; lockfile.
  • transpiler3/jvm/runtime/: the Mochi runtime jar source (Java), built once and vendored.

transpiler3/jvm/lower/ consumes aotir.Module and produces javasrc.CompilationUnit. javasrc is a small Go package mirroring JavaPoet's structural model: ClassDecl, MethodDecl, Block, Statement, Expression. The emit pass walks this tree and writes formatted Java source, then invokes javac via the JDK's Process API (or the in-VM javax.tools.ToolProvider API if the toolchain has been provisioned).

The aotir IR is target-agnostic: it has no notion of Java packages, JVM bytecode, or Loom threads. The lowering pass adds those concerns. The ClassFile API path is opt-in: a function annotated with @hot (or detected as numeric-heavy by a small heuristic) skips the source path and emits bytecode directly via java.lang.classfile.ClassFile.build.

2. Name mangling and Java reservation safety

Java identifiers are stricter than Mochi's. The mangling rules (full table in [[06-type-lowering]] §2):

  • Mochi variables that collide with a Java reserved word (per JLS §3.9: abstract, assert, boolean, break, ..., synchronized, throws, ..., plus contextual keywords record, sealed, permits, non-sealed, yield, var) get suffix _. Example: class -> class_, record -> record_.
  • Mochi package paths mathutils/extra lower to Java package dev.mochi.user.mathutils.extra for user code. Configurable via --jvm-base-package; default dev.mochi.user. The user. segment is there to make the runtime/user distinction visible in stack traces.
  • Mochi record type names lower to Java class names in PascalCase, unchanged (Book -> Book). On collision with java.lang.* (String, Object, Number, Integer, Long, Double, Boolean) the backend renames String -> String_ and emits an import-shadowing comment.
  • Mochi sum-type variant constructors lower to Java record classes nested in a sealed interface (Leaf -> Tree.Leaf).
  • Mochi local function references and method names: camelCase preserved (fooBar stays fooBar).
  • Mochi runtime-internal symbols use the $$mochi_ prefix to avoid colliding with user names.

The mangling is deterministic; reversal goes through SourceFile + LineNumberTable JVMS attributes plus the SourceDebugExtension attribute (JSR 45) so stack traces point to Mochi source lines. See [[05-codegen-design]] §8.

3. Type lowering table

The full per-type table is in [[06-type-lowering]] §1-§14. The high-level mapping:

MochiJVMNotes
intlong (primitive); Long (boxed)64-bit; Java int is 32-bit, rejected
floatdouble (primitive); Double (boxed)64-bit IEEE 754
boolboolean; Boolean (boxed)
stringjava.lang.StringUTF-16 internally (compact strings, JEP 254); Mochi exposes code-point indexing
timejava.time.InstantJSR 310; nanosecond precision
durationjava.time.DurationJSR 310
list<T>java.util.List<T> (default ArrayList backed); LongList / DoubleList (primitive-specialised) for monomorphic numeric listsImmutable wrapper at function boundaries via List.copyOf
map<K,V>java.util.LinkedHashMap<K,V>Insertion-ordered, per Mochi spec
set<T>java.util.LinkedHashSet<T>Insertion-ordered
T { ... } (record)Java record (JEP 395)All-immutable fields; auto equals/hashCode/toString
type T = A | B (sum)sealed interface (JEP 409) + record variantsExhaustive switch checked by javac
fun(A, B) RJava lambda + JDK Function/BiFunction/specialised primitive interfaceLambdaMetafactory via invokedynamic (JEP 8003895)
stream<T>java.util.concurrent.Flow.Publisher<T>JSR 266 reactive streams
agentLoom virtual thread + LinkedTransferQueue<Object> mailboxJEP 444
Option<T>sealed Option<T> with Some<T>(T) / None<T>() recordsNOT java.util.Optional (which is not for fields per JEP 269 guidance)
Result<T,E>sealed Result<T,E> with Ok<T>(T) / Err<E>(E) records

Boxing: list<int> defaults to List<Long> (boxed). When the compiler detects a monomorphic numeric list not exposed to FFI as List<Object>, it specialises to LongList (backed by long[]) at the call sites. See [[06-type-lowering]] §10.

Immutability: JDK collections are mutable by default. The lowering enforces value semantics by wrapping every collection that leaves a function via List.copyOf / Map.copyOf / Set.copyOf. For high-mutation paths, the runtime provides PersistentList/PersistentMap/PersistentSet (HAMT-based; ~2000 LOC, in dev.mochi.runtime.collections). The compiler picks JDK-immutable vs persistent based on the inferred mutation rate. See [[06-type-lowering]] §11.

4. Expression and statement lowering

Mochi expressions lower to Java expressions; statements lower to Java statements. The full lowering table is in [[05-codegen-design]] §6. Highlights:

  • Mochi if cond { a } else { b } lowers to a Java ternary cond ? a : b when both arms are expressions; lowers to a switch expression (JEP 361) when arms have side effects or complex flow.
  • Mochi match lowers to a Java switch expression with sealed-interface patterns and (where applicable) when guards (JEP 441). Exhaustiveness is double-checked: Mochi rejects non-exhaustive matches; javac independently confirms via sealed exhaustiveness.
  • Mochi for x in xs lowers to a Java enhanced-for loop. For maps, for (k, v) in m lowers to a loop over m.entrySet() with destructuring.
  • Mochi early-return lowers to Java return. Mochi break / continue lower to Java equivalents.
  • Mochi try/catch (the try { ... } catch { ... } form per MEP-19) lowers to Java try-catch; Mochi raises Mochi-specific MochiError subclasses unified under dev.mochi.runtime.error.MochiException. The JVM Throwable hierarchy is otherwise transparent to user code.
  • Block-as-expression (the last expression in a block is the block's value) lowers via a synthesised switch (1) { default -> { ...; yield e; } } when the value is consumed in an expression context.

Integer arithmetic uses native JVM long ops (silent overflow, two's complement). The --strict-int build flag wraps every arithmetic op in Math.addExact / Math.subtractExact / Math.multiplyExact, which throw ArithmeticException on overflow. Off by default; on for security-sensitive builds. See [[06-type-lowering]] §5.

Integer division is floor-division: a / b (int) lowers to Math.floorDiv(a, b). Modulo is Math.floorMod(a, b). See [[06-type-lowering]] §6.

5. Closures and lambdas

Mochi first-class function values lower to Java lambdas via invokedynamic and LambdaMetafactory (JEP 8003895, the standard javac path since Java 8). Capture is by-value of effectively-final locals (the Java rule); mutable captures lift into a 1-element array or a dev.mochi.runtime.Cell<T> heap box.

The runtime exposes a function-interface zoo for signatures the JDK does not specialise:

  • Fn0<R>, Fn1<A,R>, ..., Fn9<A,B,C,D,E,F,G,H,I,R> (generic, boxed args)
  • LongFn1<R>, LongFn2<R>, DoubleFn1<R>, DoubleFn2<R>, etc. (primitive-specialised for numeric perf)
  • LongUnaryOperator, LongBinaryOperator, DoubleUnaryOperator, etc. (re-exports of JDK types)

The lowering picks the most-specific JDK type when one matches, falling back to dev.mochi.runtime.func.*. See [[05-codegen-design]] §6 and [[06-type-lowering]] §6.

Lambda call sites can be ClassFile-API-emitted directly for hot paths, bypassing the source layer; this is opt-in via @hot annotation or a heuristic (calls-per-second in a profiling pass). See [[05-codegen-design]] §4.

6. Runtime library

The dev.mochi.runtime Maven Central artefact (coordinates dev.mochi:mochi-runtime:0.10.0+) is the sole external dep that every emitted JVM Mochi program needs. It is published to Maven Central via the new Central Publisher Portal (Sonatype OSSRH sunset 2025-06-30; see [[10-build-system]] §5).

Package layout ([[04-runtime]] §16):

dev.mochi.runtime
.agent // virtual-thread-backed agents (Agent, Supervisor, Linkage)
.ai // AI provider dispatch (OpenAI, Anthropic, local)
.collections // PersistentList, PersistentMap, PersistentSet (HAMT)
.context // scoped-value-based current-agent / trace-id context
.datalog // small in-memory semi-naive engine + tuple tables
.error // MochiException hierarchy
.ffi // FFI registry and dispatch
.func // Fn0..Fn9, LongFn*, DoubleFn*
.io // print, println, eprintln
.json // jackson 2.18 thin wrapper
.query // HashJoin, SortMergeJoin, grouping collectors
.scope // structured-concurrency wrappers (MochiScope)
.stream // SubmissionPublisher subclass, ReplayStream
.strings // codepoint helpers, byte conversions
.telemetry // JFR event definitions (AgentStart, MessageSend, QueryStart, ...)
.time // shims that delegate to java.time

Vendored deps (pinned, published to Maven Central with our SHA-256):

  • com.fasterxml.jackson.core:jackson-databind:2.18.7 (Jackson LTS)
  • org.snakeyaml:snakeyaml-engine:2.10 (YAML 1.2 parser)
  • org.bouncycastle:bcprov-jdk18on:1.78.1 (only when crypto FFI used)

Rejected as runtime deps: Spring, Guava, Apache Commons Lang/IO/Collections, Netty. See [[10-build-system]] §6 and [[12-risks-and-alternatives]] §A12.

The runtime jar size target: <2 MB before tree-shaking; <500 KB after GraalVM native-image dead-code elimination.

7. Concurrency and supervision

Mochi agents lower to Loom virtual threads. Each agent declaration produces a Java class with a LinkedTransferQueue<Object> mailbox, a Thread.ofVirtual().name(...).start(this::$$run) thread, and a $$run loop that dispatches messages by type. See [[09-agent-streams]] §2 and §3.

public final class Counter implements Agent {
private final TransferQueue<Object> $$mailbox = new LinkedTransferQueue<>();
private final Thread $$thread;
private long count;
public Counter() { this.$$thread = Thread.ofVirtual().name("mochi-Counter").start(this::$$run); }
private void $$run() {
while (!Thread.currentThread().isInterrupted()) {
var msg = $$mailbox.take();
switch (msg) {
case Inc i -> count += i.delta();
case Value v -> v.reply().complete(count);
case Stop s -> { return; }
}
}
}
public void send(Object m) { $$mailbox.put(m); }
}

Supervision: Mochi's supervisor declaration lowers to a dev.mochi.runtime.agent.Supervisor that re-implements OTP's one_for_one, one_for_all, and rest_for_one strategies in user-space. Crashed agents (uncaught exception in $$run) trigger restart per the declared policy. See [[09-agent-streams]] §10.

spawn f() lowers to Thread.ofVirtual().start(() -> f()) returning a CompletableFuture<Void>. await f() lowers to f.get() (blocking on the virtual thread, which is cheap thanks to Loom). See [[09-agent-streams]] §5.

Structured concurrency (Mochi scope { ... }) lowers to StructuredTaskScope (JEP 505, preview in JDK 21+25) via a dev.mochi.runtime.scope.MochiScope wrapper that hides the preview API. See [[09-agent-streams]] §7.

The runtime avoids synchronized everywhere; uses ReentrantLock instead. This is to side-step the JDK 21 Loom pinning issue (fixed in JDK 24 via JEP 491, but JDK 21 LTS is the floor). The CI TestPhase9NoSyncPinning gate ([[11-testing-gates]] §12) ensures no synchronized regressions.

8. Memory model

The Mochi memory model on the JVM is the Java memory model (JLS §17) restricted to Mochi-allowed operations. Highlights:

  • All value-type locals (long, double, boolean) follow Java primitive semantics: thread-local unless explicitly shared via a mailbox or CompletableFuture.
  • All reference-type locals (record instances, collection wrappers) are immutable from Mochi's POV; mutation is a transpiler-internal concern (copy-on-write via List.copyOf etc.). The JMM final-field freeze semantics apply to records by construction.
  • Cross-agent communication via mailbox is a full happens-before edge (the LinkedTransferQueue is Volatile-correct).
  • Agents do not share mutable state with each other except via mailbox; the runtime does not provide a "shared variable" primitive.

GC: the build defaults to G1GC on JDK 21+, with ZGC available via --gc=zgc for low-latency workloads (JEP 439 generational ZGC, JDK 21 final). Generational Shenandoah (JEP 519, JDK 25) is available via --gc=shenandoah for users on JDK 25+. See [[04-runtime]] §2.

Memory budgets: agents idle at ~200 B/thread (Loom). Mailbox entries cost the message size plus ~40 B queue node. Persistent collections cost more than JDK collections by ~30% for small sizes; comparable at large sizes. See [[04-runtime]] §5.

9. Error model

Mochi exceptions lower to dev.mochi.runtime.error.MochiException (extends java.lang.RuntimeException) with subclasses for each Mochi error kind:

  • MochiPanicException: panic/abort
  • MochiPatternError: non-exhaustive pattern match (should be unreachable post-typecheck)
  • MochiCastError: type cast at FFI boundary
  • MochiBoundsError: out-of-bounds index
  • MochiArithmeticError: overflow under --strict-int
  • MochiFFIError: FFI marshalling failure

Mochi try { ... } catch e { ... } lowers to Java try-catch on MochiException (and its subclasses).

JVM Error (out-of-memory, stack overflow) and non-Mochi RuntimeException are not caught by Mochi's catch; they propagate to the thread's UncaughtExceptionHandler, which the runtime sets to a JFR event emitter + structured stderr write. See [[06-type-lowering]] §14.

Stack traces: every emitted method gets a LineNumberTable mapping Java lines back to Mochi source via SourceDebugExtension (JSR 45). A Mochi user sees a Mochi-line stack trace; the underlying Java mapping is available via -XX:+PreserveFramePointer JFR output for advanced debugging. See [[05-codegen-design]] §8.

10. Target portability

The portability matrix is in [[07-jvm-target-portability]]. Tier-1 (full CI coverage; release gate green):

  • Linux x86_64 glibc (Ubuntu 24.04)
  • Linux aarch64 glibc (Ubuntu 24.04)
  • macOS aarch64 (M1+)
  • Windows x86_64

Tier-2 (smoke tests; release gate yellow):

  • Linux x86_64 musl (Alpine) - native-image only path
  • macOS x86_64 (Intel)
  • Windows aarch64

Tier-3 (best-effort; community-maintained):

  • Linux ppc64le, s390x, riscv64
  • Android (preview, MEP-47.1)

JDK matrix: JDK 21 LTS (Temurin) and JDK 25 LTS (Temurin) on every tier-1 cell. JDK 26 EA smoke-tested but non-blocking.

GraalVM native-image: tier-1 cells get a native-image build per release; the metadata is regenerated nightly by running fixtures under -agentlib:native-image-agent.

Distribution: Temurin (Eclipse Adoptium) is the recommended JDK; we document Corretto, Zulu, Liberica, Oracle JDK, Microsoft Build, SapMachine, and Red Hat Build as compatible. Semeru (OpenJ9) is acknowledged but not tested. See [[07-jvm-target-portability]] §3.

11. Build driver

The Mochi build driver routes JVM builds through transpiler3/jvm/build/. Targets:

  • --target=jvm-source: emit Java source only (for review / debugging).
  • --target=jvm-classes: emit .class files into target/jvm/classes/.
  • --target=jvm-uberjar (default for mochi build): single-file fat jar via Maven Shade-style packaging.
  • --target=jvm-jlink: custom runtime image bundling a minimal JDK.
  • --target=jvm-native: GraalVM native-image AOT binary.
  • --target=jvm-jpackage: platform installer (.deb, .dmg, .msi).
  • --target=jvm-aar: Android Archive (preview, MEP-47.1).

Output layout (mirrors MEP-45's target/c/ and MEP-46's target/beam/):

target/jvm/
source/ # emitted .java files
classes/ # compiled .class files
resources/ # META-INF/, resource files
vendor/ # vendored Maven Central jars (Jackson, snakeyaml-engine, ...)
out/ # final artefacts (jar, native-image binary, installer)
cache/ # incremental build cache (per-module fingerprints)
reachability/ # GraalVM reachability metadata
lockfile.json # Maven coordinate + SHA-256 pin per dep

The build is incremental: per-module fingerprints in cache/ skip unchanged modules. The lockfile pins every transitive dep with SHA-256. See [[10-build-system]] §2 and §13.

Gradle plugin (dev.mochi.compile, Gradle 9.5.1+, Configuration Cache aware) and Maven plugin (Maven 4.0.0-rc / 3.9.16+) are first-class: users who already have a Gradle or Maven build can add a small Mochi-build step. See [[10-build-system]] §7 and §8.

12. Reproducibility

Bit-identical builds across two CI hosts are a gate ([[11-testing-gates]] §8). The build uses:

  • SOURCE_DATE_EPOCH for jar timestamps (the OpenJDK jar tool honours this since JDK 15).
  • --release 21 flag for stable bytecode (no JDK-version-dependent constant pool ordering).
  • maven-jar-plugin >= 3.4.0 for reproducible-by-default packaging.
  • Deterministic class member ordering (alphabetical by name within each access bucket).
  • No timestamp / hostname / username in any emitted file.

For native-image: GraalVM has well-known non-determinism sources (parallel-build worker scheduling, classpath hashing). The build uses --parallelism=1 for reproducible native-image builds, with a separate --target=jvm-native-fast for non-reproducible parallel builds. See [[10-build-system]] §12.

The diffoscope tool runs as a CI gate on a subset of fixtures, comparing jar contents structurally. See [[11-testing-gates]] §8.

13. Hardening

The runtime takes a defensive posture:

  • All public runtime APIs validate inputs (null check, range check). Internal APIs trust callers.
  • JSON parsing uses Jackson's secure-by-default ObjectMapper configuration: no polymorphic deserialisation by default (CVE-2017-7525 family avoidance), no @JsonTypeInfo unless explicitly opted in.
  • YAML parsing uses snakeyaml-engine in safe mode (no !!python/object style tags).
  • HTTP via java.net.http.HttpClient defaults to TLS 1.3, hostname verification on, strict certificate chain.
  • Random uses LXM family PRNGs (JEP 356) for non-cryptographic randomness; SecureRandom for crypto.

Supply-chain hardening: Maven coordinates pinned by SHA-256 in the lockfile. mochi audit checks pinned versions against the OSV database. See [[10-build-system]] §5 and [[12-risks-and-alternatives]] §R3.

GraalVM native-image gets --strict-image-heap to reject runtime-initialised classes that should be build-time. Reachability metadata is vetted before checking in. See [[07-jvm-target-portability]] §5.

14. Diagnostics

Emitted Java source carries // comments mapping back to Mochi source positions. Generated stack traces (via SourceDebugExtension) point to Mochi lines.

JFR events ([[04-runtime]] §14) cover:

  • dev.mochi.AgentStart / AgentStop
  • dev.mochi.MessageSend / MessageReceive
  • dev.mochi.QueryStart / QueryEnd (with row count, duration)
  • dev.mochi.GCPressure (custom event on G1GC mixed-mode entry)
  • dev.mochi.FFICall (FFI call boundary, with target package)

Telemetry: the runtime optionally exports JFR events to OpenTelemetry via io.opentelemetry:opentelemetry-jfr-receiver. See [[04-runtime]] §14.

Compile-time diagnostics: the Mochi-to-Java lowering emits javac-friendly source that javac can lint with -Xlint:all -Werror. Any warning is a transpiler bug (see [[11-testing-gates]] §4). The transpiler also runs its own checks on aotir before lowering (unreachable code, unused captures, etc.).

15. Debug info

The build emits two kinds of debug info:

  1. Java debug info (-g:lines,vars,source): LineNumberTable, LocalVariableTable, SourceFile JVMS attributes. Lets debuggers (IntelliJ, jdb, async-profiler) navigate the emitted Java.
  2. Mochi source maps (JSR 45 SourceDebugExtension): maps Java lines back to Mochi source lines. Lets a Mochi user see Mochi-line stack traces.

JDWP (Java Debug Wire Protocol) works on emitted classes: mochi run --jvm --debug launches with -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005. See [[10-build-system]] §15.

16. Java FFI

The import "java/..." form imports a Java class as a Mochi type:

import "java/util/UUID" as UUID
import "java/time/Instant" as Instant
import "java/com/fasterxml/jackson/databind/ObjectMapper" as ObjectMapper

let id = UUID.randomUUID()
let now = Instant.now()

Lowers to:

import java.util.UUID;
import java.time.Instant;
import com.fasterxml.jackson.databind.ObjectMapper;
// ...
final UUID id = UUID.randomUUID();
final Instant now = Instant.now();

The lowering exposes the Java class's public API as Mochi methods, with Mochi-typed signatures derived from the class's bytecode (via java.lang.classfile introspection at build time). Mochi-Java type marshalling per [[06-type-lowering]] §13: Mochi int <=> Java long; Mochi string <=> Java String; Mochi list<T> <=> Java List<T> (with element marshalling); etc.

Maven Central coordinates can be declared in the Mochi source via a module-level pragma:

@maven("com.fasterxml.jackson.core:jackson-databind:2.18.7")
import "java/com/fasterxml/jackson/databind/ObjectMapper" as ObjectMapper

The build resolves the coordinate via Maven resolver, pins the SHA-256 in the lockfile, vendors the jar. See [[10-build-system]] §5.

Null safety at the FFI boundary: every Java return value (which can be null) crosses an Objects.requireNonNull checkpoint, or, for return types declared as Option<T> in the Mochi import, is wrapped via Optional.ofNullable(...).map(Some::new).orElseGet(None::instance). See [[06-type-lowering]] §15.

17. Output style

Emitted Java source follows a deterministic style:

  • Two-space indentation.
  • Imports sorted alphabetically; no wildcard imports.
  • One top-level class per file (matches the JLS public-class-per-file rule).
  • Class members in fixed order: fields, constructors, static factories, instance methods, nested classes.
  • Records used wherever the Mochi type is immutable and has <=10 fields.
  • Sealed interfaces used for sum types with permits clauses explicit.
  • Pattern matching for switch (JEP 441) used for match lowering.
  • var (local-variable type inference, JEP 286) used for emitted locals where the type is unambiguous; explicit types otherwise.
  • No final keyword on parameters (treated as implicit by the lowering).

The style is enforced by a CI gate (TestJvmCodegenStyle) that diffs emitted Java against a checked-in expected format for a representative set of fixtures.

Rationale

Why JVM as a third target after C and BEAM

The C target (MEP-45) gives Mochi static-linked native binaries, sub-10ms cold start, embedded-target portability, and bit-precise control. The BEAM target (MEP-46) gives Mochi telecom-grade concurrency, hot reload, supervision, and cluster pubsub. Neither gives Mochi: (a) access to Maven Central, the world's largest software-component repository; (b) Project Loom virtual threads, the cleanest concurrency model in the mainstream language ecosystem; (c) HotSpot JIT + GraalVM native-image, the best-rounded JIT+AOT story available; (d) Java's java.time / java.net.http / java.security / java.util.concurrent standard library; (e) Android via D8/R8.

The user-facing positioning: vm3 for development, C for embedded and single-file distribution, BEAM for distributed services with hot reload, JVM for everything else, especially programs that need a Maven Central dep or Android. See [[02-design-philosophy]] §1.

Why Java source via javax.tools.JavaCompiler, not bytecode-only

Five reasons (full discussion [[05-codegen-design]] §3):

  1. Free static-type checks: javac itself catches transpiler bugs (mistyped lowering, missing import, scope error). Emitting bytecode directly skips this layer.
  2. Debuggable output: a Mochi developer can read the emitted Java to understand what their program will do. Bytecode is opaque.
  3. No version churn: javac ships with every JDK; no library version-pin needed.
  4. Full feature support: record patterns, sealed interfaces, switch patterns, var, text blocks, all work via javac without us implementing them.
  5. Hybrid escape hatch: for hot lowerings (lambda call sites, numeric loops) where source is too verbose, we drop to java.lang.classfile.ClassFile.build (JEP 484, JDK 24+) or ASM 9.7 (JDK 21 fallback). This is opt-in, not the default.

Kotlin/Scala/Groovy as IR: rejected because each adds a compiler dependency, output-runtime dependency, and a debugging layer. See [[12-risks-and-alternatives]] §A1-A2.

Why JDK 21 LTS minimum

JDK 21 (2023-09-19) is the floor for these features:

  • Virtual threads (JEP 444, final): the entire MEP-47 concurrency design depends on Loom.
  • Record patterns (JEP 440, final): how match lowers cleanly.
  • Pattern matching for switch (JEP 441, final): exhaustiveness checks for sealed-interface ADTs.
  • Sealed classes (JEP 409, final since 17): sum-type lowering.
  • Records (JEP 395, final since 16): product-type lowering.
  • Sequenced collections (JEP 431, final): list<T> reverse iteration etc.

JDK 17 is rejected: no virtual threads, no record patterns. The gap between JDK 17 and JDK 21 in Mochi-relevant features is large enough that supporting both would double the lowering complexity.

JDK 25 LTS (2025-09-16) is the preferred target: AOT class loading (JEP 514), AOT method profiling (JEP 515), generational Shenandoah (JEP 519), compact source files (JEP 512), scoped values final (JEP 506). All beneficial; none required.

See [[02-design-philosophy]] §2 and [[07-jvm-target-portability]] §1.

Why reuse aotir instead of a fresh IR

The aotir IR is target-agnostic by construction (MEP-45 §1, MEP-46 §1). Reusing it for MEP-47:

  • Amortises the monomorphisation, match-to-decision-tree, and closure-conversion passes across three backends. Bug fixes in those passes improve all three targets.
  • Forces cross-target semantic consistency: a Mochi program's aotir is the same on all three targets; divergence can only come from the lowering pass, which is auditable per-target.
  • Halves the implementation effort: only the lowering pass is JVM-specific.

The cost: any pass change must be vetted against all three backends. We accept this. See [[05-codegen-design]] §5.

Why Loom virtual threads, not async/await or actor framework

Project Loom (JEP 444) gives us cheap blocking: a virtual thread parking on a BlockingQueue does not pin a carrier OS thread, so we can have thousands of agents without thread-pool exhaustion. This eliminates the standard JVM-language need for async/await coloured functions (the Kotlin coroutine state machine, the Scala Future monad, etc.).

The alternatives, all rejected:

  • Akka/Pekko (actor framework): Loom makes the actor framework unnecessary. Akka's relicensing in 2022 + Pekko fork adds governance uncertainty. See [[12-risks-and-alternatives]] §A10.
  • Reactor/RxJava as default streams: java.util.concurrent.Flow.Publisher is the de-facto interop ABI for reactive libraries; users can adapt to Reactor / RxJava if they want, but we don't impose it. See [[12-risks-and-alternatives]] §A11.
  • Kotlin coroutines: requires the Kotlin compiler and runtime. We don't compile Mochi to Kotlin source.
  • Pluggable scheduler: complexity unjustified; Loom's default scheduler is good. See [[09-agent-streams]] §16.

See [[09-agent-streams]] §1 and §6, [[02-design-philosophy]] §10.

Why JDK as fat runtime, dev.mochi.runtime as thin

The JDK ships ~3000 classes covering HTTP, JSON (preview JEP 198 on indefinite hold, but Jackson is the universal fallback), time, regex, locale, file I/O, security, concurrency, streams, reflection, JFR, and more. Reusing it gives Mochi a runtime library matching or exceeding what any Mochi-specific library could provide.

The dev.mochi.runtime artefact ships only what the JDK does not: Mochi-typed function interfaces, persistent collection variants, agent / supervisor classes, the Datalog engine, JFR event definitions, FFI dispatch, and the AI / LLM provider abstractions. Total ~5000 LOC; ~2 MB jar before tree-shaking.

Spring/Guava/Apache Commons rejected: too transitive, too opinionated, supply-chain risk. See [[10-build-system]] §6, [[02-design-philosophy]] §7, [[12-risks-and-alternatives]] §A12.

Why GraalVM native-image as a first-class build target

Mochi users want a single-file binary they can ship to a server (or hand to a colleague) without requiring a JDK install. GraalVM native-image (or its successor Project Leyden) gives us this: AOT compilation of all reachable classes into a static binary, sub-50ms cold start, no JDK on $PATH required.

The trade-off: AOT means closed-world; reflection / dynamic class loading needs reachability metadata. We mitigate via auto-generation (-agentlib:native-image-agent in CI), the GraalVM Reachability Metadata Repository, and a hand-curated reachability-metadata.json in the runtime jar. See [[07-jvm-target-portability]] §5 and [[10-build-system]] §11.

GraalVM ownership uncertainty (Oracle's 2025 shift away from GraalVM CE, see [[12-risks-and-alternatives]] §R5): mitigated by tracking Liberica NIK (BellSoft fork) and Project Leyden (the in-JDK successor).

Why differential testing as the master gate

For consistency with MEP-45 and MEP-46: every fixture's expected output is recorded by vm3 (the reference Mochi interpreter); the JVM build must produce identical stdout. This makes the transpiler's correctness empirically verifiable.

Three secondary gates: javac-clean (catches transpiler-emitted Java warnings), native-image-build (catches reflection metadata gaps), reproducible-build (catches non-determinism in the build pipeline). See [[11-testing-gates]] §3-§8.

Backwards Compatibility

MEP-47 is additive. The vm3 default (mochi run) is unchanged. The MEP-45 C target is unchanged. The MEP-46 BEAM target is unchanged. A user not invoking --target=jvm-* sees zero behaviour change.

The aotir IR is shared with MEP-45 and MEP-46. Changes to aotir are out-of-scope for MEP-47 except as required by JVM-specific lowering needs; any such change is vetted against MEP-45 and MEP-46 in the same PR.

The Mochi language surface is unchanged. No new syntax is introduced by MEP-47.

Reference Implementation

The reference implementation lives in transpiler3/jvm/. The directory structure:

transpiler3/jvm/
README.md
lower/ # aotir -> JavaPoet structural nodes
lower.go
types.go
expr.go
stmt.go
decl.go
closure.go
match.go
query.go
agent.go
stream.go
emit/ # JavaPoet -> Java source text + javac invocation
emit.go
javac.go
format.go
classfile/ # optional ClassFile API direct emission for hot paths
hot.go
bytecode.go
build/ # build driver: uberjar / jlink / native / jpackage / aar
build.go
uberjar.go
jlink.go
native.go
jpackage.go
aar.go
runtime/ # source for dev.mochi.runtime jar (Java)
src/main/java/dev/mochi/runtime/...
pom.xml # for vendoring + Central publishing
testdata/
phase01-hello/
phase02-scalars/
...

Plus shared with other backends:

  • transpiler3/aotir/ (IR, unchanged from MEP-45)
  • transpiler3/passes/ (monomorphisation etc., unchanged)

Phases

The 18 phases mirror the MEP-46 phase structure with JVM-specific additions. Each phase's gate is a Go test (TestPhase{N}{Name} in tests/transpiler3/jvm/). Phases land only when the gate is green across the full JDK matrix.

Phase 0. Spec freeze and skeleton trees

Deliverable: transpiler3/jvm/{lower,emit,classfile,build,runtime,testdata} directories created, README.md written, skeleton Go files compile, javac toolchain detected at build time. Runtime jar source compiles to an empty dev.mochi.runtime Maven module.

Gate: TestPhase0Skeleton: directory layout exists; build produces empty jar; runtime jar publishes locally.

Phase 1. Hello world

Deliverable: lowering for print("hello, world") to a Java class with main(String[] args). uberjar packaging; java -jar hello.jar prints "hello, world\n".

Gate: TestPhase1Hello: 5 fixtures green on JDK 21 and JDK 25, all four tier-1 OS cells.

Phase 2. Primitives and control flow

Deliverable: lowering for int, float, bool, string literals; arithmetic; comparisons; boolean ops; if/else; for/while; let/var. javac compiles clean with -Xlint:all -Werror.

Gate: TestPhase2Scalars: 20 fixtures green, javac-clean.

Phase 3. Collections

Phase 3.1 (lists), 3.2 (maps), 3.3 (sets), 3.4 (nested) mirror MEP-46's phase 3. Each is one gate.

Gate: TestPhase3Lists, TestPhase3Maps, TestPhase3Sets, TestPhase3ListOfRecord. 90 fixtures total.

Phase 4. Records

Deliverable: lowering for type T { ... } to Java record; methods on records; equality / hashCode via record auto-derived; record patterns in match (JEP 440 preview).

Gate: TestPhase4Records: 25 fixtures green.

Phase 5. Sum types and pattern matching

Deliverable: lowering for type T = A | B | C to sealed interface + record variants; match to switch expression with patterns and when guards (JEP 441); exhaustiveness double-checked via javac.

Gate: TestPhase5Sums: 25 fixtures green.

Phase 6. Closures and higher-order functions

Deliverable: lowering for fun(...) => to Java lambda; capture-by-value; mutable captures lifted to Cell<T>; tail-call trampoline for self-recursive funs over threshold.

Gate: TestPhase6Funs: 25 fixtures green.

Phase 7. Query DSL

Deliverable: lowering for from ... select ... to Stream + Collectors; group_by to Collectors.groupingBy; join via runtime HashJoin; order_by, limit, offset direct.

Gate: TestPhase7Query: 30 fixtures green.

Phase 8. Datalog

Deliverable: lowering for fact / rule / query to runtime Engine.register calls; semi-naive evaluator in dev.mochi.runtime.datalog.

Gate: TestPhase8Datalog: 20 fixtures green.

Phase 9. Agents and gen_server-equivalent

Deliverable: lowering for agent declarations to a Java class with virtual-thread + mailbox; spawn, send, call, cast. Supervision via dev.mochi.runtime.agent.Supervisor.

Gate: TestPhase9Agents: 25 fixtures green; TestPhase9NoSyncPinning no-pin gate green; TestPhase9AgentsJFR event-emission gate green.

Phase 10. Streams and pubsub

Deliverable: lowering for stream<T> declarations to Flow.Publisher<T> (concrete SubmissionPublisher wrapper); subscribe, publish. Replay streams.

Gate: TestPhase10Streams: 20 fixtures green.

Phase 11. async/await and structured concurrency

Deliverable: lowering for spawn, await, scope. MochiScope wraps StructuredTaskScope (preview, JEP 505); falls back to manual implementation when preview not available.

Gate: TestPhase11Async: 15 fixtures green; deterministic-mode gate green.

Phase 12. JDK FFI and Maven Central deps

Deliverable: lowering for import "java/..."; method dispatch via Mochi-typed signature derived from class bytecode; @maven(...) pragma for declaring coordinates; lockfile pinning.

Gate: TestPhase12FFI: 25 fixtures green; TestPhase12JdkFFI curated-JDK-API gate green; TestPhase12MavenRoundtrip nightly gate green.

Phase 13. LLM (generate)

Deliverable: lowering for ai(...) to dev.mochi.runtime.ai.AI.call; provider abstractions for OpenAI, Anthropic, local (Ollama).

Gate: TestPhase13LLM: 10 fixtures green (mocked providers).

Phase 14. fetch (HTTP)

Deliverable: lowering for fetch(...) to java.net.http.HttpClient via dev.mochi.runtime.io.Fetch; TLS 1.3 default; HTTP/2.

Gate: TestPhase14Fetch: 10 fixtures green (local test server).

Deliverable: build driver implements --target=jvm-uberjar, --target=jvm-jlink, --target=jvm-jpackage. uberjar uses Maven Shade-equivalent assembly; jlink derives custom JDK; jpackage produces .deb / .dmg / .msi.

Gate: TestPhase15Packaging: each packaging produces a runnable artefact; cold start under target thresholds.

Phase 16. GraalVM native-image AOT

Deliverable: --target=jvm-native builds via Liberica NIK 25; reachability metadata auto-generated; binary cold start <50ms target.

Gate: TestPhase16NativeImage: 30 fixtures green as native images.

Phase 17. Multi-JDK matrix and reproducibility

Deliverable: full CI matrix on JDK 21 + JDK 25 across all tier-1 OS cells; reproducible-build gate via SOURCE_DATE_EPOCH + maven-jar-plugin >= 3.4.0; diffoscope differential gate.

Gate: TestPhase17Matrix: full matrix green; TestPhase17Reproducible: bit-identical builds.

Phase 18. Maven Central publication and perf

Deliverable: dev.mochi:mochi-runtime:0.10.x published to Maven Central via Central Publisher Portal. Performance baselines vs vm3 published.

Gate: TestPhase18Publish nightly verifies the published artefact is consumable; perf dashboard updated.

Future sub-MEPs:

  • MEP-47.1: Android target (--target=jvm-android and --target=jvm-apk). Platform threads only (ART has no Loom).
  • MEP-47.2: iOS via GraalVM native-image (alpha as of GraalVM 25).
  • MEP-47.3: Project Valhalla value classes (when Valhalla GA's).

Dependencies

  • MEP-4 (Type System): types must be fully resolved before lowering.
  • MEP-5 (Type Inference): inferred types fill in unannotated declarations.
  • MEP-13 (ADTs and Match): the surface syntax this MEP lowers.
  • MEP-45 (C transpiler): provides aotir and shared passes.
  • MEP-46 (BEAM transpiler): shares aotir and the closure-conversion / monomorphisation passes.

External:

  • JDK 21 LTS (Temurin) or JDK 25 LTS (Temurin) on the developer machine.
  • GraalVM 25 (Liberica NIK 25 recommended) for native-image builds (optional, only if --target=jvm-native used).
  • Maven resolver (vendored as a build-time tool) for @maven(...) coordinate resolution.

Open questions

  • Should --target=jvm-native be promoted to the default for mochi build once GraalVM AOT cold start beats javac+JIT for the typical fixture? (Currently the default is uberjar.)
  • How to handle MEP-47.1 (Android) virtual-thread fallback: emit a build-time warning, or refuse to compile agent-heavy programs for Android?
  • Project Leyden (JEPs 483/514/515) vs GraalVM native-image: when Leyden subsumes native-image, do we add a third packaging target or migrate?

Security considerations

Supply chain: every Maven dep pinned by SHA-256 in mochi.lock.json. mochi audit checks against OSV. Three vendored deps (Jackson, snakeyaml-engine, bouncycastle); other Maven Central deps imported per @maven(...) pragma with lockfile pin.

Reflection / dynamic class loading: GraalVM native-image closed-world model. Reachability metadata vetted; explicit allow-list of LambdaMetafactory bootstraps. See [[05-codegen-design]] §9.

Sanitisers: standard JVM safety (bounds checks, null checks, type checks) by construction. No Unsafe use in generated code or the runtime.

Cryptography: bouncycastle is the only crypto provider; pinned at 1.78.1. TLS via JDK default; we do not bundle our own.

JFR can leak sensitive data via JFR recordings (event field values). The runtime documents this in [[04-runtime]] §14 and provides a mochi.jfr.redact mode that masks string fields above a configurable length.

References

Twelve informative research notes accompany this MEP at ~/notes/Spec/0047/01..12, mirroring the structure of MEP-45 and MEP-46:

  • [[01-language-surface]]: Mochi surface and per-form lowering obligations.
  • [[02-design-philosophy]]: design rationale for each load-bearing choice.
  • [[03-prior-art-transpilers]]: Kotlin, Scala, Clojure, Groovy, JRuby, Eta, Ceylon, Manifold, GraalVM polyglot, ASM, ByteBuddy, JavaPoet, ClassFile API, jlink, jpackage, GraalVM native-image, Project Leyden, CRaC, D8/R8/ART.
  • [[04-runtime]]: the dev.mochi.runtime jar, JDK 21 baselines, vendored deps.
  • [[05-codegen-design]]: IR layer choice (Java source via JavaPoet + ClassFile API hybrid).
  • [[06-type-lowering]]: per-type marshalling, boxing, immutability.
  • [[07-jvm-target-portability]]: JDK matrix, OS matrix, Android.
  • [[08-dataset-pipeline]]: query DSL lowering via Stream + collectors, hash-join.
  • [[09-agent-streams]]: agents on Loom, streams on Flow.Publisher, supervision.
  • [[10-build-system]]: build driver, Gradle/Maven plugins, Maven Central, GraalVM.
  • [[11-testing-gates]]: per-phase gate spec, JDK matrix, native-image gate.
  • [[12-risks-and-alternatives]]: risk register, alternatives considered and rejected.