From 8bca3cd379e8e23fc747384f56af8bc1629db14f 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/7] Initial plan From 1e9ab2a8972a80689f8dc05ece71fe29dcf941a7 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/7] 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 0131b3008e3..a6b280f3190 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 dd49e679f27..25a0076835d 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 e9a938ba023..253cc9756af 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 00000000000..11ad28126df --- /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 97ecbdaaa4f10a18065ccd15f068ee4fa3ecad43 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/7] 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 0af5cf05eaf..304b129f1eb 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 11ad28126df..00000000000 --- 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 4dc01521ac7b3d741df99fa3504afd7e30246933 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/7] 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 304b129f1eb..c41637f2fe7 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 a62981fcb9c562f9072862d510d6c06be42f92df 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/7] 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 a6b280f3190..873a48f843f 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, From a73b2d688318218e16dc2d50223c96b9d81c6338 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 26 Dec 2025 01:05:23 +0900 Subject: [PATCH 6/7] except* --- crates/codegen/src/compile.rs | 64 +++++------ crates/compiler-core/src/bytecode.rs | 11 +- crates/vm/src/exceptions.rs | 124 +++++++++++++++++++++ crates/vm/src/frame.rs | 37 +++--- extra_tests/snippets/builtin_exceptions.py | 5 - 5 files changed, 170 insertions(+), 71 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 873a48f843f..051cceb69ef 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -45,7 +45,6 @@ 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 { @@ -472,16 +471,7 @@ impl Compiler { } } else { // VISIT(c, expr, e->v.Subscript.slice) - match slice { - Expr::Starred(starred) => { - self.starunpack_helper( - &[Expr::Starred(starred.clone())], - 0, - CollectionType::Tuple, - )?; - } - other => self.compile_expression(other)?, - } + self.compile_expression(slice)?; // Emit appropriate instruction based on context match ctx { @@ -1520,7 +1510,7 @@ impl Compiler { .. }) => { if *is_star { - self.compile_try_star_statement(body, handlers, orelse, finalbody)? + self.compile_try_star_except(body, handlers, orelse, finalbody)? } else { self.compile_try_statement(body, handlers, orelse, finalbody)? } @@ -2129,13 +2119,14 @@ impl Compiler { Ok(()) } - fn compile_try_star_statement( + fn compile_try_star_except( &mut self, body: &[Stmt], handlers: &[ExceptHandler], orelse: &[Stmt], finalbody: &[Stmt], ) -> CompileResult<()> { + // Simplified except* implementation using CheckEgMatch let handler_block = self.new_block(); let finally_block = self.new_block(); @@ -2161,14 +2152,7 @@ impl Compiler { 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] + // Stack: [exc] for handler in handlers { let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { @@ -2178,7 +2162,7 @@ impl Compiler { let skip_block = self.new_block(); let next_block = self.new_block(); - // Split current remainder for this handler + // Compile exception type if let Some(exc_type) = type_ { self.compile_expression(exc_type)?; } else { @@ -2186,24 +2170,24 @@ impl Compiler { "except* must specify an exception type".to_owned(), ))); } + // Stack: [exc, type] - 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::CheckEgMatch); + // Stack: [rest, match] + + // Check if match is None (truthy check) + emit!(self, Instruction::CopyItem { index: 1 }); emit!(self, Instruction::ToBool); emit!(self, Instruction::PopJumpIfFalse { target: skip_block }); - // Handler selected, stack: [rest, match] + // Handler matched - store match to name if provided + // Stack: [rest, match] if let Some(alias) = name { self.store_name(alias.as_str())?; } else { - emit!(self, Instruction::Pop); + emit!(self, Instruction::PopTop); } + // Stack: [rest] self.compile_statements(body)?; @@ -2215,19 +2199,20 @@ impl Compiler { emit!(self, Instruction::Jump { target: next_block }); - // Skip handler when no match (stack currently [rest, match]) + // No match - pop match (None) and continue with rest self.switch_to_block(skip_block); - emit!(self, Instruction::Pop); // drop match - emit!(self, Instruction::Jump { target: next_block }); + emit!(self, Instruction::PopTop); // drop match (None) + // Stack: [rest] - // Continue with remaining exceptions self.switch_to_block(next_block); + // Stack: [rest] - continue with rest for next handler } let handled_block = self.new_block(); - // If remainder is truthy, re-raise it - emit!(self, Instruction::CopyItem { index: COPY_TOP }); + // Check if remainder is truthy (has unhandled exceptions) + // Stack: [rest] + emit!(self, Instruction::CopyItem { index: 1 }); emit!(self, Instruction::ToBool); emit!( self, @@ -2235,6 +2220,7 @@ impl Compiler { target: handled_block } ); + // Reraise unhandled exceptions emit!( self, Instruction::Raise { @@ -2244,7 +2230,7 @@ impl Compiler { // All exceptions handled self.switch_to_block(handled_block); - emit!(self, Instruction::Pop); // drop remainder (None) + emit!(self, Instruction::PopTop); // drop remainder (None) emit!(self, Instruction::PopException); if !finalbody.is_empty() { diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index 25a0076835d..add2c1c2f63 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -566,8 +566,6 @@ op_arg_enum!( /// Generic subscript for PEP 695 SubscriptGeneric = 10, TypeAlias = 11, - /// Ensure an exception is normalized to BaseExceptionGroup - EnsureExceptionGroup = 12, } ); @@ -576,14 +574,12 @@ op_arg_enum!( #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] pub enum IntrinsicFunction2 { - // PrepReraiseS tar = 1, + PrepReraiseStar = 1, TypeVarWithBound = 2, TypeVarWithConstraint = 3, SetFunctionTypeParams = 4, /// Set default value for type parameter (PEP 695) SetTypeparamDefault = 5, - /// Split an ExceptionGroup according to except* handler condition - ExceptStarMatch = 6, } ); @@ -656,6 +652,9 @@ pub enum Instruction { CallMethodPositional { nargs: Arg, }, + /// Check if exception matches except* handler type. + /// Pops exc_value and match_type, pushes (rest, match). + CheckEgMatch, CompareOperation { op: Arg, }, @@ -1725,6 +1724,7 @@ impl Instruction { CallMethodKeyword { nargs } => -1 - (nargs.get(arg) as i32) - 3 + 1, CallFunctionEx { has_kwargs } => -1 - (has_kwargs.get(arg) as i32) - 1 + 1, CallMethodEx { has_kwargs } => -1 - (has_kwargs.get(arg) as i32) - 3 + 1, + CheckEgMatch => 0, // pops 2 (exc, type), pushes 2 (rest, match) ConvertValue { .. } => 0, FormatSimple => 0, FormatWithSpec => -1, @@ -1891,6 +1891,7 @@ impl Instruction { CallMethodEx { has_kwargs } => w!(CALL_METHOD_EX, has_kwargs), CallMethodKeyword { nargs } => w!(CALL_METHOD_KEYWORD, nargs), CallMethodPositional { nargs } => w!(CALL_METHOD_POSITIONAL, nargs), + CheckEgMatch => w!(CHECK_EG_MATCH), CompareOperation { op } => w!(COMPARE_OPERATION, ?op), ContainsOp(inv) => w!(CONTAINS_OP, ?inv), Continue { target } => w!(CONTINUE, target), diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 8d6ce6142b4..7b8af83489e 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -2432,3 +2432,127 @@ pub(super) mod types { #[repr(transparent)] pub struct PyEncodingWarning(PyWarning); } + +/// Match exception against except* handler type. +/// Returns (rest, match) tuple. +pub fn exception_group_match( + exc_value: &PyObjectRef, + match_type: &PyObjectRef, + vm: &VirtualMachine, +) -> PyResult<(PyObjectRef, PyObjectRef)> { + // Implements _PyEval_ExceptionGroupMatch + + // If exc_value is None, return (None, None) + if vm.is_none(exc_value) { + return Ok((vm.ctx.none(), vm.ctx.none())); + } + + // Check if exc_value matches match_type + if exc_value.is_instance(match_type, vm)? { + // Full match of exc itself + let is_eg = exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group); + let matched = if is_eg { + exc_value.clone() + } else { + // Naked exception - wrap it in ExceptionGroup + let excs = vm.ctx.new_tuple(vec![exc_value.clone()]); + let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into(); + let wrapped = eg_type.call((vm.ctx.new_str(""), excs), vm)?; + // Copy traceback from original exception + if let Ok(exc) = exc_value.clone().downcast::() + && let Some(tb) = exc.__traceback__() + && let Ok(wrapped_exc) = wrapped.clone().downcast::() + { + let _ = wrapped_exc.set___traceback__(tb.into(), vm); + } + wrapped + }; + return Ok((vm.ctx.none(), matched)); + } + + // Check for partial match if it's an exception group + if exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group) { + let pair = vm.call_method(exc_value, "split", (match_type.clone(),))?; + let pair_tuple: PyTupleRef = pair.try_into_value(vm)?; + if pair_tuple.len() < 2 { + return Err(vm.new_type_error(format!( + "{}.split must return a 2-tuple, got tuple of size {}", + exc_value.class().name(), + pair_tuple.len() + ))); + } + let matched = pair_tuple[0].clone(); + let rest = pair_tuple[1].clone(); + return Ok((rest, matched)); + } + + // No match + Ok((exc_value.clone(), vm.ctx.none())) +} + +/// Prepare exception for reraise in except* block. +pub fn prep_reraise_star(orig: PyObjectRef, excs: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // Implements _PyExc_PrepReraiseStar + use crate::builtins::PyList; + + let excs_list = excs + .downcast::() + .map_err(|_| vm.new_type_error("expected list for prep_reraise_star"))?; + + let excs_vec: Vec = excs_list.borrow_vec().to_vec(); + + // Filter out None values + let mut raised: Vec = Vec::new(); + let mut reraised: Vec = Vec::new(); + + for exc in excs_vec { + if vm.is_none(&exc) { + continue; + } + // Check if this exception was in the original exception group + if !vm.is_none(&orig) && is_same_exception_metadata(&exc, &orig, vm) { + reraised.push(exc); + } else { + raised.push(exc); + } + } + + // If no exceptions to reraise, return None + if raised.is_empty() && reraised.is_empty() { + return Ok(vm.ctx.none()); + } + + // Combine raised and reraised exceptions + let mut all_excs = raised; + all_excs.extend(reraised); + + if all_excs.len() == 1 { + // If only one exception, just return it + return Ok(all_excs.into_iter().next().unwrap()); + } + + // Create new ExceptionGroup + let excs_tuple = vm.ctx.new_tuple(all_excs); + let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into(); + eg_type.call((vm.ctx.new_str(""), excs_tuple), vm) +} + +/// Check if two exceptions have the same metadata (for reraise detection) +fn is_same_exception_metadata(exc1: &PyObjectRef, exc2: &PyObjectRef, vm: &VirtualMachine) -> bool { + // Check if exc1 is part of exc2's exception group + if exc2.fast_isinstance(vm.ctx.exceptions.base_exception_group) { + let exc_class: PyObjectRef = exc1.class().to_owned().into(); + if let Ok(result) = vm.call_method(exc2, "subgroup", (exc_class,)) + && !vm.is_none(&result) + && let Ok(subgroup_excs) = result.get_attr("exceptions", vm) + && let Ok(tuple) = subgroup_excs.downcast::() + { + for e in tuple.iter() { + if e.is(exc1) { + return true; + } + } + } + } + exc1.is(exc2) +} diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 253cc9756af..6cb41f41d93 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, ToPyObject, ToPyResult}, + convert::{IntoObject, ToPyResult}, coroutine::Coro, exceptions::ExceptionCtor, function::{ArgMapping, Either, FuncArgs}, @@ -691,6 +691,15 @@ impl ExecutingFrame<'_> { let args = self.collect_positional_args(nargs.get(arg)); self.execute_method_call(args, vm) } + bytecode::Instruction::CheckEgMatch => { + let match_type = self.pop_value(); + let exc_value = self.pop_value(); + let (rest, matched) = + crate::exceptions::exception_group_match(&exc_value, &match_type, vm)?; + self.push_value(rest); + self.push_value(matched); + Ok(None) + } bytecode::Instruction::CompareOperation { op } => self.execute_compare(vm, op.get(arg)), bytecode::Instruction::ContainsOp(invert) => { let b = self.pop_value(); @@ -2460,18 +2469,6 @@ 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) - } - } } } @@ -2506,15 +2503,11 @@ 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()) + bytecode::IntrinsicFunction2::PrepReraiseStar => { + // arg1 = orig (original exception) + // arg2 = excs (list of exceptions raised/reraised in except* blocks) + // Returns: exception to reraise, or None if nothing to reraise + crate::exceptions::prep_reraise_star(arg1, arg2, vm) } } } diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index c41637f2fe7..246be3b8fda 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -378,8 +378,3 @@ class SubError(MyError): 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)] From 8c2cddc04088fdc773c9421bcf98f039c82dd298 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 26 Dec 2025 08:15:46 +0900 Subject: [PATCH 7/7] impl syntaxerror --- .cspell.dict/python-more.txt | 1 + Lib/test/test_compile.py | 6 ------ crates/codegen/src/compile.rs | 9 +++++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.cspell.dict/python-more.txt b/.cspell.dict/python-more.txt index e8534e9744a..d381bfe1e03 100644 --- a/.cspell.dict/python-more.txt +++ b/.cspell.dict/python-more.txt @@ -199,6 +199,7 @@ readbuffer reconstructor refcnt releaselevel +reraised reverseitemiterator reverseiterator reversekeyiterator diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index e4d335a193d..3ea09354c3c 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1536,8 +1536,6 @@ def test_try_except_as(self): """ self.check_stack_size(snippet) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_try_except_star_qualified(self): snippet = """ try: @@ -1549,8 +1547,6 @@ def test_try_except_star_qualified(self): """ self.check_stack_size(snippet) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_try_except_star_as(self): snippet = """ try: @@ -1562,8 +1558,6 @@ def test_try_except_star_as(self): """ self.check_stack_size(snippet) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_try_except_star_finally(self): snippet = """ try: diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 051cceb69ef..53cd81a4f42 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -2164,6 +2164,15 @@ impl Compiler { // Compile exception type if let Some(exc_type) = type_ { + // Check for unparenthesized tuple (e.g., `except* A, B:` instead of `except* (A, B):`) + if let Expr::Tuple(ExprTuple { elts, range, .. }) = exc_type.as_ref() + && let Some(first) = elts.first() + && range.start().to_u32() == first.range().start().to_u32() + { + return Err(self.error(CodegenErrorType::SyntaxError( + "multiple exception types must be parenthesized".to_owned(), + ))); + } self.compile_expression(exc_type)?; } else { return Err(self.error(CodegenErrorType::SyntaxError(