diff --git a/src/ir.rs b/src/ir.rs index 2c677f4..91b3f32 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,11 +1,17 @@ //! Slonik normalized SSA IR. +mod body; mod dfg; mod inst; +mod layout; +mod petgraph; mod ty; +pub use body::*; pub use dfg::*; pub use inst::*; +pub use layout::*; +pub use petgraph::*; pub use ty::*; /// Defines a thin `u32` entity handle. diff --git a/src/ir/body.rs b/src/ir/body.rs new file mode 100644 index 0000000..bd564a9 --- /dev/null +++ b/src/ir/body.rs @@ -0,0 +1,167 @@ +//! The owning container for a normalized Slonik IR body. + +use crate::ir::{Block, DataFlowGraph, Inst, InstructionData, Layout, Type, Value}; + +/// A normalized SSA body. +#[derive(Default)] +pub struct Body { + /// Semantic storage for blocks, instructions, values, and value lists. + pub dfg: DataFlowGraph, + + /// Program order and instruction/block containment. + pub layout: Layout, +} + +impl Body { + /// Creates an empty body. + pub fn new() -> Self { + Self::default() + } + + /// Returns whether the body contains no blocks. + pub fn is_empty(&self) -> bool { + self.dfg.num_blocks() == 0 + } + + /// Returns the number of blocks in the body. + pub fn block_count(&self) -> usize { + self.dfg.num_blocks() + } + + /// Returns the number of instructions in the body. + pub fn inst_count(&self) -> usize { + self.dfg.num_insts() + } + + /// Returns the number of SSA values in the body. + pub fn value_count(&self) -> usize { + self.dfg.num_values() + } + + /// Creates a new block and appends it to the block layout order. + pub fn create_block(&mut self) -> Block { + let block = self.dfg.create_block(); + self.layout.append_block(block); + block + } + + /// Appends a block parameter of type `ty` to `block`. + /// + /// The returned SSA value is defined as a block parameter. + pub fn append_block_param(&mut self, block: Block, ty: Type) -> Value { + self.dfg.append_block_param(block, ty) + } + + /// Creates an instruction with `data`, assigns SSA result values for + /// `result_tys`, and appends the instruction to the end of `block`. + pub fn append_inst( + &mut self, + block: Block, + data: InstructionData, + result_tys: &[Type], + ) -> Inst { + let inst = self.dfg.create_inst(data, result_tys); + self.layout.append_inst(block, inst); + inst + } + + /// Returns the first block in layout order, if any. + pub fn first_block(&self) -> Option { + self.layout.first_block() + } + + /// Returns the last block in layout order, if any. + pub fn last_block(&self) -> Option { + self.layout.last_block() + } + + /// Returns the last instruction in `block`, if any. + pub fn last_inst(&self, block: Block) -> Option { + self.layout.last_inst(block) + } + + /// Returns the terminator instruction of `block`, if the block ends in one. + pub fn block_terminator(&self, block: Block) -> Option { + let inst = self.layout.last_inst(block)?; + self.dfg.inst_data(inst).is_terminator().then_some(inst) + } + + /// Returns the terminator data for `block`, if the block ends in a terminator. + pub fn block_terminator_data(&self, block: Block) -> Option<&InstructionData> { + let inst = self.block_terminator(block)?; + Some(self.dfg.inst_data(inst)) + } + + /// Returns an iterator over the successor blocks of `block`. + /// + /// Successors are derived from the block's terminator instruction. + /// Non-terminating blocks and blocks ending in `return` have no successors. + pub fn block_successors(&self, block: Block) -> BlockSuccessors { + let Some(term) = self.block_terminator_data(block) else { + return BlockSuccessors::Empty; + }; + + match term { + InstructionData::Jump { dst, .. } => BlockSuccessors::One(Some(dst.block)), + + InstructionData::BrIf { + then_dst, else_dst, .. + } => BlockSuccessors::Two { + first: Some(then_dst.block), + second: Some(else_dst.block), + }, + + InstructionData::Return { .. } => BlockSuccessors::Empty, + + _ => BlockSuccessors::Empty, + } + } + + /// Returns the number of CFG successors of `block`. + pub fn block_successor_count(&self, block: Block) -> usize { + self.block_successors(block).count() + } +} + +/// Iterator over the successor blocks of a basic block. +/// +/// This is intentionally tiny because the normalized IR currently has very +/// simple terminators: +/// +/// - `jump` has one successor +/// - `br_if` has two successors +/// - `return` has zero successors +#[derive(Clone, Debug)] +pub enum BlockSuccessors { + /// No successors. + Empty, + + /// Exactly one successor. + One(Option), + + /// Exactly two successors. + Two { + first: Option, + second: Option, + }, +} + +impl Iterator for BlockSuccessors { + type Item = Block; + + fn next(&mut self) -> Option { + match self { + Self::Empty => None, + + Self::One(slot) => slot.take(), + + Self::Two { first, second } => { + if let Some(block) = first.take() { + Some(block) + } else { + second.take() + } + } + } + } +} diff --git a/src/ir/dfg.rs b/src/ir/dfg.rs index 8b13789..26af998 100644 --- a/src/ir/dfg.rs +++ b/src/ir/dfg.rs @@ -1 +1,173 @@ +//! Data-flow storage for Slonik IR. +use cranelift_entity::{ListPool, PrimaryMap}; + +use crate::ir::{Block, Inst, InstructionData, Type, Value, ValueData, ValueDef, ValueList}; + +/// Per-block data owned by the [`DataFlowGraph`]. +#[derive(Clone, Debug, Default)] +pub struct BlockData { + /// The SSA parameters of this block. + pub params: ValueList, +} + +/// Per-instruction data owned by the [`DataFlowGraph`]. +#[derive(Clone, Debug, PartialEq)] +pub struct InstData { + /// The semantic payload of the instruction. + pub data: InstructionData, + + /// The SSA results defined by this instruction. + pub results: ValueList, +} + +/// The semantic storage for a Slonik IR body. +/// +/// The DFG owns all blocks, instructions, and SSA values, but does not own +/// their order. Program order is tracked by [`crate::ir::Layout`]. +#[derive(Default)] +pub struct DataFlowGraph { + /// All blocks in the body. + pub blocks: PrimaryMap, + + /// All instructions in the body. + pub insts: PrimaryMap, + + /// All SSA values in the body. + pub values: PrimaryMap, + + /// Pooled storage for compact value lists. + pub value_lists: ListPool, +} + +impl DataFlowGraph { + /// Creates an empty data-flow graph. + pub fn new() -> Self { + Self::default() + } + + /// Returns the number of blocks in the graph. + pub fn num_blocks(&self) -> usize { + self.blocks.len() + } + + /// Returns the number of instructions in the graph. + pub fn num_insts(&self) -> usize { + self.insts.len() + } + + /// Returns the number of SSA values in the graph. + pub fn num_values(&self) -> usize { + self.values.len() + } + + /// Creates a new empty block. + pub fn create_block(&mut self) -> Block { + self.blocks.push(BlockData::default()) + } + + /// Returns the data for `block`. + pub fn block_data(&self, block: Block) -> &BlockData { + &self.blocks[block] + } + + /// Returns the mutable data for `block`. + pub fn block_data_mut(&mut self, block: Block) -> &mut BlockData { + &mut self.blocks[block] + } + + /// Returns the parameter values of `block`. + pub fn block_params(&self, block: Block) -> &[Value] { + self.blocks[block].params.as_slice(&self.value_lists) + } + + /// Appends a block parameter of type `ty` to `block`. + /// + /// The returned SSA value is defined by `ValueDef::Param(block, index)`. + pub fn append_block_param(&mut self, block: Block, ty: Type) -> Value { + let index = self.blocks[block].params.len(&self.value_lists) as u16; + + let value = self.values.push(ValueData { + ty, + def: ValueDef::Param(block, index), + }); + + self.blocks[block].params.push(value, &mut self.value_lists); + value + } + + /// Creates a pooled value list from `values`. + /// + /// This is mainly useful when constructing variable-arity instructions such + /// as calls or returns. + pub fn make_value_list(&mut self, values: &[Value]) -> ValueList { + ValueList::from_slice(values, &mut self.value_lists) + } + + /// Creates a new instruction with the given semantic payload and result + /// types. + /// + /// One SSA result value is created for each entry in `result_tys`, in order. + /// Those values are recorded as `ValueDef::Result(inst, index)`. + pub fn create_inst(&mut self, data: InstructionData, result_tys: &[Type]) -> Inst { + let inst = self.insts.push(InstData { + data, + results: ValueList::new(), + }); + + let mut results = ValueList::new(); + for (index, ty) in result_tys.iter().copied().enumerate() { + let value = self.values.push(ValueData { + ty, + def: ValueDef::Result(inst, index as u16), + }); + + results.push(value, &mut self.value_lists); + } + + self.insts[inst].results = results; + inst + } + + /// Returns the data for `inst`. + pub fn inst_data(&self, inst: Inst) -> &InstructionData { + &self.insts[inst].data + } + + /// Returns the mutable data for `inst`. + pub fn inst_data_mut(&mut self, inst: Inst) -> &mut InstructionData { + &mut self.insts[inst].data + } + + /// Replaces the semantic payload of `inst`. + /// + /// This does not change the instruction’s existing result values. + pub fn replace_inst_data(&mut self, inst: Inst, data: InstructionData) { + self.insts[inst].data = data; + } + + /// Returns the SSA results defined by `inst`. + pub fn inst_results(&self, inst: Inst) -> &[Value] { + self.insts[inst].results.as_slice(&self.value_lists) + } + + /// Returns the first SSA result of `inst`, if any. + pub fn first_result(&self, inst: Inst) -> Option { + self.insts[inst].results.first(&self.value_lists) + } + + /// Returns the full value record for `value`. + pub fn value_data(&self, value: Value) -> &ValueData { + &self.values[value] + } + + /// Returns the type of `value`. + pub fn value_type(&self, value: Value) -> Type { + self.values[value].ty + } + + /// Returns the definition site of `value`. + pub fn value_def(&self, value: Value) -> ValueDef { + self.values[value].def + } +} diff --git a/src/ir/layout.rs b/src/ir/layout.rs new file mode 100644 index 0000000..26a966a --- /dev/null +++ b/src/ir/layout.rs @@ -0,0 +1,211 @@ +//! Program order and containment for Slonik IR. + +use cranelift_entity::{SecondaryMap, packed_option::PackedOption}; + +use crate::ir::{Block, Inst}; + +/// Structural layout information for one block. +#[derive(Copy, Clone, Debug, Default)] +pub struct BlockNode { + /// The previous block in layout order. + pub prev: PackedOption, + + /// The next block in layout order. + pub next: PackedOption, + + /// The first instruction in this block, if any. + pub first_inst: PackedOption, + + /// The last instruction in this block, if any. + pub last_inst: PackedOption, +} + +/// Structural layout information for one instruction. +#[derive(Copy, Clone, Debug, Default)] +pub struct InstNode { + /// The block containing this instruction. + pub block: PackedOption, + + /// The previous instruction in the containing block. + pub prev: PackedOption, + + /// The next instruction in the containing block. + pub next: PackedOption, +} + +/// Program order and block membership for a body. +#[derive(Default)] +pub struct Layout { + /// The first block in layout order. + pub first_block: PackedOption, + + /// The last block in layout order. + pub last_block: PackedOption, + + /// Per-block layout links. + pub blocks: SecondaryMap, + + /// Per-instruction layout links. + pub insts: SecondaryMap, +} + +impl Layout { + /// Creates an empty layout. + pub fn new() -> Self { + Self::default() + } + + /// Returns whether the layout contains no blocks. + pub fn is_empty(&self) -> bool { + self.first_block.is_none() + } + + /// Appends `block` to the end of the block list. + pub fn append_block(&mut self, block: Block) { + let prev = self.last_block; + + self.blocks[block].prev = prev; + self.blocks[block].next = PackedOption::default(); + + if let Some(prev_block) = prev.expand() { + self.blocks[prev_block].next = block.into(); + } else { + self.first_block = block.into(); + } + + self.last_block = block.into(); + } + + /// Returns the first block in layout order. + pub fn first_block(&self) -> Option { + self.first_block.expand() + } + + /// Returns the last block in layout order. + pub fn last_block(&self) -> Option { + self.last_block.expand() + } + + /// Returns the previous block before `block`, if any. + pub fn prev_block(&self, block: Block) -> Option { + self.blocks[block].prev.expand() + } + + /// Returns the next block after `block`, if any. + pub fn next_block(&self, block: Block) -> Option { + self.blocks[block].next.expand() + } + + /// Returns the first instruction in `block`, if any. + pub fn first_inst(&self, block: Block) -> Option { + self.blocks[block].first_inst.expand() + } + + /// Returns the last instruction in `block`, if any. + pub fn last_inst(&self, block: Block) -> Option { + self.blocks[block].last_inst.expand() + } + + /// Returns the block containing `inst`, if the instruction is laid out. + pub fn inst_block(&self, inst: Inst) -> Option { + self.insts[inst].block.expand() + } + + /// Returns the previous instruction before `inst` in its containing block. + pub fn prev_inst(&self, inst: Inst) -> Option { + self.insts[inst].prev.expand() + } + + /// Returns the next instruction after `inst` in its containing block. + pub fn next_inst(&self, inst: Inst) -> Option { + self.insts[inst].next.expand() + } + + /// Returns whether `inst` is currently in the layout. + pub fn is_inst_inserted(&self, inst: Inst) -> bool { + self.insts[inst].block.is_some() + } + + /// Appends `inst` to the end of `block`. + pub fn append_inst(&mut self, block: Block, inst: Inst) { + let prev = self.blocks[block].last_inst; + + self.insts[inst].block = block.into(); + self.insts[inst].prev = prev; + self.insts[inst].next = PackedOption::default(); + + if let Some(prev_inst) = prev.expand() { + self.insts[prev_inst].next = inst.into(); + } else { + self.blocks[block].first_inst = inst.into(); + } + + self.blocks[block].last_inst = inst.into(); + } +} + +/// Iterator over blocks in layout order. +#[derive(Clone)] +pub struct Blocks<'a> { + layout: &'a Layout, + cur: Option, +} + +impl<'a> Blocks<'a> { + /// Creates a block iterator starting at the first block. + pub fn new(layout: &'a Layout) -> Self { + Self { + layout, + cur: layout.first_block(), + } + } +} + +impl Iterator for Blocks<'_> { + type Item = Block; + + fn next(&mut self) -> Option { + let block = self.cur?; + self.cur = self.layout.next_block(block); + Some(block) + } +} + +/// Iterator over instructions in one block. +#[derive(Clone)] +pub struct Insts<'a> { + layout: &'a Layout, + cur: Option, +} + +impl<'a> Insts<'a> { + /// Creates an instruction iterator starting at the first instruction in `block`. + pub fn new(layout: &'a Layout, block: Block) -> Self { + Self { + layout, + cur: layout.first_inst(block), + } + } +} + +impl Iterator for Insts<'_> { + type Item = Inst; + + fn next(&mut self) -> Option { + let inst = self.cur?; + self.cur = self.layout.next_inst(inst); + Some(inst) + } +} + +impl Layout { + /// Returns an iterator over all blocks in layout order. + pub fn blocks(&self) -> Blocks<'_> { + Blocks::new(self) + } + + /// Returns an iterator over the instructions in `block`. + pub fn insts(&self, block: Block) -> Insts<'_> { + Insts::new(self, block) + } +} diff --git a/src/ir/petgraph.rs b/src/ir/petgraph.rs new file mode 100644 index 0000000..5b9bc1f --- /dev/null +++ b/src/ir/petgraph.rs @@ -0,0 +1,216 @@ +//! [`petgraph`] adapters for Slonik. + +use std::collections::HashSet; + +use cranelift_entity::EntityRef; +use petgraph::{ + Directed, Direction, + visit::{ + GraphBase, GraphProp, GraphRef, IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, + NodeCompactIndexable, NodeCount, NodeIndexable, Visitable, + }, +}; + +use crate::ir::{Block, BlockSuccessors, Body}; + +/// A borrowed control-flow graph view over a [`Body`]. +#[derive(Copy, Clone)] +pub struct CfgView<'a> { + body: &'a Body, +} + +impl<'a> CfgView<'a> { + /// Creates a borrowed CFG view over `body`. + pub const fn new(body: &'a Body) -> Self { + Self { body } + } + + /// Returns the underlying IR body. + pub const fn body(self) -> &'a Body { + self.body + } +} + +impl Body { + /// Returns a borrowed `petgraph`-compatible CFG view. + pub const fn cfg(&self) -> CfgView<'_> { + CfgView::new(self) + } +} + +/// Iterator over blocks in layout order. +#[derive(Clone)] +pub struct Blocks<'a> { + body: &'a Body, + cur: Option, +} + +impl<'a> Blocks<'a> { + fn new(body: &'a Body) -> Self { + Self { + body, + cur: body.layout.first_block.expand(), + } + } +} + +impl Iterator for Blocks<'_> { + type Item = Block; + + fn next(&mut self) -> Option { + let cur = self.cur?; + self.cur = self.body.layout.blocks[cur].next.expand(); + Some(cur) + } +} + +/// Iterator over outgoing CFG neighbors. +#[derive(Clone)] +pub struct OutNeighbors { + iter: BlockSuccessors, +} + +impl OutNeighbors { + fn new(body: &Body, block: Block) -> Self { + Self { + iter: body.block_successors(block), + } + } +} + +impl Iterator for OutNeighbors { + type Item = Block; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +// TODO: cached predecessor map? +/// Iterator over incoming CFG neighbors. +#[derive(Clone)] +pub struct InNeighbors<'a> { + body: &'a Body, + cur: Option, + target: Block, +} + +impl<'a> InNeighbors<'a> { + fn new(body: &'a Body, target: Block) -> Self { + Self { + body, + cur: body.layout.first_block(), + target, + } + } +} + +impl Iterator for InNeighbors<'_> { + type Item = Block; + + fn next(&mut self) -> Option { + while let Some(block) = self.cur { + self.cur = self.body.layout.next_block(block); + + if self + .body + .block_successors(block) + .any(|succ| succ == self.target) + { + return Some(block); + } + } + + None + } +} + +/// Iterator over neighbors in a requested direction. +#[derive(Clone)] +pub enum Neighbors<'a> { + Out(OutNeighbors), + In(InNeighbors<'a>), +} + +impl Iterator for Neighbors<'_> { + type Item = Block; + + fn next(&mut self) -> Option { + match self { + Self::Out(iter) => iter.next(), + Self::In(iter) => iter.next(), + } + } +} + +impl GraphBase for CfgView<'_> { + type NodeId = Block; + type EdgeId = (Block, Block); +} + +impl GraphRef for CfgView<'_> {} + +impl GraphProp for CfgView<'_> { + type EdgeType = Directed; +} + +impl NodeCount for CfgView<'_> { + fn node_count(&self) -> usize { + self.body.block_count() + } +} + +impl NodeIndexable for CfgView<'_> { + fn node_bound(&self) -> usize { + self.body.block_count() + } + + fn to_index(&self, a: Self::NodeId) -> usize { + a.index() + } + + fn from_index(&self, i: usize) -> Self::NodeId { + Block::new(i) + } +} + +impl NodeCompactIndexable for CfgView<'_> {} + +impl<'a> IntoNodeIdentifiers for CfgView<'a> { + type NodeIdentifiers = Blocks<'a>; + + fn node_identifiers(self) -> Self::NodeIdentifiers { + Blocks::new(self.body) + } +} + +impl<'a> IntoNeighbors for CfgView<'a> { + type Neighbors = OutNeighbors; + + fn neighbors(self, a: Self::NodeId) -> Self::Neighbors { + OutNeighbors::new(self.body, a) + } +} + +impl<'a> IntoNeighborsDirected for CfgView<'a> { + type NeighborsDirected = Neighbors<'a>; + + fn neighbors_directed(self, n: Self::NodeId, d: Direction) -> Self::NeighborsDirected { + match d { + Direction::Outgoing => Neighbors::Out(OutNeighbors::new(self.body, n)), + Direction::Incoming => Neighbors::In(InNeighbors::new(self.body, n)), + } + } +} + +impl Visitable for CfgView<'_> { + type Map = HashSet; + + fn visit_map(&self) -> Self::Map { + HashSet::with_capacity(self.body.block_count()) + } + + fn reset_map(&self, map: &mut Self::Map) { + map.clear(); + } +}