Improve dead code elimination in optimizer

- Refactored dead_store_elimination to separate forward and backward passes
- Improved register forwarding to better detect backward jumps
- Fixed handling of JumpAndLink instructions in register tracking
- Updated optimizer snapshots to reflect improved code generation

The forward pass now correctly eliminates writes that are immediately overwritten.
Register forwarding now better handles conditional branches and loops.

Note: Backward pass for dead code elimination disabled for now - it needs
additional work to properly handle return values and call site analysis.
This commit is contained in:
2025-12-31 02:37:26 -07:00
parent 95c17b563c
commit d28cdfcc7b
15 changed files with 291 additions and 102 deletions

View File

@@ -25,24 +25,12 @@ pub fn constant_propagation<'a>(
Instruction::Add(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x + y),
Instruction::Sub(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x - y),
Instruction::Mul(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x * y),
Instruction::Div(dst, a, b) => {
try_fold_math(
dst,
a,
b,
&registers,
|x, y| if y.is_zero() { x } else { x / y },
)
}
Instruction::Mod(dst, a, b) => {
try_fold_math(
dst,
a,
b,
&registers,
|x, y| if y.is_zero() { x } else { x % y },
)
}
Instruction::Div(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| {
if y.is_zero() { Decimal::ZERO } else { x / y }
}),
Instruction::Mod(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| {
if y.is_zero() { Decimal::ZERO } else { x % y }
}),
Instruction::BranchEq(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x == y)
}

View File

@@ -1,5 +1,5 @@
use crate::helpers::get_destination_reg;
use il::{Instruction, InstructionNode};
use il::{Instruction, InstructionNode, Operand};
use std::collections::HashMap;
/// Pass: Dead Store Elimination
@@ -7,7 +7,20 @@ use std::collections::HashMap;
pub fn dead_store_elimination<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut changed = false;
// Forward pass: Remove writes that are immediately overwritten
let (input, forward_changed) = eliminate_overwritten_stores(input);
// Note: Backward pass disabled for now - it needs more work to handle all cases correctly
// The forward pass is sufficient for most common patterns
// (e.g., move r6 r15 immediately followed by move r6 r15 again)
(input, forward_changed)
}
/// Forward pass: Remove stores that are overwritten before being read
fn eliminate_overwritten_stores<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut last_write: HashMap<u8, usize> = HashMap::new();
let mut to_remove = Vec::new();
@@ -31,7 +44,6 @@ pub fn dead_store_elimination<'a>(
if !was_used {
// Previous write was dead
to_remove.push(prev_idx);
changed = true;
}
}
@@ -39,34 +51,31 @@ pub fn dead_store_elimination<'a>(
last_write.insert(dest_reg, i);
}
// Before clearing on labels/calls, check if current tracked writes are dead
if matches!(
node.instruction,
Instruction::LabelDef(_) | Instruction::JumpAndLink(_)
) {
// Check all currently tracked writes to see if they're dead
for (&reg, &idx) in &last_write {
// Don't remove writes to r15 (return register)
if reg == 15 {
continue;
}
// Check if this write was used between write and now
let was_used = input[idx + 1..i]
.iter()
.any(|n| reg_is_read_or_affects_control(&n.instruction, reg));
if !was_used && !to_remove.contains(&idx) {
to_remove.push(idx);
changed = true;
}
// Handle control flow instructions
match &node.instruction {
// JumpAndLink (function calls) only clobbers the return register (r15)
// We can continue tracking other registers across function calls
Instruction::JumpAndLink(_) => {
last_write.remove(&15);
}
last_write.clear();
// Other control flow instructions create complexity - clear all tracking
Instruction::Jump(_)
| Instruction::LabelDef(_)
| Instruction::BranchEq(_, _, _)
| Instruction::BranchNe(_, _, _)
| Instruction::BranchGt(_, _, _)
| Instruction::BranchLt(_, _, _)
| Instruction::BranchGe(_, _, _)
| Instruction::BranchLe(_, _, _)
| Instruction::BranchEqZero(_, _)
| Instruction::BranchNeZero(_, _) => {
last_write.clear();
}
_ => {}
}
}
if changed {
if !to_remove.is_empty() {
let output = input
.into_iter()
.enumerate()
@@ -84,6 +93,114 @@ pub fn dead_store_elimination<'a>(
}
}
/// Backward pass: Remove stores that are never read before function return
fn eliminate_unread_stores<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
use std::collections::HashSet;
let mut changed = false;
let mut to_remove = Vec::new();
// Find function boundaries by matching push/pop pairs of sp (register 17)
let mut function_ranges = Vec::new();
let mut stack = Vec::new();
for (i, node) in input.iter().enumerate() {
match &node.instruction {
Instruction::Push(Operand::Register(17)) => {
stack.push(i);
}
Instruction::Pop(Operand::Register(17)) => {
if let Some(start) = stack.pop() {
// Find the j ra after the pop sp
let mut end = i;
for j in (i + 1)..input.len() {
if matches!(input[j].instruction, Instruction::Jump(Operand::Register(16))) {
end = j;
break;
}
}
function_ranges.push((start, end));
}
}
_ => {}
}
}
// Process each function independently
for (func_start, func_end) in function_ranges {
// Process this function backward
let mut live_registers: HashSet<u8> = HashSet::new();
// First pass: find which registers are actually read in the function
for i in func_start..=func_end {
let node = &input[i];
for reg in 0..16 {
if crate::helpers::reg_is_read(&node.instruction, reg) {
live_registers.insert(reg);
}
}
}
// Clear live registers - we'll rebuild it as we go backward
live_registers.clear();
// Start from the end of the function, working backward
for i in (func_start..=func_end).rev() {
let node = &input[i];
// Skip stack management instructions
if matches!(node.instruction, Instruction::Push(_) | Instruction::Pop(_)) {
continue;
}
// If instruction writes to a register (assignment/computation)
if let Some(dest_reg) = get_destination_reg(&node.instruction) {
// If the register isn't live (not read after this write), this write is dead
if !live_registers.contains(&dest_reg) {
to_remove.push(i);
changed = true;
// Don't process the reads of this dead instruction
continue;
} else {
// This instruction is live. Check what it reads and marks as live.
for reg in 0..16 {
if crate::helpers::reg_is_read(&node.instruction, reg) {
live_registers.insert(reg);
}
}
// This instruction defines the register, so remove it from live set
live_registers.remove(&dest_reg);
}
} else {
// Instruction doesn't write - just track reads
for reg in 0..16 {
if crate::helpers::reg_is_read(&node.instruction, reg) {
live_registers.insert(reg);
}
}
}
}
}
if !to_remove.is_empty() {
let output = input
.into_iter()
.enumerate()
.filter_map(|(i, node)| {
if to_remove.contains(&i) {
None
} else {
Some(node)
}
})
.collect();
(output, true)
} else {
(input, changed)
}
}
/// Simplified check: Does this instruction read the register?
fn reg_is_read_or_affects_control(instr: &Instruction, reg: u8) -> bool {
use crate::helpers::reg_is_read;
@@ -114,4 +231,31 @@ mod tests {
assert!(changed);
assert_eq!(output.len(), 1);
}
#[test]
fn test_dead_store_in_function() {
// Test that dead stores inside functions are removed
// Function structure: push sp, push ra, code, pop ra, pop sp, j ra
let input = vec![
InstructionNode::new(Instruction::Push(Operand::Register(17)), None),
InstructionNode::new(Instruction::Push(Operand::Register(16)), None),
InstructionNode::new(
Instruction::Move(Operand::Register(1), Operand::Number(5.into())),
None,
),
// r1 is never read, so the move above should be dead
InstructionNode::new(
Instruction::Move(Operand::Register(15), Operand::Number(42.into())),
None,
),
InstructionNode::new(Instruction::Pop(Operand::Register(16)), None),
InstructionNode::new(Instruction::Pop(Operand::Register(17)), None),
InstructionNode::new(Instruction::Jump(Operand::Register(16)), None),
];
let (output, changed) = dead_store_elimination(input);
assert!(changed, "Dead store should be detected");
// Should remove the move r1 5 (index 2) and move r15 42 (index 3) since neither is read
assert_eq!(output.len(), 5);
}
}

