Repository navigation
Deep recursion: $RecursionLimit = Infinity in a Block, and Python's RecursionError - #2080
Conversation
…ws the Python stack
$RecursionLimit = Infinity in a Block bypassed the assignment that sets
the Python recursion limit, so deep recursion (Combinatorica's
NumberOfGraphs[15], PairGroupIndex) crashed with a Python RecursionError.
The depth was not counted at all under Infinity, yet still decremented,
so it drifted below zero.
Under Infinity the depth is now counted and bounded by the largest
$RecursionLimit allowed, with the Python limit raised to match.
Block evaluated its values after resetting its variables, so a nested
Block[{$RecursionLimit = Infinity}, ...] evaluated Infinity under the
default limit. The values are now evaluated first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deep recursion, as in Combinatorica's DepthFirstTraversal on a 50 x 50 grid, ran out of Python stack before $RecursionLimit was reached, and the RecursionError escaped the evaluation. The Python limit now allows 40 calls per level of $RecursionLimit instead of 30, so that the Mathics3 limit is reached first, and a RecursionError that still occurs is reported as reclim with $Aborted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| limit = MAX_RECURSION_DEPTH | ||
| if sys.getrecursionlimit() < python_recursion_depth(limit): | ||
| set_python_recursion_limit(limit) | ||
| limit = max(limit, 20) |
There was a problem hiding this comment.
How did you arrive at magic constant 20?
There was a problem hiding this comment.
The 20 isn't new in this PR: it was already in inc_recursion_depth on master (limit = max(limit, 20) inside if limit is not None:, and if limit < 20: limit = 20 in the initial version). This PR only moves the line out of the if block. It is WMA's minimum for $RecursionLimit:
In WMA 15: $RecursionLimit = 10
$RecursionLimit::limset: Cannot set $RecursionLimit to 10; value must be Infinity or an integer at least 20.
The same 20 also appears in the check of $RecursionLimit = n in assignment.py and in the text of the limset message, so I've given it a name: MIN_RECURSION_LIMIT = 20 in mathics/core/evaluation.py, with that comment, used in all three places (9bef323).
The constant that is new here is in python_recursion_depth: 40 Python calls per level of $RecursionLimit instead of 30. That one is measured. Running Combinatorica's DepthFirstTraversal on a 50×50 grid, Python's limit for $RecursionLimit = 512 (200 + 30·512 = 15,560 frames) was reached at a Mathics3 depth of about 508. That is a little over 30 Python frames per level, mostly the pattern matcher's continuations for a definition with a Condition. So the Python limit ran out just before $RecursionLimit did, and the RecursionError escaped. With 40, the Mathics3 limit is reached first. A RecursionError that still happens on some other path is now reported as $RecursionLimit::reclim with $Aborted.
… = 20
The 20 was written out in Evaluation.inc_recursion_depth, in the check
of $RecursionLimit = n and in the limset message. It is WMA's minimum
("value must be Infinity or an integer at least 20").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
LGTM |
Found while running the examples of Skiena and Pemmaraju's Computational Discrete Mathematics with the Combinatorica package and comparing every result with WMA 15.
Block[{$RecursionLimit = Infinity}, ...]overflowed the Python stack. Setting the limit in a Block bypassed the assignment that sets the Python recursion limit, and the depth was not counted under Infinity (it was still decremented, so it drifted below zero). The depth is now counted and bounded by the largest$RecursionLimitallowed (MATHICS_MAX_RECURSION_DEPTH), with the Python limit raised to match. Combinatorica uses this inNumberOfGraphs,PairGroupIndex,DepthFirstTraversaland others.Block[{$RecursionLimit = Infinity}, ...]evaluatedInfinityunder the default limit. The values are now evaluated first.RecursionErrorescaped the evaluation. Each level of Mathics3 recursion takes a little over 30 Python calls on some paths (a definition matched through a Condition), which is more than the 30 allowed for. The allowance is now 40, so$RecursionLimitis reached first, and aRecursionErrorthat still occurs is reported as$RecursionLimit::reclimwith$Aborted, not as a traceback.All the fixes from this series together, merged on one branch: full pytest 4336 passed, 0 failed; doctests 4679 passed, 0 failed.
🤖 Generated with Claude Code