Files
regalloc2/src/checker.rs
Jamey Sharp 7bb83a3361 checker: Use a couple of Rust idioms (#114)
This should have no functional change, just makes the source slightly
easier to read and reason about.
2023-02-16 01:59:20 +00:00

1165 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* The following code is derived from `lib/src/checker.rs` in the
* regalloc.rs project
* (https://github.com/bytecodealliance/regalloc.rs). regalloc.rs is
* also licensed under Apache-2.0 with the LLVM exception, as the rest
* of regalloc2 is.
*/
//! Checker: verifies that spills/reloads/moves retain equivalent
//! dataflow to original, VReg-based code.
//!
//! The basic idea is that we track symbolic values as they flow
//! through spills and reloads. The symbolic values represent
//! particular virtual registers in the original function body
//! presented to the register allocator. Any instruction in the
//! original function body (i.e., not added by the allocator)
//! conceptually generates a symbolic value "Vn" when storing to (or
//! modifying) a virtual register.
//!
//! A symbolic value is logically a *set of virtual registers*,
//! representing all virtual registers equal to the value in the given
//! storage slot at a given program point. This representation (as
//! opposed to tracking just one virtual register) is necessary
//! because the regalloc may implement moves in the source program
//! (via move instructions or blockparam assignments on edges) in
//! "intelligent" ways, taking advantage of values that are already in
//! the right place, so we need to know *all* names for a value.
//!
//! These symbolic values are precise but partial: in other words, if
//! a physical register is described as containing a virtual register
//! at a program point, it must actually contain the value of this
//! register (modulo any analysis bugs); but it may describe fewer
//! virtual registers even in cases where one *could* statically prove
//! that it contains a certain register, because the analysis is not
//! perfectly path-sensitive or value-sensitive. However, all
//! assignments *produced by our register allocator* should be
//! analyzed fully precisely. (This last point is important and bears
//! repeating: we only need to verify the programs that we produce,
//! not arbitrary programs.)
//!
//! Operand constraints (fixed register, register, any) are also checked
//! at each operand.
//!
//! ## Formal Definition
//!
//! The analysis lattice consists of the elements of 𝒫(V), the
//! powerset (set of all subsets) of V (the set of all virtual
//! registers). The (top) value in the lattice is V itself, and the
//! ⊥ (bottom) value in the lattice is ∅ (the empty set). The lattice
//! ordering relation is the subset relation: S ≤ U iff S ⊆ U. These
//! definitions imply that the lattice meet-function (greatest lower
//! bound) is set-intersection.
//!
//! (For efficiency, we represent not by actually listing out all
//! virtual registers, but by representing a special "top" value, but
//! the semantics are the same.)
//!
//! The dataflow analysis state at each program point (each point
//! before or after an instruction) is:
//!
//! - map of: Allocation -> lattice value
//!
//! And the transfer functions for instructions are (where `A` is the
//! above map from allocated physical registers to lattice values):
//!
//! - `Edit::Move` inserted by RA: [ alloc_d := alloc_s ]
//!
//! A' = A[alloc_d → A[alloc_s]]
//!
//! - statement in pre-regalloc function [ V_i := op V_j, V_k, ... ]
//! with allocated form [ A_i := op A_j, A_k, ... ]
//!
//! A' = { A_k → A[A_k] \ { V_i } for k ≠ i }
//! { A_i -> { V_i } }
//!
//! In other words, a statement, even after allocation, generates
//! a symbol that corresponds to its original virtual-register
//! def. Simultaneously, that same virtual register symbol is removed
//! from all other allocs: they no longer carry the current value.
//!
//! - Parallel moves or blockparam-assignments in original program
//! [ V_d1 := V_s1, V_d2 := V_s2, ... ]
//!
//! A' = { A_k → subst(A[A_k]) for all k }
//! where subst(S) removes symbols for overwritten virtual
//! registers (V_d1 .. V_dn) and then adds V_di whenever
//! V_si appeared prior to the removals.
//!
//! To check correctness, we first find the dataflow fixpoint with the
//! above lattice and transfer/meet functions. Then, at each op, we
//! examine the dataflow solution at the preceding program point, and
//! check that the allocation for each op arg (input/use) contains the
//! symbol corresponding to the original virtual register specified
//! for this arg.
#![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;
/// A set of errors detected by the regalloc checker.
#[derive(Clone, Debug)]
pub struct CheckerErrors {
errors: Vec<CheckerError>,
}
/// A single error detected by the regalloc checker.
#[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,
},
}
/// Abstract state for an allocation.
///
/// Equivalent to a set of virtual register names, with the
/// universe-set as top and empty set as bottom lattice element. The
/// meet-function is thus set intersection.
#[derive(Clone, Debug, PartialEq, Eq)]
enum CheckerValue {
/// The lattice top-value: this value could be equivalent to any
/// vreg (i.e., the universe set).
Universe,
/// The set of VRegs that this value is equal to.
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 {
/// Meet function of the abstract-interpretation value
/// lattice. Returns a boolean value indicating whether `self` was
/// changed.
fn meet_with(&mut self, other: &CheckerValue) {
match (self, other) {
(_, CheckerValue::Universe) => {
// Nothing.
}
(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(&reg);
}
}
}
fn copy_vreg(&mut self, src: VReg, dst: VReg) {
match self {
CheckerValue::Universe => {
// Nothing.
}
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 &param in f.branch_blockparams(block, inst, succ_idx) {
v(param);
}
}
}
}
for &vreg in f.block_params(block) {
v(vreg);
}
}
}
/// State that steps through program points as we scan over the instruction stream.
#[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(),
}
}
/// Transition from a "top" (undefined/unanalyzed) state to an empty set of allocations.
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 => {
// Nothing.
}
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() -> Self {
CheckerState::Allocations(FxHashMap::default())
}
}
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(())
}
}
}
}
/// Meet function for analysis value: meet individual values at
/// matching allocations, and intersect keys (remove key-value pairs
/// only on one side). Returns boolean flag indicating whether `into`
/// changed.
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 {
/// Create a new checker state.
fn new() -> CheckerState {
Default::default()
}
/// Merge this checker state with another at a CFG join-point.
fn meet_with(&mut self, other: &CheckerState) {
match (self, other) {
(_, CheckerState::Top) => {
// Nothing.
}
(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(())
}
/// Check an instruction against this state. This must be called
/// twice: once with `InstPosition::Before`, and once with
/// `InstPosition::After` (after updating state with defs).
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,
..
} => {
// Skip Use-checks at the After point if there are any
// reused inputs: the Def which reuses the input
// happens early.
let has_reused_input = operands
.iter()
.any(|op| matches!(op.constraint(), OperandConstraint::Reuse(_)));
if has_reused_input && pos == InstPosition::After {
return Ok(());
}
// For each operand, check (i) that the allocation
// contains the expected vreg, and (ii) that it meets
// the requirements of the OperandConstraint.
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 } => {
// Ensure that the allocator never returns stack-to-stack moves.
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 { .. } => {
// This doesn't need verification; we just update
// according to the move semantics in the step
// function below.
}
&CheckerInst::ProgramMove { inst, src, dst: _ } => {
// Validate that the fixed-reg constraint, if any, on
// `src` is satisfied.
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)?;
}
// Note that we don't do anything with `dst`
// here. That is implicitly checked whenever `dst` is
// used; the `update()` step below adds the symbolic
// vreg for `dst` into wherever `src` may be stored.
}
}
Ok(())
}
/// Update according to instruction.
fn update<'a, F: Function>(&mut self, checkinst: &CheckerInst, checker: &Checker<'a, F>) {
self.become_defined();
match checkinst {
&CheckerInst::Move { into, from } => {
// Value may not be present if this move is part of
// the parallel move resolver's fallback sequence that
// saves a victim register elsewhere. (In other words,
// that sequence saves an undefined value and restores
// it, so has no effect.) The checker needs to avoid
// putting Universe lattice values into the map.
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 } => {
// First, build map of actions for each vreg in an
// alloc. If an alloc has a reg V_i before a parallel
// move, then for each use of V_i as a source (V_i ->
// V_j), we might add a new V_j wherever V_i appears;
// and if V_i is used as a dest (at most once), then
// it must be removed first from allocs' vreg sets.
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);
}
// Now process each allocation's set of vreg labels,
// first deleting those labels that were updated by
// this parallel move, then adding back labels
// redefined by the move.
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 each def, (i) update alloc to reflect defined
// vreg (and only that vreg), and (ii) update all
// other allocs in the checker state by removing this
// vreg, if defined (other defs are now stale).
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) {
// Remove all reftyped vregs as labels.
let new_vregs = value
.vregs()
.unwrap()
.difference(&checker.reftyped_vregs)
.cloned()
.collect();
*value = CheckerValue::VRegs(new_vregs);
}
}
}
&CheckerInst::ProgramMove { inst: _, src, dst } => {
// Remove all earlier instances of `dst`: this vreg is
// now stale (it is being overwritten).
self.remove_vreg(dst.vreg());
// Define `dst` wherever `src` occurs.
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() {
// Reject pregs that represent a fixed stack slot.
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 {
// Accept pregs that represent a fixed stack slot.
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(())
}
}
/// An instruction representation in the checker's BB summary.
#[derive(Clone, Debug)]
pub(crate) enum CheckerInst {
/// A move between allocations (these could be registers or
/// spillslots).
Move { into: Allocation, from: Allocation },
/// A parallel move in the original program. Simultaneously moves
/// from all source vregs to all corresponding dest vregs,
/// permitting overlap in the src and dest sets and doing all
/// reads before any writes.
ParallelMove {
/// Vector of (dest, src) moves.
moves: Vec<(VReg, VReg)>,
},
/// A regular instruction with fixed use and def slots. Contains
/// both the original operands (as given to the regalloc) and the
/// allocation results.
Op {
inst: Inst,
operands: Vec<Operand>,
allocs: Vec<Allocation>,
clobbers: Vec<PReg>,
},
/// A safepoint, with the given Allocations specified as containing
/// reftyped values. All other reftyped values become invalid.
Safepoint { inst: Inst, allocs: Vec<Allocation> },
/// An op with one source operand, and one dest operand, that
/// copies any symbolic values from the source to the dest, in
/// addition to adding the symbolic value of the dest vreg to the
/// set. This "program move" is distinguished from the above
/// `Move` by being semantically relevant in the original
/// (pre-regalloc) program.
///
/// We transform checker values as follows: for any vreg-set that
/// contains `dst`'s vreg, we first delete that vreg (because it
/// is being redefined). Then, for any vreg-set with `src`
/// present, we add `dst`.
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> {
/// Create a new checker for the given function, initializing CFG
/// info immediately. The client should call the `add_*()`
/// methods to add abstract instructions to each BB before
/// invoking `run()` to check for errors.
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::default());
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,
}
}
/// Build the list of checker instructions based on the given func
/// and allocation results.
pub fn prepare(&mut self, out: &Output) {
trace!("checker: out = {:?}", out);
// Preprocess safepoint stack-maps into per-inst vecs.
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),
}
}
}
}
/// For each original instruction, create an `Op`.
fn handle_inst(
&mut self,
block: Block,
inst: Inst,
safepoint_slots: &mut FxHashMap<Inst, Vec<Allocation>>,
out: &Output,
) {
// If this is a safepoint, then check the spillslots at this point.
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 this is a move, handle specially. Note that the
// regalloc2-inserted moves are not semantically present in
// the original program and so do not modify the sets of
// symbolic values at all, but rather just move them around;
// but "program moves" *are* present, and have the following
// semantics: they define the destination vreg, but also
// retain any symbolic values in the source.
//
// regalloc2 reifies all moves into edits in its unified
// move/edit framework, so we don't get allocs for these moves
// in the post-regalloc output, and the embedder is not
// supposed to emit the moves. But we *do* want to check the
// semantic implications, namely definition of new vregs. So
// we emit `ProgramMove` ops that do just this.
if let Some((src, dst)) = self.f.is_move(inst) {
let src_op = Operand::any_use(src.vreg());
let dst_op = 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);
}
// Skip normal checks if this is a branch: the blockparams do
// not exist in post-regalloc code, and the edge-moves have to
// be inserted before the branch rather than after.
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);
}
// Instead, if this is a branch, emit a ParallelMove on each
// outgoing edge as necessary to handle blockparams.
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 });
}
}
}
/// Perform the dataflow analysis to compute checker state at each BB entry.
fn analyze(&mut self) {
let mut queue = Vec::new();
let mut queue_set = FxHashSet::default();
// Put every block in the queue to start with, to ensure
// everything is visited even if the initial state remains
// `Top` after preds update it.
//
// We add blocks in reverse order so that when we process
// back-to-front below, we do our initial pass in input block
// order, which is (usually) RPO order or at least a
// reasonable visit order.
for block in (0..self.f.num_blocks()).rev() {
let block = Block::new(block);
queue.push(block);
queue_set.insert(block);
}
while let Some(block) = queue.pop() {
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.insert(succ) {
queue.push(succ);
}
}
}
}
}
/// Using BB-start state computed by `analyze()`, step the checker state
/// through each BB and check each instruction's register allocations
/// for errors.
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 })
}
}
/// Find any errors, returning `Err(CheckerErrors)` with all errors found
/// or `Ok(())` otherwise.
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
}
}