Skip to content

Count a division by what it is, not by how it was written (#950) - #953

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/cost-model-feature-tests
Aug 15, 2026
Merged

Count a division by what it is, not by how it was written (#950)#953
Rafael-SOWNet merged 1 commit into
masterfrom
fix/cost-model-feature-tests

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #950.

FewestDivisions tested the exponent's nodePowf(_, Real { IsNegative: true }) — so in y ^ (1 * (-1)) * x, where the exponent is a Mulf, it fired on nothing:

expression FewestDivisions was is
x / y 1.003 1.003
y ^ (-1) * x 1.005 1.005
y ^ (1 * (-1)) * x 0.007 1.007

All three are one value written three ways. The model rated the writing whose division it could not see as the cheapest of the three — under the criterion whose whole job is removing divisions. FewestRadicals had the same shape of test and the same blind spot, so x ^ (2 ^ (-1)) was not a square root to it.

Both now ask what the exponent evaluates to. One division each, and the smallest tree wins, which is what the node term exists to settle.

Why this and not the other two options

The issue listed three, and I originally leaned to documenting the precondition and leaving the models alone. The argument against that is a split the issue did not name:

  • SmallestTree and Default count spelling. y ^ (1 * (-1)) * x really is a bigger tree. Reading the node as written is not an approximation of their question, it is their question. Evaluating inside Cost (option 1) would therefore make these two wrong, not just slower.
  • FewestDivisions and FewestRadicals count a mathematical feature. "Is there a division here" is a property of the value. For these, reading the written node is simply the wrong question.

So the fix is per-model rather than global, and documentation alone would have left a built-in model preferring a hidden division. "A caller could write a wrong model too" argues for explaining the distinction — which this does, in the type's remarks — not for leaving a shipped model wrong.

Default and SmallestTree are untouched, and there is a test pinning that.

Behaviour

Simplify's output does not change. Measured rather than reasoned: twelve division-heavy inputs under both models, on a build of each side, twenty-four results, byte-identical. Simplify scores candidates it has already evaluated, so it never reached the gap.

What reaches it is a caller scoring expressions it did not build — extraction on an e-graph, where every writing of a value is a member of one e-class at once, which is how this was found. That is exactly the use CostModel (#949, #746 item 52) exists to enable, which is why this is worth fixing rather than documenting.

Recorded in BREAKING-CHANGES.md: Cost returns different numbers for an expression with an unevaluated exponent.

Evidence

  • suite 7257 passed, 0 failed, 14 skipped
  • 2 of the 3 new tests fail without the source change. The third passes either way by design — it pins that SmallestTree still counts spelling, so it is a guard against overreach rather than evidence for the fix, and it should not be read as a third supporting test.

What it does not do

  • Does not change Default or SmallestTree, or the default Simplify path.
  • Does not evaluate inside Cost generally — a caller's own model still reads what it is given, and the remarks now say so and explain which of the two kinds it should be.
  • Does not touch Feature's node-term weighting.

🤖 Generated with Claude Code

CostModel.FewestDivisions tested the exponent's node -- Powf(_, Real { IsNegative:
true }) -- so in y ^ (1 * (-1)) * x, where the exponent is a Mulf, it fired on
nothing. The three writings of one value scored 1.003, 1.005 and 0.007, making the
form whose division the model could not see the cheapest of the three, under the
criterion whose whole job is to remove divisions. FewestRadicals had the same shape
of test and the same blind spot, so x ^ (2 ^ (-1)) was not a square root to it.

Both now ask what the exponent evaluates to. All three writings count one division,
and the smallest tree is cheapest, which is what the node term is there to settle.

Default and SmallestTree are deliberately untouched. They count how an expression is
written, and y ^ (1 * (-1)) * x really is a bigger tree -- for them the written node
is the question rather than an approximation of it. That split is now stated in the
type's remarks, because a caller writing their own model has to decide which of the
two kinds it is and nothing in the library can decide it for them.

Simplify's output does not change: twelve division-heavy inputs under both models,
measured on a build of each, twenty-four results, byte-identical. Simplify scores
candidates it has already evaluated. What reaches the gap is a caller scoring
expressions it did not build -- extraction on an e-graph -- which is how it was found.

Suite 7257 passed, 0 failed. Two of the three new tests fail without the change; the
third passes either way by design, pinning that the spelling models were not widened.
@Rafael-SOWNet
Rafael-SOWNet merged commit 2d128f3 into master Aug 15, 2026
27 of 28 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/cost-model-feature-tests branch August 15, 2026 23:49
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.

A CostModel's feature test reads the node it was written for, so an unevaluated equivalent scores as though the feature were absent

1 participant