Repository navigation
Print a node's Codomain, so the printed form stops losing it (#1022) - #1047
Merged
Merged
Conversation
`Stringize` printed no node's `Codomain`, so `domain(x, ZZ)` printed as `x` and parsing that gave a `Variable` with `Codomain = Any` back. The annotation is not decoration: `sqrt(-1)` is `i` and the same expression over the reals is `NaN`, and the two printed the same string. The parser already had the syntax -- `domain(expr, SET)` maps onto `WithCodomain` and works for every node -- so only the printing half was missing. A node whose codomain is not the one its type carries by default now prints inside `domain(...)`, and LaTeX renders it as a subscripted set. The default is not uniform, which is why the rule is a comparison and not a check against `Complex`: a variable and a matrix default to `Any`, `abs` and an interval to `Real`, every boolean node to `Boolean`, each numeric literal to its own type's domain. Each node declares that default beside its `Codomain`, both abstract so that a node declaring one and forgetting the other does not compile. The wrapper is one decision, so it lives once on `Entity` and the per-node rendering moved behind `StringizeNode`/`LatexizeNode`. That removes 130 overrides from the recorded public surface; every one of them is still callable, inherited. `Simplify` collected `x` and the same `x` narrowed to the integers as one monomial, because it keys a monomial by its base's printed form: `x - domain(x, ZZ)` was `0`, which is the answer only where `x` is an integer. It is left alone now. Two annotations still do not survive, both the grammar's limit rather than the printer's, and both pinned by tests that fail if they start working: `Any` has no special set to name it, and no input yields a `Rational` whose codomain is `Complex`, since the pass that reads `1/2` as a rational treats `Complex` as "nobody annotated this" (#873). Measured on a build of each side; 8070 -> 8516 passed, 0 failed. The two expected outputs that moved are the test written to fail when this is fixed and the recorded public surface. #1022 #746
Rafael-SOWNet
force-pushed
the
fix/stringize-prints-the-codomain
branch
from
August 23, 2026 23:44
4409014 to
3b30a1e
Compare
The entry said removing the 130 per-node `Stringize()`/`Latexize()` overrides forces a consumer compiled against 2.3.0 to be rebuilt. It does not, and the difference matters under an `AssemblyVersion` pinned at 2.0.0.0 for the whole of 2.x: there is no binding redirect to soften a break that reaches a caller. Measured, not reasoned about. A consumer calling `((Entity.Sumf)e).Stringize()` was compiled against a build of 2.3.0 and run unchanged against a build of this branch; it prints what it printed before. Its metadata says why -- the emitted reference is `AngouriMath.Entity::Stringize()` whatever the receiver's static type, because C# binds a virtual call to the declaring type, so no consumer names `Entity+Sumf::Stringize()` for the removal to reach. Devirtualising `Stringize()` cannot orphan an outside override either: `Entity` already had five `internal`/`private protected` abstract members, so no other assembly could ever derive a node from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1022.
Stringizeprinted no node'sCodomain. Measured on a build ofmaster(cd0b56b) and a build ofthis branch:
"domain(x, ZZ)".ToEntity().Stringize()xdomain(x, ZZ).CodomainAnyIntegere == e.Stringize().ToEntity()FalseTrue"domain(x + 1, RR)".ToEntity().Stringize()x + 1domain(x + 1, RR)"domain(sqrt(-1), RR)".ToEntity().Stringize()sqrt(-1), which reads back asidomain(sqrt(-1), RR), which reads back asNaN"domain([1, 2], RR)".ToEntity().Stringize()[1, 2]domain([1, 2], RR)Sin(Var("x").WithCodomain(Integer)) + Var("y").WithCodomain(Real)sin(x) + ysin(domain(x, ZZ)) + domain(y, RR).Latexize()of the firstx{\left(x\right)}_{\mathbb{Z}}JsonSerializer.Serializeof the first"x""domain(x, ZZ)"This matters past the round trip because #746 rests on it: "the printed form is contractually a
lie-free channel — parsing what
Stringizeprints gives back the expression printed." Thatsentence had to be qualified with this defect; it does not any more, bar the two corners below.
The rule for when to print, and why it is a comparison
Print
domain(inner, SET)exactly where the codomain is not the one parsing the bare text wouldgive back — that is, not the node type's default. Anything wider would put a wrapper on every
expression the library prints and move hundreds of expected outputs for nothing.
That default is not uniform, which is the interesting part of the task and why the rule cannot be
"is it
Complex":AnyVariable(henceConstant),Matrix, everySetbutInterval,Providedf,Piecewise,Application,LambdaRealModf,Absf,Minf,Maxf,IntervalBooleanInfIntegerNumber.Integer,PhifRational/Real/ComplexComplexSo
DefaultCodomainis declared beside eachCodomaininDomains.Classes.cs, both abstract onEntity: a node that declares one and forgets the other does not compile, and a test asserts thatevery freshly built node of every node type carries its own default.
What moved besides the printer
Latexizehad the same gap. It renders as{\left(x\right)}_{\mathbb{Z}}. The parentheses areunconditional, because a variable renders its own index as a subscript and
x_{\mathbb{Z}}wouldbe indistinguishable from a variable spelled that way. New output for CSharpMath.Evaluation, which
has no notion of a codomain — it only appears for an expression carrying a narrowed one (Update AGENTS.md for Latexise #822).
EntityJsonConverterneeded no change: it serialises whatStringizeprints, so Stringize drops a node's Codomain, so domain(x, ZZ) prints as x and does not round trip #1022's entryin its remarks and in
Docs/Contributing/Serialization.mdis now a description of a fixed thing.MathS.ToSympyCodedid need one. It emitted{v.Stringize()} = sympy.Symbol(...)for eachvariable, which after this change is
domain(x, ZZ) = sympy.Symbol('domain(x, ZZ)')— not a Pythonidentifier. It uses
Namenow, and its output is byte-identical to master's on both sides.A sum stops collecting two terms that are not the same term.
Simplify's polynomial collectionkeys a monomial by its base's printed form:
0is the answer only wherexis an integer, and the annotation is exactly what says it mightnot be. Confirmed as the cause rather than inferred: putting the collision back — keying on the
base with its codomain erased — brings
0and2 * xstraight back.What this does not fix
Two annotations still do not read back, and both are the grammar's limit, not the printer's.
Each is pinned by a test that fails if it starts working, so neither can outlive itself.
Domain.Anycannot be written.domain(...)'s second argument must be aSpecialSet, andthere is none for "no restriction". A node widened to
Anyfrom a narrower default prints asthough it had not been. Printing
CCinstead would be a different lie, and throwing from a printerwould take out every diagnostic message that quotes an expression.
Rationalwhose codomain isComplex. The pass that reads1/2as arational rather than a quotient (Stringize of a Rational parses back as a Divf, so the round trip is not an identity #873) uses
Complexas its "nobody annotated this" sentinel,because that is what an un-annotated
Divfcarries. Sodomain(1/2, CC)parses to the same node1/2does. Worth its own issue; it is a parser gap and there is nothing the printer can emitinstead.
The public surface
Each node declared its own
public override string Stringize()andLatexize(). The codomainwrapper is one decision and now lives once on
Entity, with the per-node rendering behindprivate protected abstract StringizeNode()/LatexizeNode().PublicApi.txttherefore loses 130overrides and gains nothing. Nothing a caller writes stops compiling —
someSumf.Stringize()still resolves, inherited — but an assembly compiled against 2.3.0 binds to
Entity+Sumf::Stringize()wherever its static type is a concrete node, and has to be rebuilt.BREAKING-CHANGES.mdsays so.Tests
dotnet test Sources/Tests/UnitTests -c Release, full run each time, build lines read:The F#, Interactive and Terminal suites are green too (134 / 18 / 7).
Exactly two expected outputs moved, both deliberately:
EntitySerializationTest.ACodomainDoesNotSurviveBecauseNothingPrintsIt, written to fail when thisis fixed (Serialize an Entity as the expression it prints (#323) #1031), is now
ACodomainSurvivesBecauseThePrintedFormCarriesIt, plus a subnode case.PublicApi.txt, regenerated withAM_UPDATE_PUBLIC_API=1: 130 deletions, 0 additions.New:
CodomainSurvivesPrintingTest— every node type there is × every writable domain, comparingentities and reusing
EveryNodeSurvivesEveryPipelineTest.EveryNodeType, so it cannot silently stopcovering a node; the default-agreement check; the two pinned residuals; the LaTeX shape. Ten more
cases in
StringizeRoundTripTest, including the binder positions (lambda,derivative,integral, set-builder), which all round trip.No wiki or website sample uses
domain(orWithCodomain, sodocsamplesis unaffected; thecorpus gate is in the suite above.
Docs/Usage/Syntax.md,Docs/Contributing/Serialization.md,NodeContract.md,Packaging.mdandAddingNode.csare corrected where they described the oldbehaviour or counted the abstract members (eleven capabilities, now twelve).
Against #746: this is a standing-condition fix rather than a tier item — it removes the exception
that had to be written into the printed-form claim that machine-to-machine exchange, caches and
agent tool calls all rest on.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd