Skip to content

Commit 0cee192

Browse files
author
metrics
committed
feat: enhance algebraic simplifications and constant propagation with multiple passes
1 parent 3802f1c commit 0cee192

4 files changed

Lines changed: 168 additions & 100 deletions

File tree

crates/herkos-core/src/optimizer/algebraic.rs

Lines changed: 102 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,40 @@
2222
//! | `x == x` | `1` |
2323
//! | `x != x` | `0` |
2424
25-
use super::utils::instr_dest;
26-
use crate::ir::{BinOp, IrFunction, IrInstr, IrValue, VarId};
27-
use std::collections::HashMap;
25+
use crate::{
26+
ir::{BinOp, IrFunction, IrInstr, IrValue, VarId},
27+
optimizer::utils::build_global_const_map,
28+
};
2829

2930
// ── Public entry point ────────────────────────────────────────────────────────
3031

32+
/// Eliminates algebraic identities and annihilators until reaching a fixed point.
33+
///
34+
/// Runs multiple passes because simplifications from one pass can expose new
35+
/// optimization opportunities. For example, `(x * 1) + 0` requires two passes:
36+
/// first `x * 1 → x`, then `x + 0 → x`.
3137
pub fn eliminate(func: &mut IrFunction) {
38+
// Run passes until reaching a fixed point (no more changes)
39+
loop {
40+
if !eliminate_once(func) {
41+
break;
42+
}
43+
}
44+
}
45+
46+
fn eliminate_once(func: &mut IrFunction) -> bool {
3247
let global_consts = build_global_const_map(func);
48+
let mut changed = false;
3349

3450
for block in &mut func.blocks {
3551
let mut local_consts = global_consts.clone();
3652

3753
for instr in &mut block.instructions {
38-
// Track constants defined in this block.
54+
// Track constants defined in this block. We walk forward and update
55+
// local_consts as we encounter Const instructions, so that later BinOp
56+
// instructions in the same block can see newly-defined constants.
57+
// (global_consts only captures constants with a single def across the
58+
// entire function, not block-local defs discovered during forward walk.)
3959
if let IrInstr::Const { dest, value } = instr {
4060
local_consts.insert(*dest, *value);
4161
continue;
@@ -50,6 +70,7 @@ pub fn eliminate(func: &mut IrFunction) {
5070
if lhs == rhs {
5171
if let Some(replacement) = same_operand_rule(op, dest, lhs) {
5272
*instr = replacement;
73+
changed = true;
5374
if let IrInstr::Const { dest, value } = instr {
5475
local_consts.insert(*dest, *value);
5576
}
@@ -62,35 +83,15 @@ pub fn eliminate(func: &mut IrFunction) {
6283

6384
if let Some(replacement) = constant_operand_rule(op, dest, lhs, rhs, lhs_val, rhs_val) {
6485
*instr = replacement;
86+
changed = true;
6587
if let IrInstr::Const { dest, value } = instr {
6688
local_consts.insert(*dest, *value);
6789
}
6890
}
6991
}
7092
}
71-
}
72-
73-
// ── Global constant map ───────────────────────────────────────────────────────
74-
75-
fn build_global_const_map(func: &IrFunction) -> HashMap<VarId, IrValue> {
76-
let mut total_defs: HashMap<VarId, usize> = HashMap::new();
77-
let mut const_defs: HashMap<VarId, IrValue> = HashMap::new();
78-
79-
for block in &func.blocks {
80-
for instr in &block.instructions {
81-
if let Some(dest) = instr_dest(instr) {
82-
*total_defs.entry(dest).or_insert(0) += 1;
83-
if let IrInstr::Const { dest, value } = instr {
84-
const_defs.insert(*dest, *value);
85-
}
86-
}
87-
}
88-
}
8993

90-
const_defs
91-
.into_iter()
92-
.filter(|(v, _)| total_defs.get(v).copied().unwrap_or(0) == 1)
93-
.collect()
94+
changed
9495
}
9596

9697
// ── Same-operand rules ────────────────────────────────────────────────────────
@@ -108,32 +109,34 @@ fn same_operand_rule(op: BinOp, dest: VarId, operand: VarId) -> Option<IrInstr>
108109
}),
109110

110111
// x == x → 1 (integers only; floats have NaN)
111-
BinOp::I32Eq | BinOp::I32LeS | BinOp::I32LeU | BinOp::I32GeS | BinOp::I32GeU => {
112-
Some(IrInstr::Const {
113-
dest,
114-
value: IrValue::I32(1),
115-
})
116-
}
117-
BinOp::I64Eq | BinOp::I64LeS | BinOp::I64LeU | BinOp::I64GeS | BinOp::I64GeU => {
118-
Some(IrInstr::Const {
119-
dest,
120-
value: IrValue::I32(1),
121-
})
122-
}
112+
BinOp::I32Eq
113+
| BinOp::I32LeS
114+
| BinOp::I32LeU
115+
| BinOp::I32GeS
116+
| BinOp::I32GeU
117+
| BinOp::I64Eq
118+
| BinOp::I64LeS
119+
| BinOp::I64LeU
120+
| BinOp::I64GeS
121+
| BinOp::I64GeU => Some(IrInstr::Const {
122+
dest,
123+
value: IrValue::I32(1),
124+
}),
123125

124126
// x != x → 0 (integers only)
125-
BinOp::I32Ne | BinOp::I32LtS | BinOp::I32LtU | BinOp::I32GtS | BinOp::I32GtU => {
126-
Some(IrInstr::Const {
127-
dest,
128-
value: IrValue::I32(0),
129-
})
130-
}
131-
BinOp::I64Ne | BinOp::I64LtS | BinOp::I64LtU | BinOp::I64GtS | BinOp::I64GtU => {
132-
Some(IrInstr::Const {
133-
dest,
134-
value: IrValue::I32(0),
135-
})
136-
}
127+
BinOp::I32Ne
128+
| BinOp::I32LtS
129+
| BinOp::I32LtU
130+
| BinOp::I32GtS
131+
| BinOp::I32GtU
132+
| BinOp::I64Ne
133+
| BinOp::I64LtS
134+
| BinOp::I64LtU
135+
| BinOp::I64GtS
136+
| BinOp::I64GtU => Some(IrInstr::Const {
137+
dest,
138+
value: IrValue::I32(0),
139+
}),
137140

138141
// x - x → 0 (integers only; floats have Inf - Inf = NaN)
139142
BinOp::I32Sub => Some(IrInstr::Const {
@@ -778,4 +781,53 @@ mod tests {
778781
}
779782
));
780783
}
784+
785+
// ── Cascading optimizations (multiple passes needed) ───────────────────
786+
787+
#[test]
788+
fn cascading_mul_one_and_add_zero() {
789+
// v1 = 1; v2 = v0 * v1; v3 = 0; v4 = v2 + v3
790+
// Pass 1: v2 = v0 * 1 → v2 = Assign(v0)
791+
// v4 = v2 + 0 → NOT YET (v2 is not recognized as const)
792+
// Pass 2: v4 = v2 + 0 → v4 = Assign(v2) → v4 = Assign(v0)
793+
let mut func = make_func(single_block(vec![
794+
IrInstr::Const {
795+
dest: VarId(1),
796+
value: IrValue::I32(1),
797+
},
798+
IrInstr::BinOp {
799+
dest: VarId(2),
800+
op: BinOp::I32Mul,
801+
lhs: VarId(0),
802+
rhs: VarId(1),
803+
},
804+
IrInstr::Const {
805+
dest: VarId(3),
806+
value: IrValue::I32(0),
807+
},
808+
IrInstr::BinOp {
809+
dest: VarId(4),
810+
op: BinOp::I32Add,
811+
lhs: VarId(2),
812+
rhs: VarId(3),
813+
},
814+
]));
815+
eliminate(&mut func);
816+
// After multiple passes, both simplifications should be applied.
817+
// v2 should be Assign(v0) and v4 should be Assign(v0) (through v2).
818+
assert!(matches!(
819+
func.blocks[0].instructions[1],
820+
IrInstr::Assign {
821+
dest: VarId(2),
822+
src: VarId(0)
823+
}
824+
));
825+
assert!(matches!(
826+
func.blocks[0].instructions[3],
827+
IrInstr::Assign {
828+
dest: VarId(4),
829+
src: VarId(2)
830+
}
831+
));
832+
}
781833
}

0 commit comments

Comments
 (0)