Skip to main content

May 2026 (v0.14.0)

v0.14.0 completes the Mochi-to-JVM transpiler. Every phase of MEP-47 is now landed and the MEP is marked Final. You can compile a Mochi source file to a runnable fat jar, a custom JRE image, a GraalVM native binary, or an OS-native installer, without writing a single line of Java or Maven configuration.

mochi build --target=jvm-uberjar hello.mochi -o hello.jar
java -jar hello.jar

The pipeline is entirely in-process: parser.Parse -> types.Check -> aotir.Lower -> jvmlower.Lower -> javasrc.CompilationUnit -> javax.tools.JavaCompiler -> .class files -> uberjar. No mvn, no gradle, no javac on $PATH required for the default uberjar target (the in-process javax.tools instance ships with the JDK you point at via JAVA_HOME).

1. JVM transpiler: MEP-47 Final

MEP-47 covers 19 phases from hello-world through scalars, collections, records, sum types, closures, queries, Datalog, Loom agents, streams, async/await, JDK FFI, LLM generation, HTTP fetch, release packaging, GraalVM native-image, CI matrix with reproducibility, and Maven Central publication. All 19 phases are now LANDED.

1.1 Pipeline and packaging (Phases 0-1)

The transpiler pipeline lives under transpiler3/jvm/. The entry point is transpiler3/jvm/build/Driver.Build, which accepts a DriverConfig struct specifying the target (uberjar, jlink, jpackage, native-image, source), output paths, and JDK root.

The pipeline stages are:

  1. parser.Parse -- identical to every other Mochi backend.
  2. types.Check -- shared type-checker; JVM backend adds no new types here.
  3. aotir.Lower -- the shared MEP-45/MEP-46 IR lowering, which runs monomorphisation, closure-conversion, and match-to-decision-tree.
  4. jvmlower.Lower -- JVM-specific lowering: converts aotir nodes to javasrc.CompilationUnit trees (a structural Java-source AST).
  5. javasrc.Emit -- renders CompilationUnit to Java source text.
  6. javax.tools.JavaCompiler -- in-process javac compiles the source to .class files.
  7. build.PackUberJar -- collects all .class files plus the mochi-runtime.jar into a zip archive with a valid MANIFEST.MF (Main-Class: dev.mochi.user.Main).

The emitted Java package for user code is dev.mochi.user. The runtime library is dev.mochi.runtime.*. A Main.java wrapper is emitted automatically; it calls the Mochi main function entry point.

BLAKE3 content-addressed cache. Unchanged source files are served from .mochi/cache/jvm/ with a copy-only no-op. The cache key is BLAKE3 over file contents concatenated with a compilerVersion sentinel. The JVM cache is a parallel slot to the BEAM and C caches; all three coexist under .mochi/cache/.

1.2 Scalars and control flow (Phases 2-3)

Integers lower to Java long. Overflow wraps at 64-bit boundaries (same as vm3). Divide-by-zero raises MochiPanic(MOCHI_ERR_DIVZERO).

Floats lower to Java double. IEEE 754 NaN/Inf propagation is preserved: 0.0 / 0.0 produces NaN, 1.0 / 0.0 produces Infinity. The Double.NaN and Double.POSITIVE_INFINITY sentinels are used directly in emitted Java.

Booleans lower to Java boolean. && and || lower to Java short-circuit &&/|| rather than &/|, preserving Mochi's short-circuit semantics.

Strings lower to Java String (UTF-16 internally, UTF-8 I/O). String concatenation uses +; the javac backend inlines to StringBuilder.append calls in the generated bytecode.

Control flow. if/else lowers to Java if/else. while lowers to Java while. for x in range(lo, hi) lowers to Java for (long x = lo; x < hi; x++). for x in collection lowers to Java enhanced for-each. break and continue lower directly. return lowers to Java return.

Print. print(x) lowers to System.out.println(str(x)) where str is the Mochi stringification builtin, itself lowered to MochiRuntime.str(x).

1.3 Collections (Phase 4)

list<T> lowers to java.util.ArrayList<T>. List literals [1, 2, 3] lower to new ArrayList<>(Arrays.asList(1L, 2L, 3L)). len(xs) lowers to xs.size(). append(xs, x) lowers to a copy- append that returns a new ArrayList (value semantics). xs[i] lowers to xs.get((int) i) with a checked cast for bounds. for x in xs lowers to enhanced for-each.

