mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
Keep escaped generator frames and wrap asyncgen throw (#8697)
* 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 * Store escaped on iframe; skip unused cold alloc Keep the escaped flag on InterpreterFrame so mark_escaped and has_escaped do not allocate FrameColdData. Clear optional cold fields only when they exist. Transfer frame owner in close() the same way send/throw already do. Assisted-by: Grok:grok-4.6 * Replace escaped flag with take_ownership gi_frame returns the frame object. Close uses a uniquely-referenced check to clear locals or take_ownership onto a husk. frame.clear() finalizes a generator-owned frame and only clears FRAME_OBJECT frames. close() leaves the generator suspended when it yields on GeneratorExit. Assisted-by: Grok:grok-4.6 * Drop frame mutex; steal frame_obj atomically take_ownership stores None in an atomic slot instead of locking and swapping a husk. gi_code reads the kept executable. Resume exclusivity stays on the running claim. Assisted-by: Grok:grok-4.6
This commit is contained in:
1 parent
f18edb72f1
commit
e18facff3e
10 files changed
+121
-147
No files matched your search
Vendored
-1
@@ -130,7 +130,6 @@ class AsyncContextManagerTestCase(unittest.TestCase):
|
||||
raise ZeroDivisionError()
|
||||
self.assertEqual(state, [1, 42, 999])
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@_async_test
|
||||
async def test_contextmanager_traceback(self):
|
||||
@asynccontextmanager
|
||||
|
||||
Vendored
-2
@@ -302,7 +302,6 @@ class GeneratorTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual([1, 2], list(i for i in C()))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False is not true
|
||||
def test_close_clears_frame(self):
|
||||
# gh-142766: Test that closing a generator clears its frame
|
||||
class DetectDelete:
|
||||
@@ -721,7 +720,6 @@ class GeneratorCloseTest(unittest.TestCase):
|
||||
|
||||
# See https://github.com/python/cpython/issues/125723
|
||||
class GeneratorDeallocTest(unittest.TestCase):
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; frame uses shared Arc, no ownership transfer
|
||||
def test_frame_outlives_generator(self):
|
||||
def g1():
|
||||
a = 42
|
||||
|
||||
-5
@@ -2930,17 +2930,14 @@ class TestGetAsyncGenState(unittest.IsolatedAsyncioTestCase):
|
||||
def _asyncgenstate(self):
|
||||
return inspect.getasyncgenstate(self.asyncgen)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
|
||||
def test_created(self):
|
||||
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CREATED)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
|
||||
async def test_suspended(self):
|
||||
value = await anext(self.asyncgen)
|
||||
self.assertEqual(self._asyncgenstate(), inspect.AGEN_SUSPENDED)
|
||||
self.assertEqual(value, 0)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
|
||||
async def test_closed_after_exhaustion(self):
|
||||
countdown = 7
|
||||
with self.assertRaises(StopAsyncIteration):
|
||||
@@ -2949,13 +2946,11 @@ class TestGetAsyncGenState(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(countdown, 1)
|
||||
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
|
||||
async def test_closed_after_immediate_exception(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
await self.asyncgen.athrow(RuntimeError)
|
||||
self.assertEqual(self._asyncgenstate(), inspect.AGEN_CLOSED)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'async_generator' object has no attribute 'ag_suspended'
|
||||
async def test_running(self):
|
||||
async def running_check_asyncgen():
|
||||
for number in range(5):
|
||||
|
||||
@@ -132,14 +132,14 @@ impl PyAsyncGen {
|
||||
|
||||
#[pygetset]
|
||||
fn ag_await(&self, _vm: &VirtualMachine) -> Option<PyObjectRef> {
|
||||
self.inner.frame().yield_from_target()
|
||||
self.inner.frame_opt().and_then(|f| f.yield_from_target())
|
||||
}
|
||||
#[pygetset]
|
||||
fn ag_frame(&self, _vm: &VirtualMachine) -> Option<FrameObjectRef> {
|
||||
if self.inner.closed() {
|
||||
None
|
||||
} else {
|
||||
Some(self.inner.frame())
|
||||
self.inner.frame_opt()
|
||||
}
|
||||
}
|
||||
#[pygetset]
|
||||
@@ -148,7 +148,11 @@ impl PyAsyncGen {
|
||||
}
|
||||
#[pygetset]
|
||||
fn ag_code(&self, _vm: &VirtualMachine) -> PyRef<PyCode> {
|
||||
self.inner.frame().iframe().code().to_owned()
|
||||
self.inner.code()
|
||||
}
|
||||
#[pygetset]
|
||||
fn ag_suspended(&self, _vm: &VirtualMachine) -> bool {
|
||||
self.inner.suspended()
|
||||
}
|
||||
|
||||
#[pyclassmethod]
|
||||
@@ -718,8 +722,6 @@ impl PyAnextAwaitable {
|
||||
if let Some(generator) = wrapped.downcast_ref::<PyGenerator>()
|
||||
&& generator
|
||||
.as_coro()
|
||||
.frame()
|
||||
.iframe()
|
||||
.code()
|
||||
.flags
|
||||
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
|
||||
@@ -841,7 +843,9 @@ impl Destructor for PyAsyncGen {
|
||||
|
||||
impl Drop for PyAsyncGen {
|
||||
fn drop(&mut self) {
|
||||
self.inner.frame().clear_generator();
|
||||
if let Some(frame) = self.inner.frame_opt() {
|
||||
frame.clear_generator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,14 +89,14 @@ impl PyCoroutine {
|
||||
|
||||
#[pygetset]
|
||||
fn cr_await(&self, _vm: &VirtualMachine) -> Option<PyObjectRef> {
|
||||
self.inner.frame().yield_from_target()
|
||||
self.inner.frame_opt().and_then(|f| f.yield_from_target())
|
||||
}
|
||||
#[pygetset]
|
||||
fn cr_frame(&self, _vm: &VirtualMachine) -> Option<FrameObjectRef> {
|
||||
if self.inner.closed() {
|
||||
None
|
||||
} else {
|
||||
Some(self.inner.frame())
|
||||
self.inner.frame_opt()
|
||||
}
|
||||
}
|
||||
#[pygetset]
|
||||
@@ -105,7 +105,7 @@ impl PyCoroutine {
|
||||
}
|
||||
#[pygetset]
|
||||
fn cr_code(&self, _vm: &VirtualMachine) -> PyRef<PyCode> {
|
||||
self.inner.frame().iframe().code().to_owned()
|
||||
self.inner.code()
|
||||
}
|
||||
#[pygetset]
|
||||
fn cr_origin(&self, _vm: &VirtualMachine) -> Option<PyTupleRef> {
|
||||
@@ -179,7 +179,7 @@ impl Destructor for PyCoroutine {
|
||||
if zelf.inner.closed() || zelf.inner.running() {
|
||||
return Ok(());
|
||||
}
|
||||
if zelf.inner.frame().lasti() == 0 {
|
||||
if zelf.inner.frame_opt().is_none_or(|f| f.lasti() == 0) {
|
||||
crate::warn::warn_unawaited_coroutine(zelf.as_object(), &zelf.inner.qualname(), vm);
|
||||
zelf.inner.closed.store(true);
|
||||
return Ok(());
|
||||
@@ -266,7 +266,9 @@ impl IterNext for PyCoroutineWrapper {
|
||||
|
||||
impl Drop for PyCoroutine {
|
||||
fn drop(&mut self) {
|
||||
self.inner.frame().clear_generator();
|
||||
if let Some(frame) = self.inner.frame_opt() {
|
||||
frame.clear_generator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
*/
|
||||
|
||||
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyIntRef, PyStrRef};
|
||||
use super::{PyAsyncGen, PyCode, PyCoroutine, PyDictRef, PyGenerator, PyIntRef, PyStrRef};
|
||||
use crate::{
|
||||
Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
class::PyClassImpl,
|
||||
@@ -778,10 +778,8 @@ impl Py<FrameObject> {
|
||||
);
|
||||
match owner {
|
||||
FrameOwner::Generator => {
|
||||
// Generator frame: check if suspended (lasti > 0 means
|
||||
// FRAME_SUSPENDED). lasti == 0 means FRAME_CREATED and
|
||||
// can be cleared. Finalize the owner so a never-started
|
||||
// coroutine emits its never-awaited warning.
|
||||
// FRAME_SUSPENDED (lasti > 0) cannot be cleared. FRAME_CREATED
|
||||
// and finished frames go through the owner finalizer.
|
||||
if self.lasti() != 0 {
|
||||
return Err(vm.new_runtime_error("cannot clear a suspended frame"));
|
||||
}
|
||||
@@ -790,17 +788,16 @@ impl Py<FrameObject> {
|
||||
let _ = PyCoroutine::del(coro, vm);
|
||||
} else if let Some(async_gen) = owner.downcast_ref::<PyAsyncGen>() {
|
||||
let _ = PyAsyncGen::del(async_gen, vm);
|
||||
} else if let Some(generator) = owner.downcast_ref::<PyGenerator>() {
|
||||
let _ = PyGenerator::del(generator, vm);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
FrameOwner::Thread => {
|
||||
// Thread-owned frame: always executing, cannot clear.
|
||||
return Err(vm.new_runtime_error("cannot clear an executing frame"));
|
||||
}
|
||||
FrameOwner::FrameObject => {
|
||||
// Check if this materialized frame is backed by a live
|
||||
// stack-allocated iframe — if so, the frame is executing.
|
||||
if !self.find_live_source_iframe().is_null() {
|
||||
return Err(vm.new_runtime_error("cannot clear an executing frame"));
|
||||
}
|
||||
@@ -822,27 +819,17 @@ impl Py<FrameObject> {
|
||||
// Clear the evaluation stack and cell references
|
||||
self.clear_stack_and_cells();
|
||||
|
||||
let cold = self.iframe().cold();
|
||||
let temporary_refs = {
|
||||
let mut guard = cold.temporary_refs.lock();
|
||||
core::mem::take(&mut *guard)
|
||||
};
|
||||
let extra_locals = {
|
||||
let mut guard = cold.f_extra_locals.lock();
|
||||
guard.take()
|
||||
};
|
||||
let locals_cache = {
|
||||
let mut guard = cold.f_locals_cache.lock();
|
||||
guard.take()
|
||||
};
|
||||
let overwritten = {
|
||||
let mut guard = cold.f_overwritten_fast_locals.lock();
|
||||
core::mem::take(&mut *guard)
|
||||
};
|
||||
let retained_back = {
|
||||
let mut guard = cold.retained_back.lock();
|
||||
guard.take()
|
||||
};
|
||||
let (temporary_refs, extra_locals, locals_cache, overwritten, retained_back) =
|
||||
match self.iframe().cold_opt() {
|
||||
Some(cold) => (
|
||||
core::mem::take(&mut *cold.temporary_refs.lock()),
|
||||
cold.f_extra_locals.lock().take(),
|
||||
cold.f_locals_cache.lock().take(),
|
||||
core::mem::take(&mut *cold.f_overwritten_fast_locals.lock()),
|
||||
cold.retained_back.lock().take(),
|
||||
),
|
||||
None => (Vec::new(), None, None, Vec::new(), None),
|
||||
};
|
||||
drop((
|
||||
fastlocals,
|
||||
temporary_refs,
|
||||
@@ -858,7 +845,6 @@ impl Py<FrameObject> {
|
||||
#[pygetset]
|
||||
fn f_locals(&self, vm: &VirtualMachine) -> PyResult {
|
||||
if self.uses_locals_proxy(vm)? {
|
||||
self.mark_escaped();
|
||||
let proxy = crate::builtins::FrameLocalsProxy::new(self.to_owned());
|
||||
Ok(proxy.into_ref(&vm.ctx).into())
|
||||
} else {
|
||||
@@ -895,7 +881,6 @@ impl Py<FrameObject> {
|
||||
// Check retained_back for frames whose callers have returned
|
||||
let retained = self.iframe().cold().retained_back.lock().clone();
|
||||
if let Some(frame) = retained {
|
||||
frame.mark_escaped();
|
||||
return Some(frame);
|
||||
}
|
||||
return None;
|
||||
@@ -911,7 +896,6 @@ impl Py<FrameObject> {
|
||||
if core::ptr::eq(cur, prev) {
|
||||
let iframe_ref = unsafe { &*cur };
|
||||
let fo = iframe_ref.materialize(vm);
|
||||
fo.mark_escaped();
|
||||
return Some(fo.to_owned());
|
||||
}
|
||||
cur = unsafe { (*cur).previous() };
|
||||
@@ -921,7 +905,6 @@ impl Py<FrameObject> {
|
||||
// The caller already returned — check retained_back
|
||||
let retained = self.iframe().cold().retained_back.lock().clone();
|
||||
if let Some(frame) = retained {
|
||||
frame.mark_escaped();
|
||||
return Some(frame);
|
||||
}
|
||||
|
||||
@@ -936,13 +919,11 @@ impl Py<FrameObject> {
|
||||
let prev_ref = unsafe { &*prev };
|
||||
// Fast path: already materialized.
|
||||
if let Some(fo) = prev_ref.frame_obj() {
|
||||
fo.mark_escaped();
|
||||
return Some(fo.to_owned());
|
||||
}
|
||||
// Slow path: copy the whole chain, linked through retained_back.
|
||||
// SAFETY: the world is stopped, so the owning thread is parked.
|
||||
let fo = unsafe { prev_ref.materialize_detached_chain(vm) };
|
||||
fo.mark_escaped();
|
||||
return Some(fo);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ impl PyGenerator {
|
||||
if self.inner.closed() {
|
||||
None
|
||||
} else {
|
||||
Some(self.inner.frame())
|
||||
self.inner.frame_opt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +85,12 @@ impl PyGenerator {
|
||||
|
||||
#[pygetset]
|
||||
fn gi_code(&self, _vm: &VirtualMachine) -> PyRef<PyCode> {
|
||||
self.inner.frame().iframe().code().to_owned()
|
||||
self.inner.code()
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn gi_yieldfrom(&self, _vm: &VirtualMachine) -> Option<PyObjectRef> {
|
||||
self.inner.frame().yield_from_target()
|
||||
self.inner.frame_opt().and_then(|f| f.yield_from_target())
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
@@ -159,12 +159,6 @@ impl Destructor for PyGenerator {
|
||||
if zelf.inner.closed() || zelf.inner.running() {
|
||||
return Ok(());
|
||||
}
|
||||
// Generator was never started, just mark as closed
|
||||
if zelf.inner.frame().lasti() == 0 {
|
||||
zelf.inner.closed.store(true);
|
||||
return Ok(());
|
||||
}
|
||||
// Throw GeneratorExit to run finally blocks
|
||||
if let Err(e) = zelf.inner.close(zelf.as_object(), vm) {
|
||||
vm.run_unraisable(e, None, zelf.as_object().to_owned());
|
||||
}
|
||||
@@ -174,7 +168,9 @@ impl Destructor for PyGenerator {
|
||||
|
||||
impl Drop for PyGenerator {
|
||||
fn drop(&mut self) {
|
||||
self.inner.frame().clear_generator();
|
||||
if let Some(frame) = self.inner.frame_opt() {
|
||||
frame.clear_generator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+69
-46
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AsObject, Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
|
||||
builtins::{PyStrRef, PyTupleRef},
|
||||
AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine,
|
||||
builtins::{PyCode, PyStrRef, PyTupleRef},
|
||||
common::lock::PyMutex,
|
||||
exceptions::types::PyBaseException,
|
||||
frame::{ExecutionResult, FrameObject, FrameObjectRef, FrameOwner, InterpreterFrame},
|
||||
@@ -30,10 +30,13 @@ impl ExecutionResult {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Coro {
|
||||
frame: FrameObjectRef,
|
||||
/// Generator iframe, like `gi_iframe`. Cleared to `None` by take_ownership
|
||||
/// the same way `frame_obj` is stolen from the iframe.
|
||||
frame: PyAtomicRef<Option<FrameObject>>,
|
||||
/// `f_executable`; survives `_PyFrame_ClearExceptCode`.
|
||||
code: PyRef<PyCode>,
|
||||
pub closed: AtomicCell<bool>, // TODO: https://github.com/RustPython/RustPython/pull/3183#discussion_r720560652
|
||||
running: AtomicCell<bool>,
|
||||
// code
|
||||
// _weakreflist
|
||||
name: PyMutex<PyStrRef>,
|
||||
qualname: PyMutex<PyStrRef>,
|
||||
@@ -42,7 +45,10 @@ pub struct Coro {
|
||||
|
||||
unsafe impl Traverse for Coro {
|
||||
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
|
||||
self.frame.traverse(tracer_fn);
|
||||
if let Some(frame) = self.frame.deref() {
|
||||
tracer_fn(frame.as_object());
|
||||
}
|
||||
self.code.traverse(tracer_fn);
|
||||
self.name.traverse(tracer_fn);
|
||||
self.qualname.traverse(tracer_fn);
|
||||
if let Some(exc) = self.exception.deref() {
|
||||
@@ -79,8 +85,10 @@ fn gen_name(jen: &PyObject, vm: &VirtualMachine) -> &'static str {
|
||||
|
||||
impl Coro {
|
||||
pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self {
|
||||
let code = frame.iframe().code().to_owned();
|
||||
Self {
|
||||
frame,
|
||||
frame: Some(frame).into(),
|
||||
code,
|
||||
closed: AtomicCell::new(false),
|
||||
running: AtomicCell::new(false),
|
||||
exception: PyAtomicRef::from(None),
|
||||
@@ -89,20 +97,32 @@ impl Coro {
|
||||
}
|
||||
}
|
||||
|
||||
/// Free the finished frame's locals and stack, unless a frame object has
|
||||
/// escaped (e.g. through an `f_locals` proxy or `sys._getframe`). An
|
||||
/// escaped frame husk owns its heap-resident locals and must keep them
|
||||
/// readable after the generator closes.
|
||||
fn clear_frame_locals_on_close(&self) {
|
||||
// Keep locals alive if a durable frame reference escaped (e.g. through
|
||||
// an `f_locals` proxy or `sys._getframe`): that reference now owns the
|
||||
// heap-resident locals and must keep them readable after close,
|
||||
// matching `take_ownership`.
|
||||
if !self.frame.has_escaped() {
|
||||
self.frame.clear_locals_and_stack();
|
||||
/// `_PyFrame_ClearExceptCode`. Clear locals unless another reference
|
||||
/// still holds the frame object; then take_ownership instead.
|
||||
fn clear_except_code(&self) {
|
||||
let Some(frame) = self.frame.deref() else {
|
||||
return;
|
||||
};
|
||||
if frame.as_object().strong_count() == 1 {
|
||||
frame.clear_locals_and_stack();
|
||||
} else {
|
||||
self.take_ownership();
|
||||
}
|
||||
}
|
||||
|
||||
/// Steal the iframe's `frame_obj` pointer (`take_ownership`). The live
|
||||
/// locals stay on that frame object; this generator no longer roots it.
|
||||
fn take_ownership(&self) {
|
||||
let Some(frame) = (unsafe { self.frame.swap(None) }) else {
|
||||
return;
|
||||
};
|
||||
frame.iframe().owner.store(
|
||||
FrameOwner::FrameObject as i8,
|
||||
core::sync::atomic::Ordering::Release,
|
||||
);
|
||||
frame.clear_generator();
|
||||
}
|
||||
|
||||
/// Retire the generator if the frame it just ran came to an end. The claim
|
||||
/// is still held, so a thread waiting for it cannot resume a frame that has
|
||||
/// already finished.
|
||||
@@ -110,14 +130,7 @@ impl Coro {
|
||||
match res {
|
||||
Ok(ExecutionResult::Return(_)) | Err(_) => {
|
||||
self.closed.store(true);
|
||||
// FrameObject is no longer suspended; allow frame.clear() to succeed.
|
||||
self.frame.iframe().owner.store(
|
||||
FrameOwner::FrameObject as i8,
|
||||
core::sync::atomic::Ordering::Release,
|
||||
);
|
||||
// Completed generators/coroutines should not keep their locals
|
||||
// alive while the wrapper object itself remains referenced.
|
||||
self.clear_frame_locals_on_close();
|
||||
self.clear_except_code();
|
||||
}
|
||||
Ok(ExecutionResult::Yield(_)) => {}
|
||||
Ok(ExecutionResult::TailCall) => unreachable!("TailCall in generator/coroutine"),
|
||||
@@ -148,7 +161,8 @@ impl Coro {
|
||||
let gen_exc = unsafe { self.exception.swap(None) };
|
||||
let exception_ptr = &self.exception as *const PyAtomicRef<Option<PyBaseException>>;
|
||||
|
||||
vm.resume_gen_frame(&self.frame, gen_exc, |f| {
|
||||
let frame = self.frame();
|
||||
vm.resume_gen_frame(&frame, gen_exc, |f| {
|
||||
let result = func(f);
|
||||
// SAFETY: exclusive access guaranteed by the claim
|
||||
let _old = unsafe { (*exception_ptr).swap(vm.current_exception()) };
|
||||
@@ -168,8 +182,8 @@ impl Coro {
|
||||
if e.fast_isinstance(vm.ctx.exceptions.stop_iteration) {
|
||||
let err =
|
||||
vm.new_runtime_error(format!("{} raised StopIteration", gen_name(jen, vm)));
|
||||
// PEP 479: chain __context__ as well as __cause__ to match
|
||||
// CPython, which sets both to the original StopIteration.
|
||||
// PEP 479: chain __context__ as well as __cause__ to the
|
||||
// original StopIteration.
|
||||
err.set___context__(Some(e.clone()));
|
||||
err.set___cause__(Some(e));
|
||||
Err(err)
|
||||
@@ -196,7 +210,7 @@ impl Coro {
|
||||
if self.closed.load() {
|
||||
return Self::send_when_closed(jen, vm);
|
||||
}
|
||||
let value = if self.frame.lasti() > 0 {
|
||||
let value = if self.frame_opt().is_some_and(|f| f.lasti() > 0) {
|
||||
Some(vm.ctx.none())
|
||||
} else {
|
||||
None
|
||||
@@ -243,7 +257,7 @@ impl Coro {
|
||||
if self.closed.load() {
|
||||
return Self::send_when_closed(jen, vm);
|
||||
}
|
||||
let value = if self.frame.lasti() > 0 {
|
||||
let value = if self.frame_opt().is_some_and(|f| f.lasti() > 0) {
|
||||
Some(value)
|
||||
} else if !vm.is_none(&value) {
|
||||
return Err(vm.new_type_error(format!(
|
||||
@@ -291,7 +305,7 @@ impl Coro {
|
||||
let result = self.run_claimed(&claim, vm, |f| f.gen_throw(vm, exc_type, exc_val, exc_tb));
|
||||
self.maybe_close(&result, &claim);
|
||||
drop(claim);
|
||||
Ok(result?.into_iter_return(vm))
|
||||
self.finalize_send_result(result, jen, vm)
|
||||
}
|
||||
|
||||
pub fn close(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
@@ -303,9 +317,10 @@ impl Coro {
|
||||
if self.closed.load() {
|
||||
return Ok(vm.ctx.none());
|
||||
}
|
||||
// If generator hasn't started (FRAME_CREATED), just mark as closed
|
||||
if self.frame.lasti() == 0 {
|
||||
// FRAME_CREATED: mark finished and clear the iframe.
|
||||
if self.frame_opt().is_none_or(|f| f.lasti() == 0) {
|
||||
self.closed.store(true);
|
||||
self.clear_except_code();
|
||||
return Ok(vm.ctx.none());
|
||||
}
|
||||
let result = self.run_claimed(&claim, vm, |f| {
|
||||
@@ -316,23 +331,27 @@ impl Coro {
|
||||
vm.ctx.none(),
|
||||
)
|
||||
});
|
||||
self.closed.store(true);
|
||||
// Release frame locals and stack to free references held by the
|
||||
// closed generator, matching gen_send_ex2 with close_on_completion.
|
||||
self.clear_frame_locals_on_close();
|
||||
drop(claim);
|
||||
match result {
|
||||
Ok(ExecutionResult::Yield(_)) => {
|
||||
Err(vm.new_runtime_error(format!("{} ignored GeneratorExit", gen_name(jen, vm))))
|
||||
}
|
||||
Err(e) if !is_gen_exit(&e, vm) => Err(e),
|
||||
Ok(ExecutionResult::Return(value)) => Ok(value),
|
||||
_ => Ok(vm.ctx.none()),
|
||||
other => {
|
||||
self.closed.store(true);
|
||||
self.clear_except_code();
|
||||
match other {
|
||||
Err(e) if !is_gen_exit(&e, vm) => Err(e),
|
||||
Ok(ExecutionResult::Return(value)) => Ok(value),
|
||||
_ => Ok(vm.ctx.none()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suspended(&self) -> bool {
|
||||
!self.closed.load() && !self.running.load() && self.frame.lasti() > 0
|
||||
!self.closed.load()
|
||||
&& !self.running.load()
|
||||
&& self.frame_opt().is_some_and(|f| f.lasti() > 0)
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
@@ -344,7 +363,15 @@ impl Coro {
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> FrameObjectRef {
|
||||
self.frame.clone()
|
||||
self.frame_opt().expect("generator frame")
|
||||
}
|
||||
|
||||
pub fn frame_opt(&self) -> Option<FrameObjectRef> {
|
||||
self.frame.to_owned()
|
||||
}
|
||||
|
||||
pub fn code(&self) -> PyRef<PyCode> {
|
||||
self.code.clone()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> PyStrRef {
|
||||
@@ -434,8 +461,6 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes
|
||||
if obj.downcastable::<PyCoroutine>()
|
||||
|| obj.downcast_ref::<PyGenerator>().is_some_and(|g| {
|
||||
g.as_coro()
|
||||
.frame()
|
||||
.iframe()
|
||||
.code()
|
||||
.flags
|
||||
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
|
||||
@@ -450,8 +475,6 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes
|
||||
if result.downcastable::<PyCoroutine>()
|
||||
|| result.downcast_ref::<PyGenerator>().is_some_and(|g| {
|
||||
g.as_coro()
|
||||
.frame()
|
||||
.iframe()
|
||||
.code()
|
||||
.flags
|
||||
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
|
||||
|
||||
+13
-35
@@ -820,7 +820,6 @@ pub(crate) struct FrameColdData {
|
||||
/// `PyEval_GetLocals()` C API. It is populated only by that adapter.
|
||||
pub f_locals_cache: PyMutex<Option<PyDictRef>>,
|
||||
pub f_overwritten_fast_locals: PyMutex<Vec<PyObjectRef>>,
|
||||
pub escaped: atomic::AtomicBool,
|
||||
pub retained_back: PyMutex<Option<FrameObjectRef>>,
|
||||
pub pending_stack_pops: PyAtomic<u32>,
|
||||
pub pending_unwind_from_stack: PyAtomic<i64>,
|
||||
@@ -842,7 +841,6 @@ impl Default for FrameColdData {
|
||||
f_extra_locals: PyMutex::new(None),
|
||||
f_locals_cache: PyMutex::new(None),
|
||||
f_overwritten_fast_locals: PyMutex::new(Vec::new()),
|
||||
escaped: atomic::AtomicBool::new(false),
|
||||
retained_back: PyMutex::new(None),
|
||||
pending_stack_pops: Default::default(),
|
||||
pending_unwind_from_stack: Default::default(),
|
||||
@@ -1243,7 +1241,6 @@ impl InterpreterFrame {
|
||||
datastack_base: core::ptr::null_mut(),
|
||||
materialized: Radium::new(0),
|
||||
cold: OnceCell::from(Box::new(FrameColdData {
|
||||
escaped: atomic::AtomicBool::new(true),
|
||||
attached_tid: atomic::AtomicU64::new(current_thread_ident()),
|
||||
..FrameColdData::default()
|
||||
})),
|
||||
@@ -1323,10 +1320,7 @@ impl InterpreterFrame {
|
||||
owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
|
||||
datastack_base: core::ptr::null_mut(),
|
||||
materialized: Radium::new(0),
|
||||
cold: OnceCell::from(Box::new(FrameColdData {
|
||||
escaped: atomic::AtomicBool::new(true),
|
||||
..FrameColdData::default()
|
||||
})),
|
||||
cold: OnceCell::new(),
|
||||
};
|
||||
|
||||
let frame_obj = FrameObject {
|
||||
@@ -1770,18 +1764,13 @@ impl FrameObject {
|
||||
.filter_map(Option::take)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let cold = self.iframe().cold();
|
||||
let extra_locals = {
|
||||
let mut guard = cold.f_extra_locals.lock();
|
||||
guard.take()
|
||||
};
|
||||
let locals_cache = {
|
||||
let mut guard = cold.f_locals_cache.lock();
|
||||
guard.take()
|
||||
};
|
||||
let overwritten = {
|
||||
let mut guard = cold.f_overwritten_fast_locals.lock();
|
||||
core::mem::take(&mut *guard)
|
||||
let (extra_locals, locals_cache, overwritten) = match self.iframe().cold_opt() {
|
||||
Some(cold) => (
|
||||
cold.f_extra_locals.lock().take(),
|
||||
cold.f_locals_cache.lock().take(),
|
||||
core::mem::take(&mut *cold.f_overwritten_fast_locals.lock()),
|
||||
),
|
||||
None => (None, None, Vec::new()),
|
||||
};
|
||||
drop((fastlocals, extra_locals, locals_cache, overwritten));
|
||||
}
|
||||
@@ -1853,21 +1842,6 @@ impl FrameObject {
|
||||
core::ptr::null()
|
||||
}
|
||||
|
||||
/// Record that a durable Python-level reference to this frame escaped.
|
||||
pub(crate) fn mark_escaped(&self) {
|
||||
self.iframe()
|
||||
.cold()
|
||||
.escaped
|
||||
.store(true, atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
/// Whether a durable reference to this frame has escaped.
|
||||
pub(crate) fn has_escaped(&self) -> bool {
|
||||
self.iframe()
|
||||
.cold_opt()
|
||||
.is_some_and(|c| c.escaped.load(atomic::Ordering::Acquire))
|
||||
}
|
||||
|
||||
pub fn lasti(&self) -> u32 {
|
||||
self.iframe().lasti.load(Relaxed)
|
||||
}
|
||||
@@ -4152,7 +4126,11 @@ impl ExecutingFrame<'_> {
|
||||
|
||||
// Check if coroutine is already being awaited
|
||||
if let Some(coro) = iter.downcast_ref::<PyCoroutine>()
|
||||
&& coro.as_coro().frame().yield_from_target().is_some()
|
||||
&& coro
|
||||
.as_coro()
|
||||
.frame_opt()
|
||||
.and_then(|f| f.yield_from_target())
|
||||
.is_some()
|
||||
{
|
||||
return Err(vm.new_runtime_error("coroutine is being awaited already"));
|
||||
}
|
||||
|
||||
@@ -990,8 +990,6 @@ pub mod sys {
|
||||
let offset = offset.into_option().unwrap_or(0);
|
||||
let frame_ref = crate::frame::frame_at_offset(offset, vm)
|
||||
.ok_or_else(|| vm.new_value_error("call stack is not deep enough"))?;
|
||||
frame_ref.mark_escaped();
|
||||
|
||||
if let Ok(audit) = vm.sys_module.get_attr("audit", vm) {
|
||||
audit.call((vm.ctx.new_str("sys._getframe"), frame_ref.to_owned()), vm)?;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user