diff --git a/hercules_ir/src/dataflow.rs b/hercules_ir/src/dataflow.rs index 69a00b65118aa357df93cfc833e8f738f28df500..92886d3b6c0eff7f685bb78330e1f97f9ad49ed8 100644 --- a/hercules_ir/src/dataflow.rs +++ b/hercules_ir/src/dataflow.rs @@ -8,7 +8,7 @@ use crate::*; * Trait for a type that is a semilattice. Semilattice types must also be Eq, * so that the dataflow analysis can determine when to terminate. */ -pub trait Semilattice: Eq { +pub trait Semilattice: Eq + Clone { fn meet(a: &Self, b: &Self) -> Self; fn bottom() -> Self; fn top() -> Self; @@ -37,10 +37,18 @@ where let uses: Vec<NodeUses> = function.nodes.iter().map(|n| get_uses(n)).collect(); // Step 2: create initial set of "out" points. + let start_node_output = flow_function(&[&L::bottom()], NodeID::new(0)); let mut outs: Vec<L> = (0..function.nodes.len()) .map(|id| { flow_function( - &vec![&(if id == 0 { L::bottom() } else { L::top() }); uses[id].as_ref().len()], + &vec![ + &(if id == 0 { + start_node_output.clone() + } else { + L::top() + }); + uses[id].as_ref().len() + ], NodeID::new(id), ) }) @@ -124,3 +132,142 @@ fn reverse_postorder_helper( (order, visited) } } + +/* + * A bit vector set is a very general kind of semilattice. This variant is for + * "intersecting" flow functions. + */ +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum IntersectNodeSet { + Empty, + Bits(BitVec<u8, Lsb0>), + Full, +} + +impl IntersectNodeSet { + pub fn is_set(&self, id: NodeID) -> bool { + match self { + IntersectNodeSet::Empty => false, + IntersectNodeSet::Bits(bits) => bits[id.idx()], + IntersectNodeSet::Full => true, + } + } +} + +impl Semilattice for IntersectNodeSet { + fn meet(a: &Self, b: &Self) -> Self { + match (a, b) { + (IntersectNodeSet::Full, b) => b.clone(), + (a, IntersectNodeSet::Full) => a.clone(), + (IntersectNodeSet::Bits(a), IntersectNodeSet::Bits(b)) => { + assert!( + a.len() == b.len(), + "IntersectNodeSets must have same length to meet." + ); + IntersectNodeSet::Bits(a.clone() & b) + } + (IntersectNodeSet::Empty, _) => IntersectNodeSet::Empty, + (_, IntersectNodeSet::Empty) => IntersectNodeSet::Empty, + } + } + + fn bottom() -> Self { + // For intersecting flow functions, the bottom state is empty. + IntersectNodeSet::Empty + } + + fn top() -> Self { + // For intersecting flow functions, the top state is full. + IntersectNodeSet::Full + } +} + +/* + * A bit vector set is a very general kind of semilattice. This variant is for + * "unioning" flow functions. + */ +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum UnionNodeSet { + Empty, + Bits(BitVec<u8, Lsb0>), + Full, +} + +impl UnionNodeSet { + pub fn is_set(&self, id: NodeID) -> bool { + match self { + UnionNodeSet::Empty => false, + UnionNodeSet::Bits(bits) => bits[id.idx()], + UnionNodeSet::Full => true, + } + } +} + +impl Semilattice for UnionNodeSet { + fn meet(a: &Self, b: &Self) -> Self { + match (a, b) { + (UnionNodeSet::Empty, b) => b.clone(), + (a, UnionNodeSet::Empty) => a.clone(), + (UnionNodeSet::Bits(a), UnionNodeSet::Bits(b)) => { + assert!( + a.len() == b.len(), + "UnionNodeSets must have same length to meet." + ); + UnionNodeSet::Bits(a.clone() | b) + } + (UnionNodeSet::Full, _) => UnionNodeSet::Full, + (_, UnionNodeSet::Full) => UnionNodeSet::Full, + } + } + + fn bottom() -> Self { + // For unioning flow functions, the bottom state is full. + UnionNodeSet::Full + } + + fn top() -> Self { + // For unioning flow functions, the top state is empty. + UnionNodeSet::Empty + } +} + +/* + * Below are some common flow functions. They all take a slice of semilattice + * references as their first argument, and a node ID as their second. However, + * they may in addition take more arguments (meaning that these functions + * should be used inside closures at a callsite of a top level dataflow + * function). + */ + +/* + * Flow function for collecting all of a node's uses of "control outputs". What + * this flow function does is collect all immediate phi, thread ID, and collect + * nodes that every other node depends on through data nodes. Flow is ended at + * a control node, or at a phi, thread ID, or collect node. + */ +pub fn control_output_flow( + inputs: &[&UnionNodeSet], + node_id: NodeID, + function: &Function, +) -> UnionNodeSet { + // Step 1: union inputs. + let mut out = UnionNodeSet::top(); + for input in inputs { + out = UnionNodeSet::meet(&out, input); + } + let node = &function.nodes[node_id.idx()]; + + // Step 2: clear all bits, if applicable. + if node.is_strictly_control() || node.is_thread_id() || node.is_collect() || node.is_phi() { + out = UnionNodeSet::Empty; + } + + // Step 3: set bit for current node, if applicable. + if node.is_thread_id() || node.is_collect() || node.is_phi() { + let mut singular = bitvec![u8, Lsb0; 0; function.nodes.len()]; + singular.set(node_id.idx(), true); + out = UnionNodeSet::meet(&out, &UnionNodeSet::Bits(singular)); + } + + out +} diff --git a/hercules_ir/src/dom.rs b/hercules_ir/src/dom.rs index 002cbd04a8b1d16d714bb72fe17a5e719e87aa79..d359db6591cd571bf18b913c67efb734ba158995 100644 --- a/hercules_ir/src/dom.rs +++ b/hercules_ir/src/dom.rs @@ -38,6 +38,14 @@ impl DomTree { pub fn does_prop_dom(&self, a: NodeID, b: NodeID) -> bool { a != b && self.does_dom(a, b) } + + /* + * Check if a node is in the dom tree (if the node is the root of the tree, + * will still return true). + */ + pub fn is_non_root(&self, x: NodeID) -> bool { + self.idom.contains_key(&x) + } } /* diff --git a/hercules_ir/src/ir.rs b/hercules_ir/src/ir.rs index 9400f0265f7937ec253c3d2279180042cffb1365..dee8fb9e0d420b5f6c691ef0099cdf89728c28e5 100644 --- a/hercules_ir/src/ir.rs +++ b/hercules_ir/src/ir.rs @@ -46,7 +46,7 @@ pub struct Function { */ #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Type { - Control(Box<[DynamicConstantID]>), + Control(Box<[NodeID]>), Boolean, Integer8, Integer16, @@ -285,17 +285,77 @@ pub enum BinaryOperator { RSh, } +/* + * Simple predicate functions on nodes take a lot of space, so use a macro. + */ + +macro_rules! define_pattern_predicate { + ($x: ident, $y: pat) => { + pub fn $x(&self) -> bool { + if let $y = self { + true + } else { + false + } + } + }; +} + impl Node { - pub fn is_return(&self) -> bool { - if let Node::Return { + define_pattern_predicate!(is_start, Node::Start); + define_pattern_predicate!(is_region, Node::Region { preds: _ }); + define_pattern_predicate!( + is_if, + Node::If { + control: _, + cond: _, + } + ); + define_pattern_predicate!( + is_fork, + Node::Fork { + control: _, + factor: _, + } + ); + define_pattern_predicate!(is_join, Node::Join { control: _ }); + define_pattern_predicate!( + is_phi, + Node::Phi { + control: _, + data: _, + } + ); + define_pattern_predicate!(is_thread_id, Node::ThreadID { control: _ }); + define_pattern_predicate!( + is_collect, + Node::Collect { + control: _, + data: _, + } + ); + define_pattern_predicate!( + is_return, + Node::Return { control: _, data: _, - } = self - { - true - } else { - false } + ); + define_pattern_predicate!(is_match, Node::Match { control: _, sum: _ }); + + /* + * ReadProd nodes can be considered control when following an if or match + * node. However, it is sometimes useful to exclude such nodes when + * considering control nodes. + */ + pub fn is_strictly_control(&self) -> bool { + self.is_start() + || self.is_region() + || self.is_if() + || self.is_fork() + || self.is_join() + || self.is_return() + || self.is_return() } pub fn upper_case_name(&self) -> &'static str { diff --git a/hercules_ir/src/parse.rs b/hercules_ir/src/parse.rs index fcabe771c6e42e10e7d0784785ea6a6b5a344ddb..8b666be06427f2e3e6436e163851d7360277847d 100644 --- a/hercules_ir/src/parse.rs +++ b/hercules_ir/src/parse.rs @@ -638,7 +638,10 @@ fn parse_type<'a>(ir_text: &'a str, context: &RefCell<Context<'a>>) -> nom::IRes nom::character::complete::char(','), nom::character::complete::multispace0, )), - |x| parse_dynamic_constant_id(x, context), + |x| { + let (ir_text, node) = parse_identifier(x)?; + Ok((ir_text, context.borrow_mut().get_node_id(node))) + }, ), nom::character::complete::multispace0, nom::character::complete::char(')'), diff --git a/hercules_ir/src/typecheck.rs b/hercules_ir/src/typecheck.rs index 6722a66dbaa0aff09ad8d5dcf254ff4ac11c24bb..137f4411ef08153a34b6fe48b52a9156a3618710 100644 --- a/hercules_ir/src/typecheck.rs +++ b/hercules_ir/src/typecheck.rs @@ -8,7 +8,7 @@ use self::TypeSemilattice::*; /* * Enum for type semilattice. */ -#[derive(Eq, Clone)] +#[derive(Eq, Clone, Debug)] enum TypeSemilattice { Unconstrained, Concrete(TypeID), @@ -25,6 +25,9 @@ impl TypeSemilattice { } } +/* Define custom PartialEq, so that dataflow will terminate right away if there + * are errors. + */ impl PartialEq for TypeSemilattice { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -39,7 +42,6 @@ impl PartialEq for TypeSemilattice { impl Semilattice for TypeSemilattice { fn meet(a: &Self, b: &Self) -> Self { match (a, b) { - (Unconstrained, Unconstrained) => Unconstrained, (Unconstrained, b) => b.clone(), (a, Unconstrained) => a.clone(), (Concrete(id1), Concrete(id2)) => { @@ -248,7 +250,10 @@ fn typeflow( inputs[0].clone() } - Node::Fork { control: _, factor } => { + Node::Fork { + control: _, + factor: _, + } => { if inputs.len() != 1 { return Error(String::from("Fork node must have exactly one input.")); } @@ -257,7 +262,7 @@ fn typeflow( if let Type::Control(factors) = &types[id.idx()] { // Fork adds a new factor to the thread spawn factor list. let mut new_factors = factors.clone().into_vec(); - new_factors.push(*factor); + new_factors.push(node_id); // Out type is control type, with the new thread spawn // factor. @@ -291,7 +296,14 @@ fn typeflow( return Error(String::from("Join node's first input must have a control type with at least one thread replication factor.")); } let mut new_factors = factors.clone().into_vec(); - join_factor_map.insert(node_id, new_factors.pop().unwrap()); + let factor = if let Node::Fork { control: _, factor } = + function.nodes[new_factors.pop().unwrap().idx()] + { + factor + } else { + panic!("Node ID in factor list doesn't correspond with a fork node."); + }; + join_factor_map.insert(node_id, factor); // Out type is the new control type. let control_out_id = get_type_id( diff --git a/hercules_ir/src/verify.rs b/hercules_ir/src/verify.rs index 89192c01a48cc58d14180bb88f70faeab3f787c4..262f749ebee5c9a26c3e3f06e0763c847b939e0d 100644 --- a/hercules_ir/src/verify.rs +++ b/hercules_ir/src/verify.rs @@ -1,5 +1,6 @@ extern crate bitvec; +use std::collections::HashMap; use std::iter::zip; use verify::bitvec::prelude::*; @@ -11,7 +12,7 @@ use crate::*; * 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<ModuleTyping, String> { +pub fn verify(module: &mut Module) -> Result<(ModuleTyping, Vec<DomTree>, Vec<DomTree>), String> { let def_uses: Vec<_> = module .functions .iter() @@ -32,20 +33,37 @@ pub fn verify(module: &mut Module) -> Result<ModuleTyping, String> { verify_structure(function, def_use, typing, &module.types)?; } - // Check SSA, fork, and join dominance relations. - for (function, def_use) in zip(module.functions.iter(), def_uses) { - let subgraph = control_subgraph(function, &def_use); + // Check SSA, fork, and join dominance relations. Collect domtrees. + let mut doms = vec![]; + let mut postdoms = vec![]; + for ((function, typing), (def_use, reverse_postorder)) in zip( + zip(module.functions.iter(), typing.iter()), + zip(def_uses.iter(), reverse_postorders.iter()), + ) { + let control_output_dependencies = + forward_dataflow(function, reverse_postorder, |inputs, id| { + control_output_flow(inputs, id, function) + }); + let subgraph = control_subgraph(function, def_use); let dom = dominator(&subgraph, NodeID::new(0)); let postdom = postdominator(subgraph, NodeID::new(function.nodes.len())); - println!("{:?}", dom); - println!("{:?}", postdom); + verify_dominance_relationships( + function, + typing, + &module.types, + &control_output_dependencies, + &dom, + &postdom, + )?; + doms.push(dom); + postdoms.push(postdom); } - Ok(typing) + Ok((typing, doms, postdoms)) } /* - * There are structural constraints the IR must follow, such as all Phi nodes' + * 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. */ @@ -161,5 +179,124 @@ fn verify_structure( _ => {} }; } + + 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, + typing: &Vec<TypeID>, + types: &Vec<Type>, + control_output_dependencies: &Vec<UnionNodeSet>, + dom: &DomTree, + postdom: &DomTree, +) -> Result<(), String> { + let mut fork_join_map = HashMap::new(); + for idx in 0..function.nodes.len() { + match function.nodes[idx] { + // Verify that joins are dominated by their corresponding forks. At + // the same time, assemble a map from forks to their corresponding + // joins. + Node::Join { control } => { + // Check type of control predecessor. The last node ID + // in the factor list is the corresponding fork node ID. + if let Type::Control(factors) = &types[typing[control.idx()].idx()] { + let join_id = NodeID::new(idx); + let fork_id = *factors.last().unwrap(); + if !dom.does_dom(fork_id, join_id) { + Err(format!("Fork node (ID {}) doesn't dominate its corresponding join node (ID {}).", fork_id.idx(), join_id.idx()))?; + } + if !postdom.does_dom(join_id, fork_id) { + Err(format!("Join node (ID {}) doesn't postdominate its corresponding fork node (ID {}).", join_id.idx(), fork_id.idx()))?; + } + fork_join_map.insert(fork_id, join_id); + } else { + panic!("Join node's control predecessor has a non-control type."); + } + } + _ => {} + } + } + + // Loop over the nodes twice, since we need to completely assemble the + // fork_join_map in the first loop before using it in this second loop. + for idx in 0..function.nodes.len() { + // 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. + let this_id = if let Node::Phi { + control: dominated_control, + data: _, + } + | Node::ThreadID { + control: dominated_control, + } + | Node::Collect { + control: dominated_control, + data: _, + } = function.nodes[idx] + { + dominated_control + } else { + NodeID::new(idx) + }; + + // 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()]); + } + 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 / collect nodes are dominated + // by the corresponding region / join nodes, respectively. + Node::Phi { control, data: _ } | Node::Collect { control, data: _ } => { + if dom.is_non_root(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, + idx + ))?; + } + } + // Verify that uses of thread ID nodes are dominated by the + // corresponding fork nodes. + Node::ThreadID { control } => { + if dom.is_non_root(this_id) && !dom.does_dom(control, this_id) { + Err(format!( + "ThreadID node (ID {}) doesn't dominate its use (ID {}).", + pred_idx, 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.is_non_root(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, idx))?; + } + } + _ => {} + } + } + } + } + Ok(()) } diff --git a/hercules_tools/src/hercules_dot/main.rs b/hercules_tools/src/hercules_dot/main.rs index 226f91db9121ef0d02cb61c9c520f2b3596ce086..c943472fa06ebde732b7a8f4a3e4da5f88bed3e5 100644 --- a/hercules_tools/src/hercules_dot/main.rs +++ b/hercules_tools/src/hercules_dot/main.rs @@ -28,8 +28,8 @@ fn main() { .expect("PANIC: Unable to read input file contents."); let mut module = hercules_ir::parse::parse(&contents).expect("PANIC: Failed to parse Hercules IR file."); - let _types = hercules_ir::verify::verify(&mut module) - .expect("PANIC: Failed to typecheck Hercules IR module."); + let (_types, _doms, _postdoms) = hercules_ir::verify::verify(&mut module) + .expect("PANIC: Failed to verify Hercules IR module."); if args.output.is_empty() { let mut tmp_path = temp_dir(); tmp_path.push("hercules_dot.dot");