Skip to content

Give NaN a token, so its printed form reads back (#906) - #910

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/nan-token
Aug 12, 2026
Merged

Give NaN a token, so its printed form reads back (#906)#910
Rafael-SOWNet merged 1 commit into
masterfrom
fix/nan-token

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #906, and takes the option both comments there converged on: NaN becomes a reserved word, like
the built-in function names.

expression was is
"NaN - NaN".Simplify() 0 NaN
"NaN / NaN".Simplify() 1 provided not NaN = 0 NaN
"NaN * 0".Evaled 0 NaN
"NaN * 2".Evaled, "NaN + NaN".Evaled left as written NaN
NaN as a variable name a variable the NaN value
NaNx, NaN_1, aNaN variables variables, unchanged

Every "was" in that table is the ordinary rewrite for a free variable, which is what NaN parsed to.
Nothing on the page said so, because a variable named NaN prints as NaN too.

Latexize needed nothing, and that is measured rather than assumed

@Happypig375's hedge in the issue — "CSharpMath uses \mathrm{undefined} as the special token here, I
think"
— is right, in both directions. AngouriMath has emitted \mathrm{undefined} for NaN all along
(Latex.Number.Classes.cs), and CSharpMath's Evaluation.cs carries

case ("undefined", 0, FontStyle.Roman): @this = MathS.NaN; break;

so the LaTeX round trip was already closed. This PR does not touch Latexize. Its three non-finite
values (\infty, -\infty, \mathrm{undefined}) were all fine; it was Stringize that printed two
names the parser knew and one it did not.

The grammar choice

Declared as a lexer rule above VARIABLE, which is where BOOLEAN sits and for the same reason:
equal-length matches go to the earlier rule, and this word would otherwise be an identifier. +oo and
-oo are inline literals instead, which is safe for them because they cannot collide with a name.

One spelling, NaN. BOOLEAN accepts two capitalisations of each word because it has to read back
its own output — Entity.Boolean prints True where a caller types true — and NaN prints and reads
the same way, so there is nothing to reconcile and no reason to enlarge the reserved surface.

Reserving the word costs the identifier, exactly as mod did in 2.0, and only the exact spelling: the
lexer takes the longest match, so NaNx, NaN_1 and aNaN are all still variables. Syntax.md lists
the new literal.

The round-trip test now runs in both directions

This is the part that catches the next one. Every case in StringizeRoundTripTest began from a
string, so it could only reach expressions the parser already produces — a value with no source form
was invisible to all of them, however many cases were added. That is why the defect lived in an output
for as long as it did.

It now also enumerates the named constants of MathS and Entity.Number.Real by reflection, prints
each and reads it back, so a constant added later is covered without anyone remembering the file exists.
MathS.NaN and Real.NaN are the two that fail against the old grammar, along with the computed cases
(0/0, 1/0, +oo - +oo).

Regeneration discipline

Regenerated with the committed antlr-4.13.1-complete.jar and the AntlrPostProcessorReplacePublicWithInternal
step. The unmodified grammar was regenerated first and its diff confirmed empty, so nothing in the
generated files here is a toolchain difference rather than my rule.

Measured

New tests fail 9 of 219 against master and pass 219 of 219 here. Suite 6451 passed / 0 failed, F#
wrapper 130 passed. casbench 117/119 with 0 wrong, rootcheck 596/596, simpsweep 10463/10463, propcheck
1340 checks with 0 failures, crashcheck 1652 cases with 0 crashes, boundcheck 2 disagreements and 0
that did not parse
— worth naming for a grammar change. docsamples has 0 compile errors; its single
output mismatch is the Solvers wiki page that #901 made stale, which I am updating separately.

One more of the same shape, filed rather than bundled: ToSympy for a Real is => Stringize(), so it
emits NaN, +oo and -oo into generated Python whose preamble is only import sympy — none of those
names is bound there. That is #909.

Cut from master at 7c3c01ab.

Stringize prints the NaN value as NaN, the grammar had no such token, so reading it back gave a
variable of that name -- which then behaved like any symbol. NaN - NaN simplified to 0,
NaN / NaN to `1 provided not NaN = 0`, and NaN * 0 to 0, each of them the ordinary rewrite for a
free variable. Nothing on the page distinguished the two, because a variable named NaN prints as
NaN as well.

Its two siblings already had tokens -- +oo and -oo both print and parse -- and Latexize has
emitted \mathrm{undefined} all along, which is the token CSharpMath decodes back to MathS.NaN, so
the LaTeX round trip was already closed and is untouched. Verified in CSharpMath's own source
rather than assumed: Evaluation.cs has
`case ("undefined", 0, FontStyle.Roman): @this = MathS.NaN; break;`.

Declared as a lexer rule above VARIABLE rather than as an inline literal, which is where BOOLEAN
sits and for the same reason: equal-length matches go to the earlier rule, and this word would
otherwise be an identifier. One spelling only. BOOLEAN carries two capitalisations of each word
because it has to read back its own output -- Entity.Boolean prints True where a caller types
true -- and NaN prints and reads the same way, so there is nothing to reconcile.

Reserving the word costs the identifier, as `mod` did in 2.0, and only the exact spelling: NaNx,
NaN_1 and aNaN are still variables, since the lexer takes the longest match.

The round trip is now tested in both directions. Every case in StringizeRoundTripTest began from a
string, so it could only reach expressions the parser already produces, and a value with no source
form was invisible to all of them however many cases were added -- which is why this defect lived
in an output. It now also enumerates the named constants of MathS and Entity.Number.Real by
reflection, prints each and reads it back, so a constant added later is covered without anyone
remembering the file exists. MathS.NaN and Real.NaN are the two that fail against the old grammar.

Regenerated with the committed antlr-4.13.1 jar and the post-processor. The unmodified grammar was
regenerated first and its diff confirmed empty, so nothing in the generated files here is a
toolchain difference. Syntax.md lists the new literal.

New tests fail 9 of 219 against master and pass 219 of 219 here. Suite 6451 passed, F# wrapper 130
passed; casbench 117/119 with 0 wrong; rootcheck 596/596; simpsweep 10463/10463; propcheck 1340
checks 0 failures; crashcheck 1652 cases 0 crashes; boundcheck 2 disagreements and 0 that did not
parse; docsamples 0 compile errors, its one output mismatch being the Solvers wiki page that #901
made stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 548ea17 into master Aug 12, 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.

Stringize prints NaN as NaN, and the parser reads NaN as a variable, so the round trip is silently broken

1 participant