Repository navigation
Description
Entity.Replace rebuilds every node on the path to a change, and the rebuilt node does not carry the Codomain the original had. A domain constraint therefore disappears from an expression that is rewritten, and the expression starts evaluating where it should be NaN.
Measured on master at a45a7256, .NET 10, default settings:
Entity e = "domain(sqrt(x), ZZ)".ToEntity();
e.Codomain // Integer
Entity replaced = e.Replace(n => n is Variable { Name: "x" } ? "y".ToEntity() : n);
replaced.Codomain // Complex <- lostAn identity Replace keeps it, which is the tell — nothing is rebuilt, so nothing is dropped:
Entity f = "domain(sqrt(4/9), ZZ)".ToEntity();
f.Evaled // NaN, correctly: 2/3 is not an integer
f.Replace(n => n).Evaled // NaN, still — no node was rebuiltWhy it matters
Codomain is what makes domain(...) mean anything, and it is checked at evaluation. Losing it turns a refusal into an answer:
- the failure is silent — no exception, the call succeeds and returns a value where it previously returned
NaN; - it is one-sided, and in the unsafe direction: constraints are only ever weakened, never strengthened, so a rewrite can make an undefined expression look defined but never the reverse;
Replaceis the traversal the whole rewriting layer is built on (Entity.Replaceis whatRewriteRuleSet.ApplyOnceuses), so any rule that rebuilds a node inside adomain(...)drops the annotation.
I have not measured how far it reaches inside Simplify — an expression under domain(...) that gets rewritten is the case to look at, and Simplify normalises heavily, so my expectation is that it reaches easily. That is the measurement this issue needs next and I did not want to hold the report for it.
How it was found
Implementing #873 — parsing a quotient of two integer literals as a Rational — by rewriting the parsed tree with Replace. domain(1/2, ZZ) stopped answering NaN, because rebuilding the quotient dropped the annotation domain(...) had just put on it. That is a Replace bug wearing a parser bug's clothes, so #873's fix is blocked behind this rather than being able to work around it: any post-parse rewrite has the same hole.
Possible directions
- Preserve
Codomainin whatever rebuilds a node —with { Codomain = original.Codomain }at the reconstruction site. Correct, and the blast radius is the question: rules that currently lose the annotation would start keeping it, so expressions underdomain(...)may begin answeringNaNwhere they now answer a value. That is the right direction for each of them, but it is a behaviour change wherever it lands. - Preserve it only where the codomain is not the default, which is the same fix with a narrower footprint and the same semantics for everything nobody annotated.
Filing rather than fixing, because (1) changes answers and the size of that change is worth measuring before choosing.
Activity
Rafael-SOWNet commentedon Aug 16, 2026
Scope note:
ReplaceisabstractonEntitywith 58 hand-written overrides across the node classes, so there is no single reconstruction site to patch — either every override carries the codomain through, or they route through a shared helper that does. That is what makes this worth measuring before picking a direction rather than a one-line fix.Keep a node's codomain when a rewrite rebuilds it (#955) (#956)
Parse a quotient of two integer literals as the Rational it denotes (#…