map<K,V> lowers to java.util.LinkedHashMap<K,V> (insertion- order preserved, matching vm3 semantics). Map literals {"a": 1} lower to a new LinkedHashMap<>() followed by put calls. m[k] lowers to MochiRuntime.mapGet(m, k), which throws MochiPanic(MOCHI_ERR_KEY_NOT_FOUND) on missing key. m[k] = v lowers to MochiRuntime.mapSet(m, k, v) returning a new copy. has(m, k) lowers to m.containsKey(k).

set<T> lowers to java.util.LinkedHashSet<T>. Set literals {1, 2, 3} lower to new LinkedHashSet<>(Arrays.asList(...)). add(s, v) returns a new LinkedHashSet with v appended. has(s, v) lowers to s.contains(v). len(s) lowers to s.size().

1.4 Records (Phase 5)

Records lower to Java record classes (JEP 395, GA in JDK 16+). A Mochi record type Point { x: int, y: int } emits:

record MochiPoint(long x, long y) {}

Field access p.x lowers to p.x() (the canonical accessor). Record update {...p, x: 10} lowers to a new MochiPoint(10, p.y()) constructor call, which javac compiles to a simple stack-push sequence.

Records are immutable by construction (Java record fields are final). The Mochi type checker guarantees structural equality semantics; Java record provides equals/hashCode/toString via RecordType.RecordComponents automatically.

1.5 Sum types and pattern matching (Phase 6)

Sum types lower to Java sealed interfaces (JEP 409, GA in JDK 17+) with record implementations for each variant.

type Shape = Circle(r: float) | Rect(w: float, h: float)

emits:

sealed interface MochiShape permits MochiShape.Circle, MochiShape.Rect {
record Circle(double r) implements MochiShape {}
record Rect(double w, double h) implements MochiShape {}
}

Pattern matching. match x { arm => expr, ... } lowers to Java switch pattern expressions (JEP 441, GA in JDK 21):

switch (x) {
case MochiShape.Circle c -> ...;
case MochiShape.Rect r -> ...;
}

The sealed interface + switch combination is exhaustiveness-checked by javac, giving a second correctness gate on top of the Mochi type checker.

option[T] lowers to MochiOption<T> (sealed interface with Some<T> and None records). option.get(x) is MochiRuntime.optGet(x).

Result[T,E] lowers to MochiResult<T,E> (sealed interface with Ok<T> and Err<E> records).

1.6 Closures and higher-order functions (Phase 7)

Closures. Anonymous functions lower to Java lambdas. Mochi's function type fun(int, int) -> int lowers to MochiFn2<Long, Long, Long> (from the runtime's typed functional-interface zoo). The lambda body captures free variables via Java's standard closure mechanism.

Named function references. &f (a first-class reference to named function f) lowers to a method reference ClassName::f.

Partial application. add(5, _) lowers to a lambda that captures 5L in the first argument position and passes its own argument in the second: (b) -> add(5L, b). The _ placeholder is detected during jvmlower.Lower; lowerPartialApply synthesises the capturing lambda.

Higher-order builtins. map(xs, f) lowers to a Java stream: xs.stream().map(f::apply).collect(Collectors.toCollection(ArrayList::new)). filter(xs, pred) uses stream().filter. reduce(xs, init, f) uses stream().reduce. The stream path is chosen over a manual loop because javac + C2 JIT can vectorise and parallelise the stream operations on warm code paths.

1.7 Query DSL (Phase 8)

from x in xs where pred select proj lowers to a Java stream pipeline:

xs.stream()
.filter(x -> pred)
.map(x -> proj)
.collect(Collectors.toCollection(ArrayList::new))

Aggregations: count(q) appends .count(), sum(q, f) appends mapToLong(f).sum(), avg(q, f) appends mapToDouble(f).average().

Group-by: group x by key select agg lowers to Collectors.groupingBy(x -> key, downstream).

Hash join: from x in xs join y in ys on x.id == y.id select ... lowers to a two-pass approach: first builds a HashMap keyed by the join column from the left side, then streams the right side doing lookups in O(1) per probe.

Sort, take, skip: .sorted(Comparator.comparingLong(x -> key)), .limit(n), .skip(n).

1.8 Datalog (Phase 9)

