Skip to content

Commit 4a6e8fb

Browse files
authored
Add except* support (#6530)
1 parent 57b4b4a commit 4a6e8fb

File tree

7 files changed

+308
-14
lines changed

7 files changed

+308
-14
lines changed

.cspell.dict/python-more.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ readbuffer
199199
reconstructor
200200
refcnt
201201
releaselevel
202+
reraised
202203
reverseitemiterator
203204
reverseiterator
204205
reversekeyiterator

Lib/test/test_compile.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,8 +1536,6 @@ def test_try_except_as(self):
15361536
"""
15371537
self.check_stack_size(snippet)
15381538

1539-
# TODO: RUSTPYTHON
1540-
@unittest.expectedFailure
15411539
def test_try_except_star_qualified(self):
15421540
snippet = """
15431541
try:
@@ -1549,8 +1547,6 @@ def test_try_except_star_qualified(self):
15491547
"""
15501548
self.check_stack_size(snippet)
15511549

1552-
# TODO: RUSTPYTHON
1553-
@unittest.expectedFailure
15541550
def test_try_except_star_as(self):
15551551
snippet = """
15561552
try:
@@ -1562,8 +1558,6 @@ def test_try_except_star_as(self):
15621558
"""
15631559
self.check_stack_size(snippet)
15641560

1565-
# TODO: RUSTPYTHON
1566-
@unittest.expectedFailure
15671561
def test_try_except_star_finally(self):
15681562
snippet = """
15691563
try:

crates/codegen/src/compile.rs

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,7 +1510,7 @@ impl Compiler {
15101510
..
15111511
}) => {
15121512
if *is_star {
1513-
self.compile_try_star_statement(body, handlers, orelse, finalbody)?
1513+
self.compile_try_star_except(body, handlers, orelse, finalbody)?
15141514
} else {
15151515
self.compile_try_statement(body, handlers, orelse, finalbody)?
15161516
}
@@ -2119,14 +2119,157 @@ impl Compiler {
21192119
Ok(())
21202120
}
21212121

2122-
fn compile_try_star_statement(
2122+
fn compile_try_star_except(
21232123
&mut self,
2124-
_body: &[Stmt],
2125-
_handlers: &[ExceptHandler],
2126-
_orelse: &[Stmt],
2127-
_finalbody: &[Stmt],
2124+
body: &[Stmt],
2125+
handlers: &[ExceptHandler],
2126+
orelse: &[Stmt],
2127+
finalbody: &[Stmt],
21282128
) -> CompileResult<()> {
2129-
Err(self.error(CodegenErrorType::NotImplementedYet))
2129+
// Simplified except* implementation using CheckEgMatch
2130+
let handler_block = self.new_block();
2131+
let finally_block = self.new_block();
2132+
2133+
if !finalbody.is_empty() {
2134+
emit!(
2135+
self,
2136+
Instruction::SetupFinally {
2137+
handler: finally_block,
2138+
}
2139+
);
2140+
}
2141+
2142+
let else_block = self.new_block();
2143+
2144+
emit!(
2145+
self,
2146+
Instruction::SetupExcept {
2147+
handler: handler_block,
2148+
}
2149+
);
2150+
self.compile_statements(body)?;
2151+
emit!(self, Instruction::PopBlock);
2152+
emit!(self, Instruction::Jump { target: else_block });
2153+
2154+
self.switch_to_block(handler_block);
2155+
// Stack: [exc]
2156+
2157+
for handler in handlers {
2158+
let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler {
2159+
type_, name, body, ..
2160+
}) = handler;
2161+
2162+
let skip_block = self.new_block();
2163+
let next_block = self.new_block();
2164+
2165+
// Compile exception type
2166+
if let Some(exc_type) = type_ {
2167+
// Check for unparenthesized tuple (e.g., `except* A, B:` instead of `except* (A, B):`)
2168+
if let Expr::Tuple(ExprTuple { elts, range, .. }) = exc_type.as_ref()
2169+
&& let Some(first) = elts.first()
2170+
&& range.start().to_u32() == first.range().start().to_u32()
2171+
{
2172+
return Err(self.error(CodegenErrorType::SyntaxError(
2173+
"multiple exception types must be parenthesized".to_owned(),
2174+
)));
2175+
}
2176+
self.compile_expression(exc_type)?;
2177+
} else {
2178+
return Err(self.error(CodegenErrorType::SyntaxError(
2179+
"except* must specify an exception type".to_owned(),
2180+
)));
2181+
}
2182+
// Stack: [exc, type]
2183+
2184+
emit!(self, Instruction::CheckEgMatch);
2185+
// Stack: [rest, match]
2186+
2187+
// Check if match is None (truthy check)
2188+
emit!(self, Instruction::CopyItem { index: 1 });
2189+
emit!(self, Instruction::ToBool);
2190+
emit!(self, Instruction::PopJumpIfFalse { target: skip_block });
2191+
2192+
// Handler matched - store match to name if provided
2193+
// Stack: [rest, match]
2194+
if let Some(alias) = name {
2195+
self.store_name(alias.as_str())?;
2196+
} else {
2197+
emit!(self, Instruction::PopTop);
2198+
}
2199+
// Stack: [rest]
2200+
2201+
self.compile_statements(body)?;
2202+
2203+
if let Some(alias) = name {
2204+
self.emit_load_const(ConstantData::None);
2205+
self.store_name(alias.as_str())?;
2206+
self.compile_name(alias.as_str(), NameUsage::Delete)?;
2207+
}
2208+
2209+
emit!(self, Instruction::Jump { target: next_block });
2210+
2211+
// No match - pop match (None) and continue with rest
2212+
self.switch_to_block(skip_block);
2213+
emit!(self, Instruction::PopTop); // drop match (None)
2214+
// Stack: [rest]
2215+
2216+
self.switch_to_block(next_block);
2217+
// Stack: [rest] - continue with rest for next handler
2218+
}
2219+
2220+
let handled_block = self.new_block();
2221+
2222+
// Check if remainder is truthy (has unhandled exceptions)
2223+
// Stack: [rest]
2224+
emit!(self, Instruction::CopyItem { index: 1 });
2225+
emit!(self, Instruction::ToBool);
2226+
emit!(
2227+
self,
2228+
Instruction::PopJumpIfFalse {
2229+
target: handled_block
2230+
}
2231+
);
2232+
// Reraise unhandled exceptions
2233+
emit!(
2234+
self,
2235+
Instruction::Raise {
2236+
kind: bytecode::RaiseKind::Raise
2237+
}
2238+
);
2239+
2240+
// All exceptions handled
2241+
self.switch_to_block(handled_block);
2242+
emit!(self, Instruction::PopTop); // drop remainder (None)
2243+
emit!(self, Instruction::PopException);
2244+
2245+
if !finalbody.is_empty() {
2246+
emit!(self, Instruction::PopBlock);
2247+
emit!(self, Instruction::EnterFinally);
2248+
}
2249+
2250+
emit!(
2251+
self,
2252+
Instruction::Jump {
2253+
target: finally_block,
2254+
}
2255+
);
2256+
2257+
// try-else path
2258+
self.switch_to_block(else_block);
2259+
self.compile_statements(orelse)?;
2260+
2261+
if !finalbody.is_empty() {
2262+
emit!(self, Instruction::PopBlock);
2263+
emit!(self, Instruction::EnterFinally);
2264+
}
2265+
2266+
self.switch_to_block(finally_block);
2267+
if !finalbody.is_empty() {
2268+
self.compile_statements(finalbody)?;
2269+
emit!(self, Instruction::EndFinally);
2270+
}
2271+
2272+
Ok(())
21302273
}
21312274

21322275
fn is_forbidden_arg_name(name: &str) -> bool {

crates/compiler-core/src/bytecode.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ op_arg_enum!(
574574
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
575575
#[repr(u8)]
576576
pub enum IntrinsicFunction2 {
577-
// PrepReraiseS tar = 1,
577+
PrepReraiseStar = 1,
578578
TypeVarWithBound = 2,
579579
TypeVarWithConstraint = 3,
580580
SetFunctionTypeParams = 4,
@@ -652,6 +652,9 @@ pub enum Instruction {
652652
CallMethodPositional {
653653
nargs: Arg<u32>,
654654
},
655+
/// Check if exception matches except* handler type.
656+
/// Pops exc_value and match_type, pushes (rest, match).
657+
CheckEgMatch,
655658
CompareOperation {
656659
op: Arg<ComparisonOperator>,
657660
},
@@ -1721,6 +1724,7 @@ impl Instruction {
17211724
CallMethodKeyword { nargs } => -1 - (nargs.get(arg) as i32) - 3 + 1,
17221725
CallFunctionEx { has_kwargs } => -1 - (has_kwargs.get(arg) as i32) - 1 + 1,
17231726
CallMethodEx { has_kwargs } => -1 - (has_kwargs.get(arg) as i32) - 3 + 1,
1727+
CheckEgMatch => 0, // pops 2 (exc, type), pushes 2 (rest, match)
17241728
ConvertValue { .. } => 0,
17251729
FormatSimple => 0,
17261730
FormatWithSpec => -1,
@@ -1887,6 +1891,7 @@ impl Instruction {
18871891
CallMethodEx { has_kwargs } => w!(CALL_METHOD_EX, has_kwargs),
18881892
CallMethodKeyword { nargs } => w!(CALL_METHOD_KEYWORD, nargs),
18891893
CallMethodPositional { nargs } => w!(CALL_METHOD_POSITIONAL, nargs),
1894+
CheckEgMatch => w!(CHECK_EG_MATCH),
18901895
CompareOperation { op } => w!(COMPARE_OPERATION, ?op),
18911896
ContainsOp(inv) => w!(CONTAINS_OP, ?inv),
18921897
Continue { target } => w!(CONTINUE, target),

crates/vm/src/exceptions.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,3 +2432,127 @@ pub(super) mod types {
24322432
#[repr(transparent)]
24332433
pub struct PyEncodingWarning(PyWarning);
24342434
}
2435+
2436+
/// Match exception against except* handler type.
2437+
/// Returns (rest, match) tuple.
2438+
pub fn exception_group_match(
2439+
exc_value: &PyObjectRef,
2440+
match_type: &PyObjectRef,
2441+
vm: &VirtualMachine,
2442+
) -> PyResult<(PyObjectRef, PyObjectRef)> {
2443+
// Implements _PyEval_ExceptionGroupMatch
2444+
2445+
// If exc_value is None, return (None, None)
2446+
if vm.is_none(exc_value) {
2447+
return Ok((vm.ctx.none(), vm.ctx.none()));
2448+
}
2449+
2450+
// Check if exc_value matches match_type
2451+
if exc_value.is_instance(match_type, vm)? {
2452+
// Full match of exc itself
2453+
let is_eg = exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group);
2454+
let matched = if is_eg {
2455+
exc_value.clone()
2456+
} else {
2457+
// Naked exception - wrap it in ExceptionGroup
2458+
let excs = vm.ctx.new_tuple(vec![exc_value.clone()]);
2459+
let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into();
2460+
let wrapped = eg_type.call((vm.ctx.new_str(""), excs), vm)?;
2461+
// Copy traceback from original exception
2462+
if let Ok(exc) = exc_value.clone().downcast::<types::PyBaseException>()
2463+
&& let Some(tb) = exc.__traceback__()
2464+
&& let Ok(wrapped_exc) = wrapped.clone().downcast::<types::PyBaseException>()
2465+
{
2466+
let _ = wrapped_exc.set___traceback__(tb.into(), vm);
2467+
}
2468+
wrapped
2469+
};
2470+
return Ok((vm.ctx.none(), matched));
2471+
}
2472+
2473+
// Check for partial match if it's an exception group
2474+
if exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2475+
let pair = vm.call_method(exc_value, "split", (match_type.clone(),))?;
2476+
let pair_tuple: PyTupleRef = pair.try_into_value(vm)?;
2477+
if pair_tuple.len() < 2 {
2478+
return Err(vm.new_type_error(format!(
2479+
"{}.split must return a 2-tuple, got tuple of size {}",
2480+
exc_value.class().name(),
2481+
pair_tuple.len()
2482+
)));
2483+
}
2484+
let matched = pair_tuple[0].clone();
2485+
let rest = pair_tuple[1].clone();
2486+
return Ok((rest, matched));
2487+
}
2488+
2489+
// No match
2490+
Ok((exc_value.clone(), vm.ctx.none()))
2491+
}
2492+
2493+
/// Prepare exception for reraise in except* block.
2494+
pub fn prep_reraise_star(orig: PyObjectRef, excs: PyObjectRef, vm: &VirtualMachine) -> PyResult {
2495+
// Implements _PyExc_PrepReraiseStar
2496+
use crate::builtins::PyList;
2497+
2498+
let excs_list = excs
2499+
.downcast::<PyList>()
2500+
.map_err(|_| vm.new_type_error("expected list for prep_reraise_star"))?;
2501+
2502+
let excs_vec: Vec<PyObjectRef> = excs_list.borrow_vec().to_vec();
2503+
2504+
// Filter out None values
2505+
let mut raised: Vec<PyObjectRef> = Vec::new();
2506+
let mut reraised: Vec<PyObjectRef> = Vec::new();
2507+
2508+
for exc in excs_vec {
2509+
if vm.is_none(&exc) {
2510+
continue;
2511+
}
2512+
// Check if this exception was in the original exception group
2513+
if !vm.is_none(&orig) && is_same_exception_metadata(&exc, &orig, vm) {
2514+
reraised.push(exc);
2515+
} else {
2516+
raised.push(exc);
2517+
}
2518+
}
2519+
2520+
// If no exceptions to reraise, return None
2521+
if raised.is_empty() && reraised.is_empty() {
2522+
return Ok(vm.ctx.none());
2523+
}
2524+
2525+
// Combine raised and reraised exceptions
2526+
let mut all_excs = raised;
2527+
all_excs.extend(reraised);
2528+
2529+
if all_excs.len() == 1 {
2530+
// If only one exception, just return it
2531+
return Ok(all_excs.into_iter().next().unwrap());
2532+
}
2533+
2534+
// Create new ExceptionGroup
2535+
let excs_tuple = vm.ctx.new_tuple(all_excs);
2536+
let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into();
2537+
eg_type.call((vm.ctx.new_str(""), excs_tuple), vm)
2538+
}
2539+
2540+
/// Check if two exceptions have the same metadata (for reraise detection)
2541+
fn is_same_exception_metadata(exc1: &PyObjectRef, exc2: &PyObjectRef, vm: &VirtualMachine) -> bool {
2542+
// Check if exc1 is part of exc2's exception group
2543+
if exc2.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2544+
let exc_class: PyObjectRef = exc1.class().to_owned().into();
2545+
if let Ok(result) = vm.call_method(exc2, "subgroup", (exc_class,))
2546+
&& !vm.is_none(&result)
2547+
&& let Ok(subgroup_excs) = result.get_attr("exceptions", vm)
2548+
&& let Ok(tuple) = subgroup_excs.downcast::<PyTuple>()
2549+
{
2550+
for e in tuple.iter() {
2551+
if e.is(exc1) {
2552+
return true;
2553+
}
2554+
}
2555+
}
2556+
}
2557+
exc1.is(exc2)
2558+
}

crates/vm/src/frame.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,15 @@ impl ExecutingFrame<'_> {
691691
let args = self.collect_positional_args(nargs.get(arg));
692692
self.execute_method_call(args, vm)
693693
}
694+
bytecode::Instruction::CheckEgMatch => {
695+
let match_type = self.pop_value();
696+
let exc_value = self.pop_value();
697+
let (rest, matched) =
698+
crate::exceptions::exception_group_match(&exc_value, &match_type, vm)?;
699+
self.push_value(rest);
700+
self.push_value(matched);
701+
Ok(None)
702+
}
694703
bytecode::Instruction::CompareOperation { op } => self.execute_compare(vm, op.get(arg)),
695704
bytecode::Instruction::ContainsOp(invert) => {
696705
let b = self.pop_value();
@@ -2494,6 +2503,12 @@ impl ExecutingFrame<'_> {
24942503
.into();
24952504
Ok(type_var)
24962505
}
2506+
bytecode::IntrinsicFunction2::PrepReraiseStar => {
2507+
// arg1 = orig (original exception)
2508+
// arg2 = excs (list of exceptions raised/reraised in except* blocks)
2509+
// Returns: exception to reraise, or None if nothing to reraise
2510+
crate::exceptions::prep_reraise_star(arg1, arg2, vm)
2511+
}
24972512
}
24982513
}
24992514

0 commit comments

Comments
 (0)