mirror of
https://github.com/imjasonh/nescript
synced 2026-07-11 02:02:58 +00:00
Codegen: A-value tracking peephole for redundant LDA elimination
Tracks what the accumulator currently holds across a linear run of instructions. When we see \`LDA addr\` or \`LDA #val\` and A is already known to hold that value, the load is dropped. A-modifying ops (ADC, SBC, shifts, transfers from X/Y, etc.) clear the tracker. Any control-flow instruction or label resets it too, so JSR / branches remain safe. Reduces STA/LDA count in bouncing_ball from 113 to 108. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
d4daa6d0a9
commit
e1bddbc0db
1 changed files with 119 additions and 3 deletions
|
|
@ -13,12 +13,110 @@ pub fn optimize(instructions: &mut Vec<Instruction>) {
|
||||||
remove_sta_then_lda(instructions);
|
remove_sta_then_lda(instructions);
|
||||||
remove_lda_then_sta_same(instructions);
|
remove_lda_then_sta_same(instructions);
|
||||||
remove_dead_temp_stores(instructions);
|
remove_dead_temp_stores(instructions);
|
||||||
|
remove_redundant_loads(instructions);
|
||||||
if instructions.len() == before {
|
if instructions.len() == before {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track what `A` holds through a linear run of instructions and
|
||||||
|
/// eliminate `LDA` that would reload a value A already has.
|
||||||
|
///
|
||||||
|
/// The tracker knows about three states:
|
||||||
|
/// - `AKnown::None` — A's value is unknown
|
||||||
|
/// - `AKnown::Addr(addr)` — A equals the byte currently at `addr`
|
||||||
|
/// - `AKnown::Imm(val)` — A equals the immediate value `val`
|
||||||
|
///
|
||||||
|
/// After any instruction that may clobber A (ADC, AND, etc.) we
|
||||||
|
/// transition to `None`. After `STA addr` A is still known and we
|
||||||
|
/// additionally record that `addr` now equals A's known value (so a
|
||||||
|
/// later `LDA addr` is redundant). Any control-flow instruction or
|
||||||
|
/// label resets the tracker.
|
||||||
|
fn remove_redundant_loads(instructions: &mut Vec<Instruction>) {
|
||||||
|
use AKnown::*;
|
||||||
|
let mut keep = vec![true; instructions.len()];
|
||||||
|
let mut a: AKnown = None;
|
||||||
|
for (i, inst) in instructions.iter().enumerate() {
|
||||||
|
if instruction_crosses_block(inst) {
|
||||||
|
a = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match (inst.opcode, &inst.mode) {
|
||||||
|
(Opcode::LDA, AddressingMode::Immediate(v)) => {
|
||||||
|
if let Imm(existing) = a {
|
||||||
|
if existing == *v {
|
||||||
|
keep[i] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a = Imm(*v);
|
||||||
|
}
|
||||||
|
(Opcode::LDA, AddressingMode::ZeroPage(addr)) => {
|
||||||
|
if let Addr(existing) = a {
|
||||||
|
if existing == *addr {
|
||||||
|
keep[i] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a = Addr(*addr);
|
||||||
|
}
|
||||||
|
(Opcode::LDA, AddressingMode::Absolute(addr)) => {
|
||||||
|
// We could track absolute addresses too, but don't
|
||||||
|
// try to unify them with ZP. Just record the value.
|
||||||
|
// Not going to eliminate against prior state.
|
||||||
|
let _ = addr;
|
||||||
|
a = None;
|
||||||
|
}
|
||||||
|
(Opcode::STA, AddressingMode::ZeroPage(addr)) => {
|
||||||
|
// After STA, A is unchanged. Additionally, `addr` now
|
||||||
|
// holds A's value. Remember that equivalence: a later
|
||||||
|
// `LDA addr` is redundant.
|
||||||
|
a = Addr(*addr);
|
||||||
|
}
|
||||||
|
(Opcode::STA, _) => {
|
||||||
|
// A unchanged, but we don't track non-ZP addresses.
|
||||||
|
}
|
||||||
|
// Ops that clobber A — clear tracker.
|
||||||
|
(
|
||||||
|
Opcode::ADC
|
||||||
|
| Opcode::SBC
|
||||||
|
| Opcode::AND
|
||||||
|
| Opcode::ORA
|
||||||
|
| Opcode::EOR
|
||||||
|
| Opcode::ASL
|
||||||
|
| Opcode::LSR
|
||||||
|
| Opcode::ROL
|
||||||
|
| Opcode::ROR
|
||||||
|
| Opcode::LDX
|
||||||
|
| Opcode::LDY
|
||||||
|
| Opcode::PLA
|
||||||
|
| Opcode::TXA
|
||||||
|
| Opcode::TYA,
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
a = None;
|
||||||
|
}
|
||||||
|
// Ops that don't touch A — leave the tracker alone.
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(instructions.len());
|
||||||
|
for (i, inst) in instructions.iter().enumerate() {
|
||||||
|
if keep[i] {
|
||||||
|
out.push(inst.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*instructions = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum AKnown {
|
||||||
|
None,
|
||||||
|
Addr(u8),
|
||||||
|
Imm(u8),
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove `STA temp_slot` instructions whose written value is never
|
/// Remove `STA temp_slot` instructions whose written value is never
|
||||||
/// read before the slot is overwritten or we cross a control-flow
|
/// read before the slot is overwritten or we cross a control-flow
|
||||||
/// boundary (label, branch, jump, call, return).
|
/// boundary (label, branch, jump, call, return).
|
||||||
|
|
@ -232,14 +330,32 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keeps_sta_then_lda_user_var() {
|
fn eliminates_sta_then_lda_via_a_tracking() {
|
||||||
// $10 is a user variable, not a temp slot — must not eliminate.
|
// Even for user variables, `STA $10; LDA $10` is redundant in
|
||||||
|
// straight-line code: A still holds the value we just stored.
|
||||||
|
// The A-value tracker handles this. (If a JSR or branch
|
||||||
|
// intervenes, the tracker clears and the LDA is preserved.)
|
||||||
let mut insts = vec![
|
let mut insts = vec![
|
||||||
Instruction::new(STA, AM::ZeroPage(0x10)),
|
Instruction::new(STA, AM::ZeroPage(0x10)),
|
||||||
Instruction::new(LDA, AM::ZeroPage(0x10)),
|
Instruction::new(LDA, AM::ZeroPage(0x10)),
|
||||||
];
|
];
|
||||||
optimize(&mut insts);
|
optimize(&mut insts);
|
||||||
assert_eq!(insts.len(), 2);
|
assert_eq!(insts.len(), 1);
|
||||||
|
assert_eq!(insts[0].opcode, STA);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_lda_across_jsr() {
|
||||||
|
// JSR clobbers A (callee can do anything), so the second LDA
|
||||||
|
// must survive.
|
||||||
|
let mut insts = vec![
|
||||||
|
Instruction::new(STA, AM::ZeroPage(0x10)),
|
||||||
|
Instruction::new(JSR, AM::Label("foo".into())),
|
||||||
|
Instruction::new(LDA, AM::ZeroPage(0x10)),
|
||||||
|
];
|
||||||
|
optimize(&mut insts);
|
||||||
|
// STA, JSR, LDA — all preserved
|
||||||
|
assert_eq!(insts.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue