Repository navigation
2.4.0
Tier 1 of the Math OS roadmap is
finished, and the thing that finished it kept finding wrong answers. Writing a rewrite rule out as
data — a pattern and a replacement, rather than an arm of a switch — makes the
correspondence between the two something you have to state. Four times this cycle, a rule did not
survive stating it.
Every changed answer is in BREAKING-CHANGES.md
under 2.4.0 — since 2.3.0, with the old value, the new one and why, each measured on a build
of both sides. Read that first if you have code depending on an answer. Twenty-seven entries, and
more than half are marked Silent — the call still succeeds and quietly returns something else.
AssemblyVersion stays 2.0.0.0. The recorded public surface shows 68 additions and 130
removals, and none of the removals is one a caller can hit: Stringize() and Latexize()
stopped being abstract on Entity and each node's override became a private helper, so the
member is still public on Entity and inherited by every node. Source- and binary-compatible.
Wrong answers fixed
- A factorisation that was not equal to what it factored.
Factor("4x² − 4y²", "x")returned
(x + y)(x − y)— the4simply gone, the difference from the input−3x² + 3y². Every
candidate in the polynomial layer is checked by exact division, but that check is on the
individual factors; nothing compared the assembled product against the input, so a constant
lost during assembly was lost silently. (#1092) (y < x) or (x = y)simplified tox <= y— its own negation off the diagonal. Four of the
eightor-with-equality rules carried their neighbour's comparison. Only reachable with both
operands symbolic: with a number on one side,2 < xis rewritten tox > 2earlier in the same
pass and one of the four correct rules matches. A test written with a numeric operand would
have passed on the defect. (#1077)x! = 0was answeredFalseeverywhere, including at the negative integers where the
factorial has a pole and the statement isNaN. The rule read a property pattern on the
factorial's argument rather than on the factorial. It tookx! / x!→1with it, and three
recorded test verdicts had been recording that. (#1081)ln(1/b) = −ln(b)was applied unconditionally, which is false on the negative reals — at
b = −0.63the two differ by the full turn of the argument the principal branch discards.
Three rules gained the guard their neighbours ten lines below already had. (#1062)
Factorisation
Factorize was built entirely out of rewrite rules, so it factored what someone had written a
rule for and handed everything else back whole — while square-free decomposition, Zassenhaus over
ℚ and the multivariate GCD sat in the tree unused by it.
"x3 - 1".Factorize() x ^ 3 - 1 → (x - 1) * (x ^ 2 + x + 1)
"x4 - 5x2 + 4".Factorize() unchanged → (x + 1) * (x + 2) * (x - 2) * (x - 1)
"x2 + 2x + 1".Factorize() unchanged → (x + 1) ^ 2
The layer speaks only where the rules said nothing, so every answer they already gave is
unchanged — the order two factors come out in is arbitrary and theirs is the one on record. (#1018)
And the layer itself reaches further. Hensel lifting along an evaluation homomorphism (#1088,
#1089) factors bivariate polynomials that Kronecker's substitution refuses — not because its image
is too large but because it over-factors: x⁷ − y⁷ maps to t⁷(1 − t⁴⁹), whose factors are
cyclotomic. An evaluation image inflates nothing.
Factor("x12 - y12", "x") null → six factors, the full cyclotomic split
Factor("x7 - y7", "x") null → (x - y)(x⁶ + x⁵y + ⋯ + y⁶)
Where the substitution gives up entirely, an evaluation image can still prove a polynomial
irreducible — which since #1059 is an answer rather than a refusal, so
Factor("x2 + y2 + z2 + w2 + 1", "x") returns the polynomial instead of null. (#1087)
The rule sets are data
Thirty of thirty rule sets now have a form in which each rule is a value — a pattern, a
replacement, a soundness tier and a direction — proven to agree with the switch it replaces over
thousands of generated expressions. Twenty-seven run it. The three that do not are the canonical
orders, and that is a measurement rather than an omission.
It buys: every rule individually addressable (407 entries), per-rule soundness where only the set
had a tier before (181 Sound, 141 SoundUnderAssumptions), and 26 rules that can be read
backwards.
Performance, honestly
SimplifyEasy is about 13% slower than before the exchange — 82,676 ns to 93,732 ns on one
desktop, both arms, standard deviation under half a per cent. Every conversion was measured
against the commit in front of it and every one came back free or better; nothing was measured
against the start, and the sum of a run of free steps is +13%.
It would have been worse. Indexing each set's rules by node type recovered most of it, and a
bounded pattern is now walked by index rather than enumerated — 165.05 MB to 163.40 MB of
SolveMediumHard on its own.
The work shape is unchanged: 4,914 rule-set invocations on that input at both ends, so this is
per-operation overhead rather than extra work. version_performance_control.md records where it
goes, what three attempts to remove it measured, and the roughly 40% still unattributed.
That file also gained a measurement worth more than the column: the Kernel Benchmark run twice
on one commit is 30–57% apart on every benchmark, while allocation over the same pair agrees to
0.03%. Timing comparisons between its columns are evidence only above about 50%.