Parse a quotient of two integer literals as the Rational it denotes (#873) - #957
Conversation
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.
…quotient-as-rational # Conflicts: # BREAKING-CHANGES.md
|
Sharper finding, and the reason this stays a draft rather than me updating the eight tests. I went to fix them and found the change costs something bigger than eight expectations: after it, no textual spelling produces an unevaluated
That shape is still reachable by construction and by substitution — I could make them pass by comparing So the trade, stated plainly:
Whether that is a good trade is a call about what the parser is for, and it is yours rather than mine. If you want it landed I will rewrite the six expectations as constructed entities and say so in each; if you would rather have the round trip fixed some other way — a rational literal in the grammar, as the issue's second option suggested — this branch is the wrong shape and should be closed. |
A quotient of two integer literals now parses as the Rational it denotes, so TestDivideByEntityStrict's expectations could no longer name the unevaluated trees the operation produces. They are constructed instead, and kept as their own test rather than folded back into the theory: comparing InnerSimplified on both sides would make them pass and would stop the test being about strictness, which is what it is named for. ToSympyCode now emits sympy.Rational(1, 2) for an unsimplified 1/2, which is what the simplified case already emitted -- the two spellings converge, which is the point of the fix rather than a casualty of it. SubstituteTest.TupleSub3 likewise: substitution leaves Divf(8, 11) and no text spells that any more. Suite 7261 passed, 0 failed.
…uotient-as-rational # Conflicts: # BREAKING-CHANGES.md
|
Out of draft — the eight failures are resolved and the suite is green (7261 passed, 0 failed). I said above I would not edit those tests until you had seen the cost. Here is how they were resolved, since the resolution is itself the evidence for what the change costs: Six
So the trade stands as described — a |
…uotient-as-rational # Conflicts: # BREAKING-CHANGES.md
|
Synced with Merged rather than rebased, deliberately: upstream squash-merges, so this branch's earlier merge of the #956 work is in The conflict was Suite 7275 passed, 0 failed on the merged tree, so this composes with all five. |
Draft, and stacked on #956 — it contains that branch's commits until it merges. Addresses #873.
@Happypig375's rule, implemented: a quotient of two integer literals parses as the
Rationalit denotes, so aRational's printed form parses back to aRationaland the round trip is an identity.3.57/2Rational0.11/10Rational1/21/2Rational4/24 / 2Divf1/01 / 0DivfA quotient that reduces to an integer is deliberately left alone. Parsing is not simplification: making
4/2into2would discard what the caller wrote, and it already round-tripped. Only the non-integer case is one aRationalcan print as.This also deletes the
["1/2"]entry fromEveryNodeSurvivesEveryPipelineTest.KnownRoundTripFailures.Draft, because the fix has a measurable cost and the call is yours
Implementing it produced the mirror asymmetry the issue body predicted, and 8 existing tests detect it:
It is narrower than "parse disagrees with construct" — building via
operator /gives aRationalthat does equal the parsed one, andInnerSimplifiedreconciles the two. The gap is only aDivfthat already existed and whose children became integers by substitution, which is consistent with how substitution behaves generally ("x + 0".Substitute("x", 1)is1 + 0, not1).The 8 failures are all of that one shape: 6
TestDivideByEntityStrictrows, 1ToSympyCodeTestrow that exists only to document #873, and 1SubstituteTest. Each compares a substituted tree against an expectation written as a string, and the string now denotes a different tree.I have not edited them, because making a test agree with new output is how a real signal gets buried, and this one is worth your eye: it is the cost of the rule, not a defect in it. If you want it landed I will update them and say in each what changed and why.
Not included
Complexhas the identical defect. A constructedComplexprints1 + 2iand parses back as aSumf, or aMinusffor3 - 4i, soKnownRoundTripFailuresstill has one entry after this. The parallel rule is "a sum or difference of a real literal and an imaginary one is aComplex" — happy to add it here or separately, whichever you prefer.Why it is stacked
The first attempt failed in a way that was not a parser bug:
domain(1/2, ZZ)stopped answeringNaN, because rewriting the parsed tree rebuilt the node and dropped the codomaindomain(...)had put on it. That is #955, fixed in #956. Any post-parse rewrite had the same hole, so this could not work around it.🤖 Generated with Claude Code