Skip to content

Keep a node's codomain when a rewrite rebuilds it (#955) - #956

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/replace-preserves-codomain
Aug 16, 2026
Merged

Keep a node's codomain when a rewrite rebuilds it (#955)#956
Rafael-SOWNet merged 1 commit into
masterfrom
fix/replace-preserves-codomain

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #955.

Entity.Replace rebuilds every node on the path to a change, and each rebuild goes through a New(...) helper whose new(...) starts from the type's default codomain rather than the one the original carried. So any rewrite dropped a domain(...) annotation — Substitute included, since that is built on Replace:

"domain(sqrt(x), ZZ)".ToEntity().Substitute("x", "4/9".ToEntity()).EvalNumerical()
// was: 2/3     now: NaN

2/3 is not an integer, and the constraint existed to refuse it.

The old behaviour was silent and one-sided: constraints were only ever weakened, never strengthened, so a rewrite could make an undefined expression look defined but never the reverse. That is the direction that gets acted on rather than noticed.

The fix

47 rebuild sites, all the same shape:

  • 41 target-typed ReferenceEquals(...) ? this : new(...) across the continuous and discrete node classes
  • 6 written with an explicit type — Providedf, Piecewise, two Interval overloads, ConditionalSet, Matrix

Each now carries the original's codomain. I audited for leftovers rather than trusting the sweep: there is no remaining ? this : new( without it.

The blast radius, which was the open question on the issue

Zero. The full suite passes 7263, 0 failed with the fix in — unchanged from master. Nothing in the library depended on the annotation being lost, which is what made me willing to take the broad option rather than the narrower "only preserve a non-default codomain" one.

Three tests pin the behaviour and all three fail without the source change (verified by stashing only Sources/AngouriMath/):

  • through Replace directly
  • through Substitute, which is what a caller actually reaches for
  • one level deeper, so a nested rebuild is covered too

Recorded in BREAKING-CHANGES.md as Silent: an expression that answered a value where it should have refused now refuses.

How it was found, and what it unblocks

Implementing #873 — parsing a quotient of two integer literals as a Rational — by rewriting the parsed tree. domain(1/2, ZZ) stopped answering NaN, and it turned out not to be a parser bug at all. #873 is blocked behind this rather than able to work around it, since any post-parse rewrite has the same hole; it becomes straightforward once this lands.

What it does not do

🤖 Generated with Claude Code

Entity.Replace rebuilds every node on the path to a change, and each rebuild went
through a New(...) helper whose `new(...)` starts from the type's default codomain.
So any rewrite dropped a domain(...) annotation, Substitute included, since that is
built on Replace:

    "domain(sqrt(x), ZZ)".ToEntity().Substitute("x", "4/9")   evaluated to 2/3

2/3 is not an integer and the constraint existed to refuse it. The failure was silent
and one-sided -- constraints were only ever weakened, so a rewrite could make an
undefined expression look defined and never the reverse.

47 rebuild sites, all of the same shape: 41 target-typed `? this : new(...)` and 6
written with an explicit type in Piecewise, Interval, ConditionalSet and Matrix. Each
now carries the original's codomain.

The blast radius was the open question on the issue and it is zero: the suite passes
7263 with the fix in, unchanged, so nothing depended on the annotation being lost.
Three tests pin it -- through Replace, through Substitute, and one level deeper --
and all three fail without the change.

Found while implementing #873, whose parser-side fix is blocked behind this: any
post-parse rewrite has the same hole.
@Rafael-SOWNet
Rafael-SOWNet merged commit f562a1f into master Aug 16, 2026
25 checks passed
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.

Entity.Replace drops a node's Codomain when it rebuilds it, so a domain constraint silently disappears

1 participant