Skip to content

Commit cddbd72

Browse files
committed
Keep escaped generator frames and wrap asyncgen throw
Mark gi_frame/cr_frame/ag_frame as escaped so locals survive deallocation, and let frame.clear() drop those locals after owner finalize. Route throw() through finalize_send_result so StopAsyncIteration is wrapped like send(). Add ag_suspended and drop never-started frame locals on close(). Assisted-by: Grok:grok-4.6
1 parent 6e3129a commit cddbd72

8 files changed

Lines changed: 20 additions & 21 deletions

File tree

‎Lib/test/test_contextlib_async.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ async def woohoo():
130130
raise ZeroDivisionError()
131131
self.assertEqual(state, [1, 42, 999])
132132

133-
@unittest.expectedFailure # TODO: RUSTPYTHON
134133
@_async_test
135134
async def test_contextmanager_traceback(self):
136135
@asynccontextmanager

‎Lib/test/test_generators.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,6 @@ def __iter__(self):
302302

303303
self.assertEqual([1, 2], list(i for i in C()))
304304

305-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False is not true
306305
def test_close_clears_frame(self):
307306
# gh-142766: Test that closing a generator clears its frame
308307
class DetectDelete:
@@ -721,7 +720,6 @@ def genfn():
721720

722721
# See https://github.com/python/cpython/issues/125723
723722
class GeneratorDeallocTest(unittest.TestCase):
724-
@unittest.expectedFailure # TODO: RUSTPYTHON; frame uses shared Arc, no ownership transfer
725723
def test_frame_outlives_generator(self):
726724
def g1():
727725
a = 42

‎Lib/test/test_inspect/test_inspect.py‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2930,17 +2930,14 @@ def tearDownClass(cls):
29302930
def _asyncgenstate(self):
29312931
return inspect.getasyncgenstate(self.asyncgen)
29322932

2933-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
29342933
def test_created(self):
29352934
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
29362935

2937-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
29382936
async def test_suspended(self):
29392937
value = await anext(self.asyncgen)
29402938
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
29412939
self.assertEqual(value, 0)
29422940

2943-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
29442941
async def test_closed_after_exhaustion(self):
29452942
countdown = 7
29462943
with self.assertRaises(StopAsyncIteration):
@@ -2949,13 +2946,11 @@ async def test_closed_after_exhaustion(self):
29492946
self.assertEqual(countdown, 1)
29502947
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
29512948

2952-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
29532949
async def test_closed_after_immediate_exception(self):
29542950
with self.assertRaises(RuntimeError):
29552951
await self.asyncgen.athrow(RuntimeError)
29562952
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
29572953

2958-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
29592954
async def test_running(self):
29602955
async def running_check_asyncgen():
29612956
for number in range(5):

‎crates/vm/src/builtins/asyncgenerator.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ impl PyAsyncGen {
139139
if self.inner.closed() {
140140
None
141141
} else {
142-
Some(self.inner.frame())
142+
let frame = self.inner.frame();
143+
frame.mark_escaped();
144+
Some(frame)
143145
}
144146
}
145147
#[pygetset]
@@ -150,6 +152,10 @@ impl PyAsyncGen {
150152
fn ag_code(&self, _vm: &VirtualMachine) -> PyRef<PyCode> {
151153
self.inner.frame().iframe().code().to_owned()
152154
}
155+
#[pygetset]
156+
fn ag_suspended(&self, _vm: &VirtualMachine) -> bool {
157+
self.inner.suspended()
158+
}
153159

154160
#[pyclassmethod]
155161
fn __class_getitem__(

‎crates/vm/src/builtins/coroutine.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ impl PyCoroutine {
9696
if self.inner.closed() {
9797
None
9898
} else {
99-
Some(self.inner.frame())
99+
let frame = self.inner.frame();
100+
frame.mark_escaped();
101+
Some(frame)
100102
}
101103
}
102104
#[pygetset]

‎crates/vm/src/builtins/frame.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
*/
44

5-
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyIntRef, PyStrRef};
5+
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef, PyStrRef};
66
use crate::{
77
Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
88
class::PyClassImpl,
@@ -790,9 +790,10 @@ impl Py<FrameObject> {
790790
let _ = PyCoroutine::del(coro, vm);
791791
} else if let Some(async_gen) = owner.downcast_ref::<PyAsyncGen>() {
792792
let _ = PyAsyncGen::del(async_gen, vm);
793+
} else if let Some(generator) = owner.downcast_ref::<PyGenerator>() {
794+
let _ = PyGenerator::del(generator, vm);
793795
}
794796
}
795-
return Ok(());
796797
}
797798
FrameOwner::Thread => {
798799
// Thread-owned frame: always executing, cannot clear.

‎crates/vm/src/builtins/generator.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ impl PyGenerator {
7474
if self.inner.closed() {
7575
None
7676
} else {
77-
Some(self.inner.frame())
77+
let frame = self.inner.frame();
78+
frame.mark_escaped();
79+
Some(frame)
7880
}
7981
}
8082

@@ -159,12 +161,6 @@ impl Destructor for PyGenerator {
159161
if zelf.inner.closed() || zelf.inner.running() {
160162
return Ok(());
161163
}
162-
// Generator was never started, just mark as closed
163-
if zelf.inner.frame().lasti() == 0 {
164-
zelf.inner.closed.store(true);
165-
return Ok(());
166-
}
167-
// Throw GeneratorExit to run finally blocks
168164
if let Err(e) = zelf.inner.close(zelf.as_object(), vm) {
169165
vm.run_unraisable(e, None, zelf.as_object().to_owned());
170166
}

‎crates/vm/src/coroutine.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ impl Coro {
291291
let result = self.run_claimed(&claim, vm, |f| f.gen_throw(vm, exc_type, exc_val, exc_tb));
292292
self.maybe_close(&result, &claim);
293293
drop(claim);
294-
Ok(result?.into_iter_return(vm))
294+
self.finalize_send_result(result, jen, vm)
295295
}
296296

297297
pub fn close(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
@@ -303,9 +303,11 @@ impl Coro {
303303
if self.closed.load() {
304304
return Ok(vm.ctx.none());
305305
}
306-
// If generator hasn't started (FRAME_CREATED), just mark as closed
306+
// If generator hasn't started (FRAME_CREATED), mark as closed and
307+
// drop frame locals so argument objects can be collected.
307308
if self.frame.lasti() == 0 {
308309
self.closed.store(true);
310+
self.clear_frame_locals_on_close();
309311
return Ok(vm.ctx.none());
310312
}
311313
let result = self.run_claimed(&claim, vm, |f| {

0 commit comments

Comments
 (0)