Skip to content

InnerSimplified is not idempotent: cos(0 ^ y) leaves -(-1) standing for one pass #930

Description

@Rafael-SOWNet

What happens

InnerSimplified is not idempotent: applying it twice gives a different tree from applying it once.

Measured on a clean build of master at b4385a86, .NET 10, default settings:

"cos(0 ^ y)".InnerSimplified                   ->  -(-1) provided y / 2 * (1 + 1 / sgn(y) ^ 2) > 0
"cos(0 ^ y)".InnerSimplified.InnerSimplified   ->      1 provided y / 2 * (1 + 1 / sgn(y) ^ 2) > 0

The answer is correct both times. What is wrong is that the first pass leaves -(-1) standing at the head of it, and a second pass folds it.

Why this is worth fixing rather than shrugging at

InnerSimplified is the normalisation every node runs on construction, and a great deal of the library assumes what it hands back is settled — rules match on its output, caches key on it, and tests compare against it. A form that changes when reapplied is not a form; whichever of the two trees a caller sees then depends on how many times something happened to normalise it.

It is also the only idempotence failure in 834 expressions, which suggests it is one rewrite rather than a design problem.

Where to look

-(-1) on its own folds immediately:

"-(-1)".ToEntity()                 ->  1        (the parser already folds it)
"-(-1)".ToEntity().InnerSimplified ->  1

So the double negation is not surviving normalisation — it is being built after normalisation has run. Some rewrite on the path for cos of a zero power constructs a negation over an already-normalised -1 and returns it without re-normalising the node it just built. The 0 ^ y argument is what gets it there: 0 ^ y is 0 under a condition on the sign of y, and cos of it goes through a branch that produces the sign as -(-1) rather than as 1.

The fix is presumably for that rewrite to return InnerSimplified of what it constructs, or to build the folded constant in the first place; the rule to check afterwards is that no rewrite returns a node it assembled without normalising it, since this one cannot be the only place that could happen.

How it was found

canoncheck, a new harness for #746 tier 1's canonical-form work. Idempotence is one of three properties it checks generatively — it needs no oracle, since it compares a form against itself — over 834 generated expressions. See Contributing/CanonicalForm.md, which records the measurement and what a canonical form is required to be.

A regression test wants to assert expr.InnerSimplified.Equals(expr.InnerSimplified.InnerSimplified) on entities and not on printed forms: the two trees here print differently, but the general class of idempotence failure includes trees that print alike and differ, which a string comparison would wave through.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions