From d0cb8e1c64ba580b30d384b331536a60b62d7394 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 13:23:51 +0900 Subject: [PATCH 1/7] Bytecode parity Compiler changes: - Remove PUSH_NULL from decorator cal ls, use CALL 0 - Collect __static_attributes__ from self.xxx = patterns - Sort __static_attributes__ alphabetically - Move __classdict__ init before __doc__ in class prologue - Fold unary negative constants - Fold constant list/set literals (3+ elements) - Use BUILD_MAP 0 + MAP_ADD for 16+ dict pairs - Always run peephole optimizer for s uperinstructions - Emit RETURN_GENERATOR for generator functions - Add is_generator flag to SymbolTabl e --- crates/codegen/src/compile.rs | 177 ++++++++++--- crates/codegen/src/ir.rs | 248 +++++++++++++++++- ...thon_codegen__compile__tests__if_ands.snap | 36 +-- ...hon_codegen__compile__tests__if_mixed.snap | 46 ++-- ...ython_codegen__compile__tests__if_ors.snap | 34 +-- ...pile__tests__nested_double_async_with.snap | 123 +++++---- crates/codegen/src/symboltable.rs | 6 + crates/vm/src/frame.rs | 1 - 8 files changed, 494 insertions(+), 177 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index a4be3fe756c..5ac8369a4fe 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1200,8 +1200,10 @@ impl Compiler { /// Emit RESUME instruction with proper handling for async preamble and module lineno. /// codegen_enter_scope equivalent for RESUME emission. fn emit_resume_for_scope(&mut self, scope_type: CompilerScope, lineno: u32) { - // For async functions/coroutines, emit RETURN_GENERATOR + POP_TOP before RESUME - if scope_type == CompilerScope::AsyncFunction { + // For generators and async functions, emit RETURN_GENERATOR + POP_TOP before RESUME + let is_gen = scope_type == CompilerScope::AsyncFunction + || self.current_symbol_table().is_generator; + if is_gen { emit!(self, Instruction::ReturnGenerator); emit!(self, Instruction::PopTop); } @@ -2758,18 +2760,15 @@ impl Compiler { fn prepare_decorators(&mut self, decorator_list: &[ast::Decorator]) -> CompileResult<()> { for decorator in decorator_list { self.compile_expression(&decorator.expression)?; - emit!(self, Instruction::PushNull); } Ok(()) } - /// Apply decorators in reverse order (LIFO from stack). - /// Stack [dec1, NULL, dec2, NULL, func] -> dec2(func) -> dec1(dec2(func)) - /// The forward loop works because each Call pops from TOS, naturally - /// applying decorators bottom-up (innermost first). + /// Apply decorators: each decorator calls the function below it. + /// Stack: [dec1, dec2, func] → CALL 0 → [dec1, dec2(func)] → CALL 0 → [dec1(dec2(func))] fn apply_decorators(&mut self, decorator_list: &[ast::Decorator]) { for _ in decorator_list { - emit!(self, Instruction::Call { argc: 1 }); + emit!(self, Instruction::Call { argc: 0 }); } } @@ -4510,6 +4509,100 @@ impl Compiler { Ok(()) } + /// Collect attribute names assigned via `self.xxx = ...` in methods. + /// These are stored as __static_attributes__ in the class dict. + fn collect_static_attributes(body: &[ast::Stmt], attrs: Option<&mut IndexSet>) { + let Some(attrs) = attrs else { return }; + for stmt in body { + // Only scan def/async def at class body level + let (params, func_body) = match stmt { + ast::Stmt::FunctionDef(f) => (&f.parameters, &f.body), + _ => continue, + }; + // Get first parameter name (usually "self" or "cls") + let first_param = params + .args + .first() + .or(params.posonlyargs.first()) + .map(|p| &p.parameter.name); + let Some(self_name) = first_param else { + continue; + }; + // Scan function body for self.xxx = ... (STORE_ATTR on first param) + Self::scan_store_attrs(func_body, self_name.as_str(), attrs); + } + } + + /// Recursively scan statements for `name.attr = value` patterns. + fn scan_store_attrs(stmts: &[ast::Stmt], name: &str, attrs: &mut IndexSet) { + for stmt in stmts { + match stmt { + ast::Stmt::Assign(a) => { + for target in &a.targets { + if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = target + { + if let ast::Expr::Name(n) = value.as_ref() { + if n.id.as_str() == name { + attrs.insert(attr.to_string()); + } + } + } + } + } + ast::Stmt::AnnAssign(a) => { + if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = + a.target.as_ref() + { + if let ast::Expr::Name(n) = value.as_ref() { + if n.id.as_str() == name { + attrs.insert(attr.to_string()); + } + } + } + } + ast::Stmt::AugAssign(a) => { + if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = + a.target.as_ref() + { + if let ast::Expr::Name(n) = value.as_ref() { + if n.id.as_str() == name { + attrs.insert(attr.to_string()); + } + } + } + } + ast::Stmt::If(s) => { + Self::scan_store_attrs(&s.body, name, attrs); + for clause in &s.elif_else_clauses { + Self::scan_store_attrs(&clause.body, name, attrs); + } + } + ast::Stmt::For(s) => { + Self::scan_store_attrs(&s.body, name, attrs); + Self::scan_store_attrs(&s.orelse, name, attrs); + } + ast::Stmt::While(s) => { + Self::scan_store_attrs(&s.body, name, attrs); + Self::scan_store_attrs(&s.orelse, name, attrs); + } + ast::Stmt::Try(s) => { + Self::scan_store_attrs(&s.body, name, attrs); + for handler in &s.handlers { + if let ast::ExceptHandler::ExceptHandler(h) = handler { + Self::scan_store_attrs(&h.body, name, attrs); + } + } + Self::scan_store_attrs(&s.orelse, name, attrs); + Self::scan_store_attrs(&s.finalbody, name, attrs); + } + ast::Stmt::With(s) => { + Self::scan_store_attrs(&s.body, name, attrs); + } + _ => {} + } + } + } + // Python/compile.c find_ann fn find_ann(body: &[ast::Stmt]) -> bool { for statement in body { @@ -4617,6 +4710,13 @@ impl Compiler { } ); + // PEP 649: Initialize __classdict__ cell (before __doc__) + if self.current_symbol_table().needs_classdict { + emit!(self, Instruction::LoadLocals); + let classdict_idx = self.get_cell_var_index("__classdict__")?; + 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() }); @@ -4645,13 +4745,6 @@ impl Compiler { ); } - // PEP 649: Initialize __classdict__ cell for class annotation scope - if self.current_symbol_table().needs_classdict { - emit!(self, Instruction::LoadLocals); - let classdict_idx = self.get_cell_var_index("__classdict__")?; - emit!(self, Instruction::StoreDeref { i: classdict_idx }); - } - // Handle class annotations based on future_annotations flag if Self::find_ann(body) { if self.future_annotations { @@ -4669,6 +4762,16 @@ impl Compiler { } } + // Collect __static_attributes__: scan methods for self.xxx = ... patterns + Self::collect_static_attributes( + body, + self.code_stack + .last_mut() + .unwrap() + .static_attributes + .as_mut(), + ); + // 3. Compile the class body self.compile_statements(body)?; @@ -4684,7 +4787,7 @@ impl Compiler { // Emit __static_attributes__ tuple { - let attrs: Vec = self + let mut attrs: Vec = self .code_stack .last() .unwrap() @@ -4692,6 +4795,7 @@ impl Compiler { .as_ref() .map(|s| s.iter().cloned().collect()) .unwrap_or_default(); + attrs.sort(); self.emit_load_const(ConstantData::Tuple { elements: attrs .into_iter() @@ -5091,8 +5195,7 @@ impl Compiler { method: SpecialMethod::AEnter } ); // [bound_aexit, bound_aenter] - // bound_aenter is already bound, call with NULL self_or_null - emit!(self, Instruction::PushNull); // [bound_aexit, bound_aenter, NULL] + emit!(self, Instruction::PushNull); emit!(self, Instruction::Call { argc: 0 }); // [bound_aexit, awaitable] emit!(self, Instruction::GetAwaitable { r#where: 1 }); self.emit_load_const(ConstantData::None); @@ -5112,8 +5215,7 @@ impl Compiler { method: SpecialMethod::Enter } ); // [bound_exit, bound_enter] - // bound_enter is already bound, call with NULL self_or_null - emit!(self, Instruction::PushNull); // [bound_exit, bound_enter, NULL] + emit!(self, Instruction::PushNull); emit!(self, Instruction::Call { argc: 0 }); // [bound_exit, enter_result] } @@ -5168,8 +5270,8 @@ impl Compiler { }); // ===== Normal exit path ===== - // Stack: [..., __exit__] - // Call __exit__(None, None, None) + // Stack: [..., bound_exit] + // Call bound_exit(None, None, None) self.set_source_range(with_range); emit!(self, Instruction::PushNull); self.emit_load_const(ConstantData::None); @@ -6894,17 +6996,28 @@ impl Compiler { let has_unpacking = items.iter().any(|item| item.key.is_none()); if !has_unpacking { - // Simple case: no ** unpacking, build all pairs directly - for item in items { - self.compile_expression(item.key.as_ref().unwrap())?; - self.compile_expression(&item.value)?; - } - emit!( - self, - Instruction::BuildMap { - count: u32::try_from(items.len()).expect("too many dict items"), + // STACK_USE_GUIDELINE: for large dicts (16+ pairs), use + // BUILD_MAP 0 + MAP_ADD to avoid excessive stack usage + let big = items.len() * 2 > 30; // ~15 pairs threshold + if big { + emit!(self, Instruction::BuildMap { count: 0 }); + for item in items { + self.compile_expression(item.key.as_ref().unwrap())?; + self.compile_expression(&item.value)?; + emit!(self, Instruction::MapAdd { i: 1 }); } - ); + } else { + for item in items { + self.compile_expression(item.key.as_ref().unwrap())?; + self.compile_expression(&item.value)?; + } + emit!( + self, + Instruction::BuildMap { + count: u32::try_from(items.len()).expect("too many dict items"), + } + ); + } return Ok(()); } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index a9923bd35be..c4a5df449a2 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -189,17 +189,19 @@ impl CodeInfo { mut self, opts: &crate::compile::CompileOpts, ) -> crate::InternalResult { - // Always fold tuple constants + // Constant folding passes + self.fold_unary_negative(); self.fold_tuple_constants(); + self.fold_list_constants(); + self.fold_set_constants(); self.convert_to_load_small_int(); self.remove_unused_consts(); self.remove_nops(); // DCE always runs (removes dead code after terminal instructions) self.dce(); - if opts.optimize > 0 { - self.peephole_optimize(); - } + // Peephole optimizer creates superinstructions matching CPython + self.peephole_optimize(); // Always apply LOAD_FAST_BORROW optimization self.optimize_load_fast_borrow(); @@ -625,6 +627,55 @@ impl CodeInfo { } } + /// Fold LOAD_CONST/LOAD_SMALL_INT + UNARY_NEGATIVE → LOAD_CONST (negative value) + fn fold_unary_negative(&mut self) { + for block in &mut self.blocks { + let mut i = 0; + while i + 1 < block.instructions.len() { + let next = &block.instructions[i + 1]; + let Some(Instruction::UnaryNegative) = next.instr.real() else { + i += 1; + continue; + }; + let curr = &block.instructions[i]; + let value = match curr.instr.real() { + Some(Instruction::LoadConst { .. }) => { + let idx = u32::from(curr.arg) as usize; + match self.metadata.consts.get_index(idx) { + Some(ConstantData::Integer { value }) => { + Some(ConstantData::Integer { value: -value }) + } + Some(ConstantData::Float { value }) => { + Some(ConstantData::Float { value: -value }) + } + _ => None, + } + } + Some(Instruction::LoadSmallInt { .. }) => { + let v = u32::from(curr.arg) as i32; + Some(ConstantData::Integer { + value: BigInt::from(-v), + }) + } + _ => None, + }; + if let Some(neg_const) = value { + let (const_idx, _) = self.metadata.consts.insert_full(neg_const); + block.instructions[i].instr = Instruction::LoadConst { + consti: Arg::marker(), + } + .into(); + block.instructions[i].arg = OpArg::new(const_idx as u32); + // Replace UNARY_NEGATIVE with NOP + block.instructions[i + 1].instr = Instruction::Nop.into(); + i += 2; + } else { + i += 1; + } + } + } + } + /// Constant folding: fold LOAD_CONST/LOAD_SMALL_INT + BUILD_TUPLE into LOAD_CONST tuple /// fold_tuple_of_constants fn fold_tuple_constants(&mut self) { @@ -723,6 +774,195 @@ impl CodeInfo { } } + /// Fold constant list literals: LOAD_CONST* + BUILD_LIST N → + /// BUILD_LIST 0 + LOAD_CONST (tuple) + LIST_EXTEND 1 + fn fold_list_constants(&mut self) { + for block in &mut self.blocks { + let mut i = 0; + while i < block.instructions.len() { + let instr = &block.instructions[i]; + let Some(Instruction::BuildList { .. }) = instr.instr.real() else { + i += 1; + continue; + }; + + let list_size = u32::from(instr.arg) as usize; + if list_size == 0 || i < list_size { + i += 1; + continue; + } + + let start_idx = i - list_size; + let mut elements = Vec::with_capacity(list_size); + let mut all_const = true; + + for j in start_idx..i { + let load_instr = &block.instructions[j]; + match load_instr.instr.real() { + Some(Instruction::LoadConst { .. }) => { + let const_idx = u32::from(load_instr.arg) as usize; + if let Some(constant) = + self.metadata.consts.get_index(const_idx).cloned() + { + elements.push(constant); + } else { + all_const = false; + break; + } + } + Some(Instruction::LoadSmallInt { .. }) => { + let value = u32::from(load_instr.arg) as i32; + elements.push(ConstantData::Integer { + value: BigInt::from(value), + }); + } + _ => { + all_const = false; + break; + } + } + } + + if !all_const || list_size < 3 { + i += 1; + continue; + } + + let tuple_const = ConstantData::Tuple { elements }; + let (const_idx, _) = self.metadata.consts.insert_full(tuple_const); + + let folded_loc = block.instructions[i].location; + let end_loc = block.instructions[i].end_location; + let eh = block.instructions[i].except_handler; + + // slot[start_idx] → BUILD_LIST 0 + block.instructions[start_idx].instr = Instruction::BuildList { + count: Arg::marker(), + } + .into(); + block.instructions[start_idx].arg = OpArg::new(0); + block.instructions[start_idx].location = folded_loc; + block.instructions[start_idx].end_location = end_loc; + block.instructions[start_idx].except_handler = eh; + + // slot[start_idx+1] → LOAD_CONST (tuple) + block.instructions[start_idx + 1].instr = Instruction::LoadConst { + consti: Arg::marker(), + } + .into(); + block.instructions[start_idx + 1].arg = OpArg::new(const_idx as u32); + block.instructions[start_idx + 1].location = folded_loc; + block.instructions[start_idx + 1].end_location = end_loc; + block.instructions[start_idx + 1].except_handler = eh; + + // NOP the rest + for j in (start_idx + 2)..i { + block.instructions[j].instr = Instruction::Nop.into(); + block.instructions[j].location = folded_loc; + } + + // slot[i] (was BUILD_LIST) → LIST_EXTEND 1 + block.instructions[i].instr = Instruction::ListExtend { i: Arg::marker() }.into(); + block.instructions[i].arg = OpArg::new(1); + + i += 1; + } + } + } + + /// Fold constant set literals: LOAD_CONST* + BUILD_SET N → + /// BUILD_SET 0 + LOAD_CONST (frozenset-as-tuple) + SET_UPDATE 1 + fn fold_set_constants(&mut self) { + for block in &mut self.blocks { + let mut i = 0; + while i < block.instructions.len() { + let instr = &block.instructions[i]; + let Some(Instruction::BuildSet { .. }) = instr.instr.real() else { + i += 1; + continue; + }; + + let set_size = u32::from(instr.arg) as usize; + if set_size < 3 || i < set_size { + i += 1; + continue; + } + + let start_idx = i - set_size; + let mut elements = Vec::with_capacity(set_size); + let mut all_const = true; + + for j in start_idx..i { + let load_instr = &block.instructions[j]; + match load_instr.instr.real() { + Some(Instruction::LoadConst { .. }) => { + let const_idx = u32::from(load_instr.arg) as usize; + if let Some(constant) = + self.metadata.consts.get_index(const_idx).cloned() + { + elements.push(constant); + } else { + all_const = false; + break; + } + } + Some(Instruction::LoadSmallInt { .. }) => { + let value = u32::from(load_instr.arg) as i32; + elements.push(ConstantData::Integer { + value: BigInt::from(value), + }); + } + _ => { + all_const = false; + break; + } + } + } + + if !all_const { + i += 1; + continue; + } + + // Use FrozenSet constant (stored as Tuple for now) + let const_data = ConstantData::Tuple { elements }; + let (const_idx, _) = self.metadata.consts.insert_full(const_data); + + let folded_loc = block.instructions[i].location; + let end_loc = block.instructions[i].end_location; + let eh = block.instructions[i].except_handler; + + block.instructions[start_idx].instr = Instruction::BuildSet { + count: Arg::marker(), + } + .into(); + block.instructions[start_idx].arg = OpArg::new(0); + block.instructions[start_idx].location = folded_loc; + block.instructions[start_idx].end_location = end_loc; + block.instructions[start_idx].except_handler = eh; + + block.instructions[start_idx + 1].instr = Instruction::LoadConst { + consti: Arg::marker(), + } + .into(); + block.instructions[start_idx + 1].arg = OpArg::new(const_idx as u32); + block.instructions[start_idx + 1].location = folded_loc; + block.instructions[start_idx + 1].end_location = end_loc; + block.instructions[start_idx + 1].except_handler = eh; + + for j in (start_idx + 2)..i { + block.instructions[j].instr = Instruction::Nop.into(); + block.instructions[j].location = folded_loc; + } + + block.instructions[i].instr = Instruction::SetUpdate { i: Arg::marker() }.into(); + block.instructions[i].arg = OpArg::new(1); + + i += 1; + } + } + } + /// Peephole optimization: combine consecutive instructions into super-instructions fn peephole_optimize(&mut self) { for block in &mut self.blocks { diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap index 6eea20c54e9..f043fa790f5 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ands.snap @@ -1,35 +1,23 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9317 +assertion_line: 9458 expression: "compile_exec(\"\\\nif True and False and False:\n pass\n\")" --- 1 0 RESUME (0) 1 LOAD_CONST (True) - 2 TO_BOOL + 2 POP_JUMP_IF_FALSE (11) >> 3 CACHE - 4 CACHE - 5 CACHE - 6 POP_JUMP_IF_FALSE (19) - 7 CACHE + 4 NOT_TAKEN + 5 LOAD_CONST (False) + 6 POP_JUMP_IF_FALSE (7) + >> 7 CACHE 8 NOT_TAKEN 9 LOAD_CONST (False) - 10 TO_BOOL + 10 POP_JUMP_IF_FALSE (3) >> 11 CACHE - 12 CACHE - 13 CACHE - 14 POP_JUMP_IF_FALSE (11) - 15 CACHE - 16 NOT_TAKEN - 17 LOAD_CONST (False) - 18 TO_BOOL - >> 19 CACHE - 20 CACHE - 21 CACHE - 22 POP_JUMP_IF_FALSE (3) - 23 CACHE - 24 NOT_TAKEN + 12 NOT_TAKEN - 2 25 LOAD_CONST (None) - 26 RETURN_VALUE - 27 LOAD_CONST (None) - 28 RETURN_VALUE + 2 13 LOAD_CONST (None) + 14 RETURN_VALUE + 15 LOAD_CONST (None) + 16 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap index b6d5edda048..076ae82fecd 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_mixed.snap @@ -1,43 +1,27 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9327 +assertion_line: 9468 expression: "compile_exec(\"\\\nif (True and False) or (False and True):\n pass\n\")" --- 1 0 RESUME (0) 1 LOAD_CONST (True) - 2 TO_BOOL + 2 POP_JUMP_IF_FALSE (5) >> 3 CACHE - 4 CACHE - 5 CACHE - 6 POP_JUMP_IF_FALSE (9) - 7 CACHE + 4 NOT_TAKEN + >> 5 LOAD_CONST (False) + 6 POP_JUMP_IF_TRUE (9) + >> 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 TO_BOOL - >> 11 CACHE - 12 CACHE - 13 CACHE - 14 POP_JUMP_IF_TRUE (17) + 10 POP_JUMP_IF_FALSE (7) + 11 CACHE + 12 NOT_TAKEN + 13 LOAD_CONST (True) + 14 POP_JUMP_IF_FALSE (3) 15 CACHE 16 NOT_TAKEN - >> 17 LOAD_CONST (False) - 18 TO_BOOL - 19 CACHE - 20 CACHE - 21 CACHE - 22 POP_JUMP_IF_FALSE (11) - 23 CACHE - 24 NOT_TAKEN - 25 LOAD_CONST (True) - 26 TO_BOOL - 27 CACHE - 28 CACHE - 29 CACHE - 30 POP_JUMP_IF_FALSE (3) - 31 CACHE - 32 NOT_TAKEN - 2 33 LOAD_CONST (None) - 34 RETURN_VALUE - 35 LOAD_CONST (None) - 36 RETURN_VALUE + 2 17 LOAD_CONST (None) + 18 RETURN_VALUE + 19 LOAD_CONST (None) + 20 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap index 52d8f1ac0b3..a7beb7766b2 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__if_ors.snap @@ -1,35 +1,23 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9307 +assertion_line: 9448 expression: "compile_exec(\"\\\nif True or False or False:\n pass\n\")" --- 1 0 RESUME (0) 1 LOAD_CONST (True) - 2 TO_BOOL + 2 POP_JUMP_IF_TRUE (9) >> 3 CACHE - 4 CACHE - 5 CACHE - 6 POP_JUMP_IF_TRUE (17) + 4 NOT_TAKEN + >> 5 LOAD_CONST (False) + 6 POP_JUMP_IF_TRUE (5) 7 CACHE 8 NOT_TAKEN >> 9 LOAD_CONST (False) - 10 TO_BOOL + 10 POP_JUMP_IF_FALSE (3) 11 CACHE - 12 CACHE - 13 CACHE - 14 POP_JUMP_IF_TRUE (9) - 15 CACHE - 16 NOT_TAKEN - >> 17 LOAD_CONST (False) - 18 TO_BOOL - 19 CACHE - 20 CACHE - 21 CACHE - 22 POP_JUMP_IF_FALSE (3) - 23 CACHE - 24 NOT_TAKEN + 12 NOT_TAKEN - 2 25 LOAD_CONST (None) - 26 RETURN_VALUE - 27 LOAD_CONST (None) - 28 RETURN_VALUE + 2 13 LOAD_CONST (None) + 14 RETURN_VALUE + 15 LOAD_CONST (None) + 16 RETURN_VALUE diff --git a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap index 438b1642926..54d00d5ddb1 100644 --- a/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap +++ b/crates/codegen/src/snapshots/rustpython_codegen__compile__tests__nested_double_async_with.snap @@ -1,6 +1,6 @@ --- source: crates/codegen/src/compile.rs -assertion_line: 9362 +assertion_line: 9496 expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):\n with self.subTest(type=type(stop_exc)):\n try:\n async with egg():\n raise stop_exc\n except Exception as ex:\n self.assertIs(ex, stop_exc)\n else:\n self.fail(f'{stop_exc} was suppressed')\n\")" --- 1 0 RESUME (0) @@ -39,8 +39,8 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 30 CACHE 31 CACHE 32 CACHE - 33 LOAD_ATTR (7, subTest, method=true) - >> 34 CACHE + >> 33 LOAD_ATTR (7, subTest, method=true) + 34 CACHE 35 CACHE 36 CACHE 37 CACHE @@ -52,8 +52,8 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 43 LOAD_GLOBAL (9, NULL + type) 44 CACHE 45 CACHE - 46 CACHE - >> 47 CACHE + >> 46 CACHE + 47 CACHE 48 LOAD_FAST (0, stop_exc) 49 CALL (1) 50 CACHE @@ -142,7 +142,7 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 128 COPY (3) 129 POP_EXCEPT 130 RERAISE (1) - 131 JUMP_FORWARD (47) + 131 JUMP_FORWARD (46) 132 PUSH_EXC_INFO 7 133 LOAD_GLOBAL (12, Exception) @@ -151,7 +151,7 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 136 CACHE 137 CACHE 138 CHECK_EXC_MATCH - 139 POP_JUMP_IF_FALSE (34) + 139 POP_JUMP_IF_FALSE (33) 140 CACHE 141 NOT_TAKEN 142 STORE_FAST (1, ex) @@ -171,34 +171,34 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 155 CACHE 156 CACHE 157 CACHE - 158 LOAD_FAST (1, ex) - 159 LOAD_FAST (0, stop_exc) - 160 CALL (2) + 158 LOAD_FAST_LOAD_FAST (ex, stop_exc) + 159 CALL (2) + 160 CACHE 161 CACHE 162 CACHE - 163 CACHE - 164 POP_TOP - 165 JUMP_FORWARD (4) - 166 LOAD_CONST (None) - 167 STORE_FAST (1, ex) - 168 DELETE_FAST (1, ex) - 169 RERAISE (1) - 170 POP_EXCEPT - 171 LOAD_CONST (None) - 172 STORE_FAST (1, ex) - 173 DELETE_FAST (1, ex) - 174 JUMP_FORWARD (28) - 175 RERAISE (0) - 176 COPY (3) - 177 POP_EXCEPT - 178 RERAISE (1) + 163 POP_TOP + 164 JUMP_FORWARD (4) + 165 LOAD_CONST (None) + 166 STORE_FAST (1, ex) + 167 DELETE_FAST (1, ex) + 168 RERAISE (1) + 169 POP_EXCEPT + 170 LOAD_CONST (None) + 171 STORE_FAST (1, ex) + 172 DELETE_FAST (1, ex) + 173 JUMP_FORWARD (28) + 174 RERAISE (0) + 175 COPY (3) + 176 POP_EXCEPT + 177 RERAISE (1) - 10 179 LOAD_GLOBAL (4, self) + 10 178 LOAD_GLOBAL (4, self) + 179 CACHE 180 CACHE 181 CACHE 182 CACHE - 183 CACHE - 184 LOAD_ATTR (17, fail, method=true) + 183 LOAD_ATTR (17, fail, method=true) + 184 CACHE 185 CACHE 186 CACHE 187 CACHE @@ -207,48 +207,47 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter 190 CACHE 191 CACHE 192 CACHE - 193 CACHE - 194 LOAD_FAST_BORROW (0, stop_exc) - 195 FORMAT_SIMPLE - 196 LOAD_CONST (" was suppressed") - 197 BUILD_STRING (2) - 198 CALL (1) + 193 LOAD_FAST_BORROW (0, stop_exc) + 194 FORMAT_SIMPLE + 195 LOAD_CONST (" was suppressed") + 196 BUILD_STRING (2) + 197 CALL (1) + 198 CACHE 199 CACHE 200 CACHE - 201 CACHE - 202 POP_TOP - 203 NOP + 201 POP_TOP + 202 NOP - 3 204 PUSH_NULL + 3 203 PUSH_NULL + 204 LOAD_CONST (None) 205 LOAD_CONST (None) 206 LOAD_CONST (None) - 207 LOAD_CONST (None) - 208 CALL (3) - >> 209 CACHE + 207 CALL (3) + >> 208 CACHE + 209 CACHE 210 CACHE - 211 CACHE - 212 POP_TOP - 213 JUMP_FORWARD (18) - 214 PUSH_EXC_INFO - 215 WITH_EXCEPT_START - 216 TO_BOOL + 211 POP_TOP + 212 JUMP_FORWARD (18) + 213 PUSH_EXC_INFO + 214 WITH_EXCEPT_START + 215 TO_BOOL + 216 CACHE 217 CACHE 218 CACHE - 219 CACHE - 220 POP_JUMP_IF_TRUE (2) - 221 CACHE - 222 NOT_TAKEN - 223 RERAISE (2) - 224 POP_TOP - 225 POP_EXCEPT + 219 POP_JUMP_IF_TRUE (2) + 220 CACHE + 221 NOT_TAKEN + 222 RERAISE (2) + 223 POP_TOP + 224 POP_EXCEPT + 225 POP_TOP 226 POP_TOP - 227 POP_TOP - 228 JUMP_FORWARD (3) - 229 COPY (3) - 230 POP_EXCEPT - 231 RERAISE (1) - 232 JUMP_BACKWARD (209) - 233 CACHE + 227 JUMP_FORWARD (3) + 228 COPY (3) + 229 POP_EXCEPT + 230 RERAISE (1) + 231 JUMP_BACKWARD (208) + 232 CACHE 2 MAKE_FUNCTION 3 STORE_NAME (0, test) diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index c6384d5f167..6fd54c551d4 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -54,6 +54,9 @@ pub struct SymbolTable { /// Whether this type param scope can see the parent class scope pub can_see_class_scope: bool, + /// Whether this scope contains yield/yield from (is a generator function) + pub is_generator: bool, + /// Whether this comprehension scope should be inlined (PEP 709) /// True for list/set/dict comprehensions in non-generator expressions pub comp_inlined: bool, @@ -89,6 +92,7 @@ impl SymbolTable { needs_class_closure: false, needs_classdict: false, can_see_class_scope: false, + is_generator: false, comp_inlined: false, annotation_block: None, has_conditional_annotations: false, @@ -1823,6 +1827,7 @@ impl SymbolTableBuilder { node_index: _, range: _, }) => { + self.tables.last_mut().unwrap().is_generator = true; if let Some(expression) = value { self.scan_expression(expression, context)?; } @@ -1832,6 +1837,7 @@ impl SymbolTableBuilder { node_index: _, range: _, }) => { + self.tables.last_mut().unwrap().is_generator = true; self.scan_expression(value, context)?; } Expr::UnaryOp(ExprUnaryOp { diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index c38c6da11ad..d60ab9ba217 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -2945,7 +2945,6 @@ impl ExecutingFrame<'_> { let bound = match vm.get_special_method(&obj, method_name)? { Some(PyMethod::Function { target, func }) => { - // Create bound method: PyBoundMethod(object=target, function=func) crate::builtins::PyBoundMethod::new(target, func) .into_ref(&vm.ctx) .into() From 6d4ed646967a5138e66286eeff2810828a51cfff Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 14:06:12 +0900 Subject: [PATCH 2/7] Fix formatting and collapsible_if clippy warnings in compile.rs --- crates/codegen/src/compile.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 5ac8369a4fe..3aa259e18bc 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1201,8 +1201,8 @@ impl Compiler { /// codegen_enter_scope equivalent for RESUME emission. fn emit_resume_for_scope(&mut self, scope_type: CompilerScope, lineno: u32) { // For generators and async functions, emit RETURN_GENERATOR + POP_TOP before RESUME - let is_gen = scope_type == CompilerScope::AsyncFunction - || self.current_symbol_table().is_generator; + let is_gen = + scope_type == CompilerScope::AsyncFunction || self.current_symbol_table().is_generator; if is_gen { emit!(self, Instruction::ReturnGenerator); emit!(self, Instruction::PopTop); @@ -4540,35 +4540,29 @@ impl Compiler { ast::Stmt::Assign(a) => { for target in &a.targets { if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = target + && let ast::Expr::Name(n) = value.as_ref() + && n.id.as_str() == name { - if let ast::Expr::Name(n) = value.as_ref() { - if n.id.as_str() == name { - attrs.insert(attr.to_string()); - } - } + attrs.insert(attr.to_string()); } } } ast::Stmt::AnnAssign(a) => { if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = a.target.as_ref() + && let ast::Expr::Name(n) = value.as_ref() + && n.id.as_str() == name { - if let ast::Expr::Name(n) = value.as_ref() { - if n.id.as_str() == name { - attrs.insert(attr.to_string()); - } - } + attrs.insert(attr.to_string()); } } ast::Stmt::AugAssign(a) => { if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = a.target.as_ref() + && let ast::Expr::Name(n) = value.as_ref() + && n.id.as_str() == name { - if let ast::Expr::Name(n) = value.as_ref() { - if n.id.as_str() == name { - attrs.insert(attr.to_string()); - } - } + attrs.insert(attr.to_string()); } } ast::Stmt::If(s) => { From 12b405d9ab16ffe5c9dcd10b4d33ee3b04c6b6f8 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 14:52:27 +0900 Subject: [PATCH 3/7] Fix clippy, fold_unary_negative chaining, and generator line tracing - Replace irrefutable if-let with let for ExceptHandler - Remove folded UNARY_NEGATIVE instead of replacing with NOP, enabling chained negation folding - Initialize prev_line to def line for generators/coroutines to suppress spurious LINE events from preamble instructions - Remove expectedFailure markers for now-passing tests --- Lib/test/test_compile.py | 4 ---- Lib/test/test_peepholer.py | 1 - crates/codegen/src/compile.rs | 5 ++--- crates/codegen/src/ir.rs | 6 +++--- crates/vm/src/frame.rs | 15 ++++++++++++++- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index b469c8ab2de..0495c58329c 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -2486,7 +2486,6 @@ def f(): class TestStaticAttributes(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: type object 'C' has no attribute '__static_attributes__' def test_basic(self): class C: def f(self): @@ -2518,7 +2517,6 @@ def h(self, a): self.assertEqual(sorted(C.__static_attributes__), ['u', 'v', 'x', 'y', 'z']) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: type object 'C' has no attribute '__static_attributes__' def test_nested_class(self): class C: def f(self): @@ -2533,7 +2531,6 @@ def g(self): self.assertEqual(sorted(C.__static_attributes__), ['x', 'y']) self.assertEqual(sorted(C.D.__static_attributes__), ['y', 'z']) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: type object 'C' has no attribute '__static_attributes__' def test_subclass(self): class C: def f(self): @@ -2593,7 +2590,6 @@ def test_tuple(self): def test_set(self): self.check_stack_size("{" + "x, " * self.N + "x}") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 202 not less than or equal to 7 def test_dict(self): self.check_stack_size("{" + "x:x, " * self.N + "x:x}") diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 847ef624d62..6bb4249b1ce 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -862,7 +862,6 @@ def setUp(self): self.addCleanup(sys.settrace, sys.gettrace()) sys.settrace(None) - @unittest.expectedFailure # TODO: RUSTPYTHON; no LOAD_FAST_BORROW_LOAD_FAST_BORROW superinstruction def test_load_fast_known_simple(self): def f(): x = 1 diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 3aa259e18bc..dba8780c902 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -4582,9 +4582,8 @@ impl Compiler { ast::Stmt::Try(s) => { Self::scan_store_attrs(&s.body, name, attrs); for handler in &s.handlers { - if let ast::ExceptHandler::ExceptHandler(h) = handler { - Self::scan_store_attrs(&h.body, name, attrs); - } + let ast::ExceptHandler::ExceptHandler(h) = handler; + Self::scan_store_attrs(&h.body, name, attrs); } Self::scan_store_attrs(&s.orelse, name, attrs); Self::scan_store_attrs(&s.finalbody, name, attrs); diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index c4a5df449a2..a8a386c32c3 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -666,9 +666,9 @@ impl CodeInfo { } .into(); block.instructions[i].arg = OpArg::new(const_idx as u32); - // Replace UNARY_NEGATIVE with NOP - block.instructions[i + 1].instr = Instruction::Nop.into(); - i += 2; + // Remove UNARY_NEGATIVE so chained negation can be folded + block.instructions.remove(i + 1); + // Don't increment i - re-check new LOAD_CONST with next instruction } else { i += 1; } diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index d60ab9ba217..4c88a58d2d8 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -705,6 +705,19 @@ impl Frame { } } + // For generators/coroutines, initialize prev_line to the def line + // so that preamble instructions (RETURN_GENERATOR, POP_TOP) don't + // fire spurious LINE events. + let prev_line = if code + .flags + .intersects(bytecode::CodeFlags::GENERATOR | bytecode::CodeFlags::COROUTINE) + { + code.first_line_number + .map_or(0, |line| line.get() as u32) + } else { + 0 + }; + let iframe = InterpreterFrame { localsplus, locals: match scope.locals { @@ -719,7 +732,7 @@ impl Frame { code, func_obj, lasti: Radium::new(0), - prev_line: 0, + prev_line, trace: PyMutex::new(vm.ctx.none()), trace_lines: PyMutex::new(true), trace_opcodes: PyMutex::new(false), From 970a9c96bbe9776d54a9e9c66e2bc4e4b1204754 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 23 Mar 2026 18:17:57 +0900 Subject: [PATCH 4/7] Fix JIT StoreFastStoreFast, format, and remove expectedFailure markers - Add StoreFastStoreFast handling in JIT instructions - Fix cargo fmt in frame.rs - Remove 11 expectedFailure markers for async jump tests in test_sys_settrace that now pass --- Lib/test/test_sys_settrace.py | 22 ---------------------- crates/jit/src/instructions.rs | 8 ++++++++ crates/vm/src/frame.rs | 3 +-- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_sys_settrace.py b/Lib/test/test_sys_settrace.py index d3232436f74..f9449c7079a 100644 --- a/Lib/test/test_sys_settrace.py +++ b/Lib/test/test_sys_settrace.py @@ -1420,8 +1420,6 @@ def test_jump_out_of_block_backwards(output): output.append(6) output.append(7) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(4, 5, [3, 5]) async def test_jump_out_of_async_for_block_forwards(output): for i in [1]: @@ -1430,8 +1428,6 @@ async def test_jump_out_of_async_for_block_forwards(output): output.append(4) output.append(5) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(5, 2, [2, 4, 2, 4, 5, 6]) async def test_jump_out_of_async_for_block_backwards(output): for i in [1]: @@ -1539,8 +1535,6 @@ def test_jump_forwards_out_of_with_block(output): output.append(2) output.append(3) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(2, 3, [1, 3]) async def test_jump_forwards_out_of_async_with_block(output): async with asynctracecontext(output, 1): @@ -1553,8 +1547,6 @@ def test_jump_backwards_out_of_with_block(output): with tracecontext(output, 2): output.append(3) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(3, 1, [1, 2, 1, 2, 3, -2]) async def test_jump_backwards_out_of_async_with_block(output): output.append(1) @@ -1624,8 +1616,6 @@ def test_jump_across_with(output): with tracecontext(output, 4): output.append(5) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(2, 4, [1, 4, 5, -4]) async def test_jump_across_async_with(output): output.append(1) @@ -1643,8 +1633,6 @@ def test_jump_out_of_with_block_within_for_block(output): output.append(5) output.append(6) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(4, 5, [1, 3, 5, 6]) async def test_jump_out_of_async_with_block_within_for_block(output): output.append(1) @@ -1663,8 +1651,6 @@ def test_jump_out_of_with_block_within_with_block(output): output.append(5) output.append(6) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(4, 5, [1, 2, 3, 5, -2, 6]) async def test_jump_out_of_async_with_block_within_with_block(output): output.append(1) @@ -1684,8 +1670,6 @@ def test_jump_out_of_with_block_within_finally_block(output): output.append(6) output.append(7) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(5, 6, [2, 4, 6, 7]) async def test_jump_out_of_async_with_block_within_finally_block(output): try: @@ -1719,8 +1703,6 @@ def test_jump_out_of_with_assignment(output): output.append(4) output.append(5) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(3, 5, [1, 2, 5]) async def test_jump_out_of_async_with_assignment(output): output.append(1) @@ -1768,8 +1750,6 @@ def test_jump_over_for_block_before_else(output): output.append(7) output.append(8) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(1, 7, [7, 8]) async def test_jump_over_async_for_block_before_else(output): output.append(1) @@ -2053,8 +2033,6 @@ def test_jump_between_with_blocks(output): with tracecontext(output, 4): output.append(5) - # TODO: RUSTPYTHON - @unittest.expectedFailure @async_jump_test(3, 5, [1, 2, 5, -2]) async def test_jump_between_async_with_blocks(output): output.append(1) diff --git a/crates/jit/src/instructions.rs b/crates/jit/src/instructions.rs index bc5c19c7d2b..956a385e29b 100644 --- a/crates/jit/src/instructions.rs +++ b/crates/jit/src/instructions.rs @@ -736,6 +736,14 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> { let val = self.stack.pop().ok_or(JitCompileError::BadBytecode)?; self.store_variable(var_num.get(arg), val) } + Instruction::StoreFastStoreFast { var_nums } => { + let oparg = var_nums.get(arg); + let (idx1, idx2) = oparg.indexes(); + let val1 = self.stack.pop().ok_or(JitCompileError::BadBytecode)?; + self.store_variable(idx1, val1)?; + let val2 = self.stack.pop().ok_or(JitCompileError::BadBytecode)?; + self.store_variable(idx2, val2) + } Instruction::Swap { i: index } => { let len = self.stack.len(); let i = len - 1; diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 4c88a58d2d8..9d0f7fa8090 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -712,8 +712,7 @@ impl Frame { .flags .intersects(bytecode::CodeFlags::GENERATOR | bytecode::CodeFlags::COROUTINE) { - code.first_line_number - .map_or(0, |line| line.get() as u32) + code.first_line_number.map_or(0, |line| line.get() as u32) } else { 0 }; From 9f952c76ef5e1603684e246a6c6fc07a0935da99 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 11:57:56 +0900 Subject: [PATCH 5/7] Fix peephole optimizer: use NOP replacement instead of remove() Using remove() shifts instruction indices and corrupts subsequent references, causing "pop stackref but null found" panics at runtime. Replace folded/combined instructions with NOP instead, which are cleaned up by the existing remove_nops pass. --- crates/codegen/src/ir.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index a8a386c32c3..f2086cdc004 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -661,14 +661,20 @@ impl CodeInfo { }; if let Some(neg_const) = value { let (const_idx, _) = self.metadata.consts.insert_full(neg_const); + // Replace LOAD_CONST/LOAD_SMALL_INT with new LOAD_CONST + let load_location = block.instructions[i].location; block.instructions[i].instr = Instruction::LoadConst { consti: Arg::marker(), } .into(); block.instructions[i].arg = OpArg::new(const_idx as u32); - // Remove UNARY_NEGATIVE so chained negation can be folded - block.instructions.remove(i + 1); - // Don't increment i - re-check new LOAD_CONST with next instruction + // Replace UNARY_NEGATIVE with NOP, inheriting the LOAD_CONST + // location so that remove_nops can clean it up + block.instructions[i + 1].instr = Instruction::Nop.into(); + block.instructions[i + 1].location = load_location; + block.instructions[i + 1].end_location = block.instructions[i].end_location; + // Skip the NOP, don't re-check + i += 2; } else { i += 1; } @@ -1048,9 +1054,11 @@ impl CodeInfo { // Combine: keep first instruction's location, replace with combined instruction block.instructions[i].instr = new_instr.into(); block.instructions[i].arg = new_arg; - // Remove the second instruction - block.instructions.remove(i + 1); - // Don't increment i - check if we can combine again with the next instruction + // Replace the second instruction with NOP + let loc = block.instructions[i].location; + block.instructions[i + 1].instr = Instruction::Nop.into(); + block.instructions[i + 1].location = loc; + i += 2; } else { i += 1; } From ce07e05795ed4bee1d87a53a0e3161ff177e373c Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 12:20:08 +0900 Subject: [PATCH 6/7] Revert peephole_optimize to use remove() for chaining support NOP replacement broke chaining of peephole optimizations (e.g. LOAD_CONST+TO_BOOL then LOAD_CONST+UNARY_NOT for 'not True'). The remove() approach is used by upstream and works correctly here; fold_unary_negative keeps NOP replacement since it doesn't need chaining. --- crates/codegen/src/ir.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index f2086cdc004..0b7eae105e2 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -1054,11 +1054,9 @@ impl CodeInfo { // Combine: keep first instruction's location, replace with combined instruction block.instructions[i].instr = new_instr.into(); block.instructions[i].arg = new_arg; - // Replace the second instruction with NOP - let loc = block.instructions[i].location; - block.instructions[i + 1].instr = Instruction::Nop.into(); - block.instructions[i + 1].location = loc; - i += 2; + // Remove the second instruction + block.instructions.remove(i + 1); + // Don't increment i - check if we can combine again with the next instruction } else { i += 1; } From 289172a2a460c707548120550dfced34623a6059 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Wed, 25 Mar 2026 14:20:29 +0900 Subject: [PATCH 7/7] Fix StoreFastStoreFast to handle NULL from LoadFastAndClear StoreFast uses pop_value_opt() to allow NULL values from LoadFastAndClear in inlined comprehension cleanup paths. StoreFastStoreFast must do the same, otherwise the peephole optimizer's fusion of two StoreFast instructions panics when restoring unbound locals after an inlined comprehension. --- crates/vm/src/frame.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 9d0f7fa8090..9338c74d8f8 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -3446,11 +3446,12 @@ impl ExecutingFrame<'_> { Instruction::StoreFastStoreFast { var_nums } => { let oparg = var_nums.get(arg); let (idx1, idx2) = oparg.indexes(); - let value1 = self.pop_value(); - let value2 = self.pop_value(); + // pop_value_opt: allows NULL from LoadFastAndClear restore path + let value1 = self.pop_value_opt(); + let value2 = self.pop_value_opt(); let fastlocals = self.localsplus.fastlocals_mut(); - fastlocals[idx1] = Some(value1); - fastlocals[idx2] = Some(value2); + fastlocals[idx1] = value1; + fastlocals[idx2] = value2; Ok(None) } Instruction::StoreGlobal { namei: idx } => {