Skip to content

Commit 4fcc142

Browse files
authored
Merge 096dcb3 into e70f719
2 parents e70f719 + 096dcb3 commit 4fcc142

File tree

6 files changed

+490
-83
lines changed

6 files changed

+490
-83
lines changed

.github/benchmark_projects.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ projects:
55
ref: *AZ_COMMIT
66
path: noir-projects/noir-protocol-circuits/crates/private-kernel-inner
77
num_runs: 5
8-
compilation-timeout: 2.5
8+
compilation-timeout: 3
99
execution-timeout: 0.08
1010
compilation-memory-limit: 350
1111
execution-memory-limit: 250

compiler/noirc_evaluator/src/ssa/ir/cfg.rs

-5
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ impl ControlFlowGraph {
8989
/// Add a directed edge making `from` a predecessor of `to`.
9090
fn add_edge(&mut self, from: BasicBlockId, to: BasicBlockId) {
9191
let predecessor_node = self.data.entry(from).or_default();
92-
assert!(
93-
predecessor_node.successors.len() < 2,
94-
"ICE: A cfg node cannot have more than two successors"
95-
);
9692
predecessor_node.successors.insert(to);
9793
let successor_node = self.data.entry(to).or_default();
9894
successor_node.predecessors.insert(from);
@@ -125,7 +121,6 @@ impl ControlFlowGraph {
125121
}
126122

127123
/// Reverse the control flow graph
128-
#[cfg(test)]
129124
pub(crate) fn reverse(&self) -> Self {
130125
let mut reversed_cfg = ControlFlowGraph::default();
131126

compiler/noirc_evaluator/src/ssa/ir/dom.rs

+214-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::cmp::Ordering;
99
use super::{
1010
basic_block::BasicBlockId, cfg::ControlFlowGraph, function::Function, post_order::PostOrder,
1111
};
12-
use fxhash::FxHashMap as HashMap;
12+
use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
1313

1414
/// Dominator tree node. We keep one of these per reachable block.
1515
#[derive(Clone, Default)]
@@ -178,10 +178,10 @@ impl DominatorTree {
178178
/// "Simple, Fast Dominator Algorithm."
179179
fn compute_dominator_tree(&mut self, cfg: &ControlFlowGraph, post_order: &PostOrder) {
180180
// We'll be iterating over a reverse post-order of the CFG, skipping the entry block.
181-
let (entry_block_id, entry_free_post_order) = post_order
182-
.as_slice()
183-
.split_last()
184-
.expect("ICE: functions always have at least one block");
181+
let Some((entry_block_id, entry_free_post_order)) = post_order.as_slice().split_last()
182+
else {
183+
return;
184+
};
185185

186186
// Do a first pass where we assign reverse post-order indices to all reachable nodes. The
187187
// entry block will be the only node with no immediate dominator.
@@ -276,18 +276,67 @@ impl DominatorTree {
276276
debug_assert_eq!(block_a_id, block_b_id, "Unreachable block passed to common_dominator?");
277277
block_a_id
278278
}
279+
280+
/// Computes the dominance frontier for all blocks in the dominator tree.
281+
///
282+
/// This method uses the algorithm specified under Cooper, Keith D. et al. “A Simple, Fast Dominance Algorithm.” (1999).
283+
/// As referenced in the paper a dominance frontier is the set of all CFG nodes, y, such that
284+
/// b dominates a predecessor of y but does not strictly d.
285+
///
286+
/// This method expects the appropriate CFG depending on whether we are operating over
287+
/// a dominator tree (standard CFG) or a post-dominator tree (reversed CFG).
288+
/// Calling this method on a dominator tree will return a function's dominance frontiers,
289+
/// while on a post-dominator tree the method will return the function's reverse (or post) dominance frontiers.
290+
pub(crate) fn compute_dominance_frontiers(
291+
&mut self,
292+
cfg: &ControlFlowGraph,
293+
) -> HashMap<BasicBlockId, HashSet<BasicBlockId>> {
294+
let mut dominance_frontiers: HashMap<BasicBlockId, HashSet<BasicBlockId>> =
295+
HashMap::default();
296+
297+
let nodes = self.nodes.keys().copied().collect::<Vec<_>>();
298+
for block_id in nodes {
299+
let predecessors = cfg.predecessors(block_id);
300+
// Dominance frontier nodes must have more than one predecessor
301+
if predecessors.len() > 1 {
302+
// Iterate over the predecessors of the current block
303+
for pred_id in predecessors {
304+
let mut runner = pred_id;
305+
// We start by checking if the current block dominates the predecessor
306+
while let Some(immediate_dominator) = self.immediate_dominator(block_id) {
307+
if immediate_dominator != runner {
308+
dominance_frontiers.entry(runner).or_default().insert(block_id);
309+
let Some(runner_immediate_dom) = self.immediate_dominator(runner)
310+
else {
311+
break;
312+
};
313+
runner = runner_immediate_dom;
314+
} else {
315+
break;
316+
}
317+
}
318+
}
319+
}
320+
}
321+
322+
dominance_frontiers
323+
}
279324
}
280325

281326
#[cfg(test)]
282327
mod tests {
283328
use std::cmp::Ordering;
284329

330+
use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
331+
285332
use crate::ssa::{
286333
function_builder::FunctionBuilder,
287334
ir::{
288-
basic_block::BasicBlockId, call_stack::CallStackId, dom::DominatorTree,
289-
function::Function, instruction::TerminatorInstruction, map::Id, types::Type,
335+
basic_block::BasicBlockId, call_stack::CallStackId, cfg::ControlFlowGraph,
336+
dom::DominatorTree, function::Function, instruction::TerminatorInstruction, map::Id,
337+
post_order::PostOrder, types::Type,
290338
},
339+
ssa_gen::Ssa,
291340
};
292341

293342
#[test]
@@ -542,6 +591,164 @@ mod tests {
542591
assert!(post_dom.dominates(block2_id, block2_id));
543592
}
544593

594+
#[test]
595+
fn dom_frontiers_backwards_layout() {
596+
let (func, ..) = backwards_layout_setup();
597+
let mut dt = DominatorTree::with_function(&func);
598+
599+
let cfg = ControlFlowGraph::with_function(&func);
600+
let dom_frontiers = dt.compute_dominance_frontiers(&cfg);
601+
assert!(dom_frontiers.is_empty());
602+
}
603+
604+
#[test]
605+
fn post_dom_frontiers_backwards_layout() {
606+
let (func, ..) = backwards_layout_setup();
607+
let mut post_dom = DominatorTree::with_function_post_dom(&func);
608+
609+
let cfg = ControlFlowGraph::with_function(&func);
610+
let dom_frontiers = post_dom.compute_dominance_frontiers(&cfg);
611+
assert!(dom_frontiers.is_empty());
612+
}
613+
614+
fn loop_with_conditional() -> Ssa {
615+
let src = "
616+
brillig(inline) predicate_pure fn main f0 {
617+
b0(v1: u32, v2: u32):
618+
v5 = eq v1, u32 5
619+
jmp b1(u32 0)
620+
b1(v3: u32):
621+
v8 = lt v3, u32 4
622+
jmpif v8 then: b2, else: b3
623+
b2():
624+
jmpif v5 then: b4, else: b5
625+
b3():
626+
return
627+
b4():
628+
v9 = mul u32 4294967295, v2
629+
constrain v9 == u32 12
630+
jmp b5()
631+
b5():
632+
v12 = unchecked_add v3, u32 1
633+
jmp b1(v12)
634+
}
635+
";
636+
Ssa::from_str(src).unwrap()
637+
}
638+
639+
#[test]
640+
fn dom_frontiers() {
641+
let ssa = loop_with_conditional();
642+
let main = ssa.main();
643+
644+
let cfg = ControlFlowGraph::with_function(main);
645+
let post_order = PostOrder::with_cfg(&cfg);
646+
647+
let mut dt = DominatorTree::with_cfg_and_post_order(&cfg, &post_order);
648+
let dom_frontiers = dt.compute_dominance_frontiers(&cfg);
649+
dbg!(dom_frontiers.clone());
650+
651+
// Convert BasicBlockIds to their underlying u32 values for easy comparisons
652+
let dom_frontiers = dom_frontiers
653+
.into_iter()
654+
.map(|(block, frontier)| {
655+
(
656+
block.to_u32(),
657+
frontier.into_iter().map(|block| block.to_u32()).collect::<HashSet<_>>(),
658+
)
659+
})
660+
.collect::<HashMap<_, _>>();
661+
662+
// b0 is the entry block which dominates all other blocks
663+
// Thus, it has an empty set for its dominance frontier
664+
assert!(!dom_frontiers.contains_key(&0));
665+
666+
// b1 is in its own DF due to the loop b5 -> b1
667+
// b1 dominates b5 which is a predecessor of b1, but b1 does not strictly dominate b1
668+
let b1_df = &dom_frontiers[&1];
669+
assert_eq!(b1_df.len(), 1);
670+
assert!(b1_df.contains(&1));
671+
672+
// b2 has DF { b1 } also due to the loop b5 -> b1.
673+
// b2 dominates b5 which is a predecessor of b1, but b2 does not strictly dominate b1
674+
let b2_df = &dom_frontiers[&2];
675+
assert_eq!(b2_df.len(), 1);
676+
assert!(b2_df.contains(&1));
677+
678+
// b3 is the exit block which does not dominate any blocks
679+
assert!(!dom_frontiers.contains_key(&3));
680+
681+
// b4 has DF { b5 } because b4 jumps to b5 (thus being a predecessor to b5)
682+
// b4 dominates itself but b5 is not strictly dominated by b4.
683+
let b4_df = &dom_frontiers[&4];
684+
assert_eq!(b4_df.len(), 1);
685+
assert!(b4_df.contains(&5));
686+
687+
// b5 has DF { b1 } also due to the loop b5 -> b1
688+
let b5_df = &dom_frontiers[&5];
689+
assert_eq!(b5_df.len(), 1);
690+
assert!(b5_df.contains(&1));
691+
}
692+
693+
#[test]
694+
fn post_dom_frontiers() {
695+
let ssa = loop_with_conditional();
696+
let main = ssa.main();
697+
698+
let cfg = ControlFlowGraph::with_function(main);
699+
let reversed_cfg = cfg.reverse();
700+
let post_order = PostOrder::with_cfg(&reversed_cfg);
701+
702+
let mut post_dom = DominatorTree::with_cfg_and_post_order(&reversed_cfg, &post_order);
703+
let post_dom_frontiers = post_dom.compute_dominance_frontiers(&reversed_cfg);
704+
dbg!(post_dom_frontiers.clone());
705+
706+
// Convert BasicBlockIds to their underlying u32 values for easy comparisons
707+
let post_dom_frontiers = post_dom_frontiers
708+
.into_iter()
709+
.map(|(block, frontier)| {
710+
(
711+
block.to_u32(),
712+
frontier.into_iter().map(|block| block.to_u32()).collect::<HashSet<_>>(),
713+
)
714+
})
715+
.collect::<HashMap<_, _>>();
716+
717+
// Another way to think about the post-dominator frontier for a node n,
718+
// is that we can reach a block in the PDF during execution without going through n.
719+
720+
// b0 is the entry node of the program and the exit block of the post-dominator tree.
721+
// Thus, it has an empty set for its post-dominance frontier (PDF)
722+
assert!(!post_dom_frontiers.contains_key(&0));
723+
724+
// b1 is in its own PDF due to the loop b5 -> b1
725+
// b1 post-dominates b2 and b5. b1 post-dominates itself but not strictly post-dominate itself.
726+
let b1_pdf = &post_dom_frontiers[&1];
727+
assert_eq!(b1_pdf.len(), 1);
728+
assert!(b1_pdf.contains(&1));
729+
730+
// b2 has DF { b1 } also due to the loop b5 -> b1.
731+
// b1 post-dominates itself is a predecessor of b2, but b1 does not strictly post-dominate b2.
732+
let b2_pdf = &post_dom_frontiers[&2];
733+
assert_eq!(b2_pdf.len(), 1);
734+
assert!(b2_pdf.contains(&1));
735+
736+
// b3 is the exit block of the program, but the starting node of the post-dominator tree
737+
// Thus, it has an empty PDF
738+
assert!(!post_dom_frontiers.contains_key(&3));
739+
740+
// b4 has DF { b2 } because b2 post-dominates itself and is a predecessor to b4.
741+
// b2 does not strictly post-dominate b4.
742+
let b4_pdf = &post_dom_frontiers[&4];
743+
assert_eq!(b4_pdf.len(), 1);
744+
assert!(b4_pdf.contains(&2));
745+
746+
// b5 has DF { b1 } also due to the loop b5 -> b1
747+
let b5_pdf = &post_dom_frontiers[&5];
748+
assert_eq!(b5_pdf.len(), 1);
749+
assert!(b5_pdf.contains(&1));
750+
}
751+
545752
#[test]
546753
fn test_find_map_dominator() {
547754
let (dt, b0, b1, b2, _b3) = unreachable_node_setup();

0 commit comments

Comments
 (0)