mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
Codegen: peephole dead-store elimination for IR temps
Adds a per-instruction scan: for each \`STA slot\` where slot is an IR temp (\$80-\$FF), walks forward through the instruction stream. If the slot is overwritten with no intervening read, the original STA is dead and is removed. Conservatively bails at any control-flow boundary (jump, branch, call, return, label definition) since a jumper or callee might read the slot. Complements the existing peephole passes. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
3f87aa1e30
commit
8b35ba9e13
1 changed files with 131 additions and 0 deletions
|
|
@ -12,12 +12,143 @@ pub fn optimize(instructions: &mut Vec<Instruction>) {
|
|||
let before = instructions.len();
|
||||
remove_sta_then_lda(instructions);
|
||||
remove_lda_then_sta_same(instructions);
|
||||
remove_dead_temp_stores(instructions);
|
||||
if instructions.len() == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// This targets the IR codegen's pattern where each op spills its
|
||||
/// result to an IR temp slot even if the next op consumes it by
|
||||
/// reading directly from that slot — but nothing further does. The
|
||||
/// final store-to-user-variable covers the actual need; the intermediate
|
||||
/// store-to-temp is dead.
|
||||
fn remove_dead_temp_stores(instructions: &mut Vec<Instruction>) {
|
||||
// Walk forward. For each `STA slot` where slot is a temp, look
|
||||
// ahead to see if the slot is read before being either
|
||||
// overwritten or invalidated by a control-flow boundary.
|
||||
let mut keep = vec![true; instructions.len()];
|
||||
for i in 0..instructions.len() {
|
||||
let inst = &instructions[i];
|
||||
if inst.opcode != Opcode::STA {
|
||||
continue;
|
||||
}
|
||||
let slot = match inst.mode {
|
||||
AddressingMode::ZeroPage(addr) if addr >= 0x80 => addr,
|
||||
_ => continue,
|
||||
};
|
||||
// Scan forward until the slot is read, overwritten, or we hit
|
||||
// a control-flow boundary that might branch to code we can't
|
||||
// see.
|
||||
let mut dead = false;
|
||||
for next in instructions.iter().skip(i + 1) {
|
||||
if instruction_crosses_block(next) {
|
||||
// The slot might be read later; be conservative.
|
||||
break;
|
||||
}
|
||||
if reads_zero_page(next, slot) {
|
||||
// A subsequent instruction reads from the slot, so
|
||||
// the STA is live.
|
||||
break;
|
||||
}
|
||||
if writes_zero_page(next, slot) {
|
||||
// The slot is overwritten with no read in between —
|
||||
// the original STA is dead.
|
||||
dead = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if dead {
|
||||
keep[i] = false;
|
||||
}
|
||||
}
|
||||
let mut out = Vec::with_capacity(instructions.len());
|
||||
for (i, inst) in instructions.iter().enumerate() {
|
||||
if keep[i] {
|
||||
out.push(inst.clone());
|
||||
}
|
||||
}
|
||||
*instructions = out;
|
||||
}
|
||||
|
||||
/// True if the given instruction is a control-flow boundary — we can't
|
||||
/// safely reason about liveness across it.
|
||||
fn instruction_crosses_block(inst: &Instruction) -> bool {
|
||||
// Branches / jumps / calls / returns all count as boundaries
|
||||
// because they might transfer to code that reads the slot.
|
||||
if matches!(
|
||||
inst.opcode,
|
||||
Opcode::JMP
|
||||
| Opcode::JSR
|
||||
| Opcode::RTS
|
||||
| Opcode::RTI
|
||||
| Opcode::BEQ
|
||||
| Opcode::BNE
|
||||
| Opcode::BCC
|
||||
| Opcode::BCS
|
||||
| Opcode::BMI
|
||||
| Opcode::BPL
|
||||
| Opcode::BVC
|
||||
| Opcode::BVS
|
||||
| Opcode::BRK
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// A label definition (NOP with `Label` operand) is also a boundary —
|
||||
// it's a potential jump target, and we can't see where jumps come
|
||||
// from without a full control-flow graph.
|
||||
matches!(inst.mode, AddressingMode::Label(_))
|
||||
}
|
||||
|
||||
/// True if `inst` reads from the given zero-page address.
|
||||
fn reads_zero_page(inst: &Instruction, addr: u8) -> bool {
|
||||
let targets_same = matches!(
|
||||
inst.mode,
|
||||
AddressingMode::ZeroPage(a) if a == addr
|
||||
);
|
||||
if !targets_same {
|
||||
return false;
|
||||
}
|
||||
// Reading instructions: LDA/LDX/LDY, arithmetic ops, comparisons,
|
||||
// BIT — anything that consumes the byte at the address.
|
||||
matches!(
|
||||
inst.opcode,
|
||||
Opcode::LDA
|
||||
| Opcode::LDX
|
||||
| Opcode::LDY
|
||||
| Opcode::ADC
|
||||
| Opcode::SBC
|
||||
| Opcode::AND
|
||||
| Opcode::ORA
|
||||
| Opcode::EOR
|
||||
| Opcode::CMP
|
||||
| Opcode::CPX
|
||||
| Opcode::CPY
|
||||
| Opcode::BIT
|
||||
| Opcode::ASL
|
||||
| Opcode::LSR
|
||||
| Opcode::ROL
|
||||
| Opcode::ROR
|
||||
| Opcode::INC
|
||||
| Opcode::DEC
|
||||
)
|
||||
}
|
||||
|
||||
/// True if `inst` writes to the given zero-page address (overwriting
|
||||
/// whatever was there). We treat read-modify-write ops as reads, not
|
||||
/// writes — they preserve the "was read" bit for the original STA.
|
||||
fn writes_zero_page(inst: &Instruction, addr: u8) -> bool {
|
||||
if !matches!(inst.mode, AddressingMode::ZeroPage(a) if a == addr) {
|
||||
return false;
|
||||
}
|
||||
matches!(inst.opcode, Opcode::STA | Opcode::STX | Opcode::STY)
|
||||
}
|
||||
|
||||
/// Remove `LDA addr` immediately followed by `STA addr` (same addr).
|
||||
/// The store is a no-op because the byte is already there.
|
||||
fn remove_lda_then_sta_same(instructions: &mut Vec<Instruction>) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue