use std::collections::BTreeMap;
use crate::{
analysis::{
cfg::SsaCfg,
verifier::{SsaVerifier, VerifierError, VerifyLevel},
},
error::{Error, Result},
graph::{
algorithms::{compute_dominators, DominatorTree},
NodeId, RootedGraph,
},
ir::{
block::{ReplaceResult, SsaBlock},
function::{CopyPropagationResult, SsaFunction},
instruction::SsaInstruction,
ops::SsaOp,
phi::{PhiNode, PhiOperand},
variable::{DefSite, SsaVarId, UseSite, VariableOrigin},
},
target::Target,
BitSet,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SsaEditScope {
None,
UsesOnly,
InstructionsOnly,
StructuredCfg,
CfgModifying,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SsaRollbackPolicy {
Never,
OnFailure,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SsaEditOptions {
pub verify: bool,
pub rollback: SsaRollbackPolicy,
}
impl SsaEditOptions {
#[must_use]
pub const fn new() -> Self {
Self {
verify: false,
rollback: SsaRollbackPolicy::Never,
}
}
#[must_use]
pub const fn with_verify(mut self, verify: bool) -> Self {
self.verify = verify;
self
}
#[must_use]
pub const fn with_rollback(mut self, rollback: SsaRollbackPolicy) -> Self {
self.rollback = rollback;
self
}
}
impl Default for SsaEditOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SsaEditReport {
pub changed: bool,
pub scope: SsaEditScope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplacementSkipReason {
SameVariable,
MissingOldVariable,
MissingNewVariable,
TypeMismatch,
UseNotFound,
SelfReference,
SameBlockUseBeforeDef,
DominanceViolation,
PhiOperand,
MissingInstruction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedReplacement {
pub site: UseSite,
pub reason: ReplacementSkipReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckedReplaceResult {
pub replaced: usize,
pub skipped: Vec<SkippedReplacement>,
}
impl CheckedReplaceResult {
#[must_use]
pub fn is_complete(&self) -> bool {
self.skipped.is_empty()
}
#[must_use]
pub fn as_replace_result(&self) -> ReplaceResult {
ReplaceResult {
replaced: self.replaced,
skipped: self.skipped.len(),
}
}
}
pub struct SsaEditor<'a, T: Target> {
ssa: &'a mut SsaFunction<T>,
changed: bool,
scope: SsaEditScope,
}
impl<T: Target> SsaFunction<T> {
pub fn edit<F>(&mut self, options: SsaEditOptions, edit: F) -> Result<SsaEditReport>
where
F: FnOnce(&mut SsaEditor<T>) -> Result<()>,
{
let original = if matches!(options.rollback, SsaRollbackPolicy::OnFailure) {
Some(self.clone())
} else {
None
};
let edit_result = {
let mut editor = SsaEditor {
ssa: self,
changed: false,
scope: SsaEditScope::None,
};
let result = edit(&mut editor);
result.map(|_| SsaEditReport {
changed: editor.changed,
scope: editor.scope,
})
};
let report = match edit_result {
Ok(report) => report,
Err(error) => {
restore_on_failure(self, original);
return Err(error);
}
};
if report.changed {
if let Err(error) = finish_edit_scope(self, report.scope) {
restore_on_failure(self, original);
return Err(error);
}
}
if options.verify {
let errors = SsaVerifier::new(self).verify(VerifyLevel::Standard);
if !errors.is_empty() {
restore_on_failure(self, original);
return Err(Error::new(format_verifier_errors(&errors)));
}
}
Ok(report)
}
#[must_use]
pub fn replace_uses_checked(
&mut self,
old_var: SsaVarId,
new_var: SsaVarId,
) -> CheckedReplaceResult {
let needs_dominance = self
.variable(new_var)
.map(|new| {
let def_block = new.def_site().block;
self.blocks().iter().enumerate().any(|(block_idx, block)| {
block_idx != def_block
&& block
.instructions()
.iter()
.any(|instr| instr.op().uses_var(old_var))
})
})
.unwrap_or(false);
let dominators = if needs_dominance && self.block_count() > 0 {
let cfg = SsaCfg::from_ssa(self);
Some(compute_dominators(&cfg, cfg.entry()))
} else {
None
};
self.replace_uses_checked_with(old_var, new_var, dominators.as_ref())
}
#[must_use]
pub(in crate::ir::function) fn replace_uses_checked_with(
&mut self,
old_var: SsaVarId,
new_var: SsaVarId,
dominators: Option<&DominatorTree>,
) -> CheckedReplaceResult {
let mut allowed = Vec::new();
let mut skipped = Vec::new();
for (block_idx, block) in self.blocks().iter().enumerate() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if !instr.op().uses_var(old_var) {
continue;
}
match can_replace_instruction_use_with_dominators(
self, old_var, new_var, block_idx, instr_idx, dominators,
) {
Ok(()) => allowed.push((block_idx, instr_idx)),
Err(reason) => skipped.push(SkippedReplacement {
site: UseSite::instruction(block_idx, instr_idx),
reason,
}),
}
}
}
let mut replaced = 0usize;
for (block_idx, instr_idx) in allowed {
if let Some(instr) = self
.block_mut(block_idx)
.and_then(|block| block.instruction_mut(instr_idx))
{
replaced = replaced.saturating_add(instr.op_mut().replace_uses(old_var, new_var));
}
}
CheckedReplaceResult { replaced, skipped }
}
pub fn can_replace_instruction_use(
&self,
old_var: SsaVarId,
new_var: SsaVarId,
block_idx: usize,
instr_idx: usize,
) -> std::result::Result<(), ReplacementSkipReason> {
let needs_dominance = self
.variable(new_var)
.is_some_and(|new| new.def_site().block != block_idx);
let dominators = if needs_dominance && self.block_count() > 0 {
let cfg = SsaCfg::from_ssa(self);
Some(compute_dominators(&cfg, cfg.entry()))
} else {
None
};
can_replace_instruction_use_with_dominators(
self,
old_var,
new_var,
block_idx,
instr_idx,
dominators.as_ref(),
)
}
}
impl<'a, T: Target> SsaEditor<'a, T> {
#[must_use]
pub const fn function(&self) -> &SsaFunction<T> {
self.ssa
}
#[must_use]
pub const fn changed(&self) -> bool {
self.changed
}
#[must_use]
pub const fn scope(&self) -> SsaEditScope {
self.scope
}
pub fn replace_uses_checked(
&mut self,
old_var: SsaVarId,
new_var: SsaVarId,
) -> CheckedReplaceResult {
let result = self.ssa.replace_uses_checked(old_var, new_var);
if result.replaced > 0 {
self.mark_changed(SsaEditScope::UsesOnly);
}
result
}
pub fn propagate_copies(
&mut self,
copies: &BTreeMap<SsaVarId, SsaVarId>,
) -> CopyPropagationResult {
let result = self.ssa.propagate_copies(copies);
if result.total_replaced > 0 {
self.mark_changed(SsaEditScope::UsesOnly);
}
result
}
pub fn nop_copy_defining(&mut self, dest: SsaVarId) -> bool {
let changed = self.ssa.nop_copy_defining(dest);
if changed {
self.mark_changed(SsaEditScope::InstructionsOnly);
}
changed
}
pub fn replace_instruction_op(
&mut self,
block_idx: usize,
instr_idx: usize,
new_op: SsaOp<T>,
) -> Result<()> {
let defs: Vec<SsaVarId> = new_op.defs().collect();
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let Some(instr) = block.instruction_mut(instr_idx) else {
return Err(Error::new(format!(
"missing instruction B{block_idx}:I{instr_idx}"
)));
};
instr.set_op(new_op);
for dest in defs {
if let Some(var) = self.ssa.variable_mut(dest) {
var.set_def_site(DefSite::instruction(block_idx, instr_idx));
}
}
self.mark_changed(SsaEditScope::InstructionsOnly);
Ok(())
}
pub fn nop_instruction(&mut self, block_idx: usize, instr_idx: usize) -> Result<()> {
self.replace_instruction_op(block_idx, instr_idx, SsaOp::Nop)
}
pub fn replace_terminator_op(&mut self, block_idx: usize, new_op: SsaOp<T>) -> Result<()> {
let Some(block) = self.ssa.block(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let Some(instr_idx) = block.instructions().len().checked_sub(1) else {
return Err(Error::new(format!("block B{block_idx} has no terminator")));
};
self.replace_instruction_op(block_idx, instr_idx, new_op)?;
self.mark_changed(SsaEditScope::CfgModifying);
Ok(())
}
pub fn remove_instruction_tail(&mut self, block_idx: usize, start_idx: usize) -> Result<usize> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let original_len = block.instructions().len();
if start_idx >= original_len {
return Ok(0);
}
block.instructions_mut().truncate(start_idx);
let removed = original_len.saturating_sub(start_idx);
if removed > 0 {
self.mark_changed(SsaEditScope::InstructionsOnly);
}
Ok(removed)
}
pub fn clear_block(&mut self, block_idx: usize) -> Result<bool> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
if block.is_empty() {
return Ok(false);
}
block.clear();
self.mark_changed(SsaEditScope::CfgModifying);
Ok(true)
}
pub fn redirect_terminator_target(
&mut self,
block_idx: usize,
old_target: usize,
new_target: usize,
) -> Result<bool> {
let Some(mut op) = self
.ssa
.block(block_idx)
.and_then(|block| block.instructions().last())
.map(|instr| instr.op().clone())
else {
return Err(Error::new(format!("block B{block_idx} has no terminator")));
};
if !op.redirect_target(old_target, new_target) {
return Ok(false);
}
self.replace_terminator_op(block_idx, op)?;
Ok(true)
}
pub fn redirect_terminator_target_structured(
&mut self,
block_idx: usize,
old_target: usize,
new_target: usize,
) -> Result<bool> {
let Some(mut op) = self
.ssa
.block(block_idx)
.and_then(|block| block.instructions().last())
.map(|instr| instr.op().clone())
else {
return Err(Error::new(format!("block B{block_idx} has no terminator")));
};
if !op.redirect_target(old_target, new_target) {
return Ok(false);
}
let instr_idx = self
.ssa
.block(block_idx)
.and_then(|block| block.instructions().len().checked_sub(1))
.ok_or_else(|| Error::new(format!("block B{block_idx} has no terminator")))?;
self.replace_instruction_op(block_idx, instr_idx, op)?;
self.mark_changed(SsaEditScope::StructuredCfg);
Ok(true)
}
pub fn expand_phi_predecessor(
&mut self,
block_idx: usize,
old_pred: usize,
new_preds: &[usize],
) -> Result<usize> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let Some((&first_pred, extra_preds)) = new_preds.split_first() else {
return Ok(0);
};
let mut updated = 0usize;
for phi in block.phi_nodes_mut() {
let Some(value) = phi
.operands()
.iter()
.find(|operand| operand.predecessor() == old_pred)
.map(|operand| operand.value())
else {
continue;
};
if let Some(operand) = phi
.operands_mut()
.iter_mut()
.find(|operand| operand.predecessor() == old_pred)
{
operand.set_predecessor(first_pred);
updated = updated.saturating_add(1);
}
for &pred in extra_preds {
phi.add_operand(PhiOperand::new(value, pred));
updated = updated.saturating_add(1);
}
}
if updated > 0 {
self.mark_changed(SsaEditScope::CfgModifying);
}
Ok(updated)
}
pub fn replace_phi_predecessor(
&mut self,
block_idx: usize,
old_pred: usize,
new_pred: usize,
) -> Result<usize> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let mut updated = 0usize;
for phi in block.phi_nodes_mut() {
for operand in phi.operands_mut() {
if operand.predecessor() == old_pred {
operand.set_predecessor(new_pred);
updated = updated.saturating_add(1);
}
}
}
if updated > 0 {
self.mark_changed(SsaEditScope::CfgModifying);
}
Ok(updated)
}
pub fn coalesce_unconditional_successor(
&mut self,
predecessor_idx: usize,
successor_idx: usize,
) -> Result<bool> {
let Some(predecessor) = self.ssa.block(predecessor_idx) else {
return Err(Error::new(format!("missing block B{predecessor_idx}")));
};
if !matches!(
predecessor.terminator_op(),
Some(SsaOp::Jump { target }) if *target == successor_idx
) {
return Err(Error::new(format!(
"block B{predecessor_idx} does not jump to B{successor_idx}"
)));
}
let Some(successor) = self.ssa.block(successor_idx) else {
return Err(Error::new(format!("missing block B{successor_idx}")));
};
if successor.instructions().is_empty() {
return Ok(false);
}
let phi_copies: Vec<SsaInstruction<T>> = successor
.phi_nodes()
.iter()
.filter_map(|phi| {
let operand = phi.operands().first()?;
let dest = phi.result();
let src = operand.value();
if dest == src {
return None;
}
Some(SsaInstruction::synthetic(SsaOp::Copy { dest, src }))
})
.collect();
let successor_instrs = successor.instructions().to_vec();
let Some(predecessor) = self.ssa.block_mut(predecessor_idx) else {
return Err(Error::new(format!("missing block B{predecessor_idx}")));
};
let instrs = predecessor.instructions_mut();
instrs.pop();
instrs.extend(phi_copies);
instrs.extend(successor_instrs);
for instr in instrs {
instr
.op_mut()
.redirect_target(successor_idx, predecessor_idx);
}
self.clear_block(successor_idx)?;
let block_count = self.ssa.block_count();
for block_idx in 0..block_count {
if block_idx == predecessor_idx || block_idx == successor_idx {
continue;
}
self.replace_phi_predecessor(block_idx, successor_idx, predecessor_idx)?;
}
self.mark_changed(SsaEditScope::CfgModifying);
Ok(true)
}
pub fn append_block(&mut self, block: SsaBlock<T>) -> Result<usize> {
let block_idx = self.ssa.block_count();
if block.id() != block_idx {
return Err(Error::new(format!(
"new block id B{} does not match next block B{block_idx}",
block.id()
)));
}
self.ssa.add_block(block);
self.mark_changed(SsaEditScope::StructuredCfg);
Ok(block_idx)
}
#[must_use]
pub fn create_variable_for_origin(
&mut self,
origin: VariableOrigin,
version: u32,
def_site: DefSite,
) -> SsaVarId {
self.ssa
.create_variable_for_origin(origin, version, def_site)
}
pub fn add_phi(&mut self, block_idx: usize, phi: PhiNode) -> Result<usize> {
let result = phi.result();
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let phi_idx = block.phi_nodes().len();
block.phi_nodes_mut().push(phi);
if let Some(var) = self.ssa.variable_mut(result) {
var.set_def_site(DefSite::phi(block_idx));
}
self.mark_changed(SsaEditScope::StructuredCfg);
Ok(phi_idx)
}
pub fn replace_phi_predecessor_group(
&mut self,
block_idx: usize,
old_preds: &[usize],
new_operand: PhiOperand,
) -> Result<bool> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let mut changed = false;
for phi in block.phi_nodes_mut() {
let before = phi.operands().len();
phi.operands_mut()
.retain(|operand| !old_preds.contains(&operand.predecessor()));
if phi.operands().len() != before {
phi.add_operand(new_operand);
changed = true;
}
}
if changed {
self.mark_changed(SsaEditScope::StructuredCfg);
}
Ok(changed)
}
pub fn replace_phi_predecessor_group_for_origin(
&mut self,
block_idx: usize,
origin: VariableOrigin,
old_preds: &[usize],
new_operand: PhiOperand,
) -> Result<bool> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let mut changed = false;
for phi in block.phi_nodes_mut() {
if phi.origin() != origin {
continue;
}
let before = phi.operands().len();
phi.operands_mut()
.retain(|operand| !old_preds.contains(&operand.predecessor()));
if phi.operands().len() != before {
phi.add_operand(new_operand);
changed = true;
}
}
if changed {
self.mark_changed(SsaEditScope::StructuredCfg);
}
Ok(changed)
}
#[must_use]
pub fn prune_phi_operands(&mut self, reachable: &BitSet) -> usize {
let pruned = self.ssa.prune_phi_operands(reachable);
if pruned > 0 {
self.mark_changed(SsaEditScope::UsesOnly);
}
pruned
}
pub fn simplify_phi_to_copy(
&mut self,
block_idx: usize,
phi_idx: usize,
source: SsaVarId,
) -> Result<()> {
if self
.ssa
.block(block_idx)
.and_then(|block| block.phi_nodes().get(phi_idx))
.is_none()
{
return Err(Error::new(format!("missing phi B{block_idx}:P{phi_idx}")));
}
if self.ssa.simplify_phi_to_copy(block_idx, phi_idx, source) {
self.mark_changed(SsaEditScope::InstructionsOnly);
Ok(())
} else {
Err(Error::new(format!(
"failed to simplify phi B{block_idx}:P{phi_idx}"
)))
}
}
pub fn insert_before_terminator(
&mut self,
block_idx: usize,
instr: SsaInstruction<T>,
) -> Result<usize> {
let defs: Vec<SsaVarId> = instr.defs().collect();
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let insert_idx = block
.instructions()
.iter()
.position(SsaInstruction::is_terminator)
.unwrap_or_else(|| block.instructions().len());
block.instructions_mut().insert(insert_idx, instr);
for dest in defs {
if let Some(var) = self.ssa.variable_mut(dest) {
var.set_def_site(DefSite::instruction(block_idx, insert_idx));
}
}
self.mark_changed(SsaEditScope::InstructionsOnly);
Ok(insert_idx)
}
pub fn insert_instruction(
&mut self,
block_idx: usize,
instr_idx: usize,
instr: SsaInstruction<T>,
) -> Result<usize> {
let defs: Vec<SsaVarId> = instr.defs().collect();
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
let insert_idx = instr_idx.min(block.instructions().len());
block.instructions_mut().insert(insert_idx, instr);
for dest in defs {
if let Some(var) = self.ssa.variable_mut(dest) {
var.set_def_site(DefSite::instruction(block_idx, insert_idx));
}
}
self.mark_changed(SsaEditScope::InstructionsOnly);
Ok(insert_idx)
}
pub fn remove_phi(&mut self, block_idx: usize, phi_idx: usize) -> Result<()> {
let Some(block) = self.ssa.block_mut(block_idx) else {
return Err(Error::new(format!("missing block B{block_idx}")));
};
if phi_idx >= block.phi_nodes().len() {
return Err(Error::new(format!("missing phi B{block_idx}:P{phi_idx}")));
}
block.phi_nodes_mut().remove(phi_idx);
self.mark_changed(SsaEditScope::InstructionsOnly);
Ok(())
}
pub fn mark_cfg_changed(&mut self) {
self.mark_changed(SsaEditScope::CfgModifying);
}
fn mark_changed(&mut self, scope: SsaEditScope) {
self.changed = true;
self.scope = self.scope.max(scope);
}
}
fn restore_on_failure<T: Target>(ssa: &mut SsaFunction<T>, original: Option<SsaFunction<T>>) {
if let Some(original) = original {
*ssa = original;
}
}
fn finish_edit_scope<T: Target>(ssa: &mut SsaFunction<T>, scope: SsaEditScope) -> Result<()> {
match scope {
SsaEditScope::None => {}
SsaEditScope::UsesOnly => ssa.recompute_uses(),
SsaEditScope::InstructionsOnly => ssa.repair_ssa(),
SsaEditScope::StructuredCfg | SsaEditScope::CfgModifying => ssa.rebuild_ssa()?,
}
Ok(())
}
fn format_verifier_errors(errors: &[VerifierError]) -> String {
let details = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
format!("checked SSA edit produced invalid SSA: {details}")
}
fn types_compatible<T: Target>(old_type: &T::Type, new_type: &T::Type) -> bool {
old_type == new_type || T::is_unknown(old_type) || T::is_unknown(new_type)
}
fn can_replace_instruction_use_with_dominators<T: Target>(
ssa: &SsaFunction<T>,
old_var: SsaVarId,
new_var: SsaVarId,
block_idx: usize,
instr_idx: usize,
dominators: Option<&DominatorTree>,
) -> std::result::Result<(), ReplacementSkipReason> {
if old_var == new_var {
return Err(ReplacementSkipReason::SameVariable);
}
let Some(old) = ssa.variable(old_var) else {
return Err(ReplacementSkipReason::MissingOldVariable);
};
let Some(new) = ssa.variable(new_var) else {
return Err(ReplacementSkipReason::MissingNewVariable);
};
if !types_compatible::<T>(old.var_type(), new.var_type()) {
return Err(ReplacementSkipReason::TypeMismatch);
}
let Some(instr) = ssa
.block(block_idx)
.and_then(|block| block.instruction(instr_idx))
else {
return Err(ReplacementSkipReason::MissingInstruction);
};
if !instr.op().uses_var(old_var) {
return Err(ReplacementSkipReason::UseNotFound);
}
if instr.op().defs().any(|def| def == new_var) {
return Err(ReplacementSkipReason::SelfReference);
}
let def_site = new.def_site();
if def_site.block == block_idx {
if def_site
.instruction
.is_some_and(|def_idx| def_idx >= instr_idx)
{
return Err(ReplacementSkipReason::SameBlockUseBeforeDef);
}
return Ok(());
}
if definition_dominates_block(ssa, def_site, block_idx, dominators) {
Ok(())
} else {
Err(ReplacementSkipReason::DominanceViolation)
}
}
fn definition_dominates_block<T: Target>(
ssa: &SsaFunction<T>,
def_site: DefSite,
use_block: usize,
dominators: Option<&DominatorTree>,
) -> bool {
if def_site.block == use_block {
return true;
}
if def_site.block >= ssa.block_count() || use_block >= ssa.block_count() {
return false;
}
dominators
.is_some_and(|tree| tree.dominates(NodeId::new(def_site.block), NodeId::new(use_block)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ir::{
block::SsaBlock,
instruction::SsaInstruction,
ops::SsaOp,
value::ConstValue,
variable::{DefSite, VariableOrigin},
},
testing::{MockTarget, MockType},
};
fn var(index: usize) -> SsaVarId {
SsaVarId::from_index(index)
}
fn same_block_late_def_function() -> SsaFunction<MockTarget> {
let mut ssa = SsaFunction::new(0, 0);
let v0 = ssa.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::I32,
);
let v1 = ssa.create_variable(
VariableOrigin::Local(1),
0,
DefSite::instruction(0, 1),
MockType::I32,
);
let v2 = ssa.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(0, 3),
MockType::I32,
);
let v3 = ssa.create_variable(
VariableOrigin::Local(3),
0,
DefSite::instruction(0, 2),
MockType::I32,
);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v0,
value: ConstValue::I32(1),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Copy { dest: v1, src: v0 }));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: v3,
left: v1,
right: v1,
flags: None,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: v2,
value: ConstValue::I32(2),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v3) }));
ssa.add_block(block);
ssa.recompute_uses();
ssa
}
fn non_dominating_definition_function() -> SsaFunction<MockTarget> {
let mut ssa = SsaFunction::new(0, 0);
let cond = ssa.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::I32,
);
let branch_value = ssa.create_variable(
VariableOrigin::Local(1),
0,
DefSite::instruction(1, 0),
MockType::I32,
);
let old = ssa.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(2, 0),
MockType::I32,
);
let dest = ssa.create_variable(
VariableOrigin::Local(3),
0,
DefSite::instruction(2, 1),
MockType::I32,
);
let mut entry = SsaBlock::new(0);
entry.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: cond,
value: ConstValue::I32(1),
}));
entry.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
let mut left = SsaBlock::new(1);
left.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: branch_value,
value: ConstValue::I32(10),
}));
left.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
let mut right = SsaBlock::new(2);
right.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: old,
value: ConstValue::I32(20),
}));
right.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest,
left: old,
right: old,
flags: None,
}));
right.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
let mut exit = SsaBlock::new(3);
exit.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(entry);
ssa.add_block(left);
ssa.add_block(right);
ssa.add_block(exit);
ssa.recompute_uses();
ssa
}
fn type_mismatch_function() -> SsaFunction<MockTarget> {
let mut ssa = SsaFunction::new(0, 0);
let old = ssa.create_variable(
VariableOrigin::Local(0),
0,
DefSite::instruction(0, 0),
MockType::I32,
);
let new = ssa.create_variable(
VariableOrigin::Local(1),
0,
DefSite::instruction(0, 1),
MockType::I64,
);
let dest = ssa.create_variable(
VariableOrigin::Local(2),
0,
DefSite::instruction(0, 2),
MockType::I32,
);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: old,
value: ConstValue::I32(1),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: new,
value: ConstValue::I64(2),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest,
left: old,
right: old,
flags: None,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(dest),
}));
ssa.add_block(block);
ssa.recompute_uses();
ssa
}
#[test]
fn checked_replacement_rejects_same_block_late_definition() {
let mut ssa = same_block_late_def_function();
let result = ssa.replace_uses_checked(var(1), var(2));
assert_eq!(result.replaced, 0);
assert_eq!(result.skipped.len(), 1);
assert_eq!(
result.skipped[0].reason,
ReplacementSkipReason::SameBlockUseBeforeDef
);
}
#[test]
fn checked_replacement_rejects_self_reference() {
let mut ssa = same_block_late_def_function();
let result = ssa.replace_uses_checked(var(1), var(3));
assert_eq!(result.replaced, 0);
assert_eq!(result.skipped.len(), 1);
assert_eq!(
result.skipped[0].reason,
ReplacementSkipReason::SelfReference
);
}
#[test]
fn checked_replacement_rejects_non_dominating_definition() {
let mut ssa = non_dominating_definition_function();
let result = ssa.replace_uses_checked(var(2), var(1));
assert_eq!(result.replaced, 0);
assert_eq!(result.skipped.len(), 1);
assert_eq!(
result.skipped[0].reason,
ReplacementSkipReason::DominanceViolation
);
}
#[test]
fn checked_replacement_rejects_type_mismatch() {
let mut ssa = type_mismatch_function();
let result = ssa.replace_uses_checked(var(0), var(1));
assert_eq!(result.replaced, 0);
assert_eq!(result.skipped.len(), 1);
assert_eq!(
result.skipped[0].reason,
ReplacementSkipReason::TypeMismatch
);
}
#[test]
fn editor_repairs_once_after_instruction_edits() {
let mut ssa = same_block_late_def_function();
let report = ssa
.edit(SsaEditOptions::new(), |editor| {
editor.nop_instruction(0, 1)?;
Ok(())
})
.unwrap();
assert!(report.changed);
assert_eq!(report.scope, SsaEditScope::InstructionsOnly);
assert!(ssa
.block(0)
.unwrap()
.instructions()
.iter()
.all(|instr| !matches!(instr.op(), SsaOp::Nop)));
}
#[test]
fn editor_rolls_back_when_verification_fails() {
let mut ssa = same_block_late_def_function();
let original = ssa.clone();
let result = ssa.edit(
SsaEditOptions::new()
.with_verify(true)
.with_rollback(SsaRollbackPolicy::OnFailure),
|editor| {
editor.replace_instruction_op(
0,
4,
SsaOp::Return {
value: Some(SsaVarId::from_index(999)),
},
)?;
Ok(())
},
);
assert!(result.is_err());
assert_eq!(ssa.variable_count(), original.variable_count());
assert_eq!(
ssa.block(0).unwrap().instructions().len(),
original.block(0).unwrap().instructions().len()
);
assert_eq!(
ssa.block(0).unwrap().instructions()[4].uses(),
original.block(0).unwrap().instructions()[4].uses()
);
}
}