use std::{collections::HashMap, sync::Arc};
use crate::{
analysis::{BinaryOpKind, SsaFunction, SsaOp, SsaVarId, UnaryOpKind},
compiler::{pass::SsaPass, CompilerContext, EventKind, EventLog},
metadata::token::Token,
CilObject, Result,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum ValueKey {
Binary(BinaryOpKind, bool, SsaVarId, SsaVarId),
Unary(UnaryOpKind, SsaVarId),
}
impl ValueKey {
fn from_op(op: &SsaOp) -> Option<(Self, SsaVarId)> {
if let Some(info) = op.as_binary_op() {
if matches!(
info.kind,
BinaryOpKind::AddOvf | BinaryOpKind::SubOvf | BinaryOpKind::MulOvf
) {
return None;
}
let normalized = info.normalized();
let (kind, unsigned, left, right) = normalized.value_key();
return Some((Self::Binary(kind, unsigned, left, right), normalized.dest));
}
if let Some(info) = op.as_unary_op() {
if info.kind == UnaryOpKind::Ckfinite {
return None;
}
return Some((Self::Unary(info.kind, info.operand), info.dest));
}
None
}
}
pub struct GlobalValueNumberingPass;
impl Default for GlobalValueNumberingPass {
fn default() -> Self {
Self::new()
}
}
impl GlobalValueNumberingPass {
#[must_use]
pub fn new() -> Self {
Self
}
fn run_gvn(ssa: &mut SsaFunction, method_token: Token, changes: &mut EventLog) -> usize {
let mut value_map: HashMap<ValueKey, SsaVarId> = HashMap::new();
let mut redundant: Vec<(SsaVarId, SsaVarId)> = Vec::new();
for block in ssa.blocks() {
for instr in block.instructions() {
if let Some((key, dest)) = ValueKey::from_op(instr.op()) {
if let Some(&original) = value_map.get(&key) {
redundant.push((dest, original));
} else {
value_map.insert(key, dest);
}
}
}
}
let mut total_replaced = 0;
for (redundant_var, original_var) in &redundant {
let replaced = ssa.replace_uses(*redundant_var, *original_var);
if replaced > 0 {
changes
.record(EventKind::ConstantFolded) .method(method_token)
.message(format!(
"GVN: {redundant_var} → {original_var} ({replaced} uses)"
));
total_replaced += replaced;
}
}
total_replaced
}
}
impl SsaPass for GlobalValueNumberingPass {
fn name(&self) -> &'static str {
"global-value-numbering"
}
fn description(&self) -> &'static str {
"Eliminates redundant computations using value numbering"
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
_assembly: &Arc<CilObject>,
) -> Result<bool> {
let mut changes = EventLog::new();
Self::run_gvn(ssa, method_token, &mut changes);
let changed = !changes.is_empty();
if changed {
ctx.events.merge(&changes);
}
Ok(changed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::{ConstValue, SsaFunctionBuilder};
#[test]
fn test_value_key_binary_commutative() {
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let v3 = SsaVarId::new();
let add_op1 = SsaOp::Add {
dest: v2,
left: v0,
right: v1,
};
let add_op2 = SsaOp::Add {
dest: v3,
left: v1,
right: v0,
};
let (key1, _) = ValueKey::from_op(&add_op1).unwrap();
let (key2, _) = ValueKey::from_op(&add_op2).unwrap();
assert_eq!(key1, key2, "Add should be commutative");
let sub_op1 = SsaOp::Sub {
dest: v2,
left: v0,
right: v1,
};
let sub_op2 = SsaOp::Sub {
dest: v3,
left: v1,
right: v0,
};
let (key3, _) = ValueKey::from_op(&sub_op1).unwrap();
let (key4, _) = ValueKey::from_op(&sub_op2).unwrap();
assert_ne!(key3, key4, "Sub should NOT be commutative");
}
#[test]
fn test_value_key_from_op() {
let v0 = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let add_op = SsaOp::Add {
dest: v2,
left: v0,
right: v1,
};
let (key, dest) = ValueKey::from_op(&add_op).unwrap();
assert_eq!(dest, v2);
assert!(matches!(key, ValueKey::Binary(BinaryOpKind::Add, _, _, _)));
let neg_op = SsaOp::Neg {
dest: v1,
operand: v0,
};
let (key, dest) = ValueKey::from_op(&neg_op).unwrap();
assert_eq!(dest, v1);
assert!(matches!(key, ValueKey::Unary(UnaryOpKind::Neg, _)));
let const_op = SsaOp::Const {
dest: v0,
value: ConstValue::I32(42),
};
assert!(ValueKey::from_op(&const_op).is_none());
}
#[test]
fn test_gvn_eliminates_redundant() {
let (mut ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(10);
let v1 = b.const_i32(20);
let v2 = b.add(v0, v1);
v2_out = v2;
let v3 = b.add(v0, v1); let v4 = b.mul(v2, v3);
b.ret_val(v4);
});
});
(ssa, v2_out)
};
let mut changes = EventLog::new();
let replaced =
GlobalValueNumberingPass::run_gvn(&mut ssa, Token::new(0x06000001), &mut changes);
assert!(replaced > 0);
assert!(!changes.is_empty());
let block = ssa.block(0).unwrap();
let mul_instr = &block.instructions()[4]; if let SsaOp::Mul { left, right, .. } = mul_instr.op() {
assert_eq!(*left, v2);
assert_eq!(*right, v2);
} else {
panic!("Expected Mul instruction");
}
}
#[test]
fn test_gvn_commutative_order() {
let (mut ssa, v2) = {
let mut v2_out = SsaVarId::new();
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(10);
let v1 = b.const_i32(20);
let v2 = b.add(v0, v1);
v2_out = v2;
let v3 = b.add(v1, v0); b.ret_val(v3);
});
});
(ssa, v2_out)
};
let mut changes = EventLog::new();
let replaced =
GlobalValueNumberingPass::run_gvn(&mut ssa, Token::new(0x06000001), &mut changes);
assert!(replaced > 0);
let block = ssa.block(0).unwrap();
let ret_instr = &block.instructions()[4]; if let SsaOp::Return {
value: Some(ret_val),
} = ret_instr.op()
{
assert_eq!(*ret_val, v2);
} else {
panic!("Expected Return instruction");
}
}
#[test]
fn test_gvn_non_commutative_preserved() {
let mut ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v0 = b.const_i32(10);
let v1 = b.const_i32(20);
let _v2 = b.sub(v0, v1);
let v3 = b.sub(v1, v0);
b.ret_val(v3);
});
});
let mut changes = EventLog::new();
let replaced =
GlobalValueNumberingPass::run_gvn(&mut ssa, Token::new(0x06000001), &mut changes);
assert_eq!(replaced, 0);
assert!(changes.is_empty());
}
}