Skip to content
124 changes: 82 additions & 42 deletions compiler/noirc_evaluator/src/brillig/brillig_gen/variable_liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ impl VariableLiveness {
back_edges: LoopMap,
) -> Self {
// First pass, propagate up the live_ins skipping back edges.
self.compute_live_in_recursive(func, func.entry_block(), constants, &back_edges);
self.compute_live_in(func, func.entry_block(), constants, &back_edges);

// Second pass, propagate header live_ins to the loop bodies.
for (back_edge, loop_body) in back_edges {
Expand All @@ -218,54 +218,94 @@ impl VariableLiveness {
/// The variables live at the *beginning* of a block are the variables used by the block,
/// plus the variables used by the successors of the block, minus the variables defined
/// in the block (by definition not alive at the beginning).
fn compute_live_in_recursive(
///
/// This is an iterative implementation to avoid stack overflows on complex programs.
fn compute_live_in(
&mut self,
func: &Function,
block_id: BasicBlockId,
entry_block: BasicBlockId,
constants: &ConstantAllocation,
back_edges: &LoopMap,
) {
let block = &func.dfg[block_id];
let mut live_out = HashSet::default();

// Collect the `live_in` of successors; their union is the `live_in` of the parent.
for successor_id in block.successors() {
// Skip back edges: do not revisit the header of the loop, to avoid infinite recursion.
// Because of this, we will have to revisit this block, to complete its definition of live_out,
// once we know what the live_in of the header is.
if back_edges.contains_key(&BackEdge { start: block_id, header: successor_id }) {
continue;
}
// If we haven't visited this successor yet, dive in.
if !self.live_in.contains_key(&successor_id) {
self.compute_live_in_recursive(func, successor_id, constants, back_edges);
// Each entry is (block_id, processing_state)
// processing_state: false = need to process successors, true = ready to compute live_in
let mut stack = vec![(entry_block, false)];
let mut visited = HashSet::default();

while let Some((block_id, processed)) = stack.pop() {
if processed {
// All successors have been processed, now compute live_in for this block
let block = &func.dfg[block_id];
let mut live_out = HashSet::default();

// Collect the `live_in` of successors; their union is the `live_out` of the parent.
for successor_id in block.successors() {
// Skip back edges: do not revisit the header of the loop
if back_edges.contains_key(&BackEdge { start: block_id, header: successor_id })
{
continue;
}
// Add the live_in of the successor to the union that forms the live_out of the parent.
live_out.extend(
self.live_in
.get(&successor_id)
.expect("live_in for successor should have been calculated")
.iter()
.copied(),
);
}

// Based on the paper mentioned in the module docs, the definition would be:
// live_in[BlockId] = before_def[BlockId] union (live_out[BlockId] - killed[BlockId])

// Variables used in this block, defined in this block or before.
let used = variables_used_in_block(block, &func.dfg);

// Variables defined in this block are not alive at the beginning.
let defined = self.variables_defined_in_block(block_id, &func.dfg, constants);

// Live at the beginning are the variables used, but not defined in this block, plus the ones
// it passes through to its successors, which are used by them, but not defined here.
// (Variables used by successors and defined in this block are part of `live_out`, but not `live_in`).
let live_in =
used.union(&live_out).filter(|v| !defined.contains(v)).copied().collect();

self.live_in.insert(block_id, live_in);
} else {
// First visit: check if we've already processed this block
if !visited.insert(block_id) {
continue;
}

let block = &func.dfg[block_id];

// Check if all successors (except back edges) have been processed
let mut all_successors_processed = true;
let mut unprocessed_successors = Vec::new();

for successor_id in block.successors() {
// Skip back edges
if back_edges.contains_key(&BackEdge { start: block_id, header: successor_id })
{
continue;
}
// If successor hasn't been processed yet, we need to process it first
if !self.live_in.contains_key(&successor_id) {
all_successors_processed = false;
unprocessed_successors.push(successor_id);
}
}

// Push this block back with processed = true (for after successors)
stack.push((block_id, true));
if !all_successors_processed {
// Push unprocessed successors with processed = false
for successor_id in unprocessed_successors {
stack.push((successor_id, false));
}
}
}
// Add the live_in of the successor to the union that forms the live_out of the parent.
// Note that because we skipped the back-edge, we couldn't call `get_live_out` here.
live_out.extend(
self.live_in
.get(&successor_id)
.expect("live_in for successor should have been calculated")
.iter()
.copied(),
);
}

// Based on the paper mentioned in the module docs, the definition would be:
// live_in[BlockId] = before_def[BlockId] union (live_out[BlockId] - killed[BlockId])

// Variables used in this block, defined in this block or before.
let used = variables_used_in_block(block, &func.dfg);

// Variables defined in this block are not alive at the beginning.
let defined = self.variables_defined_in_block(block_id, &func.dfg, constants);

// Live at the beginning are the variables used, but not defined in this block, plus the ones
// it passes through to its successors, which are used by them, but not defined here.
// (Variables used by successors and defined in this block are part of `live_out`, but not `live_in`).
let live_in = used.union(&live_out).filter(|v| !defined.contains(v)).copied().collect();

self.live_in.insert(block_id, live_in);
}

/// Collects all the variables defined in a block, which includes:
Expand Down
Loading