Skip to content

Compile without reflection, and gate on a trimmed and NativeAOT run (#363, #746) - #1016

Merged
Rafael-SOWNet merged 3 commits into
masterfrom
feat/aot-trimming-gate
Aug 23, 2026
Merged

Compile without reflection, and gate on a trimmed and NativeAOT run (#363, #746)#1016
Rafael-SOWNet merged 3 commits into
masterfrom
feat/aot-trimming-gate

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

#746 item 79 asks for a trimming and NativeAOT smoke test "and fail the build if the kernel path breaks or warns"; item 57 / #363 asks for AOT-supported Linq compilation. Neither existed: there was no PublishTrimmed, IsTrimmable, IsAotCompatible or EnableTrimAnalyzer anywhere in the tree.

The premise I was given was wrong, and that is the main finding

The obvious suspect — CompilationProtocol.cs's typeof(MathAllMethods).GetMethod(name, types), a run-time lookup by string — emits no IL warning and broke neither build. typeof(X) is a known type, so ILLink and ILC answer an unknown method name by keeping every public method of the class. Removing it is still right (it forces that retention, and costs an overload resolution per node) but it was not the defect.

The real defects, in increasing order of how hard they were to see:

  1. expr.Type.GetMethod("IsNaN")IL2075, the only warning either tool produced from the whole kernel.
  2. typeof(Nullable<>).MakeGenericType(t)IL3050, NativeAOT only.
  3. and 4. Two breakages that produce no warning at all. Expression.Add and Expression.Convert find Complex.op_Addition and Complex.op_Implicit by reflecting over the operand type. ILC kept no metadata for them. Invisible at publish time; they appeared only when the binary ran.

That last pair is why "publish it" is not the acceptance criterion and "run it" is. Baseline NativeAOT run, before the fix:

FAIL compile x ^ 2.5: `x ^ (5/2)` has no compiled form: No coercion operator is defined
     between types 'System.Double' and 'System.Numerics.Complex'.
FAIL compile log(2, x) / ln(x) / x / (x + 1) ...   [6 total]
Unhandled exception. AngouriBugException: IsNaN method expected for type System.Double
   at CompilationProtocol.ConvertType(Expression, Type)          exit 134 (core dumped)

What is here

Sources/Tests/AotSmokeTest/ — 44 value assertions over parse, Simplify, Solve, Differentiate, Integrate, Limit and Compile (22 compiled expressions covering every MathAllMethods overload group, both arities, double/long/Complex/nullable returns, booleans, piecewise). Non-zero exit on any mismatch. It sets PublishTrimmed + TrimMode=full and all three warnings-as-errors switches — TreatWarningsAsErrors, ILLinkTreatWarningsAsErrors, IlcTreatWarningsAsErrors, none of which implies another.

.github/workflows/TrimmingAndAot.yml — ubuntu/windows/macos × {trimmed publish, NativeAOT publish}, and it runs both binaries.

Kernel de-reflectionCompilationProtocol.cs and MathAllMethods.tt: four member tables named at compile time through expression trees (ldtoken), replacing the string lookup, GetMethod("IsNaN"), MakeGenericType, and the operator/conversion lookups. AngouriMath.csproj sets IsAotCompatible for net8.0+.

Verified, independently of the agent that wrote it

Published and run from a clean bin/obj on this machine:

result
dotnet publish -c Release -r linux-x64 --self-contained zero IL2xxx; 78 KB executable, 28 MB total; runs → passed: 44, failed: 0, exit 0
dotnet publish -c Release -r linux-x64 -p:AngouriMathPublishAot=true zero IL2xxx/IL3xxx; 6.2 MB single native ELF (stripped, file confirms ELF 64-bit LSB pie executable); runs → passed: 44, failed: 0, exit 0

clang, lld and zlib1g-dev were installed so the AOT half is genuinely measured rather than skipped.

One csproj detail worth knowing: -p:PublishAot=true on the command line propagates to ProjectReferences and fails on the kernel's netstandard2.0 target with NETSDK1207 before the sample is looked at. The project opts in through a private AngouriMathPublishAot property instead.

Measured

Compile<Complex, Complex> is unchanged. 26 expressions printed at G17: before-JIT vs after-JIT diffs identical, and after-trimmed and after-NativeAOT are identical to both.

Suite: 7533 passed, 0 failed, 14 skipped — the baseline exactly. F# wrapper 134/0.

Benchmarks, same machine back to back:

Mean before Mean after Alloc before Alloc after
CompileEasy 220,317 ns 193,608 ns 16,377 B 11,122 B (−32%)
CompileHard 369,994 ns 325,604 ns 38,090 B 20,433 B (−46%)

The whole "after" run is ~10% faster on every row including untouched ones, so the timings are machine state and are not claimed. Allocation is the clean signal: it moves only on the two compile rows and is identical to the byte everywhere else (SimplifyEasy 128,100 → 128,099; ParseHard 3,522,409 → 3,522,409). So this is a real allocation win on compilation and a no-op elsewhere.

Also verified that AngouriMath.CPP.Exporting still publishes NativeAOT with zero IL warnings and 72 exported symbols — which matters, because marking the kernel IsTrimmable changes what ILC removes from it in that build.

Two smaller things in the same branch

  • MathAllMethods.tt did not emit the copyright header its generated file carries, so regenerating dropped it and IDE0073 (a build error here) would have failed the build. The unmodified template was regenerated and diffed first — header only — before anything else changed.
  • The two compilation benchmarks could not be invoked: one was a commented-out BenchmarkRunner.Run, the other had no arm in the switch. Both wired, both now report Allocated. That is commit 2 and is independently revertable.

For a maintainer

  1. IsAotCompatible on the kernel is a promise to consumers. A trimmer now removes code from inside AngouriMath.dll where before it kept the assembly whole. True today and gated by this workflow, but it is a deliberate commitment rather than a warning being silenced.
  2. The new workflow has not been observed firing — that needs this push. It has no paths: filter and its trigger block is byte-identical to CPPTest.yml's, which fires on every push and PR. Demonstration by identical construction plus evidence, not proof of the file itself; the run on this PR is the proof.
  3. BREAKING-CHANGES.md gains a ## Unreleased — since 2.3.0 heading, which has no precedent in the file and will conflict with any concurrent branch adding one. Print the brackets an operator that is not associative has #1009 adds the same heading.

The CPPBuild.yml path-filter fix that was found alongside this is split out as #1015, since it has nothing to do with AOT.

Rafael-SOWNet and others added 2 commits August 23, 2026 03:50
BenchLinqCompilation and CacheCompiledFunc had no way to be run: the first was a
commented-out BenchmarkRunner.Run at the bottom of Main, the second had no arm in the
switch at all. A benchmark nobody can invoke measures nothing, and these two are the
only coverage of the Linq compilation path apart from CommonFunctionsInterVersion's
CompileEasy/CompileHard.

Both now take an argument like the others, and both report Allocated as well as Mean --
#746's item 80 asks for time and allocation, and allocation is the axis on which the
compilation path actually moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
…363, #746)

The Linq compilation path named four kinds of member at run time, and each is invisible
to the trimmer and to the NativeAOT compiler:

- typeof(MathAllMethods).GetMethod(name, types) for every mathematical function. Both
  tools answer a run-time name by keeping every public method of the class, and the run
  paid an overload resolution per node.
- expr.Type.GetMethod("IsNaN") for the NaN test, which the trimmer had no reason to
  keep. IL2075, and under NativeAOT a null that reached AngouriBugException and took the
  process down on the first double -> long? conversion.
- typeof(Nullable<>).MakeGenericType(t), which NativeAOT cannot promise. IL3050.
- the operators and conversions Expression.Add and Expression.Convert find by reflecting
  over the operand or target type. Complex and BigInteger define theirs as methods, and
  NativeAOT kept no metadata for them: "x + 1" over a complex argument threw "the binary
  operator Add is not defined for the types 'System.Numerics.Complex' and
  'System.Numerics.Complex'", and "double -> Complex" threw "no coercion operator is
  defined between types".

All four are now tables of members named at compile time. MathAllMethods.Definitions is
generated from the same lists as the methods it dispatches to, so the two cannot drift
apart, and every entry is an expression tree because the compiler emits an ldtoken for
the call inside one -- which is how C# names a method without a string, and what roots it
for the trimmer.

Sources/Tests/AotSmokeTest is the gate. It parses, simplifies, solves, differentiates,
integrates, takes a limit and compiles twenty-two expressions, asserts values rather than
printed forms, and returns non-zero on any mismatch. It publishes trimmed and NativeAOT
with TrimmerSingleWarn=false and the trimmer's and ILC's own warnings-as-errors switches,
and the workflow runs the published binary: everything that goes wrong here goes wrong
silently at publish time, so publishing proves nothing and only running does.

Measured on linux-x64, .NET 10.0.10: both publishes report zero IL2xxx/IL3xxx, and the
44 assertions give byte-identical output under the JIT, the trimmed binary and the
NativeAOT binary. Before this, the NativeAOT binary failed six of them and aborted on the
seventh. The kernel is marked IsAotCompatible for net8.0 and later as a result, which is
also a promise to consumers and is recorded in BREAKING-CHANGES.md.

CompileEasy allocates 16,377 B -> 11,122 B and CompileHard 38,090 B -> 20,433 B, with
every other row of CommonFunctionsInterVersion unchanged to the byte; BenchLinqCompilation
and CacheCompiledFunc, which run compiled delegates rather than compiling them, are
unchanged within noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator Author

The caveat in the description is closed: the workflow has now been observed firing, and it passes on every leg.

TrimmingAndAot.yml ran on this PR — three operating systems × {trimmed publish, NativeAOT publish}, each one publishing and then running the binary:

Publish (ubuntu-latest,  linux-x64)   trimmed    passed: 44, failed: 0
Publish (ubuntu-latest,  linux-x64)   NativeAOT  passed: 44, failed: 0
Publish (windows-latest, win-x64)     trimmed    passed: 44, failed: 0
Publish (windows-latest, win-x64)     NativeAOT  passed: 44, failed: 0
Publish (macos-latest,   osx-arm64)   trimmed    passed: 44, failed: 0
Publish (macos-latest,   osx-arm64)   NativeAOT  passed: 44, failed: 0

All three jobs success; 28 checks on the PR against the usual 25.

Two things that were argued rather than measured before are now measured:

  • osx-arm64 NativeAOT passes. That is a different architecture from anything tested locally, and it is where a reflection-shaped defect that survives on x64 would be most likely to show. The four defects this PR fixes were all found on linux-x64; none of them recurs elsewhere.
  • Windows and macOS were listed under "not done, and why" as unmeasured. They are measured now, by the job the change adds.

The trimmed and NativeAOT publishes are both under TreatWarningsAsErrors + ILLinkTreatWarningsAsErrors + IlcTreatWarningsAsErrors, so a green leg is also a statement that neither tool emitted an IL2xxx or IL3xxx warning on any platform.

…er scopes

Three files collided, all three additively. BREAKING-CHANGES.md and
Sources/.editorconfig each had two branches appending to the same place, so
every entry from both sides survives; AGENTS.md and DotnetBenchmark/Program.cs
merged without help.
@Rafael-SOWNet
Rafael-SOWNet merged commit e4eefde into master Aug 23, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant