From f7d1527cbd781dc980ce395f1042d0ae13b5303f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 4 Nov 2026 06:50:00 +0000 Subject: [PATCH] Build a whole power by a loop over the exponent's bits The whole power was a recursion on the half of the exponent, one frame for each of its bits, and a power of -1, i or -i is never too large to build. So (-1)^(2^20000) took 20000 frames and overflowed the stack of a thread-pool thread, 1.5 MB on Linux -- which ends the process rather than throwing. limitcheck died that way at SEED=28, evaluating a sample point. A loop from the exponent's top bit down makes the same multiplications in the same order: the recursion's half truncates toward zero, so a negative exponent is the bits of its magnitude, each multiplying by 1 / num. Exact and decimal powers, real and complex, come out digit for digit as before. limitcheck at DEPTH=3, SEED=28 now finishes its 600 expressions. No issue was filed. Co-Authored-By: Claude Opus 5.5 --- .../Entity/Continuous/Number/Operators.cs | 25 +++++++--- .../UnitTests/Core/AWholePowerOfAUnitTest.cs | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/AWholePowerOfAUnitTest.cs diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index 1b9651440..e2931bcff 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -557,18 +557,27 @@ public static Complex Pow(Complex @base, Complex power) // cube was three multiplications and a tenth power was nine, each with the // rational search on its result, and the recursion was linear in the exponent // where it should be logarithmic. + // + // A loop over the exponent's bits, from the top, rather than a recursion on its + // half: the recursion was one frame a bit, and a power of -1, i or -i is never + // too large to build, so (-1)^(2^20000) took 20000 frames and overflowed the + // stack of a thread-pool thread, which ends the process. The multiplications are + // the recursion's, in its order: the half truncates toward zero, so a negative + // exponent is the bits of its magnitude, each one multiplying by 1 / num. static Complex BinaryIntPow(Complex num, EInteger val) { if (val.IsZero) return 1; - if (val.Equals(1)) - return num; - if (val.Equals(-1)) - return 1 / num; - var divRem = val.DivRem(2); // divRem[0] == val / 2, divRem[1] == val % 2 - var half = BinaryIntPow(num, divRem[0]); - var squared = half * half; - return divRem[1].IsZero ? squared : squared * BinaryIntPow(num, divRem[1]); + var magnitude = val.Abs(); + var factor = val.Sign < 0 ? 1 / num : num; + var result = factor; + for (var bit = magnitude.GetUnsignedBitLengthAsInt64() - 2; bit >= 0; bit--) + { + result *= result; + if (magnitude.GetSignedBit(bit)) + result *= factor; + } + return result; } // A complex whose imaginary part is exactly zero is the real number it is, and // takes the real paths below: with the downcasting off, Complex.Create keeps diff --git a/Sources/Tests/UnitTests/Core/AWholePowerOfAUnitTest.cs b/Sources/Tests/UnitTests/Core/AWholePowerOfAUnitTest.cs new file mode 100644 index 000000000..ae403016b --- /dev/null +++ b/Sources/Tests/UnitTests/Core/AWholePowerOfAUnitTest.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System.Threading; +using Xunit; + +namespace AngouriMath.Tests.Core +{ + /// + /// A whole power was built by a recursion on the half of the exponent, one frame for each of its bits. A power of + /// -1, i or -i is never too large to build, so (-1)^(2^20000) took 20000 frames and overflowed the stack of a + /// thread-pool thread, 1.5 MB on Linux, which ends the process rather than throwing. Found by limitcheck at + /// DEPTH=3, SEED=28, evaluating a sample point; no issue was filed. + /// + [Trait("Area", "Core")] + public sealed class AWholePowerOfAUnitTest + { + // A quarter of a megabyte: smaller than any thread the library is run on, and enough that the recursion + // overflowed on these exponents, of 20000 bits and more, and ended the test host. + private const int SmallStack = 256 * 1024; + + [Theory] + [InlineData("(-1)^(2^20000)", "1")] + [InlineData("(-1)^(2^20000 + 1)", "-1")] + [InlineData("i^(3^20000)", "i")] + [InlineData("(-i)^(-(2^20001) - 3)", "-i")] + [InlineData("(-1)^(-(2^20000) - 1)", "-1")] + public void IsItsCycleOnASmallStack(string input, string expected) + { + Entity? value = null; + var thread = new Thread(() => value = MathS.FromString(input).EvalNumerical(), SmallStack); + thread.Start(); + thread.Join(); + Assert.Equal(MathS.FromString(expected).EvalNumerical(), value); + } + + // The multiplications are the recursion's, in its order, so a decimal power keeps every digit it had. + [Theory] + [InlineData("(1.1 + 0.2i)^(-13)", "-0.16276672053742730437722112 - 0.16876781705731410143215616i")] + [InlineData("(0.3 + 0.4i)^(-9)", "-241.716953088 - 451.350102016i")] + [InlineData("2.5^(-37)", "137438953472/72759576141834259033203125")] + [InlineData("i^(-7)", "i")] + public void IsWhatItWas(string input, string expected) + => Assert.Equal(MathS.FromString(expected).EvalNumerical(), MathS.FromString(input).EvalNumerical()); + } +}