Write down what a simplification rule may assume - #886
Merged
Conversation
Four rewrites were wrong from 2020 until #885, and every harness was green while they were: casbench 117/119 with no wrong answers, propcheck 1340 checks clean, rootcheck 596/596, simpsweep agreeing on 10463 comparisons -- and Simplify answering 3 for arcsin(sin(3)), whose value is pi - 3. Sampling cannot find a rule that is wrong only at a branch cut or only off the real line, because that is where a sweep does not look. So the check has to be a reading, and a reading needs something to read against. Transformations.md already asks a rewrite what relation it claims and how well justified the claim is. Every rule set answers SoundUnderAssumptions and names no assumptions, which is honest and says nothing. This is the missing third: the assumption set, and ten obligations a rule has to meet, numbered so a review can cite them. It is built from what the library already knows rather than proposing machinery. DomainCondition is where an expression is defined; Codomain is the type of value a node takes, Any for a bare symbol; MathS.Settings.Codomain is how the expression is being read, complex unless asked; Providedf carries a condition on the value. Four different things, and the file's central point is that conflating them is how rules go wrong -- with the trap that looks most like a fix spelled out, since attaching an interval to arcsin(sin(x)) as a Providedf would claim the expression is undefined outside it when it merely has another value there. The branch-cut conventions are measured on a 2.0.0 build rather than asserted, and one of them is a trap worth publishing: for a negative base, an exact rational exponent with an odd denominator takes the real root -- (-8)^(1/3) is -2 -- while the same exponent written as a decimal takes the principal value, 1 + 1.732i. A rewrite that moves between the two changes the answer. Two inconsistencies fall out of writing it, recorded rather than fixed: ln reports its domain by the real reading (x > 0) while sqrt reports the complex one (True) under a default complex codomain, and DomainCondition cannot read the setting at all, which is #721 in a second place.
This was referenced Aug 11, 2026
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.
Adds
Docs/Contributing/SimplificationContract.md, linked from the contributing index and fromAGENTS.md. Documentation only — no code changes.Why
#885 fixed four rewrites that had been wrong since 2020 and shipped in every release. While they
were wrong, every harness was green:
casbenchpropcheckrootchecksimpsweepAnd
Simplifywas answering3forarcsin(sin(3)), whose value ispi - 3.Sampling structurally cannot find this class: a rule wrong only at a branch cut, or only off the
real line, is wrong exactly where a sweep does not look — and
simpsweepadditionally builds itsexpressions from a grammar that never nests an inverse function around its own forward function,
so the shape was outside its space. The check has to be a reading, and a reading needs
something to read against. That is what this file is.
Its acceptance is the one stated when the work was scoped: a new rule can be judged sound or
unsound by reading the contract.
What is in it
Transformations.mdalready asks a rewrite what relation it claims and how well justified theclaim is. Every rule set answers
SoundUnderAssumptionsand names no assumptions — honest, andsays nothing. This is the missing third of that vocabulary.
DomainCondition(eachnode's
IntrinsicConditionconjoined with its children's — the whole "undefined here" story inone property),
Codomain(Domain.Anyfor a bare symbol),MathS.Settings.Codomain(complexunless asked), and
Providedf, with the two costs of using it — a condition competes oncomplexity, and it travels.
neighbourhood, and the domain of definition. Conflating any two is how rules go wrong; the first
two being distinct is Unify Codomain with a "provided ... in RR" condition #721, and
lim x->0- (x^x)is the case that separates them.provided x >= -pi/2 and x <= pi/2toarcsin(sin(x))claims the expression is undefinedoutside the interval when it merely has another value there. A
Providedfnarrows the domain; anassumption narrows when the rule may fire. Opposite moves.
guard against over-guarding: if the assumption is checkable somewhere else, the rule is in the
wrong place and moving it is the fix — a^n / b^n is gathered into (a/b)^n unconditionally, which is wrong for bases of opposite sign #802 being the worked case where nothing was sacrificed.
And O9:
Simplifyselects by node count, so an unsound rewrite only has to be short to win,and a rule that appears not to fire has not been shown to be safe.
sin(arcsin(z)),|-x| = |x|),unsound and fixable by deciding the assumption (Simplify returns wrong answers: arcsin(sin(x)) -> x and three siblings are applied off the principal branch #884), unsound with the assumption belonging
elsewhere (
log_b(a^c),a^n/b^n), sound with a condition (x/x), and correctly declining(
sqrt(x^2)).The convention worth publishing on its own
(-8)^(1/3)-2— the real root(-8)^(2/3)4— the real root(-8)^(1/4)1.1892 + 1.1892i— principal, there being no real fourth root(-8)^0.33333333331.0 + 1.7320i— principalFor a negative base, an exact
Rationalexponent with an odd denominator takes the real root;write the same exponent as a decimal and it takes the principal value, and the two differ by far
more than rounding. So a rewrite that moves between an exact rational and a decimal exponent, or
that reduces
2/6to1/3, changes which convention applies. Whether the convention itself isright is #204 and deliberately a major-version question; that a rule may not silently cross it is
not.
Two inconsistencies it exposes, recorded not fixed
lnandsqrtdisagree about which reading their domain describes. Under the defaultDomain.Complex,DomainConditionofln(x)isx > 0— the real reading, since a complexlogarithm is defined for every
x != 0— whilesqrt(x)reportsTrue, the complex one.arcsin(x)also reportsTrue, though over the reals it needs|x| <= 1.DomainConditionnever readsMathS.Settings.Codomain. It is fixed at construction, so itcannot say "defined here, given that we are doing real analysis". Unify Codomain with a "provided ... in RR" condition #721 in a second place.
I have not filed these as issues yet — they may be one issue or two depending on whether the
intended reading is per-node or per-setting, and that is a maintainer's call.
Also in this branch's workspace, not in this PR
work/probegainsdomain::andcodomain::operations, which is how the tables above weremeasured, and the harness notes lose a limitation that is now closed: with
2.0.0on nuget.org Ichecked the quickstart against the published package rather than a project reference — a fresh
dotnet new console,dotnet add package AngouriMath --version 2.0.0, and its own program printedx + sin(y * x)and1 + cos(y * x) * y; the same for F# withAngouriMath.FSharp.What it does not settle
Per-symbol assumptions (SymPy's
Symbol('x', positive=True)withrefine/ask, which most of theassumption sets here would be dischargeable by), whether
Providedfshould express aneighbourhood (#721), the fractional-power convention (#204), and the
Soundnesstiers, whichremain declared rather than checked — nothing here promotes a rule set to
Sound.The harness that would check this class is specified in §8 and does not exist. It has to be
built from the rules rather than from a grammar: for each rewrite, construct the set where its
assumption fails — poles named by
DomainCondition, either side of each cut, one period outside aprincipal interval, purely imaginary arguments, negative bases at odd and even denominators — and
compare the two sides there, counting both undefined as agreement. That would have found all four
rules of #884 immediately.