Skip to content

State what canonical form means here, and measure how far off the library is (#746) - #928

Merged
Rafael-SOWNet merged 5 commits into
masterfrom
docs/canonical-form
Aug 14, 2026
Merged

State what canonical form means here, and measure how far off the library is (#746)#928
Rafael-SOWNet merged 5 commits into
masterfrom
docs/canonical-form

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Part of #746 tier 1, which asks for "canonical forms with a written specification of what canonical means for each node class, and a stated distinction between canonical and simplest". Its item 65 asks whoever does it to take a position, write it down, and let the engine be checked against it.

This is the document. The checking is a new harness (canoncheck) in the analysis workspace, and every number below came out of it rather than out of an argument.

The position

Canonical is about identity. Simplest is about presentation. A canonical form exists so that equality becomes a structural comparison; a simplest form is the best-rated member of an equivalence class under a cost metric the caller chooses, so it is only defined relative to one. Conflating them is how a library ends up with neither.

A complete canonical form does not exist, and that is a theorem rather than a gap

Zero-equivalence is undecidable for the class AngouriMath accepts — rationals, pi, exp, the trigonometric functions, abs, and composition (Richardson 1968) — and a canonical form would decide it by canonicalising and comparing against 0.

So the specification is deliberately three things rather than one: a canonical form on a decidable sublanguage with the boundary written down, a normalisation everywhere else that must not be relied on as though it were canonical, and a search that is not required to be canonical at all. That is Moses' 1971 three-way split, and taking the position costs nothing except writing it down.

Where the library measurably stands

Three properties, each of which a canonical form must have and none of which needs an oracle — idempotence, order independence over commutative operators, and agreement between writings of one expression:

InnerSimplified Simplify
idempotence 1 failed of 834 0 failed of 120
order independence 2024 failed of 2738 8 failed of 72
listed agreements 20 failed of 30 6 failed of 30

Neither is canonical, and Simplify is much the closer of the two — the opposite of what the names suggest. InnerSimplified does not order the operands of a sum or a product at all, so it fails three quarters of the order checks by construction. Simplify reorders as a side effect of rating candidates; its eight failures are all ties settled by generation order.

The finding that matters most for anyone writing a rule or a test

(x + y) + a and x + (y + a) both print as x + y + a and are different trees. Associativity is normalised in the printer, not in the expression. A comparison of printed forms calls them equal; a comparison of entities does not. The harness compares entities, which is the only reason it saw this.

Two defects rather than decisions

Both measured, both reproducible on their own:

"cos(0 ^ y)".InnerSimplified   ->  -(-1) provided ...   and again  ->  1 provided ...
"cos(-x)".Simplify()           ->  cos(-x)      while  "cos(-2 * x)".Simplify()  ->  cos(2 * x)

The first is a rewrite building -(-1) above already-normalised children and returning without re-normalising — -(-1) on its own folds immediately. The second is the parity identities keyed on a shape a bare negation does not have; sin, tan and abs behave the same way. Filed separately.

What the document specifies

Per node class it states the target, marking each line met or not met against the measurement. Leaves, the commutative operators (flat, ordered by a specified total order, constants folded), powers, functions, and the nodes that are not values.

One line is flagged as needing a maintainer's yes before anybody implements it: making subtraction and division sugar for a sum and a product. That is what makes the commutative laws reachable — while a difference is its own node, x - y and x + (-y) are different trees denoting one thing, and no amount of ordering sums fixes it — but it removes two node classes and is a breaking change to anything matching on Divf. It is written down as the position because a specification that ducks the question is not one; it is not written down as a decision already taken.

x ^ 0 is the standing example of a canonicalisation a naive specification gets wrong: the library answers 1 provided not x = 0, and that is right.

And where a canonical form is actually available now

Rational functions over ℚ, where zero-equivalence is decidable and where the parts — multivariate GCD, expansion, a monomial order — landed with the polynomial layer in #918 and #923. That is the piece to build first, because it is the piece that is possible.

Scope

Documentation only — three files, no code. canoncheck lives in the analysis workspace with the other harnesses rather than in this repository.

🤖 Generated with Claude Code

…rary is (#746)

#746's tier 1 asks for "canonical forms with a written specification of what canonical means for each
node class, and a stated distinction between canonical and simplest", and its item 65 asks whoever
does it to take a position, write it down, and let the engine be checked against it. This is the
document; the checking is a new harness in the analysis workspace, and every number here came out of
it rather than out of an argument.

**The position.** Canonical is about identity and simplest is about presentation. A canonical form
exists to make equality a structural comparison; a simplest form is the best-rated member of a class
under a cost metric that the caller chooses, so it is only defined relative to one.

**A complete canonical form does not exist, and that is a theorem rather than a gap.** Zero-
equivalence is undecidable for the class the library accepts -- rationals, pi, exp, the trigonometric
functions, abs and composition (Richardson 1968) -- and a canonical form would decide it. So the
specification is a canonical form on a decidable sublanguage with the boundary written down, a
normalisation everywhere else that must not be mistaken for one, and a search that is not required to
be canonical at all. That is Moses' three-way split from 1971 and it costs nothing but saying so.

**Three properties, none needing an oracle, measured against both candidates:**

                        InnerSimplified      Simplify
    idempotence           1 failed of 834    0 failed of 120
    order independence 2024 failed of 2738   8 failed of  72
    listed agreements    20 failed of  30    6 failed of  30

Neither is canonical, and Simplify is much the closer of the two -- which is the opposite of what the
names suggest. InnerSimplified does not order the operands of a sum or a product at all, so it fails
three quarters of the order checks by construction. Simplify reorders as a side effect of rating
candidates, and its eight failures are all ties settled by generation order.

**The finding that matters most to anyone writing a rule or a test:** `(x + y) + a` and
`x + (y + a)` both print as `x + y + a` and are different trees. Associativity is normalised in the
printer, not in the expression, so a comparison of printed forms calls them equal and a comparison of
entities does not. The harness compares entities, which is why it saw this.

**Two defects rather than decisions**, each measured and each reproducible on its own:

    cos(0 ^ y).InnerSimplified   -(-1) provided ...   then   1 provided ...    not idempotent
    cos(-x).Simplify()           cos(-x)         while  cos(-2 * x)  ->  cos(2 * x)

The first is a rewrite building `-(-1)` above already-normalised children and returning without
re-normalising; `-(-1)` on its own folds immediately. The second is the parity identities keyed on a
shape a bare negation does not have -- sin, tan and abs behave the same way.

Per node class the file states the target rather than describing today, marking each line met or not.
One line is flagged as needing a maintainer's yes before anyone implements it: making subtraction and
division sugar for a sum and a product, which is what makes the commutative laws reachable and is
also a breaking change to everything that matches on Divf.

And it names where a canonical form is actually available now: rational functions over Q, where
zero-equivalence is decidable and the parts -- multivariate GCD, expansion, a monomial order -- landed
with the polynomial layer in #918 and #923. That is the piece to build first because it is the piece
that is possible.

#746

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s confluence (#746)

The first version of this file said a total order on operands was owed. It is not: the library has
one, at three granularities, and measuring it changes what the document should ask for.

`RewriteRules.CanonicalOrderExact` sorts and groups the operands of sums, products, conjunctions,
disjunctions and set operations by the whole subtree. Applied before the normalisation it makes
order independence **perfect** -- 0 failures of 2738, against 2024 without it. So the piece a
specification would normally have to invent is built; what is missing is that the normalisation does
not run it, which is also most of why `Simplify` agrees so much more often than `InnerSimplified`.

And the reason it cannot simply be moved there is now measured rather than guessed at. Sorting and
then normalising is **not idempotent** -- 21 of 834 -- and every failure is the same phenomenon, the
sort and `Patterns.NumericNeatRules` disagreeing about where a numeric operand belongs and each
undoing the other:

    1 / 2 - x   ->   -x + 1/2   ->   1/2 + -x   ->   ...

Neither is wrong on its own. Until they are made to agree, applying the order inside the
normalisation trades 2024 order failures for a form that never settles.

So §8's first item is no longer "specify and implement a total order" but "make the order and the
normalisation confluent", which is a much smaller and much better defined question, and it is the
one actually in the way of everything else in tier 1's canonicaliser.

The measurement table gains the third column, §4's commutative rows say ordered-but-not-run rather
than not-ordered, and §7 records that the harness now runs all three properties over all three
candidate forms -- the differences between the columns being the point rather than any one number.

Also: the two defects §3 lists are fixed, in #929 and #930, so the table is labelled with the build
it was taken on and with what it reads without them. A harness report records a build.

#746

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator Author

Pushed a correction after measuring further, since the first version of the document got a claim wrong.

The total order already exists. RewriteRules.CanonicalOrderExact sorts and groups commutative operands by the whole subtree, and applying it before the normalisation makes order independence perfect — 0 failures of 2738, against 2024 without it. So the piece a specification would normally have to invent is built; what is missing is that InnerSimplified does not run it, which is also most of why Simplify agrees so much more often.

And the real obstacle is confluence, not the order. Sorting then normalising is not idempotent — 21 of 834 — and every failure is the sort and Patterns.NumericNeatRules disagreeing about where a numeric operand belongs, each undoing the other:

1 / 2 - x   ->   -x + 1/2   ->   1/2 + -x   ->   ...

Neither is wrong alone. Until they agree, moving the order into the normalisation trades 2024 order failures for a form that never settles. §8's first item is now that question rather than "implement a total order".

The measurement table gains a third column for it, and is labelled with the build it was taken on — #929 and #930 are both fixed and land separately.

…lise (#746)

The previous revision said the order and the normalisation had to be made to agree, and left that
as the open question. It is not a disagreement between rules and nothing has to be decided between
them.

The sort's key depends on a node's class and the normalisation changes classes. In 1/2 - x the
constant reaches the sort as 1 * 2 ^ (-1), a product, and is ordered against -x as one; the
normalisation folds it to the number 1/2, and the next sort orders it the other way. So the sort was
ordering a shape about to stop existing.

Normalising first gives both properties at once:

                      InnerSimplified   order then normalise   normalise, order, normalise
    idempotence         0 of 834          21 of 834              0 of 834
    order independence  2024 of 2738       0 of 2738             0 of 2738

x + (-1/2), whose constant is already a number when the sort reads it, was stable throughout, which
is the control that makes this the explanation rather than a guess.

So §8's first item is now to expose that composition as the canonicaliser rather than to reconcile
anything. It needs no rule changed. What it needs is a decision about where it runs: opt-in changes
nothing, and inside InnerSimplified every commutative operand order in every printed answer moves at
once.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet and others added 2 commits August 14, 2026 03:42
Two corrections, both from a test that failed the way round I did not expect.

Nesting goes with order. The sort works over commutative *chains* rather than over one node, so it
flattens as it sorts: (x + y) + a and x + (y + a) both reach a + x + y and reach it as the same
tree. I had written a test asserting they stay different and it failed, which is how this was found.
So flattening is no longer owed -- what is owed is that InnerSimplified on its own does neither, and
InnerSimplified is what every rule and every cache in the library actually sees.

And the composition now exists under a name, Transformation.Canonicalisation (PR #933), so §8's
first item is no longer to build it but to decide where it runs. Offering it changes nothing;
putting it inside InnerSimplified moves every commutative operand order in every printed answer at
once.

The warning in §3 stands unsoftened for the same reason: the canonicaliser makes those two trees
one, and almost nothing calls the canonicaliser.

#746

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
§8's remaining build item is smaller and more specific than 'a rational-function canonical form'.
The greatest common divisor is already there and already verifies itself; what is missing is that
nothing in the library puts an expression over a common denominator, so 1/x + 1/y and (x+y)/(x*y)
cannot be brought to a common form by any existing route -- measured through Simplify,
InnerSimplified and Factorize alike, and Simplify actively prefers the split form.

Filed as #934 with the design and that measurement, so the next person starts from what is missing
rather than from what it is called.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 69f66da into master Aug 14, 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