#![allow(dead_code)]
use crate::{
Allocation, AllocationKind, Block, Edit, Function, Inst, InstOrEdit, InstPosition, MachineEnv,
Operand, OperandConstraint, OperandKind, OperandPos, Output, PReg, PRegSet, VReg,
};
use fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
use std::default::Default;
use std::hash::Hash;
use std::result::Result;
#[derive(Clone, Debug)]
pub struct CheckerErrors {
errors: Vec<CheckerError>,
}
#[derive(Clone, Debug)]
pub enum CheckerError {
MissingAllocation {
inst: Inst,
op: Operand,
},
UnknownValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
ConflictedValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
IncorrectValuesInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
actual: FxHashSet<VReg>,
},
ConstraintViolated {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotFixedReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReuse {
inst: Inst,
op: Operand,
alloc: Allocation,
expected_alloc: Allocation,
},
AllocationIsNotStack {
inst: Inst,
op: Operand,
alloc: Allocation,
},
ConflictedValueInStackmap {
inst: Inst,
alloc: Allocation,
},
NonRefValuesInStackmap {
inst: Inst,
alloc: Allocation,
vregs: FxHashSet<VReg>,
},
StackToStackMove {
into: Allocation,
from: Allocation,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum CheckerValue {
Universe,
VRegs(FxHashSet<VReg>),
}
impl CheckerValue {
fn vregs(&self) -> Option<&FxHashSet<VReg>> {
match self {
CheckerValue::Universe => None,
CheckerValue::VRegs(vregs) => Some(vregs),
}
}
fn vregs_mut(&mut self) -> Option<&mut FxHashSet<VReg>> {
match self {
CheckerValue::Universe => None,
CheckerValue::VRegs(vregs) => Some(vregs),
}
}
}
impl Default for CheckerValue {
fn default() -> CheckerValue {
CheckerValue::Universe
}
}
impl CheckerValue {
fn meet_with(&mut self, other: &CheckerValue) {
match (self, other) {
(_, CheckerValue::Universe) => {
}
(this @ CheckerValue::Universe, _) => {
*this = other.clone();
}
(CheckerValue::VRegs(my_vregs), CheckerValue::VRegs(other_vregs)) => {
my_vregs.retain(|vreg| other_vregs.contains(vreg));
}
}
}
fn from_reg(reg: VReg) -> CheckerValue {
CheckerValue::VRegs(std::iter::once(reg).collect())
}
fn remove_vreg(&mut self, reg: VReg) {
match self {
CheckerValue::Universe => {
panic!("Cannot remove VReg from Universe set (we do not have the full list of vregs available");
}
CheckerValue::VRegs(vregs) => {
vregs.remove(®);
}
}
}
fn copy_vreg(&mut self, src: VReg, dst: VReg) {
match self {
CheckerValue::Universe => {
}
CheckerValue::VRegs(vregs) => {
if vregs.contains(&src) {
vregs.insert(dst);
}
}
}
}
fn empty() -> CheckerValue {
CheckerValue::VRegs(FxHashSet::default())
}
}
fn visit_all_vregs<F: Function, V: FnMut(VReg)>(f: &F, mut v: V) {
for block in 0..f.num_blocks() {
let block = Block::new(block);
for inst in f.block_insns(block).iter() {
for op in f.inst_operands(inst) {
v(op.vreg());
}
if let Some((src, dst)) = f.is_move(inst) {
v(src.vreg());
v(dst.vreg());
}
if f.is_branch(inst) {
for succ_idx in 0..f.block_succs(block).len() {
for ¶m in f.branch_blockparams(block, inst, succ_idx) {
v(param);
}
}
}
}
for &vreg in f.block_params(block) {
v(vreg);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum CheckerState {
Top,
Allocations(FxHashMap<Allocation, CheckerValue>),
}
impl CheckerState {
fn get_value(&self, alloc: &Allocation) -> Option<&CheckerValue> {
match self {
CheckerState::Top => None,
CheckerState::Allocations(allocs) => allocs.get(alloc),
}
}
fn get_values_mut(&mut self) -> impl Iterator<Item = &mut CheckerValue> {
match self {
CheckerState::Top => panic!("Cannot get mutable values iterator on Top state"),
CheckerState::Allocations(allocs) => allocs.values_mut(),
}
}
fn get_mappings(&self) -> impl Iterator<Item = (&Allocation, &CheckerValue)> {
match self {
CheckerState::Top => panic!("Cannot get mappings iterator on Top state"),
CheckerState::Allocations(allocs) => allocs.iter(),
}
}
fn get_mappings_mut(&mut self) -> impl Iterator<Item = (&Allocation, &mut CheckerValue)> {
match self {
CheckerState::Top => panic!("Cannot get mutable mappings iterator on Top state"),
CheckerState::Allocations(allocs) => allocs.iter_mut(),
}
}
fn become_defined(&mut self) {
match self {
CheckerState::Top => *self = CheckerState::Allocations(FxHashMap::default()),
_ => {}
}
}
fn set_value(&mut self, alloc: Allocation, value: CheckerValue) {
match self {
CheckerState::Top => {
panic!("Cannot set value on Top state");
}
CheckerState::Allocations(allocs) => {
allocs.insert(alloc, value);
}
}
}
fn copy_vreg(&mut self, src: VReg, dst: VReg) {
match self {
CheckerState::Top => {
}
CheckerState::Allocations(allocs) => {
for value in allocs.values_mut() {
value.copy_vreg(src, dst);
}
}
}
}
fn remove_value(&mut self, alloc: &Allocation) {
match self {
CheckerState::Top => {
panic!("Cannot remove value on Top state");
}
CheckerState::Allocations(allocs) => {
allocs.remove(alloc);
}
}
}
fn initial_with_pinned_vregs<F: Function>(f: &F) -> CheckerState {
let mut pinned_vregs: FxHashMap<VReg, PReg> = FxHashMap::default();
visit_all_vregs(f, |vreg: VReg| {
if let Some(preg) = f.is_pinned_vreg(vreg) {
pinned_vregs.insert(vreg, preg);
}
});
let mut allocs = FxHashMap::default();
for (vreg, preg) in pinned_vregs {
allocs.insert(
Allocation::reg(preg),
CheckerValue::VRegs(std::iter::once(vreg).collect()),
);
}
CheckerState::Allocations(allocs)
}
}
impl Default for CheckerState {
fn default() -> CheckerState {
CheckerState::Top
}
}
impl std::fmt::Display for CheckerValue {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CheckerValue::Universe => {
write!(f, "top")
}
CheckerValue::VRegs(vregs) => {
write!(f, "{{ ")?;
for vreg in vregs {
write!(f, "{} ", vreg)?;
}
write!(f, "}}")?;
Ok(())
}
}
}
}
fn merge_map<K: Copy + Clone + PartialEq + Eq + Hash>(
into: &mut FxHashMap<K, CheckerValue>,
from: &FxHashMap<K, CheckerValue>,
) {
into.retain(|k, _| from.contains_key(k));
for (k, into_v) in into.iter_mut() {
let from_v = from.get(k).unwrap();
into_v.meet_with(from_v);
}
}
impl CheckerState {
fn new() -> CheckerState {
Default::default()
}
fn meet_with(&mut self, other: &CheckerState) {
match (self, other) {
(_, CheckerState::Top) => {
}
(this @ CheckerState::Top, _) => {
*this = other.clone();
}
(
CheckerState::Allocations(my_allocations),
CheckerState::Allocations(other_allocations),
) => {
merge_map(my_allocations, other_allocations);
}
}
}
fn check_val<'a, F: Function>(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
val: &CheckerValue,
allocs: &[Allocation],
checker: &Checker<'a, F>,
) -> Result<(), CheckerError> {
if alloc == Allocation::none() {
return Err(CheckerError::MissingAllocation { inst, op });
}
if op.as_fixed_nonallocatable().is_none() {
match val {
CheckerValue::Universe => {
return Err(CheckerError::UnknownValueInAllocation { inst, op, alloc });
}
CheckerValue::VRegs(vregs) if !vregs.contains(&op.vreg()) => {
return Err(CheckerError::IncorrectValuesInAllocation {
inst,
op,
alloc,
actual: vregs.clone(),
});
}
_ => {}
}
}
self.check_constraint(inst, op, alloc, allocs, checker)?;
Ok(())
}
fn check<'a, F: Function>(
&self,
pos: InstPosition,
checkinst: &CheckerInst,
checker: &Checker<'a, F>,
) -> Result<(), CheckerError> {
let default_val = Default::default();
match checkinst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
..
} => {
let has_reused_input = operands
.iter()
.any(|op| matches!(op.constraint(), OperandConstraint::Reuse(_)));
if has_reused_input && pos == InstPosition::After {
return Ok(());
}
for (op, alloc) in operands.iter().zip(allocs.iter()) {
let is_here = match (op.pos(), pos) {
(OperandPos::Early, InstPosition::Before) => true,
(OperandPos::Late, InstPosition::After) => true,
_ => false,
};
if !is_here {
continue;
}
if op.kind() == OperandKind::Def {
continue;
}
let val = self.get_value(alloc).unwrap_or(&default_val);
trace!(
"checker: checkinst {:?}: op {:?}, alloc {:?}, checker value {:?}",
checkinst,
op,
alloc,
val
);
self.check_val(inst, *op, *alloc, val, allocs, checker)?;
}
}
&CheckerInst::Safepoint { inst, ref allocs } => {
for &alloc in allocs {
let val = self.get_value(&alloc).unwrap_or(&default_val);
trace!(
"checker: checkinst {:?}: safepoint slot {}, checker value {:?}",
checkinst,
alloc,
val
);
let reffy = val
.vregs()
.expect("checker value should not be Universe set")
.iter()
.any(|vreg| checker.reftyped_vregs.contains(vreg));
if !reffy {
return Err(CheckerError::NonRefValuesInStackmap {
inst,
alloc,
vregs: val.vregs().unwrap().clone(),
});
}
}
}
&CheckerInst::Move { into, from } => {
let is_stack = |alloc: Allocation| {
if let Some(reg) = alloc.as_reg() {
checker.stack_pregs.contains(reg)
} else {
alloc.is_stack()
}
};
if is_stack(into) && is_stack(from) {
return Err(CheckerError::StackToStackMove { into, from });
}
}
&CheckerInst::ParallelMove { .. } => {
}
&CheckerInst::ProgramMove { inst, src, dst: _ } => {
if let OperandConstraint::FixedReg(preg) = src.constraint() {
let alloc = Allocation::reg(preg);
let val = self.get_value(&alloc).unwrap_or(&default_val);
trace!(
"checker: checkinst {:?}: cheker value in {:?} is {:?}",
checkinst,
alloc,
val
);
self.check_val(inst, src, alloc, val, &[alloc], checker)?;
}
}
}
Ok(())
}
fn update<'a, F: Function>(&mut self, checkinst: &CheckerInst, checker: &Checker<'a, F>) {
self.become_defined();
match checkinst {
&CheckerInst::Move { into, from } => {
if let Some(val) = self.get_value(&from).cloned() {
trace!(
"checker: checkinst {:?} updating: move {:?} -> {:?} val {:?}",
checkinst,
from,
into,
val
);
self.set_value(into, val);
}
}
&CheckerInst::ParallelMove { ref moves } => {
let mut additions: FxHashMap<VReg, SmallVec<[VReg; 2]>> = FxHashMap::default();
let mut deletions: FxHashSet<VReg> = FxHashSet::default();
for &(dest, src) in moves {
deletions.insert(dest);
additions
.entry(src)
.or_insert_with(|| smallvec![])
.push(dest);
}
for value in self.get_values_mut() {
if let Some(vregs) = value.vregs_mut() {
let mut insertions: SmallVec<[VReg; 2]> = smallvec![];
for &vreg in vregs.iter() {
if let Some(additions) = additions.get(&vreg) {
insertions.extend(additions.iter().cloned());
}
}
for &d in &deletions {
vregs.remove(&d);
}
vregs.extend(insertions);
}
}
}
&CheckerInst::Op {
ref operands,
ref allocs,
ref clobbers,
..
} => {
for (op, alloc) in operands.iter().zip(allocs.iter()) {
if op.kind() != OperandKind::Def {
continue;
}
self.remove_vreg(op.vreg());
self.set_value(*alloc, CheckerValue::from_reg(op.vreg()));
}
for clobber in clobbers {
self.remove_value(&Allocation::reg(*clobber));
}
}
&CheckerInst::Safepoint { ref allocs, .. } => {
for (alloc, value) in self.get_mappings_mut() {
if alloc.is_reg() {
continue;
}
if !allocs.contains(&alloc) {
let new_vregs = value
.vregs()
.unwrap()
.difference(&checker.reftyped_vregs)
.cloned()
.collect();
*value = CheckerValue::VRegs(new_vregs);
}
}
}
&CheckerInst::ProgramMove { inst: _, src, dst } => {
self.remove_vreg(dst.vreg());
for (_, value) in self.get_mappings_mut() {
value.copy_vreg(src.vreg(), dst.vreg());
}
}
}
}
fn remove_vreg(&mut self, vreg: VReg) {
for (_, value) in self.get_mappings_mut() {
value.remove_vreg(vreg);
}
}
fn check_constraint<'a, F: Function>(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
allocs: &[Allocation],
checker: &Checker<'a, F>,
) -> Result<(), CheckerError> {
match op.constraint() {
OperandConstraint::Any => {}
OperandConstraint::Reg => {
if let Some(preg) = alloc.as_reg() {
if !checker.machine_env.fixed_stack_slots.contains(&preg) {
return Ok(());
}
}
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
OperandConstraint::Stack => {
if alloc.kind() != AllocationKind::Stack {
if let Some(preg) = alloc.as_reg() {
if checker.machine_env.fixed_stack_slots.contains(&preg) {
return Ok(());
}
}
return Err(CheckerError::AllocationIsNotStack { inst, op, alloc });
}
}
OperandConstraint::FixedReg(preg) => {
if alloc != Allocation::reg(preg) {
return Err(CheckerError::AllocationIsNotFixedReg { inst, op, alloc });
}
}
OperandConstraint::Reuse(idx) => {
if alloc.kind() != AllocationKind::Reg {
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
if alloc != allocs[idx] {
return Err(CheckerError::AllocationIsNotReuse {
inst,
op,
alloc,
expected_alloc: allocs[idx],
});
}
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub(crate) enum CheckerInst {
Move { into: Allocation, from: Allocation },
ParallelMove {
moves: Vec<(VReg, VReg)>,
},
Op {
inst: Inst,
operands: Vec<Operand>,
allocs: Vec<Allocation>,
clobbers: Vec<PReg>,
},
Safepoint { inst: Inst, allocs: Vec<Allocation> },
ProgramMove {
inst: Inst,
src: Operand,
dst: Operand,
},
}
#[derive(Debug)]
pub struct Checker<'a, F: Function> {
f: &'a F,
bb_in: FxHashMap<Block, CheckerState>,
bb_insts: FxHashMap<Block, Vec<CheckerInst>>,
edge_insts: FxHashMap<(Block, Block), Vec<CheckerInst>>,
reftyped_vregs: FxHashSet<VReg>,
machine_env: &'a MachineEnv,
stack_pregs: PRegSet,
}
impl<'a, F: Function> Checker<'a, F> {
pub fn new(f: &'a F, machine_env: &'a MachineEnv) -> Checker<'a, F> {
let mut bb_in = FxHashMap::default();
let mut bb_insts = FxHashMap::default();
let mut edge_insts = FxHashMap::default();
let mut reftyped_vregs = FxHashSet::default();
for block in 0..f.num_blocks() {
let block = Block::new(block);
bb_in.insert(block, Default::default());
bb_insts.insert(block, vec![]);
for &succ in f.block_succs(block) {
edge_insts.insert((block, succ), vec![]);
}
}
for &vreg in f.reftype_vregs() {
reftyped_vregs.insert(vreg);
}
bb_in.insert(f.entry_block(), CheckerState::initial_with_pinned_vregs(f));
let mut stack_pregs = PRegSet::empty();
for &preg in &machine_env.fixed_stack_slots {
stack_pregs.add(preg);
}
Checker {
f,
bb_in,
bb_insts,
edge_insts,
reftyped_vregs,
machine_env,
stack_pregs,
}
}
pub fn prepare(&mut self, out: &Output) {
trace!("checker: out = {:?}", out);
let mut safepoint_slots: FxHashMap<Inst, Vec<Allocation>> = FxHashMap::default();
for &(progpoint, slot) in &out.safepoint_slots {
safepoint_slots
.entry(progpoint.inst())
.or_insert_with(|| vec![])
.push(slot);
}
let mut last_inst = None;
for block in 0..self.f.num_blocks() {
let block = Block::new(block);
for inst_or_edit in out.block_insts_and_edits(self.f, block) {
match inst_or_edit {
InstOrEdit::Inst(inst) => {
debug_assert!(last_inst.is_none() || inst > last_inst.unwrap());
last_inst = Some(inst);
self.handle_inst(block, inst, &mut safepoint_slots, out);
}
InstOrEdit::Edit(edit) => self.handle_edit(block, edit),
}
}
}
}
fn handle_inst(
&mut self,
block: Block,
inst: Inst,
safepoint_slots: &mut FxHashMap<Inst, Vec<Allocation>>,
out: &Output,
) {
if self.f.requires_refs_on_stack(inst) {
let allocs = safepoint_slots.remove(&inst).unwrap_or_else(|| vec![]);
let checkinst = CheckerInst::Safepoint { inst, allocs };
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
}
if let Some((src, dst)) = self.f.is_move(inst) {
let src_preg = self.f.is_pinned_vreg(src.vreg());
let src_op = match src_preg {
Some(preg) => Operand::reg_fixed_use(src.vreg(), preg),
None => Operand::any_use(src.vreg()),
};
let dst_preg = self.f.is_pinned_vreg(dst.vreg());
let dst_op = match dst_preg {
Some(preg) => Operand::reg_fixed_def(dst.vreg(), preg),
None => Operand::any_def(dst.vreg()),
};
let checkinst = CheckerInst::ProgramMove {
inst,
src: src_op,
dst: dst_op,
};
trace!("checker: adding inst {:?}", checkinst);
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
}
else if !self.f.is_branch(inst) {
let operands: Vec<_> = self.f.inst_operands(inst).iter().cloned().collect();
let allocs: Vec<_> = out.inst_allocs(inst).iter().cloned().collect();
let clobbers: Vec<_> = self.f.inst_clobbers(inst).into_iter().collect();
let checkinst = CheckerInst::Op {
inst,
operands,
allocs,
clobbers,
};
trace!("checker: adding inst {:?}", checkinst);
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
}
else {
for (i, &succ) in self.f.block_succs(block).iter().enumerate() {
let args = self.f.branch_blockparams(block, inst, i);
let params = self.f.block_params(succ);
assert_eq!(
args.len(),
params.len(),
"block{} has succ block{}; gave {} args for {} params",
block.index(),
succ.index(),
args.len(),
params.len()
);
if args.len() > 0 {
let moves = params.iter().cloned().zip(args.iter().cloned()).collect();
self.edge_insts
.get_mut(&(block, succ))
.unwrap()
.push(CheckerInst::ParallelMove { moves });
}
}
}
}
fn handle_edit(&mut self, block: Block, edit: &Edit) {
trace!("checker: adding edit {:?}", edit);
match edit {
&Edit::Move { from, to } => {
self.bb_insts
.get_mut(&block)
.unwrap()
.push(CheckerInst::Move { into: to, from });
}
}
}
fn analyze(&mut self) {
let mut queue = Vec::new();
let mut queue_set = FxHashSet::default();
queue.push(self.f.entry_block());
queue_set.insert(self.f.entry_block());
while !queue.is_empty() {
let block = queue.pop().unwrap();
queue_set.remove(&block);
let mut state = self.bb_in.get(&block).cloned().unwrap();
trace!("analyze: block {} has state {:?}", block.index(), state);
for inst in self.bb_insts.get(&block).unwrap() {
state.update(inst, self);
trace!("analyze: inst {:?} -> state {:?}", inst, state);
}
for &succ in self.f.block_succs(block) {
let mut new_state = state.clone();
for edge_inst in self.edge_insts.get(&(block, succ)).unwrap() {
new_state.update(edge_inst, self);
trace!(
"analyze: succ {:?}: inst {:?} -> state {:?}",
succ,
edge_inst,
new_state
);
}
let cur_succ_in = self.bb_in.get(&succ).unwrap();
trace!(
"meeting state {:?} for block {} with state {:?} for block {}",
new_state,
block.index(),
cur_succ_in,
succ.index()
);
new_state.meet_with(cur_succ_in);
let changed = &new_state != cur_succ_in;
trace!(" -> {:?}, changed {}", new_state, changed);
if changed {
trace!(
"analyze: block {} state changed from {:?} to {:?}; pushing onto queue",
succ.index(),
cur_succ_in,
new_state
);
self.bb_in.insert(succ, new_state);
if !queue_set.contains(&succ) {
queue.push(succ);
queue_set.insert(succ);
}
}
}
}
}
fn find_errors(&self) -> Result<(), CheckerErrors> {
let mut errors = vec![];
for (block, input) in &self.bb_in {
let mut state = input.clone();
for inst in self.bb_insts.get(block).unwrap() {
if let Err(e) = state.check(InstPosition::Before, inst, self) {
trace!("Checker error: {:?}", e);
errors.push(e);
}
state.update(inst, self);
if let Err(e) = state.check(InstPosition::After, inst, self) {
trace!("Checker error: {:?}", e);
errors.push(e);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(CheckerErrors { errors })
}
}
pub fn run(mut self) -> Result<(), CheckerErrors> {
self.analyze();
let result = self.find_errors();
trace!("=== CHECKER RESULT ===");
fn print_state(state: &CheckerState) {
if let CheckerState::Allocations(allocs) = state {
let mut s = vec![];
for (alloc, state) in allocs {
s.push(format!("{} := {}", alloc, state));
}
trace!(" {{ {} }}", s.join(", "))
}
}
for vreg in self.f.reftype_vregs() {
trace!(" REF: {}", vreg);
}
for bb in 0..self.f.num_blocks() {
let bb = Block::new(bb);
trace!("block{}:", bb.index());
let insts = self.bb_insts.get(&bb).unwrap();
let mut state = self.bb_in.get(&bb).unwrap().clone();
print_state(&state);
for inst in insts {
match inst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
ref clobbers,
} => {
trace!(
" inst{}: {:?} ({:?}) clobbers:{:?}",
inst.index(),
operands,
allocs,
clobbers
);
}
&CheckerInst::Move { from, into } => {
trace!(" {} -> {}", from, into);
}
&CheckerInst::Safepoint { ref allocs, .. } => {
let mut slotargs = vec![];
for &slot in allocs {
slotargs.push(format!("{}", slot));
}
trace!(" safepoint: {}", slotargs.join(", "));
}
&CheckerInst::ProgramMove { inst, src, dst } => {
trace!(" inst{}: prog_move {} -> {}", inst.index(), src, dst);
}
&CheckerInst::ParallelMove { .. } => {
panic!("unexpected parallel_move in body (non-edge)")
}
}
state.update(inst, &self);
print_state(&state);
}
for &succ in self.f.block_succs(bb) {
trace!(" succ {:?}:", succ);
let mut state = state.clone();
for edge_inst in self.edge_insts.get(&(bb, succ)).unwrap() {
match edge_inst {
&CheckerInst::ParallelMove { ref moves } => {
let moves = moves
.iter()
.map(|(dest, src)| format!("{} -> {}", src, dest))
.collect::<Vec<_>>();
trace!(" parallel_move {}", moves.join(", "));
}
_ => panic!("unexpected edge_inst: not a parallel move"),
}
state.update(edge_inst, &self);
print_state(&state);
}
}
}
result
}
}