Skip to content

Conditional compilation for feature subsets: use trait-based test selection and package seams instead #783

Description

@Rafael-SOWNet

A recurring suggestion is to guard subsets of the library with conditional
compilation symbols, so that CI can test less and consumers can omit parts they
do not need. This issue argues against #if as that mechanism, and proposes the
two things that do address the underlying goals.

I could not find an existing issue for this. The nearest ones are
#539 (trimming, but only as an obstacle to a
single-file AngouriMath.Terminal),
#363 (AOT-supported Linq compilation) and
#746, whose modularity section commits to
separate packages plus trimming/AOT-safety and never to #if.

Measurements

Taken on master at the time of writing:

Sources/AngouriMath 41,767 LOC, single assembly, net7.0;netstandard2.0
Conditional compilation present today TFM-based only (Compile Remove="Core/Entity/GenericMath/**" below net7.0)
UnitTests 5,387 tests, 3m52s, Release
[Trait] attributes in UnitTests 0
Reflection call sites in the library 1 — Assembly.GetExecutingAssembly().GetName().Version in Core/Exceptions/SysExceptions.cs

Why #if does not serve either goal

Test time. Conditional compilation does not select tests, it selects code.
If a symbol removes part of the library, the test project must compile in a
matching configuration, so CI builds and runs N configurations instead of one.
Wall-clock goes up. What selects tests is dotnet test --filter, and it has
nothing to filter on today because the suite carries no traits.

Consumer footprint. NuGet ships one compiled binary. A consumer restoring
AngouriMath cannot set our compilation symbols; only someone building from
source can. Making subsets reachable from NuGet means publishing N packages —
at which point the boundary is an assembly boundary and the #if is redundant.

Correctness, which is the specific objection for a CAS. Our pipelines are
order-dependent: limit rules must be appended at a particular point in
LimitSolvers, and solver ordering has already produced timeouts where a
correct rule was starved by a search in front of it. Removing a rule from a
chain therefore changes the answers to queries that were not about that rule. A
subset build is not "the same library minus a feature"; it is a different
simplifier, and it would have to be tested as a product in its own right to be
documented honestly. With N symbols that is 2^N configurations against a CI that
builds one, so unbuilt configurations rot silently. That is the failure mode to
avoid, and it arrives gradually — each individually reasonable #if added
alongside a feature is what accumulates it.

Proposed instead

1. Trait-based test selection. Add [Trait("Area", "...")] as files are
touched, so dotnet test --filter can run a subset locally and CI can shard.
No runtime cost, no build matrix, no behaviour change. This is the part that is
safe to trickle in per-PR.

2. Keep the seams package-ready, per feature. New domains behind an
interface with no core references by name; no static cross-area registries or
assembly scanning (#746 identifies these as what breaks trimming and NativeAOT);
internal unless the public API requires otherwise. Done consistently, splitting
a domain such as number theory or statistics into its own package later is a
repackaging rather than a rewrite.

3. Land the trimming/AOT smoke test (roadmap item 79 of #746): publish a
small sample app with PublishTrimmed, so regressions surface on the PR that
causes them. Worth noting that the library is closer to trim-ready than #363
suggests — the reflection surface is a single call site for the assembly
version. The remaining obstacles are System.Linq.Expressions in Compile,
which is an AOT problem rather than a trimming one, and the 4,863-line generated
ANTLR parser, which is retained for as long as MathS.FromString is reachable.

The one place #if stays appropriate is where it already is: target-framework
shims.

Opinions welcome, particularly from anyone who has a consumer scenario where
per-feature source-level exclusion would actually be usable.

Activity

  1. Rafael-SOWNet commented on Sep 5, 2026

    @Rafael-SOWNet
    MemberAuthor

    Adopted. I would close this. Both alternatives this proposed have landed, and the argument against
    #if for feature subsets should be treated as settled.

    • Trait-based test selection: [Trait("Area", …)] went from 0 to 230 of 252 test files, 12
      area values, over 3,010 Fact/Theory.
    • The trimming/AOT gate: TrimmingAndAot.yml publishes and runs AotSmokeTest on three
      operating systems; Trimming.md documents it; it passes on master today.
    • The reflection surface is now zero. The only GetMethod / Activator.CreateInstance /
      MakeGenericType occurrences left in the kernel are inside comments explaining why the table is
      written out instead.

    Two riders, both measured, and both are follow-ups rather than objections:

    1. The traits are not used by anything. No workflow passes --filter; CSharpTest runs the whole
    suite across an OS matrix. Nothing enforces the attribute either, so the 22 files without one will
    become more. Either shard CI by area or say plainly that the benefit is local-only — at the moment the
    mechanism is in place and unexercised.

    2. The TFM allowance now carries a public capability, unmeasured. Serialization is
    #if NET8_0_OR_GREATER and GenericMath is Compile Removed below net7. UnitTests targets
    net10.0 only, so PublicApi.txt records one leg and the netstandard2.0 public surface is checked
    by nothing
    . That is this issue's own "unbuilt configurations rot silently", arriving through the one
    construct it permits. A per-TFM public-surface check closes it, and it is the more valuable of the two.

    Neither of those is a reason to keep this open — they are new, smaller issues, and I would rather file
    them than leave a settled proposal open to carry them.

  2. Rafael-SOWNet commented on Sep 5, 2026

    @Rafael-SOWNet
    MemberAuthor

    Closing as adopted. Both alternatives this proposed have landed: trait-based selection is on 230 of 252 test files over 3,010 facts, and the trimming/AOT gate runs AotSmokeTest on three operating systems. Kernel reflection is zero.

    The two riders in the comment above are follow-ups rather than unfinished parts of this, and I am filing them separately: nothing consumes the traits yet, and the netstandard2.0 public surface is checked by nothing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Opinions wantedWe are interested in your opinion about the topic

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions