use alloc::{rc::Rc, vec::Vec};
use midenc_hir::{
AsValueRange, BlockRef, EntityMut, FxHashMap, Operation, OperationName, OperationRef,
RegionRef, Report, Rewriter, RewriterExt, SmallVec, ValueRef,
adt::SmallDenseMap,
cfg::Graph,
dominance::{DomTreeNode, DominanceInfo, PostDominanceInfo},
effects::MemoryEffect,
pass::{Pass, PassExecutionState, PostPassStatus},
patterns::{RewriterImpl, RewriterListener, TracingRewriterListener},
traits::{IsolatedFromAbove, Terminator},
};
#[derive(Default)]
pub struct CommonSubexpressionElimination;
midenc_hir::inventory::submit!(::midenc_hir::pass::registry::PassInfo::new::<
CommonSubexpressionElimination,
>("cse", "common subexpression elimintation"));
impl Pass for CommonSubexpressionElimination {
type Target = Operation;
fn name(&self) -> &'static str {
"cse"
}
fn argument(&self) -> &'static str {
"cse"
}
fn can_schedule_on(&self, _name: &OperationName) -> bool {
true
}
fn run_on_operation(
&mut self,
op: EntityMut<'_, Self::Target>,
state: &mut PassExecutionState,
) -> Result<(), Report> {
let op = op.into_entity_ref();
let context = op.context_rc();
let op = {
let op_ref = op.as_operation_ref();
drop(op);
op_ref
};
let dominfo = state.analysis_manager().get_analysis::<DominanceInfo>()?;
let mut rewriter = RewriterImpl::<TracingRewriterListener>::new(context)
.with_listener(TracingRewriterListener);
let mut driver = CSEDriver {
rewriter: &mut rewriter,
domtree: dominfo,
ops_to_erase: Default::default(),
mem_effects_cache: Default::default(),
};
let status = driver.simplify(op);
if status.ir_changed() {
state.preserved_analyses_mut().preserve::<DominanceInfo>();
state.preserved_analyses_mut().preserve::<PostDominanceInfo>();
} else {
state.preserved_analyses_mut().preserve_all();
}
Ok(())
}
}
struct CSEDriver<'a> {
rewriter: &'a mut RewriterImpl<TracingRewriterListener>,
ops_to_erase: Vec<OperationRef>,
domtree: Rc<DominanceInfo>,
mem_effects_cache: SmallDenseMap<OperationRef, (OperationRef, Option<MemoryEffect>)>,
}
type ScopedMap = FxHashMap<OpKey, OperationRef>;
impl CSEDriver<'_> {
pub fn simplify(&mut self, op: OperationRef) -> PostPassStatus {
let mut known_values = ScopedMap::default();
let mut next_region = op.borrow().regions().front().as_pointer();
let mut status = PostPassStatus::Unchanged;
while let Some(region) = next_region.take() {
next_region = region.next();
status |= self.simplify_region(&mut known_values, region);
}
status |= PostPassStatus::from(!self.ops_to_erase.is_empty());
for op in self.ops_to_erase.drain(..) {
self.rewriter.erase_op(op);
}
status
}
fn simplify_operation(
&mut self,
known_values: &mut ScopedMap,
op: OperationRef,
has_ssa_dominance: bool,
) -> PostPassStatus {
let operation = op.borrow();
if operation.implements::<dyn Terminator>() {
return PostPassStatus::Unchanged;
}
if operation.is_trivially_dead() {
self.ops_to_erase.push(op);
return PostPassStatus::Changed;
}
if !operation.regions().iter().all(|r| r.is_empty() || r.has_one_block()) {
return PostPassStatus::Unchanged;
}
if !operation.is_memory_effect_free() {
if !operation.has_single_memory_effect(MemoryEffect::Read) {
return PostPassStatus::Unchanged;
}
if let Some(existing) = known_values.get(&OpKey(op)).copied()
&& existing.parent() == op.parent()
&& !self.has_other_side_effecting_op_in_between(existing, op)
{
self.replace_uses_and_delete(known_values, op, existing, has_ssa_dominance);
return PostPassStatus::Changed;
}
known_values.insert(OpKey(op), op);
return PostPassStatus::Unchanged;
}
if let Some(existing) = known_values.get(&OpKey(op)).copied() {
self.replace_uses_and_delete(known_values, op, existing, has_ssa_dominance);
PostPassStatus::Changed
} else {
known_values.insert(OpKey(op), op);
PostPassStatus::Unchanged
}
}
fn simplify_block(
&mut self,
known_values: &mut ScopedMap,
block: BlockRef,
has_ssa_dominance: bool,
) -> PostPassStatus {
let mut changed = PostPassStatus::Unchanged;
let mut next_op = block.borrow().body().front().as_pointer();
while let Some(op) = next_op.take() {
next_op = op.next();
let operation = op.borrow();
if operation.has_regions() {
if operation.implements::<dyn IsolatedFromAbove>() {
let mut nested_known_values = ScopedMap::default();
let mut next_region = operation.regions().front().as_pointer();
while let Some(region) = next_region.take() {
next_region = region.next();
changed |= self.simplify_region(&mut nested_known_values, region);
}
} else {
let mut next_region = operation.regions().front().as_pointer();
while let Some(region) = next_region.take() {
next_region = region.next();
changed |= self.simplify_region(known_values, region);
}
}
}
changed |= self.simplify_operation(known_values, op, has_ssa_dominance);
}
self.mem_effects_cache.clear();
changed
}
fn simplify_region(
&mut self,
known_values: &mut ScopedMap,
region: RegionRef,
) -> PostPassStatus {
let region = region.borrow();
if region.is_empty() {
return PostPassStatus::Unchanged;
}
let has_ssa_dominance = self.domtree.has_ssa_dominance(region.as_region_ref());
if region.has_one_block() {
let mut scope = known_values.clone();
let block = region.entry_block_ref().unwrap();
drop(region);
return self.simplify_block(&mut scope, block, has_ssa_dominance);
}
if !has_ssa_dominance {
return PostPassStatus::Unchanged;
}
let mut stack = Vec::<CfgStackNode>::with_capacity(16);
let dominfo = self.domtree.dominance(region.as_region_ref());
stack.push(CfgStackNode::new(known_values.clone(), dominfo.root_node().unwrap()));
let mut changed = PostPassStatus::Unchanged;
while let Some(current_node) = stack.last_mut() {
if !current_node.processed {
current_node.processed = true;
changed |= self.simplify_block(
&mut current_node.scope,
current_node.node.block().unwrap(),
has_ssa_dominance,
);
}
if let Some(next_child) = current_node.children.next() {
let scope = current_node.scope.clone();
stack.push(CfgStackNode::new(scope, next_child));
} else {
stack.pop();
}
}
changed
}
fn replace_uses_and_delete(
&mut self,
known_values: &ScopedMap,
op: OperationRef,
mut existing: OperationRef,
has_ssa_dominance: bool,
) {
if has_ssa_dominance {
self.rewriter.notify_operation_replaced(op, existing);
let operation = op.borrow();
let existing = existing.borrow();
let op_results = operation.results().as_value_range().into_smallvec();
let existing_results = existing
.results()
.iter()
.copied()
.map(|result| Some(result as ValueRef))
.collect::<SmallVec<[_; 2]>>();
self.rewriter.replace_all_uses_with(&op_results, &existing_results);
self.ops_to_erase.push(op);
} else {
let was_visited = |operand: &midenc_hir::OpOperandImpl| {
!known_values.contains_key(&OpKey(operand.owner))
};
let op_results = op.borrow().results().as_value_range().into_smallvec();
let should_replace_op = op_results.iter().all(|v| {
let v = v.borrow();
v.iter_uses().all(|user| was_visited(&user))
});
if should_replace_op {
self.rewriter.notify_operation_replaced(op, existing);
}
let existing_results = existing.borrow().results().as_value_range().into_smallvec();
self.rewriter
.maybe_replace_uses_with(&op_results, &existing_results, was_visited);
if !op.borrow().is_used() {
self.ops_to_erase.push(op);
}
}
let mut existing = existing.borrow_mut();
let op_span = op.borrow().span;
if existing.span.is_unknown() && !op_span.is_unknown() {
existing.set_span(op_span);
}
}
fn has_other_side_effecting_op_in_between(
&mut self,
from: OperationRef,
to: OperationRef,
) -> bool {
assert_eq!(from.parent(), to.parent(), "expected operations to be in the same block");
let from_op = from.borrow();
assert!(
from_op.has_memory_effect(MemoryEffect::Read),
"expected read effect on `from` op"
);
assert!(
to.borrow().has_memory_effect(MemoryEffect::Read),
"expected read effect on `to` op"
);
let result = self.mem_effects_cache.entry(from).or_insert((from, None));
let mut next_op = if result.1.is_none() {
Some(result.0)
} else {
return true;
};
while let Some(next) = next_op.take()
&& next != to
{
next_op = next.next();
let effects = next.borrow().get_effects_recursively::<MemoryEffect>();
if let Some(effects) = effects.as_deref() {
for effect in effects {
if effect.effect() == MemoryEffect::Write {
*result = (next, Some(MemoryEffect::Write));
return true;
}
}
} else {
*result = (next, Some(MemoryEffect::Write));
return true;
}
}
*result = (to, None);
false
}
}
struct CfgStackNode {
scope: ScopedMap,
node: Rc<DomTreeNode>,
children: <Rc<DomTreeNode> as Graph>::ChildIter,
processed: bool,
}
impl CfgStackNode {
pub fn new(scope: ScopedMap, node: Rc<DomTreeNode>) -> Self {
let children = <Rc<DomTreeNode> as Graph>::children(node.clone());
Self {
scope,
node,
children,
processed: false,
}
}
}
#[derive(Copy, Clone)]
struct OpKey(OperationRef);
impl core::hash::Hash for OpKey {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
use midenc_hir::equivalence::{
DefaultValueHasher, IgnoreValueHasher, OperationEquivalenceFlags,
};
self.0.borrow().hash_with_options(
OperationEquivalenceFlags::IGNORE_LOCATIONS,
DefaultValueHasher,
IgnoreValueHasher,
state,
);
}
}
impl Eq for OpKey {}
impl PartialEq for OpKey {
fn eq(&self, other: &Self) -> bool {
use midenc_hir::equivalence::OperationEquivalenceFlags;
if self.0 == other.0 {
return true;
}
let lhs = self.0.borrow();
let rhs = other.0.borrow();
lhs.is_equivalent_with_options(&rhs, OperationEquivalenceFlags::IGNORE_LOCATIONS, |l, r| {
core::ptr::addr_eq(l, r)
})
}
}
#[cfg(test)]
mod tests {
use alloc::{format, string::ToString, sync::Arc};
use litcheck_filecheck::filecheck;
use midenc_dialect_arith::ArithOpBuilder;
use midenc_hir::{
PointerType, SourceSpan, Type, dialects::builtin::BuiltinOpBuilder, print::AsmPrinter,
testing::Test,
};
use super::*;
#[test]
fn simple_constant() {
let mut test = Test::new("simple_constant", &[], &[Type::I32, Type::I32]);
{
let mut builder = test.function_builder();
let v0 = builder.i32(1, SourceSpan::UNKNOWN);
let v1 = builder.i32(1, SourceSpan::UNKNOWN);
builder.ret([v0, v1], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @simple_constant() -> (i32, i32) {
// CHECK: [[V0:%\d+]] = arith.constant 1 : i32;
%0 = arith.constant 1 : i32;
// CHECK-NEXT: builtin.ret [[V0]], [[V0]] : (i32, i32);
%1 = arith.constant 1 : i32;
builtin.ret %0, %1 : (i32, i32);
};
"#
);
}
#[test]
fn basic() {
let mut test = Test::new("basic", &[], &[Type::I32, Type::I32]);
{
let mut builder = test.function_builder();
let v0 = builder.i32(0, SourceSpan::UNKNOWN);
let v1 = builder.i32(0, SourceSpan::UNKNOWN);
let v2 = builder.i32(1, SourceSpan::UNKNOWN);
let v3 = builder.mul(v0, v2, SourceSpan::UNKNOWN).unwrap();
let v4 = builder.mul(v1, v2, SourceSpan::UNKNOWN).unwrap();
builder.ret([v3, v4], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @basic() -> (i32, i32) {
// CHECK: [[V0:%\d+]] = arith.constant 0 : i32;
%0 = arith.constant 0 : i32;
%1 = arith.constant 0 : i32;
// CHECK-NEXT: [[V2:%\d+]] = arith.constant 1 : i32;
%2 = arith.constant 1 : i32;
// CHECK-NEXT: [[V3:%\d+]] = arith.mul [[V0]], [[V2]] <{ overflow = #builtin.overflow<checked> }>
%3 = arith.mul %0, %2 <{ overflow = #builtin.overflow<checked> }>
%4 = arith.mul %1, %2 <{ overflow = #builtin.overflow<checked> }>
// CHECK-NEXT: builtin.ret [[V3]], [[V3]] : (i32, i32);
builtin.ret %3, %3 : (i32, i32);
};
"#
);
}
#[test]
fn many() {
let mut test = Test::new("many", &[Type::I32, Type::I32], &[Type::I32]);
{
let mut builder = test.function_builder();
let [v0, v1] = *builder.entry_block().borrow().arguments()[0..2].as_array().unwrap();
let v0 = v0 as ValueRef;
let v1 = v1 as ValueRef;
let v2 = builder.add(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v3 = builder.add(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v4 = builder.add(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v5 = builder.add(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v6 = builder.add(v2, v3, SourceSpan::UNKNOWN).unwrap();
let v7 = builder.add(v4, v5, SourceSpan::UNKNOWN).unwrap();
let v8 = builder.add(v2, v4, SourceSpan::UNKNOWN).unwrap();
let v9 = builder.add(v6, v7, SourceSpan::UNKNOWN).unwrap();
let v10 = builder.add(v7, v8, SourceSpan::UNKNOWN).unwrap();
let v11 = builder.add(v9, v10, SourceSpan::UNKNOWN).unwrap();
builder.ret([v11], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @many(%0: i32, %1: i32) -> i32 {
// CHECK: [[V2:%\d+]] = arith.add %{{\d+}}, %{{\d+}} <{ overflow = #builtin.overflow<checked> }>;
%2 = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
%3 = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
%4 = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
%5 = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: [[V6:%\d+]] = arith.add [[V2]], [[V2]] <{ overflow = #builtin.overflow<checked> }>;
%6 = arith.add %2, %3 <{ overflow = #builtin.overflow<checked> }>;
%7 = arith.add %4, %5 <{ overflow = #builtin.overflow<checked> }>;
%8 = arith.add %2, %4 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: [[V9:%\d+]] = arith.add [[V6]], [[V6]] <{ overflow = #builtin.overflow<checked> }>;
%9 = arith.add %6, %7 <{ overflow = #builtin.overflow<checked> }>;
%10 = arith.add %7, %8 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: [[V11:%\d+]] = arith.add [[V9]], [[V9]] <{ overflow = #builtin.overflow<checked> }>;
%11 = arith.add %9, %10 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: builtin.ret [[V11]] : (i32);
builtin.ret %11 : (i32);
};
"#
);
}
#[test]
fn ops_with_different_operands_are_not_elimited() {
let mut test = Test::new("different_operands", &[], &[Type::I32, Type::I32]);
{
let mut builder = test.function_builder();
let v0 = builder.i32(0, SourceSpan::UNKNOWN);
let v1 = builder.i32(1, SourceSpan::UNKNOWN);
builder.ret([v0, v1], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @different_operands() -> (i32, i32) {
// CHECK: [[V0:%\d+]] = arith.constant 0 : i32;
// CHECK-NEXT: [[V1:%\d+]] = arith.constant 1 : i32;
%0 = arith.constant 0 : i32;
%1 = arith.constant 1 : i32;
// CHECK-NEXT: builtin.ret [[V0]], [[V1]] : (i32, i32);
builtin.ret %0, %1 : (i32, i32);
};
"#
);
}
#[test]
fn ops_with_different_result_types_are_not_elimited() {
let mut test = Test::new("different_results", &[Type::I32], &[Type::I64, Type::I128]);
{
let mut builder = test.function_builder();
let v0 = builder.entry_block().borrow().arguments()[0] as ValueRef;
let v1 = builder.sext(v0, Type::I64, SourceSpan::UNKNOWN).unwrap();
let v2 = builder.sext(v0, Type::I128, SourceSpan::UNKNOWN).unwrap();
builder.ret([v1, v2], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @different_results(%0: i32) -> (i64, i128) {
// CHECK: [[V1:%\d+]] = arith.sext %0 <{ ty = #builtin.type<i64> }>;
// CHECK-NEXT: [[V2:%\d+]] = arith.sext %0 <{ ty = #builtin.type<i128> }>;
v1 = arith.sext %0 <{ ty = #builtin.type<i64> }>;
v2 = arith.sext %0 <{ ty = #builtin.type<i128> }>;
// CHECK-NEXT: builtin.ret [[V1]], [[V2]] : (i64, i128);
builtin.ret %1, %2 : (i64, i128);
};
"#
);
}
#[test]
fn ops_with_different_attributes_are_not_elimited() {
let mut test = Test::new("different_attributes", &[Type::I32], &[Type::I32, Type::I32]);
{
let mut builder = test.function_builder();
let v0 = builder.entry_block().borrow().arguments()[0] as ValueRef;
let v1 = builder.i32(1, SourceSpan::UNKNOWN);
let v2 = builder.add(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v3 = builder.add_unchecked(v0, v1, SourceSpan::UNKNOWN).unwrap();
builder.ret([v2, v3], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
filecheck!(
output,
r#"
builtin.function public extern("C") @different_attributes(%0: i32) -> (i32, i32) {
// CHECK: [[V1:%\d+]] = arith.constant 1 : i32;
v1 = arith.constant 1 : i32;
// CHECK-NEXT: [[V2:%\d+]] = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
v2 = arith.add %0, %1 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: [[V3:%\d+]] = arith.add %0, %1 <{ overflow = #builtin.overflow<unchecked> }>;
v3 = arith.add %0, %1 <{ overflow = #builtin.overflow<unchecked> }>;
// CHECK-NEXT: builtin.ret [[V2]], [[V3]] : (i32, i32);
builtin.ret %2, %3 : (i32, i32);
};
"#
);
}
#[test]
fn ops_with_side_effects_are_not_elimited() {
use midenc_dialect_hir::HirOpBuilder;
let byte_ptr = Type::Ptr(Arc::new(PointerType::new(Type::U8)));
let mut test =
Test::new("side_effect", core::slice::from_ref(&byte_ptr), &[Type::U8, Type::U8]);
{
let mut builder = test.function_builder();
let v0 = builder.entry_block().borrow().arguments()[0] as ValueRef;
let v1 = builder.u8(1, SourceSpan::UNKNOWN);
builder.store(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v2 = builder.load(v0, SourceSpan::UNKNOWN).unwrap();
builder.store(v0, v1, SourceSpan::UNKNOWN).unwrap();
let v3 = builder.load(v0, SourceSpan::UNKNOWN).unwrap();
builder.ret([v2, v3], SourceSpan::UNKNOWN).unwrap();
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
std::println!("{output}");
filecheck!(
output,
r#"
builtin.function public extern("C") @side_effect(%0: ptr<u8, byte>) -> (u8, u8) {
// CHECK: [[V1:%\d+]] = arith.constant 1 : u8;
%1 = arith.constant 1 : u8;
// CHECK-NEXT: hir.store %0, %1 : (ptr<u8, byte>, u8);
// CHECK-NEXT: [[V2:%\d+]] = hir.load %0;
hir.store %0, %1 : (ptr<u8, byte>, u8);
%2 = hir.load %0;
// CHECK-NEXT: hir.store %0, %1 : (ptr<u8, byte>, u8);
// CHECK-NEXT: [[V3:%\d+]] = hir.load %0;
hir.store v0, %1 : (ptr<u8, byte>, u8);
%3 = hir.load %0;
// CHECK-NEXT: builtin.ret [[V2]], [[V3]] : (u8, u8);
builtin.ret %2, %3 : (u8, u8);
};
"#
);
}
#[test]
fn proper_propagation_of_ops_down_dominance_tree() {
use midenc_dialect_scf::StructuredControlFlowOpBuilder;
let mut test = Test::new("down_propagate_while", &[], &[]);
{
let mut builder = test.function_builder();
let v0 = builder.i32(0, SourceSpan::UNKNOWN);
let v1 = builder.i32(1, SourceSpan::UNKNOWN);
let v2 = builder.i32(4, SourceSpan::UNKNOWN);
let while_op = builder.r#while([v0, v1], &[], SourceSpan::UNKNOWN).unwrap();
builder.ret(None, SourceSpan::UNKNOWN).unwrap();
{
let before_block = while_op.borrow().before().entry().as_block_ref();
let after_block = while_op.borrow().after().entry().as_block_ref();
builder.switch_to_block(before_block);
let [v3, v4] = *before_block.borrow().arguments()[0..2].as_array().unwrap();
let v3 = v3 as ValueRef;
let v4 = v4 as ValueRef;
let v5 = builder.i32(1, SourceSpan::UNKNOWN);
let v6 = builder.add(v3, v5, SourceSpan::UNKNOWN).unwrap();
let v7 = builder.lt(v6, v2, SourceSpan::UNKNOWN).unwrap();
builder.condition(v7, [v6, v4], SourceSpan::UNKNOWN).unwrap();
builder.switch_to_block(after_block);
let v8 = builder.append_block_param(after_block, Type::I32, SourceSpan::UNKNOWN);
let v9 = builder.append_block_param(after_block, Type::I32, SourceSpan::UNKNOWN);
builder.r#yield([v8 as ValueRef, v9 as ValueRef], SourceSpan::UNKNOWN).unwrap();
}
}
test.apply_pass::<CommonSubexpressionElimination>(true).expect("invalid ir");
let flags = Default::default();
let mut printer = AsmPrinter::new(test.context_rc(), &flags);
printer.print_operation(test.function().borrow());
let output = format!("{}", printer.finish());
std::println!("output: {output}");
filecheck!(
output,
r#"
builtin.function public extern("C") @down_propagate_while() {
// CHECK: [[V0:%\d+]] = arith.constant 0 : i32;
%0 = arith.constant 0 : i32;
// CHECK: [[V1:%\d+]] = arith.constant 1 : i32;
%1 = arith.constant 1 : i32;
// CHECK: [[V2:%\d+]] = arith.constant 4 : i32;
%2 = arith.constant 4 : i32;
// CHECK-NEXT: scf.while %0, %1 before {
// CHECK-NEXT: ^block{{\d}}([[V3:%\d+]]: i32, [[V4:%\d+]]: i32):
scf.while %0, %1 before {
^block1(%3: i32, %4: i32):
// CHECK-NEXT: [[V6:%\d+]] = arith.add [[V3]], [[V1]] <{ overflow = #builtin.overflow<checked> }>;
%5 = arith.constant 1 : i32;
%6 = arith.add %3, %5 <{ overflow = #builtin.overflow<checked> }>;
// CHECK-NEXT: [[V7:%\d+]] = arith.lt [[V6]], [[V2]];
%7 = arith.lt %6, %2;
// CHECK-NEXT: scf.condition [[V7]], [[V6]], [[V4]] : (i1, i32, i32);
scf.condition %7, %6, %4 : (i1, i32, i32);
} after {
^block2(%8: i32, %9: i32):
// CHECK-NEXT: } after {
// CHECK-NEXT: ^block{{\d}}([[V8:%\d+]]: i32, [[V9:%\d+]]: i32):
// CHECK-NEXT: scf.yield [[V8]], [[V9]] : (i32, i32);
scf.yield %8, %9 : (i32, i32);
} : (i32, i32);
// CHECK: builtin.ret;
builtin.ret;
};
"#
);
}
}