0.5.0 -- tuples and more optimizations #12

Merged
dbidwell merged 34 commits from 43-tuple-return into master 2025-12-31 17:03:51 -07:00
15 changed files with 291 additions and 102 deletions
Showing only changes of commit d28cdfcc7b - Show all commits

View File

@@ -80,10 +80,14 @@ cargo test --package compiler --lib -- test::tuple_literals::test::test_tuple_li
### Quick Compilation
!IMPORTANT: make sure you use these commands instead of creating temporary files.
```bash
cd rust_compiler
# Compile Slang code to IC10 using current compiler changes
echo 'let x = 5;' | cargo run --bin slang
# Compile Slang code to IC10 with optimization
echo 'let x = 5;' | cargo run --bin slang -z
# Or from file
cargo run --bin slang -- input.slang -o output.ic10
# Optimize the output with -z flag

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 158
expression: output
---
## Unoptimized Output
@@ -34,15 +35,11 @@ pop r9
pop r10
push sp
push ra
add r1 r10 r10
move r11 r1
move r2 r9
move r12 r2
move r3 r8
move r13 r3
add r11 r10 r10
move r12 r9
move r13 r8
add r4 r11 r12
add r5 r4 r13
move r15 r5
add r15 r4 r13
pop ra
pop sp
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 103
expression: output
---
## Unoptimized Output
@@ -24,8 +25,8 @@ j main
pop r8
push sp
push ra
add r1 r8 1
move r15 r1
move r9 20
add r15 r8 1
pop ra
pop sp
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 70
expression: output
---
## Unoptimized Output
@@ -31,13 +32,12 @@ j ra
## Optimized Output
j 10
j 9
pop r8
pop r9
push sp
push ra
add r1 r9 r8
move r15 r1
add r15 r9 r8
pop ra
pop sp
j ra
@@ -46,6 +46,7 @@ push ra
push 5
push 10
jal 1
move r8 r15
pop ra
pop sp
j ra

View File

@@ -124,7 +124,7 @@ __internal_L12:
## Optimized Output
j 71
j 77
push sp
push ra
yield
@@ -139,8 +139,10 @@ push sp
push ra
s d0 Setting 1
jal 1
move r1 r15
s d0 Activate 1
jal 1
move r2 r15
s d1 Open 0
pop ra
pop sp
@@ -152,53 +154,58 @@ sle r1 r8 1
ls r15 d0 255 Seeding
slt r2 r15 1
or r3 r1 r2
beqz r3 30
j 68
beqz r3 32
j 74
ls r15 d0 255 Mature
beqz r15 35
beqz r15 37
yield
s d0 Activate 1
j 30
ls r15 d0 255 Occupied
move r9 r15
j 32
ls r9 d0 255 Occupied
s d0 Setting 1
push r8
push r9
jal 1
pop r9
pop r8
move r4 r15
push r8
push r9
jal 11
pop r9
pop r8
beqz r9 54
move r5 r15
beqz r9 58
push r8
push r9
jal 11
pop r9
pop r8
move r6 r15
s d0 Setting r8
push r8
push r9
jal 1
pop r9
pop r8
move r6 r15
ls r15 d0 0 Occupied
beqz r15 63
beqz r15 68
s d0 Activate 1
push r8
push r9
jal 1
pop r9
pop r8
move r7 r15
pop ra
pop sp
j ra
move r8 0
yield
l r1 d0 Idle
bne r1 0 75
j 71
bne r1 0 82
j 78
add r3 r8 1
sgt r4 r3 19
add r5 r8 1
@@ -207,8 +214,10 @@ move r9 r6
push r8
push r9
push r8
jal 21
jal 23
pop r9
pop r8
move r7 r15
s d0 Setting r9
j 71
move r8 r9
j 78

View File

@@ -20,4 +20,6 @@ j ra
j main
pop r8
add r1 r8 1
move r8 r1
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 173
expression: output
---
## Unoptimized Output
@@ -66,8 +67,7 @@ pop r8
pop r9
push sp
push ra
add r1 r9 r8
move r15 r1
add r15 r9 r8
pop ra
pop sp
j ra
@@ -75,8 +75,7 @@ pop r8
pop r9
push sp
push ra
add r1 r9 r9
move r15 r1
add r15 r9 r9
pop ra
pop sp
j ra
@@ -97,7 +96,7 @@ push r9
push r10
push r10
push 2
jal 10
jal 9
pop r10
pop r9
pop r8

View File

@@ -24,5 +24,6 @@ j ra
j main
pop r8
pop r9
ble r9 r8 4
ble r9 r8 5
move r10 1
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 133
expression: output
---
## Unoptimized Output
@@ -29,8 +30,7 @@ j main
pop r8
push sp
push ra
select r9 r8 10 20
move r15 r9
select r15 r8 10 20
pop ra
pop sp
j ra

View File

@@ -17,4 +17,5 @@ j ra
## Optimized Output
j main
move r8 10
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 91
expression: output
---
## Unoptimized Output
@@ -23,8 +24,7 @@ j main
pop r8
push sp
push ra
add r1 r8 r8
move r15 r1
add r15 r8 r8
pop ra
pop sp
j ra

View File

@@ -1,5 +1,6 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 206
expression: output
---
## Unoptimized Output
@@ -54,12 +55,11 @@ __internal_L4:
## Optimized Output
j 25
j 23
pop r8
push sp
push ra
add r1 r8 1
move r15 r1
add r15 r8 1
pop ra
pop sp
j ra
@@ -74,21 +74,20 @@ jal 1
move r3 r15
push r3
sub r0 sp 5
get r0 db r0
move r15 r0
get r15 db r0
sub r0 sp 4
get ra db r0
j ra
yield
jal 9
jal 8
pop r0
pop r9
pop r8
move sp r15
jal 9
jal 8
pop r0
pop r0
pop r9
move sp r15
s db Setting r9
j 25
j 23

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;
// 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);
}
// 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;
}
}
// 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,17 +63,44 @@ pub fn register_forwarding<'a>(
break;
}
// Conservative: assume liveness might leak at labels/jumps
if matches!(
node.instruction,
Instruction::LabelDef(_) | Instruction::Jump(_) | Instruction::JumpAndLink(_)
) {
// 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 {
// 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;
@@ -68,6 +110,7 @@ pub fn register_forwarding<'a>(
}
}
}
}
i += 1;
}