Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/asm/aarch64/phi_lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::collections::HashMap;

use crate::ir::function::{BasicBlock, BlockLabel};
use crate::ir::stmt::{PhiStmt, Stmt, StmtInner};
use crate::common::cfg::Cfg;
use crate::ir::Operand;
use crate::opt::cfg::Cfg;

/// Plan for destroying SSA form in one function: the block bodies with
/// their phi nodes stripped and terminators retargeted across split
Expand Down
1 change: 1 addition & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! including target platform detection and a generic code generator trait.

pub mod bitset;
pub mod cfg;
pub mod graph;
pub mod pass;

Expand Down
111 changes: 111 additions & 0 deletions src/common/cfg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! Control-flow graph wrapper over the basic blocks of a function body.
//!
//! This module provides:
//! - the [`CfgNode`] implementation for [`BasicBlock`], deriving successor
//! edges from each block's terminator: [`StmtInner::Jump`] and
//! [`StmtInner::CJump`] target their named blocks, [`StmtInner::Return`]
//! has no successors, and any other (or missing) terminator falls through
//! to the next block in layout order.
//! - [`Cfg`]: a thin wrapper pairing the block labels with the [`Graph`]
//! built from them, so analyses can address blocks by index and map them
//! back to IR labels.
//!
//! `Cfg` lives in `common` — alongside [`Graph`] — because it is consumed
//! by both the middle end ([`crate::opt`]) and the backend (`crate::asm`);
//! keeping it below both layers avoids a backend-to-middle-end dependency.

use super::graph::{CfgNode, Graph};
use crate::ir::function::{BasicBlock, BlockLabel};
use crate::ir::stmt::StmtInner;
use std::collections::HashMap;

/// Marks an IR [`BasicBlock`] as a control-flow graph node so that
/// [`Graph::from_nodes`] can build a graph directly over a function body.
///
/// The block's label serves as its name for branch-target resolution;
/// successor edges are taken from the block's terminator, defaulting to
/// fall-through to the next block in the slice.
impl CfgNode for BasicBlock {
fn label(&self) -> Option<String> {
Some(self.label.key())
}

fn successors(
&self,
idx: usize,
num_nodes: usize,
label_map: &HashMap<String, usize>,
) -> Vec<usize> {
let term = self.stmts.last();

match term.map(|s| &s.inner) {
Some(StmtInner::Jump(j)) => vec![label_map[&j.target.key()]],
Some(StmtInner::CJump(j)) => vec![
label_map[&j.true_label.key()],
label_map[&j.false_label.key()],
],
Some(StmtInner::Return(_)) => Vec::new(),
_ => {
if idx + 1 < num_nodes {
vec![idx + 1]
} else {
Vec::new()
}
}
}
}
}

/// A control-flow graph over the basic blocks of a function body.
///
/// Blocks are addressed by their index in the slice passed to
/// [`Cfg::from_blocks`]; `labels` preserves the corresponding [`BlockLabel`]
/// of each block so that graph results can be mapped back onto the IR.
pub struct Cfg {
labels: Vec<BlockLabel>,
graph: Graph,
}

impl Cfg {
/// Builds a [`Cfg`] from the basic blocks of a function body.
///
/// The order of `blocks` becomes the index space of the graph; edges
/// are derived from each block's terminator (see the [`CfgNode`]
/// implementation for [`BasicBlock`]).
pub fn from_blocks(blocks: &[BasicBlock]) -> Self {
let labels: Vec<BlockLabel> = blocks.iter().map(|b| b.label.clone()).collect();
let graph = Graph::from_nodes(blocks);
Self { labels, graph }
}

/// Returns the underlying [`Graph`], for analyses that work on the raw
/// adjacency lists.
pub fn graph(&self) -> &Graph {
&self.graph
}

/// Returns the number of basic blocks in the graph.
pub fn num_blocks(&self) -> usize {
self.graph.num_nodes()
}

/// Returns the successor indices of `block`.
pub fn successors(&self, block: usize) -> &[usize] {
self.graph.successors(block)
}

/// Returns the predecessor indices of `block`.
pub fn predecessors(&self, block: usize) -> &[usize] {
self.graph.predecessors(block)
}

/// Returns the label of `block`.
pub fn label(&self, block: usize) -> &BlockLabel {
&self.labels[block]
}

/// Returns the labels of all blocks, in index order.
pub fn labels(&self) -> &[BlockLabel] {
&self.labels
}
}
4 changes: 1 addition & 3 deletions src/opt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Post-IR optimization: the [`Optimizer`] and its function-level passes.

pub mod cfg;
mod dominator;
mod mem2reg;

Expand Down Expand Up @@ -51,15 +50,14 @@ impl<'a> Optimizer<'a> {
impl Generator for Optimizer<'_> {
type Error = Error;

/// Run every registered pass against every function in the module.
fn generate(&mut self) -> Result<(), Self::Error> {
for func in self.module.function_list.values_mut() {
self.passes.run(func);
}
Ok(())
}

/// Emit the (now-optimised) IR.
/// Emit the optimised IR module to `w`.
fn output<W: Write>(&self, w: &mut W) -> Result<(), Self::Error> {
IrPrinter::new(w).emit_module(self.module, self.registry)
}
Expand Down
72 changes: 0 additions & 72 deletions src/opt/cfg.rs

This file was deleted.

70 changes: 30 additions & 40 deletions src/opt/dominator.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
//! Dominator analysis over a control-flow graph.
//!
//! [`DominatorInfo`] computes, for a [`Graph`]:
//!
//! - the immediate dominator of every block, using the algorithm from
//! Cooper, Harvey, and Kennedy, "A Simple, Fast Dominance Algorithm"
//! (2001) — queried via [`DominatorInfo::dominates`] and the dominator
//! tree via [`DominatorInfo::dom_children`] and
//! [`DominatorInfo::dom_tree_roots`];
//! - the dominance frontier of every block
//! ([`DominatorInfo::dominance_frontier`]), used by the mem2reg pass to
//! decide where phi functions must be placed.

use crate::common::graph::Graph;
use std::collections::HashSet;

Expand Down Expand Up @@ -51,47 +64,20 @@ impl DominatorInfo {
.filter_map(|(i, idom)| if idom.is_none() { Some(i) } else { None })
}

/// Computes the immediate dominator of every block using the algorithm from
/// Cooper, Harvey, and Kennedy, "A Simple, Fast Dominance Algorithm" (2001).
///
/// **Definition.** Block `d` is the _immediate dominator_ (`idom`) of block
/// `n` if `d` strictly dominates `n` and does not strictly dominate any
/// other strict dominator of `n`. In other words, `idom(n)` is the closest
/// dominator of `n` in the dominator tree. The entry block has no
/// immediate dominator.
///
/// The algorithm works as follows:
///
/// 1. **Reverse postorder (RPO) numbering.**
/// Perform a DFS from the entry block and record blocks in reverse
/// postorder. This guarantees that (in a reducible CFG) every block's
/// dominator appears earlier in the ordering, so one pass usually
/// suffices to reach the fixed point.
///
/// 2. **Initialization.**
/// - `idom(entry) = entry` — a sentinel that anchors the tree.
/// - `idom(n) = None` for all other blocks.
///
/// 3. **Fixed-point iteration.** Traverse every non-entry block `b` in RPO:
/// - Among `b`'s predecessors whose `idom` is already known, pick the
/// first one as a tentative immediate dominator.
/// - Fold the remaining processed predecessors in with the `intersect`
/// helper: given two blocks, `intersect` walks both upward through
/// the current `idom` chain (using RPO indices to decide which side
/// to advance) until they meet. The meeting point is the nearest
/// common dominator of the two blocks.
/// - If the newly computed `idom(b)` differs from the current one,
/// record the change and mark the pass as dirty.
///
/// Repeat until a full pass produces no changes.
///
/// 4. **Clean-up.** Reset `idom(entry) = None`, since the entry block has
/// no true immediate dominator (the sentinel was only needed by the
/// iteration).
/// Computes the immediate dominator (`idom`) of every block with the
/// algorithm of Cooper, Harvey, and Kennedy, "A Simple, Fast Dominance
/// Algorithm" (2001): sweep non-entry blocks in reverse postorder, fold
/// each block's already-processed predecessors into their nearest common
/// dominator via `intersect`, and repeat until a full pass changes
/// nothing.
///
/// **Complexity.** For reducible CFGs the algorithm converges in a single
/// pass, giving O(n) time. In the worst case (irreducible CFGs) it may
/// require O(n²) time, but this is rare in practice.
/// Reverse postorder visits a block's dominators before the block itself
/// in a reducible CFG, so the fixed point is reached after few passes.
/// The `idom[start] = start` sentinel anchors the chain walks in
/// `intersect`; it is reset to `None` after the loop because the entry
/// block has no immediate dominator. Blocks unreachable from `start`
/// never enter the RPO and keep `idom = None`, which is why the
/// dominator tree can have several roots.
fn compute_idom(preds: &[Vec<usize>], succs: &[Vec<usize>]) -> Vec<Option<usize>> {
let n = succs.len();
if n == 0 {
Expand Down Expand Up @@ -137,6 +123,10 @@ impl DominatorInfo {
idom
}

/// Returns the nearest common dominator of `b1` and `b2` by walking both
/// up the `idom` chain until they meet. Every `idom` step lowers a
/// block's RPO number, so advancing the higher-numbered block always
/// steps toward the meeting point.
fn intersect(
mut b1: usize,
mut b2: usize,
Expand Down
30 changes: 20 additions & 10 deletions src/opt/mem2reg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
use super::cfg::Cfg;
//! Mem2Reg: promotes stack-allocated `i32` locals to SSA form.
//!
//! The pass runs on a function body in three stages:
//!
//! - [`AllocaAnalysis`] finds promotable variables: `alloca`s of `*i32`
//! that are only accessed through loads and stores, and whose
//! upward-exposed loads are all dominated by a store.
//! - [`Mem2RegPass::place_phis`] inserts phi functions at the iterated
//! dominance frontier of the stores, pruned by backward liveness so no
//! phi is created where the variable is dead.
//! - [`Renamer`] walks the dominator tree with a per-variable stack of
//! reaching definitions, turning loads into aliases and dropping the
//! promoted allocas, loads, and stores; [`Renamer::finish`] then
//! materialises the phis at the head of each block.

use super::dominator::DominatorInfo;
use super::FunctionPass;
use crate::common::cfg::Cfg;
use crate::common::graph::BackwardLiveness;
use crate::ir::function::{BasicBlock, BlockLabel, Function};
use crate::ir::stmt::{OperandRole, Stmt, StmtInner};
Expand Down Expand Up @@ -92,10 +107,6 @@ struct AllocaAnalysis {

impl AllocaAnalysis {
/// Constructs an `AllocaAnalysis` by scanning all basic blocks.
///
/// First identifies alloca instructions that allocate i32 pointers as
/// promotion candidates, then analyzes their load/store usage patterns
/// across all blocks.
fn from_blocks(blocks: &[BasicBlock]) -> Self {
let candidates = Self::collect_candidates(blocks);
let usage = Self::analyze_usage(blocks, &candidates);
Expand Down Expand Up @@ -152,11 +163,10 @@ impl AllocaAnalysis {
multi_def
}

/// Scans all blocks for alloca instructions that produce `*i32` pointers.
///
/// Returns the set of [`LocalId`]s for these allocas. Only i32 pointer
/// allocas are considered because the current implementation only
/// supports promoting scalar integer values.
/// Scans all blocks for alloca instructions that produce `*i32` pointers
/// and returns the set of their [`LocalId`]s. The pass is `i32`-only
/// because phi placement hardcodes `Dtype::I32` for the promoted
/// temporaries.
fn collect_candidates(blocks: &[BasicBlock]) -> HashSet<LocalId> {
let mut candidates = HashSet::new();
for stmt in blocks.iter().flat_map(|block| block.stmts.iter()) {
Expand Down