From 3dea814b5ae23128cc39423c6dbae839c1597893 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 10:33:26 +0000 Subject: [PATCH 1/5] Initial plan From c37fc4fbcbe819c9df1fdd10461c83c8480d7110 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 11:09:22 +0000 Subject: [PATCH 2/5] Implement except* handling and starred subscript support Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- crates/codegen/src/compile.rs | 159 +++++++++++++++++- crates/compiler-core/src/bytecode.rs | 4 + crates/vm/src/frame.rs | 24 ++- .../snippets/except_star_and_starred_index.py | 19 +++ 4 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 extra_tests/snippets/except_star_and_starred_index.py diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 4f7174c8ba..581e9bb00f 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -471,7 +471,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 +2130,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: 1_u32 }); // 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: 1_u32 }); + 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 11d2a7b5f1..77c001a110 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 d9cc404b47..ce7258cd6d 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/except_star_and_starred_index.py b/extra_tests/snippets/except_star_and_starred_index.py new file mode 100644 index 0000000000..11ad28126d --- /dev/null +++ b/extra_tests/snippets/except_star_and_starred_index.py @@ -0,0 +1,19 @@ +def test_except_star_with_plain_exception(): + try: + raise ValueError("x") + except* ValueError as err: + assert isinstance(err, ExceptionGroup) + assert err.exceptions == (ValueError("x"),) + else: + raise AssertionError("except* handler did not run") + + +def test_starred_index_builds_tuple(): + target = {} + target[*"ab"] = 1 + assert list(target.items()) == [(("a", "b"), 1)] + + +if __name__ == "__main__": + test_except_star_with_plain_exception() + test_starred_index_builds_tuple() From 62bb015f516d0e00aaea96d2a349179d30c936cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 12:34:23 +0000 Subject: [PATCH 3/5] Move except* and starred subscript tests into builtin_exceptions Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- extra_tests/snippets/builtin_exceptions.py | 15 +++++++++++++++ .../snippets/except_star_and_starred_index.py | 19 ------------------- 2 files changed, 15 insertions(+), 19 deletions(-) delete mode 100644 extra_tests/snippets/except_star_and_starred_index.py diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index 0af5cf05ea..304b129f1e 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -366,3 +366,18 @@ 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 err.exceptions == (ValueError("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)] diff --git a/extra_tests/snippets/except_star_and_starred_index.py b/extra_tests/snippets/except_star_and_starred_index.py deleted file mode 100644 index 11ad28126d..0000000000 --- a/extra_tests/snippets/except_star_and_starred_index.py +++ /dev/null @@ -1,19 +0,0 @@ -def test_except_star_with_plain_exception(): - try: - raise ValueError("x") - except* ValueError as err: - assert isinstance(err, ExceptionGroup) - assert err.exceptions == (ValueError("x"),) - else: - raise AssertionError("except* handler did not run") - - -def test_starred_index_builds_tuple(): - target = {} - target[*"ab"] = 1 - assert list(target.items()) == [(("a", "b"), 1)] - - -if __name__ == "__main__": - test_except_star_with_plain_exception() - test_starred_index_builds_tuple() From fbcf0b88b32724cb58d0f1e6b3502fa6c0c878f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 12:54:57 +0000 Subject: [PATCH 4/5] Fix CPython-compatible assert in builtin_exceptions test Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- extra_tests/snippets/builtin_exceptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index 304b129f1e..c41637f2fe 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -373,7 +373,9 @@ class SubError(MyError): raise ValueError("x") except* ValueError as err: assert isinstance(err, ExceptionGroup) - assert err.exceptions == (ValueError("x"),) + 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" From 9b941893d538d9a32ccb55662f3bcc9cd50d2a95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:27:13 +0000 Subject: [PATCH 5/5] Refine except* codegen using named constant Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- crates/codegen/src/compile.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 581e9bb00f..b82718ccd1 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 { @@ -2193,7 +2194,7 @@ impl Compiler { } ); emit!(self, Instruction::UnpackSequence { size: 2 }); // stack: [rest, match] - emit!(self, Instruction::CopyItem { index: 1_u32 }); // duplicate match for truthiness test + emit!(self, Instruction::CopyItem { index: COPY_TOP }); // duplicate match for truthiness test emit!(self, Instruction::ToBool); emit!(self, Instruction::PopJumpIfFalse { target: skip_block }); @@ -2226,7 +2227,7 @@ impl Compiler { let handled_block = self.new_block(); // If remainder is truthy, re-raise it - emit!(self, Instruction::CopyItem { index: 1_u32 }); + emit!(self, Instruction::CopyItem { index: COPY_TOP }); emit!(self, Instruction::ToBool); emit!( self,