Skip to content

Deep recursion: $RecursionLimit = Infinity in a Block, and Python's RecursionError - #2080

Merged
rocky merged 6 commits into
masterfrom
fix-recursion-limit-infinity
Oct 31, 2026
Merged

rocky merged 6 commits into
masterfrom
fix-recursion-limit-infinity

Conversation

@tombouctou

Copy link
Copy Markdown
Collaborator

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 $RecursionLimit allowed (MATHICS_MAX_RECURSION_DEPTH), with the Python limit raised to match. Combinatorica uses this in NumberOfGraphs, PairGroupIndex, DepthFirstTraversal and others.
  • 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.
  • A Python RecursionError escaped 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 $RecursionLimit is reached first, and a RecursionError that still occurs is reported as $RecursionLimit::reclim with $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

tombouctou and others added 3 commits October 30, 2026 02:27
…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>
Comment thread mathics/core/evaluation.py Outdated
limit = MAX_RECURSION_DEPTH
if sys.getrecursionlimit() < python_recursion_depth(limit):
set_python_recursion_limit(limit)
limit = max(limit, 20)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you arrive at magic constant 20?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

tombouctou and others added 3 commits October 30, 2026 16:38
… = 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>
@rocky

rocky commented Oct 31, 2026

Copy link
Copy Markdown
Member

LGTM

@rocky
rocky merged commit 1ca4954 into master Oct 31, 2026
15 checks passed
@rocky
rocky deleted the fix-recursion-limit-infinity branch October 31, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants