Skip to content

TypedPython: recursion with CPython's RecursionError (#57) - #101

Merged
b-re-w merged 1 commit into
developfrom
work/m2b-rec
Oct 3, 2026
Merged

b-re-w merged 1 commit into
developfrom
work/m2b-rec

Conversation

@b-re-w

@b-re-w b-re-w commented Oct 3, 2026

Copy link
Copy Markdown
Member

Closes #57

Feature added

Compiled functions can now recurse, directly or through other compiled functions, with CPython's RecursionError semantics.

  • tp_enter_call / tp_leave_call run on every compiled function's entry and every exit (ok, error, deopt).
    • Depth check: compiled frames plus Python frames are compared with sys.getrecursionlimit(); over the limit, RecursionError: maximum recursion depth exceeded is raised.
    • C stack: Py_EnterRecursiveCall keeps the process from crashing whatever the limit is set to.
  • The verifier and front end now accept call cycles.

Test added

test_recursion.py, 13 tests, compared with interpreted CPython:

  • fib, ackermann, mutual is_even/is_odd;
  • the same RecursionError past the limit, with raised and lowered limits;
  • an i64 overflow deopt inside recursion;
  • the depth counter stays balanced after errors and deopts;
  • the RecursionError path under ASan/UBSan.

Results: red 3 failed + 10 errors; green 13/13; whole suite 725. Removing tp_leave_call from the error and deopt paths makes 5 tests fail.

Measured

  • Mixed interpreted and compiled frames hit RecursionError at the same total depth: no offset at k = 1, 10 and 100 interpreted frames.
  • Divergences, recorded in design §4.3.3:
    • at a huge recursion limit the C-stack guard's message differs (3.13: ... in compiled code; 3.14: Stack overflow ...);
    • the depth counters are per module.

Not done

binary-trees still compiles nothing. make and check are skipped for None constants and the Node class, which is the fixed-layout class item (N-11), not recursion.

Note

The sub-agent's first full-suite run had 14 failures and 12 errors in Pyrefly-using tests (pyrefly wrote no readable type report). They passed when re-run. The cause is unknown; it may be temp-directory contention under the shared lock.

tp_enter_call/tp_leave_call wrap every compiled impl: a per-thread count of
compiled frames is added to the Python frame depth and checked against
sys.getrecursionlimit() (RecursionError 'maximum recursion depth exceeded'),
then Py_EnterRecursiveCall guards the C stack. leave runs on every exit
(ok, error, deopt). The verifier and front end accept call cycles; a cycle
through a may_deopt callee stays legal only in pure functions.

Mixed interpreted/compiled frames fail at the same total depth (no offset
measured). Divergences recorded in design §4.3.3: the C-stack guard's message
at huge limits, and per-module counters.

Red: verify 2, frontend 2, recursion 3+10 errors. Green: recursion 13/13
(incl. ASan/UBSan), suite 725. Mutation (leave only on ok): 5 fail.

Closes #57
@b-re-w
b-re-w merged commit 514a2fa into develop Oct 3, 2026
8 checks passed
@b-re-w
b-re-w deleted the work/m2b-rec branch October 3, 2026 10:17
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.

1 participant