Bytecode parity - folding, class prologue, except cleanup

Constant folding:
- Add string/bytes multiply and bytes concat folding in IR
- Add constant subscript folding (str, bytes, tuple indexing)
- Delegate list/set constant folding to IR passes
- Stream big non-const list/set via BUILD+LIST_APPEND

Class/generic compilation:
- Reorder class body prologue: __type_params__ before __classdict__
- Build class function before .generic_base in generic classes
- Register .type_params/.generic_base symbols in proper scopes
- Use load_name/store_name helpers for synthetic variables

Return block handling:
- Only duplicate return-None epilogues, not arbitrary returns
- Add inline_pop_except_return_blocks pass
- Add duplicate_named_except_cleanup_returns pass

Other fixes:
- Fix eliminate_dead_stores to only collapse adjacent duplicates
- Skip STORE_FAST_LOAD_FAST superinstruction in generators after FOR_ITER
- Thread jumps through NOP-only blocks
- Transfer NOP line info to following unconditional jumps
- Extract scope_needs_conditional_annotations_cell helper
- Register __conditional_annotations__ for module future annotations
This commit is contained in:
Jeong, YunWon committed 2026-04-18 15:17:56 +09:00
1 parent d7217074bf
commit ce0e41de9d
9 files changed
+668 -166

No files matched your search

