dfg,body: implement simple dfg + body

This commit is contained in:
Igor kehrazy 2026-04-06 23:14:38 +03:00
parent 38d2bc6e45
commit 73dd1215a8
5 changed files with 772 additions and 0 deletions

View file

@ -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.

167
src/ir/body.rs Normal file
View file

@ -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<Block> {
self.layout.first_block()
}
/// Returns the last block in layout order, if any.
pub fn last_block(&self) -> Option<Block> {
self.layout.last_block()
}
/// Returns the last instruction in `block`, if any.
pub fn last_inst(&self, block: Block) -> Option<Inst> {
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<Inst> {
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<Block>),
/// Exactly two successors.
Two {
first: Option<Block>,
second: Option<Block>,
},
}
impl Iterator for BlockSuccessors {
type Item = Block;
fn next(&mut self) -> Option<Self::Item> {
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()
}
}
}
}
}

View file

@ -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<Block, BlockData>,
/// All instructions in the body.
pub insts: PrimaryMap<Inst, InstData>,
/// All SSA values in the body.
pub values: PrimaryMap<Value, ValueData>,
/// Pooled storage for compact value lists.
pub value_lists: ListPool<Value>,
}
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 instructions 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<Value> {
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
}
}

211
src/ir/layout.rs Normal file
View file

@ -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<Block>,
/// The next block in layout order.
pub next: PackedOption<Block>,
/// The first instruction in this block, if any.
pub first_inst: PackedOption<Inst>,
/// The last instruction in this block, if any.
pub last_inst: PackedOption<Inst>,
}
/// Structural layout information for one instruction.
#[derive(Copy, Clone, Debug, Default)]
pub struct InstNode {
/// The block containing this instruction.
pub block: PackedOption<Block>,
/// The previous instruction in the containing block.
pub prev: PackedOption<Inst>,
/// The next instruction in the containing block.
pub next: PackedOption<Inst>,
}
/// Program order and block membership for a body.
#[derive(Default)]
pub struct Layout {
/// The first block in layout order.
pub first_block: PackedOption<Block>,
/// The last block in layout order.
pub last_block: PackedOption<Block>,
/// Per-block layout links.
pub blocks: SecondaryMap<Block, BlockNode>,
/// Per-instruction layout links.
pub insts: SecondaryMap<Inst, InstNode>,
}
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<Block> {
self.first_block.expand()
}
/// Returns the last block in layout order.
pub fn last_block(&self) -> Option<Block> {
self.last_block.expand()
}
/// Returns the previous block before `block`, if any.
pub fn prev_block(&self, block: Block) -> Option<Block> {
self.blocks[block].prev.expand()
}
/// Returns the next block after `block`, if any.
pub fn next_block(&self, block: Block) -> Option<Block> {
self.blocks[block].next.expand()
}
/// Returns the first instruction in `block`, if any.
pub fn first_inst(&self, block: Block) -> Option<Inst> {
self.blocks[block].first_inst.expand()
}
/// Returns the last instruction in `block`, if any.
pub fn last_inst(&self, block: Block) -> Option<Inst> {
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<Block> {
self.insts[inst].block.expand()
}
/// Returns the previous instruction before `inst` in its containing block.
pub fn prev_inst(&self, inst: Inst) -> Option<Inst> {
self.insts[inst].prev.expand()
}
/// Returns the next instruction after `inst` in its containing block.
pub fn next_inst(&self, inst: Inst) -> Option<Inst> {
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<Block>,
}
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<Self::Item> {
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<Inst>,
}
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<Self::Item> {
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)
}
}

216
src/ir/petgraph.rs Normal file
View file

@ -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<Block>,
}
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<Self::Item> {
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::Item> {
self.iter.next()
}
}
// TODO: cached predecessor map?
/// Iterator over incoming CFG neighbors.
#[derive(Clone)]
pub struct InNeighbors<'a> {
body: &'a Body,
cur: Option<Block>,
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<Self::Item> {
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<Self::Item> {
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<Block>;
fn visit_map(&self) -> Self::Map {
HashSet::with_capacity(self.body.block_count())
}
fn reset_map(&self, map: &mut Self::Map) {
map.clear();
}
}