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