mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
peephole: drop dead LDA #imm before mem-INC/DEC + JMP
The IR codegen lowers `i -= 1` (and friends) into a `LoadImm temp,
1; Sub d, i, temp; StoreVar i, d` triple, and the optimizer
strength-reduces the Sub+StoreVar pair into `DEC i`. The
constant-load-into-A that used to feed the Sub stays around as a
dead `LDA #1`:
LDA #1
DEC ZeroPage(rem)
JMP Label("__ir_blk_while_cond_…")
`remove_dead_loads` was set up to drop exactly this pattern but
gave up at the trailing `JMP` because it couldn't reason about
flow. Extend it to follow one unconditional `JMP <label>` to its
target and resume the dead-store scan from the next instruction.
The first instruction past the loop-condition label is reliably an
`LDA loop_var`, which overwrites A without reading it — so the
`LDA #1` is correctly identified as dead.
Conditional branches still end the scan (their not-taken path is
unconstrained) and only one JMP is followed (to keep the analysis
local). For SHA-256 specifically this drops two `LDA #1`s per
iteration of the rotate/shift bit-loops — about 1K cycles per
block. The same pattern fires across most examples' loop tails.
Verified: cargo test/clippy/fmt clean on rustc 1.95.0; emulator
harness 34/34; reproducibility diff clean; SHA-256 of "NES" still
computes to AE9145DB…4E0D. The cycle drift refreshes the four
audio hashes / golden frames timing-sensitive examples already
tracked.
https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
This commit is contained in:
parent
0600f5b872
commit
df71c2bf50
12 changed files with 40 additions and 22 deletions
|
|
@ -231,11 +231,22 @@ fn invert_branch(op: Opcode) -> Option<Opcode> {
|
|||
/// Remove `LDA …` instructions whose value is never read — the next
|
||||
/// instruction overwrites A without using the current value.
|
||||
///
|
||||
/// The heuristic looks one instruction ahead. If the next instruction
|
||||
/// is an A-clobbering load (`LDA`, `LDX`, `LDY`, `PLA`, `TXA`, `TYA`),
|
||||
/// the preceding `LDA` is dead. Shifts and arithmetic ops read A, so
|
||||
/// they don't qualify. A label or branch in between stops the scan
|
||||
/// (we can't prove A's dead across control flow).
|
||||
/// The heuristic looks forward, stepping over instructions that
|
||||
/// don't touch A (memory `INC`/`DEC`/`STX`/`STY`, labels). If the
|
||||
/// first instruction that *does* touch A overwrites it without
|
||||
/// reading it (`LDA`, `PLA`, `TXA`, `TYA`), the preceding `LDA` is
|
||||
/// dead. Shifts and arithmetic ops read A, so they end the scan
|
||||
/// without marking dead.
|
||||
///
|
||||
/// One unconditional `JMP` is followed: we look up its target label
|
||||
/// and resume scanning from the first instruction after it. This
|
||||
/// catches `LDA #imm; DEC zp; JMP loop_cond; loop_cond: LDA loop_var`
|
||||
/// patterns that the IR codegen leaves behind for `i -= 1`-style
|
||||
/// loops, where the `LDA #1` was the constant operand of a `Sub`
|
||||
/// the optimizer already strength-reduced into the `DEC`. Conditional
|
||||
/// branches and `JSR` still end the scan — JSR could land on a
|
||||
/// runtime helper that reads A, and a branch's not-taken path is
|
||||
/// unconstrained.
|
||||
fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||
let mut keep = vec![true; instructions.len()];
|
||||
for i in 0..instructions.len() {
|
||||
|
|
@ -243,19 +254,9 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
|||
if inst.opcode != Opcode::LDA {
|
||||
continue;
|
||||
}
|
||||
// Walk forward looking for the next instruction that either
|
||||
// reads A or overwrites it. If the first such instruction
|
||||
// overwrites A without reading it, our LDA is dead.
|
||||
//
|
||||
// We can safely step across instructions that neither read
|
||||
// nor write A — INC, DEC, STX, STY on memory, and label
|
||||
// definitions — because they don't observe A and don't
|
||||
// clobber it either. We must NOT step over any branch or
|
||||
// jump: control flow at that point can reach code that
|
||||
// reads A via a different path, and our local analysis
|
||||
// can't see it.
|
||||
let mut j = i + 1;
|
||||
let mut dead = false;
|
||||
let mut followed_jmp = false;
|
||||
while j < instructions.len() {
|
||||
let next = &instructions[j];
|
||||
// Labels are passive markers.
|
||||
|
|
@ -263,8 +264,7 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
|||
j += 1;
|
||||
continue;
|
||||
}
|
||||
// Instructions that don't touch A — skip over them.
|
||||
// INC/DEC on memory, STX/STY — all leave A alone.
|
||||
// Memory INC/DEC/STX/STY don't touch A.
|
||||
if matches!(
|
||||
next.opcode,
|
||||
Opcode::INC | Opcode::DEC | Opcode::STX | Opcode::STY
|
||||
|
|
@ -273,6 +273,24 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
|||
j += 1;
|
||||
continue;
|
||||
}
|
||||
// Cross one unconditional `JMP <label>` by looking the
|
||||
// target up and resuming scan there. Refuse a second
|
||||
// JMP — the analysis is meant to catch a single
|
||||
// straight-line `loop_body → loop_cond` jump, not an
|
||||
// arbitrary chain.
|
||||
if !followed_jmp && next.opcode == Opcode::JMP {
|
||||
if let AddressingMode::Label(target) = &next.mode {
|
||||
if let Some(target_idx) = instructions.iter().position(|ins| {
|
||||
ins.opcode == Opcode::NOP
|
||||
&& matches!(&ins.mode, AddressingMode::Label(name) if name == target)
|
||||
}) {
|
||||
followed_jmp = true;
|
||||
j = target_idx + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Instructions that overwrite A without reading it.
|
||||
if matches!(
|
||||
next.opcode,
|
||||
|
|
@ -281,8 +299,8 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
|||
dead = true;
|
||||
}
|
||||
// Any other instruction — might read A (STA, ADC,
|
||||
// SBC, AND, …) or transfer control (JMP, Bxx, JSR,
|
||||
// RTS) — stop scanning and leave the LDA alone.
|
||||
// SBC, AND, JSR …) or transfer control (Bxx, RTS) —
|
||||
// stop scanning.
|
||||
break;
|
||||
}
|
||||
if dead {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue