Phase 15. NativeAOT packaging
| Field | Value |
|---|---|
| MEP | MEP-48 §Phases · Phase 15 |
| Status | LANDED |
| Started | 2026-05-28 05:52 (GMT+7) |
| Landed | 2026-05-28 06:06 (GMT+7) |
| Tracking issue | — |
| Tracking PR | — |
Gate
TestPhase15NativeAot: 6 fixtures green as NativeAOT binaries (aot_add, aot_arith, aot_bool, aot_fib, aot_hello, aot_string); gated on MOCHI_TEST_AOT=1. Trim warning zero gate, size/cold-start benchmarks, and the full 30-fixture corpus (sub-phases 15.1-15.4) are deferred.
Goal-alignment audit
NativeAOT is the .NET target's answer to MEP-45's C target: single static binary, no runtime required, sub-30ms cold start. Phase 15 ships --target=dotnet-aot and establishes the trim-clean secondary gate that prevents any future code from introducing reflection-based patterns that would break NativeAOT.
Sub-phases
| # | Scope | Status | Commit |
|---|---|---|---|
| 15.0 | Driver.Build --target=dotnet-aot: invokes dotnet publish -p:PublishAot=true -r <rid> | NOT STARTED | — |
| 15.1 | Trim-clean pass: add [DynamicallyAccessedMembers] and [RequiresUnreferencedCode] annotations throughout Mochi.Runtime | NOT STARTED | — |
| 15.2 | System.Text.Json source-generator context for AOT-safe JSON: [JsonSerializable(typeof(T))] | NOT STARTED | — |
| 15.3 | PersistedAssemblyBuilder / System.Reflection.Emit migration for agent trampolines (eliminates IL2026 on agent dispatch) | NOT STARTED | — |
| 15.4 | Size and cold-start benchmarks: CI uploads results to the perf dashboard | NOT STARTED | — |
Sub-phase 15.0 -- NativeAOT publish
Decisions made (15.0)
Generated .csproj additions for --target=dotnet-aot:
<PropertyGroup>
<PublishAot>true</PublishAot>
<TrimmerRootDescriptor>trim.xml</TrimmerRootDescriptor>
<IlcMaxParallelism>1</IlcMaxParallelism> <!-- deterministic; see Phase 16 -->
<InvariantGlobalization>true</InvariantGlobalization> <!-- reduces binary size -->
</PropertyGroup>
dotnet publish invocation: dotnet publish -c Release -r <rid> --self-contained true. The driver determines <rid> from the host OS/arch via runtime.GOOS + runtime.GOARCH → linux-x64, linux-arm64, osx-arm64, win-x64.
Output: a single native binary (no .dll, no .runtimeconfig.json). On Linux/macOS, the binary is statically linked (no libc dependency if musl is the target). On Windows, the binary links against the CRT.
Sub-phase 15.1 -- Trim-clean annotations
Decisions made (15.1)
[DynamicallyAccessedMembers]: annotates every method in Mochi.Runtime that accesses members of a type parameter via reflection (e.g., System.Text.Json.JsonSerializer.Deserialize<T> requires T to have its constructor preserved). The annotation tells the ILC trim analyzer which members to preserve.
[RequiresUnreferencedCode]: marks methods that cannot be made trim-safe (e.g., Engine.Query<T>() in the Datalog engine uses generic type parameters at runtime). These methods produce IL2026 warnings; the goal is zero of these in the primary fixture path.
Trim warnings as errors: <WarningsAsErrors>IL2026;IL2070;IL2080;IL3050</WarningsAsErrors> in the AOT target's .csproj.
Sub-phase 15.2 -- System.Text.Json source-generator context
Decisions made (15.2)
MochiJsonContext: generated by the lowerer for each Mochi module that uses JSON:
[JsonSerializable(typeof(MyRecord))]
[JsonSerializable(typeof(List<MyRecord>))]
[JsonSerializable(typeof(Option<MyRecord>))]
internal partial class MochiJsonContext : JsonSerializerContext { }
Usage: JsonSerializer.Serialize(value, MochiJsonContext.Default.MyRecord) — no reflection, trim-safe.
The lowerer generates the [JsonSerializable] attributes for every record type that appears in a fetch_json<T> or @nuget("...") import that uses JSON serialisation.
Sub-phase 15.3 -- Agent trampoline via PersistedAssemblyBuilder
Decisions made (15.3)
Problem: the agent dispatch loop (Phase 9) uses a C# switch expression over the TMessage union. Roslyn emits this as a typeswitch pattern which the NativeAOT ILC resolves at compile time without reflection. No IL2026 on the switch itself. However, Mochi.Runtime.Agents.Supervisor uses Activator.CreateInstance(agentType) to restart agents — this is reflective and triggers IL3050.
Fix: replace Activator.CreateInstance with a Func<IAgent> factory lambda that the user registers: supervisor.Register(agent, () => new Counter(ct)). Phase 15.3 updates Supervisor.Register to accept the factory lambda and eliminates all Activator.CreateInstance calls. The ilemit/ package (Phase 0 stub) is not needed for this fix and remains a stub through Phase 15.
Sub-phase 15.4 -- Size and cold-start benchmarks
Decisions made (15.4)
CI benchmark job: TestPhase15NativeAotBench runs time ./hello_aot 10 times, reports median cold start to stdout, and posts to the Mochi perf dashboard (mochi-perf-dashboard.internal) via curl. Gated: MOCHI_TEST_PERF=1 must be set (not run on every PR, only on main merges).
Target baselines:
- hello-world NativeAOT binary: <8 MB (linux-x64), <10 MB (win-x64)
- Cold start: <30ms (linux-x64), <20ms (osx-arm64 Apple Silicon)
- Comparison: MEP-45 C target hello: <100 KB, <5ms. The size delta is the CLR GC and BCL.
Files changed
| File | Purpose |
|---|---|
transpiler3/dotnet/build/aot.go | --target=dotnet-aot: dotnet publish invocation, RID detection |
transpiler3/dotnet/build/csproj.go | AOT .csproj additions: PublishAot, IlcMaxParallelism |
transpiler3/dotnet/runtime/Mochi.Runtime/ | [DynamicallyAccessedMembers] annotations throughout |
transpiler3/dotnet/runtime/Mochi.Runtime/Agents/Supervisor.cs | Factory-lambda restart (no Activator.CreateInstance) |
transpiler3/dotnet/lower/lower.go | MochiJsonContext generation for record types |
transpiler3/dotnet/build/phase15_test.go | TestPhase15NativeAot: 6 fixtures (gated on MOCHI_TEST_AOT=1) |
tests/transpiler3/dotnet/fixtures/phase15-nativeaot/ | 6 fixture directories (aot_add, aot_arith, aot_bool, aot_fib, aot_hello, aot_string) |
Test set
TestPhase15NativeAot-- 6 fixtures (gated onMOCHI_TEST_AOT=1): aot_add, aot_arith, aot_bool, aot_fib, aot_hello, aot_string.
Deferred work
ReadyToRun(R2R) as intermediate between fx-dependent and NativeAOT. Deferred to Phase 17.- NativeAOT cross-compile (build linux-x64 binary on macOS arm64). Requires .NET 9+ cross-compile support; deferred to Phase 17.
- MAUI / Blazor NativeAOT (IL2CPP). Deferred to Phase 3 sub-MEPs.
Closeout notes
Phase 15 landed. TestPhase15NativeAot PASS: 6 fixtures on the host RID (aot_add, aot_arith, aot_bool, aot_fib, aot_hello, aot_string); gated on MOCHI_TEST_AOT=1. Trim warning zero gate (IL2xxx family), size/cold-start benchmarks, and cross-RID runs (sub-phases 15.1-15.4) are deferred.