Skip to content

Commit 35f5642

Browse files
committed
Bytecode parity - direct loop backedges
1 parent a49ce5b commit 35f5642

File tree

3 files changed

+109
-10
lines changed

3 files changed

+109
-10
lines changed

crates/codegen/src/compile.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11148,6 +11148,46 @@ def f(base, cls, state):
1114811148
assert_eq!(return_count, 2);
1114911149
}
1115011150

11151+
#[test]
11152+
fn test_loop_store_subscr_threads_direct_backedge() {
11153+
let code = compile_exec(
11154+
"\
11155+
def f(kwonlyargs, kwonlydefaults, arg2value):
11156+
missing = 0
11157+
for kwarg in kwonlyargs:
11158+
if kwarg not in arg2value:
11159+
if kwonlydefaults and kwarg in kwonlydefaults:
11160+
arg2value[kwarg] = kwonlydefaults[kwarg]
11161+
else:
11162+
missing += 1
11163+
return missing
11164+
",
11165+
);
11166+
let f = find_code(&code, "f").expect("missing function code");
11167+
let ops: Vec<_> = f
11168+
.instructions
11169+
.iter()
11170+
.map(|unit| unit.op)
11171+
.filter(|op| !matches!(op, Instruction::Cache))
11172+
.collect();
11173+
11174+
let store_subscr = ops
11175+
.iter()
11176+
.position(|op| matches!(op, Instruction::StoreSubscr))
11177+
.expect("missing STORE_SUBSCR");
11178+
let next_op = ops
11179+
.get(store_subscr + 1)
11180+
.expect("missing jump after STORE_SUBSCR");
11181+
let window_start = store_subscr.saturating_sub(3);
11182+
let window_end = (store_subscr + 5).min(ops.len());
11183+
let window = &ops[window_start..window_end];
11184+
11185+
assert!(
11186+
matches!(next_op, Instruction::JumpBackward { .. }),
11187+
"expected direct loop backedge after STORE_SUBSCR, got {next_op:?}; ops={window:?}"
11188+
);
11189+
}
11190+
1115111191
#[test]
1115211192
fn test_assert_without_message_raises_class_directly() {
1115311193
let code = compile_exec(

crates/codegen/src/ir.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ impl CodeInfo {
241241
self.dce(); // truncate after terminal in blocks that got return duplicated
242242
self.eliminate_unreachable_blocks(); // remove now-unreachable last block
243243
remove_redundant_nops_and_jumps(&mut self.blocks);
244+
// Some jump-only blocks only appear after late CFG cleanup. Thread them
245+
// once more so loop backedges stay direct instead of becoming
246+
// JUMP_FORWARD -> JUMP_BACKWARD chains.
247+
jump_threading(&mut self.blocks);
248+
self.eliminate_unreachable_blocks();
249+
remove_redundant_nops_and_jumps(&mut self.blocks);
244250
self.add_checks_for_loads_of_uninitialized_variables();
245251
// optimize_load_fast: after normalize_jumps
246252
self.optimize_load_fast_borrow();
@@ -3199,6 +3205,13 @@ fn is_exit_without_lineno(block: &Block) -> bool {
31993205
!instruction_has_lineno(first) && last.instr.is_scope_exit()
32003206
}
32013207

3208+
fn is_jump_only_block(block: &Block) -> bool {
3209+
let [instr] = block.instructions.as_slice() else {
3210+
return false;
3211+
};
3212+
instr.instr.is_unconditional_jump() && instr.target != BlockIdx::NULL
3213+
}
3214+
32023215
fn maybe_propagate_location(
32033216
instr: &mut InstructionInfo,
32043217
location: SourceLocation,
@@ -3321,6 +3334,45 @@ fn duplicate_exits_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Ve
33213334
}
33223335
}
33233336

3337+
fn duplicate_jump_targets_without_lineno(blocks: &mut Vec<Block>, predecessors: &mut Vec<u32>) {
3338+
let mut current = BlockIdx(0);
3339+
while current != BlockIdx::NULL {
3340+
let block = &blocks[current.idx()];
3341+
let last = match block.instructions.last() {
3342+
Some(ins) if ins.instr.is_unconditional_jump() && ins.target != BlockIdx::NULL => *ins,
3343+
_ => {
3344+
current = blocks[current.idx()].next;
3345+
continue;
3346+
}
3347+
};
3348+
3349+
let target = next_nonempty_block(blocks, last.target);
3350+
if target == BlockIdx::NULL || !is_jump_only_block(&blocks[target.idx()]) {
3351+
current = blocks[current.idx()].next;
3352+
continue;
3353+
}
3354+
if predecessors[target.idx()] <= 1 {
3355+
current = blocks[current.idx()].next;
3356+
continue;
3357+
}
3358+
3359+
let new_idx = BlockIdx(blocks.len() as u32);
3360+
let mut new_block = blocks[target.idx()].clone();
3361+
propagate_locations_in_block(&mut new_block, last.location, last.end_location);
3362+
let old_next = blocks[current.idx()].next;
3363+
new_block.next = old_next;
3364+
blocks.push(new_block);
3365+
blocks[current.idx()].next = new_idx;
3366+
3367+
let last_mut = blocks[current.idx()].instructions.last_mut().unwrap();
3368+
last_mut.target = new_idx;
3369+
predecessors[target.idx()] -= 1;
3370+
predecessors.push(1);
3371+
3372+
current = old_next;
3373+
}
3374+
}
3375+
33243376
fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) {
33253377
let mut current = BlockIdx(0);
33263378
while current != BlockIdx::NULL {
@@ -3371,6 +3423,7 @@ fn propagate_line_numbers(blocks: &mut [Block], predecessors: &[u32]) {
33713423
fn resolve_line_numbers(blocks: &mut Vec<Block>) {
33723424
let mut predecessors = compute_predecessors(blocks);
33733425
duplicate_exits_without_lineno(blocks, &mut predecessors);
3426+
duplicate_jump_targets_without_lineno(blocks, &mut predecessors);
33743427
propagate_line_numbers(blocks, &predecessors);
33753428
}
33763429

scripts/dis_dump.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
"STORE_FAST_LOAD_FAST_BORROW": "STORE_FAST_LOAD_FAST",
3434
}
3535

36+
# Superinstruction normalization: split into constituent parts so jump target
37+
# indices are computed against the same logical instruction stream on both
38+
# interpreters.
39+
_SUPER_DECOMPOSE = {
40+
"STORE_FAST_LOAD_FAST": ("STORE_FAST", "LOAD_FAST"),
41+
"STORE_FAST_STORE_FAST": ("STORE_FAST", "STORE_FAST"),
42+
"LOAD_FAST_LOAD_FAST": ("LOAD_FAST", "LOAD_FAST"),
43+
}
44+
3645
# Jump instruction names (fallback when hasjrel/hasjabs is incomplete)
3746
_JUMP_OPNAMES = frozenset(
3847
{
@@ -188,13 +197,18 @@ def _extract_instructions(code):
188197
except Exception as e:
189198
return [["ERROR", str(e)]]
190199

191-
# Build filtered list and offset-to-index mapping
200+
# Build filtered list and offset-to-index mapping for the normalized stream.
201+
# This must use post-decomposition indices; otherwise a superinstruction that
202+
# expands into multiple logical ops shifts later jump targets by 1.
192203
filtered = []
193204
offset_to_idx = {}
205+
normalized_idx = 0
194206
for inst in raw:
195207
if inst.opname in SKIP_OPS:
196208
continue
197-
offset_to_idx[inst.offset] = len(filtered)
209+
opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname)
210+
offset_to_idx[inst.offset] = normalized_idx
211+
normalized_idx += len(_SUPER_DECOMPOSE.get(opname, (opname,)))
198212
filtered.append(inst)
199213

200214
# Map offsets that land on CACHE slots to the next real instruction
@@ -205,14 +219,6 @@ def _extract_instructions(code):
205219
offset_to_idx[inst.offset] = fi
206220
break
207221

208-
# Superinstruction decomposition: split into constituent parts
209-
# so we compare individual operations regardless of combining.
210-
_SUPER_DECOMPOSE = {
211-
"STORE_FAST_LOAD_FAST": ("STORE_FAST", "LOAD_FAST"),
212-
"STORE_FAST_STORE_FAST": ("STORE_FAST", "STORE_FAST"),
213-
"LOAD_FAST_LOAD_FAST": ("LOAD_FAST", "LOAD_FAST"),
214-
}
215-
216222
result = []
217223
for inst in filtered:
218224
opname = _OPNAME_NORMALIZE.get(inst.opname, inst.opname)

0 commit comments

Comments
 (0)