verify.rs 20.14 KiB
extern crate bitvec;
use std::collections::HashMap;
use std::iter::zip;
use self::bitvec::prelude::*;
use crate::*;
/*
* Top level IR verification function. Verification runs passes that produce
* useful results (typing, dominator trees, etc.), so if verification succeeds,
* return those useful results. Otherwise, return the first error string found.
*/
pub fn verify(
module: &mut Module,
) -> Result<
(
Vec<ImmutableDefUseMap>,
Vec<Vec<NodeID>>,
ModuleTyping,
Vec<Subgraph>,
Vec<DomTree>,
Vec<DomTree>,
Vec<HashMap<NodeID, NodeID>>,
),
String,
> {
// Calculate def uses and reverse postorders.
let def_uses: Vec<_> = module.functions.iter().map(def_use).collect();
let reverse_postorders: Vec<_> = def_uses.iter().map(reverse_postorder).collect();
// Typecheck the module.
let typing = typecheck(module, &reverse_postorders)?;
// Assemble fork join maps for module.
let subgraphs: Vec<_> = zip(module.functions.iter(), def_uses.iter())
.map(|(function, def_use)| control_subgraph(function, def_use))
.collect();
let fork_join_maps: Vec<_> = zip(module.functions.iter(), subgraphs.iter())
.map(|(function, subgraph)| fork_join_map(function, subgraph))
.collect();
// Check the structure of the functions in the module.
for (function, (def_use, typing)) in
zip(module.functions.iter(), zip(def_uses.iter(), typing.iter()))
{
verify_structure(function, def_use, typing, &module.types)?;
}
// Calculate dominator and postdominator trees.
let doms: Vec<_> = subgraphs
.iter()
.map(|subgraph| dominator(subgraph, NodeID::new(0)))
.collect();
let postdoms: Vec<_> = zip(subgraphs.into_iter(), module.functions.iter())
.map(|(subgraph, function)| postdominator(subgraph, NodeID::new(function.nodes.len())))
.collect();
// Check dominance relations.
for idx in 0..module.functions.len() {
let function = &module.functions[idx];
let reverse_postorder = &reverse_postorders[idx];
let dom = &doms[idx];
let postdom = &postdoms[idx];
let fork_join_map = &fork_join_maps[idx];
// Calculate control output dependencies here, since they are not
// returned by verify.
let control_output_dependencies =
forward_dataflow(function, reverse_postorder, |inputs, id| {
control_output_flow(inputs, id, function)
});
verify_dominance_relationships(
function,
&control_output_dependencies,
dom,
postdom,
fork_join_map,
)?;
}
// Recalculate subgraphs for return since postdominator analysis modifies
// them.
let subgraphs: Vec<_> = zip(module.functions.iter(), def_uses.iter())
.map(|(function, def_use)| control_subgraph(function, def_use))
.collect();
Ok((
def_uses,
reverse_postorders,
typing,
subgraphs,
doms,
postdoms,
fork_join_maps,
))
}
/*
* There are structural constraints the IR must follow, such as all phi nodes'
* control input must be a region node. This is where those properties are
* verified.
*/
fn verify_structure(
function: &Function,
def_use: &ImmutableDefUseMap,
typing: &Vec<TypeID>,
types: &Vec<Type>,
) -> Result<(), String> {
for (idx, node) in function.nodes.iter().enumerate() {
if !node.is_start() && idx == 0 {
Err("There must be a single start node, and its ID must be 0.")?;
}
let users = def_use.get_users(NodeID::new(idx));
match node {
// A start node must have exactly one control user. Additionally, it
// may have many parameter, constant, or dynamic constant users.
Node::Start => {
if idx != 0 {
Err("The start node must be node ID 0.")?;
}
let mut found_control = false;
for user in users {
match function.nodes[user.idx()] {
Node::Parameter { index: _ }
| Node::Constant { id: _ }
| Node::DynamicConstant { id: _ } => {}
_ => {
if function.nodes[user.idx()].is_control() {
if found_control {
Err("A start node must have exactly one control user.")?;
} else {
found_control = true;
}
} else {
Err("All users of a start node must be control, Parameter, Constant, or DynamicConstant nodes.")?;
}
}
}
}
if !found_control {
Err("A start node must have exactly one control user.")?;
}
}
// A region node must have exactly one control user. Additionally,
// it may have many phi users.
Node::Region { preds: _ } => {
let mut found_control = false;
for user in users {
match function.nodes[user.idx()] {
Node::Phi {
control: _,
data: _,
} => {}
_ => {
if function.nodes[user.idx()].is_control() {
if found_control {
Err("A region node must have exactly one control user.")?;
} else {
found_control = true;
}
} else {
Err("All region of a start node must be control or Phi nodes.")?;
}
}
}
}
if !found_control {
Err("A region node must have exactly one control user.")?;
}
}
// A fork node must have exactly one control user. Additionally,
// it may have many thread ID users.
Node::Fork {
control: _,
factors: _,
} => {
let mut found_control = false;
for user in users {
match function.nodes[user.idx()] {
Node::ThreadID {
control: _,
dimension: _,
} => {}
_ => {
if function.nodes[user.idx()].is_control() {
if found_control {
Err("A fork node must have exactly one control user.")?;
} else {
found_control = true;
}
} else {
Err(
"All users of a fork node must be control or thread ID nodes.",
)?;
}
}
}
}
if !found_control {
Err("A fork node must have exactly one control user.")?;
}
}
// A join node must have exactly one control user. Additionally,
// it may have many reduce users.
Node::Join { control: _ } => {
let mut found_control = false;
for user in users {
match function.nodes[user.idx()] {
Node::Reduce {
control: _,
init: _,
reduct: _,
} => {}
_ => {
if function.nodes[user.idx()].is_control() {
if found_control {
Err("A join node must have exactly one control user.")?;
} else {
found_control = true;
}
} else {
Err("All uses of a join node must be control or Reduce nodes.")?;
}
}
}
}
if !found_control {
Err("A join node must have exactly one control user.")?;
}
}
// Each if node must have exactly two Read users, which
// reference differing elements of the node's output product.
Node::If {
control: _,
cond: _,
} => {
if users.len() != 2 {
Err(format!("If node must have 2 users, not {}.", users.len()))?;
}
if let (
Node::Projection {
control: _,
selection: result1,
},
Node::Projection {
control: _,
selection: result2,
},
) = (
&function.nodes[users[0].idx()],
&function.nodes[users[1].idx()],
) {
if !((*result1 == 0 && *result2 == 1) || (*result2 == 0 && *result1 == 1)) {
Err("If node's user Read nodes must reference different elements of output product.")?;
}
} else {
Err("If node's users must both be Read nodes.")?;
}
}
// Phi nodes must depend on a region node.
Node::Phi { control, data: _ } => {
if let Node::Region { preds: _ } = function.nodes[control.idx()] {
} else {
Err("Phi node's control input must be a region node.")?;
}
}
// ThreadID nodes must depend on a fork node.
Node::ThreadID { control, dimension } => {
if let Node::Fork {
control: _,
factors,
} = &function.nodes[control.idx()]
{
if *dimension >= factors.len() {
Err("The dimension in a thread ID must be in bounds for its fork node.")?;
}
} else {
Err("ThreadID node's control input must be a fork node.")?;
}
}
// Collect nodes must depend on a join node.
Node::Reduce {
control,
init: _,
reduct: _,
} => {
if let Node::Join { control: _ } = function.nodes[control.idx()] {
} else {
Err("Collect node's control input must be a join node.")?;
}
}
// Return nodes must have no users.
Node::Return {
control: _,
data: _,
} => {
if users.len() != 0 {
Err(format!(
"Return node must have 0 users, not {}.",
users.len()
))?;
}
}
// Match nodes are similar to if nodes, but have a variable number
// of Read users, corresponding to the sum type being matched.
Node::Match { control: _, sum } => {
let sum_ty = &types[typing[sum.idx()].idx()];
if let Type::Summation(tys) = sum_ty {
let correct_number_of_users = tys.len();
if users.len() != correct_number_of_users {
Err(format!(
"Match node must have {} users, not {}.",
correct_number_of_users,
users.len()
))?;
}
let mut users_covered = bitvec![u8, Lsb0; 0; users.len()];
for user in users {
if let Node::Projection {
control: _,
ref selection,
} = function.nodes[user.idx()]
{
assert!(*selection < users.len(), "Read child of match node reads from bad index, but ran after typecheck succeeded.");
users_covered.set(*selection, true);
}
}
if users_covered.count_ones() != users.len() {
Err(format!("Match node's user Read nodes must reference all {} elements of match node's output product, but they only reference {} of them.", users.len(), users_covered.count_ones()))?;
}
} else {
panic!("Type of match node's sum input is not a summation type, but ran after typecheck succeeded.");
}
}
_ => {}
};
}
Ok(())
}
/*
* There are dominance relationships the IR must follow, such as all uses of a
* phi node must be dominated by the corresponding region node.
*/
fn verify_dominance_relationships(
function: &Function,
control_output_dependencies: &Vec<UnionNodeSet>,
dom: &DomTree,
postdom: &DomTree,
fork_join_map: &HashMap<NodeID, NodeID>,
) -> Result<(), String> {
// Step 1: check that forks dominate joins, and joins postdominate forks.
for (fork, join) in fork_join_map.iter() {
if !dom.does_dom(*fork, *join) {
Err(format!(
"Fork node (ID {}) doesn't dominate its corresponding join node (ID {}).",
fork.idx(),
join.idx()
))?;
}
if !postdom.does_dom(*join, *fork) {
Err(format!(
"Join node (ID {}) doesn't postdominate its corresponding fork node (ID {}).",
join.idx(),
fork.idx()
))?;
}
}
// Step 2: collect nodes and corresponding control output dependencies that
// should be checked.
let mut to_check = vec![];
for idx in 0..function.nodes.len() {
// If this node is a phi node, we need to handle adding dominance checks
// completely differently. Reduce nodes need to be handled similarly.
if let Node::Phi { control, data } = &function.nodes[idx] {
// Get the control predecessors of a region.
let region_preds = if let Node::Region { preds } = &function.nodes[control.idx()] {
preds
} else {
panic!("A phi's control input must be a region node.")
};
// The inputs to a phi node don't need to dominate the phi node.
// However, the data inputs to a phi node do need to hold proper
// dominance relations with the corresponding control inputs to the
// phi node's region node.
for (control_pred, data_pred) in zip(region_preds.iter(), data.iter()) {
to_check.push((
*control_pred,
control_output_dependencies[data_pred.idx()].clone(),
));
}
} else if let Node::Reduce {
control: _,
init: _,
reduct: _,
} = &function.nodes[idx]
{
// TODO: Properly check dominance relations of reduce nodes.
} else {
// Having a control output dependency only matters if this node is a
// control node, or if this node is a control output of a control node.
// If this node is a control output, then we want to consider the
// control node itself. We exclude the case of phi and reduce nodes
// here, since these nodes can explicitly have non-dominating inputs.
let this_id = if let Node::ThreadID {
control: dominated_control,
dimension: _,
} = function.nodes[idx]
{
dominated_control
} else {
NodeID::new(idx)
};
// If the node to be added to the to_check vector isn't even in the
// dominator tree, don't bother. It doesn't need to be checked for
// dominance relations.
if !dom.contains(this_id) {
continue;
}
// control_output_dependencies contains the "out" values from the
// control output dataflow analysis, while we need the "in" values.
// This can be easily reconstructed.
let mut dependencies = UnionNodeSet::top();
for input in get_uses(&function.nodes[idx]).as_ref() {
dependencies =
UnionNodeSet::meet(&dependencies, &control_output_dependencies[input.idx()]);
}
// Add dependencies to check for this node.
to_check.push((this_id, dependencies));
}
}
// Step 3: check that every node has proper dominance relations with
// corresponding control output nodes.
for (this_id, dependencies) in to_check {
for pred_idx in 0..function.nodes.len() {
if dependencies.is_set(NodeID::new(pred_idx)) {
match function.nodes[pred_idx] {
// Verify that uses of phis / reduce nodes are dominated
// by the corresponding region / join nodes, respectively.
Node::Phi { control, data: _ }
| Node::Reduce {
control,
init: _,
reduct: _,
} => {
if dom.contains(this_id) && !dom.does_dom(control, this_id) {
Err(format!(
"{} node (ID {}) doesn't dominate its use (ID {}).",
function.nodes[pred_idx].upper_case_name(),
pred_idx,
this_id.idx()
))?;
}
}
// Verify that uses of thread ID nodes are dominated by the
// corresponding fork nodes.
Node::ThreadID {
control,
dimension: _,
} => {
if dom.contains(this_id) && !dom.does_dom(control, this_id) {
Err(format!(
"ThreadID node (ID {}) doesn't dominate its use (ID {}).",
pred_idx,
this_id.idx()
))?;
}
// Every use of a thread ID must be postdominated by
// the thread ID's fork's corresponding join node. We
// don't need to check for the case where the thread ID
// flows through the collect node out of the fork-join,
// because after the collect, the thread ID is no longer
// considered an immediate control output use.
if postdom.contains(this_id)
&& !postdom.does_dom(*fork_join_map.get(&control).unwrap(), this_id)
{
Err(format!("ThreadID node's (ID {}) fork's join doesn't postdominate its use (ID {}).", pred_idx, this_id.idx()))?;
}
}
// If a dependency is set but depended node isn't a control
// output, something is wrong.
_ => panic!(
"Control output dependency is set for a node that's not a control output."
),
}
}
}
}
Ok(())
}