Improve codegen bytecode parity (#7541)

- Add CFG block splitting, jump threading, backward jump normalization
- Add genexpr StopIteration wrapper
- Add ConstantData::Slice and constant slice folding
- Add duplicate_exits_without_lineno and Block: Clone
- Add builtin(genexpr) optimization for tuple/list/set/all/any
- Add compile_try_except_no_finally for try-except without finally
- Add module_name_declared_global_in_nested_scope
- Add constant tuple folding in try_fold_constant_expr
- Add fstring literal-only optimization and empty literal elision
- Fix duplicate_exits_without_lineno: splice new blocks into linked list
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-03-31 15:45:18 +09:00
1 parent b2b3070d12
commit 501ff88aed
9 files changed
+936 -155

No files matched your search

-1
View File
@@ -1261,7 +1261,6 @@ class TestLineAndInstructionEvents(CheckEvents):
('instruction', 'func2', 46),
('line', 'get_events', 11)])
@unittest.expectedFailure # TODO: RUSTPYTHON; - instruction offsets differ from CPython
def test_try_except(self):
def func3():
-1
View File
@@ -692,7 +692,6 @@ class ScopeTests(unittest.TestCase):
self.assertEqual(c.dec(), 1)
self.assertEqual(c.dec(), 0)
@unittest.expectedFailure # TODO: RUSTPYTHON; figure out how to communicate that `y = 9` should be stored as a global rather than a STORE_NAME, even when the `global y` is in a nested subscope
def testGlobalInParallelNestedFunctions(self):
# A symbol table bug leaked the global statement from one
# function to other nested functions in the same block.
+734 -45
View File
@@ -56,7 +56,7 @@ impl ExprExt for ast::Expr {
| ast::Expr::NoneLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::EllipsisLiteral(_)
)
) || matches!(self, ast::Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.iter().all(ExprExt::is_constant))
}
fn is_constant_slice(&self) -> bool {
@@ -121,6 +121,15 @@ enum SuperCallType<'a> {
ZeroArg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuiltinGeneratorCallKind {
Tuple,
List,
Set,
All,
Any,
}
#[derive(Debug, Clone)]
pub struct FBlockInfo {
pub fb_type: FBlockType,
@@ -2002,6 +2011,16 @@ impl Compiler {
symboltable::maybe_mangle_name(private, mangled_names, name)
}
fn module_name_declared_global_in_nested_scope(table: &SymbolTable, name: &str) -> bool {
table.sub_tables.iter().any(|subtable| {
(!subtable.comp_inlined
&& subtable
.lookup(name)
.is_some_and(|symbol| symbol.scope == SymbolScope::GlobalExplicit))
|| Self::module_name_declared_global_in_nested_scope(subtable, name)
})
}
// = compiler_nameop
fn compile_name(&mut self, name: &str, usage: NameUsage) -> CompileResult<()> {
enum NameOp {
@@ -2088,12 +2107,20 @@ impl Compiler {
}
};
let module_global_from_nested_scope = {
let current_table = self.current_symbol_table();
current_table.typ == CompilerScope::Module
&& Self::module_name_declared_global_in_nested_scope(current_table, name.as_ref())
};
// Determine operation type based on scope
let op_type = match actual_scope {
SymbolScope::Free => NameOp::Deref,
SymbolScope::Cell => NameOp::Deref,
SymbolScope::Local => {
if is_function_like {
if module_global_from_nested_scope {
NameOp::Global
} else if is_function_like {
NameOp::Fast
} else {
NameOp::Name
@@ -2111,7 +2138,13 @@ impl Compiler {
}
}
SymbolScope::GlobalExplicit => NameOp::Global,
SymbolScope::Unknown => NameOp::Name,
SymbolScope::Unknown => {
if module_global_from_nested_scope {
NameOp::Global
} else {
NameOp::Name
}
}
};
// Generate appropriate instructions based on operation type
@@ -2538,9 +2571,15 @@ impl Compiler {
statement.range(),
));
}
self.compile_expression(v)?;
// Unwind fblock stack with preserve_tos=true (preserve return value)
self.unwind_fblock_stack(true, false)?;
let folded_constant = self.try_fold_constant_expr(v)?;
let preserve_tos = folded_constant.is_none();
if preserve_tos {
self.compile_expression(v)?;
}
self.unwind_fblock_stack(preserve_tos, false)?;
if let Some(constant) = folded_constant {
self.emit_load_const(constant);
}
self.emit_return_value();
}
None => {
@@ -2987,6 +3026,10 @@ impl Compiler {
orelse: &[ast::Stmt],
finalbody: &[ast::Stmt],
) -> CompileResult<()> {
if finalbody.is_empty() {
return self.compile_try_except_no_finally(body, handlers, orelse);
}
let handler_block = self.new_block();
let finally_block = self.new_block();
@@ -3397,6 +3440,175 @@ impl Compiler {
Ok(())
}
fn compile_try_except_no_finally(
&mut self,
body: &[ast::Stmt],
handlers: &[ast::ExceptHandler],
orelse: &[ast::Stmt],
) -> CompileResult<()> {
let handler_block = self.new_block();
let cleanup_block = self.new_block();
let orelse_block = self.new_block();
let end_block = self.new_block();
emit!(self, Instruction::Nop);
emit!(
self,
PseudoInstruction::SetupFinally {
delta: handler_block
}
);
self.push_fblock(FBlockType::TryExcept, handler_block, handler_block)?;
self.compile_statements(body)?;
self.pop_fblock(FBlockType::TryExcept);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: orelse_block
}
);
self.set_no_location();
self.switch_to_block(handler_block);
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: cleanup_block
}
);
self.set_no_location();
emit!(self, Instruction::PushExcInfo);
self.set_no_location();
self.push_fblock(FBlockType::ExceptionHandler, cleanup_block, cleanup_block)?;
for handler in handlers {
let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
name,
body,
range: handler_range,
..
}) = handler;
self.set_source_range(*handler_range);
let next_handler = self.new_block();
if let Some(exc_type) = type_ {
self.compile_expression(exc_type)?;
emit!(self, Instruction::CheckExcMatch);
emit!(
self,
Instruction::PopJumpIfFalse {
delta: next_handler
}
);
}
if let Some(alias) = name {
self.store_name(alias.as_str())?;
let cleanup_end = self.new_block();
let handler_normal_exit = self.new_block();
emit!(self, PseudoInstruction::SetupCleanup { delta: cleanup_end });
self.push_fblock_full(
FBlockType::HandlerCleanup,
cleanup_end,
cleanup_end,
FBlockDatum::ExceptionName(alias.as_str().to_owned()),
)?;
self.compile_statements(body)?;
self.pop_fblock(FBlockType::HandlerCleanup);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: handler_normal_exit
}
);
self.set_no_location();
self.switch_to_block(cleanup_end);
self.emit_load_const(ConstantData::None);
self.set_no_location();
self.store_name(alias.as_str())?;
self.set_no_location();
self.compile_name(alias.as_str(), NameUsage::Delete)?;
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.switch_to_block(handler_normal_exit);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
self.pop_fblock(FBlockType::ExceptionHandler);
emit!(self, Instruction::PopExcept);
self.set_no_location();
self.emit_load_const(ConstantData::None);
self.set_no_location();
self.store_name(alias.as_str())?;
self.set_no_location();
self.compile_name(alias.as_str(), NameUsage::Delete)?;
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
} else {
emit!(self, Instruction::PopTop);
self.push_fblock(FBlockType::HandlerCleanup, end_block, end_block)?;
self.compile_statements(body)?;
self.pop_fblock(FBlockType::HandlerCleanup);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
self.pop_fblock(FBlockType::ExceptionHandler);
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
}
self.push_fblock(FBlockType::ExceptionHandler, cleanup_block, cleanup_block)?;
self.switch_to_block(next_handler);
}
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
self.pop_fblock(FBlockType::ExceptionHandler);
self.switch_to_block(cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.switch_to_block(orelse_block);
self.set_no_location();
self.compile_statements(orelse)?;
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
self.switch_to_block(end_block);
Ok(())
}
fn compile_try_star_except(
&mut self,
body: &[ast::Stmt],
@@ -6801,21 +7013,7 @@ impl Compiler {
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(last_op);
if condition {
emit!(
self,
Instruction::PopJumpIfTrue {
delta: target_block
}
);
} else {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: target_block,
}
);
}
self.emit_pop_jump_by_condition(condition, target_block);
return Ok(());
}
@@ -6834,21 +7032,7 @@ impl Compiler {
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(last_op);
if condition {
emit!(
self,
Instruction::PopJumpIfTrue {
delta: target_block
}
);
} else {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: target_block,
}
);
}
self.emit_pop_jump_by_condition(condition, target_block);
emit!(self, PseudoInstruction::Jump { delta: end });
self.switch_to_block(cleanup);
@@ -6866,6 +7050,24 @@ impl Compiler {
Ok(())
}
fn emit_pop_jump_by_condition(&mut self, condition: bool, target_block: BlockIdx) {
if condition {
emit!(
self,
Instruction::PopJumpIfTrue {
delta: target_block
}
);
} else {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: target_block,
}
);
}
}
fn compile_annotation(&mut self, annotation: &ast::Expr) -> CompileResult<()> {
if self.future_annotations {
self.emit_load_const(ConstantData::Str {
@@ -7554,7 +7756,26 @@ impl Compiler {
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
)
) || matches!(expr, ast::Expr::FString(fstring) if Self::fstring_value_is_const(&fstring.value))
}
fn fstring_value_is_const(fstring: &ast::FStringValue) -> bool {
for part in fstring {
if !Self::fstring_part_is_const(part) {
return false;
}
}
true
}
fn fstring_part_is_const(part: &ast::FStringPart) -> bool {
match part {
ast::FStringPart::Literal(_) => true,
ast::FStringPart::FString(fstring) => fstring
.elements
.iter()
.all(|element| matches!(element, ast::InterpolatedStringElement::Literal(_))),
}
}
fn compile_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> {
@@ -8081,6 +8302,149 @@ impl Compiler {
Ok(())
}
fn detect_builtin_generator_call(
&self,
func: &ast::Expr,
args: &ast::Arguments,
) -> Option<BuiltinGeneratorCallKind> {
let ast::Expr::Name(ast::ExprName { id, .. }) = func else {
return None;
};
if args.args.len() != 1
|| !args.keywords.is_empty()
|| !matches!(args.args[0], ast::Expr::Generator(_))
{
return None;
}
match id.as_str() {
"tuple" => Some(BuiltinGeneratorCallKind::Tuple),
"list" => Some(BuiltinGeneratorCallKind::List),
"set" => Some(BuiltinGeneratorCallKind::Set),
"all" => Some(BuiltinGeneratorCallKind::All),
"any" => Some(BuiltinGeneratorCallKind::Any),
_ => None,
}
}
/// Emit the optimized inline loop for builtin(genexpr) calls.
///
/// Stack on entry: `[func, iter]` where `iter` is the already-compiled
/// generator iterator and `func` is the builtin candidate.
/// On return the compiler is positioned at the fallback block with
/// `[func, iter]` still on the stack (for the normal CALL path).
fn optimize_builtin_generator_call(
&mut self,
kind: BuiltinGeneratorCallKind,
end: BlockIdx,
) -> CompileResult<()> {
let common_constant = match kind {
BuiltinGeneratorCallKind::Tuple => bytecode::CommonConstant::BuiltinTuple,
BuiltinGeneratorCallKind::List => bytecode::CommonConstant::BuiltinList,
BuiltinGeneratorCallKind::Set => bytecode::CommonConstant::BuiltinSet,
BuiltinGeneratorCallKind::All => bytecode::CommonConstant::BuiltinAll,
BuiltinGeneratorCallKind::Any => bytecode::CommonConstant::BuiltinAny,
};
let loop_block = self.new_block();
let cleanup = self.new_block();
let fallback = self.new_block();
let result = matches!(
kind,
BuiltinGeneratorCallKind::All | BuiltinGeneratorCallKind::Any
)
.then(|| self.new_block());
// Stack: [func, iter] — copy func (TOS1) for identity check
emit!(self, Instruction::Copy { i: 2 });
emit!(
self,
Instruction::LoadCommonConstant {
idx: common_constant
}
);
emit!(self, Instruction::IsOp { invert: Invert::No });
emit!(self, Instruction::PopJumpIfFalse { delta: fallback });
emit!(self, Instruction::NotTaken);
// Remove func from [func, iter] → [iter]
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::PopTop);
if matches!(
kind,
BuiltinGeneratorCallKind::Tuple | BuiltinGeneratorCallKind::List
) {
// [iter] → [iter, list] → [list, iter]
emit!(self, Instruction::BuildList { count: 0 });
emit!(self, Instruction::Swap { i: 2 });
} else if matches!(kind, BuiltinGeneratorCallKind::Set) {
// [iter] → [iter, set] → [set, iter]
emit!(self, Instruction::BuildSet { count: 0 });
emit!(self, Instruction::Swap { i: 2 });
}
self.switch_to_block(loop_block);
emit!(self, Instruction::ForIter { delta: cleanup });
match kind {
BuiltinGeneratorCallKind::Tuple | BuiltinGeneratorCallKind::List => {
emit!(self, Instruction::ListAppend { i: 2 });
emit!(self, PseudoInstruction::Jump { delta: loop_block });
}
BuiltinGeneratorCallKind::Set => {
emit!(self, Instruction::SetAdd { i: 2 });
emit!(self, PseudoInstruction::Jump { delta: loop_block });
}
BuiltinGeneratorCallKind::All => {
let result = result.expect("all() optimization should have a result block");
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfFalse { delta: result });
emit!(self, Instruction::NotTaken);
emit!(self, PseudoInstruction::Jump { delta: loop_block });
}
BuiltinGeneratorCallKind::Any => {
let result = result.expect("any() optimization should have a result block");
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfTrue { delta: result });
emit!(self, Instruction::NotTaken);
emit!(self, PseudoInstruction::Jump { delta: loop_block });
}
}
if let Some(result_block) = result {
self.switch_to_block(result_block);
emit!(self, Instruction::PopIter);
self.emit_load_const(ConstantData::Boolean {
value: matches!(kind, BuiltinGeneratorCallKind::Any),
});
emit!(self, PseudoInstruction::Jump { delta: end });
}
self.switch_to_block(cleanup);
emit!(self, Instruction::EndFor);
emit!(self, Instruction::PopIter);
match kind {
BuiltinGeneratorCallKind::Tuple => {
emit!(
self,
Instruction::CallIntrinsic1 {
func: IntrinsicFunction1::ListToTuple
}
);
}
BuiltinGeneratorCallKind::List | BuiltinGeneratorCallKind::Set => {}
BuiltinGeneratorCallKind::All => {
self.emit_load_const(ConstantData::Boolean { value: true });
}
BuiltinGeneratorCallKind::Any => {
self.emit_load_const(ConstantData::Boolean { value: false });
}
}
emit!(self, PseudoInstruction::Jump { delta: end });
self.switch_to_block(fallback);
Ok(())
}
fn compile_call(&mut self, func: &ast::Expr, args: &ast::Arguments) -> CompileResult<()> {
// Save the call expression's source range so CALL instructions use the
// call start line, not the last argument's line.
@@ -8131,6 +8495,23 @@ impl Compiler {
}
self.codegen_call_helper(0, args, call_range)?;
}
} else if let Some(kind) = (!uses_ex_call)
.then(|| self.detect_builtin_generator_call(func, args))
.flatten()
{
// Optimized builtin(genexpr) path: compile the genexpr only once
// so its code object appears exactly once in co_consts.
let end = self.new_block();
self.compile_expression(func)?;
self.compile_expression(&args.args[0])?;
// Stack: [func, iter]
self.optimize_builtin_generator_call(kind, end)?;
// Fallback block: [func, iter] → [func, null, iter] → CALL
emit!(self, Instruction::PushNull);
emit!(self, Instruction::Swap { i: 2 });
self.set_source_range(call_range);
emit!(self, Instruction::Call { argc: 1 });
self.switch_to_block(end);
} else {
// Regular call: push func, then NULL for self_or_null slot
// Stack layout: [func, NULL, args...] - same as method call [func, self, args...]
@@ -9101,6 +9482,16 @@ impl Compiler {
ast::Expr::BooleanLiteral(b) => ConstantData::Boolean { value: b.value },
ast::Expr::NoneLiteral(_) => ConstantData::None,
ast::Expr::EllipsisLiteral(_) => ConstantData::Ellipsis,
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
let mut elements = Vec::with_capacity(elts.len());
for elt in elts {
let Some(constant) = self.try_fold_constant_expr(elt)? else {
return Ok(None);
};
elements.push(constant);
}
ConstantData::Tuple { elements }
}
_ => return Ok(None),
}))
}
@@ -9618,10 +10009,8 @@ impl Compiler {
mut pending_literal: Option<Wtf8Buf>,
mut element_count: u32,
) -> CompileResult<()> {
if let Some(value) = pending_literal.take() {
self.emit_load_const(ConstantData::Str { value });
element_count += 1;
}
let keep_empty = element_count == 0;
self.emit_pending_fstring_literal(&mut pending_literal, &mut element_count, keep_empty);
if element_count == 0 {
self.emit_load_const(ConstantData::Str {
@@ -9639,6 +10028,27 @@ impl Compiler {
Ok(())
}
fn emit_pending_fstring_literal(
&mut self,
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
keep_empty: bool,
) {
let Some(value) = pending_literal.take() else {
return;
};
// CPython drops empty literal fragments when they are adjacent to
// formatted values, but still emits an empty string for a fully-empty
// f-string.
if value.is_empty() && (!keep_empty || *element_count > 0) {
return;
}
self.emit_load_const(ConstantData::Str { value });
*element_count += 1;
}
/// Optimize `'format_str' % (args,)` into f-string bytecode.
/// Returns true if optimization was applied, false to fall back to normal BINARY_OP %.
/// Matches CPython's codegen.c `compiler_formatted_value` optimization.
@@ -9824,10 +10234,7 @@ impl Compiler {
}
}
if let Some(value) = pending_literal.take() {
self.emit_load_const(ConstantData::Str { value });
*element_count += 1;
}
self.emit_pending_fstring_literal(pending_literal, element_count, false);
self.compile_expression(&fstring_expr.expression)?;
@@ -10371,6 +10778,24 @@ mod tests {
})
}
fn has_common_constant(code: &CodeObject, expected: bytecode::CommonConstant) -> bool {
code.instructions.iter().any(|unit| match unit.op {
Instruction::LoadCommonConstant { idx } => {
idx.get(OpArg::new(u32::from(u8::from(unit.arg)))) == expected
}
_ => false,
})
}
fn has_intrinsic_1(code: &CodeObject, expected: IntrinsicFunction1) -> bool {
code.instructions.iter().any(|unit| match unit.op {
Instruction::CallIntrinsic1 { func } => {
func.get(OpArg::new(u32::from(u8::from(unit.arg)))) == expected
}
_ => false,
})
}
macro_rules! assert_dis_snapshot {
($value:expr) => {
insta::assert_snapshot!(
@@ -10528,6 +10953,116 @@ def f(obj, arg):
);
}
#[test]
fn test_builtin_any_genexpr_call_is_optimized() {
let code = compile_exec(
"\
def f(xs):
return any(x for x in xs)
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(has_common_constant(f, bytecode::CommonConstant::BuiltinAny));
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. }))
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::NotTaken))
);
assert_eq!(
f.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PushNull))
.count(),
1,
"fallback call path should remain for shadowed any()"
);
}
#[test]
fn test_builtin_tuple_list_set_genexpr_calls_are_optimized() {
let code = compile_exec(
"\
def tuple_f(xs):
return tuple(x for x in xs)
def list_f(xs):
return list(x for x in xs)
def set_f(xs):
return set(x for x in xs)
",
);
let tuple_f = find_code(&code, "tuple_f").expect("missing tuple_f code");
assert!(has_common_constant(
tuple_f,
bytecode::CommonConstant::BuiltinTuple
));
assert!(has_intrinsic_1(tuple_f, IntrinsicFunction1::ListToTuple));
let tuple_list_append = tuple_f
.instructions
.iter()
.find_map(|unit| match unit.op {
Instruction::ListAppend { .. } => Some(u32::from(u8::from(unit.arg))),
_ => None,
})
.expect("tuple(genexpr) fast path should emit LIST_APPEND");
assert_eq!(tuple_list_append, 2);
let list_f = find_code(&code, "list_f").expect("missing list_f code");
assert!(has_common_constant(
list_f,
bytecode::CommonConstant::BuiltinList
));
assert!(
list_f
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListAppend { .. }))
);
let set_f = find_code(&code, "set_f").expect("missing set_f code");
assert!(has_common_constant(
set_f,
bytecode::CommonConstant::BuiltinSet
));
assert!(
set_f
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::SetAdd { .. }))
);
}
#[test]
fn test_module_store_uses_store_global_when_nested_scope_declares_global() {
let code = compile_exec(
"\
_address_fmt_re = None
class C:
def f(self):
global _address_fmt_re
if _address_fmt_re is None:
_address_fmt_re = 1
",
);
assert!(code.instructions.iter().any(|unit| match unit.op {
Instruction::StoreGlobal { namei } => {
let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
code.names[usize::try_from(idx).unwrap()].as_str() == "_address_fmt_re"
}
_ => false,
}));
}
#[test]
fn test_conditional_return_epilogue_is_duplicated() {
let code = compile_exec(
@@ -10686,6 +11221,160 @@ def f(cls, proto):
);
}
#[test]
fn test_literal_only_fstring_statement_is_optimized_away() {
let code = compile_exec(
"\
def f():
f'''Not a docstring'''
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::PopTop)),
"literal-only f-string statement should be removed"
);
assert!(
!f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.to_string() == "Not a docstring"
)),
"literal-only f-string should not survive in constants"
);
}
#[test]
fn test_empty_fstring_literals_are_elided_around_interpolation() {
let code = compile_exec(
"\
def f(x):
if '' f'{x}':
return 1
return 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let empty_string_loads = f
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadConst { consti } => {
Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))])
}
_ => None,
})
.filter(|constant| {
matches!(
constant,
ConstantData::Str { value } if value.is_empty()
)
})
.count();
let build_string_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::BuildString { .. }))
.count();
assert_eq!(empty_string_loads, 0);
assert_eq!(build_string_count, 0);
}
#[test]
fn test_large_power_is_not_constant_folded() {
let code = compile_exec("x = 2**100\n");
assert!(code.instructions.iter().any(|unit| match unit.op {
Instruction::BinaryOp { op } => {
op.get(OpArg::new(u32::from(u8::from(unit.arg)))) == oparg::BinaryOperator::Power
}
_ => false,
}));
}
#[test]
fn test_list_of_constant_tuples_uses_list_extend() {
let code = compile_exec(
"\
deprecated_cases = [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j')]
",
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListExtend { .. })),
"expected constant tuple list folding"
);
}
#[test]
fn test_constant_list_iterable_uses_tuple() {
let code = compile_exec(
"\
def f():
return {x: y for x, y in [(1, 2), ]}
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildList { .. })),
"constant list iterable should avoid BUILD_LIST before GET_ITER"
);
assert!(f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[ConstantData::Tuple { elements: inner }]
if matches!(
inner.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)
)));
}
#[test]
fn test_constant_set_iterable_keeps_runtime_set_build() {
let code = compile_exec(
"\
def f():
return [x for x in {1, 2, 3}]
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildSet { .. })),
"constant set iterable should keep BUILD_SET before GET_ITER"
);
assert!(f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)));
}
#[test]
fn test_optimized_assert_preserves_nested_scope_order() {
compile_exec_optimized(
+100 -24
View File
@@ -25,6 +25,9 @@ struct LineTableLocation {
end_col: i32,
}
const MAX_INT_SIZE_BITS: u64 = 128;
const MIN_CONST_SEQUENCE_SIZE: usize = 3;
/// Metadata for a code unit
// = _PyCompile_CodeUnitMetadata
#[derive(Clone, Debug)]
@@ -795,7 +798,12 @@ impl CodeInfo {
let result = match op {
BinOp::Add => l + r,
BinOp::Subtract => l - r,
BinOp::Multiply => l * r,
BinOp::Multiply => {
if !l.is_zero() && !r.is_zero() && l.bits() + r.bits() > MAX_INT_SIZE_BITS {
return None;
}
l * r
}
BinOp::FloorDivide => {
if r.is_zero() {
return None;
@@ -821,18 +829,22 @@ impl CodeInfo {
}
}
BinOp::Power => {
let exp: u32 = r.try_into().ok()?;
if exp > 128 {
return None;
} // prevent huge results
num_traits::pow::pow(l.clone(), exp as usize)
}
BinOp::Lshift => {
let shift: u32 = r.try_into().ok()?;
if shift > 128 {
let exp: u64 = r.try_into().ok()?;
let exp_usize = usize::try_from(exp).ok()?;
if !l.is_zero() && exp > 0 && l.bits() > MAX_INT_SIZE_BITS / exp {
return None;
}
l << (shift as usize)
num_traits::pow::pow(l.clone(), exp_usize)
}
BinOp::Lshift => {
let shift: u64 = r.try_into().ok()?;
let shift_usize = usize::try_from(shift).ok()?;
if shift > MAX_INT_SIZE_BITS
|| (!l.is_zero() && l.bits() > MAX_INT_SIZE_BITS - shift)
{
return None;
}
l << shift_usize
}
BinOp::Rshift => {
let shift: u32 = r.try_into().ok()?;
@@ -1070,7 +1082,7 @@ impl CodeInfo {
}
}
if !all_const || list_size < 3 {
if !all_const || list_size < MIN_CONST_SEQUENCE_SIZE {
i += 1;
continue;
}
@@ -1117,31 +1129,42 @@ impl CodeInfo {
}
}
/// Convert constant list/set construction before GET_ITER to just LOAD_CONST tuple.
/// Convert constant list construction before GET_ITER to just LOAD_CONST tuple.
/// BUILD_LIST 0 + LOAD_CONST (tuple) + LIST_EXTEND 1 + GET_ITER
/// → LOAD_CONST (tuple) + GET_ITER
/// Also handles BUILD_SET 0 + LOAD_CONST + SET_UPDATE 1 + GET_ITER.
fn fold_const_iterable_for_iter(&mut self) {
for block in &mut self.blocks {
let mut i = 0;
while i + 3 < block.instructions.len() {
while i + 1 < block.instructions.len() {
let is_build = matches!(
block.instructions[i].instr.real(),
Some(Instruction::BuildList { .. } | Instruction::BuildSet { .. })
Some(Instruction::BuildList { .. })
) && u32::from(block.instructions[i].arg) == 0;
let is_const = matches!(
block.instructions[i + 1].instr.real(),
block
.instructions
.get(i + 1)
.and_then(|instr| instr.instr.real()),
Some(Instruction::LoadConst { .. })
);
let is_extend = matches!(
block.instructions[i + 2].instr.real(),
Some(Instruction::ListExtend { .. } | Instruction::SetUpdate { .. })
) && u32::from(block.instructions[i + 2].arg) == 1;
block
.instructions
.get(i + 2)
.and_then(|instr| instr.instr.real()),
Some(Instruction::ListExtend { .. })
) && block
.instructions
.get(i + 2)
.is_some_and(|instr| u32::from(instr.arg) == 1);
let is_iter = matches!(
block.instructions[i + 3].instr.real(),
block
.instructions
.get(i + 3)
.and_then(|instr| instr.instr.real()),
Some(Instruction::GetIter)
);
@@ -1153,6 +1176,56 @@ impl CodeInfo {
block.instructions[i + 2].instr = Instruction::Nop.into();
block.instructions[i + 2].location = loc;
i += 4;
} else if matches!(
block.instructions[i].instr.real(),
Some(Instruction::BuildList { .. })
) && matches!(
block.instructions[i + 1].instr.real(),
Some(Instruction::GetIter)
) {
let seq_size = u32::from(block.instructions[i].arg) as usize;
if seq_size != 0 && i >= seq_size {
let start_idx = i - seq_size;
let mut elements = Vec::with_capacity(seq_size);
let mut all_const = true;
for j in start_idx..i {
match Self::get_const_value_from(&self.metadata, &block.instructions[j])
{
Some(constant) => elements.push(constant),
None => {
all_const = false;
break;
}
}
}
if all_const {
let const_data = ConstantData::Tuple { elements };
let (const_idx, _) = self.metadata.consts.insert_full(const_data);
let folded_loc = block.instructions[i].location;
for j in start_idx..i {
block.instructions[j].instr = Instruction::Nop.into();
block.instructions[j].location = folded_loc;
}
block.instructions[i].instr = Instruction::LoadConst {
consti: Arg::marker(),
}
.into();
block.instructions[i].arg = OpArg::new(const_idx as u32);
i += 2;
continue;
}
}
block.instructions[i].instr = Instruction::BuildTuple {
count: Arg::marker(),
}
.into();
i += 2;
} else {
i += 1;
}
@@ -2669,14 +2742,16 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Ve
continue;
}
// Copy the exit block
// Copy the exit block and splice it into the linked list after current
let new_idx = BlockIdx(blocks.len() as u32);
let mut new_block = blocks[target.idx()].clone();
let jump_loc = last.location;
let jump_end_loc = last.end_location;
propagate_locations_in_block(&mut new_block, jump_loc, jump_end_loc);
new_block.next = blocks[target.idx()].next;
let old_next = blocks[current.idx()].next;
new_block.next = old_next;
blocks.push(new_block);
blocks[current.idx()].next = new_idx;
// Update the jump target
let last_mut = blocks[current.idx()].instructions.last_mut().unwrap();
@@ -2684,7 +2759,8 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Ve
predecessors[target.idx()] -= 1;
predecessors.push(1);
current = blocks[current.idx()].next;
// Skip past the newly inserted block
current = old_next;
}
current = BlockIdx(0);
@@ -33,12 +33,12 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
26 CACHE
27 STORE_FAST (0, stop_exc)
3 >> 28 LOAD_GLOBAL (4, self)
3 28 LOAD_GLOBAL (4, self)
29 CACHE
30 CACHE
31 CACHE
32 CACHE
>> 33 LOAD_ATTR (7, subTest, method=true)
>> 32 CACHE
33 LOAD_ATTR (7, subTest, method=true)
34 CACHE
35 CACHE
36 CACHE
@@ -51,9 +51,9 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
43 LOAD_GLOBAL (9, NULL + type)
44 CACHE
45 CACHE
>> 46 CACHE
46 CACHE
47 CACHE
48 LOAD_FAST (0, stop_exc)
>> 48 LOAD_FAST (0, stop_exc)
49 CALL (1)
50 CACHE
51 CACHE
@@ -138,30 +138,30 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
125 POP_TOP
126 POP_TOP
127 POP_TOP
128 JUMP_FORWARD (3)
128 JUMP_FORWARD (48)
129 COPY (3)
130 POP_EXCEPT
131 RERAISE (1)
132 JUMP_FORWARD (46)
133 PUSH_EXC_INFO
132 PUSH_EXC_INFO
7 134 LOAD_GLOBAL (12, Exception)
7 133 LOAD_GLOBAL (12, Exception)
134 CACHE
135 CACHE
136 CACHE
137 CACHE
138 CACHE
139 CHECK_EXC_MATCH
140 POP_JUMP_IF_FALSE (33)
141 CACHE
142 NOT_TAKEN
143 STORE_FAST (1, ex)
138 CHECK_EXC_MATCH
139 POP_JUMP_IF_FALSE (32)
140 CACHE
141 NOT_TAKEN
142 STORE_FAST (1, ex)
8 144 LOAD_GLOBAL (4, self)
8 143 LOAD_GLOBAL (4, self)
144 CACHE
145 CACHE
146 CACHE
147 CACHE
148 CACHE
149 LOAD_ATTR (15, assertIs, method=true)
148 LOAD_ATTR (15, assertIs, method=true)
149 CACHE
150 CACHE
151 CACHE
152 CACHE
@@ -170,87 +170,85 @@ expression: "compile_exec(\"\\\nasync def test():\n for stop_exc in (StopIter
155 CACHE
156 CACHE
157 CACHE
158 CACHE
159 LOAD_FAST_LOAD_FAST (ex, 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 POP_EXCEPT
165 LOAD_CONST (None)
166 STORE_FAST (1, ex)
167 DELETE_FAST (1, ex)
168 JUMP_FORWARD (32)
169 LOAD_CONST (None)
170 STORE_FAST (1, ex)
171 DELETE_FAST (1, ex)
172 RERAISE (1)
173 RERAISE (0)
174 COPY (3)
175 POP_EXCEPT
176 RERAISE (1)
10 179 LOAD_GLOBAL (4, self)
10 177 LOAD_GLOBAL (4, self)
178 CACHE
179 CACHE
180 CACHE
181 CACHE
182 CACHE
182 LOAD_ATTR (17, fail, method=true)
183 CACHE
184 LOAD_ATTR (17, fail, method=true)
184 CACHE
185 CACHE
186 CACHE
187 CACHE
>> 187 CACHE
188 CACHE
>> 189 CACHE
189 CACHE
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)
192 LOAD_FAST_BORROW (0, stop_exc)
193 FORMAT_SIMPLE
194 LOAD_CONST (" was suppressed")
195 BUILD_STRING (2)
196 CALL (1)
197 CACHE
198 CACHE
199 CACHE
200 CACHE
201 CACHE
202 POP_TOP
203 NOP
200 POP_TOP
201 NOP
3 204 LOAD_CONST (None)
205 LOAD_CONST (None)
>> 206 LOAD_CONST (None)
207 CALL (3)
3 202 LOAD_CONST (None)
203 LOAD_CONST (None)
>> 204 LOAD_CONST (None)
205 CALL (3)
206 CACHE
207 CACHE
208 CACHE
209 CACHE
210 CACHE
211 POP_TOP
212 JUMP_BACKWARD (189)
213 CACHE
214 PUSH_EXC_INFO
215 WITH_EXCEPT_START
216 TO_BOOL
209 POP_TOP
210 JUMP_BACKWARD (187)
211 CACHE
212 PUSH_EXC_INFO
213 WITH_EXCEPT_START
214 TO_BOOL
215 CACHE
216 CACHE
217 CACHE
218 CACHE
218 POP_JUMP_IF_TRUE (2)
219 CACHE
220 POP_JUMP_IF_TRUE (2)
221 CACHE
222 NOT_TAKEN
223 RERAISE (2)
220 NOT_TAKEN
221 RERAISE (2)
222 POP_TOP
223 POP_EXCEPT
224 POP_TOP
225 POP_EXCEPT
225 POP_TOP
226 POP_TOP
227 POP_TOP
228 POP_TOP
229 JUMP_BACKWARD (206)
230 CACHE
231 COPY (3)
232 POP_EXCEPT
233 RERAISE (1)
227 JUMP_BACKWARD (204)
228 CACHE
229 COPY (3)
230 POP_EXCEPT
231 RERAISE (1)
2 234 CALL_INTRINSIC_1 (StopIterationError)
235 RERAISE (1)
2 232 CALL_INTRINSIC_1 (StopIterationError)
233 RERAISE (1)
2 MAKE_FUNCTION
3 STORE_NAME (0, test)
@@ -645,6 +645,10 @@ oparg_enum!(
BuiltinAll = 3,
/// Built-in `any` function
BuiltinAny = 4,
/// Built-in `list` type
BuiltinList = 5,
/// Built-in `set` type
BuiltinSet = 6,
}
);
@@ -656,6 +660,8 @@ impl fmt::Display for CommonConstant {
Self::BuiltinTuple => "tuple",
Self::BuiltinAll => "all",
Self::BuiltinAny => "any",
Self::BuiltinList => "list",
Self::BuiltinSet => "set",
};
write!(f, "{name}")
}
+12 -2
View File
@@ -2786,8 +2786,18 @@ impl ExecutingFrame<'_> {
vm.ctx.exceptions.not_implemented_error.to_owned().into()
}
CommonConstant::BuiltinTuple => vm.ctx.types.tuple_type.to_owned().into(),
CommonConstant::BuiltinAll => vm.builtins.get_attr("all", vm)?,
CommonConstant::BuiltinAny => vm.builtins.get_attr("any", vm)?,
CommonConstant::BuiltinAll => vm
.callable_cache
.builtin_all
.clone()
.expect("builtin_all not initialized"),
CommonConstant::BuiltinAny => vm
.callable_cache
.builtin_any
.clone()
.expect("builtin_any not initialized"),
CommonConstant::BuiltinList => vm.ctx.types.list_type.to_owned().into(),
CommonConstant::BuiltinSet => vm.ctx.types.set_type.to_owned().into(),
};
self.push_value(value);
Ok(None)
+2 -2
View File
@@ -129,8 +129,8 @@ pub fn get_git_datetime() -> String {
}
// Must be aligned to Lib/importlib/_bootstrap_external.py
// Bumped to 2997 for MAKE_CELL/COPY_FREE_VARS prolog and cell-local merging
pub const PYC_MAGIC_NUMBER: u16 = 2997;
// Bumped to 2994 for new CommonConstant discriminants (BuiltinList, BuiltinSet)
pub const PYC_MAGIC_NUMBER: u16 = 2994;
// CPython format: magic_number | ('\r' << 16) | ('\n' << 24)
// This protects against text-mode file reads
+4
View File
@@ -576,6 +576,8 @@ pub(crate) struct CallableCache {
pub len: Option<PyObjectRef>,
pub isinstance: Option<PyObjectRef>,
pub list_append: Option<PyObjectRef>,
pub builtin_all: Option<PyObjectRef>,
pub builtin_any: Option<PyObjectRef>,
}
pub struct PyGlobalState {
@@ -641,6 +643,8 @@ impl VirtualMachine {
.get_attr(self.ctx.intern_str("append"))
.ok_or_else(|| self.new_runtime_error("failed to cache list.append".to_owned()))?;
self.callable_cache.list_append = Some(list_append);
self.callable_cache.builtin_all = Some(self.builtins.get_attr("all", self)?);
self.callable_cache.builtin_any = Some(self.builtins.get_attr("any", self)?);
Ok(())
}