Skip to main content

MEP 48. Mochi-to-.NET transpiler: NuGet ecosystem, async/await colouring, NativeAOT single-file binaries

FieldValue
MEP48
TitleMochi-to-.NET transpiler
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-23 06:30 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse), MEP-46 (BEAM transpiler, IR reuse), MEP-47 (JVM transpiler, IR reuse)
Research~/notes/Spec/0048/01..12
Tracking/docs/implementation/0048/

Abstract

Mochi today ships vm3 (mochi run), an ahead-of-time C transpiler producing native single-file binaries (MEP-45), an Erlang/BEAM transpiler producing supervised concurrent runtimes (MEP-46), and a JVM transpiler producing Maven-Central-interoperable jars and GraalVM native-image binaries (MEP-47). None of these paths gives users the .NET ecosystem: 6+ million packages on NuGet, CLR reified generics (no JVM-style type erasure), value types as first-class citizens (record struct, readonly struct, Span<T>), LINQ as the canonical query API across IEnumerable<T>/IAsyncEnumerable<T>/IQueryable<T>, System.Threading.Channels as a battle-tested back-pressure-friendly mailbox, async/await colouring as the canonical .NET concurrency model, NativeAOT for ~5MB single-file static binaries with sub-30ms cold start, MAUI / Blazor WASM / Unity / Godot for mobile / web / game integration, and deep first-class Windows enterprise integration via Visual Studio, MSBuild, ASP.NET Core, Orleans, EF Core, and the Microsoft tooling stack. MEP-48 specifies a fourth transpiler pipeline that targets the .NET Common Language Runtime (CLR) via C# source emitted through Roslyn (Microsoft.CodeAnalysis.CSharp), with a System.Reflection.Emit direct-IL fallback (using .NET 9's PersistedAssemblyBuilder for saveable assemblies) 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 and MEP-47. It forks at the emit stage: instead of emitting ISO C23 (MEP-45), Core Erlang via cerl (MEP-46), or Java source via JavaPoet (MEP-47), it emits C# source as Roslyn SyntaxNode trees, feeds them to an in-process CSharpCompilation, and collects the resulting .dll files. Three packaging targets ship together: --target=dotnet-fx-dependent (default for mochi build) produces a framework-dependent .dll plus .exe apphost runnable via dotnet app.dll on any installed .NET 8+ runtime (~150 KB user code, ~500ms cold start, requires .NET runtime on $PATH); --target=dotnet-self-contained produces a self-contained publish directory bundling a minimal CLR for a specific RID (~50 MB un-trimmed, ~15 MB trimmed, ~200 ms cold start, self-contained); --target=dotnet-aot produces a NativeAOT single-file static binary (~3-6 MB, sub-30 ms cold start, no .NET runtime required, closed-world). A fourth secondary target --target=dotnet-singlefile produces a single-file extracting executable for non-AOT-eligible programs (Phase 2). Phase-3 secondary targets --target=dotnet-maui (mobile / desktop GUI), --target=dotnet-blazor (browser WebAssembly), --target=dotnet-unity (Unity game engine after Unity 6.8 CoreCLR migration), and --target=dotnet-godot (Godot game engine) ship via sub-MEPs MEP-48.1 through MEP-48.4.

The master correctness gate is byte-equal stdout from the produced .dll or NativeAOT binary versus vm3 on the entire fixture corpus, across .NET 8 LTS and .NET 10 LTS, 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. C# source via Roslyn SyntaxFactory, with System.Reflection.Emit fallback. The default emit path produces C# syntax trees, then invokes Roslyn's CSharpCompilation in-process to generate .dll files. C# source carries no version drag (Roslyn ships with the .NET SDK), gives us free static typing checks (catching transpiler bugs at compile time), produces debuggable output (a developer can read the emitted C#), and exploits the entire C# feature set including records, list patterns, switch expressions, primary constructors, collection expressions, and [ModuleInitializer]. For hot lowerings where source is too verbose or where NativeAOT trimming would strip a reflectively-invoked method (agent dispatch trampolines), System.Reflection.Emit (and PersistedAssemblyBuilder since .NET 9) emits IL directly. See 05-codegen-design §1-§4. F#/VB.NET as IR rejected: see 02-design-philosophy §7 and 12-risks-and-alternatives §A1-A2.

  2. .NET 8 LTS minimum, .NET 10 LTS preferred. .NET 8 (November 2023) is the floor: records (C# 9+, finalised C# 12), primary constructors (C# 12), collection expressions (C# 12), list patterns (C# 11), switch expressions (C# 8), System.Threading.Channels (matured), IAsyncEnumerable<T> (C# 8 with full library surface), FrozenDictionary<K,V> / FrozenSet<T> (.NET 8), NativeAOT (GA .NET 7, hardened .NET 8). .NET 10 (November 2025) adds: C# 14 with extension types (preview), OrderedDictionary<K, V> in the BCL, dotnet run app.cs file-based-app mode, refined NativeAOT (smaller binaries, faster ILC), improved trim warning diagnostics. The build emits with <TargetFramework>net8.0</TargetFramework> for compatibility; CI runs the full matrix on net8.0 and net10.0. .NET 6 LTS rejected (EOL November 2024). .NET 7 STS rejected (EOL May 2024, no current support). See 02-design-philosophy §2 and 07-dotnet-target-portability §1.

  3. System.Threading.Channels + async/await colouring for agents and streams; no Loom equivalent, no actor framework. Mochi agents lower one-to-one to a Channel<TMessage> (where TMessage is a sealed record class union over on-handlers) plus an async Task dispatch loop. Streams lower to IAsyncEnumerable<T> with operators from System.Linq.Async. The async colouring pass propagates async Task<T> (red) vs synchronous (blue) function colour through the static call graph; Mochi surface code never types async or await. No Akka.NET, Orleans, Proto.Actor, or Microsoft.Dataflow as runtime defaults; the runtime exposes adapter helpers for users who want those packages. See 09-agent-streams §1-§6, 02-design-philosophy §13, 12-risks-and-alternatives §A4.

  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 four backends. The fork is at the emit pass: transpiler3/dotnet/lower/ lowers aotir to C#-source structural nodes; 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 four targets semantically aligned and amortises pass-implementation work across all four. See 05-codegen-design §5.

  5. BCL as fat runtime; Mochi.Runtime as thin runtime. The runtime library Mochi.Runtime (Apache-2.0, ~5000 LOC of C#) provides only what the BCL does not: a Mochi-typed function delegate zoo (for arity > 16, the BCL's Func/Action ceiling), OrderedMap<K,V> / OrderedSet<T> (on .NET 8; OrderedDictionary<K,V> is BCL on .NET 10+), a small Datalog engine, DiagnosticSource event definitions, the AI / FFI dispatch tables, and the agent supervisor. Everything else (HTTP, JSON, time, regex, locale, file I/O) goes through the BCL directly. System.Text.Json, System.Threading.Channels, System.Collections.Immutable, System.Linq.Async, YamlDotNet, and optional BouncyCastle.Cryptography are the only vendored NuGet deps. Newtonsoft.Json, Autofac, AutoMapper (non-source-gen modes), and Castle.Core are explicitly rejected as runtime deps. See 04-runtime §1-§17, 02-design-philosophy §4 and §10, 12-risks-and-alternatives §A10.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/dotnet/fixtures/ must compile via the .NET pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. Roslyn-clean (<TreatWarningsAsErrors>true</TreatWarningsAsErrors> plus the Mochi-shipped Mochi.Analyzers package) on generated code is the secondary gate. NativeAOT publish (with zero trim warnings IL2026/IL2070/IL2080/IL3050 family) is the tertiary gate. Reproducibility (bit-identical .dll 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), the BEAM target (for supervised concurrent runtimes via MEP-46), and the JVM target (for Maven-Central-interoperable jars and GraalVM native binaries via MEP-47). None deliver what .NET uniquely provides:

  1. NuGet, the canonical .NET package repository. As of 2026-05, NuGet hosts more than 6 million unique package versions across ~500,000 distinct package IDs, with all major Microsoft first-party libraries (System., Microsoft.) plus a deep third-party ecosystem (Polly, MediatR, Serilog, FluentValidation, Mapster, Dapper, EF Core, MSTest / xUnit / NUnit, BenchmarkDotNet, Akka.NET, Orleans, Octopus, NodaTime, OpenTelemetry). A Mochi program that needs Azure SDK access, Office document parsing, BLE communication, ASP.NET Core integration, OAuth, AWS SDK, Google Cloud, Power BI, or any of thousands of other capabilities, can import an existing NuGet package, with a SHA-256 pin and a packages.lock.json transitive lockfile. NuGet is smaller than Maven Central (10M+) but the per-domain coverage is essentially complete for Mochi's workload classes.

  2. CLR reified generics. Unlike JVM type erasure, the CLR carries full type arguments at runtime. A Mochi list<int> is a literal List<long> with unboxed long slots; reflection over typeof(List<long>) returns the actual type argument; no per-instance type tags or bridge methods. For Mochi, this eliminates an entire class of workaround that MEP-47 had to ship (per-instance Class tokens, type-witness threading). See 06-type-lowering §3.

  3. Value types as first-class citizens. struct, record struct, readonly struct, ref struct are stack-allocated value types with no GC pressure. Mochi small records (Option, Point, ValueTuple-like records) can lower to value types, eliminating heap allocation for the common case. Span<T> and Memory<T> give zero-copy slice types over arrays and unmanaged memory. None of the other Mochi targets has this story (until JVM Valhalla GAs).

  4. LINQ as canonical query target. The Mochi query DSL is almost isomorphic to LINQ method-syntax. We get a battle-tested query engine, syntax-tree-to-method-call rewriting handled by Roslyn, and a parallelism story (PLINQ) for free. IAsyncEnumerable<T> plus System.Linq.Async extends this to back-pressure-aware streaming. See 08-dataset-pipeline §1-§6.

  5. System.Threading.Channels + async/await. A first-class, BCL-supported back-pressure-aware channel implementation, used by ASP.NET Core, Orleans, and the dotnet runtime itself. The Mochi agent mailbox lowers to one Channel<TMessage> per agent. Bounded vs unbounded is a one-flag choice (BoundedChannelOptions). C# async/await predates Loom by years and is the canonical async story in industry. We pay the function-coloring cost (red vs blue functions) but inherit the entire BCL's async surface. See 09-agent-streams §1-§6.

  6. NativeAOT. Native Ahead-of-Time compilation produces single static native binaries with sub-30ms cold start and ~3-6 MB binary size, competitive with MEP-45 C output and smaller than GraalVM native-image. Trimming is integrated; closed-world analysis is strict but mature; source generators replace runtime reflection. See 10-build-system §6-§7.

  7. BCL is one of the best-engineered runtime libraries in the industry. System.Text.Json is source-generator-AOT-friendly. System.Net.Http.HttpClient is HTTP/3-capable. System.IO.Path handles cross-platform paths correctly. System.Threading.Channels and System.Threading.Tasks are the reference design for async data flow. System.Security.Cryptography covers TLS, X.509, JWT, AEAD ciphers, message digests. LINQ provides a fluent collection-processing API. Mochi FFI to any of these is direct: import "dotnet/System.Text.Json" lowers to using System.Text.Json;. See 04-runtime §6-§13.

  8. Visual Studio, Rider, VS Code. Microsoft, JetBrains, and the C# Dev Kit team ship best-in-class IDE support, with refactoring, debugging, and Roslyn-powered analyzers. Mochi-on-.NET users get a professional IDE story out of the box (Roslyn analyzers, EditorConfig, hot reload during development, F11 step-into into emitted C# source).

  9. Windows desktop and enterprise integration. WPF, WinForms, MAUI Windows, ClickOnce, MSIX deployment, Active Directory integration, Windows Authentication, .NET Framework interop via the unified runtime, Office automation via COM Interop, all reachable from Mochi-on-.NET via FFI. The C target's Windows story is C-level; the BEAM target's is poor; the JVM target's is medium. .NET wins this quadrant decisively.

  10. Mobile and game-engine reach via MAUI / Unity / Godot. .NET MAUI ships iOS and Android apps from one codebase. Unity (the dominant indie / mobile game engine, ~50% market share) embeds the .NET runtime; Unity 6.8 announced migration to CoreCLR. Godot embeds .NET via Godot Mono / Godot CoreCLR. Phase 3 sub-MEPs (MEP-48.1 through MEP-48.4) target these surfaces. See 07-dotnet-target-portability §6-§9.

The C target (MEP-45) remains the right choice for embedded targets, single-file distribution, and minimal runtime footprint. The BEAM target (MEP-46) remains the right choice for hot-reload services, distributed pubsub, and OTP supervision. The JVM target (MEP-47) remains the right choice for Maven Central deep-cut library access, Loom concurrency, and Android via D8/R8. The .NET target is the right choice for NuGet interop, Windows enterprise, reified generics, value types, LINQ-as-canonical, NativeAOT distribution, and Unity / Godot / MAUI mobile / game integration. All five (vm3 plus four AOT targets) ship; the user picks per workload.

Specification

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

1. Pipeline and IR reuse

MEP-48 shares the front-end and aotir passes with MEP-45, MEP-46, and MEP-47 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)
| async-colour (MEP-48 pass 1; sync/async colouring per [[05-codegen-design]] §12)
v
aotir (coloured)
| dotnet-lower (MEP-48 pass 2; ./transpiler3/dotnet/lower/)
v
C#-source AST (Roslyn SyntaxNode trees; or System.Reflection.Emit IL builders for hot paths)
| emit (./transpiler3/dotnet/emit/)
v
C# source text (or .dll bytes directly for hot paths)
| Microsoft.CodeAnalysis.CSharp.CSharpCompilation in-process
v
.NET assembly (.dll)
| package as fx-dependent / self-contained / NativeAOT / single-file
v
Distributable artefact

aotir is unchanged. The MEP-48-specific work lives in transpiler3/dotnet/:

  • transpiler3/dotnet/lower/: aotir -> C#-source AST (Roslyn SyntaxNode builders).
  • transpiler3/dotnet/colour/: async colouring pass (sync vs async per function); runs after closure-convert, before lower.
  • transpiler3/dotnet/emit/: drives Microsoft.CodeAnalysis.CSharp.CSharpCompilation to produce .dll.
  • transpiler3/dotnet/ilemit/: optional System.Reflection.Emit direct emission for hot lowerings (agent trampolines, numeric loops).
  • transpiler3/dotnet/build/: build driver: fx-dependent / self-contained / NativeAOT / single-file targets; cache; lockfile.
  • transpiler3/dotnet/runtime/: the Mochi runtime NuGet package source (C#), built once and vendored.

transpiler3/dotnet/lower/ consumes aotir.Module and produces csharpsrc.CompilationUnit. csharpsrc is a small Go package mirroring Roslyn's SyntaxFactory model: ClassDecl, MethodDecl, Block, Statement, Expression. The emit pass walks this tree, formats Roslyn syntax via the dotnet-side helper (a small mochi-dotnet-codegen binary built with the pinned Roslyn version, invoked over a JSON protocol).

The aotir IR is target-agnostic: it has no notion of .NET assemblies, IL, or async colouring. The lowering pass adds those concerns. The System.Reflection.Emit path is opt-in: a function annotated with @hot (or detected as an agent dispatch trampoline) skips the source path and emits IL directly via System.Reflection.Emit.AssemblyBuilder (or PersistedAssemblyBuilder on .NET 9+ for saveable assemblies).

2. Name mangling and C# reservation safety

C# identifiers are stricter than Mochi's in some ways and more permissive in others. The mangling rules (full table in 06-type-lowering §15):

  • Mochi variables that collide with a C# reserved word (per the C# spec: abstract, as, base, bool, break, ..., where, while, yield, plus contextual keywords record, init, nint, nuint, file, required, scoped) get @ prefix in the emitted C#, which is the standard C# escape: class -> @class, record -> @record.
  • Mochi package paths mathutils/extra lower to C# namespace Mochi.User.MathUtils.Extra for user code. Configurable via --dotnet-base-namespace; default Mochi.User. The User. segment makes the runtime/user distinction visible in stack traces.
  • Mochi record type names lower to C# class names in PascalCase, unchanged (Book -> Book). On collision with System.* types (String, Object, DateTime) the backend keeps the Mochi name and adds an explicit using-alias (using SystemString = System.String;) to disambiguate references inside the emitted module.
  • Mochi sum-type variant constructors lower to C# record classes nested in a [MochiUnion]-attributed abstract base (Leaf -> Tree.Leaf inside namespace Mochi.User.Foo).
  • Mochi local function references and method names: PascalCase preserved for public, camelCase preserved for locals. The Mochi-to-C# name map is sidecar in the PDB for debugger / stack-trace mapping.
  • Mochi runtime-internal symbols use the __mochi_ prefix to avoid colliding with user names.

The mangling is deterministic; reversal goes through the C# #line directive (preserves Mochi source positions in PDBs and stack traces) plus a .mochi-pdb.json sidecar consumed by the agent supervisor and Mochi.Runtime.Diag. See 05-codegen-design §8.

3. Type lowering table

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

Mochi.NETNotes
intlong (C# long = System.Int64); long? nullable64-bit; C# int is 32-bit, REJECTED
floatdouble; double? nullable64-bit IEEE 754
boolbool; bool? nullable
stringSystem.StringUTF-16 internally; Mochi exposes UTF-8 byte length and codepoint indexing
timeSystem.DateTimeOffset (or NodaTime Instant via opt-in)100-nanosecond precision; tz-aware via opt-in
durationSystem.TimeSpan100-nanosecond precision
bigintSystem.Numerics.BigInteger (value struct, immutable)All arithmetic operators overloaded
list<T>System.Collections.Immutable.ImmutableList<T> (default); ImmutableArray<T> for dense numerics; List<T> for hot var-mutated; FrozenSet/FrozenDictionary for init-once lookup tablesPer-allocation choice driven by flow analysis (06-type-lowering §8)
map<K,V>System.Collections.Generic.OrderedDictionary<K,V> on net10.0; Mochi.Runtime.OrderedMap<K,V> on net8.0Insertion-ordered, per Mochi spec
set<T>System.Collections.Immutable.ImmutableHashSet<T> (immutable default); Mochi.Runtime.OrderedSet<T> when iteration order observable
T { ... } (record)C# sealed record class (default) or readonly record struct (small, value-type fields)All-immutable fields; auto Equals/GetHashCode/ToString/Deconstruct/with
type T = A | B (sum)abstract record + sealed record per variant + [MochiUnion] attributeExhaustiveness enforced by Mochi.Analyzers + default-arm throw
fun(A, B) RSystem.Func<A, B, R> (arity <= 16); Mochi.Runtime.Func17<...> etc. for higher arityCaptures hoisted into compiler-generated closure class
stream<T>System.Collections.Generic.IAsyncEnumerable<T> (C# 8)Operators via System.Linq.Async plus Mochi runtime extensions
agentSystem.Threading.Channels.Channel<TMessage> + async dispatch loopTMessage is sealed record union over on handlers
Option<T>Mochi.Runtime.Option<T> (sealed abstract record Option<T> + Some<T>(T) / None<T>()); or T? (nullable value type) for value-type fast pathChoice per call site by flow analysis
Result<T, E>Mochi.Runtime.Result<T, E> (sealed hierarchy)Extension methods Map / MapErr / AndThen / Unwrap

Boxing: CLR reified generics mean list<int> is ImmutableList<long> storing unboxed long slots end-to-end; no boxing at element access or generic-bound boundaries. This is a substantial positive delta vs MEP-47's JVM target. See 06-type-lowering §2-§3.

Immutability: System.Collections.Immutable.* types are immutable by construction. The lowering does NOT need to insert List.copyOf-equivalent calls at function boundaries (unlike MEP-47); the immutable types are immutable. For var-mutated bindings the lowering switches to List<T> / Dictionary<K,V> / HashSet<T> (mutable) per the flow analysis pass; the immutable-vs-mutable choice is per-binding. See 06-type-lowering §8.

4. Expression and statement lowering

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

  • Mochi if cond { a } else { b } lowers to a C# ternary cond ? a : b when both arms are expressions; lowers to a C# 8 switch expression _ => ... shape when arms are pattern matches; lowers to if/else statements when arms have side effects.
  • Mochi match lowers to a C# 8 switch expression with record-pattern deconstruction and when clauses for guards. Exhaustiveness is enforced by Mochi.Analyzers (Roslyn analyzer shipped as a NuGet dependency) plus a runtime default-arm throw. The C# compiler itself does not enforce exhaustiveness on sealed-record hierarchies.
  • Mochi for x in xs lowers to a C# foreach (var x in xs) loop. For maps, for (k, v) in m lowers to a loop over m with tuple deconstruction foreach (var (k, v) in m).
  • Mochi early-return lowers to C# return. Mochi break / continue lower to C# equivalents.
  • Mochi try/catch (the try { ... } catch { ... } form per MEP-19) lowers to C# try-catch; Mochi raises Mochi-specific MochiException subclasses unified under Mochi.Runtime.MochiException.
  • Block-as-expression (the last expression in a block is the block's value) lowers via a synthesised local function (() => { ...; return e; })() or, when the body is a single expression, via the C# ternary or switch-expression form directly.

Integer arithmetic uses native CLR long ops (silent overflow, two's complement). The --strict-int build flag wraps every arithmetic op in checked(...) blocks, which throw OverflowException. Off by default; on for security-sensitive builds. See 06-type-lowering §1.

Integer division: C# long / long is truncation toward zero, matching Mochi's spec for /. Modulo C# % matches Mochi % for positive operands; for negative operands the Mochi spec is truncated-modulo (sign follows dividend), which is C#'s default. No helper needed. See 06-type-lowering §1.

5. Closures and delegates

Mochi first-class function values lower to C# Func<...> / Action<...> delegates instantiated from lambda expressions. Capture-by-value of effectively-readonly locals; mutable captures hoist into a compiler-generated closure class field (the standard C# closure model). The CLR JIT inlines lambdas aggressively when monomorphic; for hot paths the source code may use [MethodImpl(MethodImplOptions.AggressiveInlining)] attributes via the codegen.

The runtime exposes higher-arity delegate types for signatures the BCL does not provide:

  • Func<A1, ..., A16, R> and Action<A1, ..., A16> (BCL ceiling of arity 16).
  • Mochi.Runtime.Func17<...> through Mochi.Runtime.Func32<...> (Mochi extension for arity 17 through 32).
  • Beyond arity 32, the codegen emits a record class with a single Invoke(args) method.

The lowering picks the most-specific BCL Func/Action type when one fits, falling back to Mochi.Runtime.Func<...> for higher arities. See 05-codegen-design §6 and 06-type-lowering §7.

Hot-path lambda call sites can be System.Reflection.Emit-emitted directly, 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 Mochi.Runtime NuGet package (Mochi.Runtime 0.10.0+, Apache-2.0) is the sole external dep that every emitted .NET Mochi program needs. It is published to nuget.org with Authenticode signing (see 10-build-system §5).

Package layout (04-runtime §16):

Mochi.Runtime
.Core // print formatters, panic, MochiException hierarchy
.Str // UTF-8 string ops, ReadOnlySpan<byte> helpers
.Coll // OrderedMap<K,V>, OrderedSet<T>, structural-equality helpers
.Query // Mochi query DSL runtime (group_by, hash_join, sort, set ops over LINQ)
.Streams // cold-stream operators not in System.Linq.Async (window, throttle)
.Agents // agent template, channel mailbox, intent dispatch, supervisor
.Datalog // semi-naive evaluator over Dictionary-of-tuples
.Llm // AI provider abstraction over HttpClient (OpenAI, Anthropic, local)
.Fetch // HttpClient wrapper with JSON decode shim
.Ffi // P/Invoke marshalling helpers (UTF-8 strings, Span)
.Test // xUnit/MSTest/NUnit-compatible expect/test driver
.Io // variadic print, per-type formatter dispatch
.Telemetry // DiagnosticSource event definitions

Vendored deps (pinned, listed in Mochi.Runtime.csproj <PackageReference> with Version lock and packages.lock.json):

  • System.Text.Json (BCL; explicit reference for source-gen extensions)
  • System.Threading.Channels (BCL)
  • System.Collections.Immutable (BCL)
  • System.Linq.Async (NuGet, ~250KB, Apache-2.0)
  • Microsoft.Extensions.Logging.Abstractions (BCL-adjacent)
  • YamlDotNet (NuGet, pinned, only when YAML used by Mochi.Query.Yaml extension)
  • BouncyCastle.Cryptography (NuGet, pinned, only when crypto FFI used)

Rejected as runtime deps: Newtonsoft.Json, Autofac, AutoMapper (non-source-gen), Castle.Core, FluentValidation. See 10-build-system §6 and 12-risks-and-alternatives §A10.

The runtime NuGet size target: <1 MB before trim; <300 KB after NativeAOT trim.

A companion analyzer package Mochi.Analyzers (Roslyn IIncrementalGenerator plus diagnostic analyzers) ships separately, with diagnostics MOCHI001 through MOCHI006 (11-testing-gates §4). It is referenced as a transitive dependency of Mochi.Runtime.

7. Concurrency and supervision

Mochi agents lower to a Channel<TMessage> plus an async dispatch loop. Each agent declaration produces a C# class with a Channel<TMessage> mailbox (where TMessage is a sealed record union over the on-handlers), an async Task DispatchLoop() method that awaits channel.Reader.ReadAsync(), and a Task.Run(DispatchLoop, TaskCreationOptions.LongRunning) startup. See 09-agent-streams §2 and §3.

public sealed class Counter
{
private readonly Channel<CounterMsg> _mailbox =
Channel.CreateUnbounded<CounterMsg>(new UnboundedChannelOptions
{
SingleReader = true, SingleWriter = false,
});
private long _count;
private readonly Task _loop;

public Counter()
{
_loop = Task.Run(DispatchLoopAsync, TaskCreationOptions.LongRunning);
}

private async Task DispatchLoopAsync()
{
await foreach (var msg in _mailbox.Reader.ReadAllAsync().ConfigureAwait(false))
{
switch (msg)
{
case Inc i: _count += i.Delta; break;
case Value v: v.Reply.SetResult(_count); break;
case Stop: return;
default: throw new MochiMatchExhaustivityError(msg);
}
}
}

public ValueTask SendAsync(CounterMsg m) =>
_mailbox.Writer.WriteAsync(m);
}

public abstract record CounterMsg;
public sealed record Inc(long Delta) : CounterMsg;
public sealed record Value(TaskCompletionSource<long> Reply) : CounterMsg;
public sealed record Stop() : CounterMsg;

Supervision: Mochi's supervisor declaration lowers to a Mochi.Runtime.Agents.Supervisor that re-implements OTP's one_for_one, one_for_all, and rest_for_one strategies in user-space. Crashed agents (uncaught exception escaping the dispatch loop) trigger restart per the declared policy. See 09-agent-streams §10.

spawn f() lowers to Task.Run(() => f()) returning a Task (or Task<T> when f has a return value). await f() lowers to C# await f() inside an async context (the colouring pass guarantees the caller is async). See 09-agent-streams §5.

The "no async/await in surface" position is load-bearing (02-design-philosophy §13). Mochi surface code never types async or await. The colouring pass propagates the colour through the static call graph: any function transitively calling an async primitive (channel read/write, IO, fetch, sleep, ai.chat) lowers to async Task<T> and every call site to it gets an await. Pure (sync-only) functions remain synchronous. See 05-codegen-design §12.

The runtime calls .ConfigureAwait(false) on every internal await to avoid synchronization-context capture (relevant under ASP.NET Core legacy, WPF, WinForms). Surface user code does not need to know.

8. Memory model

The Mochi memory model on the CLR is the .NET memory model (ECMA-335 §I.12.6, Phillipart-Krzeszewski talks at .NET Conf 2025) restricted to Mochi-allowed operations. Highlights:

  • All value-type locals (long, double, bool, value structs) follow .NET value-type semantics: thread-local unless explicitly shared via a channel or Task/TaskCompletionSource<T>.
  • All reference-type locals (record class instances, ImmutableList/Dictionary references) are immutable from Mochi's POV; mutation is a transpiler-internal concern (the immutable types are immutable by construction). The CLR JIT's escape analysis handles the rest.
  • Cross-agent communication via Channel<T> is a full happens-before edge (the channel's internal synchronization establishes the edge).
  • Agents do not share mutable state with each other except via channels; the runtime does not provide a "shared variable" primitive.

GC: the build defaults to Server GC on 64-bit (<ServerGarbageCollection>true</ServerGarbageCollection>) with Concurrent GC enabled. For long-running services the runtime documents the <RetainVMGarbageCollection> and <ConcurrentGarbageCollection> tuning knobs. NativeAOT uses a single-threaded GC by default (smaller binary); the build can opt into Server GC for AOT via <IlcOptimizationPreference>Speed</IlcOptimizationPreference>. See 04-runtime §2.

Memory budgets: agents idle at ~2 KB per agent (channel internal state + Task stack); message size plus ~24 B per channel slot. Immutable collections cost more than mutable for small sizes; comparable at large sizes due to structural sharing. See 04-runtime §5.

9. Error model

Mochi exceptions lower to Mochi.Runtime.MochiException (extends System.Exception) with subclasses for each Mochi error kind:

  • MochiPanic: panic/abort
  • MochiMatchExhaustivityError: 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 C# try-catch on MochiException (and its subclasses).

CLR OutOfMemoryException, StackOverflowException, AccessViolationException, and non-Mochi Exception are not caught by Mochi's catch; they propagate to the AppDomain.CurrentDomain.UnhandledException handler, which the runtime sets to a DiagnosticSource emitter + structured stderr write. See 06-type-lowering §14.

Stack traces: every emitted method gets #line directives mapping C# lines back to Mochi source positions. A Mochi user sees Mochi-line stack traces; the underlying C# mapping is preserved in PDBs (embedded or sidecar). For NativeAOT the PDBs are shipped alongside the binary or embedded via <DebugType>embedded</DebugType>. See 05-codegen-design §8.

10. Target portability

The portability matrix is in 07-dotnet-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) , NativeAOT only path
  • macOS x86_64 (Intel)
  • Windows aarch64
  • Linux arm32 (Raspberry Pi, advisory)

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

  • iOS / iPadOS via MAUI (Phase 3, MEP-48.1)
  • Android via MAUI (Phase 3, MEP-48.1)
  • Browser WebAssembly via Blazor (Phase 3, MEP-48.2)
  • Unity (Phase 3, MEP-48.3, post Unity 6.8 CoreCLR migration)
  • Godot (Phase 3, MEP-48.4)

TFM matrix: net8.0 LTS and net10.0 LTS on every tier-1 cell. net11.0 STS smoke-tested but non-blocking.

NativeAOT: tier-1 cells get a NativeAOT build per release; trim warning cleanliness is gated.

Distribution: the dotnet SDK from Microsoft (Apache-2.0) is the canonical install path. Users install via Microsoft's installers or apt/brew/winget; the build CLI requires dotnet on PATH. We do not ship a private dotnet SDK (12-risks-and-alternatives §A11). See 07-dotnet-target-portability §3.

11. Build driver

The Mochi build driver routes .NET builds through transpiler3/dotnet/build/. Targets:

  • --target=dotnet-source: emit C# source only (for review / debugging).
  • --target=dotnet-assembly: emit .dll files into target/dotnet/assemblies/.
  • --target=dotnet-fx-dependent (default for mochi build): framework-dependent publish, runs on any .NET 8+ runtime.
  • --target=dotnet-self-contained: self-contained publish for a specific RID.
  • --target=dotnet-aot: NativeAOT single-file static binary.
  • --target=dotnet-singlefile: single-file extracting executable (Phase 2, non-AOT).

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

target/dotnet/
source/ # emitted .cs files
assemblies/ # compiled .dll files
resources/ # embedded resources, .resources files
vendor/ # vendored NuGet packages (Mochi.Runtime, System.Text.Json, ...)
out/ # final artefacts (.dll, .exe wrapper, NativeAOT binary)
cache/ # incremental build cache (per-module fingerprints)
trim/ # trim warning suppression files (per fixture)
packages.lock.json # NuGet 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. Roslyn IIncrementalGenerator outputs are cached keyed by the Mochi AST hash per module. See 10-build-system §2 and §13.

MSBuild integration (a Mochi.Build.Sdk NuGet package using the MSBuild SDK pattern) and the dotnet mochi global tool are first-class: users who already have an MSBuild-based project can add a small Mochi build step via <Sdk Name="Mochi.Build.Sdk" />. 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:

  • Roslyn's /deterministic flag (on by default since C# 7.1).
  • <Deterministic>true</Deterministic> in the generated .csproj.
  • A pinned PathMap=/_/= so source paths do not leak into PDBs.
  • A pinned Roslyn version (the dotnet SDK in the build container).
  • Source-generator outputs deterministic given input (no DateTime.Now, Guid.NewGuid, or environmental input).
  • SourceRevisionId set to a deterministic hash, not git rev-parse.

PDB files reproduce separately (PDB SHA-256 must also match). Embedded PDBs (<DebugType>embedded</DebugType>) reproduce as part of the .dll SHA-256.

For NativeAOT: ILC (the IL compiler) has worker-scheduling-induced non-determinism on parallel builds. The reproducibility gate uses <IlcMaxThreads>1</IlcMaxThreads>; the default fast build uses parallel ILC. See 10-build-system §12.

The diffoscope tool runs as a CI gate on a subset of fixtures, comparing .dll and NativeAOT binary 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 System.Text.Json with JsonSerializerContext source generators (AOT-friendly, no reflection-based polymorphic deserialisation); strict mode by default.
  • YAML parsing (when used) goes through YamlDotNet's safe deserializer (no arbitrary-type instantiation).
  • HTTP via System.Net.Http.HttpClient defaults to TLS 1.3, hostname verification on, strict certificate chain via the default HttpClientHandler.
  • Random uses System.Random.Shared (xorshiro128++) for non-cryptographic randomness; System.Security.Cryptography.RandomNumberGenerator for crypto.
  • No unsafe blocks in emitted user-facing code; the runtime contains unsafe only in Mochi.Runtime.Unsafe (Span pinning), not exposed.

Supply-chain hardening: NuGet coordinates pinned by SHA-256 in packages.lock.json. mochi audit checks pinned versions against the OSV database. dotnet nuget verify is part of the build. See 10-build-system §5 and 12-risks-and-alternatives §R3.

NativeAOT gets strict trim warning enforcement (<TreatWarningsAsErrors>true</TreatWarningsAsErrors> plus <IlcGenerateTrimmingWarnings>true</IlcGenerateTrimmingWarnings>). Source-generator-emitted [DynamicallyAccessedMembers] attributes are mandatory for any reflectively-accessed surface. See 07-dotnet-target-portability §5.

14. Diagnostics

Emitted C# source carries #line directives mapping back to Mochi source positions. Generated stack traces point to Mochi lines.

DiagnosticSource events (04-runtime §14) cover:

  • Mochi.AgentStart / AgentStop
  • Mochi.MessageSend / MessageReceive
  • Mochi.QueryStart / QueryEnd (with row count, duration)
  • Mochi.GCPressure (custom event on Gen 2 collection)
  • Mochi.FFICall (FFI call boundary, with target namespace)
  • Mochi.LLMCall (LLM provider call, with provider name, model, token count)
  • Mochi.HTTPFetch (HTTP fetch, with status code, latency)

Telemetry: the runtime optionally exports DiagnosticSource events to OpenTelemetry via the OpenTelemetry.Instrumentation.Mochi package (Apache-2.0). See 04-runtime §14.

Compile-time diagnostics: the Mochi-to-C# lowering emits Roslyn-friendly source that Roslyn can lint with <TreatWarningsAsErrors>true</TreatWarningsAsErrors> and the Mochi.Analyzers package (MOCHI001 through MOCHI006). 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. C# debug info (Roslyn PDB): full source/local mapping. Lets debuggers (Visual Studio, Rider, VS Code C# Dev Kit, dotnet-dump) navigate the emitted C#.
  2. Mochi source maps (#line directives in the emitted C# plus a .mochi-pdb.json sidecar): maps C# lines back to Mochi source lines. Lets a Mochi user see Mochi-line stack traces.

PDBs are embedded into the .dll by default (<DebugType>embedded</DebugType>); portable PDBs are written separately when requested. The runtime supervisor and Mochi.Runtime.Diag consume the sidecar to resolve frames.

For NativeAOT, the PDBs are consumed by ILC and embedded into the resulting binary's .symtab. Stack traces in AOT binaries surface Mochi line numbers via the embedded mapping.

16. .NET FFI

The import "dotnet/..." form imports a .NET type as a Mochi type:

import "dotnet/System.Guid" as Guid
import "dotnet/System.DateTimeOffset" as DateTimeOffset
import "dotnet/System.Text.Json.JsonSerializer" as JsonSerializer

let id = Guid.NewGuid()
let now = DateTimeOffset.UtcNow

Lowers to:

using System;
using System.Text.Json;
// ...
var id = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;

The lowering exposes the .NET type's public API as Mochi methods, with Mochi-typed signatures derived from the assembly's metadata (via System.Reflection.Metadata or Roslyn MetadataReference at build time). Mochi-.NET type marshalling per 06-type-lowering §13: Mochi int <=> .NET long; Mochi string <=> .NET string (UTF-16 internally, UTF-8 at I/O); Mochi list<T> <=> .NET IReadOnlyList<T> (with element marshalling); etc.

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

@nuget("System.Text.Json:9.0.0")
import "dotnet/System.Text.Json.JsonSerializer" as JsonSerializer

The build resolves the coordinate via dotnet restore, pins the SHA-256 in packages.lock.json, vendors the assembly. See 10-build-system §5.

Null safety at the FFI boundary: every .NET return value declared as nullable (T? in C# 8+ NRT) crosses an explicit null-check, or, for return types declared as Option<T> in the Mochi import, is wrapped via value is null ? new None<T>() : new Some<T>(value). See 06-type-lowering §13.

Native (C/C++) library interop is via [DllImport] or, for C# 11+, [LibraryImport] (source-generated, AOT-friendly). The Mochi extern "c" declaration lowers to a [LibraryImport] with explicit marshalling attributes; Mochi UTF-8 strings cross as ReadOnlySpan<byte> plus length rather than LPStr. The FFI runtime helpers live in Mochi.Runtime.Ffi.

17. Output style

Emitted C# source follows a deterministic style:

  • Four-space indentation (C# convention).
  • Imports sorted alphabetically; no unused imports (IDE0005 enforced).
  • One top-level type per file by default; nested types for sum-type variants under a [MochiUnion] base.
  • Class members in fixed order: fields, constructors, properties, methods, nested types.
  • Positional records used wherever the Mochi type is immutable and has <= 10 fields.
  • sealed record for all leaf record classes; abstract record for sum-type bases.
  • [MochiUnion] attribute on every sum-type base for analyzer interaction.
  • Switch expressions for match lowering, with explicit default arm throwing MochiMatchExhaustivityError.
  • var (local variable type inference) used for emitted locals where the type is unambiguous; explicit types otherwise.
  • #nullable enable in every emitted file.
  • [MethodImpl(MethodImplOptions.AggressiveInlining)] on lambda call sites detected as hot.

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

Rationale

Why .NET as a fourth target after C, BEAM, and JVM

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. The JVM target (MEP-47) gives Mochi access to Maven Central, Loom virtual threads, the C2 JIT, GraalVM native-image, and Android. None give Mochi: (a) the second-largest commercial managed runtime ecosystem, with NuGet's 6M+ packages and deep Windows / Azure / Office / Unity integration; (b) CLR reified generics (eliminating MEP-47's type-erasure workarounds); (c) value types as first-class citizens (record struct, readonly struct, Span<T>), eliminating heap allocation for small immutable records; (d) LINQ as the canonical query target across IEnumerable<T>/IAsyncEnumerable<T>; (e) System.Threading.Channels as a battle-tested mailbox primitive; (f) NativeAOT for ~5MB single-file binaries with sub-30ms cold start, closer to MEP-45's distribution shape than MEP-47's GraalVM native-image; (g) MAUI for cross-platform mobile / desktop UI, Unity / Godot for game-engine integration.

The user-facing positioning: vm3 for development, C for embedded and single-file distribution, BEAM for distributed services with hot reload, JVM for Maven Central deep-cut library access, .NET for Windows enterprise / Unity / mobile / value-types performance. See 02-design-philosophy §1.

Why C# source via Roslyn, not IL-only

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

  1. Free static-type checks: the C# compiler itself catches transpiler bugs (mistyped lowering, missing using, scope error). Emitting IL directly skips this layer.
  2. Debuggable output: a Mochi developer can read the emitted C# to understand what their program will do. IL is opaque.
  3. No version churn: Roslyn ships with every .NET SDK; no library version-pin needed (we pin a specific Roslyn version for the codegen helper, but the user-side build uses whatever SDK is installed).
  4. Full feature support: records, list patterns, switch expressions, primary constructors, collection expressions, source generators, [ModuleInitializer], all work via Roslyn without us implementing them.
  5. Hybrid escape hatch: for hot lowerings (agent dispatch trampolines, numeric loops, JIT-unfriendly reflection sites that would break NativeAOT) where source is too verbose or where trim would strip a reflectively-invoked method, we drop to System.Reflection.Emit (or PersistedAssemblyBuilder on .NET 9+). This is opt-in, not the default.

F# / VB.NET as IR: rejected because each adds a compiler dependency, output-runtime dependency, and a debugging layer; F# has its own type system that does not match Mochi's. See 02-design-philosophy §7 and 12-risks-and-alternatives §A1-A2.

Why .NET 8 LTS minimum

.NET 8 (November 2023) is the floor for these features:

  • Records (C# 9+, finalised in C# 12): how product types lower cleanly.
  • List patterns (C# 11): how list destructuring lowers.
  • Switch expressions (C# 8+, refined in 9+): how match lowers cleanly.
  • System.Threading.Channels (matured): how agent mailboxes lower.
  • IAsyncEnumerable<T> (C# 8, library complete): how streams lower.
  • NativeAOT (GA in .NET 7, hardened in 8): how single-file distribution ships.
  • FrozenDictionary<K,V> / FrozenSet<T> (.NET 8): how init-once lookup tables ship.
  • Source generators (Roslyn 4.0+, refined): how AOT-friendly JSON serialisation ships.

.NET 6 LTS rejected (EOL November 2024). .NET 7 STS rejected (EOL May 2024). .NET Framework 4.x rejected (Windows-only, no modern C# features, 12-risks-and-alternatives §A12).

.NET 10 LTS (November 2025) is the preferred target: C# 14 with extension types (preview), BCL OrderedDictionary<K,V>, dotnet run app.cs, refined NativeAOT (smaller binaries, faster ILC), improved trim warning diagnostics. All beneficial; none required.

See 02-design-philosophy §2 and 07-dotnet-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, MEP-47 §1). Reusing it for MEP-48:

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

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

The .NET-specific addition: the async colouring pass (05-codegen-design §12). This pass runs after closure-convert and before lower; it propagates sync vs async colour through the call graph and produces a coloured aotir that the lower pass uses to choose between T Foo(...) and Task<T> FooAsync(...). The colouring pass is the largest novel transpiler component for MEP-48; the JVM target with Loom skipped this entirely.

Why System.Threading.Channels plus async/await, not Loom or actor framework

Mochi-on-.NET does not have a Loom equivalent (no implicit-blocking virtual threads). The available concurrency primitives are:

  • Task / ValueTask / Task<T> / ValueTask<T> (the async/await substrate).
  • System.Threading.Channels.Channel<T> (mailbox, back-pressure-aware).
  • IAsyncEnumerable<T> (cold stream).
  • Thread, ThreadPool (low-level, rarely used directly).

The alternatives, all rejected:

  • Orleans (Microsoft virtual actor framework): Orleans is excellent for distributed-actor scenarios but is heavyweight (silo-based, needs storage providers, depends on ASP.NET Core). Overkill for the Mochi agent model. See 12-risks-and-alternatives §A4.
  • Akka.NET (port of Akka): requires Akka licensing model decisions and adds a substantial runtime. Mochi avoids actor-framework lock-in. See 12-risks-and-alternatives §A4.
  • Proto.Actor: smaller than Akka.NET but still actor-framework lock-in; same rejection.
  • Microsoft.Dataflow (System.Threading.Tasks.Dataflow): a useful library for some workloads but its block-based API does not match Mochi agent semantics; conversion overhead.
  • TPL Dataflow (legacy): same rejection.

The "no async/await in surface" position (02-design-philosophy §13): Mochi source code is written in a synchronous style; the colouring pass decides per-function whether to emit async Task<T> (red) or T (blue), and call sites get await injected automatically. This is the largest operational delta vs MEP-47 (where Loom obviates colouring) and the largest novel pass in MEP-48.

See 09-agent-streams §1 and §6, 02-design-philosophy §13, 05-codegen-design §12.

Why BCL as fat runtime, Mochi.Runtime as thin

The .NET BCL ships ~10000 types covering HTTP (System.Net.Http), JSON (System.Text.Json), time (System.DateTimeOffset, System.TimeSpan), regex (System.Text.RegularExpressions), locale (System.Globalization), file I/O (System.IO), security (System.Security.Cryptography), concurrency (System.Threading.*, System.Threading.Channels, System.Threading.Tasks), collections (System.Collections.Generic, System.Collections.Immutable, System.Collections.Frozen), spans (System.Span<T>, System.Memory<T>), reflection, diagnostics, and more. Reusing it gives Mochi a runtime library matching or exceeding what any Mochi-specific library could provide.

The Mochi.Runtime NuGet ships only what the BCL does not: Mochi-typed higher-arity delegates, OrderedMap<K,V> for net8.0 compatibility, agent / supervisor classes, the Datalog engine, DiagnosticSource event definitions, FFI dispatch helpers, and the AI / LLM provider abstractions. Total ~5000 LOC; ~1 MB NuGet before trim; ~300 KB after.

Newtonsoft.Json / Autofac / AutoMapper rejected: too transitive, too reflection-heavy (hostile to NativeAOT), supply-chain risk. See 10-build-system §6, 02-design-philosophy §4, 12-risks-and-alternatives §A10.

Why NativeAOT 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 .NET runtime install. NativeAOT (Native Ahead-of-Time, GA .NET 7, hardened .NET 8/9/10) gives us this: ILC AOT-compiles all reachable IL into a static binary, sub-30ms cold start, no .NET runtime on $PATH required.

The trade-off: AOT means closed-world; reflection / dynamic code generation / Assembly.LoadFrom are restricted. We mitigate via source-generator-emitted [DynamicallyAccessedMembers] attributes, the Mochi codegen pass emitting trim roots for each program's actual reflection use, and the Mochi.Runtime package shipping its own trim attributes. See 07-dotnet-target-portability §5 and 10-build-system §6.

NativeAOT improvements in .NET 10 (smaller binaries, faster ILC, refined trim diagnostics) reduce the cost-to-benefit ratio of AOT relative to net8.0 .NET 8 AOT.

Why differential testing as the master gate

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

Four secondary gates: Roslyn-clean (catches transpiler-emitted C# warnings); NativeAOT-publish (catches trim warning regressions); self-contained-publish (catches RID-specific publish issues); reproducible-build (catches non-determinism). See 11-testing-gates §3-§8.

A cross-target differential gate (11-testing-gates §11) compares stdout byte-for-byte across C, BEAM, JVM, and .NET on shared fixtures, catching divergences early.

Backwards Compatibility

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

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

The Mochi language surface is unchanged. No new syntax is introduced by MEP-48. The async colouring is invisible to surface code.

Reference Implementation

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

transpiler3/dotnet/
README.md
colour/ # async sync/async colouring pass
colour.go
propagate.go
lower/ # aotir -> Roslyn SyntaxNode trees
lower.go
types.go
expr.go
stmt.go
decl.go
closure.go
match.go
query.go
agent.go
stream.go
emit/ # SyntaxNode -> CSharpCompilation -> .dll
emit.go
roslyn.go
format.go
ilemit/ # optional Reflection.Emit direct emission
hot.go
trampoline.go
build/ # build driver: fx-dep / self-cont / NativeAOT / single-file
build.go
fxdep.go
selfcontained.go
nativeaot.go
singlefile.go
runtime/ # source for Mochi.Runtime NuGet package (C#)
Mochi.Runtime/
Mochi.Runtime.csproj
Core/, Str/, Coll/, Query/, Streams/, Agents/, ...
Mochi.Analyzers/
Mochi.Analyzers.csproj
ExhaustivenessAnalyzer.cs
IntLoweringAnalyzer.cs
...
testdata/
phase01-hello/
phase02-scalars/
...

Plus shared with other backends:

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

A small mochi-dotnet-codegen binary built with the pinned Roslyn version is invoked from the Go transpiler over a JSON protocol; this isolates the Roslyn version dependency from the rest of the toolchain. See 05-codegen-design §13.

Phases

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

Phase 0. Spec freeze and skeleton trees

Deliverable: transpiler3/dotnet/{colour,lower,emit,ilemit,build,runtime,testdata} directories created, README.md written, skeleton Go files compile, dotnet SDK detected at build time. Runtime NuGet source compiles to an empty Mochi.Runtime package.

Gate: TestPhase0Skeleton: directory layout exists; build produces empty assembly; runtime NuGet packs locally.

Phase 1. Hello world

Deliverable: lowering for print("hello, world") to a C# class with Main(string[] args). fx-dependent packaging; dotnet hello.dll prints "hello, world\n".

Gate: TestPhase1Hello: 5 fixtures green on net8.0 and net10.0, 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. Roslyn compiles clean with <TreatWarningsAsErrors>true</TreatWarningsAsErrors>.

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

Phase 3. Collections

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

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

Phase 4. Records

Deliverable: lowering for type T { ... } to C# sealed record class (or readonly record struct for small value-typed records); methods on records; equality / GetHashCode auto-derived; record pattern matching (C# 9+).

Gate: TestPhase4Records: 25 fixtures green.

Phase 5. Sum types and pattern matching

Deliverable: lowering for type T = A | B | C to abstract record + sealed record variants with [MochiUnion] attribute; match to C# 8+ switch expression with record-pattern deconstruction and when guards; exhaustiveness enforced by Mochi.Analyzers.MOCHI001.

Gate: TestPhase5Sums: 25 fixtures green; analyzer-clean.

Phase 6. Closures and higher-order functions

Deliverable: lowering for fun(...) => to C# lambda + Func<...>/Action<...>; capture-by-value; mutable captures via compiler-generated closure class; higher-arity (Func17 through Func32) via runtime.

Gate: TestPhase6Funs: 25 fixtures green.

Phase 7. Query DSL

Deliverable: lowering for from ... select ... to LINQ method-syntax over IEnumerable<T>; group_by to GroupBy; join to Join / GroupJoin; order_by, take, skip direct.

Gate: TestPhase7Query: 30 fixtures green.

Phase 8. Datalog

Deliverable: lowering for fact / rule / query to runtime Engine.Register calls; semi-naive evaluator in Mochi.Runtime.Datalog.

Gate: TestPhase8Datalog: 20 fixtures green.

Phase 9. Agents and gen_server-equivalent

Deliverable: lowering for agent declarations to a C# class with Channel<TMessage> mailbox + async dispatch loop; spawn, send, call, cast. Supervision via Mochi.Runtime.Agents.Supervisor. Async colouring pass green on agent surface.

Gate: TestPhase9Agents: 25 fixtures green; TestPhase9ChannelClosure channel-shutdown gate green; TestPhase9AgentsDiagnostic event-emission gate green.

Phase 10. Streams and pubsub

Deliverable: lowering for stream<T> declarations to IAsyncEnumerable<T> (producer side) and await foreach (consumer side); subscribe, publish. Replay streams via Mochi.Runtime.Streams.

Gate: TestPhase10Streams: 20 fixtures green.

Phase 11. async/await and structured concurrency

Deliverable: async colouring pass fully integrated; spawn, await, scope. MochiScope wraps a TaskScope user-space supervisor; on .NET 11+ may use the BCL's structured-concurrency proposal if it ships.

Gate: TestPhase11Async: 15 fixtures green; deterministic-mode gate green; colouring-pass property tests green.

Phase 12. .NET FFI and NuGet deps

Deliverable: lowering for import "dotnet/..."; method dispatch via Mochi-typed signature derived from assembly metadata; @nuget(...) pragma for declaring coordinates; lockfile pinning; [LibraryImport] source-generator for P/Invoke.

Gate: TestPhase12FFI: 25 fixtures green; TestPhase12BclFFI curated-BCL-API gate green; TestPhase12NuGetRoundtrip nightly gate green.

Phase 13. LLM (generate)

Deliverable: lowering for ai(...) to Mochi.Runtime.Llm.Ai.CallAsync; provider abstractions for OpenAI, Anthropic, local (Ollama).

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

Phase 14. fetch (HTTP)

Deliverable: lowering for fetch(...) to HttpClient via Mochi.Runtime.Fetch.FetchAsync; TLS 1.3 default; HTTP/3 capable.

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

Phase 15. NativeAOT packaging

Deliverable: build driver implements --target=dotnet-aot. NativeAOT publish via dotnet publish -p:PublishAot=true; trim warning cleanliness (IL2026/IL2070/IL2080/IL3050 family); single-file binary <8MB; cold start <30ms.

Gate: TestPhase15NativeAot: 30 fixtures green as NativeAOT binaries; trim warnings zero.

Phase 16. Reproducibility

Deliverable: full reproducibility gate. Roslyn /deterministic; <Deterministic>true</Deterministic>; pinned PathMap; SourceRevisionId hash; embedded PDB; ILC --max-threads=1 for NativeAOT.

Gate: TestPhase16Reproducible: bit-identical builds across two CI hosts on a 10-fixture subset; diffoscope differential clean.

Phase 17. Self-contained packaging across RIDs

Deliverable: build driver implements --target=dotnet-self-contained across linux-x64, linux-arm64, osx-arm64, win-x64 RIDs; trim-enabled variant (<PublishTrimmed>true</PublishTrimmed>) gate.

Gate: TestPhase17SelfContained: 20 fixtures green per RID; TestPhase17SelfContainedTrimmed: same fixtures green with trim.

Phase 18. Trim cleanliness and NuGet publication

Deliverable: Mochi.Runtime and Mochi.Analyzers 0.10.x published to nuget.org with Authenticode signing. Trim warnings (full IL2xxx family) zero across the fixture matrix. Performance baselines vs vm3 published.

Gate: TestPhase18Publish nightly verifies the published package is consumable; TestPhase18TrimWarnings: zero trim warnings across all fixtures; perf dashboard updated.

Future sub-MEPs:

  • MEP-48.1: .NET MAUI mobile / desktop target (--target=dotnet-maui).
  • MEP-48.2: Blazor WebAssembly target (--target=dotnet-blazor).
  • MEP-48.3: Unity game engine target (--target=dotnet-unity), post Unity 6.8 CoreCLR migration.
  • MEP-48.4: Godot game engine target (--target=dotnet-godot).
  • MEP-48.5: Mono LTS branch support for legacy / IL2CPP fallbacks.

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.
  • MEP-47 (JVM transpiler): shares aotir and the closure-conversion / monomorphisation passes; the .NET target is parallel to the JVM target with deliberate symmetry in note structure.

External:

  • .NET 8 LTS SDK or .NET 10 LTS SDK on the developer machine (dotnet on PATH).
  • A pinned Roslyn version for the mochi-dotnet-codegen helper binary.
  • dotnet publish -p:PublishAot=true requires the OS native toolchain (clang on Linux/macOS, MSVC on Windows) for NativeAOT.

Open questions

  • Should --target=dotnet-aot be promoted to the default for mochi build once cold start beats JIT+fx-dependent for the typical fixture? (Currently the default is fx-dependent.)
  • How aggressive should the async colouring pass be? Should it duplicate functions called from both sync and async contexts (per-context monomorphisation), or escalate the whole module to async on first async use?
  • ValueTask<T> vs Task<T>: current lean is Task<T> for public surface and ValueTask<T> for hot-path internals; should the codegen surface a @hot annotation for users to opt into ValueTask?
  • Mono LTS branch support: when (and whether) to add as Phase-3 secondary, given the Unity 6.8 CoreCLR migration.
  • Source-generator-based exhaustiveness: should Mochi.Analyzers emit MOCHI001 via an incremental generator (build-time) or via a diagnostic analyzer (IDE-time)? Current lean: both; the generator catches CI, the analyzer catches Visual Studio / Rider authoring.

Security considerations

Supply chain: every NuGet dep pinned by SHA-256 in packages.lock.json. mochi audit checks against OSV. Six vendored deps (System.Text.Json, System.Threading.Channels, System.Collections.Immutable, System.Linq.Async, Microsoft.Extensions.Logging.Abstractions, YamlDotNet optional, BouncyCastle.Cryptography optional); other NuGet deps imported per @nuget(...) pragma with lockfile pin. dotnet nuget verify is part of the build.

Reflection / dynamic class loading: NativeAOT closed-world model. Trim attributes vetted; explicit allow-list of [DynamicallyAccessedMembers] roots. The Mochi codegen emits source-generator-based trim attributes; runtime System.Reflection.Emit is reserved for the agent-trampoline path and is wrapped in [RequiresDynamicCode] annotations so trim analysis correctly excludes it from AOT builds.

Sanitisers: standard CLR safety (bounds checks, null checks, type checks) by construction. No unsafe use in generated user code or runtime user-visible surface.

Cryptography: BouncyCastle is the optional crypto provider; pinned at the latest stable. TLS via BCL System.Net.Security.SslStream; we do not bundle our own.

DiagnosticSource can leak sensitive data via event payloads. The runtime documents this in [[04-runtime]] §14 and provides a Mochi.Telemetry.Redact mode that masks string fields above a configurable length.

Windows-specific: the build CLI binary and the runtime NuGet are Authenticode-signed to avoid Windows Defender / SmartScreen false positives.

References

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

  • 01-language-surface: Mochi surface and per-form lowering obligations.
  • 02-design-philosophy: design rationale for each load-bearing choice.
  • 03-prior-art-transpilers: C#, F#, VB.NET, IronPython, IronRuby, ClojureCLR, Boo, Nemerle, Oxygene, Mono / Xamarin / MAUI, Unity / IL2CPP, Godot, Blazor, Roslyn, Reflection.Emit, Lokad.ILPack, Mono.Cecil, source generators, NativeAOT, ReadyToRun.
  • 04-runtime: the Mochi.Runtime NuGet, .NET 8 baselines, vendored deps.
  • 05-codegen-design: IR layer choice (Roslyn SyntaxFactory + System.Reflection.Emit hybrid), async colouring pass.
  • 06-type-lowering: per-type marshalling, reified generics, value types, immutability.
  • 07-dotnet-target-portability: TFM matrix, OS matrix, MAUI / Blazor / Unity / Godot.
  • 08-dataset-pipeline: query DSL lowering via LINQ method-syntax, PLINQ, IAsyncEnumerable, hash-join.
  • 09-agent-streams: agents on Channels, streams on IAsyncEnumerable, supervisor, async colouring.
  • 10-build-system: build driver, MSBuild SDK, NuGet, NativeAOT, ReadyToRun.
  • 11-testing-gates: per-phase gate spec, TFM matrix, NativeAOT gate, trim-clean gate.
  • 12-risks-and-alternatives: risk register, alternatives considered and rejected.