diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 826848ff2..25bee4d99 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -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(_) => { diff --git a/crates/vm/src/builtins/coroutine.rs b/crates/vm/src/builtins/coroutine.rs index bca00f843..c8547b8a4 100644 --- a/crates/vm/src/builtins/coroutine.rs +++ b/crates/vm/src/builtins/coroutine.rs @@ -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 { } #[pymethod] - fn close(&self, vm: &VirtualMachine) -> PyResult<()> { + fn close(&self, vm: &VirtualMachine) -> PyResult { self.inner.close(self.as_object(), vm) } } @@ -149,6 +152,22 @@ impl IterNext for PyCoroutine { } } +impl Destructor for PyCoroutine { + fn del(zelf: &Py, 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 { self.closed.store(true); self.coro.close(vm) } diff --git a/crates/vm/src/builtins/frame.rs b/crates/vm/src/builtins/frame.rs index d6bbbf827..5d7510d8f 100644 --- a/crates/vm/src/builtins/frame.rs +++ b/crates/vm/src/builtins/frame.rs @@ -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] diff --git a/crates/vm/src/builtins/generator.rs b/crates/vm/src/builtins/generator.rs index f4deb8cc7..cdbb2af44 100644 --- a/crates/vm/src/builtins/generator.rs +++ b/crates/vm/src/builtins/generator.rs @@ -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 { } #[pymethod] - fn close(&self, vm: &VirtualMachine) -> PyResult<()> { + fn close(&self, vm: &VirtualMachine) -> PyResult { self.inner.close(self.as_object(), vm) } } @@ -140,6 +148,25 @@ impl IterNext for PyGenerator { } } +impl Destructor for PyGenerator { + fn del(zelf: &Py, 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(); diff --git a/crates/vm/src/builtins/int.rs b/crates/vm/src/builtins/int.rs index 273199f69..bbbc7d176 100644 --- a/crates/vm/src/builtins/int.rs +++ b/crates/vm/src/builtins/int.rs @@ -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()) diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 7203110c7..7e22f73f8 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -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); diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 0ee48d959..a066c9944 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -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 { + // 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 { 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 ) } diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 3a1652a28..192549d09 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -348,7 +348,13 @@ impl VirtualMachine { ) -> PyResult { // 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::().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 { 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 } } diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index f197effb7..f1647eb6f 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -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 { 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 { 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::() { + 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::>::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; diff --git a/crates/vm/src/ospath.rs b/crates/vm/src/ospath.rs index b9efccde3..00195460e 100644 --- a/crates/vm/src/ospath.rs +++ b/crates/vm/src/ospath.rs @@ -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>, + 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() + } } diff --git a/crates/vm/src/protocol/callable.rs b/crates/vm/src/protocol/callable.rs index fa5e48d58..9308ec8ff 100644 --- a/crates/vm/src/protocol/callable.rs +++ b/crates/vm/src/protocol/callable.rs @@ -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::().is_some() + || self.obj.downcast_ref::().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) -> 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) -> 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. diff --git a/crates/vm/src/protocol/mod.rs b/crates/vm/src/protocol/mod.rs index 6eb2909f1..411aa4dfa 100644 --- a/crates/vm/src/protocol/mod.rs +++ b/crates/vm/src/protocol/mod.rs @@ -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::{ diff --git a/crates/vm/src/stdlib/io.rs b/crates/vm/src/stdlib/io.rs index b15c0e804..89f567d86 100644 --- a/crates/vm/src/stdlib/io.rs +++ b/crates/vm/src/stdlib/io.rs @@ -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)); + } } } }; diff --git a/crates/vm/src/stdlib/itertools.rs b/crates/vm/src/stdlib/itertools.rs index 5a3cfee39..d1c188b88 100644 --- a/crates/vm/src/stdlib/itertools.rs +++ b/crates/vm/src/stdlib/itertools.rs @@ -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>, + 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 { + // 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())) } } diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 0f4eb0ce4..9993d8b63 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -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] diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index aa689e85f..934c79b2e 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -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 } diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 2a06fda17..7c6035b62 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -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