From ea38a10ceca782a8ad2d817019d6f858d6242402 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 9 Aug 2026 01:00:12 +0000 Subject: [PATCH 1/2] Answer the floor of an infinity instead of throwing (#830) 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) --- ...aluation.Continuous.Arithmetics.Classes.cs | 7 +++++ .../UnitTests/Convenience/FloorCeilTest.cs | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index 1cdfb8308..dfd3711b6 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -278,6 +278,11 @@ protected override Entity InnerSimplify(bool isExact) { // An integer is already its own floor, and it stays exact. Integer n => n, + // An infinity is its own floor -- there is no greatest integer below + // +oo -- and NaN propagates. Neither survives EInteger, which refuses + // both, and the exception was reaching the caller: + // https://github.com/asc-community/AngouriMath/issues/830 + Real { IsFinite: false } n => n, Rational n => Integer.Create(n.EDecimal.Floor().ToEInteger()), Real n when !isExact => Integer.Create(n.EDecimal.Floor().ToEInteger()), Complex n when !isExact => Complex.Create( @@ -301,6 +306,8 @@ protected override Entity InnerSimplify(bool isExact) a => a switch { Integer n => n, + // As in Floorf: https://github.com/asc-community/AngouriMath/issues/830 + Real { IsFinite: false } n => n, Rational n => Integer.Create(n.EDecimal.Ceiling().ToEInteger()), Real n when !isExact => Integer.Create(n.EDecimal.Ceiling().ToEInteger()), Complex n when !isExact => Complex.Create( diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs index c2c5ccf94..93d09f852 100644 --- a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs +++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs @@ -132,5 +132,32 @@ public void SolvingGivesTheWholeIntervalAndNotAPoint() $"floor({point})".ToEntity().Simplify()); Assert.Equal(Entity.Number.Integer.Create(4), "floor(4)".ToEntity().Simplify()); } + + /// + /// An infinity is its own floor and its own ceil, and NaN propagates — + /// #830. + /// + /// + /// These used to throw ("Value is infinity or + /// NaN") out of evaluation, from the EInteger conversion. An internal exception + /// from the numeric library is not one a caller has any reason to expect, and the + /// neighbours do not do it: abs(+oo) is +oo and abs(0/0) is NaN. + /// + [Theory] + [InlineData("floor(+oo)", "+oo")] + [InlineData("ceil(+oo)", "+oo")] + [InlineData("floor(-oo)", "-oo")] + [InlineData("ceil(-oo)", "-oo")] + public void AnInfiniteArgumentIsItsOwnFloorAndCeil(string input, string expected) + { + Assert.Equal(expected.ToEntity().Evaled, input.ToEntity().Evaled); + Assert.Equal(expected.ToEntity().Evaled, input.ToEntity().Simplify().Evaled); + } + + [Theory] + [InlineData("floor(0/0)")] + [InlineData("ceil(0/0)")] + public void AnUndefinedArgumentStaysUndefined(string input) + => Assert.True(input.ToEntity().Evaled.IsNaN); } } From 25a3511ff25059796e6e258e5609cb587baaf71c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 9 Aug 2026 01:01:08 +0000 Subject: [PATCH 2/2] Give floor and ceil a limit case instead of recursing (#829) 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) --- .../Limits/Solvers/Limit.Classes.cs | 68 ++++++++++++++++++ .../UnitTests/Convenience/FloorCeilTest.cs | 69 +++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index 491e881aa..6df234347 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -398,6 +398,74 @@ partial record Absf => Argument.ComputeLimitDivideEtImpera(x, dist, side)?.Abs(); } + /// + /// The limit of a function that is constant between consecutive integers and jumps at + /// each of them, given the limit of its argument. + /// + /// + /// Away from the jumps the function is locally constant, so the limit is simply the + /// function of the argument's limit. On a jump there is nothing to say: the value + /// differs on the two sides of it, and which side the argument arrives from is not + /// decided by the side 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. + /// + /// Null is returned there rather than an unevaluated limit of the very expression being + /// asked about. The latter is what the inherited default does, and it does not merely + /// fail to answer: the two-sided path compares its one-sided results by evaluating them, + /// evaluating a limit computes it, and computing it arrives back here. The recursion + /// ends by overflowing the stack, which kills the process rather than raising anything a + /// caller could catch. + /// #829 is that + /// fault on these two nodes and + /// #704 was the + /// same one on . + /// + private static Entity? LimitOfAStepFunction(Entity argument, Variable x, Entity dist, + ApproachFrom side, System.Func rebuild) + { + if (argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } limit) + return null; + if (limit.Evaled is not Number value || value.IsNaN) + return null; + // An infinity is its own floor and its own ceil, so it is not a jump. + if (!value.IsFinite) + return rebuild(limit); + // Taken componentwise, as the evaluation is, so a jump in either part is a jump -- + // except that a real value's imaginary part is identically zero rather than + // tending to zero, and the floor of a constant zero is a constant zero. Only a + // genuinely complex limit has an imaginary part that can arrive at an integer from + // one side or the other. + // + // Like the Absf and Signumf overrides above, this reads the argument as real-valued + // along the path. An argument that is complex near the destination and real at it + // can still be answered here when it should not be; that is the assumption those + // two already make, and narrowing it wants a way to decide realness that this + // library does not have yet -- https://github.com/asc-community/AngouriMath/issues/721. + if (value is not Complex { RealPart: var real, ImaginaryPart: var imaginary }) + return null; + if (SitsOnAJump(real)) + return null; + if (value is not Real && SitsOnAJump(imaginary)) + return null; + return rebuild(limit); + + static bool SitsOnAJump(Real part) => + part is Integer || part.EDecimal.CompareTo(part.EDecimal.Floor()) == 0; + } + + partial record Floorf + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => LimitOfAStepFunction(Argument, x, dist, side, static a => new Floorf(a)); + } + + partial record Ceilf + { + internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) + => LimitOfAStepFunction(Argument, x, dist, side, static a => new Ceilf(a)); + } + partial record Providedf { internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) diff --git a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs index 93d09f852..a725ee895 100644 --- a/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs +++ b/Sources/Tests/UnitTests/Convenience/FloorCeilTest.cs @@ -159,5 +159,74 @@ public void AnInfiniteArgumentIsItsOwnFloorAndCeil(string input, string expected [InlineData("ceil(0/0)")] public void AnUndefinedArgumentStaysUndefined(string input) => Assert.True(input.ToEntity().Evaled.IsNaN); + + /// + /// A limit over floor or ceil terminates — + /// #829. + /// + /// + /// Every one of these used to overflow the stack, because the nodes inherited a default + /// ComputeLimitDivideEtImpera that returns an unevaluated limit of the very node + /// being asked about, and evaluating that computes it again. That kills the process + /// rather than raising anything, so this test cannot assert an exception — it asserts + /// that an answer arrives at all, which a regression would turn into a dead test run + /// rather than a silent pass. It is the same fault + /// #704 fixed on + /// signum. + /// + [Theory] + [InlineData("floor(x)", "0")] + [InlineData("floor(x)", "2")] + [InlineData("floor(x)", "1/2")] + [InlineData("floor(x)", "+oo")] + [InlineData("floor(x)", "-oo")] + [InlineData("ceil(x)", "0")] + [InlineData("ceil(x)", "3/2")] + [InlineData("ceil(x)", "+oo")] + [InlineData("floor(cos(x))", "0")] + public void TakingALimitTerminates(string input, string destination) + { + var task = System.Threading.Tasks.Task.Run( + () => input.ToEntity().Limit("x", destination.ToEntity())); + Assert.True(task.Wait(System.TimeSpan.FromSeconds(20)), + $"limit({input}, x, {destination}) did not finish"); + Assert.NotNull(task.Result); + } + + /// + /// Between two consecutive integers the function is constant, so the limit is that + /// constant — including at the infinities, where the floor of an infinity is itself. + /// + [Theory] + [InlineData("floor(x)", "1/2", "0")] + [InlineData("floor(x)", "3/2", "1")] + [InlineData("floor(x)", "-1/2", "-1")] + [InlineData("ceil(x)", "3/2", "2")] + [InlineData("ceil(x)", "-1/2", "0")] + [InlineData("floor(x + 1/2)", "0", "0")] + [InlineData("ceil(x + 1/2)", "0", "1")] + [InlineData("floor(1/2 + x^2)", "0", "0")] + [InlineData("floor(x)", "+oo", "+oo")] + [InlineData("floor(x)", "-oo", "-oo")] + [InlineData("ceil(x)", "-oo", "-oo")] + public void TheLimitAwayFromAJumpIsTheValue(string input, string destination, string expected) + => Assert.Equal(expected.ToEntity().Evaled, + input.ToEntity().Limit("x", destination.ToEntity()).Evaled); + + /// + /// 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. So the answer is an + /// unevaluated limit — the same thing signum returns at zero. What it must not + /// do is pick one of the two values. + /// + [Theory] + [InlineData("floor(x)", "0")] + [InlineData("floor(x)", "2")] + [InlineData("floor(x)", "-3")] + [InlineData("ceil(x)", "0")] + [InlineData("ceil(x)", "2")] + public void TheLimitOnAJumpIsDeclined(string input, string destination) + => Assert.IsType( + input.ToEntity().Limit("x", destination.ToEntity())); } }