From 40a695834267fb7c182d4d41fa84fbc435ea73dd Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Tue, 25 Mar 2025 12:05:19 -0700 Subject: [PATCH 01/10] limit and repeat trucation for traceback --- vm/src/exceptions.rs | 49 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 708a93fe61..084cdfbcbc 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -43,6 +43,9 @@ impl PyPayload for PyBaseException { } } +const TRACEBACK_LIMIT: usize = 1000; +const TRACEBACK_RECURSIVE_CUTOFF: usize = 3; // Also hardcoded in traceback.py. + impl VirtualMachine { // Why `impl VirtualMachine`? // These functions are natively free function in CPython - not methods of PyException @@ -131,10 +134,40 @@ impl VirtualMachine { exc: &PyBaseExceptionRef, ) -> Result<(), W::Error> { let vm = self; + + // TODO: Get tracebacklimit from sys and replace limit with that if it exists + let limit = TRACEBACK_LIMIT; if let Some(tb) = exc.traceback.read().clone() { writeln!(output, "Traceback (most recent call last):")?; + let mut tb_list = vec![]; for tb in tb.iter() { - write_traceback_entry(output, &tb)?; + tb_list.push(tb); + } + let mut repeat_counter = 0; + let mut previous_file = "".to_string(); + let mut previous_line = 0; + let mut previous_name = "".to_string(); + // Gets the last `limit` traceback entries + for tb in tb_list.into_iter().rev().take(limit).rev() { + if previous_file != tb.frame.code.source_path.as_str() + || previous_line != tb.lineno.get() + || previous_name != tb.frame.code.obj_name.as_str() + { + if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { + write_repeat_traceback_entry(output, &tb, repeat_counter)?; + } + previous_file = tb.frame.code.source_path.as_str().to_string(); + previous_line = tb.lineno.get(); + previous_name = tb.frame.code.obj_name.as_str().to_string(); + repeat_counter = 0; + } + repeat_counter += 1; + if repeat_counter <= TRACEBACK_RECURSIVE_CUTOFF { + write_traceback_entry(output, &tb)?; + } + } + if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { + write_repeat_traceback_entry(output, &tb_list[0], repeat_counter)?; } } @@ -383,6 +416,20 @@ fn write_traceback_entry( Ok(()) } +fn write_repeat_traceback_entry( + output: &mut W, + tb_entry: &PyTracebackRef, + repeat_counter: usize, +) -> Result<(), W::Error> { + let count = repeat_counter - TRACEBACK_RECURSIVE_CUTOFF; + writeln!( + output, + r##" [Previous line repeated {} more time{}]"##, + count, + if count == 1 { "" } else { "s" } + ) +} + #[derive(Clone)] pub enum ExceptionCtor { Class(PyTypeRef), From a3c76ac1115b0aa18dbb47ad920bf42eec8a2ca1 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Tue, 25 Mar 2025 12:17:43 -0700 Subject: [PATCH 02/10] fix compile --- vm/src/exceptions.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 084cdfbcbc..5a7b7d08b0 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -154,7 +154,7 @@ impl VirtualMachine { || previous_name != tb.frame.code.obj_name.as_str() { if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { - write_repeat_traceback_entry(output, &tb, repeat_counter)?; + write_repeat_traceback_entry(output, repeat_counter)?; } previous_file = tb.frame.code.source_path.as_str().to_string(); previous_line = tb.lineno.get(); @@ -167,7 +167,7 @@ impl VirtualMachine { } } if repeat_counter > TRACEBACK_RECURSIVE_CUTOFF { - write_repeat_traceback_entry(output, &tb_list[0], repeat_counter)?; + write_repeat_traceback_entry(output, repeat_counter)?; } } @@ -407,7 +407,7 @@ fn write_traceback_entry( writeln!( output, r##" File "{}", line {}, in {}"##, - filename.trim_start_matches(r"\\?\"), + filename, tb_entry.lineno, tb_entry.frame.code.obj_name )?; @@ -418,7 +418,6 @@ fn write_traceback_entry( fn write_repeat_traceback_entry( output: &mut W, - tb_entry: &PyTracebackRef, repeat_counter: usize, ) -> Result<(), W::Error> { let count = repeat_counter - TRACEBACK_RECURSIVE_CUTOFF; From 43648cf9049e5923f36fb037e90af4a92198f948 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Sun, 30 Mar 2025 23:00:21 -0700 Subject: [PATCH 03/10] rewrite exception groups in rust --- vm/Lib/python_builtins/_py_exceptiongroup.py | 330 ------------------- vm/src/exceptions.rs | 140 +++++++- 2 files changed, 133 insertions(+), 337 deletions(-) delete mode 100644 vm/Lib/python_builtins/_py_exceptiongroup.py diff --git a/vm/Lib/python_builtins/_py_exceptiongroup.py b/vm/Lib/python_builtins/_py_exceptiongroup.py deleted file mode 100644 index 91e9354d8a..0000000000 --- a/vm/Lib/python_builtins/_py_exceptiongroup.py +++ /dev/null @@ -1,330 +0,0 @@ -# Copied from https://github.com/agronholm/ExceptionGroup/blob/1.2.1/src/exceptiongroup/_exceptions.py -# License: https://github.com/agronholm/exceptiongroup/blob/1.2.1/LICENSE -from __future__ import annotations - -from collections.abc import Callable, Sequence -from functools import partial -from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload - -_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True) -_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException) -_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True) -_ExceptionT = TypeVar("_ExceptionT", bound=Exception) -# using typing.Self would require a typing_extensions dependency on py<3.11 -_ExceptionGroupSelf = TypeVar("_ExceptionGroupSelf", bound="ExceptionGroup") -_BaseExceptionGroupSelf = TypeVar("_BaseExceptionGroupSelf", bound="BaseExceptionGroup") - - -def check_direct_subclass( - exc: BaseException, parents: tuple[type[BaseException]] -) -> bool: - from inspect import getmro # requires rustpython-stdlib - - for cls in getmro(exc.__class__)[:-1]: - if cls in parents: - return True - - return False - - -def get_condition_filter( - condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co], bool], -) -> Callable[[_BaseExceptionT_co], bool]: - from inspect import isclass # requires rustpython-stdlib - - if isclass(condition) and issubclass( - cast(Type[BaseException], condition), BaseException - ): - return partial(check_direct_subclass, parents=(condition,)) - elif isinstance(condition, tuple): - if all(isclass(x) and issubclass(x, BaseException) for x in condition): - return partial(check_direct_subclass, parents=condition) - elif callable(condition): - return cast("Callable[[BaseException], bool]", condition) - - raise TypeError("expected a function, exception type or tuple of exception types") - - -def _derive_and_copy_attributes(self, excs): - eg = self.derive(excs) - eg.__cause__ = self.__cause__ - eg.__context__ = self.__context__ - eg.__traceback__ = self.__traceback__ - if hasattr(self, "__notes__"): - # Create a new list so that add_note() only affects one exceptiongroup - eg.__notes__ = list(self.__notes__) - return eg - - -class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]): - """A combination of multiple unrelated exceptions.""" - - def __new__( - cls: type[_BaseExceptionGroupSelf], - __message: str, - __exceptions: Sequence[_BaseExceptionT_co], - ) -> _BaseExceptionGroupSelf: - if not isinstance(__message, str): - raise TypeError(f"argument 1 must be str, not {type(__message)}") - if not isinstance(__exceptions, Sequence): - raise TypeError("second argument (exceptions) must be a sequence") - if not __exceptions: - raise ValueError( - "second argument (exceptions) must be a non-empty sequence" - ) - - for i, exc in enumerate(__exceptions): - if not isinstance(exc, BaseException): - raise ValueError( - f"Item {i} of second argument (exceptions) is not an exception" - ) - - if cls is BaseExceptionGroup: - if all(isinstance(exc, Exception) for exc in __exceptions): - cls = ExceptionGroup - - if issubclass(cls, Exception): - for exc in __exceptions: - if not isinstance(exc, Exception): - if cls is ExceptionGroup: - raise TypeError( - "Cannot nest BaseExceptions in an ExceptionGroup" - ) - else: - raise TypeError( - f"Cannot nest BaseExceptions in {cls.__name__!r}" - ) - - instance = super().__new__(cls, __message, __exceptions) - instance._message = __message - instance._exceptions = __exceptions - return instance - - def add_note(self, note: str) -> None: - if not isinstance(note, str): - raise TypeError( - f"Expected a string, got note={note!r} (type {type(note).__name__})" - ) - - if not hasattr(self, "__notes__"): - self.__notes__: list[str] = [] - - self.__notes__.append(note) - - @property - def message(self) -> str: - return self._message - - @property - def exceptions( - self, - ) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: - return tuple(self._exceptions) - - @overload - def subgroup( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> ExceptionGroup[_ExceptionT] | None: ... - - @overload - def subgroup( - self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] - ) -> BaseExceptionGroup[_BaseExceptionT] | None: ... - - @overload - def subgroup( - self, - __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... - - def subgroup( - self, - __condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> BaseExceptionGroup[_BaseExceptionT] | None: - condition = get_condition_filter(__condition) - modified = False - if condition(self): - return self - - exceptions: list[BaseException] = [] - for exc in self.exceptions: - if isinstance(exc, BaseExceptionGroup): - subgroup = exc.subgroup(__condition) - if subgroup is not None: - exceptions.append(subgroup) - - if subgroup is not exc: - modified = True - elif condition(exc): - exceptions.append(exc) - else: - modified = True - - if not modified: - return self - elif exceptions: - group = _derive_and_copy_attributes(self, exceptions) - return group - else: - return None - - @overload - def split( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> tuple[ - ExceptionGroup[_ExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - @overload - def split( - self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] - ) -> tuple[ - BaseExceptionGroup[_BaseExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - @overload - def split( - self, - __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], - ) -> tuple[ - BaseExceptionGroup[_BaseExceptionT_co] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ]: ... - - def split( - self, - __condition: type[_BaseExceptionT] - | tuple[type[_BaseExceptionT], ...] - | Callable[[_BaseExceptionT_co], bool], - ) -> ( - tuple[ - ExceptionGroup[_ExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - | tuple[ - BaseExceptionGroup[_BaseExceptionT] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - | tuple[ - BaseExceptionGroup[_BaseExceptionT_co] | None, - BaseExceptionGroup[_BaseExceptionT_co] | None, - ] - ): - condition = get_condition_filter(__condition) - if condition(self): - return self, None - - matching_exceptions: list[BaseException] = [] - nonmatching_exceptions: list[BaseException] = [] - for exc in self.exceptions: - if isinstance(exc, BaseExceptionGroup): - matching, nonmatching = exc.split(condition) - if matching is not None: - matching_exceptions.append(matching) - - if nonmatching is not None: - nonmatching_exceptions.append(nonmatching) - elif condition(exc): - matching_exceptions.append(exc) - else: - nonmatching_exceptions.append(exc) - - matching_group: _BaseExceptionGroupSelf | None = None - if matching_exceptions: - matching_group = _derive_and_copy_attributes(self, matching_exceptions) - - nonmatching_group: _BaseExceptionGroupSelf | None = None - if nonmatching_exceptions: - nonmatching_group = _derive_and_copy_attributes( - self, nonmatching_exceptions - ) - - return matching_group, nonmatching_group - - @overload - def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ... - - @overload - def derive( - self, __excs: Sequence[_BaseExceptionT] - ) -> BaseExceptionGroup[_BaseExceptionT]: ... - - def derive( - self, __excs: Sequence[_BaseExceptionT] - ) -> BaseExceptionGroup[_BaseExceptionT]: - return BaseExceptionGroup(self.message, __excs) - - def __str__(self) -> str: - suffix = "" if len(self._exceptions) == 1 else "s" - return f"{self.message} ({len(self._exceptions)} sub-exception{suffix})" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.message!r}, {self._exceptions!r})" - - -class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception): - def __new__( - cls: type[_ExceptionGroupSelf], - __message: str, - __exceptions: Sequence[_ExceptionT_co], - ) -> _ExceptionGroupSelf: - return super().__new__(cls, __message, __exceptions) - - if TYPE_CHECKING: - - @property - def exceptions( - self, - ) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]: ... - - @overload # type: ignore[override] - def subgroup( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> ExceptionGroup[_ExceptionT] | None: ... - - @overload - def subgroup( - self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] - ) -> ExceptionGroup[_ExceptionT_co] | None: ... - - def subgroup( - self, - __condition: type[_ExceptionT] - | tuple[type[_ExceptionT], ...] - | Callable[[_ExceptionT_co], bool], - ) -> ExceptionGroup[_ExceptionT] | None: - return super().subgroup(__condition) - - @overload - def split( - self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] - ) -> tuple[ - ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None - ]: ... - - @overload - def split( - self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] - ) -> tuple[ - ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None - ]: ... - - def split( - self: _ExceptionGroupSelf, - __condition: type[_ExceptionT] - | tuple[type[_ExceptionT], ...] - | Callable[[_ExceptionT_co], bool], - ) -> tuple[ - ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None - ]: - return super().split(__condition) - - -BaseExceptionGroup.__module__ = 'builtins' -ExceptionGroup.__module__ = 'builtins' diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 5a7b7d08b0..5595488008 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -407,9 +407,7 @@ fn write_traceback_entry( writeln!( output, r##" File "{}", line {}, in {}"##, - filename, - tb_entry.lineno, - tb_entry.frame.code.obj_name + filename, tb_entry.lineno, tb_entry.frame.code.obj_name )?; print_source_line(output, filename, tb_entry.lineno.get())?; @@ -1204,7 +1202,6 @@ pub(crate) fn errno_to_exc_type(_errno: i32, _vm: &VirtualMachine) -> Option<&'s } pub(super) mod types { - use crate::common::lock::PyRwLock; #[cfg_attr(target_arch = "wasm32", allow(unused_imports))] use crate::{ AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine, @@ -1215,15 +1212,16 @@ pub(super) mod types { function::{ArgBytesLike, FuncArgs}, types::{Constructor, Initializer}, }; + use crate::{PyPayload, builtins::PyListRef, class::StaticType, common::lock::PyRwLock}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::str::UnicodeEscapeCodepoint; // This module is designed to be used as `use builtins::*;`. // Do not add any pub symbols not included in builtins module. - // `PyBaseExceptionRef` is the only exception. pub type PyBaseExceptionRef = PyRef; + pub type PyBaseExceptionGroupRef = PyRef; // Sorted By Hierarchy then alphabetized. @@ -1240,9 +1238,137 @@ pub(super) mod types { #[derive(Debug)] pub struct PySystemExit {} - #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group", impl)] + #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group")] #[derive(Debug)] - pub struct PyBaseExceptionGroup {} + pub struct PyBaseExceptionGroup { + pub(super) traceback: PyRwLock>, + pub(super) cause: PyRwLock>>, + pub(super) context: PyRwLock>>, + pub(super) suppress_context: AtomicCell, + pub(super) args: PyRwLock, + pub(super) message: PyRwLock, + pub(super) exceptions: PyRwLock>, + pub(super) notes: PyRwLock>, + } + + #[pyexception] + impl PyBaseExceptionGroup { + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let (message, exceptions) = args.bind(vm)?; + let exceptions = exceptions.to_sequence(); + let len = exceptions.length(vm)?; + if len == 0 { + return Err(vm.new_type_error( + "BaseExceptionGroup() requires at least one exception".to_owned(), + )); + } + for i in 0..len { + let item = exceptions.get_item(i, vm)?; + if !item.is_instance(PyBaseException::class(&vm.ctx).into(), vm)? { + return Err(vm.new_value_error(format!( + "Item {i} of second argument (exceptions) is not an exception" + ))); + } + } + + let is_subclass = !(cls.is(PyBaseExceptionGroup::class(&vm.ctx)) + || cls.is(PyExceptionGroup::class(&vm.ctx))); + for i in 0..len { + let item = exceptions.get_item(i, vm)?; + if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx), vm)? { + if is_subclass { + return Err(vm.new_type_error(format!( + "Cannot nest BaseExceptions in {}", + cls.name() + ))); + } else { + return Err(vm.new_type_error( + "Cannot nest BaseExceptions in an ExceptionGroup".to_owned(), + )); + } + } + } + let mut exceptions_vec = Vec::with_capacity(len); + for i in 0..len { + let item = exceptions.get_item(i, vm)?; + exceptions_vec.push(item.clone()); + } + let args = Vec::with_capacity(1 + len); + args.push(message.clone().into_object()); + for i in 0..len { + let item = exceptions.get_item(i, vm)?; + args.push(item.clone()); + } + Ok(PyBaseExceptionGroup { + traceback: PyRwLock::new(None), + cause: PyRwLock::new(None), + context: PyRwLock::new(None), + suppress_context: AtomicCell::new(false), + args: PyRwLock::new(vm.ctx.new_tuple(args)), + message: PyRwLock::new(message), + exceptions: PyRwLock::new(exceptions_vec), + notes: PyRwLock::new(None), + } + .into_pyobject(vm)?) + } + + #[pygetset] + fn message(&self, vm: &VirtualMachine) -> PyStrRef { + self.message + .read() + .as_ref() + .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()) + } + #[pygetset] + fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { + self.exceptions + .read() + .as_ref() + .map_or_else(|| vm.ctx.new_tuple(vec![]), |x| x.clone()) + } + + #[pymethod] + fn add_note(&self, note: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + if !note.is_instance(PyStrRef::class(&vm.ctx), vm)? { + return Err(vm.new_type_error("add_note() argument must be a string".to_owned())); + } + let mut notes = self.notes.write(); + if notes.is_none() { + *notes = Some(vm.ctx.new_list(vec![])); + } + notes.as_mut().unwrap().append(note); + Ok(()) + } + + #[pymethod(magic)] + fn str(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self + .message + .read() + .as_ref() + .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()); + let num_excs = self.exceptions.read().as_ref().map_or(0, |x| x.len()); + let s = format!( + "{msg} ({num_excs} sub-exception{p})", + p = if num_excs == 1 { "" } else { "s" } + ); + vm.ctx.new_str(s) + } + + #[pymethod(magic)] + fn repr(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self + .message + .read() + .as_ref() + .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()); + let num_excs = self.exceptions.read().as_ref().map_or(0, |x| x.len()); + // TODO: repr of message + let s = format!("{}({msg}, {num_excs})", self.class().name()); + vm.ctx.new_str(s) + } + } #[pyexception(name, base = "PyBaseExceptionGroup", ctx = "exception_group", impl)] #[derive(Debug)] From 57110a513391c1d7a7291dc2dc77983e03fb5e09 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Mon, 31 Mar 2025 11:43:20 -0700 Subject: [PATCH 04/10] fix compile errors --- vm/src/exceptions.rs | 51 ++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 5595488008..f84110468b 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1212,7 +1212,7 @@ pub(super) mod types { function::{ArgBytesLike, FuncArgs}, types::{Constructor, Initializer}, }; - use crate::{PyPayload, builtins::PyListRef, class::StaticType, common::lock::PyRwLock}; + use crate::{builtins::PyListRef, common::lock::PyRwLock, convert::IntoObject, PyPayload}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::str::UnicodeEscapeCodepoint; @@ -1255,7 +1255,7 @@ pub(super) mod types { impl PyBaseExceptionGroup { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let (message, exceptions) = args.bind(vm)?; + let (message, exceptions): (PyStrRef, PyObjectRef) = args.bind(vm)?; let exceptions = exceptions.to_sequence(); let len = exceptions.length(vm)?; if len == 0 { @@ -1264,7 +1264,7 @@ pub(super) mod types { )); } for i in 0..len { - let item = exceptions.get_item(i, vm)?; + let item = exceptions.get_item(i as isize, vm)?; if !item.is_instance(PyBaseException::class(&vm.ctx).into(), vm)? { return Err(vm.new_value_error(format!( "Item {i} of second argument (exceptions) is not an exception" @@ -1275,8 +1275,8 @@ pub(super) mod types { let is_subclass = !(cls.is(PyBaseExceptionGroup::class(&vm.ctx)) || cls.is(PyExceptionGroup::class(&vm.ctx))); for i in 0..len { - let item = exceptions.get_item(i, vm)?; - if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx), vm)? { + let item = exceptions.get_item(i as isize, vm)?; + if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx).into(), vm)? { if is_subclass { return Err(vm.new_type_error(format!( "Cannot nest BaseExceptions in {}", @@ -1291,13 +1291,13 @@ pub(super) mod types { } let mut exceptions_vec = Vec::with_capacity(len); for i in 0..len { - let item = exceptions.get_item(i, vm)?; + let item = exceptions.get_item(i as isize, vm)?; exceptions_vec.push(item.clone()); } - let args = Vec::with_capacity(1 + len); + let mut args = Vec::with_capacity(1 + len); args.push(message.clone().into_object()); for i in 0..len { - let item = exceptions.get_item(i, vm)?; + let item = exceptions.get_item(i as isize, vm)?; args.push(item.clone()); } Ok(PyBaseExceptionGroup { @@ -1310,34 +1310,29 @@ pub(super) mod types { exceptions: PyRwLock::new(exceptions_vec), notes: PyRwLock::new(None), } - .into_pyobject(vm)?) + .into_pyobject(vm)) } #[pygetset] - fn message(&self, vm: &VirtualMachine) -> PyStrRef { + fn message(&self) -> PyStrRef { self.message .read() - .as_ref() - .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()) + .clone() } #[pygetset] fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { - self.exceptions - .read() - .as_ref() - .map_or_else(|| vm.ctx.new_tuple(vec![]), |x| x.clone()) + let exceptions = self.exceptions + .read(); + vm.ctx.new_tuple(exceptions.clone()) } #[pymethod] - fn add_note(&self, note: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - if !note.is_instance(PyStrRef::class(&vm.ctx), vm)? { - return Err(vm.new_type_error("add_note() argument must be a string".to_owned())); - } + fn add_note(&self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { let mut notes = self.notes.write(); if notes.is_none() { *notes = Some(vm.ctx.new_list(vec![])); } - notes.as_mut().unwrap().append(note); + notes.as_mut().unwrap().append(note.into()); Ok(()) } @@ -1345,10 +1340,8 @@ pub(super) mod types { fn str(&self, vm: &VirtualMachine) -> PyStrRef { let msg = self .message - .read() - .as_ref() - .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()); - let num_excs = self.exceptions.read().as_ref().map_or(0, |x| x.len()); + .read(); + let num_excs = self.exceptions.read().len(); let s = format!( "{msg} ({num_excs} sub-exception{p})", p = if num_excs == 1 { "" } else { "s" } @@ -1360,12 +1353,10 @@ pub(super) mod types { fn repr(&self, vm: &VirtualMachine) -> PyStrRef { let msg = self .message - .read() - .as_ref() - .map_or_else(|| vm.ctx.new_str("".to_owned()), |x| x.clone()); - let num_excs = self.exceptions.read().as_ref().map_or(0, |x| x.len()); + .read(); + let num_excs = self.exceptions.read().len(); // TODO: repr of message - let s = format!("{}({msg}, {num_excs})", self.class().name()); + let s = format!("{}({msg}, {num_excs})", Self::class(&vm.ctx).name()); vm.ctx.new_str(s) } } From 59505bcc99cbd11a6c4a099f443742becd033f7d Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Mon, 31 Mar 2025 11:50:25 -0700 Subject: [PATCH 05/10] fix compile --- vm/src/exceptions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index f84110468b..19ed844589 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1240,6 +1240,7 @@ pub(super) mod types { #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group")] #[derive(Debug)] + #[allow(clippy::unused, clippy::dead_code)] pub struct PyBaseExceptionGroup { pub(super) traceback: PyRwLock>, pub(super) cause: PyRwLock>>, From 10e9c3fe6a9fe41b0b80f365efb86e1a3c6b0c63 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Mon, 31 Mar 2025 11:50:47 -0700 Subject: [PATCH 06/10] formatting --- vm/src/exceptions.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 19ed844589..e035807915 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1212,7 +1212,7 @@ pub(super) mod types { function::{ArgBytesLike, FuncArgs}, types::{Constructor, Initializer}, }; - use crate::{builtins::PyListRef, common::lock::PyRwLock, convert::IntoObject, PyPayload}; + use crate::{PyPayload, builtins::PyListRef, common::lock::PyRwLock, convert::IntoObject}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::str::UnicodeEscapeCodepoint; @@ -1316,14 +1316,11 @@ pub(super) mod types { #[pygetset] fn message(&self) -> PyStrRef { - self.message - .read() - .clone() + self.message.read().clone() } #[pygetset] fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { - let exceptions = self.exceptions - .read(); + let exceptions = self.exceptions.read(); vm.ctx.new_tuple(exceptions.clone()) } @@ -1339,9 +1336,7 @@ pub(super) mod types { #[pymethod(magic)] fn str(&self, vm: &VirtualMachine) -> PyStrRef { - let msg = self - .message - .read(); + let msg = self.message.read(); let num_excs = self.exceptions.read().len(); let s = format!( "{msg} ({num_excs} sub-exception{p})", @@ -1352,9 +1347,7 @@ pub(super) mod types { #[pymethod(magic)] fn repr(&self, vm: &VirtualMachine) -> PyStrRef { - let msg = self - .message - .read(); + let msg = self.message.read(); let num_excs = self.exceptions.read().len(); // TODO: repr of message let s = format!("{}({msg}, {num_excs})", Self::class(&vm.ctx).name()); From 80c1aac298afa5ce06a09494be560c6fb94795fb Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Sat, 5 Apr 2025 21:04:27 -0700 Subject: [PATCH 07/10] fix clippy allow --- vm/src/exceptions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index e035807915..8c76ad2b29 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1240,7 +1240,7 @@ pub(super) mod types { #[pyexception(name, base = "PyBaseException", ctx = "base_exception_group")] #[derive(Debug)] - #[allow(clippy::unused, clippy::dead_code)] + #[allow(unused, dead_code)] pub struct PyBaseExceptionGroup { pub(super) traceback: PyRwLock>, pub(super) cause: PyRwLock>>, From ce7a8908b04551f5ebd1f345517862f46c9fbb47 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Sat, 5 Apr 2025 21:11:20 -0700 Subject: [PATCH 08/10] make spell check happy --- vm/src/exceptions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 8c76ad2b29..dbd0245d47 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -244,9 +244,9 @@ impl VirtualMachine { if let Some(text) = maybe_text { // if text ends with \n, remove it - let rtext = text.as_str().trim_end_matches('\n'); - let l_text = rtext.trim_start_matches([' ', '\n', '\x0c']); // \x0c is \f - let spaces = (rtext.len() - l_text.len()) as isize; + let r_text = text.as_str().trim_end_matches('\n'); + let l_text = r_text.trim_start_matches([' ', '\n', '\x0c']); // \x0c is \f + let spaces = (r_text.len() - l_text.len()) as isize; writeln!(output, " {}", l_text)?; From 7f0d4fa0884e03cfad943dc26ee78c283fb29aac Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Sat, 5 Apr 2025 21:16:22 -0700 Subject: [PATCH 09/10] remove old python initialization code --- vm/src/vm/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/vm/src/vm/mod.rs b/vm/src/vm/mod.rs index 7baaae7770..cbda0f6050 100644 --- a/vm/src/vm/mod.rs +++ b/vm/src/vm/mod.rs @@ -386,21 +386,6 @@ impl VirtualMachine { ); } - if expect_stdlib { - // enable python-implemented ExceptionGroup when stdlib exists - let py_core_init = || -> PyResult<()> { - let exception_group = import::import_frozen(self, "_py_exceptiongroup")?; - let base_exception_group = exception_group.get_attr("BaseExceptionGroup", self)?; - self.builtins - .set_attr("BaseExceptionGroup", base_exception_group, self)?; - let exception_group = exception_group.get_attr("ExceptionGroup", self)?; - self.builtins - .set_attr("ExceptionGroup", exception_group, self)?; - Ok(()) - }; - self.expect_pyresult(py_core_init(), "exceptiongroup initialization failed"); - } - self.initialized = true; } From 552ea15036e87dfc3e82aafbf4ad71f69f85f0c4 Mon Sep 17 00:00:00 2001 From: Ashwin Naren Date: Sat, 5 Apr 2025 22:07:59 -0700 Subject: [PATCH 10/10] exception group impl --- vm/src/exceptions.rs | 116 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index dbd0245d47..530234c389 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1355,9 +1355,121 @@ pub(super) mod types { } } - #[pyexception(name, base = "PyBaseExceptionGroup", ctx = "exception_group", impl)] + #[pyexception(name, base = "PyBaseExceptionGroup", ctx = "exception_group")] #[derive(Debug)] - pub struct PyExceptionGroup {} + pub struct PyExceptionGroup { + pub(super) traceback: PyRwLock>, + pub(super) cause: PyRwLock>>, + pub(super) context: PyRwLock>>, + pub(super) suppress_context: AtomicCell, + pub(super) args: PyRwLock, + pub(super) message: PyRwLock, + pub(super) exceptions: PyRwLock>, + pub(super) notes: PyRwLock>, + } + + #[pyexception] + impl PyExceptionGroup { + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let (message, exceptions): (PyStrRef, PyObjectRef) = args.bind(vm)?; + let exceptions = exceptions.to_sequence(); + let len = exceptions.length(vm)?; + if len == 0 { + return Err(vm.new_type_error( + "ExceptionGroup() requires at least one exception".to_owned(), + )); + } + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if !item.is_instance(PyBaseException::class(&vm.ctx).into(), vm)? { + return Err(vm.new_value_error(format!( + "Item {i} of second argument (exceptions) is not an exception" + ))); + } + } + + let is_subclass = !(cls.is(PyBaseExceptionGroup::class(&vm.ctx)) + || cls.is(PyExceptionGroup::class(&vm.ctx))); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + if item.is_instance(PyBaseExceptionGroup::class(&vm.ctx).into(), vm)? { + if is_subclass { + return Err(vm.new_type_error(format!( + "Cannot nest BaseExceptions in {}", + cls.name() + ))); + } else { + return Err(vm.new_type_error( + "Cannot nest BaseExceptions in an ExceptionGroup".to_owned(), + )); + } + } + } + let mut exceptions_vec = Vec::with_capacity(len); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + exceptions_vec.push(item.clone()); + } + let mut args = Vec::with_capacity(1 + len); + args.push(message.clone().into_object()); + for i in 0..len { + let item = exceptions.get_item(i as isize, vm)?; + args.push(item.clone()); + } + Ok(Self { + traceback: PyRwLock::new(None), + cause: PyRwLock::new(None), + context: PyRwLock::new(None), + suppress_context: AtomicCell::new(false), + args: PyRwLock::new(vm.ctx.new_tuple(args)), + message: PyRwLock::new(message), + exceptions: PyRwLock::new(exceptions_vec), + notes: PyRwLock::new(None), + } + .into_pyobject(vm)) + } + + #[pygetset] + fn message(&self) -> PyStrRef { + self.message.read().clone() + } + #[pygetset] + fn exceptions(&self, vm: &VirtualMachine) -> PyTupleRef { + let exceptions = self.exceptions.read(); + vm.ctx.new_tuple(exceptions.clone()) + } + + #[pymethod] + fn add_note(&self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { + let mut notes = self.notes.write(); + if notes.is_none() { + *notes = Some(vm.ctx.new_list(vec![])); + } + notes.as_mut().unwrap().append(note.into()); + Ok(()) + } + + #[pymethod(magic)] + fn str(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + let s = format!( + "{msg} ({num_excs} sub-exception{p})", + p = if num_excs == 1 { "" } else { "s" } + ); + vm.ctx.new_str(s) + } + + #[pymethod(magic)] + fn repr(&self, vm: &VirtualMachine) -> PyStrRef { + let msg = self.message.read(); + let num_excs = self.exceptions.read().len(); + // TODO: repr of message + let s = format!("{}({msg}, {num_excs})", Self::class(&vm.ctx).name()); + vm.ctx.new_str(s) + } + } #[pyexception(name, base = "PyBaseException", ctx = "generator_exit", impl)] #[derive(Debug)]