mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 17:28:00 +00:00
Codegen: richer A-value tracking via equivalence classes
Previously \`remove_redundant_loads\` tracked only a single value
for A at a time, so \`STA addr\` would overwrite the prior
\`Imm(v)\` with \`Zp(addr)\`, losing the ability to eliminate a
following \`LDA #v\`. Now A's known value is an equivalence class
(\`Vec<AValue>\`): every \`STA addr\` *adds* the address to the
class instead of replacing it, so
LDA #5
STA \$10
LDA #5 ; eliminated — A still equals 5
collapses to just two instructions. Adding this also catches the
earlier \`STA \$10; LDA \$10\` idiom because \`Zp(\$10)\` is now part
of the class after the STA.
Any op that clobbers A clears the whole class; writes via STX/STY
or INC/DEC to an address in the class invalidate just that entry.
Instruction counts tighten on every example, most visibly on
bouncing_ball (123→120) and arrays_and_functions (247→245).
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
49317167da
commit
d2075bc8f9
1 changed files with 73 additions and 41 deletions
|
|
@ -349,64 +349,63 @@ fn modifies_a(op: Opcode) -> bool {
|
|||
/// 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`
|
||||
/// We track an equivalence class for A: at any point, A equals a
|
||||
/// single `AValue` (immediate, ZP cell, or absolute cell). After a
|
||||
/// `STA addr`, that address is added to the equivalence — a later
|
||||
/// `LDA` from the same class is redundant.
|
||||
///
|
||||
/// 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.
|
||||
/// Any instruction that may clobber A resets the tracker, as does
|
||||
/// a control-flow instruction or label.
|
||||
fn remove_redundant_loads(instructions: &mut Vec<Instruction>) {
|
||||
use AKnown::*;
|
||||
let mut keep = vec![true; instructions.len()];
|
||||
let mut a: AKnown = None;
|
||||
// Current equivalence class for A. All members hold the same
|
||||
// value as A right now.
|
||||
let mut eq: Vec<AValue> = Vec::new();
|
||||
|
||||
for (i, inst) in instructions.iter().enumerate() {
|
||||
if instruction_crosses_block(inst) {
|
||||
a = None;
|
||||
eq.clear();
|
||||
continue;
|
||||
}
|
||||
match (inst.opcode, &inst.mode) {
|
||||
(Opcode::LDA, AddressingMode::Immediate(v)) => {
|
||||
if let Imm(existing) = a {
|
||||
if existing == *v {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
if eq.contains(&AValue::Imm(*v)) {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
a = Imm(*v);
|
||||
eq.clear();
|
||||
eq.push(AValue::Imm(*v));
|
||||
}
|
||||
(Opcode::LDA, AddressingMode::ZeroPage(addr)) => {
|
||||
if let Zp(existing) = a {
|
||||
if existing == *addr {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
if eq.contains(&AValue::Zp(*addr)) {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
a = Zp(*addr);
|
||||
eq.clear();
|
||||
eq.push(AValue::Zp(*addr));
|
||||
}
|
||||
(Opcode::LDA, AddressingMode::Absolute(addr)) => {
|
||||
if let Abs(existing) = a {
|
||||
if existing == *addr {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
if eq.contains(&AValue::Abs(*addr)) {
|
||||
keep[i] = false;
|
||||
continue;
|
||||
}
|
||||
a = Abs(*addr);
|
||||
eq.clear();
|
||||
eq.push(AValue::Abs(*addr));
|
||||
}
|
||||
(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 = Zp(*addr);
|
||||
// A unchanged; address now holds A's value. Add the
|
||||
// address to the equivalence class.
|
||||
if eq.is_empty() {
|
||||
// No prior knowledge — start fresh with this
|
||||
// address.
|
||||
}
|
||||
eq.push(AValue::Zp(*addr));
|
||||
}
|
||||
(Opcode::STA, AddressingMode::Absolute(addr)) => {
|
||||
a = Abs(*addr);
|
||||
eq.push(AValue::Abs(*addr));
|
||||
}
|
||||
(Opcode::STA, _) => {
|
||||
// A unchanged, but we don't track exotic addressing modes.
|
||||
// Other addressing modes: A unchanged.
|
||||
}
|
||||
// Ops that clobber A — clear tracker.
|
||||
(
|
||||
|
|
@ -426,9 +425,23 @@ fn remove_redundant_loads(instructions: &mut Vec<Instruction>) {
|
|||
| Opcode::TYA,
|
||||
_,
|
||||
) => {
|
||||
a = None;
|
||||
eq.clear();
|
||||
}
|
||||
// Ops that write to an address we might be tracking:
|
||||
// invalidate any equivalence pointing at that cell,
|
||||
// because the stored value is no longer correct.
|
||||
(
|
||||
Opcode::STX | Opcode::STY | Opcode::INC | Opcode::DEC,
|
||||
AddressingMode::ZeroPage(addr),
|
||||
) => {
|
||||
eq.retain(|v| !matches!(v, AValue::Zp(a) if *a == *addr));
|
||||
}
|
||||
(
|
||||
Opcode::STX | Opcode::STY | Opcode::INC | Opcode::DEC,
|
||||
AddressingMode::Absolute(addr),
|
||||
) => {
|
||||
eq.retain(|v| !matches!(v, AValue::Abs(a) if *a == *addr));
|
||||
}
|
||||
// Ops that don't touch A — leave the tracker alone.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -442,11 +455,10 @@ fn remove_redundant_loads(instructions: &mut Vec<Instruction>) {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AKnown {
|
||||
None,
|
||||
enum AValue {
|
||||
Imm(u8),
|
||||
Zp(u8),
|
||||
Abs(u16),
|
||||
Imm(u8),
|
||||
}
|
||||
|
||||
/// Remove `STA temp_slot` instructions whose written value is never
|
||||
|
|
@ -738,6 +750,26 @@ mod tests {
|
|||
assert_eq!(insts.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eliminates_duplicate_immediate_loads_across_stores() {
|
||||
// LDA #5; STA $10; LDA #5 — the second LDA should be
|
||||
// eliminated because A still holds 5 after STA.
|
||||
let mut insts = vec![
|
||||
Instruction::new(LDA, AM::Immediate(5)),
|
||||
Instruction::new(STA, AM::ZeroPage(0x10)),
|
||||
Instruction::new(LDA, AM::Immediate(5)),
|
||||
Instruction::new(STA, AM::ZeroPage(0x11)),
|
||||
Instruction::new(RTS, AM::Implied),
|
||||
];
|
||||
optimize(&mut insts);
|
||||
// Expect: LDA #5, STA $10, STA $11, RTS
|
||||
let lda_count = insts.iter().filter(|i| i.opcode == LDA).count();
|
||||
assert_eq!(
|
||||
lda_count, 1,
|
||||
"expected one LDA (the second is redundant): {insts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_lda_then_sta_same_address() {
|
||||
let mut insts = vec![
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue