A CostModel's feature test reads the node it was written for, so an unevaluated equivalent scores as though the feature were absent. Under FewestDivisions, y ^ (1 * (-1)) * x costs less than x / y.
Measured
foreach (var model in CostModel.All)
foreach (var e in new[] { "x / y".ToEntity(),
MathS.Pow("y".ToEntity(), -1) * "x".ToEntity(),
MathS.Pow("y".ToEntity(), "1".ToEntity() * (-1)) * "x".ToEntity() })
Console.WriteLine($"{model.Name} {e.Stringize()} {model.Cost(e)}");
| expression |
Default |
SmallestTree |
FewestDivisions |
FewestRadicals |
x / y |
11.000 |
3.000 |
1.003 |
0.003 |
y ^ (-1) * x |
26.000 |
5.000 |
1.005 |
0.005 |
y ^ (1 * (-1)) * x |
22.000 |
7.000 |
0.007 |
0.007 |
All three are the same value. FewestDivisions counts one division in the first two and none in the third, so it rates the third cheapest — under the criterion whose one job is to minimise divisions, the preferred form is the one with a division written where the model cannot see it.
Why
FewestDivisions tests node is Divf || node is Powf(_, Real { IsNegative: true }). In y ^ (1 * (-1)) the exponent is a Mulf, not a negative Real, so the test does not fire. FewestRadicals has the same shape — Powf(_, Rational and not Integer) — and so the same blind spot for an exponent written as, say, 2 ^ (-1). Default is affected differently but for the same reason: it prefers x ^ 2 - 1 ^ 2 to (x + 1) * (x - 1), the unfolded 1 ^ 2 costing less than a second binomial.
The common cause is that the feature tests are written against evaluated node shapes.
Whether Simplify can reach it — measured, because the obvious answer is wrong
The tempting thing to say is that Simplificator only ever scores InnerSimplified candidates, so 1 * (-1) has folded to -1 before anything is rated. That is not what the code does. AddHistory rates both:
var refexpr = expr.Rewrite(RewriteRules.CanonicalOrder).InnerSimplified;
var compl1 = refexpr.SimplifiedRate;
var compl2 = expr.SimplifiedRate; // the raw candidate, not simplified
var n = compl1 > compl2 ? expr : refexpr;
and keeps whichever rates lower — so a raw candidate that the model under-rates is preferred to its evaluated equivalent by construction. It also adds a candidate rewritten by RewriteRules.InvertNegativePowers, which is a rule about exactly these nodes.
So the precondition is not structurally guaranteed; it just seems to hold in practice. Twelve division-heavy inputs under each of the four models — 48 Simplify calls — produced no output containing a power whose exponent is unevaluated but whose value is a negative real:
static bool HiddenDivision(Entity e) => e.Nodes.Any(n =>
n is Powf(_, var exponent) && exponent is not Number
&& exponent.Evaled is Real { IsNegative: true });
Unobserved, not impossible. I would not want that read as "cannot happen".
Why it is still worth an issue
CostModel (#949, #746 item 52) exists precisely so that a caller can supply the cost and pass it somewhere other than Simplify, and the precondition is not stated anywhere. It was found by doing exactly that: extraction on an e-graph (#746 item 51), where an e-class holds every writing of a value at once and the unevaluated ones are ordinary members. Any consumer that scores expressions it did not build itself is in the same position.
Options, none obviously right
- Evaluate inside
Cost. Correct for every spelling, and pays Evaled per node on a function called once per candidate per iteration — likely too expensive for the default path, and it also changes what SmallestTree means.
- Make the tests recognise both spellings, e.g. asking whether the exponent's value is a negative real rather than whether its node is one. Narrower, still costs an
Evaled on the exponent only, and does not generalise to a caller's own model.
- Document the precondition — a
CostModel describes the expression it is given, so give it one that is evaluated — and leave the models as they are.
I lean to 3 plus a sentence in the CostModel remarks, because 1 and 2 make the built-in models honest while a caller's own model, which the type exists to allow, would still have the same trap. But that is a call about a public API rather than a defect fix, which is why this is an issue rather than a PR.
Measured on 90bad792.
A
CostModel's feature test reads the node it was written for, so an unevaluated equivalent scores as though the feature were absent. UnderFewestDivisions,y ^ (1 * (-1)) * xcosts less thanx / y.Measured
DefaultSmallestTreeFewestDivisionsFewestRadicalsx / yy ^ (-1) * xy ^ (1 * (-1)) * xAll three are the same value.
FewestDivisionscounts one division in the first two and none in the third, so it rates the third cheapest — under the criterion whose one job is to minimise divisions, the preferred form is the one with a division written where the model cannot see it.Why
FewestDivisionstestsnode is Divf || node is Powf(_, Real { IsNegative: true }). Iny ^ (1 * (-1))the exponent is aMulf, not a negativeReal, so the test does not fire.FewestRadicalshas the same shape —Powf(_, Rational and not Integer)— and so the same blind spot for an exponent written as, say,2 ^ (-1).Defaultis affected differently but for the same reason: it prefersx ^ 2 - 1 ^ 2to(x + 1) * (x - 1), the unfolded1 ^ 2costing less than a second binomial.The common cause is that the feature tests are written against evaluated node shapes.
Whether
Simplifycan reach it — measured, because the obvious answer is wrongThe tempting thing to say is that
Simplificatoronly ever scoresInnerSimplifiedcandidates, so1 * (-1)has folded to-1before anything is rated. That is not what the code does.AddHistoryrates both:and keeps whichever rates lower — so a raw candidate that the model under-rates is preferred to its evaluated equivalent by construction. It also adds a candidate rewritten by
RewriteRules.InvertNegativePowers, which is a rule about exactly these nodes.So the precondition is not structurally guaranteed; it just seems to hold in practice. Twelve division-heavy inputs under each of the four models — 48
Simplifycalls — produced no output containing a power whose exponent is unevaluated but whose value is a negative real:Unobserved, not impossible. I would not want that read as "cannot happen".
Why it is still worth an issue
CostModel(#949, #746 item 52) exists precisely so that a caller can supply the cost and pass it somewhere other thanSimplify, and the precondition is not stated anywhere. It was found by doing exactly that: extraction on an e-graph (#746 item 51), where an e-class holds every writing of a value at once and the unevaluated ones are ordinary members. Any consumer that scores expressions it did not build itself is in the same position.Options, none obviously right
Cost. Correct for every spelling, and paysEvaledper node on a function called once per candidate per iteration — likely too expensive for the default path, and it also changes whatSmallestTreemeans.Evaledon the exponent only, and does not generalise to a caller's own model.CostModeldescribes the expression it is given, so give it one that is evaluated — and leave the models as they are.I lean to 3 plus a sentence in the
CostModelremarks, because 1 and 2 make the built-in models honest while a caller's own model, which the type exists to allow, would still have the same trap. But that is a call about a public API rather than a defect fix, which is why this is an issue rather than a PR.Measured on
90bad792.