mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +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_lda_then_sta_same(instructions);
|
||||
remove_dead_temp_stores(instructions);
|
||||
remove_redundant_loads(instructions);
|
||||
if instructions.len() == before {
|
||||
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
|
||||
/// read before the slot is overwritten or we cross a control-flow
|
||||
/// boundary (label, branch, jump, call, return).
|
||||
|
|
@ -232,14 +330,32 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_sta_then_lda_user_var() {
|
||||
// $10 is a user variable, not a temp slot — must not eliminate.
|
||||
fn eliminates_sta_then_lda_via_a_tracking() {
|
||||
// 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![
|
||||
Instruction::new(STA, AM::ZeroPage(0x10)),
|
||||
Instruction::new(LDA, AM::ZeroPage(0x10)),
|
||||
];
|
||||
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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue