diff --git a/src/asm/aarch64/phi_lowering.rs b/src/asm/aarch64/phi_lowering.rs index 8e2c716..50c2d70 100644 --- a/src/asm/aarch64/phi_lowering.rs +++ b/src/asm/aarch64/phi_lowering.rs @@ -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 diff --git a/src/common.rs b/src/common.rs index e5cbbf6..1bff7f8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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; diff --git a/src/common/cfg.rs b/src/common/cfg.rs new file mode 100644 index 0000000..82dedec --- /dev/null +++ b/src/common/cfg.rs @@ -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 { + Some(self.label.key()) + } + + fn successors( + &self, + idx: usize, + num_nodes: usize, + label_map: &HashMap, + ) -> Vec { + 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, + 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 = 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 + } +} diff --git a/src/opt.rs b/src/opt.rs index 487a9f4..16975a5 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -1,6 +1,5 @@ //! Post-IR optimization: the [`Optimizer`] and its function-level passes. -pub mod cfg; mod dominator; mod mem2reg; @@ -51,7 +50,6 @@ 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); @@ -59,7 +57,7 @@ impl Generator for Optimizer<'_> { Ok(()) } - /// Emit the (now-optimised) IR. + /// Emit the optimised IR module to `w`. fn output(&self, w: &mut W) -> Result<(), Self::Error> { IrPrinter::new(w).emit_module(self.module, self.registry) } diff --git a/src/opt/cfg.rs b/src/opt/cfg.rs deleted file mode 100644 index 3d1ed40..0000000 --- a/src/opt/cfg.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::common::graph::{CfgNode, Graph}; -use crate::ir::function::{BasicBlock, BlockLabel}; -use crate::ir::stmt::StmtInner; -use std::collections::HashMap; - -impl CfgNode for BasicBlock { - fn label(&self) -> Option { - Some(self.label.key()) - } - - fn successors( - &self, - idx: usize, - num_nodes: usize, - label_map: &HashMap, - ) -> Vec { - 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() - } - } - } - } -} - -pub struct Cfg { - labels: Vec, - graph: Graph, -} - -impl Cfg { - pub fn from_blocks(blocks: &[BasicBlock]) -> Self { - let labels: Vec = blocks.iter().map(|b| b.label.clone()).collect(); - let graph = Graph::from_nodes(blocks); - Self { labels, graph } - } - - pub fn graph(&self) -> &Graph { - &self.graph - } - - pub fn num_blocks(&self) -> usize { - self.graph.num_nodes() - } - - pub fn successors(&self, block: usize) -> &[usize] { - self.graph.successors(block) - } - - pub fn predecessors(&self, block: usize) -> &[usize] { - self.graph.predecessors(block) - } - - pub fn label(&self, block: usize) -> &BlockLabel { - &self.labels[block] - } - - pub fn labels(&self) -> &[BlockLabel] { - &self.labels - } -} diff --git a/src/opt/dominator.rs b/src/opt/dominator.rs index e036f20..355fa4c 100644 --- a/src/opt/dominator.rs +++ b/src/opt/dominator.rs @@ -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; @@ -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], succs: &[Vec]) -> Vec> { let n = succs.len(); if n == 0 { @@ -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, diff --git a/src/opt/mem2reg.rs b/src/opt/mem2reg.rs index 0b6b5ae..7046c6c 100644 --- a/src/opt/mem2reg.rs +++ b/src/opt/mem2reg.rs @@ -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}; @@ -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); @@ -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 { let mut candidates = HashSet::new(); for stmt in blocks.iter().flat_map(|block| block.stmts.iter()) {