use std::collections::HashMap;
use crate::compiler::maglev::ir::{ControlNode, MaglevGraph, NodeId, ValueNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Range {
pub min: i64,
pub max: i64,
}
impl Range {
pub const I32_FULL: Self = Self {
min: i32::MIN as i64,
max: i32::MAX as i64,
};
pub const fn exact(v: i64) -> Self {
Self { min: v, max: v }
}
pub fn fits_i32(self) -> bool {
self.min >= i32::MIN as i64 && self.max <= i32::MAX as i64
}
}
pub fn eliminate_overflow_checks(graph: &mut MaglevGraph) {
let mut ranges: HashMap<NodeId, Range> = HashMap::new();
for block in graph.blocks() {
for (id, node) in &block.nodes {
if let Some(r) = seed_range(node) {
ranges.insert(*id, r);
}
}
}
for block in graph.blocks() {
for (_id, node) in &block.nodes {
if let ValueNode::CheckSmi { receiver } = node {
ranges.entry(*receiver).or_insert(Range::I32_FULL);
}
}
}
rewrite_loop_induction_steps(graph, &mut ranges);
let mut rewrites = 0u32;
for block in graph.blocks_mut() {
for (id, node) in &mut block.nodes {
if !ranges.contains_key(id)
&& let Some(r) = seed_range(node)
{
ranges.insert(*id, r);
}
if let Some((out_range, replacement)) = try_rewrite(node, *id, &ranges) {
ranges.insert(*id, out_range);
if let Some(new_node) = replacement {
*node = new_node;
rewrites += 1;
}
}
}
}
let _ = rewrites;
}
fn seed_range(node: &ValueNode) -> Option<Range> {
match node {
ValueNode::SmiConstant { value } | ValueNode::Int32Constant { value } => {
Some(Range::exact(*value as i64))
}
ValueNode::Uint32Constant { value } => Some(Range::exact(*value as i64)),
ValueNode::Parameter { .. } | ValueNode::LoadGlobal { .. } => Some(Range::I32_FULL),
_ => None,
}
}
fn try_rewrite(
node: &ValueNode,
node_id: NodeId,
ranges: &HashMap<NodeId, Range>,
) -> Option<(Range, Option<ValueNode>)> {
match node {
ValueNode::Phi { inputs } => {
if ranges.contains_key(&node_id) {
return None;
}
let mut min = i64::MAX;
let mut max = i64::MIN;
let mut any = false;
for input in inputs {
if *input == node_id {
continue;
}
let r = ranges.get(input)?;
min = min.min(r.min);
max = max.max(r.max);
any = true;
}
if !any {
return None;
}
Some((Range { min, max }, None))
}
ValueNode::CheckedSmiAdd { left, right } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let out = Range {
min: lr.min + rr.min,
max: lr.max + rr.max,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Add {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::CheckedSmiSubtract { left, right } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let out = Range {
min: lr.min - rr.max,
max: lr.max - rr.min,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Subtract {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::CheckedSmiMultiply { left, right } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let candidates = [
lr.min * rr.min,
lr.min * rr.max,
lr.max * rr.min,
lr.max * rr.max,
];
let out = Range {
min: candidates.iter().copied().min().unwrap(),
max: candidates.iter().copied().max().unwrap(),
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Multiply {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::CheckedSmiIncrement { value } => {
let vr = ranges.get(value)?;
let out = Range {
min: vr.min + 1,
max: vr.max + 1,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Increment { value: *value })
} else {
None
};
Some((out, repl))
}
ValueNode::CheckedSmiDecrement { value } => {
let vr = ranges.get(value)?;
let out = Range {
min: vr.min - 1,
max: vr.max - 1,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Decrement { value: *value })
} else {
None
};
Some((out, repl))
}
ValueNode::Int32Add { left, right } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
Some((
Range {
min: lr.min + rr.min,
max: lr.max + rr.max,
},
None,
))
}
ValueNode::Int32Subtract { left, right } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
Some((
Range {
min: lr.min - rr.max,
max: lr.max - rr.min,
},
None,
))
}
ValueNode::GenericAdd { left, right, .. } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let out = Range {
min: lr.min + rr.min,
max: lr.max + rr.max,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Add {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::GenericSubtract { left, right, .. } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let out = Range {
min: lr.min - rr.max,
max: lr.max - rr.min,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Subtract {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::GenericMultiply { left, right, .. } => {
let (lr, rr) = (ranges.get(left)?, ranges.get(right)?);
let candidates = [
lr.min * rr.min,
lr.min * rr.max,
lr.max * rr.min,
lr.max * rr.max,
];
let out = Range {
min: candidates.iter().copied().min().unwrap(),
max: candidates.iter().copied().max().unwrap(),
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Multiply {
left: *left,
right: *right,
})
} else {
None
};
Some((out, repl))
}
ValueNode::GenericIncrement { value, .. } => {
let vr = ranges.get(value)?;
let out = Range {
min: vr.min + 1,
max: vr.max + 1,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Increment { value: *value })
} else {
None
};
Some((out, repl))
}
ValueNode::GenericDecrement { value, .. } => {
let vr = ranges.get(value)?;
let out = Range {
min: vr.min - 1,
max: vr.max - 1,
};
let repl = if out.fits_i32() {
Some(ValueNode::Int32Decrement { value: *value })
} else {
None
};
Some((out, repl))
}
ValueNode::GenericBitwiseOr { left, right, .. }
| ValueNode::Int32BitwiseOr { left, right } => {
let _lr = ranges.get(left);
let _rr = ranges.get(right);
Some((Range::I32_FULL, None))
}
_ => None,
}
}
#[derive(Debug)]
enum LoopBound {
LessThan(Range),
LessThanOrEqual(Range),
}
struct LoopMeta {
header_idx: u32,
back_pred_pos: usize,
entry_pos: usize,
max_iterations: i64,
induction_phi_ids: Vec<NodeId>,
}
fn rewrite_loop_induction_steps(graph: &mut MaglevGraph, ranges: &mut HashMap<NodeId, Range>) {
let node_map: HashMap<NodeId, (u32, ValueNode)> = graph
.blocks()
.iter()
.flat_map(|b| b.nodes.iter().map(|(id, n)| (*id, (b.id, n.clone()))))
.collect();
let back_edges: Vec<(u32, u32)> = graph
.blocks()
.iter()
.filter_map(|b| {
if let Some(ControlNode::Jump { target }) = &b.control
&& *target <= b.id
{
Some((b.id, *target))
} else {
None
}
})
.collect();
let mut step_rewrites: Vec<(u32, usize, ValueNode, NodeId, Range)> = Vec::new();
let mut phi_ranges: Vec<(NodeId, Range)> = Vec::new();
let mut loop_metas: Vec<LoopMeta> = Vec::new();
for &(back_src, header_idx) in &back_edges {
let header = &graph.blocks()[header_idx as usize];
let Some(back_pred_pos) = header.predecessors.iter().position(|&p| p == back_src) else {
continue;
};
let entry_pos = if back_pred_pos == 0 { 1 } else { 0 };
let phis: Vec<(NodeId, Vec<NodeId>)> = header
.nodes
.iter()
.filter_map(|(id, node)| {
if let ValueNode::Phi { inputs } = node
&& inputs.len() == 2
{
Some((*id, inputs.clone()))
} else {
None
}
})
.collect();
let mut induction_phi_ids = Vec::new();
let mut max_iterations: Option<i64> = None;
for (phi_id, phi_inputs) in &phis {
let init_id = phi_inputs[entry_pos];
let step_id = phi_inputs[back_pred_pos];
let Some(init_range) = ranges.get(&init_id) else {
continue;
};
let Some(delta) = find_step_delta(&step_id, phi_id, &node_map, ranges) else {
continue;
};
let Some(bound) = find_comparison_bound(header, phi_id, &node_map, ranges) else {
continue;
};
let Some((phi_range, step_range)) = compute_induction_ranges(init_range, delta, &bound)
else {
continue;
};
if !phi_range.fits_i32() || !step_range.fits_i32() {
continue;
}
let Some((step_block_idx, step_node)) = node_map.get(&step_id) else {
continue;
};
let step_block = &graph.blocks()[*step_block_idx as usize];
let Some(node_idx) = step_block.nodes.iter().position(|(id, _)| *id == step_id) else {
continue;
};
let Some(replacement) = lower_checked_to_unchecked(step_node) else {
continue;
};
step_rewrites.push((*step_block_idx, node_idx, replacement, step_id, step_range));
phi_ranges.push((*phi_id, phi_range));
induction_phi_ids.push(*phi_id);
let iters = match &bound {
LoopBound::LessThan(br) => br.max - init_range.min,
LoopBound::LessThanOrEqual(br) => br.max - init_range.min + 1,
};
max_iterations = Some(max_iterations.map_or(iters, |prev: i64| prev.max(iters)));
}
if let Some(max_iters) = max_iterations {
loop_metas.push(LoopMeta {
header_idx,
back_pred_pos,
entry_pos,
max_iterations: max_iters,
induction_phi_ids,
});
}
}
for (block_idx, node_idx, new_node, step_id, step_range) in step_rewrites {
if let Some(block) = graph.block_mut(block_idx) {
block.nodes[node_idx].1 = new_node;
}
ranges.insert(step_id, step_range);
}
for (phi_id, phi_range) in phi_ranges {
ranges.insert(phi_id, phi_range);
}
rewrite_accumulator_phis(graph, ranges, &loop_metas, &node_map);
}
fn find_step_delta(
step_id: &NodeId,
phi_id: &NodeId,
node_map: &HashMap<NodeId, (u32, ValueNode)>,
ranges: &HashMap<NodeId, Range>,
) -> Option<i64> {
let (_, step_node) = node_map.get(step_id)?;
match step_node {
ValueNode::CheckedSmiIncrement { value } | ValueNode::GenericIncrement { value, .. }
if value == phi_id =>
{
Some(1)
}
ValueNode::CheckedSmiDecrement { value } | ValueNode::GenericDecrement { value, .. }
if value == phi_id =>
{
Some(-1)
}
ValueNode::CheckedSmiAdd { left, right } | ValueNode::GenericAdd { left, right, .. }
if left == phi_id =>
{
let r = ranges.get(right)?;
if r.min == r.max { Some(r.min) } else { None }
}
_ => None,
}
}
fn find_comparison_bound(
header: &crate::compiler::maglev::ir::BasicBlock,
phi_id: &NodeId,
node_map: &HashMap<NodeId, (u32, ValueNode)>,
ranges: &HashMap<NodeId, Range>,
) -> Option<LoopBound> {
let cmp_id = match &header.control {
Some(ControlNode::Branch { condition, .. }) => condition,
_ => return None,
};
let (_, cmp_node) = node_map.get(cmp_id)?;
match cmp_node {
ValueNode::Int32LessThan { left, right } if left == phi_id => {
Some(LoopBound::LessThan(*ranges.get(right)?))
}
ValueNode::Int32LessThanOrEqual { left, right } if left == phi_id => {
Some(LoopBound::LessThanOrEqual(*ranges.get(right)?))
}
ValueNode::Int32GreaterThan { left, right } if right == phi_id => {
Some(LoopBound::LessThan(*ranges.get(left)?))
}
ValueNode::Int32GreaterThanOrEqual { left, right } if right == phi_id => {
Some(LoopBound::LessThanOrEqual(*ranges.get(left)?))
}
_ => None,
}
}
fn compute_induction_ranges(
init_range: &Range,
delta: i64,
bound: &LoopBound,
) -> Option<(Range, Range)> {
if delta <= 0 {
return None;
}
match bound {
LoopBound::LessThan(bound_range) => {
let phi_range = Range {
min: init_range.min,
max: bound_range.max,
};
let step_range = Range {
min: init_range.min + delta,
max: bound_range.max - 1 + delta,
};
Some((phi_range, step_range))
}
LoopBound::LessThanOrEqual(bound_range) => {
let phi_range = Range {
min: init_range.min,
max: bound_range.max + delta,
};
let step_range = Range {
min: init_range.min + delta,
max: bound_range.max + delta,
};
Some((phi_range, step_range))
}
}
}
fn lower_checked_to_unchecked(node: &ValueNode) -> Option<ValueNode> {
match node {
ValueNode::CheckedSmiIncrement { value } | ValueNode::GenericIncrement { value, .. } => {
Some(ValueNode::Int32Increment { value: *value })
}
ValueNode::CheckedSmiDecrement { value } | ValueNode::GenericDecrement { value, .. } => {
Some(ValueNode::Int32Decrement { value: *value })
}
ValueNode::CheckedSmiAdd { left, right } | ValueNode::GenericAdd { left, right, .. } => {
Some(ValueNode::Int32Add {
left: *left,
right: *right,
})
}
_ => None,
}
}
fn rewrite_accumulator_phis(
graph: &mut MaglevGraph,
ranges: &mut HashMap<NodeId, Range>,
loop_metas: &[LoopMeta],
node_map: &HashMap<NodeId, (u32, ValueNode)>,
) {
let mut rewrites: Vec<(u32, usize, NodeId, Range, ValueNode, NodeId, Range)> = Vec::new();
for meta in loop_metas {
let header = &graph.blocks()[meta.header_idx as usize];
for (phi_id, node) in &header.nodes {
if meta.induction_phi_ids.contains(phi_id) {
continue;
}
let ValueNode::Phi { inputs } = node else {
continue;
};
if inputs.len() != 2 {
continue;
}
let init_id = inputs[meta.entry_pos];
let back_id = inputs[meta.back_pred_pos];
let Some(init_range) = ranges.get(&init_id) else {
continue;
};
let Some((_, back_node)) = node_map.get(&back_id) else {
continue;
};
let addend_range = match back_node {
ValueNode::CheckedSmiAdd { left, right }
| ValueNode::GenericAdd { left, right, .. }
if *left == *phi_id =>
{
ranges.get(right).copied()
}
ValueNode::CheckedSmiAdd { left, right }
| ValueNode::GenericAdd { left, right, .. }
if *right == *phi_id =>
{
ranges.get(left).copied()
}
_ => None,
};
let addend_range =
addend_range.or_else(|| find_chained_addend(&back_id, phi_id, node_map, ranges));
let Some(addend_range) = addend_range else {
continue;
};
let acc_range =
match compute_accumulator_range(init_range, &addend_range, meta.max_iterations) {
Some(r) => r,
None => continue,
};
if !acc_range.fits_i32() {
continue;
}
let back_min = match acc_range.min.checked_add(addend_range.min) {
Some(v) => v,
None => continue,
};
let back_max = match acc_range.max.checked_add(addend_range.max) {
Some(v) => v,
None => continue,
};
let back_range = Range {
min: back_min,
max: back_max,
};
if !back_range.fits_i32() {
continue;
}
let Some((step_block_idx, _)) = node_map.get(&back_id) else {
continue;
};
let step_block = &graph.blocks()[*step_block_idx as usize];
let Some(node_idx) = step_block.nodes.iter().position(|(id, _)| *id == back_id) else {
continue;
};
let replacement = match back_node {
ValueNode::CheckedSmiAdd { left, right }
| ValueNode::GenericAdd { left, right, .. } => ValueNode::Int32Add {
left: *left,
right: *right,
},
_ => continue,
};
rewrites.push((
*step_block_idx,
node_idx,
back_id,
back_range,
replacement,
*phi_id,
acc_range,
));
}
}
for (block_idx, node_idx, back_id, back_range, replacement, phi_id, acc_range) in rewrites {
if let Some(block) = graph.block_mut(block_idx) {
block.nodes[node_idx].1 = replacement;
}
ranges.insert(back_id, back_range);
ranges.insert(phi_id, acc_range);
}
}
fn eager_range(
node_id: &NodeId,
phi_id: &NodeId,
ranges: &HashMap<NodeId, Range>,
node_map: &HashMap<NodeId, (u32, ValueNode)>,
depth: u32,
) -> Option<Range> {
if let Some(r) = ranges.get(node_id) {
return Some(*r);
}
if *node_id == *phi_id {
return None;
}
if depth > 8 {
return None;
}
let (_, node) = node_map.get(node_id)?;
match node {
ValueNode::GenericAdd { left, right, .. } | ValueNode::CheckedSmiAdd { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
Some(Range {
min: lr.min.checked_add(rr.min)?,
max: lr.max.checked_add(rr.max)?,
})
}
ValueNode::GenericSubtract { left, right, .. }
| ValueNode::CheckedSmiSubtract { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
Some(Range {
min: lr.min.checked_sub(rr.max)?,
max: lr.max.checked_sub(rr.min)?,
})
}
ValueNode::GenericMultiply { left, right, .. }
| ValueNode::CheckedSmiMultiply { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
let candidates = [
lr.min.checked_mul(rr.min)?,
lr.min.checked_mul(rr.max)?,
lr.max.checked_mul(rr.min)?,
lr.max.checked_mul(rr.max)?,
];
Some(Range {
min: candidates.iter().copied().min().unwrap(),
max: candidates.iter().copied().max().unwrap(),
})
}
ValueNode::GenericIncrement { value, .. } | ValueNode::CheckedSmiIncrement { value } => {
let vr = eager_range(value, phi_id, ranges, node_map, depth + 1)?;
Some(Range {
min: vr.min.checked_add(1)?,
max: vr.max.checked_add(1)?,
})
}
ValueNode::Int32Add { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
Some(Range {
min: lr.min.checked_add(rr.min)?,
max: lr.max.checked_add(rr.max)?,
})
}
ValueNode::Int32Multiply { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
let candidates = [
lr.min.checked_mul(rr.min)?,
lr.min.checked_mul(rr.max)?,
lr.max.checked_mul(rr.min)?,
lr.max.checked_mul(rr.max)?,
];
Some(Range {
min: candidates.iter().copied().min().unwrap(),
max: candidates.iter().copied().max().unwrap(),
})
}
ValueNode::Int32Increment { value } => {
let vr = eager_range(value, phi_id, ranges, node_map, depth + 1)?;
Some(Range {
min: vr.min.checked_add(1)?,
max: vr.max.checked_add(1)?,
})
}
ValueNode::Int32ShiftLeft { left, right } => {
let lr = eager_range(left, phi_id, ranges, node_map, depth + 1)?;
let rr = eager_range(right, phi_id, ranges, node_map, depth + 1)?;
if rr.min < 0 || rr.max > 31 {
return None;
}
Some(Range {
min: lr.min.checked_shl(rr.min as u32)?,
max: lr.max.checked_shl(rr.max as u32)?,
})
}
ValueNode::GenericBitwiseOr { .. } | ValueNode::Int32BitwiseOr { .. } => {
Some(Range::I32_FULL)
}
_ => None,
}
}
fn find_chained_addend(
back_id: &NodeId,
phi_id: &NodeId,
node_map: &HashMap<NodeId, (u32, ValueNode)>,
ranges: &HashMap<NodeId, Range>,
) -> Option<Range> {
let mut current = *back_id;
let mut total_min: i64 = 0;
let mut total_max: i64 = 0;
let mut depth: u32 = 0;
loop {
if depth > 8 {
return None;
}
depth += 1;
let (_, node) = node_map.get(¤t)?;
let (left, right) = match node {
ValueNode::GenericAdd { left, right, .. }
| ValueNode::CheckedSmiAdd { left, right } => (*left, *right),
_ => return None,
};
if left == *phi_id {
let addend = eager_range(&right, phi_id, ranges, node_map, 0)?;
total_min = total_min.checked_add(addend.min)?;
total_max = total_max.checked_add(addend.max)?;
return Some(Range {
min: total_min,
max: total_max,
});
}
if right == *phi_id {
let addend = eager_range(&left, phi_id, ranges, node_map, 0)?;
total_min = total_min.checked_add(addend.min)?;
total_max = total_max.checked_add(addend.max)?;
return Some(Range {
min: total_min,
max: total_max,
});
}
let right_range = eager_range(&right, phi_id, ranges, node_map, 0);
let left_range = eager_range(&left, phi_id, ranges, node_map, 0);
if let Some(addend) = right_range {
total_min = total_min.checked_add(addend.min)?;
total_max = total_max.checked_add(addend.max)?;
current = left;
} else if let Some(addend) = left_range {
total_min = total_min.checked_add(addend.min)?;
total_max = total_max.checked_add(addend.max)?;
current = right;
} else {
return None;
}
}
}
fn compute_accumulator_range(init: &Range, addend: &Range, max_iters: i64) -> Option<Range> {
let min = if addend.min < 0 {
let product = max_iters.checked_mul(addend.min)?;
init.min.checked_add(product)?
} else {
init.min
};
let max = if addend.max > 0 {
let product = max_iters.checked_mul(addend.max)?;
init.max.checked_add(product)?
} else {
init.max
};
Some(Range { min, max })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::maglev::ir::{BasicBlock, ControlNode, MaglevGraph, ValueNode};
#[test]
fn test_range_exact_fits_i32() {
assert!(Range::exact(0).fits_i32());
assert!(Range::exact(i32::MAX as i64).fits_i32());
assert!(Range::exact(i32::MIN as i64).fits_i32());
}
#[test]
fn test_range_overflow_does_not_fit_i32() {
assert!(!Range::exact(i32::MAX as i64 + 1).fits_i32());
assert!(!Range::exact(i32::MIN as i64 - 1).fits_i32());
}
#[test]
fn test_checked_add_small_constants_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c10 = block.push_value(ValueNode::SmiConstant { value: 10 });
let c20 = block.push_value(ValueNode::SmiConstant { value: 20 });
let add = block.push_value(ValueNode::CheckedSmiAdd {
left: c10,
right: c20,
});
block.set_control(ControlNode::Return { value: add });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::Int32Add { .. }
));
}
#[test]
fn test_checked_add_near_max_not_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let big = block.push_value(ValueNode::Int32Constant {
value: i32::MAX - 1,
});
let one = block.push_value(ValueNode::Int32Constant { value: 2 });
let add = block.push_value(ValueNode::CheckedSmiAdd {
left: big,
right: one,
});
block.set_control(ControlNode::Return { value: add });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::CheckedSmiAdd { .. }
));
}
#[test]
fn test_checked_subtract_small_constants_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c30 = block.push_value(ValueNode::SmiConstant { value: 30 });
let c10 = block.push_value(ValueNode::SmiConstant { value: 10 });
let sub = block.push_value(ValueNode::CheckedSmiSubtract {
left: c30,
right: c10,
});
block.set_control(ControlNode::Return { value: sub });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::Int32Subtract { .. }
));
}
#[test]
fn test_checked_multiply_small_constants_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c5 = block.push_value(ValueNode::SmiConstant { value: 5 });
let c7 = block.push_value(ValueNode::SmiConstant { value: 7 });
let mul = block.push_value(ValueNode::CheckedSmiMultiply {
left: c5,
right: c7,
});
block.set_control(ControlNode::Return { value: mul });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::Int32Multiply { .. }
));
}
#[test]
fn test_checked_multiply_overflow_not_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let big = block.push_value(ValueNode::Int32Constant {
value: i32::MAX / 2 + 1,
});
let two = block.push_value(ValueNode::Int32Constant { value: 2 });
let mul = block.push_value(ValueNode::CheckedSmiMultiply {
left: big,
right: two,
});
block.set_control(ControlNode::Return { value: mul });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::CheckedSmiMultiply { .. }
));
}
#[test]
fn test_checked_increment_small_constant_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c = block.push_value(ValueNode::SmiConstant { value: 99 });
let inc = block.push_value(ValueNode::CheckedSmiIncrement { value: c });
block.set_control(ControlNode::Return { value: inc });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[1].1,
ValueNode::Int32Increment { .. }
));
}
#[test]
fn test_checked_increment_at_max_not_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let big = block.push_value(ValueNode::Int32Constant { value: i32::MAX });
let inc = block.push_value(ValueNode::CheckedSmiIncrement { value: big });
block.set_control(ControlNode::Return { value: inc });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[1].1,
ValueNode::CheckedSmiIncrement { .. }
));
}
#[test]
fn test_checked_decrement_small_constant_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c = block.push_value(ValueNode::SmiConstant { value: 5 });
let dec = block.push_value(ValueNode::CheckedSmiDecrement { value: c });
block.set_control(ControlNode::Return { value: dec });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[1].1,
ValueNode::Int32Decrement { .. }
));
}
#[test]
fn test_parameter_add_not_lowered_full_range() {
let mut graph = MaglevGraph::new(2);
let mut block = BasicBlock::new(0);
let p0 = block.push_value(ValueNode::Parameter { index: 0 });
let p1 = block.push_value(ValueNode::Parameter { index: 1 });
let add = block.push_value(ValueNode::CheckedSmiAdd {
left: p0,
right: p1,
});
block.set_control(ControlNode::Return { value: add });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::CheckedSmiAdd { .. }
));
}
#[test]
fn test_chained_add_constants_both_lowered() {
let mut graph = MaglevGraph::new(0);
let mut block = BasicBlock::new(0);
let c10 = block.push_value(ValueNode::SmiConstant { value: 10 });
let c20 = block.push_value(ValueNode::SmiConstant { value: 20 });
let add1 = block.push_value(ValueNode::CheckedSmiAdd {
left: c10,
right: c20,
});
let c30 = block.push_value(ValueNode::SmiConstant { value: 30 });
let add2 = block.push_value(ValueNode::CheckedSmiAdd {
left: add1,
right: c30,
});
block.set_control(ControlNode::Return { value: add2 });
graph.add_block(block);
eliminate_overflow_checks(&mut graph);
assert!(matches!(
&graph.blocks()[0].nodes[2].1,
ValueNode::Int32Add { .. }
));
assert!(matches!(
&graph.blocks()[0].nodes[4].1,
ValueNode::Int32Add { .. }
));
}
fn build_loop_graph(bound_value: i32) -> (MaglevGraph, NodeId) {
let mut graph = MaglevGraph::new(0);
let init = NodeId(0);
let phi = NodeId(1);
let bound = NodeId(2);
let cmp = NodeId(3);
let step = NodeId(4);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(init, ValueNode::SmiConstant { value: 0 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
phi,
ValueNode::Phi {
inputs: vec![init, step],
},
);
b1.push_with_id(bound, ValueNode::SmiConstant { value: bound_value });
b1.push_with_id(
cmp,
ValueNode::Int32LessThan {
left: phi,
right: bound,
},
);
b1.set_control(ControlNode::Branch {
condition: cmp,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(step, ValueNode::CheckedSmiIncrement { value: phi });
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
b3.set_control(ControlNode::Return { value: phi });
graph.add_block(b3);
(graph, step)
}
#[test]
fn test_loop_counter_increment_lowered_constant_bound() {
let (mut graph, step) = build_loop_graph(40);
eliminate_overflow_checks(&mut graph);
let step_node = &graph.blocks()[2].nodes[0];
assert_eq!(step_node.0, step);
assert!(
matches!(step_node.1, ValueNode::Int32Increment { .. }),
"expected Int32Increment, got {:?}",
step_node.1
);
}
#[test]
fn test_loop_counter_increment_lowered_param_bound() {
let mut graph = MaglevGraph::new(1);
let param = NodeId(0);
let init = NodeId(1);
let phi = NodeId(2);
let cmp = NodeId(3);
let step = NodeId(4);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(param, ValueNode::Parameter { index: 0 });
b0.push_with_id(init, ValueNode::SmiConstant { value: 0 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
phi,
ValueNode::Phi {
inputs: vec![init, step],
},
);
b1.push_with_id(
cmp,
ValueNode::Int32LessThan {
left: phi,
right: param,
},
);
b1.set_control(ControlNode::Branch {
condition: cmp,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(step, ValueNode::CheckedSmiIncrement { value: phi });
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
b3.set_control(ControlNode::Return { value: phi });
graph.add_block(b3);
eliminate_overflow_checks(&mut graph);
assert!(
matches!(
graph.blocks()[2].nodes[0].1,
ValueNode::Int32Increment { .. }
),
"expected Int32Increment with parameter bound"
);
}
#[test]
fn test_loop_counter_not_lowered_when_unsafe() {
let mut graph = MaglevGraph::new(1);
let param = NodeId(0);
let init = NodeId(1);
let phi = NodeId(2);
let cmp = NodeId(3);
let step = NodeId(4);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(param, ValueNode::Parameter { index: 0 });
b0.push_with_id(init, ValueNode::SmiConstant { value: 0 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
phi,
ValueNode::Phi {
inputs: vec![init, step],
},
);
b1.push_with_id(
cmp,
ValueNode::Int32LessThanOrEqual {
left: phi,
right: param,
},
);
b1.set_control(ControlNode::Branch {
condition: cmp,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(step, ValueNode::CheckedSmiIncrement { value: phi });
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
b3.set_control(ControlNode::Return { value: phi });
graph.add_block(b3);
eliminate_overflow_checks(&mut graph);
assert!(
matches!(
graph.blocks()[2].nodes[0].1,
ValueNode::CheckedSmiIncrement { .. }
),
"expected CheckedSmiIncrement to stay checked for unsafe <= bound"
);
}
fn build_sum_loop_graph(bound_value: i32) -> (MaglevGraph, NodeId, NodeId, NodeId) {
let mut graph = MaglevGraph::new(0);
let init_i = NodeId(0);
let init_sum = NodeId(1);
let phi_sum = NodeId(2); let phi_i = NodeId(3);
let bound = NodeId(4);
let cmp = NodeId(5);
let add = NodeId(6); let step = NodeId(7);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(init_i, ValueNode::SmiConstant { value: 0 });
b0.push_with_id(init_sum, ValueNode::SmiConstant { value: 0 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
phi_sum,
ValueNode::Phi {
inputs: vec![init_sum, add],
},
);
b1.push_with_id(
phi_i,
ValueNode::Phi {
inputs: vec![init_i, step],
},
);
b1.push_with_id(bound, ValueNode::SmiConstant { value: bound_value });
b1.push_with_id(
cmp,
ValueNode::Int32LessThan {
left: phi_i,
right: bound,
},
);
b1.set_control(ControlNode::Branch {
condition: cmp,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(
add,
ValueNode::CheckedSmiAdd {
left: phi_sum,
right: phi_i,
},
);
b2.push_with_id(step, ValueNode::CheckedSmiIncrement { value: phi_i });
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
b3.set_control(ControlNode::Return { value: phi_sum });
graph.add_block(b3);
(graph, step, add, phi_i)
}
#[test]
fn test_loop_counter_lowered_when_not_first_phi() {
let (mut graph, step, _add, _phi_i) = build_sum_loop_graph(10000);
eliminate_overflow_checks(&mut graph);
let step_node = &graph.blocks()[2].nodes[1];
assert_eq!(step_node.0, step);
assert!(
matches!(step_node.1, ValueNode::Int32Increment { .. }),
"expected Int32Increment, got {:?}",
step_node.1
);
}
#[test]
fn test_accumulator_add_lowered_small_bound() {
let (mut graph, _step, add, _phi_i) = build_sum_loop_graph(10000);
eliminate_overflow_checks(&mut graph);
let add_node = &graph.blocks()[2].nodes[0];
assert_eq!(add_node.0, add);
assert!(
matches!(add_node.1, ValueNode::Int32Add { .. }),
"expected Int32Add for accumulator, got {:?}",
add_node.1
);
}
#[test]
fn test_accumulator_add_not_lowered_huge_bound() {
let (mut graph, _step, add, _phi_i) = build_sum_loop_graph(50_000);
eliminate_overflow_checks(&mut graph);
let add_node = &graph.blocks()[2].nodes[0];
assert_eq!(add_node.0, add);
assert!(
matches!(add_node.1, ValueNode::CheckedSmiAdd { .. }),
"expected CheckedSmiAdd to stay checked for large accumulator, got {:?}",
add_node.1
);
}
fn build_chained_sum_loop_graph(
bound_value: i32,
) -> (MaglevGraph, NodeId, NodeId, NodeId, NodeId) {
let mut graph = MaglevGraph::new(0);
let init_i = NodeId(0);
let init_sum = NodeId(1);
let const2 = NodeId(2);
let const1 = NodeId(3);
let phi_sum = NodeId(10);
let phi_i = NodeId(11);
let bound = NodeId(12);
let cmp = NodeId(13);
let mul = NodeId(20); let add1 = NodeId(21); let add2 = NodeId(22); let step = NodeId(23);
let mut b0 = BasicBlock::new(0);
b0.push_with_id(init_i, ValueNode::SmiConstant { value: 0 });
b0.push_with_id(init_sum, ValueNode::SmiConstant { value: 0 });
b0.push_with_id(const2, ValueNode::SmiConstant { value: 2 });
b0.push_with_id(const1, ValueNode::SmiConstant { value: 1 });
b0.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b0);
let mut b1 = BasicBlock::new(1);
b1.add_predecessor(0);
b1.add_predecessor(2);
b1.push_with_id(
phi_sum,
ValueNode::Phi {
inputs: vec![init_sum, add2],
},
);
b1.push_with_id(
phi_i,
ValueNode::Phi {
inputs: vec![init_i, step],
},
);
b1.push_with_id(bound, ValueNode::SmiConstant { value: bound_value });
b1.push_with_id(
cmp,
ValueNode::Int32LessThan {
left: phi_i,
right: bound,
},
);
b1.set_control(ControlNode::Branch {
condition: cmp,
if_true: 2,
if_false: 3,
});
graph.add_block(b1);
let mut b2 = BasicBlock::new(2);
b2.add_predecessor(1);
b2.push_with_id(
mul,
ValueNode::GenericMultiply {
left: phi_i,
right: const2,
feedback_slot: 0,
},
);
b2.push_with_id(
add1,
ValueNode::GenericAdd {
left: phi_sum,
right: mul,
feedback_slot: 0,
},
);
b2.push_with_id(
add2,
ValueNode::GenericAdd {
left: add1,
right: const1,
feedback_slot: 0,
},
);
b2.push_with_id(step, ValueNode::CheckedSmiIncrement { value: phi_i });
b2.set_control(ControlNode::Jump { target: 1 });
graph.add_block(b2);
let mut b3 = BasicBlock::new(3);
b3.add_predecessor(1);
b3.set_control(ControlNode::Return { value: phi_sum });
graph.add_block(b3);
(graph, add1, add2, mul, step)
}
#[test]
fn test_chained_accumulator_lowered() {
let (mut graph, _add1, add2, _mul, _step) = build_chained_sum_loop_graph(10_000);
eliminate_overflow_checks(&mut graph);
let back_node = graph.blocks()[2]
.nodes
.iter()
.find(|(id, _)| *id == add2)
.unwrap();
assert!(
matches!(back_node.1, ValueNode::Int32Add { .. }),
"expected Int32Add for chained accumulator back-edge, got {:?}",
back_node.1
);
}
#[test]
fn test_chained_accumulator_intermediate_lowered() {
let (mut graph, add1, _add2, _mul, _step) = build_chained_sum_loop_graph(10_000);
eliminate_overflow_checks(&mut graph);
let mid_node = graph.blocks()[2]
.nodes
.iter()
.find(|(id, _)| *id == add1)
.unwrap();
assert!(
matches!(mid_node.1, ValueNode::Int32Add { .. }),
"expected Int32Add for intermediate accumulator add, got {:?}",
mid_node.1
);
}
#[test]
fn test_chained_accumulator_multiply_lowered() {
let (mut graph, _add1, _add2, mul, _step) = build_chained_sum_loop_graph(10_000);
eliminate_overflow_checks(&mut graph);
let mul_node = graph.blocks()[2]
.nodes
.iter()
.find(|(id, _)| *id == mul)
.unwrap();
assert!(
matches!(mul_node.1, ValueNode::Int32Multiply { .. }),
"expected Int32Multiply for i*2, got {:?}",
mul_node.1
);
}
#[test]
fn test_chained_accumulator_not_lowered_huge_bound() {
let (mut graph, _add1, add2, _mul, _step) = build_chained_sum_loop_graph(50_000);
eliminate_overflow_checks(&mut graph);
let back_node = graph.blocks()[2]
.nodes
.iter()
.find(|(id, _)| *id == add2)
.unwrap();
assert!(
matches!(back_node.1, ValueNode::GenericAdd { .. }),
"expected GenericAdd to stay generic for huge accumulator, got {:?}",
back_node.1
);
}
}