diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 4f7174c8bad..b82718ccd1f 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -45,6 +45,7 @@ use rustpython_wtf8::Wtf8Buf; use std::{borrow::Cow, collections::HashSet}; const MAXBLOCKS: usize = 20; +const COPY_TOP: u32 = 1; #[derive(Debug, Clone, Copy)] pub enum FBlockType { @@ -471,7 +472,16 @@ impl Compiler { } } else { // VISIT(c, expr, e->v.Subscript.slice) - self.compile_expression(slice)?; + match slice { + Expr::Starred(starred) => { + self.starunpack_helper( + &[Expr::Starred(starred.clone())], + 0, + CollectionType::Tuple, + )?; + } + other => self.compile_expression(other)?, + } // Emit appropriate instruction based on context match ctx { @@ -2121,12 +2131,150 @@ impl Compiler { fn compile_try_star_statement( &mut self, - _body: &[Stmt], - _handlers: &[ExceptHandler], - _orelse: &[Stmt], - _finalbody: &[Stmt], + body: &[Stmt], + handlers: &[ExceptHandler], + orelse: &[Stmt], + finalbody: &[Stmt], ) -> CompileResult<()> { - Err(self.error(CodegenErrorType::NotImplementedYet)) + let handler_block = self.new_block(); + let finally_block = self.new_block(); + + if !finalbody.is_empty() { + emit!( + self, + Instruction::SetupFinally { + handler: finally_block, + } + ); + } + + let else_block = self.new_block(); + + emit!( + self, + Instruction::SetupExcept { + handler: handler_block, + } + ); + self.compile_statements(body)?; + emit!(self, Instruction::PopBlock); + emit!(self, Instruction::Jump { target: else_block }); + + self.switch_to_block(handler_block); + // incoming stack: [exc] + emit!( + self, + Instruction::CallIntrinsic1 { + func: bytecode::IntrinsicFunction1::EnsureExceptionGroup + } + ); + // stack: [remainder] + + for handler in handlers { + let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { + type_, name, body, .. + }) = handler; + + let skip_block = self.new_block(); + let next_block = self.new_block(); + + // Split current remainder for this handler + if let Some(exc_type) = type_ { + self.compile_expression(exc_type)?; + } else { + return Err(self.error(CodegenErrorType::SyntaxError( + "except* must specify an exception type".to_owned(), + ))); + } + + emit!( + self, + Instruction::CallIntrinsic2 { + func: bytecode::IntrinsicFunction2::ExceptStarMatch + } + ); + emit!(self, Instruction::UnpackSequence { size: 2 }); // stack: [rest, match] + emit!(self, Instruction::CopyItem { index: COPY_TOP }); // duplicate match for truthiness test + emit!(self, Instruction::ToBool); + emit!(self, Instruction::PopJumpIfFalse { target: skip_block }); + + // Handler selected, stack: [rest, match] + if let Some(alias) = name { + self.store_name(alias.as_str())?; + } else { + emit!(self, Instruction::Pop); + } + + self.compile_statements(body)?; + + if let Some(alias) = name { + self.emit_load_const(ConstantData::None); + self.store_name(alias.as_str())?; + self.compile_name(alias.as_str(), NameUsage::Delete)?; + } + + emit!(self, Instruction::Jump { target: next_block }); + + // Skip handler when no match (stack currently [rest, match]) + self.switch_to_block(skip_block); + emit!(self, Instruction::Pop); // drop match + emit!(self, Instruction::Jump { target: next_block }); + + // Continue with remaining exceptions + self.switch_to_block(next_block); + } + + let handled_block = self.new_block(); + + // If remainder is truthy, re-raise it + emit!(self, Instruction::CopyItem { index: COPY_TOP }); + emit!(self, Instruction::ToBool); + emit!( + self, + Instruction::PopJumpIfFalse { + target: handled_block + } + ); + emit!( + self, + Instruction::Raise { + kind: bytecode::RaiseKind::Raise + } + ); + + // All exceptions handled + self.switch_to_block(handled_block); + emit!(self, Instruction::Pop); // drop remainder (None) + emit!(self, Instruction::PopException); + + if !finalbody.is_empty() { + emit!(self, Instruction::PopBlock); + emit!(self, Instruction::EnterFinally); + } + + emit!( + self, + Instruction::Jump { + target: finally_block, + } + ); + + // try-else path + self.switch_to_block(else_block); + self.compile_statements(orelse)?; + + if !finalbody.is_empty() { + emit!(self, Instruction::PopBlock); + emit!(self, Instruction::EnterFinally); + } + + self.switch_to_block(finally_block); + if !finalbody.is_empty() { + self.compile_statements(finalbody)?; + emit!(self, Instruction::EndFinally); + } + + Ok(()) } fn is_forbidden_arg_name(name: &str) -> bool { diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index 11d2a7b5f1b..77c001a1108 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -566,6 +566,8 @@ op_arg_enum!( /// Generic subscript for PEP 695 SubscriptGeneric = 10, TypeAlias = 11, + /// Ensure an exception is normalized to BaseExceptionGroup + EnsureExceptionGroup = 12, } ); @@ -580,6 +582,8 @@ op_arg_enum!( SetFunctionTypeParams = 4, /// Set default value for type parameter (PEP 695) SetTypeparamDefault = 5, + /// Split an ExceptionGroup according to except* handler condition + ExceptStarMatch = 6, } ); diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index d9cc404b47a..ce7258cd6d6 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -8,7 +8,7 @@ use crate::{ tuple::{PyTuple, PyTupleRef}, }, bytecode, - convert::{IntoObject, ToPyResult}, + convert::{IntoObject, ToPyObject, ToPyResult}, coroutine::Coro, exceptions::ExceptionCtor, function::{ArgMapping, Either, FuncArgs}, @@ -2460,6 +2460,18 @@ impl ExecutingFrame<'_> { .map_err(|_| vm.new_type_error("LIST_TO_TUPLE expects a list"))?; Ok(vm.ctx.new_tuple(list.borrow_vec().to_vec()).into()) } + bytecode::IntrinsicFunction1::EnsureExceptionGroup => { + if arg.fast_isinstance(vm.ctx.exceptions.base_exception_group) { + Ok(arg) + } else { + let exception_group_type = crate::exception_group::exception_group(); + let wrapped = exception_group_type + .to_owned() + .to_pyobject(vm) + .call((vm.ctx.new_str(""), vm.ctx.new_tuple(vec![arg])), vm)?; + Ok(wrapped) + } + } } } @@ -2494,6 +2506,16 @@ impl ExecutingFrame<'_> { .into(); Ok(type_var) } + bytecode::IntrinsicFunction2::ExceptStarMatch => { + let result = vm.call_method(&arg1, "split", (arg2,))?; + let result_tuple: PyTupleRef = result.try_into_value(vm)?; + if result_tuple.len() != 2 { + return Err(vm.new_type_error("ExceptionGroup.split must return 2-tuple")); + } + let matched = result_tuple[0].clone(); + let rest = result_tuple[1].clone(); + Ok(vm.ctx.new_tuple(vec![matched, rest]).into()) + } } } diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index 0af5cf05eaf..c41637f2fe7 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -366,3 +366,20 @@ class SubError(MyError): vars(builtins).values(), ): assert isinstance(exc.__doc__, str) + + +# except* handling should normalize non-group exceptions +try: + raise ValueError("x") +except* ValueError as err: + assert isinstance(err, ExceptionGroup) + assert len(err.exceptions) == 1 + assert isinstance(err.exceptions[0], ValueError) + assert err.exceptions[0].args == ("x",) +else: + assert False, "except* handler did not run" + +# Starred expressions in subscripts build tuple keys +mapping = {} +mapping[*"ab"] = 1 +assert list(mapping.items()) == [(("a", "b"), 1)]