Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 12, 2025

Summary by CodeRabbit

  • New Features

    • Added qualname support for generators, coroutines, and async generators for improved introspection.
    • Introduced a new awaitable wrapper for anext(default) so anext(..., default) returns an awaitable that maps StopAsyncIteration to StopIteration(default).
  • Bug Fixes / Improvements

    • Stricter async-iterator protocol checks with clearer error messages when objects lack or misuse aiter/anext.
  • Chores

    • Added a new spelling token to the project dictionary.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 12, 2025

Walkthrough

Adds qualname storage/accessors for Coro-derived objects, introduces a new PyAnextAwaitable type and registers it, updates builtins.anext to wrap awaitables when a default is provided, and tightens async-iterator protocol checks in get_aiter().

Changes

Cohort / File(s) Summary
Core Coro qualname field
crates/vm/src/coroutine.rs
Added qualname: PyMutex<PyStrRef>, updated Coro::new(frame, name, qualname), and added qualname() / set_qualname() accessors.
Generator / Coroutine / AsyncGen constructors & accessors
crates/vm/src/builtins/generator.rs, crates/vm/src/builtins/coroutine.rs, crates/vm/src/builtins/asyncgenerator.rs
Threaded qualname through constructors to Coro::new(...); added __qualname__ getter and set___qualname__ setter on PyGenerator, PyCoroutine, and PyAsyncGen.
anext awaitable wrapper type
crates/vm/src/builtins/asyncgenerator.rs
Introduced PyAnextAwaitable type with __await__, send/throw/close, StopAsyncIteration→StopIteration(default) handling, iteration protocol support, and class registration.
anext builtin behavior
crates/vm/src/stdlib/builtins.rs
Validates presence of __anext__; when a default is provided, wraps the awaitable returned by __anext__ in PyAnextAwaitable; otherwise returns raw awaitable.
get_aiter protocol validation
crates/vm/src/protocol/object.rs
Reworked get_aiter() to call __aiter__, reject coroutine results, and verify the result has __anext__, emitting specific TypeErrors on failure.
Function constructor call sites
crates/vm/src/builtins/function.rs
Updated call sites to pass qualname into PyGenerator::new, PyCoroutine::new, and PyAsyncGen::new.
Type registration
crates/vm/src/types/zoo.rs
Added anext_awaitable: &'static Py<PyType> to TypeZoo and initialized it via PyAnextAwaitable::init_builtin_type().
Spell dictionary
.cspell.dict/python-more.txt
Added token anextawaitable.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant Builtin as builtins.anext
    participant Obj as AsyncIterableObj
    participant Awaitable as Awaitable (from __anext__)
    participant Wrapper as PyAnextAwaitable

    Caller->>Builtin: anext(obj, default?)
    Builtin->>Obj: check __anext__ exists
    alt no __anext__
        Builtin-->>Caller: TypeError (not async iterator)
    else has __anext__
        Builtin->>Obj: call __anext__()
        Obj-->>Awaitable: returns awaitable
        alt default provided
            Builtin->>Wrapper: PyAnextAwaitable::new(awaitable, default)
            Builtin-->>Caller: return Wrapper
            Caller->>Wrapper: await (calls __await__/send/throw)
            Wrapper->>Awaitable: delegate send/throw/close
            alt Awaitable raises StopAsyncIteration
                Wrapper-->>Caller: raise StopIteration(default)
            else Awaitable yields value
                Wrapper-->>Caller: return value
            end
        else no default
            Builtin-->>Caller: return Awaitable
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

  • Pay special attention to: PyAnextAwaitable correctness (await protocol, error mapping), all constructor call sites passing qualname, and get_aiter() error messages and coroutine-detection logic.

Possibly related PRs

Suggested reviewers

  • arihant2math

🐰
I hopped through frames and names so fine,
Wrapped anext in a cozy line.
Qualnames threaded through each corer,
Async hops now yield no more chore. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ⚠️ Warning The title 'PyAnextAwaitable' is overly narrow and does not capture the broader scope of changes, which include qualname support across multiple coroutine/generator types and protocol improvements. Expand the title to reflect the main objectives, such as 'Add qualname support and PyAnextAwaitable wrapper for async generators' or similar to better represent the full scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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.

@github-actions
Copy link
Contributor

Code has been automatically formatted

The code in this PR has been formatted using cargo fmt --all.
Please pull the latest changes before pushing again:

git pull origin asyncgen

@youknowone youknowone force-pushed the asyncgen branch 3 times, most recently from 7fe0cdb to 0199879 Compare December 13, 2025 02:11
@youknowone youknowone marked this pull request as ready for review December 14, 2025 00:57
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: 0

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

106-110: Consider including the actual type name in the error message.

The error message uses a hardcoded string 'async_iterator' rather than the actual type name. CPython typically includes the actual type in such messages.

         // Check that __aiter__ did not return a coroutine
         if iterator.downcast_ref::<PyCoroutine>().is_some() {
-            return Err(vm.new_type_error(
-                "'async_iterator' object cannot be interpreted as an async iterable; \
-                perhaps you forgot to call aiter()?",
-            ));
+            return Err(vm.new_type_error(format!(
+                "'{}' object cannot be interpreted as an async iterable; \
+                perhaps you forgot to call aiter()?",
+                iterator.class().name()
+            )));
         }
crates/vm/src/builtins/asyncgenerator.rs (2)

469-522: Redundant coroutine check at line 509.

The downcast_ref::<PyCoroutine>() check on line 509 is unreachable for actual coroutines because line 499 already handles the coroutine_type case and returns early. This check would only catch objects that have a __await__ method returning a PyCoroutine, which is already an error condition.

Consider clarifying the intent or removing the redundant check:

-        // Check the result is an iterator, not a coroutine
-        if awaitable.downcast_ref::<PyCoroutine>().is_some() {
-            return Err(vm.new_type_error("__await__() returned a coroutine"));
-        }
+        // Check the __await__ result is not a coroutine (invalid usage)
+        if awaitable.class().is(vm.ctx.types.coroutine_type) {
+            return Err(vm.new_type_error("__await__() returned a coroutine"));
+        }

552-558: Consider propagating close errors instead of silently ignoring them.

The close() method silently ignores both errors from get_awaitable_iter() and from the inner close() call. While ignoring get_awaitable_iter errors may be intentional (the awaitable may already be exhausted), silently ignoring close() errors could hide legitimate issues.

     #[pymethod]
     fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
         if let Ok(awaitable) = self.get_awaitable_iter(vm) {
-            let _ = vm.call_method(&awaitable, "close", ());
+            // Ignore errors from close() as is typical for cleanup methods,
+            // but the awaitable may not have a close() method
+            if let Some(close_method) = vm.get_method(awaitable.clone(), identifier!(vm, close)) {
+                let _ = close_method?.call((), vm);
+            }
         }
         Ok(())
     }
📜 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 835918a and 571539d.

⛔ Files ignored due to path filters (3)
  • Lib/test/test_asyncgen.py is excluded by !Lib/**
  • Lib/test/test_generators.py is excluded by !Lib/**
  • Lib/test/test_types.py is excluded by !Lib/**
📒 Files selected for processing (9)
  • .cspell.dict/python-more.txt (1 hunks)
  • crates/vm/src/builtins/asyncgenerator.rs (3 hunks)
  • crates/vm/src/builtins/coroutine.rs (3 hunks)
  • crates/vm/src/builtins/function.rs (1 hunks)
  • crates/vm/src/builtins/generator.rs (2 hunks)
  • crates/vm/src/coroutine.rs (3 hunks)
  • crates/vm/src/protocol/object.rs (2 hunks)
  • crates/vm/src/stdlib/builtins.rs (1 hunks)
  • crates/vm/src/types/zoo.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/stdlib/builtins.rs
  • crates/vm/src/types/zoo.rs
  • crates/vm/src/builtins/coroutine.rs
  • crates/vm/src/builtins/function.rs
  • crates/vm/src/builtins/generator.rs
  • crates/vm/src/coroutine.rs
  • crates/vm/src/protocol/object.rs
  • crates/vm/src/builtins/asyncgenerator.rs
🧬 Code graph analysis (5)
crates/vm/src/types/zoo.rs (1)
crates/vm/src/class.rs (1)
  • init_builtin_type (39-48)
crates/vm/src/builtins/coroutine.rs (2)
crates/vm/src/builtins/generator.rs (4)
  • new (35-39)
  • __qualname__ (52-54)
  • set___qualname__ (57-59)
  • close (112-114)
crates/vm/src/coroutine.rs (5)
  • new (51-60)
  • frame (180-182)
  • name (184-186)
  • qualname (192-194)
  • close (150-170)
crates/vm/src/builtins/function.rs (2)
crates/vm/src/builtins/coroutine.rs (1)
  • new (32-36)
crates/vm/src/builtins/generator.rs (1)
  • new (35-39)
crates/vm/src/builtins/generator.rs (3)
crates/vm/src/builtins/asyncgenerator.rs (4)
  • new (36-41)
  • new (455-460)
  • __qualname__ (54-56)
  • set___qualname__ (59-61)
crates/vm/src/builtins/coroutine.rs (3)
  • new (32-36)
  • __qualname__ (49-51)
  • set___qualname__ (54-56)
crates/vm/src/builtins/property.rs (1)
  • setter (249-255)
crates/vm/src/protocol/object.rs (1)
crates/vm/src/builtins/asyncgenerator.rs (5)
  • __aiter__ (89-91)
  • class (25-27)
  • class (156-158)
  • class (206-208)
  • __anext__ (94-96)
🔇 Additional comments (21)
.cspell.dict/python-more.txt (1)

7-7: LGTM!

The dictionary entry is correctly alphabetized and corresponds to the new PyAnextAwaitable type introduced in this PR.

crates/vm/src/types/zoo.rs (2)

24-24: LGTM!

The anext_awaitable field is properly added to TypeZoo and follows the established pattern for type registration, grouped appropriately with other async generator related types.


143-143: LGTM!

Initialization correctly uses init_builtin_type() consistent with other type registrations in this file.

crates/vm/src/stdlib/builtins.rs (1)

543-561: LGTM!

The anext function implementation correctly:

  1. Validates that the object is an async iterator by checking for __anext__
  2. Calls __anext__ to get the awaitable
  3. Wraps the result in PyAnextAwaitable when a default value is provided
  4. Returns the raw awaitable when no default is given

The error message appropriately mirrors CPython's behavior for non-async-iterator objects.

crates/vm/src/builtins/function.rs (1)

428-436: LGTM!

The updated constructor calls correctly pass both __name__() and __qualname__() to PyGenerator, PyCoroutine, and PyAsyncGen, aligning with the PR objective to propagate qualified names throughout the generator/coroutine hierarchy.

crates/vm/src/protocol/object.rs (1)

90-120: Enhanced async iterable protocol validation looks good.

The implementation properly:

  1. Checks for __aiter__ presence before calling
  2. Detects incorrectly defined async def __aiter__ methods that return coroutines
  3. Validates the result is an async iterator by checking for __anext__
crates/vm/src/builtins/coroutine.rs (3)

32-36: LGTM!

The constructor update correctly propagates the new qualname parameter to Coro::new, maintaining consistency with PyGenerator::new and PyAsyncGen::new.


48-56: LGTM!

The __qualname__ getter and setter follow the same pattern as the existing __name__ accessors and correctly delegate to the inner Coro methods.


169-173: LGTM!

The close method correctly delegates to the inner coroutine's close method, completing the wrapper's lifecycle management API alongside the existing send and throw methods.

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

35-35: LGTM!

The new qualname field appropriately uses PyMutex<PyStrRef> for thread-safe access, consistent with the existing name field.


51-58: LGTM!

The constructor properly initializes the new qualname field alongside existing fields.


192-198: LGTM!

The qualname() getter and set_qualname() setter follow the established pattern of the name() and set_name() methods.

crates/vm/src/builtins/generator.rs (2)

35-39: LGTM!

The constructor update correctly propagates qualname to Coro::new, consistent with the parallel changes in PyCoroutine and PyAsyncGen.


51-59: LGTM!

The __qualname__ getter and setter correctly delegate to the inner Coro methods, following the same pattern as the __name__ accessors.

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

36-41: LGTM!

The constructor update correctly propagates qualname to Coro::new, consistent with the parallel changes in PyCoroutine and PyGenerator.


53-61: LGTM!

The __qualname__ getter and setter correctly delegate to the inner Coro methods, maintaining consistency with the other generator types.


437-460: LGTM!

The PyAnextAwaitable struct is well-documented and follows the established pattern for implementing Python types in RustPython.


561-569: LGTM!

The handle_result method correctly converts StopAsyncIteration to StopIteration(default_value), which is the core functionality needed for anext() with a default value.


572-577: LGTM!

The SelfIter and IterNext implementations follow the established pattern used by other async generator helper types.


583-583: LGTM!

The new PyAnextAwaitable type is properly registered in the init function.


524-549: get_awaitable_iter() is called on every send()/throw() invocation, but this is correct and necessary.

PyAnextAwaitable wraps arbitrary async iterators (unlike PyAsyncGenASend which handles only async generators directly). For the common case of wrapping async generators, get_awaitable_iter() returns immediately via an early check. For coroutines and other awaitables, the method must call __await__() fresh on each invocation per PEP 492 requirements — caching the result would violate the protocol by reusing an exhausted iterator. The current design correctly handles all awaitable types without unnecessary overhead.

Likely an incorrect or invalid review comment.

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: 4

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

200-207: Consider using qualname (not name) in Coro::repr() for CPython parity.

If RustPython aims to match CPython’s <coroutine object outer.<locals>.inner at ...> style, self.qualname is typically the right display name (and would respect __qualname__ mutation as well).

📜 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 571539d and 455d3f6.

⛔ Files ignored due to path filters (3)
  • Lib/test/test_asyncgen.py is excluded by !Lib/**
  • Lib/test/test_generators.py is excluded by !Lib/**
  • Lib/test/test_types.py is excluded by !Lib/**
📒 Files selected for processing (9)
  • .cspell.dict/python-more.txt (1 hunks)
  • crates/vm/src/builtins/asyncgenerator.rs (3 hunks)
  • crates/vm/src/builtins/coroutine.rs (3 hunks)
  • crates/vm/src/builtins/function.rs (1 hunks)
  • crates/vm/src/builtins/generator.rs (2 hunks)
  • crates/vm/src/coroutine.rs (3 hunks)
  • crates/vm/src/protocol/object.rs (2 hunks)
  • crates/vm/src/stdlib/builtins.rs (1 hunks)
  • crates/vm/src/types/zoo.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/vm/src/types/zoo.rs
  • .cspell.dict/python-more.txt
  • crates/vm/src/builtins/function.rs
  • crates/vm/src/stdlib/builtins.rs
🧰 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/protocol/object.rs
  • crates/vm/src/builtins/coroutine.rs
  • crates/vm/src/builtins/asyncgenerator.rs
  • crates/vm/src/builtins/generator.rs
  • crates/vm/src/coroutine.rs
🧬 Code graph analysis (3)
crates/vm/src/protocol/object.rs (2)
crates/vm/src/builtins/asyncgenerator.rs (5)
  • __aiter__ (89-91)
  • class (25-27)
  • class (156-158)
  • class (206-208)
  • __anext__ (94-96)
crates/vm/src/coroutine.rs (1)
  • name (184-186)
crates/vm/src/builtins/coroutine.rs (4)
crates/vm/src/builtins/asyncgenerator.rs (7)
  • new (36-41)
  • new (455-460)
  • __qualname__ (54-56)
  • set___qualname__ (59-61)
  • close (277-279)
  • close (401-403)
  • close (553-558)
crates/vm/src/builtins/function.rs (6)
  • new (58-91)
  • new (789-791)
  • new (911-915)
  • __qualname__ (567-569)
  • __qualname__ (836-855)
  • set___qualname__ (572-585)
crates/vm/src/builtins/generator.rs (4)
  • new (35-39)
  • __qualname__ (52-54)
  • set___qualname__ (57-59)
  • close (112-114)
crates/vm/src/coroutine.rs (5)
  • new (51-60)
  • frame (180-182)
  • name (184-186)
  • qualname (192-194)
  • close (150-170)
crates/vm/src/coroutine.rs (4)
crates/vm/src/builtins/asyncgenerator.rs (2)
  • new (36-41)
  • new (455-460)
crates/vm/src/builtins/coroutine.rs (1)
  • new (32-36)
crates/vm/src/builtins/function.rs (3)
  • new (58-91)
  • new (789-791)
  • new (911-915)
crates/vm/src/builtins/generator.rs (1)
  • new (35-39)
⏰ 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). (9)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
🔇 Additional comments (9)
crates/vm/src/builtins/coroutine.rs (3)

32-56: Qualname plumbing on PyCoroutine looks consistent with Coro and other generator-like types.


169-173: Wrapper close() delegation is a good parity improvement with generator wrappers.


32-36: All call sites have been properly updated to the new constructor signature.

The search for any remaining calls to PyCoroutine::new with the old 2-parameter signature found no matches, confirming that all call sites throughout the codebase (including tests and stdlib) have been updated to use the new 3-parameter signature (frame: FrameRef, name: PyStrRef, qualname: PyStrRef).

crates/vm/src/coroutine.rs (1)

27-60: Coro qualname storage/accessors are straightforward and match the intended propagation pattern.

Also applies to: 192-198

crates/vm/src/builtins/generator.rs (2)

35-60: Generator qualname API mirrors coroutine changes cleanly.


35-39: All call sites of PyGenerator::new are properly updated. The constructor at lines 35-39 accepts three parameters (frame, name, qualname), and the single call site in crates/vm/src/builtins/function.rs:429 correctly passes all three arguments.

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

36-61: Async generator qualname changes align with the generator/coroutine pattern.


579-584: Type registration wiring looks correct (extend_class for anext_awaitable).


1-12: Please ensure cargo fmt + cargo clippy are clean for these protocol/runtime changes.

Also applies to: 579-584

Comment on lines +437 to +577
/// Awaitable wrapper for anext() builtin with default value.
/// When StopAsyncIteration is raised, it converts it to StopIteration(default).
#[pyclass(module = false, name = "anext_awaitable")]
#[derive(Debug)]
pub struct PyAnextAwaitable {
wrapped: PyObjectRef,
default_value: PyObjectRef,
}

impl PyPayload for PyAnextAwaitable {
#[inline]
fn class(ctx: &Context) -> &'static Py<PyType> {
ctx.types.anext_awaitable
}
}

#[pyclass(with(IterNext, Iterable))]
impl PyAnextAwaitable {
pub fn new(wrapped: PyObjectRef, default_value: PyObjectRef) -> Self {
Self {
wrapped,
default_value,
}
}

#[pymethod(name = "__await__")]
fn r#await(zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyRef<Self> {
zelf
}

/// Get the awaitable iterator from wrapped object.
// = anextawaitable_getiter.
fn get_awaitable_iter(&self, vm: &VirtualMachine) -> PyResult {
use crate::builtins::PyCoroutine;
use crate::protocol::PyIter;

let wrapped = &self.wrapped;

// If wrapped is already an async_generator_asend, it's an iterator
if wrapped.class().is(vm.ctx.types.async_generator_asend)
|| wrapped.class().is(vm.ctx.types.async_generator_athrow)
{
return Ok(wrapped.clone());
}

// _PyCoro_GetAwaitableIter equivalent
let awaitable = if wrapped.class().is(vm.ctx.types.coroutine_type) {
// Coroutine - get __await__ later
wrapped.clone()
} else {
// Try to get __await__ method
if let Some(await_method) = vm.get_method(wrapped.clone(), identifier!(vm, __await__)) {
await_method?.call((), vm)?
} else {
return Err(vm.new_type_error(format!(
"object {} can't be used in 'await' expression",
wrapped.class().name()
)));
}
};

// If awaitable is a coroutine, get its __await__
if awaitable.class().is(vm.ctx.types.coroutine_type) {
let coro_await = vm.call_method(&awaitable, "__await__", ())?;
// Check that __await__ returned an iterator
if !PyIter::check(&coro_await) {
return Err(vm.new_type_error("__await__ returned a non-iterable"));
}
return Ok(coro_await);
}

// Check the result is an iterator, not a coroutine
if awaitable.downcast_ref::<PyCoroutine>().is_some() {
return Err(vm.new_type_error("__await__() returned a coroutine"));
}

// Check that the result is an iterator
if !PyIter::check(&awaitable) {
return Err(vm.new_type_error(format!(
"__await__() returned non-iterator of type '{}'",
awaitable.class().name()
)));
}

Ok(awaitable)
}

#[pymethod]
fn send(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let awaitable = self.get_awaitable_iter(vm)?;
let result = vm.call_method(&awaitable, "send", (val,));
self.handle_result(result, vm)
}

#[pymethod]
fn throw(
&self,
exc_type: PyObjectRef,
exc_val: OptionalArg,
exc_tb: OptionalArg,
vm: &VirtualMachine,
) -> PyResult {
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)
}

#[pymethod]
fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
if let Ok(awaitable) = self.get_awaitable_iter(vm) {
let _ = vm.call_method(&awaitable, "close", ());
}
Ok(())
}

/// Convert StopAsyncIteration to StopIteration(default_value)
fn handle_result(&self, result: PyResult, vm: &VirtualMachine) -> PyResult {
match result {
Ok(value) => Ok(value),
Err(exc) if exc.fast_isinstance(vm.ctx.exceptions.stop_async_iteration) => {
Err(vm.new_stop_iteration(Some(self.default_value.clone())))
}
Err(exc) => Err(exc),
}
}
}

impl SelfIter for PyAnextAwaitable {}
impl IterNext for PyAnextAwaitable {
fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
PyIterReturn::from_pyresult(zelf.send(vm.ctx.none(), vm), vm)
}
}
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 | 🔴 Critical

PyAnextAwaitable is likely functionally wrong: it recreates the underlying __await__ iterator each .send() / .throw() call.

That breaks iterator state (and can repeat side effects) because __await__() is expected to be called once, and the returned iterator is then driven until completion. Cache the iterator inside PyAnextAwaitable (and mark “closed” after completion) like other awaitables in this file.

Minimal direction (illustrative diff sketch; adjust imports/types as needed):

 pub struct PyAnextAwaitable {
     wrapped: PyObjectRef,
     default_value: PyObjectRef,
+    await_iter: crate::common::lock::PyMutex<Option<PyObjectRef>>,
+    state: AtomicCell<AwaitableState>,
 }

 impl PyAnextAwaitable {
     pub fn new(wrapped: PyObjectRef, default_value: PyObjectRef) -> Self {
         Self {
             wrapped,
             default_value,
+            await_iter: crate::common::lock::PyMutex::new(None),
+            state: AtomicCell::new(AwaitableState::Init),
         }
     }

-    fn get_awaitable_iter(&self, vm: &VirtualMachine) -> PyResult {
+    fn get_awaitable_iter(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
+        if let AwaitableState::Closed = self.state.load() {
+            return Err(vm.new_runtime_error("cannot reuse already awaited anext()"));
+        }
+        if let Some(it) = self.await_iter.lock().clone() {
+            return Ok(it);
+        }
         ...
-        Ok(awaitable)
+        *self.await_iter.lock() = Some(awaitable.clone());
+        self.state.store(AwaitableState::Iter);
+        Ok(awaitable)
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
crates/vm/src/builtins/asyncgenerator.rs around lines 437 to 577: the current
implementation calls __await__ (or constructs the awaitable iterator) on every
send/throw which recreates the iterator and breaks iterator state; change
PyAnextAwaitable to hold an Option<PyObjectRef> field (e.g., awaitable:
Option<PyObjectRef>) to cache the iterator, initialize that field the first time
get_awaitable_iter is called, return the cached iterator on subsequent calls,
and set it to None/mark closed when the iterator completes (StopAsyncIteration)
or when close() is called; update get_awaitable_iter, send, throw and close to
use and mutate this cached field appropriately, following the same ownership and
VM error handling patterns used by other awaitable types in the file.

Comment on lines +524 to +550
#[pymethod]
fn send(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let awaitable = self.get_awaitable_iter(vm)?;
let result = vm.call_method(&awaitable, "send", (val,));
self.handle_result(result, vm)
}

#[pymethod]
fn throw(
&self,
exc_type: PyObjectRef,
exc_val: OptionalArg,
exc_tb: OptionalArg,
vm: &VirtualMachine,
) -> PyResult {
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)
}
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 | 🟠 Major

PyAnextAwaitable assumes the await-iterator has send/throw; CPython allows plain iterators from __await__.

If RustPython’s await-driving machinery can handle plain iterators, PyAnextAwaitable.send() should fall back to advancing via __next__ (when val is None) if there’s no send, and throw() should propagate if there’s no throw—otherwise this wrapper will reject valid __await__ implementations.

Comment on lines 89 to 121
pub fn get_aiter(&self, vm: &VirtualMachine) -> PyResult {
if self.downcastable::<PyAsyncGen>() {
vm.call_special_method(self, identifier!(vm, __aiter__), ())
} else {
Err(vm.new_type_error("wrong argument type"))
use crate::builtins::PyCoroutine;

// Check if object has __aiter__ method
let aiter_method = self.class().get_attr(identifier!(vm, __aiter__));
let Some(_aiter_method) = aiter_method else {
return Err(vm.new_type_error(format!(
"'{}' object is not an async iterable",
self.class().name()
)));
};

// Call __aiter__
let iterator = vm.call_special_method(self, identifier!(vm, __aiter__), ())?;

// Check that __aiter__ did not return a coroutine
if iterator.downcast_ref::<PyCoroutine>().is_some() {
return Err(vm.new_type_error(
"'async_iterator' object cannot be interpreted as an async iterable; \
perhaps you forgot to call aiter()?",
));
}

// Check that the result is an async iterator (has __anext__)
if !iterator.class().has_attr(identifier!(vm, __anext__)) {
return Err(vm.new_type_error(format!(
"'{}' object is not an async iterator",
iterator.class().name()
)));
}

Ok(iterator)
}
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 | 🟠 Major

Async-iterator validation is too strict: __anext__ check should be on the object, not only on iterator.class().

iterator.class().has_attr(__anext__) can reject valid async iterators that provide __anext__ dynamically (e.g., via instance __getattr__). Prefer checking attribute access on the instance (iterator.as_object().has_attr(..., vm)?) or attempt to resolve/call __anext__ via the VM to match Python’s protocol behavior.

Suggested patch (illustrative):

-        if !iterator.class().has_attr(identifier!(vm, __anext__)) {
+        if !iterator.as_object().has_attr(identifier!(vm, __anext__), vm)? {
             return Err(vm.new_type_error(format!(
                 "'{}' object is not an async iterator",
                 iterator.class().name()
             )));
         }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +92 to +110
// Check if object has __aiter__ method
let aiter_method = self.class().get_attr(identifier!(vm, __aiter__));
let Some(_aiter_method) = aiter_method else {
return Err(vm.new_type_error(format!(
"'{}' object is not an async iterable",
self.class().name()
)));
};

// Call __aiter__
let iterator = vm.call_special_method(self, identifier!(vm, __aiter__), ())?;

// Check that __aiter__ did not return a coroutine
if iterator.downcast_ref::<PyCoroutine>().is_some() {
return Err(vm.new_type_error(
"'async_iterator' object cannot be interpreted as an async iterable; \
perhaps you forgot to call aiter()?",
));
}
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

Coroutine-return check likely needs isinstance(coroutine_type) (not payload downcast) for completeness.

downcast_ref::<PyCoroutine>() may miss coroutine subclasses / alternative coroutine-like objects; if the intent is “returned a coroutine object”, prefer a type-based fast_isinstance(vm.ctx.types.coroutine_type) style check (or equivalent) for parity with other protocol checks.

🤖 Prompt for AI Agents
In crates/vm/src/protocol/object.rs around lines 92 to 110, the code checks
whether the result of __aiter__ is a coroutine by using
downcast_ref::<PyCoroutine>(), which misses coroutine subclasses or alternative
coroutine-like objects; replace that payload downcast with a type-based
isinstance check (e.g., use the VM's fast/isinstance helper against
vm.ctx.types.coroutine_type or equivalent) so the check catches coroutine
subclasses and matches other protocol checks, keeping the same error
path/message when the check is true.

@youknowone youknowone changed the title more asyncgen impl PyAnextAwaitable Dec 14, 2025
@youknowone youknowone merged commit b3c2aa6 into RustPython:main Dec 14, 2025
13 checks passed
@youknowone youknowone deleted the asyncgen branch December 14, 2025 03:15
@coderabbitai coderabbitai bot mentioned this pull request Dec 15, 2025
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