镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

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

Closed

Description

@Rafael-SOWNet

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   <- lost

An 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 rebuilt

Why 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;
  • Replace is the traversal the whole rewriting layer is built on (Entity.Replace is what RewriteRuleSet.ApplyOnce uses), so any rule that rebuilds a node inside a domain(...) 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

  1. Preserve Codomain in 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 under domain(...) may begin answering NaN where they now answer a value. That is the right direction for each of them, but it is a behaviour change wherever it lands.
  2. 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

  1. Rafael-SOWNet commented on Aug 16, 2026

    @Rafael-SOWNet
    MemberAuthor

    Scope note: Replace is abstract on Entity with 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.

  2. added theissue type on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions