Skip to content

2.4.0

Choose a tag to compare

@Rafael-SOWNet Rafael-SOWNet released this 28 Aug 12:31
· 839 commits to master since this release
de7189b

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) — the 4 simply 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 to x <= y — its own negation off the diagonal. Four of the
    eight or-with-equality rules carried their neighbour's comparison. Only reachable with both
    operands symbolic: with a number on one side, 2 < x is rewritten to x > 2 earlier 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! = 0 was answered False everywhere, including at the negative integers where the
    factorial has a pole and the statement is NaN. The rule read a property pattern on the
    factorial's argument rather than on the factorial. It took x! / x! → 1 with 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.63 the 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%.