-2
View File
@@ -2576,7 +2576,6 @@ class TestExpressionStackSize(unittest.TestCase):
def test_binop(self):
self.check_stack_size("x + " * self.N + "x")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 not less than or equal to 6
def test_list(self):
self.check_stack_size("[" + "x, " * self.N + "x]")
@@ -2584,7 +2583,6 @@ class TestExpressionStackSize(unittest.TestCase):
def test_tuple(self):
self.check_stack_size("(" + "x, " * self.N + "x)")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 not less than or equal to 6
def test_set(self):
self.check_stack_size("{" + "x, " * self.N + "x}")
-1
View File
@@ -441,7 +441,6 @@ class TestTranforms(BytecodeTestCase):
self.check_lnotab(code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_constant_folding_remove_nop_location(self):
sources = [
"""
-1
View File
@@ -111,7 +111,6 @@ class TestTString(unittest.TestCase, TStringBaseCase):
with self.assertRaises(SyntaxError):
eval("t'{num!z}'")
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ++++++
def test_debug_specifier(self):
# Test debug specifier
value = 42
-1
View File
@@ -4697,7 +4697,6 @@ class GenericTests(BaseTestCase):
with self.assertRaises(TypeError):
D[()]
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_generic_init_subclass_not_called_error(self):
notes = ["Note: this exception may have been caused by "
r"'GenericTests.test_generic_init_subclass_not_called_error.<locals>.Base.__init_subclass__' "
+391 -101
View File
@@ -673,7 +673,10 @@ impl Compiler {
let can_fold_const_collection = match collection_type {
CollectionType::Tuple => n > 0,
CollectionType::List | CollectionType::Set => n >= 3,
// Match CPython's constant ordering for list/set literals by
// letting the late IR folding passes introduce their tuple-backed
// constants instead of inserting them during AST lowering.
CollectionType::List | CollectionType::Set => false,
};
if !self.disable_const_collection_folding
&& !seen_star
@@ -720,9 +723,37 @@ impl Compiler {
}
// Has stars or too big: use streaming approach
let stream_big_nonconst_collection = if !seen_star
&& big
&& matches!(collection_type, CollectionType::List | CollectionType::Set)
{
elts.iter().try_fold(false, |has_nonconst, elt| {
if has_nonconst {
return Ok(true);
}
Ok(self.try_fold_constant_expr(elt)?.is_none())
})?
} else {
false
};
let mut sequence_built = false;
let mut i = 0u32;
if stream_big_nonconst_collection {
match collection_type {
CollectionType::List => {
emit!(self, Instruction::BuildList { count: pushed });
sequence_built = true;
}
CollectionType::Set => {
emit!(self, Instruction::BuildSet { count: pushed });
sequence_built = true;
}
CollectionType::Tuple => {}
}
}
for elt in elts.iter() {
if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = elt {
// When we hit first star, build sequence with elements so far
@@ -1220,10 +1251,8 @@ impl Compiler {
cellvar_cache.insert("__classdict__".to_string());
}
// Handle implicit __conditional_annotations__ cell if needed
if ste.has_conditional_annotations
&& matches!(scope_type, CompilerScope::Class | CompilerScope::Module)
{
// Handle implicit __conditional_annotations__ cell if needed.
if Self::scope_needs_conditional_annotations_cell(ste) {
cellvar_cache.insert("__conditional_annotations__".to_string());
}
@@ -1923,7 +1952,7 @@ impl Compiler {
self.future_annotations = symbol_table.future_annotations;
// Module-level __conditional_annotations__ cell
let has_module_cond_ann = symbol_table.has_conditional_annotations;
let has_module_cond_ann = Self::scope_needs_conditional_annotations_cell(&symbol_table);
if has_module_cond_ann {
self.current_code_info()
.metadata
@@ -1951,18 +1980,16 @@ impl Compiler {
emit!(self, Instruction::StoreName { namei: doc })
}
// Handle annotations based on future_annotations flag
// Handle annotation bookkeeping in CPython order: initialize the
// conditional annotation set first, then materialize __annotations__.
if Self::find_ann(statements) {
if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) {
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name("__conditional_annotations__")?;
}
if self.future_annotations {
// PEP 563: Initialize __annotations__ dict
emit!(self, Instruction::SetupAnnotations);
} else {
// PEP 649: Initialize __conditional_annotations__ before the body.
// CPython generates __annotate__ after the body in codegen_body().
if self.current_symbol_table().has_conditional_annotations {
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name("__conditional_annotations__")?;
}
}
}
@@ -2102,6 +2129,20 @@ impl Compiler {
Ok(())
}
fn scope_needs_conditional_annotations_cell(symbol_table: &SymbolTable) -> bool {
match symbol_table.typ {
CompilerScope::Module => {
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,
}
}
fn load_name(&mut self, name: &str) -> CompileResult<()> {
self.compile_name(name, NameUsage::Load)
}
@@ -5189,52 +5230,37 @@ impl Compiler {
}
);
// PEP 649: Initialize __classdict__ cell (before __doc__)
// Set __type_params__ from the enclosing type-params closure when
// compiling a generic class body.
if type_params.is_some() {
self.load_name(".type_params")?;
self.store_name("__type_params__")?;
}
// PEP 649: Initialize __classdict__ after synthetic generic-class
// setup so nested generic classes match CPython's prologue order.
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
// 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 });
}
// Set __type_params__ if we have type parameters
if type_params.is_some() {
// Load .type_params from enclosing scope
let dot_type_params = self.name(".type_params");
emit!(
self,
Instruction::LoadName {
namei: dot_type_params
}
);
// Store as __type_params__
let dunder_type_params = self.name("__type_params__");
emit!(
self,
Instruction::StoreName {
namei: dunder_type_params
}
);
}
// Handle class annotations based on future_annotations flag
// Handle class annotation bookkeeping in CPython order.
if Self::find_ann(body) {
if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) {
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name("__conditional_annotations__")?;
}
if self.future_annotations {
// PEP 563: Initialize __annotations__ dict for class
emit!(self, Instruction::SetupAnnotations);
} else {
// PEP 649: Initialize __conditional_annotations__ set if needed for class
if self.current_symbol_table().has_conditional_annotations {
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name("__conditional_annotations__")?;
}
}
}
@@ -5363,15 +5389,10 @@ impl Compiler {
in_async_scope: false,
};
// Compile type parameters and store as .type_params
// Compile type parameters and store them in the synthetic cell that
// generic class bodies close over.
self.compile_type_params(type_params.unwrap())?;
let dot_type_params = self.name(".type_params");
emit!(
self,
Instruction::StoreName {
namei: dot_type_params
}
);
self.store_name(".type_params")?;
}
// Step 2: Compile class body (always done, whether generic or not)
@@ -5387,47 +5408,25 @@ impl Compiler {
// Step 3: Generate the rest of the code for the call
if is_generic {
// Still in type params scope
let dot_type_params = self.name(".type_params");
let dot_generic_base = self.name(".generic_base");
// Generate class creation code
emit!(self, Instruction::LoadBuildClass);
emit!(self, Instruction::PushNull);
// Create .generic_base
emit!(
self,
Instruction::LoadName {
namei: dot_type_params
}
);
// Create the class body function with the .type_params closure
// captured through the class code object's freevars.
self.make_closure(class_code, bytecode::MakeFunctionFlags::new())?;
self.emit_load_const(ConstantData::Str { value: name.into() });
// Create .generic_base after the class function and name are on the
// stack so the remaining call shape matches CPython's ordering.
self.load_name(".type_params")?;
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::SubscriptGeneric
}
);
emit!(
self,
Instruction::StoreName {
namei: dot_generic_base
}
);
// Generate class creation code
emit!(self, Instruction::LoadBuildClass);
emit!(self, Instruction::PushNull);
// Set up the class function with type params
let mut func_flags = bytecode::MakeFunctionFlags::new();
emit!(
self,
Instruction::LoadName {
namei: dot_type_params
}
);
func_flags.insert(bytecode::MakeFunctionFlag::TypeParams);
// Create class function with closure
self.make_closure(class_code, func_flags)?;
self.emit_load_const(ConstantData::Str { value: name.into() });
self.store_name(".generic_base")?;
// Compile bases and call __build_class__
// Check for starred bases or **kwargs
@@ -5463,12 +5462,7 @@ impl Compiler {
}
// Add .generic_base as final element
emit!(
self,
Instruction::LoadName {
namei: dot_generic_base
}
);
self.load_name(".generic_base")?;
emit!(self, Instruction::ListAppend { i: 1 });
// Convert list to tuple
@@ -5499,12 +5493,7 @@ impl Compiler {
};
// Load .generic_base as the last base
emit!(
self,
Instruction::LoadName {
namei: dot_generic_base
}
);
self.load_name(".generic_base")?;
let nargs = 2 + u32::try_from(base_count).expect("too many base classes") + 1;
@@ -9785,6 +9774,19 @@ impl Compiler {
}
}
fn compile_tstring_literal_value(
&self,
string: &ast::InterpolatedStringLiteralElement,
flags: ast::TStringFlags,
) -> Wtf8Buf {
if string.value.contains(char::REPLACEMENT_CHARACTER) {
let source = self.source_file.slice(string.range);
crate::string_parser::parse_fstring_literal_element(source.into(), flags.into()).into()
} else {
string.value.to_string().into()
}
}
fn compile_fstring_part_literal_value(&self, string: &ast::StringLiteral) -> Wtf8Buf {
if string.value.contains(char::REPLACEMENT_CHARACTER) {
let source = self.source_file.slice(string.range);
@@ -9924,6 +9926,72 @@ impl Compiler {
}
ConstantData::Tuple { elements }
}
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Some(container) = self.try_fold_constant_expr(value)? else {
return Ok(None);
};
let Some(index) = self.try_fold_constant_expr(slice)? else {
return Ok(None);
};
let ConstantData::Integer { value: index } = index else {
return Ok(None);
};
let Some(index): Option<i64> = index.try_into().ok() else {
return Ok(None);
};
match container {
ConstantData::Str { value } => {
let string = value.to_string();
if string.contains(char::REPLACEMENT_CHARACTER) {
return Ok(None);
}
let chars: Vec<_> = string.chars().collect();
let Some(len) = i64::try_from(chars.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(ch) = chars.get(idx) else {
return Ok(None);
};
ConstantData::Str {
value: ch.to_string().into(),
}
}
ConstantData::Bytes { value } => {
let Some(len) = i64::try_from(value.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(byte) = value.get(idx) else {
return Ok(None);
};
ConstantData::Integer {
value: BigInt::from(*byte),
}
}
ConstantData::Tuple { elements } => {
let Some(len) = i64::try_from(elements.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(element) = elements.get(idx) else {
return Ok(None);
};
element.clone()
}
_ => return Ok(None),
}
}
ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => {
let Some(constant) = self.try_fold_constant_expr(operand)? else {
return Ok(None);
@@ -10791,7 +10859,8 @@ impl Compiler {
for element in &tstring.elements {
match element {
ast::InterpolatedStringElement::Literal(lit) => {
current_string.push_str(&lit.value);
current_string
.push_wtf8(&self.compile_tstring_literal_value(lit, tstring.flags));
}
ast::InterpolatedStringElement::Interpolation(interp) => {
if let Some(ast::DebugText { leading, trailing }) = &interp.debug_text {
@@ -12425,6 +12494,16 @@ t = t\"Value: {value=}\"
);
}
#[test]
fn test_tstring_literal_preserves_surrogate_wtf8() {
let code = compile_exec("t = t\"\\ud800\"");
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.clone().into_bytes() == [0xED, 0xA0, 0x80]
)));
}
#[test]
fn test_break_in_finally_after_return_keeps_load_fast_check_for_loop_locals() {
let code = compile_exec(
@@ -12950,6 +13029,27 @@ class C:
);
}
#[test]
fn test_future_annotations_class_keeps_conditional_annotations_cell() {
let code = compile_exec(
"\
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__"),
"expected __conditional_annotations__ cellvar, got cellvars={:?}",
class_code.cellvars
);
}
#[test]
fn test_plain_super_call_keeps_class_freevar() {
let code = compile_exec(
@@ -13243,6 +13343,91 @@ def f(names, cls):
assert_eq!(return_count, 1);
}
#[test]
fn test_non_none_final_return_is_not_duplicated() {
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)
)
)
})
.count();
assert_eq!(
minus_one_loads,
1,
"expected a single final return -1 epilogue, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn test_named_except_conditional_branch_duplicates_cleanup_return() {
let code = compile_exec(
"\
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 function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
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_listcomp_cleanup_tail_keeps_split_store_fast_pair() {
let code = compile_exec(
@@ -13315,6 +13500,43 @@ def f(cm, cond):
);
}
#[test]
fn test_genexpr_compare_header_keeps_split_store_then_borrow_load() {
let code = compile_exec(
"\
def f(it):
return (offset == (4, 10) for offset in it)
",
);
let genexpr = find_code(&code, "<genexpr>").expect("missing <genexpr> code");
let ops: Vec<_> = genexpr
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::StoreFastLoadFast { .. })),
"expected compare header to keep split STORE_FAST/LOAD_FAST_BORROW, got ops={ops:?}"
);
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::CompareOp { .. },
]
)
}),
"expected split compare header sequence, got ops={ops:?}"
);
}
#[test]
fn test_fstring_adjacent_literals_are_merged() {
let code = compile_exec(
@@ -13432,6 +13654,74 @@ def f(x):
}));
}
#[test]
fn test_string_and_bytes_binops_constant_fold_like_cpython() {
let code = compile_exec(
"\
x = b'\\\\' + b'u1881'\n\
y = 103 * 'a' + 'x'\n",
);
assert!(
!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_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::BinaryOp { .. })),
"unexpected runtime BINARY_OP after constant subscript folding: {:?}",
code.instructions
);
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
);
}
#[test]
fn test_list_of_constant_tuples_uses_list_extend() {
let code = compile_exec(
+226 -37
View File
@@ -257,7 +257,7 @@ impl CodeInfo {
self.eliminate_unreachable_blocks();
resolve_line_numbers(&mut self.blocks);
redirect_empty_block_targets(&mut self.blocks);
duplicate_end_returns(&mut self.blocks);
duplicate_end_returns(&mut self.blocks, &self.metadata);
self.dce(); // truncate after terminal in blocks that got return duplicated
self.eliminate_unreachable_blocks(); // remove now-unreachable last block
self.remove_redundant_const_pop_top_pairs();
@@ -272,6 +272,8 @@ impl CodeInfo {
self.eliminate_unreachable_blocks();
remove_redundant_nops_and_jumps(&mut self.blocks);
inline_with_suppress_return_blocks(&mut self.blocks);
inline_pop_except_return_blocks(&mut self.blocks);
duplicate_named_except_cleanup_returns(&mut self.blocks, &self.metadata);
self.eliminate_unreachable_blocks();
// Late CFG cleanup can create new same-line STORE_FAST/LOAD_FAST and
// STORE_FAST/STORE_FAST adjacencies in match/capture code paths that
@@ -1196,6 +1198,43 @@ impl CodeInfo {
value: result.into(),
})
}
(ConstantData::Integer { value: n }, ConstantData::Str { value: s })
if matches!(op, BinOp::Multiply) =>
{
let n: usize = n.try_into().ok()?;
if n > 4096 {
return None;
}
let result = s.to_string().repeat(n);
Some(ConstantData::Str {
value: result.into(),
})
}
(ConstantData::Bytes { value: l }, ConstantData::Bytes { value: r })
if matches!(op, BinOp::Add) =>
{
let mut result = l.clone();
result.extend_from_slice(r);
Some(ConstantData::Bytes { value: result })
}
(ConstantData::Bytes { value: b }, ConstantData::Integer { value: n })
if matches!(op, BinOp::Multiply) =>
{
let n: usize = n.try_into().ok()?;
if n > 4096 {
return None;
}
Some(ConstantData::Bytes { value: b.repeat(n) })
}
(ConstantData::Integer { value: n }, ConstantData::Bytes { value: b })
if matches!(op, BinOp::Multiply) =>
{
let n: usize = n.try_into().ok()?;
if n > 4096 {
return None;
}
Some(ConstantData::Bytes { value: b.repeat(n) })
}
_ => None,
}
}
@@ -1204,6 +1243,7 @@ impl CodeInfo {
match c {
ConstantData::Integer { value } => value.bits() > 4096 * 8,
ConstantData::Str { value } => value.len() > 4096,
ConstantData::Bytes { value } => value.len() > 4096,
_ => false,
}
}
@@ -1889,14 +1929,12 @@ impl CodeInfo {
/// Eliminate dead stores in STORE_FAST sequences (apply_static_swaps).
///
/// In sequences of consecutive STORE_FAST instructions (from tuple unpacking),
/// if the same variable is stored to more than once, only the first store
/// (which gets TOS — the rightmost value) matters. Later stores to the
/// same variable are dead and replaced with POP_TOP.
/// Simplified apply_static_swaps (CPython flowgraph.c):
/// In STORE_FAST sequences that follow UNPACK_SEQUENCE / UNPACK_EX,
/// replace duplicate stores to the same variable with POP_TOP.
/// UNPACK pushes values so stores execute left-to-right; the LAST
/// store to a variable carries the final value, earlier ones are dead.
/// only collapse directly adjacent duplicate targets.
///
/// CPython preserves non-adjacent duplicates such as `_, expr, _` so the
/// store layout still reflects the original unpack order. Replacing the
/// first `_` with POP_TOP there changes the emitted superinstructions and
/// bytecode shape even though the final value is the same.
fn eliminate_dead_stores(&mut self) {
for block in &mut self.blocks {
let instructions = &mut block.instructions;
@@ -1924,18 +1962,18 @@ impl CodeInfo {
run_end += 1;
}
if run_end - run_start >= 2 {
// Pass 1: find the LAST occurrence of each variable
let mut last_occurrence = std::collections::HashMap::new();
for (j, instr) in instructions[run_start..run_end].iter().enumerate() {
last_occurrence.insert(u32::from(instr.arg), j);
}
// Pass 2: non-last stores to the same variable are dead
for (j, instr) in instructions[run_start..run_end].iter_mut().enumerate() {
let idx = u32::from(instr.arg);
if last_occurrence[&idx] != j {
let mut j = run_start;
while j < run_end {
let arg = u32::from(instructions[j].arg);
let mut group_end = j + 1;
while group_end < run_end && u32::from(instructions[group_end].arg) == arg {
group_end += 1;
}
for instr in &mut instructions[j..group_end.saturating_sub(1)] {
instr.instr = Opcode::PopTop.into();
instr.arg = OpArg::new(0);
}
j = group_end;
}
}
i = run_end.max(i + 1);
@@ -2356,6 +2394,14 @@ impl CodeInfo {
continue;
}
let prev_real = block.instructions[..i]
.iter()
.rev()
.find_map(|info| info.instr.real());
let next_real = block.instructions[(j + 1)..]
.iter()
.find_map(|info| info.instr.real());
match (curr.instr.real(), next.instr.real()) {
(Some(Instruction::LoadFast { .. }), Some(Instruction::LoadFast { .. })) => {
let idx1 = u32::from(curr.arg);
@@ -2376,6 +2422,13 @@ impl CodeInfo {
Some(Instruction::StoreFast { .. }),
Some(Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }),
) => {
if self.flags.contains(CodeFlags::GENERATOR)
&& matches!(prev_real, Some(Instruction::ForIter { .. }))
&& !matches!(next_real, Some(Instruction::ToBool))
{
i += 1;
continue;
}
let store_idx = u32::from(curr.arg);
let load_idx = u32::from(next.arg);
if store_idx >= 16 || load_idx >= 16 {
@@ -3482,7 +3535,7 @@ impl CodeInfo {
self.debug_block_dump(),
));
duplicate_end_returns(&mut self.blocks);
duplicate_end_returns(&mut self.blocks, &self.metadata);
trace.push((
"after_duplicate_end_returns".to_owned(),
self.debug_block_dump(),
@@ -4172,8 +4225,14 @@ fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) {
if target == BlockIdx::NULL {
continue;
}
// Check if target block's first instruction is an unconditional jump
let target_jump = blocks[target.idx()].instructions.first().copied();
// Thread through blocks that are only leading NOPs followed by an
// unconditional jump so late line anchors do not leave
// JUMP_FORWARD -> NOP -> JUMP_BACKWARD chains behind.
let target_jump = blocks[target.idx()]
.instructions
.iter()
.find(|info| !matches!(info.instr.real(), Some(Instruction::Nop)))
.copied();
if let Some(target_ins) = target_jump
&& target_ins.instr.is_unconditional_jump()
&& target_ins.target != BlockIdx::NULL
@@ -4498,7 +4557,10 @@ fn remove_redundant_nops_in_blocks(blocks: &mut [Block]) -> usize {
if lineno < 0 || prev_lineno == lineno {
remove = true;
} else if src < src_instructions.len() - 1 {
if src_instructions[src + 1].folded_from_nonliteral_expr {
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].folded_from_nonliteral_expr {
remove = true;
} else {
let next_lineno = instruction_lineno(&src_instructions[src + 1]);
@@ -4674,6 +4736,14 @@ fn next_nonempty_block(blocks: &[Block], mut idx: BlockIdx) -> BlockIdx {
idx
}
fn is_load_const_none(instr: &InstructionInfo, metadata: &CodeUnitMetadata) -> bool {
matches!(instr.instr.real(), Some(Instruction::LoadConst { .. }))
&& matches!(
metadata.consts.get_index(u32::from(instr.arg) as usize),
Some(ConstantData::None)
)
}
fn instruction_lineno(instr: &InstructionInfo) -> i32 {
instr
.lineno_override
@@ -5271,7 +5341,7 @@ fn find_layout_predecessor(blocks: &[Block], target: BlockIdx) -> BlockIdx {
/// Duplicate `LOAD_CONST None + RETURN_VALUE` for blocks that fall through
/// to the final return block.
fn duplicate_end_returns(blocks: &mut Vec<Block>) {
fn duplicate_end_returns(blocks: &mut Vec<Block>, metadata: &CodeUnitMetadata) {
// Walk the block chain and keep the last non-cold non-empty block.
// After cold exception handlers are pushed to the end, the mainline
// return epilogue can sit before trailing cold blocks.
@@ -5295,12 +5365,13 @@ fn duplicate_end_returns(blocks: &mut Vec<Block>) {
}
let last_insts = &blocks[last_block.idx()].instructions;
// Only apply when the last block is EXACTLY a return-None epilogue
// Only apply when the last block is EXACTLY a return-None epilogue.
let is_return_block = last_insts.len() == 2
&& matches!(
last_insts[0].instr,
AnyInstruction::Real(Instruction::LoadConst { .. })
)
&& is_load_const_none(&last_insts[0], metadata)
&& matches!(
last_insts[1].instr,
AnyInstruction::Real(Instruction::ReturnValue)
@@ -5416,18 +5487,6 @@ fn duplicate_end_returns(blocks: &mut Vec<Block>) {
}
fn inline_with_suppress_return_blocks(blocks: &mut [Block]) {
fn is_return_block(block: &Block) -> bool {
block.instructions.len() == 2
&& matches!(
block.instructions[0].instr.real(),
Some(Instruction::LoadConst { .. })
)
&& matches!(
block.instructions[1].instr.real(),
Some(Instruction::ReturnValue)
)
}
fn has_with_suppress_prefix(block: &Block, jump_idx: usize) -> bool {
let tail: Vec<_> = block.instructions[..jump_idx]
.iter()
@@ -5460,7 +5519,137 @@ fn inline_with_suppress_return_blocks(blocks: &mut [Block]) {
}
let target = next_nonempty_block(blocks, jump.target);
if target == BlockIdx::NULL || !is_return_block(&blocks[target.idx()]) {
if target == BlockIdx::NULL || !is_const_return_block(&blocks[target.idx()]) {
continue;
}
let mut cloned_return = blocks[target.idx()].instructions.clone();
for instr in &mut cloned_return {
overwrite_location(instr, jump.location, jump.end_location);
}
blocks[block_idx].instructions.pop();
blocks[block_idx].instructions.extend(cloned_return);
}
}
fn is_named_except_cleanup_return_block(block: &Block, metadata: &CodeUnitMetadata) -> bool {
matches!(
block.instructions.as_slice(),
[pop_except, load_none1, store, delete, load_none2, ret]
if matches!(pop_except.instr.real(), Some(Instruction::PopExcept))
&& is_load_const_none(load_none1, metadata)
&& matches!(
store.instr.real(),
Some(Instruction::StoreFast { .. } | Instruction::StoreName { .. })
)
&& matches!(
delete.instr.real(),
Some(Instruction::DeleteFast { .. } | Instruction::DeleteName { .. })
)
&& is_load_const_none(load_none2, metadata)
&& matches!(ret.instr.real(), Some(Instruction::ReturnValue))
)
}
fn duplicate_named_except_cleanup_returns(blocks: &mut Vec<Block>, metadata: &CodeUnitMetadata) {
let predecessors = compute_predecessors(blocks);
let mut clones = Vec::new();
for target in 0..blocks.len() {
let target = BlockIdx(target as u32);
if !is_named_except_cleanup_return_block(&blocks[target.idx()], metadata) {
continue;
}
let layout_pred = find_layout_predecessor(blocks, target);
if layout_pred == BlockIdx::NULL
|| next_nonempty_block(blocks, blocks[layout_pred.idx()].next) != target
{
continue;
}
let fallthroughs_into_target = blocks[layout_pred.idx()]
.instructions
.last()
.map(|ins| !ins.instr.is_scope_exit() && !ins.instr.is_unconditional_jump())
.unwrap_or(true);
if !fallthroughs_into_target || predecessors[target.idx()] < 2 {
continue;
}
for block_idx in 0..blocks.len() {
if block_idx == target.idx() {
continue;
}
let Some(instr_idx) = trailing_conditional_jump_index(&blocks[block_idx]) else {
continue;
};
if next_nonempty_block(blocks, blocks[block_idx].instructions[instr_idx].target)
!= target
{
continue;
}
clones.push((BlockIdx(block_idx as u32), instr_idx, target));
}
}
for (block_idx, instr_idx, target) in clones.into_iter().rev() {
let jump = blocks[block_idx.idx()].instructions[instr_idx];
let mut cloned = blocks[target.idx()].instructions.clone();
if let Some(first) = cloned.first_mut() {
overwrite_location(first, jump.location, jump.end_location);
}
let new_idx = BlockIdx(blocks.len() as u32);
let next = blocks[target.idx()].next;
blocks.push(Block {
cold: blocks[target.idx()].cold,
except_handler: blocks[target.idx()].except_handler,
disable_load_fast_borrow: blocks[target.idx()].disable_load_fast_borrow,
instructions: cloned,
next,
..Block::default()
});
blocks[target.idx()].next = new_idx;
blocks[block_idx.idx()].instructions[instr_idx].target = new_idx;
}
}
fn is_const_return_block(block: &Block) -> bool {
block.instructions.len() == 2
&& matches!(
block.instructions[0].instr.real(),
Some(Instruction::LoadConst { .. })
)
&& matches!(
block.instructions[1].instr.real(),
Some(Instruction::ReturnValue)
)
}
fn inline_pop_except_return_blocks(blocks: &mut [Block]) {
for block_idx in 0..blocks.len() {
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 Some(last_real_before_jump) = blocks[block_idx].instructions[..jump_idx]
.iter()
.rev()
.find_map(|info| info.instr.real())
else {
continue;
};
if !matches!(last_real_before_jump, Instruction::PopExcept) {
continue;
}
let target = next_nonempty_block(blocks, jump.target);
if target == BlockIdx::NULL || !is_const_return_block(&blocks[target.idx()]) {
continue;
}
+26
View File
@@ -1077,6 +1077,9 @@ impl SymbolTableBuilder {
// Register .type_params as a SET symbol (it will be converted to cell variable later)
self.register_name(".type_params", SymbolUsage::Assigned, TextRange::default())?;
if for_class {
self.register_name(".generic_base", SymbolUsage::Assigned, TextRange::default())?;
}
Ok(())
}
@@ -1263,6 +1266,12 @@ 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)
);
// PEP 649: Only AnnAssign annotations can be conditional.
// Function parameter/return annotations are never conditional.
@@ -1286,6 +1295,19 @@ impl SymbolTableBuilder {
}
}
if needs_future_annotation_bookkeeping {
self.register_name(
"__conditional_annotations__",
SymbolUsage::Assigned,
annotation.range(),
)?;
self.register_name(
"__conditional_annotations__",
SymbolUsage::Used,
annotation.range(),
)?;
}
// Create annotation scope for deferred evaluation
let line_number = self.line_index_start(annotation.range());
self.enter_annotation_scope(line_number);
@@ -1437,6 +1459,10 @@ impl SymbolTableBuilder {
self.register_name("__qualname__", SymbolUsage::Assigned, *range)?;
self.register_name("__doc__", SymbolUsage::Assigned, *range)?;
self.register_name("__class__", SymbolUsage::Assigned, *range)?;
if type_params.is_some() {
self.register_name(".type_params", SymbolUsage::Used, *range)?;
self.register_name("__type_params__", SymbolUsage::Assigned, *range)?;
}
self.scan_statements(body)?;
self.leave_scope();
self.in_conditional_block = saved_in_conditional;
+2 -1
View File
@@ -663,7 +663,8 @@ impl Py<PyDict> {
) -> PyResult<Option<PyObjectRef>> {
if self.exact_dict(vm) {
self.entries.get(vm, key)
// FIXME: check __missing__?
// Match CPython's exact-dict fast path: __missing__ only participates
// for dict subclasses through the generic mapping lookup path below.
} else {
match self.as_object().get_item(key, vm) {
Ok(value) => Ok(Some(value)),
+23 -22
View File
@@ -2725,16 +2725,7 @@ impl ExecutingFrame<'_> {
let class_dict = self.pop_value();
let idx = i.get(arg).as_usize();
let name = self.localsplus_name(idx);
// Only treat KeyError as "not found", propagate other exceptions
let value = if let Some(dict_obj) = class_dict.downcast_ref::<PyDict>() {
dict_obj.get_item_opt(name, vm)?
} else {
match class_dict.get_item(name, vm) {
Ok(v) => Some(v),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => None,
Err(e) => return Err(e),
}
};
let value = self.mapping_get_optional(&class_dict, name, vm)?;
self.push_value(match value {
Some(v) => v,
None => self
@@ -2748,18 +2739,7 @@ impl ExecutingFrame<'_> {
// PEP 649: Pop dict from stack (classdict), check there first, then globals
let dict = self.pop_value();
let name = self.code.names[idx.get(arg) as usize];
// Only treat KeyError as "not found", propagate other exceptions
let value = if let Some(dict_obj) = dict.downcast_ref::<PyDict>() {
dict_obj.get_item_opt(name, vm)?
} else {
// Not an exact dict, use mapping protocol
match dict.get_item(name, vm) {
Ok(v) => Some(v),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => None,
Err(e) => return Err(e),
}
};
let value = self.mapping_get_optional(&dict, name, vm)?;
self.push_value(match value {
Some(v) => v,
@@ -6118,6 +6098,27 @@ impl ExecutingFrame<'_> {
}
}
#[inline]
fn mapping_get_optional(
&self,
mapping: &PyObjectRef,
name: &Py<PyStr>,
vm: &VirtualMachine,
) -> PyResult<Option<PyObjectRef>> {
if mapping.class().is(vm.ctx.types.dict_type) {
let dict = mapping
.downcast_ref::<PyDict>()
.expect("exact dict must have a PyDict payload");
dict.get_item_opt(name, vm)
} else {
match mapping.get_item(name, vm) {
Ok(value) => Ok(Some(value)),
Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(None),
Err(err) => Err(err),
}
}
}
#[inline]
fn load_global_or_builtin(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
if let Some(builtins_dict) = self.builtins_dict {