Facts, rules, and recursive queries compile to MochiDatalog, a semi-naive fixpoint evaluator in the runtime library. Each relation is backed by a LinkedHashSet for membership and a delta ArrayList for incremental updates per round.

Recursive rules run until the delta is empty (fixpoint). Negation-as- failure is supported when the negation is stratifiable: the type checker computes the dependency graph and rejects cycles through negation.

Multi-predicate rules and multi-free-variable queries are supported. Datalog literals in Mochi source lower to MochiDatalog.Relation instances; rule bodies lower to MochiDatalog.Rule objects with a Java lambda head and a list of body predicates.

1.9 Agents (Loom virtual threads) (Phase 10)

Agent declarations lower to a Java class implementing the agent loop. Each agent type emits a class with a LinkedTransferQueue<Object> mailbox, a run() method containing the agent's message-dispatch loop, and a static start(args...) factory method that calls Thread.ofVirtual().start(loop).

public class MochiCounter {
private final LinkedTransferQueue<Object> mailbox = new LinkedTransferQueue<>();

public static MochiCounter start(long initial) {
var agent = new MochiCounter(initial);
Thread.ofVirtual().start(agent::run);
return agent;
}

private void run() {
long count = initial;
while (true) {
Object msg = mailbox.take(); // parks virtual thread, yields carrier
// dispatch on msg type ...
}
}
}

spawn AgentType(args...) lowers to MochiCounter.start(args...). The returned reference is a plain Java object reference; message sends are direct method calls via a send(msg) method that delegates to mailbox.put(msg).

on close { ... } blocks lower to a finally block in run(), so cleanup code always runs whether the agent exits normally or via an exception.

Virtual thread overhead. Each virtual thread starts with ~200 bytes of heap and a minimal initial stack. The JDK 24 fix for synchronized pinning (JEP 491) means agents can safely use synchronized methods in dependencies without pinning carrier threads. At 100K concurrent agents the RSS footprint is under 100 MB.

1.10 Streams (Phase 11)

stream<T> lowers to MochiStream<T>, which wraps a java.util.concurrent.Flow.Publisher<T> (JDK 9 Reactive Streams API) with a SubmissionPublisher<T> implementation for backpressure.

publish(stream, value) calls SubmissionPublisher.submit(value). The submit method blocks when all subscriber buffers are full, providing automatic backpressure.

subscribe(stream, callback) calls publisher.subscribe(subscriber) with a Flow.Subscriber<T> whose onNext(item) calls callback.

subscribe_limit(stream, N) uses a bounded subscriber that drops messages when its buffer exceeds N items. MochiStream.subscribeLimit implements the bounded subscriber using Flow.Subscription.request(1) to control demand one item at a time.

Channels. chan<T> lowers to MochiChan<T>, backed by an ArrayBlockingQueue<T> of user-specified capacity.

1.11 async/await (Phase 12)

async { ... } lowers to CompletableFuture.supplyAsync(() -> body, vtExecutor) where vtExecutor is Executors.newVirtualThreadPerTaskExecutor(). Each async task runs on a fresh virtual thread from the JDK 21 virtual-thread pool.

await expr lowers to MochiRuntime.await(future), which calls CompletableFuture.get() on the carrier virtual thread. Because virtual threads park instead of blocking a carrier, thousands of concurrent await calls do not starve the carrier pool.

await_all(futures) lowers to CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) followed by a stream().map(CompletableFuture::join) to collect results in order.

let f1 = async { fetch "https://example.com/a" }
let f2 = async { fetch "https://example.com/b" }
let results = await_all([f1, f2])

1.12 JDK FFI (Phase 13)

extern java fun declarations import any method from the JDK or any Maven Central library on the classpath:

extern java fun java.time.Instant.now(): string
extern java fun java.util.UUID.randomUUID(): string

The import splits on the last dot: java.time.Instant is the class, now is the method name. Static methods lower to ClassName.method(args). Instance methods lower to args[0].method(args[1:]).

Return type mapping. The Mochi return type annotation drives wrapping: string appends .toString(); int emits (long) result; bool emits (boolean) result. Complex types use MochiRuntime.coerce.

import "dev.mochi:some-library:1.2.3" adds a Maven Central dependency to the build. The runtime resolver downloads the jar to .mochi/cache/jvm/deps/ via java.net.http.HttpClient and adds it to the javax.tools compilation classpath.

1.13 LLM generation (Phase 14)

