1
0
Fork 0
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:
Claude 2026-04-12 11:27:16 +00:00
parent 3f87aa1e30
commit 8b35ba9e13
No known key found for this signature in database

View file

@ -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>) {