mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Implement generator/coroutine lifecycle, tracing, and error handling
- Add interactive REPL mode: auto-print expression results in single mode - Implement Destructor for PyGenerator and PyCoroutine - Add locals_dirty tracking and locals_to_fast() for frame sync - Add per-line tracing with prev_line tracking in execution loop - Fix gen_throw to close sub-iterator on GeneratorExit (gen_close_iter) - Pass callable object as arg in c_call/c_return/c_exception trace events - Distinguish [Errno] vs [WinError] for CRT vs Win32 API errors - Fix tee thread safety with AtomicBool running flag - Fix division error messages to match expected format
This commit is contained in:
@@ -151,6 +151,9 @@ struct Compiler {
|
||||
ctx: CompileContext,
|
||||
opts: CompileOpts,
|
||||
in_annotation: bool,
|
||||
/// True when compiling in "single" (interactive) mode.
|
||||
/// Expression statements at module scope emit CALL_INTRINSIC_1(Print).
|
||||
interactive: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -461,6 +464,7 @@ impl Compiler {
|
||||
},
|
||||
opts,
|
||||
in_annotation: false,
|
||||
interactive: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1706,6 +1710,7 @@ impl Compiler {
|
||||
body: &[ast::Stmt],
|
||||
symbol_table: SymbolTable,
|
||||
) -> CompileResult<()> {
|
||||
self.interactive = true;
|
||||
// Set future_annotations from symbol table (detected during symbol table scan)
|
||||
self.future_annotations = symbol_table.future_annotations;
|
||||
self.symbol_table_stack.push(symbol_table);
|
||||
@@ -2151,7 +2156,15 @@ impl Compiler {
|
||||
ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
|
||||
self.compile_expression(value)?;
|
||||
|
||||
// Pop result of stack, since we not use it:
|
||||
if self.interactive && !self.ctx.in_func() && !self.ctx.in_class {
|
||||
emit!(
|
||||
self,
|
||||
Instruction::CallIntrinsic1 {
|
||||
func: bytecode::IntrinsicFunction1::Print
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
emit!(self, Instruction::PopTop);
|
||||
}
|
||||
ast::Stmt::Global(_) | ast::Stmt::Nonlocal(_) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
function::OptionalArg,
|
||||
object::{Traverse, TraverseFn},
|
||||
protocol::PyIterReturn,
|
||||
types::{IterNext, Iterable, Representable, SelfIter},
|
||||
types::{Destructor, IterNext, Iterable, Representable, SelfIter},
|
||||
};
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
|
||||
@@ -31,7 +31,10 @@ impl PyPayload for PyCoroutine {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py, IterNext, Representable))]
|
||||
#[pyclass(
|
||||
flags(DISALLOW_INSTANTIATION),
|
||||
with(Py, IterNext, Representable, Destructor)
|
||||
)]
|
||||
impl PyCoroutine {
|
||||
pub const fn as_coro(&self) -> &Coro {
|
||||
&self.inner
|
||||
@@ -130,7 +133,7 @@ impl Py<PyCoroutine> {
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
self.inner.close(self.as_object(), vm)
|
||||
}
|
||||
}
|
||||
@@ -149,6 +152,22 @@ impl IterNext for PyCoroutine {
|
||||
}
|
||||
}
|
||||
|
||||
impl Destructor for PyCoroutine {
|
||||
fn del(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if zelf.inner.closed() || zelf.inner.running() {
|
||||
return Ok(());
|
||||
}
|
||||
if zelf.inner.frame().lasti() == 0 {
|
||||
zelf.inner.closed.store(true);
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(e) = zelf.inner.close(zelf.as_object(), vm) {
|
||||
vm.run_unraisable(e, None, zelf.as_object().to_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = false, name = "coroutine_wrapper", traverse = "manual")]
|
||||
#[derive(Debug)]
|
||||
// PyCoroWrapper_Type in CPython
|
||||
@@ -209,7 +228,7 @@ impl PyCoroutineWrapper {
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
self.closed.store(true);
|
||||
self.coro.close(vm)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ impl Frame {
|
||||
|
||||
#[pygetset]
|
||||
fn f_locals(&self, vm: &VirtualMachine) -> PyResult {
|
||||
self.locals(vm).map(Into::into)
|
||||
let result = self.locals(vm).map(Into::into);
|
||||
self.locals_dirty
|
||||
.store(true, core::sync::atomic::Ordering::Release);
|
||||
result
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
function::OptionalArg,
|
||||
object::{Traverse, TraverseFn},
|
||||
protocol::PyIterReturn,
|
||||
types::{IterNext, Iterable, Representable, SelfIter},
|
||||
types::{Destructor, IterNext, Iterable, Representable, SelfIter},
|
||||
};
|
||||
|
||||
#[pyclass(module = false, name = "generator", traverse = "manual")]
|
||||
@@ -33,7 +33,10 @@ impl PyPayload for PyGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py, IterNext, Iterable))]
|
||||
#[pyclass(
|
||||
flags(DISALLOW_INSTANTIATION),
|
||||
with(Py, IterNext, Iterable, Representable, Destructor)
|
||||
)]
|
||||
impl PyGenerator {
|
||||
pub const fn as_coro(&self) -> &Coro {
|
||||
&self.inner
|
||||
@@ -89,6 +92,11 @@ impl PyGenerator {
|
||||
self.inner.frame().yield_from_target()
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn gi_suspended(&self, _vm: &VirtualMachine) -> bool {
|
||||
self.inner.suspended()
|
||||
}
|
||||
|
||||
#[pyclassmethod]
|
||||
fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
|
||||
PyGenericAlias::from_args(cls, args, vm)
|
||||
@@ -121,7 +129,7 @@ impl Py<PyGenerator> {
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn close(&self, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
self.inner.close(self.as_object(), vm)
|
||||
}
|
||||
}
|
||||
@@ -140,6 +148,25 @@ impl IterNext for PyGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
impl Destructor for PyGenerator {
|
||||
fn del(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
// _PyGen_Finalize: close the generator if it's still suspended
|
||||
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());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PyGenerator {
|
||||
fn drop(&mut self) {
|
||||
self.inner.frame().clear_generator();
|
||||
|
||||
@@ -122,7 +122,7 @@ fn inner_pow(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult {
|
||||
|
||||
fn inner_mod(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult {
|
||||
if int2.is_zero() {
|
||||
Err(vm.new_zero_division_error("integer modulo by zero"))
|
||||
Err(vm.new_zero_division_error("division by zero"))
|
||||
} else {
|
||||
Ok(vm.ctx.new_int(int1.mod_floor(int2)).into())
|
||||
}
|
||||
@@ -130,7 +130,7 @@ fn inner_mod(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult {
|
||||
|
||||
fn inner_floordiv(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult {
|
||||
if int2.is_zero() {
|
||||
Err(vm.new_zero_division_error("integer division or modulo by zero"))
|
||||
Err(vm.new_zero_division_error("division by zero"))
|
||||
} else {
|
||||
Ok(vm.ctx.new_int(int1.div_floor(int2)).into())
|
||||
}
|
||||
@@ -138,7 +138,7 @@ fn inner_floordiv(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult
|
||||
|
||||
fn inner_divmod(int1: &BigInt, int2: &BigInt, vm: &VirtualMachine) -> PyResult {
|
||||
if int2.is_zero() {
|
||||
return Err(vm.new_zero_division_error("integer division or modulo by zero"));
|
||||
return Err(vm.new_zero_division_error("division by zero"));
|
||||
}
|
||||
let (div, modulo) = int1.div_mod_floor(int2);
|
||||
Ok(vm.new_tuple((div, modulo)).into())
|
||||
|
||||
@@ -225,7 +225,13 @@ impl PyList {
|
||||
|
||||
fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
|
||||
match SequenceIndex::try_from_borrowed_object(vm, needle, "list")? {
|
||||
SequenceIndex::Int(i) => self.borrow_vec().getitem_by_index(vm, i),
|
||||
SequenceIndex::Int(i) => {
|
||||
let vec = self.borrow_vec();
|
||||
let pos = vec
|
||||
.wrap_index(i)
|
||||
.ok_or_else(|| vm.new_index_error("list index out of range"))?;
|
||||
Ok(vec.do_get(pos))
|
||||
}
|
||||
SequenceIndex::Slice(slice) => self
|
||||
.borrow_vec()
|
||||
.getitem_by_slice(vm, slice)
|
||||
@@ -448,9 +454,12 @@ impl AsSequence for PyList {
|
||||
.map(|x| x.into())
|
||||
}),
|
||||
item: atomic_func!(|seq, i, vm| {
|
||||
PyList::sequence_downcast(seq)
|
||||
.borrow_vec()
|
||||
.getitem_by_index(vm, i)
|
||||
let list = PyList::sequence_downcast(seq);
|
||||
let vec = list.borrow_vec();
|
||||
let pos = vec
|
||||
.wrap_index(i)
|
||||
.ok_or_else(|| vm.new_index_error("list index out of range"))?;
|
||||
Ok(vec.do_get(pos))
|
||||
}),
|
||||
ass_item: atomic_func!(|seq, i, value, vm| {
|
||||
let zelf = PyList::sequence_downcast(seq);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AsObject, Py, PyObject, PyObjectRef, PyResult, VirtualMachine,
|
||||
AsObject, Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine,
|
||||
builtins::{PyBaseExceptionRef, PyStrRef},
|
||||
common::lock::PyMutex,
|
||||
exceptions::types::PyBaseException,
|
||||
@@ -135,6 +135,7 @@ impl Coro {
|
||||
if self.closed.load() {
|
||||
return Ok(PyIterReturn::StopIteration(None));
|
||||
}
|
||||
self.frame.locals_to_fast(vm)?;
|
||||
let value = if self.frame.lasti() > 0 {
|
||||
Some(value)
|
||||
} else if !vm.is_none(&value) {
|
||||
@@ -176,22 +177,37 @@ impl Coro {
|
||||
exc_tb: PyObjectRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<PyIterReturn> {
|
||||
// Validate throw arguments (matching CPython _gen_throw)
|
||||
if exc_type.fast_isinstance(vm.ctx.exceptions.base_exception_type) && !vm.is_none(&exc_val)
|
||||
{
|
||||
return Err(
|
||||
vm.new_type_error("instance exception may not have a separate value".to_owned())
|
||||
);
|
||||
}
|
||||
if !vm.is_none(&exc_tb) && !exc_tb.fast_isinstance(vm.ctx.types.traceback_type) {
|
||||
return Err(
|
||||
vm.new_type_error("throw() third argument must be a traceback object".to_owned())
|
||||
);
|
||||
}
|
||||
if self.closed.load() {
|
||||
return Err(vm.normalize_exception(exc_type, exc_val, exc_tb)?);
|
||||
}
|
||||
// Validate exception type before entering generator context.
|
||||
// Invalid types propagate to caller without closing the generator.
|
||||
crate::exceptions::ExceptionCtor::try_from_object(vm, exc_type.clone())?;
|
||||
let result = self.run_with_context(jen, vm, |f| f.gen_throw(vm, exc_type, exc_val, exc_tb));
|
||||
self.maybe_close(&result);
|
||||
Ok(result?.into_iter_return(vm))
|
||||
}
|
||||
|
||||
pub fn close(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
|
||||
pub fn close(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
if self.closed.load() {
|
||||
return Ok(());
|
||||
return Ok(vm.ctx.none());
|
||||
}
|
||||
// If generator hasn't started (FRAME_CREATED), just mark as closed
|
||||
if self.frame.lasti() == 0 {
|
||||
self.closed.store(true);
|
||||
return Ok(());
|
||||
return Ok(vm.ctx.none());
|
||||
}
|
||||
let result = self.run_with_context(jen, vm, |f| {
|
||||
f.gen_throw(
|
||||
@@ -207,10 +223,15 @@ impl Coro {
|
||||
Err(vm.new_runtime_error(format!("{} ignored GeneratorExit", gen_name(jen, vm))))
|
||||
}
|
||||
Err(e) if !is_gen_exit(&e, vm) => Err(e),
|
||||
_ => Ok(()),
|
||||
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
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
self.running.load()
|
||||
}
|
||||
@@ -240,10 +261,11 @@ impl Coro {
|
||||
}
|
||||
|
||||
pub fn repr(&self, jen: &PyObject, id: usize, vm: &VirtualMachine) -> String {
|
||||
let qualname = self.qualname();
|
||||
format!(
|
||||
"<{} object {} at {:#x}>",
|
||||
gen_name(jen, vm),
|
||||
self.name.lock(),
|
||||
qualname.as_str(),
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,13 @@ impl VirtualMachine {
|
||||
) -> PyResult<PyBaseExceptionRef> {
|
||||
// TODO: fast-path built-in exceptions by directly instantiating them? Is that really worth it?
|
||||
let res = PyType::call(&cls, args.into_args(self), self)?;
|
||||
PyBaseExceptionRef::try_from_object(self, res)
|
||||
res.downcast::<PyBaseException>().map_err(|obj| {
|
||||
self.new_type_error(format!(
|
||||
"calling {} should have returned an instance of BaseException, not {}",
|
||||
cls,
|
||||
obj.class()
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,6 +1313,16 @@ impl OSErrorBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Strip winerror from the builder. Used for C runtime errors
|
||||
/// (e.g. `_wopen`, `open`) that should produce `[Errno X]` format
|
||||
/// instead of `[WinError X]`.
|
||||
#[must_use]
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn without_winerror(mut self) -> Self {
|
||||
self.winerror = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self, vm: &VirtualMachine) -> PyRef<types::PyOSError> {
|
||||
use types::PyOSError;
|
||||
|
||||
@@ -1391,12 +1407,10 @@ impl ToOSErrorBuilder for std::io::Error {
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = OSErrorBuilder::with_errno(errno, msg, vm);
|
||||
|
||||
#[cfg(windows)]
|
||||
if let Some(winerror) = self.raw_os_error() {
|
||||
builder = builder.winerror(winerror.to_pyobject(vm));
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ pub struct Frame {
|
||||
/// Used by `frame.clear()` to reject clearing an executing frame,
|
||||
/// even when called from a different thread.
|
||||
pub(crate) owner: atomic::AtomicI8,
|
||||
/// Set when f_locals is accessed. Cleared after locals_to_fast() sync.
|
||||
pub(crate) locals_dirty: atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl PyPayload for Frame {
|
||||
@@ -212,6 +214,7 @@ impl Frame {
|
||||
generator: PyAtomicBorrow::new(),
|
||||
previous: AtomicPtr::new(core::ptr::null_mut()),
|
||||
owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
|
||||
locals_dirty: atomic::AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +258,28 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync locals dict back to fastlocals. Called before generator/coroutine resume
|
||||
/// to apply any modifications made via f_locals.
|
||||
pub fn locals_to_fast(&self, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if !self.locals_dirty.load(atomic::Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
let code = &**self.code;
|
||||
let mut fastlocals = self.fastlocals.lock();
|
||||
for (i, &varname) in code.varnames.iter().enumerate() {
|
||||
if i >= fastlocals.len() {
|
||||
break;
|
||||
}
|
||||
match self.locals.mapping().subscript(varname, vm) {
|
||||
Ok(value) => fastlocals[i] = Some(value),
|
||||
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
self.locals_dirty.store(false, atomic::Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn locals(&self, vm: &VirtualMachine) -> PyResult<ArgMapping> {
|
||||
let locals = &self.locals;
|
||||
let code = &**self.code;
|
||||
@@ -438,8 +463,20 @@ impl ExecutingFrame<'_> {
|
||||
// Execute until return or exception:
|
||||
let instructions = &self.code.instructions;
|
||||
let mut arg_state = bytecode::OpArgState::default();
|
||||
let mut prev_line: usize = 0;
|
||||
loop {
|
||||
let idx = self.lasti() as usize;
|
||||
// Fire 'line' trace event when line number changes.
|
||||
// Only fire if this frame has a per-frame trace function set
|
||||
// (frames entered before sys.settrace() have trace=None).
|
||||
if vm.use_tracing.get()
|
||||
&& !vm.is_none(&self.object.trace.lock())
|
||||
&& let Some((loc, _)) = self.code.locations.get(idx)
|
||||
&& loc.line.get() != prev_line
|
||||
{
|
||||
prev_line = loc.line.get();
|
||||
vm.trace_event(crate::protocol::TraceEvent::Line, None)?;
|
||||
}
|
||||
self.update_lasti(|i| *i += 1);
|
||||
let bytecode::CodeUnit { op, arg } = instructions[idx];
|
||||
let arg = arg_state.extend(arg);
|
||||
@@ -598,44 +635,74 @@ impl ExecutingFrame<'_> {
|
||||
exc_tb: PyObjectRef,
|
||||
) -> PyResult<ExecutionResult> {
|
||||
if let Some(jen) = self.yield_from_target() {
|
||||
// borrow checker shenanigans - we only need to use exc_type/val/tb if the following
|
||||
// variable is Some
|
||||
let thrower = if let Some(coro) = self.builtin_coro(jen) {
|
||||
Some(Either::A(coro))
|
||||
// Check if the exception is GeneratorExit (type or instance).
|
||||
// For GeneratorExit, close the sub-iterator instead of throwing.
|
||||
let is_gen_exit = if let Some(typ) = exc_type.downcast_ref::<PyType>() {
|
||||
typ.fast_issubclass(vm.ctx.exceptions.generator_exit)
|
||||
} else {
|
||||
vm.get_attribute_opt(jen.to_owned(), "throw")?
|
||||
.map(Either::B)
|
||||
exc_type.fast_isinstance(vm.ctx.exceptions.generator_exit)
|
||||
};
|
||||
if let Some(thrower) = thrower {
|
||||
let ret = match thrower {
|
||||
Either::A(coro) => coro
|
||||
.throw(jen, exc_type, exc_val, exc_tb, vm)
|
||||
.to_pyresult(vm),
|
||||
Either::B(meth) => meth.call((exc_type, exc_val, exc_tb), vm),
|
||||
|
||||
if is_gen_exit {
|
||||
// gen_close_iter: close the sub-iterator
|
||||
let close_result = if let Some(coro) = self.builtin_coro(jen) {
|
||||
coro.close(jen, vm).map(|_| ())
|
||||
} else if let Some(close_meth) = vm.get_attribute_opt(jen.to_owned(), "close")? {
|
||||
close_meth.call((), vm).map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
return ret.map(ExecutionResult::Yield).or_else(|err| {
|
||||
// This pushes Py_None to stack and restarts evalloop in exception mode.
|
||||
// Stack before throw: [receiver] (YIELD_VALUE already popped yielded value)
|
||||
// After pushing None: [receiver, None]
|
||||
// Exception handler will push exc: [receiver, None, exc]
|
||||
// CLEANUP_THROW expects: [sub_iter, last_sent_val, exc]
|
||||
if let Err(err) = close_result {
|
||||
self.push_value(vm.ctx.none());
|
||||
|
||||
// Set __context__ on the exception (_PyErr_ChainStackItem)
|
||||
vm.contextualize_exception(&err);
|
||||
|
||||
// Use unwind_blocks to let exception table route to CLEANUP_THROW
|
||||
match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
|
||||
return match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
|
||||
Ok(None) => self.run(vm),
|
||||
Ok(Some(result)) => Ok(result),
|
||||
Err(exception) => Err(exception),
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
// Fall through to throw_here to raise GeneratorExit in the generator
|
||||
} else {
|
||||
// For non-GeneratorExit, delegate throw to sub-iterator
|
||||
let thrower = if let Some(coro) = self.builtin_coro(jen) {
|
||||
Some(Either::A(coro))
|
||||
} else {
|
||||
vm.get_attribute_opt(jen.to_owned(), "throw")?
|
||||
.map(Either::B)
|
||||
};
|
||||
if let Some(thrower) = thrower {
|
||||
let ret = match thrower {
|
||||
Either::A(coro) => coro
|
||||
.throw(jen, exc_type, exc_val, exc_tb, vm)
|
||||
.to_pyresult(vm),
|
||||
Either::B(meth) => meth.call((exc_type, exc_val, exc_tb), vm),
|
||||
};
|
||||
return ret.map(ExecutionResult::Yield).or_else(|err| {
|
||||
self.push_value(vm.ctx.none());
|
||||
vm.contextualize_exception(&err);
|
||||
match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
|
||||
Ok(None) => self.run(vm),
|
||||
Ok(Some(result)) => Ok(result),
|
||||
Err(exception) => Err(exception),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// throw_here: no delegate has throw method, or not in yield-from
|
||||
// gen_send_ex pushes Py_None to stack and restarts evalloop in exception mode
|
||||
let exception = vm.normalize_exception(exc_type, exc_val, exc_tb)?;
|
||||
// Validate the exception type first. Invalid types propagate directly to
|
||||
// the caller. Valid types with failed instantiation (e.g. __new__ returns
|
||||
// wrong type) get thrown into the generator via PyErr_SetObject path.
|
||||
let ctor = ExceptionCtor::try_from_object(vm, exc_type)?;
|
||||
let exception = match ctor.instantiate_value(exc_val, vm) {
|
||||
Ok(exc) => {
|
||||
if let Some(tb) = Option::<PyRef<PyTraceback>>::try_from_object(vm, exc_tb)? {
|
||||
exc.set_traceback_typed(Some(tb));
|
||||
}
|
||||
exc
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// Add traceback entry for the generator frame at the yield site
|
||||
let idx = self.lasti().saturating_sub(1) as usize;
|
||||
|
||||
@@ -327,4 +327,21 @@ impl crate::exceptions::OSErrorBuilder {
|
||||
let builder = builder.filename(filename.into().filename(vm));
|
||||
builder.build(vm).upcast()
|
||||
}
|
||||
|
||||
/// Like `with_filename`, but strips winerror on Windows.
|
||||
/// Use for C runtime errors (open, fstat, etc.) that should produce
|
||||
/// `[Errno X]` format instead of `[WinError X]`.
|
||||
#[must_use]
|
||||
pub(crate) fn with_filename_from_errno<'a>(
|
||||
error: &std::io::Error,
|
||||
filename: impl Into<OsPathOrFd<'a>>,
|
||||
vm: &VirtualMachine,
|
||||
) -> crate::builtins::PyBaseExceptionRef {
|
||||
use crate::exceptions::ToOSErrorBuilder;
|
||||
let builder = error.to_os_error_builder(vm);
|
||||
#[cfg(windows)]
|
||||
let builder = builder.without_winerror();
|
||||
let builder = builder.filename(filename.into().filename(vm));
|
||||
builder.build(vm).upcast()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
builtins::{PyBoundMethod, PyFunction},
|
||||
function::{FuncArgs, IntoFuncArgs},
|
||||
types::GenericMethod,
|
||||
{AsObject, PyObject, PyResult, VirtualMachine},
|
||||
{AsObject, PyObject, PyObjectRef, PyResult, VirtualMachine},
|
||||
};
|
||||
|
||||
impl PyObject {
|
||||
@@ -48,17 +49,35 @@ impl<'a> PyCallable<'a> {
|
||||
|
||||
pub fn invoke(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
let args = args.into_args(vm);
|
||||
vm.trace_event(TraceEvent::Call)?;
|
||||
let result = (self.call)(self.obj, args, vm);
|
||||
vm.trace_event(TraceEvent::Return)?;
|
||||
result
|
||||
// Python functions get 'call'/'return' events from with_frame().
|
||||
// Bound methods delegate to the inner callable, which fires its own events.
|
||||
// All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'.
|
||||
let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some()
|
||||
|| self.obj.downcast_ref::<PyBoundMethod>().is_some();
|
||||
if is_python_callable {
|
||||
(self.call)(self.obj, args, vm)
|
||||
} else {
|
||||
let callable = self.obj.to_owned();
|
||||
vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
|
||||
let result = (self.call)(self.obj, args, vm);
|
||||
if result.is_ok() {
|
||||
vm.trace_event(TraceEvent::CReturn, Some(callable))?;
|
||||
} else {
|
||||
let _ = vm.trace_event(TraceEvent::CException, Some(callable));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trace events for sys.settrace and sys.setprofile.
|
||||
enum TraceEvent {
|
||||
pub(crate) enum TraceEvent {
|
||||
Call,
|
||||
Return,
|
||||
Line,
|
||||
CCall,
|
||||
CReturn,
|
||||
CException,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for TraceEvent {
|
||||
@@ -67,6 +86,10 @@ impl core::fmt::Display for TraceEvent {
|
||||
match self {
|
||||
Call => write!(f, "call"),
|
||||
Return => write!(f, "return"),
|
||||
Line => write!(f, "line"),
|
||||
CCall => write!(f, "c_call"),
|
||||
CReturn => write!(f, "c_return"),
|
||||
CException => write!(f, "c_exception"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,14 +97,14 @@ impl core::fmt::Display for TraceEvent {
|
||||
impl VirtualMachine {
|
||||
/// Call registered trace function.
|
||||
#[inline]
|
||||
fn trace_event(&self, event: TraceEvent) -> PyResult<()> {
|
||||
pub(crate) fn trace_event(&self, event: TraceEvent, arg: Option<PyObjectRef>) -> PyResult<()> {
|
||||
if self.use_tracing.get() {
|
||||
self._trace_event_inner(event)
|
||||
self._trace_event_inner(event, arg)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn _trace_event_inner(&self, event: TraceEvent) -> PyResult<()> {
|
||||
fn _trace_event_inner(&self, event: TraceEvent, arg: Option<PyObjectRef>) -> PyResult<()> {
|
||||
let trace_func = self.trace_func.borrow().to_owned();
|
||||
let profile_func = self.profile_func.borrow().to_owned();
|
||||
if self.is_none(&trace_func) && self.is_none(&profile_func) {
|
||||
@@ -95,7 +118,7 @@ impl VirtualMachine {
|
||||
|
||||
let frame = frame_ref.unwrap().as_object().to_owned();
|
||||
let event = self.ctx.new_str(event.to_string()).into();
|
||||
let args = vec![frame, event, self.ctx.none()];
|
||||
let args = vec![frame, event, arg.unwrap_or_else(|| self.ctx.none())];
|
||||
|
||||
// temporarily disable tracing, during the call to the
|
||||
// tracing function itself.
|
||||
|
||||
@@ -8,6 +8,7 @@ mod sequence;
|
||||
|
||||
pub use buffer::{BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, VecBuffer};
|
||||
pub use callable::PyCallable;
|
||||
pub(crate) use callable::TraceEvent;
|
||||
pub use iter::{PyIter, PyIterIter, PyIterReturn};
|
||||
pub use mapping::{PyMapping, PyMappingMethods, PyMappingSlots};
|
||||
pub use number::{
|
||||
|
||||
@@ -5266,7 +5266,9 @@ mod fileio {
|
||||
let filename = OsPathOrFd::Path(path);
|
||||
match fd {
|
||||
Ok(fd) => (fd.into_raw(), Some(filename)),
|
||||
Err(e) => return Err(OSErrorBuilder::with_filename(&e, filename, vm)),
|
||||
Err(e) => {
|
||||
return Err(OSErrorBuilder::with_filename_from_errno(&e, filename, vm));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ mod decl {
|
||||
stdlib::sys,
|
||||
types::{Constructor, IterNext, Iterable, Representable, SelfIter},
|
||||
};
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use malachite_bigint::BigInt;
|
||||
use num_traits::One;
|
||||
@@ -943,6 +944,7 @@ mod decl {
|
||||
struct PyItertoolsTeeData {
|
||||
iterable: PyIter,
|
||||
values: PyMutex<Vec<PyObjectRef>>,
|
||||
running: AtomicBool,
|
||||
}
|
||||
|
||||
impl PyItertoolsTeeData {
|
||||
@@ -950,19 +952,33 @@ mod decl {
|
||||
Ok(PyRc::new(Self {
|
||||
iterable,
|
||||
values: PyMutex::new(vec![]),
|
||||
running: AtomicBool::new(false),
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_item(&self, vm: &VirtualMachine, index: usize) -> PyResult<PyIterReturn> {
|
||||
// Return cached value if available
|
||||
{
|
||||
let Some(values) = self.values.try_lock() else {
|
||||
return Err(vm.new_runtime_error("cannot re-enter the tee iterator"));
|
||||
};
|
||||
if index < values.len() {
|
||||
return Ok(PyIterReturn::Return(values[index].clone()));
|
||||
}
|
||||
}
|
||||
// Prevent concurrent/reentrant calls to iterable.next()
|
||||
if self.running.swap(true, Ordering::Acquire) {
|
||||
return Err(vm.new_runtime_error("cannot re-enter the tee iterator"));
|
||||
}
|
||||
let result = self.iterable.next(vm);
|
||||
self.running.store(false, Ordering::Release);
|
||||
let obj = raise_if_stop!(result?);
|
||||
let Some(mut values) = self.values.try_lock() else {
|
||||
return Err(vm.new_runtime_error("cannot re-enter the tee iterator"));
|
||||
};
|
||||
|
||||
if values.len() == index {
|
||||
let obj = raise_if_stop!(self.iterable.next(vm)?);
|
||||
values.push(obj);
|
||||
}
|
||||
|
||||
Ok(PyIterReturn::Return(values[index].clone()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ pub(super) mod _os {
|
||||
crt_fd::open(&name, flags, mode)
|
||||
}
|
||||
};
|
||||
fd.map_err(|err| OSErrorBuilder::with_filename(&err, name, vm))
|
||||
fd.map_err(|err| OSErrorBuilder::with_filename_from_errno(&err, name, vm))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
|
||||
@@ -1018,7 +1018,28 @@ impl VirtualMachine {
|
||||
crate::frame::FrameOwner::Thread as i8,
|
||||
core::sync::atomic::Ordering::AcqRel,
|
||||
);
|
||||
let result = f(frame);
|
||||
use crate::protocol::TraceEvent;
|
||||
// Fire 'call' trace event after pushing frame
|
||||
// (current_frame() now returns the callee's frame)
|
||||
let result = match self.trace_event(TraceEvent::Call, None) {
|
||||
Ok(()) => {
|
||||
// Set per-frame trace function so line events fire for this frame.
|
||||
// Frames entered before sys.settrace() keep trace=None and skip line events.
|
||||
if self.use_tracing.get() {
|
||||
let trace_func = self.trace_func.borrow().clone();
|
||||
if !self.is_none(&trace_func) {
|
||||
*frame.trace.lock() = trace_func;
|
||||
}
|
||||
}
|
||||
let result = f(frame);
|
||||
// Fire 'return' trace event on success
|
||||
if result.is_ok() {
|
||||
let _ = self.trace_event(TraceEvent::Return, None);
|
||||
}
|
||||
result
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
// SAFETY: frame_ptr is valid because self.frames holds a clone
|
||||
// of the frame, keeping the underlying allocation alive.
|
||||
unsafe { &*frame_ptr }
|
||||
|
||||
@@ -501,6 +501,7 @@ impl VirtualMachine {
|
||||
if let Some(msg) = msg.get_mut(..1) {
|
||||
msg.make_ascii_lowercase();
|
||||
}
|
||||
let mut narrow_caret = false;
|
||||
match error {
|
||||
#[cfg(feature = "parser")]
|
||||
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
|
||||
@@ -523,6 +524,14 @@ impl VirtualMachine {
|
||||
}) => {
|
||||
msg = "invalid syntax".to_owned();
|
||||
}
|
||||
#[cfg(feature = "parser")]
|
||||
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
|
||||
error: ruff_python_parser::ParseErrorType::InvalidStarredExpressionUsage,
|
||||
..
|
||||
}) => {
|
||||
msg = "invalid syntax".to_owned();
|
||||
narrow_caret = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if syntax_error_type.is(self.ctx.exceptions.tab_error) {
|
||||
@@ -543,6 +552,12 @@ impl VirtualMachine {
|
||||
|
||||
// Set end_lineno and end_offset if available
|
||||
if let Some((end_lineno, end_offset)) = error.python_end_location() {
|
||||
let (end_lineno, end_offset) = if narrow_caret {
|
||||
let (l, o) = error.python_location();
|
||||
(l, o + 1)
|
||||
} else {
|
||||
(end_lineno, end_offset)
|
||||
};
|
||||
let end_lineno = self.ctx.new_int(end_lineno);
|
||||
let end_offset = self.ctx.new_int(end_offset);
|
||||
syntax_error
|
||||
|
||||
Reference in New Issue
Block a user