From 5cf92f6b80aeae89d9cab21e2e1afb78999f53af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Thu, 25 Jun 2026 19:51:26 +0100 Subject: [PATCH 01/13] refactor: remove unused methods --- crates/jcc_backend/src/ir/analysis/cfg.rs | 17 +++++------- crates/jcc_backend/src/ir/analysis/dom.rs | 30 --------------------- crates/jcc_backend/src/ir/analysis/order.rs | 4 --- crates/jcc_backend/src/ir/builder.rs | 6 ++--- 4 files changed, 10 insertions(+), 47 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/cfg.rs b/crates/jcc_backend/src/ir/analysis/cfg.rs index 67e0271..4abd7e5 100644 --- a/crates/jcc_backend/src/ir/analysis/cfg.rs +++ b/crates/jcc_backend/src/ir/analysis/cfg.rs @@ -5,15 +5,11 @@ use jcc_entity::{EntitySlice, SecondaryMap, SlicePool}; #[derive(Default)] pub struct ControlFlowGraph { pool: SlicePool, - degree: SecondaryMap, + in_degree: SecondaryMap, preds: SecondaryMap>, } impl ControlFlowGraph { - pub fn new() -> Self { - Self::default() - } - pub fn preds(&self, block: Block) -> impl IntoIterator + '_ { self.pool[self.preds[block]].iter().copied() } @@ -25,22 +21,23 @@ impl ControlFlowGraph { // Compute degree of each block. for block in order.rpo(entry) { for succ in prog.blocks[block].term.successors() { - self.degree[succ] += 1; + self.in_degree[succ] += 1; } } // Allocate space for predecessors of each block. for block in order.rpo(entry) { - let degree = self.degree[block] as usize; + let degree = self.in_degree[block] as usize; self.preds[block] = self.pool.extend(std::iter::repeat_n(block, degree)); } // Fill predecessors of each block. for block in order.rpo(entry) { for succ in prog.blocks[block].term.successors() { - let idx = (self.degree[succ] - 1) as usize; - self.pool[self.preds[succ]][idx] = block; - self.degree[succ] = idx as u32; + let idx = self.in_degree[succ] - 1; + + self.in_degree[succ] = idx; + self.pool[self.preds[succ]][idx as usize] = block; } } } diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index 392efe8..c0f736c 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -14,44 +14,14 @@ pub struct Dominance { } impl Dominance { - pub fn new() -> Self { - Self::default() - } - pub fn idom(&self, block: Block) -> Option { self.idom[block] } - pub fn dominates(&self, a: Block, b: Block) -> bool { - if a == b { - return true; - } - let mut b = b; - while let Some(idom) = self.idom[b] { - if idom == a { - return true; - } - b = idom; - } - false - } - pub fn children(&self, block: Block) -> impl IntoIterator + '_ { self.pool[self.children[block]].iter().copied() } - pub fn intersect(&self, mut a: Block, mut b: Block, order: &Order) -> Block { - while a != b { - while order.rpo_idx(a) > order.rpo_idx(b) { - a = self.idom[a].expect("block has no idom"); - } - while order.rpo_idx(b) > order.rpo_idx(a) { - b = self.idom[b].expect("block has no idom"); - } - } - a - } - pub fn compute(&mut self, prog: &Program, order: &Order, cfg: &ControlFlowGraph) { for (_, data) in prog.functions.iter() { if let Some(entry) = data.entry { diff --git a/crates/jcc_backend/src/ir/analysis/order.rs b/crates/jcc_backend/src/ir/analysis/order.rs index 7f90638..0abbc26 100644 --- a/crates/jcc_backend/src/ir/analysis/order.rs +++ b/crates/jcc_backend/src/ir/analysis/order.rs @@ -11,10 +11,6 @@ pub struct Order { } impl Order { - pub fn new() -> Self { - Self::default() - } - pub fn rpo_idx(&self, block: Block) -> u32 { self.rpo_idx[block] } diff --git a/crates/jcc_backend/src/ir/builder.rs b/crates/jcc_backend/src/ir/builder.rs index 2bf19e3..877bcd3 100644 --- a/crates/jcc_backend/src/ir/builder.rs +++ b/crates/jcc_backend/src/ir/builder.rs @@ -33,9 +33,9 @@ impl<'a> ProgramBuilder<'a> { } pub fn finish(self) -> Program { - let mut ord = Order::new(); - let mut dom = Dominance::new(); - let mut cfg = ControlFlowGraph::new(); + let mut ord = Order::default(); + let mut dom = Dominance::default(); + let mut cfg = ControlFlowGraph::default(); ord.compute(&self.prog); cfg.compute(&self.prog, &ord); dom.compute(&self.prog, &ord, &cfg); From 44f5767bb59ef15e5d808f476697876c058a3277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Thu, 25 Jun 2026 20:49:13 +0100 Subject: [PATCH 02/13] feat: add PackedOption and SentinelValue trait for type-safe packed options --- crates/jcc_entity/src/lib.rs | 10 ++- crates/jcc_entity/src/option.rs | 151 ++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 crates/jcc_entity/src/option.rs diff --git a/crates/jcc_entity/src/lib.rs b/crates/jcc_entity/src/lib.rs index 13a873f..48e0b02 100644 --- a/crates/jcc_entity/src/lib.rs +++ b/crates/jcc_entity/src/lib.rs @@ -5,12 +5,14 @@ //! IR where you have multiple interrelated entity types (instructions, blocks, etc.) that should not be confused. mod counter; +mod option; mod primary; mod secondary; mod slice; mod sparse; pub use counter::EntityCounter; +pub use option::{PackedOption, SentinelValue}; pub use primary::PrimaryMap; pub use secondary::SecondaryMap; pub use slice::{EntitySlice, SlicePool}; @@ -59,12 +61,16 @@ macro_rules! entity_impl { ($entity:ident) => { impl $crate::IdentityHashable for $entity {} + impl $crate::SentinelValue for $entity { + const SENTINEL: Self = Self(u32::MAX); + } + impl $crate::EntityRef for $entity { #[inline] fn new(index: usize) -> Self { debug_assert!(index < (u32::MAX as usize)); #[allow(clippy::cast_possible_truncation)] - $entity(index as u32) + Self(index as u32) } #[inline] @@ -79,7 +85,7 @@ macro_rules! entity_impl { #[allow(dead_code, unreachable_pub, reason = "macro-generated code")] pub const fn from_u32(x: u32) -> Self { debug_assert!(x < u32::MAX); - $entity(x) + Self(x) } /// Return the underlying index value as a `u32`. diff --git a/crates/jcc_entity/src/option.rs b/crates/jcc_entity/src/option.rs new file mode 100644 index 0000000..5c20fd7 --- /dev/null +++ b/crates/jcc_entity/src/option.rs @@ -0,0 +1,151 @@ +/// Types that have a sentinel value which can't be created any other way. +pub trait SentinelValue: Eq { + /// The sentinel value. + const SENTINEL: Self; +} + +/// A packed representation of `Option`. +/// +/// This is a wrapper around a `T`, using `T::SENTINEL` to represent `None`. +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PackedOption(T); + +impl Default for PackedOption { + fn default() -> Self { + Self(T::SENTINEL) + } +} + +impl From> for Option { + fn from(packed: PackedOption) -> Option { + packed.expand() + } +} + +impl From> for PackedOption { + fn from(opt: Option) -> Self { + match opt { + Some(t) => t.into(), + None => Self::default(), + } + } +} + +impl From for PackedOption { + fn from(t: T) -> Self { + debug_assert!( + t != T::SENTINEL, + "PackedOption::from called with the sentinel value." + ); + Self(t) + } +} + +impl std::fmt::Debug for PackedOption +where + T: SentinelValue + std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.is_none() { + write!(f, "None") + } else { + write!(f, "Some({:?})", self.0) + } + } +} + +impl PackedOption { + /// Const constructor wrapping a `T`. + /// + /// ## Notes + /// + /// - To create `None`, pass `T::SENTINEL`. + /// - To create `Some(val)`, pass a non-reserved `val`. + pub const fn new(val: T) -> Self { + Self(val) + } + + /// Returns `true` if the packed option is a `None` value. + pub fn is_none(&self) -> bool { + self.0 == T::SENTINEL + } + + /// Returns `true` if the packed option is a `Some` value. + pub fn is_some(&self) -> bool { + self.0 != T::SENTINEL + } + + /// Expand the packed option into a regular `Option`. + pub fn expand(self) -> Option { + if self.is_none() { + None + } else { + Some(self.0) + } + } + + /// Unwrap a packed `Some` value or panic. + #[track_caller] + pub fn unwrap(self) -> T { + self.expand().unwrap() + } + + /// Unwrap a packed `Some` value or panic with a message. + #[track_caller] + pub fn expect(self, msg: &str) -> T { + self.expand().expect(msg) + } + + /// Takes the value out of the option, leaving a `None` in its place. + pub fn take(&mut self) -> Option { + std::mem::replace(self, Self(T::SENTINEL)).expand() + } + + /// Maps a `PackedOption` to `Option` by applying a function to a contained value. + pub fn map(self, f: F) -> Option + where + F: FnOnce(T) -> U, + { + self.expand().map(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, PartialEq, Eq)] + struct NoC(u32); + + impl SentinelValue for NoC { + const SENTINEL: Self = Self(13); + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + struct TestEntity(u32); + + impl SentinelValue for TestEntity { + const SENTINEL: Self = Self(13); + } + + #[test] + fn copies() { + let x = TestEntity(2); + let some: PackedOption = x.into(); + assert_eq!(some.expand(), x.into()); + assert_eq!(some, x.into()); + } + + #[test] + fn moves() { + let x = NoC(3); + let somex: PackedOption = x.into(); + assert!(!somex.is_none()); + assert_eq!(somex.expand(), Some(NoC(3))); + + let none: PackedOption = None.into(); + assert!(none.is_none()); + assert_eq!(none.expand(), None); + } +} From 2e05ffb114d8b454b820221950368da54abc13f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Thu, 25 Jun 2026 23:27:37 +0100 Subject: [PATCH 03/13] refactor: update Dominance struct to use PackedOption --- crates/jcc_backend/src/ir/analysis/cfg.rs | 4 ++- crates/jcc_backend/src/ir/analysis/dom.rs | 31 +++++++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/cfg.rs b/crates/jcc_backend/src/ir/analysis/cfg.rs index 4abd7e5..242d5fe 100644 --- a/crates/jcc_backend/src/ir/analysis/cfg.rs +++ b/crates/jcc_backend/src/ir/analysis/cfg.rs @@ -16,6 +16,7 @@ impl ControlFlowGraph { pub fn compute(&mut self, prog: &Program, order: &Order) { self.pool.clear(); + for data in prog.functions.values() { if let Some(entry) = data.entry { // Compute degree of each block. @@ -37,7 +38,8 @@ impl ControlFlowGraph { let idx = self.in_degree[succ] - 1; self.in_degree[succ] = idx; - self.pool[self.preds[succ]][idx as usize] = block; + let slice = self.preds[succ]; + self.pool[slice][idx as usize] = block; } } } diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index c0f736c..3c20227 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -3,56 +3,57 @@ use crate::ir::{ Block, Program, }; -use jcc_entity::{EntitySlice, SecondaryMap, SlicePool}; +use jcc_entity::{EntitySlice, PackedOption, SecondaryMap, SlicePool}; #[derive(Default)] pub struct Dominance { pool: SlicePool, degree: SecondaryMap, - idom: SecondaryMap>, + idom: SecondaryMap>, children: SecondaryMap>, } impl Dominance { pub fn idom(&self, block: Block) -> Option { - self.idom[block] + self.idom[block].expand() } pub fn children(&self, block: Block) -> impl IntoIterator + '_ { - self.pool[self.children[block]].iter().copied() + let slice = self.children[block]; + self.pool[slice].iter().copied() } pub fn compute(&mut self, prog: &Program, order: &Order, cfg: &ControlFlowGraph) { + self.pool.clear(); + for (_, data) in prog.functions.iter() { if let Some(entry) = data.entry { - self.idom[entry] = Some(entry); let mut changed = true; + self.idom[entry] = entry.into(); while changed { changed = false; - for block in order.rpo(entry) { if let Some(mut dom) = cfg .preds(block) .into_iter() .find(|pred| self.idom[*pred].is_some()) { - for pred in cfg.preds(block) { + for mut pred in cfg.preds(block) { if pred == dom || self.idom[pred].is_none() { continue; } - let mut pred = pred; while pred != dom { while order.rpo_idx(pred) > order.rpo_idx(dom) { - pred = self.idom[pred].expect("block has no idom"); + pred = self.idom[pred].expect("pred has no idom"); } while order.rpo_idx(dom) > order.rpo_idx(pred) { dom = self.idom[dom].expect("block has no idom"); } } } - if self.idom[block] != Some(dom) { + if self.idom[block] != dom.into() { + self.idom[block] = dom.into(); changed = true; - self.idom[block] = Some(dom); } } } @@ -77,9 +78,11 @@ impl Dominance { for block in order.rpo(entry) { if let Some(idom) = self.idom(block) { if block != idom { - let idx = (self.degree[idom] - 1) as usize; - self.pool[self.children[idom]][idx] = block; - self.degree[idom] = idx as u32; + let idx = self.degree[idom] - 1; + + self.degree[idom] = idx; + let slice = self.children[idom]; + self.pool[slice][idx as usize] = block; } } } From 3147ffa81bcd9faae0f5b9f251b1c3ae93e78009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 09:44:24 +0100 Subject: [PATCH 04/13] feat: add iterator methods for operands in Terminator enum --- crates/jcc_backend/src/ir/term.rs | 41 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/crates/jcc_backend/src/ir/term.rs b/crates/jcc_backend/src/ir/term.rs index a5b6c13..d142cae 100644 --- a/crates/jcc_backend/src/ir/term.rs +++ b/crates/jcc_backend/src/ir/term.rs @@ -62,6 +62,28 @@ impl Terminator { } } + /// Returns an iterator to the operands of this terminator. + pub fn operands(&self) -> impl Iterator { + match self { + Terminator::Ret(Some(v)) => Some(*v), + Terminator::CondBr { cond, .. } => Some(*cond), + Terminator::Switch { value, .. } => Some(*value), + _ => None, + } + .into_iter() + } + + /// Returns a mutable iterator to the operands of this terminator. + pub fn operand_mut(&mut self) -> impl Iterator { + match self { + Terminator::Ret(Some(v)) => Some(v), + Terminator::CondBr { cond, .. } => Some(cond), + Terminator::Switch { value, .. } => Some(value), + _ => None, + } + .into_iter() + } + /// Returns the successor blocks of this terminator. pub fn successors(&self) -> Successors<'_> { match self { @@ -83,6 +105,10 @@ impl Terminator { } } +// ----------------------------------------------------------------------- +// Successors +// ----------------------------------------------------------------------- + pub enum Successors<'a> { /// No successors. Empty, @@ -105,22 +131,11 @@ pub enum Successors<'a> { impl<'a> FusedIterator for Successors<'a> {} -impl<'a> ExactSizeIterator for Successors<'a> { - fn len(&self) -> usize { - match self { - Successors::Empty => 0, - Successors::One(_) => 1, - Successors::Two { .. } => 2, - Successors::Switch { cases, .. } => 1 + cases.len(), - } - } -} - impl<'a> Iterator for Successors<'a> { type Item = Block; #[inline] - fn next(&mut self) -> Option { + fn next(&mut self) -> Option { match self { Successors::Empty => None, Successors::One(block) => block.take(), @@ -134,7 +149,7 @@ impl<'a> Iterator for Successors<'a> { impl<'a> DoubleEndedIterator for Successors<'a> { #[inline] - fn next_back(&mut self) -> Option { + fn next_back(&mut self) -> Option { match self { Successors::Empty => None, Successors::One(block) => block.take(), From 0339a96258f321739d5e7c6d841359811b4dd288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 10:06:10 +0100 Subject: [PATCH 05/13] feat: add operands and operands_mut methods for Inst --- crates/jcc_backend/src/ir/inst.rs | 298 ++++++++++++++++++++++++++++-- 1 file changed, 285 insertions(+), 13 deletions(-) diff --git a/crates/jcc_backend/src/ir/inst.rs b/crates/jcc_backend/src/ir/inst.rs index e27ff9e..252c2af 100644 --- a/crates/jcc_backend/src/ir/inst.rs +++ b/crates/jcc_backend/src/ir/inst.rs @@ -1,5 +1,7 @@ use crate::ir::{ty::Ty, Function, Global, Value}; +use std::iter::FusedIterator; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum UnaryOp { /// Bitwise NOT @@ -434,30 +436,182 @@ impl Inst { Inst::Alloca { .. } => Ty::Ptr, Inst::GetElementPtr { .. } => Ty::Ptr, - Inst::Binary { ty, .. } - | Inst::Bitcast { to: ty, .. } + Inst::ConstNull(ty) | Inst::Call { ty, .. } + | Inst::Load { ty, .. } | Inst::Const { ty, .. } - | Inst::ConstNull(ty) + | Inst::Param { ty, .. } + | Inst::Unary { ty, .. } + | Inst::Binary { ty, .. } + | Inst::Select { ty, .. } + | Inst::ZExt { to: ty, .. } + | Inst::SExt { to: ty, .. } | Inst::FExt { to: ty, .. } + | Inst::Trunc { to: ty, .. } + | Inst::FTrunc { to: ty, .. } | Inst::FpToSi { to: ty, .. } | Inst::FpToUi { to: ty, .. } - | Inst::FTrunc { to: ty, .. } + | Inst::SiToFp { to: ty, .. } + | Inst::UiToFp { to: ty, .. } + | Inst::Bitcast { to: ty, .. } | Inst::IndirectCall { ty, .. } | Inst::IntToPtr { to: ty, .. } - | Inst::Load { ty, .. } - | Inst::Param { ty, .. } | Inst::PtrToInt { to: ty, .. } - | Inst::Select { ty, .. } - | Inst::SExt { to: ty, .. } - | Inst::SiToFp { to: ty, .. } - | Inst::Trunc { to: ty, .. } - | Inst::UiToFp { to: ty, .. } - | Inst::Unary { ty, .. } - | Inst::ZExt { to: ty, .. } | Inst::Phi(ty) => *ty, } } + + /// Returns the operands used by this instruction. + /// + /// Instructions that do not use any operands return `Operands::Empty`. + pub fn operands(&self) -> Operands<'_> { + match self { + Inst::Noop + | Inst::Phi(_) + | Inst::ConstNull(_) + | Inst::GlobalAddr(_) + | Inst::Const { .. } + | Inst::Param { .. } + | Inst::Alloca { .. } => Operands::Empty, + + Inst::Call { args, .. } => Operands::Slice(args.iter()), + + Inst::Select { + cond, + then_val, + else_val, + .. + } => Operands::Three { + first: Some(*cond), + second: Some(*then_val), + third: Some(*else_val), + }, + + Inst::IndirectCall { + ptr, args: rest, .. + } + | Inst::GetElementPtr { + ptr, indices: rest, .. + } => Operands::OneAndSlice { + first: Some(*ptr), + rest: rest.iter(), + }, + + Inst::ICmp { + lhs: fst, rhs: snd, .. + } + | Inst::FCmp { + lhs: fst, rhs: snd, .. + } + | Inst::Store { + ptr: fst, + value: snd, + .. + } + | Inst::Upsilon { + phi: fst, + value: snd, + .. + } + | Inst::Binary { + lhs: fst, rhs: snd, .. + } => Operands::Two { + first: Some(*fst), + second: Some(*snd), + }, + + Inst::ZExt { value, .. } + | Inst::SExt { value, .. } + | Inst::FExt { value, .. } + | Inst::Trunc { value, .. } + | Inst::FTrunc { value, .. } + | Inst::FpToSi { value, .. } + | Inst::FpToUi { value, .. } + | Inst::SiToFp { value, .. } + | Inst::UiToFp { value, .. } + | Inst::Bitcast { value, .. } + | Inst::IntToPtr { value, .. } + | Inst::PtrToInt { value, .. } + | Inst::Unary { operand: value, .. } + | Inst::Load { ptr: value, .. } => Operands::One(Some(*value)), + } + } + + /// Returns a mutable iterator over the operands used by this instruction. + /// + /// Instructions that do not use any operands return `OperandsMut::Empty`. + pub fn operands_mut(&mut self) -> OperandsMut<'_> { + match self { + Inst::Noop + | Inst::Phi(_) + | Inst::ConstNull(_) + | Inst::GlobalAddr(_) + | Inst::Const { .. } + | Inst::Param { .. } + | Inst::Alloca { .. } => OperandsMut::Empty, + + Inst::Call { args, .. } => OperandsMut::Slice(args.iter_mut()), + + Inst::Select { + cond, + then_val, + else_val, + .. + } => OperandsMut::Three { + first: Some(cond), + second: Some(then_val), + third: Some(else_val), + }, + + Inst::IndirectCall { + ptr, args: rest, .. + } + | Inst::GetElementPtr { + ptr, indices: rest, .. + } => OperandsMut::OneAndSlice { + first: Some(ptr), + rest: rest.iter_mut(), + }, + + Inst::ICmp { + lhs: fst, rhs: snd, .. + } + | Inst::FCmp { + lhs: fst, rhs: snd, .. + } + | Inst::Store { + ptr: fst, + value: snd, + .. + } + | Inst::Upsilon { + phi: fst, + value: snd, + .. + } + | Inst::Binary { + lhs: fst, rhs: snd, .. + } => OperandsMut::Two { + first: Some(fst), + second: Some(snd), + }, + + Inst::ZExt { value, .. } + | Inst::SExt { value, .. } + | Inst::FExt { value, .. } + | Inst::Trunc { value, .. } + | Inst::FTrunc { value, .. } + | Inst::FpToSi { value, .. } + | Inst::FpToUi { value, .. } + | Inst::SiToFp { value, .. } + | Inst::UiToFp { value, .. } + | Inst::Bitcast { value, .. } + | Inst::IntToPtr { value, .. } + | Inst::PtrToInt { value, .. } + | Inst::Unary { operand: value, .. } + | Inst::Load { ptr: value, .. } => OperandsMut::One(Some(value)), + } + } } impl std::fmt::Display for UnaryOp { @@ -614,3 +768,121 @@ impl std::fmt::Display for Inst { } } } + +// ----------------------------------------------------------------------- +// Operands +// ----------------------------------------------------------------------- + +pub enum Operands<'a> { + /// No operands. + Empty, + + /// One operand. + One(Option), + + /// A slice of operands. + Slice(std::slice::Iter<'a, Value>), + + /// One operands and a slice. + OneAndSlice { + first: Option, + rest: std::slice::Iter<'a, Value>, + }, + + /// Two operands. + Two { + first: Option, + second: Option, + }, + + /// Three operands. + Three { + first: Option, + second: Option, + third: Option, + }, +} + +impl<'a> FusedIterator for Operands<'a> {} + +impl<'a> Iterator for Operands<'a> { + type Item = Value; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::Empty => None, + Self::One(value) => value.take(), + Self::Slice(iter) => iter.next().copied(), + Self::Two { first, second } => first.take().or_else(|| second.take()), + Self::OneAndSlice { first, rest } => first.take().or_else(|| rest.next().copied()), + Self::Three { + first, + second, + third, + } => first + .take() + .or_else(|| second.take()) + .or_else(|| third.take()), + } + } +} + +// ----------------------------------------------------------------------- +// OperandsMut +// ----------------------------------------------------------------------- + +pub enum OperandsMut<'a> { + /// No operands. + Empty, + + /// One operand. + One(Option<&'a mut Value>), + + /// A slice of operands. + Slice(std::slice::IterMut<'a, Value>), + + /// One operands and a slice. + OneAndSlice { + first: Option<&'a mut Value>, + rest: std::slice::IterMut<'a, Value>, + }, + + /// Two operands. + Two { + first: Option<&'a mut Value>, + second: Option<&'a mut Value>, + }, + + /// Three operands. + Three { + first: Option<&'a mut Value>, + second: Option<&'a mut Value>, + third: Option<&'a mut Value>, + }, +} + +impl<'a> FusedIterator for OperandsMut<'a> {} + +impl<'a> Iterator for OperandsMut<'a> { + type Item = &'a mut Value; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::Empty => None, + Self::One(value) => value.take(), + Self::Slice(iter) => iter.next(), + Self::Two { first, second } => first.take().or_else(|| second.take()), + Self::OneAndSlice { first, rest } => first.take().or_else(|| rest.next()), + Self::Three { + first, + second, + third, + } => first + .take() + .or_else(|| second.take()) + .or_else(|| third.take()), + } + } +} From 7d5f8ad2655b4377d2e0258821e2605a3763c007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 11:04:26 +0100 Subject: [PATCH 06/13] fix: address clippy warnings --- crates/jcc_entity/Cargo.toml | 2 +- crates/jcc_entity/src/option.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/jcc_entity/Cargo.toml b/crates/jcc_entity/Cargo.toml index 95002f1..b212f3b 100644 --- a/crates/jcc_entity/Cargo.toml +++ b/crates/jcc_entity/Cargo.toml @@ -30,7 +30,7 @@ return_self_not_must_use = "allow" similar_names = "allow" dbg_macro = "warn" -expect_used = "allow" +expect_used = "warn" panic = "warn" todo = "warn" undocumented_unsafe_blocks = "warn" diff --git a/crates/jcc_entity/src/option.rs b/crates/jcc_entity/src/option.rs index 5c20fd7..91faaa7 100644 --- a/crates/jcc_entity/src/option.rs +++ b/crates/jcc_entity/src/option.rs @@ -86,13 +86,23 @@ impl PackedOption { } /// Unwrap a packed `Some` value or panic. + /// + /// # Panics + /// + /// Panics if the packed option is a `None` value. #[track_caller] + #[allow(clippy::unwrap_used)] pub fn unwrap(self) -> T { self.expand().unwrap() } /// Unwrap a packed `Some` value or panic with a message. + /// + /// # Panics + /// + /// Panics if the packed option is a `None` value. #[track_caller] + #[allow(clippy::expect_used)] pub fn expect(self, msg: &str) -> T { self.expand().expect(msg) } From 6530ab5cd86969c54dbc7b63acd735e4c6d7f8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 11:04:59 +0100 Subject: [PATCH 07/13] refactor: use Self in match patterns for Successors iterator --- crates/jcc_backend/src/ir/term.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/jcc_backend/src/ir/term.rs b/crates/jcc_backend/src/ir/term.rs index d142cae..4720b90 100644 --- a/crates/jcc_backend/src/ir/term.rs +++ b/crates/jcc_backend/src/ir/term.rs @@ -137,10 +137,10 @@ impl<'a> Iterator for Successors<'a> { #[inline] fn next(&mut self) -> Option { match self { - Successors::Empty => None, - Successors::One(block) => block.take(), - Successors::Two { first, second } => first.take().or_else(|| second.take()), - Successors::Switch { cases, default } => { + Self::Empty => None, + Self::One(block) => block.take(), + Self::Two { first, second } => first.take().or_else(|| second.take()), + Self::Switch { cases, default } => { cases.next().map(|(_, b)| *b).or_else(|| default.take()) } } @@ -151,10 +151,10 @@ impl<'a> DoubleEndedIterator for Successors<'a> { #[inline] fn next_back(&mut self) -> Option { match self { - Successors::Empty => None, - Successors::One(block) => block.take(), - Successors::Two { first, second } => second.take().or_else(|| first.take()), - Successors::Switch { cases, default } => default + Self::Empty => None, + Self::One(block) => block.take(), + Self::Two { first, second } => second.take().or_else(|| first.take()), + Self::Switch { cases, default } => default .take() .or_else(|| cases.next_back().map(|(_, b)| *b)), } From 22d4b098b2144eae92a937087d4676a2985b0d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 15:40:24 +0100 Subject: [PATCH 08/13] refactor: update iterator return types in ControlFlowGraph and Order --- crates/jcc_backend/src/ir/analysis/cfg.rs | 2 +- crates/jcc_backend/src/ir/analysis/order.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/cfg.rs b/crates/jcc_backend/src/ir/analysis/cfg.rs index 242d5fe..34aebf8 100644 --- a/crates/jcc_backend/src/ir/analysis/cfg.rs +++ b/crates/jcc_backend/src/ir/analysis/cfg.rs @@ -10,7 +10,7 @@ pub struct ControlFlowGraph { } impl ControlFlowGraph { - pub fn preds(&self, block: Block) -> impl IntoIterator + '_ { + pub fn preds(&self, block: Block) -> impl Iterator + '_ { self.pool[self.preds[block]].iter().copied() } diff --git a/crates/jcc_backend/src/ir/analysis/order.rs b/crates/jcc_backend/src/ir/analysis/order.rs index 0abbc26..70abc3b 100644 --- a/crates/jcc_backend/src/ir/analysis/order.rs +++ b/crates/jcc_backend/src/ir/analysis/order.rs @@ -15,8 +15,8 @@ impl Order { self.rpo_idx[block] } - pub fn rpo(&self, block: Block) -> impl IntoIterator + '_ { - self.pool[self.rpo[block]].iter().copied() + pub fn rpo(&self, block: Block) -> impl Iterator + '_ { + self.pool[self.rpo[block]].iter().copied().into_iter() } pub fn compute(&mut self, prog: &Program) { From b92a94c849c05184ca724dd02731f26611c73c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 15:41:11 +0100 Subject: [PATCH 09/13] feat: compute dominance frontiers --- crates/jcc_backend/src/ir/analysis/dom.rs | 308 ++++++++++++++++++---- 1 file changed, 254 insertions(+), 54 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index 3c20227..0422811 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -7,86 +7,286 @@ use jcc_entity::{EntitySlice, PackedOption, SecondaryMap, SlicePool}; #[derive(Default)] pub struct Dominance { - pool: SlicePool, + /// A temporary map for storing node degrees. degree: SecondaryMap, + /// The immediate dominator (idom) of each block. idom: SecondaryMap>, + + /// A pool of blocks storing dominator tree children. + children_pool: SlicePool, + /// Maps each block to a slice of its children in the dominator tree. children: SecondaryMap>, + + /// A pool of blocks storing dominance frontiers. + frontier_pool: SlicePool, + /// Maps each block to a slice of blocks in its dominance frontier. + frontier: SecondaryMap>, } impl Dominance { + /// Returns the immediate dominator of the given block, if it has one. pub fn idom(&self, block: Block) -> Option { self.idom[block].expand() } - pub fn children(&self, block: Block) -> impl IntoIterator + '_ { + /// Returns an iterator over the children of the given block in the dominator tree. + pub fn children(&self, block: Block) -> impl Iterator + '_ { let slice = self.children[block]; - self.pool[slice].iter().copied() + self.children_pool[slice].iter().copied() } - pub fn compute(&mut self, prog: &Program, order: &Order, cfg: &ControlFlowGraph) { - self.pool.clear(); + /// Returns an iterator over the blocks in the dominance frontier of the given block. + pub fn frontier(&self, block: Block) -> impl Iterator + '_ { + let slice = self.frontier[block]; + self.frontier_pool[slice].iter().copied() + } + + /// Computes the dominance relation, the dominator tree, and + /// the dominance frontiers for all functions in the program. + pub fn compute(&mut self, prog: &Program, ord: &Order, cfg: &ControlFlowGraph) { + self.children_pool.clear(); + self.frontier_pool.clear(); for (_, data) in prog.functions.iter() { - if let Some(entry) = data.entry { - let mut changed = true; - self.idom[entry] = entry.into(); - while changed { - changed = false; - for block in order.rpo(entry) { - if let Some(mut dom) = cfg - .preds(block) - .into_iter() - .find(|pred| self.idom[*pred].is_some()) - { - for mut pred in cfg.preds(block) { - if pred == dom || self.idom[pred].is_none() { - continue; - } - while pred != dom { - while order.rpo_idx(pred) > order.rpo_idx(dom) { - pred = self.idom[pred].expect("pred has no idom"); - } - while order.rpo_idx(dom) > order.rpo_idx(pred) { - dom = self.idom[dom].expect("block has no idom"); - } - } - } - if self.idom[block] != dom.into() { - self.idom[block] = dom.into(); - changed = true; - } + let Some(entry) = data.entry else { + continue; + }; + + let mut changed = true; + self.idom[entry] = entry.into(); + + while changed { + changed = false; + + for block in ord.rpo(entry) { + let Some(mut dom) = cfg + .preds(block) + .into_iter() + .find(|pred| self.idom(*pred).is_some()) + else { + continue; + }; + + for pred in cfg.preds(block) { + if pred == dom || self.idom[pred].is_none() { + continue; } + dom = self.intersect(dom, pred, ord); } - } - // Compute the degree of each block. - for block in order.rpo(entry) { - if let Some(idom) = self.idom(block) { - if block != idom { - self.degree[idom] += 1; - } + if self.idom[block] != dom.into() { + self.idom[block] = dom.into(); + changed = true; } } + } + + self.compute_children(entry, ord); + self.compute_frontier(entry, ord, cfg); + } + } + + fn compute_children(&mut self, entry: Block, ord: &Order) { + debug_assert!(ord.rpo(entry).all(|b| self.degree[b] == 0)); - // Allocate space for children of each block. - for block in order.rpo(entry) { - let degree = self.degree[block] as usize; - self.children[block] = self.pool.extend(std::iter::repeat_n(block, degree)); + // Count children. + for block in ord.rpo(entry) { + if let Some(idom) = self.idom(block) { + if block != idom { + self.degree[idom] += 1; } + } + } - // Fill children of each block. - for block in order.rpo(entry) { - if let Some(idom) = self.idom(block) { - if block != idom { - let idx = self.degree[idom] - 1; + // Allocate slices. + for block in ord.rpo(entry) { + let degree = self.degree[block] as usize; + self.children[block] = self + .children_pool + .extend(std::iter::repeat_n(block, degree)); + } - self.degree[idom] = idx; - let slice = self.children[idom]; - self.pool[slice][idx as usize] = block; - } - } + // Fill slices. + // + // Reuse `degree` as a reverse insertion cursor. + for block in ord.rpo(entry) { + if let Some(idom) = self.idom(block) { + if block != idom { + let idx = self.degree[idom] - 1; + + self.degree[idom] = idx; + let slice = self.children[idom]; + self.children_pool[slice][idx as usize] = block; } } } } + + fn compute_frontier(&mut self, entry: Block, ord: &Order, cfg: &ControlFlowGraph) { + debug_assert!(ord.rpo(entry).all(|b| self.degree[b] == 0)); + + // Count frontier size. + for block in ord.rpo(entry) { + if cfg.preds(block).nth(1).is_none() { + continue; + }; + self.walk_frontier(block, cfg, |this, runner| { + this.degree[runner] += 1; + }); + } + + // Allocate slices. + for block in ord.rpo(entry) { + let degree = self.degree[block] as usize; + self.frontier[block] = self + .frontier_pool + .extend(std::iter::repeat_n(block, degree)); + } + + // Fill slices. + // + // Reuse `degree` as a reverse insertion cursor. + for block in ord.rpo(entry) { + if cfg.preds(block).nth(1).is_none() { + continue; + }; + self.walk_frontier(block, cfg, |this, runner| { + let idx = this.degree[runner] - 1; + + this.degree[runner] = idx; + let slice = this.frontier[runner]; + this.frontier_pool[slice][idx as usize] = block; + }); + } + } + + fn walk_frontier( + &mut self, + block: Block, + cfg: &ControlFlowGraph, + mut visit: impl FnMut(&mut Self, Block), + ) { + let Some(idom) = self.idom(block) else { + return; + }; + for mut runner in cfg.preds(block) { + while idom != runner { + let Some(next) = self.idom(runner) else { + break; + }; + visit(self, runner); + runner = next; + } + } + } + + fn intersect(&self, mut lhs: Block, mut rhs: Block, ord: &Order) -> Block { + while lhs != rhs { + while ord.rpo_idx(lhs) > ord.rpo_idx(rhs) { + lhs = self.idom(lhs).expect("block has no idom"); + } + while ord.rpo_idx(rhs) > ord.rpo_idx(lhs) { + rhs = self.idom(rhs).expect("block has no idom"); + } + } + lhs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{ + ir::testutil::{check_parse, parse_ir}, + IdentInterner, + }; + + use jcc_codemap::simple::SimpleFiles; + + fn setup(input: &str) -> (Vec, Dominance) { + let mut db = SimpleFiles::new(); + let mut interner = IdentInterner::new(); + let ir = parse_ir(&mut db, &mut interner, input); + check_parse(&mut db, &ir).unwrap_or_else(|report| panic!("{report}")); + + let prog = &ir.program; + let mut ord = Order::default(); + let mut dom = Dominance::default(); + let mut cfg = ControlFlowGraph::default(); + + ord.compute(prog); + cfg.compute(prog, &ord); + dom.compute(prog, &ord, &cfg); + + let entry = prog.functions.values().next().unwrap().entry.unwrap(); + (ord.rpo(entry).into_iter().collect(), dom) + } + + #[test] + fn simple() { + let (rpo, dom) = setup( + r#" + define @classify { + bb0: + %0 = param i1 #0 + br i1 %0, bb1, bb2 + + bb1: + br bb3 + + bb2: + br bb3 + + bb3: + ret void + } + "#, + ); + + assert_eq!(rpo.len(), 4); + + assert!(dom.frontier(rpo[0]).eq([])); + assert!(dom.frontier(rpo[1]).eq([rpo[3]])); + assert!(dom.frontier(rpo[2]).eq([rpo[3]])); + assert!(dom.frontier(rpo[3]).eq([])); + } + + #[test] + fn looped() { + let (rpo, dom) = setup( + r#" + define @loop { + bb0: + br bb1 + + bb1: + %0 = param i1 #0 + br i1 %0, bb2, bb3 + + bb2: + br bb4 + + bb3: + br bb4 + + bb4: + %1 = param i1 #1 + br i1 %1, bb1, bb5 + + bb5: + ret void + } + "#, + ); + + assert_eq!(rpo.len(), 6); + + assert!(dom.frontier(rpo[0]).eq([])); + assert!(dom.frontier(rpo[1]).eq([rpo[1]])); + assert!(dom.frontier(rpo[2]).eq([rpo[4]])); + assert!(dom.frontier(rpo[3]).eq([rpo[4]])); + assert!(dom.frontier(rpo[4]).eq([rpo[1]])); + assert!(dom.frontier(rpo[5]).eq([])); + } } From 66b1b8f00a959a8234f9041478f4c089e3443d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 15:44:21 +0100 Subject: [PATCH 10/13] fix: clippy warnings --- crates/jcc_backend/src/ir/analysis/order.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/jcc_backend/src/ir/analysis/order.rs b/crates/jcc_backend/src/ir/analysis/order.rs index 70abc3b..273dfca 100644 --- a/crates/jcc_backend/src/ir/analysis/order.rs +++ b/crates/jcc_backend/src/ir/analysis/order.rs @@ -16,7 +16,7 @@ impl Order { } pub fn rpo(&self, block: Block) -> impl Iterator + '_ { - self.pool[self.rpo[block]].iter().copied().into_iter() + self.pool[self.rpo[block]].iter().copied() } pub fn compute(&mut self, prog: &Program) { From 241aa09300975d08afeea1eba2f235a0600ae724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 18:35:13 +0100 Subject: [PATCH 11/13] fix: document that frontier does not provide set semantics --- crates/jcc_backend/src/ir/analysis/dom.rs | 85 +++++++++++++++++------ 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index 0422811..e954f12 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -29,13 +29,18 @@ impl Dominance { self.idom[block].expand() } - /// Returns an iterator over the children of the given block in the dominator tree. + /// Returns the dominated children of `block`. pub fn children(&self, block: Block) -> impl Iterator + '_ { let slice = self.children[block]; self.children_pool[slice].iter().copied() } - /// Returns an iterator over the blocks in the dominance frontier of the given block. + /// Returns the dominance frontier of `block`. + /// + /// ## Notes + /// + /// - The iterator may contain duplicate blocks. + /// - Consumers requiring set semantics are expected to deduplicate. pub fn frontier(&self, block: Block) -> impl Iterator + '_ { let slice = self.frontier[block]; self.frontier_pool[slice].iter().copied() @@ -44,6 +49,9 @@ impl Dominance { /// Computes the dominance relation, the dominator tree, and /// the dominance frontiers for all functions in the program. pub fn compute(&mut self, prog: &Program, ord: &Order, cfg: &ControlFlowGraph) { + self.idom.clear(); + self.children.clear(); + self.frontier.clear(); self.children_pool.clear(); self.frontier_pool.clear(); @@ -204,28 +212,24 @@ mod tests { use jcc_codemap::simple::SimpleFiles; - fn setup(input: &str) -> (Vec, Dominance) { + fn setup(input: &str) -> Dominance { let mut db = SimpleFiles::new(); let mut interner = IdentInterner::new(); let ir = parse_ir(&mut db, &mut interner, input); check_parse(&mut db, &ir).unwrap_or_else(|report| panic!("{report}")); - let prog = &ir.program; let mut ord = Order::default(); let mut dom = Dominance::default(); let mut cfg = ControlFlowGraph::default(); - ord.compute(prog); cfg.compute(prog, &ord); dom.compute(prog, &ord, &cfg); - - let entry = prog.functions.values().next().unwrap().entry.unwrap(); - (ord.rpo(entry).into_iter().collect(), dom) + dom } #[test] fn simple() { - let (rpo, dom) = setup( + let dom = setup( r#" define @classify { bb0: @@ -244,17 +248,15 @@ mod tests { "#, ); - assert_eq!(rpo.len(), 4); - - assert!(dom.frontier(rpo[0]).eq([])); - assert!(dom.frontier(rpo[1]).eq([rpo[3]])); - assert!(dom.frontier(rpo[2]).eq([rpo[3]])); - assert!(dom.frontier(rpo[3]).eq([])); + assert!(dom.frontier(Block::from_u32(0)).eq([])); + assert!(dom.frontier(Block::from_u32(1)).eq([Block::from_u32(3)])); + assert!(dom.frontier(Block::from_u32(2)).eq([Block::from_u32(3)])); + assert!(dom.frontier(Block::from_u32(3)).eq([])); } #[test] fn looped() { - let (rpo, dom) = setup( + let dom = setup( r#" define @loop { bb0: @@ -280,13 +282,50 @@ mod tests { "#, ); - assert_eq!(rpo.len(), 6); + assert!(dom.frontier(Block::from_u32(0)).eq([])); + assert!(dom.frontier(Block::from_u32(1)).eq([Block::from_u32(1)])); + assert!(dom.frontier(Block::from_u32(2)).eq([Block::from_u32(4)])); + assert!(dom.frontier(Block::from_u32(3)).eq([Block::from_u32(4)])); + assert!(dom.frontier(Block::from_u32(4)).eq([Block::from_u32(1)])); + assert!(dom.frontier(Block::from_u32(5)).eq([])); + } + + #[test] + fn non_dominating_diamond() { + let dom = setup( + r#" + define @diamond { + bb0: + %0 = param i1 #0 + br i1 %0, bb1, bb2 + + bb1: + %1 = param i1 #1 + br i1 %1, bb3, bb4 + + bb2: + br bb5 + + bb3: + br bb5 + + bb4: + br bb5 + + bb5: + ret void + } + "#, + ); + + // Note: For now the frontier() API does provide set semantics + let special = [Block::from_u32(5), Block::from_u32(5)]; - assert!(dom.frontier(rpo[0]).eq([])); - assert!(dom.frontier(rpo[1]).eq([rpo[1]])); - assert!(dom.frontier(rpo[2]).eq([rpo[4]])); - assert!(dom.frontier(rpo[3]).eq([rpo[4]])); - assert!(dom.frontier(rpo[4]).eq([rpo[1]])); - assert!(dom.frontier(rpo[5]).eq([])); + assert!(dom.frontier(Block::from_u32(0)).eq([])); + assert!(dom.frontier(Block::from_u32(1)).eq(special)); + assert!(dom.frontier(Block::from_u32(2)).eq([Block::from_u32(5)])); + assert!(dom.frontier(Block::from_u32(3)).eq([Block::from_u32(5)])); + assert!(dom.frontier(Block::from_u32(4)).eq([Block::from_u32(5)])); + assert!(dom.frontier(Block::from_u32(5)).eq([])); } } From d765aac4417362da25a782314e6f3b576bf785d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 18:40:44 +0100 Subject: [PATCH 12/13] fix: cargo fmt --- crates/jcc_backend/src/ir/analysis/dom.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index e954f12..a19ba33 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -37,9 +37,9 @@ impl Dominance { /// Returns the dominance frontier of `block`. /// - /// ## Notes - /// - /// - The iterator may contain duplicate blocks. + /// ## Notes + /// + /// - The iterator may contain duplicate blocks. /// - Consumers requiring set semantics are expected to deduplicate. pub fn frontier(&self, block: Block) -> impl Iterator + '_ { let slice = self.frontier[block]; From e63d14ee0fe72911e47956747ff3a51f0d873516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Silveira?= Date: Fri, 26 Jun 2026 19:45:10 +0100 Subject: [PATCH 13/13] fix: typo --- crates/jcc_backend/src/ir/analysis/dom.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/jcc_backend/src/ir/analysis/dom.rs b/crates/jcc_backend/src/ir/analysis/dom.rs index a19ba33..23bb21b 100644 --- a/crates/jcc_backend/src/ir/analysis/dom.rs +++ b/crates/jcc_backend/src/ir/analysis/dom.rs @@ -318,7 +318,7 @@ mod tests { "#, ); - // Note: For now the frontier() API does provide set semantics + // Note: For now the frontier() API does not provide set semantics let special = [Block::from_u32(5), Block::from_u32(5)]; assert!(dom.frontier(Block::from_u32(0)).eq([]));