Skip to content

TypedPython: OSR-style exit for snapshot holders (#152, option i) - #335

Merged
b-re-w merged 1 commit into
developfrom
feat/tp-osr-exit
Oct 25, 2026
Merged

b-re-w merged 1 commit into
developfrom
feat/tp-osr-exit

Conversation

@b-re-w

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

Copy link
Copy Markdown
Member

Closes #152 (decision (i), user-approved: CPython identity).

  • SPEC N-8: a function holding a list copy-in or an entry-globals snapshot now polls inside long loops, from the second TP_POLL_INTERVAL chunk on, and each While countdown. At each poll:
    1. Write the lists back, as CPython's own stores would already be visible.
    2. End the critical section, poll, and take it again.
    3. Copy the lists in again and re-check lengths, exact element types and entry globals.
    • Unchanged: it continues compiled.
    • Changed: an OSR-style exit to a generated interpreted continuation of that iteration. The call returns or raises what the continuation does.
  • resume.py (frontend) generates one continuation per loop from the original AST:
    • The innermost loop continues from its current index.
    • Each enclosing loop runs the rest of its current iteration (a first-iteration flag keeps break/continue exact) and then its remaining iterations.
    • Range bounds are the values evaluated at loop entry.
    • The original lines and co_name/co_qualname are kept, so tracebacks match.
    • Unbound locals are passed as a sentinel and deleted.
  • cgen and runtime:
    • The wrapper passes an OSR context (lock, lists, result) through tp_tls. The impl takes it at entry, so internal and recursive calls never poll with someone else's lists.
    • At an exit the impl boxes every live local (a local array becomes a new list) and calls the continuation. The wrapper skips write-back after rc 2, because the lists are already final.
    • Functions with virtual objects or regions, or a parameter that cannot be handed over, keep the TypedPython: compiled code never checks the eval breaker (signals, GC, GIL hand-off) #141 no-poll rule.
  • Short loops (under the interval) never poll, so a short call stays one critical section, still an execution CPython can produce.
  • Tests:
    • test_resume.py (33): 320 resume points, each compared with the interpreted run.
    • test_osr_exit.py (19): deterministic, with TP_POLL_INTERVAL=32 and the test-only TP_TEST_SNAP_HOOK running Python in the poll window. Covers unchanged (no exit), length change, element type change, entry-global rebinding, nested loops with break/continue, while, an exception in the continuation, and an exception at the poll with CPython's partial writes.
    • Mutant: never taking the exit crashes the process on the length-change cases. Bounds proven against the old len no longer hold, so the re-check is also memory safety.
    • test_free_threaded: the two whole-call tests now use lists below the interval and explain why. Longer loops are visible chunk by chunk, as interpreted stores are. 25/25.
    • test_regions: the allocation check is one-sided (a call can free other tests' garbage in the full run).
    • Full TypedPython suite locally: 1678 passed. The one failure was that allocation count, now fixed. 27/27.
    • Benchmark compiled-function sets are identical to develop (7 files).
  • Not covered: OSR for ForObj sites (they never occur in a snapshot holder, which must be closed), and functions with virtual objects or regions (no poll, as before).

… interpreter on change (#152, option i)

A function holding a list copy-in or entry globals no longer starves other threads, signals and the
free-threaded stop-the-world for the length of its loops. From the second TP_POLL_INTERVAL chunk on it
writes its lists back, ends their critical section, polls, re-takes it, copies in again and re-checks
lengths, exact element types and entry globals. Unchanged: it continues compiled. Changed: an
OSR-style exit. The frontend generates, per loop, a Python continuation from the function's own
source (resume.py: the rest of the function from that iteration, enclosing loops via a first-iteration
flag, range bounds as evaluated, original lines and name); the impl boxes every live local and calls
it, and the call returns what it returns. The wrapper hands the context to its impl through tp_tls
so only that call polls with those lists. Functions whose locals cannot all be handed over (virtual
objects, regions) keep the no-poll rule.
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