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);
+ }
+}