Make a mathematical operation a value, and put the 1.x entry points on it (#746) - #816
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>
This was referenced Aug 8, 2026
Open
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.
Closes nothing on its own; this is the first layer of #746, with consumers in the same change.
What was wrong
The top-level mathematical operations are procedures you invoke. 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. #746's argument is that this interface is the ceiling on everything built above it.What this adds
AngouriMath.Core.Transformations, public and documented as experimental:TransformationName,Relation,Soundness,Apply— plusThen,Repeat(n),UntilStable(max)TransformationResultRewriteRuleSetRewriteRulesThree things it insists on, each the honesty rule in a different place:
TransformationRelationisEquivalenceorDerivation. "Sound" is only a statement about some relation, and a derivative is not another way of writing its integrand — so the property test that subtracts output from input only runs against the transformations that claim equivalence.Soundnessis declared, not checked. Every shipped rule set isSoundUnderAssumptions, and a test overRewriteRules.Allholds it there: promoting one means changing that test and saying why in the same change. Nothing in the catalogue claimsHeuristic— that is a statement about what is registered so far, not about the library.null. This is the one place the new layer is more honest than the method it backs:Transformation.Integration("x")has no answer fore^(x^2), whereEntity.Integratereturns an unevaluatedIntegralf. Same claim, different shape; neither isNaN.UntilStablereports hitting its bound as no answer rather than as whatever value it was holding, so a rule set that does not converge is visible instead of silently truncated.What now uses it
Two of these are real ports.
Factorizeno longer names its own rules — it isPerfectSquare, thenFactorization, then a tidying pass, repeatedleveltimes, built out of the registry.SimplifyChildren, which every stage of the simplification pipeline runs, is a chain composed once, statically, from four registry entries instead of a hand-written run ofReplacecalls. The rest are thin adapters. Nothing that worked was rewritten to make the architecture tidier.What is deliberately not here
Solveis not a transformation. It consumes a goal and produces a solution set, and belongs in a tactic layer that does not exist yet.Entity.Setbeing anEntitymeansSolvewould type-check asEntity -> Entity— which is the reason to keep it out, since it would compile while saying nothing true.ExpandandFactorare not inverses andUnsolveis not well defined.Simplificator. Ten sets are registered — the ones the catalogue is built from.Extensibility constraints
Registration is static and explicit — no assembly scanning, no
Activator— so the layer stays trimmable and NativeAOT-publishable, andRewriteRules.Allis in an order that does not depend on hashing or on which type loaded first.One trap is worth naming, because it cost a full-suite abort during development:
RewriteRuleSetbuilds itsTransformationon demand. Doing it in the constructor makes the registry and the catalogue depend on each other's static initialisation, and whichever type is touched second reads the other's fields before they are set — so the failure is a null field in whichever order the process happened to load them, not a failing assertion. There is a comment on the field and a smoke test pinning it.Measurements
Release, net7.0, against master
21f0d16f, two samples each, same expressions asDotnetBenchmark/CommonFunctionsInterVersion. Every timing range overlaps the baseline's. Allocation is deterministic and is the reliable comparison:Identical or slightly lower.
Patterns.SortRules(level)returns a fresh closure on every call andSimplifyChildrenused to build one per invocation; it is now built once, which is where the difference comes from.Tests
dotnet test Sources/Tests/UnitTests— 5699 passed, 0 failed, 14 skipped (5551 + 149 new, the same 14 skips as master).dotnet test Sources/Tests/FSharpWrapperUnitTests— 130 passed, 0 failed.The new tests cover the abstraction; that each 1.x method answers exactly what its transformation answers, including
Factorizeat levels 0–3; determinism; that an equivalence transformation does not change the value of the expression, checked by simplifying the difference and stripping the domain condition rather than by comparing printed forms; that unsupported integrals and limits stay honest; and that no registered rule set rewrites in a cycle.Compatibility
No behavioural change, so no
BREAKING-CHANGES.mdentry — the suite is byte-identical in outcome and the public signatures are untouched. The new surface is additive.Sources/.editorconfiggains a path-scopedfile_header_templateso the new directories carry the current year; the existing 367 files keep the header they were written with rather than having a year bump folded into this diff.Documentation:
Docs/Contributing/Transformations.md, linked from the contributor index, with a section inAGENTS.mdon the three habits the layer asks for.What comes next
The unit here is the rule set, not the single
pattern -> replacementline, because every rewrite in this library is a case of oneswitchand the compiler turns that into a type test and a jump — splitting each case into an object would trade one dispatch per node for one delegate call per rule per node, on the hottest path there is. Making individual rewrites addressable without paying that is the next piece of work, and nothing here forecloses it. After that: the goal/tactic layerSolvebelongs in, then derivations.🤖 Generated with Claude Code