Function verification should be complete, no tests yet
[bigint-presentation-code.git] / register_allocator / src / function.rs
index f9eacec07e9b3ea1a136006dcc11bdadf2c41582..2a1e811b88f0ea377f52743af3aeae5300ac76e1 100644 (file)
 use crate::{
     error::{Error, Result},
     index::{BlockIdx, InstIdx, InstRange, SSAValIdx},
-    interned::Interned,
-    loc::{Loc, Ty},
+    interned::{GlobalState, Intern, Interned},
+    loc::{BaseTy, Loc, Ty},
     loc_set::LocSet,
 };
 use core::fmt;
+use hashbrown::HashSet;
+use petgraph::{
+    algo::dominators,
+    visit::{GraphBase, GraphProp, IntoNeighbors, VisitMap, Visitable},
+    Directed,
+};
 use serde::{Deserialize, Serialize};
-use std::ops::Index;
+use smallvec::SmallVec;
+use std::{
+    collections::{btree_map, BTreeMap, BTreeSet},
+    mem,
+    ops::Index,
+};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
+pub enum SSAValDef {
+    BlockParam { block: BlockIdx, param_idx: usize },
+    Operand { inst: InstIdx, operand_idx: usize },
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)]
+pub struct BranchSuccParamUse {
+    branch_inst: InstIdx,
+    succ: BlockIdx,
+    param_idx: usize,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)]
+pub struct OperandUse {
+    inst: InstIdx,
+    operand_idx: usize,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub struct SSAVal {
     pub ty: Ty,
+    pub def: SSAValDef,
+    pub operand_uses: BTreeSet<OperandUse>,
+    pub branch_succ_param_uses: BTreeSet<BranchSuccParamUse>,
+}
+
+impl SSAVal {
+    fn validate(&self, ssa_val_idx: SSAValIdx, func: &FnFields) -> Result<()> {
+        let Self {
+            ty: _,
+            def,
+            operand_uses,
+            branch_succ_param_uses,
+        } = self;
+        match *def {
+            SSAValDef::BlockParam { block, param_idx } => {
+                let block_param = func.try_get_block_param(block, param_idx)?;
+                if ssa_val_idx != block_param {
+                    return Err(Error::MismatchedBlockParamDef {
+                        ssa_val_idx,
+                        block,
+                        param_idx,
+                    });
+                }
+            }
+            SSAValDef::Operand { inst, operand_idx } => {
+                let operand = func.try_get_operand(inst, operand_idx)?;
+                if ssa_val_idx != operand.ssa_val {
+                    return Err(Error::SSAValDefIsNotOperandsSSAVal {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+        }
+        for &OperandUse { inst, operand_idx } in operand_uses {
+            let operand = func.try_get_operand(inst, operand_idx)?;
+            if ssa_val_idx != operand.ssa_val {
+                return Err(Error::SSAValUseIsNotOperandsSSAVal {
+                    ssa_val_idx,
+                    inst,
+                    operand_idx,
+                });
+            }
+        }
+        for &BranchSuccParamUse {
+            branch_inst,
+            succ,
+            param_idx,
+        } in branch_succ_param_uses
+        {
+            if ssa_val_idx != func.try_get_branch_target_param(branch_inst, succ, param_idx)? {
+                return Err(Error::MismatchedBranchTargetBlockParamUse {
+                    ssa_val_idx,
+                    branch_inst,
+                    tgt_block: succ,
+                    param_idx,
+                });
+            }
+        }
+        Ok(())
+    }
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)]
@@ -107,35 +199,236 @@ pub enum Constraint {
     /// any stack location
     Stack,
     FixedLoc(Loc),
-    Reuse(usize),
+}
+
+impl Constraint {
+    pub fn is_any(&self) -> bool {
+        matches!(self, Self::Any)
+    }
+}
+
+impl Default for Constraint {
+    fn default() -> Self {
+        Self::Any
+    }
+}
+
+#[derive(
+    Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize, Default,
+)]
+#[serde(try_from = "OperandKind", into = "OperandKind")]
+pub struct OperandKindDefOnly;
+
+impl TryFrom<OperandKind> for OperandKindDefOnly {
+    type Error = Error;
+
+    fn try_from(value: OperandKind) -> Result<Self, Self::Error> {
+        match value {
+            OperandKind::Use => Err(Error::OperandKindMustBeDef),
+            OperandKind::Def => Ok(Self),
+        }
+    }
+}
+
+impl From<OperandKindDefOnly> for OperandKind {
+    fn from(_value: OperandKindDefOnly) -> Self {
+        Self::Def
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum KindAndConstraint {
+    Reuse {
+        kind: OperandKindDefOnly,
+        reuse_operand_idx: usize,
+    },
+    Constraint {
+        kind: OperandKind,
+        #[serde(default, skip_serializing_if = "Constraint::is_any")]
+        constraint: Constraint,
+    },
+}
+
+impl KindAndConstraint {
+    pub fn kind(self) -> OperandKind {
+        match self {
+            Self::Reuse { .. } => OperandKind::Def,
+            Self::Constraint { kind, .. } => kind,
+        }
+    }
+    pub fn is_reuse(self) -> bool {
+        matches!(self, Self::Reuse { .. })
+    }
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub struct Operand {
     pub ssa_val: SSAValIdx,
-    pub constraint: Constraint,
-    pub kind: OperandKind,
+    #[serde(flatten)]
+    pub kind_and_constraint: KindAndConstraint,
     pub stage: InstStage,
 }
 
+impl Operand {
+    pub fn try_get_reuse_src<'f>(
+        &self,
+        inst: InstIdx,
+        func: &'f FnFields,
+    ) -> Result<Option<&'f Operand>> {
+        if let KindAndConstraint::Reuse {
+            reuse_operand_idx, ..
+        } = self.kind_and_constraint
+        {
+            Ok(Some(func.try_get_operand(inst, reuse_operand_idx)?))
+        } else {
+            Ok(None)
+        }
+    }
+    pub fn try_constraint(&self, inst: InstIdx, func: &FnFields) -> Result<Constraint> {
+        Ok(match self.kind_and_constraint {
+            KindAndConstraint::Reuse {
+                kind: _,
+                reuse_operand_idx,
+            } => {
+                let operand = func.try_get_operand(inst, reuse_operand_idx)?;
+                match operand.kind_and_constraint {
+                    KindAndConstraint::Reuse { .. }
+                    | KindAndConstraint::Constraint {
+                        kind: OperandKind::Def,
+                        ..
+                    } => {
+                        return Err(Error::ReuseTargetOperandMustBeUse {
+                            inst,
+                            reuse_target_operand_idx: reuse_operand_idx,
+                        })
+                    }
+                    KindAndConstraint::Constraint {
+                        kind: OperandKind::Use,
+                        constraint,
+                    } => constraint,
+                }
+            }
+            KindAndConstraint::Constraint { constraint, .. } => constraint,
+        })
+    }
+    pub fn constraint(&self, inst: InstIdx, func: &Function) -> Constraint {
+        self.try_constraint(inst, func).unwrap()
+    }
+    fn validate(
+        self,
+        _block: BlockIdx,
+        inst: InstIdx,
+        operand_idx: usize,
+        func: &FnFields,
+        global_state: &GlobalState,
+    ) -> Result<()> {
+        let Self {
+            ssa_val: ssa_val_idx,
+            kind_and_constraint,
+            stage: _,
+        } = self;
+        let ssa_val = func.try_get_ssa_val(ssa_val_idx)?;
+        match kind_and_constraint.kind() {
+            OperandKind::Use => {
+                if !ssa_val
+                    .operand_uses
+                    .contains(&OperandUse { inst, operand_idx })
+                {
+                    return Err(Error::MissingOperandUse {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+            OperandKind::Def => {
+                let def = SSAValDef::Operand { inst, operand_idx };
+                if ssa_val.def != def {
+                    return Err(Error::OperandDefIsNotSSAValDef {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+        }
+        let constraint = self.try_constraint(inst, func)?;
+        match constraint {
+            Constraint::Any | Constraint::Stack => {}
+            Constraint::BaseGpr | Constraint::SVExtra2SGpr => {
+                if ssa_val.ty != Ty::scalar(BaseTy::Bits64) {
+                    return Err(Error::ConstraintTyMismatch {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+            Constraint::SVExtra2VGpr | Constraint::SVExtra3Gpr => {
+                if ssa_val.ty.base_ty != BaseTy::Bits64 {
+                    return Err(Error::ConstraintTyMismatch {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+            Constraint::FixedLoc(loc) => {
+                if func
+                    .try_get_inst(inst)?
+                    .clobbers
+                    .clone()
+                    .conflicts_with(loc, global_state)
+                {
+                    return Err(Error::FixedLocConflictsWithClobbers { inst, operand_idx });
+                }
+                if ssa_val.ty != loc.ty() {
+                    return Err(Error::ConstraintTyMismatch {
+                        ssa_val_idx,
+                        inst,
+                        operand_idx,
+                    });
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+/// copy concatenates all `srcs` together and de-concatenates the result into all `dests`.
 #[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
-pub struct BranchSucc {
-    pub block: BlockIdx,
-    pub params: Vec<SSAValIdx>,
+pub struct CopyInstKind {
+    pub src_operand_idxs: Vec<usize>,
+    pub dest_operand_idxs: Vec<usize>,
+    pub copy_ty: Ty,
+}
+
+impl CopyInstKind {
+    fn calc_copy_ty(operand_idxs: &[usize], inst: InstIdx, func: &FnFields) -> Result<Option<Ty>> {
+        let mut retval: Option<Ty> = None;
+        for &operand_idx in operand_idxs {
+            let operand = func.try_get_operand(inst, operand_idx)?;
+            let ssa_val = func.try_get_ssa_val(operand.ssa_val)?;
+            retval = Some(match retval {
+                Some(retval) => retval.try_concat(ssa_val.ty)?,
+                None => ssa_val.ty,
+            });
+        }
+        Ok(retval)
+    }
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
+pub struct BlockTermInstKind {
+    pub succs_and_params: BTreeMap<BlockIdx, Vec<SSAValIdx>>,
 }
 
 #[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub enum InstKind {
     Normal,
-    /// copy concatenates all `srcs` together and de-concatenates the result into all `dests`.
-    Copy {
-        srcs: Vec<Operand>,
-        dests: Vec<Operand>,
-    },
-    Return,
-    Branch {
-        succs: Vec<BranchSucc>,
-    },
+    Copy(CopyInstKind),
+    BlockTerm(BlockTermInstKind),
 }
 
 impl InstKind {
@@ -143,13 +436,21 @@ impl InstKind {
         matches!(self, Self::Normal)
     }
     pub fn is_block_term(&self) -> bool {
-        matches!(self, Self::Return | Self::Branch { .. })
+        matches!(self, Self::BlockTerm { .. })
+    }
+    pub fn is_copy(&self) -> bool {
+        matches!(self, Self::Copy { .. })
     }
-    pub fn succs(&self) -> Option<&[BranchSucc]> {
+    pub fn block_term(&self) -> Option<&BlockTermInstKind> {
         match self {
-            InstKind::Normal | InstKind::Copy { .. } => None,
-            InstKind::Return => Some(&[]),
-            InstKind::Branch { succs } => Some(succs),
+            InstKind::BlockTerm(v) => Some(v),
+            _ => None,
+        }
+    }
+    pub fn copy(&self) -> Option<&CopyInstKind> {
+        match self {
+            InstKind::Copy(v) => Some(v),
+            _ => None,
         }
     }
 }
@@ -160,19 +461,207 @@ impl Default for InstKind {
     }
 }
 
+fn loc_set_is_empty(clobbers: &Interned<LocSet>) -> bool {
+    clobbers.is_empty()
+}
+
+fn empty_loc_set() -> Interned<LocSet> {
+    GlobalState::get(|global_state| LocSet::default().into_interned(global_state))
+}
+
 #[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub struct Inst {
     #[serde(default, skip_serializing_if = "InstKind::is_normal")]
     pub kind: InstKind,
     pub operands: Vec<Operand>,
+    #[serde(default = "empty_loc_set", skip_serializing_if = "loc_set_is_empty")]
     pub clobbers: Interned<LocSet>,
 }
 
+impl Inst {
+    fn validate(
+        &self,
+        block: BlockIdx,
+        inst: InstIdx,
+        func: &FnFields,
+        global_state: &GlobalState,
+    ) -> Result<()> {
+        let Self {
+            kind,
+            operands,
+            clobbers: _,
+        } = self;
+        let is_at_end_of_block = func.blocks[block].insts.last() == Some(inst);
+        if kind.is_block_term() != is_at_end_of_block {
+            return Err(if is_at_end_of_block {
+                Error::BlocksLastInstMustBeTerm { term_idx: inst }
+            } else {
+                Error::TermInstOnlyAllowedAtBlockEnd { inst_idx: inst }
+            });
+        }
+        for (idx, operand) in operands.iter().enumerate() {
+            operand.validate(block, inst, idx, func, global_state)?;
+        }
+        match kind {
+            InstKind::Normal => {}
+            InstKind::Copy(CopyInstKind {
+                src_operand_idxs,
+                dest_operand_idxs,
+                copy_ty,
+            }) => {
+                let mut seen_dest_operands = SmallVec::<[bool; 16]>::new();
+                seen_dest_operands.resize(operands.len(), false);
+                for &dest_operand_idx in dest_operand_idxs {
+                    let seen_dest_operand = seen_dest_operands.get_mut(dest_operand_idx).ok_or(
+                        Error::OperandIndexOutOfRange {
+                            inst,
+                            operand_idx: dest_operand_idx,
+                        },
+                    )?;
+                    if mem::replace(seen_dest_operand, true) {
+                        return Err(Error::DupCopyDestOperand {
+                            inst,
+                            operand_idx: dest_operand_idx,
+                        });
+                    }
+                }
+                if Some(*copy_ty) != CopyInstKind::calc_copy_ty(&src_operand_idxs, inst, func)? {
+                    return Err(Error::CopySrcTyMismatch { inst });
+                }
+                if Some(*copy_ty) != CopyInstKind::calc_copy_ty(&dest_operand_idxs, inst, func)? {
+                    return Err(Error::CopyDestTyMismatch { inst });
+                }
+            }
+            InstKind::BlockTerm(BlockTermInstKind { succs_and_params }) => {
+                for (&succ_idx, params) in succs_and_params {
+                    let succ = func.try_get_block(succ_idx)?;
+                    if !succ.preds.contains(&block) {
+                        return Err(Error::SrcBlockMissingFromBranchTgtBlocksPreds {
+                            src_block: block,
+                            branch_inst: inst,
+                            tgt_block: succ_idx,
+                        });
+                    }
+                    if succ.params.len() != params.len() {
+                        return Err(Error::BranchSuccParamCountMismatch {
+                            inst,
+                            succ: succ_idx,
+                            block_param_count: succ.params.len(),
+                            branch_param_count: params.len(),
+                        });
+                    }
+                    for (param_idx, (&branch_ssa_val_idx, &block_ssa_val_idx)) in
+                        params.iter().zip(&succ.params).enumerate()
+                    {
+                        let branch_ssa_val = func.try_get_ssa_val(branch_ssa_val_idx)?;
+                        let block_ssa_val = func.try_get_ssa_val(block_ssa_val_idx)?;
+                        if !branch_ssa_val
+                            .branch_succ_param_uses
+                            .contains(&BranchSuccParamUse {
+                                branch_inst: inst,
+                                succ: succ_idx,
+                                param_idx,
+                            })
+                        {
+                            return Err(Error::MissingBranchSuccParamUse {
+                                ssa_val_idx: branch_ssa_val_idx,
+                                inst,
+                                succ: succ_idx,
+                                param_idx,
+                            });
+                        }
+                        if block_ssa_val.ty != branch_ssa_val.ty {
+                            return Err(Error::BranchSuccParamTyMismatch {
+                                inst,
+                                succ: succ_idx,
+                                param_idx,
+                                block_param_ty: block_ssa_val.ty,
+                                branch_param_ty: branch_ssa_val.ty,
+                            });
+                        }
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+    pub fn try_get_operand(&self, inst: InstIdx, operand_idx: usize) -> Result<&Operand> {
+        self.operands
+            .get(operand_idx)
+            .ok_or(Error::OperandIndexOutOfRange { inst, operand_idx })
+    }
+}
+
 #[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub struct Block {
     pub params: Vec<SSAValIdx>,
     pub insts: InstRange,
-    pub preds: Vec<BlockIdx>,
+    pub preds: BTreeSet<BlockIdx>,
+    pub immediate_dominator: Option<BlockIdx>,
+}
+
+impl Block {
+    fn validate(&self, block: BlockIdx, func: &FnFields, global_state: &GlobalState) -> Result<()> {
+        let Self {
+            params,
+            insts,
+            preds,
+            immediate_dominator: _,
+        } = self;
+        const _: () = assert!(BlockIdx::ENTRY_BLOCK.get() == 0);
+        let expected_start = if block == BlockIdx::ENTRY_BLOCK {
+            InstIdx::new(0)
+        } else {
+            func.blocks[block.prev()].insts.end
+        };
+        if insts.start != expected_start {
+            return Err(Error::BlockHasInvalidStart {
+                start: insts.start,
+                expected_start,
+            });
+        }
+        let term_inst_idx = insts.last().ok_or(Error::BlockIsEmpty { block })?;
+        func.insts
+            .get(term_inst_idx.get())
+            .ok_or(Error::BlockEndOutOfRange { end: insts.end })?;
+        if block.get() == func.blocks.len() - 1 && insts.end.get() != func.insts.len() {
+            return Err(Error::InstHasNoBlock { inst: insts.end });
+        }
+        if block == BlockIdx::ENTRY_BLOCK {
+            if !params.is_empty() {
+                return Err(Error::EntryBlockCantHaveParams);
+            }
+            if !preds.is_empty() {
+                return Err(Error::EntryBlockCantHavePreds);
+            }
+        }
+        for inst in *insts {
+            func.insts[inst].validate(block, inst, func, global_state)?;
+        }
+        for (param_idx, &ssa_val_idx) in params.iter().enumerate() {
+            let ssa_val = func.try_get_ssa_val(ssa_val_idx)?;
+            let def = SSAValDef::BlockParam { block, param_idx };
+            if ssa_val.def != def {
+                return Err(Error::MismatchedBlockParamDef {
+                    ssa_val_idx,
+                    block,
+                    param_idx,
+                });
+            }
+        }
+        for &pred in preds {
+            let (term_inst, BlockTermInstKind { succs_and_params }) =
+                func.try_get_block_term_inst_and_kind(pred)?;
+            if !succs_and_params.contains_key(&pred) {
+                return Err(Error::PredMissingFromPredsTermBranchsTargets {
+                    src_block: pred,
+                    branch_inst: term_inst,
+                    tgt_block: block,
+                });
+            }
+        }
+        Ok(())
+    }
 }
 
 validated_fields! {
@@ -182,68 +671,153 @@ validated_fields! {
         pub ssa_vals: Vec<SSAVal>,
         pub insts: Vec<Inst>,
         pub blocks: Vec<Block>,
+        #[serde(skip)]
+        /// map from blocks' start instruction's index to their block index, doesn't contain the entry block
+        pub start_inst_to_block_map: BTreeMap<InstIdx, BlockIdx>,
     }
 }
 
 impl Function {
     pub fn new(fields: FnFields) -> Result<Self> {
+        GlobalState::get(|global_state| Self::new_with_global_state(fields, global_state))
+    }
+    pub fn new_with_global_state(mut fields: FnFields, global_state: &GlobalState) -> Result<Self> {
+        fields.fill_start_inst_to_block_map();
         let FnFields {
             ssa_vals,
-            insts: insts_vec,
+            insts: _,
             blocks,
+            start_inst_to_block_map: _,
         } = &fields;
-        let entry_block = blocks
+        blocks
             .get(BlockIdx::ENTRY_BLOCK.get())
             .ok_or(Error::MissingEntryBlock)?;
-        if !entry_block.params.is_empty() {
-            return Err(Error::EntryBlockCantHaveParams);
-        }
-        if !entry_block.preds.is_empty() {
-            return Err(Error::EntryBlockCantHavePreds);
+        for (idx, block) in blocks.iter().enumerate() {
+            block.validate(BlockIdx::new(idx), &fields, global_state)?;
         }
-        let mut expected_start = InstIdx::new(0);
-        for (block_idx, block) in fields.blocks.iter().enumerate() {
-            let block_idx = BlockIdx::new(block_idx);
-            let Block {
-                params,
-                insts: inst_range,
-                preds,
-            } = block;
-            if inst_range.start != expected_start {
-                return Err(Error::BlockHasInvalidStart {
-                    start: inst_range.start,
-                    expected_start,
+        let dominators = dominators::simple_fast(&fields, BlockIdx::ENTRY_BLOCK);
+        for (idx, block) in blocks.iter().enumerate() {
+            let block_idx = BlockIdx::new(idx);
+            let expected = dominators.immediate_dominator(block_idx);
+            if block.immediate_dominator != expected {
+                return Err(Error::IncorrectImmediateDominator {
+                    block_idx,
+                    found: block.immediate_dominator,
+                    expected,
                 });
             }
-            let Some((term_idx, non_term_inst_range)) = inst_range.split_last() else {
-                return Err(Error::BlockIsEmpty { block: block_idx });
-            };
-            expected_start = inst_range.end;
-            let Some(Inst { kind: term_kind, .. }) = insts_vec.get(term_idx.get()) else {
-                return Err(Error::BlockEndOutOfRange { end: inst_range.end });
-            };
-            if !term_kind.is_block_term() {
-                return Err(Error::BlocksLastInstMustBeTerm { term_idx });
-            }
-            for inst_idx in non_term_inst_range {
-                if insts_vec[inst_idx].kind.is_block_term() {
-                    return Err(Error::TermInstOnlyAllowedAtBlockEnd { inst_idx });
-                }
-            }
         }
-        todo!()
+        for (idx, ssa_val) in ssa_vals.iter().enumerate() {
+            ssa_val.validate(SSAValIdx::new(idx), &fields)?;
+        }
+        Ok(Self(fields))
     }
     pub fn entry_block(&self) -> &Block {
         &self.blocks[0]
     }
-    pub fn block_succs(&self, block: BlockIdx) -> &[BranchSucc] {
+    pub fn block_term_kind(&self, block: BlockIdx) -> &BlockTermInstKind {
         self.insts[self.blocks[block].insts.last().unwrap()]
             .kind
-            .succs()
+            .block_term()
             .unwrap()
     }
 }
 
+impl FnFields {
+    pub fn fill_start_inst_to_block_map(&mut self) {
+        self.start_inst_to_block_map.clear();
+        for (idx, block) in self.blocks.iter().enumerate() {
+            let block_idx = BlockIdx::new(idx);
+            if block_idx != BlockIdx::ENTRY_BLOCK {
+                self.start_inst_to_block_map
+                    .insert(block.insts.start, block_idx);
+            }
+        }
+    }
+    pub fn try_get_ssa_val(&self, idx: SSAValIdx) -> Result<&SSAVal> {
+        self.ssa_vals
+            .get(idx.get())
+            .ok_or(Error::SSAValIdxOutOfRange { idx })
+    }
+    pub fn try_get_inst(&self, idx: InstIdx) -> Result<&Inst> {
+        self.insts
+            .get(idx.get())
+            .ok_or(Error::InstIdxOutOfRange { idx })
+    }
+    pub fn try_get_operand(&self, inst: InstIdx, operand_idx: usize) -> Result<&Operand> {
+        self.try_get_inst(inst)?.try_get_operand(inst, operand_idx)
+    }
+    pub fn try_get_block(&self, idx: BlockIdx) -> Result<&Block> {
+        self.blocks
+            .get(idx.get())
+            .ok_or(Error::BlockIdxOutOfRange { idx })
+    }
+    pub fn try_get_block_param(&self, block: BlockIdx, param_idx: usize) -> Result<SSAValIdx> {
+        self.try_get_block(block)?
+            .params
+            .get(param_idx)
+            .copied()
+            .ok_or(Error::BlockParamIdxOutOfRange { block, param_idx })
+    }
+    pub fn try_get_block_term_inst_idx(&self, block: BlockIdx) -> Result<InstIdx> {
+        self.try_get_block(block)?
+            .insts
+            .last()
+            .ok_or(Error::BlockIsEmpty { block })
+    }
+    pub fn try_get_block_term_inst_and_kind(
+        &self,
+        block: BlockIdx,
+    ) -> Result<(InstIdx, &BlockTermInstKind)> {
+        let term_idx = self.try_get_block_term_inst_idx(block)?;
+        let term_kind = self
+            .try_get_inst(term_idx)?
+            .kind
+            .block_term()
+            .ok_or(Error::BlocksLastInstMustBeTerm { term_idx })?;
+        Ok((term_idx, term_kind))
+    }
+    pub fn try_get_branch_target_params(
+        &self,
+        branch_inst: InstIdx,
+        succ: BlockIdx,
+    ) -> Result<&[SSAValIdx]> {
+        let inst = self.try_get_inst(branch_inst)?;
+        let BlockTermInstKind { succs_and_params } = inst
+            .kind
+            .block_term()
+            .ok_or(Error::InstIsNotBlockTerm { inst: branch_inst })?;
+        Ok(succs_and_params
+            .get(&succ)
+            .ok_or(Error::BranchTargetNotFound {
+                branch_inst,
+                tgt_block: succ,
+            })?)
+    }
+    pub fn try_get_branch_target_param(
+        &self,
+        branch_inst: InstIdx,
+        succ: BlockIdx,
+        param_idx: usize,
+    ) -> Result<SSAValIdx> {
+        Ok(*self
+            .try_get_branch_target_params(branch_inst, succ)?
+            .get(param_idx)
+            .ok_or(Error::BranchTargetParamIdxOutOfRange {
+                branch_inst,
+                tgt_block: succ,
+                param_idx,
+            })?)
+    }
+    pub fn inst_to_block(&self, inst: InstIdx) -> BlockIdx {
+        self.start_inst_to_block_map
+            .range(..=inst)
+            .next_back()
+            .map(|v| *v.1)
+            .unwrap_or(BlockIdx::ENTRY_BLOCK)
+    }
+}
+
 impl Index<SSAValIdx> for Vec<SSAVal> {
     type Output = SSAVal;
 
@@ -267,3 +841,61 @@ impl Index<BlockIdx> for Vec<Block> {
         &self[index.get()]
     }
 }
+
+impl GraphBase for FnFields {
+    type EdgeId = (BlockIdx, BlockIdx);
+    type NodeId = BlockIdx;
+}
+
+pub struct Neighbors<'a> {
+    iter: Option<btree_map::Keys<'a, BlockIdx, Vec<SSAValIdx>>>,
+}
+
+impl Iterator for Neighbors<'_> {
+    type Item = BlockIdx;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        Some(*self.iter.as_mut()?.next()?)
+    }
+}
+
+impl<'a> IntoNeighbors for &'a FnFields {
+    type Neighbors = Neighbors<'a>;
+
+    fn neighbors(self, block_idx: Self::NodeId) -> Self::Neighbors {
+        Neighbors {
+            iter: self
+                .try_get_block_term_inst_and_kind(block_idx)
+                .ok()
+                .map(|(_, BlockTermInstKind { succs_and_params })| succs_and_params.keys()),
+        }
+    }
+}
+
+pub struct VisitedMap(HashSet<BlockIdx>);
+
+impl VisitMap<BlockIdx> for VisitedMap {
+    fn visit(&mut self, block: BlockIdx) -> bool {
+        self.0.insert(block)
+    }
+
+    fn is_visited(&self, block: &BlockIdx) -> bool {
+        self.0.contains(block)
+    }
+}
+
+impl Visitable for FnFields {
+    type Map = VisitedMap;
+
+    fn visit_map(&self) -> Self::Map {
+        VisitedMap(HashSet::new())
+    }
+
+    fn reset_map(&self, map: &mut Self::Map) {
+        map.0.clear();
+    }
+}
+
+impl GraphProp for FnFields {
+    type EdgeType = Directed;
+}