Register every rule set the simplifier applies, and route it through them - #818
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #816 — merge that first. Prerequisite for #28.
Why this comes before #28, not after
The registry held ten rule sets.
Simplificatorapplies twenty-two, reaching most of them throughPatternsdirectly. 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 ofSimplifythat 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
Simplificatorreaches all of them throughRewriteRules.Entity.Expandis 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
internalchooser beside it (CanonicalOrderAt,CommonDenominatorAt) — the alternative is a registry entry that cannot be enumerated, which defeats the point ofAll.expression.Rewrite(ruleSet)is an internal extension overRewriteRuleSet.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.
ApplyOnceisReplace(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: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.Allnow 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()throwsAngouriBugException— a public method reporting that the library is broken, on an expressionSimplifyanswers as1 + x. It reproduces ond1c4a4dd, whereExpand'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:
{ 1, 2 } unite { 2, 3 }duly "failed". Set-valued results are now compared by simplifying both sides.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 + 1fromx + 2,x ^ 2fromx ^ 3andsqrt(x)from-sqrt(x), and must not separatex * 2from2 * x,x! * (x + 1)from(x + 1)!, orsqrt(12) + sqrt(27)from5 * sqrt(3).What still bypasses the registry
The equation and set solvers, the integrator and
TreeAnalyzerstill callPatternsdirectly. 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