Give floor and ceil a limit case, and answer the floor of an infinity (#829, #830) - #831
Merged
Merged
Conversation
floor(+oo), ceil(-oo) and floor(0/0) threw System.OverflowException out of
evaluation. The Real branch converts through EInteger, which refuses both the
infinities and NaN, and the exception reached the caller -- an internal
PeterO.Numbers fault is not one a caller of Evaled has any reason to expect, and
the neighbouring nodes do not raise it: abs(+oo) is +oo and abs(0/0) is NaN.
An infinity is its own floor and its own ceil: there is no greatest integer
below +oo, and every other system answers the infinity. NaN propagates, as it
does everywhere else. Both are exact, so the new branch sits ahead of the
!isExact guard and Simplify answers them too.
eval(floor(+oo)) OverflowException -> +oo
eval(ceil(-oo)) OverflowException -> -oo
eval(floor(0/0)) OverflowException -> NaN
Introduced by #827. Suite 5903 passed, casbench 113/117 with 0 wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A limit over floor or ceil overflowed the stack and killed the process. Neither node overrode ComputeLimitDivideEtImpera, so both took the base implementation, which returns an unevaluated limit of the very node being asked about. The two-sided path then compares its one-sided results by evaluating them, evaluating a limit computes it, and control arrives back at the same node. It is not an exception a caller can catch. This is #704 on a new pair of nodes. The Signumf override written for that issue carries a comment saying exactly this, and Modf's says it again for its own jumps; this adds the third case rather than a third copy of the reasoning. Away from its jumps a step function is locally constant, so the limit is the function of the argument's limit. On a jump the two sides disagree, and which side the argument arrives from is not decided by the side x approaches its destination from -- lim(x -> 0+) floor(2 - x^2) reaches 2 from below and lim(x -> 0+) floor(2 + x^2) from above, and both are limits from the right. So a jump is declined with null, which hands the caller an unevaluated limit, and no value is invented for it. An infinity is not a jump: it is its own floor, which is what #830 made answerable, so the limits at the infinities come out rather than being declined. lim(x -> 0) floor(x) *** stack overflow *** -> limit(floor(x), x, 0) lim(x -> 2) floor(x) *** stack overflow *** -> limit(floor(x), x, 2) lim(x -> 1/2) floor(x) *** stack overflow *** -> 0 lim(x -> +oo) floor(x) *** stack overflow *** -> +oo lim(x -> -oo) ceil(x) *** stack overflow *** -> -oo The imaginary part is only checked for a genuinely complex limit: a real value's imaginary part is identically zero rather than tending to zero, and the floor of a constant zero is constant. Like the Absf and Signumf overrides beside it, this reads the argument as real-valued along the path; narrowing that wants a way to decide realness the library does not have yet (#721), and it is noted in the remarks rather than assumed silently. Introduced by #827. Suite 5903 passed, F# 130, casbench 113/117 with 0 wrong, rootcheck 596/596, simpsweep 10463/10463, propcheck 1340 checks 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 9, 2026
… (#836) ComputeLimitDivideEtImpera defaulted to new Limitf(this, x, dist, side). That is the cycle AGENTS.md names, with this exact expression as its example: the caller evaluates the returned node to compare it, evaluating a Limitf computes the limit, and computing arrives back at the default. It overflows the stack, which kills the process rather than raising anything a caller can catch. So a node crashed by inheriting the default -- by not being mentioned anywhere. Seven did: limit(floor(x), x, 0) stack overflow, process dies limit(ceil(x), x, 0) stack overflow limit(round(x), x, 0) stack overflow limit(min(x, 1), x, 0) stack overflow limit(max(x, 1), x, 0) stack overflow limit(gcd(x, 2), x, 0) stack overflow limit(phi(x), x, 0) stack overflow The first six arrived with #827 and #828, which added the nodes without the ComputeLimitDivideEtImpera override that AddingNode.cs lists at step 3d. phi predates both and is #833, so this is not only my regression -- the default has been a landmine for every node that ever inherited it, which is what #704 was. Returning null is what AGENTS.md prescribes and what every caller already expects: each of the eleven call sites tests the result with `is { }` or switches on it, and the signature was already Entity?. All seven now come back as an unevaluated limit node, which is the honest "I could not settle this", and nothing else moves -- 5928 tests passed before this change and after it. LimitTerminatesOnEveryNodeTest enumerates the node types by reflection rather than listing them, because the defect is in what a node inherits by not being mentioned, and a per-node test cannot catch that: 45 types, each over three destinations and three sides, asserting only that the call returns. Checked against the unfixed code, where the run aborts. This does not replace #831, which gives floor and ceil real limit answers and fixes the OverflowException of #830. That adds capability; this removes the way to crash. They compose, and the order does not matter. Tests: 5973 passing, 0 failed, 14 skipped; F# 130. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #829. Closes #830.
Two defects introduced by #827, in the order they have to be fixed: the second commit's answer at the
infinities depends on the first.
#830 —
floor(+oo)threwOverflowExceptionThe
Realbranch of evaluation converts throughEInteger, which refuses the infinities andNaN,and the exception reached the caller. An internal
PeterO.Numbersfault is not one a caller ofEvaledhas any reason to expect, and the neighbours do not raise it —abs(+oo)is+ooandabs(0/0)isNaN.An infinity is its own floor and its own ceil: there is no greatest integer below
+oo.NaNpropagates. Both are exact, so the new branch sits ahead of the
!isExactguard andSimplifyanswers them too.
#829 — a limit over
floororceilkilled the processNeither node overrode
ComputeLimitDivideEtImpera, so both took the base implementation, whichreturns an unevaluated limit of the very node being asked about. The two-sided path compares its
one-sided results by evaluating them, evaluating a limit computes it, and control arrives back at the
same node. It is a stack overflow, so it is not an exception a caller can catch.
This is #704 on a new pair of nodes. The
Signumfoverride written for that issue carries a commentsaying exactly this, and
Modf's says it again for its own jumps; this adds the third case ratherthan a third copy of the reasoning.
Away from its jumps a step function is locally constant, so the limit is the function of the
argument's limit. On a jump the two sides disagree, and which side the argument arrives from is not
decided by the side
xapproaches its destination from —lim(x -> 0+) floor(2 - x^2)reaches 2 frombelow and
lim(x -> 0+) floor(2 + x^2)from above, and both are limits from the right. So a jump isdeclined with
null, which hands the caller an unevaluated limit, and no value is invented for it.The imaginary part is only checked for a genuinely complex limit: a real value's imaginary part is
identically zero rather than tending to zero, and the floor of a constant zero is constant. Like the
AbsfandSignumfoverrides beside it, this reads the argument as real-valued along the path;narrowing that wants a way to decide realness the library does not have yet (#721), and it is written
into the remarks rather than assumed silently.
Declining is conservative in a couple of places where an answer exists —
lim(x -> 0) floor(cos(x))is 0 on a punctured neighbourhood and comes back unevaluated, because
cosreaches the integer 1.Unevaluated is honest there; inventing one of the two one-sided values would not be.
Measured
casbenchrootchecksimpsweeppropcheckWorth doing next, and not in this PR
Neither of these is a mathematics mistake — #827 wired the node, evaluation,
ToString,Latexise,ToSympy, differentiation,InvertNode,Substitute,Sort,Domains, the grammar and a full testclass, and missed one file. Nothing in the repository lists where a new
Entitysubtype has to bewired, so there was nothing to check against.
A test generated by reflection over every
Functionsubtype, asserting that a limit of it terminates,would have caught #829 before merge.
round,trunc,min,maxand the rest of #809 hit the sametrap.