use qcode::value::{
BlockId, BlockParamId, FunctionBody, FunctionId, LocalValueId, QCodeView, ValueId,
insn::{Branch, InstructionId, Mnemonic},
};
use crate::{PassCtx, with_body_mut};
pub fn simplify_cfg_body<'a, 'str>(
body: &'a mut FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
function_id: FunctionId,
) -> bool {
let mut changed = false;
loop {
let mut progress = prune_unreachable(body, cx, function_id);
let blocks = cx.body_view(body).function_ref(function_id).block_ids();
for block_id in blocks {
if try_fold_cbranch(body, cx, block_id)
|| try_bypass_empty_block(body, cx, function_id, block_id)
|| try_merge_block(body, cx, function_id, block_id)
{
progress = true;
break;
}
}
if progress {
changed = true;
} else {
break;
}
}
changed
}
fn prune_unreachable<'a, 'str>(
body: &'a mut FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
function_id: FunctionId,
) -> bool {
let Some(root) = cx
.body_view(body)
.function_ref(function_id)
.root()
.map(|b| b.id)
else {
return false;
};
let mut reachable = rustc_hash::FxHashSet::default();
let mut stack = vec![root];
while let Some(b) = stack.pop() {
if !reachable.insert(b) {
continue;
}
let succs: Vec<BlockId> = cx
.body_view(body)
.block_ref(b)
.successors()
.map(|(_, s)| s)
.collect();
stack.extend(succs);
}
let dead: Vec<BlockId> = cx
.body_view(body)
.function_ref(function_id)
.block_ids()
.into_iter()
.filter(|b| !reachable.contains(b))
.collect();
if dead.is_empty() {
return false;
}
for block in dead {
body.delete_block(block);
}
true
}
fn merge_candidate<'a, 'str>(
body: &'a FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
a_id: BlockId,
) -> Option<(qcode::value::block::EdgeId, BlockId)> {
let a_succs: Vec<_> = cx
.body_view(body)
.block_ref(a_id)
.successors()
.take(2)
.collect();
let &(edge_ab, b_id) = a_succs.first()?;
if a_succs.len() != 1 || b_id == a_id {
return None; }
if cx.body_view(body).block_ref(b_id).predecessors().count() != 1 {
return None;
}
let a_terminal = cx
.body_view(body)
.block_ref(a_id)
.instruction_ids()
.last()
.copied();
let is_branch_to_b = a_terminal
.map(|id| {
matches!(
cx.body_view(body).insn_ref(id).mnemonic(),
Mnemonic::Branch(b) if BlockId::new(a_id.func, b.target) == b_id
)
})
.unwrap_or(false);
if !is_branch_to_b {
return None;
}
let b_params = cx.body_view(body).block_ref(b_id).num_params();
let branch_args = a_terminal
.and_then(|id| match cx.body_view(body).insn_ref(id).mnemonic() {
Mnemonic::Branch(b) => Some(b.args.len()),
_ => None,
})
.unwrap_or(0);
if b_params != branch_args {
return None;
}
Some((edge_ab, b_id))
}
fn try_merge_block<'a, 'str>(
body: &'a mut FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
function_id: FunctionId,
a_id: BlockId,
) -> bool {
let Some((edge_ab, b_id)) = merge_candidate(body, cx, a_id) else {
return false;
};
if b_id.func != function_id {
return false;
}
body.absorb_block(a_id, b_id, edge_ab);
true
}
fn try_fold_cbranch<'a, 'str>(
body: &'a mut FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
block_id: BlockId,
) -> bool {
let Some(term_id) = cx
.body_view(body)
.block_ref(block_id)
.instruction_ids()
.last()
.copied()
else {
return false;
};
let (target, args) = {
let Mnemonic::CBranch(cb) = cx.body_view(body).insn_ref(term_id).mnemonic() else {
return false;
};
if cb.success_block != cb.failure_block || cb.success_args != cb.failure_args {
return false;
}
(cb.success_block, cb.success_args.clone())
};
body.replace_instruction_mnemonic(term_id, Mnemonic::Branch(Branch { target, args }));
let target_full = BlockId::new(block_id.func, target);
let dup_edge = cx
.body_view(body)
.block_ref(block_id)
.successors()
.filter(|&(_, to)| to == target_full)
.map(|(e, _)| e)
.nth(1);
if let Some(dup_edge) = dup_edge {
body.remove_cfg_edge(dup_edge);
}
true
}
fn try_bypass_empty_block<'a, 'str>(
body: &'a mut FunctionBody<'str>,
cx: PassCtx<'a, 'str>,
function_id: FunctionId,
b_id: BlockId,
) -> bool {
if b_id.func != function_id {
return false;
}
if cx
.body_view(body)
.function_ref(function_id)
.root()
.map(|r| r.id)
== Some(b_id)
{
return false;
}
let (term_id, target, b_args) = {
let b = cx.body_view(body).block(b_id);
if b.instruction_ids().len() != 1 {
return false;
}
let term_id = InstructionId::new(b_id.func, b.instruction_ids()[0]);
match body.insn(term_id).mnemonic() {
Mnemonic::Branch(br) => (term_id, br.target, br.args.clone()),
_ => return false,
}
};
let target_full = BlockId::new(b_id.func, target);
if target_full == b_id {
return false; }
let params: Vec<BlockParamId> = cx
.body_view(body)
.block(b_id)
.param_ids()
.iter()
.map(|&p| BlockParamId::new(b_id.func, p))
.collect();
for &p in ¶ms {
if cx
.body_view(body)
.function_ref(b_id.func)
.users_of(ValueId::BlockParam(p))
.iter()
.any(|&u| u != term_id)
{
return false;
}
}
let preds: Vec<BlockId> = {
let mut seen = rustc_hash::FxHashSet::default();
cx.body_view(body)
.block_ref(b_id)
.predecessors()
.map(|(_, p)| p)
.filter(|&p| seen.insert(p))
.collect()
};
if preds.is_empty() {
return false;
}
if preds
.iter()
.any(|p| p.func != b_id.func || target_full.func != p.func)
{
return false;
}
for &p in &preds {
let Some(p_term) = cx
.body_view(body)
.block(p)
.instruction_ids()
.last()
.copied()
else {
return false;
};
let p_term = InstructionId::new(p.func, p_term);
match body.insn(p_term).mnemonic() {
Mnemonic::Branch(br) => {
if BlockId::new(p.func, br.target) != b_id || br.args.len() != params.len() {
return false;
}
}
Mnemonic::CBranch(cb) => {
let mut names_b = false;
if BlockId::new(p.func, cb.success_block) == b_id {
if cb.success_args.len() != params.len() {
return false;
}
names_b = true;
}
if BlockId::new(p.func, cb.failure_block) == b_id {
if cb.failure_args.len() != params.len() {
return false;
}
names_b = true;
}
if !names_b {
return false;
}
}
_ => return false,
}
}
for &p in &preds {
let p_term = cx
.body_view(body)
.block(p)
.instruction_ids()
.last()
.copied()
.unwrap();
let p_term = InstructionId::new(p.func, p_term);
let new_mnemonic = match body.insn(p_term).mnemonic().clone() {
Mnemonic::Branch(br) => Mnemonic::Branch(Branch {
target,
args: substitute(&b_args, ¶ms, &br.args),
}),
Mnemonic::CBranch(mut cb) => {
if BlockId::new(p.func, cb.success_block) == b_id {
cb.success_args = substitute(&b_args, ¶ms, &cb.success_args);
cb.success_block = target;
}
if BlockId::new(p.func, cb.failure_block) == b_id {
cb.failure_args = substitute(&b_args, ¶ms, &cb.failure_args);
cb.failure_block = target;
}
Mnemonic::CBranch(cb)
}
_ => unreachable!("predecessor terminator validated above"),
};
let mut redirect: Vec<_> = cx
.body_view(body)
.block_ref(p)
.successors()
.filter(|&(_, to)| to == b_id)
.map(|(e, _)| e)
.collect();
redirect.sort_unstable();
redirect.dedup();
body.replace_instruction_mnemonic(p_term, new_mnemonic);
for &edge in &redirect {
body.remove_cfg_edge(edge);
}
for _ in &redirect {
body.add_cfg_edge(p, target_full);
}
}
body.delete_block(b_id);
true
}
fn substitute(
template: &[LocalValueId],
params: &[BlockParamId],
incoming: &[LocalValueId],
) -> Vec<LocalValueId> {
template
.iter()
.map(|&v| match v {
LocalValueId::BlockParam(p) => params
.iter()
.position(|&q| q.local == p)
.map(|idx| incoming[idx])
.unwrap_or(v),
_ => v,
})
.collect()
}
pub fn absorb_straight_line(ctx: &mut qcode::context::Context, block: BlockId) -> usize {
let function_id = block.func;
with_body_mut(ctx, function_id, |body, cx| {
let mut absorbed = 0;
while cx
.body_view(body)
.block_ref(block)
.successors()
.next()
.is_some_and(|(_, next)| !cx.body_view(body).block_ref(next).is_empty())
&& try_merge_block(body, cx, function_id, block)
{
absorbed += 1;
}
absorbed
})
}
pub fn simplify_cfg(ctx: &mut qcode::context::Context, function_id: FunctionId) -> bool {
with_body_mut(ctx, function_id, |body, cx| {
simplify_cfg_body(body, cx, function_id)
})
}
#[cfg(test)]
fn prune_unreachable_in(ctx: &mut qcode::context::Context, function_id: FunctionId) -> bool {
with_body_mut(ctx, function_id, |body, cx| {
prune_unreachable(body, cx, function_id)
})
}
#[cfg(test)]
fn try_merge_block_in(
ctx: &mut qcode::context::Context,
function_id: FunctionId,
block: BlockId,
) -> bool {
with_body_mut(ctx, function_id, |body, cx| {
try_merge_block(body, cx, function_id, block)
})
}
#[cfg(test)]
fn try_fold_cbranch_in(ctx: &mut qcode::context::Context, block: BlockId) -> bool {
with_body_mut(ctx, block.func, |body, cx| {
try_fold_cbranch(body, cx, block)
})
}
#[cfg(test)]
fn try_bypass_empty_block_in(
ctx: &mut qcode::context::Context,
function_id: FunctionId,
block: BlockId,
) -> bool {
with_body_mut(ctx, function_id, |body, cx| {
try_bypass_empty_block(body, cx, function_id, block)
})
}
#[cfg(test)]
mod tests {
use qcode::{
context::Context,
value::{
BasicBlock, FunctionBody, ValueId,
insn::{Binary, InstructionId, Mnemonic},
},
};
use wazabin_qcode_macro::qcode;
use super::{
prune_unreachable_in, simplify_cfg, try_bypass_empty_block_in, try_fold_cbranch_in,
try_merge_block_in,
};
fn make_ctx() -> Context<'static> {
Context::new()
}
#[test]
fn prunes_unreachable_blocks() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <t>;
<t>
goto <0x1001>;
<dead @x:i64>
%s = @x + 1;
goto <t>;
"
);
let changed = simplify_cfg(&mut ctx, f);
assert!(changed, "pruning an unreachable block reports progress");
assert!(
!ctx.contains_block(dead),
"unreachable block should be pruned"
);
assert!(
!ctx.contains_instruction(s),
"instructions owned by the unreachable block should be removed"
);
assert!(
!ctx.contains_block_param(x),
"parameters owned by the unreachable block should be removed"
);
assert!(
BasicBlock::from_id(&ctx, a).parent().is_some(),
"the reachable entry survives"
);
}
#[test]
fn prune_unreachable_is_a_noop_when_all_reachable() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8>
if @cond goto <t> else goto <u>;
<t>
goto <0x1001>;
<u>
goto <0x1002>;
"
);
assert!(
!prune_unreachable_in(&mut ctx, f),
"no unreachable blocks means no change"
);
}
#[test]
fn merges_two_block_chain() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<b>
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let blocks: Vec<_> = FunctionBody::from_id(&ctx, f)
.blocks()
.map(|b| b.id)
.collect();
assert_eq!(blocks.len(), 1, "two-block chain should merge into one");
assert_eq!(blocks[0], a);
}
#[test]
fn merges_three_block_chain() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<b>
goto <c>;
<c>
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let blocks: Vec<_> = FunctionBody::from_id(&ctx, f)
.blocks()
.map(|b| b.id)
.collect();
assert_eq!(blocks.len(), 1, "three-block chain should collapse to one");
}
#[test]
fn no_merge_when_a_has_two_successors() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8>
if @cond goto <b> else goto <c>;
<b>
%x = i64 1 + i64 1;
goto <0x1001>;
<c>
%y = i64 2 + i64 2;
goto <0x1002>;
"
);
simplify_cfg(&mut ctx, f);
let blocks: Vec<_> = FunctionBody::from_id(&ctx, f)
.blocks()
.map(|b| b.id)
.collect();
assert_eq!(blocks.len(), 3, "diamond entry should not be merged");
}
#[test]
fn no_merge_when_b_has_two_predecessors() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<d>
goto <b>;
<b>
%x = i64 1 + i64 1;
goto <0x1001>;
"
);
assert!(
!try_merge_block_in(&mut ctx, f, a),
"B has two predecessors, should not merge"
);
}
#[test]
fn parent_cleared_on_merged_block() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<b>
goto <0x1001>;
"
);
{
let b = BasicBlock::from_id(&ctx, b);
assert!(b.parent().is_some(), "b should have parent before merge");
}
simplify_cfg(&mut ctx, f);
{
let a = BasicBlock::from_id(&ctx, a);
assert!(!ctx.contains_block(b), "b should be removed after merge");
assert!(a.parent().is_some(), "a should still have a parent");
}
}
#[test]
fn merged_block_arguments_are_rewritten_to_branch_args() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @input:i64>
goto <b @x=@input>;
<b @x:i64>
%sum = @x + 1;
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let blocks: Vec<_> = FunctionBody::from_id(&ctx, f)
.blocks()
.map(|b| b.id)
.collect();
assert_eq!(blocks, [a], "branch-with-args chain should merge");
assert!(!ctx.contains_block(b));
assert!(
!ctx.contains_block_param(x),
"the merged block parameter should be removed after substitution"
);
let Mnemonic::Binop(Binary { lhs, .. }) = ctx.instruction(sum).mnemonic() else {
panic!("expected merged sum to be a binop");
};
assert_eq!(*lhs, ValueId::BlockParam(input).strip_func());
}
#[test]
fn merged_instructions_have_correct_parent() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
%x = i64 1 + i64 1;
goto <b>;
<b>
%y = i64 2 + i64 2;
goto <0x1001>;
"
);
let y_insn = y;
simplify_cfg(&mut ctx, f);
let insn = ctx.get_insn(y_insn);
let parent_id = insn.parent().map(|b| b.id());
assert_eq!(
parent_id,
Some(ValueId::BasicBlock(a)),
"instruction from b should be reparented to a after merge"
);
}
#[test]
fn absorbed_block_instruction_list_is_drained() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
%x = i64 1 + i64 1;
goto <b>;
<b>
%y = i64 2 + i64 2;
goto <0x1001>;
"
);
let forwarding = BasicBlock::from_id(&ctx, a)
.instructions()
.last()
.expect("a has a forwarding branch")
.id;
simplify_cfg(&mut ctx, f);
assert!(
!ctx.contains_block(b),
"absorbed block `b` must be removed after merge"
);
assert!(
ctx.contains_instruction(y),
"instructions absorbed from `b` must remain live"
);
assert!(
!ctx.contains_instruction(forwarding),
"the forwarding branch replaced by absorption must be removed"
);
assert_eq!(
ctx.get_insn(y).parent().map(|block| block.id()),
Some(ValueId::BasicBlock(a)),
"instructions absorbed from `b` must be reparented to `a`"
);
}
#[test]
fn no_instruction_belongs_to_two_blocks_after_simplify() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
%x = i64 1 + i64 1;
goto <b>;
<b>
%y = i64 2 + i64 2;
goto <c>;
<c>
%z = i64 3 + i64 3;
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let mut seen = rustc_hash::FxHashSet::default();
for block_id in ctx.block_ids() {
for &local in ctx.block(block_id).instruction_ids() {
let insn = InstructionId::new(block_id.func, local);
assert!(
seen.insert(insn),
"instruction {insn:?} appears in more than one block after simplify_cfg"
);
}
}
}
#[test]
fn remove_dead_insns_terminates_after_merge() {
use crate::remove_dead_insns;
use qcode::value::FunctionBody;
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<b>
%y = i64 2 + i64 2;
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let blocks: Vec<_> = FunctionBody::from_id(&ctx, f)
.iter()
.map(|blk| blk.id)
.collect();
for bid in blocks {
remove_dead_insns(&mut ctx, bid);
}
assert!(
FunctionBody::from_id(&ctx, f)
.iter()
.all(|blk| !blk.instruction_ids().contains(&y)),
"dead merged instruction must be removed, not looped on"
);
}
#[test]
fn bypasses_empty_block_with_two_predecessors() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<d>
goto <b>;
<b>
goto <t>;
<t>
%s = i64 1 + i64 1;
goto <0x1001>;
"
);
let removed_insns = BasicBlock::from_id(&ctx, b).instruction_ids();
try_bypass_empty_block_in(&mut ctx, f, b);
assert!(
!ctx.contains_block(b),
"empty forwarding block b should be spliced out"
);
assert!(
removed_insns
.into_iter()
.all(|insn| !ctx.contains_instruction(insn)),
"the forwarding block's terminator should be removed"
);
for pred in [a, d] {
let term = BasicBlock::from_id(&ctx, pred)
.iter()
.last()
.expect("pred has a terminator");
let Mnemonic::Branch(br) = term.mnemonic() else {
panic!("predecessor should end in an unconditional branch");
};
assert_eq!(br.target, t.local, "predecessor should target t directly");
}
}
#[test]
fn bypass_rejects_foreign_block_with_colliding_local_param_arena() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn foreign:
<foreign_entry @input:i64>
goto <foreign_b @x=@input>;
<foreign_b @x:i64>
goto <foreign_target @y=@x>;
<foreign_target @y:i64>
return at @y;
fn own:
<own_entry @input:i64>
goto <own_b @x=@input>;
<own_b @x:i64>
goto <own_target @y=@x>;
<own_target @y:i64>
return at @y;
"
);
assert_eq!(
foreign_b.local, own_b.local,
"regression setup requires colliding local block ids"
);
assert_eq!(
ctx.block(foreign_b).param_ids()[0],
ctx.block(own_b).param_ids()[0],
"regression setup requires colliding local parameter ids"
);
assert!(!try_bypass_empty_block_in(&mut ctx, own, foreign_b));
assert!(BasicBlock::from_id(&ctx, foreign_b).parent().is_some());
assert!(BasicBlock::from_id(&ctx, own_b).parent().is_some());
}
#[test]
fn bypass_threads_block_arguments_per_predecessor() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @av:i64>
goto <b @x=@av>;
<d @dv:i64>
goto <b @x=@dv>;
<b @x:i64>
goto <t @y=@x>;
<t @y:i64>
%s = @y + 1;
goto <0x1001>;
"
);
let removed_insns = BasicBlock::from_id(&ctx, b).instruction_ids();
try_bypass_empty_block_in(&mut ctx, f, b);
assert!(
!ctx.contains_block(b),
"forwarding block b should be spliced out"
);
assert!(
!ctx.contains_block_param(x),
"the forwarding block's parameter should be removed"
);
assert!(
removed_insns
.into_iter()
.all(|insn| !ctx.contains_instruction(insn)),
"the forwarding block's terminator should be removed"
);
let arg_to_t = |pred| {
let term = BasicBlock::from_id(&ctx, pred)
.iter()
.last()
.expect("pred has a terminator");
let Mnemonic::Branch(br) = term.mnemonic() else {
panic!("expected branch");
};
assert_eq!(br.target, t.local);
br.args[0]
};
assert_eq!(arg_to_t(a), ValueId::BlockParam(av).strip_func());
assert_eq!(arg_to_t(d), ValueId::BlockParam(dv).strip_func());
}
#[test]
fn bypass_redirects_cbranch_arm() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8>
if @cond goto <b> else goto <other>;
<other>
%o = i64 9 + i64 9;
goto <0x1002>;
<b>
goto <t>;
<t>
%s = i64 1 + i64 1;
goto <0x1001>;
"
);
let removed_insns = BasicBlock::from_id(&ctx, b).instruction_ids();
simplify_cfg(&mut ctx, f);
assert!(
!ctx.contains_block(b),
"empty block b should be spliced out"
);
assert!(
removed_insns
.into_iter()
.all(|insn| !ctx.contains_instruction(insn)),
"the forwarding block's terminator should be removed"
);
let term = BasicBlock::from_id(&ctx, a)
.iter()
.last()
.expect("a has a terminator");
let Mnemonic::CBranch(cb) = term.mnemonic() else {
panic!("a should still be a conditional branch");
};
assert_eq!(cb.success_block, t.local, "the b arm should now target t");
assert_eq!(cb.failure_block, other.local, "the other arm is untouched");
}
#[test]
fn does_not_bypass_self_loop() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
goto <b>;
<b>
goto <b>;
"
);
simplify_cfg(&mut ctx, f);
assert!(
BasicBlock::from_id(&ctx, b).parent().is_some(),
"self-looping block must survive"
);
}
#[test]
fn does_not_bypass_root_block() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8>
goto <b @c=@cond>;
<b @c:i8>
if @c goto <a @cond=@c> else goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
assert!(
BasicBlock::from_id(&ctx, a).parent().is_some(),
"the root block must never be spliced out"
);
assert_eq!(
FunctionBody::from_id(&ctx, f).root().map(|r| r.id),
Some(a),
"a should remain the function root"
);
}
#[test]
fn prunes_unreachable_forwarding_block() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a>
%s = i64 1 + i64 1;
goto <0x1001>;
<orphan>
goto <t>;
<t>
%u = i64 2 + i64 2;
goto <0x1002>;
"
);
let removed_insns = [orphan, t]
.into_iter()
.flat_map(|block| BasicBlock::from_id(&ctx, block).instruction_ids())
.collect::<Vec<_>>();
simplify_cfg(&mut ctx, f);
assert!(
!ctx.contains_block(orphan),
"unreachable forwarding block should be pruned"
);
assert!(
!ctx.contains_block(t),
"block reachable only from an unreachable block should be pruned too"
);
assert!(
removed_insns
.into_iter()
.all(|insn| !ctx.contains_instruction(insn)),
"instructions owned by pruned blocks should be removed"
);
}
#[test]
fn folds_cbranch_with_identical_arms() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8>
if @cond goto <t> else goto <t>;
<d>
goto <t>;
<t>
%s = i64 1 + i64 1;
goto <0x1001>;
"
);
try_fold_cbranch_in(&mut ctx, a);
let term = BasicBlock::from_id(&ctx, a)
.iter()
.last()
.expect("a has a terminator");
let Mnemonic::Branch(br) = term.mnemonic() else {
panic!("identical-armed cbranch should fold to an unconditional branch");
};
assert_eq!(br.target, t.local);
assert_eq!(
BasicBlock::from_id(&ctx, a).successors().count(),
1,
"the duplicate parallel edge should be dropped"
);
}
#[test]
fn does_not_fold_cbranch_with_differing_args() {
let mut ctx = make_ctx();
qcode!(
ctx,
"
fn f:
<a @cond:i8 @p:i64 @q:i64>
if @cond goto <t @y=@p> else goto <t @y=@q>;
<t @y:i64>
%s = @y + 1;
goto <0x1001>;
"
);
simplify_cfg(&mut ctx, f);
let term = BasicBlock::from_id(&ctx, a)
.iter()
.last()
.expect("a has a terminator");
assert!(
matches!(term.mnemonic(), Mnemonic::CBranch(_)),
"a cbranch with differing arm arguments must not be folded"
);
}
}