Skip to content

Register every rule set the simplifier applies, and route it through them - #818

Merged
Rafael-SOWNet merged 6 commits into
masterfrom
feat/transformation-layer-rule-registry
Aug 8, 2026
Merged

Register every rule set the simplifier applies, and route it through them#818
Rafael-SOWNet merged 6 commits into
masterfrom
feat/transformation-layer-rule-registry

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Stacked on #816 — merge that first. Prerequisite for #28.

Why this comes before #28, not after

The registry held ten rule sets. Simplificator applies twenty-two, reaching most of them through Patterns directly. A set reachable only by its method has no name to report and nothing to attribute a step to, so a step recorder wired to the registry today would produce a derivation of Simplify that silently omitted most of what actually fired. That is a plausible-looking answer that is not the truth, which this repo ranks below no answer at all — so the sweep has to land first.

What changed

All twenty-two are registered, and Simplificator reaches all of them through RewriteRules. Entity.Expand is routed too, so it is not the one catalogue transformation still naming its own rules. Thirty entries in total.

Two sets are parameterised by sort level. Each is registered once per level with an internal chooser beside it (CanonicalOrderAt, CommonDenominatorAt) — the alternative is a registry entry that cannot be enumerated, which defeats the point of All.

expression.Rewrite(ruleSet) is an internal extension over RewriteRuleSet.ApplyOnce, so a sequence of a dozen rule sets still reads in the order it runs instead of inside out.

This is a rename at every call site. ApplyOnce is Replace(rules); nothing about the order, the guards or the candidate selection moved.

Measurements

Against d1c4a4dd (the parent), net10.0/Release, two samples each. Allocation is deterministic and reproduced exactly across both runs:

parent this branch
SimplifyEasy 157.6 KB 125.8 KB (−20%)
SimplifyCommon 6971.1 KB 6867.8 KB
Limit 7658.0 KB 7516.4 KB
Integrate 1774.5 KB 1759.5 KB
ParseEasy / Differentiate / Expand / Factorize / SolveEasy unchanged unchanged

Not noise, and it has a cause: Patterns.SortRules(level) returns a fresh closure on every call, and so does the common-denominator lambda. Both ran once per pass inside the simplification loop; they are now cached registry entries. Timings are unchanged within run-to-run variance.

Tests

