Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/TrimmingAndAot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: 'Trimming & NativeAOT'

# Deliberately without a paths: filter. What breaks a trimmed or NativeAOT build is a run-time
# name lookup, and one can be introduced by a change anywhere in the kernel, by a package bump,
# or by an SDK bump -- so there is no subset of the tree this gate could safely skip. Two
# workflows here have already been silenced by a filter that matched nothing
# (https://github.com/asc-community/AngouriMath/issues/775); an unconditional trigger cannot be.
on:
push:
branches:
- master
pull_request:
branches:
- '*'
workflow_dispatch:

jobs:
Publish:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
rid: linux-x64
- os: windows-latest
rid: win-x64
- os: macos-latest
rid: osx-arm64

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v6

- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.x'
dotnet-quality: 'preview'

# ILC links the runtime into the binary and needs a native linker and zlib to do it.
- name: 'Install the native toolchain for Linux'
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get update
sudo apt-get install -y clang zlib1g-dev

# The project sets PublishTrimmed, TrimmerSingleWarn=false and ILLinkTreatWarningsAsErrors,
# so any IL2xxx from the kernel fails this step. There is no flag to forget on the command
# line and no way to read a warning as noise.
- name: 'Publish trimmed'
run: dotnet publish Sources/Tests/AotSmokeTest/AotSmokeTest.csproj -c Release -r ${{ matrix.rid }} -o artifacts/trimmed

# Publishing is not the test. Everything the trimmer removed that the kernel reaches for by
# name is invisible until the binary runs, so the binary runs, and its exit code is the gate.
- name: 'Run the trimmed binary'
shell: bash
run: ./artifacts/trimmed/AotSmokeTest

- name: 'Publish NativeAOT'
run: dotnet publish Sources/Tests/AotSmokeTest/AotSmokeTest.csproj -c Release -r ${{ matrix.rid }} -p:AngouriMathPublishAot=true -o artifacts/aot

- name: 'Run the NativeAOT binary'
shell: bash
run: ./artifacts/aot/AotSmokeTest
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ are short, and a stale one is worse than none — if you change what a file desc
| [`Contributing/Transformations.md`](Sources/AngouriMath/Docs/Contributing/Transformations.md) | the transformation layer the 1.x entry points sit on, and how to add the next rule set |
| [`Contributing/SimplificationContract.md`](Sources/AngouriMath/Docs/Contributing/SimplificationContract.md) | what a rewrite may assume, and the ten obligations one has to meet. Read it *before* adding or changing a rule |
| [`Contributing/CanonicalForm.md`](Sources/AngouriMath/Docs/Contributing/CanonicalForm.md) | canonical versus simplest, why no canonical form exists for the whole language, and what one means per node class. Read it before comparing two expressions for equality |
| [`Contributing/Trimming.md`](Sources/AngouriMath/Docs/Contributing/Trimming.md) | what `IsAotCompatible` on the kernel promises, and what breaks it. Read it before adding reflection anywhere |
| [`Contributing/coding_rules.md`](Sources/AngouriMath/Docs/Contributing/coding_rules.md) | sealed-or-abstract, and immutability of `Entity` |
| [`WhatsNew/version_performance_control.md`](Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md) | the inter-version performance table, how to add a column, and when the CI performance baseline may be updated |
| `Sources/Analyzers/` | the custom analyzers, including the static-field one behind `[ConstantField]` |
Expand Down
29 changes: 29 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ read first.
| **Silent** | `"a in (b in c)".ToEntity().Stringize()`, and `Latexize` | `a in b in c` / `a \in b \in c` | `a in (b in c)` / `a \in \left(b \in c\right)` |
| **Silent** | `"x * (y mod z)".ToEntity().Stringize()`, and `Latexize` | `x * y mod z` / `x y \bmod z` | `x * (y mod z)` / `x \left(y \bmod z\right)` |
| **Silent** | `"-1 * (y mod z)".ToEntity().Stringize()` | `-y mod z` | `-(y mod z)` |
| | any expression mixing a number with a `Complex` argument, `Compile`d in a NativeAOT app — `"x + 1".Compile<Complex, Complex>("x")` | `UncompilableNodeException: ... The binary operator Add is not defined for the types 'System.Numerics.Complex' and 'System.Numerics.Complex'` | the compiled function, answering as it does under the JIT |
| | `Compile` to a nullable integral return type in a NativeAOT app | `AngouriBugException: IsNaN method expected for type System.Double`, which took the process down | the compiled function |
| **Silent** | an app publishing with `PublishTrimmed` or NativeAOT | `AngouriMath.dll` was copied in whole, being unmarked | it is trimmed with the rest, since the assembly now declares `IsTrimmable` |

### A printed operator that is not associative keeps its brackets

Expand Down Expand Up @@ -94,6 +97,32 @@ bracketing for. The change only ever adds `\left(`/`\right)` groups, which CShar
parses, so nothing downstream needs a matching change
([#822](https://github.com/asc-community/AngouriMath/issues/822)).

### `Compile` works in a trimmed or NativeAOT application

The Linq compilation path found the method for each node by name —
`typeof(MathAllMethods).GetMethod(name, ...)` for the mathematical functions,
`expr.Type.GetMethod("IsNaN")` for the NaN test — and left the operators and conversions between
`Complex`, `BigInteger` and the primitives to `Expression.Add` and `Expression.Convert`, which find
them by reflecting over the operand type. A name resolved at run time is invisible to the trimmer,
so the members were removed and the lookups came back empty. All four are now tables of members
named at compile time: `MathAllMethods.Definitions` generated beside the methods it dispatches to,
and `nanChecks`, `operators` and `conversionOperators` in `CompilationProtocol`.

Nothing changes under the JIT — the same `MethodInfo` reaches the same `Expression` node — and the
44 assertions of `Sources/Tests/AotSmokeTest` give byte-identical output under the JIT, a trimmed
publish and a NativeAOT publish. What changed is that the second and third of those now run at all:
before this, six of them threw and the seventh aborted the process.

`AngouriMath` is marked `IsAotCompatible` for `net8.0` and later as a result, which is also the
silent half of the entry above: a trimmer reads that as permission to remove unused code from
inside the assembly, so an app that trims will now get a smaller `AngouriMath` rather than all of
it. That is the point of the mark, and
[`Docs/Contributing/Trimming.md`](Sources/AngouriMath/Docs/Contributing/Trimming.md) says what
keeps it true.

[#363](https://github.com/asc-community/AngouriMath/issues/363),
[#746](https://github.com/asc-community/AngouriMath/issues/746) item 79.

---

## 2.3.0 — since 2.2.0
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed

[AngouriMath/Convenience/MathS.Polynomials.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/AotSmokeTest/*.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
7 changes: 7 additions & 0 deletions Sources/AngouriMath.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InteractiveWrapperUnitTests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetBenchmark", "Tests\DotnetBenchmark\DotnetBenchmark.csproj", "{C61DD310-AF04-4B4F-8A34-59FFC8A78F41}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AotSmokeTest", "Tests\AotSmokeTest\AotSmokeTest.csproj", "{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TerminalUnitTests", "Tests\TerminalUnitTests\TerminalUnitTests.fsproj", "{90552AEB-7563-4C06-A0B7-9F810A572E8A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Analyzers.CodeFixes", "Analyzers\Analyzers.CodeFixes\Analyzers.CodeFixes.csproj", "{9E5FEA3D-F9DE-2F38-D434-8C97DF80633F}"
Expand Down Expand Up @@ -119,6 +121,10 @@ Global
{C61DD310-AF04-4B4F-8A34-59FFC8A78F41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C61DD310-AF04-4B4F-8A34-59FFC8A78F41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C61DD310-AF04-4B4F-8A34-59FFC8A78F41}.Release|Any CPU.Build.0 = Release|Any CPU
{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1}.Release|Any CPU.Build.0 = Release|Any CPU
{90552AEB-7563-4C06-A0B7-9F810A572E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90552AEB-7563-4C06-A0B7-9F810A572E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90552AEB-7563-4C06-A0B7-9F810A572E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -183,6 +189,7 @@ Global
{3C03B5FC-D0D8-4E52-B581-743D4211F819} = {9869C55D-A753-4DE2-9C22-CCF768C6EC53}
{FDEE9428-74CB-44ED-BBD7-DAAC9CCBADCA} = {9869C55D-A753-4DE2-9C22-CCF768C6EC53}
{C61DD310-AF04-4B4F-8A34-59FFC8A78F41} = {9869C55D-A753-4DE2-9C22-CCF768C6EC53}
{4A21F0B8-9C36-4E9D-B77E-5B84C7D8F2A1} = {9869C55D-A753-4DE2-9C22-CCF768C6EC53}
{90552AEB-7563-4C06-A0B7-9F810A572E8A} = {9869C55D-A753-4DE2-9C22-CCF768C6EC53}
{9E5FEA3D-F9DE-2F38-D434-8C97DF80633F} = {358BFC2B-1A5B-4740-8BA1-0E53EFC7EDAC}
{7C7DE223-D688-E7C3-1021-C86827910720} = {358BFC2B-1A5B-4740-8BA1-0E53EFC7EDAC}
Expand Down
17 changes: 17 additions & 0 deletions Sources/AngouriMath/AngouriMath.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(MSBuildProjectName).xml</DocumentationFile>
</PropertyGroup>

<!--
The kernel is trimmable and NativeAOT-compatible, and Sources/Tests/AotSmokeTest is what says
so: it publishes an app that parses, simplifies, solves, differentiates and compiles, both
trimmed and NativeAOT, fails the build on any IL2xxx/IL3xxx warning, and then runs the binary.
This claim is an assertion to consumers (a trimmer takes it as licence to remove code inside
this assembly), so it may only be made while that gate passes.

IsAotCompatible is net8.0 and later (it implies IsTrimmable and turns on the three analyzers);
netstandard2.0 has no ahead-of-time story, and IsTrimmable there would be a claim about a
target framework where nothing checks it. Written as a compatibility test so that adding a
framework to TargetFrameworks does not silently drop it.
https://github.com/asc-community/AngouriMath/issues/363
-->
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<PropertyGroup>
<Nullable>enable</Nullable>
<NoWarn>NU5104;CA2252</NoWarn>
Expand Down
4 changes: 3 additions & 1 deletion Sources/AngouriMath/Docs/Contributing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ If you aren't sure about what to add, you may want to check the current projects
6. <a href="./CanonicalForm.md">What canonical form means here</a> — and how it differs from
"simplest". Why no canonical form exists for the whole language, what one means per node class,
and where the library measurably stands against it
7. <a href="./coding_rules.md">Coding rules</a> — sealed-or-abstract, immutability, and what may be
7. <a href="./Trimming.md">Trimming and NativeAOT</a> — what the kernel promises by being marked
`IsAotCompatible`, what checks it, and the four shapes of run-time name lookup that break it
8. <a href="./coding_rules.md">Coding rules</a> — sealed-or-abstract, immutability, and what may be
made `public`

See also <a href="../../../../BREAKING-CHANGES.md">BREAKING-CHANGES.md</a>, where a change that makes
Expand Down
64 changes: 64 additions & 0 deletions Sources/AngouriMath/Docs/Contributing/Trimming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Trimming and NativeAOT

The kernel is published with `IsAotCompatible`, which is a claim to consumers: a trimmer takes it
as licence to remove code *inside* `AngouriMath.dll`, and the NativeAOT compiler keeps no
reflection metadata for a member nothing referenced. The claim is only as good as what checks it,
and what checks it is `Sources/Tests/AotSmokeTest`.

Run it the way CI does:

```
dotnet publish Sources/Tests/AotSmokeTest/AotSmokeTest.csproj -c Release -r linux-x64 -o artifacts/trimmed
./artifacts/trimmed/AotSmokeTest

dotnet publish Sources/Tests/AotSmokeTest/AotSmokeTest.csproj -c Release -r linux-x64 -p:AngouriMathPublishAot=true -o artifacts/aot
./artifacts/aot/AotSmokeTest
```

`-r` is required for both — trimming and ILC are per-runtime-identifier. On Linux the NativeAOT
half also needs `clang` and `zlib1g-dev`; on Windows the MSVC build tools; on macOS the Xcode
command line tools.

## Two things it checks, and they are not the same

**Publishing.** The project sets `TrimmerSingleWarn=false` so a warning names a line rather than an
assembly, and `ILLinkTreatWarningsAsErrors` / `IlcTreatWarningsAsErrors` so any IL2xxx or IL3xxx
from the kernel fails the publish. `TreatWarningsAsErrors` alone does not cover either of those:
Roslyn, the trimmer and the NativeAOT compiler each have their own switch.

**Running.** The binary is then executed, and its exit code is the gate. This half is not
redundant. Everything that goes wrong here goes wrong *silently at publish time*: a member removed
because only a string named it produces no warning at all, and the failure is a `null` from
`Type.GetMethod` or a "no coercion operator is defined between types" from `Expression.Convert`
some number of calls later. Publishing proves nothing about that; running does.

The smoke test covers parse, `Simplify`, `Solve`, `Differentiate`, `Integrate`, `Limit` and
`Compile`, asserts values rather than printed forms, and returns non-zero on any mismatch.

## What breaks a trimmed build

Anything that names a member at run time.

- `Type.GetMethod("Name")`, `Activator.CreateInstance(type)`, assembly scanning. Use a table
generated beside what it dispatches to; `MathAllMethods.Definitions` is the worked example, and
its entries are expression trees precisely because the compiler emits an `ldtoken` for the call
inside one, which is how C# names a method without a string.
- `Type.MakeGenericType` over a value type, which NativeAOT cannot promise (IL3050). Where the set
of types is closed, write the closed set down: `CompilationProtocol.nullableForms`.
- Letting `System.Linq.Expressions` find an operator or a conversion for you.
`Expression.Add(l, r)` reflects over the operand type looking for `op_Addition`, and
`Expression.Convert(e, t)` reflects over the target type looking for `op_Implicit`. For `double`
and `long` those are IL instructions and there is nothing to find, but for
`System.Numerics.Complex` and `BigInteger` they are methods, and a NativeAOT build had no
metadata for them: `x + 1` compiled over a complex argument threw "the binary operator Add is not
defined for the types 'System.Numerics.Complex' and 'System.Numerics.Complex'". Pass the method
in — `CompilationProtocol.operators` and `CompilationProtocol.conversionOperators`.

`Expression.Compile()` itself is fine: under NativeAOT it falls back to the interpreter rather than
emitting IL, so a compiled expression is slower there than under the JIT but it is not broken.
See [#363](https://github.com/asc-community/AngouriMath/issues/363).

## When you add a node, a function or a protocol

Add the case to the smoke test as well. It is a few lines, it is the only place the trimmed and
NativeAOT behaviour of the new path is measured, and the publish will not tell you it is missing.
Loading
Loading