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:
Jeong, YunWon
2026-02-11 16:45:38 +09:00
parent ccd3d4f964
commit 5c341efdf0
17 changed files with 337 additions and 68 deletions

View File

@@ -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(_) => {

View File

@@ -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)
}

View File

@@ -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]

View File

@@ -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();

View File

@@ -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())

View File

@@ -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);

View File

@@ -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
)
}

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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()
}
}

View File

@@ -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.

View File

@@ -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::{

View File

@@ -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));
}
}
}
};

View File

@@ -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()))
}
}

View File

@@ -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]

View File

@@ -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 }

View File

@@ -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