From 3b978765080ed51e0d8e9344489491d02e08def8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 4 Aug 2026 21:25:37 +0000 Subject: [PATCH] Answer the limit of a signum instead of overflowing the stack (#704) lim x->2 signum(x) killed the process. Signumf was the one node whose limit override handed back an unevaluated limit of the very expression it was asked about, and that is not merely a failure to answer: the two-sided path compares its two one-sided results by evaluating them, evaluating a limit computes it, and computing it arrives back at the same override. Some four thousand frames later the stack runs out, which kills the process rather than raising anything a caller could catch. The sign is constant on either side of zero, so wherever the argument tends to anything but zero the limit is the sign of that -- including at the infinities, where it is 1 and -1. At zero there is nothing to say, since the sign is 1 on one side and -1 on the other and which one a one-sided limit takes depends on the direction the argument approaches from rather than only on what it tends to. Null is returned there rather than a limit of this expression. It means the same thing to the caller, which hands back an unevaluated limit of its own, and it takes the branch that falls through to l'Hopital's rule and returns instead of the branch that evaluates and re-enters. 13 new tests, three of which time out rather than fail without the fix, since what they are really pinning is termination. Suite 4474 passed, 0 failed. --- .../Limits/Solvers/Limit.Classes.cs | 24 +++++- .../UnitTests/Calculus/SignumLimitTest.cs | 80 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs index 6ab6be282..54ca19c0f 100644 --- a/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs +++ b/Sources/AngouriMath/Functions/Continuous/Limits/Solvers/Limit.Classes.cs @@ -295,9 +295,29 @@ partial record Limitf partial record Signumf { - // TODO: internal override Entity? ComputeLimitDivideEtImpera(Variable x, Entity dist, ApproachFrom side) - => new Limitf(this, x, dist, side); + { + if (Argument.ComputeLimitDivideEtImpera(x, dist, side) is not { } argument) + return null; + // The sign is constant on either side of zero, so wherever the argument tends + // to anything but zero the limit is simply the sign of that -- including the + // infinities, where it is 1 and -1. At zero it is the one place there is + // nothing to say: the sign is 1 on one side and -1 on the other, and which one + // a one-sided limit takes depends on the direction the argument approaches + // from rather than only on what it tends to. + // + // Nothing is returned there rather than an unevaluated limit of this very + // expression. That is what used to be here, and it does not merely fail to + // answer -- the two-sided path compares its two 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: + // https://github.com/asc-community/AngouriMath/issues/704. Null says the same + // thing to the caller, which hands back an unevaluated limit of its own. + if (argument.Evaled is not Number value || value == 0 || value.Evaled == MathS.NaN) + return null; + return new Signumf(argument); + } } partial record Absf diff --git a/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs new file mode 100644 index 000000000..ef1affb67 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/SignumLimitTest.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019-2022 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Core; +using AngouriMath.Extensions; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// The limit of a signum. It had no reading of its own and handed back an unevaluated limit + /// of the very expression it was asked about, which did not merely fail to answer: the + /// two-sided path compares its two one-sided results by evaluating them, evaluating a limit + /// computes it, and computing it arrived back at the same place. The recursion ended by + /// overflowing the stack, which kills the process rather than raising anything a caller + /// could catch -- https://github.com/asc-community/AngouriMath/issues/704. + /// + public sealed class SignumLimitTest + { + private static Entity Limit(string expression, string destination) => + expression.ToEntity().Limit("x", destination.ToEntity()).Simplify(); + + /// + /// Away from zero the sign is constant, so the limit is the sign of whatever the + /// argument tends to -- including at the infinities, where it is 1 and -1. + /// + [Theory] + [InlineData("signum(x)", "2", "1")] + [InlineData("signum(x)", "-2", "-1")] + [InlineData("signum(x - 5)", "2", "-1")] + [InlineData("signum(x ^ 2 + 1)", "0", "1")] + [InlineData("signum(x)", "+oo", "1")] + [InlineData("signum(x)", "-oo", "-1")] + public void TheSignOfWhereTheArgumentGoes(string expression, string destination, string expected) => + Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled); + + /// + /// Where the argument tends to zero there is nothing to say: the sign is 1 on one side + /// and -1 on the other, and which one is taken depends on the direction the argument + /// approaches from rather than only on what it tends to. Left unevaluated -- and, above + /// all, terminating, which is what this test is really for. + /// + [Theory] + [InlineData("signum(x)", "0")] + [InlineData("signum(x ^ 3)", "0")] + public void AtZeroItIsLeftUnevaluatedAndTerminates(string expression, string destination) + { + var task = Task.Run(() => Limit(expression, destination)); + Assert.True(task.Wait(TimeSpan.FromSeconds(30)), "the limit did not terminate"); + Assert.IsType(task.Result); + } + + [Theory] + [InlineData(ApproachFrom.Left)] + [InlineData(ApproachFrom.Right)] + public void OneSidedAtZeroTerminatesToo(ApproachFrom side) + { + var task = Task.Run(() => "signum(x)".ToEntity().Limit("x", 0, side).Simplify()); + Assert.True(task.Wait(TimeSpan.FromSeconds(30)), "the limit did not terminate"); + Assert.IsType(task.Result); + } + + /// + /// A signum inside a larger expression must not drag the whole limit down with it. + /// + [Theory] + [InlineData("signum(x) * x", "2", "2")] + [InlineData("signum(x) + 1", "3", "2")] + [InlineData("abs(x)", "2", "2")] + public void ItComposes(string expression, string destination, string expected) => + Assert.Equal(expected.ToEntity().Evaled, Limit(expression, destination).Evaled); + } +}