diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 6bd53f143..802d7ec87 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -224,6 +224,32 @@ private static Entity Normalized(Entity expr) => /// [System.ThreadStatic] private static bool descentTruncated; + /// + /// The integrals this thread is part-way through, so that asking for one again while it is + /// still being worked out is recognised as a cycle rather than followed round again. + /// + /// + /// + /// Keyed on the integrand with its variable renamed to , which + /// is the whole point. names + /// each new variable with Variable.CreateUnique, so a level and the level it came + /// from are alpha-equivalent rather than equal, and a set keyed on the integrand as + /// written would never see the same entry twice. Neither does , and + /// for the same reason — which is why the memo could not stop this. + /// + /// + /// This is what makes a backstop instead of the mechanism. + /// The bound alone stops the process dying, and it is not enough on its own: the descent + /// branches, so 32 levels of a cycle is still an enormous search, and one Rubi integrand + /// took over ten minutes under the bound where v2.4.0 passed the whole section it + /// is in within a minute. Declining at the first repeat costs nothing and answers at once. + /// + /// + [System.ThreadStatic] private static HashSet<(Entity, bool)>? inProgress; + + /// The name every in-progress integrand's variable is rewritten to. + [ConstantField] private static readonly Entity.Variable CycleVariable = Entity.Variable.CreateVariableOrConstant("__integration_cycle_var"); + /// Does not add the constant of integration because this is called recursively. internal static Entity? ComputeIndefiniteIntegral(Entity expr, Entity.Variable x, bool integrateByParts = true) { @@ -233,7 +259,22 @@ private static Entity Normalized(Entity expr) => return null; } if (descentDepth == 0) + { descentTruncated = false; + inProgress?.Clear(); + } + + // Renamed so that the same integral under two different variable names is one entry. + var cycleKey = (expr.Substitute(x, CycleVariable), integrateByParts); + var open = inProgress ??= new HashSet<(Entity, bool)>(); + if (!open.Add(cycleKey)) + { + // Already on the stack: following it again cannot reach an answer this call has + // not already tried, so declining is both correct and immediate. + descentTruncated = true; + return null; + } + descentDepth++; try { @@ -241,6 +282,7 @@ private static Entity Normalized(Entity expr) => } finally { + open.Remove(cycleKey); descentDepth--; } }