Bytecode parity - direct loop backedges (#7578)

This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-04-10 18:48:57 +09:00
1 parent 98de82c5f0
commit 4e2ce58693
3 files changed
+426 -97

No files matched your search

+98 -2
View File
@@ -1512,11 +1512,14 @@ impl Compiler {
}
FBlockType::ForLoop => {
// Pop the iterator
// When returning from a for-loop, CPython swaps the preserved
// value with the iterator and uses POP_TOP for the iterator slot.
if preserve_tos {
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::PopTop);
} else {
emit!(self, Instruction::PopIter);
}
emit!(self, Instruction::PopIter);
}
FBlockType::TryExcept => {
@@ -11148,6 +11151,99 @@ def f(base, cls, state):
assert_eq!(return_count, 2);
}
#[test]
fn test_loop_store_subscr_threads_direct_backedge() {
let code = compile_exec(
"\
def f(kwonlyargs, kwonlydefaults, arg2value):
missing = 0
for kwarg in kwonlyargs:
if kwarg not in arg2value:
if kwonlydefaults and kwarg in kwonlydefaults:
arg2value[kwarg] = kwonlydefaults[kwarg]
else:
missing += 1
return missing
",
);
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 store_subscr = ops
.iter()
.position(|op| matches!(op, Instruction::StoreSubscr))
.expect("missing STORE_SUBSCR");
let next_op = ops
.get(store_subscr + 1)
.expect("missing jump after STORE_SUBSCR");
let window_start = store_subscr.saturating_sub(3);
let window_end = (store_subscr + 5).min(ops.len());
let window = &ops[window_start..window_end];
assert!(
matches!(next_op, Instruction::JumpBackward { .. }),
"expected direct loop backedge after STORE_SUBSCR, got {next_op:?}; ops={window:?}"
);
}
#[test]
fn test_loop_return_reorders_backedge_before_exit_cleanup() {
let code = compile_exec(
"\
def f(obj):
for base in obj.__mro__:
if base is not object:
doc = base.__doc__
if doc is not None:
return doc
",
);
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 cond_idx = ops
.iter()
.position(|op| matches!(op, Instruction::PopJumpIfNotNone { .. }))
.expect("missing POP_JUMP_IF_NOT_NONE");
assert!(
matches!(ops.get(cond_idx + 1), Some(Instruction::NotTaken)),
"expected NOT_TAKEN after conditional jump, got {:?}; ops={ops:?}",
ops.get(cond_idx + 1)
);
assert!(
matches!(
ops.get(cond_idx + 2),
Some(Instruction::JumpBackward { .. })
),
"expected loop backedge immediately after NOT_TAKEN, got {:?}; ops={ops:?}",
ops.get(cond_idx + 2)
);
let end_for_idx = ops
.iter()
.position(|op| matches!(op, Instruction::EndFor))
.expect("missing END_FOR");
let return_before_end = ops[..end_for_idx]
.iter()
.rposition(|op| matches!(op, Instruction::ReturnValue))
.expect("missing loop-body RETURN_VALUE");
assert!(
matches!(ops.get(return_before_end - 1), Some(Instruction::PopTop)),
"expected POP_TOP before loop-body RETURN_VALUE, got {:?}; ops={ops:?}",
ops.get(return_before_end.saturating_sub(1))
);
}
#[test]
fn test_assert_without_message_raises_class_directly() {
let code = compile_exec(
+317 -72
View File
@@ -125,6 +125,13 @@ pub struct ExceptHandlerInfo {
pub preserve_lasti: bool,
}
fn set_to_nop(info: &mut InstructionInfo) {
info.instr = Instruction::Nop.into();
info.arg = OpArg::new(0);
info.target = BlockIdx::NULL;
info.cache_entries = 0;
}
// spell-checker:ignore petgraph
// TODO: look into using petgraph for handling blocks and stuff? it's heavier than this, but it
// might enable more analysis/optimizations
@@ -233,6 +240,8 @@ impl CodeInfo {
// Phase 2: _PyCfg_OptimizedCfgToInstructionSequence (flowgraph.c)
normalize_jumps(&mut self.blocks);
reorder_conditional_exit_and_jump_blocks(&mut self.blocks);
reorder_conditional_jump_and_exit_blocks(&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();
@@ -241,6 +250,14 @@ impl CodeInfo {
self.dce(); // truncate after terminal in blocks that got return duplicated
self.eliminate_unreachable_blocks(); // remove now-unreachable last block
remove_redundant_nops_and_jumps(&mut self.blocks);
// Some jump-only blocks only appear after late CFG cleanup. Thread them
// once more so loop backedges stay direct instead of becoming
// JUMP_FORWARD -> JUMP_BACKWARD chains.
jump_threading_unconditional(&mut self.blocks);
reorder_conditional_exit_and_jump_blocks(&mut self.blocks);
reorder_conditional_jump_and_exit_blocks(&mut self.blocks);
self.eliminate_unreachable_blocks();
remove_redundant_nops_and_jumps(&mut self.blocks);
self.add_checks_for_loads_of_uninitialized_variables();
// optimize_load_fast: after normalize_jumps
self.optimize_load_fast_borrow();
@@ -701,7 +718,7 @@ impl CodeInfo {
block.instructions[i].arg = OpArg::new(const_idx as u32);
// Replace UNARY_NEGATIVE with NOP, inheriting the LOAD_CONST
// location so that remove_nops can clean it up
block.instructions[i + 1].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[i + 1]);
block.instructions[i + 1].location = load_location;
block.instructions[i + 1].end_location = block.instructions[i].end_location;
// Skip the NOP, don't re-check
@@ -761,10 +778,10 @@ impl CodeInfo {
// NOP out the second and third instructions
let loc = block.instructions[i].location;
let end_loc = block.instructions[i].end_location;
block.instructions[i + 1].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[i + 1]);
block.instructions[i + 1].location = loc;
block.instructions[i + 1].end_location = end_loc;
block.instructions[i + 2].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[i + 2]);
block.instructions[i + 2].location = loc;
block.instructions[i + 2].end_location = end_loc;
// Don't advance - check if the result can be folded again
@@ -1026,7 +1043,7 @@ impl CodeInfo {
// BUILD_TUPLE location so remove_nops() can eliminate them.
let folded_loc = block.instructions[i].location;
for j in start_idx..i {
block.instructions[j].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[j]);
block.instructions[j].location = folded_loc;
}
@@ -1125,7 +1142,7 @@ impl CodeInfo {
// NOP the rest
for j in (start_idx + 2)..i {
block.instructions[j].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[j]);
block.instructions[j].location = folded_loc;
}
@@ -1180,9 +1197,9 @@ impl CodeInfo {
if is_build && is_const && is_extend && is_iter {
// Replace: BUILD_X 0 → NOP, keep LOAD_CONST, LIST_EXTEND → NOP
let loc = block.instructions[i].location;
block.instructions[i].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[i]);
block.instructions[i].location = loc;
block.instructions[i + 2].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[i + 2]);
block.instructions[i + 2].location = loc;
i += 4;
} else if matches!(
@@ -1216,7 +1233,7 @@ impl CodeInfo {
let folded_loc = block.instructions[i].location;
for j in start_idx..i {
block.instructions[j].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[j]);
block.instructions[j].location = folded_loc;
}
@@ -1323,7 +1340,7 @@ impl CodeInfo {
block.instructions[start_idx + 1].except_handler = eh;
for j in (start_idx + 2)..i {
block.instructions[j].instr = Instruction::Nop.into();
set_to_nop(&mut block.instructions[j]);
block.instructions[j].location = folded_loc;
}
@@ -1844,8 +1861,7 @@ impl CodeInfo {
.into();
block.instructions[i].arg = OpArg::new(packed);
// Replace second instruction with NOP (CPython: INSTR_SET_OP0(inst2, NOP))
block.instructions[i + 1].instr = Instruction::Nop.into();
block.instructions[i + 1].arg = OpArg::new(0);
set_to_nop(&mut block.instructions[i + 1]);
i += 2; // skip the NOP
} else {
i += 1;
@@ -2701,6 +2717,14 @@ fn split_blocks_at_jumps(blocks: &mut Vec<Block>) {
/// instruction is an unconditional jump, redirect to the final target.
/// flowgraph.c optimize_basic_block + jump_thread
fn jump_threading(blocks: &mut [Block]) {
jump_threading_impl(blocks, true);
}
fn jump_threading_unconditional(blocks: &mut [Block]) {
jump_threading_impl(blocks, false);
}
fn jump_threading_impl(blocks: &mut [Block], include_conditional: bool) {
let mut changed = true;
while changed {
changed = false;
@@ -2709,23 +2733,50 @@ fn jump_threading(blocks: &mut [Block]) {
Some(i) => i,
None => continue,
};
let ins = &blocks[bi].instructions[last_idx];
let ins = blocks[bi].instructions[last_idx];
let target = ins.target;
if target == BlockIdx::NULL {
continue;
}
if !ins.instr.is_unconditional_jump() && !is_conditional_jump(&ins.instr) {
if !(ins.instr.is_unconditional_jump()
|| include_conditional && is_conditional_jump(&ins.instr))
{
continue;
}
if include_conditional && is_conditional_jump(&ins.instr) {
let next = next_nonempty_block(blocks, blocks[bi].next);
if next != BlockIdx::NULL
&& blocks[next.idx()]
.instructions
.last()
.is_some_and(|instr| instr.instr.is_scope_exit())
{
continue;
}
}
// Check if target block's first instruction is an unconditional jump
let target_block = &blocks[target.idx()];
if let Some(target_ins) = target_block.instructions.first()
let target_jump = blocks[target.idx()]
.instructions
.iter()
.find(|ins| !matches!(ins.instr.real(), Some(Instruction::Nop)))
.copied();
if let Some(target_ins) = target_jump
&& target_ins.instr.is_unconditional_jump()
&& target_ins.target != BlockIdx::NULL
&& target_ins.target != target
{
let final_target = target_ins.target;
blocks[bi].instructions[last_idx].target = final_target;
if ins.target == final_target {
continue;
}
set_to_nop(&mut blocks[bi].instructions[last_idx]);
let mut threaded = ins;
threaded.arg = OpArg::new(0);
threaded.target = final_target;
threaded.location = target_ins.location;
threaded.end_location = target_ins.end_location;
threaded.cache_entries = 0;
blocks[bi].instructions.push(threaded);
changed = true;
}
}
@@ -2794,8 +2845,7 @@ fn normalize_jumps(blocks: &mut Vec<Block>) {
&& ins.target == next
});
if is_jump_to_next && let Some(last_instr) = blocks[idx].instructions.last_mut() {
last_instr.instr = Instruction::Nop.into();
last_instr.target = BlockIdx::NULL;
set_to_nop(last_instr);
}
}
@@ -2888,51 +2938,6 @@ fn normalize_jumps(blocks: &mut Vec<Block>) {
current = blocks[current.idx()].next;
}
// 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();
let mut replacements: Vec<(usize, Vec<InstructionInfo>)> = Vec::new();
for (i, ins) in blocks[idx].instructions.iter().enumerate() {
if !ins.instr.is_unconditional_jump() || ins.target == BlockIdx::NULL {
continue;
}
// Follow through empty blocks (next_nonempty_block)
let mut target_idx = ins.target.idx();
while blocks[target_idx].instructions.is_empty()
&& blocks[target_idx].next != BlockIdx::NULL
{
target_idx = blocks[target_idx].next.idx();
}
let target_block = &blocks[target_idx];
// 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, AnyInstruction::Real(_))
&& !t0.instr.is_scope_exit()
&& !t0.instr.is_unconditional_jump()
&& matches!(t1.instr.real(), Some(Instruction::ReturnValue))
{
let mut load = *t0;
let mut ret = *t1;
// Use the jump's location for the inlined return
load.location = ins.location;
load.end_location = ins.end_location;
load.except_handler = ins.except_handler;
ret.location = ins.location;
ret.end_location = ins.end_location;
ret.except_handler = ins.except_handler;
replacements.push((i, vec![load, ret]));
}
}
}
// Apply replacements in reverse order
for (i, new_insts) in replacements.into_iter().rev() {
blocks[idx].instructions.splice(i..i + 1, new_insts);
}
}
// Resolve JUMP/JUMP_NO_INTERRUPT pseudo instructions before offset fixpoint.
let mut block_order = vec![0u32; blocks.len()];
for (pos, &block_idx) in visit_order.iter().enumerate() {
@@ -3019,9 +3024,7 @@ fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) {
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;
set_to_nop(last_instr);
}
let appended = blocks[target.idx()].instructions.clone();
blocks[current.idx()].instructions.extend(appended);
@@ -3118,9 +3121,7 @@ fn remove_redundant_jumps_in_blocks(blocks: &mut [Block]) -> usize {
&& next_nonempty_block(blocks, target) == next
&& let Some(last_instr) = blocks[idx].instructions.last_mut()
{
last_instr.instr = Instruction::Nop.into();
last_instr.arg = OpArg::new(0);
last_instr.target = BlockIdx::NULL;
set_to_nop(last_instr);
changes += 1;
}
current = blocks[idx].next;
@@ -3199,6 +3200,210 @@ fn is_exit_without_lineno(block: &Block) -> bool {
!instruction_has_lineno(first) && last.instr.is_scope_exit()
}
fn is_jump_only_block(block: &Block) -> bool {
let [instr] = block.instructions.as_slice() else {
return false;
};
instr.instr.is_unconditional_jump() && instr.target != BlockIdx::NULL
}
fn is_scope_exit_block(block: &Block) -> bool {
block
.instructions
.last()
.is_some_and(|instr| instr.instr.is_scope_exit())
}
fn trailing_conditional_jump_index(block: &Block) -> Option<usize> {
let last_idx = block.instructions.len().checked_sub(1)?;
if is_conditional_jump(&block.instructions[last_idx].instr)
&& block.instructions[last_idx].target != BlockIdx::NULL
{
return Some(last_idx);
}
let cond_idx = last_idx.checked_sub(1)?;
if matches!(
block.instructions[last_idx].instr.real(),
Some(Instruction::NotTaken)
) && is_conditional_jump(&block.instructions[cond_idx].instr)
&& block.instructions[cond_idx].target != BlockIdx::NULL
{
Some(cond_idx)
} else {
None
}
}
fn reorder_conditional_exit_and_jump_blocks(blocks: &mut [Block]) {
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let idx = current.idx();
let next = blocks[idx].next;
let Some(cond_idx) = trailing_conditional_jump_index(&blocks[idx]) else {
current = next;
continue;
};
let last = blocks[idx].instructions[cond_idx];
let Some(reversed) = reversed_conditional(&last.instr) else {
current = next;
continue;
};
let exit_start = next;
let jump_start = last.target;
if exit_start == BlockIdx::NULL || jump_start == BlockIdx::NULL || exit_start == jump_start
{
current = next;
continue;
}
let mut exit_end = BlockIdx::NULL;
let mut exit_block = BlockIdx::NULL;
let mut cursor = exit_start;
let mut exit_segment_valid = true;
while cursor != BlockIdx::NULL && cursor != jump_start {
if !blocks[cursor.idx()].instructions.is_empty() {
if exit_block != BlockIdx::NULL {
exit_segment_valid = false;
break;
}
exit_block = cursor;
}
exit_end = cursor;
cursor = blocks[cursor.idx()].next;
}
if !exit_segment_valid
|| cursor != jump_start
|| exit_end == BlockIdx::NULL
|| exit_block == BlockIdx::NULL
|| !is_scope_exit_block(&blocks[exit_block.idx()])
{
current = next;
continue;
}
let mut jump_end = BlockIdx::NULL;
let mut jump_block = BlockIdx::NULL;
cursor = jump_start;
while cursor != BlockIdx::NULL {
jump_end = cursor;
if blocks[cursor.idx()].instructions.is_empty() {
cursor = blocks[cursor.idx()].next;
continue;
}
if is_jump_only_block(&blocks[cursor.idx()]) {
jump_block = cursor;
}
break;
}
if jump_block == BlockIdx::NULL {
current = next;
continue;
}
let after_jump = blocks[jump_end.idx()].next;
blocks[idx].next = jump_start;
blocks[jump_end.idx()].next = exit_start;
blocks[exit_end.idx()].next = after_jump;
let cond_mut = &mut blocks[idx].instructions[cond_idx];
cond_mut.instr = reversed;
cond_mut.target = exit_start;
current = after_jump;
}
}
fn reorder_conditional_jump_and_exit_blocks(blocks: &mut [Block]) {
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
let idx = current.idx();
let next = blocks[idx].next;
let Some(cond_idx) = trailing_conditional_jump_index(&blocks[idx]) else {
current = next;
continue;
};
let last = blocks[idx].instructions[cond_idx];
let Some(reversed) = reversed_conditional(&last.instr) else {
current = next;
continue;
};
let jump_start = next;
let exit_start = last.target;
if jump_start == BlockIdx::NULL || exit_start == BlockIdx::NULL || jump_start == exit_start
{
current = next;
continue;
}
let mut jump_end = BlockIdx::NULL;
let mut jump_block = BlockIdx::NULL;
let mut cursor = jump_start;
let mut jump_segment_valid = true;
while cursor != BlockIdx::NULL && cursor != exit_start {
if !blocks[cursor.idx()].instructions.is_empty() {
if jump_block != BlockIdx::NULL || !is_jump_only_block(&blocks[cursor.idx()]) {
jump_segment_valid = false;
break;
}
jump_block = cursor;
}
jump_end = cursor;
cursor = blocks[cursor.idx()].next;
}
if !jump_segment_valid || cursor != exit_start || jump_block == BlockIdx::NULL {
current = next;
continue;
}
let jump_instr = blocks[jump_block.idx()].instructions[0];
if !matches!(
jump_instr.instr.real(),
Some(Instruction::JumpForward { .. })
) {
current = next;
continue;
}
let mut exit_end = BlockIdx::NULL;
let mut exit_block = BlockIdx::NULL;
let after_exit = loop {
if cursor == BlockIdx::NULL {
break BlockIdx::NULL;
}
if !blocks[cursor.idx()].instructions.is_empty() {
if exit_block != BlockIdx::NULL {
break cursor;
}
if !is_scope_exit_block(&blocks[cursor.idx()]) {
exit_block = BlockIdx::NULL;
break BlockIdx::NULL;
}
exit_block = cursor;
}
exit_end = cursor;
cursor = blocks[cursor.idx()].next;
};
if exit_block == BlockIdx::NULL || exit_end == BlockIdx::NULL {
current = next;
continue;
}
blocks[idx].next = exit_start;
blocks[exit_end.idx()].next = jump_start;
blocks[jump_end.idx()].next = after_exit;
let cond_mut = &mut blocks[idx].instructions[cond_idx];
cond_mut.instr = reversed;
cond_mut.target = jump_start;
current = after_exit;
}
}
fn maybe_propagate_location(
instr: &mut InstructionInfo,
location: SourceLocation,
@@ -3321,6 +3526,45 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Ve
}
}
fn duplicate_jump_targets_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.instr.is_unconditional_jump() && ins.target != BlockIdx::NULL => *ins,
_ => {
current = blocks[current.idx()].next;
continue;
}
};
let target = next_nonempty_block(blocks, last.target);
if target == BlockIdx::NULL || !is_jump_only_block(&blocks[target.idx()]) {
current = blocks[current.idx()].next;
continue;
}
if predecessors[target.idx()] <= 1 {
current = blocks[current.idx()].next;
continue;
}
let new_idx = BlockIdx(blocks.len() as u32);
let mut new_block = blocks[target.idx()].clone();
propagate_locations_in_block(&mut new_block, last.location, last.end_location);
let old_next = blocks[current.idx()].next;
new_block.next = old_next;
blocks.push(new_block);
blocks[current.idx()].next = new_idx;
let last_mut = blocks[current.idx()].instructions.last_mut().unwrap();
last_mut.target = new_idx;
predecessors[target.idx()] -= 1;
predecessors.push(1);
current = old_next;
}
}
fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) {
let mut current = BlockIdx(0);
while current != BlockIdx::NULL {
@@ -3371,6 +3615,7 @@ fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) {
fn resolve_line_numbers(blocks: &mut Vec<Block>) {
let mut predecessors = compute_predecessors(blocks);
duplicate_exits_without_lineno(blocks, &mut predecessors);
duplicate_jump_targets_without_lineno(blocks, &mut predecessors);
propagate_line_numbers(blocks, &predecessors);
}
@@ -3516,7 +3761,7 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) {
);
stack.pop();
// POP_BLOCK → NOP
blocks[bi].instructions[i].instr = Instruction::Nop.into();
set_to_nop(&mut blocks[bi].instructions[i]);
} else {
// Set except_handler for this instruction from except stack top
// stack_depth placeholder: filled by fixup_handler_depths
@@ -3595,12 +3840,12 @@ pub(crate) fn convert_pseudo_ops(blocks: &mut [Block], cellfixedoffsets: &[u32])
PseudoInstruction::SetupCleanup { .. }
| PseudoInstruction::SetupFinally { .. }
| PseudoInstruction::SetupWith { .. } => {
info.instr = Instruction::Nop.into();
set_to_nop(info);
}
// PopBlock in reachable blocks is converted to NOP by
// label_exception_targets. Dead blocks may still have them.
PseudoInstruction::PopBlock => {
info.instr = Instruction::Nop.into();
set_to_nop(info);
}
// LOAD_CLOSURE → LOAD_FAST (using cellfixedoffsets for merged layout)
PseudoInstruction::LoadClosure { i } => {
+11 -23
View File
@@ -18,20 +18,11 @@ import re
import sys
import types
# Non-semantic filler instructions to skip
SKIP_OPS = frozenset({"CACHE", "PRECALL", "EXTENDED_ARG"})
# Raw bytecode parity mode: do not skip any instructions.
SKIP_OPS = frozenset()
# Opname normalization: map variant instructions to their base form.
# These variants differ only in optimization hints, not semantics.
_OPNAME_NORMALIZE = {
"LOAD_FAST_BORROW": "LOAD_FAST",
"LOAD_FAST_BORROW_LOAD_FAST_BORROW": "LOAD_FAST_LOAD_FAST",
"LOAD_FAST_CHECK": "LOAD_FAST",
"JUMP_BACKWARD_NO_INTERRUPT": "JUMP_BACKWARD",
"POP_ITER": "POP_TOP",
# Superinstruction normalization: these get decomposed in _extract_instructions
"STORE_FAST_LOAD_FAST_BORROW": "STORE_FAST_LOAD_FAST",
}
_OPNAME_NORMALIZE = {}
_SUPER_DECOMPOSE = {}
# Jump instruction names (fallback when hasjrel/hasjabs is incomplete)
_JUMP_OPNAMES = frozenset(
@@ -188,13 +179,18 @@ def _extract_instructions(code):
except Exception as e:
return [["ERROR", str(e)]]
# Build filtered list and offset-to-index mapping
# Build filtered list and offset-to-index mapping for the normalized stream.
# This must use post-decomposition indices; otherwise a superinstruction that
# expands into multiple logical ops shifts later jump targets by 1.
filtered = []
offset_to_idx = {}
normalized_idx = 0
for inst in raw:
if inst.opname in SKIP_OPS:
continue
offset_to_idx[inst.offset] = len(filtered)
opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname)
offset_to_idx[inst.offset] = normalized_idx
normalized_idx += len(_SUPER_DECOMPOSE.get(opname, (opname,)))
filtered.append(inst)
# Map offsets that land on CACHE slots to the next real instruction
@@ -205,14 +201,6 @@ def _extract_instructions(code):
offset_to_idx[inst.offset] = fi
break
# Superinstruction decomposition: split into constituent parts
# so we compare individual operations regardless of combining.
_SUPER_DECOMPOSE = {
"STORE_FAST_LOAD_FAST": ("STORE_FAST", "LOAD_FAST"),
"STORE_FAST_STORE_FAST": ("STORE_FAST", "STORE_FAST"),
"LOAD_FAST_LOAD_FAST": ("LOAD_FAST", "LOAD_FAST"),
}
result = []
for inst in filtered:
opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname)