mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
d7217074bf
commit
ce0e41de9d
9 files changed
+668
-166
No files matched your search
Vendored
-2
@@ -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}")
|
||||
|
||||
|
||||
Vendored
-1
@@ -441,7 +441,6 @@ class TestTranforms(BytecodeTestCase):
|
||||
self.check_lnotab(code)
|
||||
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_constant_folding_remove_nop_location(self):
|
||||
sources = [
|
||||
"""
|
||||
|
||||
Vendored
-1
@@ -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
|
||||
|
||||
Vendored
-1
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user