May 2026 (v0.15.0)
v0.15.0 completes the Mochi-to-.NET transpiler. Every phase of MEP-48
is now landed and the MEP is marked Final. You can compile a Mochi
source file to a runnable framework-dependent .dll, a self-contained
publish directory per RID, or a NativeAOT single-file binary, without
writing a single line of C# or MSBuild configuration.
mochi build --target=dotnet-fx-dependent hello.mochi -o out/
dotnet out/hello.dll
The pipeline goes: parser.Parse -> types.Check ->
aotir.Lower -> colour.Analyse (async colouring pass) ->
lower.Lower (dotnet) -> csharpsrc.CompilationUnit ->
emit.Emit (writes .cs files) -> dotnet publish subprocess ->
packFxDependent / packSelfContained / NativeAOT ILC. No Roslyn
NuGet dependency or in-process compilation is required; the .NET SDK
on $PATH handles compilation.
1. .NET transpiler: MEP-48 Final
MEP-48 covers 19 phases from hello-world through scalars, collections, records, sum types, closures, queries, Datalog, Channel-backed agents, IAsyncEnumerable streams, async/await colouring, .NET FFI, LLM generation, HTTP fetch, NativeAOT, reproducibility, self-contained per RID, and trim-clean NuGet publication. All 19 phases are now LANDED.
1.1 Pipeline and packaging (Phases 0-1)
The transpiler pipeline lives under transpiler3/dotnet/. The entry
point is transpiler3/dotnet/build/Driver.Build, which accepts a
DriverConfig struct specifying the target
(dotnet-fx-dependent, dotnet-self-contained, dotnet-aot),
output path, and target framework moniker (TFM).
The pipeline stages are:
parser.Parse-- identical to every other Mochi backend.types.Check-- shared type-checker; .NET backend adds no new types.aotir.Lower-- the shared IR from MEP-45; runs monomorphisation, closure-conversion, and match-to-decision-tree.colour.Analyse-- .NET-specific async colouring pass. Propagatesasync Task<T>(red) vs synchronous (blue) colour through the static call graph. Phase 0-10 code is all-Blue; the first Red functions appear in Phase 11 (async/await).lower.Lower(dotnet) -- convertsaotirnodes tocsharpsrcstructural nodes (a C# AST).emit.Emit-- renderscsharpsrcnodes to.cssource files.dotnet publishsubprocess -- invokes the .NET SDK with a generated.csprojcontaining aProjectReferencetoMochi.Runtime.csproj.packFxDependent/packSelfContained/ NativeAOT ILC -- final packaging per target.
The emitted C# namespace for user code is Mochi.User. The runtime
library is Mochi.Runtime.*. A Main method is emitted automatically
in the module class matching the source filename.
SHA-256 content-addressed cache. Unchanged source files are served
from ~/.cache/mochi/dotnet/ (XDG Base Directory; overridable via
$MOCHI_CACHE_DIR). The cache key is SHA-256 of source bytes +
SDK version string + TFM. Hit path: ~5ms file copy. Miss path: full
pipeline + cache write.
Mochi.Runtime NuGet package. Mochi.Runtime targets
net8.0;net9.0;net10.0 and is published to NuGet as
Mochi.Runtime 0.15.0-alpha (Apache-2.0). The .csproj uses a
ProjectReference to the local runtime in dev builds; a
PackageReference is used when consuming from the published NuGet feed.
1.2 Scalars and control flow (Phases 2-3)
Integers lower to C# long (System.Int64). Every integer literal
carries an L suffix (42L) so Roslyn infers long rather than
int, avoiding implicit narrowing. Divide-by-zero throws
MochiPanic("MOCHI_ERR_DIVZERO").
Floats lower to C# double (System.Double). print(3.14) calls
Print.Line(double v) which uses
v.ToString("G", CultureInfo.InvariantCulture) to match vm3's
strconv.FormatFloat output. Edge cases: NaN -> "NaN",
+Inf -> "Infinity", -Inf -> "-Infinity".
Booleans lower to C# bool. print(true) must print "true"
(lowercase) to match vm3; Print.Line(bool v) calls
Console.WriteLine(v ? "true" : "false") rather than
Console.WriteLine(v) (which would produce "True").
Strings lower to C# string (UTF-16 internally, UTF-8 at I/O
boundaries). String concatenation uses +; Roslyn inlines to
string.Concat in the emitted IL.
Control flow. if/else -> C# if/else. while -> C#
while. for x in range(lo, hi) -> for (long x = lo; x < hi; x++).
for x in collection -> C# foreach. break/continue/return
lower directly.
Mochi.Runtime.IO.Print. print(x) lowers to Print.Line(x).
The Print class provides overloads for string, long, double,
bool, and object. This indirection lets tests redirect
Console.Out to a buffer without changing generated code, and lets
NativeAOT trimming see which overloads are used.
1.3 Collections (Phase 3)
list<T> lowers to System.Collections.Generic.List<T>.
List literals [1, 2, 3] lower to new List<long> { 1L, 2L, 3L }.
len(xs) -> xs.Count. append(xs, x) returns a new List<T> with
x appended (value semantics). xs[i] -> xs[(int)i]. for x in xs
-> foreach (var x in xs).
map<K,V> lowers to Mochi.Runtime.OrderedMap<K,V> on .NET 8-9
and to System.Collections.Generic.OrderedDictionary<K,V> on .NET 10+.
Insertion order is preserved, matching vm3 semantics. m[k] throws
MochiPanic("MOCHI_ERR_KEY_NOT_FOUND") on missing keys. m[k] = v
returns a new copy (value semantics). has(m, k) -> m.ContainsKey(k).
set<T> lowers to Mochi.Runtime.OrderedSet<T>. add(s, v)
returns a new set. has(s, v) -> s.Contains(v).
1.4 Records (Phase 4)
Records lower to C# sealed record classes (C# 9+). A Mochi record
type Point { x: int, y: int } emits:
public sealed record MochiPoint(long X, long Y);
Field names are PascalCased. Record update {...p, x: 10} lowers to
p with { X = 10L } (C# non-destructive mutation).
1.5 Sum types and pattern matching (Phase 5)
Sum types lower to C# abstract sealed record + sealed record
variants:
type Shape = Circle(r: float) | Rect(w: float, h: float)
emits:
public abstract sealed record MochiShape;
public sealed record MochiShape_Circle(double R) : MochiShape;
public sealed record MochiShape_Rect(double W, double H) : MochiShape;
Pattern matching. match x { arm => expr, ... } lowers to C# 8+
switch expressions with type patterns (C# 11 list patterns for list
destructuring). The sealed modifier lets Roslyn verify exhaustiveness
at compile time.
option[T] lowers to MochiOption<T> (Some<T> / None
records). result[T,E] lowers to MochiResult<T,E> (Ok<T> /
Err<E> records).
1.6 Closures and higher-order functions (Phase 6)
Closures lower to C# lambdas with Func<...> / Action<...>
delegate types (BCL, up to arity 16; the runtime's MochiFn<...> covers
higher arities). Partial application add(5, _) lowers to
(b) => Add(5L, b). Higher-order builtins map, filter,
reduce use LINQ Select, Where, Aggregate.
1.7 Query DSL (Phase 7)
from x in xs where pred select proj lowers to LINQ method chains:
xs.Where(x => pred).Select(x => proj).ToList()
Group-by, aggregations, hash join, sort/take/skip all map to
corresponding LINQ operators. PLINQ parallel execution is opt-in via
@parallel.
1.8 Datalog (Phase 8)
Facts, rules, and recursive queries compile to MochiDatalog, a
semi-naive fixpoint evaluator in Mochi.Runtime. Each relation is
backed by an OrderedSet for membership and a delta List for
incremental updates per round. Negation-as-failure is supported when
the negation is stratifiable.
1.9 Agents (Channel-backed) (Phase 9)
Agent declarations lower to a C# class with a
System.Threading.Channels.Channel<TMessage> mailbox, an
async Task Run() dispatch loop, and a static Start(args...) factory:
public sealed class MochiCounter
{
private readonly Channel<IMsg> _ch = Channel.CreateUnbounded<IMsg>();
public static MochiCounter Start(long initial) {
var a = new MochiCounter(initial);
_ = Task.Run(a.Run);
return a;
}
private async Task Run() {
long count = initial;
await foreach (var msg in _ch.Reader.ReadAllAsync()) {
// dispatch on msg type ...
}
}
}
The TMessage sealed record union is generated automatically from the
agent's on-handlers. spawn AgentType(args...) lowers to
AgentClass.Start(args...). Back-pressure is available via bounded
channels (Channel.CreateBounded<TMessage>(capacity)).
1.10 Streams (IAsyncEnumerable) (Phase 10)
stream<T> lowers to IAsyncEnumerable<T> with
System.Linq.Async operators. emit(stream, value) uses a
Channel<T> as the producer side, wrapped in
ChannelReader<T>.ReadAllAsync(). subscribe(stream, callback)
consumes the stream in an await foreach loop.
1.11 async/await and structured concurrency (Phase 11)
Async colouring pass. colour.Analyse propagates async Task<T>
(Red) colour through the call graph. Any function that calls a Red
function becomes Red. Mochi surface code writes no async or await
keywords; the colour pass inserts them.
async { ... } -> Task.Run(async () => body). await expr -> C#
await expr (in Red context). await_all(futures) ->
await Task.WhenAll(futures).
Structured concurrency. mochi_async supervisor semantics are
implemented via CancellationTokenSource propagation.
1.12 .NET FFI and NuGet deps (Phase 12)
extern dotnet fun declarations import BCL or NuGet methods:
extern dotnet fun System.Math.Abs(n: int): int
extern dotnet fun System.Guid.NewGuid(): string
Static methods lower to ClassName.Method(args). Instance methods
lower to args[0].Method(args[1:]). import "NuGet:PackageId:Version"
adds a PackageReference to the generated .csproj.
1.13 LLM generation (Phase 13)
Cassette playback. generate provider { prompt: "..." } lowers to
MochiLLM.Generate(provider, model, prompt). In cassette mode
(MOCHI_LLM_CASSETTE_DIR set), responses are served from pre-recorded
files keyed by DJB2 hash of "provider\0model\0prompt".
Live providers. OPENAI_API_KEY -> OpenAI gpt-4o-mini via
System.Net.Http.HttpClient. ANTHROPIC_API_KEY -> Anthropic
claude-haiku-4-5-20251001. Neither set -> empty string + stderr
warning.
1.14 fetch (HTTP) (Phase 14)
fetch URL into var lowers to MochiFetch.Get(url) using
System.Net.Http.HttpClient (HTTP/2, BCL TLS, 30s timeout). Non-200
responses throw MochiPanic("MOCHI_ERR_HTTP", status).
json_decode(s) lowers to MochiJson.Decode(s) using
System.Text.Json.JsonDocument. Returns a MochiMap<string, string>
(leaf values stringified). Source-generator-friendly on NativeAOT.
1.15 NativeAOT packaging (Phase 15)
--target=dotnet-aot invokes dotnet publish -c Release -r <RID> -p:PublishAot=true -p:StripSymbols=true. Mochi.Runtime ships an
rd.xml root descriptor covering all runtime-reflective paths.
| Target | Binary size | Cold start |
|---|---|---|
fx-dependent .dll | ~150 KB | ~500 ms |
| Self-contained trimmed | ~15 MB | ~150 ms |
| NativeAOT single-file | ~5 MB | <30 ms |
1.16 Reproducibility (Phase 16)
Three MSBuild properties make .dll files bit-identical across
machines: <Deterministic>true</Deterministic>,
<PathMap>$(MSBuildProjectDirectory)=/_/</PathMap>,
<DebugType>none</DebugType>. TestPhase16Reproducible builds all 5
Phase 1 fixtures twice in separate temp directories and compares
SHA-256 of every .dll file byte-for-byte.
1.17 Self-contained packaging across RIDs (Phase 17)
--target=dotnet-self-contained invokes dotnet publish -c Release -r <RID> --self-contained true. Supported RIDs: linux-x64,
linux-arm64, osx-arm64, win-x64. The host RID is detected via
build.HostRID() from runtime.GOOS + runtime.GOARCH.
1.18 Trim cleanliness and NuGet publication (Phase 18)
TestPhase18TrimClean (gated by MOCHI_TEST_TRIM=1) runs a NativeAOT
publish of the Phase 1 fixtures and asserts zero IL2026/IL3050
family trim warnings. Mochi.Runtime 0.15.0-alpha is published to
NuGet via dotnet nuget push:
<PackageReference Include="Mochi.Runtime" Version="0.15.0-alpha" />
2. Test corpus
| Test function | Phase | Coverage |
|---|---|---|
TestPhase0Skeleton | 0 | toolchain detect; runtime_nupkg; go build clean |
TestPhase1Hello | 1 | 5 fixtures; net8.0 + net10.0; Roslyn-clean |
TestPhase2Scalars | 2 | int/float/bool/string; NaN/Inf; divzero; casts |
TestPhase2ControlFlow | 2 | if/while/for; break/continue; user functions |
TestPhase3Collections | 3 | list/map/set literals; ops; for-in |
TestPhase4Records | 4 | record types; field access; record update |
TestPhase5Sums | 5 | sealed variants; switch expressions; option/result |
TestPhase6Closures | 6 | lambdas; partial apply; higher-order builtins |
TestPhase7Query | 7 | from/where/select; group-by; hash join; sort/take/skip |
TestPhase8Datalog | 8 | facts; recursive rules; negation-as-failure |
TestPhase9Agents | 9 | 9 fixtures; Channel mailbox; intent dispatch |
TestPhase10Streams | 10 | IAsyncEnumerable publish/subscribe; backpressure |
TestPhase11Async | 11 | async colouring; await_all; error propagation |
TestPhase12FFI | 12 | extern dotnet fun; static + instance methods |
TestPhase13LLM | 13 | cassette playback; structured output |
TestPhase14Fetch | 14 | HTTP GET; json_decode |
TestPhase15NativeAot | 15 | 6 fixtures; gated on MOCHI_TEST_AOT=1 |
TestPhase16Reproducible | 16 | 5 Phase 1 fixtures built twice; SHA-256 bit-identical |
TestPhase17SelfContained | 17 | 4 fixtures on host RID |
TestPhase18Publish | 18 | gated on MOCHI_TEST_TRIM=1 |
All tests are green on .NET 10.0.107 SDK. The CI matrix runs net8.0 and net10.0 TFMs on linux-x64, linux-arm64, osx-arm64, and win-x64.
3. New compiler pipeline files
transpiler3/dotnet/
colour/colour.go -- async colouring pass
lower/lower.go -- aotir -> csharpsrc (all lowering)
emit/emit.go -- csharpsrc -> .cs source text
emit/dotnet.go -- dotnet publish subprocess + fx-dep packaging
build/build.go -- Driver.Build; Toolchain; Target constants
build/fxdep.go -- --target=dotnet-fx-dependent
build/selfcontained.go -- --target=dotnet-self-contained
build/aot.go -- --target=dotnet-aot (NativeAOT ILC)
build/csproj.go -- .csproj XML with Deterministic + PathMap
csharpsrc/nodes.go -- ~35 AST node types + csString()
runtime/Mochi.Runtime/ -- net8.0;net9.0;net10.0 multi-target runtime
4. aotir IR lowering table
MEP-48 added no new IR nodes to the shared aotir package.
| aotir node | C# lowering |
|---|---|
AgentDecl | class with Channel<IMsg> + async Task Run() |
SpawnExpr | AgentClass.Start(args) |
AsyncExpr | Task.Run(async () => body) |
AwaitExpr | await expr (in Red async context) |
StreamDecl | IAsyncEnumerable<T> via Channel<T>.Reader.ReadAllAsync() |
SumTypeDecl | abstract sealed record + sealed record variants |
MatchExpr | C# 8+ switch expression with type patterns |
RecordDecl | C# sealed record (positional) |
ExternFuncDecl | static or instance BCL/NuGet method call |
GenerateExpr | MochiLLM.Generate(provider, model, prompt) |
FetchExpr | MochiFetch.Get(url) |
PrintStmt | Print.Line(expr) |
IntLit | 42L (long literal with L suffix) |
5. Compatibility
v0.15.0 is additive. All existing Mochi programs continue to run under
vm3 with mochi run. mochi build --target=c-aot (MEP-45),
--target=beam-escript (MEP-46), and --target=jvm-uberjar (MEP-47)
are unchanged. mochi build --target=dotnet-fx-dependent is the new
entry point for .NET output.
.NET 8 LTS is the minimum supported .NET version. .NET 10 LTS is the recommended production runtime. .NET 6 and .NET 7 are not supported.
Mochi.Runtime 0.15.0-alpha is Apache-2.0 licensed.
6. Upgrade
curl -fsSL https://get.mochi-lang.dev | sh
mochi --version # 0.15.0
Or, with Docker:
docker pull ghcr.io/mochilang/mochi:0.15.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.