@@ -28,9 +28,9 @@ use rustpython_compiler_core::{
2828 Mode, OneIndexed, PositionEncoding, SourceFile, SourceLocation,
2929 bytecode::{
3030 self, AnyInstruction, Arg as OpArgMarker, BinaryOperator, BuildSliceArgCount, CodeObject,
31- ComparisonOperator, ConstantData, ConvertValueOparg, Instruction, IntrinsicFunction1 ,
32- Invert, LoadAttr, LoadSuperAttr, OpArg, OpArgType, PseudoInstruction, SpecialMethod ,
33- UnpackExArgs, oparg,
31+ ComparisonOperator, ConstantData, ConvertValueOparg, Instruction, InstructionMetadata ,
32+ IntrinsicFunction1, Invert, LoadAttr, LoadSuperAttr, OpArg, OpArgType, PseudoInstruction,
33+ SpecialMethod, UnpackExArgs, oparg,
3434 },
3535};
3636use rustpython_wtf8::Wtf8Buf;
@@ -608,6 +608,16 @@ impl Compiler {
608608 }
609609 }
610610
611+ fn statements_end_with_with_cleanup_scope_exit(body: &[ast::Stmt]) -> bool {
612+ body.last().is_some_and(|stmt| match stmt {
613+ ast::Stmt::With(ast::StmtWith { body, .. }) => {
614+ Self::statements_end_with_scope_exit(body)
615+ || Self::statements_end_with_with_cleanup_scope_exit(body)
616+ }
617+ _ => false,
618+ })
619+ }
620+
611621 fn preserves_finally_entry_nop(body: &[ast::Stmt]) -> bool {
612622 body.last().is_some_and(|stmt| match stmt {
613623 ast::Stmt::Try(ast::StmtTry {
@@ -5818,6 +5828,10 @@ impl Compiler {
58185828 self.compile_with(items, body, is_async)?;
58195829 }
58205830
5831+ let preserve_outer_cleanup_target_nop = !is_async
5832+ && (Self::statements_end_with_with_cleanup_scope_exit(body)
5833+ || self.current_block_has_terminal_with_suppress_exit_predecessor());
5834+
58215835 // Pop fblock before normal exit. CPython emits this POP_BLOCK with
58225836 // no location for sync with, but with the with-item location for
58235837 // async with.
@@ -5827,7 +5841,11 @@ impl Compiler {
58275841 emit!(self, PseudoInstruction::PopBlock);
58285842 if !is_async {
58295843 self.set_no_location();
5830- self.remove_last_no_location_nop();
5844+ if preserve_outer_cleanup_target_nop {
5845+ self.preserve_last_redundant_nop();
5846+ } else {
5847+ self.remove_last_no_location_nop();
5848+ }
58315849 self.set_source_range(with_range);
58325850 }
58335851 self.pop_fblock(if is_async {
@@ -10003,6 +10021,44 @@ impl Compiler {
1000310021 }
1000410022 }
1000510023
10024+ fn current_block_has_terminal_with_suppress_exit_predecessor(&self) -> bool {
10025+ let code = self.code_stack.last().expect("no code on stack");
10026+ let target = code.current_block;
10027+ let mut has_suppress_exit = false;
10028+ let mut has_normal_exit = false;
10029+
10030+ for block in &code.blocks {
10031+ let Some((last, prefix)) = block.instructions.split_last() else {
10032+ continue;
10033+ };
10034+ if last.target != target {
10035+ continue;
10036+ }
10037+ match last.instr.pseudo() {
10038+ Some(PseudoInstruction::JumpNoInterrupt { .. }) => {
10039+ let real_instrs: Vec<_> =
10040+ prefix.iter().filter_map(|info| info.instr.real()).collect();
10041+ has_suppress_exit |= matches!(
10042+ real_instrs.as_slice(),
10043+ [
10044+ Instruction::PopTop,
10045+ Instruction::PopExcept,
10046+ Instruction::PopTop,
10047+ Instruction::PopTop,
10048+ Instruction::PopTop,
10049+ ]
10050+ );
10051+ }
10052+ Some(PseudoInstruction::Jump { .. }) => {
10053+ has_normal_exit |= !prefix.iter().any(|info| info.instr.is_scope_exit());
10054+ }
10055+ _ => {}
10056+ }
10057+ }
10058+
10059+ has_suppress_exit && !has_normal_exit
10060+ }
10061+
1000610062 fn remove_last_no_location_nop(&mut self) {
1000710063 if let Some(info) = self.current_block().instructions.last_mut() {
1000810064 info.remove_no_location_nop = true;
@@ -20181,6 +20237,78 @@ async def foo():
2018120237 );
2018220238 }
2018320239
20240+ #[test]
20241+ fn test_nested_terminal_with_keeps_outer_cleanup_target_nop() {
20242+ let code = compile_exec(
20243+ "\
20244+ def f():
20245+ with a():
20246+ with b():
20247+ raise E()
20248+ ",
20249+ );
20250+ let f = find_code(&code, "f").expect("missing f code");
20251+ let ops: Vec<_> = f
20252+ .instructions
20253+ .iter()
20254+ .map(|unit| unit.op)
20255+ .filter(|op| !matches!(op, Instruction::Cache))
20256+ .collect();
20257+
20258+ assert!(
20259+ ops.windows(6).any(|window| {
20260+ matches!(
20261+ window,
20262+ [
20263+ Instruction::Copy { .. },
20264+ Instruction::PopExcept,
20265+ Instruction::Reraise { .. },
20266+ Instruction::Nop,
20267+ Instruction::LoadConst { .. },
20268+ Instruction::LoadConst { .. },
20269+ ]
20270+ )
20271+ }),
20272+ "expected CPython-style outer with-exit target NOP after terminal nested with cleanup, got ops={ops:?}"
20273+ );
20274+ }
20275+
20276+ #[test]
20277+ fn test_nested_nonterminal_with_drops_outer_cleanup_target_nop() {
20278+ let code = compile_exec(
20279+ "\
20280+ def f():
20281+ with a():
20282+ with b():
20283+ x()
20284+ ",
20285+ );
20286+ let f = find_code(&code, "f").expect("missing f code");
20287+ let ops: Vec<_> = f
20288+ .instructions
20289+ .iter()
20290+ .map(|unit| unit.op)
20291+ .filter(|op| !matches!(op, Instruction::Cache))
20292+ .collect();
20293+
20294+ assert!(
20295+ !ops.windows(6).any(|window| {
20296+ matches!(
20297+ window,
20298+ [
20299+ Instruction::Copy { .. },
20300+ Instruction::PopExcept,
20301+ Instruction::Reraise { .. },
20302+ Instruction::Nop,
20303+ Instruction::LoadConst { .. },
20304+ Instruction::LoadConst { .. },
20305+ ]
20306+ )
20307+ }),
20308+ "unexpected outer with-exit target NOP for nested with with normal fallthrough, got ops={ops:?}"
20309+ );
20310+ }
20311+
2018420312 #[test]
2018520313 fn test_try_loop_elif_places_return_before_orelse_tail() {
2018620314 let code = compile_exec(
0 commit comments