Bytecode parity (#7536)

* Add CFG block splitting, jump threading, backward jump normalization, genexpr StopIteration wrapper

- split_blocks_at_jumps: split blocks at branch points so each has one exit
- jump_threading: thread jumps through single-jump blocks (flowgraph.c jump_thread)
- Backward conditional jump normalization: invert and create NOT_TAKEN+JUMP block
- Follow empty blocks in jump-to-return optimization (next_nonempty_block)
- Add PEP 479 StopIteration handler to compile_comprehension for generators

* Add ConstantData::Slice and constant slice folding

- Add Slice variant to ConstantData and BorrowedConstant
- Fold constant slices (x[:3], x[1:4]) into LOAD_CONST(slice(...))
- Marshal serialization/deserialization for Slice type
- Box::leak in borrow_obj_constant for PySlice roundtrip

* Add duplicate_exits_without_lineno (disabled) and Block: Clone

Prepare infrastructure for exit block duplication optimization.
Currently disabled pending stackdepth integration.

* Improve codegen bytecode parity
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-03-30 18:50:58 +09:00
1 parent 6f98ae3f79
commit 844fdf3a8d
4 files changed
+791 -216

No files matched your search

-1
View File
@@ -529,7 +529,6 @@ class TestTranforms(BytecodeTestCase):
self.assertEqual(len(returns), 1)
self.check_lnotab(f)
@unittest.expectedFailure # TODO: RUSTPYTHON; KeyError: 20
def test_elim_jump_to_return(self):
# JUMP_FORWARD to RETURN --> RETURN
def f(cond, true_value, false_value):
+551 -149
View File
@@ -2481,15 +2481,10 @@ impl Compiler {
idx: bytecode::CommonConstant::AssertionError
}
);
emit!(self, Instruction::PushNull);
match msg {
Some(e) => {
self.compile_expression(e)?;
emit!(self, Instruction::Call { argc: 1 });
}
None => {
emit!(self, Instruction::Call { argc: 0 });
}
if let Some(e) = msg {
emit!(self, Instruction::PushNull);
self.compile_expression(e)?;
emit!(self, Instruction::Call { argc: 1 });
}
emit!(
self,
@@ -3221,20 +3216,19 @@ impl Compiler {
emit!(self, PseudoInstruction::PopBlock);
}
// Create a block for normal path continuation (after handler body succeeds)
let handler_normal_exit = self.new_block();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: handler_normal_exit,
}
);
// cleanup_end block for named handler
// IMPORTANT: In CPython, cleanup_end is within outer SETUP_CLEANUP scope.
// so when RERAISE is executed, it goes to the cleanup block which does POP_EXCEPT.
// We MUST compile cleanup_end BEFORE popping ExceptionHandler so RERAISE routes to cleanup_block.
if let Some(cleanup_end) = handler_cleanup_block {
let handler_normal_exit = self.new_block();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: handler_normal_exit,
}
);
self.switch_to_block(cleanup_end);
if let Some(alias) = name {
// name = None; del name; before RERAISE
@@ -3247,10 +3241,10 @@ impl Compiler {
// This RERAISE is within ExceptionHandler scope, so it routes to cleanup_block
// which does COPY 3; POP_EXCEPT; RERAISE
emit!(self, Instruction::Reraise { depth: 1 });
}
// Switch to normal exit block - this is where handler body success continues
self.switch_to_block(handler_normal_exit);
// Switch to normal exit block - this is where handler body success continues
self.switch_to_block(handler_normal_exit);
}
// PopBlock for outer SETUP_CLEANUP (ExceptionHandler)
emit!(self, PseudoInstruction::PopBlock);
@@ -6789,6 +6783,89 @@ impl Compiler {
Ok(())
}
fn compile_jump_if_compare(
&mut self,
left: &ast::Expr,
ops: &[ast::CmpOp],
comparators: &[ast::Expr],
condition: bool,
target_block: BlockIdx,
) -> CompileResult<()> {
let compare_range = self.current_source_range;
let (last_op, mid_ops) = ops.split_last().unwrap();
let (last_comparator, mid_comparators) = comparators.split_last().unwrap();
self.compile_expression(left)?;
if mid_comparators.is_empty() {
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,
}
);
}
return Ok(());
}
let cleanup = self.new_block();
let end = self.new_block();
for (op, comparator) in mid_ops.iter().zip(mid_comparators) {
self.compile_expression(comparator)?;
self.set_source_range(compare_range);
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::Copy { i: 2 });
self.compile_addcompare(op);
emit!(self, Instruction::PopJumpIfFalse { delta: cleanup });
}
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,
}
);
}
emit!(self, PseudoInstruction::Jump { delta: end });
self.switch_to_block(cleanup);
emit!(self, Instruction::PopTop);
if !condition {
emit!(
self,
PseudoInstruction::Jump {
delta: target_block
}
);
}
self.switch_to_block(end);
Ok(())
}
fn compile_annotation(&mut self, annotation: &ast::Expr) -> CompileResult<()> {
if self.future_annotations {
self.emit_load_const(ConstantData::Str {
@@ -7066,8 +7143,11 @@ impl Compiler {
condition: bool,
target_block: BlockIdx,
) -> CompileResult<()> {
let prev_source_range = self.current_source_range;
self.set_source_range(expression.range());
// Compile expression for test, and jump to label if false
match &expression {
let result = match &expression {
ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => {
match op {
ast::BoolOp::And => {
@@ -7113,13 +7193,20 @@ impl Compiler {
}
}
}
Ok(())
}
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::Not,
operand,
..
}) => {
self.compile_jump_if(operand, !condition, target_block)?;
}) => self.compile_jump_if(operand, !condition, target_block),
ast::Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
..
}) if ops.len() > 1 => {
self.compile_jump_if_compare(left, ops, comparators, condition, target_block)
}
// `x is None` / `x is not None` → POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE
ast::Expr::Compare(ast::ExprCompare {
@@ -7154,6 +7241,7 @@ impl Compiler {
}
);
}
Ok(())
}
_ => {
// Fall back case which always will work!
@@ -7177,9 +7265,12 @@ impl Compiler {
}
);
}
Ok(())
}
}
Ok(())
};
self.set_source_range(prev_source_range);
result
}
/// Compile a boolean operation as an expression.
@@ -7575,8 +7666,11 @@ impl Compiler {
lower, upper, step, ..
}) => {
// Try constant slice folding first
if self.try_fold_constant_slice(lower.as_deref(), upper.as_deref(), step.as_deref())
{
if self.try_fold_constant_slice(
lower.as_deref(),
upper.as_deref(),
step.as_deref(),
)? {
return Ok(());
}
let mut compile_bound = |bound: Option<&ast::Expr>| match bound {
@@ -7991,12 +8085,15 @@ impl Compiler {
// Save the call expression's source range so CALL instructions use the
// call start line, not the last argument's line.
let call_range = self.current_source_range;
let uses_ex_call = self.call_uses_ex_call(args);
// Method call: obj → LOAD_ATTR_METHOD → [method, self_or_null] → args → CALL
// Regular call: func → PUSH_NULL → args → CALL
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = &func {
// Check for super() method call optimization
if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) {
if !uses_ex_call
&& let Some(super_type) = self.can_optimize_super_call(value, attr.as_str())
{
// super().method() or super(cls, self).method() optimization
// Stack: [global_super, class, self] → LOAD_SUPER_METHOD → [method, self]
// Set source range to the super() call for LOAD_GLOBAL/LOAD_DEREF/etc.
@@ -8021,12 +8118,12 @@ impl Compiler {
} else {
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
// Imported names use plain LOAD_ATTR + PUSH_NULL;
// other names use method call mode LOAD_ATTR.
// Imported names and CALL_FUNCTION_EX-style calls use plain
// LOAD_ATTR + PUSH_NULL; other names use method-call mode.
// Check current scope and enclosing scopes for IMPORTED flag.
let is_import = matches!(value.as_ref(), ast::Expr::Name(ast::ExprName { id, .. })
if self.is_name_imported(id.as_str()));
if is_import {
if is_import || uses_ex_call {
self.emit_load_attr(idx);
emit!(self, Instruction::PushNull);
} else {
@@ -8044,6 +8141,16 @@ impl Compiler {
Ok(())
}
fn call_uses_ex_call(&self, arguments: &ast::Arguments) -> bool {
let has_starred = arguments
.args
.iter()
.any(|arg| matches!(arg, ast::Expr::Starred(_)));
let has_double_star = arguments.keywords.iter().any(|k| k.arg.is_none());
let too_big = arguments.args.len() + arguments.keywords.len() > 15;
has_starred || has_double_star || too_big
}
/// Compile subkwargs: emit key-value pairs for BUILD_MAP
fn codegen_subkwargs(
&mut self,
@@ -8929,6 +9036,28 @@ impl Compiler {
}
}
fn compile_fstring_literal_value(
&self,
string: &ast::InterpolatedStringLiteralElement,
flags: ast::FStringFlags,
) -> 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);
crate::string_parser::parse_string_literal(source, string.flags.into()).into()
} else {
string.value.to_string().into()
}
}
fn arg_constant(&mut self, constant: ConstantData) -> oparg::ConstIdx {
let info = self.current_code_info();
info.metadata.consts.insert_full(constant).0.to_u32().into()
@@ -8942,48 +9071,40 @@ impl Compiler {
) -> CompileResult<Option<ConstantData>> {
let mut constants = Vec::with_capacity(elts.len());
for elt in elts {
match elt {
ast::Expr::NumberLiteral(num) => match &num.value {
ast::Number::Int(int) => {
let value = ruff_int_to_bigint(int).map_err(|e| self.error(e))?;
constants.push(ConstantData::Integer { value });
}
ast::Number::Float(f) => {
constants.push(ConstantData::Float { value: *f });
}
ast::Number::Complex { real, imag } => {
constants.push(ConstantData::Complex {
value: Complex::new(*real, *imag),
});
}
},
ast::Expr::StringLiteral(s) => {
constants.push(ConstantData::Str {
value: self.compile_string_value(s),
});
}
ast::Expr::BytesLiteral(b) => {
constants.push(ConstantData::Bytes {
value: b.value.bytes().collect(),
});
}
ast::Expr::BooleanLiteral(b) => {
constants.push(ConstantData::Boolean { value: b.value });
}
ast::Expr::NoneLiteral(_) => {
constants.push(ConstantData::None);
}
ast::Expr::EllipsisLiteral(_) => {
constants.push(ConstantData::Ellipsis);
}
_ => return Ok(None),
}
let Some(constant) = self.try_fold_constant_expr(elt)? else {
return Ok(None);
};
constants.push(constant);
}
Ok(Some(ConstantData::Tuple {
elements: constants,
}))
}
fn try_fold_constant_expr(&mut self, expr: &ast::Expr) -> CompileResult<Option<ConstantData>> {
Ok(Some(match expr {
ast::Expr::NumberLiteral(num) => match &num.value {
ast::Number::Int(int) => ConstantData::Integer {
value: ruff_int_to_bigint(int).map_err(|e| self.error(e))?,
},
ast::Number::Float(f) => ConstantData::Float { value: *f },
ast::Number::Complex { real, imag } => ConstantData::Complex {
value: Complex::new(*real, *imag),
},
},
ast::Expr::StringLiteral(s) => ConstantData::Str {
value: self.compile_string_value(s),
},
ast::Expr::BytesLiteral(b) => ConstantData::Bytes {
value: b.value.bytes().collect(),
},
ast::Expr::BooleanLiteral(b) => ConstantData::Boolean { value: b.value },
ast::Expr::NoneLiteral(_) => ConstantData::None,
ast::Expr::EllipsisLiteral(_) => ConstantData::Ellipsis,
_ => return Ok(None),
}))
}
fn emit_load_const(&mut self, constant: ConstantData) {
let idx = self.arg_constant(constant);
self.emit_arg(idx, |consti| Instruction::LoadConst { consti })
@@ -8995,38 +9116,26 @@ impl Compiler {
lower: Option<&ast::Expr>,
upper: Option<&ast::Expr>,
step: Option<&ast::Expr>,
) -> bool {
fn to_const(expr: Option<&ast::Expr>) -> Option<ConstantData> {
) -> CompileResult<bool> {
let to_const = |expr: Option<&ast::Expr>, this: &mut Self| -> CompileResult<_> {
match expr {
None | Some(ast::Expr::NoneLiteral(_)) => Some(ConstantData::None),
Some(ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. })) => match value
{
ast::Number::Int(i) => Some(ConstantData::Integer {
value: i.as_i64()?.into(),
}),
ast::Number::Float(f) => Some(ConstantData::Float { value: *f }),
ast::Number::Complex { real, imag } => Some(ConstantData::Complex {
value: num_complex::Complex64::new(*real, *imag),
}),
},
Some(ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. })) => {
Some(ConstantData::Boolean { value: *value })
}
// Only match Constant_kind nodes (no UnaryOp)
_ => None,
None => Ok(Some(ConstantData::None)),
Some(expr) => this.try_fold_constant_expr(expr),
}
}
};
let (Some(start), Some(stop), Some(step_val)) =
(to_const(lower), to_const(upper), to_const(step))
else {
return false;
let (Some(start), Some(stop), Some(step_val)) = (
to_const(lower, self)?,
to_const(upper, self)?,
to_const(step, self)?,
) else {
return Ok(false);
};
self.emit_load_const(ConstantData::Slice {
elements: Box::new([start, stop, step_val]),
});
true
Ok(true)
}
fn emit_return_const(&mut self, constant: ConstantData) {
@@ -9493,45 +9602,63 @@ impl Compiler {
fn compile_expr_fstring(&mut self, fstring: &ast::ExprFString) -> CompileResult<()> {
let fstring = &fstring.value;
let mut element_count = 0;
let mut pending_literal = None;
for part in fstring {
self.compile_fstring_part(part)?;
self.compile_fstring_part_into(part, &mut pending_literal, &mut element_count)?;
}
let part_count: u32 = fstring
.iter()
.len()
.try_into()
.expect("BuildString size overflowed");
if part_count > 1 {
emit!(self, Instruction::BuildString { count: part_count });
}
Ok(())
self.finish_fstring(pending_literal, element_count)
}
fn compile_fstring_part(&mut self, part: &ast::FStringPart) -> CompileResult<()> {
fn compile_fstring_part_into(
&mut self,
part: &ast::FStringPart,
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
) -> CompileResult<()> {
match part {
ast::FStringPart::Literal(string) => {
if string.value.contains(char::REPLACEMENT_CHARACTER) {
// might have a surrogate literal; should reparse to be sure
let source = self.source_file.slice(string.range);
let value =
crate::string_parser::parse_string_literal(source, string.flags.into());
self.emit_load_const(ConstantData::Str {
value: value.into(),
});
let value = self.compile_fstring_part_literal_value(string);
if let Some(pending) = pending_literal.as_mut() {
pending.push_wtf8(value.as_ref());
} else {
self.emit_load_const(ConstantData::Str {
value: string.value.to_string().into(),
});
*pending_literal = Some(value);
}
Ok(())
}
ast::FStringPart::FString(fstring) => self.compile_fstring(fstring),
ast::FStringPart::FString(fstring) => self.compile_fstring_elements_into(
fstring.flags,
&fstring.elements,
pending_literal,
element_count,
),
}
}
fn compile_fstring(&mut self, fstring: &ast::FString) -> CompileResult<()> {
self.compile_fstring_elements(fstring.flags, &fstring.elements)
fn finish_fstring(
&mut self,
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;
}
if element_count == 0 {
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
} else if element_count > 1 {
emit!(
self,
Instruction::BuildString {
count: element_count
}
);
}
Ok(())
}
/// Optimize `'format_str' % (args,)` into f-string bytecode.
@@ -9659,24 +9786,31 @@ impl Compiler {
fstring_elements: &ast::InterpolatedStringElements,
) -> CompileResult<()> {
let mut element_count = 0;
let mut pending_literal: Option<Wtf8Buf> = None;
self.compile_fstring_elements_into(
flags,
fstring_elements,
&mut pending_literal,
&mut element_count,
)?;
self.finish_fstring(pending_literal, element_count)
}
fn compile_fstring_elements_into(
&mut self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
) -> CompileResult<()> {
for element in fstring_elements {
element_count += 1;
match element {
ast::InterpolatedStringElement::Literal(string) => {
if string.value.contains(char::REPLACEMENT_CHARACTER) {
// might have a surrogate literal; should reparse to be sure
let source = self.source_file.slice(string.range);
let value = crate::string_parser::parse_fstring_literal_element(
source.into(),
flags.into(),
);
self.emit_load_const(ConstantData::Str {
value: value.into(),
});
let value = self.compile_fstring_literal_value(string, flags);
if let Some(pending) = pending_literal.as_mut() {
pending.push_wtf8(value.as_ref());
} else {
self.emit_load_const(ConstantData::Str {
value: string.value.to_string().into(),
});
*pending_literal = Some(value);
}
}
ast::InterpolatedStringElement::Interpolation(fstring_expr) => {
@@ -9697,8 +9831,10 @@ impl Compiler {
]
.concat();
self.emit_load_const(ConstantData::Str { value: text.into() });
element_count += 1;
let text: Wtf8Buf = text.into();
pending_literal
.get_or_insert_with(Wtf8Buf::new)
.push_wtf8(text.as_ref());
// If debug text is present, apply repr conversion when no `format_spec` specified.
// See action_helpers.c: fstring_find_expr_replacement
@@ -9710,6 +9846,11 @@ impl Compiler {
}
}
if let Some(value) = pending_literal.take() {
self.emit_load_const(ConstantData::Str { value });
*element_count += 1;
}
self.compile_expression(&fstring_expr.expression)?;
match conversion {
@@ -9731,24 +9872,12 @@ impl Compiler {
emit!(self, Instruction::FormatSimple);
}
}
*element_count += 1;
}
}
}
if element_count == 0 {
// ensure to put an empty string on the stack if there aren't any fstring elements
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
} else if element_count > 1 {
emit!(
self,
Instruction::BuildString {
count: element_count
}
);
}
Ok(())
}
@@ -10195,7 +10324,28 @@ mod ruff_tests {
#[cfg(test)]
mod tests {
use super::*;
use rustpython_compiler_core::SourceFileBuilder;
use rustpython_compiler_core::{SourceFileBuilder, bytecode::OpArg};
fn assert_scope_exit_locations(code: &CodeObject) {
for (instr, (location, _)) in code.instructions.iter().zip(code.locations.iter()) {
if matches!(
instr.op,
Instruction::ReturnValue
| Instruction::RaiseVarargs { .. }
| Instruction::Reraise { .. }
) {
assert!(
location.line.get() > 0,
"scope-exit instruction {instr:?} is missing a line number"
);
}
}
for constant in code.constants.iter() {
if let ConstantData::Code { code } = constant {
assert_scope_exit_locations(code);
}
}
}
fn compile_exec(source: &str) -> CodeObject {
let opts = CompileOpts::default();
@@ -10230,6 +10380,19 @@ mod tests {
compiler.exit_scope()
}
fn find_code<'a>(code: &'a CodeObject, name: &str) -> Option<&'a CodeObject> {
if code.obj_name == name {
return Some(code);
}
code.constants.iter().find_map(|constant| {
if let ConstantData::Code { code } = constant {
find_code(code, name)
} else {
None
}
})
}
macro_rules! assert_dis_snapshot {
($value:expr) => {
insta::assert_snapshot!(
@@ -10306,6 +10469,245 @@ async def test():
));
}
#[test]
fn test_scope_exit_instructions_keep_line_numbers() {
let code = compile_exec(
"\
async def test():
for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):
with self.subTest(type=type(stop_exc)):
try:
async with egg():
raise stop_exc
except Exception as ex:
self.assertIs(ex, stop_exc)
else:
self.fail(f'{stop_exc} was suppressed')
",
);
assert_scope_exit_locations(&code);
}
#[test]
fn test_attribute_ex_call_uses_plain_load_attr() {
let code = compile_exec(
"\
def f(cls, args, kwargs):
cls.__new__(cls, *args)
cls.__new__(cls, *args, **kwargs)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ex_call_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::CallFunctionEx))
.count();
let load_attr_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::LoadAttr { .. }))
.count();
assert_eq!(ex_call_count, 2);
assert_eq!(load_attr_count, 2);
for unit in f.instructions.iter() {
if let Instruction::LoadAttr { namei } = unit.op {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
assert!(
!load_attr.is_method(),
"CALL_FUNCTION_EX should use plain LOAD_ATTR"
);
}
}
}
#[test]
fn test_simple_attribute_call_keeps_method_load() {
let code = compile_exec(
"\
def f(obj, arg):
return obj.method(arg)
",
);
let f = find_code(&code, "f").expect("missing function code");
let load_attr = f
.instructions
.iter()
.find_map(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
Some(namei.get(OpArg::new(u32::from(u8::from(unit.arg)))))
}
_ => None,
})
.expect("missing LOAD_ATTR");
assert!(
load_attr.is_method(),
"simple method calls should stay optimized"
);
}
#[test]
fn test_conditional_return_epilogue_is_duplicated() {
let code = compile_exec(
"\
def f(base, cls, state):
if base is object:
obj = object.__new__(cls)
else:
obj = base.__new__(cls, state)
return obj
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(return_count, 2);
}
#[test]
fn test_assert_without_message_raises_class_directly() {
let code = compile_exec(
"\
def f(x):
assert x
",
);
let f = find_code(&code, "f").expect("missing function code");
let call_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::Call { .. }))
.count();
let push_null_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PushNull))
.count();
assert_eq!(call_count, 0);
assert_eq!(push_null_count, 0);
}
#[test]
fn test_chained_compare_jump_uses_single_cleanup_copy() {
let code = compile_exec(
"\
def f(code):
if not 1 <= code <= 2147483647:
raise ValueError('x')
",
);
let f = find_code(&code, "f").expect("missing function code");
let copy_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::Copy { .. }))
.count();
let pop_top_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PopTop))
.count();
assert_eq!(copy_count, 1);
assert_eq!(pop_top_count, 1);
}
#[test]
fn test_constant_slice_folding_handles_string_and_bigint_bounds() {
let code = compile_exec(
"\
def f(obj):
return obj['a':123456789012345678901234567890]
",
);
let f = find_code(&code, "f").expect("missing function code");
let slice = f
.constants
.iter()
.find_map(|constant| match constant {
ConstantData::Slice { elements } => Some(elements),
_ => None,
})
.expect("missing folded slice constant");
assert!(matches!(slice[0], ConstantData::Str { .. }));
assert!(matches!(slice[1], ConstantData::Integer { .. }));
assert!(matches!(slice[2], ConstantData::None));
}
#[test]
fn test_exception_cleanup_jump_to_return_is_inlined() {
let code = compile_exec(
"\
def f(names, cls):
try:
cls.attr = names
except:
pass
return names
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(return_count, 2);
}
#[test]
fn test_fstring_adjacent_literals_are_merged() {
let code = compile_exec(
"\
def f(cls, proto):
raise TypeError(
f\"cannot pickle {cls.__name__!r} object: \"
f\"a class that defines __slots__ without \"
f\"defining __getstate__ cannot be pickled \"
f\"with protocol {proto}\"
)
",
);
let f = find_code(&code, "f").expect("missing function code");
let string_consts = f
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadConst { consti } => {
Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))])
}
_ => None,
})
.filter_map(|constant| match constant {
ConstantData::Str { value } => Some(value.to_string()),
_ => None,
})
.collect::<Vec<_>>();
assert!(
string_consts.iter().any(|value| {
value
== " object: a class that defines __slots__ without defining __getstate__ cannot be pickled with protocol "
}),
"expected merged trailing f-string literal, got {string_consts:?}"
);
assert!(
!string_consts.iter().any(|value| value == " object: "),
"did not expect split trailing literal, got {string_consts:?}"
);
}
#[test]
fn test_optimized_assert_preserves_nested_scope_order() {
compile_exec_optimized(
+230 -66
View File
@@ -223,10 +223,10 @@ impl CodeInfo {
// Phase 2: _PyCfg_OptimizedCfgToInstructionSequence (flowgraph.c)
normalize_jumps(&mut self.blocks);
inline_small_or_no_lineno_blocks(&mut self.blocks);
self.dce(); // re-run within-block DCE after normalize_jumps creates new instructions
self.eliminate_unreachable_blocks();
// TODO: duplicate_exits_without_lineno disabled pending stackdepth fix
// duplicate_exits_without_lineno(&mut self.blocks);
resolve_line_numbers(&mut self.blocks);
duplicate_end_returns(&mut self.blocks);
self.dce(); // truncate after terminal in blocks that got return duplicated
self.eliminate_unreachable_blocks(); // remove now-unreachable last block
@@ -1630,21 +1630,9 @@ impl CodeInfo {
let mut maxdepth = 0u32;
let mut stack = Vec::with_capacity(self.blocks.len());
let mut start_depths = vec![u32::MAX; self.blocks.len()];
start_depths[0] = 0;
stack.push(BlockIdx(0));
stackdepth_push(&mut stack, &mut start_depths, BlockIdx(0), 0);
const DEBUG: bool = false;
// Global iteration limit as safety guard
// The algorithm is monotonic (depths only increase), so it should converge quickly.
// Max iterations = blocks * max_possible_depth_increases per block
let max_iterations = self.blocks.len() * 100;
let mut iterations = 0usize;
'process_blocks: while let Some(block_idx) = stack.pop() {
iterations += 1;
if iterations > max_iterations {
// Safety guard: should never happen in valid code
// Return error instead of silently breaking to avoid underestimated stack depth
return Err(InternalError::StackOverflow);
}
let idx = block_idx.idx();
let mut depth = start_depths[idx];
if DEBUG {
@@ -1727,6 +1715,10 @@ impl CodeInfo {
eprintln!("DONE: {maxdepth}");
}
for (block, &start_depth) in self.blocks.iter_mut().zip(&start_depths) {
block.start_depth = (start_depth != u32::MAX).then_some(start_depth);
}
// Fix up handler stack_depth in ExceptHandlerInfo using start_depths
// computed above: depth = start_depth - 1 - preserve_lasti
for block in self.blocks.iter_mut() {
@@ -1789,7 +1781,6 @@ fn stackdepth_push(
let idx = target.idx();
let block_depth = &mut start_depths[idx];
if depth > *block_depth || *block_depth == u32::MAX {
// Found a path with higher depth (or first visit): update max and queue
*block_depth = depth;
stack.push(target);
}
@@ -2410,7 +2401,7 @@ fn normalize_jumps(blocks: &mut Vec<Block>) {
current = blocks[current.idx()].next;
}
// Replace JUMP → LOAD_CONST + RETURN_VALUE with inline return.
// Replace JUMP → value-producing-instr + RETURN_VALUE with inline return.
// This matches CPython's optimize_basic_block: "Replace JUMP to a RETURN".
for &block_idx in &visit_order {
let idx = block_idx.idx();
@@ -2427,11 +2418,13 @@ fn normalize_jumps(blocks: &mut Vec<Block>) {
target_idx = blocks[target_idx].next.idx();
}
let target_block = &blocks[target_idx];
// Target must start with LOAD_CONST + RETURN_VALUE
if target_block.instructions.len() >= 2 {
// Target must be exactly `value; RETURN_VALUE`.
if target_block.instructions.len() == 2 {
let t0 = &target_block.instructions[0];
let t1 = &target_block.instructions[1];
if matches!(t0.instr.real(), Some(Instruction::LoadConst { .. }))
if matches!(t0.instr, AnyInstruction::Real(_))
&& !t0.instr.is_scope_exit()
&& !t0.instr.is_unconditional_jump()
&& matches!(t1.instr.real(), Some(Instruction::ReturnValue))
{
let mut load = *t0;
@@ -2500,13 +2493,61 @@ fn normalize_jumps(blocks: &mut Vec<Block>) {
}
}
#[allow(dead_code)]
fn is_simple_return_block(block: &Block) -> bool {
block.instructions.len() == 1
&& matches!(
block.instructions[0].instr.real(),
Some(Instruction::ReturnValue)
)
/// flowgraph.c inline_small_or_no_lineno_blocks
fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) {
const MAX_COPY_SIZE: usize = 4;
let block_exits_scope = |block: &Block| {
block
.instructions
.last()
.is_some_and(|ins| ins.instr.is_scope_exit())
};
let block_has_no_lineno = |block: &Block| {
block
.instructions
.iter()
.all(|ins| !instruction_has_lineno(ins))
};
loop {
let mut changes = false;
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let next = blocks[current.idx()].next;
let Some(last) = blocks[current.idx()].instructions.last().copied() else {
current = next;
continue;
};
if !last.instr.is_unconditional_jump() || last.target == BlockIdx::NULL {
current = next;
continue;
}
let target = last.target;
let small_exit_block = block_exits_scope(&blocks[target.idx()])
&& blocks[target.idx()].instructions.len() <= MAX_COPY_SIZE;
let no_lineno_no_fallthrough = block_has_no_lineno(&blocks[target.idx()])
&& !block_has_fallthrough(&blocks[target.idx()]);
if small_exit_block || no_lineno_no_fallthrough {
if let Some(last_instr) = blocks[current.idx()].instructions.last_mut() {
last_instr.instr = Instruction::Nop.into();
last_instr.arg = OpArg::new(0);
last_instr.target = BlockIdx::NULL;
}
let appended = blocks[target.idx()].instructions.clone();
blocks[current.idx()].instructions.extend(appended);
changes = true;
}
current = next;
}
if !changes {
break;
}
}
}
/// Follow chain of empty blocks to find first non-empty block.
@@ -2520,25 +2561,79 @@ fn next_nonempty_block(blocks: &[Block], mut idx: BlockIdx) -> BlockIdx {
idx
}
#[allow(dead_code)]
fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>) {
// Count predecessors for each block
fn instruction_lineno(instr: &InstructionInfo) -> i32 {
instr
.lineno_override
.unwrap_or_else(|| instr.location.line.get() as i32)
}
fn instruction_has_lineno(instr: &InstructionInfo) -> bool {
instruction_lineno(instr) > 0
}
fn block_has_fallthrough(block: &Block) -> bool {
block
.instructions
.last()
.is_none_or(|ins| !ins.instr.is_scope_exit() && !ins.instr.is_unconditional_jump())
}
fn is_jump_instruction(instr: &InstructionInfo) -> bool {
instr.instr.is_unconditional_jump() || is_conditional_jump(&instr.instr)
}
fn is_exit_without_lineno(block: &Block) -> bool {
let Some(first) = block.instructions.first() else {
return false;
};
let Some(last) = block.instructions.last() else {
return false;
};
!instruction_has_lineno(first) && last.instr.is_scope_exit()
}
fn maybe_propagate_location(
instr: &mut InstructionInfo,
location: SourceLocation,
end_location: SourceLocation,
) {
if !instruction_has_lineno(instr) {
instr.location = location;
instr.end_location = end_location;
instr.lineno_override = None;
}
}
fn propagate_locations_in_block(
block: &mut Block,
location: SourceLocation,
end_location: SourceLocation,
) {
let mut prev_location = location;
let mut prev_end_location = end_location;
for instr in &mut block.instructions {
maybe_propagate_location(instr, prev_location, prev_end_location);
prev_location = instr.location;
prev_end_location = instr.end_location;
}
}
fn compute_predecessors(blocks: &[Block]) -> Vec<u32> {
let mut predecessors = vec![0u32; blocks.len()];
if blocks.is_empty() {
return predecessors;
}
predecessors[0] = 1;
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let block = &blocks[current.idx()];
// Fall-through predecessor
let next = next_nonempty_block(blocks, block.next);
if next != BlockIdx::NULL {
let has_fallthrough = block
.instructions
.last()
.is_none_or(|ins| !ins.instr.is_scope_exit() && !ins.instr.is_unconditional_jump());
if has_fallthrough {
if block_has_fallthrough(block) {
let next = next_nonempty_block(blocks, block.next);
if next != BlockIdx::NULL {
predecessors[next.idx()] += 1;
}
}
// Jump target predecessor
for ins in &block.instructions {
if ins.target != BlockIdx::NULL {
let target = next_nonempty_block(blocks, ins.target);
@@ -2549,26 +2644,23 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>) {
}
current = block.next;
}
predecessors
}
// For each block, if its last instruction jumps to an exit block without lineno
// that has >1 predecessor, copy that exit block.
current = BlockIdx(0);
fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Vec<u32>) {
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let block = &blocks[current.idx()];
let last = match block.instructions.last() {
Some(ins) if ins.target != BlockIdx::NULL => ins,
Some(ins) if ins.target != BlockIdx::NULL && is_jump_instruction(ins) => ins,
_ => {
current = blocks[current.idx()].next;
continue;
}
};
if !last.instr.is_unconditional_jump() && !is_conditional_jump(&last.instr) {
current = blocks[current.idx()].next;
continue;
}
let target = next_nonempty_block(blocks, last.target);
if target == BlockIdx::NULL || !is_simple_return_block(&blocks[target.idx()]) {
if target == BlockIdx::NULL || !is_exit_without_lineno(&blocks[target.idx()]) {
current = blocks[current.idx()].next;
continue;
}
@@ -2580,17 +2672,9 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>) {
// Copy the exit block
let new_idx = BlockIdx(blocks.len() as u32);
let mut new_block = blocks[target.idx()].clone();
// Set location from the jump instruction
let jump_loc = blocks[current.idx()].instructions.last().unwrap().location;
let jump_end_loc = blocks[current.idx()]
.instructions
.last()
.unwrap()
.end_location;
if let Some(first) = new_block.instructions.first_mut() {
first.location = jump_loc;
first.end_location = jump_end_loc;
}
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;
blocks.push(new_block);
@@ -2598,26 +2682,106 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>) {
let last_mut = blocks[current.idx()].instructions.last_mut().unwrap();
last_mut.target = new_idx;
predecessors[target.idx()] -= 1;
predecessors.push(1);
current = blocks[current.idx()].next;
}
current = BlockIdx(0);
while current != BlockIdx::NULL {
let block = &blocks[current.idx()];
if let Some(last) = block.instructions.last()
&& block_has_fallthrough(block)
{
let target = next_nonempty_block(blocks, block.next);
if target != BlockIdx::NULL
&& predecessors[target.idx()] == 1
&& is_exit_without_lineno(&blocks[target.idx()])
{
let last_location = last.location;
let last_end_location = last.end_location;
propagate_locations_in_block(
&mut blocks[target.idx()],
last_location,
last_end_location,
);
}
}
current = blocks[current.idx()].next;
}
}
fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) {
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let last = blocks[current.idx()].instructions.last().copied();
if let Some(last) = last {
let (next_block, has_fallthrough) = {
let block = &blocks[current.idx()];
(block.next, block_has_fallthrough(block))
};
{
let block = &mut blocks[current.idx()];
let mut prev_location = None;
for instr in &mut block.instructions {
if let Some((location, end_location)) = prev_location {
maybe_propagate_location(instr, location, end_location);
}
prev_location = Some((instr.location, instr.end_location));
}
}
if has_fallthrough {
let target = next_nonempty_block(blocks, next_block);
if target != BlockIdx::NULL && predecessors[target.idx()] == 1 {
propagate_locations_in_block(
&mut blocks[target.idx()],
last.location,
last.end_location,
);
}
}
if is_jump_instruction(&last) {
let target = next_nonempty_block(blocks, last.target);
if target != BlockIdx::NULL && predecessors[target.idx()] == 1 {
propagate_locations_in_block(
&mut blocks[target.idx()],
last.location,
last.end_location,
);
}
}
}
current = blocks[current.idx()].next;
}
}
fn resolve_line_numbers(blocks: &mut Vec<Block>) {
let mut predecessors = compute_predecessors(blocks);
duplicate_exits_without_lineno(blocks, &mut predecessors);
propagate_line_numbers(blocks, &predecessors);
}
/// Duplicate `LOAD_CONST None + RETURN_VALUE` for blocks that fall through
/// to the final return block.
fn duplicate_end_returns(blocks: &mut [Block]) {
// Walk the block chain to find the last block
let mut last_block = BlockIdx(0);
// Walk the block chain and keep the last non-empty block.
let mut last_block = BlockIdx::NULL;
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
last_block = current;
if !blocks[current.idx()].instructions.is_empty() {
last_block = current;
}
current = blocks[current.idx()].next;
}
if last_block == BlockIdx::NULL {
return;
}
// Check if the last block ends with LOAD_CONST + RETURN_VALUE (the implicit return)
let last_insts = &blocks[last_block.idx()].instructions;
// Only apply when the last block is EXACTLY a return-None epilogue
// AND the return instructions have no explicit line number (lineno <= 0)
let is_return_block = last_insts.len() == 2
&& matches!(
last_insts[0].instr,
@@ -2639,8 +2803,8 @@ fn duplicate_end_returns(blocks: &mut [Block]) {
current = BlockIdx(0);
while current != BlockIdx::NULL {
let block = &blocks[current.idx()];
if current != last_block && block.next == last_block && !block.cold && !block.except_handler
{
let next = next_nonempty_block(blocks, block.next);
if current != last_block && next == last_block && !block.cold && !block.except_handler {
let last_ins = block.instructions.last();
let has_fallthrough = last_ins
.map(|ins| !ins.instr.is_scope_exit() && !ins.instr.is_unconditional_jump())
+10
View File
@@ -42,6 +42,7 @@ impl Function {
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
enum StackValue {
String(String),
@@ -49,6 +50,8 @@ enum StackValue {
Map(HashMap<Wtf8Buf, StackValue>),
Code(Box<CodeObject>),
Function(Function),
Slice(Box<[StackValue; 3]>),
Frozenset(Vec<StackValue>),
}
impl From<ConstantData> for StackValue {
@@ -59,6 +62,13 @@ impl From<ConstantData> for StackValue {
}
ConstantData::None => StackValue::None,
ConstantData::Code { code } => StackValue::Code(code),
ConstantData::Slice { elements } => {
let [start, stop, step] = *elements;
StackValue::Slice(Box::new([start.into(), stop.into(), step.into()]))
}
ConstantData::Frozenset { elements } => {
StackValue::Frozenset(elements.into_iter().map(Into::into).collect())
}
c => unimplemented!("constant {:?} isn't yet supported in py_function!", c),
}
}