diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 3bbe7426c7..fe217a4b47 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -4,6 +4,7 @@ argdefs argtypes asdl asname +atopen attro augassign badcert @@ -30,6 +31,8 @@ cellvar cellvars ceval cfield +cfws +CFWS CLASSDEREF classdict cmpop @@ -47,6 +50,7 @@ datastack defaultdict denom deopt +deopts dictbytype DICTFLAG dictoffset @@ -62,6 +66,7 @@ fastlocals fblock fblocks fdescr +fdst ffi_argtypes fielddesc fieldlist @@ -75,6 +80,7 @@ freelist freevar freevars fromlist +fsrc getdict getfunc getiter @@ -94,8 +100,10 @@ IMMUTABLETYPE INCREF inlinedepth inplace +inpos ismine ISPOINTER +isoctal iteminfo Itertool keeped @@ -105,12 +113,14 @@ kwonlyargs lasti libffi linearise +lineful lineiterator linetable loadfast localsplus localspluskinds Lshift +lslpp lsprof MAXBLOCKS maxdepth @@ -130,6 +140,7 @@ nfrees nkwargs nkwelts nlocalsplus +nointerrupt Nondescriptor noninteger nops @@ -152,6 +163,7 @@ platstdlib posonlyarg posonlyargs prec +preds preinitialized pybuilddir pycore @@ -169,6 +181,7 @@ PYTHREAD_NAME releasebuffer repr resinfo +retarget Rshift SA_ONSTACK saveall @@ -180,6 +193,7 @@ SETREF setresult setslice settraceallthreads +sget SLOTDEFINED SMALLBUF SOABI @@ -197,6 +211,7 @@ subscr sval swappedbytes sysdict +tbstderr templatelib testconsole threadstate diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index 2a3ce809fb..adf774f102 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -187,7 +187,6 @@ def test_named_expression_invalid_rebinding_iteration_variable(self): with self.assertRaisesRegex(SyntaxError, msg): exec(f"lambda: {code}", {}) # Function scope - @unittest.expectedFailure # TODO: RUSTPYTHON def test_named_expression_invalid_rebinding_list_comprehension_iteration_variable(self): cases = [ ("Local reuse", 'i', "[i := 0 for i in range(5)]"), @@ -246,7 +245,6 @@ def test_named_expression_invalid_list_comprehension_iterable_expression(self): with self.assertRaisesRegex(SyntaxError, msg): exec(f"lambda: {code}", {}) # Function scope - @unittest.expectedFailure # TODO: RUSTPYTHON def test_named_expression_invalid_rebinding_set_comprehension_iteration_variable(self): cases = [ ("Local reuse", 'i', "{i := 0 for i in range(5)}"), @@ -310,7 +308,6 @@ def test_named_expression_invalid_set_comprehension_iterable_expression(self): with self.assertRaisesRegex(SyntaxError, msg): exec(f"lambda: {code}", {}) # Function scope - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_rebinding_dict_comprehension_iteration_variable(self): cases = [ ("Key reuse", 'i', "{(i := 0): 1 for i in range(5)}"), diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 6654a8830b..c9d79de30b 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -92,7 +92,6 @@ def unot(x): self.assertInBytecode(unot, 'POP_JUMP_IF_TRUE') self.check_lnotab(unot) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_elim_inversion_of_is_or_in(self): for line, cmp_op, invert in ( ('not a is b', 'IS_OP', 1,), @@ -933,7 +932,6 @@ def f(): self.assertInBytecode(f, 'LOAD_FAST_CHECK') self.assertNotInBytecode(f, 'LOAD_FAST') - @unittest.expectedFailure # TODO: RUSTPYTHON; RETURN_VALUE def test_load_fast_too_many_locals(self): # When there get to be too many locals to analyze completely, # later locals are all converted to LOAD_FAST_CHECK, except diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index e002babab4..1817592ca2 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -437,7 +437,6 @@ def method(self, /): self.assertEqual(C().method(), sentinel) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_annotations_constant_fold(self): def g(): def f(x: not (int is int), /): ... diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index af26a6a0a0..ffafedc775 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -713,7 +713,6 @@ def meth[T: x](self, arg: x): ... self.assertEqual(cls.bound, "nonlocal") self.assertEqual(cls.meth.__annotations__["arg"], "nonlocal") - @unittest.expectedFailure # TODO: RUSTPYTHON; + global def test_explicit_global(self): ns = run_code(""" x = "global" @@ -741,7 +740,6 @@ class Cls: cls = ns["outer"]() self.assertEqual(cls.Alias.__value__, "global") - @unittest.expectedFailure # TODO: RUSTPYTHON; + global from class def test_explicit_global_with_assignment(self): ns = run_code(""" x = "global" diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index 68ee372508..f16611b29a 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -807,7 +807,6 @@ def testEnterReturnsTuple(self): self.assertEqual(10, b1) self.assertEqual(20, b2) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FrameSummary' object has no attribute 'end_lineno' def testExceptionLocation(self): # The location of an exception raised from # __init__, __enter__ or __exit__ of a context diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 6147368736..029edcb8ed 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -13,12 +13,12 @@ use crate::{ IndexMap, IndexSet, ToPythonName, error::{CodegenError, CodegenErrorType, InternalError, PatternUnreachableReason}, ir::{self, BlockIdx}, + preprocess, symboltable::{self, CompilerScope, Symbol, SymbolFlags, SymbolScope, SymbolTable}, unparse::UnparseExpr, }; use alloc::borrow::Cow; use core::mem; -use itertools::Itertools; use malachite_bigint::BigInt; use num_complex::Complex; use num_traits::{Num, ToPrimitive, Zero}; @@ -173,6 +173,11 @@ struct Compiler { /// Disable constant tuple/list/set collection folding in contexts where /// CPython keeps the builder form for later assignment lowering. disable_const_collection_folding: bool, + split_next_for_normal_exit_from_break: bool, + fallthrough_has_statement_successor: bool, + fallthrough_has_local_statement_successor: bool, + fallthrough_successor_stack: Vec<(bool, bool)>, + try_else_orelse_conditional_base_stack: Vec, } #[derive(Clone, Copy)] @@ -222,12 +227,6 @@ impl CompileContext { } } -/// Segment of a parsed %-format string for optimize_format_str. -struct FormatSegment { - literal: String, - conversion: Option, -} - #[derive(Debug, Clone, Copy, PartialEq)] enum ComprehensionType { Generator, @@ -266,11 +265,12 @@ fn validate_duplicate_params(params: &ast::Parameters) -> Result<(), CodegenErro /// Compile an Mod produced from ruff parser pub fn compile_top( - ast: ruff_python_ast::Mod, + mut ast: ruff_python_ast::Mod, source_file: SourceFile, mode: Mode, opts: CompileOpts, ) -> CompileResult { + preprocess::preprocess_mod(&mut ast); match ast { ruff_python_ast::Mod::Module(module) => match mode { Mode::Exec | Mode::Eval => compile_program(&module, source_file, opts), @@ -497,12 +497,50 @@ impl Compiler { .map(|constant| Self::constant_truthiness(&constant))) } + fn has_always_taken_jump_in_test( + &mut self, + expr: &ast::Expr, + condition: bool, + ) -> CompileResult { + Ok(match expr { + ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => { + let (last_value, prefix_values) = values.split_last().unwrap(); + let cond2 = matches!(op, ast::BoolOp::Or); + for value in prefix_values { + if self.has_always_taken_jump_in_test(value, cond2)? { + return Ok(true); + } + } + self.has_always_taken_jump_in_test(last_value, condition)? + } + ast::Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::Not, + operand, + .. + }) => self.has_always_taken_jump_in_test(operand, !condition)?, + ast::Expr::If(ast::ExprIf { + test, body, orelse, .. + }) => { + self.has_always_taken_jump_in_test(test, false)? + || self.has_always_taken_jump_in_test(body, condition)? + || self.has_always_taken_jump_in_test(orelse, condition)? + } + _ => matches!(self.constant_expr_truthiness(expr)?, Some(value) if value == condition), + }) + } + fn disable_load_fast_borrow_for_block(&mut self, block: BlockIdx) { if block != BlockIdx::NULL { self.current_code_info().blocks[block.idx()].disable_load_fast_borrow = true; } } + fn mark_try_else_orelse_entry_block(&mut self, block: BlockIdx) { + if block != BlockIdx::NULL { + self.current_code_info().blocks[block.idx()].try_else_orelse_entry = true; + } + } + fn new(opts: CompileOpts, source_file: SourceFile, code_name: String) -> Self { let module_code = ir::CodeInfo { // CPython convention: top-level module / interactive / @@ -539,6 +577,8 @@ impl Compiler { fblock: Vec::with_capacity(MAXBLOCKS), symbol_table_index: 0, // Module is always the first symbol table in_conditional_block: 0, + in_final_with_cleanup_statement: 0, + in_try_else_orelse: 0, next_conditional_annotation_index: 0, }; Self { @@ -561,6 +601,11 @@ impl Compiler { do_not_emit_bytecode: 0, disable_const_boolop_folding: false, disable_const_collection_folding: false, + split_next_for_normal_exit_from_break: false, + fallthrough_has_statement_successor: false, + fallthrough_has_local_statement_successor: false, + fallthrough_successor_stack: Vec::new(), + try_else_orelse_conditional_base_stack: Vec::new(), } } @@ -595,6 +640,24 @@ impl Compiler { .is_some_and(Self::statement_ends_with_scope_exit) } + fn statements_end_with_finally_entry_scope_exit(body: &[ast::Stmt]) -> bool { + body.last() + .is_some_and(Self::statement_ends_with_finally_entry_scope_exit) + } + + fn statement_ends_with_finally_entry_scope_exit(stmt: &ast::Stmt) -> bool { + match stmt { + ast::Stmt::Return(_) + | ast::Stmt::Raise(_) + | ast::Stmt::Break(_) + | ast::Stmt::Continue(_) => true, + ast::Stmt::If(ast::StmtIf { body, .. }) => { + Self::statements_end_with_finally_entry_scope_exit(body) + } + _ => false, + } + } + fn statement_ends_with_scope_exit(stmt: &ast::Stmt) -> bool { match stmt { ast::Stmt::Return(_) | ast::Stmt::Raise(_) => true, @@ -616,7 +679,218 @@ impl Compiler { } } - fn preserves_finally_entry_nop(body: &[ast::Stmt]) -> bool { + fn statements_end_with_with_cleanup_scope_exit(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::With(ast::StmtWith { body, .. }) => { + Self::statements_end_with_scope_exit(body) + || Self::statements_end_with_with_cleanup_scope_exit(body) + } + _ => false, + }) + } + + fn statements_end_with_nonterminal_with_cleanup(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::With(ast::StmtWith { body, .. }) => { + !Self::statements_end_with_scope_exit(body) + && !Self::statements_end_with_with_cleanup_scope_exit(body) + } + _ => false, + }) + } + + fn statements_end_with_try_finally(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| { + matches!( + stmt, + ast::Stmt::Try(ast::StmtTry { finalbody, .. }) if !finalbody.is_empty() + ) + }) + } + + fn statements_end_with_nested_finalbody_try_finally(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::Try(ast::StmtTry { finalbody, .. }) if !finalbody.is_empty() => { + Self::statements_end_with_try_finally(finalbody) + } + _ => false, + }) + } + + fn statements_end_with_try_star_except(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| { + matches!( + stmt, + ast::Stmt::Try(ast::StmtTry { + handlers, + finalbody, + is_star: true, + .. + }) if !handlers.is_empty() && finalbody.is_empty() + ) + }) + } + + fn statements_end_with_try_except_handler_fallthrough(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::Try(ast::StmtTry { + body, + handlers, + finalbody, + is_star: false, + .. + }) => { + finalbody.is_empty() + && !handlers.is_empty() + && Self::statements_end_with_scope_exit(body) + && handlers.iter().any(|handler| { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + !Self::statements_end_with_scope_exit(&handler.body) + }) + } + _ => false, + }) + } + + fn statements_end_with_try_except_else_handler_scope_exit(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::Try(ast::StmtTry { + handlers, + orelse, + finalbody, + is_star: false, + .. + }) => { + !orelse.is_empty() + && finalbody.is_empty() + && handlers.iter().any(|handler| { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + Self::statements_end_with_finally_entry_scope_exit(&handler.body) + }) + } + _ => false, + }) + } + + fn statements_end_with_open_conditional_fallthrough(body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::If(ast::StmtIf { + elif_else_clauses, .. + }) => elif_else_clauses + .last() + .is_none_or(|clause| clause.test.is_some()), + _ => false, + }) + } + + fn statements_end_with_conditional_scope_exit(&self, body: &[ast::Stmt]) -> bool { + body.last().is_some_and(|stmt| match stmt { + ast::Stmt::Assert(_) => self.opts.optimize == 0, + ast::Stmt::If(ast::StmtIf { + body, + elif_else_clauses, + .. + }) => { + Self::statements_end_with_scope_exit(body) + || elif_else_clauses + .iter() + .any(|clause| Self::statements_end_with_scope_exit(&clause.body)) + } + _ => false, + }) + } + + fn statements_end_with_loop_fallthrough(&mut self, body: &[ast::Stmt]) -> CompileResult { + match body.last() { + Some(ast::Stmt::For(ast::StmtFor { body, .. })) => { + Ok(!Self::statements_contain_direct_break(body)) + } + Some(ast::Stmt::While(ast::StmtWhile { test, body, .. })) => { + Ok(!matches!(self.constant_expr_truthiness(test)?, Some(true)) + && !Self::statements_contain_direct_break(body)) + } + _ => Ok(false), + } + } + + fn statements_contain_direct_break(body: &[ast::Stmt]) -> bool { + body.iter().any(Self::statement_contains_direct_break) + } + + fn statements_are_single_for_direct_break(body: &[ast::Stmt]) -> bool { + matches!( + body, + [ast::Stmt::For(ast::StmtFor { body, .. })] + if Self::statements_contain_direct_break(body) + ) + } + + fn statement_contains_direct_break(stmt: &ast::Stmt) -> bool { + match stmt { + ast::Stmt::Break(_) => true, + ast::Stmt::If(ast::StmtIf { + body, + elif_else_clauses, + .. + }) => { + Self::statements_contain_direct_break(body) + || elif_else_clauses + .iter() + .any(|clause| Self::statements_contain_direct_break(&clause.body)) + } + ast::Stmt::With(ast::StmtWith { body, .. }) => { + Self::statements_contain_direct_break(body) + } + ast::Stmt::Try(ast::StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) => { + Self::statements_contain_direct_break(body) + || handlers.iter().any(|handler| { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + Self::statements_contain_direct_break(&handler.body) + }) + || Self::statements_contain_direct_break(orelse) + || Self::statements_contain_direct_break(finalbody) + } + ast::Stmt::Match(ast::StmtMatch { cases, .. }) => cases + .iter() + .any(|case| Self::statements_contain_direct_break(&case.body)), + ast::Stmt::For(_) | ast::Stmt::While(_) => false, + _ => false, + } + } + + fn has_resuming_bare_except(handlers: &[ast::ExceptHandler]) -> bool { + handlers.iter().any(|handler| { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + body, + .. + }) = handler; + type_.is_none() && !Self::statements_end_with_scope_exit(body) + }) + } + + fn statements_end_with_optimized_finally_entry_scope_exit(&self, body: &[ast::Stmt]) -> bool { + body.last() + .is_some_and(|stmt| self.statement_ends_with_optimized_finally_entry_scope_exit(stmt)) + } + + fn statement_ends_with_optimized_finally_entry_scope_exit(&self, stmt: &ast::Stmt) -> bool { + match stmt { + ast::Stmt::Assert(_) => self.opts.optimize == 0, + ast::Stmt::If(ast::StmtIf { body, .. }) => { + self.statements_end_with_optimized_finally_entry_scope_exit(body) + } + _ => Self::statement_ends_with_finally_entry_scope_exit(stmt), + } + } + + fn preserves_finally_entry_nop(&self, body: &[ast::Stmt]) -> bool { body.last().is_some_and(|stmt| match stmt { ast::Stmt::Try(ast::StmtTry { body, @@ -625,8 +899,18 @@ impl Compiler { .. }) => { !finalbody.is_empty() + && !Self::statements_end_with_open_conditional_fallthrough(finalbody) || (!handlers.is_empty() && Self::statements_end_with_scope_exit(body)) } + ast::Stmt::If(ast::StmtIf { + body, + elif_else_clauses, + .. + }) => { + elif_else_clauses.is_empty() + && self.statements_end_with_optimized_finally_entry_scope_exit(body) + } + ast::Stmt::Assert(_) => self.opts.optimize == 0, _ => false, }) } @@ -932,6 +1216,10 @@ impl Compiler { .expect("symbol_table_stack is empty! This is a compiler bug.") } + fn has_enclosing_non_module_code_scope(&self) -> bool { + self.code_stack.len() > 1 + } + /// Match CPython's `is_import_originated()`: only imports recorded in the /// module-level symbol table suppress method-call optimization. fn is_name_imported(&self, name: &str) -> bool { @@ -1318,9 +1606,7 @@ impl Compiler { annotation .symbols .iter() - .filter(|(_, s)| { - s.scope == SymbolScope::Free || s.flags.contains(SymbolFlags::FREE_CLASS) - }) + .filter(|(_, s)| s.scope == SymbolScope::Free) .map(|(name, _)| name.clone()) .collect() }) @@ -1329,7 +1615,12 @@ impl Compiler { .symbols .iter() .filter(|(_, s)| { - s.scope == SymbolScope::Free || s.flags.contains(SymbolFlags::FREE_CLASS) + s.scope == SymbolScope::Free + || (scope_type != CompilerScope::Class + && s.flags.contains(SymbolFlags::FREE_CLASS)) + || (scope_type == CompilerScope::Class + && s.flags.contains(SymbolFlags::FREE_CLASS) + && self.has_enclosing_non_module_code_scope()) }) .filter(|(name, symbol)| { if !matches!( @@ -1424,11 +1715,19 @@ impl Compiler { fblock: Vec::with_capacity(MAXBLOCKS), symbol_table_index: key, in_conditional_block: 0, + in_final_with_cleanup_statement: 0, + in_try_else_orelse: 0, next_conditional_annotation_index: 0, }; // Push the old compiler unit on the stack (like PyCapsule) // This happens before setting qualname + self.fallthrough_successor_stack.push(( + self.fallthrough_has_statement_successor, + self.fallthrough_has_local_statement_successor, + )); + self.fallthrough_has_statement_successor = false; + self.fallthrough_has_local_statement_successor = false; self.code_stack.push(code_info); // Set qualname after pushing (uses compiler_set_qualname logic) @@ -1487,7 +1786,10 @@ impl Compiler { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); } @@ -1558,6 +1860,10 @@ impl Compiler { // compiler_exit_scope fn exit_scope(&mut self) -> CodeObject { let _table = self.pop_symbol_table(); + if let Some((previous, previous_local)) = self.fallthrough_successor_stack.pop() { + self.fallthrough_has_statement_successor = previous; + self.fallthrough_has_local_statement_successor = previous_local; + } // Various scopes can have sub_tables: // - ast::TypeParams scope can have sub_tables (the function body's symbol table) @@ -1575,6 +1881,10 @@ impl Compiler { fn exit_annotation_scope(&mut self, saved_ctx: CompileContext) -> CodeObject { self.pop_annotation_symbol_table(); self.ctx = saved_ctx; + if let Some((previous, previous_local)) = self.fallthrough_successor_stack.pop() { + self.fallthrough_has_statement_successor = previous; + self.fallthrough_has_local_statement_successor = previous_local; + } let pop = self.code_stack.pop(); let stack_top = compiler_unwrap_option(self, pop); @@ -2070,16 +2380,8 @@ impl Compiler { emit!(self, PseudoInstruction::AnnotationsPlaceholder); let (doc, statements) = split_doc(&body.body, &self.opts); - if let Some(value) = doc { - self.emit_load_const(ConstantData::Str { - value: value.into(), - }); - let doc = self.name("__doc__"); - emit!(self, Instruction::StoreName { namei: doc }) - } - - // Handle annotation bookkeeping in CPython order: initialize the - // conditional annotation set first, then materialize __annotations__. + // Handle annotation bookkeeping before the docstring assignment, as + // codegen_body() does after _PyCodegen_Module() inserts the prefix set. if Self::find_ann(statements) { if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { emit!(self, Instruction::BuildSet { count: 0 }); @@ -2091,6 +2393,14 @@ impl Compiler { } } + if let Some(value) = doc { + self.emit_load_const(ConstantData::Str { + value: value.into(), + }); + let doc = self.name("__doc__"); + emit!(self, Instruction::StoreName { namei: doc }) + } + // Compile all statements self.compile_statements(statements)?; @@ -2225,21 +2535,61 @@ impl Compiler { } fn compile_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> { - for statement in statements { - self.compile_statement(statement)? + let inherited_successor = self.fallthrough_has_statement_successor; + for (idx, statement) in statements.iter().enumerate() { + let previous_successor = self.fallthrough_has_statement_successor; + let previous_local_successor = self.fallthrough_has_local_statement_successor; + self.fallthrough_has_statement_successor = + inherited_successor || idx + 1 < statements.len(); + self.fallthrough_has_local_statement_successor = idx + 1 < statements.len(); + let result = self.compile_statement(statement); + self.fallthrough_has_statement_successor = previous_successor; + self.fallthrough_has_local_statement_successor = previous_local_successor; + result?; + } + Ok(()) + } + + fn compile_with_body_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> { + let inherited_successor = self.fallthrough_has_statement_successor; + for (idx, statement) in statements.iter().enumerate() { + let previous_successor = self.fallthrough_has_statement_successor; + let previous_local_successor = self.fallthrough_has_local_statement_successor; + self.fallthrough_has_statement_successor = + inherited_successor || idx + 1 < statements.len(); + self.fallthrough_has_local_statement_successor = idx + 1 < statements.len(); + if idx + 1 == statements.len() && matches!(statement, ast::Stmt::Try(_)) { + self.current_code_info().in_final_with_cleanup_statement += 1; + let result = self.compile_statement(statement); + self.current_code_info().in_final_with_cleanup_statement -= 1; + self.fallthrough_has_statement_successor = previous_successor; + self.fallthrough_has_local_statement_successor = previous_local_successor; + result?; + } else { + let result = self.compile_statement(statement); + self.fallthrough_has_statement_successor = previous_successor; + self.fallthrough_has_local_statement_successor = previous_local_successor; + result?; + } } Ok(()) } + fn compile_loop_body_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> { + let previous_successor = self.fallthrough_has_statement_successor; + let previous_local_successor = self.fallthrough_has_local_statement_successor; + self.fallthrough_has_statement_successor = false; + self.fallthrough_has_local_statement_successor = false; + let result = self.compile_statements(statements); + self.fallthrough_has_statement_successor = previous_successor; + self.fallthrough_has_local_statement_successor = previous_local_successor; + result + } + fn scope_needs_conditional_annotations_cell(symbol_table: &SymbolTable) -> bool { match symbol_table.typ { - CompilerScope::Module => { + CompilerScope::Module | CompilerScope::Class => { symbol_table.has_conditional_annotations - || (symbol_table.future_annotations && symbol_table.annotation_block.is_some()) - } - CompilerScope::Class => { - symbol_table.has_conditional_annotations - || symbol_table.lookup("__conditional_annotations__").is_some() } _ => false, } @@ -2303,9 +2653,6 @@ impl Compiler { let is_typeparams = current_table.typ == CompilerScope::TypeParams; let is_annotation = current_table.typ == CompilerScope::Annotation; let can_see_class = current_table.can_see_class_scope; - let parent_table = current_idx - .checked_sub(1) - .and_then(|idx| self.symbol_table_stack.get(idx)); // First try to find in current table let symbol = current_table.lookup(name.as_ref()); @@ -2320,8 +2667,10 @@ impl Compiler { symbol }; let class_declared_global = can_see_class - && parent_table.is_some_and(|table| table.typ == CompilerScope::Class) - && parent_table + && self.symbol_table_stack[..current_idx] + .iter() + .rev() + .find(|table| table.typ == CompilerScope::Class) .and_then(|table| table.lookup(name.as_ref())) .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::GLOBAL)); @@ -2842,7 +3191,11 @@ impl Compiler { statement.range(), )); } - let folded_constant = self.try_fold_constant_expr(v)?; + let folded_constant = if v.is_constant() { + self.try_fold_constant_expr(v)? + } else { + None + }; let preserve_tos = folded_constant.is_none(); if preserve_tos { self.compile_expression(v)?; @@ -3346,16 +3699,7 @@ impl Compiler { // End block - continuation point after try-finally // Normal path jumps here to skip exception path blocks let end_block = self.new_block(); - let has_bare_except = handlers.iter().any(|handler| { - matches!( - handler, - ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { - type_: None, - .. - }) - ) - }); - if has_bare_except { + if Self::has_resuming_bare_except(handlers) { self.disable_load_fast_borrow_for_block(end_block); } @@ -3384,12 +3728,11 @@ impl Compiler { )?; } - let else_block = self.new_block(); - // if handlers is empty, compile body directly // without wrapping in TryExcept (only FinallyTry is needed) if handlers.is_empty() { - let preserve_finally_entry_nop = Self::preserves_finally_entry_nop(body); + let preserve_finally_entry_nop = self.preserves_finally_entry_nop(body) + || self.statements_end_with_loop_fallthrough(body)?; // Just compile body with FinallyTry fblock active (if finalbody exists) self.compile_statements(body)?; @@ -3429,7 +3772,6 @@ impl Compiler { PseudoInstruction::JumpNoInterrupt { delta: end_block } ); self.set_no_location(); - self.preserve_last_redundant_jump_as_nop(); if let Some(finally_except) = finally_except_block { // Restore sub_tables for exception path compilation @@ -3487,19 +3829,11 @@ impl Compiler { self.compile_statements(body)?; emit!(self, PseudoInstruction::PopBlock); self.set_no_location(); - self.remove_last_no_location_nop(); self.pop_fblock(FBlockType::TryExcept); - emit!( - self, - PseudoInstruction::JumpNoInterrupt { delta: else_block } - ); - self.set_no_location(); - self.remove_last_no_location_nop(); let cleanup_block = self.new_block(); // We successfully ran the try block: // else: - self.switch_to_block(else_block); self.compile_statements(orelse)?; emit!( @@ -3645,6 +3979,8 @@ impl Compiler { if !finalbody.is_empty() { let preserve_finally_normal_pop_block_nop = orelse.is_empty() && !Self::statements_end_with_scope_exit(body) + && (!Self::statements_end_with_open_conditional_fallthrough(body) + || Self::statements_end_with_finally_entry_scope_exit(body)) && handlers.iter().all(|handler| match handler { ast::ExceptHandler::ExceptHandler(handler) => { Self::statements_end_with_scope_exit(&handler.body) @@ -3676,7 +4012,6 @@ impl Compiler { PseudoInstruction::JumpNoInterrupt { delta: end_block } ); self.set_no_location(); - self.preserve_last_redundant_jump_as_nop(); // finally (exception path) // This is where exceptions go to run finally before reraise @@ -3747,23 +4082,32 @@ impl Compiler { handlers: &[ast::ExceptHandler], orelse: &[ast::Stmt], ) -> CompileResult<()> { - let normal_exit_range = orelse - .last() - .map(ast::Stmt::range) - .or_else(|| body.last().map(ast::Stmt::range)); let handler_block = self.new_block(); let cleanup_block = self.new_block(); let end_block = self.new_block(); - let has_bare_except = handlers.iter().any(|handler| { - matches!( - handler, - ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { - type_: None, - .. - }) - ) + let has_terminal_raise_handlers = handlers.iter().all(|handler| { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = + handler; + body.last() + .is_some_and(|stmt| matches!(stmt, ast::Stmt::Raise(_))) }); - if has_bare_except { + let handlers_end_with_scope_exit = handlers.iter().all(|handler| { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { body, .. }) = + handler; + Self::statements_end_with_scope_exit(body) + }); + let typed_handlers_end_with_scope_exit = handlers.iter().all(|handler| { + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + body, + .. + }) = handler; + type_.is_some() && Self::statements_end_with_scope_exit(body) + }); + if Self::has_resuming_bare_except(handlers) { + self.disable_load_fast_borrow_for_block(end_block); + } + if typed_handlers_end_with_scope_exit { self.disable_load_fast_borrow_for_block(end_block); } @@ -3775,22 +4119,69 @@ impl Compiler { ); self.push_fblock(FBlockType::TryExcept, handler_block, handler_block)?; - self.compile_statements(body)?; + let split_for_normal_exit_from_break = orelse.is_empty() + && self.fallthrough_has_statement_successor + && Self::statements_are_single_for_direct_break(body) + && !self + .current_code_info() + .fblock + .iter() + .any(|info| matches!(info.fb_type, FBlockType::With | FBlockType::AsyncWith)); + let previous_split_for_normal_exit_from_break = self.split_next_for_normal_exit_from_break; + self.split_next_for_normal_exit_from_break = + previous_split_for_normal_exit_from_break || split_for_normal_exit_from_break; + let compile_body_result = self.compile_statements(body); + self.split_next_for_normal_exit_from_break = previous_split_for_normal_exit_from_break; + compile_body_result?; self.pop_fblock(FBlockType::TryExcept); emit!(self, PseudoInstruction::PopBlock); self.set_no_location(); - self.remove_last_no_location_nop(); - self.compile_statements(orelse)?; + let exits_directly_to_with_cleanup = { + let code_info = self.current_code_info(); + code_info.in_final_with_cleanup_statement > 0 + && code_info.fblock.last().is_some_and(|info| { + matches!(info.fb_type, FBlockType::With | FBlockType::AsyncWith) + }) + }; + if !orelse.is_empty() && self.statements_end_with_conditional_scope_exit(body) { + self.preserve_last_redundant_nop(); + } else { + self.remove_last_no_location_nop(); + } + if !orelse.is_empty() { + if has_terminal_raise_handlers { + let orelse_block = self.new_block(); + self.switch_to_block(orelse_block); + } + let current = self.current_code_info().current_block; + self.mark_try_else_orelse_entry_block(current); + } + let try_else_orelse_conditional_base = self.current_code_info().in_conditional_block; + self.current_code_info().in_try_else_orelse += 1; + self.try_else_orelse_conditional_base_stack + .push(try_else_orelse_conditional_base); + let compile_orelse_result = self.compile_statements(orelse); + self.try_else_orelse_conditional_base_stack.pop(); + self.current_code_info().in_try_else_orelse -= 1; + compile_orelse_result?; emit!( self, PseudoInstruction::JumpNoInterrupt { delta: end_block } ); self.set_no_location(); - self.remove_last_no_location_nop(); - - self.switch_to_block(handler_block); - emit!( - self, + if (!orelse.is_empty() && self.statements_end_with_loop_fallthrough(orelse)?) + || (exits_directly_to_with_cleanup + && handlers_end_with_scope_exit + && !Self::statements_end_with_nonterminal_with_cleanup(body)) + { + self.preserve_last_redundant_jump_as_nop(); + } else { + self.remove_last_no_location_nop(); + } + + self.switch_to_block(handler_block); + emit!( + self, PseudoInstruction::SetupCleanup { delta: cleanup_block } @@ -3810,6 +4201,8 @@ impl Compiler { }) = handler; self.set_source_range(*handler_range); let next_handler = self.new_block(); + let handler_body_exits = Self::statements_end_with_scope_exit(body); + let mut exception_handler_was_popped = false; if let Some(exc_type) = type_ { self.compile_expression(exc_type)?; @@ -3827,9 +4220,11 @@ impl Compiler { let cleanup_end = self.new_block(); emit!(self, PseudoInstruction::SetupCleanup { delta: cleanup_end }); + let cleanup_body = self.new_block(); + self.switch_to_block(cleanup_body); self.push_fblock_full( FBlockType::HandlerCleanup, - cleanup_end, + cleanup_body, cleanup_end, FBlockDatum::ExceptionName(alias.as_str().to_owned()), )?; @@ -3837,26 +4232,29 @@ impl Compiler { self.compile_statements(body)?; self.pop_fblock(FBlockType::HandlerCleanup); - emit!(self, PseudoInstruction::PopBlock); - self.set_no_location(); - emit!(self, PseudoInstruction::PopBlock); - self.set_no_location(); - self.pop_fblock(FBlockType::ExceptionHandler); - emit!(self, Instruction::PopExcept); - self.set_no_location(); + if !handler_body_exits { + emit!(self, PseudoInstruction::PopBlock); + self.set_no_location(); + emit!(self, PseudoInstruction::PopBlock); + self.set_no_location(); + self.pop_fblock(FBlockType::ExceptionHandler); + exception_handler_was_popped = true; + emit!(self, Instruction::PopExcept); + self.set_no_location(); - self.emit_load_const(ConstantData::None); - self.set_no_location(); - self.store_name(alias.as_str())?; - self.set_no_location(); - self.compile_name(alias.as_str(), NameUsage::Delete)?; - self.set_no_location(); + self.emit_load_const(ConstantData::None); + self.set_no_location(); + self.store_name(alias.as_str())?; + self.set_no_location(); + self.compile_name(alias.as_str(), NameUsage::Delete)?; + self.set_no_location(); - emit!( - self, - PseudoInstruction::JumpNoInterrupt { delta: end_block } - ); - self.set_no_location(); + emit!( + self, + PseudoInstruction::JumpNoInterrupt { delta: end_block } + ); + self.set_no_location(); + } self.switch_to_block(cleanup_end); self.emit_load_const(ConstantData::None); @@ -3869,24 +4267,31 @@ impl Compiler { self.set_no_location(); } else { emit!(self, Instruction::PopTop); - self.push_fblock(FBlockType::HandlerCleanup, end_block, end_block)?; + let cleanup_body = self.new_block(); + self.switch_to_block(cleanup_body); + self.push_fblock(FBlockType::HandlerCleanup, cleanup_body, end_block)?; self.compile_statements(body)?; self.pop_fblock(FBlockType::HandlerCleanup); - emit!(self, PseudoInstruction::PopBlock); - self.set_no_location(); - self.pop_fblock(FBlockType::ExceptionHandler); - emit!(self, Instruction::PopExcept); - self.set_no_location(); - emit!( - self, - PseudoInstruction::JumpNoInterrupt { delta: end_block } - ); - self.set_no_location(); + if !handler_body_exits { + emit!(self, PseudoInstruction::PopBlock); + self.set_no_location(); + self.pop_fblock(FBlockType::ExceptionHandler); + exception_handler_was_popped = true; + emit!(self, Instruction::PopExcept); + self.set_no_location(); + emit!( + self, + PseudoInstruction::JumpNoInterrupt { delta: end_block } + ); + self.set_no_location(); + } } - self.push_fblock(FBlockType::ExceptionHandler, cleanup_block, cleanup_block)?; + if exception_handler_was_popped { + self.push_fblock(FBlockType::ExceptionHandler, cleanup_block, cleanup_block)?; + } self.switch_to_block(next_handler); } @@ -3903,9 +4308,6 @@ impl Compiler { self.set_no_location(); self.switch_to_block(end_block); - if let Some(range) = normal_exit_range { - self.set_source_range(range); - } Ok(()) } @@ -4849,6 +5251,9 @@ impl Compiler { if funcflags.contains(&bytecode::MakeFunctionFlag::KwOnlyDefaults) { num_typeparam_args += 1; } + if num_typeparam_args == 2 { + emit!(self, Instruction::Swap { i: 2 }); + } // Enter type params scope let type_params_name = format!(""); @@ -4931,33 +5336,17 @@ impl Compiler { // Make closure for type params code self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?; - // Call the type params closure with defaults/kwdefaults as arguments. - // Call protocol: [callable, self_or_null, arg1, ..., argN] - // We need to reorder: [args..., closure] -> [closure, NULL, args...] - // Using Swap operations to move closure down and insert NULL. - // Note: num_typeparam_args is at most 2 (defaults tuple, kwdefaults dict). if num_typeparam_args > 0 { - match num_typeparam_args { - 1 => { - // Stack: [arg1, closure] - emit!(self, Instruction::Swap { i: 2 }); // [closure, arg1] - emit!(self, Instruction::PushNull); // [closure, arg1, NULL] - emit!(self, Instruction::Swap { i: 2 }); // [closure, NULL, arg1] - } - 2 => { - // Stack: [arg1, arg2, closure] - emit!(self, Instruction::Swap { i: 3 }); // [closure, arg2, arg1] - emit!(self, Instruction::Swap { i: 2 }); // [closure, arg1, arg2] - emit!(self, Instruction::PushNull); // [closure, arg1, arg2, NULL] - emit!(self, Instruction::Swap { i: 3 }); // [closure, NULL, arg2, arg1] - emit!(self, Instruction::Swap { i: 2 }); // [closure, NULL, arg1, arg2] + emit!( + self, + Instruction::Swap { + i: num_typeparam_args as u32 + 1 } - _ => unreachable!("only defaults and kwdefaults are supported"), - } + ); emit!( self, Instruction::Call { - argc: num_typeparam_args as u32 + argc: num_typeparam_args as u32 - 1 } ); } else { @@ -5296,13 +5685,6 @@ impl Compiler { emit!(self, Instruction::StoreDeref { i: classdict_idx }); } - // Store __doc__ only if there's an explicit docstring. - if let Some(doc) = doc_str { - self.emit_load_const(ConstantData::Str { value: doc.into() }); - let doc_name = self.name("__doc__"); - emit!(self, Instruction::StoreName { namei: doc_name }); - } - // Handle class annotation bookkeeping in CPython order. if Self::find_ann(body) { if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { @@ -5315,6 +5697,13 @@ impl Compiler { } } + // Store __doc__ only if there's an explicit docstring. + if let Some(doc) = doc_str { + self.emit_load_const(ConstantData::Str { value: doc.into() }); + let doc_name = self.name("__doc__"); + emit!(self, Instruction::StoreName { namei: doc_name }); + } + // 3. Compile the class body self.compile_statements(body)?; @@ -5498,7 +5887,7 @@ impl Compiler { let has_double_star = arguments.is_some_and(|args| args.keywords.iter().any(|kw| kw.arg.is_none())); - if has_starred || has_double_star { + if has_starred { // Use CallFunctionEx for *bases or **kwargs // Stack has: [__build_class__, NULL, class_func, name] // Need to build: args tuple = (class_func, name, *bases, .generic_base) @@ -5533,12 +5922,25 @@ impl Compiler { } ); - // Build kwargs if needed - if arguments.is_some_and(|args| !args.keywords.is_empty()) { - self.compile_keywords(&arguments.unwrap().keywords)?; - } else { - emit!(self, Instruction::PushNull); + self.compile_call_function_ex_keywords( + arguments.map_or(&[][..], |args| &args.keywords[..]), + )?; + emit!(self, Instruction::CallFunctionEx); + } else if has_double_star { + if let Some(arguments) = arguments { + for arg in &arguments.args { + self.compile_expression(arg)?; + } } + self.load_name(".generic_base")?; + emit!( + self, + Instruction::BuildTuple { + count: 3 + arguments + .map_or(0, |args| u32::try_from(args.args.len()).unwrap()) + } + ); + self.compile_call_function_ex_keywords(&arguments.unwrap().keywords[..])?; emit!(self, Instruction::CallFunctionEx); } else { // Simple case: no starred bases, no **kwargs @@ -5635,14 +6037,29 @@ impl Compiler { self.new_block() }; - if matches!(self.constant_expr_truthiness(test)?, Some(false)) { + if self.has_always_taken_jump_in_test(test, false)? { self.disable_load_fast_borrow_for_block(next_block); + self.disable_load_fast_borrow_for_block(end_block); } + let in_direct_try_else_orelse_conditional = { + let base = self.try_else_orelse_conditional_base_stack.last().copied(); + let code_info = self.current_code_info(); + code_info.in_try_else_orelse > 0 + && base.is_some_and(|base| code_info.in_conditional_block == base + 1) + }; + let preserve_try_else_scope_exit_target_nop = elif_else_clauses.is_empty() + && in_direct_try_else_orelse_conditional + && !self.fallthrough_has_local_statement_successor + && Self::statements_end_with_scope_exit(body); self.compile_jump_if(test, false, next_block)?; self.compile_statements(body)?; let Some((clause, rest)) = elif_else_clauses.split_first() else { self.switch_to_block(end_block); + if preserve_try_else_scope_exit_target_nop { + self.set_source_range(test.range()); + emit!(self, Instruction::Nop); + } return Ok(()); }; @@ -5672,8 +6089,12 @@ impl Compiler { self.enter_conditional_block(); let while_block = self.new_block(); - let else_block = self.new_block(); let after_block = self.new_block(); + let else_block = if orelse.is_empty() { + after_block + } else { + self.new_block() + }; self.switch_to_block(while_block); self.push_fblock(FBlockType::WhileLoop, while_block, after_block)?; @@ -5684,7 +6105,7 @@ impl Compiler { self.compile_jump_if(test, false, else_block)?; let was_in_loop = self.ctx.loop_data.replace((while_block, after_block)); - self.compile_statements(body)?; + self.compile_loop_body_statements(body)?; self.ctx.loop_data = was_in_loop; emit!(self, PseudoInstruction::Jump { delta: while_block }); self.set_no_location(); @@ -5692,7 +6113,9 @@ impl Compiler { self.pop_fblock(FBlockType::WhileLoop); self.compile_statements(orelse)?; - self.switch_to_block(after_block); + if !orelse.is_empty() { + self.switch_to_block(after_block); + } self.leave_conditional_block(); Ok(()) @@ -5733,11 +6156,10 @@ impl Compiler { // RERAISE 1 // after: ... - let with_range = self.current_source_range; - let Some((item, items)) = items.split_first() else { return Err(self.error(CodegenErrorType::EmptyWithItems)); }; + let with_range = item.context_expr.range(); let exc_handler_block = self.new_block(); let after_block = self.new_block(); @@ -5827,27 +6249,59 @@ impl Compiler { if body.is_empty() { return Err(self.error(CodegenErrorType::EmptyWithBody)); } - self.compile_statements(body)?; + self.compile_with_body_statements(body)?; } else { - self.set_source_range(with_range); + self.set_source_range(items[0].context_expr.range()); self.compile_with(items, body, is_async)?; } + let nested_multiline_with_cleanup_target_nop = + !is_async && Self::statements_end_with_scope_exit(body) && { + let parent_with_ranges: Vec<_> = self + .current_code_info() + .fblock + .iter() + .rev() + .skip(1) + .filter_map(|info| { + matches!(info.fb_type, FBlockType::With).then_some(info.fb_range) + }) + .collect(); + let source = self.source_file.to_source_code(); + let current_line = source.line_index(with_range.start()); + parent_with_ranges + .iter() + .any(|range| source.line_index(range.start()) != current_line) + }; + let preserve_outer_cleanup_target_nop = !is_async + && (Self::statements_end_with_with_cleanup_scope_exit(body) + || self.statements_end_with_conditional_scope_exit(body) + || Self::statements_end_with_try_except_handler_fallthrough(body) + || Self::statements_end_with_try_except_else_handler_scope_exit(body) + || Self::statements_end_with_try_finally(body) + || self.statements_end_with_loop_fallthrough(body)?); + let materialize_async_with_outer_cleanup_target_nop = is_async + && Self::statements_end_with_nested_finalbody_try_finally(body) + && self + .current_code_info() + .fblock + .iter() + .any(|info| matches!(info.fb_type, FBlockType::With)); + // Pop fblock before normal exit. CPython emits this POP_BLOCK with // no location for sync with, but with the with-item location for // async with. if is_async { self.set_source_range(with_range); } - let preserve_pop_block_nop = !is_async && self.current_block_follows_end_async_for(); - if preserve_pop_block_nop { - emit!(self, Instruction::Nop); - self.preserve_last_redundant_nop(); - } emit!(self, PseudoInstruction::PopBlock); if !is_async { self.set_no_location(); - if !preserve_pop_block_nop { + if preserve_outer_cleanup_target_nop { + self.preserve_last_redundant_nop(); + } else if Self::statements_end_with_try_star_except(body) { + self.force_remove_last_no_location_nop(); + } else { self.remove_last_no_location_nop(); } self.set_source_range(with_range); @@ -5944,6 +6398,12 @@ impl Compiler { // ===== After block ===== self.switch_to_block(after_block); + if materialize_async_with_outer_cleanup_target_nop + || nested_multiline_with_cleanup_target_nop + { + self.set_source_range(with_range); + emit!(self, Instruction::Nop); + } self.leave_conditional_block(); Ok(()) @@ -5963,6 +6423,19 @@ impl Compiler { let for_block = self.new_block(); let else_block = self.new_block(); let after_block = self.new_block(); + let split_normal_exit_from_break = !is_async + && self.split_next_for_normal_exit_from_break + && Self::statements_contain_direct_break(body) + && self + .current_code_info() + .fblock + .iter() + .any(|info| matches!(info.fb_type, FBlockType::TryExcept)); + let normal_exit_block = if split_normal_exit_from_break { + self.new_block() + } else { + after_block + }; let mut end_async_for_target = BlockIdx::NULL; // The thing iterated: @@ -6011,7 +6484,7 @@ impl Compiler { }; let was_in_loop = self.ctx.loop_data.replace((for_block, after_block)); - self.compile_statements(body)?; + self.compile_loop_body_statements(body)?; self.ctx.loop_data = was_in_loop; emit!(self, PseudoInstruction::Jump { delta: for_block }); self.set_no_location(); @@ -6034,10 +6507,25 @@ impl Compiler { self.set_source_range(saved_range); self.compile_statements(orelse)?; - self.switch_to_block(after_block); + self.switch_to_block(normal_exit_block); // Implicit return after for-loop should be attributed to the `for` line self.set_source_range(iter.range()); + if split_normal_exit_from_break { + emit!(self, Instruction::Nop); + self.set_no_location(); + self.preserve_last_redundant_nop(); + self.preserve_last_redundant_jump_as_nop(); + self.mark_last_no_location_exit(); + emit!( + self, + PseudoInstruction::JumpNoInterrupt { delta: after_block } + ); + self.set_no_location(); + self.remove_last_no_location_nop(); + self.switch_to_block(after_block); + self.set_source_range(iter.range()); + } self.leave_conditional_block(); Ok(()) @@ -6054,6 +6542,7 @@ impl Compiler { // the original object semantics. if !is_async && let ast::Expr::List(ast::ExprList { elts, .. }) = iter + && elts.len() <= usize::try_from(STACK_USE_GUIDELINE).unwrap() && !elts.iter().any(|e| matches!(e, ast::Expr::Starred(_))) { if let Some(folded) = self.try_fold_constant_collection(elts, CollectionType::List)? { @@ -7072,10 +7561,17 @@ impl Compiler { emit!(self, Instruction::Nop); } emit!(self, Instruction::PopTop); + if matches!(m.body.first(), Some(ast::Stmt::Try(_))) { + let body_block = self.new_block(); + self.switch_to_block(body_block); + } } self.compile_statements(&m.body)?; emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end }); + if let Some(last) = self.current_block().instructions.last_mut() { + last.match_success_jump = true; + } self.set_source_range(m.pattern.range()); self.emit_and_reset_fail_pop(pattern_context)?; } @@ -7212,7 +7708,8 @@ impl Compiler { self.compile_addcompare(last_op); let end = self.new_block(); - emit!(self, PseudoInstruction::Jump { delta: end }); + emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end }); + self.set_no_location(); // early exit left us with stack: `rhs, comparison_result`. We need to clean up rhs. self.switch_to_block(cleanup); @@ -7263,7 +7760,8 @@ impl Compiler { self.compile_addcompare(last_op); emit!(self, Instruction::ToBool); self.emit_pop_jump_by_condition(condition, target_block); - emit!(self, PseudoInstruction::Jump { delta: end }); + emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end }); + self.set_no_location(); self.switch_to_block(cleanup); emit!(self, Instruction::PopTop); @@ -7746,6 +8244,10 @@ impl Compiler { } _ => { // Fall back case which always will work! + if matches!(self.constant_expr_truthiness(expression)?, Some(value) if value == condition) + { + self.disable_load_fast_borrow_for_block(target_block); + } self.compile_expression(expression)?; emit!(self, Instruction::ToBool); if condition { @@ -7813,22 +8315,7 @@ impl Compiler { for value in prefix_values { let continue_block = self.new_block(); - match value { - ast::Expr::BoolOp(ast::ExprBoolOp { - op: inner_op, - values, - .. - }) if inner_op != op => { - let (last_inner_value, inner_prefix_values) = values.split_last().unwrap(); - for inner_value in inner_prefix_values { - self.compile_expression(inner_value)?; - self.emit_short_circuit_test(inner_op, continue_block); - emit!(self, Instruction::PopTop); - } - self.compile_expression(last_inner_value)?; - } - _ => self.compile_expression(value)?, - } + self.compile_expression(value)?; self.emit_short_circuit_test(op, after_block); self.switch_to_block(continue_block); emit!(self, Instruction::PopTop); @@ -8202,15 +8689,6 @@ impl Compiler { ast::Expr::BinOp(ast::ExprBinOp { left, op, right, .. }) => { - // optimize_format_str: 'format' % (args,) → f-string bytecode - if matches!(op, ast::Operator::Mod) - && let ast::Expr::StringLiteral(s) = left.as_ref() - && let ast::Expr::Tuple(ast::ExprTuple { elts, .. }) = right.as_ref() - && !elts.iter().any(|e| matches!(e, ast::Expr::Starred(_))) - && self.try_optimize_format_str(s.value.to_str(), elts, range)? - { - return Ok(()); - } self.compile_expression(left)?; self.compile_expression(right)?; @@ -8707,39 +9185,6 @@ impl Compiler { Ok(()) } - fn compile_keywords(&mut self, keywords: &[ast::Keyword]) -> CompileResult<()> { - let mut size = 0; - let groupby = keywords.iter().chunk_by(|e| e.arg.is_none()); - for (is_unpacking, sub_keywords) in &groupby { - if is_unpacking { - for keyword in sub_keywords { - self.compile_expression(&keyword.value)?; - size += 1; - } - } else { - let mut sub_size = 0; - for keyword in sub_keywords { - if let Some(name) = &keyword.arg { - self.emit_load_const(ConstantData::Str { - value: name.as_str().into(), - }); - self.compile_expression(&keyword.value)?; - sub_size += 1; - } - } - emit!(self, Instruction::BuildMap { count: sub_size }); - size += 1; - } - } - if size > 1 { - // Merge all dicts: first dict is accumulator, merge rest into it - for _ in 1..size { - emit!(self, Instruction::DictMerge { i: 1 }); - } - } - Ok(()) - } - fn detect_builtin_generator_call( &self, func: &ast::Expr, @@ -8884,6 +9329,12 @@ impl Compiler { self.emit_load_zero_super_attr(idx); } } + // CPython's Attribute_kind super path emits an attr-line + // NOP after LOAD_SUPER_ATTR, even when the call later uses + // CALL_FUNCTION_EX for starred arguments. + self.set_source_range(attr.range()); + emit!(self, Instruction::Nop); + self.set_source_range(super_range); emit!(self, Instruction::PushNull); self.codegen_call_helper(0, args, call_range)?; } else { @@ -9082,54 +9533,59 @@ impl Compiler { ); } - // Compile keyword arguments - if nkwelts > 0 { - let mut have_dict = false; - let mut nseen = 0usize; - - for (i, keyword) in arguments.keywords.iter().enumerate() { - if keyword.arg.is_none() { - // **kwargs unpacking - if nseen > 0 { - // Pack up preceding keywords using codegen_subkwargs - self.codegen_subkwargs(&arguments.keywords, i - nseen, i)?; - if have_dict { - emit!(self, Instruction::DictMerge { i: 1 }); - } - have_dict = true; - nseen = 0; - } + self.compile_call_function_ex_keywords(&arguments.keywords)?; - if !have_dict { - emit!(self, Instruction::BuildMap { count: 0 }); - have_dict = true; - } + self.set_source_range(call_range); + emit!(self, Instruction::CallFunctionEx); + } - self.compile_expression_without_const_boolop_folding(&keyword.value)?; - emit!(self, Instruction::DictMerge { i: 1 }); - } else { - nseen += 1; - } - } + Ok(()) + } + + fn compile_call_function_ex_keywords( + &mut self, + keywords: &[ast::Keyword], + ) -> CompileResult<()> { + if keywords.is_empty() { + emit!(self, Instruction::PushNull); + return Ok(()); + } + + let mut have_dict = false; + let mut nseen = 0usize; - // Pack up any trailing keyword arguments + for (i, keyword) in keywords.iter().enumerate() { + if keyword.arg.is_none() { if nseen > 0 { - self.codegen_subkwargs(&arguments.keywords, nkwelts - nseen, nkwelts)?; + self.codegen_subkwargs(keywords, i - nseen, i)?; if have_dict { emit!(self, Instruction::DictMerge { i: 1 }); } have_dict = true; + nseen = 0; + } + + if !have_dict { + emit!(self, Instruction::BuildMap { count: 0 }); + have_dict = true; } - assert!(have_dict); + self.compile_expression_without_const_boolop_folding(&keyword.value)?; + emit!(self, Instruction::DictMerge { i: 1 }); } else { - emit!(self, Instruction::PushNull); + nseen += 1; } + } - self.set_source_range(call_range); - emit!(self, Instruction::CallFunctionEx); + if nseen > 0 { + self.codegen_subkwargs(keywords, keywords.len() - nseen, keywords.len())?; + if have_dict { + emit!(self, Instruction::DictMerge { i: 1 }); + } + have_dict = true; } + debug_assert!(have_dict); Ok(()) } @@ -9663,15 +10119,10 @@ impl Compiler { if sym.flags.contains(SymbolFlags::PARAMETER) { continue; // skip .0 } - // Walrus operator targets (ASSIGNED_IN_COMPREHENSION without ITER) - // are not local to the comprehension; they leak to the outer scope. - let is_walrus = sym.flags.contains(SymbolFlags::ASSIGNED_IN_COMPREHENSION) - && !sym.flags.contains(SymbolFlags::ITER); let is_local = sym .flags .intersects(SymbolFlags::ASSIGNED | SymbolFlags::ITER) - && !sym.flags.contains(SymbolFlags::NONLOCAL) - && !is_walrus; + && !sym.flags.contains(SymbolFlags::NONLOCAL); if is_local { pushed_locals.push(name); } @@ -10012,7 +10463,10 @@ impl Compiler { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); } @@ -10040,6 +10494,13 @@ impl Compiler { } } + fn force_remove_last_no_location_nop(&mut self) { + if let Some(info) = self.current_block().instructions.last_mut() { + info.remove_no_location_nop = true; + info.folded_operand_nop = true; + } + } + /// Mark the last emitted instruction as having no source location. /// Prevents it from triggering LINE events in sys.monitoring. fn set_no_location(&mut self) { @@ -10048,6 +10509,12 @@ impl Compiler { } } + fn mark_last_no_location_exit(&mut self) { + if let Some(last) = self.current_block().instructions.last_mut() { + last.no_location_exit = true; + } + } + fn emit_no_arg>(&mut self, ins: I) { self._emit(ins, OpArg::NULL, BlockIdx::NULL) } @@ -10561,8 +11028,10 @@ impl Compiler { fn emit_return_const_no_location(&mut self, constant: ConstantData) { self.emit_load_const(constant); self.set_no_location(); + self.mark_last_no_location_exit(); emit!(self, Instruction::ReturnValue); self.set_no_location(); + self.mark_last_no_location_exit(); } fn emit_end_async_for(&mut self, send_target: BlockIdx) { @@ -10790,6 +11259,7 @@ impl Compiler { } // Emit cleanup for each fblock + let mut jump_no_location = false; for action in unwind_actions { match action { UnwindAction::With { is_async, range } => { @@ -10810,6 +11280,7 @@ impl Compiler { emit!(self, Instruction::PopTop); self.set_source_range(saved_range); + jump_no_location = true; } UnwindAction::HandlerCleanup { ref name } => { // codegen_unwind_fblock(HANDLER_CLEANUP) @@ -10846,6 +11317,7 @@ impl Compiler { // this keeps the fblock stack consistent for error checking) let code = self.current_code_info(); code.fblock.insert(fblock_idx, saved_fblock); + jump_no_location = true; } UnwindAction::FinallyEnd => { // codegen_unwind_fblock(FINALLY_END) @@ -10867,7 +11339,13 @@ impl Compiler { // Jump to target let target = if is_break { exit_block } else { loop_block }; + let saved_range = self.current_source_range; + self.set_source_range(range); emit!(self, PseudoInstruction::Jump { delta: target }); + if jump_no_location { + self.set_no_location(); + } + self.set_source_range(saved_range); Ok(()) } @@ -10877,29 +11355,6 @@ impl Compiler { &mut info.blocks[info.current_block] } - fn current_block_follows_end_async_for(&mut self) -> bool { - let info = self.current_code_info(); - let current = info.current_block; - if info.blocks[current] - .instructions - .iter() - .rev() - .find_map(|info| info.instr.real()) - .is_some_and(|instr| matches!(instr, Instruction::EndAsyncFor)) - { - return true; - } - info.blocks.iter().any(|block| { - block.next == current - && block - .instructions - .iter() - .rev() - .find_map(|info| info.instr.real()) - .is_some_and(|instr| matches!(instr, Instruction::EndAsyncFor)) - }) - } - fn new_block(&mut self) -> BlockIdx { let code = self.current_code_info(); let idx = BlockIdx::new(code.blocks.len().to_u32()); @@ -11007,6 +11462,7 @@ impl Compiler { } fn compile_expr_fstring(&mut self, fstring: &ast::ExprFString) -> CompileResult<()> { + let fstring_range = fstring.range; let fstring = fstring.value.as_slice(); if self.count_fstring_parts(fstring) > STACK_USE_GUIDELINE { return self.compile_fstring_parts_joined(fstring); @@ -11014,10 +11470,18 @@ impl Compiler { let mut element_count = 0; let mut pending_literal = None; + let mut pending_literal_no_location = false; for part in fstring { - self.compile_fstring_part_into(part, &mut pending_literal, &mut element_count, false)?; + self.compile_fstring_part_into( + part, + &mut pending_literal, + &mut pending_literal_no_location, + &mut element_count, + false, + )?; } - self.finish_fstring(pending_literal, element_count) + self.set_source_range(fstring_range); + self.finish_fstring(pending_literal, pending_literal_no_location, element_count) } fn compile_fstring_parts_joined(&mut self, fstring: &[ast::FStringPart]) -> CompileResult<()> { @@ -11030,10 +11494,17 @@ impl Compiler { let mut element_count = 0; let mut pending_literal = None; + let mut pending_literal_no_location = false; for part in fstring { - self.compile_fstring_part_into(part, &mut pending_literal, &mut element_count, true)?; + self.compile_fstring_part_into( + part, + &mut pending_literal, + &mut pending_literal_no_location, + &mut element_count, + true, + )?; } - self.finish_fstring_join(pending_literal, element_count); + self.finish_fstring_join(pending_literal, pending_literal_no_location, element_count); Ok(()) } @@ -11041,16 +11512,20 @@ impl Compiler { &mut self, part: &ast::FStringPart, pending_literal: &mut Option, + pending_literal_no_location: &mut bool, element_count: &mut u32, append_to_join_list: bool, ) -> CompileResult<()> { match part { ast::FStringPart::Literal(string) => { let value = self.compile_fstring_part_literal_value(string); - if let Some(pending) = pending_literal.as_mut() { - pending.push_wtf8(value.as_ref()); - } else { + if pending_literal.is_none() { + self.set_source_range(string.range); + *pending_literal_no_location = string.range == TextRange::default(); *pending_literal = Some(value); + } else if let Some(pending) = pending_literal.as_mut() { + *pending_literal_no_location &= string.range == TextRange::default(); + pending.push_wtf8(value.as_ref()); } Ok(()) } @@ -11058,6 +11533,7 @@ impl Compiler { fstring.flags, &fstring.elements, pending_literal, + pending_literal_no_location, element_count, append_to_join_list, ), @@ -11067,11 +11543,13 @@ impl Compiler { fn finish_fstring( &mut self, mut pending_literal: Option, + mut pending_literal_no_location: bool, mut element_count: u32, ) -> CompileResult<()> { let keep_empty = element_count == 0; self.emit_pending_fstring_literal( &mut pending_literal, + &mut pending_literal_no_location, &mut element_count, keep_empty, false, @@ -11096,11 +11574,13 @@ impl Compiler { fn finish_fstring_join( &mut self, mut pending_literal: Option, + mut pending_literal_no_location: bool, mut element_count: u32, ) { let keep_empty = element_count == 0; self.emit_pending_fstring_literal( &mut pending_literal, + &mut pending_literal_no_location, &mut element_count, keep_empty, true, @@ -11111,6 +11591,7 @@ impl Compiler { fn emit_pending_fstring_literal( &mut self, pending_literal: &mut Option, + pending_literal_no_location: &mut bool, element_count: &mut u32, keep_empty: bool, append_to_join_list: bool, @@ -11118,6 +11599,8 @@ impl Compiler { let Some(value) = pending_literal.take() else { return; }; + let no_location = *pending_literal_no_location; + *pending_literal_no_location = false; // CPython drops empty literal fragments when they are adjacent to // formatted values, but still emits an empty string for a fully-empty @@ -11127,7 +11610,10 @@ impl Compiler { } self.emit_load_const(ConstantData::Str { value }); - *element_count += 1; + if no_location { + self.set_no_location(); + } + *element_count += 1; if append_to_join_list { emit!(self, Instruction::ListAppend { i: 1 }); } @@ -11184,125 +11670,6 @@ impl Compiler { *element_count += 1; } - /// Optimize `'format_str' % (args,)` into f-string bytecode. - /// Returns true if optimization was applied, false to fall back to normal BINARY_OP %. - /// Matches CPython's codegen.c `compiler_formatted_value` optimization. - fn try_optimize_format_str( - &mut self, - format_str: &str, - args: &[ast::Expr], - range: ruff_text_size::TextRange, - ) -> CompileResult { - // Parse format string into segments - let Some(segments) = Self::parse_percent_format(format_str) else { - return Ok(false); - }; - - // Verify arg count matches specifier count - let spec_count = segments.iter().filter(|s| s.conversion.is_some()).count(); - if spec_count != args.len() { - return Ok(false); - } - - self.set_source_range(range); - - // Special case: no specifiers, just %% escaping → constant fold - if spec_count == 0 { - let folded: String = segments.iter().map(|s| s.literal.as_str()).collect(); - self.emit_load_const(ConstantData::Str { - value: folded.into(), - }); - return Ok(true); - } - - // Emit f-string style bytecode - let mut part_count: u32 = 0; - let mut arg_idx = 0; - - for seg in &segments { - if !seg.literal.is_empty() { - self.emit_load_const(ConstantData::Str { - value: seg.literal.clone().into(), - }); - part_count += 1; - } - if let Some(conv) = seg.conversion { - self.compile_expression(&args[arg_idx])?; - self.set_source_range(range); - emit!(self, Instruction::ConvertValue { oparg: conv }); - emit!(self, Instruction::FormatSimple); - part_count += 1; - arg_idx += 1; - } - } - - if part_count == 0 { - self.emit_load_const(ConstantData::Str { - value: String::new().into(), - }); - } else if part_count > 1 { - emit!(self, Instruction::BuildString { count: part_count }); - } - - Ok(true) - } - - /// Parse a %-format string into segments of (literal_prefix, optional conversion). - /// Returns None if the format string contains unsupported specifiers. - fn parse_percent_format(format_str: &str) -> Option> { - let mut segments = Vec::new(); - let mut chars = format_str.chars().peekable(); - let mut current_literal = String::new(); - - while let Some(ch) = chars.next() { - if ch == '%' { - match chars.peek() { - Some('%') => { - chars.next(); - current_literal.push('%'); - } - Some('s') => { - chars.next(); - segments.push(FormatSegment { - literal: core::mem::take(&mut current_literal), - conversion: Some(oparg::ConvertValueOparg::Str), - }); - } - Some('r') => { - chars.next(); - segments.push(FormatSegment { - literal: core::mem::take(&mut current_literal), - conversion: Some(oparg::ConvertValueOparg::Repr), - }); - } - Some('a') => { - chars.next(); - segments.push(FormatSegment { - literal: core::mem::take(&mut current_literal), - conversion: Some(oparg::ConvertValueOparg::Ascii), - }); - } - _ => { - // Unsupported: %d, %f, %(name)s, %10s, etc. - return None; - } - } - } else { - current_literal.push(ch); - } - } - - // Trailing literal - if !current_literal.is_empty() { - segments.push(FormatSegment { - literal: current_literal, - conversion: None, - }); - } - - Some(segments) - } - fn compile_fstring_elements( &mut self, flags: ast::FStringFlags, @@ -11314,14 +11681,16 @@ impl Compiler { let mut element_count = 0; let mut pending_literal: Option = None; + let mut pending_literal_no_location = false; self.compile_fstring_elements_into( flags, fstring_elements, &mut pending_literal, + &mut pending_literal_no_location, &mut element_count, false, )?; - self.finish_fstring(pending_literal, element_count) + self.finish_fstring(pending_literal, pending_literal_no_location, element_count) } fn compile_fstring_elements_joined( @@ -11338,14 +11707,16 @@ impl Compiler { let mut element_count = 0; let mut pending_literal: Option = None; + let mut pending_literal_no_location = false; self.compile_fstring_elements_into( flags, fstring_elements, &mut pending_literal, + &mut pending_literal_no_location, &mut element_count, true, )?; - self.finish_fstring_join(pending_literal, element_count); + self.finish_fstring_join(pending_literal, pending_literal_no_location, element_count); Ok(()) } @@ -11354,6 +11725,7 @@ impl Compiler { flags: ast::FStringFlags, fstring_elements: &ast::InterpolatedStringElements, pending_literal: &mut Option, + pending_literal_no_location: &mut bool, element_count: &mut u32, append_to_join_list: bool, ) -> CompileResult<()> { @@ -11361,10 +11733,13 @@ impl Compiler { match element { ast::InterpolatedStringElement::Literal(string) => { let value = self.compile_fstring_literal_value(string, flags); - if let Some(pending) = pending_literal.as_mut() { - pending.push_wtf8(value.as_ref()); - } else { + if pending_literal.is_none() { + self.set_source_range(string.range); + *pending_literal_no_location = string.range == TextRange::default(); *pending_literal = Some(value); + } else if let Some(pending) = pending_literal.as_mut() { + *pending_literal_no_location &= string.range == TextRange::default(); + pending.push_wtf8(value.as_ref()); } } ast::InterpolatedStringElement::Interpolation(fstring_expr) => { @@ -11386,9 +11761,11 @@ impl Compiler { .concat(); let text: Wtf8Buf = text.into(); - pending_literal - .get_or_insert_with(Wtf8Buf::new) - .push_wtf8(text.as_ref()); + if pending_literal.is_none() { + *pending_literal_no_location = false; + *pending_literal = Some(Wtf8Buf::new()); + } + pending_literal.as_mut().unwrap().push_wtf8(text.as_ref()); // If debug text is present, apply repr conversion when no `format_spec` specified. // See action_helpers.c: fstring_find_expr_replacement @@ -11402,6 +11779,7 @@ impl Compiler { self.emit_pending_fstring_literal( pending_literal, + pending_literal_no_location, element_count, false, append_to_join_list, @@ -11943,7 +12321,10 @@ mod ruff_tests { #[cfg(test)] mod tests { use super::*; - use rustpython_compiler_core::{SourceFileBuilder, bytecode::OpArg}; + use rustpython_compiler_core::{ + SourceFileBuilder, + bytecode::{CodeUnit, OpArg}, + }; fn assert_scope_exit_locations(code: &CodeObject) { for (instr, (location, _)) in code.instructions.iter().zip(code.locations.iter()) { @@ -11998,7 +12379,8 @@ mod tests { ruff_python_parser::Mode::Module.into(), ) .unwrap(); - let ast = parsed.into_syntax(); + let mut ast = parsed.into_syntax(); + preprocess::preprocess_mod(&mut ast); let ast = match ast { ruff_python_ast::Mod::Module(stmts) => stmts, _ => unreachable!(), @@ -12121,6 +12503,43 @@ mod tests { stack_top.debug_late_cfg_trace().unwrap() } + #[test] + #[ignore = "debug helper"] + fn debug_trace_nested_continue_after_optional_body() { + let trace = compile_single_function_late_cfg_trace( + "\ +def f(names, show_empty, keywords, args_buffer, args, cls, object, level): + for name in names: + value = getattr(cls, name) + if not show_empty: + if value == []: + field_type = cls._field_types.get(name, object) + if getattr(field_type, '__origin__', ...) is list: + if not keywords: + args_buffer.append(repr(value)) + continue + if not keywords: + args.extend(args_buffer) + args_buffer = [] + value, simple = _format(value, level) + if keywords: + args.append('%s=%s' % (name, value)) + else: + args.append(value) +", + "f", + ); + for (label, dump) in trace { + if label == "after_reorder" + || label == "after_remove_redundant_nops_and_jumps" + || label == "after_final_cfg_cleanup" + || label == "after_borrow_deopts" + { + eprintln!("=== {label} ===\n{dump}"); + } + } + } + #[test] #[ignore = "debug helper"] fn debug_trace_make_dataclass_borrow_tail() { @@ -12239,6 +12658,186 @@ def f(sys, os, file): } } + #[test] + fn test_named_except_continue_resume_try_body_keeps_method_borrows() { + let code = compile_exec( + r#" +def f(self, block=True): + if not block and not self.wait(timeout=0): + return None + while self.event_queue.empty(): + while True: + try: + self.push_char(self.read(1)) + except OSError as err: + if err.errno == errno.EINTR: + if not self.event_queue.empty(): + return self.event_queue.get() + else: + continue + else: + raise + else: + break + return self.event_queue.get() +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + let attr_idx = |name: &str| { + instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == name + } + _ => false, + }) + .unwrap_or_else(|| panic!("missing {name} LOAD_ATTR")) + }; + let push_char_idx = attr_idx("push_char"); + let read_idx = attr_idx("read"); + + assert!( + matches!( + instructions[push_char_idx - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "named-except cleanup continue backedge should not deopt the protected try-body method receiver, got instructions={:?}", + instructions.iter().map(|unit| unit.op).collect::>() + ); + assert!( + matches!( + instructions[read_idx - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "nested protected try-body method receiver should remain borrowed like CPython, got instructions={:?}", + instructions.iter().map(|unit| unit.op).collect::>() + ); + } + + #[test] + fn test_boolop_or_shared_body_keeps_false_jump_before_loop_backedge() { + let code = compile_exec( + r#" +def f(value): + digits = [] + for digit in value: + if isinstance(digit, int) and 0 <= digit <= 9: + if digits or digit != 0: + digits.append(digit) + else: + raise ValueError + return digits +"#, + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(11).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "OR-shared body should keep CPython last-condition false jump before the loop backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "OR-shared body should not be moved after the implicit loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_single_if_loop_backedge_keeps_true_body_fallthrough_backedge_shape() { + let code = compile_exec( + r##" +def f(buffer, pos, last_char): + while pos > 0: + pos -= 1 + if buffer[pos] == "#": + last_char = None + return last_char +"##, + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "single-if loop tail should keep CPython true-body plus fallthrough-backedge shape, got ops={ops:?}", + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadConst { .. }, + ] + ) + }), + "single-if loop tail should not be inverted away from CPython shape, got ops={ops:?}", + ); + } + fn find_code<'a>(code: &'a CodeObject, name: &str) -> Option<&'a CodeObject> { if code.obj_name == name { return Some(code); @@ -12626,26 +13225,97 @@ def outer(null): } #[test] - fn test_nonliteral_constant_bool_op_preserves_short_circuit_shape() { - let code = compile_exec( + fn test_taken_constant_boolop_jump_disables_following_borrows() { + for source in [ "\ -x = (\"a\"[0]) or 2 +def f(self): + if 0 and self.h: + self.x = self.y + elif self.a and self.b: + self.x = self.y + self.z = self.w ", - ); - let ops: Vec<_> = code - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + "\ +def f(self): + if 1 or self.h: + self.x = self.y + self.z = self.w +", + ] { + let code = compile_exec(source); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::LoadFast { .. })), + "CPython keeps plain LOAD_FAST after an always-taken constant bool-op jump, got ops={ops:?}" + ); + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "always-taken constant bool-op jump should suppress later LOAD_FAST_BORROW, got ops={ops:?}" + ); + } + } - assert!( - !code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BinaryOp { op } - if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == oparg::BinaryOperator::Subscr - )), + #[test] + fn test_untaken_constant_boolop_jump_keeps_following_borrows() { + for source in [ + "\ +def f(self): + if 1 and self.h: + self.x = self.y + self.z = self.w +", + "\ +def f(self): + if 0 or self.h: + self.x = self.y + self.z = self.w +", + ] { + let code = compile_exec(source); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "constant bool-op jump that is not taken should keep CPython-style borrows, got ops={ops:?}" + ); + } + } + + #[test] + fn test_nonliteral_constant_bool_op_preserves_short_circuit_shape() { + let code = compile_exec( + "\ +x = (\"a\"[0]) or 2 +", + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !code.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::BinaryOp { op } + if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == oparg::BinaryOperator::Subscr + )), "constant subscript should fold before bool-op lowering, got ops={ops:?}" ); assert!( @@ -12890,6 +13560,42 @@ def f(obj, arg): ); } + #[test] + fn test_starred_super_call_keeps_attr_line_nop() { + let code = compile_exec( + "\ +def outer(log): + class DelegatingHTTPRequestHandler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + if log: + super(DelegatingHTTPRequestHandler, + self).log_message(format, *args) +", + ); + let log_message = find_code(&code, "log_message").expect("missing log_message code"); + let ops: Vec<_> = log_message + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadSuperAttr { .. }, + Instruction::Nop, + Instruction::PushNull, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "starred super call should keep CPython's attr-line NOP after LOAD_SUPER_ATTR, got ops={ops:?}" + ); + } + #[test] fn test_builtin_any_genexpr_call_is_optimized() { let code = compile_exec( @@ -12979,6 +13685,135 @@ def set_f(xs): ); } + #[test] + fn test_builtin_tuple_genexpr_try_assignment_uses_shared_tail() { + let code = compile_exec( + "\ +def f(xs): + global y + try: + y = tuple(int(i) for i in xs.split('.')) + except ValueError: + y = () + return y +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let intrinsic = ops + .iter() + .position(|op| matches!(op, Instruction::CallIntrinsic1 { .. })) + .expect("tuple(genexpr) fast path should emit LIST_TO_TUPLE"); + let first_fallback = ops[intrinsic + 1..] + .iter() + .position(|op| matches!(op, Instruction::PushNull)) + .map(|offset| intrinsic + 1 + offset) + .expect("shadowed tuple fallback call should remain after fast path"); + let first_store = ops[intrinsic + 1..] + .iter() + .position(|op| matches!(op, Instruction::StoreGlobal { .. })) + .map(|offset| intrinsic + 1 + offset) + .expect("tuple(genexpr) result should be stored after fast or fallback call"); + + assert!( + matches!(ops[intrinsic + 1], Instruction::JumpForward { .. }) + && first_fallback < first_store, + "tuple(genexpr) fast path should jump over fallback to CPython-style shared store tail, got ops={ops:?}" + ); + } + + #[test] + fn test_builtin_tuple_genexpr_unprotected_assignment_return_duplicates_tail() { + let code = compile_exec( + "\ +def f(arg): + if isinstance(arg, (list, tuple)): + arg = tuple(a for a in arg) + elif not p(arg): + raise TypeError(f'bad {arg}') + return arg +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let intrinsic = ops + .iter() + .position(|op| matches!(op, Instruction::CallIntrinsic1 { .. })) + .expect("tuple(genexpr) fast path should emit LIST_TO_TUPLE"); + + assert!( + matches!(ops[intrinsic + 1], Instruction::StoreFast { .. }) + && matches!( + ops[intrinsic + 2], + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. } + ) + && matches!(ops[intrinsic + 3], Instruction::ReturnValue), + "unprotected tuple(genexpr) assignment before return should inline CPython's assignment-return tail, got ops={ops:?}" + ); + } + + #[test] + fn test_unprotected_builtin_any_prefix_before_returning_try_keeps_borrow() { + let code = compile_exec( + "\ +def f(template): + if any(part.expression.strip() == '' for part in template.interpolations): + return ctor(template) + try: + parsed = tuple( + ast.parse(f'({part.expression})', mode='eval').body + for part in template.interpolations + ) + except SyntaxError: + return ctor(template) + return lit(template, parsed) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let first_try_nop = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::Nop)) + .expect("missing try entry NOP"); + let mut saw_interpolations = false; + for window in instructions[..first_try_nop].windows(2) { + let [receiver, attr] = window else { + continue; + }; + let Instruction::LoadAttr { namei } = attr.op else { + continue; + }; + let load_attr = namei.get(OpArg::new(u32::from(u8::from(attr.arg)))); + if f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() != "interpolations" + { + continue; + } + saw_interpolations = true; + assert!( + matches!(receiver.op, Instruction::LoadFastBorrow { .. }), + "unprotected builtin any(genexpr) prefix before a later returning try should keep CPython-style borrowed receiver, got instructions={instructions:?}" + ); + } + assert!( + saw_interpolations, + "missing interpolations attr load in builtin any prefix, got instructions={instructions:?}" + ); + } + #[test] fn test_module_store_uses_store_global_when_nested_scope_declares_global() { let code = compile_exec( @@ -13064,6 +13899,88 @@ def f(kwonlyargs, kw_only_defaults, arg2value): ); } + #[test] + fn test_protected_store_subscr_tail_uses_strong_loads() { + let code = compile_exec( + "\ +def f(cache, lock, format): + with lock: + format_regex = cache.get(format) + if not format_regex: + try: + format_regex = cache.compile(format) + except KeyError as err: + bad_directive = err.args[0] + del err + raise ValueError(bad_directive) from None + cache[format] = format_regex + return format_regex.match('x') +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFastLoadFast { .. }, + Instruction::LoadFast { .. }, + Instruction::StoreSubscr, + Instruction::LoadConst { .. }, + ] + ) + }), + "expected CPython-style strong loads before protected STORE_SUBSCR tail, got ops={ops:?}" + ); + + let code = compile_exec( + "\ +cache = {} +def g(lock, format): + with lock: + format_regex = cache.get(format) + if not format_regex: + try: + format_regex = compile(format) + except KeyError as err: + bad_directive = err.args[0] + del err + raise ValueError(bad_directive) from None + cache[format] = format_regex + return format_regex.match('x') +", + ); + let g = find_code(&code, "g").expect("missing function code"); + let ops: Vec<_> = g + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadGlobal { .. }, + Instruction::LoadFast { .. }, + Instruction::StoreSubscr, + ] + ) + }), + "expected CPython-style strong value/key loads around global STORE_SUBSCR tail, got ops={ops:?}" + ); + } + #[test] fn test_augassign_two_part_slice_uses_slice_opcodes() { let code = compile_exec( @@ -13306,18 +14223,22 @@ def f(msg): } #[test] - fn test_try_else_return_keeps_nop_before_final_call_return() { + fn test_nested_try_line_nops_after_for_cleanup_are_preserved() { let code = compile_exec( "\ -def f(msg): +def f(xs, env): + for x in xs: + pass try: - fw = _wm.formatwarning - except AttributeError: + try: + if env is not None: + env_list = [] + else: + env_list = None + finally: + pass + finally: pass - else: - if fw is not _formatwarning_orig: - return fw(msg.message, msg.category, msg.filename, msg.lineno, msg.line) - return _wm._formatwarnmsg_impl(msg) ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13329,53 +14250,32 @@ def f(msg): .collect(); assert!( - ops.windows(7).any(|window| { + ops.windows(6).any(|window| { matches!( window, [ - Instruction::ReturnValue, + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, Instruction::Nop, - Instruction::LoadGlobal { .. }, - Instruction::LoadAttr { .. }, Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::Call { .. }, - Instruction::ReturnValue, + Instruction::PopJumpIfNone { .. }, ] ) }), - "expected CPython-style NOP between conditional return and final call return, got ops={ops:?}" - ); - } - - #[test] - fn test_conditional_compare_uses_bool_compare_oparg() { - let code = compile_exec( - "\ -def f(x, y): - if x == y: - return 1 - return 0 -", + "expected CPython-style outer and inner try-line NOPs after for cleanup, got ops={ops:?}" ); - let f = find_code(&code, "f").expect("missing function code"); - let compare = f - .instructions - .iter() - .find(|unit| matches!(unit.op, Instruction::CompareOp { .. })) - .expect("missing COMPARE_OP"); - - assert_eq!(u8::from(compare.arg), 88); } #[test] - fn test_multiline_is_none_conditional_keeps_comparator_nop() { + fn test_try_finally_assert_keeps_finalbody_entry_nop() { let code = compile_exec( "\ def f(x): - if x.find( - 'a') is not None: - return 1 - return 0 + try: + assert x + finally: + g() ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13387,56 +14287,93 @@ def f(x): .collect(); assert!( - ops.windows(3).any(|window| { + ops.windows(4).any(|window| { matches!( window, [ - Instruction::Call { .. }, + Instruction::RaiseVarargs { .. }, Instruction::Nop, - Instruction::PopJumpIfNone { .. }, + Instruction::LoadGlobal { .. }, + Instruction::Call { .. }, ] ) }), - "expected CPython-style comparator NOP before folded POP_JUMP_IF_NONE, got ops={ops:?}" + "assert in try/finally should preserve CPython finalbody-entry NOP after the raise edge, got ops={ops:?}" + ); + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadCommonConstant { .. }, + Instruction::RaiseVarargs { .. }, + Instruction::Nop, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "assert true edge should land on a distinct finalbody-entry NOP, got ops={ops:?}" ); } #[test] - fn test_chained_conditional_compares_use_bool_compare_oparg() { + fn test_try_finally_if_break_false_edge_keeps_finalbody_entry_nop() { let code = compile_exec( "\ -def f(a, b, c): - if a < b < c: - return 1 - return 0 +def f(self, pid): + while True: + try: + if pid == self.pid: + self.h() + break + finally: + self.r() + self.g() + return self.x ", ); let f = find_code(&code, "f").expect("missing function code"); - let compare_args: Vec<_> = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| matches!(unit.op, Instruction::CompareOp { .. })) - .map(|unit| u8::from(unit.arg)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert_eq!(compare_args, vec![18, 18]); + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::ReturnValue, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] + ) + }), + "expected CPython-style if-line NOP before fallthrough finally body, got ops={ops:?}" + ); } #[test] - fn test_shared_final_return_is_cloned_for_jump_target() { + fn test_try_percent_format_preprocess_removes_redundant_try_nop() { let code = compile_exec( "\ -def f(node): - if not isinstance( - node, (AsyncFunctionDef, FunctionDef, ClassDef, Module) - ) or len(node.body) < 1: - return None - node = node.body[0] - if not isinstance(node, Expr): - return None - node = node.value - if isinstance(node, Constant) and isinstance(node.value, str): - return node +def f(self, signal): + if self.returncode and self.returncode < 0: + try: + return \"Command '%s' died with %r.\" % ( + self.cmd, signal.Signals(-self.returncode)) + except ValueError: + return \"Command '%s' died with unknown signal %d.\" % ( + self.cmd, -self.returncode) + return \"Command '%s' returned non-zero exit status %d.\" % ( + self.cmd, self.returncode) ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13447,24 +14384,53 @@ def f(node): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let return_count = ops - .iter() - .filter(|op| matches!(op, Instruction::ReturnValue)) - .count(); - assert_eq!( - return_count, 5, - "expected cloned return sites for each shared return edge, got ops={ops:?}" + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::NotTaken, + Instruction::LoadConst { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "expected preprocessed percent-format body immediately after condition, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::NotTaken, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "percent-format preprocessing should let CFG remove the try-line NOP, got ops={ops:?}" ); } #[test] - fn test_for_break_uses_poptop_cleanup() { + fn test_nested_try_except_in_finally_exception_path_shares_continuation() { let code = compile_exec( "\ -def f(parts): - for value in parts: - if value: - break +def f(self, exc_type, KeyboardInterrupt, TimeoutExpired): + try: + if self.stdin: + self.stdin.close() + finally: + if exc_type == KeyboardInterrupt: + if self._sigint_wait_secs > 0: + try: + self._wait(timeout=self._sigint_wait_secs) + except TimeoutExpired: + pass + self._sigint_wait_secs = 0 + else: + self.wait() ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13474,55 +14440,54 @@ def f(parts): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - - let pop_iter_count = ops - .iter() - .filter(|op| matches!(op, Instruction::PopIter)) + let store_reraise_tails = ops + .windows(2) + .filter(|window| { + matches!( + window, + [Instruction::StoreAttr { .. }, Instruction::Reraise { .. },] + ) + }) .count(); + assert_eq!( - pop_iter_count, 1, - "expected only the loop-exhaustion POP_ITER, got ops={ops:?}" + store_reraise_tails, 1, + "nested try/except inside an exceptional finally body should share the remaining finalbody tail before RERAISE, got ops={ops:?}" ); - - let break_cleanup_idx = ops - .windows(3) - .position(|window| { + assert!( + ops.windows(5).any(|window| { matches!( window, [ - Instruction::PopTop, + Instruction::LoadSmallInt { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::StoreAttr { .. }, Instruction::LoadConst { .. }, - Instruction::ReturnValue + Instruction::ReturnValue, ] ) - }) - .expect("missing POP_TOP/LOAD_CONST/RETURN_VALUE break cleanup"); - let end_for_idx = ops - .iter() - .position(|op| matches!(op, Instruction::EndFor)) - .expect("missing END_FOR"); - assert!( - break_cleanup_idx < end_for_idx, - "expected break cleanup before END_FOR, got ops={ops:?}" + }), + "normal finally body should keep CPython-style borrowed load before STORE_ATTR, got ops={ops:?}" ); } #[test] - fn test_for_exit_before_elif_does_not_leave_line_anchor_nop() { + fn test_try_else_return_keeps_nop_before_final_call_return() { let code = compile_exec( "\ -from sys import maxsize -if maxsize == 2147483647: - for s in ('2147483648', '0o40000000000', '0x100000000', '0b10000000000000000000000000000000'): - try: - x = eval(s) - except OverflowError: - fail('OverflowError on huge integer literal %r' % s) -elif maxsize == 9223372036854775807: - pass +def f(msg): + try: + fw = _wm.formatwarning + except AttributeError: + pass + else: + if fw is not _formatwarning_orig: + return fw(msg.message, msg.category, msg.filename, msg.lineno, msg.line) + return _wm._formatwarnmsg_impl(msg) ", ); - let ops: Vec<_> = code + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) @@ -13530,42 +14495,40 @@ elif maxsize == 9223372036854775807: .collect(); assert!( - ops.windows(4).any(|window| { + ops.windows(7).any(|window| { matches!( window, [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::LoadConst { .. }, Instruction::ReturnValue, - ] - ) - }), - "expected for-exit epilogue without extra NOP, got ops={ops:?}" - ); - assert!( - !ops.windows(4).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, Instruction::Nop, - Instruction::LoadConst { .. }, + Instruction::LoadGlobal { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::Call { .. }, + Instruction::ReturnValue, ] ) }), - "unexpected line-anchor NOP before for-exit epilogue, got ops={ops:?}" + "expected CPython-style NOP between conditional return and final call return, got ops={ops:?}" ); } #[test] - fn test_for_tuple_target_does_not_leave_loop_header_nop() { + fn test_try_else_conditional_scope_exit_keeps_pop_block_nop() { let code = compile_exec( "\ -def f(pairs): - for left, right in pairs: - pass +def f(values, check): + found = '' + for value in values: + try: + if check(value): + raise UnicodeError + except UnicodeError: + pass + else: + found = value + break + return found ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13577,42 +14540,343 @@ def f(pairs): .collect(); assert!( - ops.windows(2).any(|window| { + ops.windows(5).any(|window| { matches!( window, [ - Instruction::ForIter { .. }, - Instruction::UnpackSequence { .. } + Instruction::RaiseVarargs { .. }, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::StoreFast { .. }, + Instruction::PopTop, ] ) }), - "expected FOR_ITER to flow directly into UNPACK_SEQUENCE, got ops={ops:?}" + "try-else after conditional scope exit should keep CPython's POP_BLOCK NOP anchor, got ops={ops:?}" + ); + } + + #[test] + fn test_try_else_loop_fallthrough_keeps_end_jump_nop_before_finally() { + let code = compile_exec( + "\ +def f(locale, category, locales): + try: + orig_locale = locale.setlocale(category) + except AttributeError: + raise + except Exception: + locale = orig_locale = None + if '' not in locales: + raise SkipTest('no locales') + else: + for loc in locales: + try: + locale.setlocale(category, loc) + break + except locale.Error: + pass + else: + if '' not in locales: + raise SkipTest(locales) + try: + yield + finally: + if locale and orig_locale: + locale.setlocale(category, orig_locale) +", ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( - !ops.windows(3).any(|window| { + ops.windows(5).any(|window| { matches!( window, [ - Instruction::ForIter { .. }, + Instruction::RaiseVarargs { .. }, Instruction::Nop, - Instruction::UnpackSequence { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::YieldValue { .. }, ] ) }), - "unexpected loop-header NOP before tuple unpack, got ops={ops:?}" + "try-else loop fallthrough should keep CPython's end-label NOP before following try/finally, got ops={ops:?}" ); } #[test] - fn test_tstring_build_template_matches_cpython_stack_order() { - let code = compile_exec("t = t\"{0}\""); - let units: Vec<_> = code + fn test_conditional_compare_uses_bool_compare_oparg() { + let code = compile_exec( + "\ +def f(x, y): + if x == y: + return 1 + return 0 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let compare = f .instructions .iter() - .copied() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - + .find(|unit| matches!(unit.op, Instruction::CompareOp { .. })) + .expect("missing COMPARE_OP"); + + assert_eq!(u8::from(compare.arg), 88); + } + + #[test] + fn test_multiline_is_none_conditional_keeps_comparator_nop() { + let code = compile_exec( + "\ +def f(x): + if x.find( + 'a') is not None: + return 1 + return 0 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::Call { .. }, + Instruction::Nop, + Instruction::PopJumpIfNone { .. }, + ] + ) + }), + "expected CPython-style comparator NOP before folded POP_JUMP_IF_NONE, got ops={ops:?}" + ); + } + + #[test] + fn test_chained_conditional_compares_use_bool_compare_oparg() { + let code = compile_exec( + "\ +def f(a, b, c): + if a < b < c: + return 1 + return 0 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let compare_args: Vec<_> = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::CompareOp { .. })) + .map(|unit| u8::from(unit.arg)) + .collect(); + + assert_eq!(compare_args, vec![18, 18]); + } + + #[test] + fn test_shared_final_return_is_cloned_for_jump_target() { + let code = compile_exec( + "\ +def f(node): + if not isinstance( + node, (AsyncFunctionDef, FunctionDef, ClassDef, Module) + ) or len(node.body) < 1: + return None + node = node.body[0] + if not isinstance(node, Expr): + return None + node = node.value + if isinstance(node, Constant) and isinstance(node.value, str): + return node +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let return_count = ops + .iter() + .filter(|op| matches!(op, Instruction::ReturnValue)) + .count(); + assert_eq!( + return_count, 5, + "expected cloned return sites for each shared return edge, got ops={ops:?}" + ); + } + + #[test] + fn test_for_break_uses_poptop_cleanup() { + let code = compile_exec( + "\ +def f(parts): + for value in parts: + if value: + break +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let pop_iter_count = ops + .iter() + .filter(|op| matches!(op, Instruction::PopIter)) + .count(); + assert_eq!( + pop_iter_count, 1, + "expected only the loop-exhaustion POP_ITER, got ops={ops:?}" + ); + + let break_cleanup_idx = ops + .windows(3) + .position(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::LoadConst { .. }, + Instruction::ReturnValue + ] + ) + }) + .expect("missing POP_TOP/LOAD_CONST/RETURN_VALUE break cleanup"); + let end_for_idx = ops + .iter() + .position(|op| matches!(op, Instruction::EndFor)) + .expect("missing END_FOR"); + assert!( + break_cleanup_idx < end_for_idx, + "expected break cleanup before END_FOR, got ops={ops:?}" + ); + } + + #[test] + fn test_for_exit_before_elif_does_not_leave_line_anchor_nop() { + let code = compile_exec( + "\ +from sys import maxsize +if maxsize == 2147483647: + for s in ('2147483648', '0o40000000000', '0x100000000', '0b10000000000000000000000000000000'): + try: + x = eval(s) + except OverflowError: + fail('OverflowError on huge integer literal %r' % s) +elif maxsize == 9223372036854775807: + pass +", + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::LoadConst { .. }, + Instruction::ReturnValue, + ] + ) + }), + "expected for-exit epilogue without extra NOP, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, + Instruction::LoadConst { .. }, + ] + ) + }), + "unexpected line-anchor NOP before for-exit epilogue, got ops={ops:?}" + ); + } + + #[test] + fn test_for_tuple_target_does_not_leave_loop_header_nop() { + let code = compile_exec( + "\ +def f(pairs): + for left, right in pairs: + pass +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::ForIter { .. }, + Instruction::UnpackSequence { .. } + ] + ) + }), + "expected FOR_ITER to flow directly into UNPACK_SEQUENCE, got ops={ops:?}" + ); + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::ForIter { .. }, + Instruction::Nop, + Instruction::UnpackSequence { .. }, + ] + ) + }), + "unexpected loop-header NOP before tuple unpack, got ops={ops:?}" + ); + } + + #[test] + fn test_tstring_build_template_matches_cpython_stack_order() { + let code = compile_exec("t = t\"{0}\""); + let units: Vec<_> = code + .instructions + .iter() + .copied() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + assert!( units.windows(6).any(|window| { matches!( @@ -13738,11 +15002,82 @@ def g2(x): } #[test] - fn test_assert_without_message_raises_class_directly() { - let code = compile_exec( + fn test_high_index_parameter_stays_initialized_in_fast_scan() { + let params = (0..65) + .map(|idx| format!("p{idx}")) + .collect::>() + .join(", "); + let code = compile_exec(&format!( "\ -def f(x): - assert x +def f({params}): + return p64 +" + )); + let f = find_code(&code, "f").expect("missing f code"); + + assert!( + f.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } + if f.varnames + [usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))] + == "p64" + )), + "expected high-index parameter p64 to use LOAD_FAST, got ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + !f.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::LoadFastCheck { var_num } + if f.varnames + [usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))] + == "p64" + )), + "high-index parameter p64 should not use LOAD_FAST_CHECK before deletion" + ); + } + + #[test] + fn test_deleted_high_index_parameter_uses_load_fast_check() { + let params = (0..65) + .map(|idx| format!("p{idx}")) + .collect::>() + .join(", "); + let code = compile_exec(&format!( + "\ +def f({params}): + del p64 + return p64 +" + )); + let f = find_code(&code, "f").expect("missing f code"); + + assert!( + f.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::LoadFastCheck { var_num } + if f.varnames + [usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))] + == "p64" + )), + "expected deleted high-index parameter p64 to use LOAD_FAST_CHECK, got ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_assert_without_message_raises_class_directly() { + let code = compile_exec( + "\ +def f(x): + assert x ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -13849,6 +15184,46 @@ def f(fname): ); } + #[test] + fn test_chained_compare_assert_message_keeps_borrowed_load_fast() { + let code = compile_exec( + "\ +def f(month): + assert 1 <= month <= 12, f'month must be in 1..12, not {month}' +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let assertion_error = ops + .iter() + .position(|op| matches!(op, Instruction::LoadCommonConstant { .. })) + .expect("missing LOAD_COMMON_CONSTANT AssertionError"); + let raise = ops[assertion_error..] + .iter() + .position(|op| matches!(op, Instruction::RaiseVarargs { .. })) + .map(|idx| assertion_error + idx) + .expect("missing assert raise"); + let message_path = &ops[assertion_error..raise]; + + assert!( + message_path + .iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "CPython keeps assert message loads borrowed for same-line chained compare failure blocks, got {message_path:?}" + ); + assert!( + !message_path + .iter() + .any(|op| matches!(op, Instruction::LoadFast { .. })), + "same-line chained compare assert message should not be deoptimized to LOAD_FAST, got {message_path:?}" + ); + } + #[test] fn test_assert_message_after_condition_in_same_block_keeps_borrowed_loads() { let code = compile_exec( @@ -13925,6 +15300,123 @@ def f(expected_ns, namespace): } } + #[test] + fn test_bare_function_annotations_check_attribute_and_subscript_expressions() { + let code = compile_exec( + "\ +def f(one: int): + int.new_attr: int + [list][0].new_attr: [int, str] + my_lst = [1] + my_lst[one]: int + return my_lst +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let loads_global = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadGlobal { namei } => { + let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1; + f.names[usize::try_from(namei).unwrap()].as_str() == name + } + _ => false, + }) + }; + assert!( + loads_global("int"), + "bare attribute annotations should evaluate int, got ops={:?}", + f.instructions + ); + assert!( + loads_global("list"), + "bare subscript annotations should evaluate list, got ops={:?}", + f.instructions + ); + } + + #[test] + fn test_function_local_annassign_annotation_does_not_capture_outer_local() { + let code = compile_exec( + "\ +def f(): + from collections import namedtuple + + class MyHandler: + def __init__(self, resource): + self.resource: namedtuple = resource + + return namedtuple +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let class_code = find_code(&code, "MyHandler").expect("missing MyHandler code"); + let init = find_code(&code, "__init__").expect("missing __init__ code"); + + assert!( + !f.cellvars.iter().any(|name| name == "namedtuple"), + "function-local AnnAssign annotation must not make namedtuple a cell, got cellvars={:?}", + f.cellvars + ); + assert!( + !class_code.freevars.iter().any(|name| name == "namedtuple"), + "class body must not close over function-local annotation-only name, got freevars={:?}", + class_code.freevars + ); + assert!( + !init.freevars.iter().any(|name| name == "namedtuple"), + "method body must not close over function-local annotation-only name, got freevars={:?}", + init.freevars + ); + } + + #[test] + fn test_finally_exception_path_inlines_except_pass_reraise_tail() { + let source = "\ +def f(self, file, backupfilename): + try: + if file: + file.close() + finally: + backupfilename = self._backupfilename + self._backupfilename = None + if backupfilename and not self._backup: + try: + os.unlink(backupfilename) + except OSError: + pass + self._isstdin = False +"; + let code = compile_exec(source); + let f = find_code(&code, "f").expect("missing f code"); + assert!( + f.instructions.windows(9).any(|window| { + matches!( + [ + window[0].op, + window[1].op, + window[2].op, + window[3].op, + window[8].op, + ], + [ + Instruction::PopExcept, + Instruction::LoadConst { .. }, + Instruction::LoadFast { .. }, + Instruction::StoreAttr { .. }, + Instruction::Reraise { .. }, + ] + ) && match window[8].op { + Instruction::Reraise { depth } => { + depth.get(OpArg::new(u32::from(u8::from(window[8].arg)))) == 0 + } + _ => false, + } + }), + "except-pass normal exit in a finally exception path should inline the CPython reraise tail, got instructions={:?}", + f.instructions + ); + } + #[test] fn test_non_simple_bare_name_annotation_does_not_create_local_binding() { let code = compile_exec( @@ -14097,6 +15589,124 @@ def gen(fields): ); } + #[test] + fn test_loop_filter_with_nested_loop_body_uses_cpython_implicit_continue_layout() { + let code = compile_exec( + "\ +def f(values): + for fmt, items in values: + nd = make(items, fmt) + for i in range(-5, 5): + check(nd[i], items[i]) + check(nd[-6]) + check(nd[5]) + if is_memoryview_format(fmt): + mv = memoryview(nd) + check(mv, nd) + for i in range(-5, 5): + check(mv[i], items[i]) + check(mv[-6]) + check(mv[5]) + return None +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected CPython-style nested loop filter to fall through into the implicit continue and jump on true into the body, got ops={ops:?}" + ); + } + + #[test] + fn test_final_elif_with_inlined_comprehensions_threads_backedge_before_body() { + let code = compile_exec( + "\ +def f(checks, enumeration, named): + for check in checks: + if check == 1: + pass + elif check is named: + member_names = enumeration._member_names_ + member_values = [m.value for m in enumeration] + missing_names = [] + missing_value = 0 + for name, alias in enumeration._member_map_.items(): + if name in member_names: + continue + if alias.value < 0: + continue + values = list(_iter_bits_lsb(alias.value)) + missed = [v for v in values if v not in member_values] + if missed: + missing_names.append(name) + for val in missed: + missing_value |= val + if missing_names: + raise ValueError('x') + return enumeration +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::IsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "expected CPython-style final elif to put loop backedge before the inlined-comprehension body, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::IsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "unexpected final elif body before loop backedge, got ops={ops:?}" + ); + } + #[test] fn test_multi_with_header_uses_store_fast_load_fast() { let code = compile_exec( @@ -14254,6 +15864,42 @@ def http_error(status): ); } + #[test] + fn test_match_try_body_keeps_setup_nop_after_success_pop() { + let code = compile_exec( + "\ +def f(x): + match x: + case 1: + try: + y = 2 + except Exception: + pass + case 2: + y = 3 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| matches!( + window, + [ + Instruction::PopTop, + Instruction::Nop, + Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. } + ] + )), + "expected CPython-style match success POP_TOP followed by try-entry NOP, got ops={ops:?}" + ); + } + #[test] fn test_match_mapping_attribute_key_keeps_plain_load_fast() { let code = compile_exec( @@ -14507,101 +16153,70 @@ def f(a, b, d): } #[test] - fn test_try_except_finally_normal_cleanup_keeps_body_exit_nop() { + fn test_nested_finally_open_conditional_falls_through_without_entry_nop() { let code = compile_exec( "\ -def f(self, x): - if x and self.sock: - saved = self.sock.gettimeout() - self.sock.settimeout(x) +def f(self, f, closed, new_key): + try: try: - resp = self._get_line() - except TimeoutError as err: - raise self._timeout from err + work() finally: - self.sock.settimeout(saved) - else: - resp = self._get_line() - return resp + if self._locked: + _unlock_file(f) + finally: + if not closed: + _sync_close(f) + return new_key ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache | Instruction::NotTaken)) .collect(); - let resp_store = ops - .iter() - .position(|unit| match unit.op { - Instruction::StoreFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == "resp" - } - _ => false, - }) - .expect("missing resp store"); - - assert!( - matches!( - ops.get(resp_store + 1).map(|unit| unit.op), - Some(Instruction::Nop) - ), - "expected CPython-style NOP between try/except normal body exit and finally cleanup, got ops={ops:?}", - ); - } - - #[test] - fn test_try_except_finally_suppressing_handler_drops_body_exit_nop() { - let code = compile_exec( - "\ -def f(self): - try: - self.sock.shutdown(socket.SHUT_RDWR) - except OSError as exc: - if exc.errno != errno.ENOTCONN: - raise - finally: - self.sock.close() -", - ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - let shutdown_pop = ops - .iter() - .position(|op| matches!(op, Instruction::PopTop)) - .expect("missing shutdown POP_TOP"); assert!( - !matches!(ops.get(shutdown_pop + 1), Some(Instruction::Nop)), - "suppressing except handler should fall directly into finally cleanup without a CPython body-exit NOP, got ops={ops:?}", + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + ] + ) + }), + "expected CPython-style fallthrough from inner finally body into outer finalbody condition, got ops={ops:?}" ); assert!( - matches!( - ops.get(shutdown_pop + 1), - Some(Instruction::LoadFastBorrow { .. }) - ), - "suppressing except handler should keep CPython-style borrowed finally cleanup receiver, got ops={ops:?}", + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + ] + ) + }), + "unexpected preserved inner-finally entry NOP before outer finalbody condition, got ops={ops:?}" ); } #[test] - fn test_conditional_break_finally_does_not_keep_break_cleanup_nop() { + fn test_with_try_finally_normal_cleanup_keeps_redundant_jump_nop() { let code = compile_exec( "\ -def f(tar1, x): - try: - while True: - if x: - break +def f(cm): + with cm: + try: x = 1 - finally: - tar1.close() + finally: + del x + return x ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -14615,103 +16230,34 @@ def f(tar1, x): .collect(); assert!( - !ops_lines.windows(2).any(|window| { + ops_lines.windows(6).any(|window| { matches!( window, [ - (Instruction::Nop, 5), - ( - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - 8 - ), + (Instruction::DeleteFast { .. }, 6), + (Instruction::Nop, 6), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::Call { .. }, 2), ] ) }), - "expected CPython-style break cleanup to jump directly into finally body, got ops_lines={ops_lines:?}", - ); - } - - #[test] - fn test_nested_boolop_same_or_prefixes_compile_without_extra_boolop_block() { - let code = compile_exec( - "\ -def f(c, encodeO, encodeWS): - return ( - (c > 127 or utf7_special[c] == 1) - or (encodeWS and (utf7_special[c] == 2)) - or (encodeO and (utf7_special[c] == 3)) - ) -", - ); - let f = find_code(&code, "f").expect("missing function code"); - let pop_jump_if_true_count = f - .instructions - .iter() - .filter(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })) - .count(); - - assert!( - pop_jump_if_true_count >= 3, - "expected nested boolop prefix path to compile short-circuit jumps, got ops={:?}", - f.instructions - .iter() - .map(|unit| unit.op) - .collect::>() + "expected CPython-style redundant finally cleanup jump to become a line NOP before with-exit cleanup, got ops_lines={ops_lines:?}", ); } #[test] - fn test_broad_exception_import_keeps_borrow_in_common_tail() { + fn test_with_try_except_normal_cleanup_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(msg): - if msg.source is not None: +def f(cm, names, modname): + with cm: try: - import tracemalloc - except Exception: - suggest_tracemalloc = False - tb = None - suggest_tracemalloc = not tracemalloc.is_tracing() - tb = tracemalloc.get_object_traceback(msg.source) - if tb is not None: - for frame in tb: - pass - return 0 -", - ); - let f = find_code(&code, "f").expect("missing function code"); - let import_idx = f - .instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::ImportName { .. })) - .expect("missing IMPORT_NAME"); - - assert!( - f.instructions[import_idx + 1..] - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), - "expected common tail after broad-exception import to keep LOAD_FAST_BORROW, got ops={:?}", - f.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); - } - - #[test] - fn test_try_import_return_handler_deopts_common_tail_borrow() { - let code = compile_exec( - "\ -def f(): - try: - import pwd, grp - except ImportError: - return False - if pwd.getpwuid(0)[0] != 'root': - return False - if grp.getgrgid(0)[0] != 'root': - return False - return True + exec('import %s' % modname, names) + except: + raise FailedImport(modname) + return names ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -14723,23 +16269,35 @@ def f(): .collect(); assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), - "expected CPython-style LOAD_FAST after protected import common tail, got ops={ops:?}", + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "try/except inside with should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}" ); } #[test] - fn test_try_import_pass_else_keeps_borrow() { + fn test_with_try_except_return_handler_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(self): - try: - from _ctypes import set_conversion_mode - except ImportError: - pass - else: - self.prev_conv_mode = set_conversion_mode('ascii', 'strict') +def f(cm): + with cm: + try: + x = 1 + except OSError: + return False + return True ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -14749,361 +16307,412 @@ def f(self): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let normal_tail = &ops[..handler_start]; assert!( - normal_tail.iter().any(|op| { + ops.windows(6).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] ) }), - "try-import pass/else normal path should keep CPython-style borrows, got tail={normal_tail:?}" + "scope-exiting except handler inside with should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}" ); } #[test] - fn test_protected_attr_direct_return_keeps_borrow() { + fn test_with_try_except_else_return_handler_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(obj): - try: - x = 1 - except ValueError: - return False - return obj.values() +def f(cm, func, check): + with cm: + try: + func() + except ValueError: + return False + else: + check() + return True ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let protected_tail = &ops[..handler_start]; assert!( - protected_tail.windows(4).any(|window| { + ops.windows(7).any(|window| { matches!( window, [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. }, Instruction::Call { .. }, - Instruction::ReturnValue, + Instruction::PopTop, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, ] ) }), - "expected protected direct attr-call return to keep LOAD_FAST_BORROW, got tail={protected_tail:?}" + "try/except/else inside with with scope-exiting handler should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}" ); } #[test] - fn test_protected_store_normal_tail_uses_strong_loads() { + fn test_with_try_except_else_continue_handler_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(tarfile, tarinfo, self): - try: - filtered = tarfile.tar_filter(tarinfo, '') - except UnicodeEncodeError: - return None - self.assertIs(filtered.name, tarinfo.name) - return filtered +def f(meta_path, cm): + for finder in meta_path: + with cm: + try: + find_spec = finder.find_spec + except AttributeError: + continue + else: + spec = find_spec() + if spec is not None: + return spec + return None ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing f code"); + let ops_lines: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) .collect(); - let filtered_store = ops - .iter() - .position(|op| matches!(op, Instruction::StoreFast { .. })) - .expect("missing filtered store"); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let normal_tail = &ops[filtered_store + 1..handler_start]; assert!( - !normal_tail.iter().any(|op| matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - )), - "expected CPython-style strong LOAD_FAST in protected store normal tail, got tail={normal_tail:?}", + ops_lines.windows(6).any(|window| { + matches!( + window, + [ + (Instruction::Call { .. }, 9), + (Instruction::StoreFast { .. }, 9), + (Instruction::Nop, 9), + (Instruction::LoadConst { .. }, 3), + (Instruction::LoadConst { .. }, 3), + (Instruction::LoadConst { .. }, 3), + ] + ) + }), + "try/except/else with continue handler should preserve the CPython body-exit NOP before with cleanup, got ops_lines={ops_lines:?}", ); } #[test] - fn test_generator_protected_store_subscr_tail_uses_strong_loads() { + fn test_elif_boolop_skips_following_elif_with_forward_jumpback_block() { let code = compile_exec( - "\ -def f(names, modules): - for name in names: - try: - mod = __import__(name) - except ImportError: - continue - modules[name] = mod - yield mod -", + r#" +def f(module, fromlist, import_, recursive=False): + for x in fromlist: + if not isinstance(x, str): + raise TypeError + elif x == '*': + if not recursive and hasattr(module, '__all__'): + _handle_fromlist(module, module.__all__, import_, recursive=True) + elif not hasattr(module, x): + pass +"#, ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let store_subscr = ops - .iter() - .position(|op| matches!(op, Instruction::StoreSubscr)) - .expect("missing STORE_SUBSCR"); - let window = &ops[store_subscr.saturating_sub(2)..(store_subscr + 3).min(ops.len())]; + let is_load_global = + |unit: &&rustpython_compiler_core::bytecode::CodeUnit, name: &str| match unit.op { + Instruction::LoadGlobal { namei } => { + let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1; + f.names[usize::try_from(namei).unwrap()].as_str() == name + } + _ => false, + }; assert!( - matches!( - window, - [ - Instruction::LoadFastLoadFast { .. }, - Instruction::LoadFast { .. }, - Instruction::StoreSubscr, - Instruction::LoadFast { .. }, - Instruction::YieldValue { .. }, - .. - ] - ), - "expected CPython-style strong LOAD_FAST around protected STORE_SUBSCR generator tail, got {window:?}" + ops.windows(4).any(|window| { + matches!( + window, + [ + unit0, + unit1, + unit2, + unit3, + ] if matches!(unit0.op, Instruction::ToBool) + && matches!(unit1.op, Instruction::PopJumpIfTrue { .. }) + && matches!(unit2.op, Instruction::NotTaken) + && is_load_global(unit3, "hasattr") + ) + }), + "boolop branch in elif should keep CPython's forward jump-back block before hasattr, got ops={ops:?}", ); } #[test] - fn test_protected_call_function_ex_store_tail_uses_strong_loads() { + fn test_with_nonterminal_try_except_normal_cleanup_drops_body_exit_nop() { let code = compile_exec( "\ -def f(func, *args): - try: - result = func(*args) - except Exception: - return None - return type(result) +def f(cm): + with cm: + try: + x = 1 + except Exception: + pass + return x ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let tail_call = ops - .iter() - .rposition(|op| matches!(op, Instruction::Call { .. })) - .expect("missing tail CALL"); - let result_store = ops[..tail_call] - .iter() - .rposition(|op| matches!(op, Instruction::StoreFast { .. })) - .expect("missing protected result STORE_FAST"); - let tail = &ops[result_store + 1..tail_call]; assert!( - tail.iter() - .any(|op| matches!(op, Instruction::LoadFast { .. })), - "expected CPython-style strong LOAD_FAST after protected CALL_FUNCTION_EX store, got ops={ops:?}", + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "non-terminal except inside with should not preserve a body-exit NOP before with cleanup, got ops={ops:?}" ); assert!( - !tail - .iter() - .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), - "protected CALL_FUNCTION_EX store tail should not borrow result, got ops={ops:?}", + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::StoreFast { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "expected CPython-style direct fallthrough into with cleanup, got ops={ops:?}" ); } #[test] - fn test_protected_attr_subscript_tail_uses_strong_load_fast() { + fn test_with_try_except_scope_exit_body_handler_fallthrough_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(obj, idx): - try: - x = 1 - except ValueError: - return False - return obj.__closure__[idx] +def f(cm, ValueError): + with cm: + try: + raise ValueError + except 42: + pass ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing f code"); + let ops_lines: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let protected_tail = &ops[..handler_start]; assert!( - !protected_tail.iter().any(|op| { + ops_lines.windows(5).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + (Instruction::Nop, 6), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::Call { .. }, 2), + ] ) }), - "expected protected attr-subscript tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}" + "handler fallthrough target should preserve the CPython NOP before with cleanup, got ops_lines={ops_lines:?}", ); } #[test] - fn test_protected_direct_subscript_tail_uses_strong_load_fast() { + fn test_with_try_except_nested_with_normal_cleanup_drops_body_exit_nop() { let code = compile_exec( "\ -def f(seq): - try: - items = [int(item) for item in seq] - except ValueError: - return None - return items[0] + items[1] +def f(open, src, dst, copyfileobj): + with open(src) as fsrc: + try: + with open(dst) as fdst: + copyfileobj(fsrc, fdst) + except IsADirectoryError: + raise + return dst ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let protected_store = ops[..handler_start] - .iter() - .rposition(|op| matches!(op, Instruction::StoreFast { .. })) - .expect("missing protected local store"); - let tail = &ops[protected_store + 1..handler_start]; assert!( - !tail.iter().any(|op| { + ops.windows(7).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] ) }), - "expected protected direct-subscript tail to keep strong LOAD_FAST ops, got tail={tail:?}" + "nested with normal cleanup in try/except should fall directly into outer with cleanup, got ops={ops:?}" + ); + assert!( + !ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] + ) + }), + "nested with normal cleanup in try/except should not preserve a body-exit NOP before outer with cleanup, got ops={ops:?}" ); } #[test] - fn test_protected_attr_iter_chain_uses_strong_load_fast() { + fn test_with_nested_if_try_except_normal_cleanup_drops_body_exit_nop() { let code = compile_exec( "\ -def f(fields): - try: - x = 1 - except ValueError: - return False - return tuple(v for v in fields.values()) +def f(cm, root): + with cm: + if root: + try: + x = 1 + except Exception as e: + raise ValueError from e + return root ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let protected_tail = &ops[..handler_start]; assert!( - !protected_tail.iter().any(|op| { + !ops.windows(6).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "expected protected attr-iter chain to keep strong LOAD_FAST ops, got tail={protected_tail:?}" + window, + [ + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "nested try/except should not preserve a body-exit NOP before with cleanup, got ops={ops:?}" ); } #[test] - fn test_generator_except_return_handler_deopts_normal_tail_borrows() { + fn test_try_except_finally_normal_cleanup_keeps_body_exit_nop() { let code = compile_exec( "\ -def f(fields): - try: - x = 1 - except ValueError: - return - for fielddesc in fields: - yield fielddesc +def f(self, x): + if x and self.sock: + saved = self.sock.gettimeout() + self.sock.settimeout(x) + try: + resp = self._get_line() + except TimeoutError as err: + raise self._timeout from err + finally: + self.sock.settimeout(saved) + else: + resp = self._get_line() + return resp ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let handler_start = ops + let resp_store = ops .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let normal_tail = &ops[..handler_start]; + .position(|unit| match unit.op { + Instruction::StoreFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "resp" + } + _ => false, + }) + .expect("missing resp store"); assert!( - normal_tail - .iter() - .any(|op| matches!(op, Instruction::LoadFast { .. })), - "generator tail after non-yielding except return should keep CPython-style strong LOAD_FAST, got tail={normal_tail:?}" - ); - assert!( - !normal_tail.iter().any(|op| { - matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "generator tail after non-yielding except return should not borrow, got tail={normal_tail:?}" + matches!( + ops.get(resp_store + 1).map(|unit| unit.op), + Some(Instruction::Nop) + ), + "expected CPython-style NOP between try/except normal body exit and finally cleanup, got ops={ops:?}", ); } #[test] - fn test_generator_except_yielding_handler_keeps_normal_tail_borrows() { + fn test_try_except_finally_open_conditional_fallthrough_drops_body_exit_nop() { let code = compile_exec( "\ -def f(tp, parent=None): +def f(err, ov, self): try: - fields = tp._fields_ - except AttributeError: - yield parent - else: - for fielddesc in fields: - yield fielddesc + if err: + assert ov + except: + ov.cancel() + raise + finally: + self.cleanup() ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -15113,360 +16722,353 @@ def f(tp, parent=None): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = ops + let assert_raise = ops .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let normal_tail = &ops[..handler_start]; + .position(|op| matches!(op, Instruction::RaiseVarargs { .. })) + .expect("missing assertion raise"); assert!( - normal_tail.iter().any(|op| { - matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "generator tail after yielding except handler should keep CPython-style borrows, got tail={normal_tail:?}" + !matches!(ops.get(assert_raise + 1), Some(Instruction::Nop)), + "open conditional fallthrough should go directly into finally cleanup, got ops={ops:?}", ); } #[test] - fn test_generator_except_pass_resume_tail_keeps_borrows() { + fn test_try_finally_loop_fallthrough_keeps_finalbody_entry_nop() { let code = compile_exec( "\ -def f(self, msg): - if self.log_queue is not None: - yield - output = [] - try: - while True: - output.append(self.log_queue.get_nowait().getMessage()) - except queue.Empty: - pass - else: - with self.assertLogs('concurrent.futures', 'CRITICAL') as cm: - yield - output = cm.output - self.assertTrue(any(msg in line for line in output), output) +def f(close, dup, first, second): + try: + retries = 0 + while second != first + 1: + close(first) + retries += 1 + if retries > 10: + raise RuntimeError + first, second = second, dup(second) + finally: + close(second) + close(first) ", ); let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); - let has_strong_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - let has_borrow_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - - for name in ["msg", "output"] { - assert!( - has_borrow_load(name), - "generator except-pass resume tail should borrow {name}, got instructions={:?}", - f.instructions - ); - assert!( - !has_strong_load(name), - "generator except-pass resume tail should not force strong LOAD_FAST for {name}, got instructions={:?}", - f.instructions - ); - } + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. }, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::PushNull, + ] + ) + }), + "try/finally loop fallthrough should preserve CPython finalbody-entry NOP, got ops={ops:?}" + ); } #[test] - fn test_async_for_cleanup_resume_tail_uses_strong_loads() { + fn test_try_finally_loop_direct_break_drops_finalbody_entry_nop() { let code = compile_exec( "\ -async def f(g, self, x): - async for val in g: - break - self.x(x) - await g.aclose() +def f(lines, close): + try: + while lines: + if lines[0]: + break + close(1) + finally: + close(2) + close(3) ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); - let aclose_idx = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "aclose" - } - _ => false, - }) - .expect("missing aclose load"); assert!( - ops.windows(4).any(|window| { + ops.windows(3).any(|window| { matches!( window, [ - Instruction::LoadFast { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadFast { .. }, - Instruction::Call { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::PushNull, ] ) }), - "async-for cleanup resume tail should use strong LOAD_FAST ops before the await, got ops={ops:?}" + "direct loop break should enter CPython finalbody without a NOP, got ops={ops:?}", ); assert!( - matches!( - instructions - .get(aclose_idx.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFast { .. }) - ), - "async-for cleanup resume tail should keep g strong before aclose, got ops={ops:?}" + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::PushNull, + ] + ) + }), + "direct loop break should not preserve finalbody-entry NOP, got ops={ops:?}", ); } #[test] - fn test_async_generator_async_with_yield_keeps_borrow() { + fn test_try_except_finally_suppressing_handler_drops_body_exit_nop() { let code = compile_exec( "\ -async def f(self, my_cm): - async with self.exit_stack() as stack: - await stack.enter_async_context(my_cm()) - yield stack +def f(self): + try: + self.sock.shutdown(socket.SHUT_RDWR) + except OSError as exc: + if exc.errno != errno.ENOTCONN: + raise + finally: + self.sock.close() ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); - let wrap_idx = instructions + let shutdown_pop = ops .iter() - .position(|unit| match unit.op { - Instruction::CallIntrinsic1 { func } => { - func.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == IntrinsicFunction1::AsyncGenWrap - } - _ => false, - }) - .expect("missing async generator wrap"); + .position(|op| matches!(op, Instruction::PopTop)) + .expect("missing shutdown POP_TOP"); + assert!( + !matches!(ops.get(shutdown_pop + 1), Some(Instruction::Nop)), + "suppressing except handler should fall directly into finally cleanup without a CPython body-exit NOP, got ops={ops:?}", + ); assert!( matches!( - ops.get(wrap_idx.saturating_sub(1)), + ops.get(shutdown_pop + 1), Some(Instruction::LoadFastBorrow { .. }) ), - "async generator yield inside async-with should borrow stack like CPython, got ops={ops:?}" + "suppressing except handler should keep CPython-style borrowed finally cleanup receiver, got ops={ops:?}", ); } #[test] - fn test_deoptimized_async_with_enter_continuation_uses_strong_loads() { + fn test_conditional_break_finally_does_not_keep_break_cleanup_nop() { let code = compile_exec( "\ -async def f(): - async def cm(): - pass - try: - async with cm(): - 1 / 0 - except ZeroDivisionError as e: - frames = e - class E(RuntimeError): - pass +def f(tar1, x): try: - async with cm(): - raise E(42) - except E as e: - frames = e + while True: + if x: + break + x = 1 + finally: + tar1.close() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let ops_lines: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) .collect(); assert!( - ops.windows(5).any(|window| { + !ops_lines.windows(2).any(|window| { matches!( window, [ - Instruction::LoadFast { .. }, - Instruction::PushNull, - Instruction::LoadSmallInt { .. }, - Instruction::Call { .. }, - Instruction::RaiseVarargs { .. }, + (Instruction::Nop, 5), + ( + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + 8 + ), ] ) }), - "async-with enter continuation after a deoptimized setup block should keep raised class strong, got ops={ops:?}" + "expected CPython-style break cleanup to jump directly into finally body, got ops_lines={ops_lines:?}", ); } #[test] - fn test_async_with_bare_raise_continuation_keeps_borrow() { + fn test_with_break_cleanup_makes_following_jump_artificial() { let code = compile_exec( "\ -async def f(tg): - class E(Exception): - pass - try: - async with tg: - raise E - except ExceptionGroup: - pass +def f(self): + while self.returncode is None: + with self._waitpid_lock: + if self.returncode is not None: + break + self.work() + return self.returncode ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let ops_lines: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) .collect(); - let raise_idx = ops - .iter() - .position(|op| matches!(op, Instruction::RaiseVarargs { .. })) - .expect("missing raise"); assert!( - matches!( - ops.get(raise_idx.saturating_sub(1)), - Some(Instruction::LoadFastBorrow { .. }) - ), - "bare async-with raise continuation should keep the raised class borrowed like CPython, got ops={ops:?}" + !ops_lines.windows(2).any(|window| { + matches!( + window, + [ + (Instruction::Nop, 5), + ( + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + 7 + ), + ] + ) + }), + "expected CPython-style artificial jump after with-break cleanup, got ops_lines={ops_lines:?}", ); } #[test] - fn test_except_star_tail_uses_strong_loads() { + fn test_while_exit_before_with_cleanup_materializes_anchor_nop() { let code = compile_exec( "\ -def f(self): +def f(selector, self): + with selector: + while selector.get_map(): + pass try: + self.wait() + except Exception: pass - except* ValueError: - pass - self.fail('x') ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let ops_lines: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) .collect(); assert!( - ops.windows(4).any(|window| { - matches!( - window, - [ - Instruction::LoadFast { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - ] - ) - }), - "except* tail should use strong LOAD_FAST like CPython, got ops={ops:?}" - ); - assert!( - !ops.windows(4).any(|window| { + ops_lines.windows(6).any(|window| { matches!( window, [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, + (Instruction::JumpBackward { .. }, 4), + (Instruction::Nop, 3), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::LoadConst { .. }, 2), + (Instruction::Call { .. }, 2), ] ) }), - "except* tail should not borrow the receiver after the handler region, got ops={ops:?}" + "expected CPython-style while-exit anchor NOP before with cleanup, got ops_lines={ops_lines:?}", ); } #[test] - fn test_protected_attr_subscript_store_tail_uses_strong_load_fast() { + fn test_nested_boolop_same_or_prefixes_compile_without_extra_boolop_block() { let code = compile_exec( "\ -def f(f, oldcls, newcls): - try: - idx = f.__code__.co_freevars.index('__class__') - except ValueError: - return False - closure = f.__closure__[idx] - if closure.cell_contents is oldcls: - closure.cell_contents = newcls - return True - return False +def f(c, encodeO, encodeWS): + return ( + (c > 127 or utf7_special[c] == 1) + or (encodeWS and (utf7_special[c] == 2)) + or (encodeO and (utf7_special[c] == 3)) + ) ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let pop_jump_if_true_count = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let protected_tail = &ops[..handler_start]; - let store_closure_idx = protected_tail - .windows(2) - .position(|window| { - matches!( - window, - [Instruction::BinaryOp { .. }, Instruction::StoreFast { .. }] - ) - }) - .map(|idx| idx + 1) - .expect("missing STORE_FAST for closure"); - let post_store_tail = &protected_tail[store_closure_idx + 1..]; + .filter(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })) + .count(); assert!( - !post_store_tail.iter().any(|op| { - matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "expected protected attr-subscript store tail to keep strong LOAD_FAST ops, got tail={post_store_tail:?}" + pop_jump_if_true_count >= 3, + "expected nested boolop prefix path to compile short-circuit jumps, got ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); } #[test] - fn test_plain_attr_subscript_tail_keeps_borrow() { + fn test_nested_opposite_boolop_threads_to_fallthrough_like_cpython() { + for source in [ + "\ +def f(a, b, c): + return ((a and b) + or c) +", + "\ +def f(a, b, c): + return ((a or b) + and c) +", + ] { + let code = compile_exec(source); + let f = find_code(&code, "f").expect("missing f code"); + let jumps: Vec<_> = f + .instructions + .iter() + .filter(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .collect(); + + assert_eq!( + jumps.len(), + 2, + "expected two conditional jumps, got {jumps:?}" + ); + assert!( + u8::from(jumps[0].arg) > u8::from(jumps[1].arg), + "expected CPython-style first jump to bypass the opposite short-circuit test, got jumps={jumps:?}" + ); + } + } + + #[test] + fn test_loop_or_continue_keeps_boolop_true_edge_to_continue() { let code = compile_exec( "\ -def f(self, name): - annotations = self.method_annotations[name] - return annotations +def f(numpy_array, lshape, rshape, litems, fmt, tl): + for _ in range(3): + if numpy_array: + if 0 in lshape or 0 in rshape: + continue + zl = numpy_array_from_structure(litems, fmt, tl) ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -15478,127 +17080,104 @@ def f(self, name): .collect(); assert!( - ops.windows(4).any(|window| { + ops.windows(8).any(|window| { matches!( window, [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::BinaryOp { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadSmallInt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, ] ) }), - "expected plain attr-subscript tail to keep borrowed receiver/index loads, got ops={ops:?}" - ); - } - - #[test] - fn test_plain_attr_iter_chain_keeps_borrow() { - let code = compile_exec( - "\ -def f(fields): - return tuple(v for v in fields.values()) -", + "expected CPython-style `or` continue test to keep first true edge to continue, got ops={ops:?}" ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - assert!( - ops.windows(4).any(|window| { + !ops.windows(5).any(|window| { matches!( window, [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. }, - Instruction::Call { .. }, - Instruction::GetIter, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadSmallInt { .. }, ] ) }), - "expected plain attr-iter chain to keep borrowed receiver, got ops={ops:?}" + "unexpected inverted first `or` continue condition before second operand, got ops={ops:?}" ); } #[test] - fn test_genexpr_true_filter_omits_bool_scaffolding() { + fn test_nested_and_or_expression_threads_same_false_short_circuit() { let code = compile_exec( "\ -def f(it): - return (x for x in it if True) +def f(fmt, MEMORYVIEW): + x = len(fmt) + return ((x == 1 or (x == 2 and fmt[0] == '@')) and + fmt[x - 1] in MEMORYVIEW) ", ); - let genexpr = find_code(&code, "").expect("missing code"); - assert!( - !genexpr.instructions.iter().any(|unit| { - matches!(unit.op, Instruction::LoadConst { .. }) - && matches!( - genexpr.constants.get(usize::from(u8::from(unit.arg))), - Some(ConstantData::Boolean { value: true }) - ) - }), - "constant-true filter should not load True, got ops={:?}", - genexpr - .instructions + let f = find_code(&code, "f").expect("missing f code"); + let false_jumps: Vec<_> = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::PopJumpIfFalse { .. })) + .collect(); + + assert!( + false_jumps.len() >= 2, + "expected nested boolop false jumps, got ops={:?}", + f.instructions .iter() .map(|unit| unit.op) .collect::>() ); assert!( - !genexpr - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })), - "constant-true filter should not leave POP_JUMP_IF_TRUE scaffolding, got ops={:?}", - genexpr - .instructions - .iter() - .map(|unit| unit.op) - .collect::>() + u8::from(false_jumps[0].arg) > u8::from(false_jumps[1].arg), + "expected CPython-style same-false short-circuit threading to outer end, got false_jumps={false_jumps:?}" ); } #[test] - fn test_classdictcell_uses_load_closure_path_and_borrows_after_optimize() { + fn test_broad_exception_import_keeps_borrow_in_common_tail() { let code = compile_exec( "\ -class C: - def method(self): - return 1 +def f(msg): + if msg.source is not None: + try: + import tracemalloc + except Exception: + suggest_tracemalloc = False + tb = None + suggest_tracemalloc = not tracemalloc.is_tracing() + tb = tracemalloc.get_object_traceback(msg.source) + if tb is not None: + for frame in tb: + pass + return 0 ", ); - let class_code = find_code(&code, "C").expect("missing class code"); - let store_classdictcell = class_code + let f = find_code(&code, "f").expect("missing function code"); + let import_idx = f .instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::StoreName { namei } - if class_code.names - [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] - .as_str() - == "__classdictcell__" - ) - }) - .expect("missing STORE_NAME __classdictcell__"); + .position(|unit| matches!(unit.op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); assert!( - matches!( - class_code - .instructions - .get(store_classdictcell.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFastBorrow { .. }) - ), - "expected LOAD_FAST_BORROW before __classdictcell__ store, got ops={:?}", - class_code - .instructions + f.instructions[import_idx + 1..] + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), + "expected common tail after broad-exception import to keep LOAD_FAST_BORROW, got ops={:?}", + f.instructions .iter() .map(|unit| unit.op) .collect::>() @@ -15606,406 +17185,607 @@ class C: } #[test] - fn test_conditional_class_body_duplicates_no_location_exit_tail() { + fn test_try_import_return_handler_deopts_common_tail_borrow() { let code = compile_exec( "\ -flag = False -class C: - if flag: - value = 1 +def f(): + try: + import pwd, grp + except ImportError: + return False + if pwd.getpwuid(0)[0] != 'root': + return False + if grp.getgrgid(0)[0] != 'root': + return False + return True ", ); - let class_code = find_code(&code, "C").expect("missing class code"); - let ops: Vec<_> = class_code + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let return_count = ops - .iter() - .filter(|op| matches!(op, Instruction::ReturnValue)) - .count(); - let static_attrs_count = class_code - .instructions - .iter() - .filter(|unit| { - matches!( - unit.op, - Instruction::StoreName { namei } - if class_code.names - [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] - .as_str() - == "__static_attributes__" - ) - }) - .count(); - assert_eq!( - return_count, 2, - "conditional class body should duplicate CPython no-location return tail, got ops={ops:?}" - ); - assert_eq!( - static_attrs_count, 2, - "conditional class body should duplicate __static_attributes__ tail, got ops={ops:?}" + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "expected CPython-style LOAD_FAST after protected import common tail, got ops={ops:?}", ); } #[test] - fn test_class_lambda_assignment_does_not_create_classdictcell() { + fn test_try_import_return_handler_deopts_later_protected_tail_borrow() { let code = compile_exec( "\ -class C: - data = start = end = lambda *a: None +def f(info_add): + try: + import pwd + except ImportError: + return + import os + uid = os.getuid() + try: + entry = pwd.getpwuid(uid) + except KeyError: + entry = None + info_add(uid, entry) + if entry is None: + return + if hasattr(os, 'getgrouplist'): + groups = os.getgrouplist(entry.pw_name, entry.pw_gid) + groups = ', '.join(map(str, groups)) + info_add('os.getgrouplist', groups) ", ); - let class_code = find_code(&code, "C").expect("missing class code"); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let import_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let protected_tail = &ops[import_idx + 1..]; assert!( - !class_code.instructions.iter().any(|unit| { + !protected_tail.iter().any(|op| { matches!( - unit.op, - Instruction::StoreName { namei } - if class_code.names - [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] - .as_str() - == "__classdictcell__" + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) }), - "lambda-only class should not create __classdictcell__, got ops={:?}", - class_code - .instructions - .iter() - .map(|unit| unit.op) - .collect::>() + "CPython keeps strong LOAD_FAST ops after protected import with return handler, even across later protected tails, got tail={protected_tail:?}" ); } #[test] - fn test_nested_function_static_attributes_are_collected() { + fn test_try_import_continue_handler_deopts_loop_tail_borrow() { let code = compile_exec( "\ -class C: - def f(self): - self.x = 1 - self.y = 2 - self.x = 3 - - def g(self, obj): - self.y = 4 - self.z = 5 - - def h(self, a): - self.u = 6 - self.v = 7 - - obj.self = 8 +def f(size): + pos = 0 + while pos < size: + try: + import unicodedata + except ImportError: + continue + if pos < size: + pos += 1 + return pos ", ); - let class_code = find_code(&code, "C").expect("missing class code"); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let import_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[import_idx + 1..handler_start]; + let return_idx = normal_tail + .iter() + .position(|op| matches!(op, Instruction::ReturnValue)) + .unwrap_or(normal_tail.len()); + let loop_tail = &normal_tail[..return_idx.saturating_sub(1)]; assert!( - class_code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements - == &[ - ConstantData::Str { value: "u".into() }, - ConstantData::Str { value: "v".into() }, - ConstantData::Str { value: "x".into() }, - ConstantData::Str { value: "y".into() }, - ConstantData::Str { value: "z".into() }, - ] - )), - "expected nested function static attributes in class consts" + !loop_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "CPython keeps strong LOAD_FAST ops in the loop tail after protected import with continue handler, got tail={loop_tail:?}", ); } #[test] - fn test_static_attributes_match_cpython_store_rule() { + fn test_try_import_continue_inside_loop_keeps_earlier_loop_body_borrows() { let code = compile_exec( - "\ -class C: - @staticmethod - def f(): - self.x = 1 - - @classmethod - def g(cls): - self.y = 2 - - def h(obj): - obj.z = 3 - tarinfo.uid = 4 - - def i(self): - self.a: int - self.b: int = 1 - self.c += 1 - del self.d -", + r#" +def f(s, size, errors): + p = [] + pos = 0 + while pos < size: + if s[pos] != ord("\\"): + p.append(chr(s[pos])) + pos += 1 + continue + if pos < size: + try: + import unicodedata + except ImportError: + errors(pos, size) + continue + if pos < size: + p.append(chr(s[pos])) + pos += 1 + return p, pos +"#, ); - let class_code = find_code(&code, "C").expect("missing class code"); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let import_idx = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let is_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str, borrowed: bool| { + let Some(var_nums) = (match (unit.op, borrowed) { + (Instruction::LoadFastBorrowLoadFastBorrow { var_nums }, true) + | (Instruction::LoadFastLoadFast { var_nums }, false) => Some(var_nums), + _ => None, + }) else { + return false; + }; + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == left_name + && f.varnames[usize::from(right)] == right_name + }; + let before_import = &instructions[..import_idx]; assert!( - class_code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements - == &[ - ConstantData::Str { value: "b".into() }, - ConstantData::Str { value: "x".into() }, - ConstantData::Str { value: "y".into() }, - ] - )), - "expected only CPython-collected static attributes in class consts" + before_import + .iter() + .any(|unit| is_pair(unit, "s", "pos", true)), + "loop body before protected import should keep CPython-style borrowed s/pos pair, got instructions={instructions:?}" + ); + assert!( + !before_import + .iter() + .any(|unit| is_pair(unit, "s", "pos", false)), + "protected import later in the loop should not deopt earlier s/pos pair to strong LOAD_FAST_LOAD_FAST, got instructions={instructions:?}" ); } #[test] - fn test_decorated_class_uses_first_decorator_for_firstlineno() { + fn test_try_import_pass_else_keeps_borrow() { let code = compile_exec( "\ -@dec1 -@dec2 -class C: - pass +def f(self): + try: + from _ctypes import set_conversion_mode + except ImportError: + pass + else: + self.prev_conv_mode = set_conversion_mode('ascii', 'strict') ", ); - let class_code = find_code(&code, "C").expect("missing class code"); - let store_firstlineno = class_code + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f .instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::StoreName { namei } - if class_code.names - [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] - .as_str() - == "__firstlineno__" - ) - }) - .expect("missing STORE_NAME __firstlineno__"); - let load_firstlineno = class_code - .instructions - .get(store_firstlineno.saturating_sub(1)) - .expect("missing LOAD_CONST for __firstlineno__"); + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[..handler_start]; - let expected = ConstantData::Integer { - value: BigInt::from(1), - }; assert!( - matches!( - load_firstlineno.op, - Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. } - ), - "expected LOAD_SMALL_INT/LOAD_CONST before __firstlineno__, got {:?}", - load_firstlineno.op + normal_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "try-import pass/else normal path should keep CPython-style borrows, got tail={normal_tail:?}" ); - if let Instruction::LoadConst { consti } = load_firstlineno.op { - let value = &class_code.constants - [consti.get(OpArg::new(u32::from(u8::from(load_firstlineno.arg))))]; - assert_eq!(value, &expected); - } else { - assert_eq!(u32::from(u8::from(load_firstlineno.arg)), 1); - } } #[test] - fn test_future_annotations_class_keeps_conditional_annotations_cell() { + fn test_try_import_broad_handler_implicit_return_keeps_borrow() { let code = compile_exec( "\ -from __future__ import annotations -class C: - x: int +def f(self, record): + try: + import smtplib + port = self.mailport + if not port: + port = smtplib.SMTP_PORT + smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout) + smtp.quit() + except Exception: + self.handleError(record) ", ); - let class_code = find_code(&code, "C").expect("missing class code"); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &instructions[..handler_start]; + let local_name = |unit: &&CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; - assert!( - class_code - .cellvars - .iter() - .any(|name| name.as_str() == "__conditional_annotations__"), - "expected __conditional_annotations__ cellvar, got cellvars={:?}", - class_code.cellvars - ); + for name in ["self", "smtplib", "smtp"] { + assert!( + normal_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. }) + && local_name(unit) == Some(name)), + "broad except handler with implicit return should keep CPython-style borrowed {name} loads, got tail={normal_tail:?}" + ); + assert!( + !normal_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. }) + && local_name(unit) == Some(name)), + "broad except handler with implicit return should not force strong {name} loads, got tail={normal_tail:?}" + ); + } } #[test] - fn test_plain_super_call_keeps_class_freevar() { + fn test_try_import_handler_assignment_resume_tail_keeps_borrow() { let code = compile_exec( "\ -class A: - pass - -class B(A): - def method(self): - return super() +def f(): + try: + import subprocess + out = subprocess.check_output(['/usr/bin/lslpp', '-Lqc', 'bos.rte']) + except ImportError: + out = _read_cmd_output('/usr/bin/lslpp -Lqc bos.rte') + out = out.decode('utf-8') + out = out.strip().split(':') + _bd = int(out[-1]) if out[-1] != '' else 9988 + return (str(out[2]), _bd) ", ); - let method = find_code(&code, "method").expect("missing method code"); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &instructions[..handler_start]; + let out_idx = f + .varnames + .iter() + .position(|name| name == "out") + .expect("missing out local"); + let load_out_is = |unit: &&CodeUnit, borrowed: bool| match (unit.op, borrowed) { + (Instruction::LoadFastBorrow { var_num }, true) + | (Instruction::LoadFast { var_num }, false) => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + usize::from(var_num.get(arg)) == out_idx + } + _ => false, + }; + assert!( - method.freevars.iter().any(|name| name == "__class__"), - "plain super() must keep __class__ freevar, got freevars={:?}", - method.freevars + normal_tail.iter().any(|unit| load_out_is(unit, true)), + "handler assignment resume tail should keep CPython-style borrowed out loads, got tail={normal_tail:?}" ); assert!( - method - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::CopyFreeVars { .. })), - "plain super() must keep COPY_FREE_VARS prelude, got ops={:?}", - method - .instructions - .iter() - .map(|unit| unit.op) - .collect::>() + !normal_tail.iter().any(|unit| load_out_is(unit, false)), + "handler assignment resume tail should not force strong out loads, got tail={normal_tail:?}" ); } #[test] - fn test_nested_class_super_does_not_create_outer_class_closure() { + fn test_protected_attr_direct_return_keeps_borrow() { let code = compile_exec( "\ -class C: - def outer(self): - class D: - def __init__(self): - super().__init__() +def f(obj): + try: + x = 1 + except ValueError: + return False + return obj.values() ", ); - let outer_class = find_code(&code, "C").expect("missing outer class code"); - let nested_class = find_code(&code, "D").expect("missing nested class code"); - let init = find_code(&code, "__init__").expect("missing nested __init__ code"); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let protected_tail = &ops[..handler_start]; assert!( - !outer_class.cellvars.iter().any(|name| name == "__class__"), - "nested super() must not force __class__ on outer class, got cellvars={:?}", - outer_class.cellvars - ); - assert!( - nested_class.cellvars.iter().any(|name| name == "__class__"), - "nested class should own __class__ cell, got cellvars={:?}", - nested_class.cellvars - ); - assert!( - init.freevars.iter().any(|name| name == "__class__"), - "method using super() should close over nested class, got freevars={:?}", - init.freevars + protected_tail.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::Call { .. }, + Instruction::ReturnValue, + ] + ) + }), + "expected protected direct attr-call return to keep LOAD_FAST_BORROW, got tail={protected_tail:?}" ); } #[test] - fn test_nested_closure_parameter_class_does_not_create_outer_class_closure() { + fn test_protected_store_normal_tail_uses_strong_loads() { let code = compile_exec( "\ -class C: - def m(self): - def create_closure(__class__): - return (lambda: __class__).__closure__ +def f(tarfile, tarinfo, self): + try: + filtered = tarfile.tar_filter(tarinfo, '') + except UnicodeEncodeError: + return None + self.assertIs(filtered.name, tarinfo.name) + return filtered ", ); - let outer_class = find_code(&code, "C").expect("missing class code"); - let create_closure = - find_code(&code, "create_closure").expect("missing create_closure code"); - let lambda = find_code(&code, "").expect("missing lambda code"); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let filtered_store = ops + .iter() + .position(|op| matches!(op, Instruction::StoreFast { .. })) + .expect("missing filtered store"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[filtered_store + 1..handler_start]; assert!( - !outer_class.cellvars.iter().any(|name| name == "__class__"), - "nested __class__ parameter must not force outer class cell, got cellvars={:?}", - outer_class.cellvars - ); - assert!( - create_closure - .cellvars - .iter() - .any(|name| name == "__class__"), - "create_closure should own __class__ parameter cell, got cellvars={:?}", - create_closure.cellvars - ); - assert!( - lambda.freevars.iter().any(|name| name == "__class__"), - "lambda should close over create_closure parameter, got freevars={:?}", - lambda.freevars + !normal_tail.iter().any(|op| matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + )), + "expected CPython-style strong LOAD_FAST in protected store normal tail, got tail={normal_tail:?}", ); } #[test] - fn test_chained_compare_jump_uses_single_cleanup_copy() { + fn test_protected_call_arm_final_store_return_uses_strong_load() { let code = compile_exec( "\ -def f(code): - if not 1 <= code <= 2147483647: - raise ValueError('x') +def f(self, action, default_metavar): + get_metavar = self._metavar_formatter(action, default_metavar) + if action.nargs is None: + result = '%s' % get_metavar(1) + elif action.nargs == OPTIONAL: + result = '[%s]' % get_metavar(1) + elif action.nargs == ZERO_OR_MORE: + metavar = get_metavar(1) + if len(metavar) == 2: + result = '[%s [%s ...]]' % metavar + else: + result = '[%s ...]' % metavar + elif action.nargs == ONE_OR_MORE: + result = '%s [%s ...]' % get_metavar(2) + elif action.nargs == REMAINDER: + result = '...' + elif action.nargs == PARSER: + result = '%s ...' % get_metavar(1) + elif action.nargs == SUPPRESS: + result = '' + else: + try: + formats = ['%s' for _ in range(action.nargs)] + except TypeError: + raise ValueError(\"invalid nargs value\") from None + result = ' '.join(formats) % get_metavar(action.nargs) + return result ", ); let f = find_code(&code, "f").expect("missing function code"); - let copy_count = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| matches!(unit.op, Instruction::Copy { .. })) - .count(); - let pop_top_count = f - .instructions + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let modulo = ops .iter() - .filter(|unit| matches!(unit.op, Instruction::PopTop)) - .count(); - - assert_eq!(copy_count, 1); - assert_eq!(pop_top_count, 1); + .position(|unit| { + matches!( + unit.op, + Instruction::BinaryOp { op } + if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == BinaryOperator::Remainder + ) + }) + .expect("missing final format modulo"); + let tail = &ops[modulo..]; + assert!( + tail.windows(3).any(|window| { + matches!( + window, + [ + bytecode::CodeUnit { + op: Instruction::StoreFast { .. }, + .. + }, + bytecode::CodeUnit { + op: Instruction::LoadFast { .. }, + .. + }, + bytecode::CodeUnit { + op: Instruction::ReturnValue, + .. + }, + ] + ) + }), + "protected call arm final store-return should keep CPython-style strong LOAD_FAST, got tail={tail:?}" + ); } #[test] - fn test_yield_from_cleanup_jumps_to_shared_end_send() { + fn test_protected_store_try_else_tail_keeps_borrowed_loads() { let code = compile_exec( "\ -def outer(): - def inner(): - yield from outer_gen - return inner +def f(value): + message_id = MessageID() + try: + token, value = get_msg_id(value) + message_id.append(token) + except HeaderParseError as ex: + token = get_unstructured(value) + message_id = InvalidMessageID(token) + message_id.defects.append(InvalidHeaderDefect('Invalid msg-id: {!r}'.format(ex))) + else: + if value: + message_id.defects.append(InvalidHeaderDefect('Unexpected {!r}'.format(value))) + return message_id ", ); - let inner = find_code(&code, "inner").expect("missing inner code"); - let ops: Vec<_> = inner + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[..handler_start]; - let cleanup_idx = ops + assert!( + normal_tail.windows(3).any(|window| { + matches!( + window, + [ + CodeUnit { + op: Instruction::LoadFastBorrow { .. }, + .. + }, + CodeUnit { + op: Instruction::ToBool, + .. + }, + CodeUnit { + op: Instruction::PopJumpIfFalse { .. } + | Instruction::PopJumpIfTrue { .. }, + .. + }, + ] + ) + }), + "try/except/else bool guard should keep CPython-style borrowed load, got tail={normal_tail:?}", + ); + + let defects_idx = normal_tail .iter() - .position(|op| matches!(op, Instruction::CleanupThrow)) - .expect("missing CLEANUP_THROW"); + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "defects" + } + _ => false, + }) + .expect("missing defects LOAD_ATTR in else tail"); assert!( matches!( - ops.get(cleanup_idx + 1), - Some(Instruction::JumpBackwardNoInterrupt { .. } | Instruction::JumpForward { .. }) + normal_tail[defects_idx - 1].op, + Instruction::LoadFastBorrow { .. } ), - "expected CLEANUP_THROW to jump to shared END_SEND block, got ops={ops:?}" + "try/except/else method receiver should stay borrowed like CPython, got tail={normal_tail:?}", ); + + let format_idx = normal_tail + .iter() + .rposition(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "format" + } + _ => false, + }) + .expect("missing format LOAD_ATTR in else tail"); assert!( - !matches!(ops.get(cleanup_idx + 1), Some(Instruction::EndSend)), - "CLEANUP_THROW should not inline END_SEND directly, got ops={ops:?}" + matches!( + normal_tail[format_idx + 1].op, + Instruction::LoadFastBorrow { .. } + ), + "try/except/else format argument should stay borrowed like CPython, got tail={normal_tail:?}", ); } #[test] - fn test_try_except_falls_through_to_post_handler_code() { + fn test_nested_try_except_common_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(): +def f(value): + address = Address() try: - line = 2 - raise KeyError - except: - line = 5 - line = 6 + token, value = get_group(value) + except HeaderParseError: + try: + token, value = get_mailbox(value) + except HeaderParseError: + raise HeaderParseError('expected {}'.format(value)) + address.append(token) + return address, value ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -16013,47 +17793,55 @@ def f(): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let first_pop_except = ops - .iter() - .position(|op| matches!(op, Instruction::PopExcept)) - .expect("missing POP_EXCEPT"); - assert!( - !matches!( - ops.get(first_pop_except + 1), - Some(Instruction::JumpForward { .. }) - ), - "expected except body to fall through to post-handler code, got ops={ops:?}" - ); assert!( - matches!( - ops.get(first_pop_except + 1), - Some(Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }) - ), - "expected line-after-except code immediately after POP_EXCEPT, got ops={ops:?}" + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFast { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::LoadFastLoadFast { .. }, + ] + ) + }), + "nested try/except common tail should use CPython-style strong loads, got ops={ops:?}" ); } #[test] - fn test_named_except_cleanup_keeps_jump_over_cleanup_and_next_try() { + fn test_nested_try_except_branch_tail_with_following_try_uses_strong_loads() { let code = compile_exec( r#" -def f(self): +def f(value): + msg_id = MsgID() try: - assert 0, 'msg' - except AssertionError as e: - self.assertEqual(e.args[0], 'msg') - else: - self.fail("AssertionError not raised by assert 0") - + token, value = get_dot_atom_text(value) + except HeaderParseError: + try: + token, value = get_obs_local_part(value) + msg_id.defects.append(ObsoleteHeaderDefect("obsolete id-left in msg-id")) + except HeaderParseError: + raise HeaderParseError("expected {}".format(value)) + msg_id.append(token) + if not value or value[0] != "@": + msg_id.defects.append(InvalidHeaderDefect("msg-id with no id-right")) + if value and value[0] == ">": + msg_id.append(ValueTerminal(">", "msg-id-end")) + value = value[1:] + return msg_id, value + msg_id.append(ValueTerminal("@", "address-at-symbol")) + value = value[1:] try: - assert False - except AssertionError as e: - self.assertEqual(len(e.args), 0) - else: - self.fail("AssertionError not raised by 'assert False'") + token, value = get_dot_atom_text(value) + except HeaderParseError: + pass + return msg_id, value "#, ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -16061,41 +17849,62 @@ def f(self): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let first_pop_except = ops - .iter() - .position(|op| matches!(op, Instruction::PopExcept)) - .expect("missing POP_EXCEPT"); - let window = &ops[first_pop_except..(first_pop_except + 6).min(ops.len())]; assert!( - matches!( - window, - [ - Instruction::PopExcept, - Instruction::LoadConst { .. }, - Instruction::StoreName { .. } | Instruction::StoreFast { .. }, - Instruction::DeleteName { .. } | Instruction::DeleteFast { .. }, - Instruction::JumpForward { .. }, - .. - ] - ), - "expected named except cleanup to jump over cleanup reraise block, got ops={window:?}" + ops.windows(16).any(|window| { + matches!( + window, + [ + Instruction::ReturnValue, + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadGlobal { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::LoadFast { .. }, + Instruction::LoadConst { .. }, + Instruction::BinaryOp { .. }, + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadGlobal { .. }, + Instruction::LoadFast { .. }, + ] + ) + }), + "nested try branch tail before a following try should use CPython-style strong loads, got ops={ops:?}" ); } #[test] - fn test_bare_except_deopts_post_handler_load_fast_borrow() { + fn test_nested_try_store_subscr_following_try_tail_uses_strong_loads() { let code = compile_exec( - "\ -def f(self): + r#" +def f(value): + local_part = LocalPart() try: - 1 / 0 - except: - pass - with self.assertRaises(SyntaxError): - pass -", + token, value = get_dot_atom(value) + except HeaderParseError: + try: + token, value = get_word(value) + except HeaderParseError: + token = TokenList() + if value: + obs_local_part, value = get_obs_local_part(str(local_part) + value) + if obs_local_part.token_type == "invalid-obs-local-part": + local_part.defects.append(InvalidHeaderDefect("invalid")) + else: + local_part.defects.append(ObsoleteHeaderDefect("obsolete")) + local_part[0] = obs_local_part + try: + local_part.value.encode("ascii") + except UnicodeEncodeError: + local_part.defects.append(NonASCIILocalPartDefect("non-ascii")) + return local_part, value +"#, ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -16103,30 +17912,53 @@ def f(self): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let attr_idx = ops - .iter() - .position(|op| matches!(op, Instruction::LoadAttr { .. })) - .expect("missing LOAD_ATTR for assertRaises"); assert!( - matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })), - "bare except tail should deopt self to LOAD_FAST, got ops={ops:?}" + ops.windows(10).any(|window| { + matches!( + window, + [ + Instruction::StoreSubscr, + Instruction::Nop, + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::LoadFastLoadFast { .. }, + Instruction::BuildTuple { .. }, + ] + ) + }), + "nested try STORE_SUBSCR tail before a following try should use CPython-style strong loads, got ops={ops:?}" ); } #[test] - fn test_typed_except_keeps_post_handler_load_fast_borrow() { + fn test_resuming_except_in_loop_keeps_post_try_store_tail_borrowed() { let code = compile_exec( "\ -def f(self): - try: - 1 / 0 - except ZeroDivisionError: - pass - with self.assertRaises(SyntaxError): - pass -", - ); - let f = find_code(&code, "f").expect("missing f code"); +def f(part, lines, maxlen, encoding): + for name, value in part.params: + charset = encoding + error_handler = 'strict' + try: + value.encode(encoding) + encoding_required = False + except UnicodeEncodeError: + encoding_required = True + charset = 'utf-8' + if encoding_required: + encoded_value = quote(value, safe='', errors=error_handler) + tstr = \"{}*={}''{}\".format(name, charset, encoded_value) + else: + tstr = '{}={}'.format(name, quote_string(value)) + if len(lines[-1]) + len(tstr) + 1 < maxlen: + lines[-1] = lines[-1] + ' ' + tstr + continue +", + ); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -16134,34 +17966,57 @@ def f(self): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let attr_idx = ops - .iter() - .position(|op| matches!(op, Instruction::LoadAttr { .. })) - .expect("missing LOAD_ATTR for assertRaises"); assert!( - matches!( - ops.get(attr_idx - 1), - Some(Instruction::LoadFastBorrow { .. }) - ), - "typed except tail should keep LOAD_FAST_BORROW, got ops={ops:?}" + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }, + ] + ) + }), + "resuming except handler should keep CPython-style borrowed bool guard, got ops={ops:?}" + ); + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreSubscr, + ] + ) + }), + "resuming except handler should not deopt the post-try STORE_SUBSCR tail, got ops={ops:?}" ); } #[test] - fn test_reraising_typed_except_deopts_post_handler_loads() { + fn test_handler_resume_loop_latch_method_call_uses_strong_loads() { let code = compile_exec( "\ -def f(x, os, self, pid, exitcode): - try: - y = 1 - except RuntimeError: - raise - if x: - os._exit(exitcode) - self.wait_impl(pid, exitcode=exitcode) +def f(phrase, value): + while value and value[0] not in PHRASE_ENDS: + if value[0] == '.': + phrase.append(DOT) + phrase.defects.append(ObsoleteHeaderDefect('period in phrase')) + value = value[1:] + else: + try: + token, value = get_word(value) + except HeaderParseError: + if value[0] in CFWS_LEADER: + token, value = get_cfws(value) + phrase.defects.append(ObsoleteHeaderDefect('comment found without atom')) + else: + raise + phrase.append(token) ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -16169,309 +18024,305 @@ def f(x, os, self, pid, exitcode): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let guard_idx = ops - .iter() - .position(|op| matches!(op, Instruction::ToBool)) - .and_then(|idx| idx.checked_sub(1)) - .expect("missing post-handler bool guard"); - assert!( - matches!(ops.get(guard_idx), Some(Instruction::LoadFast { .. })), - "reraising typed except tail should deopt guard load, got ops={ops:?}" - ); - - let wait_idx = ops - .iter() - .position(|op| matches!(op, Instruction::CallKw { .. })) - .expect("missing wait_impl CALL_KW"); - let call_args = &ops[wait_idx.saturating_sub(3)..wait_idx]; - assert!( - call_args.iter().any(|op| matches!( - op, - Instruction::LoadFastLoadFast { .. } | Instruction::LoadFast { .. } - )), - "reraising typed except tail should keep strong fast loads for call args, got ops={ops:?}" - ); assert!( - !call_args.iter().any(|op| matches!( - op, - Instruction::LoadFastBorrowLoadFastBorrow { .. } - | Instruction::LoadFastBorrow { .. } - )), - "reraising typed except tail should not borrow call args, got ops={ops:?}" + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFast { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::JumpBackward { .. }, + ] + ) + }) || ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFast { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::JumpBackward { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "exception-handler resume loop latch should match CPython's strong method-call loads, got ops={ops:?}" ); } #[test] - fn test_reraising_except_loop_backedge_keeps_loop_header_borrow() { + fn test_single_handler_multiple_resume_branches_keep_post_try_tail_borrowed() { let code = compile_exec( "\ -def f(self, tag, expect_bye): - while 1: - result = self.tagged_commands[tag] - if result is not None: - del self.tagged_commands[tag] - return result - if expect_bye: - typ = 'BYE' - bye = self.untagged_responses.pop(typ, None) - if bye is not None: - return (typ, bye) - self._check_bye() +def f(part, lines, maxlen, encoding): + for name, value in part.params: + charset = encoding + error_handler = 'strict' try: - self._get_response() - except self.abort as val: - if __debug__: - if self.debug >= 1: - self.print_log() - raise + value.encode(encoding) + encoding_required = False + except UnicodeEncodeError: + encoding_required = True + if utils._has_surrogates(value): + charset = 'unknown-8bit' + error_handler = 'surrogateescape' + else: + charset = 'utf-8' + if encoding_required: + encoded_value = quote(value, safe='', errors=error_handler) + tstr = \"{}*={}''{}\".format(name, charset, encoded_value) + else: + tstr = '{}={}'.format(name, quote_string(value)) + if len(lines[-1]) + len(tstr) + 1 < maxlen: + lines[-1] = lines[-1] + ' ' + tstr + continue ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions - .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let warm_ops: Vec<_> = instructions[..handler_start] .iter() .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); assert!( - warm_ops.iter().any(|op| matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - )), - "expected loop body before reraising handler to keep borrowed loads, got ops={warm_ops:?}" + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }, + ] + ) + }), + "single except handler with multiple resume branches should keep the post-try bool guard borrowed, got ops={ops:?}" ); assert!( - warm_ops - .iter() - .all(|op| !matches!(op, Instruction::LoadFast { .. })), - "loop backedge into reraising handler should not deopt warm loop loads, got ops={warm_ops:?}" + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreSubscr, + ] + ) + }), + "single except handler with multiple resume branches should keep the post-try STORE_SUBSCR tail borrowed, got ops={ops:?}" ); } #[test] - fn test_protected_store_break_handler_deopts_bool_guard_tail() { + fn test_nested_exception_handler_resume_update_tail_uses_strong_load() { let code = compile_exec( "\ -def f(self, size): - parts = [] - while size > 0: +def f(inpos, size, g, replacement): + while inpos < size: try: - buf = self.sock.recv(DEFAULT_BUFFER_SIZE) - except ConnectionError: - break - if not buf: - break - self._readbuf.append(buf) - size -= len(buf) - return b''.join(parts) + g() + except KeyError: + try: + for y in replacement: + g(y) + except KeyError: + raise ValueError(inpos) + inpos += 1 + return inpos ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let guard_bool = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::ToBool)) - .expect("missing bool guard"); - let store_buf = instructions[..guard_bool] - .iter() - .rposition(|unit| matches!(unit.op, Instruction::StoreFast { .. })) - .expect("missing protected STORE_FAST before bool guard"); - let guard_load = instructions[store_buf + 1].op; - let append_call = instructions[store_buf + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::Call { .. })) - .map(|idx| idx + store_buf + 1) - .expect("missing append call"); - let append_arg = instructions[append_call - 1].op; + let arg = |unit: &&bytecode::CodeUnit| OpArg::new(u32::from(u8::from(unit.arg))); + let is_inpos_load = |unit: &&bytecode::CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + f.varnames[usize::from(var_num.get(arg(unit)))].as_str() == "inpos" + } + _ => false, + }; + let is_inpos_store = |unit: &&bytecode::CodeUnit| match unit.op { + Instruction::StoreFast { var_num } => { + f.varnames[usize::from(var_num.get(arg(unit)))].as_str() == "inpos" + } + _ => false, + }; + let update = ops + .windows(4) + .find(|window| { + is_inpos_load(&window[0]) + && matches!( + window[1].op, + Instruction::LoadSmallInt { i } if i.get(arg(&window[1])) == 1 + ) + && matches!( + window[2].op, + Instruction::BinaryOp { op } + if op.get(arg(&window[2])) == BinaryOperator::InplaceAdd + ) + && is_inpos_store(&window[3]) + }) + .expect("missing inpos += 1 update"); assert!( - matches!(guard_load, Instruction::LoadFast { .. }), - "CPython uses strong LOAD_FAST for protected-store break guard, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() - ); - assert!( - matches!(append_arg, Instruction::LoadFast { .. }), - "CPython uses strong LOAD_FAST for protected-store append arg, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() + matches!(update[0].op, Instruction::LoadFast { .. }), + "CPython keeps a strong LOAD_FAST for nested-handler resumed inplace update, got update={update:?}" ); } #[test] - fn test_assertion_success_join_deopts_following_debug_tail() { + fn test_protected_store_finally_cleanup_keeps_borrow_tail() { let code = compile_exec( "\ -def f(self, typ, dat): - if self._idle_capture: - if self._idle_responses: - response = self._idle_responses[-1] - assert response[0] == typ - response[1].append(dat) - else: - self._idle_responses.append((typ, [dat])) - if self.debug >= 5: - self._mesg(f'idle: queue untagged {typ} {dat!r}') - return +def f(re, f): + try: + try: + m = re.search('x', f.read()) + finally: + f.close() + if m is not None: + return m.group(1) + except OSError: + pass + return None ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let debug_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "debug" - } - _ => false, - }) - .expect("missing debug LOAD_ATTR"); - let mesg_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_mesg" - } - _ => false, - }) - .expect("missing _mesg LOAD_ATTR"); + let is_m_borrow = |unit: &bytecode::CodeUnit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))].as_str() == "m" + } + _ => false, + }; assert!( - matches!( - instructions[debug_attr - 1].op, - Instruction::LoadFast { .. } - ), - "CPython uses strong LOAD_FAST after assertion success join, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() - ); - assert!( - matches!(instructions[mesg_attr - 1].op, Instruction::LoadFast { .. }), - "CPython uses strong LOAD_FAST in assertion-success debug body, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() + ops.windows(3).any(|window| { + is_m_borrow(window[0]) + && matches!(window[1].op, Instruction::PopJumpIfNone { .. }) + && matches!(window[2].op, Instruction::NotTaken) + }) && ops.windows(2).any(|window| { + is_m_borrow(window[0]) && matches!(window[1].op, Instruction::LoadAttr { .. }) + }), + "finally cleanup RERAISE should not make the outer except deopt the normal m tail, got ops={ops:?}" ); } #[test] - fn test_multi_protected_method_call_terminal_handler_deopts_block() { + fn test_try_else_finally_cleanup_keeps_borrow_tail() { let code = compile_exec( "\ -def f(self, literal): - try: - self.send(literal) - self.send(CRLF) - except OSError as val: - raise self.abort('socket error: %s' % val) +def f(re, open): + global _SYSTEM_VERSION + if _SYSTEM_VERSION is None: + _SYSTEM_VERSION = '' + try: + f = open('/System/Library/CoreServices/SystemVersion.plist', encoding='utf-8') + except OSError: + pass + else: + try: + m = re.search('x', f.read()) + finally: + f.close() + if m is not None: + _SYSTEM_VERSION = '.'.join(m.group(1).split('.')[:2]) + return _SYSTEM_VERSION ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let first_send = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send" - } - _ => false, - }) - .expect("missing send LOAD_ATTR"); - let first_literal = instructions[first_send + 1].op; - let second_send = instructions[first_send + 1..] - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send" - } - _ => false, - }) - .map(|idx| idx + first_send + 1) - .expect("missing second send LOAD_ATTR"); + let is_m_borrow = |unit: &bytecode::CodeUnit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))].as_str() == "m" + } + _ => false, + }; assert!( - matches!( - instructions[first_send - 1].op, - Instruction::LoadFast { .. } - ), - "CPython uses strong LOAD_FAST for first protected send receiver, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() - ); - assert!( - matches!(first_literal, Instruction::LoadFast { .. }), - "CPython uses strong LOAD_FAST for first protected send arg, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() - ); - assert!( - matches!( - instructions[second_send - 1].op, - Instruction::LoadFast { .. } - ), - "CPython uses strong LOAD_FAST for second protected send receiver, got ops={:?}", - instructions.iter().map(|unit| unit.op).collect::>() + ops.windows(3).any(|window| { + is_m_borrow(window[0]) + && matches!(window[1].op, Instruction::PopJumpIfNone { .. }) + && matches!(window[2].op, Instruction::NotTaken) + }) && ops.windows(2).any(|window| { + is_m_borrow(window[0]) && matches!(window[1].op, Instruction::LoadAttr { .. }) + }), + "try/else finally cleanup should keep CPython-style borrowed m tail, got ops={ops:?}" ); } #[test] - fn test_dunder_debug_constant_false_if_deopts_tail_borrow() { + fn test_generator_protected_store_subscr_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self): - if not __debug__: - self.skipTest('need asserts, run without -O') - self.do_disassembly_test() +def f(names, modules): + for name in names: + try: + mod = __import__(name) + except ImportError: + continue + modules[name] = mod + yield mod ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let attr_idx = instructions + let store_subscr = ops .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "do_disassembly_test" - } - _ => false, - }) - .expect("missing LOAD_ATTR for do_disassembly_test"); - let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); + .position(|op| matches!(op, Instruction::StoreSubscr)) + .expect("missing STORE_SUBSCR"); + let window = &ops[store_subscr.saturating_sub(2)..(store_subscr + 3).min(ops.len())]; + assert!( - matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })), - "constant-false __debug__ tail should deopt self to LOAD_FAST, got ops={ops:?}" + matches!( + window, + [ + Instruction::LoadFastLoadFast { .. }, + Instruction::LoadFast { .. }, + Instruction::StoreSubscr, + Instruction::LoadFast { .. }, + Instruction::YieldValue { .. }, + .. + ] + ), + "expected CPython-style strong LOAD_FAST around protected STORE_SUBSCR generator tail, got {window:?}" ); } #[test] - fn test_constant_slice_folds_constant_bounds() { + fn test_protected_call_function_ex_store_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(obj): - return obj['a':123456789012345678901234567890] +def f(func, *args): + try: + result = func(*args) + except Exception: + return None + return type(result) ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -16481,46 +18332,39 @@ def f(obj): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let folded_slice = f - .constants + let tail_call = ops .iter() - .find_map(|constant| match constant { - ConstantData::Slice { elements } => Some(elements), - _ => None, - }) - .expect("missing folded slice constant"); + .rposition(|op| matches!(op, Instruction::Call { .. })) + .expect("missing tail CALL"); + let result_store = ops[..tail_call] + .iter() + .rposition(|op| matches!(op, Instruction::StoreFast { .. })) + .expect("missing protected result STORE_FAST"); + let tail = &ops[result_store + 1..tail_call]; + assert!( - matches!( - folded_slice.as_ref(), - [ - ConstantData::Str { .. }, - ConstantData::Integer { .. }, - ConstantData::None, - ] - ), - "expected folded slice('a', 123456789012345678901234567890, None), got {folded_slice:?}" + tail.iter() + .any(|op| matches!(op, Instruction::LoadFast { .. })), + "expected CPython-style strong LOAD_FAST after protected CALL_FUNCTION_EX store, got ops={ops:?}", ); assert!( - matches!( - ops.as_slice(), - [ - Instruction::Resume { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::LoadConst { .. }, - Instruction::BinaryOp { .. }, - Instruction::ReturnValue, - ] - ), - "expected CPython-style LOAD_CONST(slice(...)) path for constant bounds, got ops={ops:?}" + !tail + .iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "protected CALL_FUNCTION_EX store tail should not borrow result, got ops={ops:?}", ); } #[test] - fn test_negative_step_slice_uses_build_slice() { + fn test_protected_attr_subscript_tail_uses_strong_load_fast() { let code = compile_exec( "\ -def f(obj): - return obj[::-1] +def f(obj, idx): + try: + x = 1 + except ValueError: + return False + return obj.__closure__[idx] ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -16530,34 +18374,34 @@ def f(obj): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let protected_tail = &ops[..handler_start]; assert!( - matches!( - ops.as_slice(), - [ - Instruction::Resume { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::BuildSlice { .. }, - Instruction::BinaryOp { .. }, - Instruction::ReturnValue, - ] - ), - "expected CPython-style BUILD_SLICE path for non-literal negative step, got ops={ops:?}" + !protected_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected protected attr-subscript tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}" ); } #[test] - fn test_bool_int_binop_constants_fold() { + fn test_protected_direct_subscript_tail_uses_strong_load_fast() { let code = compile_exec( "\ -def f(): - return False + 2, True + 2, False + False, True / 1, True & False - -def g(): - return False + 2 +def f(seq): + try: + items = [int(item) for item in seq] + except ValueError: + return None + return items[0] + items[1] ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -16567,45 +18411,38 @@ def g(): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let protected_store = ops[..handler_start] + .iter() + .rposition(|op| matches!(op, Instruction::StoreFast { .. })) + .expect("missing protected local store"); + let tail = &ops[protected_store + 1..handler_start]; assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::BinaryOp { .. })), - "expected CPython-style folded bool/int binops, got ops={ops:?}" - ); - assert!( - matches!( - ops.as_slice(), - [ - Instruction::Resume { .. }, - Instruction::LoadConst { .. }, - Instruction::ReturnValue - ] - ), - "expected folded constants for bool/int binops, got ops={ops:?}" - ); - - let g = find_code(&code, "g").expect("missing function code"); - let g_ops: Vec<_> = g - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - assert!( - !g_ops - .iter() - .any(|op| matches!(op, Instruction::BinaryOp { .. })), - "expected top-level bool/int binop to fold, got ops={g_ops:?}" + !tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected protected direct-subscript tail to keep strong LOAD_FAST ops, got tail={tail:?}" ); } #[test] - fn test_double_not_expression_folds_to_bool_conversion() { + fn test_protected_attr_iter_chain_uses_strong_load_fast() { let code = compile_exec( "\ -def f(x): - return not not x +def f(fields): + try: + x = 1 + except ValueError: + return False + return tuple(v for v in fields.values()) ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -16615,297 +18452,321 @@ def f(x): .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let protected_tail = &ops[..handler_start]; assert!( - matches!( - ops.as_slice(), - [ - Instruction::Resume { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::ToBool, - Instruction::ReturnValue, - ] - ), - "expected CPython-style double-not bool conversion, got ops={ops:?}" + !protected_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected protected attr-iter chain to keep strong LOAD_FAST ops, got tail={protected_tail:?}" ); } #[test] - fn test_tuple_bound_slice_uses_two_part_slice_path() { + fn test_generator_except_return_handler_deopts_normal_tail_borrows() { let code = compile_exec( "\ -def f(obj): - return obj[(1, 2):] +def f(fields): + try: + x = 1 + except ValueError: + return + for fielddesc in fields: + yield fielddesc ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[..handler_start]; assert!( - matches!( - ops.as_slice(), - [ - Instruction::Resume { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::BinarySlice, - Instruction::ReturnValue, - ] - ), - "expected CPython-style BINARY_SLICE path for tuple lower bound, got ops={ops:?}" - ); - } - - #[test] - fn test_exception_cleanup_jump_to_return_is_inlined() { - let code = compile_exec( - "\ -def f(names, cls): - try: - cls.attr = names - except: - pass - return names -", + normal_tail + .iter() + .any(|op| matches!(op, Instruction::LoadFast { .. })), + "generator tail after non-yielding except return should keep CPython-style strong LOAD_FAST, got tail={normal_tail:?}" ); - let f = find_code(&code, "f").expect("missing function code"); - let return_count = f - .instructions - .iter() - .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) - .count(); - - assert_eq!( - return_count, 2, - "expected CPython-style distinct return sites for normal and except paths" + assert!( + !normal_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "generator tail after non-yielding except return should not borrow, got tail={normal_tail:?}" ); } #[test] - fn test_nested_with_bare_except_keeps_handler_cleanup_before_following_code() { + fn test_generator_except_yielding_handler_keeps_normal_tail_borrows() { let code = compile_exec( "\ -def f(cm, self): +def f(tp, parent=None): try: - with cm: - raise Exception - except: - pass - self.g() + fields = tp._fields_ + except AttributeError: + yield parent + else: + for fielddesc in fields: + yield fielddesc ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - - let outer_handler = ops + let handler_start = ops .iter() - .enumerate() - .filter_map(|(idx, op)| matches!(op, Instruction::PushExcInfo).then_some(idx)) - .next_back() - .expect("missing outer handler"); + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &ops[..handler_start]; + assert!( - ops[outer_handler..].windows(6).any(|window| { + normal_tail.iter().any(|op| { matches!( - window, - [ - Instruction::PopExcept, - Instruction::JumpForward { .. }, - Instruction::Copy { .. }, - Instruction::PopExcept, - Instruction::Reraise { .. }, - Instruction::LoadFast { .. }, - ] + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) }), - "expected CPython-style handler cleanup before following code, got ops={ops:?}" + "generator tail after yielding except handler should keep CPython-style borrows, got tail={normal_tail:?}" ); } #[test] - fn test_try_else_for_cleanup_drops_redundant_jump_nop() { + fn test_generator_returning_except_keeps_yield_from_resume_tail_borrow() { let code = compile_exec( "\ -def f(self, xs, ys, cm1, cm2): - for x in xs: - with self.subTest(x=x): - try: - with cm1: - self.a() - except Exception: - if x: - pass - else: - raise - else: - for y in ys: - with self.subTest(y=y): - with cm2: - self.b() +def f(self, action): + try: + get_subactions = action._get_subactions + except AttributeError: + pass + else: + self._indent() + yield from get_subactions() + self._dedent() ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let end_send = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::EndSend)) + .expect("missing END_SEND"); + let dedent_attr = instructions[end_send..] + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_dedent" + } + _ => false, + }) + .map(|idx| end_send + idx) + .expect("missing _dedent LOAD_ATTR"); assert!( - ops.windows(7).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::PopTop, - ] - ) - }), - "expected inner for cleanup to fall directly into surrounding with cleanup, got ops={ops:?}", - ); - assert!( - !ops.windows(8).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::Nop, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::PopTop, - ] - ) - }), - "expected CPython-style removal of the redundant jump NOP after for cleanup, got ops={ops:?}", + matches!( + instructions[dedent_attr - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "CPython keeps yield-from resume receiver borrowed after END_SEND, got instructions={instructions:?}" ); } #[test] - fn test_non_none_final_return_is_not_duplicated() { + fn test_generator_except_pass_resume_tail_keeps_borrows() { let code = compile_exec( "\ -def f(p, s): - if p == '': - if s == '': - return 0 - return -1 -", - ); - let f = find_code(&code, "f").expect("missing function code"); - let minus_one_loads = f - .instructions - .iter() - .filter(|unit| { - matches!( - unit.op, - Instruction::LoadConst { consti } - if matches!( - f.constants.get( - consti - .get(OpArg::new(u32::from(u8::from(unit.arg)))) - .as_usize() - ), - Some(ConstantData::Integer { value }) if value == &BigInt::from(-1) - ) - ) +def f(self, msg): + if self.log_queue is not None: + yield + output = [] + try: + while True: + output.append(self.log_queue.get_nowait().getMessage()) + except queue.Empty: + pass + else: + with self.assertLogs('concurrent.futures', 'CRITICAL') as cm: + yield + output = cm.output + self.assertTrue(any(msg in line for line in output), output) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + let has_strong_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, }) - .count(); + }; + let has_borrow_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; - assert_eq!( - minus_one_loads, - 1, - "expected a single final return -1 epilogue, got ops={:?}", - f.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); + for name in ["msg", "output"] { + assert!( + has_borrow_load(name), + "generator except-pass resume tail should borrow {name}, got instructions={:?}", + f.instructions + ); + assert!( + !has_strong_load(name), + "generator except-pass resume tail should not force strong LOAD_FAST for {name}, got instructions={:?}", + f.instructions + ); + } } #[test] - fn test_try_else_if_return_keeps_conditional_target_nop() { + fn test_async_for_cleanup_resume_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(cond): - try: - x = cond - except E: - pass - else: - if x: - return 1 - return 2 +async def f(g, self, x): + async for val in g: + break + self.x(x) + await g.aclose() ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); + let aclose_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "aclose" + } + _ => false, + }) + .expect("missing aclose load"); - let has_cpython_nop_target = ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, - Instruction::ReturnValue, - Instruction::Nop, - Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, - Instruction::ReturnValue, - ] - ) - }); - let has_direct_fallthrough = ops.windows(4).any(|window| { + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFast { .. }, + Instruction::Call { .. }, + ] + ) + }), + "async-for cleanup resume tail should use strong LOAD_FAST ops before the await, got ops={ops:?}" + ); + assert!( matches!( - window, - [ - Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, - Instruction::ReturnValue, - Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, - Instruction::ReturnValue, - ] - ) - }); + instructions + .get(aclose_idx.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFast { .. }) + ), + "async-for cleanup resume tail should keep g strong before aclose, got ops={ops:?}" + ); + } + + #[test] + fn test_async_generator_async_with_yield_keeps_borrow() { + let code = compile_exec( + "\ +async def f(self, my_cm): + async with self.exit_stack() as stack: + await stack.enter_async_context(my_cm()) + yield stack +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); + let wrap_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::CallIntrinsic1 { func } => { + func.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == IntrinsicFunction1::AsyncGenWrap + } + _ => false, + }) + .expect("missing async generator wrap"); + assert!( - has_cpython_nop_target || has_direct_fallthrough, - "expected adjacent try-else return and final return targets, got ops={ops:?}" + matches!( + ops.get(wrap_idx.saturating_sub(1)), + Some(Instruction::LoadFastBorrow { .. }) + ), + "async generator yield inside async-with should borrow stack like CPython, got ops={ops:?}" ); } #[test] - fn test_named_except_conditional_branch_duplicates_cleanup_return() { + fn test_deoptimized_async_with_enter_continuation_uses_strong_loads() { let code = compile_exec( "\ -def f(self): +async def f(): + async def cm(): + pass try: - raise TypeError('x') - except TypeError as e: - if '+' not in str(e): - self.fail('join() ate exception message') + async with cm(): + 1 / 0 + except ZeroDivisionError as e: + frames = e + class E(RuntimeError): + pass + try: + async with cm(): + raise E(42) + except E as e: + frames = e ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() @@ -16913,76 +18774,71 @@ def f(self): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let cleanup_return_count = ops - .windows(6) - .filter(|window| { + assert!( + ops.windows(5).any(|window| { matches!( window, [ - Instruction::PopExcept, - Instruction::LoadConst { .. }, - Instruction::StoreFast { .. } | Instruction::StoreName { .. }, - Instruction::DeleteFast { .. } | Instruction::DeleteName { .. }, - Instruction::LoadConst { .. }, - Instruction::ReturnValue, + Instruction::LoadFast { .. }, + Instruction::PushNull, + Instruction::LoadSmallInt { .. }, + Instruction::Call { .. }, + Instruction::RaiseVarargs { .. }, ] ) - }) - .count(); - - assert_eq!( - cleanup_return_count, 2, - "expected duplicated named-except cleanup return blocks, got ops={ops:?}" + }), + "async-with enter continuation after a deoptimized setup block should keep raised class strong, got ops={ops:?}" ); } #[test] - fn test_listcomp_cleanup_tail_keeps_split_store_fast_pair() { + fn test_async_with_bare_raise_continuation_keeps_borrow() { let code = compile_exec( "\ -def f(escaped_string, quote_types): - possible_quotes = [q for q in quote_types if q not in escaped_string] - return possible_quotes +async def f(tg): + class E(Exception): + pass + try: + async with tg: + raise E + except ExceptionGroup: + pass ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - - let pop_iter_idx = ops + let raise_idx = ops .iter() - .position(|op| matches!(op, Instruction::PopIter)) - .expect("missing POP_ITER"); - let tail = &ops[pop_iter_idx + 1..]; + .position(|op| matches!(op, Instruction::RaiseVarargs { .. })) + .expect("missing raise"); assert!( matches!( - tail, - [ - Instruction::StoreFast { .. }, - Instruction::StoreFast { .. }, - Instruction::LoadFastBorrow { .. }, - Instruction::ReturnValue, - .. - ] + ops.get(raise_idx.saturating_sub(1)), + Some(Instruction::LoadFastBorrow { .. }) ), - "expected split STORE_FAST pair after listcomp cleanup, got ops={ops:?}" + "bare async-with raise continuation should keep the raised class borrowed like CPython, got ops={ops:?}" ); } #[test] - fn test_dictcomp_cleanup_tail_keeps_split_store_fast_pair() { + fn test_except_star_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(obj, g): - return {g(k): g(v) for k, v in obj.items()} +def f(self): + try: + pass + except* ValueError: + pass + self.fail('x') ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() @@ -16990,69 +18846,98 @@ def f(obj, g): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let pop_iter_idx = ops - .iter() - .position(|op| matches!(op, Instruction::PopIter)) - .expect("missing POP_ITER"); - let tail = &ops[pop_iter_idx + 1..]; - assert!( - matches!( - tail, - [ - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - Instruction::StoreFast { .. }, - Instruction::ReturnValue, - .. - ] - ), - "expected split STORE_FAST pair after dictcomp cleanup, got ops={ops:?}" + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "except* tail should use strong LOAD_FAST like CPython, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "except* tail should not borrow the receiver after the handler region, got ops={ops:?}" ); } #[test] - fn test_static_swap_triple_assign_keeps_store_fast_store_fast() { + fn test_protected_attr_subscript_store_tail_uses_strong_load_fast() { let code = compile_exec( "\ -def f(x, y, z): - a, b, a = x, y, z - return a +def f(f, oldcls, newcls): + try: + idx = f.__code__.co_freevars.index('__class__') + except ValueError: + return False + closure = f.__closure__[idx] + if closure.cell_contents is oldcls: + closure.cell_contents = newcls + return True + return False ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let protected_tail = &ops[..handler_start]; + let store_closure_idx = protected_tail + .windows(2) + .position(|window| { + matches!( + window, + [Instruction::BinaryOp { .. }, Instruction::StoreFast { .. }] + ) + }) + .map(|idx| idx + 1) + .expect("missing STORE_FAST for closure"); + let post_store_tail = &protected_tail[store_closure_idx + 1..]; assert!( - ops.windows(3).any(|window| { + !post_store_tail.iter().any(|op| { matches!( - window, - [ - Instruction::Swap { .. }, - Instruction::StoreFastStoreFast { .. }, - Instruction::StoreFast { .. } - ] + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) }), - "expected CPython-style SWAP/STORE_FAST_STORE_FAST/STORE_FAST sequence, got ops={ops:?}" + "expected protected attr-subscript store tail to keep strong LOAD_FAST ops, got tail={post_store_tail:?}" ); } #[test] - fn test_static_swap_duplicate_pair_eliminates_swap() { + fn test_plain_attr_subscript_tail_keeps_borrow() { let code = compile_exec( "\ -def f(x, y): - a, a = x, y - return a +def f(self, name): + annotations = self.method_annotations[name] + return annotations ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() @@ -17061,27 +18946,30 @@ def f(x, y): .collect(); assert!( - !ops.iter().any(|op| matches!(op, Instruction::Swap { .. })), - "duplicate pair assignment should statically eliminate SWAP, got ops={ops:?}" - ); - assert!( - ops.windows(2).any(|window| { - matches!(window, [Instruction::StoreFast { .. }, Instruction::PopTop]) + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::BinaryOp { .. }, + ] + ) }), - "expected CPython-style STORE_FAST/POP_TOP duplicate assignment, got ops={ops:?}" + "expected plain attr-subscript tail to keep borrowed receiver/index loads, got ops={ops:?}" ); } #[test] - fn test_static_swap_duplicate_prefix_eliminates_swap() { + fn test_plain_attr_iter_chain_keeps_borrow() { let code = compile_exec( "\ -def f(x, y, z): - a, a, b = x, y, z - return a +def f(fields): + return tuple(v for v in fields.values()) ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() @@ -17090,1094 +18978,1206 @@ def f(x, y, z): .collect(); assert!( - !ops.iter().any(|op| matches!(op, Instruction::Swap { .. })), - "duplicate-prefix assignment should statically eliminate SWAP, got ops={ops:?}" - ); - assert!( - ops.windows(2).any(|window| { + ops.windows(4).any(|window| { matches!( window, - [Instruction::StoreFastStoreFast { .. }, Instruction::PopTop] + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::Call { .. }, + Instruction::GetIter, + ] ) }), - "expected CPython-style STORE_FAST_STORE_FAST/POP_TOP duplicate prefix, got ops={ops:?}" + "expected plain attr-iter chain to keep borrowed receiver, got ops={ops:?}" ); } #[test] - fn test_constant_if_expression_stmt_in_loop_removes_empty_body() { + fn test_genexpr_true_filter_omits_bool_scaffolding() { let code = compile_exec( "\ -def f(x): - while x: - 0 if 1 else 0 +def f(it): + return (x for x in it if True) ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - + let genexpr = find_code(&code, "").expect("missing code"); assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::LoadSmallInt { .. })), - "expected constant if-expression statement to compile away inside loop, got ops={ops:?}" + !genexpr.instructions.iter().any(|unit| { + matches!(unit.op, Instruction::LoadConst { .. }) + && matches!( + genexpr.constants.get(usize::from(u8::from(unit.arg))), + Some(ConstantData::Boolean { value: true }) + ) + }), + "constant-true filter should not load True, got ops={:?}", + genexpr + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + !genexpr + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })), + "constant-true filter should not leave POP_JUMP_IF_TRUE scaffolding, got ops={:?}", + genexpr + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); } #[test] - fn test_if_expression_in_jump_context_skips_constant_true_arm_load() { + fn test_classdictcell_uses_load_closure_path_and_borrows_after_optimize() { let code = compile_exec( "\ -def f(): - a if (1 if b else c) else d +class C: + def method(self): + return 1 ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let class_code = find_code(&code, "C").expect("missing class code"); + let store_classdictcell = class_code .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + .position(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__classdictcell__" + ) + }) + .expect("missing STORE_NAME __classdictcell__"); assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::LoadSmallInt { .. })), - "expected jump-context if-expression to avoid materializing constant truthy arm, got ops={ops:?}" + matches!( + class_code + .instructions + .get(store_classdictcell.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "expected LOAD_FAST_BORROW before __classdictcell__ store, got ops={:?}", + class_code + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); } #[test] - fn test_with_suppress_tail_duplicates_final_return_none() { + fn test_conditional_class_body_duplicates_no_location_exit_tail() { let code = compile_exec( "\ -def f(cm, cond): - if cond: - with cm(): - pass +flag = False +class C: + if flag: + value = 1 ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let class_code = find_code(&code, "C").expect("missing class code"); + let ops: Vec<_> = class_code .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let return_count = ops .iter() .filter(|op| matches!(op, Instruction::ReturnValue)) .count(); + let static_attrs_count = class_code + .instructions + .iter() + .filter(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__static_attributes__" + ) + }) + .count(); assert_eq!( - return_count, 3, - "expected duplicated return-none epilogues, got ops={ops:?}" + return_count, 2, + "conditional class body should duplicate CPython no-location return tail, got ops={ops:?}" ); - assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::JumpBackwardNoInterrupt { .. })), - "with suppress tail should not jump back to shared return block, got ops={ops:?}" + assert_eq!( + static_attrs_count, 2, + "conditional class body should duplicate __static_attributes__ tail, got ops={ops:?}" ); } #[test] - fn test_with_conditional_bare_return_keeps_return_line_nop_before_exit_cleanup() { + fn test_class_lambda_assignment_does_not_create_classdictcell() { let code = compile_exec( "\ -def f(cm, registry, altkey): - with cm: - if registry.get(altkey): - return - registry[altkey] = 1 +class C: + data = start = end = lambda *a: None ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + let class_code = find_code(&code, "C").expect("missing class code"); assert!( - ops.windows(8).any(|window| { + !class_code.instructions.iter().any(|unit| { matches!( - window, - [ - Instruction::Nop, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::PopTop, - Instruction::LoadConst { .. }, - Instruction::ReturnValue, - ] + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__classdictcell__" ) }), - "expected CPython-style return-line NOP before with-exit cleanup return, got ops={ops:?}" + "lambda-only class should not create __classdictcell__, got ops={:?}", + class_code + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); } #[test] - fn test_try_finally_conditional_return_duplicates_finally_exit_return() { + fn test_nested_function_static_attributes_are_collected() { let code = compile_exec( "\ -def f(flag, data, callback): - try: - if flag: - return - value = 1 - finally: - if data: - callback(data) +class C: + def f(self): + self.x = 1 + self.y = 2 + self.x = 3 + + def g(self, obj): + self.y = 4 + self.z = 5 + + def h(self, a): + self.u = 6 + self.v = 7 + + obj.self = 8 ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + let class_code = find_code(&code, "C").expect("missing class code"); - let return_count = ops - .iter() - .filter(|op| matches!(op, Instruction::ReturnValue)) - .count(); - assert_eq!( - return_count, 4, - "try-finally return unwind should keep CPython-style distinct true/false finalbody exits, got ops={ops:?}" + assert!( + class_code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements + == &[ + ConstantData::Str { value: "u".into() }, + ConstantData::Str { value: "v".into() }, + ConstantData::Str { value: "x".into() }, + ConstantData::Str { value: "y".into() }, + ConstantData::Str { value: "z".into() }, + ] + )), + "expected nested function static attributes in class consts" ); } #[test] - fn test_named_except_conditional_cleanup_is_inlined_per_branch() { + fn test_static_attributes_match_cpython_store_rule() { let code = compile_exec( "\ -def f(self, logger): - try: - work() - except A as exc: - if not self.closing: - self.fatal(exc, 'msg') - elif self.loop.get_debug(): - logger.debug('closing', exc_info=True) - finally: - if self.length > -1: - self.recv() +class C: + @staticmethod + def f(): + self.x = 1 + + @classmethod + def g(cls): + self.y = 2 + + def h(obj): + obj.z = 3 + tarinfo.uid = 4 + + def i(self): + self.a: int + self.b: int = 1 + self.c += 1 + del self.d ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + let class_code = find_code(&code, "C").expect("missing class code"); - let cleanup_after_branch_count = ops - .windows(6) - .filter(|window| { - matches!( - window, - [ - Instruction::PopTop, - Instruction::PopExcept, - Instruction::LoadConst { .. }, - Instruction::StoreFast { .. }, - Instruction::DeleteFast { .. }, - Instruction::JumpBackwardNoInterrupt { .. }, - ] - ) - }) - .count(); - assert_eq!( - cleanup_after_branch_count, 2, - "named except branch exits should inline cleanup like CPython, got ops={ops:?}" + assert!( + class_code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements + == &[ + ConstantData::Str { value: "b".into() }, + ConstantData::Str { value: "x".into() }, + ConstantData::Str { value: "y".into() }, + ] + )), + "expected only CPython-collected static attributes in class consts" ); } #[test] - fn test_try_finally_exception_path_duplicates_conditional_reraise() { + fn test_decorated_class_uses_first_decorator_for_firstlineno() { let code = compile_exec( "\ -def f(flag, callback): - try: - work() - finally: - if flag: - callback() +@dec1 +@dec2 +class C: + pass ", ); - let f = find_code(&code, "f").expect("missing function code"); - let ops: Vec<_> = f + let class_code = find_code(&code, "C").expect("missing class code"); + let store_firstlineno = class_code .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + .position(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__firstlineno__" + ) + }) + .expect("missing STORE_NAME __firstlineno__"); + let load_firstlineno = class_code + .instructions + .get(store_firstlineno.saturating_sub(1)) + .expect("missing LOAD_CONST for __firstlineno__"); - let reraise_count = ops - .iter() - .filter(|op| matches!(op, Instruction::Reraise { .. })) - .count(); - assert_eq!( - reraise_count, 3, - "try-finally exception finalbody should duplicate CPython no-location RERAISE exits, got ops={ops:?}" + let expected = ConstantData::Integer { + value: BigInt::from(1), + }; + assert!( + matches!( + load_firstlineno.op, + Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. } + ), + "expected LOAD_SMALL_INT/LOAD_CONST before __firstlineno__, got {:?}", + load_firstlineno.op ); + if let Instruction::LoadConst { consti } = load_firstlineno.op { + let value = &class_code.constants + [consti.get(OpArg::new(u32::from(u8::from(load_firstlineno.arg))))]; + assert_eq!(value, &expected); + } else { + assert_eq!(u32::from(u8::from(load_firstlineno.arg)), 1); + } } #[test] - fn test_genexpr_compare_header_uses_store_fast_load_fast_like_cpython() { + fn test_future_annotations_class_uses_direct_annotation_store() { let code = compile_exec( "\ -def f(it): - return (offset == (4, 10) for offset in it) -", - ); - let genexpr = find_code(&code, "").expect("missing code"); - let ops: Vec<_> = genexpr +from __future__ import annotations +class C: + x: int +", + ); + let class_code = find_code(&code, "C").expect("missing class code"); + + assert!( + !class_code + .cellvars + .iter() + .any(|name| name.as_str() == "__conditional_annotations__"), + "future annotations should not create __conditional_annotations__ cellvar, got cellvars={:?}", + class_code.cellvars + ); + let ops: Vec<_> = class_code .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert!( - ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::StoreFastLoadFast { .. }, - Instruction::LoadConst { .. }, - Instruction::CompareOp { .. }, - ] - ) - }), - "expected CPython-style STORE_FAST_LOAD_FAST compare header, got ops={ops:?}" + ops.iter() + .any(|op| matches!(op, Instruction::SetupAnnotations)), + "future annotations should emit SETUP_ANNOTATIONS, got ops={ops:?}" + ); + assert!( + ops.iter().any(|op| matches!(op, Instruction::StoreSubscr)), + "future annotations should store directly into __annotations__, got ops={ops:?}" + ); + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::BuildSet { .. })), + "future annotations should not initialize __conditional_annotations__, got ops={ops:?}" ); } #[test] - fn test_fstring_adjacent_literals_are_merged() { + fn test_future_annotations_module_keeps_conditional_annotations_cell() { let code = compile_exec( "\ -def f(cls, proto): - raise TypeError( - f\"cannot pickle {cls.__name__!r} object: \" - f\"a class that defines __slots__ without \" - f\"defining __getstate__ cannot be pickled \" - f\"with protocol {proto}\" - ) +from __future__ import annotations +x: int = 1 ", ); - let f = find_code(&code, "f").expect("missing function code"); - let string_consts = f - .instructions - .iter() - .filter_map(|unit| match unit.op { - Instruction::LoadConst { consti } => { - Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))]) - } - _ => None, - }) - .filter_map(|constant| match constant { - ConstantData::Str { value } => Some(value.to_string()), - _ => None, - }) - .collect::>(); assert!( - string_consts.iter().any(|value| { - value - == " object: a class that defines __slots__ without defining __getstate__ cannot be pickled with protocol " - }), - "expected merged trailing f-string literal, got {string_consts:?}" - ); - assert!( - !string_consts.iter().any(|value| value == " object: "), - "did not expect split trailing literal, got {string_consts:?}" + code.cellvars + .iter() + .any(|name| name.as_str() == "__conditional_annotations__"), + "module annotations should create __conditional_annotations__ cellvar, got cellvars={:?}", + code.cellvars ); } #[test] - fn test_literal_only_fstring_statement_is_optimized_away() { + fn test_future_annotations_conditional_class_keeps_conditional_annotations_cell() { let code = compile_exec( "\ -def f(): - f'''Not a docstring''' +from __future__ import annotations +class C: + if True: + x: int = 1 ", ); - let f = find_code(&code, "f").expect("missing function code"); + let class_code = find_code(&code, "C").expect("missing class code"); assert!( - !f.instructions + class_code + .cellvars .iter() - .any(|unit| matches!(unit.op, Instruction::PopTop)), - "literal-only f-string statement should be removed" - ); - assert!( - !f.constants.iter().any(|constant| matches!( - constant, - ConstantData::Str { value } if value.to_string() == "Not a docstring" - )), - "literal-only f-string should not survive in constants" + .any(|name| name.as_str() == "__conditional_annotations__"), + "conditional class annotations should create __conditional_annotations__ cellvar, got cellvars={:?}", + class_code.cellvars ); } #[test] - fn test_empty_fstring_literals_are_elided_around_interpolation() { + fn test_future_annotations_setup_precedes_docstring() { let code = compile_exec( "\ -def f(x): - if '' f'{x}': - return 1 - return 2 +\"module doc\" +from __future__ import annotations +x: int = 1 + +class C: + \"class doc\" + x: int = 1 ", ); - let f = find_code(&code, "f").expect("missing function code"); - - let empty_string_loads = f + let module_setup = code .instructions .iter() - .filter_map(|unit| match unit.op { - Instruction::LoadConst { consti } => { - Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))]) - } - _ => None, - }) - .filter(|constant| { + .position(|unit| matches!(unit.op, Instruction::SetupAnnotations)) + .expect("missing module SETUP_ANNOTATIONS"); + let module_doc = code + .instructions + .iter() + .position(|unit| { matches!( - constant, - ConstantData::Str { value } if value.is_empty() + unit.op, + Instruction::StoreName { namei } + if code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__doc__" ) }) - .count(); - let build_string_count = f - .instructions - .iter() - .filter(|unit| matches!(unit.op, Instruction::BuildString { .. })) - .count(); - - assert_eq!(empty_string_loads, 0); - assert_eq!(build_string_count, 0); - } - - #[test] - fn test_large_fstring_uses_join_list_like_cpython() { - let mut source = String::from("def f(x):\n return f\""); - for _ in 0..=STACK_USE_GUIDELINE { - source.push_str("{x}"); - } - source.push_str("\"\n"); + .expect("missing module doc store"); + assert!( + module_setup < module_doc, + "module SETUP_ANNOTATIONS should precede docstring store, got instructions={:?}", + code.instructions + ); - let code = compile_exec(&source); - let f = find_code(&code, "f").expect("missing function code"); - let build_string_count = f - .instructions - .iter() - .filter(|unit| matches!(unit.op, Instruction::BuildString { .. })) - .count(); - let list_append_count = f + let class_code = find_code(&code, "C").expect("missing class code"); + let class_setup = class_code .instructions .iter() - .filter(|unit| matches!(unit.op, Instruction::ListAppend { .. })) - .count(); - let join_attr_count = f + .position(|unit| matches!(unit.op, Instruction::SetupAnnotations)) + .expect("missing class SETUP_ANNOTATIONS"); + let class_doc = class_code .instructions .iter() - .filter(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - load_attr.is_method() - && f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "join" - } - _ => false, + .position(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__doc__" + ) }) - .count(); - - assert_eq!(build_string_count, 0); - assert_eq!( - list_append_count, - usize::try_from(STACK_USE_GUIDELINE + 1).unwrap() + .expect("missing class doc store"); + assert!( + class_setup < class_doc, + "class SETUP_ANNOTATIONS should precede docstring store, got instructions={:?}", + class_code.instructions ); - assert_eq!(join_attr_count, 1); - } - - #[test] - fn test_large_power_is_not_constant_folded() { - let code = compile_exec("x = 2**100\n"); - - assert!(code.instructions.iter().any(|unit| match unit.op { - Instruction::BinaryOp { op } => { - op.get(OpArg::new(u32::from(u8::from(unit.arg)))) == oparg::BinaryOperator::Power - } - _ => false, - })); } #[test] - fn test_string_and_bytes_binops_constant_fold_like_cpython() { + fn test_plain_super_call_keeps_class_freevar() { let code = compile_exec( "\ -x = b'\\\\' + b'u1881'\n\ -y = 103 * 'a' + 'x'\n", - ); +class A: + pass +class B(A): + def method(self): + return super() +", + ); + let method = find_code(&code, "method").expect("missing method code"); assert!( - !code + method.freevars.iter().any(|name| name == "__class__"), + "plain super() must keep __class__ freevar, got freevars={:?}", + method.freevars + ); + assert!( + method .instructions .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "unexpected runtime BINARY_OP in folded string/bytes constants: {:?}", - code.instructions + .any(|unit| matches!(unit.op, Instruction::CopyFreeVars { .. })), + "plain super() must keep COPY_FREE_VARS prelude, got ops={:?}", + method + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Bytes { value } if value == b"\\u1881" - ))); - let expected = format!("{}x", "a".repeat(103)); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Str { value } - if value.to_string() == expected - ))); } #[test] - fn test_float_floor_division_constant_folds_like_cpython() { + fn test_nested_class_super_does_not_create_outer_class_closure() { let code = compile_exec( "\ -x = 1.0 // 0.1\n\ -y = 1.0 % 0.1\n\ -z = 1e300 * 1e300 * 0\n", +class C: + def outer(self): + class D: + def __init__(self): + super().__init__() +", ); + let outer_class = find_code(&code, "C").expect("missing outer class code"); + let nested_class = find_code(&code, "D").expect("missing nested class code"); + let init = find_code(&code, "__init__").expect("missing nested __init__ code"); assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "float constant floor-div/mod should fold away, got instructions={:?}", - code.instructions + !outer_class.cellvars.iter().any(|name| name == "__class__"), + "nested super() must not force __class__ on outer class, got cellvars={:?}", + outer_class.cellvars ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Float { value } if value.to_bits() == 9.0f64.to_bits() - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Float { value } - if value.to_bits() == 0.09999999999999995f64.to_bits() - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Float { value } if value.is_nan() - ))); - } - - #[test] - fn test_float_power_overflow_constant_does_not_fold() { - let code = compile_exec("x = 1e300 ** 2\n"); - assert!( - code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BinaryOp { op } - if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == oparg::BinaryOperator::Power - )), - "overflowing float power should stay runtime like CPython, got instructions={:?}", - code.instructions - ); - } - - #[test] - fn test_large_string_and_bytes_binops_constant_fold_like_cpython() { - let code = compile_exec( - r#" -encoded = b'\xff\xfe\x00\x00' + b'\x00\x00\x01\x00' * 1024 -text = '\U00010000' * 1024 -"#, + nested_class.cellvars.iter().any(|name| name == "__class__"), + "nested class should own __class__ cell, got cellvars={:?}", + nested_class.cellvars ); - assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "large safe string/bytes constants should fold away, got instructions={:?}", - code.instructions + init.freevars.iter().any(|name| name == "__class__"), + "method using super() should close over nested class, got freevars={:?}", + init.freevars ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Bytes { value } if value.len() == 4100 - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Str { value } if value.code_points().count() == 1024 - ))); } #[test] - fn test_constant_string_subscript_folds_inside_collection() { + fn test_nested_closure_parameter_class_does_not_create_outer_class_closure() { let code = compile_exec( "\ -values = [item for item in [r\"\\\\'a\\\\'\", r\"\\t3\", r\"\\\\\"[0]]]\n", +class C: + def m(self): + def create_closure(__class__): + return (lambda: __class__).__closure__ +", ); + let outer_class = find_code(&code, "C").expect("missing class code"); + let create_closure = + find_code(&code, "create_closure").expect("missing create_closure code"); + let lambda = find_code(&code, "").expect("missing lambda code"); assert!( - !code - .instructions + !outer_class.cellvars.iter().any(|name| name == "__class__"), + "nested __class__ parameter must not force outer class cell, got cellvars={:?}", + outer_class.cellvars + ); + assert!( + create_closure + .cellvars .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "unexpected runtime BINARY_OP after constant subscript folding: {:?}", - code.instructions + .any(|name| name == "__class__"), + "create_closure should own __class__ parameter cell, got cellvars={:?}", + create_closure.cellvars ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements.len() == 3 - && matches!(&elements[2], ConstantData::Str { value } if value.to_string() == "\\") - ))); - } - - #[test] - fn test_constant_string_subscript_with_surrogate_skips_lossy_fold() { - let code = compile_exec("value = \"\\ud800\"[0]\n"); - assert!( - code.instructions.iter().any(|unit| match unit.op { - Instruction::BinaryOp { op } => { - op.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == oparg::BinaryOperator::Subscr - } - _ => false, - }), - "expected runtime subscript for surrogate literal, got instructions={:?}", - code.instructions + lambda.freevars.iter().any(|name| name == "__class__"), + "lambda should close over create_closure parameter, got freevars={:?}", + lambda.freevars ); } #[test] - fn test_constant_subscript_folds_in_load_context() { - let cases = [ - ("value = (1, 2, 3)[0]\n", Some(BigInt::from(1)), None), - ("value = b\"abc\"[0]\n", Some(BigInt::from(97)), None), - ("value = \"abc\"[0]\n", None, Some("a")), - ]; - - for (source, expected_int, expected_str) in cases { - let code = compile_exec(source); - assert!( - !code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BinaryOp { op } - if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == oparg::BinaryOperator::Subscr - )), - "expected folded constant subscript for {source:?}, got instructions={:?}", - code.instructions - ); - - if let Some(expected_int) = expected_int.as_ref() { - let has_small_int = code.instructions.iter().any(|unit| { - matches!( - unit.op, - Instruction::LoadSmallInt { i } - if BigInt::from(i.get(OpArg::new(u32::from(u8::from(unit.arg))))) - == *expected_int - ) - }); - let has_const_int = code.constants.iter().any(|constant| { - matches!(constant, ConstantData::Integer { value } if value == expected_int) - }); - assert!( - has_small_int || has_const_int, - "missing folded integer constant {expected_int} for {source:?}, instructions={:?}", - code.instructions - ); - } - - if let Some(expected_str) = expected_str { - assert!( - code.constants.iter().any(|constant| { - matches!(constant, ConstantData::Str { value } if value.to_string() == expected_str) - }), - "missing folded string constant {expected_str:?} for {source:?}", - ); - } - } - } - - #[test] - fn test_constant_slice_subscript_folds_in_load_context() { + fn test_chained_compare_jump_uses_single_cleanup_copy() { let code = compile_exec( "\ -a = 'hello'[:4]\n\ -b = b'abcd'[1:3]\n\ -c = (1, 2, 3)[:2]\n", +def f(code): + if not 1 <= code <= 2147483647: + raise ValueError('x') +", ); + let f = find_code(&code, "f").expect("missing function code"); + let copy_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::Copy { .. })) + .count(); + let pop_top_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::PopTop)) + .count(); - assert!( - !code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BinaryOp { op } - if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) - == oparg::BinaryOperator::Subscr - )), - "expected folded constant slice subscripts, got instructions={:?}", - code.instructions - ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Str { value } if value.to_string() == "hell" - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Bytes { value } if value == b"bc" - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if matches!( - elements.as_slice(), - [ - ConstantData::Integer { value: a }, - ConstantData::Integer { value: b }, - ] if *a == BigInt::from(1) - && *b == BigInt::from(2) - ) - ))); + assert_eq!(copy_count, 1); + assert_eq!(pop_top_count, 1); } #[test] - fn test_list_of_constant_tuples_uses_list_extend() { + fn test_yield_from_cleanup_jumps_to_shared_end_send() { let code = compile_exec( "\ -deprecated_cases = [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j')] +def outer(): + def inner(): + yield from outer_gen + return inner ", ); + let inner = find_code(&code, "inner").expect("missing inner code"); + let ops: Vec<_> = inner + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let cleanup_idx = ops + .iter() + .position(|op| matches!(op, Instruction::CleanupThrow)) + .expect("missing CLEANUP_THROW"); assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), - "expected constant tuple list folding" + matches!( + ops.get(cleanup_idx + 1), + Some(Instruction::JumpBackwardNoInterrupt { .. } | Instruction::JumpForward { .. }) + ), + "expected CLEANUP_THROW to jump to shared END_SEND block, got ops={ops:?}" + ); + assert!( + !matches!(ops.get(cleanup_idx + 1), Some(Instruction::EndSend)), + "CLEANUP_THROW should not inline END_SEND directly, got ops={ops:?}" ); } #[test] - fn test_large_list_of_unary_constants_uses_list_extend() { + fn test_try_except_falls_through_to_post_handler_code() { let code = compile_exec( "\ -values = [-1, not True, ~0, +True, 5] +def f(): + try: + line = 2 + raise KeyError + except: + line = 5 + line = 6 ", ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); - assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), - "expected unary-folded constants to participate in list folding, got instructions={:?}", - code.instructions + let first_pop_except = ops + .iter() + .position(|op| matches!(op, Instruction::PopExcept)) + .expect("missing POP_EXCEPT"); + assert!( + !matches!( + ops.get(first_pop_except + 1), + Some(Instruction::JumpForward { .. }) + ), + "expected except body to fall through to post-handler code, got ops={ops:?}" + ); + assert!( + matches!( + ops.get(first_pop_except + 1), + Some(Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }) + ), + "expected line-after-except code immediately after POP_EXCEPT, got ops={ops:?}" ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements.len() == 5 - && matches!(&elements[0], ConstantData::Integer { value } if *value == BigInt::from(-1)) - && matches!(&elements[1], ConstantData::Boolean { value } if !value) - && matches!(&elements[2], ConstantData::Integer { value } if *value == BigInt::from(-1)) - && matches!(&elements[3], ConstantData::Integer { value } if *value == BigInt::from(1)) - && matches!(&elements[4], ConstantData::Integer { value } if *value == BigInt::from(5)) - ))); } #[test] - fn test_outer_unary_after_binop_folds_before_list_folding() { + fn test_try_except_while_body_preserves_while_exit_line_nop() { let code = compile_exec( "\ -values = [2.0**53, -0.5, -2.0**-54] +def f(x, E): + try: + while x: + x -= 1 + except E: + if not x: + return None + assert x + return x ", ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let assertion_error = ops + .iter() + .position(|op| matches!(op, Instruction::LoadCommonConstant { .. })) + .expect("missing assertion error load"); assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), - "expected binop-folded constants to participate in list folding, got instructions={:?}", - code.instructions - ); - assert!( - !code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BinaryOp { .. } | Instruction::UnaryNegative - )), - "constant expression list should not leave runtime ops, got instructions={:?}", - code.instructions + ops[..assertion_error].windows(4).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. }, + Instruction::Nop, + Instruction::LoadFastBorrow { .. }, + Instruction::ToBool, + ] + ) + }), + "try/except while body should preserve CPython while-exit NOP before following assert, got ops={ops:?}" ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements.len() == 3 - && matches!(&elements[0], ConstantData::Float { value } if *value == 9007199254740992.0) - && matches!(&elements[1], ConstantData::Float { value } if *value == -0.5) - && matches!(&elements[2], ConstantData::Float { value } if value.is_sign_negative()) - ))); } #[test] - fn test_negative_integer_power_folds_to_float_constant() { - let code = compile_exec("value = -3.0 * 2**(-333)\n"); + fn test_try_except_for_direct_break_preserves_normal_exhaustion_nop() { + let code = compile_exec( + "\ +def f(xs, g, E): + try: + for x in xs: + if x: + break + g() + except E: + pass + g() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "negative integer power should fold through the enclosing multiply, got instructions={:?}", - code.instructions + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + ] + ) + }), + "try/except for-body with direct break should keep CPython normal-exhaustion NOP, got ops={ops:?}" ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Float { value } - if value.is_sign_negative() && *value < 0.0 && value.abs() < 1.0e-90 - ))); } #[test] - fn test_complex_power_constants_fold_like_cpython() { + fn test_try_except_for_without_direct_break_drops_normal_exhaustion_nop() { let code = compile_exec( "\ -one = 3j ** 0j -zero = 0j ** 2 +def f(xs, g, E): + try: + for x in xs: + g() + except E: + pass + g() ", ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "safe complex power constants should fold away, got instructions={:?}", - code.instructions + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + ] + ) + }), + "try/except for-body without direct break should not keep a redundant normal-exhaustion NOP, got ops={ops:?}" ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Complex { value } if value.re == 1.0 && value.im == 0.0 - ))); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Complex { value } if value.re == 0.0 && value.im == 0.0 - ))); } #[test] - fn test_zero_complex_power_exception_constants_do_not_fold() { - let code = compile_exec("value = 0j ** (3 - 2j)\n"); + fn test_terminal_except_before_conditional_tail_uses_strong_load() { + let code = compile_exec( + "\ +def f(self, Exception): + try: + tree = self.g() + except Exception: + return False + if tree.body: + return True + return False +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let tree_load_before_body_attr = instructions.windows(2).find(|window| { + let loads_tree = match window[0].op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + f.varnames + [usize::from(var_num.get(OpArg::new(u32::from(u8::from(window[0].arg)))))] + == "tree" + } + _ => false, + }; + loads_tree && matches!(window[1].op, Instruction::LoadAttr { .. }) + }); assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "zero complex to complex power should stay runtime so ZeroDivisionError is preserved, got instructions={:?}", - code.instructions + matches!( + tree_load_before_body_attr.map(|window| window[0].op), + Some(Instruction::LoadFast { .. }) + ), + "conditional tail after terminal except should match CPython's strong LOAD_FAST, got instructions={instructions:?}", ); } #[test] - fn test_large_constant_list_keeps_streaming_build() { - let source = format!( - "values = [{}]\n", - (0..31) - .map(|i| format!("'v{i}'")) - .collect::>() - .join(", ") - ); - let code = compile_exec(&source); + fn test_try_except_continuation_folded_tuple_drops_operand_nop() { + let code = compile_exec( + "\ +def f(): + try: + import sqlite3 + except ImportError: + return - assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::ListAppend { .. })), - "large constant lists should keep LIST_APPEND streaming form, got instructions={:?}", - code.instructions + attributes = ('sqlite_version',) +", ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), - "large constant lists should not fold to LIST_EXTEND, got instructions={:?}", - code.instructions + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "expected CPython nop_out-style folded tuple operand NOP to be removed, got ops={ops:?}", ); } #[test] - fn test_large_constant_tuple_stream_folds_to_tuple_const() { - let source = format!( - "values = ({},)\n", - (0..31) - .map(|i| format!("'v{i}'")) - .collect::>() - .join(", ") + fn test_if_else_normal_fallthrough_end_label_drops_return_anchor_nop() { + let code = compile_exec( + "\ +def f(s): + if s[0] in (0o200, 0o377): + n = 0 + for i in range(len(s) - 1): + n <<= 8 + n += s[i + 1] + if s[0] == 0o377: + n = -(256 ** (len(s) - 1) - n) + else: + try: + s = nts(s, 'ascii', 'strict') + n = int(s.strip() or '0', 8) + except ValueError: + raise InvalidHeaderError('invalid header') + return n +", ); - let code = compile_exec(&source); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); assert!( - !code.instructions.iter().any(|unit| matches!( - unit.op, - Instruction::BuildList { .. } - | Instruction::ListAppend { .. } - | Instruction::CallIntrinsic1 { .. } - )), - "large constant tuple should fold the LIST_TO_TUPLE stream, got instructions={:?}", - code.instructions + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::StoreFast { .. }, + Instruction::Nop, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::ReturnValue, + ] + ) + }), + "normal fallthrough into an if-end final return should not keep a CPython return-anchor NOP, got ops={ops:?}", ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } if elements.len() == 31 - ))); } #[test] - fn test_annotation_closure_uses_format_varname() { + fn test_explicit_final_return_none_is_not_duplicated() { let code = compile_exec( "\ -class C: - x: int +def f(src, dst, length, exception, bufsize): + if length == 0: + return + if length is None: + copyfileobj(src, dst, bufsize) + return + + blocks, remainder = divmod(length, bufsize) + for b in range(blocks): + buf = src.read(bufsize) + if len(buf) < bufsize: + raise exception('unexpected end of data') + dst.write(buf) + + if remainder != 0: + buf = src.read(remainder) + if len(buf) < remainder: + raise exception('unexpected end of data') + dst.write(buf) + return ", ); - let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); - let varnames = annotate - .varnames + let f = find_code(&code, "f").expect("missing f code"); + let return_count = f + .instructions .iter() - .map(|name| name.as_str()) - .collect::>(); - assert_eq!(varnames, vec!["format"]); + .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) + .count(); + + assert_eq!( + return_count, + 3, + "explicit final return None should not be duplicated as a synthetic no-location epilogue, got ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); } #[test] - fn test_type_param_evaluator_uses_dot_format_varname() { + fn test_named_except_cleanup_keeps_jump_over_cleanup_and_next_try() { let code = compile_exec( - "\ -class C[T: int]: - pass -", + r#" +def f(self): + try: + assert 0, 'msg' + except AssertionError as e: + self.assertEqual(e.args[0], 'msg') + else: + self.fail("AssertionError not raised by assert 0") + + try: + assert False + except AssertionError as e: + self.assertEqual(len(e.args), 0) + else: + self.fail("AssertionError not raised by 'assert False'") +"#, ); - let evaluator = find_code(&code, "T").expect("missing type parameter evaluator"); - let varnames = evaluator - .varnames + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions .iter() - .map(|name| name.as_str()) - .collect::>(); - assert_eq!(varnames, vec![".format"]); - } + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); - #[test] - fn test_class_annotation_global_resolution_matches_cpython() { - let class_global = compile_exec( - "\ -X = 'global' -class C: - locals()['X'] = 'class' - global X - y: X -", - ); - let annotate = - find_code(&class_global, "__annotate__").expect("missing class __annotate__ code"); - assert!( - annotate - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })), - "expected explicit class global to use LOAD_GLOBAL, got instructions={:?}", - annotate.instructions - ); + let first_pop_except = ops + .iter() + .position(|op| matches!(op, Instruction::PopExcept)) + .expect("missing POP_EXCEPT"); + let window = &ops[first_pop_except..(first_pop_except + 6).min(ops.len())]; assert!( - !annotate - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), - "did not expect class explicit global to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}", - annotate.instructions + matches!( + window, + [ + Instruction::PopExcept, + Instruction::LoadConst { .. }, + Instruction::StoreName { .. } | Instruction::StoreFast { .. }, + Instruction::DeleteName { .. } | Instruction::DeleteFast { .. }, + Instruction::JumpForward { .. }, + .. + ] + ), + "expected named except cleanup to jump over cleanup reraise block, got ops={window:?}" ); + } - let outer_global = compile_exec( + #[test] + fn test_named_except_with_suppress_does_not_duplicate_following_with() { + let code = compile_exec( "\ -def f(): - global X - class C: - locals()['X'] = 'class' - y: X +def f(StringIO, captured_output, print): + try: + raise KeyError + except KeyError as e: + with captured_output('stderr') as tbstderr: + print('x') + with captured_output('stderr') as tbstderr: + print('y') + else: + print('else') + s = StringIO() + return s ", ); - let annotate = find_code(&outer_global, "__annotate__") - .expect("missing nested class __annotate__ code"); - assert!( - annotate - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), - "expected outer explicit global in class annotation to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}", - annotate.instructions + let f = find_code(&code, "f").expect("missing f code"); + let load_y_count = f + .instructions + .iter() + .filter(|unit| match unit.op { + Instruction::LoadConst { consti } => { + matches!( + &f.constants + [consti.get(OpArg::new(u32::from(u8::from(unit.arg))))], + ConstantData::Str { value } if value.as_str() == Ok("y") + ) + } + _ => false, + }) + .count(); + + assert_eq!( + load_y_count, 1, + "following with body should not be duplicated into the previous with suppress path" ); } #[test] - fn test_constant_tuple_binops_fold_like_cpython() { - let code = compile_exec("value = (1,) * 17 + ('spam',)\n"); + fn test_bare_except_deopts_post_handler_load_fast_borrow() { + let code = compile_exec( + "\ +def f(self): + try: + 1 / 0 + except: + pass + with self.assertRaises(SyntaxError): + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let attr_idx = ops + .iter() + .position(|op| matches!(op, Instruction::LoadAttr { .. })) + .expect("missing LOAD_ATTR for assertRaises"); assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), - "tuple constant binops should fold away, got instructions={:?}", - code.instructions + matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })), + "bare except tail should deopt self to LOAD_FAST, got ops={ops:?}" ); - assert!(code.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if elements.len() == 18 - && elements[..17] - .iter() - .all(|elt| matches!(elt, ConstantData::Integer { value } if *value == BigInt::from(1))) - && matches!(&elements[17], ConstantData::Str { value } if value.to_string() == "spam") - ))); } #[test] - fn test_constant_list_iterable_uses_tuple() { + fn test_typed_except_keeps_post_handler_load_fast_borrow() { let code = compile_exec( "\ -def f(): - return {x: y for x, y in [(1, 2), ]} +def f(self): + try: + 1 / 0 + except ZeroDivisionError: + pass + with self.assertRaises(SyntaxError): + pass ", ); - let f = find_code(&code, "f").expect("missing function code"); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let attr_idx = ops + .iter() + .position(|op| matches!(op, Instruction::LoadAttr { .. })) + .expect("missing LOAD_ATTR for assertRaises"); assert!( - !f.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BuildList { .. })), - "constant list iterable should avoid BUILD_LIST before GET_ITER" + matches!( + ops.get(attr_idx - 1), + Some(Instruction::LoadFastBorrow { .. }) + ), + "typed except tail should keep LOAD_FAST_BORROW, got ops={ops:?}" ); - assert!(f.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if matches!( - elements.as_slice(), - [ConstantData::Tuple { elements: inner }] - if matches!( - inner.as_slice(), - [ - ConstantData::Integer { .. }, - ConstantData::Integer { .. } - ] - ) - ) - ))); } #[test] - fn test_constant_set_iterable_uses_frozenset_const() { + fn test_conditional_typed_except_return_join_keeps_borrow() { let code = compile_exec( "\ -def f(): - return [x for x in {1, 2, 3}] -", - ); - let f = find_code(&code, "f").expect("missing function code"); - - assert!( - !f.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BuildSet { .. })), - "constant set iterable should avoid BUILD_SET before GET_ITER" - ); - assert!(f.constants.iter().any(|constant| matches!( - constant, - ConstantData::Frozenset { elements } - if matches!( - elements.as_slice(), - [ - ConstantData::Integer { .. }, - ConstantData::Integer { .. }, - ConstantData::Integer { .. } - ] - ) - ))); - } - - #[test] - fn test_constant_list_membership_uses_tuple_const() { - let code = compile_exec( - "\ -f = lambda x: x in [1, 2, 3] +def f(cond, obj, xs, E): + if cond: + try: + obj.m() + except E: + return 1 + for x in xs: + obj.n(x) + return obj ", ); - let lambda = find_code(&code, "").expect("missing lambda code"); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_tail = &instructions[..handler_start]; + let load_name = |unit: &&CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; - assert!( - !lambda - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BuildList { .. })), - "constant list membership should avoid BUILD_LIST before CONTAINS_OP" - ); - assert!(lambda.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if matches!( - elements.as_slice(), - [ - ConstantData::Integer { .. }, - ConstantData::Integer { .. }, - ConstantData::Integer { .. } - ] - ) - ))); + for name in ["xs", "obj", "x"] { + assert!( + normal_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. }) + && load_name(unit) == Some(name)), + "conditional typed except return join should keep CPython-style borrowed {name} loads, got tail={normal_tail:?}" + ); + assert!( + !normal_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. }) + && load_name(unit) == Some(name)), + "conditional typed except return join should not force strong {name} loads, got tail={normal_tail:?}" + ); + } } #[test] - fn test_small_constant_set_membership_uses_frozenset_const() { + fn test_typed_except_pass_resume_store_subscr_tail_keeps_borrows() { let code = compile_exec( "\ -f = lambda x: x in {0} +def f(self, sys, KeyError): + mod_name = self.mod_name + try: + self._saved_module.append(sys.modules[mod_name]) + except KeyError: + pass + sys.modules[mod_name] = self.module + return self ", ); - let lambda = find_code(&code, "").expect("missing lambda code"); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let store_subscr_idx = ops + .iter() + .position(|op| matches!(op, Instruction::StoreSubscr)) + .expect("missing STORE_SUBSCR tail"); assert!( - !lambda - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BuildSet { .. })), - "constant set membership should avoid BUILD_SET before CONTAINS_OP" + matches!( + ops.get(store_subscr_idx - 5), + Some(Instruction::LoadFastBorrow { .. }) + ) && matches!( + ops.get(store_subscr_idx - 3), + Some(Instruction::LoadFastBorrow { .. }) + ) && matches!( + ops.get(store_subscr_idx - 1), + Some(Instruction::LoadFastBorrow { .. }) + ), + "typed except pass tail should keep STORE_SUBSCR operands borrowed, got ops={ops:?}" + ); + assert!( + matches!( + ops.get(store_subscr_idx + 1), + Some(Instruction::LoadFastBorrow { .. }) + ), + "typed except pass return should keep self borrowed, got ops={ops:?}" ); - assert!(lambda.constants.iter().any(|constant| matches!( - constant, - ConstantData::Frozenset { elements } - if matches!(elements.as_slice(), [ConstantData::Integer { value }] if *value == BigInt::from(0)) - ))); } #[test] - fn test_nonconstant_list_membership_uses_tuple() { + fn test_reraising_typed_except_deopts_post_handler_loads() { let code = compile_exec( "\ -def f(a, b, c, x): - return x in [a, b, c] +def f(x, os, self, pid, exitcode): + try: + y = 1 + except RuntimeError: + raise + if x: + os._exit(exitcode) + self.wait_impl(pid, exitcode=exitcode) ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -18188,290 +20188,412 @@ def f(a, b, c, x): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let guard_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ToBool)) + .and_then(|idx| idx.checked_sub(1)) + .expect("missing post-handler bool guard"); assert!( - ops.windows(2).any(|window| { - matches!( - window, - [ - Instruction::BuildTuple { .. }, - Instruction::ContainsOp { .. } - ] - ) - }), - "expected BUILD_TUPLE before CONTAINS_OP for non-constant list membership, got ops={ops:?}" - ); - } - - #[test] - fn test_starred_tuple_iterable_drops_list_to_tuple_before_get_iter() { - let code = compile_exec( - "\ -def f(a, b, c): - for x in *a, *b, *c: - pass -", + matches!(ops.get(guard_idx), Some(Instruction::LoadFast { .. })), + "reraising typed except tail should deopt guard load, got ops={ops:?}" ); - let f = find_code(&code, "f").expect("missing function code"); + let wait_idx = ops + .iter() + .position(|op| matches!(op, Instruction::CallKw { .. })) + .expect("missing wait_impl CALL_KW"); + let call_args = &ops[wait_idx.saturating_sub(3)..wait_idx]; assert!( - !has_intrinsic_1(f, IntrinsicFunction1::ListToTuple), - "LIST_TO_TUPLE should be removed before GET_ITER in for-iterable context" + call_args.iter().any(|op| matches!( + op, + Instruction::LoadFastLoadFast { .. } | Instruction::LoadFast { .. } + )), + "reraising typed except tail should keep strong fast loads for call args, got ops={ops:?}" ); assert!( - f.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::GetIter)), - "expected GET_ITER in for loop" + !call_args.iter().any(|op| matches!( + op, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastBorrow { .. } + )), + "reraising typed except tail should not borrow call args, got ops={ops:?}" ); } #[test] - fn test_comprehension_single_list_iterable_uses_tuple() { + fn test_reraising_outer_handler_keeps_explicit_raise_call_arg_borrow() { let code = compile_exec( "\ -def g(): - [x for x in [(yield 1)]] +def f(file, os, stat, errno, self, fd): + try: + self._stat_atopen = os.fstat(fd) + try: + if stat.S_ISDIR(self._stat_atopen.st_mode): + raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), file) + except AttributeError: + pass + self.name = file + try: + os.lseek(fd, 0, SEEK_END) + except OSError as e: + if e.errno != errno.ESPIPE: + raise + except OSError: + raise ", ); - let g = find_code(&code, "g").expect("missing g code"); - let ops: Vec<_> = g + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let file_borrow_before_raise_call = instructions.windows(3).any(|window| { + let is_file_borrow = match window[0].op { + Instruction::LoadFastBorrow { var_num } => { + f.varnames + [usize::from(var_num.get(OpArg::new(u32::from(u8::from(window[0].arg)))))] + == "file" + } + _ => false, + }; + is_file_borrow + && matches!(window[1].op, Instruction::Call { .. }) + && matches!(window[2].op, Instruction::RaiseVarargs { .. }) + }); assert!( - ops.windows(2).any(|window| { - matches!( - window, - [Instruction::BuildTuple { .. }, Instruction::GetIter] - ) - }), - "expected BUILD_TUPLE before GET_ITER for single-item list iterable in comprehension, got ops={ops:?}" + file_borrow_before_raise_call, + "outer reraising handler should not deopt explicit raise call args; CPython keeps file as LOAD_FAST_BORROW, got instructions={instructions:?}" ); } #[test] - fn test_nested_comprehension_list_iterable_uses_tuple() { + fn test_reraising_except_loop_backedge_keeps_loop_header_borrow() { let code = compile_exec( "\ -def f(): - return [[y for y in [x, x + 1]] for x in [1, 3, 5]] +def f(self, tag, expect_bye): + while 1: + result = self.tagged_commands[tag] + if result is not None: + del self.tagged_commands[tag] + return result + if expect_bye: + typ = 'BYE' + bye = self.untagged_responses.pop(typ, None) + if bye is not None: + return (typ, bye) + self._check_bye() + try: + self._get_response() + except self.abort as val: + if __debug__: + if self.debug >= 1: + self.print_log() + raise ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_ops: Vec<_> = instructions[..handler_start] .iter() .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) .collect(); assert!( - ops.windows(2).any(|window| { - matches!( - window, - [Instruction::BuildTuple { .. }, Instruction::GetIter] - ) - }), - "expected BUILD_TUPLE before GET_ITER for nested list iterable in comprehension, got ops={ops:?}" + warm_ops.iter().any(|op| matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + )), + "expected loop body before reraising handler to keep borrowed loads, got ops={warm_ops:?}" + ); + assert!( + warm_ops + .iter() + .all(|op| !matches!(op, Instruction::LoadFast { .. })), + "loop backedge into reraising handler should not deopt warm loop loads, got ops={warm_ops:?}" ); } #[test] - fn test_comprehension_singleton_sub_iter_uses_assignment_idiom() { + fn test_protected_store_break_handler_deopts_bool_guard_tail() { let code = compile_exec( "\ -def f(): - return {j: j * j for i in range(4) for j in [i + 1]} +def f(self, size): + parts = [] + while size > 0: + try: + buf = self.sock.recv(DEFAULT_BUFFER_SIZE) + except ConnectionError: + break + if not buf: + break + self._readbuf.append(buf) + size -= len(buf) + return b''.join(parts) ", ); let f = find_code(&code, "f").expect("missing f code"); - let for_iter_count = f + let instructions: Vec<_> = f .instructions .iter() - .filter(|unit| matches!(unit.op, Instruction::ForIter { .. })) - .count(); - let has_map_add_depth_2 = f.instructions.iter().any(|unit| { - matches!( - unit.op, - Instruction::MapAdd { i } - if i.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 2 - ) - }); + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let guard_bool = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::ToBool)) + .expect("missing bool guard"); + let store_buf = instructions[..guard_bool] + .iter() + .rposition(|unit| matches!(unit.op, Instruction::StoreFast { .. })) + .expect("missing protected STORE_FAST before bool guard"); + let guard_load = instructions[store_buf + 1].op; + let append_call = instructions[store_buf + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::Call { .. })) + .map(|idx| idx + store_buf + 1) + .expect("missing append call"); + let append_arg = instructions[append_call - 1].op; - assert_eq!( - for_iter_count, 1, - "singleton sub-iter should not emit its own FOR_ITER, got instructions={:?}", - f.instructions - ); assert!( - has_map_add_depth_2, - "assignment-idiom dictcomp should use MAP_ADD depth 2, got instructions={:?}", - f.instructions + matches!(guard_load, Instruction::LoadFast { .. }), + "CPython uses strong LOAD_FAST for protected-store break guard, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); assert!( - !f.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::BuildTuple { .. })), - "singleton sub-iter should not materialize an iterator tuple, got instructions={:?}", - f.instructions + matches!(append_arg, Instruction::LoadFast { .. }), + "CPython uses strong LOAD_FAST for protected-store append arg, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); } #[test] - fn test_constant_comprehension_iterable_with_unary_int_uses_tuple_const() { + fn test_assertion_success_join_keeps_following_debug_tail_borrowed() { let code = compile_exec( "\ -l = lambda : [2 < x for x in [-1, 3, 0]] +def f(self, typ, dat): + if self._idle_capture: + if self._idle_responses: + response = self._idle_responses[-1] + assert response[0] == typ + response[1].append(dat) + else: + self._idle_responses.append((typ, [dat])) + if self.debug >= 5: + self._mesg(f'idle: queue untagged {typ} {dat!r}') + return ", ); - let lambda = find_code(&code, "").expect("missing lambda code"); - - assert!( - lambda.constants.iter().any(|constant| matches!( - constant, - ConstantData::Tuple { elements } - if matches!( - elements.as_slice(), - [ - ConstantData::Integer { .. }, - ConstantData::Integer { .. }, - ConstantData::Integer { .. } - ] - ) - )), - "expected folded tuple constant for comprehension iterable" - ); - } - - #[test] - fn test_module_scope_listcomp_is_inlined() { - let code = compile_exec("values = [i for i in range(3)]\n"); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let debug_attr = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "debug" + } + _ => false, + }) + .expect("missing debug LOAD_ATTR"); + let mesg_attr = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_mesg" + } + _ => false, + }) + .expect("missing _mesg LOAD_ATTR"); assert!( - find_code(&code, "").is_none(), - "module-scope list comprehension should be inlined" + matches!( + instructions[debug_attr - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "CPython keeps LOAD_FAST_BORROW after assertion success join, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })), - "inlined module-scope list comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}", - code.instructions + matches!( + instructions[mesg_attr - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "CPython keeps LOAD_FAST_BORROW in assertion-success debug body, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); } #[test] - fn test_module_scope_dictcomp_is_inlined() { - let code = compile_exec("mapping = {i: i for i in range(3)}\n"); + fn test_multi_protected_method_call_terminal_handler_keeps_try_body_borrows() { + let code = compile_exec( + "\ +def f(self, literal): + try: + self.send(literal) + self.send(CRLF) + except OSError as val: + raise self.abort('socket error: %s' % val) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let first_send = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send" + } + _ => false, + }) + .expect("missing send LOAD_ATTR"); + let first_literal = instructions[first_send + 1].op; + let second_send = instructions[first_send + 1..] + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send" + } + _ => false, + }) + .map(|idx| idx + first_send + 1) + .expect("missing second send LOAD_ATTR"); assert!( - find_code(&code, "").is_none(), - "module-scope dict comprehension should be inlined" + matches!( + instructions[first_send - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "CPython keeps first protected send receiver borrowed, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })), - "inlined module-scope dict comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}", - code.instructions + matches!(first_literal, Instruction::LoadFastBorrow { .. }), + "CPython keeps first protected send arg borrowed, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() + ); + assert!( + matches!( + instructions[second_send - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "CPython keeps second protected send receiver borrowed, got ops={:?}", + instructions.iter().map(|unit| unit.op).collect::>() ); } #[test] - fn test_async_dictcomp_in_async_function_is_inlined() { + fn test_dunder_debug_constant_false_if_deopts_tail_borrow() { let code = compile_exec( "\ -async def f(items): - return {item: item async for item in items} +def f(self): + if not __debug__: + self.skipTest('need asserts, run without -O') + self.do_disassembly_test() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - - assert!( - find_code(&code, "").is_none(), - "async dict comprehension should be inlined" - ); - assert!( - ops.iter().any(|op| matches!(op, Instruction::GetAiter)), - "inlined async dict comprehension should keep GET_AITER in outer code, got ops={ops:?}" - ); - assert!( - ops.iter() - .any(|op| matches!(op, Instruction::LoadFastAndClear { .. })), - "inlined async dict comprehension should use LOAD_FAST_AND_CLEAR, got ops={ops:?}" - ); + let attr_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "do_disassembly_test" + } + _ => false, + }) + .expect("missing LOAD_ATTR for do_disassembly_test"); + let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); assert!( - !ops.iter().any(|op| matches!(op, Instruction::MakeFunction)), - "inlined async dict comprehension should not materialize MAKE_FUNCTION, got ops={ops:?}" + matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })), + "constant-false __debug__ tail should deopt self to LOAD_FAST, got ops={ops:?}" ); } #[test] - fn test_async_inlined_comprehension_inlines_restore_return_into_end_async_for() { + fn test_constant_slice_folds_constant_bounds() { let code = compile_exec( "\ -async def f(): - return [i + 1 async for i in g([10, 20])] +def f(obj): + return obj['a':123456789012345678901234567890] ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - - assert!( - ops.windows(8).any(|window| { - matches!( - window, - [ - Instruction::EndAsyncFor, - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - Instruction::ReturnValue, - Instruction::Swap { .. }, - Instruction::PopTop, - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - ] - ) - }), - "expected CPython-style restore+return inlined into END_ASYNC_FOR before cleanup, got ops={ops:?}" + let folded_slice = f + .constants + .iter() + .find_map(|constant| match constant { + ConstantData::Slice { elements } => Some(elements), + _ => None, + }) + .expect("missing folded slice constant"); + assert!( + matches!( + folded_slice.as_ref(), + [ + ConstantData::Str { .. }, + ConstantData::Integer { .. }, + ConstantData::None, + ] + ), + "expected folded slice('a', 123456789012345678901234567890, None), got {folded_slice:?}" ); assert!( - !ops.windows(2).any(|window| { - matches!( - window, - [ - Instruction::EndAsyncFor, - Instruction::JumpForward { .. } | Instruction::JumpBackward { .. }, - ] - ) - }), - "unexpected jump from END_ASYNC_FOR to the normal restore tail, got ops={ops:?}" + matches!( + ops.as_slice(), + [ + Instruction::Resume { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::BinaryOp { .. }, + Instruction::ReturnValue, + ] + ), + "expected CPython-style LOAD_CONST(slice(...)) path for constant bounds, got ops={ops:?}" ); } #[test] - fn test_await_cleanup_throw_falls_through_until_cold_reorder() { + fn test_negative_step_slice_uses_build_slice() { let code = compile_exec( "\ -async def f(): - await 1 +def f(obj): + return obj[::-1] ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18480,42 +20602,35 @@ async def f(): .collect(); assert!( - ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::CleanupThrow, - Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::CallIntrinsic1 { .. }, - ] - ) - }), - "expected CPython-style cold CLEANUP_THROW jump before StopIteration handler, got ops={ops:?}" - ); - assert!( - !ops.windows(2).any(|window| { - matches!(window, [Instruction::CleanupThrow, Instruction::EndSend]) - }), - "CLEANUP_THROW should not inline the normal END_SEND return tail, got ops={ops:?}" + matches!( + ops.as_slice(), + [ + Instruction::Resume { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::BuildSlice { .. }, + Instruction::BinaryOp { .. }, + Instruction::ReturnValue, + ] + ), + "expected CPython-style BUILD_SLICE path for non-literal negative step, got ops={ops:?}" ); } #[test] - fn test_match_async_inlined_comprehension_success_jump_no_interrupt() { + fn test_bool_int_binop_constants_fold() { let code = compile_exec( "\ -async def f(name_3, name_5): - match b'': - case True: - pass - case name_5 if f'e': - {name_3: f async for name_2 in name_5} - case []: - pass - [[]] +def f(): + return False + 2, True + 2, False + False, True / 1, True & False + +def g(): + return False + 2 ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18524,47 +20639,46 @@ async def f(name_3, name_5): .collect(); assert!( - ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::PopTop, - Instruction::StoreFast { .. }, - Instruction::JumpBackwardNoInterrupt { .. }, - ] - ) - }), - "expected CPython-style no-interrupt match success backedge after async comprehension cleanup, got ops={ops:?}" + !ops.iter() + .any(|op| matches!(op, Instruction::BinaryOp { .. })), + "expected CPython-style folded bool/int binops, got ops={ops:?}" ); assert!( - !ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::PopTop, - Instruction::StoreFast { .. }, - Instruction::JumpBackward { .. }, - ] - ) - }), - "match success cleanup backedge should not be a regular interrupting jump, got ops={ops:?}" + matches!( + ops.as_slice(), + [ + Instruction::Resume { .. }, + Instruction::LoadConst { .. }, + Instruction::ReturnValue + ] + ), + "expected folded constants for bool/int binops, got ops={ops:?}" + ); + + let g = find_code(&code, "g").expect("missing function code"); + let g_ops: Vec<_> = g + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( + !g_ops + .iter() + .any(|op| matches!(op, Instruction::BinaryOp { .. })), + "expected top-level bool/int binop to fold, got ops={g_ops:?}" ); } #[test] - fn test_for_loop_if_return_reorders_continue_backedge_before_exit_body() { + fn test_double_not_expression_folds_to_bool_conversion() { let code = compile_exec( "\ -def f(items, occurrence): - for item in items: - if item: - occurrence -= 1 - if not occurrence: - return item - return None +def f(x): + return not not x ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18573,44 +20687,28 @@ def f(items, occurrence): .collect(); assert!( - ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. }, - ] - ) - }), - "expected CPython-style inverted return guard followed by loop backedge, got ops={ops:?}" - ); - assert!( - !ops.windows(3).any(|window| { - matches!( - window, - [ - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, - ] - ) - }), - "return guard should not fall through into the return body before the loop backedge, got ops={ops:?}" + matches!( + ops.as_slice(), + [ + Instruction::Resume { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::ToBool, + Instruction::ReturnValue, + ] + ), + "expected CPython-style double-not bool conversion, got ops={ops:?}" ); } #[test] - fn test_sync_with_after_async_for_keeps_end_async_for_line_marker() { + fn test_tuple_bound_slice_uses_two_part_slice_path() { let code = compile_exec( "\ -async def f(cm, source, tgt): - with cm: - async for tgt[0] in source(): - pass +def f(obj): + return obj[(1, 2):] ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18619,195 +20717,110 @@ async def f(cm, source, tgt): .collect(); assert!( - ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::EndAsyncFor, - Instruction::Nop, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - ] - ) - }), - "expected CPython-style line-marker NOP between END_ASYNC_FOR and with cleanup, got ops={ops:?}" + matches!( + ops.as_slice(), + [ + Instruction::Resume { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::BinarySlice, + Instruction::ReturnValue, + ] + ), + "expected CPython-style BINARY_SLICE path for tuple lower bound, got ops={ops:?}" ); } #[test] - fn test_genexpr_with_async_comprehension_element_is_async_generator() { + fn test_exception_cleanup_jump_to_return_is_inlined() { let code = compile_exec( "\ -async def f(): - gen = ([i async for i in asynciter([1, 2])] for j in [10, 20]) - return [x async for x in gen] +def f(names, cls): + try: + cls.attr = names + except: + pass + return names ", ); - let genexpr = find_code(&code, "").expect("missing genexpr code"); - let units: Vec<_> = genexpr + let f = find_code(&code, "f").expect("missing function code"); + let return_count = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - - assert!( - units.windows(2).any(|window| { - let [wrap, yield_value] = window else { - return false; - }; - matches!(yield_value.op, Instruction::YieldValue { .. }) - && match wrap.op { - Instruction::CallIntrinsic1 { func } => { - func.get(OpArg::new(u32::from(u8::from(wrap.arg)))) - == bytecode::IntrinsicFunction1::AsyncGenWrap - } - _ => false, - } - }), - "expected CPython-style ASYNC_GEN_WRAP before genexpr yield, got units={units:?}" - ); - } - - #[test] - fn test_nested_module_scope_dictcomp_symbols_are_local() { - let symbol_table = scan_program_symbol_table( - "\ -deoptmap = { - specialized: base - for base, family in _specializations.items() - for specialized in family -} -", - ); - - for name in ["base", "family", "specialized"] { - let symbol = symbol_table - .lookup(name) - .unwrap_or_else(|| panic!("missing module symbol {name}")); - assert_eq!( - symbol.scope, - SymbolScope::Local, - "expected module-scope inlined comprehension symbol {name} to be Local, got {symbol:?}" - ); - } - - let comp = symbol_table - .sub_tables - .first() - .expect("missing comprehension symbol table"); - assert!(comp.comp_inlined, "expected comprehension to be inlined"); - for name in ["base", "family", "specialized"] { - let symbol = comp - .lookup(name) - .unwrap_or_else(|| panic!("missing comprehension symbol {name}")); - assert_eq!( - symbol.scope, - SymbolScope::Local, - "expected comprehension symbol {name} to be Local, got {symbol:?}" - ); - } - } - - #[test] - fn test_nested_module_scope_dictcomp_uses_fast_locals() { - let code = compile_exec( - "\ -deoptmap = { - specialized: base - for base, family in _specializations.items() - for specialized in family -} -", - ); - let ops: Vec<_> = code - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) + .count(); - assert!( - ops.iter() - .any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })), - "expected outer target unpack to use STORE_FAST_STORE_FAST, got ops={ops:?}" - ); - assert!( - ops.iter().any(|op| matches!( - op, - Instruction::StoreFastLoadFast { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - )), - "expected inner target/store-use path to use fast locals, got ops={ops:?}" - ); - assert!( - ops.iter() - .filter(|op| matches!(op, Instruction::LoadName { .. })) - .count() - <= 1, - "unexpected extra LOAD_NAME ops in nested inlined comprehension, got ops={ops:?}" - ); - assert!( - ops.iter() - .filter(|op| matches!(op, Instruction::StoreName { .. })) - .count() - <= 1, - "unexpected extra STORE_NAME ops in nested inlined comprehension, got ops={ops:?}" + assert_eq!( + return_count, 2, + "expected CPython-style distinct return sites for normal and except paths" ); } #[test] - fn test_module_scope_inlined_comprehension_keeps_outer_iter_as_name_lookup() { + fn test_except_break_preserves_plain_jump_when_inlining_no_lineno_tail() { let code = compile_exec( "\ -path_separators = ['/'] -_pathseps_with_colon = {f':{s}' for s in path_separators} +def f(compiler_so, cc_args): + strip_sysroot = True + if '-arch' in cc_args: + while True: + try: + index = compiler_so.index('-arch') + del compiler_so[index:index + 2] + except ValueError: + break + if strip_sysroot: + while True: + indices = [i for i, x in enumerate(compiler_so) if x.startswith('-isysroot')] + if not indices: + break + index = indices[0] + del compiler_so[index:index + 1] + return compiler_so ", ); - let ops: Vec<_> = code + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let load_name_path = ops - .windows(2) - .any(|window| matches!(window, [Instruction::LoadName { .. }, Instruction::GetIter])); - assert!( - load_name_path, - "expected outer iterable to stay a NAME lookup before GET_ITER, got ops={ops:?}" - ); - assert!( - !ops.windows(2).any(|window| matches!( - window, - [ - Instruction::LoadFast { .. } | Instruction::LoadFastCheck { .. }, - Instruction::GetIter - ] - )), - "module local outer iterable should not become a fast local, got ops={ops:?}" - ); assert!( - ops.iter().any(|op| matches!( - op, - Instruction::StoreFastLoadFast { .. } | Instruction::StoreFast { .. } - )), - "comprehension target should still use fast locals, got ops={ops:?}" + ops.windows(2).any(|window| { + matches!( + window, + [Instruction::PopExcept, Instruction::JumpBackward { .. }] + ) + }) && !ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::PopExcept, + Instruction::JumpBackwardNoInterrupt { .. } + ] + ) + }), + "except-break cleanup should preserve CPython's plain JUMP when a no-lineno tail is inlined, got ops={ops:?}" ); } #[test] - fn test_function_scope_inlined_comprehension_restore_keeps_swap_before_duplicate_store() { + fn test_nested_with_bare_except_keeps_handler_cleanup_before_following_code() { let code = compile_exec( "\ -def f(): - a = [1 for a in [0]] - return 1 +def f(cm, self): + try: + with cm: + raise Exception + except: + pass + self.g() ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18815,34 +20828,53 @@ def f(): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let outer_handler = ops + .iter() + .enumerate() + .filter_map(|(idx, op)| matches!(op, Instruction::PushExcInfo).then_some(idx)) + .next_back() + .expect("missing outer handler"); assert!( - ops.windows(4).any(|window| matches!( - window, - [ - Instruction::PopIter, - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - Instruction::StoreFast { .. } - ] - )), - "expected PopIter/SWAP 2/STORE_FAST/STORE_FAST restore tail, got ops={ops:?}" + ops[outer_handler..].windows(6).any(|window| { + matches!( + window, + [ + Instruction::PopExcept, + Instruction::JumpForward { .. }, + Instruction::Copy { .. }, + Instruction::PopExcept, + Instruction::Reraise { .. }, + Instruction::LoadFast { .. }, + ] + ) + }), + "expected CPython-style handler cleanup before following code, got ops={ops:?}" ); } #[test] - fn test_inlined_comprehension_restore_does_not_form_store_fast_load_fast() { + fn test_try_else_for_cleanup_drops_redundant_jump_nop() { let code = compile_exec( "\ -def f(e): - e[1:3] = [g(i) for i in range(2)] - -def g(datadir): - files = [filename[:-4] for filename in sorted(os.listdir(datadir)) if filename.endswith('.xml')] - input_files = [filename for filename in files if filename.startswith('in')] - return files, input_files +def f(self, xs, ys, cm1, cm2): + for x in xs: + with self.subTest(x=x): + try: + with cm1: + self.a() + except Exception: + if x: + pass + else: + raise + else: + for y in ys: + with self.subTest(y=y): + with cm2: + self.b() ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -18857,143 +20889,169 @@ def g(datadir): [ Instruction::EndFor, Instruction::PopIter, - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - Instruction::LoadFastBorrow { .. }, Instruction::LoadConst { .. }, - Instruction::StoreSubscr, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, ] ) }), - "expected CPython-style inlined comprehension restore before slice store, got ops={ops:?}" + "expected inner for cleanup to fall directly into surrounding with cleanup, got ops={ops:?}", ); assert!( - !ops.windows(3).any(|window| { + !ops.windows(8).any(|window| { matches!( window, [ - Instruction::StoreFastLoadFast { .. }, + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, Instruction::LoadConst { .. }, - Instruction::StoreSubscr, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, ] ) }), - "inlined comprehension restore should not be folded into STORE_FAST_LOAD_FAST, got ops={ops:?}" + "expected CPython-style removal of the redundant jump NOP after for cleanup, got ops={ops:?}", ); - - let g = find_code(&code, "g").expect("missing g code"); - let g_ops: Vec<_> = g - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - assert!( - g_ops.windows(4).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::StoreFast { .. }, - Instruction::StoreFast { .. }, - ] - ) - }), - "expected CPython-style static swap over STORE_FAST_MAYBE_NULL restore, got ops={g_ops:?}" - ); - assert!( - !g_ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::Swap { .. }, - Instruction::StoreFast { .. }, - Instruction::StoreFast { .. }, - ] - ) - }), - "inlined comprehension restore should statically remove SWAP before adjacent stores, got ops={g_ops:?}" - ); - } + } #[test] - fn test_single_mode_folded_multiline_constant_does_not_leave_nops() { - let code = compile_single( + fn test_non_none_final_return_is_not_duplicated() { + let code = compile_exec( "\ -(- - - - - - 1) +def f(p, s): + if p == '': + if s == '': + return 0 + return -1 ", ); + let f = find_code(&code, "f").expect("missing function code"); + let minus_one_loads = f + .instructions + .iter() + .filter(|unit| { + matches!( + unit.op, + Instruction::LoadConst { consti } + if matches!( + f.constants.get( + consti + .get(OpArg::new(u32::from(u8::from(unit.arg)))) + .as_usize() + ), + Some(ConstantData::Integer { value }) if value == &BigInt::from(-1) + ) + ) + }) + .count(); - assert!( - !code - .instructions + assert_eq!( + minus_one_loads, + 1, + "expected a single final return -1 epilogue, got ops={:?}", + f.instructions .iter() - .any(|unit| matches!(unit.op, Instruction::Nop)), - "expected folded single-mode multiline constant to drop NOP anchors, got instructions={:?}", - code.instructions + .map(|unit| unit.op) + .collect::>() ); } #[test] - fn test_folded_multiline_tuple_constant_does_not_leave_operand_nops() { + fn test_for_return_unary_constant_preserves_value_over_iterator_cleanup() { let code = compile_exec( "\ -values = ( - (1 + 1j, 0 + 0j), - (1 + 1j, 0.0), - (1 + 1j, 0), -) +def f(xs): + for x in xs: + return -1 ", ); + let f = find_code(&code, "f").expect("missing function code"); + let units: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); assert!( - !code - .instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::Nop)), - "expected CPython nop_out-style folded tuple operands to have no surviving NOPs, got instructions={:?}", - code.instructions + units.windows(4).any(|window| { + matches!( + window[0].op, + Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. } + ) && matches!( + window[1].op, + Instruction::Swap { i } + if i.get(OpArg::new(u32::from(u8::from(window[1].arg)))) == 2 + ) && matches!(window[2].op, Instruction::PopTop) + && matches!(window[3].op, Instruction::ReturnValue) + }), + "expected CPython-style LOAD_CONST/SWAP/POP_TOP/RETURN_VALUE cleanup, got units={units:?}" ); } #[test] - fn test_folded_multiline_bytes_binop_does_not_leave_operand_nops() { + fn test_try_else_if_return_keeps_conditional_target_nop() { let code = compile_exec( "\ -def f(self, out): - self.assertIn( - b'gnu' + (b'/123' * 125) + b'/longlink' + (b'/123' * 125) + b'/longname', - out) +def f(cond): + try: + x = cond + except E: + pass + else: + if x: + return 1 + return 2 ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let has_cpython_nop_target = ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, + Instruction::ReturnValue, + Instruction::Nop, + Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, + Instruction::ReturnValue, + ] + ) + }); assert!( - !f.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::Nop)), - "expected CPython nop_out-style folded operands to have no surviving NOPs, got instructions={:?}", - f.instructions + has_cpython_nop_target, + "expected CPython-style try-else conditional target NOP, got ops={ops:?}" ); } #[test] - fn test_folded_binop_at_branch_body_start_does_not_leave_nop() { + fn test_try_else_nested_if_return_drops_inner_conditional_target_nop() { let code = compile_exec( "\ -def f(sys): - if sys.platform == 'win32': - component = 'd' * 25 - return component +def f(obj, Sig): + try: + sig = obj.__signature__ + except AttributeError: + pass + else: + if sig is not None: + if not isinstance(sig, Sig): + raise TypeError(sig) + return sig + return obj ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19002,31 +21060,52 @@ def f(sys): .collect(); assert!( - !ops.windows(3).any(|window| { + !ops.windows(4).any(|window| { matches!( window, [ - Instruction::NotTaken, + Instruction::RaiseVarargs { .. }, Instruction::Nop, - Instruction::LoadConst { .. } + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::ReturnValue, ] ) }), - "expected CPython nop_out-style folded branch body to drop operand NOP, got ops={ops:?}", + "inner try-else if target should not materialize as a NOP before the return, got ops={ops:?}" + ); + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::RaiseVarargs { .. }, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::ReturnValue, + Instruction::Nop, + ] + ) + }), + "expected only the outer try-else if false target after the return, got ops={ops:?}" ); } #[test] - fn test_folded_iterable_at_assert_target_does_not_leave_nop() { + fn test_try_else_nested_final_if_return_drops_nested_conditional_target_nop() { let code = compile_exec( - r#" -def f(caches, non_caches): - assert 1 / 3 <= caches / non_caches, "this test needs more caches!" - for show_caches in (False, True): + "\ +def f(cond, outer): + try: + x = cond + except E: pass -"#, + else: + if outer: + if x: + return 1 + return 2 +", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19035,24 +21114,34 @@ def f(caches, non_caches): .collect(); assert!( - !ops.windows(2).any(|window| { - matches!(window, [Instruction::Nop, Instruction::LoadConst { .. }]) + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::ReturnValue, + Instruction::Nop, + Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }, + Instruction::ReturnValue, + ] + ) }), - "expected folded for-iterable at assert target to drop operand NOP, got ops={ops:?}", + "nested try-else conditional target should fall through directly to following code, got ops={ops:?}" ); } #[test] - fn test_multiline_unpack_target_uses_element_locations() { + fn test_named_except_conditional_branch_duplicates_cleanup_return() { let code = compile_exec( "\ -def f(cm): - with cm as (_, - filename_2): - return filename_2 +def f(self): + try: + raise TypeError('x') + except TypeError as e: + if '+' not in str(e): + self.fail('join() ate exception message') ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19060,25 +21149,43 @@ def f(cm): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert!( - !ops.iter() - .any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })), - "expected multiline target elements to keep separate STORE_FAST instructions, got ops={ops:?}", + let cleanup_return_count = ops + .windows(6) + .filter(|window| { + matches!( + window, + [ + Instruction::PopExcept, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. } | Instruction::StoreName { .. }, + Instruction::DeleteFast { .. } | Instruction::DeleteName { .. }, + Instruction::LoadConst { .. }, + Instruction::ReturnValue, + ] + ) + }) + .count(); + + assert_eq!( + cleanup_return_count, 2, + "expected duplicated named-except cleanup return blocks, got ops={ops:?}" ); } #[test] - fn test_or_condition_in_jump_context_uses_shared_true_fallthrough() { + fn test_named_except_conditional_before_explicit_return_shares_cleanup_return() { let code = compile_exec( "\ -def f(lines): - for line in lines: - if line.startswith('--') or not line.strip(): - continue - return line +def f(onerror, err, OSError): + try: + x() + except OSError as err: + if onerror is not None: + onerror(err) + return ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19086,40 +21193,39 @@ def f(lines): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let first_pop_jump = ops - .iter() - .find(|op| { + let cleanup_return_count = ops + .windows(6) + .filter(|window| { matches!( - op, - Instruction::PopJumpIfTrue { .. } | Instruction::PopJumpIfFalse { .. } - ) - }) - .copied() - .expect("missing conditional jump"); - assert!( - matches!(first_pop_jump, Instruction::PopJumpIfTrue { .. }), - "expected first OR branch to jump on true into shared fallthrough, got ops={ops:?}" + window, + [ + Instruction::PopExcept, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. } | Instruction::StoreName { .. }, + Instruction::DeleteFast { .. } | Instruction::DeleteName { .. }, + Instruction::LoadConst { .. }, + Instruction::ReturnValue, + ] + ) + }) + .count(); + + assert_eq!( + cleanup_return_count, 1, + "explicit return after the conditional should share the named-except cleanup return block, got ops={ops:?}" ); } #[test] - fn test_loop_break_bool_chain_reorders_false_path_to_jump_back() { + fn test_listcomp_cleanup_tail_keeps_split_store_fast_pair() { let code = compile_exec( "\ -def f(filters, text, category, module, lineno, defaultaction): - for item in filters: - action, msg, cat, mod, ln = item - if ((msg is None or msg.match(text)) and - issubclass(category, cat) and - (mod is None or mod.match(module)) and - (ln == 0 or lineno == ln)): - break - else: - action = defaultaction - return action +def f(escaped_string, quote_types): + possible_quotes = [q for q in quote_types if q not in escaped_string] + return possible_quotes ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19127,36 +21233,36 @@ def f(filters, text, category, module, lineno, defaultaction): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let pop_iter_idx = ops + .iter() + .position(|op| matches!(op, Instruction::PopIter)) + .expect("missing POP_ITER"); + let tail = &ops[pop_iter_idx + 1..]; + assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::ToBool, - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadGlobal { .. }, - ] - ) - }), - "expected CPython-style false path to fall through into loop jump-back, got ops={ops:?}" + matches!( + tail, + [ + Instruction::StoreFast { .. }, + Instruction::StoreFast { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::ReturnValue, + .. + ] + ), + "expected split STORE_FAST pair after listcomp cleanup, got ops={ops:?}" ); } #[test] - fn test_loop_conditional_body_keeps_duplicate_jump_back_paths() { + fn test_dictcomp_cleanup_tail_keeps_split_store_fast_pair() { let code = compile_exec( "\ -def f(new, old): - for replace in ['__module__', '__name__', '__qualname__', '__doc__']: - if hasattr(old, replace): - setattr(new, replace, getattr(old, replace)) - return new +def f(obj, g): + return {g(k): g(v) for k, v in obj.items()} ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19164,53 +21270,34 @@ def f(new, old): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let jump_back_count = ops + let pop_iter_idx = ops .iter() - .filter(|op| { - matches!( - op, - Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } - ) - }) - .count(); - assert!( - jump_back_count >= 2, - "expected separate false-path and body jump-back blocks, got ops={ops:?}" - ); + .position(|op| matches!(op, Instruction::PopIter)) + .expect("missing POP_ITER"); + let tail = &ops[pop_iter_idx + 1..]; + assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::ToBool, - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadGlobal { .. }, - ] - ) - }), - "expected false path to jump back before body, got ops={ops:?}" + matches!( + tail, + [ + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + Instruction::StoreFast { .. }, + Instruction::ReturnValue, + .. + ] + ), + "expected split STORE_FAST pair after dictcomp cleanup, got ops={ops:?}" ); } #[test] - fn test_loop_multiblock_conditional_body_keeps_body_before_jump_back() { + fn test_static_swap_triple_assign_keeps_store_fast_store_fast() { let code = compile_exec( "\ -def f(random, d, f): - for dummy in range(100): - k = random.choice('abc') - if random.random() < 0.2: - if k in d: - del d[k] - del f[k] - else: - v = random.choice((1, 2)) - d[k] = v - f[k] = v - check(f[k], v) +def f(x, y, z): + a, b, a = x, y, z + return a ", ); let f = find_code(&code, "f").expect("missing function code"); @@ -19222,33 +21309,30 @@ def f(random, d, f): .collect(); assert!( - ops.windows(5).any(|window| { + ops.windows(3).any(|window| { matches!( window, [ - Instruction::ContainsOp { .. }, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::LoadFastBorrowLoadFastBorrow { .. }, - Instruction::DeleteSubscr, + Instruction::Swap { .. }, + Instruction::StoreFastStoreFast { .. }, + Instruction::StoreFast { .. } ] ) }), - "expected CPython-style multi-block body before false jump-back, got ops={ops:?}" + "expected CPython-style SWAP/STORE_FAST_STORE_FAST/STORE_FAST sequence, got ops={ops:?}" ); } #[test] - fn test_loop_not_conditional_body_threads_true_path_to_jump_back() { + fn test_static_swap_duplicate_pair_eliminates_swap() { let code = compile_exec( "\ -def f(xs): - for x in xs: - if not x: - g(x) +def f(x, y): + a, a = x, y + return a ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19257,34 +21341,27 @@ def f(xs): .collect(); assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadGlobal { .. }, - ] - ) + !ops.iter().any(|op| matches!(op, Instruction::Swap { .. })), + "duplicate pair assignment should statically eliminate SWAP, got ops={ops:?}" + ); + assert!( + ops.windows(2).any(|window| { + matches!(window, [Instruction::StoreFast { .. }, Instruction::PopTop]) }), - "expected CPython-style true path to jump back before not-body, got ops={ops:?}" + "expected CPython-style STORE_FAST/POP_TOP duplicate assignment, got ops={ops:?}" ); } #[test] - fn test_loop_if_pass_uses_line_bearing_jump_back_instead_of_nop() { + fn test_static_swap_duplicate_prefix_eliminates_swap() { let code = compile_exec( "\ -def f(x, y): - for i in x: - if y: - pass +def f(x, y, z): + a, a, b = x, y, z + return a ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19293,51 +21370,30 @@ def f(x, y): .collect(); assert!( - ops.windows(5).any(|window| { + !ops.iter().any(|op| matches!(op, Instruction::Swap { .. })), + "duplicate-prefix assignment should statically eliminate SWAP, got ops={ops:?}" + ); + assert!( + ops.windows(2).any(|window| { matches!( window, - [ - Instruction::ToBool, - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - ] + [Instruction::StoreFastStoreFast { .. }, Instruction::PopTop] ) }), - "expected CPython-style synthetic false-path jump-back plus body jump-back, got ops={ops:?}" - ); - assert!( - !ops.iter().any(|op| matches!(op, Instruction::Nop)), - "expected pass body line to attach to loop backedge instead of leaving a NOP, got ops={ops:?}" + "expected CPython-style STORE_FAST_STORE_FAST/POP_TOP duplicate prefix, got ops={ops:?}" ); } #[test] - fn test_nested_if_shared_jump_back_target_is_duplicated() { + fn test_constant_if_expression_stmt_in_loop_removes_empty_body() { let code = compile_exec( "\ -def f(s, size, encodeSetO, encodeWhiteSpace): - inShift = True - base64bits = 0 - out = [] - for i, ch in enumerate(s): - if base64bits == 0: - if i + 1 < size: - ch2 = s[i + 1] - if E(ch2, encodeSetO, encodeWhiteSpace): - if B(ch2) or ch2 == '-': - out.append(b'-') - inShift = False - else: - out.append(b'-') - inShift = False - return out +def f(x): + while x: + 0 if 1 else 0 ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19346,42 +21402,21 @@ def f(s, size, encodeSetO, encodeWhiteSpace): .collect(); assert!( - ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::PopTop, - Instruction::LoadConst { .. }, - Instruction::StoreFast { .. }, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, - ] - ) - }), - "expected separate nested-if and outer-if jump-back tails, got ops={ops:?}" + !ops.iter() + .any(|op| matches!(op, Instruction::LoadSmallInt { .. })), + "expected constant if-expression statement to compile away inside loop, got ops={ops:?}" ); } #[test] - fn test_protected_loop_conditional_keeps_forward_body_entry() { + fn test_if_expression_in_jump_context_skips_constant_true_arm_load() { let code = compile_exec( "\ -def outer(it, C1): - def f(): - for x in it: - try: - if C1: - yield 2 - except OSError: - pass - return f +def f(): + a if (1 if b else c) else d ", ); - let outer = find_code(&code, "outer").expect("missing outer code"); - let f = find_code(outer, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19390,42 +21425,23 @@ def outer(it, C1): .collect(); assert!( - ops.windows(7).any(|window| { - matches!( - window, - [ - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::LoadSmallInt { .. }, - Instruction::YieldValue { .. }, - Instruction::Resume { .. }, - Instruction::PopTop, - ] - ) - }), - "expected protected conditional to keep CPython-style forward body entry, got ops={ops:?}" + !ops.iter() + .any(|op| matches!(op, Instruction::LoadSmallInt { .. })), + "expected jump-context if-expression to avoid materializing constant truthy arm, got ops={ops:?}" ); } #[test] - fn test_nested_except_false_path_duplicates_pop_except_jump_back_tail() { + fn test_with_suppress_tail_duplicates_final_return_none() { let code = compile_exec( "\ -def f(it, C3): - for x in it: - try: - X = 3 - except OSError: - try: - if C3: - X = 4 - except OSError: - pass - return 42 +def f(cm, cond): + if cond: + with cm(): + pass ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19433,50 +21449,34 @@ def f(it, C3): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); + let return_count = ops + .iter() + .filter(|op| matches!(op, Instruction::ReturnValue)) + .count(); + + assert_eq!( + return_count, 3, + "expected duplicated return-none epilogues, got ops={ops:?}" + ); assert!( - ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::LoadSmallInt { .. }, - Instruction::StoreFast { .. }, - Instruction::PopExcept, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::PopExcept, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - ] - ) - }), - "expected CPython-style duplicated false-path exit tail, got ops={ops:?}" + !ops.iter() + .any(|op| matches!(op, Instruction::JumpBackwardNoInterrupt { .. })), + "with suppress tail should not jump back to shared return block, got ops={ops:?}" ); } #[test] - fn test_more_nested_except_false_paths_duplicate_all_jump_back_tails() { + fn test_with_conditional_bare_return_keeps_return_line_nop_before_exit_cleanup() { let code = compile_exec( "\ -def f(it, C3, C4): - for x in it: - try: - X = 3 - except OSError: - try: - if C3: - if C4: - X = 4 - except OSError: - try: - if C3: - if C4: - X = 5 - except OSError: - pass - return 42 +def f(cm, registry, altkey): + with cm: + if registry.get(altkey): + return + registry[altkey] = 1 ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19489,34 +21489,38 @@ def f(it, C3, C4): matches!( window, [ - Instruction::LoadSmallInt { .. }, - Instruction::StoreFast { .. }, - Instruction::PopExcept, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::PopExcept, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::PopExcept, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::LoadConst { .. }, + Instruction::ReturnValue, ] ) }), - "expected CPython-style duplicated nested false-path exit tails, got ops={ops:?}" + "expected CPython-style return-line NOP before with-exit cleanup return, got ops={ops:?}" ); } #[test] - fn test_no_wraparound_jump_keeps_forward_hop_before_loop_backedge() { + fn test_multiline_nested_with_return_finally_keeps_inner_cleanup_anchor_nop() { let code = compile_exec( "\ -def while_not_chained(a, b, c): - while not (a < b < c): - pass +def f(a, b, path): + try: + with cm(a) as x, \\ + cm(b): + return g(x).copy() + finally: + try: + cleanup(path) + except ValueError: + pass ", ); - let f = find_code(&code, "while_not_chained").expect("missing while_not_chained code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19525,38 +21529,39 @@ def while_not_chained(a, b, c): .collect(); assert!( - ops.windows(5).any(|window| { + ops.windows(7).any(|window| { matches!( window, [ - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpForward { .. }, - Instruction::PopTop, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::Copy { .. }, + Instruction::PopExcept, + Instruction::Reraise { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, ] ) }), - "expected CPython-style no-wraparound forward hop before the loop backedge, got ops={ops:?}" + "multi-line nested with return/finally cleanup should keep CPython's inner item anchor NOP before outer __exit__, got ops={ops:?}" ); } #[test] - fn test_while_break_else_keeps_true_edge_into_forward_break_body() { + fn test_try_finally_conditional_return_duplicates_finally_exit_return() { let code = compile_exec( "\ -def f(i): - while i: - i -= 1 - if i < 4: - break - else: - print('x') - print('y') +def f(flag, data, callback): + try: + if flag: + return + value = 1 + finally: + if data: + callback(data) ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19564,36 +21569,34 @@ def f(i): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert!( - ops.windows(4).any(|window| { - matches!( - window, - [ - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::JumpForward { .. }, - ] - ) - }), - "expected CPython-style true edge into forward break body with false path falling into the loop backedge, got ops={ops:?}" + let return_count = ops + .iter() + .filter(|op| matches!(op, Instruction::ReturnValue)) + .count(); + assert_eq!( + return_count, 4, + "try-finally return unwind should keep CPython-style distinct true/false finalbody exits, got ops={ops:?}" ); } #[test] - fn test_nested_if_continue_reorders_false_path_to_loop_backedge() { + fn test_named_except_conditional_cleanup_is_inlined_per_branch() { let code = compile_exec( "\ -def f(items, changes): - for x in items: - if not x: - if x in changes: - raise TypeError - continue +def f(self, logger): + try: + work() + except A as exc: + if not self.closing: + self.fatal(exc, 'msg') + elif self.loop.get_debug(): + logger.debug('closing', exc_info=True) + finally: + if self.length > -1: + self.recv() ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19601,37 +21604,41 @@ def f(items, changes): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert!( - ops.windows(7).any(|window| { + let cleanup_after_branch_count = ops + .windows(6) + .filter(|window| { matches!( window, [ - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadFastBorrowLoadFastBorrow { .. } - | Instruction::LoadFastLoadFast { .. }, - Instruction::ContainsOp { .. }, - Instruction::PopJumpIfFalse { .. }, + Instruction::PopTop, + Instruction::PopExcept, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. }, + Instruction::DeleteFast { .. }, + Instruction::JumpBackwardNoInterrupt { .. }, ] ) - }), - "expected nested if/continue to keep CPython-style false-edge jump-back tails, got ops={ops:?}" + }) + .count(); + assert_eq!( + cleanup_after_branch_count, 2, + "named except branch exits should inline cleanup like CPython, got ops={ops:?}" ); } #[test] - fn test_loop_assert_keeps_false_edge_into_raise_body() { + fn test_try_finally_exception_path_duplicates_conditional_reraise() { let code = compile_exec( "\ -def f(bytecode): - for instr, positions in zip(bytecode, bytecode.codeobj.co_positions()): - assert instr.positions == positions +def f(flag, callback): + try: + work() + finally: + if flag: + callback() ", ); - let f = find_code(&code, "f").expect("missing f code"); + let f = find_code(&code, "f").expect("missing function code"); let ops: Vec<_> = f .instructions .iter() @@ -19639,38 +21646,26 @@ def f(bytecode): .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - assert!( - ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::CompareOp { .. }, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadCommonConstant { .. }, - Instruction::RaiseVarargs { .. }, - ] - ) - }), - "expected loop assert to keep CPython-style false-edge into the raise body, got ops={ops:?}" + let reraise_count = ops + .iter() + .filter(|op| matches!(op, Instruction::Reraise { .. })) + .count(); + assert_eq!( + reraise_count, 3, + "try-finally exception finalbody should duplicate CPython no-location RERAISE exits, got ops={ops:?}" ); } #[test] - fn test_and_is_not_none_loop_guard_uses_direct_jump_back_false_path() { + fn test_genexpr_compare_header_uses_store_fast_load_fast_like_cpython() { let code = compile_exec( "\ -def f(code): - last_line = -2 - for _, _, line in code.co_lines(): - if line is not None and line != last_line: - last_line = line +def f(it): + return (offset == (4, 10) for offset in it) ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let genexpr = find_code(&code, "").expect("missing code"); + let ops: Vec<_> = genexpr .instructions .iter() .map(|unit| unit.op) @@ -19678,841 +21673,5002 @@ def f(code): .collect(); assert!( - ops.windows(6).any(|window| { + ops.windows(3).any(|window| { matches!( window, [ - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::PopJumpIfNotNone { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadFastBorrowLoadFastBorrow { .. } - | Instruction::LoadFastLoadFast { .. }, + Instruction::StoreFastLoadFast { .. }, + Instruction::LoadConst { .. }, Instruction::CompareOp { .. }, ] ) }), - "expected CPython-style direct jump-back false path for 'is not None and ...', got ops={ops:?}" + "expected CPython-style STORE_FAST_LOAD_FAST compare header, got ops={ops:?}" ); } #[test] - fn test_large_is_not_none_loop_guard_uses_direct_jump_back_false_path() { + fn test_fstring_adjacent_literals_are_merged() { let code = compile_exec( "\ -def f(cls, _FIELDS, _PARAMS): - all_frozen_bases = None - any_frozen_base = False - has_dataclass_bases = False - for b in cls.__mro__[-1:0:-1]: - base_fields = getattr(b, _FIELDS, None) - if base_fields is not None: - has_dataclass_bases = True - for field in base_fields.values(): - name = field.name - if all_frozen_bases is None: - all_frozen_bases = True - current_frozen = getattr(b, _PARAMS).frozen - all_frozen_bases = all_frozen_bases and current_frozen - any_frozen_base = any_frozen_base or current_frozen +def f(cls, proto): + raise TypeError( + f\"cannot pickle {cls.__name__!r} object: \" + f\"a class that defines __slots__ without \" + f\"defining __getstate__ cannot be pickled \" + f\"with protocol {proto}\" + ) ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let string_consts = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + .filter_map(|unit| match unit.op { + Instruction::LoadConst { consti } => { + Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))]) + } + _ => None, + }) + .filter_map(|constant| match constant { + ConstantData::Str { value } => Some(value.to_string()), + _ => None, + }) + .collect::>(); assert!( - ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::PopJumpIfNotNone { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadConst { .. }, - Instruction::StoreFast { .. }, - ] - ) + string_consts.iter().any(|value| { + value + == " object: a class that defines __slots__ without defining __getstate__ cannot be pickled with protocol " }), - "expected CPython-style direct jump-back false path for large 'is not None' loop body, got ops={ops:?}" + "expected merged trailing f-string literal, got {string_consts:?}" + ); + assert!( + !string_consts.iter().any(|value| value == " object: "), + "did not expect split trailing literal, got {string_consts:?}" ); } #[test] - fn test_continue_inside_with_keeps_line_marker_nop_before_exit_cleanup() { + fn test_literal_only_fstring_statement_is_optimized_away() { let code = compile_exec( "\ -def f(it): - for func in it: - with cm(): - if cond(): - continue +def f(): + f'''Not a docstring''' ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + let f = find_code(&code, "f").expect("missing function code"); assert!( - ops.windows(9).any(|window| { - matches!( - window, - [ - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::Nop, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::PopTop, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - ] - ) - }), - "expected CPython-style line-marker NOP before with-exit cleanup on continue, got ops={ops:?}" + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::PopTop)), + "literal-only f-string statement should be removed" + ); + assert!( + !f.constants.iter().any(|constant| matches!( + constant, + ConstantData::Str { value } if value.to_string() == "Not a docstring" + )), + "literal-only f-string should not survive in constants" ); } #[test] - fn test_nested_async_with_normal_cleanup_drops_pop_block_nop() { + fn test_empty_fstring_literals_are_elided_around_interpolation() { let code = compile_exec( "\ -async def foo(): - async with CM(): - async with CM(): - raise RuntimeError +def f(x): + if '' f'{x}': + return 1 + return 2 ", ); - let f = find_code(&code, "foo").expect("missing foo code"); - let ops: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + + let empty_string_loads = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - - assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::GetAwaitable { .. }, - ] - ) - }), - "expected CPython-style async-with normal cleanup without a POP_BLOCK NOP, got ops={ops:?}" - ); - assert!( - !ops.windows(6).any(|window| { + .filter_map(|unit| match unit.op { + Instruction::LoadConst { consti } => { + Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))]) + } + _ => None, + }) + .filter(|constant| { matches!( - window, - [ - Instruction::Nop, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - Instruction::GetAwaitable { .. }, - ] + constant, + ConstantData::Str { value } if value.is_empty() ) - }), - "unexpected POP_BLOCK NOP before async-with normal cleanup, got ops={ops:?}" - ); + }) + .count(); + let build_string_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::BuildString { .. })) + .count(); + + assert_eq!(empty_string_loads, 0); + assert_eq!(build_string_count, 0); } #[test] - fn test_try_loop_elif_places_return_before_orelse_tail() { - let code = compile_exec( - "\ -def f(source, suggest, tb, s): - if source is not None: - try: - tb = tb - except Exception: - suggest = False - tb = None - if tb is not None: - for frame in tb: - s += frame - elif suggest: - s += 'x' - return s -", - ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + fn test_large_fstring_uses_join_list_like_cpython() { + let mut source = String::from("def f(x):\n return f\""); + for _ in 0..=STACK_USE_GUIDELINE { + source.push_str("{x}"); + } + source.push_str("\"\n"); + + let code = compile_exec(&source); + let f = find_code(&code, "f").expect("missing function code"); + let build_string_count = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + .filter(|unit| matches!(unit.op, Instruction::BuildString { .. })) + .count(); + let list_append_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::ListAppend { .. })) + .count(); + let join_attr_count = f + .instructions + .iter() + .filter(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + load_attr.is_method() + && f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "join" + } + _ => false, + }) + .count(); - let has_direct_return = ops.windows(8).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::ReturnValue, - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - ] - ) - }); - let has_nop_anchored_return = ops.windows(9).any(|window| { - matches!( - window, - [ - Instruction::EndFor, - Instruction::PopIter, - Instruction::Nop, - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::ReturnValue, - Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - ] - ) - }); - assert!( - has_direct_return || has_nop_anchored_return, - "expected CPython-style duplicated return between loop exit and elif tail, got ops={ops:?}" + assert_eq!(build_string_count, 0); + assert_eq!( + list_append_count, + usize::try_from(STACK_USE_GUIDELINE + 1).unwrap() ); + assert_eq!(join_attr_count, 1); } #[test] - fn test_constant_false_while_else_deopts_post_else_borrows() { + fn test_large_power_is_not_constant_folded() { + let code = compile_exec("x = 2**100\n"); + + assert!(code.instructions.iter().any(|unit| match unit.op { + Instruction::BinaryOp { op } => { + op.get(OpArg::new(u32::from(u8::from(unit.arg)))) == oparg::BinaryOperator::Power + } + _ => false, + })); + } + + #[test] + fn test_string_and_bytes_binops_constant_fold_like_cpython() { let code = compile_exec( "\ -def f(self): - x = 0 - while 0: - x = 1 - else: - x = 2 - self.assertEqual(x, 2) -", +x = b'\\\\' + b'u1881'\n\ +y = 103 * 'a' + 'x'\n", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - let assert_idx = ops - .iter() - .position(|op| matches!(op, Instruction::LoadAttr { .. })) - .expect("missing assertEqual call"); - let window = &ops[assert_idx.saturating_sub(1)..(assert_idx + 3).min(ops.len())]; + assert!( - matches!( - window, - [ - Instruction::LoadFast { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadFast { .. }, - .. - ] - ), - "expected post-else assertEqual call to use plain LOAD_FAST, got ops={window:?}" + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "unexpected runtime BINARY_OP in folded string/bytes constants: {:?}", + code.instructions ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Bytes { value } if value == b"\\u1881" + ))); + let expected = format!("{}x", "a".repeat(103)); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Str { value } + if value.to_string() == expected + ))); } #[test] - fn test_single_unpack_assignment_disables_constant_collection_folding() { - let code = compile_exec("a, b, c = 1, 2, 3\n"); - - assert!( - !code.instructions.iter().any(|unit| { - matches!(unit.op, Instruction::UnpackSequence { .. }) - || matches!(unit.op, Instruction::LoadConst { .. }) - && matches!( - code.constants.get(usize::from(u8::from(unit.arg))), - Some(ConstantData::Tuple { .. }) - ) - }), - "single unpack assignment should keep builder form for later lowering, got ops={:?}", - code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() + fn test_float_floor_division_constant_folds_like_cpython() { + let code = compile_exec( + "\ +x = 1.0 // 0.1\n\ +y = 1.0 % 0.1\n\ +z = 1e300 * 1e300 * 0\n", ); + assert!( - code.instructions + !code + .instructions .iter() - .filter(|unit| matches!(unit.op, Instruction::LoadSmallInt { .. })) - .count() - >= 3, - "expected individual constant loads before unpack-target stores, got ops={:?}", + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "float constant floor-div/mod should fold away, got instructions={:?}", code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Float { value } if value.to_bits() == 9.0f64.to_bits() + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Float { value } + if value.to_bits() == 0.09999999999999995f64.to_bits() + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Float { value } if value.is_nan() + ))); } #[test] - fn test_chained_unpack_assignment_keeps_constant_collection_folding() { - let code = compile_exec("(a, b) = c = d = (1, 2)\n"); + fn test_float_power_overflow_constant_does_not_fold() { + let code = compile_exec("x = 1e300 ** 2\n"); assert!( + code.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::BinaryOp { op } + if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == oparg::BinaryOperator::Power + )), + "overflowing float power should stay runtime like CPython, got instructions={:?}", code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadConst { .. })), - "chained unpack assignment should keep tuple constant, got ops={:?}", - code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() ); + } + + #[test] + fn test_large_string_and_bytes_binops_constant_fold_like_cpython() { + let code = compile_exec( + r#" +encoded = b'\xff\xfe\x00\x00' + b'\x00\x00\x01\x00' * 1024 +text = '\U00010000' * 1024 +"#, + ); + assert!( - code.instructions + !code + .instructions .iter() - .any(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })), - "chained unpack assignment should still unpack the copied tuple, got ops={:?}", + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "large safe string/bytes constants should fold away, got instructions={:?}", code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Bytes { value } if value.len() == 4100 + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Str { value } if value.code_points().count() == 1024 + ))); } #[test] - fn test_constant_true_assert_skips_message_nested_scope() { - let code = compile_exec("assert 1, (lambda x: x + 1)\n"); - - assert_eq!( - code.constants - .iter() - .filter(|constant| matches!(constant, ConstantData::Code { .. })) - .count(), - 0, - "constant-true assert should not compile the skipped message lambda" + fn test_constant_string_subscript_folds_inside_collection() { + let code = compile_exec( + "\ +values = [item for item in [r\"\\\\'a\\\\'\", r\"\\t3\", r\"\\\\\"[0]]]\n", ); + assert!( !code .instructions .iter() - .any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })), - "constant-true assert should be elided, got ops={:?}", + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "unexpected runtime BINARY_OP after constant subscript folding: {:?}", code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements.len() == 3 + && matches!(&elements[2], ConstantData::Str { value } if value.to_string() == "\\") + ))); } #[test] - fn test_constant_false_assert_uses_direct_raise_shape() { - let code = compile_exec("assert 0, (lambda x: x + 1)\n"); + fn test_constant_string_subscript_with_surrogate_skips_lossy_fold() { + let code = compile_exec("value = \"\\ud800\"[0]\n"); assert!( - !code.instructions.iter().any(|unit| { - matches!( - unit.op, - Instruction::ToBool - | Instruction::PopJumpIfTrue { .. } - | Instruction::PopJumpIfFalse { .. } - ) + code.instructions.iter().any(|unit| match unit.op { + Instruction::BinaryOp { op } => { + op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == oparg::BinaryOperator::Subscr + } + _ => false, }), - "constant-false assert should use direct raise shape, got ops={:?}", - code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); - assert!( - code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })), - "constant-false assert should still raise, got ops={:?}", + "expected runtime subscript for surrogate literal, got instructions={:?}", code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); - assert_eq!( - code.constants - .iter() - .filter(|constant| matches!(constant, ConstantData::Code { .. })) - .count(), - 1, - "constant-false assert should still compile the message lambda" ); } #[test] - fn test_constant_unary_positive_and_invert_fold() { - let code = compile_exec("x = +1\nx = ~1\n"); + fn test_constant_subscript_folds_in_load_context() { + let cases = [ + ("value = (1, 2, 3)[0]\n", Some(BigInt::from(1)), None), + ("value = b\"abc\"[0]\n", Some(BigInt::from(97)), None), + ("value = \"abc\"[0]\n", None, Some("a")), + ]; - assert!( - !code.instructions.iter().any(|unit| { - matches!( + for (source, expected_int, expected_str) in cases { + let code = compile_exec(source); + assert!( + !code.instructions.iter().any(|unit| matches!( unit.op, - Instruction::CallIntrinsic1 { .. } | Instruction::UnaryInvert - ) - }), - "constant unary ops should fold away, got ops={:?}", - code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); + Instruction::BinaryOp { op } + if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == oparg::BinaryOperator::Subscr + )), + "expected folded constant subscript for {source:?}, got instructions={:?}", + code.instructions + ); + + if let Some(expected_int) = expected_int.as_ref() { + let has_small_int = code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadSmallInt { i } + if BigInt::from(i.get(OpArg::new(u32::from(u8::from(unit.arg))))) + == *expected_int + ) + }); + let has_const_int = code.constants.iter().any(|constant| { + matches!(constant, ConstantData::Integer { value } if value == expected_int) + }); + assert!( + has_small_int || has_const_int, + "missing folded integer constant {expected_int} for {source:?}, instructions={:?}", + code.instructions + ); + } + + if let Some(expected_str) = expected_str { + assert!( + code.constants.iter().any(|constant| { + matches!(constant, ConstantData::Str { value } if value.to_string() == expected_str) + }), + "missing folded string constant {expected_str:?} for {source:?}", + ); + } + } } #[test] - fn test_bool_invert_is_not_const_folded() { - let code = compile_exec("x = ~True\n"); + fn test_constant_slice_subscript_folds_in_load_context() { + let code = compile_exec( + "\ +a = 'hello'[:4]\n\ +b = b'abcd'[1:3]\n\ +c = (1, 2, 3)[:2]\n", + ); assert!( + !code.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::BinaryOp { op } + if op.get(OpArg::new(u32::from(u8::from(unit.arg)))) + == oparg::BinaryOperator::Subscr + )), + "expected folded constant slice subscripts, got instructions={:?}", code.instructions - .iter() - .any(|unit| matches!(unit.op, Instruction::UnaryInvert)), - "~bool should remain unfurled to match CPython, got ops={:?}", - code.instructions - .iter() - .map(|unit| unit.op) - .collect::>() ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Str { value } if value.to_string() == "hell" + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Bytes { value } if value == b"bc" + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if matches!( + elements.as_slice(), + [ + ConstantData::Integer { value: a }, + ConstantData::Integer { value: b }, + ] if *a == BigInt::from(1) + && *b == BigInt::from(2) + ) + ))); } #[test] - fn test_optimized_assert_preserves_nested_scope_order() { - compile_exec_optimized( + fn test_list_of_constant_tuples_uses_list_extend() { + let code = compile_exec( "\ -class S: - def f(self, sequence): - _formats = [self._types_mapping[type(item)] for item in sequence] - _list_len = len(_formats) - assert sum(len(fmt) <= 8 for fmt in _formats) == _list_len - _recreation_codes = [self._extract_recreation_code(item) for item in sequence] +deprecated_cases = [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j')] ", ); - } - #[test] - fn test_optimized_assert_with_nested_scope_in_first_iter() { - // First iterator of a comprehension is evaluated in the enclosing - // scope, so nested scopes inside it (the generator here) must also - // be consumed when the assert is optimized away. - compile_exec_optimized( - "\ -def f(items): - assert [x for x in (y for y in items)] - return [x for x in items] -", + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), + "expected constant tuple list folding" ); } #[test] - fn test_optimized_assert_with_lambda_defaults() { - // Lambda default values are evaluated in the enclosing scope, - // so nested scopes inside defaults must be consumed. - compile_exec_optimized( + fn test_large_list_of_unary_constants_uses_list_extend() { + let code = compile_exec( "\ -def f(items): - assert (lambda x=[i for i in items]: x)() - return [x for x in items] +values = [-1, not True, ~0, +True, 5] ", ); + + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), + "expected unary-folded constants to participate in list folding, got instructions={:?}", + code.instructions + ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements.len() == 5 + && matches!(&elements[0], ConstantData::Integer { value } if *value == BigInt::from(-1)) + && matches!(&elements[1], ConstantData::Boolean { value } if !value) + && matches!(&elements[2], ConstantData::Integer { value } if *value == BigInt::from(-1)) + && matches!(&elements[3], ConstantData::Integer { value } if *value == BigInt::from(1)) + && matches!(&elements[4], ConstantData::Integer { value } if *value == BigInt::from(5)) + ))); } #[test] - fn test_try_else_nested_scopes_keep_subtable_cursor_aligned() { + fn test_outer_unary_after_binop_folds_before_list_folding() { let code = compile_exec( "\ -try: - import missing_mod -except ImportError: - def fallback(): - return 0 -else: - def impl(): - return reversed('abc') +values = [2.0**53, -0.5, -2.0**-54] ", ); assert!( - find_code(&code, "fallback").is_some(), - "missing fallback code" + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), + "expected binop-folded constants to participate in list folding, got instructions={:?}", + code.instructions ); - let impl_code = find_code(&code, "impl").expect("missing impl code"); assert!( - impl_code.instructions.iter().any(|unit| { - matches!( - unit.op, - Instruction::LoadGlobal { .. } | Instruction::LoadName { .. } - ) - }), - "expected impl to compile global name access, got ops={:?}", - impl_code + !code.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::BinaryOp { .. } | Instruction::UnaryNegative + )), + "constant expression list should not leave runtime ops, got instructions={:?}", + code.instructions + ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements.len() == 3 + && matches!(&elements[0], ConstantData::Float { value } if *value == 9007199254740992.0) + && matches!(&elements[1], ConstantData::Float { value } if *value == -0.5) + && matches!(&elements[2], ConstantData::Float { value } if value.is_sign_negative()) + ))); + } + + #[test] + fn test_negative_integer_power_folds_to_float_constant() { + let code = compile_exec("value = -3.0 * 2**(-333)\n"); + + assert!( + !code .instructions .iter() - .map(|unit| unit.op) - .collect::>() + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "negative integer power should fold through the enclosing multiply, got instructions={:?}", + code.instructions ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Float { value } + if value.is_sign_negative() && *value < 0.0 && value.abs() < 1.0e-90 + ))); } #[test] - fn test_nested_try_else_multi_resume_join_keeps_strong_load_fast_tail() { + fn test_complex_power_constants_fold_like_cpython() { let code = compile_exec( "\ -def f(msg): - s = '' - try: - import a - except Exception: - suggest = False - tb = None - else: - try: - suggest = not t() - tb = g(msg) - except Exception: - suggest = False - tb = None - if tb is not None: - for frame in tb: - s += frame - elif suggest: - s += 'y' - return s +one = 3j ** 0j +zero = 0j ** 2 ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f - .instructions - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); - let tail_start = ops - .iter() - .position(|op| matches!(op, Instruction::PopJumpIfNone { .. })) - .expect("missing tail POP_JUMP_IF_NONE") - .saturating_sub(1); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &ops[tail_start..handler_start]; + assert!( + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "safe complex power constants should fold away, got instructions={:?}", + code.instructions + ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Complex { value } if value.re == 1.0 && value.im == 0.0 + ))); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Complex { value } if value.re == 0.0 && value.im == 0.0 + ))); + } + + #[test] + fn test_zero_complex_power_exception_constants_do_not_fold() { + let code = compile_exec("value = 0j ** (3 - 2j)\n"); assert!( - !tail.iter().any(|op| { - matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "expected nested try/except else-resume tail to keep strong LOAD_FAST ops, got tail={tail:?}" + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "zero complex to complex power should stay runtime so ZeroDivisionError is preserved, got instructions={:?}", + code.instructions + ); + } + + #[test] + fn test_large_constant_list_keeps_streaming_build() { + let source = format!( + "values = [{}]\n", + (0..31) + .map(|i| format!("'v{i}'")) + .collect::>() + .join(", ") ); + let code = compile_exec(&source); assert!( - tail.iter() - .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), - "expected loop body to keep LOAD_FAST_LOAD_FAST in the resume tail, got tail={tail:?}" + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListAppend { .. })), + "large constant lists should keep LIST_APPEND streaming form, got instructions={:?}", + code.instructions + ); + assert!( + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListExtend { .. })), + "large constant lists should not fold to LIST_EXTEND, got instructions={:?}", + code.instructions ); } #[test] - fn test_protected_conditional_tail_keeps_strong_load_fast() { + fn test_large_constant_tuple_stream_folds_to_tuple_const() { + let source = format!( + "values = ({},)\n", + (0..31) + .map(|i| format!("'v{i}'")) + .collect::>() + .join(", ") + ); + let code = compile_exec(&source); + + assert!( + !code.instructions.iter().any(|unit| matches!( + unit.op, + Instruction::BuildList { .. } + | Instruction::ListAppend { .. } + | Instruction::CallIntrinsic1 { .. } + )), + "large constant tuple should fold the LIST_TO_TUPLE stream, got instructions={:?}", + code.instructions + ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } if elements.len() == 31 + ))); + } + + #[test] + fn test_annotation_closure_uses_format_varname() { let code = compile_exec( "\ -def f(m, class_name, category, warning_base): - try: - cat = getattr(m, class_name) - except AttributeError: - raise ValueError(category) - if not issubclass(cat, warning_base): - raise TypeError(category) - return cat +class C: + x: int ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + let varnames = annotate + .varnames + .iter() + .map(|name| name.as_str()) + .collect::>(); + assert_eq!(varnames, vec!["format"]); + } + + #[test] + fn test_type_param_evaluator_uses_dot_format_varname() { + let code = compile_exec( + "\ +class C[T: int]: + pass +", + ); + let evaluator = find_code(&code, "T").expect("missing type parameter evaluator"); + let varnames = evaluator + .varnames + .iter() + .map(|name| name.as_str()) + .collect::>(); + assert_eq!(varnames, vec![".format"]); + } + + #[test] + fn test_generic_class_double_star_bases_use_tuple_ex_call_path() { + let code = compile_exec( + "\ +def f(Base, kwargs): + class C[T](Base, **kwargs): + pass + return C +", + ); + let type_params = + find_code(&code, "").expect("missing type params code"); + let ops: Vec<_> = type_params .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let tail_start = ops - .iter() - .position(|op| matches!(op, Instruction::StoreFast { .. })) - .expect("missing STORE_FAST cat"); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &ops[tail_start + 1..handler_start]; - assert!( - !tail.iter().any(|op| { + !has_intrinsic_1(type_params, IntrinsicFunction1::ListToTuple), + "generic class call with **kwargs but no starred bases should not use list-to-tuple, got ops={ops:?}" + ); + assert!( + ops.windows(4).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::BuildTuple { .. }, + Instruction::BuildMap { .. }, + Instruction::LoadDeref { .. } | Instruction::LoadFast { .. }, + Instruction::DictMerge { .. }, + ] ) }), - "expected protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}" - ); - - assert!( - tail.iter() - .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), - "expected protected tail to keep LOAD_FAST_LOAD_FAST for issubclass args, got tail={tail:?}" + "expected CPython-style BUILD_TUPLE/BUILD_MAP/DICT_MERGE ex-call path, got ops={ops:?}" ); } #[test] - fn test_nonresuming_protected_conditional_tail_keeps_strong_load_fast() { + fn test_generic_function_defaults_call_type_params_like_cpython() { let code = compile_exec( "\ -def f(href, parse='xml'): - try: - data = XINCLUDE[href] - except KeyError: - raise OSError('resource not found') - if parse == 'xml': - data = ET.XML(data) - return data +def func[T](a: T = 'a', *, b: T = 'b'): + return a, b ", ); - let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let ops: Vec<_> = code .instructions .iter() .map(|unit| unit.op) .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let tail_start = ops - .iter() - .position(|op| matches!(op, Instruction::StoreFast { .. })) - .expect("missing protected STORE_FAST data"); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &ops[tail_start + 1..handler_start]; - assert!( - !tail.iter().any(|op| { + ops.windows(5).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::Swap { .. }, + Instruction::LoadConst { .. }, + Instruction::MakeFunction, + Instruction::Swap { .. }, + Instruction::Call { .. }, + ] ) }), - "expected non-resuming protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}" + "expected CPython generic defaults call pattern SWAP/MAKE_FUNCTION/SWAP/CALL, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::MakeFunction, + Instruction::Swap { .. }, + Instruction::Swap { .. }, + Instruction::PushNull, + Instruction::Swap { .. }, + ] + ) + }), + "generic defaults call should not use RustPython-specific PUSH_NULL reshuffle, got ops={ops:?}" ); } #[test] - fn test_optional_nonresuming_protected_tail_keeps_borrow() { + fn test_class_type_param_bound_prefers_classdict_over_outer_function_local() { let code = compile_exec( "\ -def f(b): - if type(b) is not bytes: - try: - b = bytes(memoryview(b)) - except TypeError: - raise TypeError(f'bad {type(b).__name__}') from None - if b: - sink(b) - return len(b) +def f(self): + class X: + T = int + def foo[U: T](self): ... + T, U = X.foo.__type_params__ + return T.__bound__, U.__bound__ ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f - .instructions - .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - let b_index = f - .varnames - .iter() - .position(|name| name.as_str() == "b") - .expect("missing b varname"); - let store_b = instructions - .iter() - .position(|unit| match unit.op { - Instruction::StoreFast { var_num } => { - usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) == b_index - } - _ => false, - }) - .expect("missing protected STORE_FAST b"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[store_b + 1..handler_start]; + assert!( + !f.cellvars.iter().any(|name| name == "T"), + "class-local type-param bound must not force outer function T cell, got cellvars={:?}", + f.cellvars + ); + let class_code = find_code(f, "X").expect("missing X class code"); assert!( - tail.iter() - .filter(|unit| match unit.op { - Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { - usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) - == b_index - } - _ => false, - }) - .all(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), - "optional protected tail should keep CPython-style borrowed b loads, got tail={tail:?}" + !class_code.freevars.iter().any(|name| name == "T"), + "class body must not close over outer T for class-local bound, got freevars={:?}", + class_code.freevars + ); + + let type_params = + find_code(class_code, "").expect("missing type params code"); + assert_eq!( + type_params + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["__classdict__"], + "type params scope should close only over __classdict__, got freevars={:?}", + type_params.freevars + ); + + let bound = find_code(type_params, "U").expect("missing U bound code"); + assert_eq!( + bound + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["__classdict__"], + "bound evaluator should close only over __classdict__, got freevars={:?}", + bound.freevars + ); + assert!( + bound + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), + "bound evaluator should use LOAD_FROM_DICT_OR_GLOBALS for class-local T, got instructions={:?}", + bound.instructions ); } #[test] - fn test_handled_except_conditional_tail_keeps_borrow() { + fn test_class_type_param_bound_respects_class_global_over_outer_function_local() { let code = compile_exec( "\ def f(self): - try: - if self.active: - self.step() - if self.waiter is not None and self.pending is None: - self.waiter.set_result(None) - except ConnectionResetError as exc: - self.close(exc) - except OSError as exc: - self.fail(exc, 'x') + T = int + class X: + global T + def foo[U: T](self): ... + return X.foo.__type_params__ ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f - .instructions - .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - let waiter_idx = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "waiter" - } - _ => false, - }) - .expect("missing waiter LOAD_ATTR"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[waiter_idx.saturating_sub(1)..handler_start]; + assert!( + f.cellvars.is_empty(), + "class global type-param bound must not force outer function cells, got cellvars={:?}", + f.cellvars + ); + let class_code = find_code(f, "X").expect("missing X class code"); + let type_params = + find_code(class_code, "").expect("missing type params code"); + let bound = find_code(type_params, "U").expect("missing U bound code"); assert!( - tail.iter().any(|unit| { + bound + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })), + "explicit class global should resolve as LOAD_GLOBAL in bound evaluator, got instructions={:?}", + bound.instructions + ); + assert!( + !bound.instructions.iter().any(|unit| { matches!( unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + Instruction::LoadFromDictOrGlobals { .. } + | Instruction::LoadFromDictOrDeref { .. } ) }), - "handled-except conditional tail should keep borrowed loads, got tail={tail:?}" + "explicit class global should not use classdict/deref lookup, got instructions={:?}", + bound.instructions + ); + } + + #[test] + fn test_generic_type_alias_in_class_does_not_capture_module_name() { + let code = compile_exec( + r#" +T = U = "global" +class C: + T = "class" + U = "class" + type Alias[T] = lambda: (T, U) +"#, ); assert!( - !tail + code.cellvars.is_empty(), + "module T must remain a global name, got cellvars={:?}", + code.cellvars + ); + + let class_code = find_code(&code, "C").expect("missing class code"); + assert!( + class_code.freevars.is_empty(), + "plain module-level class must not close over module T, got freevars={:?}", + class_code.freevars + ); + + let type_params = find_code(class_code, "") + .expect("missing alias type params"); + assert_eq!( + type_params + .cellvars .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "handled-except conditional tail should not force strong LOAD_FAST, got tail={tail:?}" + .map(String::as_str) + .collect::>(), + vec!["T"], + "alias type parameter T should be local to the type-params scope, got cellvars={:?}", + type_params.cellvars + ); + assert_eq!( + type_params + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["__classdict__"], + "alias type params should close only over the classdict, got freevars={:?}", + type_params.freevars + ); + + let alias = find_code(type_params, "Alias").expect("missing alias value code"); + assert_eq!( + alias + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["T", "__classdict__"], + "alias value should close over its type parameter and classdict, got freevars={:?}", + alias.freevars + ); + let lambda = find_code(alias, "").expect("missing alias lambda"); + assert_eq!( + lambda + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["T"], + "lambda should close over alias type parameter T only, got freevars={:?}", + lambda.freevars ); } #[test] - fn test_handled_except_else_tail_keeps_borrow() { - let code = compile_exec( + fn test_nested_generic_class_base_child_free_keeps_classdict_lookup() { + for (child_name, base_expr) in [ + ("", "make_base(T for _ in (1,))"), + ("", "make_base([T for _ in (1,)])"), + ("", "make_base(lambda: T)"), + ] { + let code = compile_exec(&format!( + "\ +class C[T]: + T = 'class' + class Inner[U]({base_expr}, make_base(T)): + pass +" + )); + let type_params = find_code(&code, "") + .expect("missing inner type params code"); + assert_eq!( + type_params + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["T", "__classdict__"], + "inner type params should keep T as a child-only freevar for {child_name}, got freevars={:?}", + type_params.freevars + ); + assert!( + type_params + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), + "direct class-local T lookup should still use classdict/global path for {child_name}, got instructions={:?}", + type_params.instructions + ); + + let child = find_code(type_params, child_name).expect("missing child code"); + assert_eq!( + child + .freevars + .iter() + .map(String::as_str) + .collect::>(), + vec!["T"], + "{child_name} should close over T from the type-params scope, got freevars={:?}", + child.freevars + ); + } + } + + #[test] + fn test_class_annotation_global_resolution_matches_cpython() { + let class_global = compile_exec( "\ -def f(self, fut=None): - try: - if self.closed: - return - item = self.queue.popleft() - self.size -= len(item) - if self.addr is not None: - self.future = self.loop.send(self.sock, item) - else: - self.future = self.loop.sendto(self.sock, item, addr=item) - except OSError as exc: - self.protocol.error_received(exc) - except Exception as exc: - self.fatal(exc, 'x') - else: - self.future.add_done_callback(self.loop_writing) - self.resume() +X = 'global' +class C: + locals()['X'] = 'class' + global X + y: X ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f - .instructions - .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) - .collect(); - let done_callback_idx = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "add_done_callback" - } - _ => false, - }) - .expect("missing add_done_callback LOAD_ATTR"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[done_callback_idx.saturating_sub(3)..handler_start]; + let annotate = + find_code(&class_global, "__annotate__").expect("missing class __annotate__ code"); + assert!( + annotate + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })), + "expected explicit class global to use LOAD_GLOBAL, got instructions={:?}", + annotate.instructions + ); + assert!( + !annotate + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), + "did not expect class explicit global to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}", + annotate.instructions + ); + let outer_global = compile_exec( + "\ +def f(): + global X + class C: + locals()['X'] = 'class' + y: X +", + ); + let annotate = find_code(&outer_global, "__annotate__") + .expect("missing nested class __annotate__ code"); assert!( - tail.iter().any(|unit| { - matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "handled-except else tail should keep borrowed loads, got tail={tail:?}" + annotate + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })), + "expected outer explicit global in class annotation to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}", + annotate.instructions ); + } + + #[test] + fn test_constant_tuple_binops_fold_like_cpython() { + let code = compile_exec("value = (1,) * 17 + ('spam',)\n"); + assert!( - !tail + !code + .instructions .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "handled-except else tail should not force strong LOAD_FAST, got tail={tail:?}" + .any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })), + "tuple constant binops should fold away, got instructions={:?}", + code.instructions ); + assert!(code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if elements.len() == 18 + && elements[..17] + .iter() + .all(|elt| matches!(elt, ConstantData::Integer { value } if *value == BigInt::from(1))) + && matches!(&elements[17], ConstantData::Str { value } if value.to_string() == "spam") + ))); } #[test] - fn test_reraising_handler_with_handled_returns_keeps_borrow() { + fn test_constant_list_iterable_uses_tuple() { let code = compile_exec( "\ -def f(self, fut=None): - try: +def f(): + return {x: y for x, y in [(1, 2), ]} +", + ); + let f = find_code(&code, "f").expect("missing function code"); + + assert!( + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildList { .. })), + "constant list iterable should avoid BUILD_LIST before GET_ITER" + ); + assert!(f.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if matches!( + elements.as_slice(), + [ConstantData::Tuple { elements: inner }] + if matches!( + inner.as_slice(), + [ + ConstantData::Integer { .. }, + ConstantData::Integer { .. } + ] + ) + ) + ))); + } + + #[test] + fn test_large_constant_list_iterable_keeps_streaming_list_build() { + let source = format!( + "def f():\n for x in [{}]:\n pass\n", + (0..=STACK_USE_GUIDELINE) + .map(|i| format!("'v{i}'")) + .collect::>() + .join(", ") + ); + let code = compile_exec(&source); + let f = find_code(&code, "f").expect("missing function code"); + + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildList { .. })), + "large list iterable should keep CPython streaming BUILD_LIST form, got instructions={:?}", + f.instructions + ); + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ListAppend { .. })), + "large list iterable should use LIST_APPEND streaming form, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_constant_set_iterable_uses_frozenset_const() { + let code = compile_exec( + "\ +def f(): + return [x for x in {1, 2, 3}] +", + ); + let f = find_code(&code, "f").expect("missing function code"); + + assert!( + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildSet { .. })), + "constant set iterable should avoid BUILD_SET before GET_ITER" + ); + assert!(f.constants.iter().any(|constant| matches!( + constant, + ConstantData::Frozenset { elements } + if matches!( + elements.as_slice(), + [ + ConstantData::Integer { .. }, + ConstantData::Integer { .. }, + ConstantData::Integer { .. } + ] + ) + ))); + } + + #[test] + fn test_constant_list_membership_uses_tuple_const() { + let code = compile_exec( + "\ +f = lambda x: x in [1, 2, 3] +", + ); + let lambda = find_code(&code, "").expect("missing lambda code"); + + assert!( + !lambda + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildList { .. })), + "constant list membership should avoid BUILD_LIST before CONTAINS_OP" + ); + assert!(lambda.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if matches!( + elements.as_slice(), + [ + ConstantData::Integer { .. }, + ConstantData::Integer { .. }, + ConstantData::Integer { .. } + ] + ) + ))); + } + + #[test] + fn test_small_constant_set_membership_uses_frozenset_const() { + let code = compile_exec( + "\ +f = lambda x: x in {0} +", + ); + let lambda = find_code(&code, "").expect("missing lambda code"); + + assert!( + !lambda + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildSet { .. })), + "constant set membership should avoid BUILD_SET before CONTAINS_OP" + ); + assert!(lambda.constants.iter().any(|constant| matches!( + constant, + ConstantData::Frozenset { elements } + if matches!(elements.as_slice(), [ConstantData::Integer { value }] if *value == BigInt::from(0)) + ))); + } + + #[test] + fn test_nonconstant_list_membership_uses_tuple() { + let code = compile_exec( + "\ +def f(a, b, c, x): + return x in [a, b, c] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::BuildTuple { .. }, + Instruction::ContainsOp { .. } + ] + ) + }), + "expected BUILD_TUPLE before CONTAINS_OP for non-constant list membership, got ops={ops:?}" + ); + } + + #[test] + fn test_unary_not_membership_and_identity_invert_compare_op() { + let code = compile_exec( + "\ +def f(a, b, d): + x = not (a in d) + y = not (a is b) + return x, y +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); + + assert!( + !ops.iter().any(|op| matches!(op, Instruction::UnaryNot)), + "CPython folds CONTAINS_OP/IS_OP + UNARY_NOT into inverted op, got ops={ops:?}" + ); + assert!(instructions.iter().any(|unit| { + matches!(unit.op, Instruction::ContainsOp { invert } if invert.get(OpArg::new(unit.arg.as_u32())) == Invert::Yes) + })); + assert!(instructions.iter().any(|unit| { + matches!(unit.op, Instruction::IsOp { invert } if invert.get(OpArg::new(unit.arg.as_u32())) == Invert::Yes) + })); + } + + #[test] + fn test_starred_tuple_iterable_drops_list_to_tuple_before_get_iter() { + let code = compile_exec( + "\ +def f(a, b, c): + for x in *a, *b, *c: + pass +", + ); + let f = find_code(&code, "f").expect("missing function code"); + + assert!( + !has_intrinsic_1(f, IntrinsicFunction1::ListToTuple), + "LIST_TO_TUPLE should be removed before GET_ITER in for-iterable context" + ); + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::GetIter)), + "expected GET_ITER in for loop" + ); + } + + #[test] + fn test_comprehension_single_list_iterable_uses_tuple() { + let code = compile_exec( + "\ +def g(): + [x for x in [(yield 1)]] +", + ); + let g = find_code(&code, "g").expect("missing g code"); + let ops: Vec<_> = g + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [Instruction::BuildTuple { .. }, Instruction::GetIter] + ) + }), + "expected BUILD_TUPLE before GET_ITER for single-item list iterable in comprehension, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_comprehension_list_iterable_uses_tuple() { + let code = compile_exec( + "\ +def f(): + return [[y for y in [x, x + 1]] for x in [1, 3, 5]] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [Instruction::BuildTuple { .. }, Instruction::GetIter] + ) + }), + "expected BUILD_TUPLE before GET_ITER for nested list iterable in comprehension, got ops={ops:?}" + ); + } + + #[test] + fn test_comprehension_singleton_sub_iter_uses_assignment_idiom() { + let code = compile_exec( + "\ +def f(): + return {j: j * j for i in range(4) for j in [i + 1]} +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let for_iter_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::ForIter { .. })) + .count(); + let has_map_add_depth_2 = f.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::MapAdd { i } + if i.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 2 + ) + }); + + assert_eq!( + for_iter_count, 1, + "singleton sub-iter should not emit its own FOR_ITER, got instructions={:?}", + f.instructions + ); + assert!( + has_map_add_depth_2, + "assignment-idiom dictcomp should use MAP_ADD depth 2, got instructions={:?}", + f.instructions + ); + assert!( + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::BuildTuple { .. })), + "singleton sub-iter should not materialize an iterator tuple, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_constant_comprehension_iterable_with_unary_int_uses_tuple_const() { + let code = compile_exec( + "\ +l = lambda : [2 < x for x in [-1, 3, 0]] +", + ); + let lambda = find_code(&code, "").expect("missing lambda code"); + + assert!( + lambda.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if matches!( + elements.as_slice(), + [ + ConstantData::Integer { .. }, + ConstantData::Integer { .. }, + ConstantData::Integer { .. } + ] + ) + )), + "expected folded tuple constant for comprehension iterable" + ); + } + + #[test] + fn test_module_scope_listcomp_is_inlined() { + let code = compile_exec("values = [i for i in range(3)]\n"); + + assert!( + find_code(&code, "").is_none(), + "module-scope list comprehension should be inlined" + ); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })), + "inlined module-scope list comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}", + code.instructions + ); + } + + #[test] + fn test_module_scope_dictcomp_is_inlined() { + let code = compile_exec("mapping = {i: i for i in range(3)}\n"); + + assert!( + find_code(&code, "").is_none(), + "module-scope dict comprehension should be inlined" + ); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })), + "inlined module-scope dict comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}", + code.instructions + ); + } + + #[test] + fn test_async_dictcomp_in_async_function_is_inlined() { + let code = compile_exec( + "\ +async def f(items): + return {item: item async for item in items} +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + find_code(&code, "").is_none(), + "async dict comprehension should be inlined" + ); + assert!( + ops.iter().any(|op| matches!(op, Instruction::GetAiter)), + "inlined async dict comprehension should keep GET_AITER in outer code, got ops={ops:?}" + ); + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::LoadFastAndClear { .. })), + "inlined async dict comprehension should use LOAD_FAST_AND_CLEAR, got ops={ops:?}" + ); + assert!( + !ops.iter().any(|op| matches!(op, Instruction::MakeFunction)), + "inlined async dict comprehension should not materialize MAKE_FUNCTION, got ops={ops:?}" + ); + } + + #[test] + fn test_async_inlined_comprehension_inlines_restore_return_into_end_async_for() { + let code = compile_exec( + "\ +async def f(): + return [i + 1 async for i in g([10, 20])] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::EndAsyncFor, + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + Instruction::ReturnValue, + Instruction::Swap { .. }, + Instruction::PopTop, + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "expected CPython-style restore+return inlined into END_ASYNC_FOR before cleanup, got ops={ops:?}" + ); + assert!( + !ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::EndAsyncFor, + Instruction::JumpForward { .. } | Instruction::JumpBackward { .. }, + ] + ) + }), + "unexpected jump from END_ASYNC_FOR to the normal restore tail, got ops={ops:?}" + ); + } + + #[test] + fn test_await_cleanup_throw_falls_through_until_cold_reorder() { + let code = compile_exec( + "\ +async def f(): + await 1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::CleanupThrow, + Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::CallIntrinsic1 { .. }, + ] + ) + }), + "expected CPython-style cold CLEANUP_THROW jump before StopIteration handler, got ops={ops:?}" + ); + assert!( + !ops.windows(2).any(|window| { + matches!(window, [Instruction::CleanupThrow, Instruction::EndSend]) + }), + "CLEANUP_THROW should not inline the normal END_SEND return tail, got ops={ops:?}" + ); + } + + #[test] + fn test_match_async_inlined_comprehension_success_jump_no_interrupt() { + let code = compile_exec( + "\ +async def f(name_3, name_5): + match b'': + case True: + pass + case name_5 if f'e': + {name_3: f async for name_2 in name_5} + case []: + pass + [[]] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::StoreFast { .. }, + Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style no-interrupt match success backedge after async comprehension cleanup, got ops={ops:?}" + ); + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::StoreFast { .. }, + Instruction::JumpBackward { .. }, + ] + ) + }), + "match success cleanup backedge should not be a regular interrupting jump, got ops={ops:?}" + ); + } + + #[test] + fn test_for_loop_if_return_reorders_continue_backedge_before_exit_body() { + let code = compile_exec( + "\ +def f(items, occurrence): + for item in items: + if item: + occurrence -= 1 + if not occurrence: + return item + return None +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + ] + ) + }), + "expected CPython-style inverted return guard followed by loop backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + ] + ) + }), + "return guard should not fall through into the return body before the loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_sync_with_after_async_for_keeps_end_async_for_line_marker() { + let code = compile_exec( + "\ +async def f(cm, source, tgt): + with cm: + async for tgt[0] in source(): + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::EndAsyncFor, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "expected CPython-style line-marker NOP between END_ASYNC_FOR and with cleanup, got ops={ops:?}" + ); + } + + #[test] + fn test_genexpr_with_async_comprehension_element_is_async_generator() { + let code = compile_exec( + "\ +async def f(): + gen = ([i async for i in asynciter([1, 2])] for j in [10, 20]) + return [x async for x in gen] +", + ); + let genexpr = find_code(&code, "").expect("missing genexpr code"); + let units: Vec<_> = genexpr + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + assert!( + units.windows(2).any(|window| { + let [wrap, yield_value] = window else { + return false; + }; + matches!(yield_value.op, Instruction::YieldValue { .. }) + && match wrap.op { + Instruction::CallIntrinsic1 { func } => { + func.get(OpArg::new(u32::from(u8::from(wrap.arg)))) + == bytecode::IntrinsicFunction1::AsyncGenWrap + } + _ => false, + } + }), + "expected CPython-style ASYNC_GEN_WRAP before genexpr yield, got units={units:?}" + ); + } + + #[test] + fn test_nested_module_scope_dictcomp_symbols_are_local() { + let symbol_table = scan_program_symbol_table( + "\ +deoptmap = { + specialized: base + for base, family in _specializations.items() + for specialized in family +} +", + ); + + for name in ["base", "family", "specialized"] { + let symbol = symbol_table + .lookup(name) + .unwrap_or_else(|| panic!("missing module symbol {name}")); + assert_eq!( + symbol.scope, + SymbolScope::Local, + "expected module-scope inlined comprehension symbol {name} to be Local, got {symbol:?}" + ); + } + + let comp = symbol_table + .sub_tables + .first() + .expect("missing comprehension symbol table"); + assert!(comp.comp_inlined, "expected comprehension to be inlined"); + for name in ["base", "family", "specialized"] { + let symbol = comp + .lookup(name) + .unwrap_or_else(|| panic!("missing comprehension symbol {name}")); + assert_eq!( + symbol.scope, + SymbolScope::Local, + "expected comprehension symbol {name} to be Local, got {symbol:?}" + ); + } + } + + #[test] + fn test_nested_module_scope_dictcomp_uses_fast_locals() { + let code = compile_exec( + "\ +deoptmap = { + specialized: base + for base, family in _specializations.items() + for specialized in family +} +", + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })), + "expected outer target unpack to use STORE_FAST_STORE_FAST, got ops={ops:?}" + ); + assert!( + ops.iter().any(|op| matches!( + op, + Instruction::StoreFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + )), + "expected inner target/store-use path to use fast locals, got ops={ops:?}" + ); + assert!( + ops.iter() + .filter(|op| matches!(op, Instruction::LoadName { .. })) + .count() + <= 1, + "unexpected extra LOAD_NAME ops in nested inlined comprehension, got ops={ops:?}" + ); + assert!( + ops.iter() + .filter(|op| matches!(op, Instruction::StoreName { .. })) + .count() + <= 1, + "unexpected extra STORE_NAME ops in nested inlined comprehension, got ops={ops:?}" + ); + } + + #[test] + fn test_module_scope_inlined_comprehension_keeps_outer_iter_as_name_lookup() { + let code = compile_exec( + "\ +path_separators = ['/'] +_pathseps_with_colon = {f':{s}' for s in path_separators} +", + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let load_name_path = ops + .windows(2) + .any(|window| matches!(window, [Instruction::LoadName { .. }, Instruction::GetIter])); + assert!( + load_name_path, + "expected outer iterable to stay a NAME lookup before GET_ITER, got ops={ops:?}" + ); + assert!( + !ops.windows(2).any(|window| matches!( + window, + [ + Instruction::LoadFast { .. } | Instruction::LoadFastCheck { .. }, + Instruction::GetIter + ] + )), + "module local outer iterable should not become a fast local, got ops={ops:?}" + ); + assert!( + ops.iter().any(|op| matches!( + op, + Instruction::StoreFastLoadFast { .. } | Instruction::StoreFast { .. } + )), + "comprehension target should still use fast locals, got ops={ops:?}" + ); + } + + #[test] + fn test_function_scope_inlined_comprehension_restore_keeps_swap_before_duplicate_store() { + let code = compile_exec( + "\ +def f(): + a = [1 for a in [0]] + return 1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| matches!( + window, + [ + Instruction::PopIter, + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + Instruction::StoreFast { .. } + ] + )), + "expected PopIter/SWAP 2/STORE_FAST/STORE_FAST restore tail, got ops={ops:?}" + ); + } + + #[test] + fn test_inlined_comprehension_namedexpr_target_stays_parent_fast_local() { + let code = compile_exec( + "\ +def f(seq, emit): + return [(x, y) for x in seq if (y := emit(x))] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + assert!(f.varnames.iter().any(|name| name == "y")); + assert!(!f.cellvars.iter().any(|name| name == "y")); + assert!(!f.freevars.iter().any(|name| name == "y")); + assert!( + !f.names.iter().any(|name| name == "y"), + "inlined comprehension namedexpr target should not use NAME ops, got names={:?}", + f.names + ); + } + + #[test] + fn test_global_namedexpr_in_inlined_comprehension_saves_fast_slot() { + let code = compile_exec( + "\ +def f(seq, value): + global G + [G := value for _ in seq] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + assert!(f.varnames.iter().any(|name| name == "G")); + assert!(f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastAndClear { var_num } => { + let idx = var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.varnames[usize::from(idx)] == "G" + } + _ => false, + })); + assert!(f.instructions.iter().any(|unit| match unit.op { + Instruction::StoreGlobal { namei } => { + let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(idx).unwrap()] == "G" + } + _ => false, + })); + } + + #[test] + fn test_genexpr_namedexpr_target_is_cell_not_fast_local() { + let code = compile_exec( + "\ +def f(seq): + a = 1 + return (c := x + a for x in seq) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + assert!(!f.varnames.iter().any(|name| name == "c")); + assert_eq!( + f.cellvars.iter().map(String::as_str).collect::>(), + ["a", "c"] + ); + } + + #[test] + fn test_inlined_comprehension_restore_does_not_form_store_fast_load_fast() { + let code = compile_exec( + "\ +def f(e): + e[1:3] = [g(i) for i in range(2)] + +def g(datadir): + files = [filename[:-4] for filename in sorted(os.listdir(datadir)) if filename.endswith('.xml')] + input_files = [filename for filename in files if filename.startswith('in')] + return files, input_files +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + Instruction::LoadFastBorrow { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreSubscr, + ] + ) + }), + "expected CPython-style inlined comprehension restore before slice store, got ops={ops:?}" + ); + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::StoreFastLoadFast { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreSubscr, + ] + ) + }), + "inlined comprehension restore should not be folded into STORE_FAST_LOAD_FAST, got ops={ops:?}" + ); + + let g = find_code(&code, "g").expect("missing g code"); + let g_ops: Vec<_> = g + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( + g_ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::StoreFast { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "expected CPython-style static swap over STORE_FAST_MAYBE_NULL restore, got ops={g_ops:?}" + ); + assert!( + !g_ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Swap { .. }, + Instruction::StoreFast { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "inlined comprehension restore should statically remove SWAP before adjacent stores, got ops={g_ops:?}" + ); + } + + #[test] + fn test_single_mode_folded_multiline_constant_does_not_leave_nops() { + let code = compile_single( + "\ +(- + - + - + 1) +", + ); + + assert!( + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::Nop)), + "expected folded single-mode multiline constant to drop NOP anchors, got instructions={:?}", + code.instructions + ); + } + + #[test] + fn test_folded_multiline_tuple_constant_does_not_leave_operand_nops() { + let code = compile_exec( + "\ +values = ( + (1 + 1j, 0 + 0j), + (1 + 1j, 0.0), + (1 + 1j, 0), +) +", + ); + + assert!( + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::Nop)), + "expected CPython nop_out-style folded tuple operands to have no surviving NOPs, got instructions={:?}", + code.instructions + ); + } + + #[test] + fn test_folded_multiline_bytes_binop_does_not_leave_operand_nops() { + let code = compile_exec( + "\ +def f(self, out): + self.assertIn( + b'gnu' + (b'/123' * 125) + b'/longlink' + (b'/123' * 125) + b'/longname', + out) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + assert!( + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::Nop)), + "expected CPython nop_out-style folded operands to have no surviving NOPs, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_folded_binop_at_branch_body_start_does_not_leave_nop() { + let code = compile_exec( + "\ +def f(sys): + if sys.platform == 'win32': + component = 'd' * 25 + return component +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::NotTaken, + Instruction::Nop, + Instruction::LoadConst { .. } + ] + ) + }), + "expected CPython nop_out-style folded branch body to drop operand NOP, got ops={ops:?}", + ); + } + + #[test] + fn test_folded_iterable_at_assert_target_does_not_leave_nop() { + let code = compile_exec( + r#" +def f(caches, non_caches): + assert 1 / 3 <= caches / non_caches, "this test needs more caches!" + for show_caches in (False, True): + pass +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.windows(2).any(|window| { + matches!(window, [Instruction::Nop, Instruction::LoadConst { .. }]) + }), + "expected folded for-iterable at assert target to drop operand NOP, got ops={ops:?}", + ); + } + + #[test] + fn test_multiline_unpack_target_uses_element_locations() { + let code = compile_exec( + "\ +def f(cm): + with cm as (_, + filename_2): + return filename_2 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })), + "expected multiline target elements to keep separate STORE_FAST instructions, got ops={ops:?}", + ); + } + + #[test] + fn test_or_condition_in_jump_context_uses_shared_true_fallthrough() { + let code = compile_exec( + "\ +def f(lines): + for line in lines: + if line.startswith('--') or not line.strip(): + continue + return line +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let first_pop_jump = ops + .iter() + .find(|op| { + matches!( + op, + Instruction::PopJumpIfTrue { .. } | Instruction::PopJumpIfFalse { .. } + ) + }) + .copied() + .expect("missing conditional jump"); + assert!( + matches!(first_pop_jump, Instruction::PopJumpIfTrue { .. }), + "expected first OR branch to jump on true into shared fallthrough, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_break_bool_chain_reorders_false_path_to_jump_back() { + let code = compile_exec( + "\ +def f(filters, text, category, module, lineno, defaultaction): + for item in filters: + action, msg, cat, mod, ln = item + if ((msg is None or msg.match(text)) and + issubclass(category, cat) and + (mod is None or mod.match(module)) and + (ln == 0 or lineno == ln)): + break + else: + action = defaultaction + return action +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected CPython-style false path to fall through into loop jump-back, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_conditional_body_keeps_duplicate_jump_back_paths() { + let code = compile_exec( + "\ +def f(new, old): + for replace in ['__module__', '__name__', '__qualname__', '__doc__']: + if hasattr(old, replace): + setattr(new, replace, getattr(old, replace)) + return new +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let jump_back_count = ops + .iter() + .filter(|op| { + matches!( + op, + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + }) + .count(); + assert!( + jump_back_count >= 2, + "expected separate false-path and body jump-back blocks, got ops={ops:?}" + ); + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected false path to jump back before body, got ops={ops:?}" + ); + } + + #[test] + fn test_try_loop_inner_if_keeps_duplicate_jump_back_paths() { + let code = compile_exec( + "\ +def f(config, logging): + handlers = config.get('handlers', {}) + for name in handlers: + if name not in logging._handlers: + raise ValueError('missing') + else: + try: + handler = logging._handlers[name] + handler_config = handlers[name] + level = handler_config.get('level', None) + if level: + handler.setLevel(logging._checkLevel(level)) + except Exception as e: + raise ValueError('bad') from e + loggers = config.get('loggers', {}) + for name in loggers: + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::EndFor, + ] + ) + }), + "expected CPython-style separate body and false-path jump-back blocks, got ops={ops:?}" + ); + } + + #[test] + fn test_try_loop_nested_bool_tail_keeps_duplicate_jump_back_paths() { + let code = compile_exec( + "\ +def f(obj, flags, writer, value, Error): + while obj.running: + try: + if value == 0: + return + elif obj.ready and obj.active and value == 1: + obj.work() + if flags.verbose and obj.chatty: + writer.write('trace') + elif value == 2: + obj.other() + else: + obj.default() + except Error as e: + obj.running = False +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. }, + ] + ) + }), + "expected CPython-style body and both bool false-path jump-back blocks, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_continue_shares_backedge_with_fallthrough_body() { + let code = compile_exec( + "\ +def f(names, show_empty, keywords, args_buffer, args, cls, object, level): + for name in names: + value = getattr(cls, name) + if not show_empty: + if value == []: + field_type = cls._field_types.get(name, object) + if getattr(field_type, '__origin__', ...) is list: + if not keywords: + args_buffer.append(repr(value)) + continue + if not keywords: + args.extend(args_buffer) + args_buffer = [] + value, simple = _format(value, level) + if keywords: + args.append('%s=%s' % (name, value)) + else: + args.append(value) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache | Instruction::NotTaken)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + ] + ) + }), + "expected CPython-style shared continue backedge before outer condition, got ops={ops:?}" + ); + assert!( + !ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "unexpected duplicated continue/backedge jumps, got ops={ops:?}" + ); + } + + #[test] + fn test_line_bearing_loop_if_false_backedge_keeps_body_before_jump_back() { + let code = compile_exec( + "\ +def f(self, replacement_pairs): + for n, d in [(19, '%OC'), (2, '%Ow')]: + if self.LC_alt_digits is None: + s = str(n) + replacement_pairs.append((s, d)) + if n < 10: + replacement_pairs.append((s[1], d)) + elif len(self.LC_alt_digits) > n: + replacement_pairs.append((self.LC_alt_digits[n], d)) + else: + replacement_pairs.append((d, d)) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "expected CPython-style line-bearing false target to keep body before backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "unexpected no-lineno-style inverted loop-if body, got ops={ops:?}" + ); + } + + #[test] + fn test_branch_local_implicit_continue_keeps_body_before_jump_back() { + let code = compile_exec( + "\ +def f(items, outer, cond, sub, out): + for x in items: + if outer: + if cond: + out.append(x) + if sub: + out.append(1) + out.append(2) + else: + out.append(3) + return out +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "expected branch-local implicit continue target to stay after the body, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "unexpected direct-loop-body implicit continue layout for branch-local target, got ops={ops:?}" + ); + } + + #[test] + fn test_boolop_continue_deduplicates_marker_jump_back() { + let code = compile_exec( + "\ +def f(ws, seen, more_than): + while ws: + w = ws.pop() + if w in seen or w <= more_than: + continue + seen.add(w) + return seen +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected adjacent equivalent continue backedges to be deduplicated, got ops={ops:?}" + ); + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "expected CPython-style boolop continue fallthrough before body, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_elif_nested_if_false_backedge_keeps_body_before_jump_back() { + let code = compile_exec( + "\ +def f(keys, parse_int, d, ampm, AM, PM): + hour = minute = 0 + for group_key in keys: + if group_key == 'I': + hour = parse_int(d['I']) + if ampm in ('', AM): + if hour == 12: + hour = 0 + elif ampm == PM: + if hour != 12: + hour += 12 + elif group_key == 'M': + minute = parse_int(d['M']) + return hour, minute +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadSmallInt { .. }, + ] + ) + }), + "expected CPython-style nested elif body before false backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadSmallInt { .. }, + ] + ) + }), + "unexpected inverted nested elif false path before body, got ops={ops:?}" + ); + assert!( + ops.windows(15).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::BinaryOp { .. }, + Instruction::StoreFast { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style duplicated body/false loop exits for nested elif, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_nested_if_before_elif_keeps_body_before_false_backedge() { + let code = compile_exec( + "\ +def f(keys, parse_int, found_dict, locale_time): + hour = minute = 0 + for group_key in keys: + if group_key == 'I': + hour = parse_int(found_dict['I']) + ampm = found_dict.get('p', '').lower() + if ampm in ('', locale_time.am_pm[0]): + if hour == 12: + hour = 0 + elif ampm == locale_time.am_pm[1]: + if hour != 12: + hour += 12 + elif group_key == 'M': + minute = parse_int(found_dict['M']) + return hour, minute +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadSmallInt { .. }, + Instruction::StoreFast { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style nested if body before false backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadSmallInt { .. }, + ] + ) + }), + "unexpected inverted nested if body after false backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_elif_pass_before_raise_keeps_line_bearing_forward_jump() { + let code = compile_exec( + "\ +def f(entries, path, self): + if entries == ['.mh_sequences']: + os.remove(os.path.join(path, '.mh_sequences')) + elif entries == []: + pass + else: + raise NotEmptyError('Folder not empty: %s' % self._path) + os.rmdir(path) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpForward { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected CPython-style pass branch forward jump before raise body, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "unexpected inverted pass branch before raise body, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_multiblock_conditional_body_keeps_body_before_jump_back() { + let code = compile_exec( + "\ +def f(random, d, f): + for dummy in range(100): + k = random.choice('abc') + if random.random() < 0.2: + if k in d: + del d[k] + del f[k] + else: + v = random.choice((1, 2)) + d[k] = v + f[k] = v + check(f[k], v) +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrowLoadFastBorrow { .. }, + Instruction::DeleteSubscr, + ] + ) + }), + "expected CPython-style multi-block body before false jump-back, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_not_conditional_body_threads_true_path_to_jump_back() { + let code = compile_exec( + "\ +def f(xs): + for x in xs: + if not x: + g(x) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected CPython-style true path to jump back before not-body, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_not_in_conditional_body_threads_true_path_to_jump_back() { + let code = compile_exec( + "\ +def f(native, array): + for k in native: + if k not in 'bBhHiIlLfd': + del array[k] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + ] + ) + }), + "expected CPython-style true path to jump back before not-in body, got ops={ops:?}" + ); + } + + #[test] + fn test_while_implicit_continue_body_after_jumpback_for_boolop_call_arg() { + let code = compile_exec( + "\ +def f(source, state, verbose, nested): + items = [] + itemsappend = items.append + sourcematch = source.match + while True: + itemsappend(parse(source, state, verbose, nested + 1, + not nested and not items)) + if not sourcematch('|'): + break + if not nested: + verbose = state.flags & 64 + return verbose +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style while implicit-continue jumpback before conditional body, got ops={ops:?}" + ); + } + + #[test] + fn test_multiblock_elif_continue_keeps_next_test_before_backedge() { + let code = compile_exec( + "\ +def f(source, state, verbose, nested, subpatternappend, start, MAXGROUPS): + sourceget = source.get + sourcematch = source.match + while True: + this = source.next + sourceget() + if this == '(': + if sourcematch('?'): + char = sourceget() + if char in '=!<': + dir = 1 + if char == '<': + char = sourceget() + if char not in '=!': + raise source.error('unknown extension ?<' + char, len(char) + 2) + dir = -1 + lookbehindgroups = state.lookbehindgroups + if lookbehindgroups is None: + state.lookbehindgroups = state.groups + p = parse_sub(source, state, verbose, nested + 1) + if dir < 0: + if lookbehindgroups is None: + state.lookbehindgroups = None + if not sourcematch(')'): + raise source.error('missing ), unterminated subpattern', source.tell() - start) + if char == '=': + subpatternappend(('ASSERT', (dir, p))) + elif p: + subpatternappend(('ASSERT_NOT', (dir, p))) + else: + subpatternappend(('FAILURE', ())) + continue + elif char == '(': + condname = source.getuntil(')', 'group name') + if not (condname.isdecimal() and condname.isascii()): + source.checkgroupname(condname, 1) + condgroup = state.groupdict.get(condname) + if condgroup is None: + msg = 'unknown group name %r' % condname + raise source.error(msg, len(condname) + 1) + else: + condgroup = int(condname) + if not condgroup: + raise source.error('bad group number', len(condname) + 1) + if condgroup >= MAXGROUPS: + msg = 'invalid group reference %d' % condgroup + raise source.error(msg, len(condname) + 1) + state.checklookbehindgroup(condgroup, source) + item_yes = parse(source, state, verbose, nested + 1) + if source.match('|'): + item_no = parse(source, state, verbose, nested + 1) + if source.next == '|': + raise source.error('conditional backref with more than two branches') + else: + item_no = None + if not source.match(')'): + raise source.error('missing ), unterminated subpattern', source.tell() - start) + subpatternappend(('GROUPREF_EXISTS', (condgroup, item_yes, item_no))) + continue + elif char == '>': + capture = False + return +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style elif test between separate continue backedges, got ops={ops:?}" + ); + } + + #[test] + fn test_while_scope_exit_body_keeps_line_backedge_before_raise_body() { + let code = compile_exec( + "\ +FLAGS = {} +TYPE_FLAGS = 0 +GLOBAL_FLAGS = 0 + +def f(source, state, char): + sourceget = source.get + add_flags = 0 + del_flags = 0 + if char != '-': + while True: + flag = FLAGS[char] + if source.istext: + if char == 'L': + msg = 'bad inline flags' + raise source.error(msg) + else: + if char == 'u': + msg = 'bad inline flags' + raise source.error(msg) + add_flags |= flag + if (flag & TYPE_FLAGS) and (add_flags & TYPE_FLAGS) != flag: + msg = 'bad inline flags' + raise source.error(msg) + char = sourceget() + if char is None: + raise source.error('missing -, : or )') + if char in ')-:': + break + if char not in FLAGS: + msg = 'unknown flag' if char.isalpha() else 'missing -, : or )' + raise source.error(msg, len(char)) + if char == ')': + state.flags |= add_flags + return None + if add_flags & GLOBAL_FLAGS: + raise source.error('bad inline flags: cannot turn on global flag', 1) + if char == '-': + char = sourceget() + if char is None: + raise source.error('missing flag') + if char not in FLAGS: + msg = 'unknown flag' if char.isalpha() else 'missing flag' + raise source.error(msg, len(char)) + while True: + flag = FLAGS[char] + if flag & TYPE_FLAGS: + msg = 'bad inline flags' + raise source.error(msg) + del_flags |= flag + char = sourceget() + if char is None: + raise source.error('missing :') + if char == ':': + break + if char not in FLAGS: + msg = 'unknown flag' if char.isalpha() else 'missing :' + raise source.error(msg, len(char)) + return add_flags, del_flags +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let cpython_style_not_in_raise_body_count = ops + .windows(6) + .filter(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }) + .count(); + assert!( + cpython_style_not_in_raise_body_count >= 2, + "expected both while-loop not-in checks to put the false-path backedge before the conditional-expression raise body, got ops={ops:?}" + ); + } + + #[test] + fn test_call_body_implicit_continue_keeps_cpython_normalized_forward_jump() { + let code = compile_exec( + "\ +DIGITS = '0123456789' + +def f(s, sget, lappend, addgroup, this, c): + while True: + if c in DIGITS: + isoctal = False + if s.next in DIGITS: + this += sget() + if not isoctal: + addgroup(int(this[1:]), len(this) - 1) + else: + lappend(this) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "expected CPython-style forward jump over call body, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "call body should not be moved after an implicit continue backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_empty_if_end_label_preserves_cpython_return_anchor_nop() { + let code = compile_exec( + "\ +SRE_FLAG_LOCALE = 1 +SRE_FLAG_ASCII = 2 +SRE_FLAG_UNICODE = 4 + +def f(src, flags): + if isinstance(src, str): + if flags & SRE_FLAG_LOCALE: + raise ValueError('cannot use LOCALE flag with a str pattern') + if not flags & SRE_FLAG_ASCII: + flags |= SRE_FLAG_UNICODE + elif flags & SRE_FLAG_UNICODE: + raise ValueError('ASCII and UNICODE flags are incompatible') + else: + if flags & SRE_FLAG_UNICODE: + raise ValueError('cannot use UNICODE flag with a bytes pattern') + if flags & SRE_FLAG_LOCALE and flags & SRE_FLAG_ASCII: + raise ValueError('ASCII and LOCALE flags are incompatible') + return flags +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::RaiseVarargs { .. }, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ReturnValue, + ] + ) + }), + "expected CPython-style NOP return anchor after elif raise body, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_except_normal_exit_return_uses_strong_loads() { + let code = compile_exec( + "\ +LITERAL = 1 + +def f(source, escape): + try: + c = escape[1:2] + if c == 'N' and source.istext: + try: + c = ord(source.lookup()) + except (KeyError, TypeError): + raise source.error() from None + return LITERAL, c + except ValueError: + pass + raise source.error(escape) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadGlobal { .. }, + Instruction::LoadFast { .. }, + Instruction::BuildTuple { .. }, + Instruction::ReturnValue, + ] + ) + }), + "expected CPython-style strong LOAD_FAST in nested except normal-exit return, got ops={ops:?}" + ); + } + + #[test] + fn test_targeted_nop_after_prefix_for_else_uses_strong_for_tail_loads() { + let code = compile_exec( + "\ +LITERAL = 1 +IN = 2 +NEGATE = 3 + +def f(items): + while True: + prefix = None + for item in items: + if not item: + break + if prefix is None: + prefix = item[0] + elif item[0] != prefix: + break + else: + for item in items: + del item[0] + continue + break + set = [] + for item in items: + if len(item) != 1: + break + op, av = item[0] + if op is LITERAL: + set.append((op, av)) + elif op is IN and av[0][0] is not NEGATE: + set.extend(av) + else: + break + return set +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let get_iter_idx = ops + .iter() + .rposition(|op| matches!(op, Instruction::GetIter)) + .expect("missing GET_ITER"); + assert!( + matches!(ops[get_iter_idx - 1], Instruction::LoadFast { .. }), + "targeted NOP after prefix for/else should use strong iterable LOAD_FAST, got ops={ops:?}" + ); + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastLoadFast { .. }, + Instruction::BuildTuple { .. }, + ] + ) + }), + "targeted NOP after prefix for/else should keep set.append tuple loads strong, got ops={ops:?}" + ); + } + + #[test] + fn test_plain_pass_before_for_tail_keeps_borrows() { + let code = compile_exec( + "\ +def f(xs): + pass + for x in xs: + pass + return xs +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let get_iter_idx = ops + .iter() + .position(|op| matches!(op, Instruction::GetIter)) + .expect("missing GET_ITER"); + assert!( + matches!(ops[get_iter_idx - 1], Instruction::LoadFastBorrow { .. }), + "plain pass before for-tail should keep borrowed iterable load, got ops={ops:?}" + ); + } + + #[test] + fn test_targeted_nop_after_return_uses_strong_pair_call_args() { + let code = compile_exec( + "\ +def f(x, d, count, inner, hi, w1, lo, w2): + if x: + return + if 0: + pass + d[count] += 1 + inner(hi, w1) + del hi + inner(lo, w2) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::PushNull, + Instruction::LoadFastLoadFast { .. }, + ] + ) + }), + "targeted NOP after return should keep CPython-style strong pair call args, got ops={ops:?}" + ); + let nop_idx = ops + .iter() + .position(|op| matches!(op, Instruction::Nop)) + .expect("missing targeted NOP"); + assert!( + !ops[nop_idx..].iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "targeted NOP tail should not reintroduce borrowed fast loads, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_if_pass_uses_line_bearing_jump_back_instead_of_nop() { + let code = compile_exec( + "\ +def f(x, y): + for i in x: + if y: + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style synthetic false-path jump-back plus body jump-back, got ops={ops:?}" + ); + assert!( + !ops.iter().any(|op| matches!(op, Instruction::Nop)), + "expected pass body line to attach to loop backedge instead of leaving a NOP, got ops={ops:?}" + ); + } + + #[test] + fn test_constant_true_while_pass_keeps_loop_header_nop() { + let code = compile_exec( + "\ +def f(): + while 1: + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::Nop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style loop-header NOP before self backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_if_shared_jump_back_target_is_duplicated() { + let code = compile_exec( + "\ +def f(s, size, encodeSetO, encodeWhiteSpace): + inShift = True + base64bits = 0 + out = [] + for i, ch in enumerate(s): + if base64bits == 0: + if i + 1 < size: + ch2 = s[i + 1] + if E(ch2, encodeSetO, encodeWhiteSpace): + if B(ch2) or ch2 == '-': + out.append(b'-') + inShift = False + else: + out.append(b'-') + inShift = False + return out +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + ] + ) + }), + "expected separate nested-if and outer-if jump-back tails, got ops={ops:?}" + ); + } + + #[test] + fn test_exception_cleanup_backedge_target_is_shared() { + let code = compile_exec( + "\ +def f(enum_class, value, Flag, int_type, is_single_bit): + try: + try: + enum_member = enum_class[value] + except TypeError: + raise KeyError + except KeyError: + if Flag is None or not issubclass(enum_class, Flag): + enum_class.names.append(value) + elif ( + Flag is not None + and issubclass(enum_class, Flag) + and isinstance(value, int_type) + and is_single_bit(value) + ): + enum_class.names.append(value) + enum_class.add(enum_member) + return enum_member +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopExcept, + Instruction::JumpBackwardNoInterrupt { .. } + | Instruction::JumpBackward { .. }, + Instruction::Reraise { .. }, + ] + ) + }), + "expected CPython-style shared exception cleanup backedge before reraise, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopExcept, + Instruction::JumpBackwardNoInterrupt { .. } + | Instruction::JumpBackward { .. }, + Instruction::PopExcept, + Instruction::JumpBackwardNoInterrupt { .. } + | Instruction::JumpBackward { .. }, + ] + ) + }), + "exception cleanup backedge should be shared, not duplicated per conditional edge, got ops={ops:?}" + ); + } + + #[test] + fn test_protected_loop_conditional_keeps_forward_body_entry() { + let code = compile_exec( + "\ +def outer(it, C1): + def f(): + for x in it: + try: + if C1: + yield 2 + except OSError: + pass + return f +", + ); + let outer = find_code(&code, "outer").expect("missing outer code"); + let f = find_code(outer, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadSmallInt { .. }, + Instruction::YieldValue { .. }, + Instruction::Resume { .. }, + Instruction::PopTop, + ] + ) + }), + "expected protected conditional to keep CPython-style forward body entry, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_except_false_path_duplicates_pop_except_jump_back_tail() { + let code = compile_exec( + "\ +def f(it, C3): + for x in it: + try: + X = 3 + except OSError: + try: + if C3: + X = 4 + except OSError: + pass + return 42 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadSmallInt { .. }, + Instruction::StoreFast { .. }, + Instruction::PopExcept, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::PopExcept, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style duplicated false-path exit tail, got ops={ops:?}" + ); + } + + #[test] + fn test_more_nested_except_false_paths_duplicate_all_jump_back_tails() { + let code = compile_exec( + "\ +def f(it, C3, C4): + for x in it: + try: + X = 3 + except OSError: + try: + if C3: + if C4: + X = 4 + except OSError: + try: + if C3: + if C4: + X = 5 + except OSError: + pass + return 42 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::LoadSmallInt { .. }, + Instruction::StoreFast { .. }, + Instruction::PopExcept, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::PopExcept, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::PopExcept, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style duplicated nested false-path exit tails, got ops={ops:?}" + ); + } + + #[test] + fn test_no_wraparound_jump_keeps_forward_hop_before_loop_backedge() { + let code = compile_exec( + "\ +def while_not_chained(a, b, c): + while not (a < b < c): + pass +", + ); + let f = find_code(&code, "while_not_chained").expect("missing while_not_chained code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpForward { .. }, + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style no-wraparound forward hop before the loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_while_chained_compare_break_keeps_break_jump_block() { + let code = compile_exec( + "\ +def f(start, self, stop, size): + while size > 0: + while True: + if start <= self.position < stop: + break + else: + self.map_index += 1 + if self.map_index == len(self.map): + self.map_index = 0 + length = min(size, stop - self.position) + size -= length + return size +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpForward { .. }, + Instruction::PopTop, + Instruction::JumpForward { .. }, + Instruction::JumpForward { .. }, + ] + ) + }), + "expected CPython-style chained-compare success hop into the break jump block, got ops={ops:?}" + ); + } + + #[test] + fn test_while_break_else_keeps_true_edge_into_forward_break_body() { + let code = compile_exec( + "\ +def f(i): + while i: + i -= 1 + if i < 4: + break + else: + print('x') + print('y') +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpForward { .. }, + ] + ) + }), + "expected CPython-style true edge into forward break body with false path falling into the loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_if_continue_reorders_false_path_to_loop_backedge() { + let code = compile_exec( + "\ +def f(items, changes): + for x in items: + if not x: + if x in changes: + raise TypeError + continue +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + ] + ) + }), + "expected nested if/continue to keep CPython-style false-edge jump-back tails, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_assert_keeps_false_edge_into_raise_body() { + let code = compile_exec( + "\ +def f(bytecode): + for instr, positions in zip(bytecode, bytecode.codeobj.co_positions()): + assert instr.positions == positions +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadCommonConstant { .. }, + Instruction::RaiseVarargs { .. }, + ] + ) + }), + "expected loop assert to keep CPython-style false-edge into the raise body, got ops={ops:?}" + ); + } + + #[test] + fn test_and_is_not_none_loop_guard_uses_direct_jump_back_false_path() { + let code = compile_exec( + "\ +def f(code): + last_line = -2 + for _, _, line in code.co_lines(): + if line is not None and line != last_line: + last_line = line +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::PopJumpIfNotNone { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::CompareOp { .. }, + ] + ) + }), + "expected CPython-style direct jump-back false path for 'is not None and ...', got ops={ops:?}" + ); + } + + #[test] + fn test_large_is_not_none_loop_guard_uses_direct_jump_back_false_path() { + let code = compile_exec( + "\ +def f(cls, _FIELDS, _PARAMS): + all_frozen_bases = None + any_frozen_base = False + has_dataclass_bases = False + for b in cls.__mro__[-1:0:-1]: + base_fields = getattr(b, _FIELDS, None) + if base_fields is not None: + has_dataclass_bases = True + for field in base_fields.values(): + name = field.name + if all_frozen_bases is None: + all_frozen_bases = True + current_frozen = getattr(b, _PARAMS).frozen + all_frozen_bases = all_frozen_bases and current_frozen + any_frozen_base = any_frozen_base or current_frozen +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::PopJumpIfNotNone { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadConst { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "expected CPython-style direct jump-back false path for large 'is not None' loop body, got ops={ops:?}" + ); + } + + #[test] + fn test_continue_inside_with_keeps_line_marker_nop_before_exit_cleanup() { + let code = compile_exec( + "\ +def f(it): + for func in it: + with cm(): + if cond(): + continue +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(9).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "expected CPython-style line-marker NOP before with-exit cleanup on continue, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_async_with_normal_cleanup_drops_pop_block_nop() { + let code = compile_exec( + "\ +async def foo(): + async with CM(): + async with CM(): + raise RuntimeError +", + ); + let f = find_code(&code, "foo").expect("missing foo code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::GetAwaitable { .. }, + ] + ) + }), + "expected CPython-style async-with normal cleanup without a POP_BLOCK NOP, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::GetAwaitable { .. }, + ] + ) + }), + "unexpected POP_BLOCK NOP before async-with normal cleanup, got ops={ops:?}" + ); + } + + #[test] + fn test_async_with_try_finally_before_outer_sync_with_cleanup_keeps_anchor_nop() { + let code = compile_exec( + "\ +async def foo(self): + with self.assertRaises(ZeroDivisionError): + async with timeout(): + try: + try: + raise ValueError + finally: + await sleep(1) + finally: + try: + raise KeyError + finally: + 1 / 0 + after() +", + ); + let f = find_code(&code, "foo").expect("missing foo code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::Copy { .. }, + Instruction::PopExcept, + Instruction::Reraise { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "expected CPython-style async-with after-block NOP before outer sync-with cleanup, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_terminal_with_keeps_outer_cleanup_target_nop() { + let code = compile_exec( + "\ +def f(): + with a(): + with b(): + raise E() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::Copy { .. }, + Instruction::PopExcept, + Instruction::Reraise { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "expected CPython-style outer with-exit target NOP after terminal nested with cleanup, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_nonterminal_with_drops_outer_cleanup_target_nop() { + let code = compile_exec( + "\ +def f(): + with a(): + with b(): + x() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::Copy { .. }, + Instruction::PopExcept, + Instruction::Reraise { .. }, + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "unexpected outer with-exit target NOP for nested with with normal fallthrough, got ops={ops:?}" + ); + } + + #[test] + fn test_try_loop_elif_places_return_before_orelse_tail() { + let code = compile_exec( + "\ +def f(source, suggest, tb, s): + if source is not None: + try: + tb = tb + except Exception: + suggest = False + tb = None + if tb is not None: + for frame in tb: + s += frame + elif suggest: + s += 'x' + return s +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let has_direct_return = ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ReturnValue, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + ] + ) + }); + let has_nop_anchored_return = ops.windows(9).any(|window| { + matches!( + window, + [ + Instruction::EndFor, + Instruction::PopIter, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ReturnValue, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + ] + ) + }); + assert!( + has_direct_return || has_nop_anchored_return, + "expected CPython-style duplicated return between loop exit and elif tail, got ops={ops:?}" + ); + } + + #[test] + fn test_constant_false_while_else_deopts_post_else_borrows() { + let code = compile_exec( + "\ +def f(self): + x = 0 + while 0: + x = 1 + else: + x = 2 + self.assertEqual(x, 2) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let assert_idx = ops + .iter() + .position(|op| matches!(op, Instruction::LoadAttr { .. })) + .expect("missing assertEqual call"); + let window = &ops[assert_idx.saturating_sub(1)..(assert_idx + 3).min(ops.len())]; + assert!( + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFast { .. }, + .. + ] + ), + "expected post-else assertEqual call to use plain LOAD_FAST, got ops={window:?}" + ); + } + + #[test] + fn test_single_unpack_assignment_disables_constant_collection_folding() { + let code = compile_exec("a, b, c = 1, 2, 3\n"); + + assert!( + !code.instructions.iter().any(|unit| { + matches!(unit.op, Instruction::UnpackSequence { .. }) + || matches!(unit.op, Instruction::LoadConst { .. }) + && matches!( + code.constants.get(usize::from(u8::from(unit.arg))), + Some(ConstantData::Tuple { .. }) + ) + }), + "single unpack assignment should keep builder form for later lowering, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + code.instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::LoadSmallInt { .. })) + .count() + >= 3, + "expected individual constant loads before unpack-target stores, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_four_item_unpack_assignment_folds_tuple_constant_like_cpython() { + let code = compile_exec("a, b, c, d = 1, 2, 3, 4\n"); + + assert!( + code.instructions.iter().any(|unit| { + matches!(unit.op, Instruction::LoadConst { .. }) + && matches!( + code.constants.get(usize::from(u8::from(unit.arg))), + Some(ConstantData::Tuple { elements }) if elements.len() == 4 + ) + }), + "four-item unpack assignment should fold BUILD_TUPLE before UNPACK_SEQUENCE like CPython, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })), + "four-item unpack assignment should keep UNPACK_SEQUENCE after tuple folding, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_chained_unpack_assignment_keeps_constant_collection_folding() { + let code = compile_exec("(a, b) = c = d = (1, 2)\n"); + + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadConst { .. })), + "chained unpack assignment should keep tuple constant, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })), + "chained unpack assignment should still unpack the copied tuple, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_constant_true_assert_skips_message_nested_scope() { + let code = compile_exec("assert 1, (lambda x: x + 1)\n"); + + assert_eq!( + code.constants + .iter() + .filter(|constant| matches!(constant, ConstantData::Code { .. })) + .count(), + 0, + "constant-true assert should not compile the skipped message lambda" + ); + assert!( + !code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })), + "constant-true assert should be elided, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_constant_false_assert_uses_direct_raise_shape() { + let code = compile_exec("assert 0, (lambda x: x + 1)\n"); + + assert!( + !code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::ToBool + | Instruction::PopJumpIfTrue { .. } + | Instruction::PopJumpIfFalse { .. } + ) + }), + "constant-false assert should use direct raise shape, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })), + "constant-false assert should still raise, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + assert_eq!( + code.constants + .iter() + .filter(|constant| matches!(constant, ConstantData::Code { .. })) + .count(), + 1, + "constant-false assert should still compile the message lambda" + ); + } + + #[test] + fn test_constant_unary_positive_and_invert_fold() { + let code = compile_exec("x = +1\nx = ~1\n"); + + assert!( + !code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::CallIntrinsic1 { .. } | Instruction::UnaryInvert + ) + }), + "constant unary ops should fold away, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_bool_invert_is_not_const_folded() { + let code = compile_exec("x = ~True\n"); + + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::UnaryInvert)), + "~bool should remain unfurled to match CPython, got ops={:?}", + code.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_optimized_assert_preserves_nested_scope_order() { + compile_exec_optimized( + "\ +class S: + def f(self, sequence): + _formats = [self._types_mapping[type(item)] for item in sequence] + _list_len = len(_formats) + assert sum(len(fmt) <= 8 for fmt in _formats) == _list_len + _recreation_codes = [self._extract_recreation_code(item) for item in sequence] +", + ); + } + + #[test] + fn test_optimized_assert_with_nested_scope_in_first_iter() { + // First iterator of a comprehension is evaluated in the enclosing + // scope, so nested scopes inside it (the generator here) must also + // be consumed when the assert is optimized away. + compile_exec_optimized( + "\ +def f(items): + assert [x for x in (y for y in items)] + return [x for x in items] +", + ); + } + + #[test] + fn test_optimized_assert_with_lambda_defaults() { + // Lambda default values are evaluated in the enclosing scope, + // so nested scopes inside defaults must be consumed. + compile_exec_optimized( + "\ +def f(items): + assert (lambda x=[i for i in items]: x)() + return [x for x in items] +", + ); + } + + #[test] + fn test_try_else_nested_scopes_keep_subtable_cursor_aligned() { + let code = compile_exec( + "\ +try: + import missing_mod +except ImportError: + def fallback(): + return 0 +else: + def impl(): + return reversed('abc') +", + ); + + assert!( + find_code(&code, "fallback").is_some(), + "missing fallback code" + ); + let impl_code = find_code(&code, "impl").expect("missing impl code"); + assert!( + impl_code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadGlobal { .. } | Instruction::LoadName { .. } + ) + }), + "expected impl to compile global name access, got ops={:?}", + impl_code + .instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn test_nested_try_else_multi_resume_join_keeps_strong_load_fast_tail() { + let code = compile_exec( + "\ +def f(msg): + s = '' + try: + import a + except Exception: + suggest = False + tb = None + else: + try: + suggest = not t() + tb = g(msg) + except Exception: + suggest = False + tb = None + if tb is not None: + for frame in tb: + s += frame + elif suggest: + s += 'y' + return s +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let tail_start = ops + .iter() + .position(|op| matches!(op, Instruction::PopJumpIfNone { .. })) + .expect("missing tail POP_JUMP_IF_NONE") + .saturating_sub(1); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &ops[tail_start..handler_start]; + + assert!( + !tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected nested try/except else-resume tail to keep strong LOAD_FAST ops, got tail={tail:?}" + ); + + assert!( + tail.iter() + .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), + "expected loop body to keep LOAD_FAST_LOAD_FAST in the resume tail, got tail={tail:?}" + ); + } + + #[test] + fn test_protected_conditional_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(m, class_name, category, warning_base): + try: + cat = getattr(m, class_name) + except AttributeError: + raise ValueError(category) + if not issubclass(cat, warning_base): + raise TypeError(category) + return cat +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let tail_start = ops + .iter() + .position(|op| matches!(op, Instruction::StoreFast { .. })) + .expect("missing STORE_FAST cat"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &ops[tail_start + 1..handler_start]; + + assert!( + !tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}" + ); + + assert!( + tail.iter() + .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), + "expected protected tail to keep LOAD_FAST_LOAD_FAST for issubclass args, got tail={tail:?}" + ); + } + + #[test] + fn test_nonresuming_protected_conditional_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(href, parse='xml'): + try: + data = XINCLUDE[href] + except KeyError: + raise OSError('resource not found') + if parse == 'xml': + data = ET.XML(data) + return data +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let tail_start = ops + .iter() + .position(|op| matches!(op, Instruction::StoreFast { .. })) + .expect("missing protected STORE_FAST data"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &ops[tail_start + 1..handler_start]; + + assert!( + !tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected non-resuming protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}" + ); + } + + #[test] + fn test_optional_nonresuming_protected_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(b): + if type(b) is not bytes: + try: + b = bytes(memoryview(b)) + except TypeError: + raise TypeError(f'bad {type(b).__name__}') from None + if b: + sink(b) + return len(b) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let b_index = f + .varnames + .iter() + .position(|name| name.as_str() == "b") + .expect("missing b varname"); + let store_b = instructions + .iter() + .position(|unit| match unit.op { + Instruction::StoreFast { var_num } => { + usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) == b_index + } + _ => false, + }) + .expect("missing protected STORE_FAST b"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[store_b + 1..handler_start]; + + assert!( + tail.iter() + .filter(|unit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) + == b_index + } + _ => false, + }) + .all(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), + "optional protected tail should keep CPython-style borrowed b loads, got tail={tail:?}" + ); + } + + #[test] + fn test_handled_except_conditional_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(self): + try: + if self.active: + self.step() + if self.waiter is not None and self.pending is None: + self.waiter.set_result(None) + except ConnectionResetError as exc: + self.close(exc) + except OSError as exc: + self.fail(exc, 'x') +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let waiter_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "waiter" + } + _ => false, + }) + .expect("missing waiter LOAD_ATTR"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[waiter_idx.saturating_sub(1)..handler_start]; + + assert!( + tail.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "handled-except conditional tail should keep borrowed loads, got tail={tail:?}" + ); + assert!( + !tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "handled-except conditional tail should not force strong LOAD_FAST, got tail={tail:?}" + ); + } + + #[test] + fn test_handled_except_else_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, fut=None): + try: + if self.closed: + return + item = self.queue.popleft() + self.size -= len(item) + if self.addr is not None: + self.future = self.loop.send(self.sock, item) + else: + self.future = self.loop.sendto(self.sock, item, addr=item) + except OSError as exc: + self.protocol.error_received(exc) + except Exception as exc: + self.fatal(exc, 'x') + else: + self.future.add_done_callback(self.loop_writing) + self.resume() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let done_callback_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "add_done_callback" + } + _ => false, + }) + .expect("missing add_done_callback LOAD_ATTR"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[done_callback_idx.saturating_sub(3)..handler_start]; + + assert!( + tail.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "handled-except else tail should keep borrowed loads, got tail={tail:?}" + ); + assert!( + !tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "handled-except else tail should not force strong LOAD_FAST, got tail={tail:?}" + ); + } + + #[test] + fn test_reraising_handler_with_handled_returns_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, fut=None): + try: if fut is not None: fut.result() if self.future is not fut: @@ -20520,82 +26676,2884 @@ def f(self, fut=None): fut = self.reader.recv(self.sock, 4096) except CancelledError: return - except (SystemExit, KeyboardInterrupt): + except (SystemExit, KeyboardInterrupt): + raise + except BaseException as exc: + self.handle({'exception': exc, 'loop': self}) + else: + self.future = fut + fut.add_done_callback(self.loop_reading) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let recv_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "recv" + } + _ => false, + }) + .expect("missing recv LOAD_ATTR"); + let done_callback_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "add_done_callback" + } + _ => false, + }) + .expect("missing add_done_callback LOAD_ATTR"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = + &instructions[recv_idx.saturating_sub(3)..handler_start.min(done_callback_idx + 3)]; + + assert!( + tail.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "handler chain with handled returns should keep borrowed warm/else loads, got tail={tail:?}" + ); + assert!( + !tail.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } + ) + }), + "handler chain with handled returns should not force strong warm/else loads, got tail={tail:?}" + ); + } + + #[test] + fn test_with_protected_conditional_tail_without_exception_match_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, cm, p, platform): + with cm: + if p.returncode != 0: + if platform.machine() == 'x86_64': + p.check_returncode() + else: + self.skipTest(f'could not compile indirect function: {p}') + done() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + let attr_load_uses_borrow = |name: &str| { + let attr_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == name + } + _ => false, + }) + .unwrap_or_else(|| panic!("missing {name} attr load")); + matches!( + instructions + .get(attr_idx.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ) + }; + + assert!( + attr_load_uses_borrow("check_returncode"), + "plain with-protected conditional tail should keep borrowed p load, got instructions={instructions:?}" + ); + assert!( + attr_load_uses_borrow("skipTest"), + "plain with-protected conditional tail should keep borrowed self load, got instructions={instructions:?}" + ); + } + + #[test] + fn test_listcomp_cleanup_predecessor_does_not_deopt_following_conditional_tail() { + let code = compile_exec( + "\ +def f(self, compile_snippet): + sizes = [compile_snippet(i).co_stacksize for i in range(2, 5)] + if len(set(sizes)) != 1: + import dis, io + out = io.StringIO() + dis.dis(compile_snippet(1), file=out) + self.fail('%s\\n%s' % (sizes, out.getvalue())) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + let has_strong_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + let has_borrow_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + + for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] { + assert!( + has_borrow_load(name), + "expected listcomp-following conditional tail to borrow {name}, got instructions={:?}", + f.instructions + ); + } + for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] { + assert!( + !has_strong_load(name), + "listcomp cleanup predecessor should not force strong LOAD_FAST for {name}, got instructions={:?}", + f.instructions + ); + } + } + + #[test] + fn test_handler_resume_loop_conditional_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(self): + is_utf8 = (self.ENCODING == 'utf-8') + encode_errors = 'surrogateescape' if is_utf8 else 'strict' + strings = list(self.BYTES_STRINGS) + for text in self.STRINGS: + try: + encoded = text.encode(self.ENCODING, encode_errors) + if encoded not in strings: + strings.append(encoded) + except UnicodeEncodeError: + encoded = None + if is_utf8: + encoded2 = text.encode(self.ENCODING, 'surrogatepass') + if encoded2 != encoded: + strings.append(encoded2) + for encoded in strings: + self.consume(encoded) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + let has_strong_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + let has_borrow_load = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + + for name in ["is_utf8", "text", "self", "strings", "encoded2"] { + assert!( + has_strong_load(name), + "expected handler-resume loop tail to use strong LOAD_FAST for {name}, got instructions={:?}", + f.instructions + ); + } + assert!( + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastLoadFast { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == "encoded2" + && f.varnames[usize::from(right)] == "encoded" + } + _ => false, + }), + "expected encoded2/encoded comparison to use strong LOAD_FAST_LOAD_FAST, got instructions={:?}", + f.instructions + ); + assert!( + !f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == "encoded2" + && f.varnames[usize::from(right)] == "encoded" + } + _ => false, + }), + "handler-resume loop tail should not borrow encoded2/encoded comparison, got instructions={:?}", + f.instructions + ); + assert!( + has_borrow_load("strings"), + "expected later loop/list uses outside the deopt tail to keep borrowing strings" + ); + } + + #[test] + fn test_handler_resume_while_conditional_tail_keeps_borrow_load_fast() { + let code = compile_exec( + "\ +def f(value): + items = [] + while value: + try: + token, value = parse(value) + items.append(token) + except Error: + token, value = recover(value) + items.append(token) + if value and value[0] != ',': + item = items[-1] + token, value = recover(value) + item.extend(token) + if value and value[0] == ',': + items.append(',') + value = value[1:] + return items, value +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let handler_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_region = &f.instructions[..handler_start]; + let is_strong_local_load = |unit: &CodeUnit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + matches!( + f.varnames[usize::from(var_num.get(arg))].as_str(), + "value" | "items" | "token" | "item" + ) + } + Instruction::LoadFastLoadFast { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + let left = f.varnames[usize::from(left)].as_str(); + let right = f.varnames[usize::from(right)].as_str(); + matches!(left, "value" | "items" | "token" | "item") + || matches!(right, "value" | "items" | "token" | "item") + } + _ => false, + }; + + assert!( + !normal_region.iter().any(is_strong_local_load), + "while-loop handler resume tail should keep CPython-style borrowed normal loads, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_multi_handler_resume_while_tail_keeps_borrow_load_fast() { + let code = compile_exec( + "\ +def f(value): + items = [] + while value and value[0] != ';': + try: + token, value = parse(value) + items.append(token) + except Error: + leader = None + if value[0] in leaders: + leader, value = recover(value) + if not value or value[0] in ',;': + items.append(leader) + else: + token, value = recover_invalid(value) + if leader is not None: + token[:0] = [leader] + items.append(token) + elif value[0] == ',': + items.append(',') + else: + token, value = recover_invalid(value) + if leader is not None: + token[:0] = [leader] + items.append(token) + if value and value[0] not in ',;': + item = items[-1] + token, value = recover_invalid(value) + item.extend(token) + if value and value[0] == ',': + items.append(',') + value = value[1:] + return items, value +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let handler_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_region = &f.instructions[..handler_start]; + + assert!( + normal_region.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected multi-handler while tail to keep borrowed normal loads, got instructions={:?}", + f.instructions + ); + assert!( + !normal_region + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "multi-handler while tail should not be deoptimized to strong LOAD_FAST, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_multi_handler_resume_before_with_keeps_with_body_borrows() { + let code = compile_exec( + "\ +def f(self, input, cm): + try: + self.stdin.flush() + except BrokenPipeError: + pass + except ValueError: + if not self.stdin.closed: + raise + if not input: + self.stdin.close() + with cm() as selector: + if self.stdin and self._input: + selector.register(self.stdin, EVENT_WRITE) + while selector.get_map(): + ready = selector.select() + for key in ready: + self._fileobj2output[key].append(key) + return self.stdin +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let handler_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &f.instructions[..handler_start]; + let self_load_is = |unit: &CodeUnit, borrowed: bool| match unit.op { + Instruction::LoadFast { var_num } if !borrowed => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "self" + } + Instruction::LoadFastBorrow { var_num } if borrowed => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "self" + } + _ => false, + }; + + assert!( + warm_path.iter().any(|unit| self_load_is(unit, true)), + "expected multi-handler resume before with-body to keep borrowed self loads, got warm_path={warm_path:?}" + ); + assert!( + !warm_path.iter().any(|unit| self_load_is(unit, false)), + "multi-handler resume must not deopt with-body self loads to strong LOAD_FAST, got warm_path={warm_path:?}" + ); + } + + #[test] + fn test_suppressing_with_and_typed_except_resume_loop_method_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(proc, text): + try: + with proc.stdin as pipe: + try: + pipe.write(text) + except KeyboardInterrupt: + pass + except OSError: + pass + while True: + try: + proc.wait() + break + except KeyboardInterrupt: + pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let load_name = |unit: &CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + + let wait_receiver_is_strong = f.instructions.windows(2).any(|window| { + matches!(window[0].op, Instruction::LoadFast { .. }) + && load_name(&window[0]) == Some("proc") + && matches!(window[1].op, Instruction::LoadAttr { .. }) + }); + assert!( + wait_receiver_is_strong, + "CPython keeps proc.wait() receiver strong after suppressing-with and typed-except loop resumes, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_handler_break_join_loop_body_and_tail_keep_strong_load_fast() { + let code = compile_exec( + "\ +def f(function, stem): + result = [] + state = 0 + while True: + try: + next = function(stem, state) + except Exception: + break + if not isinstance(next, str): + break + result.append(next) + state += 1 + result.sort() + return result +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let load_name = |unit: &CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + let body_method_call_uses_strong_loads = instructions.windows(4).any(|window| { + matches!(window[0].op, Instruction::LoadFast { .. }) + && load_name(window[0]) == Some("result") + && matches!(window[1].op, Instruction::LoadAttr { .. }) + && matches!(window[2].op, Instruction::LoadFast { .. }) + && load_name(window[2]) == Some("next") + && matches!(window[3].op, Instruction::Call { .. }) + }); + assert!( + body_method_call_uses_strong_loads, + "CPython keeps result.append(next) loads strong after an except-break join, got instructions={:?}", + f.instructions + ); + + let body_update_uses_strong_load = instructions.windows(3).any(|window| { + matches!(window[0].op, Instruction::LoadFast { .. }) + && load_name(window[0]) == Some("state") + && matches!(window[1].op, Instruction::LoadSmallInt { .. }) + && matches!(window[2].op, Instruction::BinaryOp { .. }) + }); + assert!( + body_update_uses_strong_load, + "CPython keeps state += 1 load strong after an except-break join, got instructions={:?}", + f.instructions + ); + + let tail_method_call_uses_strong_load = instructions.windows(2).any(|window| { + matches!(window[0].op, Instruction::LoadFast { .. }) + && load_name(window[0]) == Some("result") + && matches!(window[1].op, Instruction::LoadAttr { .. }) + }); + assert!( + tail_method_call_uses_strong_load, + "CPython keeps result.sort() receiver strong after an except-break join, got instructions={:?}", + f.instructions + ); + + let return_uses_strong_load = instructions.windows(2).any(|window| { + matches!(window[0].op, Instruction::LoadFast { .. }) + && load_name(window[0]) == Some("result") + && matches!(window[1].op, Instruction::ReturnValue) + }); + assert!( + return_uses_strong_load, + "CPython keeps post-loop result return strong after an except-break join, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_handler_resume_to_loop_header_keeps_loop_header_borrows() { + let code = compile_exec( + "\ +def f(value, Phrase, get_word, errors, ENDS, DOT): + phrase = Phrase() + try: + token, value = get_word(value) + phrase.append(token) + except errors.HeaderParseError: + phrase.defects.append(errors.InvalidHeaderDefect('bad')) + while value and value[0] not in ENDS: + if value[0] == '.': + phrase.append(DOT) + phrase.defects.append(errors.ObsoleteHeaderDefect('dot')) + value = value[1:] + else: + token, value = get_word(value) + phrase.append(token) + return phrase, value +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let load_name = |unit: &CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + let loop_header_uses_borrow = instructions.windows(3).any(|window| { + matches!(window[0].op, Instruction::LoadFastBorrow { .. }) + && load_name(window[0]) == Some("value") + && matches!(window[1].op, Instruction::ToBool) + && matches!(window[2].op, Instruction::PopJumpIfFalse { .. }) + }); + assert!( + loop_header_uses_borrow, + "CPython keeps loop-header value test borrowed after a pre-loop handler resumes to that header, got instructions={:?}", + f.instructions + ); + + let return_pair_uses_borrow = instructions.windows(3).any(|window| { + matches!( + window[0].op, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) && matches!(window[1].op, Instruction::BuildTuple { .. }) + && matches!(window[2].op, Instruction::ReturnValue) + }); + assert!( + return_pair_uses_borrow, + "CPython keeps phrase/value return pair borrowed after a pre-loop handler resumes to the loop header, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_reraising_except_loop_break_tail_keeps_post_loop_borrows() { + let code = compile_exec( + "\ +def f(flag=1, count=0): + value = 2 + while value: + count += 1 + try: + if flag and value == 1: + flag -= 1 + break + value -= 1 + continue + except: + raise + return count > 2 or value != 1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let return_idx = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .expect("missing return"); + let tail = &instructions[return_idx.saturating_sub(12)..return_idx]; + + let load_name = |unit: &CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + for name in ["count", "value"] { + assert!( + tail.iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. }) + && load_name(unit) == Some(name)), + "post-loop condition should borrow {name}, got tail={tail:?}" + ); + assert!( + !tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. }) + && load_name(unit) == Some(name)), + "post-loop condition should not deopt {name} to LOAD_FAST, got tail={tail:?}" + ); + } + } + + #[test] + fn test_try_except_continue_keeps_try_line_nop_before_continue_jump() { + let code = compile_exec( + "\ +def f(done=False): + while not done: + done = True + try: + continue + except: + done = False + return done +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + assert!( + instructions.windows(2).any(|window| matches!( + window, + [ + CodeUnit { + op: Instruction::Nop, + .. + }, + CodeUnit { + op: Instruction::JumpBackward { .. }, + .. + } + ] + )), + "try/except continue should keep CPython-style try-line NOP before continue jump, got instructions={instructions:?}" + ); + } + + #[test] + fn test_for_else_pass_keeps_line_marker_after_pop_iter() { + let code = compile_exec( + "\ +def f(): + for item in (): + pass + else: + pass + marker = 1 + return marker +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + + assert!( + instructions.windows(2).any(|window| matches!( + window, + [ + CodeUnit { + op: Instruction::PopIter, + .. + }, + CodeUnit { + op: Instruction::Nop, + .. + } + ] + )), + "for-else pass should keep CPython-style else-line NOP after POP_ITER, got instructions={instructions:?}" + ); + } + + #[test] + fn test_folded_if_chain_after_previous_chain_keeps_final_elif_line_marker() { + let code = compile_exec( + "\ +def f(): + if 0: pass + elif 0: pass + if 0: pass + elif 0: pass + elif 0: pass + elif 0: pass + else: pass +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let nop_count = f + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::Nop)) + .count(); + + assert_eq!( + nop_count, 6, + "folded if chains should preserve CPython-style line-marker NOPs, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_handler_resume_before_later_loop_keeps_borrowed_tail_loads() { + let code = compile_exec( + "\ +def f(msg, category): + s = 'x' + if msg.line is None: + try: + import linecache + line = linecache.getline(msg.filename, msg.lineno) + except Exception: + line = None + linecache = None + else: + line = msg.line + if line: + line = line.strip() + s += ' %s\\n' % line + if msg.source is not None: + try: + import tracemalloc + except Exception: + suggest_tracemalloc = False + tb = None + else: + try: + suggest_tracemalloc = not tracemalloc.is_tracing() + tb = tracemalloc.get_object_traceback(msg.source) + except Exception: + suggest_tracemalloc = False + tb = None + if tb is not None: + for frame in tb: + s += frame.filename + elif suggest_tracemalloc: + s += category + return s +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let is_borrow_load = |unit: &&bytecode::CodeUnit, name: &str| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }; + + assert!( + ops.windows(5).any(|window| { + is_borrow_load(&window[0], "line") + && matches!(window[1].op, Instruction::ToBool) + && matches!(window[2].op, Instruction::PopJumpIfFalse { .. }) + && matches!(window[3].op, Instruction::NotTaken) + && is_borrow_load(&window[4], "line") + }), + "handler resume before a later independent loop should preserve CPython-style borrowed line loads, got instructions={:?}", + f.instructions + ); + assert!( + ops.windows(2) + .filter(|window| { + is_borrow_load(&window[0], "tracemalloc") + && matches!(window[1].op, Instruction::LoadAttr { .. }) + }) + .count() + >= 2, + "independent later loop should not deopt tracemalloc loads before the loop, got instructions={:?}", + f.instructions + ); + assert!( + ops.windows(2).any(|window| { + is_borrow_load(&window[0], "s") && matches!(window[1].op, Instruction::ReturnValue) + }), + "final return should keep CPython-style borrowed s load, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_async_early_return_send_tail_uses_strong_load_fast_after_entry() { + let code = compile_exec( + "\ +class C: + async def _sock_sendfile_native(self, sock, file, offset, count): + try: + fileno = file.fileno() + except (AttributeError, io.UnsupportedOperation) as err: + raise exceptions.SendfileNotAvailableError('not a regular file') + try: + fsize = os.fstat(fileno).st_size + except OSError: + raise exceptions.SendfileNotAvailableError('not a regular file') + blocksize = count if count else fsize + if not blocksize: + return 0 + blocksize = min(blocksize, 0xffff_ffff) + end_pos = min(offset + count, fsize) if count else fsize + offset = min(offset, fsize) + total_sent = 0 + try: + while True: + blocksize = min(end_pos - offset, blocksize) + if blocksize <= 0: + return total_sent + await self._proactor.sendfile(sock, file, offset, blocksize) + offset += blocksize + total_sent += blocksize + finally: + if total_sent > 0: + file.seek(offset) +", + ); + let f = find_code(&code, "_sock_sendfile_native").expect("missing method code"); + + let names_for_unit = |unit: &bytecode::CodeUnit| -> Vec { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + vec![f.varnames[usize::from(var_num.get(arg))].to_string()] + } + Instruction::LoadFastLoadFast { var_nums } + | Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let (left, right) = var_nums.get(arg).indexes(); + vec![ + f.varnames[usize::from(left)].to_string(), + f.varnames[usize::from(right)].to_string(), + ] + } + _ => Vec::new(), + } + }; + + let borrowed_names: Vec<_> = f + .instructions + .iter() + .filter_map(|unit| match unit.op { + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } => Some(names_for_unit(unit)), + _ => None, + }) + .collect(); + assert_eq!( + borrowed_names, + vec![vec!["file".to_owned()]], + "only the initial file.fileno() receiver should borrow, got instructions={:?}", + f.instructions + ); + + let has_strong_name = |name: &str| { + f.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } + ) && names_for_unit(unit).iter().any(|loaded| loaded == name) + }) + }; + + for name in [ + "fileno", + "count", + "blocksize", + "offset", + "fsize", + "end_pos", + "total_sent", + "file", + "self", + "sock", + ] { + assert!( + has_strong_name(name), + "async early-return send tail should use strong LOAD_FAST for {name}, got instructions={:?}", + f.instructions + ); + } + } + + #[test] + fn test_async_with_return_await_after_early_return_keeps_borrow_load_fast() { + let code = compile_exec( + "\ +async def wait_for(fut, timeout): + if timeout is not None and timeout <= 0: + fut = ensure_future(fut) + if fut.done(): + return fut.result() + await cancel_and_wait(fut) + try: + return fut.result() + except CancelledError as exc: + raise TimeoutError from exc + async with timeout_cm(timeout): + return await fut +", + ); + let f = find_code(&code, "wait_for").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let loaded_names = |unit: &bytecode::CodeUnit| -> Vec { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + match unit.op { + Instruction::LoadFastBorrow { var_num } => { + vec![f.varnames[usize::from(var_num.get(arg))].to_string()] + } + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let (left, right) = var_nums.get(arg).indexes(); + vec![ + f.varnames[usize::from(left)].to_string(), + f.varnames[usize::from(right)].to_string(), + ] + } + _ => Vec::new(), + } + }; + let borrowed_names: Vec<_> = f + .instructions + .iter() + .filter_map(|unit| { + let names = loaded_names(unit); + (!names.is_empty()).then_some(names) + }) + .collect(); + + assert!( + borrowed_names + .iter() + .flatten() + .any(|name| name == "timeout"), + "CPython preserves borrowed timeout loads in async-with setup, got instructions={:?}", + f.instructions + ); + assert!( + ops.windows(4).any(|window| { + matches!(window[0].op, Instruction::LoadFastBorrow { .. }) + && loaded_names(window[0]).iter().any(|name| name == "fut") + && matches!(window[1].op, Instruction::LoadAttr { namei } + if f.names[usize::try_from(namei.get(OpArg::new(u32::from(u8::from(window[1].arg)))).name_idx()).unwrap()] == "result") + && matches!(window[2].op, Instruction::Call { .. }) + && matches!(window[3].op, Instruction::ReturnValue) + }), + "CPython preserves borrowed fut loads for direct try-return method calls, got instructions={:?}", + f.instructions + ); + } + + #[test] + fn test_protected_import_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(s, size, pos, errors): + message = 'x' + look = pos + try: + import unicodedata + except ImportError: + return None + if look < size and chr(s[look]) == '{': + while look < size and chr(s[look]) != '}': + look += 1 + if look > pos + 1 and look < size and chr(s[look]) == '}': + message = 'y' + return message +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let import_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let protected_tail = &ops[import_idx + 1..]; + + assert!( + !protected_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected protected import tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}" + ); + + assert!( + protected_tail + .iter() + .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), + "expected protected import tail to keep LOAD_FAST_LOAD_FAST ops, got tail={protected_tail:?}" + ); + } + + #[test] + fn test_nested_protected_import_tail_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(self, mode, compresslevel): + try: + try: + import zlib + except ImportError: + raise RuntimeError from None + self.zlib = zlib + self.crc = zlib.crc32(b'') + if mode == 'r': + self.exception = zlib.error + self._init_read_gz() + else: + self._init_write_gz(compresslevel) + except: + self.closed = True + raise +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let import_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let nested_protected_tail = &ops[import_idx + 1..handler_start]; + + assert!( + !nested_protected_tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "CPython keeps strong LOAD_FAST ops after a nested protected import tail, got tail={nested_protected_tail:?}" + ); + assert!( + nested_protected_tail + .iter() + .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), + "expected nested protected import tail to keep LOAD_FAST_LOAD_FAST, got tail={nested_protected_tail:?}" + ); + } + + #[test] + fn test_unprotected_import_before_with_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, document): + from xml.etree import ElementInclude + document = self.xinclude_loader('C1.xml') + with self.assertRaises(OSError) as cm: + ElementInclude.include(document, self.xinclude_loader) + self.assertEqual(str(cm.exception), 'resource not found') +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let import_idx = ops + .iter() + .position(|op| matches!(op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let handler_start = ops + .iter() + .position(|op| matches!(op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &ops[import_idx + 1..handler_start]; + + assert!( + warm_path.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "expected unprotected import before with-block to keep LOAD_FAST_BORROW ops, got warm_path={warm_path:?}" + ); + assert!( + warm_path + .iter() + .any(|op| matches!(op, Instruction::LoadFastBorrowLoadFastBorrow { .. })), + "expected with body arguments to keep LOAD_FAST_BORROW_LOAD_FAST_BORROW, got warm_path={warm_path:?}" + ); + } + + #[test] + fn test_from_import_after_conditional_store_join_uses_strong_prefix_loads() { + let code = compile_exec( + "\ +def f(x): + if x is None: + x = 'a' + y = x.rpartition('.')[0] + from pkgutil import get_importer + return y +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let import_from_idx = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::ImportFrom { .. })) + .expect("missing IMPORT_FROM"); + let rpartition_idx = ops[..import_from_idx] + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "rpartition" + } + _ => false, + }) + .expect("missing rpartition LOAD_ATTR"); + + assert!( + matches!(ops[rpartition_idx - 1].op, Instruction::LoadFast { .. }), + "CPython keeps the conditional-store join receiver strong before IMPORT_FROM, got ops={ops:?}" + ); + } + + #[test] + fn test_plain_import_after_conditional_store_join_keeps_borrow_prefix_loads() { + let code = compile_exec( + "\ +def f(x): + if x is None: + x = 'a' + y = x.rpartition('.')[0] + import pkgutil + return y +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let import_name_idx = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::ImportName { .. })) + .expect("missing IMPORT_NAME"); + let rpartition_idx = ops[..import_name_idx] + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "rpartition" + } + _ => false, + }) + .expect("missing rpartition LOAD_ATTR"); + + assert!( + matches!( + ops[rpartition_idx - 1].op, + Instruction::LoadFastBorrow { .. } + ), + "plain import after conditional-store join should keep CPython-style borrowed receiver, got ops={ops:?}" + ); + } + + #[test] + fn test_unprotected_prefix_before_try_keeps_attr_subscript_borrow() { + let code = compile_exec( + "\ +def f(): + import sys, getopt + usage = f'usage: {sys.argv[0]}' + try: + opts, args = getopt.getopt(sys.argv[1:], 'h') + except getopt.error as msg: + sys.stdout = sys.stderr + print(msg) + print(usage) + sys.exit(2) + return usage +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let first_argv_idx = f + .instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "argv" + } + _ => false, + }) + .expect("missing argv attr load"); + let receiver = f.instructions[..first_argv_idx] + .iter() + .rev() + .find(|unit| !matches!(unit.op, Instruction::Cache)) + .expect("missing argv receiver") + .op; + + assert!( + matches!(receiver, Instruction::LoadFastBorrow { .. }), + "unprotected prefix before try should keep CPython-style LOAD_FAST_BORROW receiver, got {receiver:?}" + ); + } + + #[test] + fn test_terminal_except_inlined_comprehension_keeps_borrowed_warm_loads() { + let code = compile_exec( + r##" +def f(output): + output = re.sub(r"\[[0-9]+ refs\]", "", output) + try: + result = [ + row.split("\t") + for row in output.splitlines() + if row and not row.startswith('#') + ] + result.sort(key=lambda row: int(row[0])) + result = [row[1] for row in result] + return "\n".join(result) + except (IndexError, ValueError): + raise AssertionError( + "tracer produced unparsable output:\n{}".format(output) + ) +"##, + ); + let f = find_code(&code, "f").expect("missing f code"); + let handler_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &f.instructions[..handler_start]; + let load_fast_name = |unit: &bytecode::CodeUnit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + let borrow_name = |unit: &bytecode::CodeUnit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + + assert!( + warm_path + .iter() + .filter_map(load_fast_name) + .all(|name| name != "row" && name != "result"), + "terminal-except inlined comprehension warm path should keep CPython-style borrowed row/result loads, got warm_path={warm_path:?}" + ); + for name in ["row", "result"] { + assert!( + warm_path + .iter() + .filter_map(borrow_name) + .any(|var| var == name), + "expected borrowed {name} load in terminal-except inlined comprehension warm path, got warm_path={warm_path:?}" + ); + } + } + + #[test] + fn test_outer_guarded_protected_import_keeps_borrow_tail() { + let code = compile_exec( + "\ +def f(sys, os, file): + if sys.platform == 'win32': + try: + import nt + if not nt._supports_virtual_terminal(): + return False + except (ImportError, AttributeError): + return False + try: + return os.isatty(file.fileno()) + except OSError: + return hasattr(file, 'isatty') and file.isatty() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let borrows_name = |name: &str| { + f.instructions.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + } + _ => false, + }) + }; + + for name in ["nt", "os", "file"] { + assert!( + borrows_name(name), + "outer-guarded protected import should keep CPython-style borrow for {name}, got instructions={:?}", + f.instructions + ); + } + } + + #[test] + fn test_loop_or_break_continue_orders_break_before_backedge() { + let code = compile_exec( + "\ +def f(self, quoted): + while True: + if self.state == 'x': + if self.token or (self.posix and quoted): + break + else: + continue + elif self.state == 'y': + self.consume() + x = self.a + self.b + self.c + return x +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let quoted_load = ops + .iter() + .position(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "quoted" + } + _ => false, + }) + .expect("missing quoted LOAD_FAST_BORROW"); + let final_cond = ops[quoted_load + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| quoted_load + 1 + idx) + .expect("missing final conditional jump"); + assert!( + matches!(ops[final_cond].op, Instruction::PopJumpIfFalse { .. }), + "expected CPython-style inverted final condition, got ops={ops:?}" + ); + let break_jump_idx = ops[final_cond + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpForward { .. })) + .map(|idx| final_cond + 1 + idx) + .expect("missing break jump after condition"); + let jump_back_idx = ops[final_cond + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| final_cond + 1 + idx) + .expect("missing continue backedge"); + assert!( + break_jump_idx < jump_back_idx, + "expected break jump before continue backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_for_continue_before_return_orders_backedge_before_return_body() { + let code = compile_exec( + "\ +def f(self): + for version in AllowedVersions: + if not version in self.capabilities: + continue + self.PROTOCOL_VERSION = version + return + raise self.error('x') +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let contains_idx = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::ContainsOp { .. })) + .expect("missing containment test"); + let cond_idx = ops[contains_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| contains_idx + 1 + idx) + .expect("missing conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style condition targeting the return body, got ops={ops:?}" + ); + + let backedge_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing continue backedge"); + let store_attr_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| match unit.op { + Instruction::StoreAttr { namei } => { + let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(namei).unwrap()].as_str() == "PROTOCOL_VERSION" + } + _ => false, + }) + .map(|idx| cond_idx + 1 + idx) + .expect("missing PROTOCOL_VERSION store"); + assert!( + backedge_idx < store_attr_idx, + "expected continue backedge before return body, got ops={ops:?}" + ); + } + + #[test] + fn test_while_conditional_return_orders_backedge_before_return_body() { + let code = compile_exec( + "\ +def f(self, tag): + while self._get_response(): + if self.tagged_commands[tag]: + return tag +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let subscript_idx = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::BinaryOp { .. })) + .expect("missing tagged_commands subscript"); + let cond_idx = ops[subscript_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| subscript_idx + 1 + idx) + .expect("missing conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style condition targeting return body, got ops={ops:?}" + ); + let backedge_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing loop backedge"); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing return"); + assert!( + backedge_idx < return_idx, + "expected loop backedge before return body, got ops={ops:?}" + ); + } + + #[test] + fn test_while_boolop_conditional_return_splits_backedges_before_return_body() { + let code = compile_exec( + "\ +def f(flags, A, B, stop): + while True: + if stop: + break + if flags & A and flags & B: + return None + return flags +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(12).any(|window| { + matches!( + window, + [ + Instruction::BinaryOp { .. }, + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::BinaryOp { .. }, + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "boolop conditional return in a while tail should split CPython-style false backedges before the return body, got ops={ops:?}" + ); + } + + #[test] + fn test_for_break_to_return_orders_backedge_before_return() { + let code = compile_exec( + "\ +def f(it): + best = 10 + body = None + for prio, part in it: + if prio < best: + best = prio + body = part + if prio == 0: + break + return body +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .nth(1) + .map(|(idx, _)| idx) + .expect("missing break comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing break conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style true jump to break return path, got ops={ops:?}" + ); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing loop backedge before break return"); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing break return path"); + assert!( + jump_back_idx < return_idx, + "expected loop backedge before break return block, got ops={ops:?}" + ); + } + + #[test] + fn test_for_conditional_raise_orders_backedge_before_raise() { + let code = compile_exec( + "\ +def f(items, limit): + found = 0 + for item in items: + if item: + found += 1 + if found >= limit: + raise ValueError(found) + return found +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .find(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .map(|(idx, _)| idx) + .expect("missing raise comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing raise conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style true jump to raise path, got ops={ops:?}" + ); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing loop backedge before raise"); + let raise_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing raise path"); + assert!( + jump_back_idx < raise_idx, + "expected loop backedge before conditional raise block, got ops={ops:?}" + ); + } + + #[test] + fn test_simple_for_conditional_raise_orders_backedge_before_raise() { + let code = compile_exec( + "\ +def f(kw): + for k in ('stdout', 'check'): + if k in kw: + raise ValueError(f'{k} argument not allowed, it will be overridden.') +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected CPython-style true jump to raise path after loop backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "unexpected conditional raise body before loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_nested_boolop_exit_keeps_cpython_backedge_line_order() { + let code = compile_exec( + "\ +def f(found, value, m, done, name, renamed_variables, keep_unresolved, variables): + for _ in [0]: + if m is not None: + if found: + if '$' in value: + done[name] = value + else: + try: + value = int(value) + except ValueError: + done[name] = value.strip() + else: + done[name] = value + variables.remove(name) + if name.startswith('PY_') \\ + and name[3:] in renamed_variables: + name = name[3:] + if name not in done: + done[name] = value + else: + if keep_unresolved: + done[name] = value + variables.remove(name) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops_lines: Vec<_> = f + .instructions + .iter() + .zip(&f.locations) + .filter_map(|(unit, (location, _))| { + (!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get())) + }) + .collect(); + + assert!( + ops_lines.windows(9).any(|window| { + matches!( + window, + [ + (Instruction::StoreSubscr, 19), + (Instruction::JumpBackward { .. }, 19), + (Instruction::JumpBackward { .. }, 18), + (Instruction::JumpBackward { .. }, 16), + (Instruction::JumpBackward { .. }, 15), + (Instruction::JumpBackward { .. }, 4), + ( + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + 21 + ), + (Instruction::ToBool, 21), + (Instruction::PopJumpIfFalse { .. }, 21), + ] + ) + }), + "expected CPython-style nested boolop backedge line order before enclosing else, got ops_lines={ops_lines:?}" + ); + } + + #[test] + fn test_loop_conditional_raise_before_elif_keeps_raise_before_backedge() { + let code = compile_exec( + "\ +def f(checks, missing, named): + for check in checks: + if check == 1: + if missing: + raise ValueError('x') + elif check is named: + pass + return checks +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadGlobal { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "expected CPython-style false edge into raise body before following elif chain, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + ] + ) + }), + "unexpected loop backedge before conditional raise body in if/elif chain, got ops={ops:?}" + ); + } + + #[test] + fn test_protected_for_is_none_raise_threads_backedge_before_raise() { + let code = compile_exec( + "\ +def f(stacklevel, frame, skip_file_prefixes): + try: + for x in range(stacklevel - 1): + frame = _next_external_frame(frame, skip_file_prefixes) + if frame is None: + raise ValueError + except ValueError: + frame = None + return frame +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::PopJumpIfNone { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected protected is-None raise path to match CPython's backedge-before-raise layout, got ops={ops:?}" + ); + } + + #[test] + fn test_exception_handler_loop_conditional_raise_orders_backedge_before_raise() { + let code = compile_exec( + "\ +def f(chunk, dec, i): + try: + for c in chunk: + acc = dec[c] + except TypeError: + for j, c in enumerate(chunk): + if dec[c] is None: + raise ValueError('%d' % (i + j)) from None raise - except BaseException as exc: - self.handle({'exception': exc, 'loop': self}) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::BinaryOp { .. }, + Instruction::PopJumpIfNone { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "expected exception-handler loop false path to jump back before raise body, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::BinaryOp { .. }, + Instruction::PopJumpIfNotNone { .. }, + Instruction::NotTaken, + Instruction::LoadGlobal { .. }, + ] + ) + }), + "unexpected exception-handler loop raise body before backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_exception_handler_loop_conditional_return_orders_backedge_before_return() { + let code = compile_exec( + "\ +def f(cls, value): + try: + return cls[value] + except TypeError: + for name, values in cls.items(): + if value in values: + return cls[name] + for name, member in cls.items(): + if value == member.value: + return cls[name] +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. }, + ] + ) + }), + "expected exception-handler loop false path to jump back before return body, got ops={ops:?}" + ); + assert!( + !ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. }, + ] + ) + }), + "unexpected exception-handler loop return body before backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_loop_if_body_keeps_fallthrough_before_implicit_continue_backedge() { + let code = compile_exec( + "\ +def f(b, curr, curr_append, decoded_append, packI, curr_clear): + for x in b: + if 33 <= x <= 117: + curr_append(x) + if len(curr) == 5: + acc = 0 + for x in curr: + acc = 85 * acc + (x - 33) + decoded_append(packI(acc)) + curr_clear() + elif x == 122: + decoded_append(0) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadSmallInt { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "expected CPython-style conditional body fallthrough before implicit continue backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::StoreFast { .. }, + ] + ) + }), + "unexpected inverted conditional with implicit continue backedge before body, got ops={ops:?}" + ); + } + + #[test] + fn test_if_not_continue_before_conditional_listcomp_body_keeps_cpython_layout() { + let code = compile_exec( + "\ +def f(data, use): + for line in data: + line = line.strip() + if not line: + continue + if line.startswith('@'): + continue + values = [use(x) for x in line] + use(values) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "if-not continue should keep CPython's forward true edge over the continue backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_chained_compare_continue_does_not_duplicate_cleanup_backedge() { + let code = compile_exec( + "\ +def f(items): + offsets = [] + ranges = [(0, 10), (20, 30)] + for item in items: + trans_time, offset_before, offset_after = item + for dt_min, dt_max in ranges: + if trans_time is not None and not (dt_min <= trans_time <= dt_max): + continue + if offset_before not in offsets: + offsets.append(offset_before) + if offset_after not in offsets: + offsets.append(offset_after) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(4).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::ContainsOp { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFast { .. }, + ] + ) + }), + "chained-compare continue cleanup should fall through to the following body after one backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(3).any(|window| { + matches!( + window, + [ + Instruction::PopTop, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "chained-compare continue cleanup should not duplicate the loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_try_else_loop_if_body_keeps_cpython_fallthrough_before_backedge() { + let code = compile_exec( + "\ +def f(self, ready, selector, key, input_view, os, BrokenPipeError): + for key, events in ready: + if key.fileobj is self.stdin: + chunk = input_view[self._input_offset:self._input_offset + 1] + try: + self._input_offset += os.write(key.fd, chunk) + except BrokenPipeError: + selector.unregister(key.fileobj) + key.fileobj.close() + else: + if self._input_offset >= len(input_view): + selector.unregister(key.fileobj) + key.fileobj.close() + elif key.fileobj in (self.stdout, self.stderr): + self.read(key) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "expected CPython-style try-else if body fallthrough before loop backedge, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "unexpected inverted try-else conditional with loop backedge before body, got ops={ops:?}" + ); + } + + #[test] + fn test_try_else_after_conditional_raise_keeps_loop_if_body_before_backedge() { + let code = compile_exec( + "\ +def f(seq, flag, stat, OSError, pred, SpecialFileError): + for i in seq: + try: + st = stat(i) + except OSError: + pass + else: + if pred(st.mode): + raise SpecialFileError(i) + if flag and i == 0: + x = st.real +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::StoreFast { .. }, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "try-else tail after conditional raise should keep CPython body-before-backedge layout, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "try-else tail should not invert the inner condition before its body, got ops={ops:?}" + ); + } + + #[test] + fn test_explicit_continue_after_return_orders_return_before_backedge() { + let code = compile_exec( + "\ +def f(j, n): + while j < n: + if j < 0: + return j + continue + return -1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .nth(1) + .map(|(idx, _)| idx) + .expect("missing inner comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }), + "expected CPython-style false jump to explicit continue, got ops={ops:?}" + ); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing return path"); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing explicit continue backedge"); + assert!( + return_idx < jump_back_idx, + "expected return block before explicit continue backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_implicit_while_tail_return_orders_backedge_before_return() { + let code = compile_exec( + "\ +def f(self, j, n): + while j < n: + name, j = self.scan(j) + if j < 0: + return j + return -1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .nth(1) + .map(|(idx, _)| idx) + .expect("missing inner comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style true jump to return, got ops={ops:?}" + ); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing implicit backedge"); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing return path"); + assert!( + jump_back_idx < return_idx, + "expected implicit loop backedge before return block, got ops={ops:?}" + ); + } + + #[test] + fn test_branch_arm_implicit_continue_keeps_return_before_backedge() { + let code = compile_exec( + "\ +def f(self, j, n, c): + while j < n: + if c == 'x': + j = self.step(j) + if j < 0: + return j + elif c == 'y': + j = j + 1 + return -1 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .nth(2) + .map(|(idx, _)| idx) + .expect("missing branch-arm return comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing branch-arm conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }), + "expected CPython-style false jump to branch-arm continuation, got ops={ops:?}" + ); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing branch-arm return path"); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing branch-arm loop backedge"); + assert!( + return_idx < jump_back_idx, + "expected branch-arm return before loop backedge, got ops={ops:?}" + ); + } + + #[test] + fn test_nested_implicit_while_tail_return_orders_backedge_before_return() { + let code = compile_exec( + "\ +def f(self, rawdata, j, match): + while 1: + c = rawdata[j:j + 1] + if c in \"'\\\"\": + m = match(rawdata, j) + if not m: + return -1 + j = m.end() + else: + name, j = self.scan(j) + if j < 0: + return j +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let compare_idx = ops + .iter() + .enumerate() + .rfind(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) + .map(|(idx, _)| idx) + .expect("missing nested tail comparison"); + let cond_idx = ops[compare_idx + 1..] + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + }) + .map(|idx| compare_idx + 1 + idx) + .expect("missing nested tail conditional jump"); + assert!( + matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), + "expected CPython-style true jump to nested return path, got ops={ops:?}" + ); + let jump_back_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) + .map(|idx| cond_idx + 1 + idx) + .expect("missing nested tail loop backedge"); + let return_idx = ops[cond_idx + 1..] + .iter() + .position(|unit| matches!(unit.op, Instruction::ReturnValue)) + .map(|idx| cond_idx + 1 + idx) + .expect("missing nested tail return path"); + assert!( + jump_back_idx < return_idx, + "expected nested implicit loop backedge before return block, got ops={ops:?}" + ); + } + + #[test] + fn test_join_store_global_before_import_keeps_strong_load_fast() { + let code = compile_exec( + "\ +def f(module=None): + global ET + if module is None: + module = pyET + ET = module + from xml.etree import ElementPath +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::LoadFast { .. }, + Instruction::StoreGlobal { .. }, + ] + ) + }), + "expected CPython-style strong LOAD_FAST before join STORE_GLOBAL followed by import, got ops={ops:?}" + ); + } + + #[test] + fn test_handler_resume_join_keeps_borrow_in_common_tail() { + let code = compile_exec( + "\ +def f(p, errors, s, pos, look, final, escape_start, st): + try: + chr_codec = unicodedata.lookup('%s' % st) + except LookupError as e: + x = unicode_call_errorhandler( + errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1 + ) else: - self.future = fut - fut.add_done_callback(self.loop_reading) + x = chr_codec, look + 1 + p.append(x[0]) + pos = x[1] + if not final: + pos = escape_start + return p, pos + return unicode_call_errorhandler( + errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1 + ) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let append_idx = f + .instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append" + } + _ => false, + }) + .expect("missing append tail"); + let tail: Vec<_> = f.instructions[append_idx.saturating_sub(1)..] + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + matches!( + tail.as_slice(), + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastBorrow { .. }, + .., + ] + ), + "expected handler resume common tail to start with borrowed append receiver/arg loads, got tail={tail:?}" + ); + assert!( + tail.iter().any(|op| { + matches!( + op, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastBorrow { .. } + ) + }), + "expected handler resume common tail to keep borrowed LOAD_FAST ops, got tail={tail:?}" + ); + } + + #[test] + fn test_multi_handler_guarded_resume_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(a): + try: + g() + except ValueError: + pass + except TypeError: + pass + if a: + return a.x + return 0 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. }, + ] + ) + }), + "expected guarded resume tail to keep borrowed guard/body loads, got ops={ops:?}" + ); + assert!( + ops.windows(2).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. } + ] + ) + }), + "expected guarded resume tail attr access to keep borrowed receiver, got ops={ops:?}" + ); + } + + #[test] + fn test_multi_handler_method_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, xs): + for vals, expected in xs: + try: + actual = g(vals) + except OverflowError: + self.fail(expected) + except ValueError: + self.fail(expected) + self.assertEqual(actual, expected) ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let recv_idx = instructions + + let assert_equal_idx = ops .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "recv" - } - _ => false, - }) - .expect("missing recv LOAD_ATTR"); - let done_callback_idx = instructions + .position(|op| matches!(op, Instruction::LoadAttr { .. })) + .expect("missing assertEqual LOAD_ATTR"); + let tail = &ops[assert_equal_idx.saturating_sub(1)..]; + + assert!( + matches!(tail.first(), Some(Instruction::LoadFastBorrow { .. })), + "expected multi-handler method-call tail receiver to keep LOAD_FAST_BORROW, got tail={tail:?}" + ); + assert!( + tail.iter() + .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), + "expected multi-handler method-call tail args to keep borrowed loads, got tail={tail:?}" + ); + } + + #[test] + fn test_named_except_cleanup_loop_header_keeps_borrow_in_for_loop() { + let code = compile_exec( + "\ +def f(args): + for arg in args: + try: + _wm._setoption(arg) + except _wm._OptionError as msg: + print('Invalid -W option ignored:', msg, file=sys.stderr) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let attr_idx = f + .instructions .iter() .position(|unit| match unit.op { Instruction::LoadAttr { namei } => { let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "add_done_callback" + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_setoption" } _ => false, }) - .expect("missing add_done_callback LOAD_ATTR"); - let handler_start = instructions + .expect("missing _setoption attr load"); + let window: Vec<_> = f.instructions[attr_idx + 1..] .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = - &instructions[recv_idx.saturating_sub(3)..handler_start.min(done_callback_idx + 3)]; + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .take(3) + .collect(); + assert!( + matches!( + window.as_slice(), + [ + Instruction::LoadFastBorrow { .. }, + Instruction::Call { .. }, + Instruction::PopTop + ] + ), + "expected loop body call to keep borrowed arg load after named-except cleanup, got window={window:?}" + ); + } + + #[test] + fn test_multi_named_except_loop_header_keeps_borrow_for_normal_path() { + let code = compile_exec( + "\ +def f(self): + for badval in ['illegal', -1, 1 << 32]: + class A: + def __len__(self): + return badval + try: + bool(A()) + except (Exception) as e_bool: + try: + len(A()) + except (Exception) as e_len: + self.assertEqual(str(e_bool), str(e_len)) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); assert!( - tail.iter().any(|unit| { + ops.windows(4).any(|window| { matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::LoadBuildClass, + Instruction::PushNull, + Instruction::LoadFastBorrow { .. }, + Instruction::BuildTuple { .. }, + ] ) }), - "handler chain with handled returns should keep borrowed warm/else loads, got tail={tail:?}" + "expected class closure setup in loop header to borrow badval, got ops={ops:?}" ); assert!( - !tail.iter().any(|unit| { + ops.windows(5).any(|window| { matches!( - unit.op, - Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } + window, + [ + Instruction::LoadFastBorrow { .. }, + Instruction::PushNull, + Instruction::Call { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] ) }), - "handler chain with handled returns should not force strong warm/else loads, got tail={tail:?}" + "expected normal bool(A()) path in loop header to borrow A, got ops={ops:?}" ); } #[test] - fn test_with_protected_conditional_tail_without_exception_match_keeps_borrow() { + fn test_named_except_cleanup_simple_resume_tail_keeps_borrow() { let code = compile_exec( "\ -def f(self, cm, p, platform): - with cm: - if p.returncode != 0: - if platform.machine() == 'x86_64': - p.check_returncode() - else: - self.skipTest(f'could not compile indirect function: {p}') - done() +def f(self): + try: + 1 / 0 + except Exception as e: + tb = e.__traceback__ + self.get_disassemble_as_string(tb.tb_frame.f_code, tb.tb_lasti) ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -20604,1447 +29562,1448 @@ def f(self, cm, p, platform): .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - - let attr_load_uses_borrow = |name: &str| { - let attr_idx = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == name - } - _ => false, - }) - .unwrap_or_else(|| panic!("missing {name} attr load")); + let attr_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "get_disassemble_as_string" + } + _ => false, + }) + .expect("missing LOAD_ATTR for get_disassemble_as_string"); + let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); + assert!( matches!( - instructions - .get(attr_idx.saturating_sub(1)) - .map(|unit| unit.op), + ops.get(attr_idx - 1), Some(Instruction::LoadFastBorrow { .. }) - ) - }; - - assert!( - attr_load_uses_borrow("check_returncode"), - "plain with-protected conditional tail should keep borrowed p load, got instructions={instructions:?}" + ), + "expected named-except resume tail to keep borrowed self load, got ops={ops:?}" ); assert!( - attr_load_uses_borrow("skipTest"), - "plain with-protected conditional tail should keep borrowed self load, got instructions={instructions:?}" + matches!( + ops.get(attr_idx + 4), + Some(Instruction::LoadFastBorrow { .. }) + ), + "expected named-except resume tail to keep borrowed tb load, got ops={ops:?}" ); } #[test] - fn test_listcomp_cleanup_predecessor_does_not_deopt_following_conditional_tail() { + fn test_named_except_cleanup_conditional_raise_tail_keeps_borrow() { let code = compile_exec( "\ -def f(self, compile_snippet): - sizes = [compile_snippet(i).co_stacksize for i in range(2, 5)] - if len(set(sizes)) != 1: - import dis, io - out = io.StringIO() - dis.dis(compile_snippet(1), file=out) - self.fail('%s\\n%s' % (sizes, out.getvalue())) +def f(self): + try: + output = self.trace() + output = output.strip() + except (A, B, C) as fnfe: + output = str(fnfe) + if output != 'probe: success': + raise E('{} {}'.format(self.command[0], output)) ", ); let f = find_code(&code, "f").expect("missing f code"); - - let has_strong_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - let has_borrow_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - - for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] { - assert!( - has_borrow_load(name), - "expected listcomp-following conditional tail to borrow {name}, got instructions={:?}", - f.instructions - ); - } - for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] { - assert!( - !has_strong_load(name), - "listcomp cleanup predecessor should not force strong LOAD_FAST for {name}, got instructions={:?}", - f.instructions - ); - } + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let raise_idx = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })) + .expect("missing conditional raise"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[..handler_start.min(raise_idx)]; + + assert!( + tail.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "named-except cleanup conditional raise tail should keep borrowed loads, got tail={tail:?}" + ); + assert!( + !tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "named-except cleanup conditional raise tail should not force strong LOAD_FAST, got tail={tail:?}" + ); } #[test] - fn test_handler_resume_loop_conditional_tail_keeps_strong_load_fast() { + fn test_with_suppress_named_except_resume_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self): - is_utf8 = (self.ENCODING == 'utf-8') - encode_errors = 'surrogateescape' if is_utf8 else 'strict' - strings = list(self.BYTES_STRINGS) - for text in self.STRINGS: - try: - encoded = text.encode(self.ENCODING, encode_errors) - if encoded not in strings: - strings.append(encoded) - except UnicodeEncodeError: - encoded = None - if is_utf8: - encoded2 = text.encode(self.ENCODING, 'surrogatepass') - if encoded2 != encoded: - strings.append(encoded2) - for encoded in strings: - self.consume(encoded) +def f(self, cm, E): + try: + with cm: + pass + except E as e: + frames = e + self.x(frames) + self.y(frames) ", ); let f = find_code(&code, "f").expect("missing f code"); - - let has_strong_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - let has_borrow_load = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let first_tail_attr = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x" } _ => false, }) - }; + .expect("missing x attr load"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start]; - for name in ["is_utf8", "text", "self", "strings", "encoded2"] { - assert!( - has_strong_load(name), - "expected handler-resume loop tail to use strong LOAD_FAST for {name}, got instructions={:?}", - f.instructions - ); - } assert!( - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastLoadFast { var_nums } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - let (left, right) = var_nums.get(arg).indexes(); - f.varnames[usize::from(left)] == "encoded2" - && f.varnames[usize::from(right)] == "encoded" - } - _ => false, - }), - "expected encoded2/encoded comparison to use strong LOAD_FAST_LOAD_FAST, got instructions={:?}", - f.instructions + tail.iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "expected with-suppress/named-except resume tail to use strong LOAD_FAST, got tail={tail:?}" ); assert!( - !f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - let (left, right) = var_nums.get(arg).indexes(); - f.varnames[usize::from(left)] == "encoded2" - && f.varnames[usize::from(right)] == "encoded" - } - _ => false, + tail.iter().all(|unit| { + !matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) }), - "handler-resume loop tail should not borrow encoded2/encoded comparison, got instructions={:?}", - f.instructions - ); - assert!( - has_borrow_load("strings"), - "expected later loop/list uses outside the deopt tail to keep borrowing strings" + "expected with-suppress/named-except resume tail not to borrow, got tail={tail:?}" ); } #[test] - fn test_async_early_return_send_tail_uses_strong_load_fast_after_entry() { + fn test_with_named_except_return_value_keeps_borrow() { let code = compile_exec( "\ -class C: - async def _sock_sendfile_native(self, sock, file, offset, count): - try: - fileno = file.fileno() - except (AttributeError, io.UnsupportedOperation) as err: - raise exceptions.SendfileNotAvailableError('not a regular file') - try: - fsize = os.fstat(fileno).st_size - except OSError: - raise exceptions.SendfileNotAvailableError('not a regular file') - blocksize = count if count else fsize - if not blocksize: - return 0 - blocksize = min(blocksize, 0xffff_ffff) - end_pos = min(offset + count, fsize) if count else fsize - offset = min(offset, fsize) - total_sent = 0 - try: - while True: - blocksize = min(end_pos - offset, blocksize) - if blocksize <= 0: - return total_sent - await self._proactor.sendfile(sock, file, offset, blocksize) - offset += blocksize - total_sent += blocksize - finally: - if total_sent > 0: - file.seek(offset) +def f(self, b, BlockingIOError): + with self._write_lock: + written = len(self._write_buf) + if len(self._write_buf) > self.buffer_size: + try: + self._flush_unlocked() + except BlockingIOError as e: + if len(self._write_buf) > self.buffer_size: + overage = len(self._write_buf) - self.buffer_size + written -= overage + raise BlockingIOError(e.errno, e.strerror, written) + return written ", ); - let f = find_code(&code, "_sock_sendfile_native").expect("missing method code"); - - let names_for_unit = |unit: &bytecode::CodeUnit| -> Vec { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - match unit.op { - Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { - vec![f.varnames[usize::from(var_num.get(arg))].to_string()] - } - Instruction::LoadFastLoadFast { var_nums } - | Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { - let (left, right) = var_nums.get(arg).indexes(); - vec![ - f.varnames[usize::from(left)].to_string(), - f.varnames[usize::from(right)].to_string(), - ] - } - _ => Vec::new(), - } - }; - - let borrowed_names: Vec<_> = f + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f .instructions .iter() - .filter_map(|unit| match unit.op { - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } => Some(names_for_unit(unit)), - _ => None, - }) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - assert_eq!( - borrowed_names, - vec![vec!["file".to_owned()]], - "only the initial file.fileno() receiver should borrow, got instructions={:?}", - f.instructions - ); - - let has_strong_name = |name: &str| { - f.instructions.iter().any(|unit| { + let with_exit_start = instructions + .windows(3) + .position(|window| { matches!( - unit.op, - Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } - ) && names_for_unit(unit).iter().any(|loaded| loaded == name) + window, + [ + CodeUnit { + op: Instruction::Swap { .. }, + .. + }, + CodeUnit { + op: Instruction::Swap { .. }, + .. + }, + CodeUnit { + op: Instruction::LoadConst { .. }, + .. + }, + ] + ) }) - }; + .expect("missing with-exit cleanup"); + let return_value_load = instructions + .get(with_exit_start.saturating_sub(1)) + .expect("missing return value load"); + let arg = OpArg::new(u32::from(u8::from(return_value_load.arg))); - for name in [ - "fileno", - "count", - "blocksize", - "offset", - "fsize", - "end_pos", - "total_sent", - "file", - "self", - "sock", - ] { - assert!( - has_strong_name(name), - "async early-return send tail should use strong LOAD_FAST for {name}, got instructions={:?}", - f.instructions - ); - } + assert!( + matches!( + return_value_load.op, + Instruction::LoadFastBorrow { var_num } + if f.varnames[usize::from(var_num.get(arg))] == "written" + ), + "return value loaded through with-exit cleanup should keep borrowed written, got instructions={instructions:?}" + ); } #[test] - fn test_protected_import_tail_keeps_strong_load_fast() { + fn test_with_final_conditional_return_preserves_fallthrough_cleanup_nop() { let code = compile_exec( "\ -def f(s, size, pos, errors): - message = 'x' - look = pos - try: - import unicodedata - except ImportError: - return None - if look < size and chr(s[look]) == '{': - while look < size and chr(s[look]) != '}': - look += 1 - if look > pos + 1 and look < size and chr(s[look]) == '}': - message = 'y' - return message +def f(self): + with self.lock: + if self.raw is None or self.closed: + return + self.flush() ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let import_idx = ops - .iter() - .position(|op| matches!(op, Instruction::ImportName { .. })) - .expect("missing IMPORT_NAME"); - let protected_tail = &ops[import_idx + 1..]; - assert!( - !protected_tail.iter().any(|op| { + ops.windows(6).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] ) }), - "expected protected import tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}" - ); - - assert!( - protected_tail - .iter() - .any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })), - "expected protected import tail to keep LOAD_FAST_LOAD_FAST ops, got tail={protected_tail:?}" + "with fallthrough cleanup should preserve the CPython POP_BLOCK NOP, got ops={ops:?}" ); } #[test] - fn test_unprotected_import_before_with_keeps_borrow() { + fn test_with_while_fallthrough_preserves_cleanup_nop() { let code = compile_exec( "\ -def f(self, document): - from xml.etree import ElementInclude - document = self.xinclude_loader('C1.xml') - with self.assertRaises(OSError) as cm: - ElementInclude.include(document, self.xinclude_loader) - self.assertEqual(str(cm.exception), 'resource not found') +def f(cm, source): + with cm as out: + s = source.read() + while s: + out.write(s) + s = source.read() + source.close() ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let import_idx = ops - .iter() - .position(|op| matches!(op, Instruction::ImportName { .. })) - .expect("missing IMPORT_NAME"); - let handler_start = ops - .iter() - .position(|op| matches!(op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let warm_path = &ops[import_idx + 1..handler_start]; - assert!( - warm_path.iter().any(|op| { + ops.windows(6).any(|window| { matches!( - op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + window, + [ + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + Instruction::PopTop, + ] ) }), - "expected unprotected import before with-block to keep LOAD_FAST_BORROW ops, got warm_path={warm_path:?}" - ); - assert!( - warm_path - .iter() - .any(|op| matches!(op, Instruction::LoadFastBorrowLoadFastBorrow { .. })), - "expected with body arguments to keep LOAD_FAST_BORROW_LOAD_FAST_BORROW, got warm_path={warm_path:?}" + "with cleanup after while fallthrough should preserve the CPython POP_BLOCK NOP, got ops={ops:?}" ); } #[test] - fn test_unprotected_prefix_before_try_keeps_attr_subscript_borrow() { + fn test_with_while_true_break_drops_cleanup_nop() { let code = compile_exec( "\ -def f(): - import sys, getopt - usage = f'usage: {sys.argv[0]}' - try: - opts, args = getopt.getopt(sys.argv[1:], 'h') - except getopt.error as msg: - sys.stdout = sys.stderr - print(msg) - print(usage) - sys.exit(2) - return usage +def f(cm, source): + with cm as out: + while True: + data = source.read() + if not data: + break + out.write(data) + source.close() ", ); let f = find_code(&code, "f").expect("missing f code"); - let first_argv_idx = f + let ops: Vec<_> = f .instructions .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "argv" - } - _ => false, - }) - .expect("missing argv attr load"); - let receiver = f.instructions[..first_argv_idx] - .iter() - .rev() - .find(|unit| !matches!(unit.op, Instruction::Cache)) - .expect("missing argv receiver") - .op; + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .collect(); assert!( - matches!(receiver, Instruction::LoadFastBorrow { .. }), - "unprotected prefix before try should keep CPython-style LOAD_FAST_BORROW receiver, got {receiver:?}" + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "with cleanup after while True break should not preserve a POP_BLOCK NOP, got ops={ops:?}" ); } #[test] - fn test_terminal_except_inlined_comprehension_keeps_borrowed_warm_loads() { + fn test_with_final_assert_preserves_cleanup_nop() { let code = compile_exec( - r##" -def f(output): - output = re.sub(r"\[[0-9]+ refs\]", "", output) - try: - result = [ - row.split("\t") - for row in output.splitlines() - if row and not row.startswith('#') - ] - result.sort(key=lambda row: int(row[0])) - result = [row[1] for row in result] - return "\n".join(result) - except (IndexError, ValueError): - raise AssertionError( - "tracer produced unparsable output:\n{}".format(output) - ) -"##, + "\ +def f(cm, dst): + with cm: + assert not dst.closed + return dst +", ); let f = find_code(&code, "f").expect("missing f code"); - let handler_start = f + let ops: Vec<_> = f .instructions .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let warm_path = &f.instructions[..handler_start]; - let load_fast_name = |unit: &bytecode::CodeUnit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - Some(f.varnames[usize::from(var_num.get(arg))].as_str()) - } - _ => None, - }; - let borrow_name = |unit: &bytecode::CodeUnit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - Some(f.varnames[usize::from(var_num.get(arg))].as_str()) - } - _ => None, - }; + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .collect(); assert!( - warm_path - .iter() - .filter_map(load_fast_name) - .all(|name| name != "row" && name != "result"), - "terminal-except inlined comprehension warm path should keep CPython-style borrowed row/result loads, got warm_path={warm_path:?}" + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::Nop, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) + }), + "with cleanup after a final assert should preserve CPython's POP_BLOCK NOP anchor, got ops={ops:?}" ); - for name in ["row", "result"] { - assert!( - warm_path - .iter() - .filter_map(borrow_name) - .any(|var| var == name), - "expected borrowed {name} load in terminal-except inlined comprehension warm path, got warm_path={warm_path:?}" - ); - } } #[test] - fn test_outer_guarded_protected_import_keeps_borrow_tail() { + fn test_named_except_conditional_reraise_final_store_attr_keeps_borrow() { let code = compile_exec( "\ -def f(sys, os, file): - if sys.platform == 'win32': - try: - import nt - if not nt._supports_virtual_terminal(): - return False - except (ImportError, AttributeError): - return False +def f(self, fd, file, closefd, owned_fd, OSError, AttributeError, errno, os, stat, _setmode): try: - return os.isatty(file.fileno()) - except OSError: - return hasattr(file, 'isatty') and file.isatty() + self._closefd = closefd + self._stat_atopen = os.fstat(fd) + try: + if stat.S_ISDIR(self._stat_atopen.st_mode): + raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), file) + except AttributeError: + pass + if _setmode: + _setmode(fd, os.O_BINARY) + self.name = file + if self._appending: + try: + os.lseek(fd, 0, SEEK_END) + except OSError as e: + if e.errno != errno.ESPIPE: + raise + except: + self._stat_atopen = None + if owned_fd is not None: + os.close(owned_fd) + raise + self._fd = fd ", ); let f = find_code(&code, "f").expect("missing f code"); - let borrows_name = |name: &str| { - f.instructions.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let load_attr_name = |unit: &&bytecode::CodeUnit, expected: &str| match unit.op { + Instruction::LoadAttr { namei } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let load_attr = namei.get(arg); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == expected + } + _ => false, + }; + let lseek_attr = instructions + .iter() + .position(|unit| load_attr_name(unit, "lseek")) + .expect("missing lseek load"); + let store_attr = instructions + .iter() + .position(|unit| match unit.op { + Instruction::StoreAttr { namei } => { let arg = OpArg::new(u32::from(u8::from(unit.arg))); - let (left, right) = var_nums.get(arg).indexes(); - f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + f.names[usize::try_from(namei.get(arg)).unwrap()].as_str() == "_fd" } _ => false, }) - }; + .expect("missing _fd store"); - for name in ["nt", "os", "file"] { - assert!( - borrows_name(name), - "outer-guarded protected import should keep CPython-style borrow for {name}, got instructions={:?}", - f.instructions - ); - } + assert!( + matches!( + instructions + .get(lseek_attr.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "nested conditional reraise try body should keep borrowed os receiver, got instructions={instructions:?}" + ); + assert!( + matches!( + instructions.get(lseek_attr + 1).map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "nested conditional reraise try body should keep borrowed fd argument, got instructions={instructions:?}" + ); + assert!( + matches!( + instructions + .get(store_attr.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) + ), + "conditional reraise named-except final store tail should keep borrowed pair loads, got instructions={instructions:?}" + ); } #[test] - fn test_loop_or_break_continue_orders_break_before_backedge() { + fn test_with_except_else_with_resume_loop_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self, quoted): - while True: - if self.state == 'x': - if self.token or (self.posix and quoted): - break - else: - continue - elif self.state == 'y': - self.consume() - x = self.a + self.b + self.c - return x +def f(self, cm, E): + with cm: + try: + g() + except E: + pass + else: + with self.z(E): + h() + for _ in support.sleeping_retry(support.SHORT_TIMEOUT, 'not ready'): + if self.x: + break + self.y() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let quoted_load = ops + let get_iter = instructions .iter() - .position(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == "quoted" - } - _ => false, - }) - .expect("missing quoted LOAD_FAST_BORROW"); - let final_cond = ops[quoted_load + 1..] + .position(|unit| matches!(unit.op, Instruction::GetIter)) + .expect("missing loop iterator"); + let handler_start = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) - }) - .map(|idx| quoted_load + 1 + idx) - .expect("missing final conditional jump"); + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[get_iter.saturating_sub(1)..handler_start]; + assert!( - matches!(ops[final_cond].op, Instruction::PopJumpIfFalse { .. }), - "expected CPython-style inverted final condition, got ops={ops:?}" + tail.iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "expected with except/else-with resume loop tail to use strong LOAD_FAST ops, got tail={tail:?}" ); - let break_jump_idx = ops[final_cond + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpForward { .. })) - .map(|idx| final_cond + 1 + idx) - .expect("missing break jump after condition"); - let jump_back_idx = ops[final_cond + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| final_cond + 1 + idx) - .expect("missing continue backedge"); assert!( - break_jump_idx < jump_back_idx, - "expected break jump before continue backedge, got ops={ops:?}" + tail.iter().all(|unit| { + !matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "with except/else-with resume loop tail should not borrow LOAD_FAST ops, got tail={tail:?}" ); } #[test] - fn test_for_continue_before_return_orders_backedge_before_return_body() { + fn test_plain_with_then_global_loop_tail_keeps_borrow() { let code = compile_exec( "\ -def f(self): - for version in AllowedVersions: - if not version in self.capabilities: - continue - self.PROTOCOL_VERSION = version - return - raise self.error('x') +def f(self, cm): + with cm: + self.x() + for value in ITEMS: + self.y(value) ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let contains_idx = ops - .iter() - .position(|unit| matches!(unit.op, Instruction::ContainsOp { .. })) - .expect("missing containment test"); - let cond_idx = ops[contains_idx + 1..] - .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) - }) - .map(|idx| contains_idx + 1 + idx) - .expect("missing conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style condition targeting the return body, got ops={ops:?}" - ); - - let backedge_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing continue backedge"); - let store_attr_idx = ops[cond_idx + 1..] + let y_attr = instructions .iter() .position(|unit| match unit.op { - Instruction::StoreAttr { namei } => { - let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(namei).unwrap()].as_str() == "PROTOCOL_VERSION" + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "y" } _ => false, }) - .map(|idx| cond_idx + 1 + idx) - .expect("missing PROTOCOL_VERSION store"); + .expect("missing y attr load"); + assert!( - backedge_idx < store_attr_idx, - "expected continue backedge before return body, got ops={ops:?}" + matches!( + instructions + .get(y_attr.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "plain with/global-loop tail should keep CPython-style borrowed self load, got instructions={instructions:?}" ); } #[test] - fn test_while_conditional_return_orders_backedge_before_return_body() { + fn test_context_manager_for_join_tail_keeps_borrow() { let code = compile_exec( "\ -def f(self, tag): - while self._get_response(): - if self.tagged_commands[tag]: - return tag +def f(self, factory): + with factory() as e: + executor = e + self.x(e) + for t in executor._threads: + t.join() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let subscript_idx = ops - .iter() - .position(|unit| matches!(unit.op, Instruction::BinaryOp { .. })) - .expect("missing tagged_commands subscript"); - let cond_idx = ops[subscript_idx + 1..] + let join_attr = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "join" + } + _ => false, }) - .map(|idx| subscript_idx + 1 + idx) - .expect("missing conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style condition targeting return body, got ops={ops:?}" - ); - let backedge_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing loop backedge"); - let return_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing return"); + .expect("missing join attr load"); + assert!( - backedge_idx < return_idx, - "expected loop backedge before return body, got ops={ops:?}" + matches!( + instructions + .get(join_attr.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "context-manager for-join tail should keep CPython-style borrowed t load, got instructions={instructions:?}" ); } #[test] - fn test_for_break_to_return_orders_backedge_before_return() { + fn test_with_except_resume_normal_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(it): - best = 10 - body = None - for prio, part in it: - if prio < best: - best = prio - body = part - if prio == 0: - break - return body +def f(self, cm, E): + try: + with self.assertRaises(E): + with cm: + h() + except TimeoutError: + self._fail_on_deadlock(cm) + cm.shutdown(wait=True) ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops - .iter() - .enumerate() - .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .nth(1) - .map(|(idx, _)| idx) - .expect("missing break comparison"); - let cond_idx = ops[compare_idx + 1..] + let shutdown_attr = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "shutdown" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing break conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style true jump to break return path, got ops={ops:?}" - ); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing loop backedge before break return"); - let return_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing break return path"); + .expect("missing shutdown attr load"); + assert!( - jump_back_idx < return_idx, - "expected loop backedge before break return block, got ops={ops:?}" + matches!( + instructions + .get(shutdown_attr.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFast { .. }) + ), + "with/except resume normal tail should keep CPython-style strong cm load, got instructions={instructions:?}" ); } - #[test] - fn test_for_conditional_raise_orders_backedge_before_raise() { - let code = compile_exec( - "\ -def f(items, limit): - found = 0 - for item in items: - if item: - found += 1 - if found >= limit: - raise ValueError(found) - return found + #[test] + fn test_with_except_else_attr_subscript_tail_keeps_borrow() { + let code = compile_exec( + "\ +def f(self, cm, E, obj): + try: + with cm: + pass + except E as exc: + self.x(exc) + else: + self.fail('Expected') + inner = obj.saved_details[1] + self.x(inner) ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops - .iter() - .enumerate() - .find(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .map(|(idx, _)| idx) - .expect("missing raise comparison"); - let cond_idx = ops[compare_idx + 1..] + let saved_details = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "saved_details" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing raise conditional jump"); + .expect("missing saved_details attr load"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[saved_details.saturating_sub(1)..handler_start]; + assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style true jump to raise path, got ops={ops:?}" + tail.iter().any(|unit| matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + )), + "expected except-else attr-subscript tail to keep borrowed LOAD_FAST ops, got tail={tail:?}" ); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing loop backedge before raise"); - let raise_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing raise path"); assert!( - jump_back_idx < raise_idx, - "expected loop backedge before conditional raise block, got ops={ops:?}" + tail.iter() + .all(|unit| !matches!(unit.op, Instruction::LoadFast { .. })), + "except-else attr-subscript tail should not be deoptimized to strong LOAD_FAST, got tail={tail:?}" ); } #[test] - fn test_exception_handler_loop_conditional_raise_orders_backedge_before_raise() { + fn test_with_suppress_attr_subscript_tail_keeps_borrow() { let code = compile_exec( "\ -def f(chunk, dec, i): - try: - for c in chunk: - acc = dec[c] - except TypeError: - for j, c in enumerate(chunk): - if dec[c] is None: - raise ValueError('%d' % (i + j)) from None - raise +def f(self, cm): + stack = self.exit_stack() + with self.assertRaisesRegex(TypeError, 'the context manager'): + stack.enter_context(cm) + stack.push(cm) + self.assertIs(stack._exit_callbacks[-1][1], cm) ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let exit_callbacks = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "_exit_callbacks" + } + _ => false, + }) + .expect("missing _exit_callbacks attr load"); assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::BinaryOp { .. }, - Instruction::PopJumpIfNone { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadGlobal { .. }, - ] - ) - }), - "expected exception-handler loop false path to jump back before raise body, got ops={ops:?}" - ); - assert!( - !ops.windows(4).any(|window| { - matches!( - window, - [ - Instruction::BinaryOp { .. }, - Instruction::PopJumpIfNotNone { .. }, - Instruction::NotTaken, - Instruction::LoadGlobal { .. }, - ] - ) - }), - "unexpected exception-handler loop raise body before backedge, got ops={ops:?}" + matches!( + instructions + .get(exit_callbacks.saturating_sub(1)) + .map(|unit| unit.op), + Some(Instruction::LoadFastBorrow { .. }) + ), + "with-suppress attr-subscript tail should keep CPython-style borrowed stack load, got instructions={instructions:?}" ); } #[test] - fn test_loop_if_body_keeps_fallthrough_before_implicit_continue_backedge() { + fn test_named_except_conditional_reraise_deopts_with_chain_tail() { let code = compile_exec( "\ -def f(b, curr, curr_append, decoded_append, packI, curr_clear): - for x in b: - if 33 <= x <= 117: - curr_append(x) - if len(curr) == 5: - acc = 0 - for x in curr: - acc = 85 * acc + (x - 33) - decoded_append(packI(acc)) - curr_clear() - elif x == 122: - decoded_append(0) +def f(self, arc, tmp_filename, new_mode): + try: + os.chmod(tmp_filename, new_mode) + except OSError as exc: + if exc.errno == ERR: + self.skipTest() + else: + raise + with self.check_context(arc.open(), 'fully_trusted'): + self.expect_file('a') + with self.check_context(arc.open(), 'tar'): + self.expect_file('b') ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let first_check_context = instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() + == "check_context" + } + _ => false, + }) + .expect("missing check_context load"); + let first_handler = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .unwrap_or(instructions.len()); + let warm_tail = &instructions[first_check_context.saturating_sub(1)..first_handler]; assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::CompareOp { .. }, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::LoadSmallInt { .. }, - Instruction::StoreFast { .. }, - ] - ) - }), - "expected CPython-style conditional body fallthrough before implicit continue backedge, got ops={ops:?}" + warm_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "expected conditional named-except reraise tail to use strong LOAD_FAST ops, got tail={warm_tail:?}" ); assert!( - !ops.windows(6).any(|window| { - matches!( - window, - [ - Instruction::CompareOp { .. }, - Instruction::PopJumpIfTrue { .. }, - Instruction::NotTaken, - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. }, - Instruction::LoadSmallInt { .. }, - Instruction::StoreFast { .. }, - ] + warm_tail.iter().all(|unit| { + !matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) }), - "unexpected inverted conditional with implicit continue backedge before body, got ops={ops:?}" + "expected all warm with-chain tail loads to stay strong after named-except reraise, got tail={warm_tail:?}" ); } #[test] - fn test_explicit_continue_after_return_orders_return_before_backedge() { + fn test_terminal_except_before_with_deopts_with_body_borrows() { let code = compile_exec( "\ -def f(j, n): - while j < n: - if j < 0: - return j - continue - return -1 +def f(self, cm): + try: + g() + except OSError: + raise Exception('skip') + with cm: + self.x() + self.y() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops - .iter() - .enumerate() - .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .nth(1) - .map(|(idx, _)| idx) - .expect("missing inner comparison"); - let cond_idx = ops[compare_idx + 1..] + let first_tail_attr = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing conditional jump"); + .expect("missing x attr load"); + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let with_tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start]; + assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }), - "expected CPython-style false jump to explicit continue, got ops={ops:?}" + with_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "expected terminal-except before with to use strong LOAD_FAST ops, got tail={with_tail:?}" ); - let return_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing return path"); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing explicit continue backedge"); assert!( - return_idx < jump_back_idx, - "expected return block before explicit continue backedge, got ops={ops:?}" + with_tail.iter().all(|unit| { + !matches!( + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + }), + "terminal-except before with should not borrow protected with body loads, got tail={with_tail:?}" ); } #[test] - fn test_implicit_while_tail_return_orders_backedge_before_return() { + fn test_terminal_except_resume_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self, j, n): - while j < n: - name, j = self.scan(j) - if j < 0: - return j - return -1 +def f(re, proc, unittest): + try: + version = proc.communicate() + except OSError: + raise unittest.SkipTest('x') + match = re.search('pat', version) + if match is None: + raise unittest.SkipTest(f'Unable to parse readelf version: {version}') + return int(match.group(1)), int(match.group(2)) ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops - .iter() - .enumerate() - .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .nth(1) - .map(|(idx, _)| idx) - .expect("missing inner comparison"); - let cond_idx = ops[compare_idx + 1..] + let search_attr = instructions .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadAttr { namei } => { + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "search" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style true jump to return, got ops={ops:?}" - ); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing implicit backedge"); - let return_idx = ops[cond_idx + 1..] + .expect("missing re.search attr load"); + let handler_start = instructions .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing return path"); - assert!( - jump_back_idx < return_idx, - "expected implicit loop backedge before return block, got ops={ops:?}" - ); + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail = &instructions[search_attr.saturating_sub(1)..handler_start]; + + let strong_loads_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastLoadFast { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + } + _ => false, + }) + }; + let borrows_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + } + _ => false, + }) + }; + + for name in ["re", "version", "match"] { + assert!( + strong_loads_name(name), + "terminal-except resume tail should use strong LOAD_FAST for {name}, got tail={tail:?}" + ); + assert!( + !borrows_name(name), + "terminal-except resume tail should not borrow {name}, got tail={tail:?}" + ); + } } #[test] - fn test_branch_arm_implicit_continue_keeps_return_before_backedge() { + fn test_terminal_except_conditional_return_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self, j, n, c): - while j < n: - if c == 'x': - j = self.step(j) - if j < 0: - return j - elif c == 'y': - j = j + 1 - return -1 +def f(param, value, quote): + try: + value.encode('ascii') + except UnicodeEncodeError: + return param + if quote: + return param + return value ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops + let handler_start = instructions .iter() - .enumerate() - .filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .nth(2) - .map(|(idx, _)| idx) - .expect("missing branch-arm return comparison"); - let cond_idx = ops[compare_idx + 1..] + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let quote_idx = instructions[..handler_start] .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .position(|unit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "quote" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing branch-arm conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }), - "expected CPython-style false jump to branch-arm continuation, got ops={ops:?}" - ); - let return_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing branch-arm return path"); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing branch-arm loop backedge"); - assert!( - return_idx < jump_back_idx, - "expected branch-arm return before loop backedge, got ops={ops:?}" - ); + .expect("missing quote guard load"); + let tail = &instructions[quote_idx..handler_start]; + + let strong_loads_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + let borrows_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + + for name in ["quote", "param", "value"] { + assert!( + strong_loads_name(name), + "terminal-except conditional tail should use strong LOAD_FAST for {name}, got tail={tail:?}" + ); + assert!( + !borrows_name(name), + "terminal-except conditional tail should not borrow {name}, got tail={tail:?}" + ); + } } #[test] - fn test_nested_implicit_while_tail_return_orders_backedge_before_return() { + fn test_terminal_except_successor_call_tail_uses_strong_load() { let code = compile_exec( "\ -def f(self, rawdata, j, match): - while 1: - c = rawdata[j:j + 1] - if c in \"'\\\"\": - m = match(rawdata, j) - if not m: - return -1 - j = m.end() - else: - name, j = self.scan(j) - if j < 0: - return j +def f(curr, decoded_append, packI, curr_clear, Error): + if len(curr) == 5: + acc = 0 + for x in curr: + acc = 85 * acc + (x - 33) + try: + decoded_append(packI(acc)) + except Error: + raise ValueError('overflow') from None + curr_clear() ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let compare_idx = ops + let handler_start = instructions .iter() - .enumerate() - .rfind(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. })) - .map(|(idx, _)| idx) - .expect("missing nested tail comparison"); - let cond_idx = ops[compare_idx + 1..] + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let curr_clear_load = instructions[..handler_start] .iter() - .position(|unit| { - matches!( - unit.op, - Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } - ) + .rev() + .find(|unit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == "curr_clear" + } + _ => false, }) - .map(|idx| compare_idx + 1 + idx) - .expect("missing nested tail conditional jump"); - assert!( - matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }), - "expected CPython-style true jump to nested return path, got ops={ops:?}" - ); - let jump_back_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::JumpBackward { .. })) - .map(|idx| cond_idx + 1 + idx) - .expect("missing nested tail loop backedge"); - let return_idx = ops[cond_idx + 1..] - .iter() - .position(|unit| matches!(unit.op, Instruction::ReturnValue)) - .map(|idx| cond_idx + 1 + idx) - .expect("missing nested tail return path"); + .expect("missing curr_clear load"); + assert!( - jump_back_idx < return_idx, - "expected nested implicit loop backedge before return block, got ops={ops:?}" + matches!(curr_clear_load.op, Instruction::LoadFast { .. }), + "terminal except successor call tail should use strong LOAD_FAST for curr_clear, got instructions={instructions:?}" ); } #[test] - fn test_join_store_global_before_import_keeps_strong_load_fast() { + fn test_terminal_except_following_if_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(module=None): - global ET - if module is None: - module = pyET - ET = module - from xml.etree import ElementPath +def f(s): + try: + t = s[1:] + d = g(s) + except ValueError: + raise ValueError('bad') from None + if t: + try: + a, b, c = h(t) + except ValueError: + raise ValueError('bad') from None + else: + if b: + x, y, z = d + if y <= 12 and z <= q(x, y): + z += 1 + d = [x, y, z] + else: + a = [0] + return k(*(d + a)) ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let tail_start = ops + .iter() + .position(|unit| { + matches!( + unit.op, + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } + if f.varnames[usize::from( + var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))) + )] == "t" + ) + }) + .expect("missing post-try t load"); + let tail = &ops[tail_start..handler_start]; assert!( - ops.windows(2).any(|window| { + !tail.iter().any(|unit| { matches!( - window, - [ - Instruction::LoadFast { .. }, - Instruction::StoreGlobal { .. }, - ] + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) }), - "expected CPython-style strong LOAD_FAST before join STORE_GLOBAL followed by import, got ops={ops:?}" + "terminal except following conditional tail should use CPython-style strong LOAD_FAST ops, got tail={tail:?}" ); } #[test] - fn test_handler_resume_join_keeps_borrow_in_common_tail() { + fn test_bare_except_internal_condition_keeps_try_body_borrows() { let code = compile_exec( "\ -def f(p, errors, s, pos, look, final, escape_start, st): +def f(buffering, raw, binary, result, BufferedReader): try: - chr_codec = unicodedata.lookup('%s' % st) - except LookupError as e: - x = unicode_call_errorhandler( - errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1 - ) - else: - x = chr_codec, look + 1 - p.append(x[0]) - pos = x[1] - if not final: - pos = escape_start - return p, pos - return unicode_call_errorhandler( - errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1 - ) + line_buffering = False + if buffering == 1 or buffering < 0 and raw._isatty_open_only(): + buffering = -1 + line_buffering = True + if buffering < 0: + buffering = max(min(raw._blksize, 8192 * 1024), 8192) + if buffering < 0: + raise ValueError('invalid buffering size') + if buffering == 0: + if binary: + return result + raise ValueError(\"can't have unbuffered text I/O\") + buffer = BufferedReader(raw, buffering) + result = buffer + if binary: + return result + return result + except: + result.close() + raise ", ); let f = find_code(&code, "f").expect("missing f code"); - let append_idx = f + let ops: Vec<_> = f .instructions .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append" + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let handler_start = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &ops[..handler_start]; + + let borrows_name = |name: &str| { + warm_path.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name } _ => false, }) - .expect("missing append tail"); - let tail: Vec<_> = f.instructions[append_idx.saturating_sub(1)..] - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .collect(); + }; + let strong_loads_name = |name: &str| { + warm_path.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastLoadFast { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + } + _ => false, + }) + }; - assert!( - matches!( - tail.as_slice(), - [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. }, - Instruction::LoadFastBorrow { .. }, - .., - ] - ), - "expected handler resume common tail to start with borrowed append receiver/arg loads, got tail={tail:?}" - ); - assert!( - tail.iter().any(|op| { - matches!( - op, - Instruction::LoadFastBorrowLoadFastBorrow { .. } - | Instruction::LoadFastBorrow { .. } - ) - }), - "expected handler resume common tail to keep borrowed LOAD_FAST ops, got tail={tail:?}" - ); + for name in ["buffering", "raw", "binary", "result", "BufferedReader"] { + assert!( + borrows_name(name), + "CPython keeps {name} borrowed inside the same bare-except protected region, got warm_path={warm_path:?}" + ); + assert!( + !strong_loads_name(name), + "same protected-region conditional tail should not be terminal-except deoptimized for {name}, got warm_path={warm_path:?}" + ); + } } #[test] - fn test_multi_handler_guarded_resume_tail_keeps_borrow() { + fn test_try_except_else_terminal_handler_conditional_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(a): +def f(self, pos, whence): try: - g() - except ValueError: - pass - except TypeError: - pass - if a: - return a.x - return 0 + pos_index = pos.__index__ + except AttributeError: + raise TypeError(f'{pos!r} is not an integer') + else: + pos = pos_index() + if whence == 0: + if pos < 0: + raise ValueError(f'negative {pos!r}') + self._pos = pos + elif whence == 1: + self._pos = max(0, self._pos + pos) + return self._pos ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &ops[..handler_start]; + + let op_mentions_name = |unit: &CodeUnit, name: &str| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + Instruction::LoadFastLoadFast { var_nums } + | Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name + } + _ => false, + }; + let is_borrow_for_name = |unit: &CodeUnit, name: &str| match unit.op { + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } => op_mentions_name(unit, name), + _ => false, + }; + let is_strong_for_name = |unit: &CodeUnit, name: &str| match unit.op { + Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } => { + op_mentions_name(unit, name) + } + _ => false, + }; assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::LoadFastBorrow { .. }, - Instruction::ToBool, - Instruction::PopJumpIfFalse { .. }, - Instruction::NotTaken, - Instruction::LoadFastBorrow { .. }, - ] - ) - }), - "expected guarded resume tail to keep borrowed guard/body loads, got ops={ops:?}" - ); - assert!( - ops.windows(2).any(|window| { - matches!( - window, - [ - Instruction::LoadFastBorrow { .. }, - Instruction::LoadAttr { .. } - ] - ) - }), - "expected guarded resume tail attr access to keep borrowed receiver, got ops={ops:?}" + warm_path + .iter() + .any(|unit| is_borrow_for_name(unit, "pos_index")), + "try-else call should remain borrowed before the terminal-handler tail, got warm_path={warm_path:?}" ); + + let tail_start = warm_path + .iter() + .position(|unit| { + is_strong_for_name(unit, "whence") || is_borrow_for_name(unit, "whence") + }) + .expect("missing post-try conditional tail"); + let post_try_tail = &warm_path[tail_start..]; + for name in ["whence", "pos", "self"] { + assert!( + post_try_tail + .iter() + .any(|unit| is_strong_for_name(unit, name)), + "terminal except conditional tail should use strong loads for {name}, got tail={post_try_tail:?}" + ); + assert!( + !post_try_tail + .iter() + .any(|unit| is_borrow_for_name(unit, name)), + "terminal except conditional tail should not borrow {name}, got tail={post_try_tail:?}" + ); + } } #[test] - fn test_multi_handler_method_tail_keeps_borrow() { + fn test_try_except_else_outer_join_keeps_borrowed_loads() { let code = compile_exec( "\ -def f(self, xs): - for vals, expected in xs: +def f(self, pos=None): + if self.closed: + raise ValueError('closed') + if pos is None: + pos = self._pos + else: try: - actual = g(vals) - except OverflowError: - self.fail(expected) - except ValueError: - self.fail(expected) - self.assertEqual(actual, expected) + pos_index = pos.__index__ + except AttributeError: + raise TypeError(f'{pos!r} is not an integer') + else: + pos = pos_index() + if pos < 0: + raise ValueError(f'negative {pos!r}') + del self._buffer[pos:] + return pos ", ); let f = find_code(&code, "f").expect("missing f code"); let ops: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); + let handler_start = ops + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let warm_path = &ops[..handler_start]; - let assert_equal_idx = ops + let mentions_name = |unit: &CodeUnit, name: &str| match unit.op { + Instruction::LoadFast { var_num } + | Instruction::LoadFastBorrow { var_num } + | Instruction::StoreFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }; + let else_store = warm_path .iter() - .position(|op| matches!(op, Instruction::LoadAttr { .. })) - .expect("missing assertEqual LOAD_ATTR"); - let tail = &ops[assert_equal_idx.saturating_sub(1)..]; + .position(|unit| { + matches!(unit.op, Instruction::StoreFast { .. }) && mentions_name(unit, "pos") + }) + .expect("missing try-else pos store"); + let delete_subscr = warm_path + .iter() + .position(|unit| matches!(unit.op, Instruction::DeleteSubscr)) + .expect("missing join delete"); + let else_tail = &warm_path[else_store + 1..delete_subscr]; assert!( - matches!(tail.first(), Some(Instruction::LoadFastBorrow { .. })), - "expected multi-handler method-call tail receiver to keep LOAD_FAST_BORROW, got tail={tail:?}" + else_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. }) + && mentions_name(unit, "pos")), + "terminal try-else conditional should use strong pos loads, got tail={else_tail:?}" ); + + let join_tail = &warm_path[delete_subscr.saturating_sub(6)..]; + for name in ["self", "pos"] { + assert!( + join_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. }) + && mentions_name(unit, name)), + "outer if/else join should keep borrowed {name} loads, got tail={join_tail:?}" + ); + } assert!( - tail.iter() - .any(|op| matches!(op, Instruction::LoadFastBorrow { .. })), - "expected multi-handler method-call tail args to keep borrowed loads, got tail={tail:?}" + !join_tail + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. }) + && (mentions_name(unit, "self") || mentions_name(unit, "pos"))), + "outer if/else join should not inherit terminal try-else deopts, got tail={join_tail:?}" ); } #[test] - fn test_named_except_cleanup_loop_header_keeps_borrow_in_for_loop() { + fn test_terminal_except_else_final_store_attr_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(args): - for arg in args: - try: - _wm._setoption(arg) - except _wm._OptionError as msg: - print('Invalid -W option ignored:', msg, file=sys.stderr) +def f(self, E, Event): + try: + decoded = bytes(self.buf).decode(self.encoding) + except E: + return + else: + self.insert(Event('key', decoded, self.flush_buf())) + self.keymap = self.compiled_keymap ", ); let f = find_code(&code, "f").expect("missing f code"); - let attr_idx = f + let instructions: Vec<_> = f .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let store_idx = instructions + .iter() + .position(|unit| match unit.op { + Instruction::StoreAttr { namei } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.names[usize::try_from(namei.get(arg)).unwrap()].as_str() == "keymap" + } + _ => false, + }) + .expect("missing keymap STORE_ATTR"); + let insert_attr_idx = instructions .iter() .position(|unit| match unit.op { Instruction::LoadAttr { namei } => { let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_setoption" + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "insert" } _ => false, }) - .expect("missing _setoption attr load"); - let window: Vec<_> = f.instructions[attr_idx + 1..] - .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) - .take(3) - .collect(); + .expect("missing insert LOAD_ATTR"); + assert!( matches!( - window.as_slice(), - [ - Instruction::LoadFastBorrow { .. }, - Instruction::Call { .. }, - Instruction::PopTop - ] + instructions[insert_attr_idx - 1].op, + Instruction::LoadFastBorrow { .. } ), - "expected loop body call to keep borrowed arg load after named-except cleanup, got window={window:?}" + "terminal except should not deopt the try/else method receiver before the final tail, got instructions={instructions:?}" + ); + assert!( + matches!( + ( + instructions[store_idx - 3].op, + instructions[store_idx - 1].op + ), + (Instruction::LoadFast { .. }, Instruction::LoadFast { .. }) + ), + "terminal except final STORE_ATTR tail should use CPython-style strong LOAD_FAST ops, got instructions={instructions:?}" ); } #[test] - fn test_multi_named_except_loop_header_keeps_borrow_for_normal_path() { + fn test_except_break_try_else_loop_tail_keeps_else_borrows() { let code = compile_exec( "\ def f(self): - for badval in ['illegal', -1, 1 << 32]: - class A: - def __len__(self): - return badval + self.setup() + prompt = 'Hit Return for more, or q (and Return) to quit: ' + lineno = 0 + while 1: try: - bool(A()) - except (Exception) as e_bool: - try: - len(A()) - except (Exception) as e_len: - self.assertEqual(str(e_bool), str(e_len)) + for i in range(lineno, lineno + self.MAXLINES): + print(self.lines[i]) + except IndexError: + break + else: + lineno += self.MAXLINES + key = None + while key is None: + key = input(prompt) + if key not in ('', 'q'): + key = None + if key == 'q': + break ", ); let f = find_code(&code, "f").expect("missing f code"); - let ops: Vec<_> = f + let instructions: Vec<_> = f .instructions .iter() - .map(|unit| unit.op) - .filter(|op| !matches!(op, Instruction::Cache)) + .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - - assert!( - ops.windows(4).any(|window| { + let borrowed_loads = instructions + .iter() + .filter(|unit| { matches!( - window, - [ - Instruction::LoadBuildClass, - Instruction::PushNull, - Instruction::LoadFastBorrow { .. }, - Instruction::BuildTuple { .. }, - ] + unit.op, + Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } ) - }), - "expected class closure setup in loop header to borrow badval, got ops={ops:?}" + }) + .count(); + let strong_loads_in_else_tail = instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } + ) + }); + + assert!( + borrowed_loads >= 7, + "except-break try/else loop tail should keep CPython-style borrowed loads, got instructions={instructions:?}" ); assert!( - ops.windows(5).any(|window| { - matches!( - window, - [ - Instruction::LoadFastBorrow { .. }, - Instruction::PushNull, - Instruction::Call { .. }, - Instruction::Call { .. }, - Instruction::PopTop, - ] - ) - }), - "expected normal bool(A()) path in loop header to borrow A, got ops={ops:?}" + !strong_loads_in_else_tail, + "except-break try/else loop tail must not be deoptimized as terminal return/raise, got instructions={instructions:?}" ); } #[test] - fn test_named_except_cleanup_simple_resume_tail_keeps_borrow() { + fn test_protected_method_call_after_terminal_except_tail_uses_strong_loads() { let code = compile_exec( "\ -def f(self): - try: - 1 / 0 - except Exception as e: - tb = e.__traceback__ - self.get_disassemble_as_string(tb.tb_frame.f_code, tb.tb_lasti) +def f(items, chunk, out, packI, Error): + for i in items: + acc = 0 + try: + for c in chunk: + acc = acc * 85 + c + except TypeError: + raise + try: + out.append(packI(acc)) + except Error: + raise ValueError from None ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -22053,46 +31012,73 @@ def f(self): .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let attr_idx = instructions + let handler_start = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let append_attr = instructions[..handler_start] .iter() .position(|unit| match unit.op { Instruction::LoadAttr { namei } => { let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "get_disassemble_as_string" + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append" } _ => false, }) - .expect("missing LOAD_ATTR for get_disassemble_as_string"); - let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect(); - assert!( - matches!( - ops.get(attr_idx - 1), - Some(Instruction::LoadFastBorrow { .. }) - ), - "expected named-except resume tail to keep borrowed self load, got ops={ops:?}" - ); - assert!( - matches!( - ops.get(attr_idx + 4), - Some(Instruction::LoadFastBorrow { .. }) - ), - "expected named-except resume tail to keep borrowed tb load, got ops={ops:?}" - ); + .expect("missing append LOAD_ATTR"); + let tail = &instructions[append_attr.saturating_sub(1)..handler_start]; + + let strong_loads_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFast { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + let borrows_name = |name: &str| { + tail.iter().any(|unit| match unit.op { + Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + f.varnames[usize::from(var_num.get(arg))] == name + } + _ => false, + }) + }; + + for name in ["out", "packI", "acc"] { + assert!( + strong_loads_name(name), + "protected method-call after terminal except tail should use strong LOAD_FAST for {name}, got tail={tail:?}" + ); + assert!( + !borrows_name(name), + "protected method-call after terminal except tail should not borrow {name}, got tail={tail:?}" + ); + } } #[test] - fn test_named_except_cleanup_conditional_raise_tail_keeps_borrow() { + fn test_terminal_reraising_handler_keeps_try_body_method_borrows() { let code = compile_exec( "\ def f(self): try: - output = self.trace() - output = output.strip() - except (A, B, C) as fnfe: - output = str(fnfe) - if output != 'probe: success': - raise E('{} {}'.format(self.command[0], output)) + self.console.prepare() + self.arg = None + self.finished = False + del self.buffer[:] + self.pos = 0 + self.dirty = True + self.last_command = None + self.calc_screen() + except BaseException: + self.restore() + raise + while self.scheduled_commands: + cmd = self.scheduled_commands.pop() + self.do_cmd((cmd, [])) ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -22101,46 +31087,41 @@ def f(self): .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let raise_idx = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })) - .expect("missing conditional raise"); - let handler_start = instructions + let first_handler = instructions .iter() .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) .expect("missing handler entry"); - let tail = &instructions[..handler_start.min(raise_idx)]; + let try_body = &instructions[..first_handler]; + let self_borrows = try_body + .iter() + .filter(|unit| { + matches!(unit.op, Instruction::LoadFastBorrow { var_num } + if f.varnames[usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))] + == "self") + }) + .count(); assert!( - tail.iter().any(|unit| { - matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "named-except cleanup conditional raise tail should keep borrowed loads, got tail={tail:?}" - ); - assert!( - !tail - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "named-except cleanup conditional raise tail should not force strong LOAD_FAST, got tail={tail:?}" + self_borrows >= 8, + "terminal reraising handler should keep try-body self loads borrowed, got try_body={try_body:?}" ); } #[test] - fn test_with_suppress_named_except_resume_tail_uses_strong_loads() { + fn test_terminal_except_loop_successor_augassign_uses_strong_load_pair() { let code = compile_exec( "\ -def f(self, cm, E): - try: - with cm: - pass - except E as e: - frames = e - self.x(frames) - self.y(frames) +def f(items, decoded, b32rev): + for i in range(0, len(items), 8): + quanta = items[i:i + 8] + acc = 0 + try: + for c in quanta: + acc = (acc << 5) + b32rev[c] + except KeyError: + raise ValueError from None + decoded += acc.to_bytes(5) + return decoded ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -22149,56 +31130,39 @@ def f(self, cm, E): .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let first_tail_attr = instructions + let to_bytes_attr = instructions .iter() .position(|unit| match unit.op { Instruction::LoadAttr { namei } => { let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x" + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "to_bytes" } _ => false, }) - .expect("missing x attr load"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start]; + .expect("missing to_bytes LOAD_ATTR"); + let pair = instructions[to_bytes_attr.saturating_sub(1)].op; assert!( - tail.iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "expected with-suppress/named-except resume tail to use strong LOAD_FAST, got tail={tail:?}" - ); - assert!( - tail.iter().all(|unit| { - !matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - }), - "expected with-suppress/named-except resume tail not to borrow, got tail={tail:?}" + matches!(pair, Instruction::LoadFastLoadFast { .. }), + "terminal-except loop successor augassign should use strong LOAD_FAST_LOAD_FAST, got instructions={instructions:?}" ); } #[test] - fn test_with_except_else_with_resume_loop_tail_uses_strong_loads() { + fn test_terminal_except_loop_backedge_keeps_header_borrows() { let code = compile_exec( "\ -def f(self, cm, E): - with cm: +def f(self, value, start=0, stop=None): + i = start + while stop is None or i < stop: try: - g() - except E: - pass - else: - with self.z(E): - h() - for _ in support.sleeping_retry(support.SHORT_TIMEOUT, 'not ready'): - if self.x: - break - self.y() + v = self[i] + except IndexError: + break + if v is value or v == value: + return i + i += 1 + raise ValueError ", ); let f = find_code(&code, "f").expect("missing f code"); @@ -22207,660 +31171,695 @@ def f(self, cm, E): .iter() .filter(|unit| !matches!(unit.op, Instruction::Cache)) .collect(); - let get_iter = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::GetIter)) - .expect("missing loop iterator"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[get_iter.saturating_sub(1)..handler_start]; assert!( - tail.iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "expected with except/else-with resume loop tail to use strong LOAD_FAST ops, got tail={tail:?}" - ); - assert!( - tail.iter().all(|unit| { - !matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) + instructions.windows(7).any(|window| { + matches!(window[0].op, Instruction::StoreFast { .. }) + && matches!(window[1].op, Instruction::LoadFastBorrow { .. }) + && matches!(window[2].op, Instruction::PopJumpIfNone { .. }) + && matches!(window[3].op, Instruction::NotTaken) + && matches!( + window[4].op, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + && matches!(window[5].op, Instruction::CompareOp { .. }) + && matches!(window[6].op, Instruction::PopJumpIfFalse { .. }) }), - "with except/else-with resume loop tail should not borrow LOAD_FAST ops, got tail={tail:?}" + "terminal-except loop backedge deopt should not cross into the loop header, got instructions={instructions:?}" ); } #[test] - fn test_plain_with_then_global_loop_tail_keeps_borrow() { + fn test_loop_if_implicit_continue_places_body_after_jumpback() { let code = compile_exec( "\ -def f(self, cm): - with cm: - self.x() - for value in ITEMS: - self.y(value) +def f(_config_vars, _INITPRE): + for k in list(_config_vars): + if k.startswith(_INITPRE): + del _config_vars[k] ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let y_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "y" - } - _ => false, - }) - .expect("missing y attr load"); assert!( - matches!( - instructions - .get(y_attr.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFastBorrow { .. }) - ), - "plain with/global-loop tail should keep CPython-style borrowed self load, got instructions={instructions:?}" + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::DeleteSubscr, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::EndFor, + ] + ) + }), + "loop if with implicit continue should use CPython body-after-jumpback layout, got ops={ops:?}" ); } #[test] - fn test_context_manager_for_join_tail_keeps_borrow() { + fn test_loop_if_call_body_implicit_continue_places_body_after_jumpback() { let code = compile_exec( "\ -def f(self, factory): - with factory() as e: - executor = e - self.x(e) - for t in executor._threads: - t.join() +def f(seq, db): + count = 0 + for c in seq: + dec = db.value(c, -1) + if dec != -1: + db.check(dec, c) + count += 1 + return count ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let join_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "join" - } - _ => false, - }) - .expect("missing join attr load"); assert!( - matches!( - instructions - .get(join_attr.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFastBorrow { .. }) - ), - "context-manager for-join tail should keep CPython-style borrowed t load, got instructions={instructions:?}" + ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::Call { .. }, + ] + ) + }), + "no-else loop call body should use CPython body-after-jumpback layout, got ops={ops:?}" ); } #[test] - fn test_with_except_resume_normal_tail_uses_strong_loads() { + fn test_nested_loop_if_try_body_implicit_continue_places_body_after_jumpback() { let code = compile_exec( "\ -def f(self, cm, E): - try: - with self.assertRaises(E): - with cm: - h() - except TimeoutError: - self._fail_on_deadlock(cm) - cm.shutdown(wait=True) +def f(seq, broken, codecs, LookupError, s, Queue, bytes): + for encoding in seq: + if encoding not in broken: + q = Queue(b'') + writer = codecs.getwriter(encoding)(q) + encodedresult = b'' + for c in s: + writer.write(c) + chunk = q.read() + encodedresult += chunk + q = Queue(b'') + reader = codecs.getreader(encoding)(q) + decodedresult = '' + for c in encodedresult: + q.write(bytes([c])) + decodedresult += reader.read() + if encoding not in broken: + try: + encoder = codecs.getincrementalencoder(encoding)() + except LookupError: + pass + else: + encoder.encode('x') + if encoding not in ('idna', 'mbcs'): + try: + encoder = codecs.getincrementalencoder(encoding)('ignore') + except LookupError: + pass + else: + encodedresult = b''.join(encoder.encode(c) for c in s) + decoder = codecs.getincrementaldecoder(encoding)('ignore') + decodedresult = ''.join(decoder.decode(bytes([c])) for c in encodedresult) ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let shutdown_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "shutdown" - } - _ => false, - }) - .expect("missing shutdown attr load"); assert!( - matches!( - instructions - .get(shutdown_attr.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFast { .. }) - ), - "with/except resume normal tail should keep CPython-style strong cm load, got instructions={instructions:?}" + ops.windows(8).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "nested final if with try body should put false backedge before body, got ops={ops:?}" + ); + assert!( + !ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::Nop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "nested final if with try body should not leave body before false backedge, got ops={ops:?}" ); } #[test] - fn test_with_except_else_attr_subscript_tail_keeps_borrow() { + fn test_loop_branch_raise_before_elif_keeps_body_before_backedge() { let code = compile_exec( "\ -def f(self, cm, E, obj): - try: - with cm: - pass - except E as exc: - self.x(exc) - else: - self.fail('Expected') - inner = obj.saved_details[1] - self.x(inner) +def f(checks, UNIQUE, CONTINUOUS, ValueError): + for check in checks: + if check is UNIQUE: + duplicates = [] + for name in checks: + if name: + duplicates.append(name) + if duplicates: + detail = ', '.join(str(name) for name in duplicates) + raise ValueError('aliases: %s' % detail) + elif check is CONTINUOUS: + value = 1 + return value ", - ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + ); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let saved_details = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "saved_details" - } - _ => false, - }) - .expect("missing saved_details attr load"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[saved_details.saturating_sub(1)..handler_start]; assert!( - tail.iter().any(|unit| matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - )), - "expected except-else attr-subscript tail to keep borrowed LOAD_FAST ops, got tail={tail:?}" + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadConst { .. }, + Instruction::LoadAttr { .. }, + ] + ) + }), + "raise body before an elif chain should stay before the branch backedge, got ops={ops:?}" ); assert!( - tail.iter() - .all(|unit| !matches!(unit.op, Instruction::LoadFast { .. })), - "except-else attr-subscript tail should not be deoptimized to strong LOAD_FAST, got tail={tail:?}" + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadConst { .. }, + ] + ) + }), + "raise body before an elif chain should not be moved after the branch backedge, got ops={ops:?}" ); } #[test] - fn test_with_suppress_attr_subscript_tail_keeps_borrow() { + fn test_loop_nested_raise_then_append_places_body_after_false_backedge() { let code = compile_exec( "\ -def f(self, cm): - stack = self.exit_stack() - with self.assertRaisesRegex(TypeError, 'the context manager'): - stack.enter_context(cm) - stack.push(cm) - self.assertIs(stack._exit_callbacks[-1][1], cm) +def f(args, parameters, enforce_default_ordering, type_var_tuple_encountered, default_encountered, TypeError): + for t in args: + if t not in parameters: + if enforce_default_ordering: + if type_var_tuple_encountered and t.has_default(): + raise TypeError('a') + if t.has_default(): + default_encountered = True + elif default_encountered: + raise TypeError('b') + parameters.append(t) + return parameters ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let exit_callbacks = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "_exit_callbacks" - } - _ => false, - }) - .expect("missing _exit_callbacks attr load"); assert!( - matches!( - instructions - .get(exit_callbacks.saturating_sub(1)) - .map(|unit| unit.op), - Some(Instruction::LoadFastBorrow { .. }) - ), - "with-suppress attr-subscript tail should keep CPython-style borrowed stack load, got instructions={instructions:?}" + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + ] + ) + }), + "CPython places the nested raise/append body after the false backedge for this loop tail, got ops={ops:?}" + ); + assert!( + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::ToBool, + ] + ) + }), + "this case should not keep the body before the false backedge; CPython emits the split backedge first, got ops={ops:?}" ); } #[test] - fn test_named_except_conditional_reraise_deopts_with_chain_tail() { + fn test_loop_break_before_adjacent_break_keeps_body_before_backedge() { let code = compile_exec( "\ -def f(self, arc, tmp_filename, new_mode): - try: - os.chmod(tmp_filename, new_mode) - except OSError as exc: - if exc.errno == ERR: - self.skipTest() +def f(pattern, prefix, get_prefix): + for op, av in pattern: + if op == 1: + prefix.append(av) + elif op == 2: + prefix1, got_all = get_prefix(av) + prefix.extend(prefix1) + if not got_all: + break else: - raise - with self.check_context(arc.open(), 'fully_trusted'): - self.expect_file('a') - with self.check_context(arc.open(), 'tar'): - self.expect_file('b') + break + else: + return prefix, True + return prefix, False ", ); let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let first_check_context = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() - == "check_context" - } - _ => false, - }) - .expect("missing check_context load"); - let first_handler = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .unwrap_or(instructions.len()); - let warm_tail = &instructions[first_check_context.saturating_sub(1)..first_handler]; assert!( - warm_tail - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "expected conditional named-except reraise tail to use strong LOAD_FAST ops, got tail={warm_tail:?}" + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::PopTop, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::LoadConst { .. }, + Instruction::BuildTuple { .. }, + ] + ) + }), + "break before an adjacent break exit should stay before the loop backedge, got ops={ops:?}" ); assert!( - warm_tail.iter().all(|unit| { - !matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::PopTop, + ] ) }), - "expected all warm with-chain tail loads to stay strong after named-except reraise, got tail={warm_tail:?}" + "break before an adjacent break exit should not be moved after the loop backedge, got ops={ops:?}" ); } #[test] - fn test_terminal_except_before_with_deopts_with_body_borrows() { + fn test_loop_elif_and_pass_keeps_shared_false_backedge_after_body() { let code = compile_exec( "\ -def f(self, cm): - try: - g() - except OSError: - raise Exception('skip') - with cm: - self.x() - self.y() +def f(methods, simple_keys, checked_keys, checked_enum, simple_enum, failed): + for method in methods: + if method in simple_keys and method in checked_keys: + continue + elif method not in simple_keys and method not in checked_keys: + checked_method = getattr(checked_enum, method, None) + simple_method = getattr(simple_enum, method, None) + if hasattr(checked_method, '__func__'): + checked_method = checked_method.__func__ + simple_method = simple_method.__func__ + if checked_method != simple_method: + failed.append(method) + else: + pass ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let first_tail_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x" - } - _ => false, - }) - .expect("missing x attr load"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let with_tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start]; assert!( - with_tail - .iter() - .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), - "expected terminal-except before with to use strong LOAD_FAST ops, got tail={with_tail:?}" + ops.windows(9).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadGlobal { .. }, + _, + ] + ) + }), + "elif-and body should stay before the shared false backedge, got ops={ops:?}" ); assert!( - with_tail.iter().all(|unit| { - !matches!( - unit.op, - Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + !ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] ) }), - "terminal-except before with should not borrow protected with body loads, got tail={with_tail:?}" + "elif-and shared false backedge should not be split before the body, got ops={ops:?}" ); } #[test] - fn test_terminal_except_resume_tail_uses_strong_loads() { + fn test_loop_nested_if_delete_slice_places_body_after_jumpback() { let code = compile_exec( "\ -def f(re, proc, unittest): - try: - version = proc.communicate() - except OSError: - raise unittest.SkipTest('x') - match = re.search('pat', version) - if match is None: - raise unittest.SkipTest(f'Unable to parse readelf version: {version}') - return int(match.group(1)), int(match.group(2)) +def f(compiler_so): + for idx in reversed(range(len(compiler_so))): + if compiler_so[idx] == '-arch' and compiler_so[idx + 1] == 'arm64': + del compiler_so[idx:idx + 2] ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let search_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "search" - } - _ => false, - }) - .expect("missing re.search attr load"); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let tail = &instructions[search_attr.saturating_sub(1)..handler_start]; - - let strong_loads_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - Instruction::LoadFastLoadFast { var_nums } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - let (left, right) = var_nums.get(arg).indexes(); - f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name - } - _ => false, - }) - }; - let borrows_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - let (left, right) = var_nums.get(arg).indexes(); - f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name - } - _ => false, - }) - }; - for name in ["re", "version", "match"] { - assert!( - strong_loads_name(name), - "terminal-except resume tail should use strong LOAD_FAST for {name}, got tail={tail:?}" - ); - assert!( - !borrows_name(name), - "terminal-except resume tail should not borrow {name}, got tail={tail:?}" - ); - } + assert!( + ops.windows(15).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::BinaryOp { .. }, + Instruction::BinaryOp { .. }, + Instruction::LoadConst { .. }, + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + ] + ) + }), + "nested loop delete-slice condition should put false jump-back before body, got ops={ops:?}" + ); } #[test] - fn test_terminal_except_conditional_return_tail_uses_strong_loads() { + fn test_loop_if_subscr_store_delete_places_body_after_jumpback() { let code = compile_exec( "\ -def f(param, value, quote): - try: - value.encode('ascii') - except UnicodeEncodeError: - return param - if quote: - return param - return value +def f(chunks): + for k in range(len(chunks)-1, 0, -1): + if chunks[k-1][-1] > chunks[k][0]: + chunks[k-1] = chunks[k-1][:-1] + chunks[k][1:] + del chunks[k] ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let quote_idx = instructions[..handler_start] - .iter() - .position(|unit| match unit.op { - Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == "quote" - } - _ => false, - }) - .expect("missing quote guard load"); - let tail = &instructions[quote_idx..handler_start]; - - let strong_loads_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - let borrows_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - for name in ["quote", "param", "value"] { - assert!( - strong_loads_name(name), - "terminal-except conditional tail should use strong LOAD_FAST for {name}, got tail={tail:?}" - ); - assert!( - !borrows_name(name), - "terminal-except conditional tail should not borrow {name}, got tail={tail:?}" - ); - } + assert!( + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::CompareOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::LoadSmallInt { .. }, + Instruction::BinaryOp { .. }, + ] + ) + }), + "loop if with subscript store/delete body should put false jump-back before body, got ops={ops:?}" + ); } #[test] - fn test_terminal_except_successor_call_tail_uses_strong_load() { + fn test_final_elif_implicit_continue_places_jumpback_before_body() { let code = compile_exec( "\ -def f(curr, decoded_append, packI, curr_clear, Error): - if len(curr) == 5: - acc = 0 - for x in curr: - acc = 85 * acc + (x - 33) - try: - decoded_append(packI(acc)) - except Error: - raise ValueError('overflow') from None - curr_clear() +def f(state, nextchar, whitespace, token, posix, quoted, debug): + while True: + if state is None: + break + elif state == ' ': + if not nextchar: + state = None + break + elif nextchar in whitespace: + if debug >= 2: + print('x') + if token or (posix and quoted): + break + else: + continue + elif state in ('a', 'c'): + if not nextchar: + state = None + break + elif nextchar in whitespace: + if debug >= 2: + print('y') + state = ' ' + if token or (posix and quoted): + break + else: + continue + return token ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let curr_clear_load = instructions[..handler_start] - .iter() - .rev() - .find(|unit| match unit.op { - Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == "curr_clear" - } - _ => false, - }) - .expect("missing curr_clear load"); assert!( - matches!(curr_clear_load.op, Instruction::LoadFast { .. }), - "terminal except successor call tail should use strong LOAD_FAST for curr_clear, got instructions={instructions:?}" + ops.windows(5).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "final elif with implicit continue should put false jump-back before body, got ops={ops:?}" ); } #[test] - fn test_protected_method_call_after_terminal_except_tail_uses_strong_loads() { + fn test_final_attribute_elif_implicit_continue_places_jumpback_before_body() { let code = compile_exec( "\ -def f(items, chunk, out, packI, Error): - for i in items: - acc = 0 - try: - for c in chunk: - acc = acc * 85 + c - except TypeError: - raise - try: - out.append(packI(acc)) - except Error: - raise ValueError from None +def f(self, nextchar, quoted): + while True: + if self.state is None: + break + elif self.state in ('a', 'c'): + if not nextchar: + self.state = None + break + elif nextchar in self.whitespace: + self.state = ' ' + if self.token or (self.posix and quoted): + break + else: + continue + return self.token ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let handler_start = instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) - .expect("missing handler entry"); - let append_attr = instructions[..handler_start] - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append" - } - _ => false, - }) - .expect("missing append LOAD_ATTR"); - let tail = &instructions[append_attr.saturating_sub(1)..handler_start]; - - let strong_loads_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFast { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - let borrows_name = |name: &str| { - tail.iter().any(|unit| match unit.op { - Instruction::LoadFastBorrow { var_num } => { - let arg = OpArg::new(u32::from(u8::from(unit.arg))); - f.varnames[usize::from(var_num.get(arg))] == name - } - _ => false, - }) - }; - for name in ["out", "packI", "acc"] { - assert!( - strong_loads_name(name), - "protected method-call after terminal except tail should use strong LOAD_FAST for {name}, got tail={tail:?}" - ); - assert!( - !borrows_name(name), - "protected method-call after terminal except tail should not borrow {name}, got tail={tail:?}" - ); - } + assert!( + ops.windows(6).any(|window| { + matches!( + window, + [ + Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. }, + Instruction::LoadAttr { .. }, + Instruction::ContainsOp { .. }, + Instruction::PopJumpIfTrue { .. }, + Instruction::NotTaken, + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ] + ) + }), + "final attribute elif with implicit continue should put false jump-back before body, got ops={ops:?}" + ); } #[test] - fn test_terminal_except_loop_successor_augassign_uses_strong_load_pair() { + fn test_inner_if_implicit_continue_keeps_line_bearing_body_before_backedge() { let code = compile_exec( "\ -def f(items, decoded, b32rev): - for i in range(0, len(items), 8): - quanta = items[i:i + 8] - acc = 0 - try: - for c in quanta: - acc = (acc << 5) + b32rev[c] - except KeyError: - raise ValueError from None - decoded += acc.to_bytes(5) - return decoded +def f(self, nextchar, quoted): + while True: + if self.state is None: + break + elif self.state in ('a', 'c'): + if not nextchar: + self.state = None + break + elif nextchar in self.whitespace: + self.state = ' ' + if self.token or (self.posix and quoted): + break + else: + continue + elif nextchar in self.commenters: + self.instream.readline() + self.lineno += 1 + if self.posix: + self.state = ' ' + if self.token or (self.posix and quoted): + break + else: + continue + elif self.state == 'c': + break + return self.token ", ); - let f = find_code(&code, "f").expect("missing f code"); - let instructions: Vec<_> = f + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f .instructions .iter() - .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) .collect(); - let to_bytes_attr = instructions - .iter() - .position(|unit| match unit.op { - Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "to_bytes" - } - _ => false, - }) - .expect("missing to_bytes LOAD_ATTR"); - let pair = instructions[to_bytes_attr.saturating_sub(1)].op; assert!( - matches!(pair, Instruction::LoadFastLoadFast { .. }), - "terminal-except loop successor augassign should use strong LOAD_FAST_LOAD_FAST, got instructions={instructions:?}" + ops.windows(7).any(|window| { + matches!( + window, + [ + Instruction::LoadAttr { .. }, + Instruction::ToBool, + Instruction::PopJumpIfFalse { .. }, + Instruction::NotTaken, + Instruction::LoadConst { .. }, + Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. }, + Instruction::StoreAttr { .. }, + ] + ) + }), + "line-bearing inner if with implicit continue should keep body before backedge, got ops={ops:?}" ); } @@ -22919,6 +31918,89 @@ def f(formatstr, args, output, overflowok): ); } + #[test] + fn test_typed_except_resume_import_warning_tail_keeps_borrows() { + let code = compile_exec( + r#" +def f(mod_name, error, sys, RuntimeWarning): + pkg_name, _, _ = mod_name.rpartition(".") + if pkg_name: + try: + __import__(pkg_name) + except ImportError as e: + if e.name is None or (e.name != pkg_name and + not pkg_name.startswith(e.name + ".")): + raise + existing = sys.modules.get(mod_name) + if existing is not None and not hasattr(existing, "__path__"): + from warnings import warn + msg = "{mod_name!r} found in sys.modules after import of " \ + "package {pkg_name!r}, but prior to execution of " \ + "{mod_name!r}; this may result in unpredictable " \ + "behaviour".format(mod_name=mod_name, pkg_name=pkg_name) + warn(RuntimeWarning(msg)) + return mod_name +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let arg = |unit: &bytecode::CodeUnit| OpArg::new(u32::from(u8::from(unit.arg))); + let strong_names = |unit: &bytecode::CodeUnit| -> Vec<&str> { + match unit.op { + Instruction::LoadFast { var_num } => { + vec![f.varnames[usize::from(var_num.get(arg(unit)))].as_str()] + } + Instruction::LoadFastLoadFast { var_nums } => { + let (left, right) = var_nums.get(arg(unit)).indexes(); + vec![ + f.varnames[usize::from(left)].as_str(), + f.varnames[usize::from(right)].as_str(), + ] + } + _ => Vec::new(), + } + }; + let borrowed_names = |unit: &bytecode::CodeUnit| -> Vec<&str> { + match unit.op { + Instruction::LoadFastBorrow { var_num } => { + vec![f.varnames[usize::from(var_num.get(arg(unit)))].as_str()] + } + Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let (left, right) = var_nums.get(arg(unit)).indexes(); + vec![ + f.varnames[usize::from(left)].as_str(), + f.varnames[usize::from(right)].as_str(), + ] + } + _ => Vec::new(), + } + }; + + let handler_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::PushExcInfo)) + .expect("missing handler entry"); + let normal_path = &f.instructions[..handler_start]; + for name in ["mod_name", "existing", "warn", "msg"] { + assert!( + normal_path + .iter() + .flat_map(borrowed_names) + .any(|borrowed| borrowed == name), + "CPython keeps {name} borrowed after typed-except resume, got ops={:?}", + normal_path.iter().map(|unit| unit.op).collect::>() + ); + assert!( + !normal_path + .iter() + .flat_map(strong_names) + .any(|strong| strong == name), + "typed-except resume should not force strong {name} loads, got ops={:?}", + normal_path.iter().map(|unit| unit.op).collect::>() + ); + } + } + #[test] fn test_reraising_except_else_tail_keeps_borrow() { let code = compile_exec( @@ -23306,6 +32388,95 @@ def f(self, xs): ); } + #[test] + fn test_except_pass_resume_loop_branch_keeps_borrows() { + let code = compile_exec( + r#" +def f(self, cls, fns): + for name, fn in zip(self.names, fns): + try: + annotation_fields, return_type = self.method_annotations[name] + except KeyError: + pass + else: + annotate_fn = _make_annotate_function(cls, name, annotation_fields, return_type) + fn.__annotate__ = annotate_fn + if self.unconditional_adds.get(name, False): + setattr(cls, name, fn) + else: + already_exists = _set_new_attribute(cls, name, fn) + if already_exists and (msg_extra := self.overwrite_errors.get(name)): + error_msg = (f'Cannot overwrite attribute {fn.__name__} ' + f'in class {cls.__name__}') + if not msg_extra is True: + error_msg = f'{error_msg} {msg_extra}' + raise TypeError(error_msg) +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let load_global_name = |unit: &&CodeUnit, name: &str| match unit.op { + Instruction::LoadGlobal { namei } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let load_global = namei.get(arg) >> 1; + f.names[usize::try_from(load_global).unwrap()].as_str() == name + } + _ => false, + }; + let load_name = |unit: &&CodeUnit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + Some(f.varnames[usize::from(var_num.get(arg))].as_str()) + } + _ => None, + }; + let load_pair_names = |unit: &&CodeUnit| match unit.op { + Instruction::LoadFastLoadFast { var_nums } + | Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + let (left, right) = var_nums.get(arg).indexes(); + Some(( + f.varnames[usize::from(left)].as_str(), + f.varnames[usize::from(right)].as_str(), + )) + } + _ => None, + }; + + let set_new_attribute = instructions + .iter() + .position(|unit| load_global_name(unit, "_set_new_attribute")) + .expect("missing _set_new_attribute call"); + let tail = &instructions[set_new_attribute..instructions.len()]; + assert!( + tail.iter().any(|unit| { + matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. }) + && load_pair_names(unit) == Some(("cls", "name")) + }) && tail.iter().any(|unit| { + matches!(unit.op, Instruction::LoadFastBorrow { .. }) + && load_name(unit) == Some("fn") + }), + "except-pass resume should keep CPython-style borrowed loads in loop branch, got tail={tail:?}" + ); + assert!( + !tail.iter().any(|unit| { + matches!(unit.op, Instruction::LoadFastLoadFast { .. }) + && load_pair_names(unit) == Some(("cls", "name")) + }) && !tail.iter().any(|unit| { + matches!(unit.op, Instruction::LoadFast { .. }) + && matches!( + load_name(unit), + Some("fn" | "already_exists" | "msg_extra" | "error_msg") + ) + }), + "except-pass resume must not deopt the independent loop branch, got tail={tail:?}" + ); + } + #[test] fn test_named_except_cleanup_deopts_same_guard_fallbacks_not_outer_tail() { let code = compile_exec( @@ -23495,4 +32666,128 @@ async def name_4(): "expected async comprehension iterator capture to borrow name_5 before GET_AITER, got {prev:?}" ); } + + #[test] + fn test_with_protected_generator_tail_after_cleanup_uses_strong_loads() { + let code = compile_exec( + r#" +def f(scandir, fspath, path, reversed, top, OSError, topdown=True, followlinks=False): + stack = [fspath(top)] + islink, join = path.islink, path.join + while stack: + top = stack.pop() + dirs = [] + nondirs = [] + walk_dirs = [] + try: + with scandir(top) as entries: + for entry in entries: + try: + is_dir = entry.is_dir() + except OSError: + is_dir = False + if is_dir: + dirs.append(entry.name) + else: + nondirs.append(entry.name) + if not topdown and is_dir: + if followlinks: + walk_into = True + else: + try: + is_symlink = entry.is_symlink() + except OSError: + is_symlink = False + walk_into = not is_symlink + if walk_into: + walk_dirs.append(entry.path) + except OSError: + continue + if topdown: + yield top, dirs, nondirs + for dirname in reversed(dirs): + new_path = join(top, dirname) + if not followlinks and islink(new_path): + continue + stack.append(new_path) + else: + stack.append((top, dirs, nondirs)) + for new_path in reversed(walk_dirs): + stack.append(new_path) +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let yield_pos = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::YieldValue { .. })) + .expect("missing YIELD_VALUE"); + assert!( + matches!( + instructions[yield_pos - 3].op, + Instruction::LoadFastLoadFast { .. } + ) && matches!(instructions[yield_pos - 2].op, Instruction::LoadFast { .. }), + "CPython keeps the yielded tuple inputs strong after with cleanup, got {:?} {:?}", + instructions[yield_pos - 3], + instructions[yield_pos - 2], + ); + assert!( + instructions[yield_pos + 1..] + .iter() + .take(30) + .any(|unit| matches!(unit.op, Instruction::LoadFast { .. })), + "expected post-yield traversal tail to contain strong LOAD_FAST ops" + ); + } + + #[test] + fn test_yield_from_finally_cleanup_keeps_normal_path_borrows() { + let code = compile_exec( + r#" +def f(_fwalk, stack, isbytes, topdown, onerror, follow_symlinks, close): + try: + while stack: + yield from _fwalk(stack, isbytes, topdown, onerror, follow_symlinks) + finally: + while stack: + action, value = stack.pop() + if action == 2: + close(value) +"#, + ); + let f = find_code(&code, "f").expect("missing f code"); + let instructions: Vec<_> = f + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Cache)) + .collect(); + let send_pos = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::Send { .. })) + .expect("missing SEND"); + assert!( + instructions[..send_pos] + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), + "CPython keeps yield-from normal path loads borrowed, got prefix={:?}", + &instructions[..send_pos] + ); + let cleanup_pop = instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::StoreFastStoreFast { .. })) + .expect("missing finally cleanup unpack"); + assert!( + instructions[..cleanup_pop] + .iter() + .rev() + .take(12) + .any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })), + "CPython keeps normal finally cleanup loads borrowed, got cleanup prefix={:?}", + &instructions[cleanup_pop.saturating_sub(12)..cleanup_pop] + ); + } } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 8d77b4688b..fc24eb01de 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -125,8 +125,14 @@ pub struct InstructionInfo { pub preserve_redundant_jump_as_nop: bool, /// Drop this NOP before line propagation if it still has no location. pub remove_no_location_nop: bool, + /// This no-location NOP was created by CPython-style nop_out() folding. + pub folded_operand_nop: bool, + /// This instruction was emitted as part of a synthetic no-location exit. + pub no_location_exit: bool, /// Keep this no-location NOP until line propagation when it starts a block. pub preserve_block_start_no_location_nop: bool, + /// This is the success jump emitted after a non-final match case body. + pub match_success_jump: bool, } /// Exception handler information for an instruction. @@ -148,13 +154,17 @@ fn set_to_nop(info: &mut InstructionInfo) { info.cache_entries = 0; info.preserve_redundant_jump_as_nop = false; info.remove_no_location_nop = false; + info.folded_operand_nop = false; + info.no_location_exit = false; info.preserve_block_start_no_location_nop = false; + info.match_success_jump = false; } fn nop_out_no_location(info: &mut InstructionInfo) { set_to_nop(info); info.lineno_override = Some(-1); info.remove_no_location_nop = true; + info.folded_operand_nop = true; } fn is_named_except_cleanup_normal_exit_block(block: &Block) -> bool { @@ -176,6 +186,37 @@ fn is_named_except_cleanup_normal_exit_block(block: &Block) -> bool { && tail[4].instr.is_unconditional_jump() } +fn is_standalone_named_except_cleanup_normal_exit_block(block: &Block) -> bool { + let len = block.instructions.len(); + if len < 5 || !is_named_except_cleanup_normal_exit_block(block) { + return false; + } + block.instructions[..len - 5].iter().all(|info| { + matches!( + info.instr.real(), + Some(Instruction::Nop | Instruction::NotTaken) + ) + }) +} + +fn named_except_cleanup_body_is_fast_local_only(block: &Block) -> bool { + let len = block.instructions.len(); + if len < 5 || !is_named_except_cleanup_normal_exit_block(block) { + return false; + } + block.instructions[..len - 5].iter().all(|info| { + matches!( + info.instr.real(), + Some( + Instruction::Nop + | Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::StoreFast { .. } + ) + ) + }) +} + // spell-checker:ignore petgraph // TODO: look into using petgraph for handling blocks and stuff? it's heavier than this, but it // might enable more analysis/optimizations @@ -194,6 +235,8 @@ pub struct Block { pub cold: bool, /// Whether LOAD_FAST borrow optimization should be suppressed for this block. pub disable_load_fast_borrow: bool, + /// Entry block for a try-else orelse suite split after POP_BLOCK. + pub try_else_orelse_entry: bool, } impl Default for Block { @@ -206,6 +249,7 @@ impl Default for Block { start_depth: None, cold: false, disable_load_fast_borrow: false, + try_else_orelse_entry: false, } } } @@ -237,6 +281,12 @@ pub struct CodeInfo { // u_in_conditional_block pub in_conditional_block: u32, + // Track when compiling the final direct statement in a sync with body. + pub in_final_with_cleanup_statement: u32, + + // Track when compiling the orelse suite of try/except. + pub in_try_else_orelse: u32, + // PEP 649: Next index for conditional annotation tracking // u_next_conditional_annotation_index pub next_conditional_annotation_index: u32, @@ -319,7 +369,7 @@ impl CodeInfo { reorder_conditional_explicit_continue_scope_exit_blocks(&mut self.blocks); reorder_conditional_implicit_continue_scope_exit_blocks(&mut self.blocks); reorder_conditional_scope_exit_and_jump_back_blocks(&mut self.blocks, true, true); - reorder_exception_handler_conditional_continue_raise_blocks(&mut self.blocks); + reorder_exception_handler_conditional_continue_scope_exit_blocks(&mut self.blocks); deduplicate_adjacent_jump_back_blocks(&mut self.blocks); reorder_conditional_body_and_implicit_continue_blocks(&mut self.blocks); reorder_conditional_scope_exit_and_jump_back_blocks(&mut self.blocks, true, true); @@ -331,9 +381,11 @@ impl CodeInfo { self.eliminate_unreachable_blocks(); resolve_line_numbers(&mut self.blocks); materialize_empty_conditional_exit_targets(&mut self.blocks); - materialize_exception_cleanup_jump_targets(&mut self.blocks); redirect_empty_block_targets(&mut self.blocks); + inline_small_fast_return_blocks(&mut self.blocks); + inline_unprotected_tuple_genexpr_assignment_return_blocks(&mut self.blocks); duplicate_end_returns(&mut self.blocks, &self.metadata); + duplicate_fallthrough_jump_back_targets(&mut self.blocks); duplicate_shared_jump_back_targets(&mut self.blocks); self.dce(); // truncate after terminal in blocks that got return duplicated self.eliminate_unreachable_blocks(); // remove now-unreachable last block @@ -371,8 +423,10 @@ impl CodeInfo { // before optimize_load_fast. convert_pseudo_ops(&mut self.blocks, &cellfixedoffsets); remove_redundant_nops_and_jumps(&mut self.blocks); - self.mark_assertion_success_tail_borrow_disabled(); self.mark_unprotected_debug_four_tails_borrow_disabled(); + self.mark_exception_handler_transition_targets_borrow_disabled(); + self.mark_targeted_nop_for_tails_borrow_disabled(); + self.restore_conditional_exception_for_iter_join_borrows(); self.compute_load_fast_start_depths(); // optimize_load_fast: after normalize_jumps self.optimize_load_fast_borrow(); @@ -388,13 +442,11 @@ impl CodeInfo { self.deoptimize_borrow_after_except_star_try_tail(); self.deoptimize_borrow_in_protected_method_call_after_terminal_except_tail(); self.deoptimize_borrow_after_terminal_except_before_with(); - self.deoptimize_borrow_in_async_finally_early_return_tail(); self.deoptimize_borrow_after_handler_resume_loop_tail(); self.deoptimize_borrow_after_protected_import(); self.deoptimize_borrow_before_import_after_join_store(); self.deoptimize_borrow_after_protected_store_tail(); self.deoptimize_borrow_after_deoptimized_async_with_enter(); - self.deoptimize_borrow_after_push_exc_info(); self.deoptimize_borrow_for_handler_return_paths(); self.deoptimize_borrow_for_match_keys_attr(); self.deoptimize_borrow_in_protected_attr_chain_tail(); @@ -420,6 +472,8 @@ impl CodeInfo { fblock: _, symbol_table_index: _, in_conditional_block: _, + in_final_with_cleanup_statement: _, + in_try_else_orelse: _, next_conditional_annotation_index: _, } = self; @@ -449,7 +503,6 @@ impl CodeInfo { // Convert pseudo ops (LoadClosure uses cellfixedoffsets) and fixup DEREF opargs convert_pseudo_ops(&mut blocks, &cellfixedoffsets); fixup_deref_opargs(&mut blocks, &cellfixedoffsets); - deoptimize_borrow_after_push_exc_info_in_blocks(&mut blocks); // Remove redundant NOPs, keeping line-marker NOPs only when // they are needed to preserve tracing. let mut block_order = Vec::new(); @@ -472,7 +525,9 @@ impl CodeInfo { let mut remove = false; if matches!(instr.instr.real(), Some(Instruction::Nop)) { - if lineno < 0 || prev_lineno == lineno { + if instr.preserve_redundant_jump_as_nop { + remove = false; + } else if lineno < 0 || prev_lineno == lineno { remove = true; } else if src < src_instructions.len() - 1 { if src_instructions[src + 1].instr.is_block_push() { @@ -591,6 +646,7 @@ impl CodeInfo { op = Opcode::Nop.into(); info.instr = op.into(); info.target = BlockIdx::NULL; + recompile = true; let updated_cache = op.cache_entries() as u32; recompile |= updated_cache != old_cache_entries; info.cache_entries = updated_cache; @@ -1676,18 +1732,19 @@ impl CodeInfo { }; let tuple_size = u32::from(block.instructions[i].arg) as usize; - if block - .instructions - .get(i + 1) - .and_then(|next| next.instr.real()) - .is_some_and(|next| { - matches!( - next, - Instruction::UnpackSequence { .. } - if usize::try_from(u32::from(block.instructions[i + 1].arg)).ok() - == Some(tuple_size) - ) - }) + if tuple_size <= 3 + && block + .instructions + .get(i + 1) + .and_then(|next| next.instr.real()) + .is_some_and(|next| { + matches!( + next, + Instruction::UnpackSequence { .. } + if usize::try_from(u32::from(block.instructions[i + 1].arg)).ok() + == Some(tuple_size) + ) + }) { return false; } @@ -2595,7 +2652,7 @@ impl CodeInfo { Instruction::LoadSmallInt { i } => Some(i.get(arg) != 0), _ => None, }; - for block in &mut self.blocks { + for (block_idx, block) in self.blocks.iter_mut().enumerate() { let mut i = 0; while i + 1 < block.instructions.len() { let curr = &block.instructions[i]; @@ -2632,6 +2689,18 @@ impl CodeInfo { continue; } + if matches!( + curr_instr, + Instruction::ContainsOp { .. } | Instruction::IsOp { .. } + ) && matches!(next_instr, Instruction::UnaryNot) + { + set_to_nop(&mut block.instructions[i]); + block.instructions[i + 1].instr = curr_instr.into(); + block.instructions[i + 1].arg = OpArg::new(u32::from(curr_arg) ^ 1); + i += 1; + continue; + } + if let Some(is_true) = const_truthiness(curr_instr, curr.arg, &self.metadata) { let jump_if_true = match next_instr { Instruction::PopJumpIfTrue { .. } => Some(true), @@ -2645,6 +2714,22 @@ impl CodeInfo { _ => unreachable!(), }; set_to_nop(&mut block.instructions[i]); + let preserves_pure_self_loop_anchor = i == 0 + && block.instructions[i + 1..].iter().all(|info| { + if info.target == BlockIdx(block_idx as u32) + && info.instr.is_unconditional_jump() + { + return true; + } + matches!(info.instr.real(), Some(Instruction::Nop)) + }) + && block.instructions[i + 1..].iter().any(|info| { + info.target == BlockIdx(block_idx as u32) + && info.instr.is_unconditional_jump() + }); + if preserves_pure_self_loop_anchor { + block.instructions[i].preserve_block_start_no_location_nop = true; + } if is_true == jump_if_true { block.instructions[i + 1].instr = PseudoInstruction::Jump { delta: Arg::marker(), @@ -2966,13 +3051,75 @@ impl CodeInfo { /// Remove NOP instructions from all blocks, but keep NOPs that introduce /// a new source line (they serve as line markers for monitoring LINE events). fn remove_nops(&mut self) { - for block in &mut self.blocks { + let layout_predecessors = compute_layout_predecessors(&self.blocks); + let keep_target_start_nops: Vec<_> = (0..self.blocks.len()) + .map(|idx| { + keep_target_start_no_location_nop( + &self.blocks, + BlockIdx(idx as u32), + &layout_predecessors, + ) + }) + .collect(); + let mut conditional_targets = vec![false; self.blocks.len()]; + for block in &self.blocks { + for instr in &block.instructions { + if instr.target != BlockIdx::NULL && is_conditional_jump(&instr.instr) { + let target = next_nonempty_block(&self.blocks, instr.target); + if target != BlockIdx::NULL { + conditional_targets[target.idx()] = true; + } + } + } + } + let preserve_loop_exit_pop_block_nops: Vec<_> = (0..self.blocks.len()) + .map(|idx| { + let block_idx = BlockIdx(idx as u32); + let block = &self.blocks[idx]; + let layout_pred = layout_predecessors[idx]; + block.instructions.first().is_some_and(|instr| { + matches!(instr.instr.real(), Some(Instruction::Nop)) + && instr.remove_no_location_nop + && instruction_lineno(instr) < 0 + && conditional_targets[idx] + && layout_pred != BlockIdx::NULL + && self.blocks[layout_pred.idx()] + .instructions + .last() + .is_some_and(|last| { + matches!( + last.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && next_nonempty_block(&self.blocks, last.target) != block_idx + }) + }) + }) + .collect(); + + for (block_idx, block) in self.blocks.iter_mut().enumerate() { let mut prev_line = None; let mut src = 0usize; block.instructions.retain(|ins| { let keep = 'keep: { if matches!(ins.instr.real(), Some(Instruction::Nop)) { - if ins.remove_no_location_nop && instruction_lineno(ins) < 0 { + let keep_loop_exit_pop_block = src == 0 + && preserve_loop_exit_pop_block_nops + .get(block_idx) + .copied() + .unwrap_or(false); + let keep_target_start = src == 0 + && keep_target_start_nops + .get(block_idx) + .copied() + .unwrap_or(false); + if ins.remove_no_location_nop + && instruction_lineno(ins) < 0 + && !keep_loop_exit_pop_block + && (!keep_target_start || ins.folded_operand_nop) + { break 'keep false; } let line = ins.location.line.get() as i32; @@ -3106,6 +3253,63 @@ impl CodeInfo { (((packed >> 4) & 0xF) as usize, (packed & 0xF) as usize) } + fn is_handler_resume_predecessor(block: &Block, target: BlockIdx) -> bool { + let has_pop_except = block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + let jumps_to_target = block.instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some(Instruction::JumpBackwardNoInterrupt { .. }) + ) + }); + has_pop_except && jumps_to_target + } + + fn block_falls_through_after_store_fast_store_fast( + block: &Block, + target: BlockIdx, + ) -> bool { + block.next == target + && matches!( + block.instructions.last().and_then(|info| info.instr.real()), + Some(Instruction::StoreFastStoreFast { .. }) + ) + } + + fn block_ends_with_backward_jump(block: &Block) -> bool { + matches!( + block.instructions.last().and_then(|info| info.instr.real()), + Some( + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + } + + fn block_is_backward_jump_only(block: &Block) -> bool { + let mut real = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)); + matches!( + real.next(), + Some( + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && real.next().is_none() + } + + fn block_is_resume_loop_latch(blocks: &[Block], target: BlockIdx) -> bool { + let block = &blocks[target.idx()]; + if block_ends_with_backward_jump(block) { + return true; + } + block.next != BlockIdx::NULL && block_is_backward_jump_only(&blocks[block.next.idx()]) + } + fn push_block( worklist: &mut Vec, visited: &mut [bool], @@ -3138,6 +3342,23 @@ impl CodeInfo { } } + let mut handler_resume_loop_latch = vec![false; self.blocks.len()]; + for block in &self.blocks { + let Some(target) = block.instructions.last().map(|info| info.target) else { + continue; + }; + if target != BlockIdx::NULL + && is_handler_resume_predecessor(block, target) + && block_is_resume_loop_latch(&self.blocks, target) + && self + .blocks + .iter() + .any(|pred| block_falls_through_after_store_fast_store_fast(pred, target)) + { + handler_resume_loop_latch[target.idx()] = true; + } + } + let mut visited = vec![false; self.blocks.len()]; let mut worklist = vec![BlockIdx(0)]; visited[0] = true; @@ -3384,7 +3605,7 @@ impl CodeInfo { } let block = &mut self.blocks[block_idx]; - if block.disable_load_fast_borrow { + if block.disable_load_fast_borrow || handler_resume_loop_latch[block_idx.idx()] { continue; } for (i, info) in block.instructions.iter_mut().enumerate() { @@ -3505,13 +3726,32 @@ impl CodeInfo { } } + fn has_same_line_target_predecessor( + blocks: &[Block], + incoming_origins: &[Vec], + block_idx: BlockIdx, + line: i32, + ) -> bool { + incoming_origins[block_idx.idx()].iter().any(|&pred| { + blocks[pred.idx()].instructions.iter().any(|info| { + info.target != BlockIdx::NULL + && next_nonempty_block(blocks, info.target) == block_idx + && instruction_lineno(info) == line + }) + }) + } + let target_flags = compute_target_predecessor_flags(&self.blocks); - for (block_idx, block) in self.blocks.iter_mut().enumerate() { + let reachable = compute_reachable_blocks(&self.blocks); + let incoming_origins = compute_incoming_origins(&self.blocks, &reachable); + for block_idx in 0..self.blocks.len() { if block_idx == 0 || !target_flags.targeted[block_idx] { continue; } + let block = &self.blocks[block_idx]; let mut assert_start = None; + let mut ranges = Vec::new(); for i in 0..block.instructions.len() { if is_assertion_error_load(&block.instructions[i]) { assert_start = Some(i); @@ -3536,271 +3776,113 @@ impl CodeInfo { continue; } - for info in &mut block.instructions[start + 1..i] { - deoptimize_borrow(info); + let assert_line = instruction_lineno(&block.instructions[start]); + if has_same_line_target_predecessor( + &self.blocks, + &incoming_origins, + BlockIdx::new(block_idx as u32), + assert_line, + ) { + assert_start = None; + continue; } + + ranges.push((start + 1, i)); assert_start = None; } + + let block = &mut self.blocks[block_idx]; + for (start, end) in ranges { + for info in &mut block.instructions[start..end] { + deoptimize_borrow(info); + } + } } } - fn mark_assertion_success_tail_borrow_disabled(&mut self) { - fn starts_with_fast_load(block: &Block) -> bool { + fn mark_unprotected_debug_four_tails_borrow_disabled(&mut self) { + fn block_has_protected_instructions(block: &Block) -> bool { block .instructions .iter() - .find(|info| { - info.instr.real().is_some_and(|instr| { - !matches!(instr, Instruction::Nop | Instruction::NotTaken) - }) - }) - .is_some_and(|info| { - matches!( - info.instr.real(), - Some( - Instruction::LoadFast { .. } - | Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastLoadFast { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } - ) - ) - }) + .any(|info| info.except_handler.is_some()) } - fn starts_with_assertion_error(block: &Block) -> bool { - block + fn debug_four_guard_name_load( + block: &Block, + names: &IndexSet, + varnames: &IndexSet, + ) -> bool { + let reals: Vec<_> = block .instructions .iter() - .find(|info| { + .filter(|info| { info.instr.real().is_some_and(|instr| { !matches!(instr, Instruction::Nop | Instruction::NotTaken) }) }) - .is_some_and(|info| { - matches!( - info.instr.real(), - Some(Instruction::LoadCommonConstant { idx }) - if idx.get(info.arg) == oparg::CommonConstant::AssertionError - ) - }) - } - - fn block_contains_assertion_error(block: &Block) -> bool { - block.instructions.iter().any(|info| { + .take(6) + .collect(); + if reals.len() < 5 { + return false; + } + let loads_imap_fast = match reals[0].instr.real() { + Some( + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }, + ) => varnames + .get_index(usize::from(var_num.get(reals[0].arg))) + .is_some_and(|name| name.as_str() == "imap"), + _ => false, + }; + let loads_debug_attr = matches!( + reals[1].instr.real(), + Some(Instruction::LoadAttr { namei }) + if names[usize::try_from(namei.get(reals[1].arg).name_idx()).unwrap()].as_str() + == "debug" + ); + let compares_with_four = reals.iter().any(|info| { matches!( info.instr.real(), - Some(Instruction::LoadCommonConstant { idx }) - if idx.get(info.arg) == oparg::CommonConstant::AssertionError + Some(Instruction::LoadSmallInt { i }) if i.get(info.arg) == 4 ) - }) + }) && reals + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::CompareOp { .. }))); + let has_conditional = reals.iter().any(|info| is_conditional_jump(&info.instr)); + loads_imap_fast && loads_debug_attr && compares_with_four && has_conditional } - fn assert_check_success_target(blocks: &[Block], block: &Block) -> Option { - if block.next != BlockIdx::NULL - && starts_with_assertion_error(&blocks[block.next.idx()]) - { - return block - .instructions - .iter() - .find(|info| is_conditional_jump(&info.instr) && info.target != BlockIdx::NULL) - .map(|info| info.target); - } - - let conditional = block.instructions.iter().enumerate().find(|(_, info)| { - is_conditional_jump(&info.instr) && info.target != BlockIdx::NULL - }); - let (cond_idx, conditional) = conditional?; - let has_inline_assertion_failure = - block.instructions[cond_idx + 1..].iter().any(|info| { - matches!( - info.instr.real(), - Some(Instruction::LoadCommonConstant { idx }) - if idx.get(info.arg) == oparg::CommonConstant::AssertionError - ) - }) && block.instructions[cond_idx + 1..].iter().any(|info| { - matches!(info.instr.real(), Some(Instruction::RaiseVarargs { .. })) - }); - has_inline_assertion_failure.then_some(conditional.target) + fn block_has_jump_back_predecessor_to( + blocks: &[Block], + predecessors: &[Vec], + target: BlockIdx, + ) -> bool { + predecessors[target.idx()].iter().any(|pred| { + blocks[pred.idx()].instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }) + }) } - fn block_stores_fast(block: &Block) -> bool { + fn block_has_mesg_call(block: &Block, names: &IndexSet) -> bool { block.instructions.iter().any(|info| { matches!( info.instr.real(), - Some( - Instruction::StoreFast { .. } - | Instruction::StoreFastLoadFast { .. } - | Instruction::StoreFastStoreFast { .. } - ) + Some(Instruction::LoadAttr { namei }) + if names[usize::try_from(namei.get(info.arg).name_idx()).unwrap()].as_str() + == "_mesg" ) - }) - } - - let mut predecessors = vec![Vec::new(); self.blocks.len()]; - for (pred_idx, block) in self.blocks.iter().enumerate() { - if block.next != BlockIdx::NULL { - predecessors[block.next.idx()].push(BlockIdx::new(pred_idx as u32)); - } - for info in &block.instructions { - if info.target != BlockIdx::NULL { - predecessors[info.target.idx()].push(BlockIdx::new(pred_idx as u32)); - } - } - } - - let mut assertion_success_tail = vec![false; self.blocks.len()]; - for (tail_idx, tail) in self.blocks.iter().enumerate() { - if tail.cold - || block_is_exceptional(tail) - || !starts_with_fast_load(tail) - || block_contains_assertion_error(tail) - || assert_check_success_target(&self.blocks, tail).is_some() - { - continue; - } - let tail_idx = BlockIdx::new(tail_idx as u32); - for pred in &predecessors[tail_idx.idx()] { - if assert_check_success_target(&self.blocks, &self.blocks[pred.idx()]) - == Some(tail_idx) - && trailing_conditional_jump_index(tail).is_some() - && !block_contains_assertion_error(tail) - { - assertion_success_tail[tail_idx.idx()] = true; - break; - } - for assert_check in &predecessors[pred.idx()] { - if assert_check_success_target(&self.blocks, &self.blocks[assert_check.idx()]) - == Some(*pred) - { - assertion_success_tail[tail_idx.idx()] = true; - break; - } - } - if assertion_success_tail[tail_idx.idx()] { - break; - } - } - } - - let mut visited = vec![false; self.blocks.len()]; - for (idx, is_assertion_success_tail) in assertion_success_tail.iter().enumerate() { - if !*is_assertion_success_tail { - continue; - } - let mut segment = Vec::new(); - let mut cursor = BlockIdx::new(idx as u32); - while cursor != BlockIdx::NULL && !visited[cursor.idx()] { - let block = &self.blocks[cursor.idx()]; - if block.cold || block_is_exceptional(block) { - break; - } - segment.push(cursor); - if block_stores_fast(block) { - segment.clear(); - break; - } - if block - .instructions - .last() - .is_some_and(|info| info.instr.is_scope_exit()) - { - break; - } - cursor = self.blocks[cursor.idx()].next; - } - for block_idx in segment { - if visited[block_idx.idx()] { - continue; - } - visited[block_idx.idx()] = true; - self.blocks[block_idx.idx()].disable_load_fast_borrow = true; - } - } - } - - fn mark_unprotected_debug_four_tails_borrow_disabled(&mut self) { - fn block_has_protected_instructions(block: &Block) -> bool { - block - .instructions - .iter() - .any(|info| info.except_handler.is_some()) - } - - fn debug_four_guard_name_load( - block: &Block, - names: &IndexSet, - varnames: &IndexSet, - ) -> bool { - let reals: Vec<_> = block - .instructions - .iter() - .filter(|info| { - info.instr.real().is_some_and(|instr| { - !matches!(instr, Instruction::Nop | Instruction::NotTaken) - }) - }) - .take(6) - .collect(); - if reals.len() < 5 { - return false; - } - let loads_imap_fast = match reals[0].instr.real() { - Some( - Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }, - ) => varnames - .get_index(usize::from(var_num.get(reals[0].arg))) - .is_some_and(|name| name.as_str() == "imap"), - _ => false, - }; - let loads_debug_attr = matches!( - reals[1].instr.real(), - Some(Instruction::LoadAttr { namei }) - if names[usize::try_from(namei.get(reals[1].arg).name_idx()).unwrap()].as_str() - == "debug" - ); - let compares_with_four = reals.iter().any(|info| { - matches!( - info.instr.real(), - Some(Instruction::LoadSmallInt { i }) if i.get(info.arg) == 4 - ) - }) && reals - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::CompareOp { .. }))); - let has_conditional = reals.iter().any(|info| is_conditional_jump(&info.instr)); - loads_imap_fast && loads_debug_attr && compares_with_four && has_conditional - } - - fn block_has_jump_back_predecessor_to( - blocks: &[Block], - predecessors: &[Vec], - target: BlockIdx, - ) -> bool { - predecessors[target.idx()].iter().any(|pred| { - blocks[pred.idx()].instructions.iter().any(|info| { - info.target == target - && matches!( - info.instr.real(), - Some( - Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. } - ) - ) - }) - }) - } - - fn block_has_mesg_call(block: &Block, names: &IndexSet) -> bool { - block.instructions.iter().any(|info| { - matches!( - info.instr.real(), - Some(Instruction::LoadAttr { namei }) - if names[usize::try_from(namei.get(info.arg).name_idx()).unwrap()].as_str() - == "_mesg" - ) - }) && block - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::Call { .. }))) + }) && block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::Call { .. }))) } fn normal_successors( @@ -3907,69 +3989,343 @@ impl CodeInfo { } } - fn deoptimize_borrow_for_handler_return_paths(&mut self) { - for block in &mut self.blocks { - let len = block.instructions.len(); - for i in 0..len { - let Some(Instruction::LoadFastBorrow { .. }) = block.instructions[i].instr.real() - else { - continue; - }; - let tail = &block.instructions[i + 1..]; - if tail.len() < 3 { - continue; - } - if !matches!(tail[0].instr.real(), Some(Instruction::Swap { .. })) { - continue; - } - if !matches!(tail[1].instr.real(), Some(Instruction::PopExcept)) { - continue; - } - if !matches!(tail[2].instr.real(), Some(Instruction::ReturnValue)) { - continue; - } - block.instructions[i].instr = Instruction::LoadFast { - var_num: Arg::marker(), + fn mark_exception_handler_transition_targets_borrow_disabled(&mut self) { + fn first_real_handler(block: &Block) -> Option { + block + .instructions + .iter() + .find(|info| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }) + .and_then(|info| info.except_handler) + } + + fn last_real_handler(block: &Block) -> Option { + block + .instructions + .iter() + .rev() + .find(|info| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }) + .and_then(|info| info.except_handler) + } + + fn has_direct_tuple_return_tail(block: &Block) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .collect(); + reals + .iter() + .any(|instr| matches!(instr, Instruction::BuildTuple { .. })) + && reals + .last() + .is_some_and(|instr| matches!(instr, Instruction::ReturnValue)) + && !reals.iter().any(|instr| { + matches!( + instr, + Instruction::Swap { .. } + | Instruction::PopExcept + | Instruction::Reraise { .. } + | Instruction::WithExceptStart + ) + }) + } + + let mut predecessors = vec![Vec::new(); self.blocks.len()]; + for (idx, block) in self.blocks.iter().enumerate() { + let block_idx = BlockIdx::new(idx as u32); + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + predecessors[block.next.idx()].push(block_idx); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + predecessors[info.target.idx()].push(block_idx); } - .into(); } } + + let mut to_disable = Vec::new(); + for (idx, block) in self.blocks.iter().enumerate() { + if !has_direct_tuple_return_tail(block) { + continue; + } + let Some(handler) = first_real_handler(block) else { + continue; + }; + if predecessors[idx].iter().any(|pred| { + last_real_handler(&self.blocks[pred.idx()]) + .is_some_and(|pred_handler| pred_handler != handler) + }) { + to_disable.push(idx); + } + } + + for idx in to_disable { + self.blocks[idx].disable_load_fast_borrow = true; + } } - fn deoptimize_borrow_after_generator_exception_return(&mut self) { - if !self.flags.contains(CodeFlags::GENERATOR) { - return; + fn mark_targeted_nop_for_tails_borrow_disabled(&mut self) { + fn is_nop_only_block(block: &Block) -> bool { + !block.instructions.is_empty() + && block.instructions.iter().all(|info| { + matches!( + info.instr.real(), + Some(Instruction::Nop | Instruction::NotTaken) + ) + }) } - fn deoptimize_block_borrows(block: &mut Block) { - for info in &mut block.instructions { + fn starts_for_iter_tail(block: &Block) -> bool { + let mut saw_iterable = false; + for info in block.instructions.iter().filter(|info| { + info.instr + .real() + .is_some_and(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + }) { match info.instr.real() { - Some(Instruction::LoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFast { - var_num: Arg::marker(), - } - .into(); - } - Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), - } - .into(); - } - _ => {} + Some( + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadName { .. } + | Instruction::LoadGlobal { .. }, + ) if !saw_iterable => saw_iterable = true, + Some(Instruction::GetIter) if saw_iterable => return true, + Some(Instruction::BuildList { .. } | Instruction::StoreFast { .. }) + if !saw_iterable => {} + _ => return false, } } + false } - fn handler_checks_exception(blocks: &[Block], handler: BlockIdx) -> bool { - let mut stack = vec![handler]; - let mut visited = vec![false; blocks.len()]; - while let Some(block_idx) = stack.pop() { - if block_idx == BlockIdx::NULL { - continue; + let mut fallthrough_predecessors = vec![Vec::new(); self.blocks.len()]; + let mut jump_predecessors = vec![Vec::new(); self.blocks.len()]; + for (idx, block) in self.blocks.iter().enumerate() { + let block_idx = BlockIdx::new(idx as u32); + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + fallthrough_predecessors[block.next.idx()].push(block_idx); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + jump_predecessors[info.target.idx()].push(block_idx); } - let idx = block_idx.idx(); - if visited[idx] { + } + } + + let mut seeds = Vec::new(); + for (idx, block) in self.blocks.iter().enumerate() { + if !starts_for_iter_tail(block) { + continue; + } + let has_targeted_nop_predecessor = fallthrough_predecessors[idx].iter().any(|pred| { + is_nop_only_block(&self.blocks[pred.idx()]) + && !jump_predecessors[pred.idx()].is_empty() + }); + if has_targeted_nop_predecessor { + seeds.push(BlockIdx::new(idx as u32)); + } + } + + let mut seen = vec![false; self.blocks.len()]; + for seed in seeds { + let mut stack = vec![seed]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || seen[block_idx.idx()] { + continue; + } + seen[block_idx.idx()] = true; + self.blocks[block_idx.idx()].disable_load_fast_borrow = true; + + let block = &self.blocks[block_idx.idx()]; + if block + .instructions + .last() + .is_some_and(|info| info.instr.is_scope_exit()) + { + continue; + } + if block.next != BlockIdx::NULL && block.next.idx() >= seed.idx() { + stack.push(block.next); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL && info.target.idx() >= seed.idx() { + stack.push(info.target); + } + } + } + } + } + + fn restore_conditional_exception_for_iter_join_borrows(&mut self) { + fn block_has_protected_instructions(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| info.except_handler.is_some()) + } + + fn is_conditional_predecessor_to(block: &Block, target: BlockIdx) -> bool { + block.instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some( + Instruction::PopJumpIfFalse { .. } + | Instruction::PopJumpIfTrue { .. } + | Instruction::PopJumpIfNone { .. } + | Instruction::PopJumpIfNotNone { .. } + ) + ) + }) + } + + fn starts_with_for_iter(block: &Block) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .take(3) + .collect(); + matches!( + reals.as_slice(), + [ + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::GetIter, + .. + ] | [ + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::LoadAttr { .. }, + Instruction::GetIter, + .. + ] + ) + } + + let mut predecessors = vec![Vec::new(); self.blocks.len()]; + for (idx, block) in self.blocks.iter().enumerate() { + let block_idx = BlockIdx::new(idx as u32); + if block.next != BlockIdx::NULL { + predecessors[block.next.idx()].push(block_idx); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + predecessors[info.target.idx()].push(block_idx); + } + } + } + + let mut to_restore = Vec::new(); + for (idx, block) in self.blocks.iter().enumerate() { + if !block.disable_load_fast_borrow + || block.cold + || block_is_exceptional(block) + || !starts_with_for_iter(block) + { + continue; + } + let target = BlockIdx::new(idx as u32); + let has_protected_predecessor = predecessors[idx] + .iter() + .any(|pred| block_has_protected_instructions(&self.blocks[pred.idx()])); + let has_conditional_normal_predecessor = predecessors[idx].iter().any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + !pred_block.disable_load_fast_borrow + && !pred_block.cold + && !block_is_exceptional(pred_block) + && !block_has_protected_instructions(pred_block) + && is_conditional_predecessor_to(pred_block, target) + }); + if has_protected_predecessor && has_conditional_normal_predecessor { + to_restore.push(idx); + } + } + + for idx in to_restore { + self.blocks[idx].disable_load_fast_borrow = false; + } + } + + fn deoptimize_borrow_for_handler_return_paths(&mut self) { + for block in &mut self.blocks { + let len = block.instructions.len(); + for i in 0..len { + let Some(Instruction::LoadFastBorrow { .. }) = block.instructions[i].instr.real() + else { + continue; + }; + let tail = &block.instructions[i + 1..]; + if tail.len() < 3 { + continue; + } + if !matches!(tail[0].instr.real(), Some(Instruction::Swap { .. })) { + continue; + } + if !matches!(tail[1].instr.real(), Some(Instruction::PopExcept)) { + continue; + } + if !matches!(tail[2].instr.real(), Some(Instruction::ReturnValue)) { + continue; + } + block.instructions[i].instr = Instruction::LoadFast { + var_num: Arg::marker(), + } + .into(); + } + } + } + + fn deoptimize_borrow_after_generator_exception_return(&mut self) { + if !self.flags.contains(CodeFlags::GENERATOR) { + return; + } + + fn deoptimize_block_borrows(block: &mut Block) { + let mut after_end_send = false; + for info in &mut block.instructions { + if matches!(info.instr.real(), Some(Instruction::EndSend)) { + after_end_send = true; + continue; + } + if after_end_send { + continue; + } + match info.instr.real() { + Some(Instruction::LoadFastBorrow { .. }) => { + info.instr = Instruction::LoadFast { + var_num: Arg::marker(), + } + .into(); + } + Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) => { + info.instr = Instruction::LoadFastLoadFast { + var_nums: Arg::marker(), + } + .into(); + } + _ => {} + } + } + } + + fn handler_checks_exception(blocks: &[Block], handler: BlockIdx) -> bool { + let mut stack = vec![handler]; + let mut visited = vec![false; blocks.len()]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL { + continue; + } + let idx = block_idx.idx(); + if visited[idx] { continue; } visited[idx] = true; @@ -4049,6 +4405,56 @@ impl CodeInfo { false } + fn normal_path_reaches_returning_handler( + blocks: &[Block], + start: BlockIdx, + returning_handler: &[bool], + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block.instructions.iter().any(|info| { + info.except_handler + .is_some_and(|handler| returning_handler[handler.handler_block.idx()]) + }) { + return true; + } + let Some(last) = block.instructions.last() else { + if block.next != BlockIdx::NULL { + stack.push(block.next); + } + continue; + }; + if last.instr.is_scope_exit() { + continue; + } + if last.instr.is_unconditional_jump() { + if last.target != BlockIdx::NULL { + stack.push(last.target); + } + continue; + } + if let Some(cond_idx) = trailing_conditional_jump_index(block) { + let target = block.instructions[cond_idx].target; + if target != BlockIdx::NULL { + stack.push(target); + } + } + if block.next != BlockIdx::NULL { + stack.push(block.next); + } + } + false + } + let mut returning_handler = vec![false; self.blocks.len()]; for block in &self.blocks { for handler in block @@ -4081,7 +4487,13 @@ impl CodeInfo { }) }) }); - prev_protected_return.then_some(seed) + (prev_protected_return + && !normal_path_reaches_returning_handler( + &self.blocks, + seed, + &returning_handler, + )) + .then_some(seed) }) .collect(); @@ -4290,6 +4702,37 @@ impl CodeInfo { }) } + fn block_has_push_exc_info(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PushExcInfo))) + } + + fn mark_handler_entries( + blocks: &[Block], + predecessors: &[Vec], + start: BlockIdx, + stop: BlockIdx, + entries: &mut [bool], + ) { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || cursor == stop || visited[cursor.idx()] { + continue; + } + visited[cursor.idx()] = true; + if block_has_push_exc_info(&blocks[cursor.idx()]) { + entries[cursor.idx()] = true; + continue; + } + for pred in &predecessors[cursor.idx()] { + stack.push(*pred); + } + } + } + fn predecessor_chain_has_check_exc_match( blocks: &[Block], predecessors: &[Vec], @@ -4403,21 +4846,9 @@ impl CodeInfo { )) } - let mut handler_resume_predecessors = vec![0usize; self.blocks.len()]; + let mut handler_resume_predecessors = vec![Vec::new(); self.blocks.len()]; let mut is_handler_resume_block = vec![false; self.blocks.len()]; let mut predecessors = vec![Vec::new(); self.blocks.len()]; - for (block_idx, block) in self.blocks.iter().enumerate() { - if !is_handler_resume_jump_block(block) { - continue; - } - is_handler_resume_block[block_idx] = true; - let target = block - .instructions - .last() - .expect("resume jump block has a last instruction") - .target; - handler_resume_predecessors[target.idx()] += 1; - } for (pred_idx, block) in self.blocks.iter().enumerate() { if block.next != BlockIdx::NULL { predecessors[block.next.idx()].push(BlockIdx::new(pred_idx as u32)); @@ -4428,20 +4859,51 @@ impl CodeInfo { } } } - - let mut visited = vec![false; self.blocks.len()]; - for (idx, &count) in handler_resume_predecessors.iter().enumerate() { - if count < 2 { - continue; - } - let seed = BlockIdx::new(idx as u32); - if matches!( - first_real_instr(&self.blocks[seed.idx()]), - Some(Instruction::ForIter { .. }) - ) { + for (block_idx, block) in self.blocks.iter().enumerate() { + if !is_handler_resume_jump_block(block) { continue; } - let mut segment = Vec::new(); + is_handler_resume_block[block_idx] = true; + let target = block + .instructions + .last() + .expect("resume jump block has a last instruction") + .target; + handler_resume_predecessors[target.idx()].push(BlockIdx::new(block_idx as u32)); + } + let function_has_with_cleanup = self.blocks.iter().any(|block| { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::WithExceptStart))) + }); + + let mut visited = vec![false; self.blocks.len()]; + for (idx, resume_preds) in handler_resume_predecessors.iter().enumerate() { + if resume_preds.len() < 2 { + continue; + } + let seed = BlockIdx::new(idx as u32); + let mut handler_entries = vec![false; self.blocks.len()]; + for pred in resume_preds { + mark_handler_entries( + &self.blocks, + &predecessors, + *pred, + seed, + &mut handler_entries, + ); + } + if handler_entries.iter().filter(|&&is_entry| is_entry).count() < 2 { + continue; + } + if matches!( + first_real_instr(&self.blocks[seed.idx()]), + Some(Instruction::ForIter { .. }) + ) { + continue; + } + let mut segment = Vec::new(); let mut cursor = seed; while cursor != BlockIdx::NULL { if block_is_exceptional(&self.blocks[cursor.idx()]) { @@ -4459,8 +4921,6 @@ impl CodeInfo { info.instr.real(), Some( Instruction::ForIter { .. } - | Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. } | Instruction::EndFor | Instruction::PopIter | Instruction::LoadFastAndClear { .. } @@ -4472,13 +4932,20 @@ impl CodeInfo { ) }) }); + let tail_enters_stacked_context = segment.iter().any(|block_idx| { + self.blocks[block_idx.idx()] + .start_depth + .is_some_and(|depth| depth > 1) + }); let has_simple_with_except_resume_tail = starts_with_fast_attr_call(&self.blocks[seed.idx()]) && has_exception_match_resume_predecessor(&self.blocks, &predecessors, seed) && predecessors[seed.idx()] .iter() .any(|pred| is_with_suppress_resume_jump_block(&self.blocks[pred.idx()])); - if !(starts_with_conditional_guard(&self.blocks[seed.idx()]) && has_complex_tail + if !(starts_with_conditional_guard(&self.blocks[seed.idx()]) + && has_complex_tail + && !(function_has_with_cleanup && tail_enters_stacked_context) || has_simple_with_except_resume_tail) { continue; @@ -4488,7 +4955,6 @@ impl CodeInfo { for block_idx in &segment { in_segment[block_idx.idx()] = true; } - for block_idx in segment { if visited[block_idx.idx()] { continue; @@ -4570,6 +5036,33 @@ impl CodeInfo { } } + fn block_has_simple_scope_exit(block: &Block) -> bool { + for info in &block.instructions { + match info.instr.real() { + Some(instr) if instr.is_scope_exit() => return true, + Some( + Instruction::Nop + | Instruction::NotTaken + | Instruction::LoadConst { .. } + | Instruction::LoadSmallInt { .. } + | Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } + | Instruction::StoreName { .. } + | Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastCheck { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::BuildTuple { .. }, + ) => {} + Some(_) => return false, + None => {} + } + } + false + } + fn normal_successors(block: &Block) -> Vec { let Some(last_info) = block.instructions.last() else { return (block.next != BlockIdx::NULL) @@ -4603,6 +5096,72 @@ impl CodeInfo { .collect() } + fn is_return_value_through_with_exit(block: &Block) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .collect(); + if !matches!( + reals.first(), + Some( + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + ) { + return false; + } + if !matches!(reals.last(), Some(Instruction::ReturnValue)) { + return false; + } + reals.iter().skip(1).all(|instr| { + matches!( + instr, + Instruction::Swap { .. } + | Instruction::LoadConst { .. } + | Instruction::Call { .. } + | Instruction::PopTop + | Instruction::ReturnValue + ) + }) + } + + fn is_simple_store_attr_tail(block: &Block) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .collect(); + matches!( + reals.as_slice(), + [ + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::StoreAttr { .. }, + .. + ] | [ + Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. }, + Instruction::StoreAttr { .. }, + .. + ] + ) + } + + fn leading_bool_guard_has_scope_exit_successor(blocks: &[Block], block: &Block) -> bool { + let Some(cond_idx) = trailing_conditional_jump_index(block) else { + return false; + }; + [block.instructions[cond_idx].target, block.next] + .into_iter() + .any(|successor| { + successor != BlockIdx::NULL + && block_has_simple_scope_exit(&blocks[successor.idx()]) + }) + } + fn path_reaches_named_cleanup( blocks: &[Block], start: BlockIdx, @@ -4702,6 +5261,34 @@ impl CodeInfo { false } + fn linear_tail_has_with_setup(blocks: &[Block], start: BlockIdx) -> bool { + let mut cursor = start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && !visited[cursor.idx()] { + visited[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + if block_is_exceptional(block) { + return false; + } + if block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::LoadSpecial { .. }))) + { + return true; + } + let Some(last_info) = block.instructions.last() else { + cursor = block.next; + continue; + }; + if last_info.instr.is_scope_exit() || last_info.instr.is_unconditional_jump() { + return false; + } + cursor = block.next; + } + false + } + fn is_with_suppress_resume_block(block: &Block) -> bool { let Some(last_info) = block.instructions.last() else { return false; @@ -4724,6 +5311,13 @@ impl CodeInfo { saw_pop_except && pop_top_after_pop_except >= 3 } + fn block_has_protected_instructions(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| info.except_handler.is_some()) + } + let mut named_cleanup_predecessors = vec![0usize; self.blocks.len()]; let mut named_cleanup_requires_deopt = vec![false; self.blocks.len()]; let mut has_with_suppress_resume_predecessor = vec![false; self.blocks.len()]; @@ -4756,12 +5350,21 @@ impl CodeInfo { ) { continue; } + if matches!( + last_info.instr.real(), + Some(Instruction::JumpBackward { .. }) + ) && block_has_protected_instructions(&self.blocks[last_info.target.idx()]) + { + continue; + } named_cleanup_predecessors[last_info.target.idx()] += 1; - if named_cleanup_has_conditional_raise_sibling( - &self.blocks, - BlockIdx::new(block_idx as u32), - last_info.target, - ) { + if linear_tail_has_with_setup(&self.blocks, last_info.target) + && named_cleanup_has_conditional_raise_sibling( + &self.blocks, + BlockIdx::new(block_idx as u32), + last_info.target, + ) + { named_cleanup_requires_deopt[last_info.target.idx()] = true; } } @@ -4793,6 +5396,7 @@ impl CodeInfo { let seed = BlockIdx::new(idx as u32); let mut segment = Vec::new(); let mut cursor = seed; + let seed_guard_local = leading_bool_guard_local(&self.blocks[seed.idx()]); let mut fallback_guard_local = None; while cursor != BlockIdx::NULL { let block = &self.blocks[cursor.idx()]; @@ -4802,6 +5406,12 @@ impl CodeInfo { if cursor != seed && let Some(local) = leading_bool_guard_local(block) { + if !leading_bool_guard_has_scope_exit_successor(&self.blocks, block) { + break; + } + if seed_guard_local.is_some_and(|seed_local| seed_local != local) { + break; + } match fallback_guard_local { None => fallback_guard_local = Some(local), Some(expected) if expected != local => break, @@ -4811,7 +5421,8 @@ impl CodeInfo { segment.push(cursor); cursor = block.next; } - if fallback_guard_local.is_none() && !named_cleanup_requires_deopt[idx] { + let requires_deopt = named_cleanup_requires_deopt[idx]; + if fallback_guard_local.is_none() && !requires_deopt { continue; } @@ -4819,7 +5430,6 @@ impl CodeInfo { for block_idx in &segment { in_segment[block_idx.idx()] = true; } - for block_idx in segment { if visited[block_idx.idx()] { continue; @@ -4827,6 +5437,17 @@ impl CodeInfo { let is_same_guard_fallback = fallback_guard_local.is_some_and(|local| { leading_bool_guard_local(&self.blocks[block_idx.idx()]) == Some(local) }); + if !requires_deopt && !is_same_guard_fallback { + continue; + } + if requires_deopt + && is_return_value_through_with_exit(&self.blocks[block_idx.idx()]) + { + continue; + } + if requires_deopt && is_simple_store_attr_tail(&self.blocks[block_idx.idx()]) { + continue; + } if block_idx != seed && !is_same_guard_fallback && predecessors[block_idx.idx()].iter().any(|pred| { @@ -4862,28 +5483,55 @@ impl CodeInfo { } } - fn starts_with_fast_load(block: &Block) -> bool { - block - .instructions - .iter() - .find(|info| { - info.instr.real().is_some_and(|instr| { - !matches!(instr, Instruction::Nop | Instruction::NotTaken) - }) - }) - .is_some_and(|info| { + fn block_has_fast_load(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + ) + }) + } + + fn block_requires_post_reraise_strong_loads(block: &Block) -> bool { + block_has_protected_instructions(block) + || block.instructions.iter().any(|info| { matches!( info.instr.real(), Some( - Instruction::LoadFast { .. } - | Instruction::LoadFastBorrow { .. } - | Instruction::LoadFastLoadFast { .. } - | Instruction::LoadFastBorrowLoadFastBorrow { .. } + Instruction::Call { .. } + | Instruction::CallKw { .. } + | Instruction::DeleteFast { .. } + | Instruction::StoreAttr { .. } + | Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } + | Instruction::StoreSubscr ) ) }) } + fn block_ends_with_explicit_raise(block: &Block) -> bool { + block + .instructions + .iter() + .rev() + .filter_map(|info| info.instr.real().map(|instr| (instr, info.arg))) + .find(|(instr, _)| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .is_some_and(|(instr, arg)| { + matches!( + instr, + Instruction::RaiseVarargs { argc } + if argc.get(arg) != oparg::RaiseKind::BareRaise + ) + }) + } + fn block_has_protected_instructions(block: &Block) -> bool { block .instructions @@ -4912,6 +5560,13 @@ impl CodeInfo { }) } + fn block_jumps_unconditionally_to(block: &Block, target: BlockIdx) -> bool { + block + .instructions + .iter() + .any(|info| info.target == target && info.instr.is_unconditional_jump()) + } + fn handler_chain_has_explicit_reraise(blocks: &[Block], handler: BlockIdx) -> bool { let mut cursor = handler; let mut visited = vec![false; blocks.len()]; @@ -5001,68 +5656,215 @@ impl CodeInfo { false } - fn block_has_nonresuming_reraise_handler(blocks: &[Block], block: &Block) -> bool { - let mut seen_handlers = Vec::new(); - for handler in block - .instructions - .iter() - .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) - { - if seen_handlers.contains(&handler) { + fn handler_chain_has_named_cleanup_or_explicit_reraise( + blocks: &[Block], + handler: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![handler]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { continue; } - seen_handlers.push(handler); - if handler_chain_has_explicit_reraise(blocks, handler) - && !handler_chain_resumes_normally(blocks, handler) + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block + .instructions + .iter() + .any(|info| match info.instr.real() { + Some( + Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. }, + ) => true, + Some(Instruction::RaiseVarargs { argc }) => { + argc.get(info.arg) == oparg::RaiseKind::BareRaise + } + _ => false, + }) { return true; } - } - false - } - - let mut predecessors = vec![Vec::new(); self.blocks.len()]; - for (pred_idx, block) in self.blocks.iter().enumerate() { - if block_has_fallthrough(block) && block.next != BlockIdx::NULL { - predecessors[block.next.idx()].push(BlockIdx::new(pred_idx as u32)); - } - for info in &block.instructions { - if info.target != BlockIdx::NULL { - predecessors[info.target.idx()].push(BlockIdx::new(pred_idx as u32)); + if block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::PopExcept | Instruction::Reraise { .. }) + ) + }) { + continue; + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + stack.push(block.next); } } + false } - let has_reraising_except_handler = self.blocks.iter().any(|block| { + fn protected_block_handler_has_named_cleanup_or_explicit_reraise( + blocks: &[Block], + block: &Block, + ) -> bool { block .instructions .iter() .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) - .any(|handler| handler_chain_has_explicit_reraise(&self.blocks, handler)) - }); - if !has_reraising_except_handler { - return; + .any(|handler| handler_chain_has_named_cleanup_or_explicit_reraise(blocks, handler)) } - let mut follows_protected_body = vec![false; self.blocks.len()]; - for (idx, block) in self.blocks.iter().enumerate() { - if block_is_exceptional(block) || block.cold || !starts_with_fast_load(block) { - continue; - } - let mut seen = vec![false; self.blocks.len()]; - let mut stack = predecessors[idx].clone(); - while let Some(pred) = stack.pop() { - if pred == BlockIdx::NULL || seen[pred.idx()] { + fn nonresuming_reraise_handlers(blocks: &[Block], block: &Block) -> Vec { + let mut handlers = Vec::new(); + for handler in block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + { + if handlers.contains(&handler) { continue; } - seen[pred.idx()] = true; + if handler_chain_has_explicit_reraise(blocks, handler) + && !handler_chain_resumes_normally(blocks, handler) + { + handlers.push(handler); + } + } + handlers + } + + fn normal_successors(block: &Block) -> Vec { + let Some(last) = block.instructions.last() else { + return (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect(); + }; + if last.instr.is_scope_exit() { + return Vec::new(); + } + if matches!( + last.instr.real(), + Some( + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) { + return Vec::new(); + } + if last.instr.is_unconditional_jump() { + return (last.target != BlockIdx::NULL) + .then_some(last.target) + .into_iter() + .collect(); + } + if let Some(cond_idx) = trailing_conditional_jump_index(block) { + let mut successors = Vec::with_capacity(2); + let target = block.instructions[cond_idx].target; + if target != BlockIdx::NULL { + successors.push(target); + } + if block.next != BlockIdx::NULL { + successors.push(block.next); + } + return successors; + } + (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect() + } + + fn normal_path_reaches_handler( + blocks: &[Block], + start: BlockIdx, + handler: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block.instructions.iter().any(|info| { + info.except_handler + .is_some_and(|except_handler| except_handler.handler_block == handler) + }) { + return true; + } + stack.extend(normal_successors(block)); + } + false + } + + let mut predecessors = vec![Vec::new(); self.blocks.len()]; + for (pred_idx, block) in self.blocks.iter().enumerate() { + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + predecessors[block.next.idx()].push(BlockIdx::new(pred_idx as u32)); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + predecessors[info.target.idx()].push(BlockIdx::new(pred_idx as u32)); + } + } + } + + let has_reraising_except_handler = self.blocks.iter().any(|block| { + block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .any(|handler| handler_chain_has_explicit_reraise(&self.blocks, handler)) + }); + if !has_reraising_except_handler { + return; + } + + let mut follows_protected_body = vec![false; self.blocks.len()]; + for (idx, block) in self.blocks.iter().enumerate() { + if block_is_exceptional(block) + || block.cold + || !block_has_fast_load(block) + || !block_requires_post_reraise_strong_loads(block) + { + continue; + } + if predecessors[idx] + .iter() + .any(|pred| is_named_except_cleanup_normal_exit_block(&self.blocks[pred.idx()])) + { + continue; + } + let mut seen = vec![false; self.blocks.len()]; + let mut stack = predecessors[idx].clone(); + while let Some(pred) = stack.pop() { + if pred == BlockIdx::NULL || seen[pred.idx()] { + continue; + } + seen[pred.idx()] = true; let pred_block = &self.blocks[pred.idx()]; if block_has_protected_instructions(pred_block) { if block_jumps_backward_to(pred_block, BlockIdx::new(idx as u32)) { continue; } - follows_protected_body[idx] = - block_has_nonresuming_reraise_handler(&self.blocks, pred_block); + if block_jumps_unconditionally_to(pred_block, BlockIdx::new(idx as u32)) { + continue; + } + let handlers = nonresuming_reraise_handlers(&self.blocks, pred_block); + follows_protected_body[idx] = !handlers.is_empty() + && !handlers.iter().any(|handler| { + normal_path_reaches_handler( + &self.blocks, + BlockIdx::new(idx as u32), + *handler, + ) + }); break; } if !block_is_exceptional(pred_block) @@ -5074,27 +5876,47 @@ impl CodeInfo { } } - let mut visited = vec![false; self.blocks.len()]; for (idx, follows_protected_body) in follows_protected_body.iter().enumerate() { if !*follows_protected_body { continue; } - let mut cursor = BlockIdx::new(idx as u32); - while cursor != BlockIdx::NULL && !visited[cursor.idx()] { - let block = &self.blocks[cursor.idx()]; + let mut visited = vec![false; self.blocks.len()]; + let mut stack = vec![BlockIdx::new(idx as u32)]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + let block = &self.blocks[block_idx.idx()]; if block_is_exceptional(block) || block.cold { - break; + continue; } - visited[cursor.idx()] = true; - deoptimize_block_borrows(&mut self.blocks[cursor.idx()]); - if self.blocks[cursor.idx()] + if protected_block_handler_has_named_cleanup_or_explicit_reraise( + &self.blocks, + block, + ) { + visited[block_idx.idx()] = true; + stack.extend(normal_successors(block)); + continue; + } + if predecessors[block_idx.idx()] + .iter() + .any(|pred| is_named_except_cleanup_normal_exit_block(&self.blocks[pred.idx()])) + { + continue; + } + if block_ends_with_explicit_raise(block) { + continue; + } + visited[block_idx.idx()] = true; + deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); + if self.blocks[block_idx.idx()] .instructions .last() .is_some_and(|info| info.instr.is_scope_exit()) { - break; + continue; } - cursor = self.blocks[cursor.idx()].next; + stack.extend(normal_successors(&self.blocks[block_idx.idx()])); } } } @@ -5206,6 +6028,27 @@ impl CodeInfo { has_pop_except && pop_top_count == 0 && jumps_to_target } + fn has_direct_handler_resume_predecessor(blocks: &[Block], target: BlockIdx) -> bool { + blocks.iter().any(|pred_block| { + let has_pop_except = pred_block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + let jumps_to_target = pred_block.instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some( + Instruction::JumpForward { .. } + | Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }); + has_pop_except && jumps_to_target + }) + } + fn handler_chain_resumes_normally(blocks: &[Block], handler: BlockIdx) -> bool { let mut visited = vec![false; blocks.len()]; let mut stack = vec![handler]; @@ -5397,6 +6240,94 @@ impl CodeInfo { false } + fn nonresuming_handlers(blocks: &[Block], block: &Block) -> Vec { + let mut handlers = Vec::new(); + for handler in block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + { + if handlers.contains(&handler) { + continue; + } + if handler_chain_has_explicit_raise(blocks, handler) + && !handler_chain_has_multiple_handled_returns(blocks, handler) + && !handler_chain_resumes_normally(blocks, handler) + { + handlers.push(handler); + } + } + handlers + } + + fn normal_successors(block: &Block) -> Vec { + let Some(last) = block.instructions.last() else { + return (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect(); + }; + if last.instr.is_scope_exit() { + return Vec::new(); + } + if matches!( + last.instr.real(), + Some( + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) { + return Vec::new(); + } + if last.instr.is_unconditional_jump() { + return (last.target != BlockIdx::NULL) + .then_some(last.target) + .into_iter() + .collect(); + } + if let Some(cond_idx) = trailing_conditional_jump_index(block) { + let mut successors = Vec::with_capacity(2); + let target = block.instructions[cond_idx].target; + if target != BlockIdx::NULL { + successors.push(target); + } + if block.next != BlockIdx::NULL { + successors.push(block.next); + } + return successors; + } + (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect() + } + + fn normal_path_reaches_handler( + blocks: &[Block], + start: BlockIdx, + handler: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block.instructions.iter().any(|info| { + info.except_handler + .is_some_and(|except_handler| except_handler.handler_block == handler) + }) { + return true; + } + stack.extend(normal_successors(block)); + } + false + } + let mut predecessors = vec![Vec::new(); self.blocks.len()]; let mut is_handler_resume_block = vec![false; self.blocks.len()]; for (pred_idx, block) in self.blocks.iter().enumerate() { @@ -5423,12 +6354,26 @@ impl CodeInfo { .enumerate() .filter_map(|(idx, block)| { let cond_idx = trailing_conditional_jump_index(block)?; + if block.try_else_orelse_entry { + return None; + } let prev_protected = predecessors[idx].iter().any(|pred| { let pred_block = &self.blocks[pred.idx()]; block_has_protected_instructions(pred_block) && block_has_exception_match_handler(&self.blocks, pred_block) && block_has_nonresuming_exception_match_handler(&self.blocks, pred_block) }); + let prev_nonresuming_handlers: Vec<_> = predecessors[idx] + .iter() + .flat_map(|pred| { + let pred_block = &self.blocks[pred.idx()]; + if block_has_protected_instructions(pred_block) { + nonresuming_handlers(&self.blocks, pred_block) + } else { + Vec::new() + } + }) + .collect(); let has_unprotected_normal_predecessor = predecessors[idx].iter().any(|pred| { let pred_block = &self.blocks[pred.idx()]; !block_is_exceptional(pred_block) @@ -5456,9 +6401,13 @@ impl CodeInfo { } else { prev_protected }; + let same_handler_continuation = prev_nonresuming_handlers + .iter() + .any(|handler| normal_path_reaches_handler(&self.blocks, seed, *handler)); (!block_is_exceptional(block) && seed != BlockIdx::NULL && seed_enabled + && !same_handler_continuation && !has_unprotected_normal_predecessor) .then_some((seed, force_deopt)) }) @@ -5472,10 +6421,26 @@ impl CodeInfo { if block_is_exceptional(&self.blocks[cursor.idx()]) { break; } + if cursor != seed + && predecessors[cursor.idx()].iter().any(|pred| { + is_handler_resume_block[pred.idx()] + || is_handler_resume_predecessor(&self.blocks[pred.idx()], cursor) + || is_named_except_cleanup_normal_exit_block(&self.blocks[pred.idx()]) + }) + { + break; + } segment.push(cursor); cursor = self.blocks[cursor.idx()].next; } - + if segment.iter().any(|block_idx| { + predecessors[block_idx.idx()] + .iter() + .any(|pred| is_named_except_cleanup_normal_exit_block(&self.blocks[pred.idx()])) + }) { + continue; + } + let segment_ops: Vec<_> = segment .iter() .flat_map(|block_idx| { @@ -5509,10 +6474,11 @@ impl CodeInfo { ) }) .count(); - let has_handler_resume_predecessor = predecessors[seed.idx()].iter().any(|pred| { - is_handler_resume_block[pred.idx()] - || is_handler_resume_predecessor(&self.blocks[pred.idx()], seed) - }); + let has_handler_resume_predecessor = + predecessors[seed.idx()].iter().any(|pred| { + is_handler_resume_block[pred.idx()] + || is_handler_resume_predecessor(&self.blocks[pred.idx()], seed) + }) || has_direct_handler_resume_predecessor(&self.blocks, seed); if has_handler_resume_predecessor { continue; } @@ -5602,7 +6568,12 @@ impl CodeInfo { if predecessors[block_idx.idx()].iter().any(|pred| { is_handler_resume_block[pred.idx()] || is_handler_resume_predecessor(&self.blocks[pred.idx()], block_idx) - }) { + || is_named_except_cleanup_normal_exit_block(&self.blocks[pred.idx()]) + }) || has_direct_handler_resume_predecessor(&self.blocks, block_idx) + { + continue; + } + if block_has_protected_instructions(&self.blocks[block_idx.idx()]) { continue; } if !force_deopt @@ -5734,6 +6705,9 @@ impl CodeInfo { while cursor != BlockIdx::NULL && !visited[cursor.idx()] { visited[cursor.idx()] = true; let block = &blocks[cursor.idx()]; + if cursor != handler && block.except_handler { + return false; + } for info in &block.instructions { match info.instr.real() { Some(Instruction::CheckExcMatch | Instruction::CheckEgMatch) => { @@ -5809,28 +6783,145 @@ impl CodeInfo { false } - fn protected_block_has_terminal_exception_handler(blocks: &[Block], block: &Block) -> bool { - let mut seen_handlers = Vec::new(); - for handler in block - .instructions - .iter() - .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) - { - if seen_handlers.contains(&handler) { + fn handler_is_terminal_exception_handler(blocks: &[Block], handler: BlockIdx) -> bool { + handler_reaches_match_before_terminal(blocks, handler) + && !handler_chain_has_multiple_handled_returns(blocks, handler) + && !handler_chain_resumes_normally(blocks, handler) + } + + fn handler_chain_has_handled_return_or_bare_reraise( + blocks: &[Block], + handler: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![handler]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { continue; } - seen_handlers.push(handler); - let has_exception_match = handler_reaches_match_before_terminal(blocks, handler); - if has_exception_match - && !handler_chain_has_multiple_handled_returns(blocks, handler) - && !handler_chain_resumes_normally(blocks, handler) - { + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + let has_pop_except = block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + let has_return = block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::ReturnValue))); + if has_pop_except && has_return { + return true; + } + if block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::RaiseVarargs { argc }) + if argc.get(info.arg) == oparg::RaiseKind::BareRaise + ) + }) { return true; } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + stack.push(block.next); + } + } + false + } + + fn handler_body_has_conditional_after_match(blocks: &[Block], handler: BlockIdx) -> bool { + let mut cursor = handler; + let mut visited = vec![false; blocks.len()]; + let mut in_body = false; + while cursor != BlockIdx::NULL && !visited[cursor.idx()] { + visited[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + for info in &block.instructions { + match info.instr.real() { + Some(Instruction::PopTop) if !in_body => { + in_body = true; + } + Some(instr) + if in_body && is_conditional_jump(&AnyInstruction::Real(instr)) => + { + return true; + } + Some( + Instruction::PopExcept + | Instruction::RaiseVarargs { .. } + | Instruction::Reraise { .. } + | Instruction::ReturnValue, + ) => return false, + _ => {} + } + } + cursor = block.next; } false } + fn handler_is_terminal_for_conditional_tail(blocks: &[Block], handler: BlockIdx) -> bool { + handler_reaches_match_before_terminal(blocks, handler) + && handler_chain_has_handled_return_or_bare_reraise(blocks, handler) + && !handler_body_has_conditional_after_match(blocks, handler) + && !handler_chain_resumes_normally(blocks, handler) + } + + fn trailing_protected_tail_terminal_exception_handler( + blocks: &[Block], + block: &Block, + ) -> Option { + for info in block.instructions.iter().rev() { + match info.instr.real() { + Some(Instruction::Nop | Instruction::NotTaken | Instruction::PopTop) => {} + Some(_) => { + let handler = info.except_handler.map(|handler| handler.handler_block)?; + return handler_is_terminal_exception_handler(blocks, handler) + .then_some(handler); + } + None => {} + } + } + None + } + + fn trailing_protected_tail_conditional_exception_handler( + blocks: &[Block], + block: &Block, + ) -> Option { + for info in block.instructions.iter().rev() { + match info.instr.real() { + Some(Instruction::Nop | Instruction::NotTaken | Instruction::PopTop) => {} + Some(_) => { + let handler = info.except_handler.map(|handler| handler.handler_block)?; + return handler_is_terminal_for_conditional_tail(blocks, handler) + .then_some(handler); + } + None => {} + } + } + None + } + + fn protected_tail_ends_with_conditional(block: &Block) -> bool { + block + .instructions + .iter() + .rev() + .filter_map(|info| info.instr.real()) + .find(|instr| { + !matches!( + instr, + Instruction::Nop | Instruction::NotTaken | Instruction::PopTop + ) + }) + .is_some_and(|instr| is_conditional_jump(&AnyInstruction::Real(instr))) + } + fn normal_successors(block: &Block) -> Vec { let Some(last) = block.instructions.last() else { return (block.next != BlockIdx::NULL) @@ -5841,6 +6932,14 @@ impl CodeInfo { if last.instr.is_scope_exit() { return Vec::new(); } + if matches!( + last.instr.real(), + Some( + Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) { + return Vec::new(); + } if last.instr.is_unconditional_jump() { return (last.target != BlockIdx::NULL) .then_some(last.target) @@ -5864,23 +6963,52 @@ impl CodeInfo { .collect() } + fn normal_path_reaches_handler( + blocks: &[Block], + start: BlockIdx, + handler: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block.instructions.iter().any(|info| { + info.except_handler + .is_some_and(|except_handler| except_handler.handler_block == handler) + }) { + return true; + } + stack.extend(normal_successors(block)); + } + false + } + fn has_call_store_before_trailing_conditional(block: &Block) -> bool { let Some(cond_idx) = trailing_conditional_jump_index(block) else { return false; }; - block.instructions[..cond_idx] - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::Call { .. }))) - && block.instructions[..cond_idx].iter().any(|info| { - matches!( - info.instr.real(), - Some( - Instruction::StoreFast { .. } - | Instruction::StoreFastLoadFast { .. } - | Instruction::StoreFastStoreFast { .. } - ) + block.instructions[..cond_idx].iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::Call { .. } | Instruction::CallKw { .. }) + ) + }) && block.instructions[..cond_idx].iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } ) - }) + ) + }) } fn has_call_and_store(block: &Block) -> bool { @@ -5888,7 +7016,7 @@ impl CodeInfo { let mut has_store_fast = false; for info in &block.instructions { match info.instr.real() { - Some(Instruction::Call { .. }) => has_call = true, + Some(Instruction::Call { .. } | Instruction::CallKw { .. }) => has_call = true, Some( Instruction::StoreFast { .. } | Instruction::StoreFastLoadFast { .. } @@ -5900,6 +7028,35 @@ impl CodeInfo { has_call && has_store_fast } + fn normal_tail_reaches_conditional(blocks: &[Block], start: BlockIdx) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if trailing_conditional_jump_index(block).is_some() + || has_call_store_before_trailing_conditional(block) + { + return true; + } + if block + .instructions + .last() + .is_some_and(|info| info.instr.is_scope_exit()) + { + continue; + } + stack.extend(normal_successors(block)); + } + false + } + fn has_load_fast_pair(block: &Block) -> bool { block.instructions.iter().any(|info| { matches!( @@ -5913,10 +7070,12 @@ impl CodeInfo { } fn has_call(block: &Block) -> bool { - block - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::Call { .. }))) + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::Call { .. } | Instruction::CallKw { .. }) + ) + }) } fn is_handler_resume_predecessor(block: &Block, target: BlockIdx) -> bool { @@ -5938,6 +7097,31 @@ impl CodeInfo { has_pop_except && jumps_to_target } + fn is_unprotected_call_store_bridge_to(block: &Block, successor: BlockIdx) -> bool { + !block_is_exceptional(block) + && !block.cold + && !block_has_protected_instructions(block) + && has_call_and_store(block) + && trailing_conditional_jump_index(block).is_none() + && normal_successors(block).contains(&successor) + } + + fn is_simple_fast_return_block(block: &Block) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .collect(); + matches!( + reals.as_slice(), + [ + Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }, + Instruction::ReturnValue, + ] + ) + } + let mut predecessors = vec![Vec::new(); self.blocks.len()]; for (pred_idx, block) in self.blocks.iter().enumerate() { if block.next != BlockIdx::NULL { @@ -5975,6 +7159,7 @@ impl CodeInfo { && block.start_depth.is_some_and(|depth| depth > 0) && !has_protected_call_predecessor && !has_call_store_tail) + || block.try_else_orelse_entry || predecessors[idx].iter().any(|pred| { is_handler_resume_predecessor( &self.blocks[pred.idx()], @@ -5987,6 +7172,10 @@ impl CodeInfo { && !pred_block.cold && !block_has_protected_instructions(pred_block) && block_has_non_nop_real_instructions(pred_block) + && !is_unprotected_call_store_bridge_to( + pred_block, + BlockIdx::new(idx as u32), + ) }) || !(has_structured_terminal_tail_shape || has_protected_call_predecessor @@ -6004,15 +7193,29 @@ impl CodeInfo { seen[pred.idx()] = true; let pred_block = &self.blocks[pred.idx()]; if block_has_protected_instructions(pred_block) { - if protected_block_has_terminal_exception_handler(&self.blocks, pred_block) { + if protected_tail_ends_with_conditional(pred_block) { + break; + } + if let Some(handler) = + trailing_protected_tail_terminal_exception_handler(&self.blocks, pred_block) + { + let seed = BlockIdx::new(idx as u32); + if normal_path_reaches_handler(&self.blocks, seed, handler) { + break; + } seeds.push(( - BlockIdx::new(idx as u32), + seed, (has_protected_call_predecessor || has_call_store_tail) && !has_structured_terminal_tail_shape, + false, )); } break; } + if is_unprotected_call_store_bridge_to(pred_block, BlockIdx::new(idx as u32)) { + stack.extend(predecessors[pred.idx()].iter().copied()); + continue; + } if !block_is_exceptional(pred_block) && !pred_block.cold && !block_has_non_nop_real_instructions(pred_block) @@ -6022,32 +7225,89 @@ impl CodeInfo { } } - let mut visited = vec![false; self.blocks.len()]; - for (seed, direct_only) in seeds { - let mut stack = vec![seed]; - while let Some(block_idx) = stack.pop() { - if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + for (pred_idx, pred_block) in self.blocks.iter().enumerate() { + if block_is_exceptional(pred_block) || pred_block.cold { + continue; + } + let Some(handler) = + trailing_protected_tail_conditional_exception_handler(&self.blocks, pred_block) + else { + continue; + }; + for successor in normal_successors(pred_block) { + if successor == BlockIdx::NULL { continue; } - let block = &self.blocks[block_idx.idx()]; - if block_is_exceptional(block) - || block.cold - || block_has_protected_instructions(block) + let successor_block = &self.blocks[successor.idx()]; + if block_is_exceptional(successor_block) + || successor_block.cold + || successor_block.try_else_orelse_entry + || !normal_tail_reaches_conditional(&self.blocks, successor) + || normal_path_reaches_handler(&self.blocks, successor, handler) + || predecessors[successor.idx()].iter().any(|pred| { + *pred != BlockIdx::new(pred_idx as u32) + && is_handler_resume_predecessor(&self.blocks[pred.idx()], successor) + }) { continue; } - visited[block_idx.idx()] = true; - let successors = normal_successors(&self.blocks[block_idx.idx()]); - deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); - if direct_only { + seeds.push((successor, false, true)); + } + } + + let mut visited = vec![false; self.blocks.len()]; + for (seed, direct_only, skip_simple_fast_returns) in seeds { + let mut reachable_from_seed = vec![false; self.blocks.len()]; + let mut reachability_stack = vec![seed]; + while let Some(block_idx) = reachability_stack.pop() { + if block_idx == BlockIdx::NULL || reachable_from_seed[block_idx.idx()] { continue; } - for successor in successors { - stack.push(successor); + let block = &self.blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; } + reachable_from_seed[block_idx.idx()] = true; + reachability_stack.extend(normal_successors(block)); } - } - } + + let mut stack = vec![seed]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + let block = &self.blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block_idx != seed + && predecessors[block_idx.idx()].iter().any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + !block_is_exceptional(pred_block) + && !pred_block.cold + && !reachable_from_seed[pred.idx()] + && normal_successors(pred_block).contains(&block_idx) + }) + { + continue; + } + visited[block_idx.idx()] = true; + let successors = normal_successors(&self.blocks[block_idx.idx()]); + if !(self.blocks[block_idx.idx()].try_else_orelse_entry + || skip_simple_fast_returns + && is_simple_fast_return_block(&self.blocks[block_idx.idx()])) + { + deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); + } + if direct_only { + continue; + } + for successor in successors { + stack.push(successor); + } + } + } + } fn deoptimize_borrow_in_protected_method_call_after_terminal_except_tail(&mut self) { fn deoptimize_block_borrows(block: &mut Block) { @@ -6085,6 +7345,15 @@ impl CodeInfo { }) } + fn block_ends_with_return_value(block: &Block) -> bool { + block + .instructions + .iter() + .rev() + .find_map(|info| info.instr.real()) + .is_some_and(|instr| matches!(instr, Instruction::ReturnValue)) + } + fn handler_chain_has_exception_match(blocks: &[Block], handler: BlockIdx) -> bool { let mut cursor = handler; let mut visited = vec![false; blocks.len()]; @@ -6141,36 +7410,6 @@ impl CodeInfo { handlers } - fn protected_method_call_count(blocks: &[Block], block: &Block) -> usize { - block - .instructions - .iter() - .filter(|info| { - let is_method_load = matches!( - info.instr.real(), - Some(Instruction::LoadAttr { namei }) if namei.get(info.arg).is_method() - ); - is_method_load - && info.except_handler.is_some_and(|handler| { - handler_chain_has_exception_match(blocks, handler.handler_block) - }) - }) - .count() - } - - fn block_stores_fast(block: &Block) -> bool { - block.instructions.iter().any(|info| { - matches!( - info.instr.real(), - Some( - Instruction::StoreFast { .. } - | Instruction::StoreFastLoadFast { .. } - | Instruction::StoreFastStoreFast { .. } - ) - ) - }) - } - fn block_shares_handler(block: &Block, handlers: &[BlockIdx]) -> bool { block .instructions @@ -6384,20 +7623,11 @@ impl CodeInfo { let mut to_deopt = Vec::new(); for (idx, block) in self.blocks.iter().enumerate() { - if !block_is_exceptional(block) - && !block.cold - && !block_stores_fast(block) - && protected_method_call_count(&self.blocks, block) >= 2 - && protected_block_has_terminal_exception_handler(&self.blocks, block) - && protected_block_has_raising_exception_handler(&self.blocks, block) - { - to_deopt.push(BlockIdx::new(idx as u32)); - continue; - } if block_is_exceptional(block) || block.cold || starts_with_inlined_comprehension_restore(block) || !block_has_protected_method_call(&self.blocks, block) + || block_ends_with_return_value(block) || predecessors[idx].iter().any(|pred| { let pred_block = &self.blocks[pred.idx()]; !block_is_exceptional(pred_block) @@ -6863,7 +8093,7 @@ impl CodeInfo { } } - fn deoptimize_borrow_in_async_finally_early_return_tail(&mut self) { + fn deoptimize_borrow_after_handler_resume_loop_tail(&mut self) { fn deoptimize_block_borrows(block: &mut Block) { for info in &mut block.instructions { match info.instr.real() { @@ -6884,156 +8114,120 @@ impl CodeInfo { } } - fn block_has_send(block: &Block) -> bool { - block - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::Send { .. }))) - } - - fn block_has_get_anext(block: &Block) -> bool { - block - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::GetAnext))) - } - - fn block_has_return(block: &Block) -> bool { - block + fn is_handler_resume_predecessor(block: &Block, target: BlockIdx) -> bool { + let has_pop_except = block .instructions .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::ReturnValue))) - } - - fn normal_successors(block: &Block) -> Vec { - let Some(last) = block.instructions.last() else { - return (block.next != BlockIdx::NULL) - .then_some(block.next) - .into_iter() - .collect(); - }; - if last.instr.is_scope_exit() { - return Vec::new(); - } - if last.instr.is_unconditional_jump() { - return (last.target != BlockIdx::NULL) - .then_some(last.target) - .into_iter() - .collect(); - } - if let Some(cond_idx) = trailing_conditional_jump_index(block) { - let mut successors = Vec::with_capacity(2); - let target = block.instructions[cond_idx].target; - if target != BlockIdx::NULL { - successors.push(target); - } - if block.next != BlockIdx::NULL { - successors.push(block.next); - } - return successors; - } - (block.next != BlockIdx::NULL) - .then_some(block.next) - .into_iter() - .collect() - } - - let mut predecessors = vec![Vec::new(); self.blocks.len()]; - for (idx, block) in self.blocks.iter().enumerate() { - for successor in normal_successors(block) { - predecessors[successor.idx()].push(BlockIdx::new(idx as u32)); - } - } - let relevant_send_blocks: Vec<_> = self - .blocks - .iter() - .enumerate() - .map(|(idx, block)| { - block_has_send(block) - && !block_has_get_anext(block) - && !predecessors[idx] - .iter() - .any(|pred| block_has_get_anext(&self.blocks[pred.idx()])) - }) - .collect(); - if !relevant_send_blocks.iter().any(|has_send| *has_send) { - return; + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + let jumps_to_target = block.instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some( + Instruction::JumpForward { .. } + | Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }); + has_pop_except && jumps_to_target } - fn has_early_return_before_send(blocks: &[Block], relevant_send_blocks: &[bool]) -> bool { + fn handler_chain_resumes_to_loop_header( + blocks: &[Block], + handler: BlockIdx, + loop_header: BlockIdx, + ) -> bool { let mut visited = vec![false; blocks.len()]; - let mut stack = vec![BlockIdx(0)]; + let mut stack = vec![handler]; while let Some(block_idx) = stack.pop() { if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { continue; } visited[block_idx.idx()] = true; let block = &blocks[block_idx.idx()]; - if block.cold || block_is_exceptional(block) { - continue; + let has_pop_except = block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + if has_pop_except + && block.instructions.iter().any(|info| { + info.target == loop_header + && matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }) + { + return true; } - if relevant_send_blocks[block_idx.idx()] { - continue; + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push(info.target); + } } - if block_has_return(block) { - return true; + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + stack.push(block.next); } - stack.extend(normal_successors(block)); } false } - let has_early_return = has_early_return_before_send(&self.blocks, &relevant_send_blocks); - if !has_early_return { - return; - } - - for (idx, block) in self.blocks.iter_mut().enumerate() { - if idx == 0 || block.cold || block_is_exceptional(block) { - continue; - } - deoptimize_block_borrows(block); - } - } - - fn deoptimize_borrow_after_handler_resume_loop_tail(&mut self) { - fn deoptimize_block_borrows(block: &mut Block) { - for info in &mut block.instructions { - match info.instr.real() { - Some(Instruction::LoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFast { - var_num: Arg::marker(), - } - .into(); - } - Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), - } - .into(); + fn handler_chain_stores_and_resumes_to_loop_header( + blocks: &[Block], + handler: BlockIdx, + loop_header: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![(handler, false)]; + while let Some((block_idx, stores_local)) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + let stores_local = stores_local || block_has_store_fast(block); + let has_pop_except = block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); + if has_pop_except + && stores_local + && block.instructions.iter().any(|info| { + info.target == loop_header + && matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }) + { + return true; + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push((info.target, stores_local)); } - _ => {} + } + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + stack.push((block.next, stores_local)); } } + false } - fn is_handler_resume_predecessor(block: &Block, target: BlockIdx) -> bool { - let has_pop_except = block + fn protected_block_handler_resumes_to_self(blocks: &[Block], block_idx: BlockIdx) -> bool { + let block = &blocks[block_idx.idx()]; + block .instructions .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))); - let jumps_to_target = block.instructions.iter().any(|info| { - info.target == target - && matches!( - info.instr.real(), - Some( - Instruction::JumpForward { .. } - | Instruction::JumpBackward { .. } - | Instruction::JumpBackwardNoInterrupt { .. } - ) - ) - }); - has_pop_except && jumps_to_target + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .any(|handler| handler_chain_resumes_to_loop_header(blocks, handler, block_idx)) } fn is_suppressing_with_resume_predecessor(block: &Block, target: BlockIdx) -> bool { @@ -7070,6 +8264,21 @@ impl CodeInfo { }) } + fn block_has_protected_instructions(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| info.except_handler.is_some()) + } + + fn block_has_non_nop_real_instructions(block: &Block) -> bool { + block.instructions.iter().any(|info| { + info.instr + .real() + .is_some_and(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + }) + } + fn predecessor_chain_has_check_exc_match( blocks: &[Block], predecessors: &[Vec], @@ -7106,10 +8315,102 @@ impl CodeInfo { }) } - fn starts_with_bool_guard(block: &Block) -> bool { - let infos: Vec<_> = block - .instructions - .iter() + fn is_plain_protected_resume_successor( + blocks: &[Block], + predecessors: &[Vec], + target: BlockIdx, + ) -> bool { + let mut has_handler_resume_predecessor = false; + let mut has_normal_fallthrough_predecessor = false; + for pred in &predecessors[target.idx()] { + let pred_block = &blocks[pred.idx()]; + if is_handler_resume_predecessor(pred_block, target) { + has_handler_resume_predecessor = true; + continue; + } + if block_is_exceptional(pred_block) || pred_block.cold { + continue; + } + if next_nonempty_block(blocks, pred_block.next) == target { + has_normal_fallthrough_predecessor = true; + continue; + } + return false; + } + has_handler_resume_predecessor && has_normal_fallthrough_predecessor + } + + fn predecessor_chain_has_protected_instructions( + blocks: &[Block], + predecessors: &[Vec], + start: BlockIdx, + stop: BlockIdx, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || cursor == stop || visited[cursor.idx()] { + continue; + } + visited[cursor.idx()] = true; + if block_has_protected_instructions(&blocks[cursor.idx()]) { + return true; + } + for pred in &predecessors[cursor.idx()] { + stack.push(*pred); + } + } + false + } + + fn block_stores_local(block: &Block, local: usize) -> bool { + block + .instructions + .iter() + .any(|info| match info.instr.real() { + Some(Instruction::StoreFast { var_num }) => { + usize::from(var_num.get(info.arg)) == local + } + Some(Instruction::StoreFastLoadFast { var_nums }) => { + let (store_idx, _) = var_nums.get(info.arg).indexes(); + usize::from(store_idx) == local + } + Some(Instruction::StoreFastStoreFast { var_nums }) => { + let (left, right) = var_nums.get(info.arg).indexes(); + usize::from(left) == local || usize::from(right) == local + } + _ => false, + }) + } + + fn predecessor_chain_stores_local( + blocks: &[Block], + predecessors: &[Vec], + start: BlockIdx, + stop: BlockIdx, + local: usize, + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || cursor == stop || visited[cursor.idx()] { + continue; + } + visited[cursor.idx()] = true; + if block_stores_local(&blocks[cursor.idx()], local) { + return true; + } + for pred in &predecessors[cursor.idx()] { + stack.push(*pred); + } + } + false + } + + fn starts_with_bool_guard(block: &Block) -> bool { + let infos: Vec<_> = block + .instructions + .iter() .filter(|info| { info.instr.real().is_some_and(|instr| { !matches!(instr, Instruction::Nop | Instruction::NotTaken) @@ -7152,6 +8453,23 @@ impl CodeInfo { }) } + fn block_has_loop_back_to_or_before( + blocks: &[Block], + block: &Block, + target_block: BlockIdx, + ) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && (info.target == target_block + || comes_before(blocks, info.target, target_block)) + }) + } + fn block_has_for_iter(block: &Block) -> bool { block .instructions @@ -7180,6 +8498,18 @@ impl CodeInfo { }) } + fn block_has_fast_load_pair(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + ) + ) + }) + } + fn block_has_method_load(block: &Block) -> bool { block.instructions.iter().any(|info| { matches!( @@ -7189,6 +8519,193 @@ impl CodeInfo { }) } + fn block_has_store_fast(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } + ) + ) + }) + } + + fn block_has_call(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::Call { .. } + | Instruction::CallKw { .. } + | Instruction::CallFunctionEx + ) + ) + }) + } + + fn block_has_conditional_jump_to(block: &Block, target: BlockIdx) -> bool { + block + .instructions + .iter() + .any(|info| info.target == target && is_conditional_jump(&info.instr)) + } + + fn loop_back_target(block: &Block) -> Option { + block.instructions.iter().find_map(|info| { + matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + .then_some(info.target) + }) + } + + fn conditional_fallthrough_loop_header( + blocks: &[Block], + block: &Block, + target: BlockIdx, + ) -> Option { + if !block_has_conditional_jump_to(block, target) { + return None; + } + loop_back_target(block).or_else(|| { + (block.next != BlockIdx::NULL) + .then(|| loop_back_target(&blocks[block.next.idx()]))? + }) + } + + fn any_protected_handler_resumes_to_loop_header( + blocks: &[Block], + loop_header: BlockIdx, + ) -> bool { + blocks.iter().any(|block| { + block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .any(|handler| { + handler_chain_resumes_to_loop_header(blocks, handler, loop_header) + }) + }) + } + + fn any_storing_protected_handler_resumes_to_loop_header( + blocks: &[Block], + loop_header: BlockIdx, + ) -> bool { + blocks.iter().any(|block| { + block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .any(|handler| { + handler_chain_stores_and_resumes_to_loop_header( + blocks, + handler, + loop_header, + ) + }) + }) + } + + fn any_exception_cleanup_jumps_to(blocks: &[Block], target: BlockIdx) -> bool { + blocks.iter().any(|block| { + block.cold + && block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))) + && block.instructions.iter().any(|info| { + info.target == target + && matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }) + }) + } + + fn fallthrough_chain_reaches_target( + blocks: &[Block], + start: BlockIdx, + target: BlockIdx, + ) -> bool { + let mut cursor = start; + let mut seen = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && !seen[cursor.idx()] { + if cursor == target { + return true; + } + seen[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + if block_has_non_nop_real_instructions(block) || !block_has_fallthrough(block) { + return false; + } + cursor = block.next; + } + false + } + + fn block_is_normal_finally_cleanup_call(block: &Block) -> bool { + block_has_call(block) + && !block_has_store_fast(block) + && block_has_fallthrough(block) + && block + .instructions + .last() + .is_some_and(|info| matches!(info.instr.real(), Some(Instruction::PopTop))) + } + + fn first_fast_load_local(block: &Block) -> Option { + block + .instructions + .iter() + .find_map(|info| match info.instr.real() { + Some( + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }, + ) => Some(usize::from(var_num.get(info.arg))), + _ => None, + }) + } + + fn trailing_conditional_guard_local(block: &Block, target: BlockIdx) -> Option { + let infos: Vec<_> = block + .instructions + .iter() + .filter(|info| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }) + .collect(); + let jump = infos.last()?; + if jump.target != target || !is_conditional_jump(&jump.instr) { + return None; + } + let load = if infos + .get(infos.len().wrapping_sub(2)) + .is_some_and(|info| matches!(info.instr.real(), Some(Instruction::ToBool))) + { + infos.get(infos.len().wrapping_sub(3))? + } else { + infos.get(infos.len().wrapping_sub(2))? + }; + match load.instr.real() { + Some( + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }, + ) => Some(usize::from(var_num.get(load.arg))), + _ => None, + } + } + fn block_is_calling_finally_cleanup(block: &Block) -> bool { let has_call = block.instructions.iter().any(|info| { matches!( @@ -7213,10 +8730,46 @@ impl CodeInfo { }) } - fn has_jump_back_predecessor_to( + fn handler_chain_calls_finally_cleanup(blocks: &[Block], handler: BlockIdx) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![handler]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_calling_finally_cleanup(block) { + return true; + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + stack.push(block.next); + } + } + false + } + + fn protected_block_handler_calls_finally_cleanup( blocks: &[Block], - predecessors: &[Vec], - target: BlockIdx, + block_idx: BlockIdx, + ) -> bool { + let block = &blocks[block_idx.idx()]; + block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .any(|handler| handler_chain_calls_finally_cleanup(blocks, handler)) + } + + fn has_jump_back_predecessor_to( + blocks: &[Block], + predecessors: &[Vec], + target: BlockIdx, ) -> bool { predecessors[target.idx()].iter().any(|pred| { let pred_block = &blocks[pred.idx()]; @@ -7292,9 +8845,43 @@ impl CodeInfo { .collect() } + fn tail_has_for_loop_back( + blocks: &[Block], + predecessors: &[Vec], + seed: BlockIdx, + ) -> bool { + let mut seen = vec![false; blocks.len()]; + let mut stack = vec![seed]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || seen[cursor.idx()] { + continue; + } + seen[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + for info in &block.instructions { + if matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && info.target != BlockIdx::NULL + && block_has_for_iter(&blocks[info.target.idx()]) + { + return true; + } + } + for successor in tail_successors(blocks, predecessors, cursor) { + stack.push(successor); + } + } + false + } + let mut predecessors = vec![Vec::new(); self.blocks.len()]; let mut is_handler_resume_block = vec![false; self.blocks.len()]; for (pred_idx, block) in self.blocks.iter().enumerate() { + let block_idx = BlockIdx::new(pred_idx as u32); if block .instructions .iter() @@ -7305,17 +8892,15 @@ impl CodeInfo { { is_handler_resume_block[pred_idx] = true; } - if block.next != BlockIdx::NULL { - predecessors[block.next.idx()].push(BlockIdx::new(pred_idx as u32)); + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + predecessors[block.next.idx()].push(block_idx); } for info in &block.instructions { if info.target != BlockIdx::NULL { - predecessors[info.target.idx()].push(BlockIdx::new(pred_idx as u32)); + predecessors[info.target.idx()].push(block_idx); } } } - - let has_calling_finally_cleanup = self.blocks.iter().any(block_is_calling_finally_cleanup); let has_exception_match_handler = self.blocks.iter().any(|block| { block.instructions.iter().any(|info| { matches!( @@ -7330,88 +8915,103 @@ impl CodeInfo { .iter() .any(|info| matches!(info.instr.real(), Some(Instruction::CheckEgMatch))) }); - let suppressing_exception_match_method_tails: Vec<_> = self .blocks .iter() .enumerate() .filter_map(|(idx, block)| { if block_is_exceptional(block) - || !has_exception_group_match_handler + || block_has_for_iter(block) || !block_has_fast_load(block) || !block_has_method_load(block) { return None; } + let target = BlockIdx::new(idx as u32); + let has_suppressing_with_resume_predecessor = + predecessors[idx].iter().any(|pred| { + is_suppressing_with_resume_predecessor(&self.blocks[pred.idx()], target) + }); + (has_suppressing_with_resume_predecessor + && (has_exception_group_match_handler + || has_exception_match_resume_predecessor( + &self.blocks, + &predecessors, + target, + ))) + .then_some(target) + }) + .collect(); + for block_idx in suppressing_exception_match_method_tails { + deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); + } + + let handler_resume_loop_successor_tails: Vec<_> = self + .blocks + .iter() + .enumerate() + .filter_map(|(idx, block)| { + if block_is_exceptional(block) + || block.cold + || !block_has_fast_load_pair(block) + || !block_has_call(block) + { + return None; + } predecessors[idx] .iter() .any(|pred| { - is_suppressing_with_resume_predecessor( - &self.blocks[pred.idx()], - BlockIdx::new(idx as u32), - ) + !block_is_exceptional(&self.blocks[pred.idx()]) + && !self.blocks[pred.idx()].cold + && !block_has_store_fast(&self.blocks[pred.idx()]) + && protected_block_handler_resumes_to_self(&self.blocks, *pred) }) .then_some(BlockIdx::new(idx as u32)) }) .collect(); - for block_idx in suppressing_exception_match_method_tails { + for block_idx in handler_resume_loop_successor_tails { deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); } - let seeds: Vec<_> = self + let finally_cleanup_successor_tails: Vec<_> = self .blocks .iter() .enumerate() .filter_map(|(idx, block)| { - let has_bool_guard_tail = starts_with_bool_guard(block); - let has_loop_tail = block_has_for_iter(block) || block_has_get_iter(block); - let has_finally_except_loop_tail = - has_calling_finally_cleanup && has_exception_match_handler && has_loop_tail; - let has_protected_predecessor = predecessors[idx].iter().any(|pred| { - self.blocks[pred.idx()] - .instructions - .iter() - .any(|info| info.except_handler.is_some()) - }); - let has_handler_resume_predecessor = predecessors[idx].iter().any(|pred| { - is_handler_resume_block[pred.idx()] - || is_handler_resume_predecessor( - &self.blocks[pred.idx()], - BlockIdx::new(idx as u32), - ) - }); - let has_suppressing_with_resume_predecessor = - predecessors[idx].iter().any(|pred| { - is_suppressing_with_resume_predecessor( - &self.blocks[pred.idx()], - BlockIdx::new(idx as u32), - ) - }); - let has_exception_match_resume_predecessor = has_exception_match_resume_predecessor( - &self.blocks, - &predecessors, - BlockIdx::new(idx as u32), - ); - let has_handler_resume_loop_tail = block_has_get_iter(block) - && has_suppressing_with_resume_predecessor - && has_exception_match_resume_predecessor; - let has_supported_tail = has_bool_guard_tail - || has_finally_except_loop_tail - || has_handler_resume_loop_tail; - if block_is_exceptional(block) || !has_supported_tail { + if block_is_exceptional(block) + || block.cold + || block_has_protected_instructions(block) + || block_has_call(block) + || !starts_with_bool_guard(block) + || !block_has_fast_load(block) + { return None; } - let should_seed = (has_protected_predecessor && has_finally_except_loop_tail) - || (has_bool_guard_tail && has_handler_resume_predecessor) - || has_handler_resume_loop_tail; - should_seed.then_some((BlockIdx::new(idx as u32), has_handler_resume_loop_tail)) + let target = BlockIdx::new(idx as u32); + predecessors[idx] + .iter() + .any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + block_has_conditional_jump_to(pred_block, target) + && pred_block.next != BlockIdx::NULL + && { + let cleanup_block = &self.blocks[pred_block.next.idx()]; + block_is_normal_finally_cleanup_call(cleanup_block) + && trailing_conditional_guard_local(pred_block, target) + == first_fast_load_local(cleanup_block) + && fallthrough_chain_reaches_target( + &self.blocks, + cleanup_block.next, + target, + ) + } + }) + .then_some(BlockIdx::new(idx as u32)) }) .collect(); - - let mut visited = vec![false; self.blocks.len()]; - for (seed, include_join_tail) in seeds { + let mut visited_finally_tail = vec![false; self.blocks.len()]; + for seed in finally_cleanup_successor_tails { let mut segment = Vec::new(); - let mut found_loop_back = false; let mut seen = vec![false; self.blocks.len()]; let mut stack = vec![seed]; while let Some(cursor) = stack.pop() { @@ -7420,22 +9020,14 @@ impl CodeInfo { } seen[cursor.idx()] = true; let block = &self.blocks[cursor.idx()]; - if block_is_exceptional(block) { + if block_is_exceptional(block) || block.cold || block_has_loop_back(block) { continue; } segment.push(cursor); - if block_has_loop_back(block) { - found_loop_back = true; - continue; - } for successor in tail_successors(&self.blocks, &predecessors, cursor) { stack.push(successor); } } - if !found_loop_back { - continue; - } - let segment_ops: Vec<_> = segment .iter() .flat_map(|block_idx| { @@ -7459,117 +9051,418 @@ impl CodeInfo { if !has_call || !has_store_fast { continue; } - - let mut in_segment = vec![false; self.blocks.len()]; - for block_idx in &segment { - in_segment[block_idx.idx()] = true; - } - for block_idx in segment { - if visited[block_idx.idx()] { - continue; - } - if !include_join_tail - && block_idx != seed - && predecessors[block_idx.idx()] - .iter() - .any(|pred| !in_segment[pred.idx()] && !is_handler_resume_block[pred.idx()]) - { + if visited_finally_tail[block_idx.idx()] { continue; } - visited[block_idx.idx()] = true; + visited_finally_tail[block_idx.idx()] = true; deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); } } - } - fn deoptimize_borrow_after_push_exc_info(&mut self) { - for block in &mut self.blocks { - let mut in_exception_state = false; - for info in &mut block.instructions { - match info.instr.real() { - Some(Instruction::PushExcInfo) => { - in_exception_state = true; - } - Some(Instruction::PopExcept | Instruction::Reraise { .. }) => { - in_exception_state = false; + let handler_break_join_tails: Vec<_> = self + .blocks + .iter() + .enumerate() + .filter_map(|(idx, block)| { + if block_is_exceptional(block) + || block.cold + || !block_has_fast_load(block) + || !block_has_method_load(block) + || !block_has_call(block) + || !has_exception_match_resume_predecessor( + &self.blocks, + &predecessors, + BlockIdx::new(idx as u32), + ) + { + return None; + } + let join = BlockIdx::new(idx as u32); + let body = predecessors[idx].iter().find_map(|jump_pred| { + let jump_block = &self.blocks[jump_pred.idx()]; + if block_is_exceptional(jump_block) + || jump_block.cold + || !jump_block.instructions.last().is_some_and(|info| { + info.target == join && info.instr.is_unconditional_jump() + }) + { + return None; } - Some(Instruction::LoadFastBorrow { .. }) if in_exception_state => { - info.instr = Instruction::LoadFast { - var_num: Arg::marker(), + predecessors[jump_pred.idx()].iter().find_map(|cond_pred| { + let cond_block = &self.blocks[cond_pred.idx()]; + if block_is_exceptional(cond_block) || cond_block.cold { + return None; } - .into(); - } - Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) - if in_exception_state => - { - info.instr = Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), + let cond_idx = trailing_conditional_jump_index(cond_block)?; + let cond = cond_block.instructions[cond_idx]; + if cond.target == *jump_pred + || cond.target == BlockIdx::NULL + || cond_block.next != *jump_pred + { + return None; } - .into(); - } - _ => {} - } - } + let body = next_nonempty_block(&self.blocks, cond.target); + if body == BlockIdx::NULL { + return None; + } + let body_block = &self.blocks[body.idx()]; + (!block_is_exceptional(body_block) + && !body_block.cold + && block_has_loop_back(body_block) + && block_has_fast_load(body_block) + && block_has_method_load(body_block) + && block_has_call(body_block) + && block_has_store_fast(body_block)) + .then_some(body) + }) + })?; + Some((join, body)) + }) + .collect(); + for (join, body) in handler_break_join_tails { + deoptimize_block_borrows(&mut self.blocks[join.idx()]); + deoptimize_block_borrows(&mut self.blocks[body.idx()]); } - } - fn deoptimize_borrow_after_protected_import(&mut self) { - fn deoptimize_borrow(info: &mut InstructionInfo) { - match info.instr.real() { - Some(Instruction::LoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFast { - var_num: Arg::marker(), - } - .into(); + let conditional_loop_bypass_tails: Vec<_> = self + .blocks + .iter() + .enumerate() + .filter_map(|(idx, block)| { + if block_is_exceptional(block) + || block.cold + || !block_has_fast_load(block) + || !has_exception_match_handler + { + return None; } - Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) => { - info.instr = Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), - } - .into(); + let target = BlockIdx::new(idx as u32); + if is_plain_protected_resume_successor(&self.blocks, &predecessors, target) { + return None; } - _ => {} - } - } - - fn deoptimize_block_borrows_from(block: &mut Block, start: usize) { - for info in block.instructions.iter_mut().skip(start) { - deoptimize_borrow(info); - } - } - - fn deoptimize_protected_block_borrows_from( - block: &mut Block, - start: usize, - protected_store_locals: &[bool], - ) { - for info in block.instructions.iter_mut().skip(start) { - if info.except_handler.is_none() { - break; + predecessors[idx] + .iter() + .any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + conditional_fallthrough_loop_header(&self.blocks, pred_block, target) + .is_some_and(|loop_header| { + let storing_handler_resumes = + any_storing_protected_handler_resumes_to_loop_header( + &self.blocks, + loop_header, + ) || any_storing_protected_handler_resumes_to_loop_header( + &self.blocks, + *pred, + ); + block_has_for_iter(&self.blocks[loop_header.idx()]) + && predecessor_chain_has_protected_instructions( + &self.blocks, + &predecessors, + *pred, + loop_header, + ) + && storing_handler_resumes + && (any_protected_handler_resumes_to_loop_header( + &self.blocks, + loop_header, + ) || any_protected_handler_resumes_to_loop_header( + &self.blocks, + *pred, + ) || any_exception_cleanup_jumps_to( + &self.blocks, + loop_header, + ) || any_exception_cleanup_jumps_to(&self.blocks, *pred)) + }) + }) + .then_some(target) + }) + .collect(); + let mut visited_conditional_tail = vec![false; self.blocks.len()]; + for seed in conditional_loop_bypass_tails { + let mut segment = Vec::new(); + let mut seen = vec![false; self.blocks.len()]; + let mut stack = vec![seed]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || seen[cursor.idx()] { + continue; } - match info.instr.real() { - Some(Instruction::LoadFastBorrow { var_num }) => { - let local = usize::from(var_num.get(info.arg)); - if protected_store_locals.get(local).copied().unwrap_or(false) { - continue; - } + seen[cursor.idx()] = true; + let block = &self.blocks[cursor.idx()]; + if block_is_exceptional(block) || block.cold || block_has_loop_back(block) { + continue; + } + segment.push(cursor); + for successor in tail_successors(&self.blocks, &predecessors, cursor) { + stack.push(successor); + } + } + let segment_ops: Vec<_> = segment + .iter() + .flat_map(|block_idx| { + self.blocks[block_idx.idx()] + .instructions + .iter() + .filter_map(|info| info.instr.real()) + }) + .collect(); + let has_store_fast = segment_ops.iter().any(|instr| { + matches!( + instr, + Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } + ) + }); + if !has_store_fast { + continue; + } + for block_idx in segment { + if visited_conditional_tail[block_idx.idx()] { + continue; + } + visited_conditional_tail[block_idx.idx()] = true; + deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); + } + } + + let seeds: Vec<_> = self + .blocks + .iter() + .enumerate() + .filter_map(|(idx, block)| { + let has_bool_guard_tail = starts_with_bool_guard(block); + let has_loop_tail = block_has_for_iter(block) || block_has_get_iter(block); + let has_protected_predecessor = predecessors[idx].iter().any(|pred| { + self.blocks[pred.idx()] + .instructions + .iter() + .any(|info| info.except_handler.is_some()) + }); + let has_protected_finally_cleanup_predecessor = predecessors[idx] + .iter() + .any(|pred| protected_block_handler_calls_finally_cleanup(&self.blocks, *pred)); + let has_finally_except_loop_tail = has_exception_match_handler + && has_loop_tail + && has_protected_finally_cleanup_predecessor; + let has_handler_resume_predecessor = predecessors[idx].iter().any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + !is_named_except_cleanup_normal_exit_block(pred_block) + && (is_handler_resume_block[pred.idx()] + || is_handler_resume_predecessor(pred_block, BlockIdx::new(idx as u32))) + }); + let is_plain_protected_resume_successor = is_plain_protected_resume_successor( + &self.blocks, + &predecessors, + BlockIdx::new(idx as u32), + ); + let has_suppressing_with_resume_predecessor = + predecessors[idx].iter().any(|pred| { + is_suppressing_with_resume_predecessor( + &self.blocks[pred.idx()], + BlockIdx::new(idx as u32), + ) + }); + let has_exception_match_resume_predecessor = has_exception_match_resume_predecessor( + &self.blocks, + &predecessors, + BlockIdx::new(idx as u32), + ); + let bool_guard_local = has_bool_guard_tail + .then(|| first_fast_load_local(block)) + .flatten(); + let handler_resume_predecessor_stores_guard = + bool_guard_local.is_some_and(|local| { + predecessors[idx].iter().any(|pred| { + let pred_block = &self.blocks[pred.idx()]; + is_handler_resume_predecessor(pred_block, BlockIdx::new(idx as u32)) + && predecessor_chain_stores_local( + &self.blocks, + &predecessors, + *pred, + BlockIdx::new(idx as u32), + local, + ) + }) + }); + let is_loop_header = has_jump_back_predecessor_to( + &self.blocks, + &predecessors, + BlockIdx::new(idx as u32), + ); + let has_handler_resume_loop_tail = block_has_get_iter(block) + && has_suppressing_with_resume_predecessor + && has_exception_match_resume_predecessor; + let has_supported_tail = has_bool_guard_tail + || has_finally_except_loop_tail + || has_handler_resume_loop_tail; + if block_is_exceptional(block) || !has_supported_tail { + return None; + } + let should_seed = (has_protected_predecessor + && has_finally_except_loop_tail + && !has_suppressing_with_resume_predecessor) + || (has_bool_guard_tail + && has_handler_resume_predecessor + && !handler_resume_predecessor_stores_guard + && !is_plain_protected_resume_successor + && !is_loop_header + && tail_has_for_loop_back( + &self.blocks, + &predecessors, + BlockIdx::new(idx as u32), + )) + || has_handler_resume_loop_tail; + let allow_any_loop_back = + has_finally_except_loop_tail || has_handler_resume_loop_tail; + should_seed.then_some(( + BlockIdx::new(idx as u32), + has_handler_resume_loop_tail, + allow_any_loop_back, + )) + }) + .collect(); + + let mut visited = vec![false; self.blocks.len()]; + for (seed, include_join_tail, allow_any_loop_back) in seeds { + let mut segment = Vec::new(); + let mut found_loop_back = false; + let mut seen = vec![false; self.blocks.len()]; + let mut stack = vec![seed]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || seen[cursor.idx()] { + continue; + } + seen[cursor.idx()] = true; + let block = &self.blocks[cursor.idx()]; + if block_is_exceptional(block) { + continue; + } + segment.push(cursor); + if block_has_loop_back(block) { + found_loop_back |= allow_any_loop_back + || block_has_loop_back_to_or_before(&self.blocks, block, seed); + continue; + } + for successor in tail_successors(&self.blocks, &predecessors, cursor) { + stack.push(successor); + } + } + if !found_loop_back { + continue; + } + + let segment_ops: Vec<_> = segment + .iter() + .flat_map(|block_idx| { + self.blocks[block_idx.idx()] + .instructions + .iter() + .filter_map(|info| info.instr.real()) + }) + .collect(); + let has_call = segment_ops.iter().any(|instr| { + matches!(instr, Instruction::Call { .. } | Instruction::CallKw { .. }) + }); + let has_store_fast = segment_ops.iter().any(|instr| { + matches!( + instr, + Instruction::StoreFast { .. } + | Instruction::StoreFastLoadFast { .. } + | Instruction::StoreFastStoreFast { .. } + ) + }); + if !has_call || !has_store_fast { + continue; + } + + let mut in_segment = vec![false; self.blocks.len()]; + for block_idx in &segment { + in_segment[block_idx.idx()] = true; + } + + for block_idx in segment { + if visited[block_idx.idx()] { + continue; + } + if !include_join_tail + && block_idx != seed + && predecessors[block_idx.idx()] + .iter() + .any(|pred| !in_segment[pred.idx()] && !is_handler_resume_block[pred.idx()]) + { + continue; + } + if has_exception_group_match_handler + && block_has_for_iter(&self.blocks[block_idx.idx()]) + && block_has_protected_instructions(&self.blocks[block_idx.idx()]) + { + continue; + } + visited[block_idx.idx()] = true; + deoptimize_block_borrows(&mut self.blocks[block_idx.idx()]); + } + } + } + + fn deoptimize_borrow_after_protected_import(&mut self) { + fn deoptimize_borrow(info: &mut InstructionInfo) { + match info.instr.real() { + Some(Instruction::LoadFastBorrow { .. }) => { + info.instr = Instruction::LoadFast { + var_num: Arg::marker(), } - Some(Instruction::LoadFastBorrowLoadFastBorrow { var_nums }) => { - let (left, right) = var_nums.get(info.arg).indexes(); - let skip_left = protected_store_locals - .get(usize::from(left)) - .copied() - .unwrap_or(false); - let skip_right = protected_store_locals - .get(usize::from(right)) - .copied() - .unwrap_or(false); - if skip_left && skip_right { - continue; + .into(); + } + Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) => { + info.instr = Instruction::LoadFastLoadFast { + var_nums: Arg::marker(), + } + .into(); + } + _ => {} + } + } + + fn deoptimize_block_borrows_from(block: &mut Block, start: usize) { + for info in block.instructions.iter_mut().skip(start) { + deoptimize_borrow(info); + } + } + + fn deoptimize_protected_block_borrows_from( + block: &mut Block, + start: usize, + protected_store_locals: Option<&[bool]>, + ) { + for info in block.instructions.iter_mut().skip(start) { + if info.except_handler.is_none() { + break; + } + if let Some(protected_store_locals) = protected_store_locals { + match info.instr.real() { + Some(Instruction::LoadFastBorrow { var_num }) => { + let local = usize::from(var_num.get(info.arg)); + if protected_store_locals.get(local).copied().unwrap_or(false) { + continue; + } + } + Some(Instruction::LoadFastBorrowLoadFastBorrow { var_nums }) => { + let (left, right) = var_nums.get(info.arg).indexes(); + let skip_left = protected_store_locals + .get(usize::from(left)) + .copied() + .unwrap_or(false); + let skip_right = protected_store_locals + .get(usize::from(right)) + .copied() + .unwrap_or(false); + if skip_left && skip_right { + continue; + } } + _ => {} } - _ => {} } deoptimize_borrow(info); } @@ -7597,15 +9490,18 @@ impl CodeInfo { fn handler_chain_returns(blocks: &[Block], handler_block: BlockIdx) -> bool { let mut cursor = handler_block; let mut visited = vec![false; blocks.len()]; + let mut after_pop_except = false; while cursor != BlockIdx::NULL && !visited[cursor.idx()] { visited[cursor.idx()] = true; - let mut saw_pop_except = false; for info in &blocks[cursor.idx()].instructions { match info.instr.real() { - Some(Instruction::ReturnValue) => return true, - Some(Instruction::PopExcept) => saw_pop_except = true, + Some(Instruction::ReturnValue) if !info.no_location_exit => return true, + Some(Instruction::PopExcept) => after_pop_except = true, + Some(_) if after_pop_except && is_conditional_jump(&info.instr) => { + return false; + } Some(instr) - if saw_pop_except + if after_pop_except && (instr.is_unconditional_jump() || instr.is_scope_exit()) => { return false; @@ -7613,8 +9509,45 @@ impl CodeInfo { _ => {} } } - if saw_pop_except { - return false; + cursor = blocks[cursor.idx()].next; + } + false + } + + fn handler_chain_continues_to_or_before( + blocks: &[Block], + handler_block: BlockIdx, + seed: BlockIdx, + block_order: &[u32], + ) -> bool { + let mut cursor = handler_block; + let mut visited = vec![false; blocks.len()]; + let mut after_pop_except = false; + while cursor != BlockIdx::NULL && !visited[cursor.idx()] { + visited[cursor.idx()] = true; + for info in &blocks[cursor.idx()].instructions { + match info.instr.real() { + Some(Instruction::PopExcept) => after_pop_except = true, + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. }, + ) if after_pop_except + && info.target != BlockIdx::NULL + && block_order[info.target.idx()] <= block_order[seed.idx()] => + { + return true; + } + Some(_) if after_pop_except && is_conditional_jump(&info.instr) => { + return false; + } + Some(instr) + if after_pop_except + && (instr.is_unconditional_jump() || instr.is_scope_exit()) => + { + return false; + } + _ => {} + } } cursor = blocks[cursor.idx()].next; } @@ -7628,6 +9561,49 @@ impl CodeInfo { .any(|info| info.except_handler.is_some()) } + fn push_normal_successors(stack: &mut Vec, block: &Block) { + if block.next != BlockIdx::NULL { + stack.push(block.next); + } + for info in &block.instructions { + if info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + } + + fn has_nested_protected_import_tail( + blocks: &[Block], + seed: BlockIdx, + import_idx: usize, + ) -> bool { + let Some(import_handler) = blocks[seed.idx()].instructions[import_idx].except_handler + else { + return false; + }; + let mut cursor = seed; + let mut start = import_idx + 1; + while cursor != BlockIdx::NULL && !block_is_exceptional(&blocks[cursor.idx()]) { + let block = &blocks[cursor.idx()]; + let mut saw_protected = false; + for info in block.instructions.iter().skip(start) { + let Some(handler) = info.except_handler else { + return false; + }; + saw_protected = true; + if handler.handler_block != import_handler.handler_block { + return true; + } + } + if !saw_protected { + return false; + } + cursor = block.next; + start = 0; + } + false + } + let mut predecessors = vec![Vec::new(); self.blocks.len()]; for (pred_idx, block) in self.blocks.iter().enumerate() { if block.next != BlockIdx::NULL { @@ -7640,6 +9616,15 @@ impl CodeInfo { } } + let mut block_order = vec![u32::MAX; self.blocks.len()]; + let mut cursor = BlockIdx(0); + let mut pos = 0u32; + while cursor != BlockIdx::NULL { + block_order[cursor.idx()] = pos; + pos += 1; + cursor = self.blocks[cursor.idx()].next; + } + let seeds: Vec<_> = self.blocks .iter() @@ -7657,24 +9642,36 @@ impl CodeInfo { .is_some_and(|handler| { handler_chain_returns(&self.blocks, handler.handler_block) }); - if !handler_returns { + let handler_continues = block.instructions[import_idx] + .except_handler + .is_some_and(|handler| { + handler_chain_continues_to_or_before( + &self.blocks, + handler.handler_block, + BlockIdx::new(idx as u32), + &block_order, + ) + }); + let nested_protected_tail = has_nested_protected_import_tail( + &self.blocks, + BlockIdx::new(idx as u32), + import_idx, + ); + if !handler_returns && !handler_continues && !nested_protected_tail { return None; } - Some((BlockIdx::new(idx as u32), import_idx)) + Some(( + BlockIdx::new(idx as u32), + import_idx, + handler_returns, + handler_continues, + nested_protected_tail, + )) }) .collect(); - let mut block_order = vec![u32::MAX; self.blocks.len()]; - let mut cursor = BlockIdx(0); - let mut pos = 0u32; - while cursor != BlockIdx::NULL { - block_order[cursor.idx()] = pos; - pos += 1; - cursor = self.blocks[cursor.idx()].next; - } - let mut visited = vec![false; self.blocks.len()]; - for (seed, import_idx) in seeds { + for (seed, import_idx, handler_returns, handler_continues, nested_protected_tail) in seeds { let mut protected_store_locals = vec![false; self.metadata.varnames.len()]; for info in self.blocks[seed.idx()] .instructions @@ -7697,7 +9694,11 @@ impl CodeInfo { let mut segment = vec![(seed, import_idx + 1)]; let mut cursor = self.blocks[seed.idx()].next; while cursor != BlockIdx::NULL && !block_is_exceptional(&self.blocks[cursor.idx()]) { - if block_has_protected_instructions(&self.blocks[cursor.idx()]) { + if !handler_continues + && !handler_returns + && !nested_protected_tail + && block_has_protected_instructions(&self.blocks[cursor.idx()]) + { break; } if predecessors[cursor.idx()].iter().any(|pred| { @@ -7713,22 +9714,52 @@ impl CodeInfo { .instructions .iter() .any(|info| info.instr.real().is_some_and(|instr| instr.is_scope_exit())) + && !handler_returns + && !handler_continues { break; } cursor = self.blocks[cursor.idx()].next; } + if nested_protected_tail { + let mut stack = Vec::new(); + for (block_idx, _) in &segment { + push_normal_successors(&mut stack, &self.blocks[block_idx.idx()]); + } + while let Some(candidate) = stack.pop() { + if candidate == BlockIdx::NULL + || in_segment[candidate.idx()] + || block_is_exceptional(&self.blocks[candidate.idx()]) + || !block_has_protected_instructions(&self.blocks[candidate.idx()]) + { + continue; + } + if predecessors[candidate.idx()].iter().any(|pred| { + !in_segment[pred.idx()] + && block_order[pred.idx()] < block_order[candidate.idx()] + && !is_handler_resume_predecessor(&self.blocks[pred.idx()], candidate) + }) { + continue; + } + in_segment[candidate.idx()] = true; + segment.push((candidate, 0)); + push_normal_successors(&mut stack, &self.blocks[candidate.idx()]); + } + } + for (block_idx, start) in segment { if visited[block_idx.idx()] { continue; } visited[block_idx.idx()] = true; if block_idx == seed { + let protected_store_locals = + (!nested_protected_tail).then_some(protected_store_locals.as_slice()); deoptimize_protected_block_borrows_from( &mut self.blocks[block_idx.idx()], start, - &protected_store_locals, + protected_store_locals, ); } else { deoptimize_block_borrows_from(&mut self.blocks[block_idx.idx()], start); @@ -7855,6 +9886,88 @@ impl CodeInfo { false } + fn handler_chain_exits_loop_after_pop_except(blocks: &[Block], block: &Block) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .collect(); + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || visited[cursor.idx()] { + continue; + } + visited[cursor.idx()] = true; + let handler = &blocks[cursor.idx()]; + let mut after_pop_except = false; + for info in &handler.instructions { + if matches!(info.instr.real(), Some(Instruction::PopExcept)) { + after_pop_except = true; + continue; + } + if after_pop_except + && matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + { + return true; + } + if is_conditional_jump(&info.instr) && info.target != BlockIdx::NULL { + stack.push(info.target); + } + if info.instr.is_unconditional_jump() && info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + if handler.next != BlockIdx::NULL { + stack.push(handler.next); + } + } + false + } + + fn handler_chain_has_nested_exception_match(blocks: &[Block], block: &Block) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .collect(); + let mut matches_seen = 0; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || visited[cursor.idx()] { + continue; + } + visited[cursor.idx()] = true; + let handler = &blocks[cursor.idx()]; + for info in &handler.instructions { + if matches!( + info.instr.real(), + Some(Instruction::CheckExcMatch | Instruction::CheckEgMatch) + ) { + matches_seen += 1; + if matches_seen > 1 { + return true; + } + } + if is_conditional_jump(&info.instr) && info.target != BlockIdx::NULL { + stack.push(info.target); + } + if info.instr.is_unconditional_jump() && info.target != BlockIdx::NULL { + stack.push(info.target); + } + } + if handler.next != BlockIdx::NULL { + stack.push(handler.next); + } + } + false + } + fn block_has_tail_deopt_trigger_from(block: &Block, start: usize) -> bool { block.instructions.iter().skip(start).any(|info| { matches!( @@ -7870,6 +9983,256 @@ impl CodeInfo { }) } + fn block_has_generator_delegation(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::GetYieldFromIter + | Instruction::Send { .. } + | Instruction::YieldValue { .. } + ) + ) + }) + } + + fn block_has_external_backward_jump(block: &Block, in_segment: &[bool]) -> bool { + block.instructions.iter().any(|info| { + let target_is_external = + info.target != BlockIdx::NULL && !in_segment[info.target.idx()]; + matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && target_is_external + }) + } + + fn block_has_for_iter(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::ForIter { .. }))) + } + + fn block_suffix_has_for_loop_back(block: &Block, blocks: &[Block], start: usize) -> bool { + block.instructions.iter().skip(start).any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) && info.target != BlockIdx::NULL + && block_has_for_iter(&blocks[info.target.idx()]) + }) + } + + fn block_has_for_loop_back(block: &Block, blocks: &[Block]) -> bool { + block_suffix_has_for_loop_back(block, blocks, 0) + } + + fn block_has_backward_jump(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some( + Instruction::JumpBackward { .. } + | Instruction::JumpBackwardNoInterrupt { .. } + ) + ) + }) + } + + fn normal_successors(block: &Block) -> Vec { + let Some(last) = block.instructions.last() else { + return (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect(); + }; + if last.instr.is_scope_exit() { + return Vec::new(); + } + if last.instr.is_unconditional_jump() { + return (last.target != BlockIdx::NULL) + .then_some(last.target) + .into_iter() + .collect(); + } + if let Some(cond_idx) = trailing_conditional_jump_index(block) { + let mut successors = Vec::with_capacity(2); + let target = block.instructions[cond_idx].target; + if target != BlockIdx::NULL { + successors.push(target); + } + if block.next != BlockIdx::NULL { + successors.push(block.next); + } + return successors; + } + (block.next != BlockIdx::NULL) + .then_some(block.next) + .into_iter() + .collect() + } + + fn segment_reaches_external_backward_jump( + blocks: &[Block], + segment: &[(BlockIdx, usize)], + in_segment: &[bool], + ) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = segment + .iter() + .map(|(block_idx, _)| *block_idx) + .collect::>(); + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block_has_external_backward_jump(block, in_segment) { + return true; + } + stack.extend(normal_successors(block)); + } + false + } + + fn normal_path_reaches_for_loop_back(blocks: &[Block], start: BlockIdx) -> bool { + let mut visited = vec![false; blocks.len()]; + let mut stack = vec![start]; + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block_is_exceptional(block) || block.cold { + continue; + } + if block_has_for_loop_back(block, blocks) { + return true; + } + stack.extend(normal_successors(block)); + } + false + } + + fn segment_has_for_loop_back(blocks: &[Block], segment: &[(BlockIdx, usize)]) -> bool { + segment + .iter() + .any(|(block_idx, _)| block_has_for_loop_back(&blocks[block_idx.idx()], blocks)) + } + + fn segment_has_backward_jump(blocks: &[Block], segment: &[(BlockIdx, usize)]) -> bool { + segment + .iter() + .any(|(block_idx, _)| block_has_backward_jump(&blocks[block_idx.idx()])) + } + + fn segment_has_yield_value(blocks: &[Block], segment: &[(BlockIdx, usize)]) -> bool { + segment.iter().any(|(block_idx, start)| { + blocks[block_idx.idx()] + .instructions + .iter() + .skip(*start) + .any(|info| matches!(info.instr.real(), Some(Instruction::YieldValue { .. }))) + }) + } + + fn block_suffix_starts_with_builtin_any_all_fast_path( + block: &Block, + names: &IndexSet, + start: usize, + ) -> bool { + block + .instructions + .iter() + .skip(start) + .find_map(|info| { + let instr = info.instr.real()?; + if matches!(instr, Instruction::Nop | Instruction::NotTaken) { + return None; + } + Some((instr, info.arg)) + }) + .is_some_and(|(instr, arg)| { + matches!( + instr, + Instruction::LoadGlobal { namei } + if names[usize::try_from(namei.get(arg) >> 1).unwrap()].as_str() + == "any" + || names[usize::try_from(namei.get(arg) >> 1).unwrap()].as_str() + == "all" + ) + }) + } + + fn segment_starts_with_builtin_any_all_fast_path( + blocks: &[Block], + names: &IndexSet, + segment: &[(BlockIdx, usize)], + ) -> bool { + let Some((block_idx, start)) = segment.first() else { + return false; + }; + block_suffix_starts_with_builtin_any_all_fast_path( + &blocks[block_idx.idx()], + names, + *start, + ) + } + + fn segment_has_named_except_cleanup_predecessor( + blocks: &[Block], + predecessors: &[Vec], + segment: &[(BlockIdx, usize)], + ) -> bool { + segment.iter().any(|(block_idx, _)| { + predecessors[block_idx.idx()] + .iter() + .any(|pred| is_named_except_cleanup_normal_exit_block(&blocks[pred.idx()])) + }) + } + + fn protected_store_subscr_operand_start(block: &Block) -> Option { + let store_idx = block.instructions.iter().position(|info| { + matches!(info.instr.real(), Some(Instruction::StoreSubscr)) + && info.except_handler.is_some() + })?; + + let mut stack_items = 0; + for start in (0..store_idx).rev() { + let produced = match block.instructions[start].instr.real() { + Some( + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadGlobal { .. } + | Instruction::LoadName { .. } + | Instruction::LoadDeref { .. }, + ) => 1, + Some( + Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. }, + ) => 2, + _ => return None, + }; + stack_items += produced; + if stack_items >= 3 { + return Some(start); + } + } + None + } + fn block_has_attr_named(block: &Block, names: &IndexSet, attr: &str) -> bool { block.instructions.iter().any(|info| { let raw = u32::from(info.arg) as usize; @@ -7912,28 +10275,181 @@ impl CodeInfo { return Some(idx); } } - None + None + } + + fn collect_stored_fast_locals_until(block: &Block, end: usize) -> Vec { + let mut locals = Vec::new(); + for info in block.instructions.iter().take(end) { + collect_stored_fast_local(info, &mut locals); + } + locals + } + + fn collect_protected_stored_fast_locals_until(block: &Block, end: usize) -> Vec { + let mut locals = Vec::new(); + for info in block + .instructions + .iter() + .take(end) + .filter(|info| info.except_handler.is_some()) + { + collect_stored_fast_local(info, &mut locals); + } + locals + } + + fn collect_stored_fast_local(info: &InstructionInfo, locals: &mut Vec) { + match info.instr.real() { + Some(Instruction::StoreFast { var_num }) => { + locals.push(usize::from(var_num.get(info.arg))); + } + Some(Instruction::StoreFastLoadFast { var_nums }) => { + let (store_idx, _) = var_nums.get(info.arg).indexes(); + locals.push(usize::from(store_idx)); + } + Some(Instruction::StoreFastStoreFast { var_nums }) => { + let (idx1, idx2) = var_nums.get(info.arg).indexes(); + locals.push(usize::from(idx1)); + locals.push(usize::from(idx2)); + } + _ => {} + } + } + + fn collect_borrowed_stored_locals_in_segment( + blocks: &[Block], + segment: &[(BlockIdx, usize)], + stored_locals: &[usize], + ) -> Vec { + let mut borrowed = Vec::new(); + for (block_idx, start) in segment { + for info in blocks[block_idx.idx()].instructions.iter().skip(*start) { + match info.instr.real() { + Some(Instruction::LoadFastBorrow { var_num }) => { + let local = usize::from(var_num.get(info.arg)); + if stored_locals.contains(&local) { + borrowed.push(local); + } + } + Some(Instruction::LoadFastBorrowLoadFastBorrow { var_nums }) => { + let (left, right) = var_nums.get(info.arg).indexes(); + for local in [usize::from(left), usize::from(right)] { + if stored_locals.contains(&local) { + borrowed.push(local); + } + } + } + _ => {} + } + } + } + borrowed.sort_unstable(); + borrowed.dedup(); + borrowed + } + + fn handler_chain_resumes_after_assigning_locals( + blocks: &[Block], + block: &Block, + in_segment: &[bool], + locals: &[usize], + ) -> bool { + if locals.is_empty() { + return false; + } + + let mut visited = vec![false; blocks.len()]; + let handler_blocks: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.except_handler.map(|handler| handler.handler_block)) + .collect(); + for handler_block in handler_blocks { + let mut cursor = handler_block; + let mut assigned = Vec::new(); + while cursor != BlockIdx::NULL && !visited[cursor.idx()] { + visited[cursor.idx()] = true; + let handler = &blocks[cursor.idx()]; + let mut after_pop_except = false; + for info in &handler.instructions { + if !after_pop_except { + collect_stored_fast_local(info, &mut assigned); + } + if matches!(info.instr.real(), Some(Instruction::PopExcept)) { + after_pop_except = true; + continue; + } + if after_pop_except + && info.target != BlockIdx::NULL + && in_segment[info.target.idx()] + && info.instr.is_unconditional_jump() + { + assigned.sort_unstable(); + assigned.dedup(); + if locals.iter().all(|local| assigned.contains(local)) { + return true; + } + } + } + cursor = handler.next; + } + } + false + } + + fn block_is_normal_cleanup_call(block: &Block, metadata: &CodeUnitMetadata) -> bool { + if !block_has_fallthrough(block) { + return false; + } + let reals: Vec<_> = block + .instructions + .iter() + .filter(|info| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }) + .collect(); + let [.., none1, none2, none3, call, pop_top] = reals.as_slice() else { + return false; + }; + is_load_const_none(none1, metadata) + && is_load_const_none(none2, metadata) + && is_load_const_none(none3, metadata) + && matches!(call.instr.real(), Some(Instruction::Call { .. })) + && matches!(pop_top.instr.real(), Some(Instruction::PopTop)) + && collect_stored_fast_locals_until(block, block.instructions.len()).is_empty() } - fn collect_stored_fast_locals_until(block: &Block, end: usize) -> Vec { + fn collect_protected_predecessor_stored_fast_locals( + blocks: &[Block], + predecessors: &[Vec], + start: BlockIdx, + ) -> Vec { let mut locals = Vec::new(); - for info in block.instructions.iter().take(end) { - match info.instr.real() { - Some(Instruction::StoreFast { var_num }) => { - locals.push(usize::from(var_num.get(info.arg))); - } - Some(Instruction::StoreFastLoadFast { var_nums }) => { - let (store_idx, _) = var_nums.get(info.arg).indexes(); - locals.push(usize::from(store_idx)); - } - Some(Instruction::StoreFastStoreFast { var_nums }) => { - let (idx1, idx2) = var_nums.get(info.arg).indexes(); - locals.push(usize::from(idx1)); - locals.push(usize::from(idx2)); - } - _ => {} + let mut visited = vec![false; blocks.len()]; + let mut stack = predecessors[start.idx()].clone(); + while let Some(block_idx) = stack.pop() { + if block_idx == BlockIdx::NULL || visited[block_idx.idx()] { + continue; + } + visited[block_idx.idx()] = true; + let block = &blocks[block_idx.idx()]; + if block.cold + || block_is_exceptional(block) + || !block_has_protected_instructions(block) + { + continue; } + locals.extend(collect_protected_stored_fast_locals_until( + block, + block.instructions.len(), + )); + stack.extend(predecessors[block_idx.idx()].iter().copied()); } + locals.sort_unstable(); + locals.dedup(); locals } @@ -7954,10 +10470,56 @@ impl CodeInfo { }) } - fn starts_with_borrowed_local_bool_guard(block: &Block, locals: &[usize]) -> bool { + fn borrowed_inplace_local_update_start(block: &Block) -> Option { + for i in 0..block.instructions.len().saturating_sub(3) { + let local = match block.instructions[i].instr.real() { + Some(Instruction::LoadFastBorrow { var_num }) => { + usize::from(var_num.get(block.instructions[i].arg)) + } + _ => continue, + }; + let Some(Instruction::BinaryOp { op }) = block.instructions[i + 2].instr.real() + else { + continue; + }; + if !matches!( + op.get(block.instructions[i + 2].arg), + oparg::BinaryOperator::InplaceAdd + | oparg::BinaryOperator::InplaceSubtract + | oparg::BinaryOperator::InplaceMultiply + | oparg::BinaryOperator::InplaceMatrixMultiply + | oparg::BinaryOperator::InplaceTrueDivide + | oparg::BinaryOperator::InplaceFloorDivide + | oparg::BinaryOperator::InplaceRemainder + | oparg::BinaryOperator::InplacePower + | oparg::BinaryOperator::InplaceLshift + | oparg::BinaryOperator::InplaceRshift + | oparg::BinaryOperator::InplaceAnd + | oparg::BinaryOperator::InplaceXor + | oparg::BinaryOperator::InplaceOr + ) { + continue; + } + if matches!( + block.instructions[i + 3].instr.real(), + Some(Instruction::StoreFast { var_num }) + if usize::from(var_num.get(block.instructions[i + 3].arg)) == local + ) { + return Some(i); + } + } + None + } + + fn starts_with_borrowed_local_bool_guard_from( + block: &Block, + locals: &[usize], + start: usize, + ) -> bool { let mut reals = block .instructions .iter() + .skip(start) .filter(|info| { info.instr.real().is_some_and(|instr| { !matches!(instr, Instruction::Nop | Instruction::NotTaken) @@ -7987,6 +10549,71 @@ impl CodeInfo { ) } + fn starts_with_borrowed_local_bool_guard(block: &Block, locals: &[usize]) -> bool { + starts_with_borrowed_local_bool_guard_from(block, locals, 0) + } + + fn protected_store_bool_guard_start(block: &Block) -> Option<(usize, Vec)> { + let mut saw_call = false; + for store_idx in 0..block.instructions.len() { + saw_call |= matches!( + block.instructions[store_idx].instr.real(), + Some( + Instruction::Call { .. } + | Instruction::CallKw { .. } + | Instruction::CallFunctionEx + ) + ); + if !saw_call { + continue; + } + let local = match block.instructions[store_idx].instr.real() { + Some(Instruction::StoreFast { var_num }) => { + usize::from(var_num.get(block.instructions[store_idx].arg)) + } + _ => continue, + }; + let mut reals = block + .instructions + .iter() + .enumerate() + .skip(store_idx + 1) + .filter(|(_, info)| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }); + let Some((load_idx, load)) = reals.next() else { + continue; + }; + let borrows_stored_local = matches!( + load.instr.real(), + Some(Instruction::LoadFastBorrow { var_num }) + if usize::from(var_num.get(load.arg)) == local + ); + if !borrows_stored_local { + continue; + } + let Some((_, second)) = reals.next() else { + continue; + }; + let Some((_, third)) = reals.next() else { + continue; + }; + if matches!(second.instr.real(), Some(Instruction::ToBool)) + && matches!( + third.instr.real(), + Some( + Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. } + ) + ) + { + return Some((load_idx, vec![local])); + } + } + None + } + fn conditional_target(block: &Block) -> Option { block .instructions @@ -8006,62 +10633,317 @@ impl CodeInfo { }) } - fn contains_debug_four_guard(block: &Block, names: &IndexSet) -> bool { - let reals: Vec<_> = block - .instructions - .iter() - .filter(|info| { - info.instr.real().is_some_and(|instr| { - !matches!(instr, Instruction::Nop | Instruction::NotTaken) - }) - }) - .collect(); - if reals.len() < 5 { - return false; + fn contains_debug_four_guard(block: &Block, names: &IndexSet) -> bool { + let reals: Vec<_> = block + .instructions + .iter() + .filter(|info| { + info.instr.real().is_some_and(|instr| { + !matches!(instr, Instruction::Nop | Instruction::NotTaken) + }) + }) + .collect(); + if reals.len() < 5 { + return false; + } + reals.windows(5).any(|window| { + let loads_debug_attr = window.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::LoadAttr { namei }) + if names[usize::try_from(namei.get(info.arg).name_idx()).unwrap()].as_str() + == "debug" + ) + }); + let compares_with_four = window.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::LoadSmallInt { i }) if i.get(info.arg) == 4 + ) + }) && window + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::CompareOp { .. }))); + let has_conditional = window.iter().any(|info| is_conditional_jump(&info.instr)); + loads_debug_attr && compares_with_four && has_conditional + }) + } + + fn marker_only_block(block: &Block) -> bool { + block.instructions.iter().all(|info| { + info.instr + .real() + .is_none_or(|instr| matches!(instr, Instruction::Nop | Instruction::NotTaken)) + }) + } + + fn predecessor_chain_contains_debug_four_guard( + blocks: &[Block], + predecessors: &[Vec], + block_idx: BlockIdx, + names: &IndexSet, + ) -> bool { + predecessors[block_idx.idx()].iter().any(|pred| { + contains_debug_four_guard(&blocks[pred.idx()], names) + || (marker_only_block(&blocks[pred.idx()]) + && predecessors[pred.idx()].iter().any(|pred_pred| { + contains_debug_four_guard(&blocks[pred_pred.idx()], names) + })) + }) + } + + fn collect_unprotected_tail_segment( + blocks: &[Block], + tail: BlockIdx, + ) -> (Vec<(BlockIdx, usize)>, Vec) { + let mut in_segment = vec![false; blocks.len()]; + let mut segment = Vec::new(); + let mut cursor = tail; + if cursor == BlockIdx::NULL + || block_is_exceptional(&blocks[cursor.idx()]) + || blocks[cursor.idx()].try_else_orelse_entry + || block_has_protected_instructions(&blocks[cursor.idx()]) + { + return (segment, in_segment); + } + while cursor != BlockIdx::NULL { + let segment_block = &blocks[cursor.idx()]; + if block_is_exceptional(segment_block) + || segment_block.try_else_orelse_entry + || block_has_protected_instructions(segment_block) + { + break; + } + segment.push((cursor, 0)); + in_segment[cursor.idx()] = true; + let last_real = segment_block + .instructions + .iter() + .rev() + .find_map(|info| info.instr.real()); + if last_real.is_some_and(|instr| { + instr.is_scope_exit() || AnyInstruction::Real(instr).is_unconditional_jump() + }) { + break; + } + cursor = next_nonempty_block(blocks, segment_block.next); + } + (segment, in_segment) + } + + fn collect_unprotected_tail_region( + blocks: &[Block], + tail: BlockIdx, + ) -> (Vec<(BlockIdx, usize)>, Vec) { + let mut in_segment = vec![false; blocks.len()]; + let mut segment = Vec::new(); + let mut stack = vec![tail]; + while let Some(cursor) = stack.pop() { + if cursor == BlockIdx::NULL || in_segment[cursor.idx()] { + continue; + } + let segment_block = &blocks[cursor.idx()]; + if block_is_exceptional(segment_block) || segment_block.try_else_orelse_entry { + continue; + } + segment.push((cursor, 0)); + in_segment[cursor.idx()] = true; + let last_real = segment_block + .instructions + .iter() + .rev() + .find_map(|info| info.instr.real()); + if last_real.is_some_and(|instr| { + instr.is_scope_exit() || AnyInstruction::Real(instr).is_unconditional_jump() + }) { + continue; + } + stack.extend(normal_successors(segment_block)); + } + (segment, in_segment) + } + + fn is_plain_protected_resume_successor( + blocks: &[Block], + predecessors: &[Vec], + target: BlockIdx, + ) -> bool { + if target == BlockIdx::NULL { + return false; + } + let mut has_handler_resume_predecessor = false; + let mut has_normal_fallthrough_predecessor = false; + for pred in &predecessors[target.idx()] { + let pred_block = &blocks[pred.idx()]; + if is_handler_resume_predecessor(pred_block, target) { + has_handler_resume_predecessor = true; + continue; + } + if block_is_exceptional(pred_block) || pred_block.cold { + continue; + } + if next_nonempty_block(blocks, pred_block.next) == target { + has_normal_fallthrough_predecessor = true; + continue; + } + return false; + } + has_handler_resume_predecessor && has_normal_fallthrough_predecessor + } + + fn protected_call_store_return_load_start(block: &Block) -> Option { + let mut saw_call = false; + let end = block + .instructions + .iter() + .position(|info| matches!(info.instr.real(), Some(Instruction::PushExcInfo))) + .unwrap_or(block.instructions.len()); + for idx in 0..end.saturating_sub(2) { + match block.instructions[idx].instr.real() { + Some( + Instruction::Call { .. } + | Instruction::CallKw { .. } + | Instruction::CallFunctionEx, + ) => { + saw_call = true; + continue; + } + Some(Instruction::StoreFast { var_num }) if saw_call => { + let stored = usize::from(var_num.get(block.instructions[idx].arg)); + let loaded = match block.instructions[idx + 1].instr.real() { + Some(Instruction::LoadFastBorrow { var_num }) => { + usize::from(var_num.get(block.instructions[idx + 1].arg)) + } + _ => continue, + }; + if stored == loaded + && matches!( + block.instructions[idx + 2].instr.real(), + Some(Instruction::ReturnValue) + ) + { + return Some(idx + 1); + } + } + _ => {} + } + } + None + } + + fn block_has_exception_match_trailer(block: &Block) -> bool { + let mut saw_push_exc_info = false; + for info in &block.instructions { + match info.instr.real() { + Some(Instruction::PushExcInfo) => { + saw_push_exc_info = true; + } + Some(Instruction::CheckExcMatch | Instruction::CheckEgMatch) + if saw_push_exc_info => + { + return true; + } + _ => {} + } + } + false + } + + fn block_starts_with_borrowed_local_return(block: &Block) -> Option<(usize, usize)> { + let mut reals = block.instructions.iter().enumerate().filter(|(_, info)| { + info.instr + .real() + .is_some_and(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + }); + let (load_idx, load) = reals.next()?; + let local = match load.instr.real() { + Some(Instruction::LoadFastBorrow { var_num }) => usize::from(var_num.get(load.arg)), + _ => return None, + }; + let (_, ret) = reals.next()?; + if matches!(ret.instr.real(), Some(Instruction::ReturnValue)) { + Some((load_idx, local)) + } else { + None } - reals.windows(5).any(|window| { - let loads_debug_attr = window.iter().any(|info| { - matches!( - info.instr.real(), - Some(Instruction::LoadAttr { namei }) - if names[usize::try_from(namei.get(info.arg).name_idx()).unwrap()].as_str() - == "debug" - ) - }); - let compares_with_four = window.iter().any(|info| { + } + + fn block_is_exception_match_entry(block: &Block) -> bool { + block.cold + && block.instructions.iter().any(|info| { matches!( info.instr.real(), - Some(Instruction::LoadSmallInt { i }) if i.get(info.arg) == 4 + Some(Instruction::CheckExcMatch | Instruction::CheckEgMatch) ) - }) && window - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::CompareOp { .. }))); - let has_conditional = window.iter().any(|info| is_conditional_jump(&info.instr)); - loads_debug_attr && compares_with_four && has_conditional - }) + }) } - fn marker_only_block(block: &Block) -> bool { - block.instructions.iter().all(|info| { - info.instr - .real() - .is_none_or(|instr| matches!(instr, Instruction::Nop | Instruction::NotTaken)) - }) + fn cold_layout_tail_reaches_exception_match_entry( + blocks: &[Block], + start: BlockIdx, + ) -> bool { + let mut cursor = start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && !visited[cursor.idx()] { + visited[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + if !block.cold { + return false; + } + if block_is_exception_match_entry(block) { + return true; + } + cursor = block.next; + } + false } - fn predecessor_chain_contains_debug_four_guard( + fn protected_call_store_local_predecessor(block: &Block, local: usize) -> bool { + if !block.disable_load_fast_borrow { + return false; + } + let mut saw_call = false; + for info in &block.instructions { + match info.instr.real() { + Some( + Instruction::Call { .. } + | Instruction::CallKw { .. } + | Instruction::CallFunctionEx, + ) => { + saw_call = true; + } + Some(Instruction::StoreFast { var_num }) + if saw_call && usize::from(var_num.get(info.arg)) == local => + { + return true; + } + _ => {} + } + } + false + } + + fn predecessor_chain_has_protected_call_store_local( blocks: &[Block], predecessors: &[Vec], - block_idx: BlockIdx, - names: &IndexSet, + target: BlockIdx, + local: usize, ) -> bool { - predecessors[block_idx.idx()].iter().any(|pred| { - contains_debug_four_guard(&blocks[pred.idx()], names) - || (marker_only_block(&blocks[pred.idx()]) - && predecessors[pred.idx()].iter().any(|pred_pred| { - contains_debug_four_guard(&blocks[pred_pred.idx()], names) - })) - }) + let mut visited = vec![false; blocks.len()]; + let mut stack = predecessors[target.idx()].clone(); + while let Some(pred) = stack.pop() { + if pred == BlockIdx::NULL || visited[pred.idx()] { + continue; + } + visited[pred.idx()] = true; + let block = &blocks[pred.idx()]; + if protected_call_store_local_predecessor(block, local) { + return true; + } + if marker_only_block(block) { + stack.extend(predecessors[pred.idx()].iter().copied()); + } + } + false } let mut predecessors = vec![Vec::new(); self.blocks.len()]; @@ -8077,7 +10959,47 @@ impl CodeInfo { } let mut to_deopt = Vec::new(); - for block in &self.blocks { + let has_exception_match_handler = self.blocks.iter().any(|block| { + block.instructions.iter().any(|info| { + matches!( + info.instr.real(), + Some(Instruction::CheckExcMatch | Instruction::CheckEgMatch) + ) + }) + }); + if has_exception_match_handler { + for (block_idx, block) in self.blocks.iter().enumerate() { + if block_has_protected_instructions(block) + && let Some(start) = protected_store_subscr_operand_start(block) + { + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + } + } + } + for (block_idx, block) in self.blocks.iter().enumerate() { + if block_has_exception_match_trailer(block) + && let Some(start) = protected_call_store_return_load_start(block) + { + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + } + } + for (block_idx, block) in self.blocks.iter().enumerate() { + if !cold_layout_tail_reaches_exception_match_entry(&self.blocks, block.next) { + continue; + } + let Some((start, local)) = block_starts_with_borrowed_local_return(block) else { + continue; + }; + if predecessor_chain_has_protected_call_store_local( + &self.blocks, + &predecessors, + BlockIdx::new(block_idx as u32), + local, + ) { + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + } + } + for (block_idx, block) in self.blocks.iter().enumerate() { if block_is_exceptional(block) || !block .instructions @@ -8093,23 +11015,179 @@ impl CodeInfo { ) ) }) + || block_has_generator_delegation(block) || !block_has_exception_match_handler(&self.blocks, block) { continue; } + if let Some(start) = protected_store_subscr_operand_start(block) { + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + continue; + } + if let Some((start, stored_locals)) = protected_store_bool_guard_start(block) { + if block_suffix_has_for_loop_back(block, &self.blocks, start) + && !block_suffix_starts_with_builtin_any_all_fast_path( + block, + &self.metadata.names, + start, + ) + { + continue; + } + if !handler_chain_exits_loop_after_pop_except(&self.blocks, block) { + continue; + } + let handler_has_explicit_raise = + handler_chain_has_explicit_raise(&self.blocks, block); + let jump_target = conditional_target(block); + let fallthrough = next_nonempty_block(&self.blocks, block.next); + if !handler_has_explicit_raise && let Some(jump_target) = jump_target { + let branches = [(jump_target, fallthrough), (fallthrough, jump_target)]; + for (work, exit) in branches { + if work == BlockIdx::NULL || exit == BlockIdx::NULL { + continue; + } + let work_block = &self.blocks[work.idx()]; + let exit_block = &self.blocks[exit.idx()]; + if !block_is_exceptional(work_block) + && !block_has_protected_instructions(work_block) + && block_has_tail_deopt_trigger_from(work_block, 0) + && block_is_simple_exit_branch(exit_block) + { + if normal_path_reaches_for_loop_back(&self.blocks, work) { + continue; + } + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + if borrows_any_local_from(work_block, &stored_locals, 0) { + to_deopt.push((work, 0)); + } + } + } + } + } let same_block_tail_start = first_unprotected_suffix(block); - if same_block_tail_start.is_some() { + if let Some(start) = same_block_tail_start { + if block.try_else_orelse_entry { + continue; + } + if block_suffix_has_for_loop_back(block, &self.blocks, start) + && !block_suffix_starts_with_builtin_any_all_fast_path( + block, + &self.metadata.names, + start, + ) + { + continue; + } + let stored_locals = collect_protected_stored_fast_locals_until(block, start); + let handler_has_explicit_raise = + handler_chain_has_explicit_raise(&self.blocks, block); + if stored_locals.is_empty() + || handler_has_explicit_raise + || !starts_with_borrowed_local_bool_guard_from(block, &stored_locals, start) + { + continue; + } + let jump_target = conditional_target(block); + let fallthrough = next_nonempty_block(&self.blocks, block.next); + if let Some(jump_target) = jump_target { + let branches = [(jump_target, fallthrough), (fallthrough, jump_target)]; + for (work, exit) in branches { + if work == BlockIdx::NULL || exit == BlockIdx::NULL { + continue; + } + let work_block = &self.blocks[work.idx()]; + let exit_block = &self.blocks[exit.idx()]; + if !block_is_exceptional(work_block) + && !block_has_protected_instructions(work_block) + && block_has_tail_deopt_trigger_from(work_block, 0) + && block_is_simple_exit_branch(exit_block) + { + if normal_path_reaches_for_loop_back(&self.blocks, work) { + continue; + } + to_deopt.push((BlockIdx::new(block_idx as u32), start)); + if borrows_any_local_from(work_block, &stored_locals, 0) { + to_deopt.push((work, 0)); + } + } + } + } continue; } - let stored_locals = collect_stored_fast_locals_until(block, block.instructions.len()); + let tail = next_nonempty_block(&self.blocks, block.next); + let (segment, in_segment) = collect_unprotected_tail_segment(&self.blocks, tail); + let handler_has_nested_exception_match = + handler_chain_has_nested_exception_match(&self.blocks, block); + let linear_segment_reaches_external_backward_jump = + segment_reaches_external_backward_jump(&self.blocks, &segment, &in_segment); + let linear_segment_has_for_loop_back = + segment_has_for_loop_back(&self.blocks, &segment); + let linear_segment_has_backward_jump = + segment_has_backward_jump(&self.blocks, &segment); + let linear_segment_starts_with_builtin_any_all_fast_path = + segment_starts_with_builtin_any_all_fast_path( + &self.blocks, + &self.metadata.names, + &segment, + ); + if handler_has_nested_exception_match + && handler_chain_can_resume_to_segment(&self.blocks, block, &in_segment) + { + for (block_idx, start) in &segment { + if let Some(update_start) = + borrowed_inplace_local_update_start(&self.blocks[block_idx.idx()]) + { + to_deopt.push((*block_idx, (*start).max(update_start))); + } + } + } + let segment_has_yield = segment_has_yield_value(&self.blocks, &segment); + let mut stored_locals = + collect_protected_stored_fast_locals_until(block, block.instructions.len()); + if stored_locals.is_empty() && segment_has_yield { + stored_locals = collect_protected_predecessor_stored_fast_locals( + &self.blocks, + &predecessors, + BlockIdx::new(block_idx as u32), + ); + } if stored_locals.is_empty() { continue; } + let borrowed_stored_locals = + collect_borrowed_stored_locals_in_segment(&self.blocks, &segment, &stored_locals); + if !handler_has_nested_exception_match + && handler_chain_resumes_after_assigning_locals( + &self.blocks, + block, + &in_segment, + &borrowed_stored_locals, + ) + { + continue; + } let handler_has_explicit_raise = handler_chain_has_explicit_raise(&self.blocks, block); - let tail = next_nonempty_block(&self.blocks, block.next); + if !handler_has_explicit_raise + && segment_has_yield + && segment.iter().any(|(block_idx, start)| { + block_has_tail_deopt_trigger_from(&self.blocks[block_idx.idx()], *start) + }) + && segment.iter().any(|(block_idx, start)| { + borrows_any_local_from(&self.blocks[block_idx.idx()], &stored_locals, *start) + }) + { + let (yield_tail_region, _) = collect_unprotected_tail_region(&self.blocks, tail); + for (block_idx, start) in yield_tail_region { + to_deopt.push((block_idx, start)); + } + continue; + } if tail != BlockIdx::NULL && !block_is_exceptional(&self.blocks[tail.idx()]) + && !self.blocks[tail.idx()].try_else_orelse_entry && !block_has_protected_instructions(&self.blocks[tail.idx()]) + && !is_plain_protected_resume_successor(&self.blocks, &predecessors, tail) && starts_with_borrowed_local_bool_guard(&self.blocks[tail.idx()], &stored_locals) && !handler_has_explicit_raise { @@ -8126,47 +11204,19 @@ impl CodeInfo { if !block_is_exceptional(work_block) && !block_has_protected_instructions(work_block) && block_has_tail_deopt_trigger_from(work_block, 0) - && borrows_any_local_from(work_block, &stored_locals, 0) && block_is_simple_exit_branch(exit_block) { + if normal_path_reaches_for_loop_back(&self.blocks, work) { + continue; + } to_deopt.push((tail, 0)); - to_deopt.push((work, 0)); + if borrows_any_local_from(work_block, &stored_locals, 0) { + to_deopt.push((work, 0)); + } } } } } - let mut in_segment = vec![false; self.blocks.len()]; - let mut segment = Vec::new(); - let mut cursor = { - if tail == BlockIdx::NULL - || block_is_exceptional(&self.blocks[tail.idx()]) - || block_has_protected_instructions(&self.blocks[tail.idx()]) - { - continue; - } - tail - }; - while cursor != BlockIdx::NULL { - let segment_block = &self.blocks[cursor.idx()]; - if block_is_exceptional(segment_block) - || block_has_protected_instructions(segment_block) - { - break; - } - segment.push((cursor, 0)); - in_segment[cursor.idx()] = true; - let last_real = segment_block - .instructions - .iter() - .rev() - .find_map(|info| info.instr.real()); - if last_real.is_some_and(|instr| { - instr.is_scope_exit() || AnyInstruction::Real(instr).is_unconditional_jump() - }) { - break; - } - cursor = next_nonempty_block(&self.blocks, segment_block.next); - } if segment.is_empty() || segment.iter().any(|(block_idx, _)| { contains_debug_four_guard(&self.blocks[block_idx.idx()], &self.metadata.names) @@ -8180,7 +11230,17 @@ impl CodeInfo { || !segment.iter().any(|(block_idx, start)| { block_has_tail_deopt_trigger_from(&self.blocks[block_idx.idx()], *start) }) - || handler_chain_can_resume_to_segment(&self.blocks, block, &in_segment) + || segment_has_named_except_cleanup_predecessor( + &self.blocks, + &predecessors, + &segment, + ) + || (linear_segment_has_for_loop_back + && !linear_segment_starts_with_builtin_any_all_fast_path) + || (handler_chain_can_resume_to_segment(&self.blocks, block, &in_segment) + && (linear_segment_reaches_external_backward_jump + || (!handler_has_nested_exception_match + && linear_segment_has_backward_jump))) || segment.iter().any(|(block_idx, _)| { predecessors[block_idx.idx()].iter().any(|pred| { let pred_block = &self.blocks[pred.idx()]; @@ -8197,10 +11257,33 @@ impl CodeInfo { { continue; } - for (block_idx, start) in segment { - if predecessors[block_idx.idx()] - .iter() - .any(|pred| is_handler_resume_predecessor(&self.blocks[pred.idx()], block_idx)) + let (deopt_segment, deopt_in_segment) = if handler_has_nested_exception_match + && !linear_segment_reaches_external_backward_jump + { + collect_unprotected_tail_region(&self.blocks, tail) + } else { + (segment, in_segment) + }; + if segment_has_for_loop_back(&self.blocks, &deopt_segment) + && !segment_starts_with_builtin_any_all_fast_path( + &self.blocks, + &self.metadata.names, + &deopt_segment, + ) + { + continue; + } + let deopt_segment_reaches_external_backward_jump = + segment_reaches_external_backward_jump( + &self.blocks, + &deopt_segment, + &deopt_in_segment, + ); + for (block_idx, start) in deopt_segment { + if deopt_segment_reaches_external_backward_jump + && predecessors[block_idx.idx()].iter().any(|pred| { + is_handler_resume_predecessor(&self.blocks[pred.idx()], block_idx) + }) { continue; } @@ -8282,10 +11365,41 @@ impl CodeInfo { ) }); if tail_jumps_back_to_target { + if normal_path_reaches_for_loop_back(&self.blocks, tail) + && !block_suffix_starts_with_builtin_any_all_fast_path( + &self.blocks[tail.idx()], + &self.metadata.names, + 0, + ) + { + continue; + } to_deopt.push((tail, 0)); } } + for block in &self.blocks { + if block.cold + || block_is_exceptional(block) + || !block_is_normal_cleanup_call(block, &self.metadata) + { + continue; + } + let tail = next_nonempty_block(&self.blocks, block.next); + let (region, _) = collect_unprotected_tail_region(&self.blocks, tail); + if region.is_empty() + || !segment_has_yield_value(&self.blocks, ®ion) + || !region.iter().any(|(block_idx, start)| { + block_has_tail_deopt_trigger_from(&self.blocks[block_idx.idx()], *start) + }) + { + continue; + } + for (block_idx, start) in region { + to_deopt.push((block_idx, start)); + } + } + to_deopt.sort_by_key(|(idx, start)| (idx.idx(), *start)); let mut merged: Vec<(BlockIdx, usize)> = Vec::new(); for (idx, start) in to_deopt { @@ -8399,16 +11513,32 @@ impl CodeInfo { if starts_with_named_except_value_load { continue; } - if let Some(info) = self.blocks[block_idx].instructions.iter_mut().find(|info| { + let first_real = self.blocks[block_idx].instructions.iter().position(|info| { info.instr .real() .is_some_and(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) - }) && matches!(info.instr.real(), Some(Instruction::LoadFast { .. })) - { - info.instr = Instruction::LoadFastBorrow { - var_num: Arg::marker(), + }); + if let Some(first_real) = first_real { + let starts_with_receiver_load = matches!( + ( + self.blocks[block_idx].instructions[first_real].instr.real(), + self.blocks[block_idx] + .instructions + .get(first_real + 1) + .and_then(|info| info.instr.real()), + ), + ( + Some(Instruction::LoadFast { .. }), + Some(Instruction::LoadAttr { .. } | Instruction::LoadSuperAttr { .. }) + ) + ); + if starts_with_receiver_load { + self.blocks[block_idx].instructions[first_real].instr = + Instruction::LoadFastBorrow { + var_num: Arg::marker(), + } + .into(); } - .into(); } } } @@ -8427,16 +11557,37 @@ impl CodeInfo { } for (block_idx, block) in self.blocks.iter_mut().enumerate() { - if predecessor_count[block_idx] < 2 - || !block - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::ImportName { .. }))) - { + if predecessor_count[block_idx] < 2 { continue; } let len = block.instructions.len(); + let first_import_from = block + .instructions + .iter() + .position(|info| matches!(info.instr.real(), Some(Instruction::ImportFrom { .. }))); + if let Some(first_import_from) = first_import_from { + for idx in 0..first_import_from { + if matches!( + block.instructions[idx].instr.real(), + Some(Instruction::LoadFastBorrow { .. }) + ) { + block.instructions[idx].instr = Instruction::LoadFast { + var_num: Arg::marker(), + } + .into(); + } + } + } + + if !block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::ImportName { .. }))) + { + continue; + } + for idx in 0..len.saturating_sub(1) { if !matches!( block.instructions[idx].instr.real(), @@ -8905,6 +12056,11 @@ impl CodeInfo { continue; }; if let DeoptKind::ReturnIter { tail_start_idx } = deopt_kind { + if !(reachable_from_protected_predecessor[block_idx.idx()] + || is_protected_source[block_idx.idx()]) + { + continue; + } let tail_instr_idx = real_instrs .get(tail_start_idx) .map_or(block_instr_len, |(instr_idx, _)| *instr_idx); @@ -9137,18 +12293,217 @@ impl CodeInfo { } } + fn fast_scan_many_locals( + &mut self, + nlocals: usize, + nparams: usize, + merged_cell_local: &impl Fn(usize) -> Option, + ) { + const PARAM_INITIALIZED: usize = usize::MAX; + + debug_assert!(nlocals > 64); + let mut states = vec![0usize; nlocals - 64]; + let high_params = nparams.saturating_sub(64).min(states.len()); + for state in states.iter_mut().take(high_params) { + *state = PARAM_INITIALIZED; + } + + let is_known = |idx: usize, state: usize, blocknum: usize| { + state == blocknum || (idx < nparams && state == PARAM_INITIALIZED) + }; + + let mut blocknum = 0usize; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + blocknum += 1; + let old_instructions = self.blocks[current.idx()].instructions.clone(); + let mut new_instructions = Vec::with_capacity(old_instructions.len()); + let mut changed = false; + + for mut info in old_instructions { + match info.instr.real() { + Some( + Instruction::DeleteFast { var_num } + | Instruction::LoadFastAndClear { var_num }, + ) => { + let idx = usize::from(var_num.get(info.arg)); + if idx >= 64 && idx < nlocals { + states[idx - 64] = blocknum - 1; + } + new_instructions.push(info); + } + None if matches!( + info.instr.pseudo(), + Some(PseudoInstruction::StoreFastMaybeNull { .. }) + ) => + { + let Some(PseudoInstruction::StoreFastMaybeNull { var_num }) = + info.instr.pseudo() + else { + unreachable!(); + }; + let idx = var_num.get(info.arg) as usize; + if idx >= 64 && idx < nlocals { + states[idx - 64] = blocknum - 1; + } + new_instructions.push(info); + } + Some(Instruction::DeleteDeref { i }) => { + let cell_relative = usize::from(i.get(info.arg)); + if let Some(idx) = merged_cell_local(cell_relative) + && idx >= 64 + && idx < nlocals + { + states[idx - 64] = blocknum - 1; + } + new_instructions.push(info); + } + Some(Instruction::StoreFast { var_num }) => { + let idx = usize::from(var_num.get(info.arg)); + if idx >= 64 && idx < nlocals { + states[idx - 64] = blocknum; + } + new_instructions.push(info); + } + Some(Instruction::StoreDeref { i }) => { + let cell_relative = usize::from(i.get(info.arg)); + if let Some(idx) = merged_cell_local(cell_relative) + && idx >= 64 + && idx < nlocals + { + states[idx - 64] = blocknum; + } + new_instructions.push(info); + } + Some(Instruction::StoreFastStoreFast { var_nums }) => { + let packed = var_nums.get(info.arg); + let (idx1, idx2) = packed.indexes(); + let idx1 = usize::from(idx1); + let idx2 = usize::from(idx2); + if idx1 >= 64 && idx1 < nlocals { + states[idx1 - 64] = blocknum; + } + if idx2 >= 64 && idx2 < nlocals { + states[idx2 - 64] = blocknum; + } + new_instructions.push(info); + } + Some(Instruction::StoreFastLoadFast { var_nums }) => { + let packed = var_nums.get(info.arg); + let (store_idx, load_idx) = packed.indexes(); + let store_idx = usize::from(store_idx); + let load_idx = usize::from(load_idx); + if store_idx >= 64 && store_idx < nlocals { + states[store_idx - 64] = blocknum; + } + if load_idx >= 64 && load_idx < nlocals { + if !is_known(load_idx, states[load_idx - 64], blocknum) { + let mut first = info; + first.instr = Instruction::StoreFast { + var_num: Arg::marker(), + } + .into(); + first.arg = OpArg::new(store_idx as u32); + + let mut second = info; + second.instr = Opcode::LoadFastCheck.into(); + second.arg = OpArg::new(load_idx as u32); + + new_instructions.push(first); + new_instructions.push(second); + changed = true; + } else { + new_instructions.push(info); + } + } else { + new_instructions.push(info); + } + } + Some(Instruction::LoadFast { var_num }) => { + let idx = usize::from(var_num.get(info.arg)); + if idx >= 64 && idx < nlocals && !is_known(idx, states[idx - 64], blocknum) + { + info.instr = Opcode::LoadFastCheck.into(); + states[idx - 64] = blocknum; + changed = true; + } + new_instructions.push(info); + } + Some(Instruction::LoadFastLoadFast { var_nums }) => { + let packed = var_nums.get(info.arg); + let (idx1, idx2) = packed.indexes(); + let idx1 = usize::from(idx1); + let idx2 = usize::from(idx2); + let needs_check_1 = idx1 >= 64 + && idx1 < nlocals + && !is_known(idx1, states[idx1 - 64], blocknum); + if needs_check_1 { + states[idx1 - 64] = blocknum; + } + let needs_check_2 = idx2 >= 64 + && idx2 < nlocals + && !is_known(idx2, states[idx2 - 64], blocknum); + + if needs_check_1 || needs_check_2 { + let mut first = info; + first.instr = if needs_check_1 { + Opcode::LoadFastCheck + } else { + Opcode::LoadFast + } + .into(); + first.arg = OpArg::new(idx1 as u32); + + let mut second = info; + second.instr = if needs_check_2 { + Opcode::LoadFastCheck.into() + } else { + Opcode::LoadFast.into() + }; + second.arg = OpArg::new(idx2 as u32); + + new_instructions.push(first); + new_instructions.push(second); + changed = true; + if needs_check_2 { + states[idx2 - 64] = blocknum; + } + } else { + new_instructions.push(info); + } + } + Some(Instruction::LoadFastCheck { var_num }) => { + let idx = usize::from(var_num.get(info.arg)); + if idx >= 64 && idx < nlocals { + states[idx - 64] = blocknum; + } + new_instructions.push(info); + } + _ => new_instructions.push(info), + } + } + + if changed { + self.blocks[current.idx()].instructions = new_instructions; + } + current = self.blocks[current.idx()].next; + } + } + fn add_checks_for_loads_of_uninitialized_variables(&mut self) { - let nlocals = self.metadata.varnames.len(); + let mut nlocals = self.metadata.varnames.len(); if nlocals == 0 { return; } - let merged_cell_local = |cell_relative: usize| { - self.metadata - .cellvars - .get_index(cell_relative) - .and_then(|name| self.metadata.varnames.get_index_of(name.as_str())) - }; + let cell_to_local: Vec<_> = self + .metadata + .cellvars + .iter() + .map(|name| self.metadata.varnames.get_index_of(name.as_str())) + .collect(); + let merged_cell_local = + |cell_relative: usize| cell_to_local.get(cell_relative).copied().flatten(); let mut nparams = self.metadata.argcount as usize + self.metadata.kwonlyargcount as usize; if self.flags.contains(CodeFlags::VARARGS) { @@ -9159,6 +12514,11 @@ impl CodeInfo { } nparams = nparams.min(nlocals); + if nlocals > 64 { + self.fast_scan_many_locals(nlocals, nparams, &merged_cell_local); + nlocals = 64; + } + let mut in_masks: Vec>> = vec![None; self.blocks.len()]; let mut start_mask = vec![false; nlocals]; for slot in start_mask.iter_mut().skip(nparams) { @@ -9577,6 +12937,7 @@ impl CodeInfo { reorder_conditional_explicit_continue_scope_exit_blocks(&mut self.blocks); reorder_conditional_implicit_continue_scope_exit_blocks(&mut self.blocks); reorder_conditional_scope_exit_and_jump_back_blocks(&mut self.blocks, true, true); + reorder_exception_handler_conditional_continue_scope_exit_blocks(&mut self.blocks); deduplicate_adjacent_jump_back_blocks(&mut self.blocks); reorder_conditional_body_and_implicit_continue_blocks(&mut self.blocks); reorder_conditional_scope_exit_and_jump_back_blocks(&mut self.blocks, true, true); @@ -9597,7 +12958,6 @@ impl CodeInfo { )); materialize_empty_conditional_exit_targets(&mut self.blocks); - materialize_exception_cleanup_jump_targets(&mut self.blocks); trace.push(( "after_materialize_empty_conditional_exit_targets".to_owned(), self.debug_block_dump(), @@ -9608,10 +12968,18 @@ impl CodeInfo { self.debug_block_dump(), )); + inline_small_fast_return_blocks(&mut self.blocks); + inline_unprotected_tuple_genexpr_assignment_return_blocks(&mut self.blocks); + trace.push(( + "after_inline_small_fast_return_blocks".to_owned(), + self.debug_block_dump(), + )); + duplicate_end_returns(&mut self.blocks, &self.metadata); + duplicate_fallthrough_jump_back_targets(&mut self.blocks); duplicate_shared_jump_back_targets(&mut self.blocks); trace.push(( - "after_duplicate_end_returns".to_owned(), + "after_duplicate_jump_back_targets".to_owned(), self.debug_block_dump(), )); @@ -9665,8 +13033,9 @@ impl CodeInfo { let _ = self.max_stackdepth()?; convert_pseudo_ops(&mut self.blocks, &cellfixedoffsets); remove_redundant_nops_and_jumps(&mut self.blocks); - self.mark_assertion_success_tail_borrow_disabled(); self.mark_unprotected_debug_four_tails_borrow_disabled(); + self.mark_exception_handler_transition_targets_borrow_disabled(); + self.mark_targeted_nop_for_tails_borrow_disabled(); trace.push(( "after_convert_pseudo_ops".to_owned(), self.debug_block_dump(), @@ -9738,11 +13107,6 @@ impl CodeInfo { "after_deoptimize_borrow_after_terminal_except_before_with".to_owned(), self.debug_block_dump(), )); - self.deoptimize_borrow_in_async_finally_early_return_tail(); - trace.push(( - "after_deoptimize_borrow_in_async_finally_early_return_tail".to_owned(), - self.debug_block_dump(), - )); self.deoptimize_borrow_after_handler_resume_loop_tail(); trace.push(( "after_deoptimize_borrow_after_handler_resume_loop_tail".to_owned(), @@ -9768,7 +13132,6 @@ impl CodeInfo { "after_optimize_load_fast_borrow".to_owned(), self.debug_block_dump(), )); - self.deoptimize_borrow_after_push_exc_info(); self.deoptimize_borrow_for_handler_return_paths(); self.deoptimize_borrow_for_match_keys_attr(); self.deoptimize_borrow_in_protected_attr_chain_tail(); @@ -9780,7 +13143,6 @@ impl CodeInfo { self.optimize_load_global_push_null(); self.reorder_entry_prefix_cell_setup(); self.remove_unused_consts(); - deoptimize_borrow_after_push_exc_info_in_blocks(&mut self.blocks); Ok(trace) } @@ -10231,7 +13593,10 @@ fn push_cold_blocks_to_end(blocks: &mut Vec) { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); jump_block.next = blocks[cold_idx.idx()].next; blocks[cold_idx.idx()].next = jump_block_idx; @@ -10339,6 +13704,64 @@ fn jump_threading_unconditional(blocks: &mut [Block]) { jump_threading_impl(blocks, false); } +fn short_circuit_stub_conditional(block: &Block) -> Option { + let cond_idx = trailing_conditional_jump_index(block)?; + if cond_idx < 2 { + return None; + } + let [first, second, ..] = block.instructions.as_slice() else { + return None; + }; + if !matches!(first.instr.real(), Some(Instruction::Copy { i }) if i.get(first.arg) == 1) + || !matches!(second.instr.real(), Some(Instruction::ToBool)) + { + return None; + } + + let only_markers_between = block.instructions[2..cond_idx].iter().all(|info| { + matches!( + info.instr.real(), + None | Some(Instruction::Nop | Instruction::NotTaken) + ) + }); + if !only_markers_between { + return None; + } + + block.instructions[cond_idx].instr.real() +} + +fn opposite_short_circuit_target(block: &Block, source: AnyInstruction) -> bool { + let Some(conditional) = short_circuit_stub_conditional(block) else { + return false; + }; + matches!( + (source.real(), Some(conditional)), + ( + Some(Instruction::PopJumpIfFalse { .. }), + Some(Instruction::PopJumpIfTrue { .. }) + ) | ( + Some(Instruction::PopJumpIfTrue { .. }), + Some(Instruction::PopJumpIfFalse { .. }) + ) + ) +} + +fn same_short_circuit_target(block: &Block, source: AnyInstruction) -> Option { + let conditional = short_circuit_stub_conditional(block)?; + matches!( + (source.real(), Some(conditional)), + ( + Some(Instruction::PopJumpIfFalse { .. }), + Some(Instruction::PopJumpIfFalse { .. }) + ) | ( + Some(Instruction::PopJumpIfTrue { .. }), + Some(Instruction::PopJumpIfTrue { .. }) + ) + ) + .then_some(block.instructions[trailing_conditional_jump_index(block)?].target) +} + #[derive(Clone, Copy, PartialEq, Eq)] enum JumpThreadKind { Plain, @@ -10392,6 +13815,17 @@ fn threaded_jump_instr( }) } +fn can_thread_conditional_through_forward_nointerrupt( + source: AnyInstruction, + target_pos: u32, + final_target_pos: u32, +) -> bool { + matches!( + source.real(), + Some(Instruction::PopJumpIfNone { .. } | Instruction::PopJumpIfNotNone { .. }) + ) && final_target_pos > target_pos +} + fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { let mut changed = true; while changed { @@ -10419,19 +13853,76 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { { continue; } + target = next_nonempty_block(blocks, target); + if target == BlockIdx::NULL { + continue; + } if include_conditional && is_conditional_jump(&ins.instr) { let next = next_nonempty_block(blocks, blocks[bi].next); - if next != BlockIdx::NULL + let next_is_scope_exit = next != BlockIdx::NULL && blocks[next.idx()] .instructions .last() - .is_some_and(|instr| instr.instr.is_scope_exit()) - { + .is_some_and(|instr| instr.instr.is_scope_exit()); + if next_is_scope_exit { + let target_pos = block_order.get(target.idx()).copied().unwrap_or(u32::MAX); + let target_first_jump = blocks[target.idx()].instructions.first().copied(); + let threads_match_success_jump_to_forward_nointerrupt = + matches!(ins.instr.real(), Some(Instruction::PopJumpIfNone { .. })) + && target_first_jump + .filter(|target_ins| target_ins.instr.is_unconditional_jump()) + .filter(|target_ins| target_ins.target != BlockIdx::NULL) + .is_some_and(|target_ins| { + let final_target_pos = block_order + .get(target_ins.target.idx()) + .copied() + .unwrap_or(u32::MAX); + jump_thread_kind(target_ins.instr) + == Some(JumpThreadKind::NoInterrupt) + && target_ins.match_success_jump + && final_target_pos > target_pos + }); + let next_raises = blocks[next.idx()].instructions.iter().any(|instr| { + matches!(instr.instr.real(), Some(Instruction::RaiseVarargs { .. })) + }); + let target_is_loop_backedge = blocks[target.idx()] + .instructions + .first() + .filter(|target_ins| target_ins.instr.is_unconditional_jump()) + .map(|target_ins| next_nonempty_block(blocks, target_ins.target)) + .is_some_and(|final_target| { + final_target == BlockIdx(bi as u32) + || comes_before(blocks, final_target, BlockIdx(bi as u32)) + }); + if !(threads_match_success_jump_to_forward_nointerrupt + || block_is_protected(&blocks[bi]) + && next_raises + && target_is_loop_backedge) + { + continue; + } + } + } + if include_conditional + && is_conditional_jump(&ins.instr) + && opposite_short_circuit_target(&blocks[target.idx()], ins.instr) + { + let final_target = next_nonempty_block(blocks, blocks[target.idx()].next); + if final_target != BlockIdx::NULL && ins.target != final_target { + blocks[bi].instructions[last_idx].target = final_target; + changed = true; continue; } } - target = next_nonempty_block(blocks, target); - if target == BlockIdx::NULL { + if include_conditional + && is_conditional_jump(&ins.instr) + && let Some(final_target) = + same_short_circuit_target(&blocks[target.idx()], ins.instr) + && final_target != BlockIdx::NULL + && ins.target != final_target + { + blocks[bi].instructions[last_idx].target = final_target; + changed = true; continue; } if include_conditional && is_conditional_jump(&ins.instr) { @@ -10459,6 +13950,18 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { && target_ins.target != BlockIdx::NULL && target_ins.target != target { + if !include_conditional + && blocks[target.idx()] + .instructions + .iter() + .take_while(|info| matches!(info.instr.real(), Some(Instruction::Nop))) + .any(instruction_has_lineno) + { + continue; + } + if !include_conditional && instruction_has_lineno(&target_ins) { + continue; + } let source_pos = block_order[bi]; let target_pos = block_order.get(target.idx()).copied().unwrap_or(u32::MAX); let final_target = target_ins.target; @@ -10467,22 +13970,6 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { .copied() .unwrap_or(u32::MAX); let conditional = is_conditional_jump(&ins.instr); - if conditional && final_target_pos <= source_pos { - let mut scan = blocks[bi].next; - let mut chain_has_delete_subscr = false; - let mut seen = vec![false; blocks.len()]; - while scan != BlockIdx::NULL && scan != target && !seen[scan.idx()] { - seen[scan.idx()] = true; - chain_has_delete_subscr |= - blocks[scan.idx()].instructions.iter().any(|info| { - matches!(info.instr.real(), Some(Instruction::DeleteSubscr)) - }); - scan = blocks[scan.idx()].next; - } - if chain_has_delete_subscr { - continue; - } - } if !include_conditional && source_pos < target_pos && final_target_pos < target_pos { // Keep the forward hop when threading would turn it into a @@ -10504,17 +13991,30 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { // the line-anchored continue/break jump that follows. continue; } - if conditional - && !matches!( - jump_thread_kind(target_ins.instr), - Some(JumpThreadKind::Plain) - ) - { - continue; - } - let Some(threaded_instr) = - threaded_jump_instr(ins.instr, target_ins.instr, conditional) - else { + let Some(threaded_instr) = (if conditional { + match jump_thread_kind(target_ins.instr) { + Some(JumpThreadKind::Plain) => Some(ins.instr), + Some(JumpThreadKind::NoInterrupt) + if target_ins.match_success_jump + && can_thread_conditional_through_forward_nointerrupt( + ins.instr, + target_pos, + final_target_pos, + ) => + { + // A forward JUMP_NO_INTERRUPT assembles to the same + // JUMP_FORWARD opcode as CPython's plain match + // success jump. Limit this to None-check match + // success tests; boolean finally-cleanup jumps need + // the stronger no-interrupt shape for later CFG + // cleanup decisions. + Some(ins.instr) + } + _ => None, + } + } else { + threaded_jump_instr(ins.instr, target_ins.instr, false) + }) else { continue; }; if ins.target == final_target { @@ -10532,6 +14032,9 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) { changed = true; } } + if include_conditional { + break; + } } } @@ -10605,7 +14108,10 @@ fn normalize_jumps(blocks: &mut Vec) { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }; blocks[idx].instructions.push(not_taken); } else { @@ -10640,7 +14146,10 @@ fn normalize_jumps(blocks: &mut Vec) { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); new_block.instructions.push(InstructionInfo { instr: PseudoOpcode::Jump.into(), @@ -10654,7 +14163,10 @@ fn normalize_jumps(blocks: &mut Vec) { cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); new_block.next = old_next; @@ -10739,6 +14251,64 @@ fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) { .iter() .any(|ins| ins.instr.is_block_push()) }; + let block_ends_with_list_to_tuple_jump = |block: &Block| { + let ops: Vec<_> = block + .instructions + .iter() + .filter(|info| !matches!(info.instr.real(), Some(Instruction::Nop))) + .collect(); + let Some((last, prefix)) = ops.split_last() else { + return false; + }; + if !last.instr.is_unconditional_jump() { + return false; + } + let Some(prev) = prefix.last() else { + return false; + }; + match prev.instr.real() { + Some(Instruction::CallIntrinsic1 { func }) => { + func.get(prev.arg) == IntrinsicFunction1::ListToTuple + } + _ => false, + } + }; + let block_starts_with_store_and_exits = |block: &Block| { + block.instructions.first().is_some_and(|info| { + matches!( + info.instr.real(), + Some( + Instruction::StoreFast { .. } + | Instruction::StoreGlobal { .. } + | Instruction::StoreName { .. } + | Instruction::StoreDeref { .. } + ) + ) + }) && block_exits_scope(block) + }; + let block_is_simple_fast_return = |block: &Block| { + matches!( + block.instructions.as_slice(), + [load, ret] + if matches!( + load.instr.real(), + Some(Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }) + ) && matches!(ret.instr.real(), Some(Instruction::ReturnValue)) + ) + }; + let normal_layout_fallthrough_into = |blocks: &[Block], target: BlockIdx| { + let mut current = BlockIdx(0); + let mut previous_nonempty = BlockIdx::NULL; + while current != BlockIdx::NULL && current != target { + if !blocks[current.idx()].instructions.is_empty() { + previous_nonempty = current; + } + current = blocks[current.idx()].next; + } + previous_nonempty != BlockIdx::NULL + && block_has_fallthrough(&blocks[previous_nonempty.idx()]) + && next_nonempty_block(blocks, blocks[previous_nonempty.idx()].next) == target + }; loop { let mut changes = false; let mut predecessors = vec![0usize; blocks.len()]; @@ -10765,9 +14335,9 @@ fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) { } let target = last.target; - if block_is_exceptional(&blocks[target.idx()]) - || (is_named_except_cleanup_normal_exit_block(&blocks[current.idx()]) - && target_pushes_handler(&blocks[target.idx()])) + if is_named_except_cleanup_normal_exit_block(&blocks[current.idx()]) + && target_pushes_handler(&blocks[target.idx()]) + && !named_except_cleanup_body_is_fast_local_only(&blocks[current.idx()]) { current = next; continue; @@ -10782,14 +14352,38 @@ fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) { && !instruction_has_lineno(&blocks[target.idx()].instructions[0]) && !instruction_has_lineno(&blocks[target.idx()].instructions[1]) && !instruction_has_lineno(&blocks[target.idx()].instructions[2]); - if !shared_artificial_expr_exit && (small_exit_block || no_lineno_no_fallthrough) { - let removed_jump_had_lineno = blocks[current.idx()] - .instructions - .last() - .is_some_and(instruction_has_lineno); - if removed_jump_had_lineno { + let shared_tuple_genexpr_assignment_tail = small_exit_block + && predecessors[target.idx()] > 1 + && block_ends_with_list_to_tuple_jump(&blocks[current.idx()]) + && block_starts_with_store_and_exits(&blocks[target.idx()]); + if !shared_artificial_expr_exit + && !shared_tuple_genexpr_assignment_tail + && (small_exit_block || no_lineno_no_fallthrough) + { + let removed_jump_kind = jump_thread_kind(last.instr); + let preserve_removed_jump_nop = last.preserve_redundant_jump_as_nop; + let keep_removed_jump_nop = removed_jump_kind == Some(JumpThreadKind::NoInterrupt) + || blocks[current.idx()] + .instructions + .last() + .is_some_and(instruction_has_lineno); + if keep_removed_jump_nop { + let preserve_empty_end_label_nop = removed_jump_kind + == Some(JumpThreadKind::NoInterrupt) + && small_exit_block + && blocks[current.idx()].instructions.len() == 1 + && block_is_simple_fast_return(&blocks[target.idx()]) + && !normal_layout_fallthrough_into(blocks, current); if let Some(last_instr) = blocks[current.idx()].instructions.last_mut() { + let lineno_override = last_instr.lineno_override; set_to_nop(last_instr); + last_instr.lineno_override = lineno_override; + last_instr.preserve_block_start_no_location_nop |= + preserve_removed_jump_nop; + if preserve_empty_end_label_nop { + last_instr.lineno_override = None; + last_instr.preserve_block_start_no_location_nop = true; + } } } else { let _ = blocks[current.idx()].instructions.pop(); @@ -10797,6 +14391,21 @@ fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) { blocks[current.idx()] .instructions .extend(blocks[target.idx()].instructions.clone()); + if no_lineno_no_fallthrough + && removed_jump_kind == Some(JumpThreadKind::Plain) + && let Some(last) = blocks[current.idx()].instructions.last_mut() + && jump_thread_kind(last.instr) == Some(JumpThreadKind::NoInterrupt) + { + last.instr = match last.instr.into() { + AnyOpcode::Pseudo(PseudoOpcode::JumpNoInterrupt) => { + PseudoOpcode::Jump.into() + } + AnyOpcode::Real(Opcode::JumpBackwardNoInterrupt) => { + Opcode::JumpBackward.into() + } + _ => last.instr, + }; + } changes = true; } @@ -10909,47 +14518,20 @@ fn compute_target_predecessor_flags(blocks: &[Block]) -> TargetPredecessorFlags fn remove_redundant_nops_in_blocks(blocks: &mut [Block]) -> usize { let mut changes = 0; - let TargetPredecessorFlags { - targeted: targeted_blocks, - plain_jump: plain_jump_targets, - .. - } = compute_target_predecessor_flags(blocks); + let plain_jump_targets = compute_target_predecessor_flags(blocks).plain_jump; + let layout_predecessors = compute_layout_predecessors(blocks); let mut block_order = Vec::new(); let mut current = BlockIdx(0); while current != BlockIdx::NULL { block_order.push(current); current = blocks[current.idx()].next; } - let mut fallthrough_prev_lineno = vec![None; blocks.len()]; - let mut fallthrough_prev_pop_top_lineno = vec![None; blocks.len()]; - let mut prev_nonempty = BlockIdx::NULL; - for &block_idx in &block_order { - if blocks[block_idx.idx()].instructions.is_empty() { - continue; - } - if prev_nonempty != BlockIdx::NULL - && !targeted_blocks[block_idx.idx()] - && block_has_fallthrough(&blocks[prev_nonempty.idx()]) - && next_nonempty_block(blocks, blocks[prev_nonempty.idx()].next) == block_idx - { - fallthrough_prev_lineno[block_idx.idx()] = blocks[prev_nonempty.idx()] - .instructions - .last() - .map(instruction_lineno); - } - if prev_nonempty != BlockIdx::NULL - && block_has_fallthrough(&blocks[prev_nonempty.idx()]) - && next_nonempty_block(blocks, blocks[prev_nonempty.idx()].next) == block_idx - && let Some(last) = blocks[prev_nonempty.idx()].instructions.last() - && matches!(last.instr.real(), Some(Instruction::PopTop)) - { - fallthrough_prev_pop_top_lineno[block_idx.idx()] = Some(instruction_lineno(last)); - } - prev_nonempty = block_idx; - } - for block_idx in block_order { let bi = block_idx.idx(); + let keep_target_start_nop = + keep_target_start_no_location_nop(blocks, block_idx, &layout_predecessors); + let follows_same_line_pop_iter = + layout_predecessor_ends_with_pop_iter_on_line(blocks, block_idx, &layout_predecessors); let mut src_instructions = core::mem::take(&mut blocks[bi].instructions); let mut kept = Vec::with_capacity(src_instructions.len()); let mut prev_lineno = -1i32; @@ -10960,21 +14542,28 @@ fn remove_redundant_nops_in_blocks(blocks: &mut [Block]) -> usize { let mut remove = false; if matches!(instr.instr.real(), Some(Instruction::Nop)) { - if instr.preserve_block_start_no_location_nop { + if instr.no_location_exit && instr.preserve_redundant_jump_as_nop { remove = false; - } else if lineno < 0 - || (src == 0 - && fallthrough_prev_lineno[block_idx.idx()] - .is_some_and(|prev_lineno| prev_lineno == lineno)) - || (src == 0 - && src_instructions.len() == 1 - && fallthrough_prev_pop_top_lineno[block_idx.idx()] - .is_some_and(|prev_lineno| prev_lineno == lineno)) + } else if src == 0 + && lineno > 0 + && ((!keep_target_start_nop && follows_same_line_pop_iter == Some(lineno)) + || (instr.preserve_block_start_no_location_nop + && block_tail_starts_with_async_with_normal_exit( + &src_instructions[src + 1..], + ))) + { + remove = true; + } else if instr.preserve_redundant_jump_as_nop + || instr.preserve_block_start_no_location_nop { + remove = false; + } else if lineno < 0 { remove = true; } else if instr.remove_no_location_nop && src == 0 && plain_jump_targets[block_idx.idx()] + && instr.lineno_override.is_some() + && !keep_target_start_nop { let next_lineno = src_instructions[src + 1..].iter().find_map(|next_instr| { let line = instruction_lineno(next_instr); @@ -10992,9 +14581,14 @@ fn remove_redundant_nops_in_blocks(blocks: &mut [Block]) -> usize { } else if src < src_instructions.len() - 1 { if src_instructions[src + 1].instr.is_block_push() { remove = false; - } else if src_instructions[src + 1].instr.is_unconditional_jump() { - src_instructions[src + 1].lineno_override = Some(lineno); - remove = true; + } else if src_instructions[src + 1].instr.is_unconditional_jump() + && src_instructions[src + 1].target != block_idx + { + let next_lineno = instruction_lineno(&src_instructions[src + 1]); + if next_lineno == lineno || next_lineno < 0 { + src_instructions[src + 1].lineno_override = Some(lineno); + remove = true; + } } else if src_instructions[src + 1].folded_from_nonliteral_expr { remove = true; } else { @@ -11063,33 +14657,35 @@ fn remove_redundant_jumps_in_blocks(blocks: &mut [Block]) -> usize { && last_instr.target != BlockIdx::NULL && next_nonempty_block(blocks, last_instr.target) == next { - let preserve_as_nop = if last_instr.preserve_redundant_jump_as_nop { + let preserve_redundant_jump_nop = if last_instr.preserve_redundant_jump_as_nop { let line = instruction_lineno(&last_instr); let next_line = blocks[next.idx()].instructions.iter().find_map(|instr| { let line = instruction_lineno(instr); (!matches!(instr.instr.real(), Some(Instruction::Nop)) || line >= 0) .then_some(line) }); - line > 0 && next_line.is_some_and(|next_line| next_line < line) + line < 0 + || line > 0 + && !block_jump_follows_async_send_pop(&blocks[idx]) + && !(block_jump_follows_with_normal_exit(&blocks[idx]) + && block_tail_starts_with_async_with_normal_exit( + &blocks[next.idx()].instructions, + )) + && next_line.is_some_and(|next_line| next_line < line) } else { false }; - if preserve_as_nop { - current = blocks[idx].next; - continue; - } - if last_instr.preserve_redundant_jump_as_nop { - let last_instr = blocks[idx].instructions.last_mut().unwrap(); - last_instr.preserve_redundant_jump_as_nop = false; - } let last_instr = blocks[idx].instructions.last_mut().unwrap(); let remove_no_location_nop = last_instr.remove_no_location_nop; + let folded_operand_nop = last_instr.folded_operand_nop; let preserve_block_start_no_location_nop = last_instr.preserve_block_start_no_location_nop; set_to_nop(last_instr); + last_instr.preserve_redundant_jump_as_nop = preserve_redundant_jump_nop; last_instr.remove_no_location_nop = remove_no_location_nop; + last_instr.folded_operand_nop = folded_operand_nop; last_instr.preserve_block_start_no_location_nop = - preserve_block_start_no_location_nop; + preserve_block_start_no_location_nop || preserve_redundant_jump_nop; changes += 1; current = blocks[idx].next; continue; @@ -11138,6 +14734,42 @@ fn redirect_empty_block_targets(blocks: &mut [Block]) { } fn redirect_empty_unconditional_jump_targets(blocks: &mut [Block]) { + const MAX_COPY_SIZE: usize = 4; + + let block_exits_to_large_reraise = |block_idx: BlockIdx| { + let block = &blocks[block_idx.idx()]; + let Some(last) = block.instructions.last() else { + return false; + }; + let reraise_block = if matches!(last.instr.real(), Some(Instruction::Reraise { .. })) { + block_idx + } else if last.instr.is_unconditional_jump() && last.target != BlockIdx::NULL { + next_nonempty_block(blocks, last.target) + } else { + BlockIdx::NULL + }; + reraise_block != BlockIdx::NULL + && blocks[reraise_block.idx()].instructions.len() > MAX_COPY_SIZE + && blocks[reraise_block.idx()] + .instructions + .last() + .is_some_and(|instr| { + matches!(instr.instr.real(), Some(Instruction::Reraise { .. })) + }) + }; + + let mut raw_predecessors = vec![0u32; blocks.len()]; + for block in blocks.iter() { + if block_has_fallthrough(block) && block.next != BlockIdx::NULL { + raw_predecessors[block.next.idx()] += 1; + } + for instr in &block.instructions { + if instr.target != BlockIdx::NULL { + raw_predecessors[instr.target.idx()] += 1; + } + } + } + let redirected_targets: Vec> = blocks .iter() .map(|block| { @@ -11148,6 +14780,15 @@ fn redirect_empty_unconditional_jump_targets(blocks: &mut [Block]) { if instr.target == BlockIdx::NULL || !instr.instr.is_unconditional_jump() { instr.target } else { + if blocks[instr.target.idx()].instructions.is_empty() + && raw_predecessors[instr.target.idx()] > 1 + && { + let target = next_nonempty_block(blocks, instr.target); + target != BlockIdx::NULL && block_exits_to_large_reraise(target) + } + { + return instr.target; + } let target = next_nonempty_block(blocks, instr.target); if matches!( jump_thread_kind(instr.instr), @@ -11175,17 +14816,102 @@ fn redirect_empty_unconditional_jump_targets(blocks: &mut [Block]) { } fn materialize_empty_conditional_exit_targets(blocks: &mut [Block]) { + fn block_starts_with_with_normal_exit(block: &Block) -> bool { + matches!( + block.instructions.as_slice(), + [ + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::Call { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::PopTop), + .. + }, + .. + ] + ) + } + + fn with_normal_exit_is_followed_by_try(blocks: &[Block], block_idx: BlockIdx) -> bool { + if block_idx == BlockIdx::NULL + || !block_starts_with_with_normal_exit(&blocks[block_idx.idx()]) + { + return false; + } + let next = next_nonempty_block(blocks, blocks[block_idx.idx()].next); + next != BlockIdx::NULL + && blocks[next.idx()].instructions.first().is_some_and(|info| { + matches!( + info.instr, + AnyInstruction::Pseudo(PseudoInstruction::SetupFinally { .. }) + | AnyInstruction::Real(Instruction::Nop) + ) + }) + } + + fn has_loop_backedge_to(blocks: &[Block], target: BlockIdx) -> bool { + blocks.iter().enumerate().any(|(source_idx, block)| { + let source = BlockIdx(source_idx as u32); + comes_before(blocks, target, source) + && block.instructions.iter().any(|info| { + info.instr.is_unconditional_jump() + && info.target != BlockIdx::NULL + && next_nonempty_block(blocks, info.target) == target + }) + }) + } + let mut jump_back_inserts = Vec::new(); let mut inserts = Vec::new(); + let mut target_start_inserts = Vec::new(); + let mut jump_back_target_locations = Vec::new(); for (block_idx, block) in blocks.iter().enumerate() { - let Some(last) = block.instructions.last() else { + let source = BlockIdx(block_idx as u32); + let (last, allow_scope_exit_target) = if let Some(last) = block + .instructions + .last() + .filter(|info| is_conditional_jump(&info.instr)) + { + (last, true) + } else if let Some(cond_idx) = trailing_conditional_jump_index(block) { + (&block.instructions[cond_idx], false) + } else { continue; }; - if !is_conditional_jump(&last.instr) || last.target == BlockIdx::NULL { + if last.target == BlockIdx::NULL { continue; } let target = last.target; if !blocks[target.idx()].instructions.is_empty() { + if is_jump_back_only_block(blocks, target) && block_has_no_lineno(&blocks[target.idx()]) + { + jump_back_target_locations.push((*last, target)); + } + if with_normal_exit_is_followed_by_try(blocks, target) + && has_loop_backedge_to(blocks, source) + && !matches!( + blocks[target.idx()] + .instructions + .first() + .and_then(|info| info.instr.real()), + Some(Instruction::Nop) + ) + { + target_start_inserts.push((*last, target)); + } continue; } let next = next_nonempty_block(blocks, blocks[target.idx()].next); @@ -11201,10 +14927,23 @@ fn materialize_empty_conditional_exit_targets(blocks: &mut [Block]) { jump_back_inserts.push((BlockIdx(block_idx as u32), target, next)); continue; } - if next == BlockIdx::NULL || !is_scope_exit_block(&blocks[next.idx()]) { + if next == BlockIdx::NULL + || !((allow_scope_exit_target && is_scope_exit_block(&blocks[next.idx()])) + || (with_normal_exit_is_followed_by_try(blocks, next) + && has_loop_backedge_to(blocks, source))) + { + continue; + } + inserts.push((*last, target)); + } + + for (source, target) in jump_back_target_locations { + if !is_jump_back_only_block(blocks, target) || !block_has_no_lineno(&blocks[target.idx()]) { continue; } - inserts.push((BlockIdx(block_idx as u32), target)); + if let Some(first) = blocks[target.idx()].instructions.first_mut() { + overwrite_location(first, source.location, source.end_location); + } } for (source, target, next) in jump_back_inserts { @@ -11223,160 +14962,76 @@ fn materialize_empty_conditional_exit_targets(blocks: &mut [Block]) { if !blocks[target.idx()].instructions.is_empty() { continue; } - let Some(last) = blocks[source.idx()].instructions.last().copied() else { - continue; - }; blocks[target.idx()].instructions.push(InstructionInfo { instr: Instruction::Nop.into(), arg: OpArg::NULL, target: BlockIdx::NULL, - location: last.location, - end_location: last.end_location, + location: source.location, + end_location: source.end_location, except_handler: None, folded_from_nonliteral_expr: false, lineno_override: None, cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, preserve_block_start_no_location_nop: false, + match_success_jump: false, }); } -} - -fn materialize_exception_cleanup_jump_targets(blocks: &mut [Block]) { - let mut inserts = Vec::new(); - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let idx = current.idx(); - let Some(last) = blocks[idx].instructions.last() else { - current = blocks[idx].next; - continue; - }; - if !last.instr.is_unconditional_jump() || last.target == BlockIdx::NULL { - current = blocks[idx].next; - continue; - } - let mut cursor = blocks[idx].next; - let mut saw_cleanup = false; - while cursor != BlockIdx::NULL && cursor != last.target { - let block = &blocks[cursor.idx()]; - if !block_is_exceptional(block) && !is_exception_cleanup_block(block) { - break; - } - saw_cleanup = true; - cursor = block.next; - } - if saw_cleanup - && cursor == last.target - && !blocks[last.target.idx()].instructions.is_empty() - && !block_starts_with_with_exit_none_call(&blocks[last.target.idx()]) - && !matches!( - blocks[last.target.idx()] + for (source, target) in target_start_inserts.into_iter().rev() { + if !with_normal_exit_is_followed_by_try(blocks, target) + || matches!( + blocks[target.idx()] .instructions .first() .and_then(|info| info.instr.real()), Some(Instruction::Nop) ) { - inserts.push(last.target); - } - - current = blocks[idx].next; - } - - inserts.sort_by_key(|idx| idx.idx()); - inserts.dedup(); - for target in inserts { - let Some(first) = blocks[target.idx()].instructions.first().copied() else { continue; - }; + } blocks[target.idx()].instructions.insert( 0, InstructionInfo { instr: Instruction::Nop.into(), arg: OpArg::NULL, target: BlockIdx::NULL, - location: first.location, - end_location: first.end_location, + location: source.location, + end_location: source.end_location, except_handler: None, folded_from_nonliteral_expr: false, - lineno_override: Some(-1), + lineno_override: None, cache_entries: 0, preserve_redundant_jump_as_nop: false, remove_no_location_nop: false, - preserve_block_start_no_location_nop: true, + folded_operand_nop: false, + no_location_exit: false, + preserve_block_start_no_location_nop: false, + match_success_jump: false, }, - ); - } -} - -fn block_starts_with_with_exit_none_call(block: &Block) -> bool { - let real_instrs: Vec<_> = block - .instructions - .iter() - .filter_map(|info| info.instr.real()) - .take(4) - .collect(); - matches!( - real_instrs.as_slice(), - [ - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::LoadConst { .. }, - Instruction::Call { .. }, - ] - ) -} - -fn merge_unsafe_mask(slot: &mut Option>, incoming: &[bool]) -> bool { - match slot { - Some(existing) => { - let mut changed = false; - for (dst, src) in existing.iter_mut().zip(incoming.iter().copied()) { - if src && !*dst { - *dst = true; - changed = true; - } - } - changed - } - None => { - *slot = Some(incoming.to_vec()); - true - } - } -} - -fn deoptimize_borrow_after_push_exc_info_in_blocks(blocks: &mut [Block]) { - let mut in_exception_state = false; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let block = &mut blocks[current.idx()]; - for info in &mut block.instructions { - match info.instr.real() { - Some(Instruction::PushExcInfo) => { - in_exception_state = true; - } - Some(Instruction::PopExcept | Instruction::Reraise { .. }) => { - in_exception_state = false; - } - Some(Instruction::LoadFastBorrow { .. }) if in_exception_state => { - info.instr = Instruction::LoadFast { - var_num: Arg::marker(), - } - .into(); - } - Some(Instruction::LoadFastBorrowLoadFastBorrow { .. }) if in_exception_state => { - info.instr = Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), - } - .into(); + ); + } +} + +fn merge_unsafe_mask(slot: &mut Option>, incoming: &[bool]) -> bool { + match slot { + Some(existing) => { + let mut changed = false; + for (dst, src) in existing.iter_mut().zip(incoming.iter().copied()) { + if src && !*dst { + *dst = true; + changed = true; } - _ => {} } + changed + } + None => { + *slot = Some(incoming.to_vec()); + true } - current = block.next; } } @@ -11399,6 +15054,35 @@ fn is_load_const_none(instr: &InstructionInfo, metadata: &CodeUnitMetadata) -> b ) } +fn block_tail_starts_with_async_with_normal_exit(instructions: &[InstructionInfo]) -> bool { + matches!( + instructions, + [ + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::LoadConst { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::Call { .. }), + .. + }, + InstructionInfo { + instr: AnyInstruction::Real(Instruction::GetAwaitable { .. }), + .. + }, + .. + ] + ) +} + fn instruction_lineno(instr: &InstructionInfo) -> i32 { instr .lineno_override @@ -11464,6 +15148,30 @@ fn is_exit_without_lineno(blocks: &[Block], block_idx: BlockIdx) -> bool { && has_non_exception_loop_backedge_to(blocks, block_idx, last.target) } +fn is_eval_break_without_lineno(blocks: &[Block], block_idx: BlockIdx) -> bool { + let block = &blocks[block_idx.idx()]; + let Some(first) = block.instructions.first() else { + return false; + }; + !instruction_has_lineno(first) && block_has_no_lineno(block) && block_has_eval_break(block) +} + +fn block_has_eval_break(block: &Block) -> bool { + block.instructions.iter().any(|info| { + matches!( + info.instr, + AnyInstruction::Pseudo(PseudoInstruction::Jump { .. }) + | AnyInstruction::Real( + Instruction::Call { .. } + | Instruction::CallFunctionEx + | Instruction::CallKw { .. } + | Instruction::JumpBackward { .. } + | Instruction::Resume { .. } + ) + ) + }) +} + fn block_has_no_lineno(block: &Block) -> bool { block .instructions @@ -11493,26 +15201,33 @@ fn shared_jump_back_target(block: &Block) -> Option { Some(last.target) } -fn has_non_exception_loop_backedge_to( +fn block_has_non_exception_loop_backedge_to( blocks: &[Block], - cleanup_block: BlockIdx, + source: BlockIdx, target: BlockIdx, ) -> bool { let target = next_nonempty_block(blocks, target); - if target == BlockIdx::NULL { - return false; - } + source != BlockIdx::NULL + && target != BlockIdx::NULL + && !block_is_exceptional(&blocks[source.idx()]) + && comes_before(blocks, target, source) + && blocks[source.idx()].instructions.iter().any(|info| { + info.instr.is_unconditional_jump() + && info.target != BlockIdx::NULL + && next_nonempty_block(blocks, info.target) == target + }) +} +fn has_non_exception_loop_backedge_to( + blocks: &[Block], + cleanup_block: BlockIdx, + target: BlockIdx, +) -> bool { blocks.iter().enumerate().any(|(source_idx, block)| { let source = BlockIdx(source_idx as u32); source != cleanup_block && !block_is_exceptional(block) - && comes_before(blocks, target, source) - && block.instructions.iter().any(|info| { - info.instr.is_unconditional_jump() - && info.target != BlockIdx::NULL - && next_nonempty_block(blocks, info.target) == target - }) + && block_has_non_exception_loop_backedge_to(blocks, source, target) }) } @@ -11558,6 +15273,21 @@ fn is_scope_exit_block(block: &Block) -> bool { .is_some_and(|instr| instr.instr.is_scope_exit()) } +fn is_pop_top_scope_exit_block(block: &Block) -> bool { + is_scope_exit_block(block) + && matches!( + block + .instructions + .first() + .and_then(|info| info.instr.real()), + Some(Instruction::PopTop) + ) +} + +fn is_pop_top_exit_like_block(block: &Block) -> bool { + is_pop_top_scope_exit_block(block) || is_pop_top_jump_block(block) +} + fn is_loop_cleanup_block(block: &Block) -> bool { block .instructions @@ -11571,6 +15301,13 @@ fn is_loop_cleanup_block(block: &Block) -> bool { }) } +fn is_async_loop_cleanup_block(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::EndAsyncFor))) +} + fn is_exception_cleanup_block(block: &Block) -> bool { block .instructions @@ -11582,6 +15319,75 @@ fn is_exception_cleanup_block(block: &Block) -> bool { .is_some_and(|instr| matches!(instr.instr.real(), Some(Instruction::Reraise { .. }))) } +fn is_reraise_scope_exit_block(block: &Block) -> bool { + block + .instructions + .last() + .is_some_and(|instr| matches!(instr.instr.real(), Some(Instruction::Reraise { .. }))) +} + +fn block_starts_with_with_exit_none_call(block: &Block) -> bool { + let real_instrs: Vec<_> = block + .instructions + .iter() + .filter_map(|info| { + let instr = info.instr.real()?; + (!matches!(instr, Instruction::Nop)).then_some(instr) + }) + .take(4) + .collect(); + matches!( + real_instrs.as_slice(), + [ + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::LoadConst { .. }, + Instruction::Call { .. }, + ] + ) +} + +fn keep_target_start_no_location_nop( + blocks: &[Block], + target: BlockIdx, + layout_predecessors: &[BlockIdx], +) -> bool { + if target == BlockIdx::NULL { + return false; + } + let Some(first) = blocks[target.idx()].instructions.first() else { + return false; + }; + if !matches!(first.instr.real(), Some(Instruction::Nop)) { + return false; + } + let layout_pred = layout_predecessors[target.idx()]; + if layout_pred == BlockIdx::NULL { + return false; + } + if is_async_loop_cleanup_block(&blocks[layout_pred.idx()]) { + return true; + } + is_exception_cleanup_block(&blocks[layout_pred.idx()]) + && !block_starts_with_with_exit_none_call(&blocks[target.idx()]) +} + +fn layout_predecessor_ends_with_pop_iter_on_line( + blocks: &[Block], + target: BlockIdx, + layout_predecessors: &[BlockIdx], +) -> Option { + let layout_pred = layout_predecessors[target.idx()]; + if layout_pred == BlockIdx::NULL + || !block_has_fallthrough(&blocks[layout_pred.idx()]) + || next_nonempty_block(blocks, blocks[layout_pred.idx()].next) != target + { + return None; + } + let last = blocks[layout_pred.idx()].instructions.last()?; + matches!(last.instr.real(), Some(Instruction::PopIter)).then_some(instruction_lineno(last)) +} + fn is_with_suppress_exit_block(block: &Block) -> bool { let real_instrs: Vec<_> = block .instructions @@ -11624,6 +15430,52 @@ fn block_contains_suspension_point(block: &Block) -> bool { }) } +fn block_jump_follows_async_send_pop(block: &Block) -> bool { + let mut before_jump = + block + .instructions + .iter() + .rev() + .skip(1) + .filter_map(|info| match info.instr.real() { + Some(Instruction::Nop) => None, + instr => instr, + }); + matches!( + (before_jump.next(), before_jump.next()), + (Some(Instruction::PopTop), Some(Instruction::EndSend)) + ) +} + +fn block_jump_follows_with_normal_exit(block: &Block) -> bool { + let mut before_jump = + block + .instructions + .iter() + .rev() + .skip(1) + .filter_map(|info| match info.instr.real() { + Some(Instruction::Nop) => None, + instr => instr, + }); + matches!( + ( + before_jump.next(), + before_jump.next(), + before_jump.next(), + before_jump.next(), + before_jump.next(), + ), + ( + Some(Instruction::PopTop), + Some(Instruction::Call { .. }), + Some(Instruction::LoadConst { .. }), + Some(Instruction::LoadConst { .. }), + Some(Instruction::LoadConst { .. }), + ) + ) +} + fn is_stop_iteration_error_handler_block(block: &Block) -> bool { matches!( block.instructions.as_slice(), @@ -11676,6 +15528,13 @@ fn block_has_exception_match_handler(blocks: &[Block], block: &Block) -> bool { }) { return true; } + if blocks[cursor.idx()] + .instructions + .iter() + .any(|info| info.instr.is_scope_exit()) + { + break; + } cursor = blocks[cursor.idx()].next; } } @@ -11686,6 +15545,17 @@ fn block_is_exceptional(block: &Block) -> bool { block.except_handler || block.preserve_lasti || is_exception_cleanup_block(block) } +fn has_exceptional_duplicate_lineno(blocks: &[Block], source: BlockIdx, lineno: i32) -> bool { + blocks.iter().enumerate().any(|(idx, block)| { + BlockIdx(idx as u32) != source + && (block.cold || block_is_exceptional(block) || block_is_protected(block)) + && block + .instructions + .iter() + .any(|info| instruction_lineno(info) == lineno) + }) +} + fn trailing_conditional_jump_index(block: &Block) -> Option { let last_idx = block.instructions.len().checked_sub(1)?; if is_conditional_jump(&block.instructions[last_idx].instr) @@ -11707,6 +15577,34 @@ fn trailing_conditional_jump_index(block: &Block) -> Option { } } +fn block_is_pure_conditional_test(block: &Block) -> bool { + let Some(cond_idx) = trailing_conditional_jump_index(block) else { + return false; + }; + block.instructions[..cond_idx].iter().all(|info| { + matches!( + info.instr.real(), + Some( + Instruction::Nop + | Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadDeref { .. } + | Instruction::LoadGlobal { .. } + | Instruction::LoadConst { .. } + | Instruction::LoadSmallInt { .. } + | Instruction::LoadAttr { .. } + | Instruction::BinaryOp { .. } + | Instruction::ContainsOp { .. } + | Instruction::IsOp { .. } + | Instruction::CompareOp { .. } + | Instruction::ToBool + ) + ) + }) +} + fn reorder_conditional_exit_and_jump_blocks(blocks: &mut [Block]) { let mut current = BlockIdx(0); while current != BlockIdx::NULL { @@ -11822,7 +15720,9 @@ fn reorder_conditional_exit_and_jump_blocks(blocks: &mut [Block]) { .instructions .iter() .any(|info| matches!(info.instr.real(), Some(Instruction::EndAsyncFor))); - if after_jump_is_end_async_for { + let after_jump_is_adjacent_scope_exit = after_jump != BlockIdx::NULL + && is_pop_top_exit_like_block(&blocks[after_jump.idx()]); + if after_jump_is_end_async_for || after_jump_is_adjacent_scope_exit { current = next; continue; } @@ -11917,6 +15817,12 @@ fn reorder_conditional_jump_and_exit_blocks(blocks: &mut [Block]) { current = next; continue; } + if jump_instr.lineno_override.is_some_and(|line| line >= 0) + && instruction_lineno(&jump_instr) != instruction_lineno(&last) + { + current = next; + continue; + } let mut exit_end = BlockIdx::NULL; let mut exit_block = BlockIdx::NULL; @@ -11959,22 +15865,160 @@ fn reorder_conditional_jump_and_exit_blocks(blocks: &mut [Block]) { current = after_exit; } -} +} + +fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { + let target_comes_before = |target: BlockIdx, block: BlockIdx, blocks: &[Block]| -> bool { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + if current == target { + return true; + } + if current == block { + return false; + } + current = blocks[current.idx()].next; + } + false + }; + + fn is_single_delete_subscr_body(block: &Block) -> bool { + let real: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .collect(); + real.iter() + .filter(|instr| matches!(instr, Instruction::DeleteSubscr)) + .count() + == 1 + && matches!(real.last(), Some(Instruction::DeleteSubscr)) + && real.iter().all(|instr| { + matches!( + instr, + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadSmallInt { .. } + | Instruction::LoadConst { .. } + | Instruction::Copy { .. } + | Instruction::Swap { .. } + | Instruction::BinaryOp { .. } + | Instruction::BinarySlice + | Instruction::BuildSlice { .. } + | Instruction::StoreSubscr + | Instruction::DeleteSubscr + ) + }) + } + + fn chain_is_conditional_single_delete_body( + blocks: &[Block], + chain_start: BlockIdx, + jump_start: BlockIdx, + ) -> bool { + if chain_start == BlockIdx::NULL || jump_start == BlockIdx::NULL { + return false; + } + let Some(chain_cond_idx) = trailing_conditional_jump_index(&blocks[chain_start.idx()]) + else { + return false; + }; + let chain_cond = blocks[chain_start.idx()].instructions[chain_cond_idx]; + if !is_false_path_conditional_jump(&chain_cond.instr) || chain_cond.target != jump_start { + return false; + } + let body = next_nonempty_block(blocks, blocks[chain_start.idx()].next); + body != BlockIdx::NULL + && !block_is_exceptional(&blocks[body.idx()]) + && !block_is_protected(&blocks[body.idx()]) + && next_nonempty_block(blocks, blocks[body.idx()].next) == jump_start + && is_single_delete_subscr_body(&blocks[body.idx()]) + } + + fn jump_targets_for_iter(blocks: &[Block], jump_block: BlockIdx) -> bool { + if jump_block == BlockIdx::NULL { + return false; + } + let Some(info) = blocks[jump_block.idx()].instructions.first() else { + return false; + }; + let target = next_nonempty_block(blocks, info.target); + target != BlockIdx::NULL + && blocks[target.idx()] + .instructions + .first() + .is_some_and(|target_info| { + matches!(target_info.instr.real(), Some(Instruction::ForIter { .. })) + }) + } + + fn true_body_false_backedge_is_already_normalized( + blocks: &[Block], + conditional: InstructionInfo, + false_backedge: BlockIdx, + true_body: BlockIdx, + ) -> bool { + fn comes_before(blocks: &[Block], target: BlockIdx, block: BlockIdx) -> bool { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + if current == target { + return true; + } + if current == block { + return false; + } + current = blocks[current.idx()].next; + } + false + } + + if !matches!( + conditional.instr.real(), + Some(Instruction::PopJumpIfTrue { .. }) + ) || false_backedge == BlockIdx::NULL + || true_body == BlockIdx::NULL + || !is_jump_only_block(&blocks[false_backedge.idx()]) + { + return false; + } + let false_target = blocks[false_backedge.idx()].instructions[0].target; + if false_target == BlockIdx::NULL || !comes_before(blocks, false_target, false_backedge) { + return false; + } + let true_tail = next_nonempty_block(blocks, blocks[true_body.idx()].next); + true_tail != BlockIdx::NULL + && is_jump_only_block(&blocks[true_tail.idx()]) + && blocks[true_tail.idx()].instructions[0].target == false_target + } + + fn has_other_conditional_predecessor_to( + blocks: &[Block], + conditional_target_counts: &[usize], + target: BlockIdx, + current: BlockIdx, + ) -> bool { + let current_targets = blocks[current.idx()] + .instructions + .iter() + .filter(|info| info.target == target && is_conditional_jump(&info.instr)) + .count(); + conditional_target_counts[target.idx()] > current_targets + } -fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { - let target_comes_before = |target: BlockIdx, block: BlockIdx, blocks: &[Block]| -> bool { - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - if current == target { - return true; - } - if current == block { - return false; + let has_exceptional_lineno_sources = blocks + .iter() + .any(|block| block.cold || block_is_exceptional(block) || block_is_protected(block)); + let mut conditional_target_counts = vec![0usize; blocks.len()]; + for block in blocks.iter() { + for info in &block.instructions { + if info.target != BlockIdx::NULL && is_conditional_jump(&info.instr) { + conditional_target_counts[info.target.idx()] += 1; } - current = blocks[current.idx()].next; } - false - }; + } let mut current = BlockIdx(0); while current != BlockIdx::NULL { @@ -12000,6 +16044,10 @@ fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { current = next; continue; } + if true_body_false_backedge_is_already_normalized(blocks, last, chain_start, jump_start) { + current = next; + continue; + } let mut chain_has_suspension_point = false; let mut scan = chain_start; while scan != BlockIdx::NULL && scan != jump_start { @@ -12047,6 +16095,24 @@ fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { current = next; continue; } + if has_exceptional_lineno_sources + && is_generic_false_path_reorder + && has_exceptional_duplicate_lineno(blocks, current, instruction_lineno(&last)) + { + current = next; + continue; + } + if is_generic_false_path_reorder + && has_other_conditional_predecessor_to( + blocks, + &conditional_target_counts, + chain_start, + current, + ) + { + current = next; + continue; + } if block_is_protected(&blocks[idx]) && block_contains_suspension_point(&blocks[idx]) { current = next; continue; @@ -12100,7 +16166,12 @@ fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { current = next; continue; } - if is_generic_false_path_reorder && nonempty_blocks > 1 { + let chain_is_conditional_single_delete_body = + chain_is_conditional_single_delete_body(blocks, chain_start, jump_start); + if is_generic_false_path_reorder + && nonempty_blocks > 1 + && !chain_is_conditional_single_delete_body + { current = next; continue; } @@ -12135,8 +16206,36 @@ fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { current = next; continue; } - let after_jump = next_nonempty_block(blocks, blocks[jump_block.idx()].next); + let jump_is_artificial = blocks[jump_block.idx()] + .instructions + .first() + .is_some_and(|info| matches!(info.lineno_override, Some(line) if line < 0)); + let after_jump_is_adjacent_scope_exit = + after_jump != BlockIdx::NULL && is_pop_top_exit_like_block(&blocks[after_jump.idx()]); + if !is_generic_false_path_reorder + && chain_is_single_exit_block + && after_jump_is_adjacent_scope_exit + { + current = next; + continue; + } + if is_generic_false_path_reorder + && jump_is_artificial + && after_jump != BlockIdx::NULL + && is_loop_cleanup_block(&blocks[after_jump.idx()]) + && !chain_is_conditional_single_delete_body + { + current = next; + continue; + } + if !is_generic_false_path_reorder + && jump_is_artificial + && jump_targets_for_iter(blocks, jump_block) + { + current = next; + continue; + } if nonempty_blocks == 1 && !is_jump_only_block(&blocks[chain_start.idx()]) && after_jump != BlockIdx::NULL @@ -12154,8 +16253,13 @@ fn reorder_conditional_chain_and_jump_back_blocks(blocks: &mut Vec) { cloned_jump.start_depth = None; let cloned_idx = BlockIdx::new(blocks.len() as u32); blocks.push(cloned_jump); + conditional_target_counts.push(0); blocks[idx].next = cloned_idx; let cond_mut = &mut blocks[idx].instructions[cond_idx]; + if cond_mut.target != BlockIdx::NULL { + conditional_target_counts[cond_mut.target.idx()] -= 1; + } + conditional_target_counts[chain_start.idx()] += 1; cond_mut.instr = reversed; cond_mut.target = chain_start; @@ -12168,6 +16272,42 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( allow_for_iter_jump_targets: bool, allow_true_scope_exit_reorder: bool, ) { + fn retarget_empty_chain_targets( + blocks: &mut [Block], + chain_start: BlockIdx, + chain_end: BlockIdx, + replacement: BlockIdx, + ) { + if chain_start == BlockIdx::NULL + || chain_end == BlockIdx::NULL + || replacement == BlockIdx::NULL + { + return; + } + + let mut in_chain = vec![false; blocks.len()]; + let mut cursor = chain_start; + while cursor != BlockIdx::NULL && cursor != chain_end { + let block = &blocks[cursor.idx()]; + if !block.instructions.is_empty() { + return; + } + in_chain[cursor.idx()] = true; + cursor = block.next; + } + if cursor != chain_end { + return; + } + + for block in blocks { + for instr in &mut block.instructions { + if instr.target != BlockIdx::NULL && in_chain[instr.target.idx()] { + instr.target = replacement; + } + } + } + } + fn jump_targets_for_iter(blocks: &[Block], jump_block: BlockIdx) -> bool { if jump_block == BlockIdx::NULL { return false; @@ -12199,6 +16339,20 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( && jump_targets_for_iter(blocks, jump_block) } + fn is_explicit_continue_after_conditional( + blocks: &[Block], + jump_block: BlockIdx, + cond: InstructionInfo, + ) -> bool { + if !is_explicit_continue_to_for_iter(blocks, jump_block) { + return false; + } + let Some(info) = blocks[jump_block.idx()].instructions.first() else { + return false; + }; + instruction_lineno(info) > instruction_lineno(&cond) + } + fn is_explicit_non_for_jump_back(blocks: &[Block], jump_block: BlockIdx) -> bool { if jump_block == BlockIdx::NULL || jump_targets_for_iter(blocks, jump_block) { return false; @@ -12229,11 +16383,14 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( } else { BlockIdx::NULL }; + let after_jump_continues_conditional_chain = after_jump != BlockIdx::NULL + && block_is_pure_conditional_test(&blocks[after_jump.idx()]); if exit_start == BlockIdx::NULL || exit_block == BlockIdx::NULL || jump_start == BlockIdx::NULL || jump_block == BlockIdx::NULL || jump_block == exit_block + || after_jump_continues_conditional_chain || block_is_exceptional(&blocks[idx]) || block_is_exceptional(&blocks[jump_block.idx()]) || block_is_exceptional(&blocks[exit_block.idx()]) @@ -12249,11 +16406,13 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( }) || (!allow_for_iter_jump_targets && is_explicit_non_for_jump_back(blocks, jump_block)) - || (!allow_for_iter_jump_targets - && after_jump != BlockIdx::NULL + || (after_jump != BlockIdx::NULL + && is_pop_top_exit_like_block(&blocks[after_jump.idx()])) + || (after_jump != BlockIdx::NULL && !blocks[after_jump.idx()].cold && !is_scope_exit_block(&blocks[after_jump.idx()]) - && !is_loop_cleanup_block(&blocks[after_jump.idx()])) + && !is_loop_cleanup_block(&blocks[after_jump.idx()]) + && jump_targets_for_iter(blocks, jump_block)) || next_nonempty_block(blocks, blocks[exit_block.idx()].next) != jump_block { current = next; @@ -12285,7 +16444,7 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( || !is_scope_exit_block(&blocks[exit_block.idx()]) || !is_jump_only_block(&blocks[jump_block.idx()]) || (jump_targets_for_iter(blocks, jump_block) - && !is_explicit_continue_to_for_iter(blocks, jump_block)) + && !is_explicit_continue_after_conditional(blocks, jump_block, cond)) || next_nonempty_block(blocks, blocks[jump_block.idx()].next) != exit_block || !comes_before( blocks, @@ -12296,8 +16455,8 @@ fn reorder_conditional_scope_exit_and_jump_back_blocks( current = next; continue; } - let after_exit = blocks[exit_block.idx()].next; + retarget_empty_chain_targets(blocks, next, jump_block, jump_block); blocks[idx].instructions[cond_idx].instr = reversed_conditional(&cond.instr).expect("PopJumpIfTrue has a reversed conditional"); blocks[idx].instructions[cond_idx].target = jump_block; @@ -12439,6 +16598,67 @@ fn reorder_conditional_implicit_continue_scope_exit_blocks(blocks: &mut [Block]) } } + fn scope_exit_segment_tail_before_jump( + blocks: &[Block], + start: BlockIdx, + jump_start: BlockIdx, + ) -> Option { + let jump_block = next_nonempty_block(blocks, jump_start); + if jump_block == BlockIdx::NULL { + return None; + } + + let mut segment = Vec::new(); + let mut cursor = next_nonempty_block(blocks, start); + while cursor != BlockIdx::NULL && cursor != jump_block { + if segment.len() >= blocks.len() { + return None; + } + let block = &blocks[cursor.idx()]; + if block_is_exceptional(block) || block_is_protected(block) { + return None; + } + segment.push(cursor); + cursor = next_nonempty_block(blocks, block.next); + } + if cursor != jump_block || segment.is_empty() { + return None; + } + + let mut in_segment = vec![false; blocks.len()]; + for block_idx in &segment { + in_segment[block_idx.idx()] = true; + } + + let mut has_scope_exit = false; + for block_idx in &segment { + let block = &blocks[block_idx.idx()]; + if is_scope_exit_block(block) { + has_scope_exit = true; + continue; + } + + if block_has_fallthrough(block) { + let next = next_nonempty_block(blocks, block.next); + if next == BlockIdx::NULL || !in_segment[next.idx()] { + return None; + } + } + + for info in &block.instructions { + if info.target == BlockIdx::NULL { + continue; + } + let target = next_nonempty_block(blocks, info.target); + if target == BlockIdx::NULL || !in_segment[target.idx()] { + return None; + } + } + } + + has_scope_exit.then_some(*segment.last().expect("non-empty segment")) + } + let mut current = BlockIdx(0); while current != BlockIdx::NULL { let idx = current.idx(); @@ -12481,6 +16701,11 @@ fn reorder_conditional_implicit_continue_scope_exit_blocks(blocks: &mut [Block]) || next_nonempty_block(blocks, loop_exit_start) == after_jump }) }); + let jump_has_lineno = blocks[jump_block.idx()] + .instructions + .first() + .is_some_and(|info| instruction_lineno(info) >= 0); + let exit_segment_tail = scope_exit_segment_tail_before_jump(blocks, exit_start, jump_start); if exit_start == BlockIdx::NULL || exit_block == BlockIdx::NULL || jump_start == BlockIdx::NULL @@ -12492,34 +16717,42 @@ fn reorder_conditional_implicit_continue_scope_exit_blocks(blocks: &mut [Block]) || block_is_protected(&blocks[idx]) || block_is_protected(&blocks[exit_block.idx()]) || block_is_protected(&blocks[jump_block.idx()]) - || !is_scope_exit_block(&blocks[exit_block.idx()]) + || exit_segment_tail.is_none() || !is_jump_back_only_block(blocks, jump_block) - || blocks[jump_block.idx()] - .instructions - .first() - .is_some_and(|info| info.lineno_override.is_some_and(|lineno| lineno >= 0)) || jumps_to_for_iter || (after_jump != BlockIdx::NULL && !blocks[after_jump.idx()].cold - && !jump_exits_to_loop_exit) - || next_nonempty_block(blocks, blocks[exit_block.idx()].next) != jump_block + && !jump_exits_to_loop_exit + && !jump_has_lineno) { current = next; continue; } + let exit_segment_tail = exit_segment_tail.expect("checked above"); let after_jump = blocks[jump_block.idx()].next; blocks[idx].instructions[cond_idx].instr = reversed_conditional(&cond.instr).expect("PopJumpIfFalse has a reversed conditional"); blocks[idx].instructions[cond_idx].target = exit_start; blocks[idx].next = jump_start; blocks[jump_block.idx()].next = exit_start; - blocks[exit_block.idx()].next = after_jump; + blocks[exit_segment_tail.idx()].next = after_jump; current = next; } } -fn reorder_exception_handler_conditional_continue_raise_blocks(blocks: &mut [Block]) { +fn reorder_exception_handler_conditional_continue_scope_exit_blocks(blocks: &mut [Block]) { + fn handler_scope_exit_returns(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))) + && block + .instructions + .last() + .is_some_and(|info| matches!(info.instr.real(), Some(Instruction::ReturnValue))) + } + let mut current = BlockIdx(0); while current != BlockIdx::NULL { let idx = current.idx(); @@ -12547,6 +16780,13 @@ fn reorder_exception_handler_conditional_continue_raise_blocks(blocks: &mut [Blo } else { BlockIdx::NULL }; + let exit_raises = exit_block != BlockIdx::NULL + && blocks[exit_block.idx()] + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::RaiseVarargs { .. }))); + let exit_returns = + exit_block != BlockIdx::NULL && handler_scope_exit_returns(&blocks[exit_block.idx()]); if exit_start == BlockIdx::NULL || exit_block == BlockIdx::NULL || jump_start == BlockIdx::NULL @@ -12555,10 +16795,7 @@ fn reorder_exception_handler_conditional_continue_raise_blocks(blocks: &mut [Blo || exit_block == jump_block || !is_scope_exit_block(&blocks[exit_block.idx()]) || !is_jump_back_only_block(blocks, jump_block) - || !blocks[exit_block.idx()] - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::RaiseVarargs { .. }))) + || !(exit_raises || exit_returns) || !blocks[jump_target.idx()] .instructions .first() @@ -12594,6 +16831,33 @@ fn deduplicate_adjacent_jump_back_blocks(blocks: &mut [Block]) { } } + fn jump_back_lineno(block: &Block) -> Option { + let [info] = block.instructions.as_slice() else { + return None; + }; + matches!(info.instr.real(), Some(Instruction::JumpBackward { .. })) + .then(|| instruction_lineno(info)) + } + + fn has_protected_conditional_predecessor( + blocks: &[Block], + incoming_origins: &[Vec], + target: BlockIdx, + ) -> bool { + incoming_origins[target.idx()] + .iter() + .copied() + .any(|origin| { + blocks[origin.idx()].instructions.iter().any(|info| { + info.except_handler.is_some() + && is_conditional_jump(&info.instr) + && next_nonempty_block(blocks, info.target) == target + }) + }) + } + + let reachable = compute_reachable_blocks(blocks); + let incoming_origins = compute_incoming_origins(blocks, &reachable); let mut current = BlockIdx(0); while current != BlockIdx::NULL { let Some(target) = jump_back_target(&blocks[current.idx()]) else { @@ -12613,75 +16877,221 @@ fn deduplicate_adjacent_jump_back_blocks(blocks: &mut [Block]) { || block_is_exceptional(&blocks[duplicate.idx()]) || block_is_protected(&blocks[duplicate.idx()]) || jump_back_target(&blocks[duplicate.idx()]) != Some(target) + || jump_back_lineno(&blocks[duplicate.idx()]) + != jump_back_lineno(&blocks[current.idx()]) + || has_protected_conditional_predecessor(blocks, &incoming_origins, current) + || has_protected_conditional_predecessor(blocks, &incoming_origins, duplicate) { current = blocks[current.idx()].next; continue; } - for block in blocks.iter_mut() { - for info in &mut block.instructions { - if info.target == duplicate { - info.target = current; - } + for block in blocks.iter_mut() { + for info in &mut block.instructions { + if info.target == duplicate { + info.target = current; + } + } + } + current = blocks[current.idx()].next; + } +} + +fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut Vec) { + fn jump_back_target(blocks: &[Block], block_idx: BlockIdx) -> Option { + if block_idx == BlockIdx::NULL { + return None; + } + let instructions = blocks[block_idx.idx()].instructions.as_slice(); + let jump = match instructions { + [jump] if jump.instr.is_unconditional_jump() => jump, + [not_taken, jump] + if matches!(not_taken.instr.real(), Some(Instruction::NotTaken)) + && jump.instr.is_unconditional_jump() => + { + jump + } + _ => return None, + }; + if jump.target == BlockIdx::NULL + || !comes_before(blocks, next_nonempty_block(blocks, jump.target), block_idx) + { + return None; + } + Some(jump.target) + } + + fn find_body_tail( + blocks: &[Block], + body_start: BlockIdx, + false_jump: BlockIdx, + target: BlockIdx, + ) -> Option { + let mut cursor = body_start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && cursor != false_jump { + if visited[cursor.idx()] { + return None; + } + visited[cursor.idx()] = true; + if block_is_exceptional(&blocks[cursor.idx()]) { + return None; + } + if jump_back_target(blocks, cursor) == Some(target) { + return Some(cursor); + } + cursor = blocks[cursor.idx()].next; + } + None + } + + fn find_body_tail_before_jump( + blocks: &[Block], + body_start: BlockIdx, + jump_start: BlockIdx, + ) -> Option { + let mut cursor = body_start; + let mut tail = BlockIdx::NULL; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && cursor != jump_start { + if visited[cursor.idx()] { + return None; + } + visited[cursor.idx()] = true; + if block_is_exceptional(&blocks[cursor.idx()]) { + return None; + } + tail = cursor; + cursor = blocks[cursor.idx()].next; + } + (tail != BlockIdx::NULL && cursor == jump_start).then_some(tail) + } + + fn body_segment_contains_for_iter( + blocks: &[Block], + body_start: BlockIdx, + body_tail: BlockIdx, + ) -> bool { + let mut cursor = body_start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL { + if visited[cursor.idx()] { + return false; + } + visited[cursor.idx()] = true; + if blocks[cursor.idx()] + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::ForIter { .. }))) + { + return true; + } + if cursor == body_tail { + return false; + } + cursor = blocks[cursor.idx()].next; + } + false + } + + fn body_segment_contains_protected_block( + blocks: &[Block], + body_start: BlockIdx, + body_tail: BlockIdx, + ) -> bool { + let mut cursor = body_start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL { + if visited[cursor.idx()] { + return false; + } + visited[cursor.idx()] = true; + if block_is_protected(&blocks[cursor.idx()]) { + return true; + } + if cursor == body_tail { + return false; } + cursor = blocks[cursor.idx()].next; } - current = blocks[current.idx()].next; + false } -} -fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut [Block]) { - fn jump_back_target(blocks: &[Block], block_idx: BlockIdx) -> Option { - if block_idx == BlockIdx::NULL { - return None; - } - let instructions = blocks[block_idx.idx()].instructions.as_slice(); - let jump = match instructions { - [jump] if jump.instr.is_unconditional_jump() => jump, - [not_taken, jump] - if matches!(not_taken.instr.real(), Some(Instruction::NotTaken)) - && jump.instr.is_unconditional_jump() => + fn body_segment_contains_scope_exit( + blocks: &[Block], + body_start: BlockIdx, + body_tail: BlockIdx, + ) -> bool { + let mut cursor = body_start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL { + if visited[cursor.idx()] { + return false; + } + visited[cursor.idx()] = true; + if blocks[cursor.idx()] + .instructions + .iter() + .any(|info| info.instr.is_scope_exit()) { - jump + return true; } - _ => return None, - }; - if jump.target == BlockIdx::NULL - || !comes_before(blocks, next_nonempty_block(blocks, jump.target), block_idx) - { - return None; + if cursor == body_tail { + return false; + } + cursor = blocks[cursor.idx()].next; } - Some(jump.target) + false } - fn find_body_tail( + fn body_segment_contains_jump_back_to( blocks: &[Block], body_start: BlockIdx, - false_jump: BlockIdx, + body_tail: BlockIdx, target: BlockIdx, - ) -> Option { + ) -> bool { let mut cursor = body_start; let mut visited = vec![false; blocks.len()]; - while cursor != BlockIdx::NULL && cursor != false_jump { + while cursor != BlockIdx::NULL { if visited[cursor.idx()] { - return None; + return false; } visited[cursor.idx()] = true; - if block_is_exceptional(&blocks[cursor.idx()]) { - return None; - } if jump_back_target(blocks, cursor) == Some(target) { - return Some(cursor); + return true; + } + if cursor == body_tail { + return false; } cursor = blocks[cursor.idx()].next; } - None + false } - fn body_segment_contains_for_iter( + fn body_segment_contains_any_jump_back( blocks: &[Block], body_start: BlockIdx, body_tail: BlockIdx, ) -> bool { + fn jump_back_or_self_target(blocks: &[Block], block_idx: BlockIdx) -> Option { + if block_idx == BlockIdx::NULL { + return None; + } + let jump = blocks[block_idx.idx()].instructions.last()?; + if !jump.instr.is_unconditional_jump() { + return None; + } + if jump.target == BlockIdx::NULL { + return None; + } + let target = next_nonempty_block(blocks, jump.target); + if target == block_idx || comes_before(blocks, target, block_idx) { + Some(jump.target) + } else { + None + } + } + let mut cursor = body_start; let mut visited = vec![false; blocks.len()]; while cursor != BlockIdx::NULL { @@ -12689,11 +17099,7 @@ fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut [Block]) { return false; } visited[cursor.idx()] = true; - if blocks[cursor.idx()] - .instructions - .iter() - .any(|info| matches!(info.instr.real(), Some(Instruction::ForIter { .. }))) - { + if jump_back_or_self_target(blocks, cursor).is_some() { return true; } if cursor == body_tail { @@ -12704,6 +17110,128 @@ fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut [Block]) { false } + fn block_ends_with_jump_back_to( + blocks: &[Block], + block_idx: BlockIdx, + target: BlockIdx, + ) -> bool { + let Some(last) = blocks[block_idx.idx()].instructions.last() else { + return false; + }; + last.instr.is_unconditional_jump() + && last.target != BlockIdx::NULL + && next_nonempty_block(blocks, last.target) == next_nonempty_block(blocks, target) + && comes_before(blocks, next_nonempty_block(blocks, target), block_idx) + } + + fn conditional_jump_target_count(blocks: &[Block], target: BlockIdx) -> usize { + let target = next_nonempty_block(blocks, target); + blocks + .iter() + .flat_map(|block| &block.instructions) + .filter(|info| { + is_conditional_jump(&info.instr) + && info.target != BlockIdx::NULL + && next_nonempty_block(blocks, info.target) == target + }) + .count() + } + + fn empty_chain_reaches(blocks: &[Block], start: BlockIdx, target: BlockIdx) -> bool { + let mut cursor = start; + let mut visited = vec![false; blocks.len()]; + while cursor != BlockIdx::NULL && cursor != target { + if visited[cursor.idx()] + || block_is_exceptional(&blocks[cursor.idx()]) + || block_is_protected(&blocks[cursor.idx()]) + || !blocks[cursor.idx()].instructions.is_empty() + { + return false; + } + visited[cursor.idx()] = true; + cursor = blocks[cursor.idx()].next; + } + cursor == target + } + + fn block_starts_loop_cleanup(blocks: &[Block], block_idx: BlockIdx) -> bool { + block_idx != BlockIdx::NULL + && matches!( + blocks[block_idx.idx()] + .instructions + .first() + .and_then(|info| info.instr.real()), + Some(Instruction::EndFor | Instruction::PopIter) + ) + } + + fn is_single_delete_subscr_body(block: &Block) -> bool { + let real: Vec<_> = block + .instructions + .iter() + .filter_map(|info| info.instr.real()) + .filter(|instr| !matches!(instr, Instruction::Nop | Instruction::NotTaken)) + .collect(); + real.iter() + .filter(|instr| matches!(instr, Instruction::DeleteSubscr)) + .count() + == 1 + && matches!(real.last(), Some(Instruction::DeleteSubscr)) + && real.iter().all(|instr| { + matches!( + instr, + Instruction::LoadFast { .. } + | Instruction::LoadFastBorrow { .. } + | Instruction::LoadFastLoadFast { .. } + | Instruction::LoadFastBorrowLoadFastBorrow { .. } + | Instruction::LoadSmallInt { .. } + | Instruction::LoadConst { .. } + | Instruction::Copy { .. } + | Instruction::Swap { .. } + | Instruction::BinaryOp { .. } + | Instruction::BinarySlice + | Instruction::BuildSlice { .. } + | Instruction::StoreSubscr + | Instruction::DeleteSubscr + ) + }) + } + + fn block_has_call(block: &Block) -> bool { + block + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::Call { .. }))) + } + + fn protected_region_has_prior_scope_exit( + blocks: &[Block], + loop_target: BlockIdx, + current: BlockIdx, + ) -> bool { + let mut cursor = next_nonempty_block(blocks, loop_target); + let mut visited = vec![false; blocks.len()]; + let mut saw_protected = false; + while cursor != BlockIdx::NULL && cursor != current { + if visited[cursor.idx()] { + return false; + } + visited[cursor.idx()] = true; + let block = &blocks[cursor.idx()]; + saw_protected |= block_is_protected(block); + if saw_protected + && block + .instructions + .iter() + .any(|info| info.instr.is_scope_exit()) + { + return true; + } + cursor = block.next; + } + false + } + let mut current = BlockIdx(0); while current != BlockIdx::NULL { let idx = current.idx(); @@ -12713,6 +17241,136 @@ fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut [Block]) { continue; }; let cond = blocks[idx].instructions[cond_idx]; + let Some(reversed_cond) = reversed_conditional(&cond.instr) else { + current = next; + continue; + }; + + let true_jump_start = cond.target; + let true_jump = next_nonempty_block(blocks, true_jump_start); + let body_start = next; + let body = next_nonempty_block(blocks, body_start); + let true_jump_loop_target = jump_back_target(blocks, true_jump); + let normalized_forward_conditional = blocks[idx] + .instructions + .get(cond_idx + 1) + .is_some_and(|info| matches!(info.instr.real(), Some(Instruction::NotTaken))); + if true_jump_loop_target.is_some() + && (true_jump_start == true_jump + || empty_chain_reaches(blocks, true_jump_start, true_jump)) + && body_start != BlockIdx::NULL + && body != BlockIdx::NULL + && true_jump != BlockIdx::NULL + && !block_is_exceptional(&blocks[idx]) + && !block_is_exceptional(&blocks[true_jump.idx()]) + && !block_is_protected(&blocks[idx]) + && !block_is_protected(&blocks[true_jump.idx()]) + && next_nonempty_block(blocks, blocks[true_jump.idx()].next) != body + && let Some(body_tail) = find_body_tail_before_jump(blocks, body, true_jump_start) + { + let loop_target = true_jump_loop_target.expect("checked above"); + let after_jump = blocks[true_jump.idx()].next; + let body_tail_is_conditional = + trailing_conditional_jump_index(&blocks[body_tail.idx()]).is_some(); + let body_is_single_block = body == body_tail; + let body_has_scope_exit = body_segment_contains_scope_exit(blocks, body, body_tail); + let body_tail_has_scope_exit = is_scope_exit_block(&blocks[body_tail.idx()]); + let body_starts_conditional_chain = block_is_pure_conditional_test(&blocks[body.idx()]); + let body_tail_ends_with_loop_backedge = + block_ends_with_jump_back_to(blocks, body_tail, loop_target); + let body_has_loop_backedge = body_tail_ends_with_loop_backedge + || body_segment_contains_jump_back_to(blocks, body, body_tail, loop_target); + if body_has_scope_exit && !body_tail_has_scope_exit { + current = next; + continue; + } + let body_has_inner_for_iter = body_segment_contains_for_iter(blocks, body, body_tail); + let body_has_any_loop_backedge = body_has_inner_for_iter + && body_segment_contains_any_jump_back(blocks, body, body_tail); + let normalized_single_block_can_reorder = + !normalized_forward_conditional || !block_has_call(&blocks[body.idx()]); + let has_exceptional_duplicate_condition_line = + has_exceptional_duplicate_lineno(blocks, current, instruction_lineno(&cond)); + let has_prior_protected_scope_exit = + protected_region_has_prior_scope_exit(blocks, loop_target, current); + let after_jump_target = next_nonempty_block(blocks, after_jump); + let after_jump_starts_loop_cleanup = + block_starts_loop_cleanup(blocks, after_jump_target); + let after_jump_continues_conditional_chain = after_jump_target != BlockIdx::NULL + && block_is_pure_conditional_test(&blocks[after_jump_target.idx()]); + let body_is_pop_top_exit_like = + body_is_single_block && is_pop_top_exit_like_block(&blocks[body.idx()]); + if (body_has_scope_exit || body_is_pop_top_exit_like) + && after_jump_target != BlockIdx::NULL + && is_pop_top_exit_like_block(&blocks[after_jump_target.idx()]) + { + current = next; + continue; + } + if after_jump_continues_conditional_chain { + current = next; + continue; + } + let jump_target_has_multiple_conditional_predecessors = + conditional_jump_target_count(blocks, true_jump_start) > 1; + let simple_single_block_can_reorder = body_is_single_block + && !body_tail_is_conditional + && !has_exceptional_duplicate_condition_line + && after_jump_starts_loop_cleanup + && !body_has_scope_exit + && (!blocks[body.idx()] + .instructions + .iter() + .any(|info| info.instr.is_unconditional_jump()) + || body_tail_ends_with_loop_backedge) + && !after_jump_continues_conditional_chain + && matches!(cond.instr.real(), Some(Instruction::PopJumpIfFalse { .. })); + let trailing_implicit_continue_can_reorder = after_jump != BlockIdx::NULL + && next_nonempty_block(blocks, after_jump) != body + && (!after_jump_starts_loop_cleanup + || body_has_loop_backedge + || body_has_any_loop_backedge) + && !(body_has_scope_exit + && after_jump_target != BlockIdx::NULL + && is_scope_exit_block(&blocks[after_jump_target.idx()]) + && !body_starts_conditional_chain) + && !is_scope_exit_block(&blocks[body.idx()]); + let can_reorder = !has_exceptional_duplicate_condition_line + && !has_prior_protected_scope_exit + && (!jump_target_has_multiple_conditional_predecessors || body_tail_has_scope_exit) + && ((!body_tail_is_conditional + && ((normalized_single_block_can_reorder + && body_is_single_block + && matches!(cond.instr.real(), Some(Instruction::PopJumpIfTrue { .. }))) + || (body == body_tail + && is_single_delete_subscr_body(&blocks[body.idx()])) + || simple_single_block_can_reorder)) + || (trailing_implicit_continue_can_reorder + && ((body_has_scope_exit && body_tail_has_scope_exit) + || body_has_loop_backedge + || (after_jump_starts_loop_cleanup && body_has_any_loop_backedge)) + && !after_jump_continues_conditional_chain)); + if can_reorder && after_jump != BlockIdx::NULL && after_jump != body_start { + blocks[idx].instructions[cond_idx].instr = reversed_cond; + blocks[idx].instructions[cond_idx].target = body_start; + if body_tail_ends_with_loop_backedge && true_jump_start == true_jump { + blocks[idx].next = true_jump_start; + blocks[true_jump.idx()].next = body_start; + blocks[body_tail.idx()].next = after_jump; + } else { + let cloned_jump_idx = BlockIdx(blocks.len() as u32); + let mut cloned_jump = blocks[true_jump.idx()].clone(); + cloned_jump.next = body_start; + blocks.push(cloned_jump); + + blocks[idx].next = cloned_jump_idx; + blocks[body_tail.idx()].next = true_jump_start; + } + current = blocks[idx].next; + continue; + } + } + if !matches!(cond.instr.real(), Some(Instruction::PopJumpIfTrue { .. })) { current = next; continue; @@ -12747,12 +17405,27 @@ fn reorder_conditional_body_and_implicit_continue_blocks(blocks: &mut [Block]) { current = next; continue; }; - if !body_segment_contains_for_iter(blocks, body, body_tail) { + let after_body = blocks[body_tail.idx()].next; + if after_body == BlockIdx::NULL || after_body == false_jump_start { current = next; continue; } - let after_body = blocks[body_tail.idx()].next; - if after_body == BlockIdx::NULL || after_body == false_jump_start { + let body_contains_for_iter = body_segment_contains_for_iter(blocks, body, body_tail); + let simple_body_can_reorder = !block_has_call(&blocks[body.idx()]) + && jump_back_target(blocks, body_tail) == Some(loop_target); + let false_jump_starts_with_not_taken = blocks[false_jump.idx()] + .instructions + .first() + .is_some_and(|info| matches!(info.instr.real(), Some(Instruction::NotTaken))); + if simple_body_can_reorder && false_jump_starts_with_not_taken && !body_contains_for_iter { + current = next; + continue; + } + if (!body_contains_for_iter && !simple_body_can_reorder) + || body_segment_contains_protected_block(blocks, body, body_tail) + || trailing_conditional_jump_index(&blocks[body.idx()]).is_some() + || block_starts_loop_cleanup(blocks, next_nonempty_block(blocks, after_body)) + { current = next; continue; } @@ -12827,8 +17500,9 @@ fn reorder_jump_over_exception_cleanup_blocks(blocks: &mut [Block]) { cursor = blocks[cursor.idx()].next; continue; } - if !block_is_exceptional(&blocks[cursor.idx()]) - && !is_exception_cleanup_block(&blocks[cursor.idx()]) + if !(block_is_exceptional(&blocks[cursor.idx()]) + || is_exception_cleanup_block(&blocks[cursor.idx()]) + || blocks[cursor.idx()].cold && is_reraise_scope_exit_block(&blocks[cursor.idx()])) { cleanup_end = BlockIdx::NULL; break; @@ -13027,7 +17701,14 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec, predecessors: &mut Ve }; let target = next_nonempty_block(blocks, last.target); - if target == BlockIdx::NULL || !is_exit_without_lineno(blocks, target) { + if target == BlockIdx::NULL { + current = blocks[current.idx()].next; + continue; + } + let target_is_exit_without_lineno = is_exit_without_lineno(blocks, target); + let target_is_protected_eval_break = + is_eval_break_without_lineno(blocks, target) && last.except_handler.is_some(); + if !target_is_exit_without_lineno && !target_is_protected_eval_break { current = blocks[current.idx()].next; continue; } @@ -13076,7 +17757,8 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec, predecessors: &mut Ve current, target, )) - && is_exit_without_lineno(blocks, target) + && (is_exit_without_lineno(blocks, target) + || is_eval_break_without_lineno(blocks, target)) && let Some((location, end_location)) = propagation_location(last) && let Some(first) = blocks[target.idx()].instructions.first_mut() { @@ -13138,7 +17820,14 @@ fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) { target = blocks[target.idx()].next; } if target != BlockIdx::NULL - && predecessors[target.idx()] == 1 + && (predecessors[target.idx()] == 1 + || has_unique_jump_origin( + blocks, + &reachable, + &incoming_origins, + current, + target, + )) && let Some((location, end_location)) = prev_location && let Some(first) = blocks[target.idx()].instructions.first_mut() { @@ -13165,12 +17854,73 @@ fn find_layout_predecessor(blocks: &[Block], target: BlockIdx) -> BlockIdx { if blocks[current.idx()].next == target { return current; } - current = blocks[current.idx()].next; + current = blocks[current.idx()].next; + } + BlockIdx::NULL +} + +fn compute_layout_predecessors(blocks: &[Block]) -> Vec { + let mut predecessors = vec![BlockIdx::NULL; blocks.len()]; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let next = blocks[current.idx()].next; + if next != BlockIdx::NULL { + predecessors[next.idx()] = current; + } + current = next; + } + predecessors +} + +fn has_unique_fallthrough_origin( + blocks: &[Block], + reachable: &[bool], + incoming_origins: &[Vec], + source: BlockIdx, + target: BlockIdx, +) -> bool { + if source == BlockIdx::NULL + || target == BlockIdx::NULL + || !reachable[source.idx()] + || !block_has_fallthrough(&blocks[source.idx()]) + || next_nonempty_block(blocks, blocks[source.idx()].next) != target + { + return false; + } + + let chain_start = blocks[source.idx()].next; + let mut current = chain_start; + while current != target { + if current == BlockIdx::NULL { + return false; + } + if !blocks[current.idx()].instructions.is_empty() { + return false; + } + current = blocks[current.idx()].next; + } + + fn empty_chain_contains( + blocks: &[Block], + mut current: BlockIdx, + target: BlockIdx, + needle: BlockIdx, + ) -> bool { + while current != target { + if current == needle { + return true; + } + current = blocks[current.idx()].next; + } + false } - BlockIdx::NULL + + incoming_origins[target.idx()].iter().all(|&origin| { + origin == source || empty_chain_contains(blocks, chain_start, target, origin) + }) } -fn has_unique_fallthrough_origin( +fn has_unique_jump_origin( blocks: &[Block], reachable: &[bool], incoming_origins: &[Vec], @@ -13180,30 +17930,19 @@ fn has_unique_fallthrough_origin( if source == BlockIdx::NULL || target == BlockIdx::NULL || !reachable[source.idx()] - || !block_has_fallthrough(&blocks[source.idx()]) - || next_nonempty_block(blocks, blocks[source.idx()].next) != target + || !blocks[source.idx()] + .instructions + .last() + .is_some_and(|instr| is_jump_instruction(instr) && instr.target != BlockIdx::NULL) { return false; } - let mut allowed = vec![false; blocks.len()]; - allowed[source.idx()] = true; - - let mut current = blocks[source.idx()].next; - while current != BlockIdx::NULL && current != target { - if !blocks[current.idx()].instructions.is_empty() { - return false; - } - allowed[current.idx()] = true; - current = blocks[current.idx()].next; - } - if current != target { - return false; - } - - incoming_origins[target.idx()] - .iter() - .all(|origin| allowed[origin.idx()]) + incoming_origins[target.idx()].iter().all(|&origin| { + origin == source + || (blocks[origin.idx()].instructions.is_empty() + && next_nonempty_block(blocks, blocks[origin.idx()].next) == target) + }) } fn comes_before(blocks: &[Block], first: BlockIdx, second: BlockIdx) -> bool { @@ -13223,12 +17962,75 @@ fn comes_before(blocks: &[Block], first: BlockIdx, second: BlockIdx) -> bool { fn duplicate_shared_jump_back_targets(blocks: &mut Vec) { let predecessors = compute_predecessors(blocks); let mut clones = Vec::new(); + let mut lineful_clones_before_target = Vec::new(); + let mut block_order = vec![usize::MAX; blocks.len()]; + let mut current = BlockIdx(0); + let mut pos = 0usize; + while current != BlockIdx::NULL { + block_order[current.idx()] = pos; + pos += 1; + current = blocks[current.idx()].next; + } for target in 0..blocks.len() { let target = BlockIdx(target as u32); + if is_jump_back_only_block(blocks, target) + && instruction_lineno(&blocks[target.idx()].instructions[0]) >= 0 + { + let jump_target = + next_nonempty_block(blocks, blocks[target.idx()].instructions[0].target); + let layout_pred = find_layout_predecessor(blocks, target); + if jump_target != BlockIdx::NULL + && comes_before(blocks, jump_target, target) + && layout_pred != BlockIdx::NULL + && !block_has_fallthrough(&blocks[layout_pred.idx()]) + && predecessors[target.idx()] >= 2 + { + let target_location = blocks[target.idx()].instructions[0].location; + let target_end_location = blocks[target.idx()].instructions[0].end_location; + let target_follows_forward_jump = blocks[layout_pred.idx()] + .instructions + .last() + .is_some_and(|info| { + matches!(info.instr.real(), Some(Instruction::JumpForward { .. })) + }); + for block_idx in 0..blocks.len() { + let block_idx = BlockIdx(block_idx as u32); + for (instr_idx, info) in blocks[block_idx.idx()].instructions.iter().enumerate() + { + if !is_conditional_jump(&info.instr) + || info.target == BlockIdx::NULL + || next_nonempty_block(blocks, info.target) != target + || block_order[block_idx.idx()] >= block_order[target.idx()] + { + continue; + } + let jump_lineno = instruction_lineno(info); + if target_follows_forward_jump && target_location.line >= info.location.line + { + continue; + } + if jump_lineno >= 0 + && (info.location != target_location + || info.end_location != target_end_location) + { + lineful_clones_before_target.push((target, block_idx, instr_idx)); + } + } + } + } + } + let Some(jump_target) = shared_jump_back_target(&blocks[target.idx()]) else { continue; }; + if blocks[target.idx()] + .instructions + .iter() + .any(|info| matches!(info.instr.real(), Some(Instruction::PopExcept))) + { + continue; + } let jump_target = next_nonempty_block(blocks, jump_target); if jump_target == BlockIdx::NULL || !comes_before(blocks, jump_target, target) { @@ -13239,6 +18041,43 @@ fn duplicate_shared_jump_back_targets(blocks: &mut Vec) { } let layout_pred = find_layout_predecessor(blocks, target); + if layout_pred != BlockIdx::NULL + && (!block_has_fallthrough(&blocks[layout_pred.idx()]) + || next_nonempty_block(blocks, blocks[layout_pred.idx()].next) != target) + && predecessors[target.idx()] >= 2 + { + let mut jump_predecessors = Vec::new(); + for block_idx in 0..blocks.len() { + let block_idx = BlockIdx(block_idx as u32); + for (instr_idx, info) in blocks[block_idx.idx()].instructions.iter().enumerate() { + if !is_jump_instruction(info) || info.target == BlockIdx::NULL { + continue; + } + if next_nonempty_block(blocks, info.target) == target { + jump_predecessors.push((block_idx, instr_idx)); + } + } + } + if jump_predecessors.len() >= 2 + && jump_predecessors.iter().all(|(block_idx, instr_idx)| { + is_conditional_jump(&blocks[block_idx.idx()].instructions[*instr_idx].instr) + && block_order[block_idx.idx()] < block_order[target.idx()] + }) + && let Some((keep_block, keep_instr)) = jump_predecessors + .iter() + .max_by_key(|(block_idx, _)| block_order[block_idx.idx()]) + .copied() + { + for (block_idx, instr_idx) in jump_predecessors { + if block_idx == keep_block && instr_idx == keep_instr { + continue; + } + clones.push((target, block_idx, instr_idx)); + } + continue; + } + } + if layout_pred == BlockIdx::NULL || !block_has_fallthrough(&blocks[layout_pred.idx()]) || next_nonempty_block(blocks, blocks[layout_pred.idx()].next) != target @@ -13265,6 +18104,37 @@ fn duplicate_shared_jump_back_targets(blocks: &mut Vec) { } } + lineful_clones_before_target.sort_by_key(|(target, block_idx, _)| { + ( + block_order[target.idx()], + usize::MAX - block_order[block_idx.idx()], + ) + }); + for (target, block_idx, instr_idx) in lineful_clones_before_target { + if next_nonempty_block( + blocks, + blocks[block_idx.idx()].instructions[instr_idx].target, + ) != target + { + continue; + } + let jump = blocks[block_idx.idx()].instructions[instr_idx]; + let layout_pred = find_layout_predecessor(blocks, target); + if layout_pred == BlockIdx::NULL { + continue; + } + + let mut cloned = blocks[target.idx()].clone(); + if let Some(first) = cloned.instructions.first_mut() { + overwrite_location(first, jump.location, jump.end_location); + } + let new_idx = BlockIdx(blocks.len() as u32); + cloned.next = target; + blocks.push(cloned); + blocks[layout_pred.idx()].next = new_idx; + blocks[block_idx.idx()].instructions[instr_idx].target = new_idx; + } + for (target, block_idx, instr_idx) in clones.into_iter().rev() { let jump = blocks[block_idx.idx()].instructions[instr_idx]; let mut cloned = blocks[target.idx()].clone(); @@ -13281,6 +18151,94 @@ fn duplicate_shared_jump_back_targets(blocks: &mut Vec) { } } +fn duplicate_fallthrough_jump_back_targets(blocks: &mut Vec) { + fn block_has_real_fallthrough_body(block: &Block) -> bool { + block.instructions.iter().any(|info| { + !matches!( + info.instr.real(), + Some(Instruction::Nop | Instruction::NotTaken | Instruction::PopTop) + ) + }) + } + + let predecessors = compute_predecessors(blocks); + let mut clones = Vec::new(); + + let mut layout_pred = BlockIdx(0); + while layout_pred != BlockIdx::NULL { + if !block_has_fallthrough(&blocks[layout_pred.idx()]) + || !block_has_real_fallthrough_body(&blocks[layout_pred.idx()]) + { + layout_pred = blocks[layout_pred.idx()].next; + continue; + } + + let target = next_nonempty_block(blocks, blocks[layout_pred.idx()].next); + if target == BlockIdx::NULL + || predecessors[target.idx()] < 2 + || !is_jump_back_only_block(blocks, target) + { + layout_pred = blocks[layout_pred.idx()].next; + continue; + } + if blocks[target.idx()].instructions[0] + .lineno_override + .is_some_and(|lineno| lineno >= 0) + { + layout_pred = blocks[layout_pred.idx()].next; + continue; + } + if !block_has_no_lineno(&blocks[target.idx()]) + && trailing_conditional_jump_index(&blocks[layout_pred.idx()]).is_some() + { + layout_pred = blocks[layout_pred.idx()].next; + continue; + } + let jump_target = next_nonempty_block(blocks, blocks[target.idx()].instructions[0].target); + if jump_target == BlockIdx::NULL + || (!has_non_exception_loop_backedge_to(blocks, target, jump_target) + && !block_has_non_exception_loop_backedge_to(blocks, target, jump_target)) + { + layout_pred = blocks[layout_pred.idx()].next; + continue; + } + + let has_non_layout_jump_predecessor = blocks.iter().enumerate().any(|(idx, block)| { + let block_idx = BlockIdx(idx as u32); + block_idx != layout_pred + && block_idx != target + && block.instructions.iter().any(|info| { + is_jump_instruction(info) + && info.target != BlockIdx::NULL + && next_nonempty_block(blocks, info.target) == target + }) + }); + if has_non_layout_jump_predecessor { + clones.push((layout_pred, target)); + } + + layout_pred = blocks[layout_pred.idx()].next; + } + + for (layout_pred, target) in clones.into_iter().rev() { + if next_nonempty_block(blocks, blocks[layout_pred.idx()].next) != target { + continue; + } + let Some(last) = blocks[layout_pred.idx()].instructions.last().copied() else { + continue; + }; + + let new_idx = BlockIdx(blocks.len() as u32); + let mut cloned = blocks[target.idx()].clone(); + if let Some(first) = cloned.instructions.first_mut() { + overwrite_location(first, last.location, last.end_location); + } + cloned.next = blocks[layout_pred.idx()].next; + blocks.push(cloned); + blocks[layout_pred.idx()].next = new_idx; + } +} + /// Duplicate `LOAD_CONST None + RETURN_VALUE` for blocks that fall through /// to the final return block. fn duplicate_end_returns(blocks: &mut Vec, metadata: &CodeUnitMetadata) { @@ -13314,6 +18272,8 @@ fn duplicate_end_returns(blocks: &mut Vec, metadata: &CodeUnitMetadata) { AnyInstruction::Real(Instruction::LoadConst { .. }) ) && is_load_const_none(&last_insts[0], metadata) + && last_insts[0].no_location_exit + && last_insts[1].no_location_exit && matches!( last_insts[1].instr, AnyInstruction::Real(Instruction::ReturnValue) @@ -13384,7 +18344,9 @@ fn duplicate_end_returns(blocks: &mut Vec, metadata: &CodeUnitMetadata) { .last() .map(|instr| (instr.location, instr.end_location)); let mut cloned_return = return_insts.clone(); - if let Some((location, end_location)) = propagated_location { + if !instruction_has_lineno(&cloned_return[0]) + && let Some((location, end_location)) = propagated_location + { for instr in &mut cloned_return { overwrite_location(instr, location, end_location); } @@ -13427,6 +18389,127 @@ fn duplicate_end_returns(blocks: &mut Vec, metadata: &CodeUnitMetadata) { } } +fn inline_small_fast_return_blocks(blocks: &mut [Block]) { + fn block_is_small_fast_return(block: &Block) -> bool { + if block.instructions.len() > 3 { + return false; + } + let real: Vec<_> = block + .instructions + .iter() + .filter(|info| !matches!(info.instr.real(), Some(Instruction::Nop))) + .collect(); + matches!( + real.as_slice(), + [load, ret] + if matches!( + load.instr.real(), + Some(Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }) + ) && matches!(ret.instr.real(), Some(Instruction::ReturnValue)) + ) + } + + loop { + let mut changed = false; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let next = blocks[current.idx()].next; + let Some(last) = blocks[current.idx()].instructions.last().copied() else { + current = next; + continue; + }; + if !last.instr.is_unconditional_jump() || last.target == BlockIdx::NULL { + current = next; + continue; + } + let target = next_nonempty_block(blocks, last.target); + if target == BlockIdx::NULL || !block_is_small_fast_return(&blocks[target.idx()]) { + current = next; + continue; + } + + if jump_thread_kind(last.instr) == Some(JumpThreadKind::NoInterrupt) + || instruction_has_lineno(&last) + { + let last_instr = blocks[current.idx()].instructions.last_mut().unwrap(); + let lineno_override = last_instr.lineno_override; + set_to_nop(last_instr); + last_instr.lineno_override = lineno_override; + } else { + blocks[current.idx()].instructions.pop(); + } + let cloned = blocks[target.idx()].instructions.clone(); + blocks[current.idx()].instructions.extend(cloned); + changed = true; + current = next; + } + if !changed { + break; + } + } +} + +fn is_fast_store_load_return_block(block: &Block) -> bool { + let [store, load, ret] = block.instructions.as_slice() else { + return false; + }; + let stored = match store.instr.real() { + Some(Instruction::StoreFast { var_num }) => usize::from(var_num.get(store.arg)), + _ => return false, + }; + let loaded = match load.instr.real() { + Some(Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }) => { + usize::from(var_num.get(load.arg)) + } + _ => return false, + }; + stored == loaded && matches!(ret.instr.real(), Some(Instruction::ReturnValue)) +} + +fn inline_unprotected_tuple_genexpr_assignment_return_blocks(blocks: &mut [Block]) { + for block_idx in 0..blocks.len() { + if block_is_protected(&blocks[block_idx]) { + continue; + } + + let Some(jump_idx) = blocks[block_idx].instructions.len().checked_sub(1) else { + continue; + }; + let jump = blocks[block_idx].instructions[jump_idx]; + if !jump.instr.is_unconditional_jump() || jump.target == BlockIdx::NULL { + continue; + } + + let previous_is_list_to_tuple = blocks[block_idx].instructions[..jump_idx] + .iter() + .rev() + .find_map(|info| match info.instr.real() { + Some(Instruction::CallIntrinsic1 { func }) => { + Some(func.get(info.arg) == IntrinsicFunction1::ListToTuple) + } + Some(Instruction::Nop | Instruction::NotTaken) => None, + Some(_) => Some(false), + None => None, + }) + .unwrap_or(false); + if !previous_is_list_to_tuple { + continue; + } + + let target = next_nonempty_block(blocks, jump.target); + if target == BlockIdx::NULL || !is_fast_store_load_return_block(&blocks[target.idx()]) { + continue; + } + + let mut cloned = blocks[target.idx()].instructions.clone(); + if let Some(first) = cloned.first_mut() { + overwrite_location(first, jump.location, jump.end_location); + } + blocks[block_idx].instructions.pop(); + blocks[block_idx].instructions.extend(cloned); + } +} + fn inline_with_suppress_return_blocks(blocks: &mut [Block]) { fn has_with_suppress_prefix(block: &Block, jump_idx: usize) -> bool { let tail: Vec<_> = block.instructions[..jump_idx] @@ -13517,6 +18600,20 @@ fn duplicate_named_except_cleanup_returns(blocks: &mut Vec, metadata: &Co continue; } + let target_lineno = blocks[target.idx()] + .instructions + .first() + .map_or(-1, instruction_lineno); + let layout_pred_lineno = blocks[layout_pred.idx()] + .instructions + .iter() + .rev() + .find(|info| info.instr.real().is_some()) + .map_or(-1, instruction_lineno); + if target_lineno > 0 && layout_pred_lineno > 0 && target_lineno != layout_pred_lineno { + continue; + } + for block_idx in 0..blocks.len() { if block_idx == target.idx() { continue; @@ -13615,7 +18712,7 @@ fn inline_named_except_cleanup_normal_exit_jumps(blocks: &mut [Block]) { let target = next_nonempty_block(blocks, jump.target); if target == BlockIdx::NULL || target == BlockIdx(block_idx as u32) - || !is_named_except_cleanup_normal_exit_block(&blocks[target.idx()]) + || !is_standalone_named_except_cleanup_normal_exit_block(&blocks[target.idx()]) { continue; } @@ -13698,10 +18795,12 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) { stack.pop(); // POP_BLOCK → NOP let remove_no_location_nop = blocks[bi].instructions[i].remove_no_location_nop; + let folded_operand_nop = blocks[bi].instructions[i].folded_operand_nop; let preserve_block_start_no_location_nop = blocks[bi].instructions[i].preserve_block_start_no_location_nop; set_to_nop(&mut blocks[bi].instructions[i]); blocks[bi].instructions[i].remove_no_location_nop = remove_no_location_nop; + blocks[bi].instructions[i].folded_operand_nop = folded_operand_nop; blocks[bi].instructions[i].preserve_block_start_no_location_nop = preserve_block_start_no_location_nop; } else { @@ -13781,16 +18880,22 @@ pub(crate) fn convert_pseudo_ops(blocks: &mut [Block], cellfixedoffsets: &[u32]) PseudoInstruction::SetupCleanup { .. } | PseudoInstruction::SetupFinally { .. } | PseudoInstruction::SetupWith { .. } => { + let preserve_block_start_no_location_nop = + info.preserve_block_start_no_location_nop; set_to_nop(info); + info.preserve_block_start_no_location_nop = + preserve_block_start_no_location_nop; } // PopBlock in reachable blocks is converted to NOP by // label_exception_targets. Dead blocks may still have them. PseudoInstruction::PopBlock => { let remove_no_location_nop = info.remove_no_location_nop; + let folded_operand_nop = info.folded_operand_nop; let preserve_block_start_no_location_nop = info.preserve_block_start_no_location_nop; set_to_nop(info); info.remove_no_location_nop = remove_no_location_nop; + info.folded_operand_nop = folded_operand_nop; info.preserve_block_start_no_location_nop = preserve_block_start_no_location_nop; } @@ -13870,3 +18975,97 @@ pub(crate) fn fixup_deref_opargs(blocks: &mut [Block], cellfixedoffsets: &[u32]) } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn instruction_info(instr: Instruction, arg: u32, target: BlockIdx) -> InstructionInfo { + InstructionInfo { + instr: instr.into(), + arg: OpArg::new(arg), + target, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + folded_from_nonliteral_expr: false, + lineno_override: None, + cache_entries: 0, + preserve_redundant_jump_as_nop: false, + remove_no_location_nop: false, + folded_operand_nop: false, + no_location_exit: false, + preserve_block_start_no_location_nop: false, + match_success_jump: false, + } + } + + #[test] + fn short_circuit_stub_allows_only_marker_instructions_before_jump() { + let final_target = BlockIdx(7); + let block = Block { + instructions: vec![ + instruction_info(Instruction::Copy { i: Arg::marker() }, 1, BlockIdx::NULL), + instruction_info(Instruction::ToBool, 0, BlockIdx::NULL), + instruction_info(Instruction::Nop, 0, BlockIdx::NULL), + instruction_info(Instruction::NotTaken, 0, BlockIdx::NULL), + instruction_info( + Instruction::PopJumpIfFalse { + delta: Arg::marker(), + }, + 0, + final_target, + ), + ], + ..Block::default() + }; + + assert_eq!( + same_short_circuit_target( + &block, + Instruction::PopJumpIfFalse { + delta: Arg::marker(), + } + .into(), + ), + Some(final_target) + ); + } + + #[test] + fn short_circuit_stub_rejects_real_instruction_before_jump() { + let block = Block { + instructions: vec![ + instruction_info(Instruction::Copy { i: Arg::marker() }, 1, BlockIdx::NULL), + instruction_info(Instruction::ToBool, 0, BlockIdx::NULL), + instruction_info(Instruction::PopTop, 0, BlockIdx::NULL), + instruction_info( + Instruction::PopJumpIfFalse { + delta: Arg::marker(), + }, + 0, + BlockIdx(7), + ), + ], + ..Block::default() + }; + + assert_eq!( + same_short_circuit_target( + &block, + Instruction::PopJumpIfFalse { + delta: Arg::marker(), + } + .into(), + ), + None + ); + assert!(!opposite_short_circuit_target( + &block, + Instruction::PopJumpIfTrue { + delta: Arg::marker(), + } + .into() + )); + } +} diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index c1a318c19c..8d6ad98435 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -14,6 +14,7 @@ type IndexSet = indexmap::IndexSet; pub mod compile; pub mod error; pub mod ir; +mod preprocess; mod string_parser; pub mod symboltable; mod unparse; diff --git a/crates/codegen/src/preprocess.rs b/crates/codegen/src/preprocess.rs new file mode 100644 index 0000000000..ae2e65bf3f --- /dev/null +++ b/crates/codegen/src/preprocess.rs @@ -0,0 +1,225 @@ +use alloc::{boxed::Box, string::String, vec::Vec}; + +use ruff_python_ast::{ + self as ast, AtomicNodeIndex, ConversionFlag, Expr, ExprFString, FString, FStringFlags, + FStringValue, HasNodeIndex, InterpolatedElement, InterpolatedStringElement, + InterpolatedStringElements, InterpolatedStringFormatSpec, InterpolatedStringLiteralElement, + Operator, + visitor::transformer::{self, Transformer}, +}; +use ruff_text_size::{Ranged, TextRange}; + +const MAXDIGITS: usize = 3; +const F_LJUST: u8 = 1; + +pub(crate) fn preprocess_mod(module: &mut ast::Mod) { + let preprocessor = AstPreprocessor; + match module { + ast::Mod::Module(module) => preprocessor.visit_body(&mut module.body), + ast::Mod::Expression(expr) => preprocessor.visit_expr(&mut expr.body), + } +} + +struct AstPreprocessor; + +impl Transformer for AstPreprocessor { + fn visit_expr(&self, expr: &mut Expr) { + transformer::walk_expr(self, expr); + if let Some(optimized) = optimize_format(expr) { + *expr = optimized; + } + } +} + +fn optimize_format(expr: &Expr) -> Option { + let Expr::BinOp(binop) = expr else { + return None; + }; + if !matches!(binop.op, Operator::Mod) { + return None; + } + let Expr::StringLiteral(format) = binop.left.as_ref() else { + return None; + }; + let Expr::Tuple(tuple) = binop.right.as_ref() else { + return None; + }; + if tuple + .elts + .iter() + .any(|expr| matches!(expr, Expr::Starred(_))) + { + return None; + } + + let elements = parse_format(format.value.to_str(), &tuple.elts)?; + Some(Expr::FString(ExprFString { + node_index: binop.node_index.clone(), + range: binop.range, + value: FStringValue::single(FString { + range: binop.range, + node_index: binop.node_index.clone(), + elements: InterpolatedStringElements::from(elements), + flags: FStringFlags::empty(), + }), + })) +} + +fn parse_format(format: &str, args: &[Expr]) -> Option> { + let chars: Vec = format.chars().collect(); + let mut elements = Vec::with_capacity(args.len().saturating_mul(2).saturating_add(1)); + let mut pos = 0; + let mut arg_idx = 0; + + loop { + if let Some(literal) = parse_literal(&chars, &mut pos) { + elements.push(literal.into()); + } + if pos >= chars.len() { + break; + } + if arg_idx >= args.len() { + return None; + } + debug_assert_eq!(chars[pos], '%'); + pos += 1; + let formatted = parse_format_arg(&chars, &mut pos, args[arg_idx].clone())?; + elements.push(formatted.into()); + arg_idx += 1; + } + + (arg_idx == args.len()).then_some(elements) +} + +fn parse_literal(chars: &[char], pos: &mut usize) -> Option { + let start = *pos; + let mut has_percents = false; + while *pos < chars.len() { + if chars[*pos] != '%' { + *pos += 1; + } else if *pos + 1 < chars.len() && chars[*pos + 1] == '%' { + has_percents = true; + *pos += 2; + } else { + break; + } + } + if *pos == start { + return None; + } + + let mut value = String::new(); + let mut i = start; + while i < *pos { + if has_percents && chars[i] == '%' && i + 1 < *pos && chars[i + 1] == '%' { + value.push('%'); + i += 2; + } else { + value.push(chars[i]); + i += 1; + } + } + + Some(generated_literal(value)) +} + +fn parse_format_arg(chars: &[char], pos: &mut usize, arg: Expr) -> Option { + let (spec, flags, width, precision) = simple_format_arg_parse(chars, pos)?; + let conversion = match spec { + 's' => ConversionFlag::Str, + 'r' => ConversionFlag::Repr, + 'a' => ConversionFlag::Ascii, + _ => return None, + }; + + let mut format_spec = String::new(); + if flags & F_LJUST == 0 + && let Some(width) = width + && width > 0 + { + format_spec.push('>'); + } + if let Some(width) = width { + format_spec.push_str(&width.to_string()); + } + if let Some(precision) = precision { + format_spec.push('.'); + format_spec.push_str(&precision.to_string()); + } + + let range = arg.range(); + let format_spec = (!format_spec.is_empty()).then(|| { + Box::new(InterpolatedStringFormatSpec { + range: TextRange::default(), + node_index: AtomicNodeIndex::NONE, + elements: InterpolatedStringElements::from(vec![generated_literal(format_spec).into()]), + }) + }); + + Some(InterpolatedElement { + range, + node_index: arg.node_index().clone(), + expression: Box::new(arg), + debug_text: None, + conversion, + format_spec, + }) +} + +fn simple_format_arg_parse( + chars: &[char], + pos: &mut usize, +) -> Option<(char, u8, Option, Option)> { + let mut flags = 0; + let mut ch = next_char(chars, pos)?; + loop { + match ch { + '-' => flags |= F_LJUST, + '+' | ' ' | '#' | '0' => {} + _ => break, + } + ch = next_char(chars, pos)?; + } + + let width = parse_digits(chars, pos, &mut ch)?; + let precision = if ch == '.' { + ch = next_char(chars, pos)?; + Some(parse_digits(chars, pos, &mut ch)?.unwrap_or(0)) + } else { + None + }; + + Some((ch, flags, width, precision)) +} + +fn parse_digits(chars: &[char], pos: &mut usize, ch: &mut char) -> Option> { + if !ch.is_ascii_digit() { + return Some(None); + } + + let mut value = 0u16; + let mut digits = 0usize; + while ch.is_ascii_digit() { + value = value * 10 + (*ch as u16 - b'0' as u16); + *ch = next_char(chars, pos)?; + digits += 1; + if digits >= MAXDIGITS { + return None; + } + } + Some(Some(value)) +} + +fn next_char(chars: &[char], pos: &mut usize) -> Option { + let ch = chars.get(*pos).copied()?; + *pos += 1; + Some(ch) +} + +fn generated_literal(value: String) -> InterpolatedStringLiteralElement { + InterpolatedStringLiteralElement { + range: TextRange::default(), + node_index: AtomicNodeIndex::NONE, + value: value.into_boxed_str(), + } +} diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index 31da7c164e..4e762ff8b1 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -591,14 +591,7 @@ impl SymbolTableAnalyzer { } // Analyze symbols in current scope - let remove_owned_cells_from_free = matches!( - symbol_table.typ, - CompilerScope::Function - | CompilerScope::AsyncFunction - | CompilerScope::Lambda - | CompilerScope::Comprehension - | CompilerScope::Annotation - ); + let function_like_scope = SymbolTableBuilder::is_function_like_scope(symbol_table.typ); for symbol in symbol_table.symbols.values_mut() { self.analyze_symbol( symbol, @@ -611,7 +604,7 @@ impl SymbolTableAnalyzer { // CPython analyze_cells(): once a function-like scope owns a // child-requested name as a cell, that name is no longer free in // the enclosing scope. - if remove_owned_cells_from_free && symbol.scope == SymbolScope::Cell { + if function_like_scope && symbol.scope == SymbolScope::Cell { newfree.shift_remove(symbol.name.as_str()); } @@ -625,17 +618,9 @@ impl SymbolTableAnalyzer { // - only promote LOCAL -> CELL in function-like scopes, where // analyze_cells() runs. Module and class scopes keep their normal // scope and rely on DEF_COMP_CELL for comprehension-only cells. - let promote_inlined_cells_to_cell = matches!( - symbol_table.typ, - CompilerScope::Function - | CompilerScope::AsyncFunction - | CompilerScope::Lambda - | CompilerScope::Comprehension - | CompilerScope::Annotation - ); for symbol in symbol_table.symbols.values_mut() { if inlined_cells.contains(&symbol.name) - && promote_inlined_cells_to_cell + && function_like_scope && symbol.scope == SymbolScope::Local { symbol.scope = SymbolScope::Cell; @@ -647,6 +632,29 @@ impl SymbolTableAnalyzer { drop_class_free(symbol_table, &mut newfree); } + // CPython update_symbols(..., classflag): after class implicit frees + // are dropped, a class block, or an annotation/type-params block that + // can see a class scope, records existing child-free names with + // DEF_FREE_CLASS. This preserves the current scope's own lookup kind + // (for example GLOBAL_IMPLICIT via __classdict__) while still making + // the name available as a closure cell for nested children such as + // generator expressions. + if symbol_table.typ == CompilerScope::Class { + for name in &newfree { + if let Some(symbol) = symbol_table.symbols.get_mut(name) { + symbol.flags.insert(SymbolFlags::FREE_CLASS); + } + } + } else if symbol_table.can_see_class_scope { + for name in &newfree { + if let Some(symbol) = symbol_table.symbols.get_mut(name) + && !symbol.is_local() + { + symbol.flags.insert(SymbolFlags::FREE_CLASS); + } + } + } + Ok(newfree) } @@ -739,6 +747,21 @@ impl SymbolTableAnalyzer { self.found_in_inner_scope(sub_tables, &symbol.name, st_typ) .unwrap_or(SymbolScope::Local) } + } else if let Some(scope) = class_entry + .and_then(|class_symbols| class_symbols.get(&symbol.name)) + .and_then(|class_sym| { + if class_sym.flags.contains(SymbolFlags::GLOBAL) { + Some(SymbolScope::GlobalExplicit) + } else if class_sym.is_bound() && class_sym.scope != SymbolScope::Free { + // If name is bound in enclosing class, use GlobalImplicit + // so it can be accessed via __classdict__ + Some(SymbolScope::GlobalImplicit) + } else { + None + } + }) + { + scope } else if let Some(scope) = self.found_in_outer_scope( &symbol.name, st_typ, @@ -746,14 +769,6 @@ impl SymbolTableAnalyzer { ) { // If found in enclosing scope (function/TypeParams), use that scope - } else if let Some(class_symbols) = class_entry - && let Some(class_sym) = class_symbols.get(&symbol.name) - && class_sym.is_bound() - && class_sym.scope != SymbolScope::Free - { - // If name is bound in enclosing class, use GlobalImplicit - // so it can be accessed via __classdict__ - SymbolScope::GlobalImplicit } else if self.tables.is_empty() { // Don't make assumptions when we don't know. SymbolScope::Unknown @@ -880,7 +895,10 @@ impl SymbolTableAnalyzer { return self.found_in_inner_scope(&st.sub_tables, name, st_typ); } let sym = st.symbols.get(name)?; - if sym.scope == SymbolScope::Free || sym.flags.contains(SymbolFlags::FREE_CLASS) { + if sym.scope == SymbolScope::Free + || (sym.flags.contains(SymbolFlags::FREE_CLASS) + && !matches!(st_typ, CompilerScope::Module)) + { if st_typ == CompilerScope::Class && name != "__class__" { None } else { @@ -1030,6 +1048,9 @@ struct SymbolTableBuilder { in_iter_def_exp: bool, // Track if we're inside an annotation (yield/await/named expr not allowed) in_annotation: bool, + // CPython's ste_in_unevaluated_annotation: function-local AnnAssign + // annotations are not executed and do not contribute name bindings. + in_unevaluated_annotation: bool, // Track if we're inside a type alias (yield/await/named expr not allowed) in_type_alias: bool, // Track if we're scanning an inner loop iteration target (not the first generator) @@ -1064,6 +1085,7 @@ impl SymbolTableBuilder { varnames_stack: Vec::new(), in_iter_def_exp: false, in_annotation: false, + in_unevaluated_annotation: false, in_type_alias: false, in_comp_inner_loop_target: false, scope_info: None, @@ -1073,6 +1095,17 @@ impl SymbolTableBuilder { this } + fn is_function_like_scope(typ: CompilerScope) -> bool { + matches!( + typ, + CompilerScope::Function + | CompilerScope::AsyncFunction + | CompilerScope::Lambda + | CompilerScope::Comprehension + | CompilerScope::Annotation + ) + } + fn finish(mut self) -> Result { assert_eq!(self.tables.len(), 1); let mut symbol_table = self.tables.pop().unwrap(); @@ -1328,24 +1361,23 @@ impl SymbolTableBuilder { is_ann_assign: bool, ) -> SymbolTableResult { let current_scope = self.tables.last().map(|t| t.typ); - let needs_future_annotation_bookkeeping = is_ann_assign - && self.future_annotations - && matches!( - current_scope, - Some(CompilerScope::Module | CompilerScope::Class) - ); - let needs_non_future_conditional_annotations = is_ann_assign - && !self.future_annotations + let is_unevaluated = is_ann_assign + && current_scope.is_some_and(|scope| { + matches!( + scope, + CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda + ) + }); + let needs_conditional_annotations = is_ann_assign && (matches!(current_scope, Some(CompilerScope::Module)) || (matches!(current_scope, Some(CompilerScope::Class)) && self.in_conditional_block)); - let should_register_conditional_annotations = needs_future_annotation_bookkeeping - || (needs_non_future_conditional_annotations - && !self.tables.last().unwrap().has_conditional_annotations); + let should_register_conditional_annotations = needs_conditional_annotations + && !self.tables.last().unwrap().has_conditional_annotations; // PEP 649: Only AnnAssign annotations can be conditional. // Function parameter/return annotations are never conditional. - if needs_non_future_conditional_annotations { + if needs_conditional_annotations { self.tables.last_mut().unwrap().has_conditional_annotations = true; } @@ -1377,9 +1409,12 @@ impl SymbolTableBuilder { // PEP 649: scan expression for symbol references // Class annotations are evaluated in class locals (not module globals) let was_in_annotation = self.in_annotation; + let was_in_unevaluated_annotation = self.in_unevaluated_annotation; self.in_annotation = true; + self.in_unevaluated_annotation = is_unevaluated; let result = self.scan_expression(annotation, ExpressionContext::Load); self.in_annotation = was_in_annotation; + self.in_unevaluated_annotation = was_in_unevaluated_annotation; self.leave_annotation_scope(); @@ -1467,7 +1502,11 @@ impl SymbolTableBuilder { parameters, self.line_index_start(*range), has_return_annotation, - *is_async, + if *is_async { + CompilerScope::AsyncFunction + } else { + CompilerScope::Function + }, has_type_params, // skip_defaults: already scanned above )?; self.scan_statements(body)?; @@ -2096,33 +2135,35 @@ impl SymbolTableBuilder { self.check_name(id, context, *range)?; - // Determine the contextual usage of this symbol: - match context { - ExpressionContext::Delete => { - self.register_name(id, SymbolUsage::Assigned, *range)?; - self.register_name(id, SymbolUsage::Used, *range)?; - } - ExpressionContext::Load | ExpressionContext::IterDefinitionExp => { - self.register_name(id, SymbolUsage::Used, *range)?; - } - ExpressionContext::Store => { - self.register_name(id, SymbolUsage::Assigned, *range)?; + if !self.in_unevaluated_annotation { + // Determine the contextual usage of this symbol: + match context { + ExpressionContext::Delete => { + self.register_name(id, SymbolUsage::Assigned, *range)?; + self.register_name(id, SymbolUsage::Used, *range)?; + } + ExpressionContext::Load | ExpressionContext::IterDefinitionExp => { + self.register_name(id, SymbolUsage::Used, *range)?; + } + ExpressionContext::Store => { + self.register_name(id, SymbolUsage::Assigned, *range)?; + } + ExpressionContext::Iter => { + self.register_name(id, SymbolUsage::Iter, *range)?; + } } - ExpressionContext::Iter => { - self.register_name(id, SymbolUsage::Iter, *range)?; + // Interesting stuff about the __class__ variable: + // https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object + if context == ExpressionContext::Load + && matches!( + self.tables.last().unwrap().typ, + CompilerScope::Function | CompilerScope::AsyncFunction + ) + && id == "super" + { + self.register_name("__class__", SymbolUsage::Used, *range)?; } } - // Interesting stuff about the __class__ variable: - // https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object - if context == ExpressionContext::Load - && matches!( - self.tables.last().unwrap().typ, - CompilerScope::Function | CompilerScope::AsyncFunction - ) - && id == "super" - { - self.register_name("__class__", SymbolUsage::Used, *range)?; - } } Expr::Lambda(ExprLambda { body, @@ -2136,10 +2177,9 @@ impl SymbolTableBuilder { parameters, self.line_index_start(expression.range()), false, // lambdas have no return annotation - false, // lambdas are never async + CompilerScope::Lambda, false, // don't skip defaults )?; - self.tables.last_mut().unwrap().typ = CompilerScope::Lambda; } else { self.enter_scope( "lambda", @@ -2228,6 +2268,7 @@ impl SymbolTableBuilder { self.check_name(id, ExpressionContext::Store, *range)?; let table = self.tables.last().unwrap(); if table.typ == CompilerScope::Comprehension { + self.extend_namedexpr_scope(id, *range)?; self.register_name( id, SymbolUsage::AssignedNamedExprInComprehension, @@ -2549,7 +2590,7 @@ impl SymbolTableBuilder { parameters: &ast::Parameters, line_number: u32, has_return_annotation: bool, - is_async: bool, + scope_type: CompilerScope, skip_defaults: bool, ) -> SymbolTableResult { // Evaluate eventual default parameters (unless already scanned before type_param_block): @@ -2609,11 +2650,6 @@ impl SymbolTableBuilder { None }; - let scope_type = if is_async { - CompilerScope::AsyncFunction - } else { - CompilerScope::Function - }; self.enter_scope(name, scope_type, line_number); // Move annotation_block to function scope only if we have one @@ -2669,6 +2705,127 @@ impl SymbolTableBuilder { Ok(()) } + fn add_varname_to_scope(&mut self, table_idx: usize, name: &str) { + let varnames = if table_idx + 1 == self.tables.len() { + &mut self.current_varnames + } else { + &mut self.varnames_stack[table_idx + 1] + }; + if !varnames.iter().any(|existing| existing == name) { + varnames.push(name.to_owned()); + } + } + + // Mirrors CPython symtable_extend_namedexpr_scope(): assignment expressions + // inside comprehensions bind in the nearest function/module-like scope, not + // in the synthetic comprehension scope itself. + fn extend_namedexpr_scope(&mut self, name: &str, range: TextRange) -> SymbolTableResult { + let location = Some( + self.source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8), + ); + + for table_idx in (0..self.tables.len()).rev() { + let table_type = self.tables[table_idx].typ; + let mangled = maybe_mangle_name( + self.class_name.as_deref(), + self.tables[table_idx].mangled_names.as_ref(), + name, + ) + .into_owned(); + + if table_type == CompilerScope::Comprehension { + if self.tables[table_idx] + .symbols + .get(mangled.as_str()) + .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::ITER)) + { + return Err(SymbolTableError { + error: format!( + "assignment expression cannot rebind comprehension iteration variable '{}'", + mangled + ), + location, + }); + } + continue; + } + + match table_type { + CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda => { + let current_comp_inlined = self.tables.last().is_some_and(|table| { + table.typ == CompilerScope::Comprehension && table.comp_inlined + }); + let parent_is_global = self.tables[table_idx] + .symbols + .get(mangled.as_str()) + .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::GLOBAL)); + let current = self.tables.last_mut().unwrap(); + let current_symbol = current + .symbols + .entry(mangled.clone()) + .or_insert_with(|| Symbol::new(mangled.as_str())); + if parent_is_global { + current_symbol.flags.insert(SymbolFlags::GLOBAL); + current_symbol.scope = SymbolScope::GlobalExplicit; + } else { + current_symbol.flags.insert(SymbolFlags::NONLOCAL); + current_symbol.scope = SymbolScope::Free; + } + + let symbol = self.tables[table_idx] + .symbols + .entry(mangled.clone()) + .or_insert_with(|| Symbol::new(mangled.as_str())); + symbol.flags.insert(SymbolFlags::ASSIGNED); + if !parent_is_global && current_comp_inlined { + self.add_varname_to_scope(table_idx, mangled.as_str()); + } + return Ok(()); + } + CompilerScope::Module => { + let current = self.tables.last_mut().unwrap(); + let current_symbol = current + .symbols + .entry(mangled.clone()) + .or_insert_with(|| Symbol::new(mangled.as_str())); + current_symbol.flags.insert(SymbolFlags::GLOBAL); + current_symbol.scope = SymbolScope::GlobalExplicit; + + let symbol = self.tables[table_idx] + .symbols + .entry(mangled.clone()) + .or_insert_with(|| Symbol::new(mangled.as_str())); + symbol.flags.insert(SymbolFlags::GLOBAL); + symbol.scope = SymbolScope::GlobalExplicit; + return Ok(()); + } + CompilerScope::Class => { + return Err(SymbolTableError { + error: "assignment expression within a comprehension cannot be used in a class body".to_string(), + location, + }); + } + CompilerScope::TypeParams => { + return Err(SymbolTableError { + error: "assignment expression within a comprehension cannot be used within the definition of a generic".to_string(), + location, + }); + } + CompilerScope::Annotation => { + return Err(SymbolTableError { + error: "named expression cannot be used within an annotation".to_string(), + location, + }); + } + CompilerScope::Comprehension => unreachable!(), + } + } + + unreachable!("named expression scope extension requires an enclosing scope") + } + fn register_name( &mut self, name: &str,