5773 passing, 0 failed, 15 skipped (up from 5707/14 — the extra skip is #817's). F# 130 passing.

The tests over RewriteRules.All now cover thirty sets rather than ten, and the corpus gained boolean, comparison, set, factorial and totient expressions so those sets are actually exercised rather than passed over on inputs they cannot match. Every registered set still reaches a fixed point within 32 passes, and every one is deterministic.

Two things the widened corpus turned up

A pre-existing crash, filed as #817. "(x + 1)! / x!".Expand() throws AngouriBugException — a public method reporting that the library is broken, on an expression Simplify answers as 1 + x. It reproduces on d1c4a4dd, where Expand's logic is untouched by either PR, so it is not from this work. A skipped regression test is included here referencing the issue; the fix belongs in its own change.

The equivalence property test was wrong twice over. Both are fixed, and both are worth naming because each would have produced a confident false report:

  • Sets subtract elementwise, so the difference of two equal sets is the set of pairwise differences, not zero. { 1, 2 } unite { 2, 3 } duly "failed". Set-valued results are now compared by simplifying both sides.
  • Simplifying a difference to zero proves the two agree; failing to does not prove they differ. x! * (x + 1) expands to (x + 1)! — correct — and the difference does not reduce. The test now looks for an actual counterexample at sample points before calling anything a defect, with a relative tolerance and with points where either side is undefined or infinite passed over rather than counted against a rewrite that is only valid away from a pole.

The counterexample search has its own test, because a property check that silently catches nothing is worse than none: it must separate x + 1 from x + 2, x ^ 2 from x ^ 3 and sqrt(x) from -sqrt(x), and must not separate x * 2 from 2 * x, x! * (x + 1) from (x + 1)!, or sqrt(12) + sqrt(27) from 5 * sqrt(3).

What still bypasses the registry

The equation and set solvers, the integrator and TreeAnalyzer still call Patterns directly. That is outside "what the simplifier did" and can follow separately. Patterns.TrigonometricToExponentialRules(from, to) cannot become an entry as it stands — parameterised by two variables, it is a family of sets rather than one.

🤖 Generated with Claude Code

Rafael-SOWNet and others added 4 commits August 8, 2026 12:45
…n it

The top-level operations are procedures you invoke: `Simplify`, `Expand`,
`Factorize`, `Differentiate`, `Integrate`, `Limit`. There is nowhere to say what
one of them claims about its output, how well justified the claim is, or that it
could not settle the question -- and nowhere to compose two of them, because a
`Func<Entity, Entity>` carries none of that. That is the first layer #746 asks
for, and this adds the smallest version of it that has real consumers.

`AngouriMath.Core.Transformations` is:

  Transformation        Name, Relation, Soundness, Apply -- plus Then, Repeat and
                        UntilStable, all bounded by the caller
  TransformationResult  input, output-or-nothing, which transformation ran; a
                        struct, so routing an ordinary call allocates nothing
  RewriteRuleSet        a named, attributed group of rewrites
  RewriteRules          the registry: ten shipped sets, explicitly listed,
                        enumerable in a fixed order

Relation is `Equivalence` or `Derivation`, because "sound" is only a statement
about some relation and a derivative is not another way of writing its
integrand. Soundness is declared, not checked, so every shipped rule set is
`SoundUnderAssumptions` and a test over `RewriteRules.All` holds it there. No
answer is `null`, the same distinction AGENTS.md draws between an unevaluated
node and NaN -- which is where this layer is more honest than the method it
backs: `Transformation.Integration` has no answer for `e^(x^2)` where
`Entity.Integrate` returns an unevaluated `Integralf`.

Two real ports, not wrappers. `Factorize` no longer names its own rules: it is
PerfectSquare, then Factorization, then a tidying pass, repeated `level` times,
composed out of the registry. `SimplifyChildren` -- run by every stage of the
simplification pipeline -- is a chain built once, statically, from four registry
entries. The other five entry points are thin adapters over the algorithm that
was already there; nothing that worked was rewritten.

`Solve` is deliberately absent. It consumes a goal and produces a solution set,
and belongs in a tactic layer that does not exist yet; `Entity.Set` being an
`Entity` means it would type-check here, which is the reason to keep it out. No
inverse machinery either -- Expand and Factor are not inverses.

Registration is static and explicit, so the layer stays trimmable and
NativeAOT-publishable, and `RewriteRules.All` is in an order that does not depend
on hashing or type-load order. `RewriteRuleSet` builds its transformation on
demand: doing it eagerly makes the registry and the catalogue depend on each
other's static initialisation, and whichever is touched second reads null fields.

Measured on net7.0/Release against master 21f0d16, two samples each, same
expressions as DotnetBenchmark/CommonFunctionsInterVersion: every timing range
overlaps the baseline's, and allocation -- which is deterministic -- is identical
or lower (SimplifyEasy 158.6 -> 157.6 KB, SimplifyCommon 6976.8 -> 6973.6 KB,
SimplifyHard 3616232 -> 3616197 KB). `Patterns.SortRules` used to build a fresh
closure on every `SimplifyChildren` call and is now built once, which is where
the difference comes from.

Tests: 5551 -> 5699 passing, 0 failed, the same 14 skipped; F# 130 passing. The
149 new tests cover the abstraction, that each 1.x method answers what its
transformation answers, determinism, that an equivalence transformation does not
change the value of the expression, that unsupported cases stay honest, and that
no registered rule set rewrites in a cycle.

No behavioural change, so no BREAKING-CHANGES.md entry. The new surface is
additive and marked experimental in its own documentation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…them

The registry held ten sets; `Simplificator` applied twenty-two, reaching most of
them through `Patterns` directly. A set reachable only by its method has no name
to report and nothing to attribute a step to, so any account of what `Simplify`
did to an expression would have quietly omitted most of what actually fired --
which is the wrong kind of answer to give, and the reason this comes before #28
rather than after it.

All twenty-two are registered now, and `Simplificator` reaches all of them
through `RewriteRules`. Two are parameterised by sort level, so each is
registered once per level with an internal chooser beside it
(`CanonicalOrderAt`, `CommonDenominatorAt`) -- the alternative is an entry that
cannot be enumerated, which defeats the list. `Entity.Expand` is routed too, so
that it is not the one catalogue transformation still naming its own rules.
Thirty entries in total; `Rewrite(ruleSet)` is an internal extension so a
sequence of them still reads in the order it runs.

This is a rename at every call site: `ApplyOnce` *is* `Replace(rules)`. Nothing
about the order, the guards or the candidate selection moved.

Measured against d1c4a4d on net10.0/Release, two samples each. Allocation is
deterministic and reproduces exactly:

  SimplifyEasy     157.6 -> 125.8 KB   (-20%)
  SimplifyCommon  6971.1 -> 6867.8 KB
  Limit           7658.0 -> 7516.4 KB
  Integrate       1774.5 -> 1759.5 KB

`Patterns.SortRules(level)` builds a fresh closure on every call and so does the
common-denominator lambda; both ran once per pass inside the simplification
loop and are now cached registry entries. Timings are unchanged within noise.

The tests over `RewriteRules.All` now cover thirty sets rather than ten, and the
corpus gained boolean, comparison, set, factorial and totient expressions so
that those sets are exercised rather than passed over. Every registered set
still reaches a fixed point within 32 passes and is deterministic.

Two things the widened corpus turned up:

`Expand` throws `AngouriBugException` on `(x + 1)! / x!` -- a public method
crashing on an ordinary expression that `Simplify` answers as `1 + x`. It
reproduces on d1c4a4d, where `Expand`'s logic is untouched, so it is
pre-existing: filed as #817, with a skipped regression test.

The equivalence property test was wrong twice over, and both are fixed. Sets
subtract elementwise, so the difference of two equal sets is the set of pairwise
differences rather than zero; set-valued results are now compared by simplifying
both sides. And simplifying a difference to zero proves agreement while failing
to do so proves nothing -- `x! * (x + 1)` expands to `(x + 1)!`, correctly, and
the difference does not reduce. The test now looks for an actual counterexample
at sample points before reporting a defect, with a relative tolerance, and has
its own test: it must separate `sqrt(x)` from `-sqrt(x)` and must not separate
`x! * (x + 1)` from `(x + 1)!`.

Tests: 5773 passing, 0 failed, 15 skipped (the new one is #817's); F# 130.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e-set sweep

# Conflicts:
#	Sources/AngouriMath/Core/Transformations/RewriteRules.cs
#	Sources/AngouriMath/Docs/Contributing/Transformations.md
#	Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs
@Rafael-SOWNet
Rafael-SOWNet changed the base branch from feat/transformation-layer-core to master August 8, 2026 15:38
FactorizationAtLevel was swept across levels and the other two were compared
against the legacy API at the default only -- three tests of the same shape,
written three different ways, which is the inconsistency AGENTS.md calls a bug in
its own right.

All three are swept now, including negative levels, which Simplify passes itself
when it re-simplifies a candidate, and levels outside the -4..4 range the
catalogue keeps built: those are constructed on the spot, and the test says they
behave the same rather than merely not throwing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit ddf2c7f into master Aug 8, 2026
25 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