Cassette playback. generate provider { prompt: "..." } lowers to MochiLLM.generate(provider, model, prompt). In cassette mode (MOCHI_LLM_CASSETTE_DIR set), responses are looked up from pre-recorded files keyed by a DJB2 hash of "provider\0model\0prompt". This makes LLM-using programs deterministic and credential-free in CI.

Live providers. When MOCHI_LLM_CASSETTE_DIR is not set:

  • OPENAI_API_KEY set: POST to api.openai.com/v1/chat/completions via java.net.http.HttpClient with HTTP/2. Default model gpt-4o-mini. JSON parsed via Jackson ObjectMapper.
  • ANTHROPIC_API_KEY set: POST to api.anthropic.com/v1/messages with anthropic-version: 2023-06-01. Default model claude-haiku-4-5-20251001.
  • Neither set: warning to stderr, returns empty string.

Structured output. generate provider { prompt: "...", schema: s } appends "\nRespond with JSON matching this schema: <schema>" to the prompt before cassette lookup. The schema expression evaluates to a Mochi record type at compile time; MochiRuntime.schemaOf(T.class) generates the JSON Schema descriptor.

1.14 HTTP fetch (Phase 15)

fetch URL into var lowers to MochiFetch.get(url). The runtime method uses java.net.http.HttpClient (JDK 11+) with:

  • HTTP/2 preferred, HTTP/1.1 fallback.
  • TLS: system default SSLContext (JDK trust store; no custom CA bundle needed on modern JDKs with updated roots).
  • Timeout: 30 seconds connect + read (configurable via MOCHI_HTTP_TIMEOUT_SECONDS).
  • Response body returned as String decoded with UTF-8.
  • Non-200 responses throw MochiPanic(MOCHI_ERR_HTTP, status).

json_decode(s) lowers to MochiJson.decode(s). The runtime uses Jackson ObjectMapper to parse the JSON string and returns a LinkedHashMap<String, Object> coerced to Mochi's map<string,string> (leaf values stringified).

1.15 Release packaging (Phase 16)

Five packaging targets are available:

Target flagOutputCold-start targetSelf-contained?
--target=jvm-uberjarSingle fat .jar (default)<= 500 msNo (needs JDK)
--target=jlinkCustom JRE image directory<= 200 msYes (bundles JDK modules)
--target=jpackageOS installer (.dmg/.deb/.msi)<= 200 msYes (bundles custom JRE)
--target=jvm-nativeGraalVM native binary<= 50 msYes (no JDK needed)
--target=jvm-sourceJava source files for inspection----

Uberjar (PackUberJar): Extracts all entries from the runtime jar, collects all .class files from user compilation, sorts entries lexicographically, writes with a fixed timestamp from SOURCE_DATE_EPOCH, and prepends the META-INF/MANIFEST.MF with Main-Class: dev.mochi.user.Main. The resulting .jar is runnable with java -jar.

jlink: Calls jlink --add-modules <detected-modules> --output outDir. Module detection runs jdeps --list-deps on the uberjar to find the minimal module set. The result is a self-contained directory (~50 MB) with a bundled JRE and a bin/java launcher.

jpackage: Calls jpackage --type <platform-type> --app-image <jlink-output> --input . --main-jar hello.jar --dest outDir. Platform types: dmg on macOS, deb on Linux, msi on Windows.

Java source: Writes all emitted .java files to an output directory for inspection, debugging, or manual compilation.

1.16 GraalVM native-image (Phase 17)

BuildNativeImage(outJar, outExe) calls the native-image tool (from GraalVM or Liberica NIK) with:

native-image -jar outJar -H:Name=<base> -H:Path=<dir>
--no-fallback --initialize-at-build-time
-H:+ReportExceptionStackTraces

The -jar flag reads the main class from MANIFEST.MF. --main-class is not passed (it is invalid with -jar and causes a build error). --no-fallback prevents native-image from producing a fallback image that silently requires a JVM. --initialize-at-build-time initialises all classes at build time for maximum startup performance.

Reachability metadata. The mochi-runtime.jar ships a META-INF/native-image/ directory with pre-generated reachability metadata covering reflection, proxy, and resource configs. This covers all runtime uses of reflection for JSON serialisation, virtual-thread scheduling, and Flow publisher internals.

MeasureStartup(exe) runs the executable and measures wall-clock time until exit. The CI gate asserts cold start <= 50 ms on the hello-world fixture.

Auto-skip. Tests requiring native-image call build.FindNativeImage() and skip with t.Skip("native-image not found") when GraalVM is absent. The blocking CI job installs graalvm/setup-graalvm@v1 with Liberica NIK 25.

1.17 CI matrix and reproducibility (Phase 18)

CI matrix (jvm.yml): JDK 21 and JDK 25 (Temurin) on:

  • ubuntu-24.04 (x86-64 Linux)
  • ubuntu-24.04-arm (arm64 Linux)
  • macos-14 (Apple Silicon macOS)
  • windows-2022 (x86-64 Windows)

Windows matrix cells use sparse checkout to avoid tests/rosetta/ (which contains filenames with trailing ... that are invalid on Windows NTFS). The native-image job runs on ubuntu-24.04 with Liberica NIK 25. JDK 26 EA runs nightly as a non-blocking smoke test.

Reproducible uberjars. Uberjar builds are bit-identical when SOURCE_DATE_EPOCH is fixed. The CI sets SOURCE_DATE_EPOCH=1700000000. The implementation:

  1. Collects all jar entries into a []JarEntry slice.
  2. Sorts lexicographically by path (sort.Slice).
  3. Writes each entry with w.CreateHeader(&zip.FileHeader{Modified: ts}) using the fixed ts from SOURCE_DATE_EPOCH.

TestPhase17Reproducible builds the same fixture twice in separate temp directories with separate Driver instances, then opens both jars and compares SHA-256 of every entry byte-for-byte. Both builds must be identical.

Implementation-Version in the jar manifest is set to 0.14.0 matching the pom.xml artifact version.

1.18 Maven Central (Phase 19)

dev.mochi:mochi-runtime:0.14.0 is published to Maven Central via the Central Publisher Portal (central.sonatype.com). Users can add a standard Maven dependency:

<dependency>
<groupId>dev.mochi</groupId>
<artifactId>mochi-runtime</artifactId>
<version>0.14.0</version>
</dependency>

or Gradle:

implementation("dev.mochi:mochi-runtime:0.14.0")

Artifact bundle includes:

  • mochi-runtime-0.14.0.jar -- runtime classes
  • mochi-runtime-0.14.0-sources.jar -- all .java source files
  • mochi-runtime-0.14.0-javadoc.jar -- Javadoc for all public APIs
  • mochi-runtime-0.14.0.pom -- POM with required Central metadata
  • .asc signatures for each artifact (via maven-gpg-plugin 3.2.4)

Publish workflow (.github/workflows/jvm-publish.yml): triggers on v0.* tags and nightly. Uses central-publishing-maven-plugin 0.5.0 with mvn deploy. The MAVEN_CENTRAL_TOKEN, GPG_PRIVATE_KEY, and GPG_PASSPHRASE CI secrets must be set in the repository settings.

TestPhase18Publish: resolves mochi-runtime-0.14.0.jar from the local build target directory, compiles a minimal Java class against it, runs it, and verifies stdout. Skipped in -short mode.

2. Test corpus

The JVM transpiler test suite lives in transpiler3/jvm/build/ and covers all 19 phases:

Test functionPhaseCoverage
TestPhase0Skeleton0Driver.Build round-trip; toolchain detection
TestPhase1Hello1Hello-world fixture; stdout byte-equal to vm3
TestPhase2Scalars2int/float/bool/string; NaN/Inf; divzero
TestPhase2ControlFlow2if/while/for; break/continue; nested loops
TestPhase3Lists3list literals; append/len/index; for-in
TestPhase3Maps3map literals; get/set/has; for-in
TestPhase3Sets3set literals; add/has/len
TestPhase4Records4record types; field access; record update
TestPhase5Sums5sealed variants; match patterns; option/Result
TestPhase6Closures6lambdas; partial apply; higher-order builtins
TestPhase7Query7from/where/select; group-by; hash join; sort/take/skip
TestPhase8Datalog8facts; recursive rules; negation-as-failure
TestPhase9Agents9spawn; mailbox; on close; 100K agent RSS
TestPhase10Streams10publish/subscribe; backpressure; channels
TestPhase11Async11async/await; await_all; error propagation
TestPhase12FFI12extern java fun; static + instance methods
TestPhase13LLM13cassette playback; structured output
TestPhase14Fetch14HTTP GET; json_decode
TestPhase15Packaging15uberjar; jlink; jpackage; jvm-source
TestPhase16NativeImage16native-image build; cold-start <= 50 ms
TestPhase17Reproducible17two builds; SHA-256 byte-equal per entry
TestPhase17Matrix17hello.mochi on current JDK; stdout check
TestPhase18Publish18local jar compile; run; stdout check
BenchmarkIntLoop17warm throughput vs vm3 baseline
BenchmarkAgentMemory10RSS at 100K agents <= 100 MB
BenchmarkColdStart16cold start: uberjar <= 500 ms; native <= 50 ms

All tests are green on JDK 21 and JDK 25 on Linux x86-64, Linux arm64, macOS arm64, and Windows x86-64. Native-image tests auto-skip on standard JDK installs and run under Liberica NIK 25 in the native-image CI job.

3. New compiler pipeline files

transpiler3/jvm/ directory tree:

transpiler3/jvm/
build/
driver.go -- Driver.Build; target dispatch
uberjar.go -- PackUberJar; reproducible zip packing
native.go -- BuildNativeImage; MeasureStartup
toolchain.go -- FindJavac, FindJava, FindNativeImage, FindJlink
cache.go -- BLAKE3 content-addressed cache
phase{00-18}_test.go
bench_test.go
lower/
lower.go -- aotir -> javasrc CompilationUnit
lower_expr.go -- expression lowering
lower_stmt.go -- statement lowering
lower_type.go -- type-to-Java-type mapping
javasrc/
ast.go -- CompilationUnit, ClassDecl, MethodDecl, ...
emit.go -- renders AST to Java source string
runtime/
pom.xml -- dev.mochi:mochi-runtime:0.14.0 (Maven Central)
src/main/java/dev/mochi/runtime/
MochiRuntime.java -- core helpers (str, coerce, await, mapGet, ...)
MochiFetch.java -- HTTP GET via java.net.http
MochiLLM.java -- generate; cassette; OpenAI; Anthropic
MochiJson.java -- json_decode via Jackson
MochiDatalog.java -- semi-naive fixpoint evaluator
MochiStream.java -- Flow.Publisher wrapper + backpressure
MochiChan.java -- ArrayBlockingQueue channel
MochiFn{1-8}.java -- typed functional interfaces
MochiOption.java -- sealed interface Some<T> | None
MochiResult.java -- sealed interface Ok<T> | Err<E>
MochiPanic.java -- runtime exception

4. aotir IR lowering table

MEP-47 added no new IR nodes to the shared aotir package. All JVM- specific patterns are handled in jvmlower.Lower by mapping existing nodes to the appropriate Java construct. The IR remains shared across the C, BEAM, and JVM backends.

aotir nodeJava lowering
AgentDeclclass with LinkedTransferQueue mailbox + Thread.ofVirtual() start
SpawnExprAgentClass.start(args)
AsyncExprCompletableFuture.supplyAsync(() -> body, vtExecutor)
AwaitExprMochiRuntime.await(future)
StreamDeclMochiStream<T> wrapping SubmissionPublisher<T>
SumTypeDeclsealed interface + record variants
MatchExprswitch pattern expression (JEP 441)
RecordDeclJava record class
ExternFuncDeclstatic or instance method call on the named class
GenerateExprMochiLLM.generate(provider, model, prompt)
FetchExprMochiFetch.get(url)

5. Compatibility

v0.14.0 is additive. All existing Mochi programs continue to run under vm3 with mochi run. mochi build --target=c-aot from MEP-45 is unchanged. mochi build --target=beam-escript from MEP-46 is unchanged. mochi build --target=jvm-uberjar is the new entry point for JVM output.

JDK 21 is the minimum supported JDK version. JDK 25 (LTS) is the recommended production JDK. JDK 17 and older are not supported.

dev.mochi:mochi-runtime:0.14.0 is Apache-2.0 licensed and compatible with GPL/MIT/BSD downstream uses.

6. Upgrade

curl -fsSL https://get.mochi-lang.dev | sh
mochi --version # 0.14.0

Or, with Docker:

docker pull ghcr.io/mochilang/mochi:0.14.0

Or, from source:

git pull && make build

Pre-built binaries for all five tier-1 triples (linux/amd64, linux/arm64, darwin/arm64, windows/amd64, darwin/amd64) are available on the GitHub release page.