Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 15, 2025

Summary by CodeRabbit

  • Bug Fixes
    • Improved lifecycle management for async generators and coroutines to prevent reuse after completion
    • Enhanced validation of async iterator initialization with clearer error messages when __await__ returns invalid types

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 15, 2025

Walkthrough

The changes introduce lifecycle and state management patterns to async generators and coroutines, adding hook initialization, finalizer invocation, and closed-state tracking to prevent reuse after completion. Additionally, await() result validation is added to enforce iterator protocol compliance.

Changes

Cohort / File(s) Summary
Async Generator Lifecycle Management
crates/vm/src/builtins/asyncgenerator.rs
Adds hook lifecycle tracking (ag_hooks_inited, ag_finalizer fields) with init_hooks() method to read thread-local finalizer and firstiter hooks. Introduces call_finalizer() for guarded finalizer invocation. Updates method signatures (__anext__, asend, athrow, aclose) to return PyResult and initialize hooks before returning awaitables.
Awaitable State Management
crates/vm/src/builtins/asyncgenerator.rs
Adds state field to PyAnextAwaitable with AwaitableState enum for lifecycle tracking. Implements check_closed() validation; send() and throw() now call it and update state to Iter, while close() marks state as Closed.
Coroutine State Protection
crates/vm/src/builtins/coroutine.rs
Adds closed: AtomicCell<bool> field to PyCoroutineWrapper for reuse prevention. Implements check_closed() helper. Methods send(), throw(), and close() now track closed state, with send() and throw() marking closed on StopIteration.
Awaitable Result Validation
crates/vm/src/frame.rs
Adds validation in GetAwaitable handling to ensure __await__() returns an iterator; raises TypeError with message "await() returned non-iterator of type 'X'" if violated.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant VM as VirtualMachine
    participant AG as AsyncGenerator
    participant Hooks as Thread-local Hooks
    participant Awaitable as PyAnextAwaitable
    
    App->>AG: Call __anext__()
    activate AG
    AG->>VM: init_hooks(self, vm)
    activate VM
    VM->>Hooks: Read finalizer & firstiter
    Hooks-->>VM: Return hooks
    VM->>AG: Store finalizer in ag_finalizer
    VM->>VM: Call firstiter (if present)
    VM-->>AG: Hooks initialized
    deactivate VM
    AG->>Awaitable: Create new PyAnextAwaitable(state=Init)
    AG-->>App: Return Awaitable
    deactivate AG
    
    App->>Awaitable: Call send(val)
    activate Awaitable
    Awaitable->>Awaitable: check_closed()?
    alt Not Closed
        Awaitable->>Awaitable: state = Iter
        Awaitable-->>App: Yield value
    else Closed
        Awaitable-->>App: RuntimeError
    end
    deactivate Awaitable
    
    App->>Awaitable: Call close()
    activate Awaitable
    Awaitable->>Awaitable: state = Closed
    Awaitable-->>App: Closed
    deactivate Awaitable
    
    Note over AG,Awaitable: On AsyncGen finalization
    AG->>AG: call_finalizer(self, vm)
    AG->>VM: Invoke stored finalizer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • High-complexity areas: asyncgenerator.rs — dense state management with hook lifecycle, finalizer handling, and multiple method signature changes; requires understanding of thread-local hook semantics and state transitions across PyAnextAwaitable.
  • Moderate-complexity areas: coroutine.rs — similar state pattern but narrower scope; validate consistency with async generator approach.
  • Attention needed: Verify init_hooks() thread-safety and finalizer invocation semantics; ensure closed-state checks prevent all reuse paths; validate __await__() result validation in frame.rs against protocol requirements.

Possibly related PRs

  • PyAnextAwaitable  #6427 — Introduces the PyAnextAwaitable type and qualname support for async generators; this PR extends that work by adding lifecycle hooks, finalizer handling, and state management to the same types.

Poem

🐰 Hooks and finalizers now take flight,
Async generators locked up tight,
Closed-state checks prevent reuse with care,
Lifecycle management everywhere!
The awaitable dance—once, then done.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'sys.set_asyncgen_hook' directly references the main feature being implemented, which is a system hook for async generators based on the code changes introducing async generator hook lifecycle management.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@youknowone youknowone marked this pull request as ready for review December 15, 2025 14:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/vm/src/frame.rs (1)

921-922: Remove redundant local import.

PyIter is already imported at line 15 in the file's global imports block (protocol::{PyIter, PyIterReturn}). This local import is unnecessary.

Apply this diff:

 bytecode::Instruction::GetAwaitable => {
-    use crate::protocol::PyIter;
-
     let awaited_obj = self.pop_value();
crates/vm/src/builtins/asyncgenerator.rs (2)

588-595: Inconsistency: PyAnextAwaitable doesn't close on error unlike PyAsyncGenASend.

In PyAsyncGenASend::send (lines 299-301), when the result is an error, self.close() is called. However, PyAnextAwaitable::send doesn't close on error, only on explicit close() call.

This inconsistency could lead to different behavior when errors occur. Consider aligning the behavior:

 fn send(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult {
     self.check_closed(vm)?;
     self.state.store(AwaitableState::Iter);
     let awaitable = self.get_awaitable_iter(vm)?;
     let result = vm.call_method(&awaitable, "send", (val,));
-    self.handle_result(result, vm)
+    let res = self.handle_result(result, vm);
+    if res.is_err() {
+        self.state.store(AwaitableState::Closed);
+    }
+    res
 }

597-618: Same inconsistency in throw - should close on error.

Similar to send, the throw method doesn't mark the state as closed when an error occurs, unlike PyAsyncGenASend::throw.

 fn throw(
     &self,
     exc_type: PyObjectRef,
     exc_val: OptionalArg,
     exc_tb: OptionalArg,
     vm: &VirtualMachine,
 ) -> PyResult {
     self.check_closed(vm)?;
     self.state.store(AwaitableState::Iter);
     let awaitable = self.get_awaitable_iter(vm)?;
     let result = vm.call_method(
         &awaitable,
         "throw",
         (
             exc_type,
             exc_val.unwrap_or_none(vm),
             exc_tb.unwrap_or_none(vm),
         ),
     );
-    self.handle_result(result, vm)
+    let res = self.handle_result(result, vm);
+    if res.is_err() {
+        self.state.store(AwaitableState::Closed);
+    }
+    res
 }
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 550497e and f19fe28.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_asyncgen.py is excluded by !Lib/**
📒 Files selected for processing (3)
  • crates/vm/src/builtins/asyncgenerator.rs (13 hunks)
  • crates/vm/src/builtins/coroutine.rs (5 hunks)
  • crates/vm/src/frame.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/frame.rs
  • crates/vm/src/builtins/coroutine.rs
  • crates/vm/src/builtins/asyncgenerator.rs
🧬 Code graph analysis (1)
crates/vm/src/builtins/coroutine.rs (1)
crates/vm/src/coroutine.rs (3)
  • new (51-60)
  • send (92-132)
  • close (150-170)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
🔇 Additional comments (8)
crates/vm/src/frame.rs (1)

937-945: LGTM - iterator validation for __await__() result.

The validation correctly enforces that __await__() must return an iterator, raising a TypeError with an appropriate message if not. This aligns with Python's async protocol requirements.

crates/vm/src/builtins/coroutine.rs (3)

60-65: LGTM - closed-state initialization for coroutine wrapper.

The wrapper now correctly initializes with closed: false and tracks whether the coroutine has been awaited to prevent reuse.


159-164: LGTM - reuse prevention check.

The check_closed method correctly prevents reusing an already-awaited coroutine with an appropriate error message.


195-198: LGTM - close marks wrapper as closed first.

Setting closed before delegating to the underlying coroutine ensures the wrapper cannot be reused even if the close operation fails.

crates/vm/src/builtins/asyncgenerator.rs (4)

50-74: LGTM - async generator hook initialization.

The init_hooks method correctly implements one-time initialization of async generator hooks by:

  1. Checking if already initialized (idempotent)
  2. Storing the finalizer from thread-local storage
  3. Calling the firstiter hook if set

This aligns with CPython's async_gen_init_hooks behavior.


141-148: LGTM - hook initialization in __anext__.

Correctly initializes hooks before creating the async send object, ensuring firstiter callback is invoked on first iteration.


524-529: Correct error message for PyAnextAwaitable::check_closed.

The error message correctly identifies the reuse scenario for __anext__()/asend() context.


76-88: Dead code added for future deallocation support.

The call_finalizer method is marked as dead code but appears to be scaffolding for implementing proper async generator finalization during deallocation (per the "= gen_dealloc" comment). This is acceptable for incremental development.

Note: Lines 80-81 use let_chains syntax (if let Some(x) = y && cond), which requires Rust 1.76+. The project's MSRV is 1.89.0, so this is fully supported.

Comment on lines 166 to 175
#[pymethod]
fn send(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
self.coro.send(val, vm)
self.check_closed(vm)?;
let result = self.coro.send(val, vm);
// Mark as closed if exhausted
if let Ok(PyIterReturn::StopIteration(_)) = &result {
self.closed.store(true);
}
result
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a pattern in the codebase for closing on errors
rg -n "closed\.store\(true\)" --type rust -C3

Repository: RustPython/RustPython

Length of output: 4621


🏁 Script executed:

#!/bin/bash
# Get the full coroutine.rs file to understand the structure
wc -l crates/vm/src/builtins/coroutine.rs

Repository: RustPython/RustPython

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Find the Coro struct and understand its send method
rg -n "impl.*Coro" --type rust -A 5 crates/vm/src/builtins/

Repository: RustPython/RustPython

Length of output: 3678


🏁 Script executed:

#!/bin/bash
# Check if there are other methods that handle closing and errors
rg -n "fn.*(&self.*vm.*PyResult" crates/vm/src/builtins/coroutine.rs -A 10

Repository: RustPython/RustPython

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Get the Coro struct and send method implementation
sed -n '40,120p' crates/vm/src/coroutine.rs | head -80

Repository: RustPython/RustPython

Length of output: 2533


🏁 Script executed:

#!/bin/bash
# Find the send method in the Coro struct
rg -n "fn send" crates/vm/src/coroutine.rs -A 15

Repository: RustPython/RustPython

Length of output: 647


🏁 Script executed:

#!/bin/bash
# See the full context of the PyCoroutineWrapper send and throw methods
sed -n '166,200p' crates/vm/src/builtins/coroutine.rs

Repository: RustPython/RustPython

Length of output: 1028


🏁 Script executed:

#!/bin/bash
# Look at the rest of Coro.send to see full error handling
sed -n '92,125p' crates/vm/src/coroutine.rs

Repository: RustPython/RustPython

Length of output: 1455


🏁 Script executed:

#!/bin/bash
# Verify throw method in Coro as well to see if it uses maybe_close
rg -n "pub fn throw" crates/vm/src/coroutine.rs -A 20

Repository: RustPython/RustPython

Length of output: 866


Mark wrapper as closed on errors, not just StopIteration.

The wrapper's closed flag only closes when StopIteration is returned. When send() returns an error, the wrapper remains "open", allowing another call that will unnecessarily pass through check_closed() before the underlying Coro (which already closed itself via maybe_close) returns StopIteration. Close the wrapper on errors to match the underlying Coro's error-handling pattern and align with CPython behavior where a coroutine cannot resume after raising an exception.

🤖 Prompt for AI Agents
In crates/vm/src/builtins/coroutine.rs around lines 166 to 175, the wrapper's
closed flag is only set when send() returns Ok(PyIterReturn::StopIteration(_)),
but it must also be marked closed when send() returns an Err so the wrapper
cannot be resumed after the underlying coroutine has raised; update the logic to
set self.closed.store(true) both for Ok(PyIterReturn::StopIteration(_)) and for
Err(_) (i.e., treat any error as terminal), preserving the original return value
so the error or StopIteration still propagates to the caller.

@youknowone youknowone merged commit 30cc772 into RustPython:main Dec 16, 2025
23 of 24 checks passed
@youknowone youknowone deleted the asyncgen branch December 16, 2025 01:11
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