View File

@@ -1,5 +1,6 @@
use crate::helpers::{get_destination_reg, reg_is_read, set_destination_reg};
use il::{Instruction, InstructionNode};
use il::{Instruction, InstructionNode, Operand};
use std::collections::HashMap;
/// Pass: Register Forwarding
/// Eliminates intermediate moves by writing directly to the final destination.
@@ -10,6 +11,20 @@ pub fn register_forwarding<'a>(
let mut changed = false;
let mut i = 0;
// Build a map of label positions to detect backward jumps
// Use String keys to avoid lifetime issues with references into input
let label_positions: HashMap<String, usize> = input
.iter()
.enumerate()
.filter_map(|(idx, node)| {
if let Instruction::LabelDef(label) = &node.instruction {
Some((label.to_string(), idx))
} else {
None
}
})
.collect();
while i < input.len().saturating_sub(1) {
let next_idx = i + 1;
@@ -48,23 +63,51 @@ pub fn register_forwarding<'a>(
break;
}
// Conservative: assume liveness might leak at labels/jumps
if matches!(
node.instruction,
Instruction::LabelDef(_) | Instruction::Jump(_) | Instruction::JumpAndLink(_)
) {
temp_is_dead = false;
// Function calls (jal) clobber the return register (r15)
// So if we're tracking r15 and hit a function call, the old value is dead
if matches!(node.instruction, Instruction::JumpAndLink(_)) && temp_reg == 15 {
break;
}
// Labels are just markers - they don't affect register liveness
// But backward jumps create loops we need to analyze carefully
let jump_target = match &node.instruction {
Instruction::Jump(Operand::Label(target)) => Some(target.as_ref()),
Instruction::BranchEq(_, _, Operand::Label(target))
| Instruction::BranchNe(_, _, Operand::Label(target))
| Instruction::BranchGt(_, _, Operand::Label(target))
| Instruction::BranchLt(_, _, Operand::Label(target))
| Instruction::BranchGe(_, _, Operand::Label(target))
| Instruction::BranchLe(_, _, Operand::Label(target))
| Instruction::BranchEqZero(_, Operand::Label(target))
| Instruction::BranchNeZero(_, Operand::Label(target)) => Some(target.as_ref()),
_ => None,
};
if let Some(target) = jump_target {
// Check if this is a backward jump (target appears before current position)
if let Some(&target_pos) = label_positions.get(target) {
if target_pos < i {
// Backward jump - could loop back, register might be live
temp_is_dead = false;
break;
}
// Forward jump is OK - doesn't affect liveness before it
}
}
}
if temp_is_dead {
// Rewrite to use final destination directly
if let Some(new_instr) = set_destination_reg(&input[i].instruction, final_reg) {
input[i].instruction = new_instr;
input.remove(next_idx);
changed = true;
continue;
// Safety check: ensure final_reg is not used as an operand in the current instruction.
// This prevents generating invalid instructions like `sub r5 r0 r5` (read and write same register).
if !reg_is_read(&input[i].instruction, final_reg) {
// Rewrite to use final destination directly
if let Some(new_instr) = set_destination_reg(&input[i].instruction, final_reg) {
input[i].instruction = new_instr;
input.remove(next_idx);
changed = true;
continue;
}
}
}
}