Skip to main content

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:

  1. parser.Parse -- identical to every other Mochi backend.
  2. types.Check -- shared type-checker; .NET backend adds no new types.
  3. aotir.Lower -- the shared IR from MEP-45; runs monomorphisation, closure-conversion, and match-to-decision-tree.
  4. colour.Analyse -- .NET-specific async colouring pass. Propagates async 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).
  5. lower.Lower (dotnet) -- converts aotir nodes to csharpsrc structural nodes (a C# AST).
  6. emit.Emit -- renders csharpsrc nodes to .cs source files.
  7. dotnet publish subprocess -- invokes the .NET SDK with a generated .csproj containing a ProjectReference to Mochi.Runtime.csproj.
  8. 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.

TargetBinary sizeCold 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 functionPhaseCoverage
TestPhase0Skeleton0toolchain detect; runtime_nupkg; go build clean
TestPhase1Hello15 fixtures; net8.0 + net10.0; Roslyn-clean
TestPhase2Scalars2int/float/bool/string; NaN/Inf; divzero; casts
TestPhase2ControlFlow2if/while/for; break/continue; user functions
TestPhase3Collections3list/map/set literals; ops; for-in
TestPhase4Records4record types; field access; record update
TestPhase5Sums5sealed variants; switch expressions; option/result
TestPhase6Closures6lambdas; partial apply; higher-order builtins
TestPhase7Query7from/where/select; group-by; hash join; sort/take/skip
TestPhase8Datalog8facts; recursive rules; negation-as-failure
TestPhase9Agents99 fixtures; Channel mailbox; intent dispatch
TestPhase10Streams10IAsyncEnumerable publish/subscribe; backpressure
TestPhase11Async11async colouring; await_all; error propagation
TestPhase12FFI12extern dotnet fun; static + instance methods
TestPhase13LLM13cassette playback; structured output
TestPhase14Fetch14HTTP GET; json_decode
TestPhase15NativeAot156 fixtures; gated on MOCHI_TEST_AOT=1
TestPhase16Reproducible165 Phase 1 fixtures built twice; SHA-256 bit-identical
TestPhase17SelfContained174 fixtures on host RID
TestPhase18Publish18gated 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 nodeC# lowering
AgentDeclclass with Channel<IMsg> + async Task Run()
SpawnExprAgentClass.Start(args)
AsyncExprTask.Run(async () => body)
AwaitExprawait expr (in Red async context)
StreamDeclIAsyncEnumerable<T> via Channel<T>.Reader.ReadAllAsync()
SumTypeDeclabstract sealed record + sealed record variants
MatchExprC# 8+ switch expression with type patterns
RecordDeclC# sealed record (positional)
ExternFuncDeclstatic or instance BCL/NuGet method call
GenerateExprMochiLLM.Generate(provider, model, prompt)
FetchExprMochiFetch.Get(url)
PrintStmtPrint.Line(expr)
IntLit42L (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.