From b9e2af966a89dc7aa4794924ec8ee031e59b62a1 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 11:03:20 +0100 Subject: [PATCH 01/49] Generalize InlineDFG to any HugrNode --- hugr-core/src/hugr/patch/inline_dfg.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/hugr-core/src/hugr/patch/inline_dfg.rs b/hugr-core/src/hugr/patch/inline_dfg.rs index bc14ae8af1..fe193c1f2f 100644 --- a/hugr-core/src/hugr/patch/inline_dfg.rs +++ b/hugr-core/src/hugr/patch/inline_dfg.rs @@ -3,36 +3,37 @@ //! and deleting the DFG along with its Input + Output use super::{PatchHugrMut, PatchVerification}; +use crate::core::HugrNode; use crate::ops::handle::{DfgID, NodeHandle}; use crate::{HugrView, IncomingPort, Node, OutgoingPort, PortIndex}; /// Structure identifying an `InlineDFG` rewrite from the spec -pub struct InlineDFG(pub DfgID); +pub struct InlineDFG(pub DfgID); /// Errors from an [`InlineDFG`] rewrite. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] #[non_exhaustive] -pub enum InlineDFGError { +pub enum InlineDFGError { /// Node to inline was not a DFG. (E.g. node has been overwritten since the `DfgID` originated.) #[error("{node} was not a DFG")] NotDFG { /// The node we tried to inline - node: Node, + node: N, }, /// The DFG node is the hugr entrypoint #[error("Cannot inline the entrypoint node, {node}")] CantInlineEntrypoint { /// The node we tried to inline - node: Node, + node: N, }, } -impl PatchVerification for InlineDFG { - type Error = InlineDFGError; +impl PatchVerification for InlineDFG { + type Error = InlineDFGError; - type Node = Node; + type Node = N; - fn verify(&self, h: &impl crate::HugrView) -> Result<(), Self::Error> { + fn verify(&self, h: &impl crate::HugrView) -> Result<(), Self::Error> { let n = self.0.node(); if h.get_optype(n).as_dfg().is_none() { return Err(InlineDFGError::NotDFG { node: n }); @@ -51,15 +52,15 @@ impl PatchVerification for InlineDFG { } } -impl PatchHugrMut for InlineDFG { +impl PatchHugrMut for InlineDFG { /// The removed nodes: the DFG, and its Input and Output children. - type Outcome = [Node; 3]; + type Outcome = [N; 3]; const UNCHANGED_ON_FAILURE: bool = true; fn apply_hugr_mut( self, - h: &mut impl crate::hugr::HugrMut, + h: &mut impl crate::hugr::HugrMut, ) -> Result { self.verify(h)?; let n = self.0.node(); From e39db0bd6d6d5beccbb7f713fbb48fe1076abe72 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 11:08:17 +0100 Subject: [PATCH 02/49] Generalize merge_bbs to any HugrNode --- hugr-passes/src/merge_bbs.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 3c6d9c9b2d..20869ded65 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -16,9 +16,15 @@ use hugr_core::{Hugr, HugrView, Node}; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor /// is B, B and B' can be combined. -pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) +/// +/// # Panics +/// +/// If the [HugrMut::entrypoint] of `cfg` is not an [OpType::CFG] +/// +/// [OpType::CFG]: hugr_core::ops::OpType::CFG +pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) where - H: 'h + HugrMut, + H: 'h + HugrMut, { let checked = cfg.try_into_checked().expect("Hugr must be a CFG region"); let cfg = checked.into_hugr(); @@ -49,11 +55,11 @@ where } } -fn mk_rep( - cfg: &impl HugrView, - pred: Node, - succ: Node, -) -> (Replacement, Node, [Node; 2]) { +fn mk_rep( + cfg: &H, + pred: H::Node, + succ: H::Node, +) -> (Replacement, Node, [Node; 2]) { let pred_ty = cfg.get_optype(pred).as_dataflow_block().unwrap(); let succ_ty = cfg.get_optype(succ).as_dataflow_block().unwrap(); let succ_sig = succ_ty.inner_signature(); From fbfa4de8e6da515e30b5dcfd7457c0b76a9c27a7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 12:24:48 +0100 Subject: [PATCH 03/49] factor out mk_rep2 --- hugr-passes/src/merge_bbs.rs | 40 ++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 20869ded65..4e7f4bad34 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -1,16 +1,18 @@ //! Merge BBs along control-flow edges where the source BB has no other successors //! and the target BB has no other predecessors. +use std::borrow::Cow; use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; use hugr_core::hugr::hugrmut::HugrMut; use hugr_core::hugr::views::RootCheckable; +use hugr_core::types::Signature; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, Output}; +use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, OpType, Output}; use hugr_core::{Hugr, HugrView, Node}; /// Merge any basic blocks that are direct children of the specified CFG @@ -64,16 +66,38 @@ fn mk_rep( let succ_ty = cfg.get_optype(succ).as_dataflow_block().unwrap(); let succ_sig = succ_ty.inner_signature(); + let opty = DataflowBlock { + inputs: pred_ty.inputs.clone(), + ..succ_ty.clone() + }; + mk_rep2( + cfg, + cfg.entrypoint_optype().clone(), + Some(opty.into()), + pred, + succ, + succ_sig, + ) +} + +fn mk_rep2( + cfg: &H, + root_opty: OpType, + parent_opty: Option, + pred: H::Node, + succ: H::Node, + succ_sig: Cow, +) -> (Replacement, Node, [Node; 2]) { + let pred_ty = cfg.get_optype(pred).as_dataflow_block().unwrap(); + // Make a Hugr with just a single CFG root node having the same signature. - let mut replacement: Hugr = Hugr::new_with_entrypoint(cfg.entrypoint_optype().clone()) - .expect("Replacement should have a CFG entrypoint"); + let mut replacement = + Hugr::new_with_entrypoint(root_opty).expect("Replacement should have a CFG entrypoint"); - let merged = replacement.add_node_with_parent(replacement.entrypoint(), { - DataflowBlock { - inputs: pred_ty.inputs.clone(), - ..succ_ty.clone() - } + let merged = parent_opty.map_or(replacement.entrypoint(), |opty| { + replacement.add_node_with_parent(replacement.entrypoint(), opty) }); + let input = replacement.add_node_with_parent( merged, Input { From 26e3668a4dc2950b86875484628d3c3e0ddd79e6 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 13:17:55 +0100 Subject: [PATCH 04/49] Option -> parametrize --- hugr-passes/src/merge_bbs.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 4e7f4bad34..f4114e7d49 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -70,32 +70,35 @@ fn mk_rep( inputs: pred_ty.inputs.clone(), ..succ_ty.clone() }; - mk_rep2( + let (repl, [merged], dfgs) = mk_rep2( cfg, cfg.entrypoint_optype().clone(), - Some(opty.into()), + [opty.into()], pred, succ, succ_sig, - ) + ); + (repl, merged, dfgs) } -fn mk_rep2( +fn mk_rep2( cfg: &H, root_opty: OpType, - parent_opty: Option, + parent_optys: [OpType; N], pred: H::Node, succ: H::Node, succ_sig: Cow, -) -> (Replacement, Node, [Node; 2]) { +) -> (Replacement, [Node; N], [Node; 2]) { let pred_ty = cfg.get_optype(pred).as_dataflow_block().unwrap(); // Make a Hugr with just a single CFG root node having the same signature. let mut replacement = Hugr::new_with_entrypoint(root_opty).expect("Replacement should have a CFG entrypoint"); - let merged = parent_opty.map_or(replacement.entrypoint(), |opty| { - replacement.add_node_with_parent(replacement.entrypoint(), opty) + let mut merged = replacement.entrypoint(); + let nested_blocks = parent_optys.map(|opty| { + merged = replacement.add_node_with_parent(merged, opty); + merged }); let input = replacement.add_node_with_parent( @@ -182,7 +185,7 @@ fn mk_rep2( .collect(), mu_new: vec![], }; - (rep, merged, [dfg1, dfg2]) + (rep, nested_blocks, [dfg1, dfg2]) } #[cfg(test)] From a5718100a7edcfe8649d51ffcc83c0a351fe0a17 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 13:37:38 +0100 Subject: [PATCH 05/49] factor out add_unpack --- hugr-passes/src/merge_bbs.rs | 41 ++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index f4114e7d49..46d97a30b9 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -6,14 +6,14 @@ use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; use hugr_core::hugr::hugrmut::HugrMut; use hugr_core::hugr::views::RootCheckable; -use hugr_core::types::Signature; +use hugr_core::types::{Signature, TypeRow}; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, OpType, Output}; -use hugr_core::{Hugr, HugrView, Node}; +use hugr_core::{Hugr, HugrView, Node, OutgoingPort}; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor @@ -136,15 +136,13 @@ fn mk_rep2( // At the junction, must unpack the first (tuple, branch predicate) output let tuple_elems = pred_ty.sum_rows.clone().into_iter().exactly_one().unwrap(); - let unp = replacement.add_node_with_parent(merged, UnpackTuple::new(tuple_elems.clone())); - replacement.connect(dfg1, 0, unp, 0); - let other_start = tuple_elems.len(); - for (i, _) in tuple_elems.iter().enumerate() { - replacement.connect(unp, i, dfg2, i); - } - for (i, _) in pred_ty.other_outputs.iter().enumerate() { - replacement.connect(dfg1, i + 1, dfg2, i + other_start); - } + let dfg1_outs = replacement + .out_value_types(dfg1) + .enumerate() + .map(|(i, _)| (dfg1, i.into())) + .collect::>(); + add_unpack(&mut replacement, dfg1_outs, tuple_elems, dfg2); + // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new // edge-maps as both source and target of the new edge are in the replacement Hugr for (_, src_pos) in cfg.all_linked_outputs(pred).filter(|(src, _)| *src == succ) { @@ -188,6 +186,27 @@ fn mk_rep2( (rep, nested_blocks, [dfg1, dfg2]) } +fn add_unpack( + h: &mut H, + srcs: impl IntoIterator, + tuple_tys: TypeRow, + dst: H::Node, +) { + let parent = h.get_parent(dst).unwrap(); + let mut srcs = srcs.into_iter(); + let (src, src_p) = srcs.next().unwrap(); + let tuple_len = tuple_tys.len(); + let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); + h.connect(src, src_p, unp, 0); + + for i in 0..tuple_len { + h.connect(unp, i, dst, i); + } + for (i, (src, src_p)) in srcs.enumerate() { + h.connect(src, src_p, dst, i + tuple_len); + } +} + #[cfg(test)] mod test { use std::collections::HashSet; From 4777809710b3e3cbb28f28a7cb217cbcb0b63bc5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 18:30:04 +0100 Subject: [PATCH 06/49] Add normalize_cfgs and error/result enums, deprecate merge_basic_blocks --- hugr-passes/src/merge_bbs.rs | 211 ++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 4 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 46d97a30b9..96ebd01b92 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -4,16 +4,16 @@ use std::borrow::Cow; use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; -use hugr_core::hugr::hugrmut::HugrMut; -use hugr_core::hugr::views::RootCheckable; +use hugr_core::hugr::views::{RootCheckable, RootChecked}; +use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; use hugr_core::types::{Signature, TypeRow}; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, OpType, Output}; -use hugr_core::{Hugr, HugrView, Node, OutgoingPort}; +use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output}; +use hugr_core::{Hugr, HugrView, Node, OutgoingPort, PortIndex}; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor @@ -24,6 +24,7 @@ use hugr_core::{Hugr, HugrView, Node, OutgoingPort}; /// If the [HugrMut::entrypoint] of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG +#[deprecated(note = "Use normalize_cfg")] pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) where H: 'h + HugrMut, @@ -57,6 +58,208 @@ where } } +#[derive(Clone, Debug, thiserror::Error)] +pub enum NormalizeCFGError { + /// The requested node was not a CFG. ALAN note this could just be [hugr_core::hugr::HugrError]?? + #[error("Requested node was not a CFG but {_0}")] + NotCFG(OpTag), +} + +pub enum NormalizeCFGResult { + CFGRemoved, + CFGPreserved { + entry_changed: bool, + exit_changed: bool, + }, +} + +#[allow(deprecated)] +pub fn normalize_cfg>( + mut cfg: &mut H, +) -> Result { + let checked: RootChecked<_, CfgID> = + RootChecked::<_, CfgID>::try_new(&mut cfg).map_err(|e| match e { + HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), + _ => unreachable!(), + })?; + merge_basic_blocks(checked); + + // Further normalizations with effects outside the CFG + let [entry, exit] = cfg + .children(cfg.entrypoint()) + .take(2) + .collect_array() + .unwrap(); + let entry_blk = cfg.get_optype(entry).as_dataflow_block().unwrap(); + let cfg_parent = cfg.get_parent(cfg.entrypoint()).unwrap(); + // Note the spec says the entry block is distinct from the exit block + // 1. If the entry block has only one successor and that is the exit block, the CFG can be removed. + // (It does not matter if the entry block has other predecessors: they are not reachable!) + if cfg.output_neighbours(entry).exactly_one().ok() == Some(exit) { + // 1a. If that successor is the exit block, the CFG can be removed + assert_eq!( + &Signature::new( + entry_blk.inputs.clone(), + entry_blk.successor_input(0).unwrap() + ), + cfg.signature(cfg.entrypoint()).unwrap().as_ref() + ); + + let (mut repl, [], dfgs) = mk_rep2( + cfg, + OpType::DFG(DFG { + signature: Signature::new_endo(vec![]), + }), + [], + entry, + exit, + Cow::Owned(Signature::new_endo( + cfg.get_optype(exit) + .as_exit_block() + .unwrap() + .cfg_outputs + .clone(), + )), + ); + { + let [exit_inp, exit_out] = repl.replacement.get_io(dfgs[1]).unwrap(); + for p in cfg.node_outputs(exit_inp) { + repl.replacement.connect(exit_inp, p, exit_out, p.index()); + } + repl.removal = vec![cfg.entrypoint()]; + } + let node_map = cfg.apply_patch(repl).unwrap(); + for dfg_id in dfgs { + let n_id = *node_map.get(&dfg_id).unwrap(); + cfg.apply_patch(InlineDFG(n_id.into())).unwrap(); + } + // This has replaced the entire CFG with a DFG...or should we just inline that?? + return Ok(NormalizeCFGResult::CFGRemoved); + } + // 2. If the entry block has a single successor (and no predecessor DataflowBlocks)... + let mut entry_changed = false; + if let Some(succ) = cfg + .output_neighbours(entry) + .exactly_one() + .ok() + .filter(|_| cfg.input_neighbours(entry).next().is_none()) + { + // Turn the entry block into a DFG outside/before the CFG; the successor becomes the entry block. + // Note if the entry block has multiple successors, but no predecessors, we could move its + // contents outside (before) the CFG, but would need to keep an empty/identity entry block + // - we do not do this here + assert!(cfg.input_neighbours(succ).count() > 1); + let [entry_input, entry_output] = cfg.get_io(entry).unwrap(); + let tuple_elems = entry_blk + .sum_rows + .clone() + .into_iter() + .exactly_one() + .unwrap(); + for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { + // TODO order edges?? Might need to generalize beyond single_linked... + let src = cfg.single_linked_output(cfg.entrypoint(), inp).unwrap(); + cfg.disconnect(cfg.entrypoint(), inp); + for tgt in cfg + .linked_inputs(entry_input, inp.index()) + .collect::>() + { + cfg.connect(src.0, src.1, tgt.0, tgt.1); + } + } + cfg.remove_node(entry_input); + let entry_results = cfg + .in_value_types(entry_output) + .enumerate() + .map(|(i, _)| cfg.single_linked_output(entry_output, i).unwrap()) + .collect::>(); + + add_unpack(cfg, entry_results, tuple_elems, cfg.entrypoint()); + cfg.remove_node(entry_output); + // Transfer remaining entry children - including any used to compute the predicate + while let Some(n) = cfg.first_child(entry) { + cfg.set_parent(n, cfg_parent); + } + cfg.move_before_sibling(succ, entry); + cfg.remove_node(entry); + entry_changed = true; + } + // 3. If the exit node has a single predecessor and that predecessor has no other successors... + let mut exit_changed = false; + if let Some(pred) = cfg + .input_neighbours(exit) + .exactly_one() + .ok() + .filter(|pred| cfg.output_neighbours(*pred).count() == 1) + { + // Code in that predecessor can be moved outside (after the CFG), and the predecessor deleted + let [_, output] = cfg.get_io(pred).unwrap(); + let pred_blk = cfg.get_optype(pred).as_dataflow_block().unwrap(); + let new_cfg_outs = pred_blk.inner_signature().into_owned().input; + let [tuple_tys] = pred_blk.sum_rows.iter().cloned().collect_array().unwrap(); + + // new CFG result type and exit block + let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { + panic!() + }; + let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); + // TODO update number of CFG outports + + *cfg.optype_mut(pred) = ExitBlock { + cfg_outputs: new_cfg_outs.clone(), + } + .into(); + // TODO update number of input ports + cfg.move_before_sibling(pred, exit); + cfg.remove_node(exit); + // Move contents into new DFG + let dfg = cfg.add_node_with_parent( + cfg_parent, + DFG { + signature: Signature::new(new_cfg_outs, result_tys.clone()), + }, + ); + while let Some(n) = cfg.first_child(pred) { + cfg.set_parent(n, dfg); + } + // Add tuple-unpack inside the DFG + let result_srcs = cfg + .node_inputs(output) + .collect::>() + .into_iter() + .map(|p| { + let src = cfg.single_linked_output(output, p).unwrap(); + cfg.disconnect(output, p); + src + }) + .collect::>(); + let OpType::Output(ou) = cfg.optype_mut(output) else { + panic!() + }; + ou.types = result_tys; + // TODO update number of input ports + + add_unpack(cfg, result_srcs, tuple_tys, output); + // Move output edges. + // TODO result_tys is almost this, but we want to move Order edges too + for p in cfg.node_outputs(cfg.entrypoint()).collect_vec() { + let tgts = cfg.linked_inputs(cfg.entrypoint(), p).collect_vec(); + cfg.disconnect(cfg.entrypoint(), p); + for tgt in tgts { + cfg.connect(dfg, p, tgt.0, tgt.1) + } + } + for p in cfg.node_inputs(dfg).collect_vec() { + cfg.connect(cfg.entrypoint(), p.index(), dfg, p); + } + exit_changed = true; + } + Ok(NormalizeCFGResult::CFGPreserved { + entry_changed, + exit_changed, + }) +} + fn mk_rep( cfg: &H, pred: H::Node, From d5ae400317807be8ec504f326db9088268d38b49 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 23 Sep 2025 19:23:01 +0100 Subject: [PATCH 07/49] Combine two entry-elisions, plus some missing edges/ports; revert mk_rep2 --- hugr-passes/src/merge_bbs.rs | 170 +++++++++++++++++------------------ 1 file changed, 80 insertions(+), 90 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 96ebd01b92..117614eb3e 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -1,6 +1,5 @@ //! Merge BBs along control-flow edges where the source BB has no other successors //! and the target BB has no other predecessors. -use std::borrow::Cow; use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; @@ -13,7 +12,7 @@ use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output}; -use hugr_core::{Hugr, HugrView, Node, OutgoingPort, PortIndex}; +use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor @@ -92,63 +91,14 @@ pub fn normalize_cfg>( .unwrap(); let entry_blk = cfg.get_optype(entry).as_dataflow_block().unwrap(); let cfg_parent = cfg.get_parent(cfg.entrypoint()).unwrap(); - // Note the spec says the entry block is distinct from the exit block - // 1. If the entry block has only one successor and that is the exit block, the CFG can be removed. - // (It does not matter if the entry block has other predecessors: they are not reachable!) - if cfg.output_neighbours(entry).exactly_one().ok() == Some(exit) { - // 1a. If that successor is the exit block, the CFG can be removed - assert_eq!( - &Signature::new( - entry_blk.inputs.clone(), - entry_blk.successor_input(0).unwrap() - ), - cfg.signature(cfg.entrypoint()).unwrap().as_ref() - ); - - let (mut repl, [], dfgs) = mk_rep2( - cfg, - OpType::DFG(DFG { - signature: Signature::new_endo(vec![]), - }), - [], - entry, - exit, - Cow::Owned(Signature::new_endo( - cfg.get_optype(exit) - .as_exit_block() - .unwrap() - .cfg_outputs - .clone(), - )), - ); - { - let [exit_inp, exit_out] = repl.replacement.get_io(dfgs[1]).unwrap(); - for p in cfg.node_outputs(exit_inp) { - repl.replacement.connect(exit_inp, p, exit_out, p.index()); - } - repl.removal = vec![cfg.entrypoint()]; - } - let node_map = cfg.apply_patch(repl).unwrap(); - for dfg_id in dfgs { - let n_id = *node_map.get(&dfg_id).unwrap(); - cfg.apply_patch(InlineDFG(n_id.into())).unwrap(); - } - // This has replaced the entire CFG with a DFG...or should we just inline that?? - return Ok(NormalizeCFGResult::CFGRemoved); - } - // 2. If the entry block has a single successor (and no predecessor DataflowBlocks)... + // 1. If the entry block has only one successor, and no predecessors, then move contents + // outside/before CFG. let mut entry_changed = false; - if let Some(succ) = cfg - .output_neighbours(entry) - .exactly_one() - .ok() - .filter(|_| cfg.input_neighbours(entry).next().is_none()) - { - // Turn the entry block into a DFG outside/before the CFG; the successor becomes the entry block. + if let Some(succ) = can_elide_entry(cfg, entry, entry_blk, exit) { + // Move contents of entry block outside/before the CFG; the successor becomes the entry block. // Note if the entry block has multiple successors, but no predecessors, we could move its // contents outside (before) the CFG, but would need to keep an empty/identity entry block // - we do not do this here - assert!(cfg.input_neighbours(succ).count() > 1); let [entry_input, entry_output] = cfg.get_io(entry).unwrap(); let tuple_elems = entry_blk .sum_rows @@ -156,6 +106,8 @@ pub fn normalize_cfg>( .into_iter() .exactly_one() .unwrap(); + let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); + // Inputs to CFG go directly to consumers of the entry block's Input node for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { // TODO order edges?? Might need to generalize beyond single_linked... let src = cfg.single_linked_output(cfg.entrypoint(), inp).unwrap(); @@ -168,6 +120,17 @@ pub fn normalize_cfg>( } } cfg.remove_node(entry_input); + + // Update input ports + let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { + panic!() + }; + let inputs_to_add = new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; + cfg_ty.signature.input = new_cfg_inputs; + cfg.add_ports(cfg.entrypoint(), Direction::Incoming, inputs_to_add); + + // Inputs to entry block Output node go instead to CFG + // TODO order edges again let entry_results = cfg .in_value_types(entry_output) .enumerate() @@ -180,11 +143,40 @@ pub fn normalize_cfg>( while let Some(n) = cfg.first_child(entry) { cfg.set_parent(n, cfg_parent); } + if succ == exit { + // 1a. CFG must have distinct entry/exit; but we are left with a no-op + let tys = cfg + .get_optype(exit) + .as_exit_block() + .unwrap() + .cfg_outputs + .clone(); + let opty = cfg.optype_mut(cfg.entrypoint()); + let OpType::CFG(cfg_ty) = std::mem::take(opty) else { + panic!() + }; + assert_eq!(cfg_ty.signature.input, tys); + assert_eq!(cfg_ty.signature.output, tys); + *opty = OpType::DFG(DFG { + signature: cfg_ty.signature, + }); + while let Some(c) = cfg.first_child(cfg.entrypoint()) { + cfg.remove_subtree(c); + } + let inp = cfg.add_node_with_parent(cfg.entrypoint(), Input { types: tys.clone() }); + let out = cfg.add_node_with_parent(cfg.entrypoint(), Input { types: tys.clone() }); + for p in cfg.node_outputs(inp).collect_vec() { + cfg.connect(inp, p, out, p.index()); + } + // This has replaced the entire CFG with an empty DFG...should we just inline/remove that?? + return Ok(NormalizeCFGResult::CFGRemoved); + } + // 1b. else, old entry-node's successor is the new entry node, move into place cfg.move_before_sibling(succ, entry); cfg.remove_node(entry); entry_changed = true; } - // 3. If the exit node has a single predecessor and that predecessor has no other successors... + // 2. If the exit node has a single predecessor and that predecessor has no other successors... let mut exit_changed = false; if let Some(pred) = cfg .input_neighbours(exit) @@ -209,7 +201,8 @@ pub fn normalize_cfg>( cfg_outputs: new_cfg_outs.clone(), } .into(); - // TODO update number of input ports + cfg.set_num_ports(pred, cfg.num_ports(pred, Direction::Incoming), 0); + cfg.move_before_sibling(pred, exit); cfg.remove_node(exit); // Move contents into new DFG @@ -260,6 +253,28 @@ pub fn normalize_cfg>( }) } +fn can_elide_entry( + h: &H, + entry: H::Node, + entry_blk: &DataflowBlock, + exit: H::Node, +) -> Option { + let succ = h.output_neighbours(entry).exactly_one().ok()?; + if succ == exit { + // Any other predecessors are unreachable! + assert_eq!( + &Signature::new( + entry_blk.inputs.clone(), + entry_blk.successor_input(0).unwrap() + ), + h.signature(h.entrypoint()).unwrap().as_ref() + ); + } else if h.input_neighbours(entry).count() > 1 { + return None; + } + Some(succ) +} + fn mk_rep( cfg: &H, pred: H::Node, @@ -269,41 +284,16 @@ fn mk_rep( let succ_ty = cfg.get_optype(succ).as_dataflow_block().unwrap(); let succ_sig = succ_ty.inner_signature(); - let opty = DataflowBlock { - inputs: pred_ty.inputs.clone(), - ..succ_ty.clone() - }; - let (repl, [merged], dfgs) = mk_rep2( - cfg, - cfg.entrypoint_optype().clone(), - [opty.into()], - pred, - succ, - succ_sig, - ); - (repl, merged, dfgs) -} - -fn mk_rep2( - cfg: &H, - root_opty: OpType, - parent_optys: [OpType; N], - pred: H::Node, - succ: H::Node, - succ_sig: Cow, -) -> (Replacement, [Node; N], [Node; 2]) { - let pred_ty = cfg.get_optype(pred).as_dataflow_block().unwrap(); - // Make a Hugr with just a single CFG root node having the same signature. - let mut replacement = - Hugr::new_with_entrypoint(root_opty).expect("Replacement should have a CFG entrypoint"); + let mut replacement: Hugr = Hugr::new_with_entrypoint(cfg.entrypoint_optype().clone()) + .expect("Replacement should have a CFG entrypoint"); - let mut merged = replacement.entrypoint(); - let nested_blocks = parent_optys.map(|opty| { - merged = replacement.add_node_with_parent(merged, opty); - merged + let merged = replacement.add_node_with_parent(replacement.entrypoint(), { + DataflowBlock { + inputs: pred_ty.inputs.clone(), + ..succ_ty.clone() + } }); - let input = replacement.add_node_with_parent( merged, Input { @@ -386,7 +376,7 @@ fn mk_rep2( .collect(), mu_new: vec![], }; - (rep, nested_blocks, [dfg1, dfg2]) + (rep, merged, [dfg1, dfg2]) } fn add_unpack( From c4de50a2deefc8bea2066724fc9f9cfad241af4d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 14:24:18 +0100 Subject: [PATCH 08/49] Some fixes, docs --- hugr-passes/src/merge_bbs.rs | 48 +++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 117614eb3e..f8af3b96d2 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -23,7 +23,7 @@ use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; /// If the [HugrMut::entrypoint] of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG -#[deprecated(note = "Use normalize_cfg")] +#[deprecated(note = "Use normalize_cfg")] // Note: as a first step, just hide this pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) where H: 'h + HugrMut, @@ -57,22 +57,43 @@ where } } -#[derive(Clone, Debug, thiserror::Error)] +/// Errors from [normalize_cfg] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] pub enum NormalizeCFGError { - /// The requested node was not a CFG. ALAN note this could just be [hugr_core::hugr::HugrError]?? + /// The requested node was not a CFG #[error("Requested node was not a CFG but {_0}")] NotCFG(OpTag), } +/// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum NormalizeCFGResult { - CFGRemoved, + /// The entire [CFG] was converted into a [DFG] + CFGToDFG, + /// The CFG was preserved, but the entry or exit blocks may have changed. + #[allow(missing_docs)] CFGPreserved { entry_changed: bool, exit_changed: bool, }, } -#[allow(deprecated)] +/// Normalize a CFG in a Hugr: +/// * Merge consecutive basic blocks i.e. where a BB has only a single successor which +/// has no predecessors +/// * If the entry block has only one successor, and no predecessors, then move its contents +/// outside/before CFG. +/// * (Similarly) if the exit block has only one predecessor, then move contents +/// outside/after CFG. +/// * If that predecessor is the entry block, then remove the CFG. +/// +/// *Note that this may remove the entrypoint*; such will be reported in the result +/// ([NormalizeCFGResult::CFGRemoved]) +/// +/// # Errors +/// +/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG +#[allow(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter pub fn normalize_cfg>( mut cfg: &mut H, ) -> Result { @@ -109,14 +130,18 @@ pub fn normalize_cfg>( let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); // Inputs to CFG go directly to consumers of the entry block's Input node for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { - // TODO order edges?? Might need to generalize beyond single_linked... - let src = cfg.single_linked_output(cfg.entrypoint(), inp).unwrap(); + let srcs = cfg + .linked_outputs(cfg.entrypoint(), inp) + .collect::>(); cfg.disconnect(cfg.entrypoint(), inp); for tgt in cfg .linked_inputs(entry_input, inp.index()) .collect::>() { - cfg.connect(src.0, src.1, tgt.0, tgt.1); + // Connecting all sources to all targets handles Order edges as well as Value + for src in srcs.iter() { + cfg.connect(src.0, src.1, tgt.0, tgt.1); + } } } cfg.remove_node(entry_input); @@ -144,7 +169,7 @@ pub fn normalize_cfg>( cfg.set_parent(n, cfg_parent); } if succ == exit { - // 1a. CFG must have distinct entry/exit; but we are left with a no-op + // 1a. We are left with a "CFG" whose only block is the exit, this is illegal. let tys = cfg .get_optype(exit) .as_exit_block() @@ -164,12 +189,12 @@ pub fn normalize_cfg>( cfg.remove_subtree(c); } let inp = cfg.add_node_with_parent(cfg.entrypoint(), Input { types: tys.clone() }); - let out = cfg.add_node_with_parent(cfg.entrypoint(), Input { types: tys.clone() }); + let out = cfg.add_node_with_parent(cfg.entrypoint(), Output { types: tys.clone() }); for p in cfg.node_outputs(inp).collect_vec() { cfg.connect(inp, p, out, p.index()); } // This has replaced the entire CFG with an empty DFG...should we just inline/remove that?? - return Ok(NormalizeCFGResult::CFGRemoved); + return Ok(NormalizeCFGResult::CFGToDFG); } // 1b. else, old entry-node's successor is the new entry node, move into place cfg.move_before_sibling(succ, entry); @@ -401,6 +426,7 @@ fn add_unpack( } #[cfg(test)] +#[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { use std::collections::HashSet; use std::sync::Arc; From ad58ea3784f05b71f47aab098532442ac13648d4 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 14:25:01 +0100 Subject: [PATCH 09/49] Attempt first test == elision. Resulting structure is a PITA to inspect --- hugr-core/src/ops/tag.rs | 2 +- hugr-passes/src/merge_bbs.rs | 53 +++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/hugr-core/src/ops/tag.rs b/hugr-core/src/ops/tag.rs index 2834cd94eb..f329bcb7b9 100644 --- a/hugr-core/src/ops/tag.rs +++ b/hugr-core/src/ops/tag.rs @@ -9,7 +9,7 @@ use std::{cmp, fmt::Display}; /// /// Uses a flat representation for all the variants, in contrast to the complex /// `OpType` structures. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] #[non_exhaustive] pub enum OpTag { /// All operations allowed. diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index f8af3b96d2..bfef9fc2b0 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -438,10 +438,12 @@ mod test { use hugr_core::builder::{CFGBuilder, DFGWrapper, Dataflow, HugrBuilder, endo_sig, inout_sig}; use hugr_core::extension::prelude::{ConstUsize, qb_t, usize_t}; use hugr_core::ops::constant::Value; - use hugr_core::ops::{LoadConstant, OpTrait, OpType}; + use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType}; use hugr_core::types::{Signature, Type, TypeRow}; use hugr_core::{Extension, Hugr, HugrView, Wire, const_extension_ids, type_row}; + use crate::merge_bbs::{NormalizeCFGResult, normalize_cfg}; + use super::merge_basic_blocks; const_extension_ids! { @@ -652,4 +654,53 @@ mod test { fn find_unique(items: impl Iterator, pred: impl Fn(&T) -> bool) -> T { items.filter(pred).exactly_one().ok().unwrap() } + + #[rstest] + fn elide_cfg() { + let ext = extension(); + let op = ext.instantiate_extension_op("Test", []).unwrap(); + let out_ty = op.signature().output().clone(); + let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); + let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); + let op_res = entry + .add_dataflow_op(op.clone(), entry.input_wires()) + .unwrap(); + let predicate = entry.add_load_value(Value::unary_unit_sum()); + let entry = entry + .finish_with_outputs(predicate, op_res.outputs()) + .unwrap(); + cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); + let mut h = cfg.finish_hugr().unwrap(); + + let func = h.children(h.module_root()).exactly_one().ok().unwrap(); + assert_eq!( + h.children(func) + .map(|n| h.get_optype(n).tag()) + .collect::>(), + HashSet::from([OpTag::Input, OpTag::Output, OpTag::Cfg]) + ); + let res = normalize_cfg(&mut h); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); + h.validate().unwrap(); + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!( + h.children(func) + .map(|n| h.get_optype(n).tag()) + .collect::>(), + HashSet::from([ + OpTag::Input, + OpTag::Output, + OpTag::Dfg, + OpTag::Leaf, + OpTag::LoadConst, + OpTag::Const + ]) + ); + assert_eq!( + h.children(func) + .flat_map(|n| h.get_optype(n).as_extension_op()) + .collect_vec(), + [&op] + ); + } } From 43ecfba7f6c84aa2e83f29c8702c75ff074d2532 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 15:43:12 +0100 Subject: [PATCH 10/49] Do elision as separate case, w/ take_inputs --- hugr-passes/src/merge_bbs.rs | 284 ++++++++++++++++++----------------- 1 file changed, 148 insertions(+), 136 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index bfef9fc2b0..57c8751a0d 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; use hugr_core::hugr::views::{RootCheckable, RootChecked}; use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; -use hugr_core::types::{Signature, TypeRow}; +use hugr_core::types::{EdgeKind, Signature, TypeRow}; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; @@ -113,93 +113,94 @@ pub fn normalize_cfg>( let entry_blk = cfg.get_optype(entry).as_dataflow_block().unwrap(); let cfg_parent = cfg.get_parent(cfg.entrypoint()).unwrap(); // 1. If the entry block has only one successor, and no predecessors, then move contents - // outside/before CFG. + // outside/before CFG. (Note if the entry block has multiple successors, but no predecessors, + // we could move its contents outside (before) the CFG, but would need to keep an empty/identity + // entry block - we do not do this here let mut entry_changed = false; - if let Some(succ) = can_elide_entry(cfg, entry, entry_blk, exit) { - // Move contents of entry block outside/before the CFG; the successor becomes the entry block. - // Note if the entry block has multiple successors, but no predecessors, we could move its - // contents outside (before) the CFG, but would need to keep an empty/identity entry block - // - we do not do this here - let [entry_input, entry_output] = cfg.get_io(entry).unwrap(); - let tuple_elems = entry_blk - .sum_rows - .clone() - .into_iter() - .exactly_one() - .unwrap(); - let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); - // Inputs to CFG go directly to consumers of the entry block's Input node - for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { - let srcs = cfg - .linked_outputs(cfg.entrypoint(), inp) - .collect::>(); - cfg.disconnect(cfg.entrypoint(), inp); - for tgt in cfg - .linked_inputs(entry_input, inp.index()) - .collect::>() - { - // Connecting all sources to all targets handles Order edges as well as Value - for src in srcs.iter() { - cfg.connect(src.0, src.1, tgt.0, tgt.1); - } - } - } - cfg.remove_node(entry_input); - - // Update input ports - let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { - panic!() - }; - let inputs_to_add = new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; - cfg_ty.signature.input = new_cfg_inputs; - cfg.add_ports(cfg.entrypoint(), Direction::Incoming, inputs_to_add); - - // Inputs to entry block Output node go instead to CFG - // TODO order edges again - let entry_results = cfg - .in_value_types(entry_output) - .enumerate() - .map(|(i, _)| cfg.single_linked_output(entry_output, i).unwrap()) - .collect::>(); - - add_unpack(cfg, entry_results, tuple_elems, cfg.entrypoint()); - cfg.remove_node(entry_output); - // Transfer remaining entry children - including any used to compute the predicate - while let Some(n) = cfg.first_child(entry) { - cfg.set_parent(n, cfg_parent); - } + #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `cfg` + if let Some(succ) = cfg.output_neighbours(entry).exactly_one().ok() { if succ == exit { - // 1a. We are left with a "CFG" whose only block is the exit, this is illegal. - let tys = cfg - .get_optype(exit) - .as_exit_block() - .unwrap() - .cfg_outputs - .clone(); - let opty = cfg.optype_mut(cfg.entrypoint()); - let OpType::CFG(cfg_ty) = std::mem::take(opty) else { + // Ignore any other predecessors as they are unreachable! + assert_eq!( + &Signature::new( + entry_blk.inputs.clone(), + entry_blk.successor_input(0).unwrap() + ), + cfg.signature(cfg.entrypoint()).unwrap().as_ref() + ); + // Turn the CFG into a DFG containing only what was in the entry block + // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive + let children_to_remove: Vec<_> = cfg.children(cfg.entrypoint()).skip(1).collect(); + for blk in children_to_remove { + cfg.remove_subtree(blk); + } + while let Some(ch) = cfg.first_child(entry) { + cfg.set_parent(ch, cfg.entrypoint()); + } + cfg.remove_node(entry); + let cfg_ty = cfg.optype_mut(cfg.entrypoint()); + let OpType::CFG(cfg_) = std::mem::take(cfg_ty) else { panic!() }; - assert_eq!(cfg_ty.signature.input, tys); - assert_eq!(cfg_ty.signature.output, tys); - *opty = OpType::DFG(DFG { - signature: cfg_ty.signature, + *cfg_ty = OpType::DFG(DFG { + signature: cfg_.signature, }); - while let Some(c) = cfg.first_child(cfg.entrypoint()) { - cfg.remove_subtree(c); + let [_, outp] = cfg.get_io(cfg.entrypoint()).unwrap(); + let (values, orders) = take_inputs(cfg, outp); + let mut unpacked = tuple_elems(cfg, values[0].0, values[0].1).into_owned(); + let OpType::Output(ou) = cfg.optype_mut(outp) else { + panic!() + }; + let rest = std::mem::take(&mut ou.types).into_owned(); + unpacked.extend(rest.into_iter().skip(1)); + ou.types = unpacked.into(); + add_unpack(cfg, values, orders, outp); + return Ok(NormalizeCFGResult::CFGToDFG); + } else if cfg.input_neighbours(succ).count() == 1 { + // 1b. Move contents of entry block outside/before the CFG; the successor becomes the entry block. + let [entry_input, entry_output] = cfg.get_io(entry).unwrap(); + let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); + // Inputs to CFG go directly to consumers of the entry block's Input node + for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { + let srcs = cfg + .linked_outputs(cfg.entrypoint(), inp) + .collect::>(); + cfg.disconnect(cfg.entrypoint(), inp); + for tgt in cfg + .linked_inputs(entry_input, inp.index()) + .collect::>() + { + // Connecting all sources to all targets handles Order edges as well as Value + for src in srcs.iter() { + cfg.connect(src.0, src.1, tgt.0, tgt.1); + } + } } - let inp = cfg.add_node_with_parent(cfg.entrypoint(), Input { types: tys.clone() }); - let out = cfg.add_node_with_parent(cfg.entrypoint(), Output { types: tys.clone() }); - for p in cfg.node_outputs(inp).collect_vec() { - cfg.connect(inp, p, out, p.index()); + cfg.remove_node(entry_input); + + // Update input ports + let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { + panic!() + }; + let inputs_to_add = + new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; + cfg_ty.signature.input = new_cfg_inputs; + cfg.add_ports(cfg.entrypoint(), Direction::Incoming, inputs_to_add); + + // Inputs to entry block Output node go instead to CFG + let (entry_results, orders) = take_inputs(cfg, entry_output); + cfg.remove_node(entry_output); + add_unpack(cfg, entry_results, orders, cfg.entrypoint()); + + // Transfer remaining entry children - including any used to compute the predicate + while let Some(n) = cfg.first_child(entry) { + cfg.set_parent(n, cfg_parent); } - // This has replaced the entire CFG with an empty DFG...should we just inline/remove that?? - return Ok(NormalizeCFGResult::CFGToDFG); + // old entry-node's successor is the new entry node, move into place + cfg.move_before_sibling(succ, entry); + cfg.remove_node(entry); + entry_changed = true; } - // 1b. else, old entry-node's successor is the new entry node, move into place - cfg.move_before_sibling(succ, entry); - cfg.remove_node(entry); - entry_changed = true; } // 2. If the exit node has a single predecessor and that predecessor has no other successors... let mut exit_changed = false; @@ -213,7 +214,6 @@ pub fn normalize_cfg>( let [_, output] = cfg.get_io(pred).unwrap(); let pred_blk = cfg.get_optype(pred).as_dataflow_block().unwrap(); let new_cfg_outs = pred_blk.inner_signature().into_owned().input; - let [tuple_tys] = pred_blk.sum_rows.iter().cloned().collect_array().unwrap(); // new CFG result type and exit block let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { @@ -241,23 +241,14 @@ pub fn normalize_cfg>( cfg.set_parent(n, dfg); } // Add tuple-unpack inside the DFG - let result_srcs = cfg - .node_inputs(output) - .collect::>() - .into_iter() - .map(|p| { - let src = cfg.single_linked_output(output, p).unwrap(); - cfg.disconnect(output, p); - src - }) - .collect::>(); + let (values, orders) = take_inputs(cfg, output); let OpType::Output(ou) = cfg.optype_mut(output) else { panic!() }; ou.types = result_tys; // TODO update number of input ports - add_unpack(cfg, result_srcs, tuple_tys, output); + add_unpack(cfg, values, orders, output); // Move output edges. // TODO result_tys is almost this, but we want to move Order edges too for p in cfg.node_outputs(cfg.entrypoint()).collect_vec() { @@ -278,28 +269,6 @@ pub fn normalize_cfg>( }) } -fn can_elide_entry( - h: &H, - entry: H::Node, - entry_blk: &DataflowBlock, - exit: H::Node, -) -> Option { - let succ = h.output_neighbours(entry).exactly_one().ok()?; - if succ == exit { - // Any other predecessors are unreachable! - assert_eq!( - &Signature::new( - entry_blk.inputs.clone(), - entry_blk.successor_input(0).unwrap() - ), - h.signature(h.entrypoint()).unwrap().as_ref() - ); - } else if h.input_neighbours(entry).count() > 1 { - return None; - } - Some(succ) -} - fn mk_rep( cfg: &H, pred: H::Node, @@ -353,13 +322,16 @@ fn mk_rep( } // At the junction, must unpack the first (tuple, branch predicate) output - let tuple_elems = pred_ty.sum_rows.clone().into_iter().exactly_one().unwrap(); let dfg1_outs = replacement .out_value_types(dfg1) .enumerate() .map(|(i, _)| (dfg1, i.into())) .collect::>(); - add_unpack(&mut replacement, dfg1_outs, tuple_elems, dfg2); + let dfg_order_out = [( + dfg1, + replacement.get_optype(dfg1).other_output_port().unwrap(), + )]; + add_unpack(&mut replacement, dfg1_outs, dfg_order_out, dfg2); // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new // edge-maps as both source and target of the new edge are in the replacement Hugr @@ -404,25 +376,66 @@ fn mk_rep( (rep, merged, [dfg1, dfg2]) } +type NodePorts = Vec<(N, OutgoingPort)>; +fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { + let mut values = vec![]; + let mut orders = vec![]; + for p in h.node_inputs(n).collect_vec() { + let srcs = h.linked_outputs(n, p).collect_vec(); + h.disconnect(n, p); + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(_)) => { + assert_eq!(srcs.len(), 1); + values.extend(srcs); + } + Some(EdgeKind::StateOrder) => { + assert_eq!(orders, []); + orders.extend(srcs); + } + k => panic!("Unexpected port kind: {:?}", k), + } + } + (values, orders) +} + +fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(ty)) => ty.as_sum().unwrap().as_tuple().unwrap().clone(), + p => panic!("Expected Value port not {:?}", p), + } + .try_into() + .unwrap() +} + fn add_unpack( h: &mut H, - srcs: impl IntoIterator, - tuple_tys: TypeRow, + value_srcs: impl IntoIterator, + order_srcs: impl IntoIterator, dst: H::Node, ) { let parent = h.get_parent(dst).unwrap(); - let mut srcs = srcs.into_iter(); - let (src, src_p) = srcs.next().unwrap(); + let mut srcs = value_srcs.into_iter(); + let src_to_unpack = srcs.next().unwrap(); + let tuple_tys = tuple_elems(h, src_to_unpack.0, src_to_unpack.1); let tuple_len = tuple_tys.len(); let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); - h.connect(src, src_p, unp, 0); + h.connect(src_to_unpack.0, src_to_unpack.1, unp, 0); for i in 0..tuple_len { h.connect(unp, i, dst, i); } + assert_eq!( + h.get_optype(dst).other_port_kind(Direction::Incoming), + Some(EdgeKind::StateOrder) + ); + let order_tgt = h.get_optype(dst).other_input_port().unwrap(); for (i, (src, src_p)) in srcs.enumerate() { + assert!(i + tuple_len < order_tgt.index()); h.connect(src, src_p, dst, i + tuple_len); } + for (src, src_p) in order_srcs { + h.connect(src, src_p, dst, order_tgt); + } } #[cfg(test)] @@ -432,6 +445,7 @@ mod test { use std::sync::Arc; use hugr_core::extension::prelude::PRELUDE_ID; + use hugr_core::ops::handle::NodeHandle; use itertools::Itertools; use rstest::rstest; @@ -676,31 +690,29 @@ mod test { assert_eq!( h.children(func) .map(|n| h.get_optype(n).tag()) - .collect::>(), - HashSet::from([OpTag::Input, OpTag::Output, OpTag::Cfg]) + .collect_vec(), + [OpTag::Input, OpTag::Output, OpTag::Cfg] ); + //let dfb = h.nodes().filter(|n| h.get_optype(*n).is_dataflow_block()).exactly_one().ok().unwrap(); + let mut dfb_children = h + .children(entry.node()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(); let res = normalize_cfg(&mut h); assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - h.validate().unwrap(); assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); assert_eq!( h.children(func) .map(|n| h.get_optype(n).tag()) - .collect::>(), - HashSet::from([ - OpTag::Input, - OpTag::Output, - OpTag::Dfg, - OpTag::Leaf, - OpTag::LoadConst, - OpTag::Const - ]) + .collect_vec(), + [OpTag::Input, OpTag::Output, OpTag::Dfg,] ); + dfb_children.push(OpTag::Leaf); assert_eq!( - h.children(func) - .flat_map(|n| h.get_optype(n).as_extension_op()) + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) .collect_vec(), - [&op] + dfb_children ); } } From 271fe8a82e28ee6a6f574f28b22cb015a684f6e3 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 15:58:57 +0100 Subject: [PATCH 11/49] Rename cfg->h, outline cfg_node --- hugr-passes/src/merge_bbs.rs | 123 +++++++++++++++++------------------ 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 57c8751a0d..e37d66d9da 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -95,30 +95,27 @@ pub enum NormalizeCFGResult { /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG #[allow(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter pub fn normalize_cfg>( - mut cfg: &mut H, + mut h: &mut H, ) -> Result { - let checked: RootChecked<_, CfgID> = - RootChecked::<_, CfgID>::try_new(&mut cfg).map_err(|e| match e { + let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) + .map_err(|e| match e { HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), _ => unreachable!(), })?; merge_basic_blocks(checked); + let cfg_node = h.entrypoint(); // Further normalizations with effects outside the CFG - let [entry, exit] = cfg - .children(cfg.entrypoint()) - .take(2) - .collect_array() - .unwrap(); - let entry_blk = cfg.get_optype(entry).as_dataflow_block().unwrap(); - let cfg_parent = cfg.get_parent(cfg.entrypoint()).unwrap(); + let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); + let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); + let cfg_parent = h.get_parent(cfg_node).unwrap(); // 1. If the entry block has only one successor, and no predecessors, then move contents // outside/before CFG. (Note if the entry block has multiple successors, but no predecessors, // we could move its contents outside (before) the CFG, but would need to keep an empty/identity // entry block - we do not do this here let mut entry_changed = false; #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `cfg` - if let Some(succ) = cfg.output_neighbours(entry).exactly_one().ok() { + if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { if succ == exit { // Ignore any other predecessors as they are unreachable! assert_eq!( @@ -126,140 +123,140 @@ pub fn normalize_cfg>( entry_blk.inputs.clone(), entry_blk.successor_input(0).unwrap() ), - cfg.signature(cfg.entrypoint()).unwrap().as_ref() + h.signature(cfg_node).unwrap().as_ref() ); // Turn the CFG into a DFG containing only what was in the entry block // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive - let children_to_remove: Vec<_> = cfg.children(cfg.entrypoint()).skip(1).collect(); + let children_to_remove: Vec<_> = h.children(cfg_node).skip(1).collect(); for blk in children_to_remove { - cfg.remove_subtree(blk); + h.remove_subtree(blk); } - while let Some(ch) = cfg.first_child(entry) { - cfg.set_parent(ch, cfg.entrypoint()); + while let Some(ch) = h.first_child(entry) { + h.set_parent(ch, cfg_node); } - cfg.remove_node(entry); - let cfg_ty = cfg.optype_mut(cfg.entrypoint()); + h.remove_node(entry); + let cfg_ty = h.optype_mut(cfg_node); let OpType::CFG(cfg_) = std::mem::take(cfg_ty) else { panic!() }; *cfg_ty = OpType::DFG(DFG { signature: cfg_.signature, }); - let [_, outp] = cfg.get_io(cfg.entrypoint()).unwrap(); - let (values, orders) = take_inputs(cfg, outp); - let mut unpacked = tuple_elems(cfg, values[0].0, values[0].1).into_owned(); - let OpType::Output(ou) = cfg.optype_mut(outp) else { + // Unpack the first output and shuffle all the rest along + let [_, outp] = h.get_io(cfg_node).unwrap(); + let (values, orders) = take_inputs(h, outp); + let mut unpacked = tuple_elems(h, values[0].0, values[0].1).into_owned(); + h.add_ports(outp, Direction::Incoming, unpacked.len() as isize - 1); + let OpType::Output(ou) = h.optype_mut(outp) else { panic!() }; let rest = std::mem::take(&mut ou.types).into_owned(); unpacked.extend(rest.into_iter().skip(1)); ou.types = unpacked.into(); - add_unpack(cfg, values, orders, outp); + add_unpack(h, values, orders, outp); return Ok(NormalizeCFGResult::CFGToDFG); - } else if cfg.input_neighbours(succ).count() == 1 { + } else if h.input_neighbours(succ).count() == 1 { // 1b. Move contents of entry block outside/before the CFG; the successor becomes the entry block. - let [entry_input, entry_output] = cfg.get_io(entry).unwrap(); + let [entry_input, entry_output] = h.get_io(entry).unwrap(); let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); // Inputs to CFG go directly to consumers of the entry block's Input node - for inp in cfg.node_inputs(cfg.entrypoint()).collect::>() { - let srcs = cfg - .linked_outputs(cfg.entrypoint(), inp) - .collect::>(); - cfg.disconnect(cfg.entrypoint(), inp); - for tgt in cfg + for inp in h.node_inputs(cfg_node).collect::>() { + let srcs = h.linked_outputs(cfg_node, inp).collect::>(); + h.disconnect(cfg_node, inp); + for tgt in h .linked_inputs(entry_input, inp.index()) .collect::>() { // Connecting all sources to all targets handles Order edges as well as Value for src in srcs.iter() { - cfg.connect(src.0, src.1, tgt.0, tgt.1); + h.connect(src.0, src.1, tgt.0, tgt.1); } } } - cfg.remove_node(entry_input); + h.remove_node(entry_input); // Update input ports - let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { panic!() }; let inputs_to_add = new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; cfg_ty.signature.input = new_cfg_inputs; - cfg.add_ports(cfg.entrypoint(), Direction::Incoming, inputs_to_add); + h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); // Inputs to entry block Output node go instead to CFG - let (entry_results, orders) = take_inputs(cfg, entry_output); - cfg.remove_node(entry_output); - add_unpack(cfg, entry_results, orders, cfg.entrypoint()); + let (entry_results, orders) = take_inputs(h, entry_output); + h.remove_node(entry_output); + add_unpack(h, entry_results, orders, cfg_node); // Transfer remaining entry children - including any used to compute the predicate - while let Some(n) = cfg.first_child(entry) { - cfg.set_parent(n, cfg_parent); + while let Some(n) = h.first_child(entry) { + h.set_parent(n, cfg_parent); } // old entry-node's successor is the new entry node, move into place - cfg.move_before_sibling(succ, entry); - cfg.remove_node(entry); + h.move_before_sibling(succ, entry); + h.remove_node(entry); entry_changed = true; } } // 2. If the exit node has a single predecessor and that predecessor has no other successors... let mut exit_changed = false; - if let Some(pred) = cfg + if let Some(pred) = h .input_neighbours(exit) .exactly_one() .ok() - .filter(|pred| cfg.output_neighbours(*pred).count() == 1) + .filter(|pred| h.output_neighbours(*pred).count() == 1) { // Code in that predecessor can be moved outside (after the CFG), and the predecessor deleted - let [_, output] = cfg.get_io(pred).unwrap(); - let pred_blk = cfg.get_optype(pred).as_dataflow_block().unwrap(); + let [_, output] = h.get_io(pred).unwrap(); + let pred_blk = h.get_optype(pred).as_dataflow_block().unwrap(); let new_cfg_outs = pred_blk.inner_signature().into_owned().input; // new CFG result type and exit block - let OpType::CFG(cfg_ty) = cfg.optype_mut(cfg.entrypoint()) else { + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { panic!() }; let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); // TODO update number of CFG outports - *cfg.optype_mut(pred) = ExitBlock { + *h.optype_mut(pred) = ExitBlock { cfg_outputs: new_cfg_outs.clone(), } .into(); - cfg.set_num_ports(pred, cfg.num_ports(pred, Direction::Incoming), 0); + h.set_num_ports(pred, h.num_ports(pred, Direction::Incoming), 0); - cfg.move_before_sibling(pred, exit); - cfg.remove_node(exit); + h.move_before_sibling(pred, exit); + h.remove_node(exit); // Move contents into new DFG - let dfg = cfg.add_node_with_parent( + let dfg = h.add_node_with_parent( cfg_parent, DFG { signature: Signature::new(new_cfg_outs, result_tys.clone()), }, ); - while let Some(n) = cfg.first_child(pred) { - cfg.set_parent(n, dfg); + while let Some(n) = h.first_child(pred) { + h.set_parent(n, dfg); } // Add tuple-unpack inside the DFG - let (values, orders) = take_inputs(cfg, output); - let OpType::Output(ou) = cfg.optype_mut(output) else { + let (values, orders) = take_inputs(h, output); + let OpType::Output(ou) = h.optype_mut(output) else { panic!() }; ou.types = result_tys; // TODO update number of input ports - add_unpack(cfg, values, orders, output); + add_unpack(h, values, orders, output); // Move output edges. // TODO result_tys is almost this, but we want to move Order edges too - for p in cfg.node_outputs(cfg.entrypoint()).collect_vec() { - let tgts = cfg.linked_inputs(cfg.entrypoint(), p).collect_vec(); - cfg.disconnect(cfg.entrypoint(), p); + for p in h.node_outputs(cfg_node).collect_vec() { + let tgts = h.linked_inputs(cfg_node, p).collect_vec(); + h.disconnect(cfg_node, p); for tgt in tgts { - cfg.connect(dfg, p, tgt.0, tgt.1) + h.connect(dfg, p, tgt.0, tgt.1) } } - for p in cfg.node_inputs(dfg).collect_vec() { - cfg.connect(cfg.entrypoint(), p.index(), dfg, p); + for p in h.node_inputs(dfg).collect_vec() { + h.connect(cfg_node, p.index(), dfg, p); } exit_changed = true; } From 80c31a9f35640db29888318b1ec830d5a0657160 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 16:48:44 +0100 Subject: [PATCH 12/49] Test adding some unreachable blocks --- hugr-passes/src/merge_bbs.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index e37d66d9da..55b6c84c27 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -667,7 +667,7 @@ mod test { } #[rstest] - fn elide_cfg() { + fn elide_cfg(#[values(false, true)] extra_blocks: bool) { let ext = extension(); let op = ext.instantiate_extension_op("Test", []).unwrap(); let out_ty = op.signature().output().clone(); @@ -681,6 +681,16 @@ mod test { .finish_with_outputs(predicate, op_res.outputs()) .unwrap(); cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); + if extra_blocks { + let Signature { input, output } = op.signature().as_ref().clone(); + for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { + let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); + let inp = extra.input_wires(); + let branch = extra.add_load_value(Value::unary_unit_sum()); + let extra = extra.finish_with_outputs(branch, inp).unwrap(); + cfg.branch(&extra, 0, &dest).unwrap(); + } + } let mut h = cfg.finish_hugr().unwrap(); let func = h.children(h.module_root()).exactly_one().ok().unwrap(); @@ -690,11 +700,11 @@ mod test { .collect_vec(), [OpTag::Input, OpTag::Output, OpTag::Cfg] ); - //let dfb = h.nodes().filter(|n| h.get_optype(*n).is_dataflow_block()).exactly_one().ok().unwrap(); let mut dfb_children = h .children(entry.node()) .map(|n| h.get_optype(n).tag()) .collect_vec(); + let res = normalize_cfg(&mut h); assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); From 02d39eb32f635d5b30467fb730a41fca0714c0ef Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 17:13:38 +0100 Subject: [PATCH 13/49] Inline unary_unit_sum, sort out imports --- hugr-passes/src/merge_bbs.rs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 55b6c84c27..f5077cff8a 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -441,17 +441,16 @@ mod test { use std::collections::HashSet; use std::sync::Arc; - use hugr_core::extension::prelude::PRELUDE_ID; - use hugr_core::ops::handle::NodeHandle; use itertools::Itertools; use rstest::rstest; - use hugr_core::builder::{CFGBuilder, DFGWrapper, Dataflow, HugrBuilder, endo_sig, inout_sig}; - use hugr_core::extension::prelude::{ConstUsize, qb_t, usize_t}; + use hugr_core::builder::{CFGBuilder, Dataflow, HugrBuilder, endo_sig, inout_sig}; + use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, qb_t, usize_t}; use hugr_core::ops::constant::Value; + use hugr_core::ops::handle::NodeHandle; use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType}; use hugr_core::types::{Signature, Type, TypeRow}; - use hugr_core::{Extension, Hugr, HugrView, Wire, const_extension_ids, type_row}; + use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; use crate::merge_bbs::{NormalizeCFGResult, normalize_cfg}; @@ -480,10 +479,6 @@ mod test { ) } - fn unary_unit_sum + AsRef, T>(b: &mut DFGWrapper) -> Wire { - b.add_load_value(Value::unary_unit_sum()) - } - #[rstest] #[case(true)] #[case(false)] @@ -500,7 +495,6 @@ mod test { \--<--<--/ \--<-----<--/ */ - use hugr_core::extension::prelude::Noop; let loop_variants: TypeRow = vec![qb_t()].into(); let exit_types: TypeRow = vec![usize_t()].into(); let e = extension(); @@ -508,7 +502,7 @@ mod test { let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; let mut no_b1 = h.simple_entry_builder(loop_variants.clone(), 1)?; let n = no_b1.add_dataflow_op(Noop::new(qb_t()), no_b1.input_wires())?; - let br = unary_unit_sum(&mut no_b1); + let br = no_b1.add_load_value(Value::unary_unit_sum()); let no_b1 = no_b1.finish_with_outputs(br, n.outputs())?; let mut test_block = h.block_builder( loop_variants.clone(), @@ -526,7 +520,7 @@ mod test { } else { let mut no_b2 = h.simple_block_builder(endo_sig(loop_variants), 1)?; let n = no_b2.add_dataflow_op(Noop::new(qb_t()), no_b2.input_wires())?; - let br = unary_unit_sum(&mut no_b2); + let br = no_b2.add_load_value(Value::unary_unit_sum()); let nid = no_b2.finish_with_outputs(br, n.outputs())?; h.branch(&nid, 0, &no_b1)?; nid @@ -604,7 +598,7 @@ mod test { let mut bb1 = h.simple_entry_builder(vec![usize_t(), qb_t()].into(), 1)?; let [inw] = bb1.input_wires_arr(); let load_cst = bb1.add_load_value(ConstUsize::new(1)); - let pred = unary_unit_sum(&mut bb1); + let pred = bb1.add_load_value(Value::unary_unit_sum()); let bb1 = bb1.finish_with_outputs(pred, [load_cst, inw])?; let mut bb2 = h.block_builder( @@ -613,7 +607,7 @@ mod test { vec![qb_t(), usize_t()].into(), )?; let [u, q] = bb2.input_wires_arr(); - let pred = unary_unit_sum(&mut bb2); + let pred = bb2.add_load_value(Value::unary_unit_sum()); let bb2 = bb2.finish_with_outputs(pred, [q, u])?; let mut bb3 = h.block_builder( @@ -623,7 +617,7 @@ mod test { )?; let [q, u] = bb3.input_wires_arr(); let tst = bb3.add_dataflow_op(tst_op, [q, u])?; - let pred = unary_unit_sum(&mut bb3); + let pred = bb3.add_load_value(Value::unary_unit_sum()); let bb3 = bb3.finish_with_outputs(pred, tst.outputs())?; // Now add control-flow edges between basic blocks h.branch(&bb1, 0, &bb2)?; From 9242b4c3528c4fb6bb3d256d4420e0dc7ff078af Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 17:15:32 +0100 Subject: [PATCH 14/49] Test removing entry block --- hugr-passes/src/merge_bbs.rs | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index f5077cff8a..34ca0c227f 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -716,4 +716,66 @@ mod test { dfb_children ); } + + #[test] + fn entry_before_loop() -> Result<(), Box> { + /* -> Noop --> Test -> Exit -> Test --> Exit + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let entry = nop_b.finish_with_outputs(br, n.outputs())?; + + let mut loop_b = h.block_builder( + loop_variants.clone(), + vec![loop_variants, exit_types], + type_row![], + )?; + let [tst] = loop_b + .add_dataflow_op(tst_op, loop_b.input_wires())? + .outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&entry, 0, &loop_)?; + h.branch(&loop_, 0, &loop_)?; + h.branch(&loop_, 1, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: false + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + let func_children = h + .children(func) + .map(|n| h.get_optype(n).tag()) + .collect_vec(); + assert_eq!(func_children.len(), 6); + { + use OpTag::*; + assert_eq!( + HashSet::from_iter(func_children), + HashSet::from([Input, Output, Leaf, Cfg, Const, LoadConst]) + ); + } + Ok(()) + } } From 6f296982ed2b2e7ec1f6c245fcca3f405f0182fe Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 20:51:36 +0100 Subject: [PATCH 15/49] fix test (and business logic) --- hugr-passes/src/merge_bbs.rs | 45 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 34ca0c227f..af8f7c8ad8 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -155,7 +155,7 @@ pub fn normalize_cfg>( ou.types = unpacked.into(); add_unpack(h, values, orders, outp); return Ok(NormalizeCFGResult::CFGToDFG); - } else if h.input_neighbours(succ).count() == 1 { + } else if h.input_neighbours(entry).count() == 0 { // 1b. Move contents of entry block outside/before the CFG; the successor becomes the entry block. let [entry_input, entry_output] = h.get_io(entry).unwrap(); let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); @@ -324,11 +324,11 @@ fn mk_rep( .enumerate() .map(|(i, _)| (dfg1, i.into())) .collect::>(); - let dfg_order_out = [( - dfg1, - replacement.get_optype(dfg1).other_output_port().unwrap(), - )]; - add_unpack(&mut replacement, dfg1_outs, dfg_order_out, dfg2); + + let dfg_order_out = replacement.get_optype(dfg1).other_output_port().unwrap(); + let order_srcs = (dfg1_outs.is_empty()).then_some((dfg1, dfg_order_out)); + // Do not add Order edges between DFGs unless there are no value edges + add_unpack(&mut replacement, dfg1_outs, order_srcs, dfg2); // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new // edge-maps as both source and target of the new edge are in the replacement Hugr @@ -727,7 +727,7 @@ mod test { let exit_types: TypeRow = vec![usize_t()].into(); let e = extension(); let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + let mut h = CFGBuilder::new(inout_sig(qb_t(), usize_t()))?; let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; let br = nop_b.add_load_value(Value::unary_unit_sum()); @@ -735,12 +735,12 @@ mod test { let mut loop_b = h.block_builder( loop_variants.clone(), - vec![loop_variants, exit_types], + [loop_variants, exit_types], type_row![], )?; - let [tst] = loop_b - .add_dataflow_op(tst_op, loop_b.input_wires())? - .outputs_arr(); + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); let loop_ = loop_b.finish_with_outputs(tst, [])?; h.branch(&entry, 0, &loop_)?; h.branch(&loop_, 0, &loop_)?; @@ -764,18 +764,29 @@ mod test { [OpTag::DataflowBlock, OpTag::BasicBlockExit] ); let func = h.get_parent(h.entrypoint()).unwrap(); - let func_children = h + let mut func_children = h .children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(); - assert_eq!(func_children.len(), 6); + .map(|n| (h.get_optype(n).tag(), n)) + .into_group_map(); + let ext_ops = func_children.remove(&OpTag::Leaf).unwrap(); + assert_eq!( + ext_ops + .into_iter() + .map(|n| h.get_optype(n).as_extension_op().unwrap().unqualified_id()) + .sorted() + .collect_vec(), + ["Noop", "UnpackTuple"] + ); + { use OpTag::*; assert_eq!( - HashSet::from_iter(func_children), - HashSet::from([Input, Output, Leaf, Cfg, Const, LoadConst]) + func_children.keys().copied().collect::>(), + HashSet::from([Input, Output, Cfg, Const, LoadConst]) ); } + assert!(func_children.values().all(|v| v.len() == 1)); + Ok(()) } } From 1d4780b12466431a002873d7e86a6818cd56266a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 19:33:19 +0100 Subject: [PATCH 16/49] update numbers of ports --- hugr-passes/src/merge_bbs.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index af8f7c8ad8..68ea1bc604 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -217,13 +217,18 @@ pub fn normalize_cfg>( panic!() }; let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); - // TODO update number of CFG outports + h.add_ports( + cfg_node, + Direction::Outgoing, + new_cfg_outs.len() as isize - result_tys.len() as isize, + ); *h.optype_mut(pred) = ExitBlock { cfg_outputs: new_cfg_outs.clone(), } .into(); - h.set_num_ports(pred, h.num_ports(pred, Direction::Incoming), 0); + debug_assert_eq!(h.num_ports(pred, Direction::Outgoing), 1); + h.set_num_ports(pred, 1, 0); h.move_before_sibling(pred, exit); h.remove_node(exit); @@ -242,12 +247,12 @@ pub fn normalize_cfg>( let OpType::Output(ou) = h.optype_mut(output) else { panic!() }; + let ports_to_add = result_tys.len() as isize - ou.types.len() as isize; ou.types = result_tys; - // TODO update number of input ports + h.add_ports(output, Direction::Incoming, ports_to_add); add_unpack(h, values, orders, output); // Move output edges. - // TODO result_tys is almost this, but we want to move Order edges too for p in h.node_outputs(cfg_node).collect_vec() { let tgts = h.linked_inputs(cfg_node, p).collect_vec(); h.disconnect(cfg_node, p); From 4a9de85ebee095ca754a065c4ce1849c7a6d360b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 26 Sep 2025 22:03:09 +0100 Subject: [PATCH 17/49] Test removing block before exit --- hugr-passes/src/merge_bbs.rs | 100 ++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 68ea1bc604..f325fea9c3 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -443,7 +443,7 @@ fn add_unpack( #[cfg(test)] #[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; use std::sync::Arc; use itertools::Itertools; @@ -487,7 +487,7 @@ mod test { #[rstest] #[case(true)] #[case(false)] - fn in_loop(#[case] self_loop: bool) -> Result<(), Box> { + fn merge_bbs_in_loop(#[case] self_loop: bool) -> Result<(), Box> { /* self_loop==False: -> Noop1 -----> Test -> Exit -> Noop1AndTest --> Exit | | => / \ @@ -585,7 +585,7 @@ mod test { } #[test] - fn triple_with_permute() -> Result<(), Box> { + fn merge_bbs_triple_with_permute() -> Result<(), Box> { // Blocks are just BB1 -> BB2 -> BB3 --> Exit. // CFG Normalization would move everything outside the CFG and elide the CFG altogether, // but this is an easy-to-construct test of merge-basic-blocks only (no CFG normalization). @@ -705,6 +705,7 @@ mod test { .collect_vec(); let res = normalize_cfg(&mut h); + h.validate().unwrap(); assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); assert_eq!( @@ -769,17 +770,9 @@ mod test { [OpTag::DataflowBlock, OpTag::BasicBlockExit] ); let func = h.get_parent(h.entrypoint()).unwrap(); - let mut func_children = h - .children(func) - .map(|n| (h.get_optype(n).tag(), n)) - .into_group_map(); - let ext_ops = func_children.remove(&OpTag::Leaf).unwrap(); + let mut func_children = child_optypes(&h, func); assert_eq!( - ext_ops - .into_iter() - .map(|n| h.get_optype(n).as_extension_op().unwrap().unqualified_id()) - .sorted() - .collect_vec(), + ext_op_ids(&h, func_children.remove(&OpTag::Leaf).unwrap()), ["Noop", "UnpackTuple"] ); @@ -794,4 +787,85 @@ mod test { Ok(()) } + + #[test] + fn loop_before_exit() -> Result<(), Box> { + /* -> Test -> Noop -> Exit -> Test --> Exit (then Noop) + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + + let mut loop_b = h.entry_builder(vec![loop_variants, exit_types.clone()], type_row![])?; + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&loop_, 0, &loop_)?; + + let mut nop_b = h.simple_block_builder(endo_sig(exit_types), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(usize_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let tail = nop_b.finish_with_outputs(br, n.outputs())?; + + h.branch(&loop_, 1, &tail)?; + h.branch(&tail, 0, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: false, + exit_changed: true + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + assert_eq!( + h.children(func) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] + ); + + let dfg = h.children(func).last().unwrap(); + let mut dfg_children = child_optypes(&h, dfg); + let ext_ops = dfg_children.remove(&OpTag::Leaf).unwrap(); + assert_eq!(ext_op_ids(&h, ext_ops), ["Noop", "UnpackTuple"]); + + { + use OpTag::*; + assert_eq!( + dfg_children.keys().copied().collect::>(), + HashSet::from([Input, Output, Const, LoadConst]) + ); + } + assert!(dfg_children.values().all(|v| v.len() == 1)); + Ok(()) + } + + fn child_optypes(h: &H, n: H::Node) -> HashMap> { + h.children(n) + .map(|n| (h.get_optype(n).tag(), n)) + .into_group_map() + } + + fn ext_op_ids(h: &H, nodes: Vec) -> Vec<&str> { + nodes + .into_iter() + .map(|n| h.get_optype(n).as_extension_op().unwrap().unqualified_id()) + .sorted() + .collect_vec() + } } From 32f5e3286c4a67d0fd7ff2b4912c2bf6484c1ae7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 27 Sep 2025 09:24:26 +0100 Subject: [PATCH 18/49] rename add_unpack => wire_unpack_first --- hugr-passes/src/merge_bbs.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index f325fea9c3..03e4727003 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -114,10 +114,10 @@ pub fn normalize_cfg>( // we could move its contents outside (before) the CFG, but would need to keep an empty/identity // entry block - we do not do this here let mut entry_changed = false; - #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `cfg` + #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { if succ == exit { - // Ignore any other predecessors as they are unreachable! + // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore assert_eq!( &Signature::new( entry_blk.inputs.clone(), @@ -153,7 +153,7 @@ pub fn normalize_cfg>( let rest = std::mem::take(&mut ou.types).into_owned(); unpacked.extend(rest.into_iter().skip(1)); ou.types = unpacked.into(); - add_unpack(h, values, orders, outp); + wire_unpack_first(h, values, orders, outp); return Ok(NormalizeCFGResult::CFGToDFG); } else if h.input_neighbours(entry).count() == 0 { // 1b. Move contents of entry block outside/before the CFG; the successor becomes the entry block. @@ -187,7 +187,7 @@ pub fn normalize_cfg>( // Inputs to entry block Output node go instead to CFG let (entry_results, orders) = take_inputs(h, entry_output); h.remove_node(entry_output); - add_unpack(h, entry_results, orders, cfg_node); + wire_unpack_first(h, entry_results, orders, cfg_node); // Transfer remaining entry children - including any used to compute the predicate while let Some(n) = h.first_child(entry) { @@ -250,8 +250,8 @@ pub fn normalize_cfg>( let ports_to_add = result_tys.len() as isize - ou.types.len() as isize; ou.types = result_tys; h.add_ports(output, Direction::Incoming, ports_to_add); + wire_unpack_first(h, values, orders, output); - add_unpack(h, values, orders, output); // Move output edges. for p in h.node_outputs(cfg_node).collect_vec() { let tgts = h.linked_inputs(cfg_node, p).collect_vec(); @@ -333,7 +333,7 @@ fn mk_rep( let dfg_order_out = replacement.get_optype(dfg1).other_output_port().unwrap(); let order_srcs = (dfg1_outs.is_empty()).then_some((dfg1, dfg_order_out)); // Do not add Order edges between DFGs unless there are no value edges - add_unpack(&mut replacement, dfg1_outs, order_srcs, dfg2); + wire_unpack_first(&mut replacement, dfg1_outs, order_srcs, dfg2); // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new // edge-maps as both source and target of the new edge are in the replacement Hugr @@ -409,7 +409,7 @@ fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { .unwrap() } -fn add_unpack( +fn wire_unpack_first( h: &mut H, value_srcs: impl IntoIterator, order_srcs: impl IntoIterator, From 6f9a7aec52d02a18cd55b56890c4dc416b7a0e64 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 27 Sep 2025 09:40:15 +0100 Subject: [PATCH 19/49] common up unpack_before_output (via an extra clone of a typerow) --- hugr-passes/src/merge_bbs.rs | 46 ++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 03e4727003..7b2ae93545 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -11,7 +11,9 @@ use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output}; +use hugr_core::ops::{ + CFG, DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output, +}; use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; /// Merge any basic blocks that are direct children of the specified CFG @@ -136,24 +138,12 @@ pub fn normalize_cfg>( } h.remove_node(entry); let cfg_ty = h.optype_mut(cfg_node); - let OpType::CFG(cfg_) = std::mem::take(cfg_ty) else { + let OpType::CFG(CFG { signature }) = std::mem::take(cfg_ty) else { panic!() }; - *cfg_ty = OpType::DFG(DFG { - signature: cfg_.signature, - }); - // Unpack the first output and shuffle all the rest along - let [_, outp] = h.get_io(cfg_node).unwrap(); - let (values, orders) = take_inputs(h, outp); - let mut unpacked = tuple_elems(h, values[0].0, values[0].1).into_owned(); - h.add_ports(outp, Direction::Incoming, unpacked.len() as isize - 1); - let OpType::Output(ou) = h.optype_mut(outp) else { - panic!() - }; - let rest = std::mem::take(&mut ou.types).into_owned(); - unpacked.extend(rest.into_iter().skip(1)); - ou.types = unpacked.into(); - wire_unpack_first(h, values, orders, outp); + let result_tys = signature.output.clone(); + *cfg_ty = OpType::DFG(DFG { signature }); + unpack_before_output(h, h.get_io(cfg_node).unwrap()[1], result_tys); return Ok(NormalizeCFGResult::CFGToDFG); } else if h.input_neighbours(entry).count() == 0 { // 1b. Move contents of entry block outside/before the CFG; the successor becomes the entry block. @@ -242,15 +232,7 @@ pub fn normalize_cfg>( while let Some(n) = h.first_child(pred) { h.set_parent(n, dfg); } - // Add tuple-unpack inside the DFG - let (values, orders) = take_inputs(h, output); - let OpType::Output(ou) = h.optype_mut(output) else { - panic!() - }; - let ports_to_add = result_tys.len() as isize - ou.types.len() as isize; - ou.types = result_tys; - h.add_ports(output, Direction::Incoming, ports_to_add); - wire_unpack_first(h, values, orders, output); + unpack_before_output(h, output, result_tys); // Move output edges. for p in h.node_outputs(cfg_node).collect_vec() { @@ -440,6 +422,18 @@ fn wire_unpack_first( } } +/// Unpack the first input to specified [Output] node and shuffle all the rest along +fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: TypeRow) { + let (values, orders) = take_inputs(h, output_node); + let OpType::Output(ou) = h.optype_mut(output_node) else { + panic!() + }; + let ports_to_add = new_types.len() as isize - ou.types.len() as isize; + ou.types = new_types; + h.add_ports(output_node, Direction::Incoming, ports_to_add); + wire_unpack_first(h, values, orders, output_node); +} + #[cfg(test)] #[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { From 6f5e59581392d779337f1afc1718a33f6ed9f3b9 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 27 Sep 2025 22:07:54 +0100 Subject: [PATCH 20/49] simplify test checks using strings --- hugr-passes/src/merge_bbs.rs | 94 ++++++++++++++---------------------- 1 file changed, 35 insertions(+), 59 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 7b2ae93545..2bf8ba0e4d 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -437,7 +437,7 @@ fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: #[cfg(test)] #[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { - use std::collections::{HashMap, HashSet}; + use std::collections::HashSet; use std::sync::Arc; use itertools::Itertools; @@ -687,34 +687,16 @@ mod test { let mut h = cfg.finish_hugr().unwrap(); let func = h.children(h.module_root()).exactly_one().ok().unwrap(); - assert_eq!( - h.children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::Input, OpTag::Output, OpTag::Cfg] - ); - let mut dfb_children = h - .children(entry.node()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); + let mut dfb_children = child_tags_ext_ids(&h, entry.node()); let res = normalize_cfg(&mut h); h.validate().unwrap(); assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!( - h.children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::Input, OpTag::Output, OpTag::Dfg,] - ); - dfb_children.push(OpTag::Leaf); - assert_eq!( - h.children(h.entrypoint()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - dfb_children - ); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); + dfb_children.push("UnpackTuple".to_string()); + assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); } #[test] @@ -764,21 +746,19 @@ mod test { [OpTag::DataflowBlock, OpTag::BasicBlockExit] ); let func = h.get_parent(h.entrypoint()).unwrap(); - let mut func_children = child_optypes(&h, func); + let func_children = child_tags_ext_ids(&h, func); assert_eq!( - ext_op_ids(&h, func_children.remove(&OpTag::Leaf).unwrap()), - ["Noop", "UnpackTuple"] + func_children.into_iter().sorted().collect_vec(), + [ + "Cfg", + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] ); - - { - use OpTag::*; - assert_eq!( - func_children.keys().copied().collect::>(), - HashSet::from([Input, Output, Cfg, Const, LoadConst]) - ); - } - assert!(func_children.values().all(|v| v.len() == 1)); - Ok(()) } @@ -834,32 +814,28 @@ mod test { ); let dfg = h.children(func).last().unwrap(); - let mut dfg_children = child_optypes(&h, dfg); - let ext_ops = dfg_children.remove(&OpTag::Leaf).unwrap(); - assert_eq!(ext_op_ids(&h, ext_ops), ["Noop", "UnpackTuple"]); + let dfg_children = child_tags_ext_ids(&h, dfg); + assert_eq!( + dfg_children.into_iter().sorted().collect_vec(), + [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] + ); - { - use OpTag::*; - assert_eq!( - dfg_children.keys().copied().collect::>(), - HashSet::from([Input, Output, Const, LoadConst]) - ); - } - assert!(dfg_children.values().all(|v| v.len() == 1)); Ok(()) } - fn child_optypes(h: &H, n: H::Node) -> HashMap> { + fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { h.children(n) - .map(|n| (h.get_optype(n).tag(), n)) - .into_group_map() - } - - fn ext_op_ids(h: &H, nodes: Vec) -> Vec<&str> { - nodes - .into_iter() - .map(|n| h.get_optype(n).as_extension_op().unwrap().unqualified_id()) - .sorted() - .collect_vec() + .map(|n| match h.get_optype(n) { + OpType::ExtensionOp(e) => e.unqualified_id().to_string(), + op => format!("{:?}", op.tag()), + }) + .collect() } } From 32519184ee524e26412c54544b7a042ddf26c078 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 27 Sep 2025 22:17:18 +0100 Subject: [PATCH 21/49] Convert triple-BB test to use normalize_cfgs --- hugr-passes/src/merge_bbs.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 2bf8ba0e4d..12f6f195f1 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -579,10 +579,10 @@ mod test { } #[test] - fn merge_bbs_triple_with_permute() -> Result<(), Box> { + fn elide_triple_with_permute() -> Result<(), Box> { // Blocks are just BB1 -> BB2 -> BB3 --> Exit. - // CFG Normalization would move everything outside the CFG and elide the CFG altogether, - // but this is an easy-to-construct test of merge-basic-blocks only (no CFG normalization). + // Should be merged into one BB (we don't check that specifically) + // and then the whole CFG elided. let e = extension(); let tst_op: OpType = e.instantiate_extension_op("Test", &[])?.into(); let [res_t] = tst_op @@ -624,20 +624,22 @@ mod test { h.branch(&bb3, 0, &h.exit_block())?; let mut h = h.finish_hugr()?; - merge_basic_blocks(&mut h); + let res = normalize_cfg(&mut h); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); h.validate()?; - - // Should only be one BB left - let [bb, _exit] = h - .children(h.entrypoint()) - .collect::>() - .try_into() - .unwrap(); + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!( + h.entry_descendants().find(|n| matches!( + h.get_optype(*n), + OpType::DataflowBlock(_) | OpType::CFG(_) | OpType::ExitBlock(_) + )), + None + ); let tst = find_unique( h.entry_descendants(), |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), ); - assert_eq!(h.get_parent(tst), Some(bb)); + assert_eq!(h.get_parent(tst), Some(h.entrypoint())); let inp = find_unique(h.entry_descendants(), |n| { matches!(h.get_optype(*n), OpType::Input(_)) From 4fa1f408ddc0ea43699967ab043eb983199fdc75 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 27 Sep 2025 22:20:30 +0100 Subject: [PATCH 22/49] elide_cfg only tests w/ unreachable entry-predecessor --- hugr-passes/src/merge_bbs.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 12f6f195f1..4fd341dc1f 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -662,7 +662,7 @@ mod test { } #[rstest] - fn elide_cfg(#[values(false, true)] extra_blocks: bool) { + fn elide_cfg() { let ext = extension(); let op = ext.instantiate_extension_op("Test", []).unwrap(); let out_ty = op.signature().output().clone(); @@ -676,16 +676,17 @@ mod test { .finish_with_outputs(predicate, op_res.outputs()) .unwrap(); cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); - if extra_blocks { - let Signature { input, output } = op.signature().as_ref().clone(); - for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { - let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); - let inp = extra.input_wires(); - let branch = extra.add_load_value(Value::unary_unit_sum()); - let extra = extra.finish_with_outputs(branch, inp).unwrap(); - cfg.branch(&extra, 0, &dest).unwrap(); - } + + // Add an (unreachable) block before the entry + let Signature { input, output } = op.signature().as_ref().clone(); + for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { + let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); + let inp = extra.input_wires(); + let branch = extra.add_load_value(Value::unary_unit_sum()); + let extra = extra.finish_with_outputs(branch, inp).unwrap(); + cfg.branch(&extra, 0, &dest).unwrap(); } + let mut h = cfg.finish_hugr().unwrap(); let func = h.children(h.module_root()).exactly_one().ok().unwrap(); From 3593a6616824f725fa8f42d975060250be3f8b7d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 09:52:52 +0100 Subject: [PATCH 23/49] docs --- hugr-passes/src/merge_bbs.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 4fd341dc1f..27722ca17a 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -22,7 +22,7 @@ use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; /// /// # Panics /// -/// If the [HugrMut::entrypoint] of `cfg` is not an [OpType::CFG] +/// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG #[deprecated(note = "Use normalize_cfg")] // Note: as a first step, just hide this @@ -87,10 +87,9 @@ pub enum NormalizeCFGResult { /// outside/before CFG. /// * (Similarly) if the exit block has only one predecessor, then move contents /// outside/after CFG. -/// * If that predecessor is the entry block, then remove the CFG. +/// * If that predecessor is the entry block, then convert the CFG to a DFG. +/// This will be reported via [NormalizeCFGResult::CFGToDFG] /// -/// *Note that this may remove the entrypoint*; such will be reported in the result -/// ([NormalizeCFGResult::CFGRemoved]) /// /// # Errors /// From 6ad9ff48999d708fbc50a8562b0755bd592c5e4a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 09:55:07 +0100 Subject: [PATCH 24/49] InlineDFG defaults to Node --- hugr-core/src/hugr/patch/inline_dfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/patch/inline_dfg.rs b/hugr-core/src/hugr/patch/inline_dfg.rs index fe193c1f2f..307be2eb27 100644 --- a/hugr-core/src/hugr/patch/inline_dfg.rs +++ b/hugr-core/src/hugr/patch/inline_dfg.rs @@ -8,7 +8,7 @@ use crate::ops::handle::{DfgID, NodeHandle}; use crate::{HugrView, IncomingPort, Node, OutgoingPort, PortIndex}; /// Structure identifying an `InlineDFG` rewrite from the spec -pub struct InlineDFG(pub DfgID); +pub struct InlineDFG(pub DfgID); /// Errors from an [`InlineDFG`] rewrite. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] From d60928ff334e1618ed123986cd989bd07adb5700 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 10:00:37 +0100 Subject: [PATCH 25/49] fmt --- hugr-core/src/hugr/patch/inline_dfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-core/src/hugr/patch/inline_dfg.rs b/hugr-core/src/hugr/patch/inline_dfg.rs index 307be2eb27..f5a3653ee4 100644 --- a/hugr-core/src/hugr/patch/inline_dfg.rs +++ b/hugr-core/src/hugr/patch/inline_dfg.rs @@ -8,7 +8,7 @@ use crate::ops::handle::{DfgID, NodeHandle}; use crate::{HugrView, IncomingPort, Node, OutgoingPort, PortIndex}; /// Structure identifying an `InlineDFG` rewrite from the spec -pub struct InlineDFG(pub DfgID); +pub struct InlineDFG(pub DfgID); /// Errors from an [`InlineDFG`] rewrite. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] From d89a65fc559ee5830bc8e9d63c9948200d916fed Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 13:21:01 +0100 Subject: [PATCH 26/49] Generalize normalize_cfg to any node type --- hugr-passes/src/merge_bbs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 27722ca17a..fb2c3756b3 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -95,7 +95,7 @@ pub enum NormalizeCFGResult { /// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG #[allow(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter -pub fn normalize_cfg>( +pub fn normalize_cfg( mut h: &mut H, ) -> Result { let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) From 8f44608f89d7389d639c085f52bd56c3f95af749 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 14:32:28 +0100 Subject: [PATCH 27/49] WIP test on nested cfg --- hugr-passes/src/merge_bbs.rs | 113 +++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index fb2c3756b3..395fa03625 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -16,6 +16,8 @@ use hugr_core::ops::{ }; use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; +use crate::ComposablePass; + /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor /// is B, B and B' can be combined. @@ -80,6 +82,45 @@ pub enum NormalizeCFGResult { }, } +/// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. +#[derive(Clone, Debug, Default)] +pub struct NormalizeCFGPass { + cfgs: Vec, +} + +impl NormalizeCFGPass { + /// Allows mutating the set of CFG nodes that will be normalized. + /// + /// If empty (the default), all (non-strict) descendants of the [HugrView::entrypoint] + /// will be normalized. + pub fn cfgs(&mut self) -> &mut Vec { + &mut self.cfgs + } +} + +impl ComposablePass for NormalizeCFGPass { + type Error = NormalizeCFGError; + + /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG + type Result = HashMap; + + fn run(&self, hugr: &mut H) -> Result { + let cfgs = if self.cfgs.is_empty() { + hugr.entry_descendants() + .filter(|n| hugr.get_optype(*n).is_cfg()) + .collect() + } else { + self.cfgs.clone() + }; + let mut results = HashMap::new(); + for cfg in cfgs { + let res = normalize_cfg(hugr)?; + results.insert(cfg, res); + } + Ok(results) + } +} + /// Normalize a CFG in a Hugr: /// * Merge consecutive basic blocks i.e. where a BB has only a single successor which /// has no predecessors @@ -95,9 +136,7 @@ pub enum NormalizeCFGResult { /// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG #[allow(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter -pub fn normalize_cfg( - mut h: &mut H, -) -> Result { +pub fn normalize_cfg(mut h: &mut H) -> Result { let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) .map_err(|e| match e { HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), @@ -442,11 +481,13 @@ mod test { use itertools::Itertools; use rstest::rstest; - use hugr_core::builder::{CFGBuilder, Dataflow, HugrBuilder, endo_sig, inout_sig}; + use hugr_core::builder::{ + CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, + }; use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, qb_t, usize_t}; use hugr_core::ops::constant::Value; use hugr_core::ops::handle::NodeHandle; - use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType}; + use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; use hugr_core::types::{Signature, Type, TypeRow}; use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; @@ -832,6 +873,68 @@ mod test { Ok(()) } + #[test] + fn nested_cfgs_pass() { + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", []).unwrap(); + let qqu = vec![qb_t(), qb_t(), usize_t()]; + let qq = TypeRow::from(vec![qb_t(); 2]); + let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); + let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2, u] = entry.input_wires_arr(); + let [q1, q2] = { + let mut inner = entry + .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) + .unwrap(); + let mut entry = inner.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2] = entry.input_wires_arr(); + let [pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(pred, []).unwrap(); + inner.branch(&entry, 0, &inner.exit_block()).unwrap(); + inner.finish_sub_container().unwrap().outputs_arr() + }; + let [pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(pred, []).unwrap(); + + let loop_b = { + let mut loop_b = outer + .block_builder(qq.clone(), [qb_t().into(), usize_t().into()], qb_t().into()) + .unwrap(); + let [q1, q2] = loop_b.input_wires_arr(); + // u here is `dom` edge from entry block + let [pred] = loop_b + .add_dataflow_op(tst_op, [q1, u]) + .unwrap() + .outputs_arr(); + loop_b.finish_with_outputs(pred, [q2]).unwrap() + }; + outer.branch(&entry, 0, &loop_b).unwrap(); + outer.branch(&loop_b, 0, &loop_b).unwrap(); + + let tail_b = { + let uq = TypeRow::from(vec![usize_t(), qb_t()]); + let mut tail_b = outer + .block_builder(uq.clone(), vec![uq.clone()], type_row![]) + .unwrap(); + let [u, q] = tail_b.input_wires_arr(); + let [br] = tail_b + .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) + .unwrap() + .outputs_arr(); + tail_b.finish_with_outputs(br, []).unwrap() + }; + outer.branch(&loop_b, 1, &tail_b).unwrap(); + outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); + let mut h = outer.finish_hugr().unwrap(); + normalize_cfg(&mut h).unwrap(); + } + fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { h.children(n) .map(|n| match h.get_optype(n) { From 476e97fcc7e99e1f6d6e79f043681d6497308ac8 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 20:33:47 +0100 Subject: [PATCH 28/49] test result, validate, test structure; fix entrypoint --- hugr-passes/src/merge_bbs.rs | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 395fa03625..c9e39f8b3c 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -83,11 +83,17 @@ pub enum NormalizeCFGResult { } /// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct NormalizeCFGPass { cfgs: Vec, } +impl Default for NormalizeCFGPass { + fn default() -> Self { + Self { cfgs: vec![] } + } +} + impl NormalizeCFGPass { /// Allows mutating the set of CFG nodes that will be normalized. /// @@ -114,7 +120,7 @@ impl ComposablePass for NormalizeCFGPass { }; let mut results = HashMap::new(); for cfg in cfgs { - let res = normalize_cfg(hugr)?; + let res = normalize_cfg(&mut hugr.with_entrypoint_mut(cfg))?; results.insert(cfg, res); } Ok(results) @@ -475,7 +481,7 @@ fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: #[cfg(test)] #[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; use std::sync::Arc; use itertools::Itertools; @@ -491,7 +497,8 @@ mod test { use hugr_core::types::{Signature, Type, TypeRow}; use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; - use crate::merge_bbs::{NormalizeCFGResult, normalize_cfg}; + use crate::ComposablePass; + use crate::merge_bbs::{NormalizeCFGPass, NormalizeCFGResult, normalize_cfg}; use super::merge_basic_blocks; @@ -875,6 +882,9 @@ mod test { #[test] fn nested_cfgs_pass() { + // --> Entry --> Loop --> Tail --> EXIT + // | / \ + // (E->X) \<-/ let e = extension(); let tst_op = e.instantiate_extension_op("Test", []).unwrap(); let qqu = vec![qb_t(), qb_t(), usize_t()]; @@ -882,7 +892,7 @@ mod test { let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); let [q1, q2, u] = entry.input_wires_arr(); - let [q1, q2] = { + let inner = { let mut inner = entry .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) .unwrap(); @@ -894,8 +904,9 @@ mod test { .outputs_arr(); let entry = entry.finish_with_outputs(pred, []).unwrap(); inner.branch(&entry, 0, &inner.exit_block()).unwrap(); - inner.finish_sub_container().unwrap().outputs_arr() + inner.finish_sub_container().unwrap() }; + let [q1, q2] = inner.outputs_arr(); let [pred] = entry .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) .unwrap() @@ -932,7 +943,30 @@ mod test { outer.branch(&loop_b, 1, &tail_b).unwrap(); outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); let mut h = outer.finish_hugr().unwrap(); - normalize_cfg(&mut h).unwrap(); + let res = NormalizeCFGPass::default().run(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + HashMap::from([ + (inner.node(), NormalizeCFGResult::CFGToDFG), + ( + h.entrypoint(), + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: true + } + ) + ]) + ); + // Now contains only one CFG with one BB (self-loop) + let cfg = h + .nodes() + .filter(|n| h.get_optype(*n).is_cfg()) + .exactly_one() + .ok() + .unwrap(); + let [entry, exit] = h.children(cfg).collect_array().unwrap(); + assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); } fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { From 3830cafabfe7048429f04e3ccb058c8c27b67301 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 21:18:09 +0100 Subject: [PATCH 29/49] Test predicates in right place and followed by unpack. --- hugr-passes/src/merge_bbs.rs | 70 +++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index c9e39f8b3c..294a078c7c 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -484,23 +484,21 @@ mod test { use std::collections::{HashMap, HashSet}; use std::sync::Arc; + use hugr_core::extension::simple_op::MakeExtensionOp; use itertools::Itertools; use rstest::rstest; use hugr_core::builder::{ - CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, + CFGBuilder, Container, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, }; - use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, qb_t, usize_t}; - use hugr_core::ops::constant::Value; - use hugr_core::ops::handle::NodeHandle; + use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; + use hugr_core::ops::{constant::Value, handle::NodeHandle}; use hugr_core::types::{Signature, Type, TypeRow}; use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; + use super::{NormalizeCFGPass, NormalizeCFGResult, merge_basic_blocks, normalize_cfg}; use crate::ComposablePass; - use crate::merge_bbs::{NormalizeCFGPass, NormalizeCFGResult, normalize_cfg}; - - use super::merge_basic_blocks; const_extension_ids! { const EXT_ID: ExtensionId = "TestExt"; @@ -892,7 +890,7 @@ mod test { let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); let [q1, q2, u] = entry.input_wires_arr(); - let inner = { + let (inner, inner_pred) = { let mut inner = entry .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) .unwrap(); @@ -904,14 +902,19 @@ mod test { .outputs_arr(); let entry = entry.finish_with_outputs(pred, []).unwrap(); inner.branch(&entry, 0, &inner.exit_block()).unwrap(); - inner.finish_sub_container().unwrap() + (inner.finish_sub_container().unwrap(), pred.node()) }; let [q1, q2] = inner.outputs_arr(); - let [pred] = entry + let [entry_pred] = entry .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) .unwrap() .outputs_arr(); - let entry = entry.finish_with_outputs(pred, []).unwrap(); + let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); + eprintln!( + "ALAN inner cfg is {} of type {:?}", + inner.node(), + outer.hugr().get_optype(inner.node()) + ); let loop_b = { let mut loop_b = outer @@ -928,7 +931,7 @@ mod test { outer.branch(&entry, 0, &loop_b).unwrap(); outer.branch(&loop_b, 0, &loop_b).unwrap(); - let tail_b = { + let (tail_b, tail_pred) = { let uq = TypeRow::from(vec![usize_t(), qb_t()]); let mut tail_b = outer .block_builder(uq.clone(), vec![uq.clone()], type_row![]) @@ -938,11 +941,18 @@ mod test { .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) .unwrap() .outputs_arr(); - tail_b.finish_with_outputs(br, []).unwrap() + (tail_b.finish_with_outputs(br, []).unwrap(), br.node()) }; outer.branch(&loop_b, 1, &tail_b).unwrap(); outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); let mut h = outer.finish_hugr().unwrap(); + assert_eq!( + h.get_parent(h.get_parent(inner_pred).unwrap()), + Some(inner.node()) + ); + assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); + assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); + let res = NormalizeCFGPass::default().run(&mut h).unwrap(); h.validate().unwrap(); assert_eq!( @@ -959,14 +969,34 @@ mod test { ]) ); // Now contains only one CFG with one BB (self-loop) - let cfg = h - .nodes() - .filter(|n| h.get_optype(*n).is_cfg()) - .exactly_one() - .ok() - .unwrap(); - let [entry, exit] = h.children(cfg).collect_array().unwrap(); + assert_eq!( + h.nodes() + .filter(|n| h.get_optype(*n).is_cfg()) + .exactly_one() + .ok(), + Some(h.entrypoint()) + ); + let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); + // Predicates lifted appropriately + assert_eq!(h.get_parent(inner_pred), Some(inner.node())); + assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); + assert_eq!( + h.get_parent(entry_pred.node()), + h.get_parent(h.entrypoint()) + ); + let tail_dfg = h.get_parent(tail_pred.node()).unwrap(); + assert_eq!(h.get_optype(tail_dfg).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(tail_dfg), h.get_parent(h.entrypoint())); + for n in [inner_pred, entry_pred.node(), tail_pred.node()] { + let [unpack] = h.output_neighbours(n).collect_array().unwrap(); + assert!( + h.get_optype(unpack) + .as_extension_op() + .and_then(|e| UnpackTuple::from_extension_op(e).ok()) + .is_some() + ); + } } fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { From 28032a9da332b1e97729e417ad80660e15b13543 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 21:30:11 +0100 Subject: [PATCH 30/49] comment CFGToDFG --- hugr-passes/src/merge_bbs.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 294a078c7c..b238d2af1b 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -72,7 +72,10 @@ pub enum NormalizeCFGError { /// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. #[derive(Clone, Debug, PartialEq, Eq)] pub enum NormalizeCFGResult { - /// The entire [CFG] was converted into a [DFG] + /// The entire [`CFG`] was converted into a [`DFG`]. + /// + /// The entrypoint node id is preserved after the conversion, but now identifies + /// the new [`DFG`]. CFGToDFG, /// The CFG was preserved, but the entry or exit blocks may have changed. #[allow(missing_docs)] From 7ebc9f8364e8a456cbb69971074c6395f5da03ab Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 21:32:46 +0100 Subject: [PATCH 31/49] comments --- hugr-passes/src/merge_bbs.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index b238d2af1b..de61e99a0c 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -174,7 +174,7 @@ pub fn normalize_cfg(mut h: &mut H) -> Result = h.children(cfg_node).skip(1).collect(); for blk in children_to_remove { @@ -244,7 +244,8 @@ pub fn normalize_cfg(mut h: &mut H) -> Result Date: Mon, 29 Sep 2025 21:51:40 +0100 Subject: [PATCH 32/49] Avoid error if we process CFGs in wrong order --- hugr-passes/src/merge_bbs.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index de61e99a0c..de52cf44f8 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -115,9 +115,14 @@ impl ComposablePass for NormalizeCFGPass { fn run(&self, hugr: &mut H) -> Result { let cfgs = if self.cfgs.is_empty() { - hugr.entry_descendants() + let mut v = hugr + .entry_descendants() .filter(|n| hugr.get_optype(*n).is_cfg()) - .collect() + .collect::>(); + // Process inner CFGs first, in case they are removed + // (if they are in an unreachable block when the Entry node has only the Exit as successor) + v.reverse(); + v } else { self.cfgs.clone() }; From 33aada64cee23d89040e94f4247011cf1abe221f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 22:54:08 +0100 Subject: [PATCH 33/49] remove old debug --- hugr-passes/src/merge_bbs.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index de52cf44f8..0f901ba86a 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -919,11 +919,6 @@ mod test { .unwrap() .outputs_arr(); let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); - eprintln!( - "ALAN inner cfg is {} of type {:?}", - inner.node(), - outer.hugr().get_optype(inner.node()) - ); let loop_b = { let mut loop_b = outer From 317b5968215cf3cc3768fdfbd244266c7660410c Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 22:54:56 +0100 Subject: [PATCH 34/49] Keep entry block stuff in a DFG --- hugr-passes/src/merge_bbs.rs | 86 +++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 0f901ba86a..286fd27c74 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -164,9 +164,10 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(mut h: &mut H) -> Result>() { - let srcs = h.linked_outputs(cfg_node, inp).collect::>(); - h.disconnect(cfg_node, inp); - for tgt in h - .linked_inputs(entry_input, inp.index()) - .collect::>() - { - // Connecting all sources to all targets handles Order edges as well as Value - for src in srcs.iter() { - h.connect(src.0, src.1, tgt.0, tgt.1); - } + for src in h.linked_outputs(cfg_node, inp).collect::>() { + h.connect(src.0, src.1, dfg, inp.index()); } + h.disconnect(cfg_node, inp); } - h.remove_node(entry_input); // Update input ports let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { @@ -226,18 +233,10 @@ pub fn normalize_cfg(mut h: &mut H) -> Result>() { + h.connect(dfg, src, cfg_node, src.index()); } - // old entry-node's successor is the new entry node, move into place - h.move_before_sibling(succ, entry); - h.remove_node(entry); entry_changed = true; } } @@ -808,14 +807,20 @@ mod test { func_children.into_iter().sorted().collect_vec(), [ "Cfg", - "Const", + "Dfg", "Input", - "LoadConst", - "Noop", "Output", - "UnpackTuple" ] ); + let [dfg] = h.children(func).filter(|n| h.get_optype(*n).is_dfg()).collect_array().unwrap(); + assert_eq!(child_tags_ext_ids(&h, dfg).into_iter().sorted().collect_vec(), [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ]); Ok(()) } @@ -982,16 +987,17 @@ mod test { ); let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); - // Predicates lifted appropriately + // Inner CFG is now a DFG (and still sibling of entry_pred)... assert_eq!(h.get_parent(inner_pred), Some(inner.node())); assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); - assert_eq!( - h.get_parent(entry_pred.node()), - h.get_parent(h.entrypoint()) - ); - let tail_dfg = h.get_parent(tail_pred.node()).unwrap(); - assert_eq!(h.get_optype(tail_dfg).tag(), OpTag::Dfg); - assert_eq!(h.get_parent(tail_dfg), h.get_parent(h.entrypoint())); + assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); + // Predicates lifted appropriately... + for n in [entry_pred.node(), tail_pred.node()] { + let parent = h.get_parent(n).unwrap(); + assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); + } + // ...and followed by UnpackTuple's for n in [inner_pred, entry_pred.node(), tail_pred.node()] { let [unpack] = h.output_neighbours(n).collect_array().unwrap(); assert!( From 6e0a89726b8a6e4947d47e20df07174884ca2b5d Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 29 Sep 2025 22:55:26 +0100 Subject: [PATCH 35/49] comments requested in review --- hugr-passes/src/merge_bbs.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 286fd27c74..2f087aa57e 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -413,6 +413,8 @@ fn mk_rep( } type NodePorts = Vec<(N, OutgoingPort)>; +/// Remove all input wires to `n` and return them in two groups: +/// the [EdgeKind::Value] inputs, and the [EdgeKind::StateOrder] inputs fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { let mut values = vec![]; let mut orders = vec![]; @@ -443,6 +445,8 @@ fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { .unwrap() } +/// Unpack the first `value_srcs`; wire the unpacked elements and remaining `value_srcs` into +/// consecutive ports of `dst`. Finally wire `order_srcs` all to the order input of `dst`. fn wire_unpack_first( h: &mut H, value_srcs: impl IntoIterator, From 3737f01aa4ad37ca28ac0b7af0405c088f6a1e8c Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 08:45:57 +0100 Subject: [PATCH 36/49] allow->expect, fmt --- hugr-passes/src/merge_bbs.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 2f087aa57e..78ae296c4b 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -149,7 +149,7 @@ impl ComposablePass for NormalizeCFGPass { /// # Errors /// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG -#[allow(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter +#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter pub fn normalize_cfg(mut h: &mut H) -> Result { let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) .map_err(|e| match e { @@ -491,7 +491,7 @@ fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: } #[cfg(test)] -#[allow(deprecated)] // remove tests of merge_bbs, or just hide the latter +#[expect(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -501,7 +501,7 @@ mod test { use rstest::rstest; use hugr_core::builder::{ - CFGBuilder, Container, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, + CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, }; use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; @@ -809,22 +809,27 @@ mod test { let func_children = child_tags_ext_ids(&h, func); assert_eq!( func_children.into_iter().sorted().collect_vec(), + ["Cfg", "Dfg", "Input", "Output",] + ); + let [dfg] = h + .children(func) + .filter(|n| h.get_optype(*n).is_dfg()) + .collect_array() + .unwrap(); + assert_eq!( + child_tags_ext_ids(&h, dfg) + .into_iter() + .sorted() + .collect_vec(), [ - "Cfg", - "Dfg", + "Const", "Input", + "LoadConst", + "Noop", "Output", + "UnpackTuple" ] ); - let [dfg] = h.children(func).filter(|n| h.get_optype(*n).is_dfg()).collect_array().unwrap(); - assert_eq!(child_tags_ext_ids(&h, dfg).into_iter().sorted().collect_vec(), [ - "Const", - "Input", - "LoadConst", - "Noop", - "Output", - "UnpackTuple" - ]); Ok(()) } From 3050976227088f10a4990f4cafad9a06fb921d38 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 08:59:20 +0100 Subject: [PATCH 37/49] Split apart normalize_cfg.rs, taking all tests with it --- hugr-passes/src/lib.rs | 2 + hugr-passes/src/merge_bbs.rs | 880 +------------------------------ hugr-passes/src/normalize_cfg.rs | 879 ++++++++++++++++++++++++++++++ 3 files changed, 890 insertions(+), 871 deletions(-) create mode 100644 hugr-passes/src/normalize_cfg.rs diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 6e97f3422e..0a3dae4691 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -17,8 +17,10 @@ pub use inline_funcs::inline_acyclic; pub mod linearize_array; pub use linearize_array::LinearizeArrayPass; pub mod lower; +#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] pub mod merge_bbs; mod monomorphize; +pub mod normalize_cfg; pub mod untuple; pub use monomorphize::{MonomorphizePass, mangle_name, monomorphize}; diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 78ae296c4b..60395cbe3f 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -2,21 +2,17 @@ //! and the target BB has no other predecessors. use std::collections::HashMap; -use hugr_core::extension::prelude::UnpackTuple; -use hugr_core::hugr::views::{RootCheckable, RootChecked}; -use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; -use hugr_core::types::{EdgeKind, Signature, TypeRow}; +use hugr_core::hugr::hugrmut::HugrMut; +use hugr_core::hugr::views::RootCheckable; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{ - CFG, DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output, -}; -use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; +use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, Output}; +use hugr_core::{Hugr, HugrView, Node}; -use crate::ComposablePass; +use crate::normalize_cfg::wire_unpack_first; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor @@ -27,7 +23,7 @@ use crate::ComposablePass; /// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG -#[deprecated(note = "Use normalize_cfg")] // Note: as a first step, just hide this +#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] // Note: as a first step, just hide this pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) where H: 'h + HugrMut, @@ -50,6 +46,7 @@ where // - the exit block, nodes in n should move *outside* the CFG - a separate pass. continue; } + #[expect(deprecated)] // undeprecate when hidden let (rep, merge_bb, dfgs) = mk_rep(cfg, n, succ); let node_map = cfg.apply_patch(rep).unwrap(); let merged_bb = *node_map.get(&merge_bb).unwrap(); @@ -61,250 +58,6 @@ where } } -/// Errors from [normalize_cfg] -#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] -pub enum NormalizeCFGError { - /// The requested node was not a CFG - #[error("Requested node was not a CFG but {_0}")] - NotCFG(OpTag), -} - -/// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NormalizeCFGResult { - /// The entire [`CFG`] was converted into a [`DFG`]. - /// - /// The entrypoint node id is preserved after the conversion, but now identifies - /// the new [`DFG`]. - CFGToDFG, - /// The CFG was preserved, but the entry or exit blocks may have changed. - #[allow(missing_docs)] - CFGPreserved { - entry_changed: bool, - exit_changed: bool, - }, -} - -/// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. -#[derive(Clone, Debug)] -pub struct NormalizeCFGPass { - cfgs: Vec, -} - -impl Default for NormalizeCFGPass { - fn default() -> Self { - Self { cfgs: vec![] } - } -} - -impl NormalizeCFGPass { - /// Allows mutating the set of CFG nodes that will be normalized. - /// - /// If empty (the default), all (non-strict) descendants of the [HugrView::entrypoint] - /// will be normalized. - pub fn cfgs(&mut self) -> &mut Vec { - &mut self.cfgs - } -} - -impl ComposablePass for NormalizeCFGPass { - type Error = NormalizeCFGError; - - /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG - type Result = HashMap; - - fn run(&self, hugr: &mut H) -> Result { - let cfgs = if self.cfgs.is_empty() { - let mut v = hugr - .entry_descendants() - .filter(|n| hugr.get_optype(*n).is_cfg()) - .collect::>(); - // Process inner CFGs first, in case they are removed - // (if they are in an unreachable block when the Entry node has only the Exit as successor) - v.reverse(); - v - } else { - self.cfgs.clone() - }; - let mut results = HashMap::new(); - for cfg in cfgs { - let res = normalize_cfg(&mut hugr.with_entrypoint_mut(cfg))?; - results.insert(cfg, res); - } - Ok(results) - } -} - -/// Normalize a CFG in a Hugr: -/// * Merge consecutive basic blocks i.e. where a BB has only a single successor which -/// has no predecessors -/// * If the entry block has only one successor, and no predecessors, then move its contents -/// outside/before CFG. -/// * (Similarly) if the exit block has only one predecessor, then move contents -/// outside/after CFG. -/// * If that predecessor is the entry block, then convert the CFG to a DFG. -/// This will be reported via [NormalizeCFGResult::CFGToDFG] -/// -/// -/// # Errors -/// -/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG -#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter -pub fn normalize_cfg(mut h: &mut H) -> Result { - let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) - .map_err(|e| match e { - HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), - _ => unreachable!(), - })?; - merge_basic_blocks(checked); - let cfg_node = h.entrypoint(); - - // Further normalizations with effects outside the CFG - let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); - let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); - let cfg_parent = h.get_parent(cfg_node).unwrap(); - // 1. If the entry block has only one successor, and no predecessors, then move contents - // outside/before CFG. - // (Note if the entry block has multiple successors, but no predecessors, we *could* move - // its contents outside/before the CFG, but would need to keep an empty/identity entry - // block - we do not do this here.) - let mut entry_changed = false; - #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` - if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { - if succ == exit { - // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore - assert_eq!( - &Signature::new( - entry_blk.inputs.clone(), - entry_blk.successor_input(0).unwrap() - ), - h.signature(cfg_node).unwrap().as_ref() - ); - // 1a. Turn the CFG into a DFG containing only what was in the entry block - // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive - let children_to_remove: Vec<_> = h.children(cfg_node).skip(1).collect(); - for blk in children_to_remove { - h.remove_subtree(blk); - } - while let Some(ch) = h.first_child(entry) { - h.set_parent(ch, cfg_node); - } - h.remove_node(entry); - let cfg_ty = h.optype_mut(cfg_node); - let OpType::CFG(CFG { signature }) = std::mem::take(cfg_ty) else { - panic!() - }; - let result_tys = signature.output.clone(); - *cfg_ty = OpType::DFG(DFG { signature }); - unpack_before_output(h, h.get_io(cfg_node).unwrap()[1], result_tys); - return Ok(NormalizeCFGResult::CFGToDFG); - } else if h.input_neighbours(entry).count() == 0 { - // 1b. Move entry block outside/before the CFG into a DFG; its successor becomes the entry block. - let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); - let dfg = h.add_node_with_parent( - cfg_parent, - DFG { - signature: Signature::new(entry_blk.inputs.clone(), new_cfg_inputs.clone()), - }, - ); - let [_, entry_output] = h.get_io(entry).unwrap(); - while let Some(n) = h.first_child(entry) { - h.set_parent(n, dfg); - } - h.move_before_sibling(succ, entry); - h.remove_node(entry); - - unpack_before_output(h, entry_output, new_cfg_inputs.clone()); - - // Inputs to CFG go directly to DFG - for inp in h.node_inputs(cfg_node).collect::>() { - for src in h.linked_outputs(cfg_node, inp).collect::>() { - h.connect(src.0, src.1, dfg, inp.index()); - } - h.disconnect(cfg_node, inp); - } - - // Update input ports - let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { - panic!() - }; - let inputs_to_add = - new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; - cfg_ty.signature.input = new_cfg_inputs; - h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); - - // Wire outputs of DFG directly to CFG - for src in h.node_outputs(dfg).collect::>() { - h.connect(dfg, src, cfg_node, src.index()); - } - entry_changed = true; - } - } - // 2. If the exit node has a single predecessor and that predecessor has no other successors... - let mut exit_changed = false; - if let Some(pred) = h - .input_neighbours(exit) - .exactly_one() - .ok() - .filter(|pred| h.output_neighbours(*pred).count() == 1) - { - // Code in that predecessor can be moved outside (into a new DFG after the CFG), - // and the predecessor deleted - let [_, output] = h.get_io(pred).unwrap(); - let pred_blk = h.get_optype(pred).as_dataflow_block().unwrap(); - let new_cfg_outs = pred_blk.inner_signature().into_owned().input; - - // new CFG result type and exit block - let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { - panic!() - }; - let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); - h.add_ports( - cfg_node, - Direction::Outgoing, - new_cfg_outs.len() as isize - result_tys.len() as isize, - ); - - *h.optype_mut(pred) = ExitBlock { - cfg_outputs: new_cfg_outs.clone(), - } - .into(); - debug_assert_eq!(h.num_ports(pred, Direction::Outgoing), 1); - h.set_num_ports(pred, 1, 0); - - h.move_before_sibling(pred, exit); - h.remove_node(exit); - // Move contents into new DFG - let dfg = h.add_node_with_parent( - cfg_parent, - DFG { - signature: Signature::new(new_cfg_outs, result_tys.clone()), - }, - ); - while let Some(n) = h.first_child(pred) { - h.set_parent(n, dfg); - } - unpack_before_output(h, output, result_tys); - - // Move output edges. - for p in h.node_outputs(cfg_node).collect_vec() { - let tgts = h.linked_inputs(cfg_node, p).collect_vec(); - h.disconnect(cfg_node, p); - for tgt in tgts { - h.connect(dfg, p, tgt.0, tgt.1) - } - } - for p in h.node_inputs(dfg).collect_vec() { - h.connect(cfg_node, p.index(), dfg, p); - } - exit_changed = true; - } - Ok(NormalizeCFGResult::CFGPreserved { - entry_changed, - exit_changed, - }) -} - fn mk_rep( cfg: &H, pred: H::Node, @@ -365,8 +118,9 @@ fn mk_rep( .collect::>(); let dfg_order_out = replacement.get_optype(dfg1).other_output_port().unwrap(); - let order_srcs = (dfg1_outs.is_empty()).then_some((dfg1, dfg_order_out)); // Do not add Order edges between DFGs unless there are no value edges + let order_srcs = (dfg1_outs.is_empty()).then_some((dfg1, dfg_order_out)); + wire_unpack_first(&mut replacement, dfg1_outs, order_srcs, dfg2); // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new @@ -411,619 +165,3 @@ fn mk_rep( }; (rep, merged, [dfg1, dfg2]) } - -type NodePorts = Vec<(N, OutgoingPort)>; -/// Remove all input wires to `n` and return them in two groups: -/// the [EdgeKind::Value] inputs, and the [EdgeKind::StateOrder] inputs -fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { - let mut values = vec![]; - let mut orders = vec![]; - for p in h.node_inputs(n).collect_vec() { - let srcs = h.linked_outputs(n, p).collect_vec(); - h.disconnect(n, p); - match h.get_optype(n).port_kind(p) { - Some(EdgeKind::Value(_)) => { - assert_eq!(srcs.len(), 1); - values.extend(srcs); - } - Some(EdgeKind::StateOrder) => { - assert_eq!(orders, []); - orders.extend(srcs); - } - k => panic!("Unexpected port kind: {:?}", k), - } - } - (values, orders) -} - -fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { - match h.get_optype(n).port_kind(p) { - Some(EdgeKind::Value(ty)) => ty.as_sum().unwrap().as_tuple().unwrap().clone(), - p => panic!("Expected Value port not {:?}", p), - } - .try_into() - .unwrap() -} - -/// Unpack the first `value_srcs`; wire the unpacked elements and remaining `value_srcs` into -/// consecutive ports of `dst`. Finally wire `order_srcs` all to the order input of `dst`. -fn wire_unpack_first( - h: &mut H, - value_srcs: impl IntoIterator, - order_srcs: impl IntoIterator, - dst: H::Node, -) { - let parent = h.get_parent(dst).unwrap(); - let mut srcs = value_srcs.into_iter(); - let src_to_unpack = srcs.next().unwrap(); - let tuple_tys = tuple_elems(h, src_to_unpack.0, src_to_unpack.1); - let tuple_len = tuple_tys.len(); - let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); - h.connect(src_to_unpack.0, src_to_unpack.1, unp, 0); - - for i in 0..tuple_len { - h.connect(unp, i, dst, i); - } - assert_eq!( - h.get_optype(dst).other_port_kind(Direction::Incoming), - Some(EdgeKind::StateOrder) - ); - let order_tgt = h.get_optype(dst).other_input_port().unwrap(); - for (i, (src, src_p)) in srcs.enumerate() { - assert!(i + tuple_len < order_tgt.index()); - h.connect(src, src_p, dst, i + tuple_len); - } - for (src, src_p) in order_srcs { - h.connect(src, src_p, dst, order_tgt); - } -} - -/// Unpack the first input to specified [Output] node and shuffle all the rest along -fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: TypeRow) { - let (values, orders) = take_inputs(h, output_node); - let OpType::Output(ou) = h.optype_mut(output_node) else { - panic!() - }; - let ports_to_add = new_types.len() as isize - ou.types.len() as isize; - ou.types = new_types; - h.add_ports(output_node, Direction::Incoming, ports_to_add); - wire_unpack_first(h, values, orders, output_node); -} - -#[cfg(test)] -#[expect(deprecated)] // remove tests of merge_bbs, or just hide the latter -mod test { - use std::collections::{HashMap, HashSet}; - use std::sync::Arc; - - use hugr_core::extension::simple_op::MakeExtensionOp; - use itertools::Itertools; - use rstest::rstest; - - use hugr_core::builder::{ - CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, - }; - use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; - use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; - use hugr_core::ops::{constant::Value, handle::NodeHandle}; - use hugr_core::types::{Signature, Type, TypeRow}; - use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; - - use super::{NormalizeCFGPass, NormalizeCFGResult, merge_basic_blocks, normalize_cfg}; - use crate::ComposablePass; - - const_extension_ids! { - const EXT_ID: ExtensionId = "TestExt"; - } - - fn extension() -> Arc { - Extension::new_arc( - EXT_ID, - hugr_core::extension::Version::new(0, 1, 0), - |ext, extension_ref| { - ext.add_op( - "Test".into(), - String::new(), - Signature::new( - vec![qb_t(), usize_t()], - TypeRow::from(vec![Type::new_sum(vec![vec![qb_t()], vec![usize_t()]])]), - ), - extension_ref, - ) - .unwrap(); - }, - ) - } - - #[rstest] - #[case(true)] - #[case(false)] - fn merge_bbs_in_loop(#[case] self_loop: bool) -> Result<(), Box> { - /* self_loop==False: - -> Noop1 -----> Test -> Exit -> Noop1AndTest --> Exit - | | => / \ - \-<- Noop2 <-/ \-<- Noop2 <-/ - (Noop2 -> Noop1 cannot be merged because Noop1 is the entry node) - - self_loop==True: - -> Noop --> Test -> Exit -> NoopAndTest --> Exit - | | => / \ - \--<--<--/ \--<-----<--/ - */ - - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; - let mut no_b1 = h.simple_entry_builder(loop_variants.clone(), 1)?; - let n = no_b1.add_dataflow_op(Noop::new(qb_t()), no_b1.input_wires())?; - let br = no_b1.add_load_value(Value::unary_unit_sum()); - let no_b1 = no_b1.finish_with_outputs(br, n.outputs())?; - let mut test_block = h.block_builder( - loop_variants.clone(), - vec![loop_variants.clone(), exit_types], - type_row![], - )?; - let [test_input] = test_block.input_wires_arr(); - let usize_cst = test_block.add_load_value(ConstUsize::new(1)); - let [tst] = test_block - .add_dataflow_op(tst_op, [test_input, usize_cst])? - .outputs_arr(); - let test_block = test_block.finish_with_outputs(tst, [])?; - let loop_backedge_target = if self_loop { - no_b1 - } else { - let mut no_b2 = h.simple_block_builder(endo_sig(loop_variants), 1)?; - let n = no_b2.add_dataflow_op(Noop::new(qb_t()), no_b2.input_wires())?; - let br = no_b2.add_load_value(Value::unary_unit_sum()); - let nid = no_b2.finish_with_outputs(br, n.outputs())?; - h.branch(&nid, 0, &no_b1)?; - nid - }; - h.branch(&no_b1, 0, &test_block)?; - h.branch(&test_block, 0, &loop_backedge_target)?; - h.branch(&test_block, 1, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let r = h.entrypoint(); - merge_basic_blocks(&mut h); - h.validate().unwrap(); - assert_eq!(r, h.entrypoint()); - assert!(matches!(h.get_optype(r), OpType::CFG(_))); - let [entry, exit] = h - .children(r) - .take(2) - .collect::>() - .try_into() - .unwrap(); - // Check the Noop('s) is/are in the right block(s) - let nops = h - .entry_descendants() - .filter(|n| h.get_optype(*n).cast::().is_some()); - let (entry_nop, expected_backedge_target) = if self_loop { - assert_eq!(h.children(r).count(), 2); - (nops.exactly_one().ok().unwrap(), entry) - } else { - let [_, _, no_b2] = h.children(r).collect::>().try_into().unwrap(); - let mut nops = nops.collect::>(); - let entry_nop_idx = nops - .iter() - .position(|n| h.get_parent(*n) == Some(entry)) - .unwrap(); - let entry_nop = nops[entry_nop_idx]; - nops.remove(entry_nop_idx); - let [n_op2] = nops.try_into().unwrap(); - assert_eq!(h.get_parent(n_op2), Some(no_b2)); - (entry_nop, no_b2) - }; - assert_eq!(h.get_parent(entry_nop), Some(entry)); - assert_eq!( - h.output_neighbours(entry).collect::>(), - HashSet::from([expected_backedge_target, exit]) - ); - // And the Noop in the entry block is consumed by the custom Test op - let tst = find_unique( - h.entry_descendants(), - |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), - ); - assert_eq!(h.get_parent(tst), Some(entry)); - assert_eq!( - h.output_neighbours(entry_nop).collect::>(), - vec![tst] - ); - Ok(()) - } - - #[test] - fn elide_triple_with_permute() -> Result<(), Box> { - // Blocks are just BB1 -> BB2 -> BB3 --> Exit. - // Should be merged into one BB (we don't check that specifically) - // and then the whole CFG elided. - let e = extension(); - let tst_op: OpType = e.instantiate_extension_op("Test", &[])?.into(); - let [res_t] = tst_op - .dataflow_signature() - .unwrap() - .into_owned() - .output - .into_owned() - .try_into() - .unwrap(); - let mut h = CFGBuilder::new(inout_sig(qb_t(), res_t.clone()))?; - let mut bb1 = h.simple_entry_builder(vec![usize_t(), qb_t()].into(), 1)?; - let [inw] = bb1.input_wires_arr(); - let load_cst = bb1.add_load_value(ConstUsize::new(1)); - let pred = bb1.add_load_value(Value::unary_unit_sum()); - let bb1 = bb1.finish_with_outputs(pred, [load_cst, inw])?; - - let mut bb2 = h.block_builder( - vec![usize_t(), qb_t()].into(), - vec![type_row![]], - vec![qb_t(), usize_t()].into(), - )?; - let [u, q] = bb2.input_wires_arr(); - let pred = bb2.add_load_value(Value::unary_unit_sum()); - let bb2 = bb2.finish_with_outputs(pred, [q, u])?; - - let mut bb3 = h.block_builder( - vec![qb_t(), usize_t()].into(), - vec![type_row![]], - res_t.clone().into(), - )?; - let [q, u] = bb3.input_wires_arr(); - let tst = bb3.add_dataflow_op(tst_op, [q, u])?; - let pred = bb3.add_load_value(Value::unary_unit_sum()); - let bb3 = bb3.finish_with_outputs(pred, tst.outputs())?; - // Now add control-flow edges between basic blocks - h.branch(&bb1, 0, &bb2)?; - h.branch(&bb2, 0, &bb3)?; - h.branch(&bb3, 0, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let res = normalize_cfg(&mut h); - assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - h.validate()?; - assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!( - h.entry_descendants().find(|n| matches!( - h.get_optype(*n), - OpType::DataflowBlock(_) | OpType::CFG(_) | OpType::ExitBlock(_) - )), - None - ); - let tst = find_unique( - h.entry_descendants(), - |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), - ); - assert_eq!(h.get_parent(tst), Some(h.entrypoint())); - - let inp = find_unique(h.entry_descendants(), |n| { - matches!(h.get_optype(*n), OpType::Input(_)) - }); - let mut tst_inputs = h.input_neighbours(tst).collect::>(); - tst_inputs.remove(tst_inputs.iter().find_position(|n| **n == inp).unwrap().0); - let [other_input] = tst_inputs.try_into().unwrap(); - assert_eq!( - h.get_optype(other_input), - &(LoadConstant { - datatype: usize_t() - } - .into()) - ); - Ok(()) - } - - fn find_unique(items: impl Iterator, pred: impl Fn(&T) -> bool) -> T { - items.filter(pred).exactly_one().ok().unwrap() - } - - #[rstest] - fn elide_cfg() { - let ext = extension(); - let op = ext.instantiate_extension_op("Test", []).unwrap(); - let out_ty = op.signature().output().clone(); - let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); - let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); - let op_res = entry - .add_dataflow_op(op.clone(), entry.input_wires()) - .unwrap(); - let predicate = entry.add_load_value(Value::unary_unit_sum()); - let entry = entry - .finish_with_outputs(predicate, op_res.outputs()) - .unwrap(); - cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); - - // Add an (unreachable) block before the entry - let Signature { input, output } = op.signature().as_ref().clone(); - for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { - let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); - let inp = extra.input_wires(); - let branch = extra.add_load_value(Value::unary_unit_sum()); - let extra = extra.finish_with_outputs(branch, inp).unwrap(); - cfg.branch(&extra, 0, &dest).unwrap(); - } - - let mut h = cfg.finish_hugr().unwrap(); - - let func = h.children(h.module_root()).exactly_one().ok().unwrap(); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); - let mut dfb_children = child_tags_ext_ids(&h, entry.node()); - - let res = normalize_cfg(&mut h); - h.validate().unwrap(); - assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); - dfb_children.push("UnpackTuple".to_string()); - assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); - } - - #[test] - fn entry_before_loop() -> Result<(), Box> { - /* -> Noop --> Test -> Exit -> Test --> Exit - | | => | | - \<-/ \<-/ - */ - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(qb_t(), usize_t()))?; - let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; - let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; - let br = nop_b.add_load_value(Value::unary_unit_sum()); - let entry = nop_b.finish_with_outputs(br, n.outputs())?; - - let mut loop_b = h.block_builder( - loop_variants.clone(), - [loop_variants, exit_types], - type_row![], - )?; - let [qb] = loop_b.input_wires_arr(); - let usz = loop_b.add_load_value(ConstUsize::new(3)); - let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); - let loop_ = loop_b.finish_with_outputs(tst, [])?; - h.branch(&entry, 0, &loop_)?; - h.branch(&loop_, 0, &loop_)?; - h.branch(&loop_, 1, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - - let res = normalize_cfg(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: false - } - ); - assert_eq!( - h.children(h.entrypoint()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::DataflowBlock, OpTag::BasicBlockExit] - ); - let func = h.get_parent(h.entrypoint()).unwrap(); - let func_children = child_tags_ext_ids(&h, func); - assert_eq!( - func_children.into_iter().sorted().collect_vec(), - ["Cfg", "Dfg", "Input", "Output",] - ); - let [dfg] = h - .children(func) - .filter(|n| h.get_optype(*n).is_dfg()) - .collect_array() - .unwrap(); - assert_eq!( - child_tags_ext_ids(&h, dfg) - .into_iter() - .sorted() - .collect_vec(), - [ - "Const", - "Input", - "LoadConst", - "Noop", - "Output", - "UnpackTuple" - ] - ); - Ok(()) - } - - #[test] - fn loop_before_exit() -> Result<(), Box> { - /* -> Test -> Noop -> Exit -> Test --> Exit (then Noop) - | | => | | - \<-/ \<-/ - */ - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; - - let mut loop_b = h.entry_builder(vec![loop_variants, exit_types.clone()], type_row![])?; - let [qb] = loop_b.input_wires_arr(); - let usz = loop_b.add_load_value(ConstUsize::new(3)); - let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); - let loop_ = loop_b.finish_with_outputs(tst, [])?; - h.branch(&loop_, 0, &loop_)?; - - let mut nop_b = h.simple_block_builder(endo_sig(exit_types), 1)?; - let n = nop_b.add_dataflow_op(Noop::new(usize_t()), nop_b.input_wires())?; - let br = nop_b.add_load_value(Value::unary_unit_sum()); - let tail = nop_b.finish_with_outputs(br, n.outputs())?; - - h.branch(&loop_, 1, &tail)?; - h.branch(&tail, 0, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let res = normalize_cfg(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: false, - exit_changed: true - } - ); - assert_eq!( - h.children(h.entrypoint()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::DataflowBlock, OpTag::BasicBlockExit] - ); - let func = h.get_parent(h.entrypoint()).unwrap(); - assert_eq!( - h.children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] - ); - - let dfg = h.children(func).last().unwrap(); - let dfg_children = child_tags_ext_ids(&h, dfg); - assert_eq!( - dfg_children.into_iter().sorted().collect_vec(), - [ - "Const", - "Input", - "LoadConst", - "Noop", - "Output", - "UnpackTuple" - ] - ); - - Ok(()) - } - - #[test] - fn nested_cfgs_pass() { - // --> Entry --> Loop --> Tail --> EXIT - // | / \ - // (E->X) \<-/ - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", []).unwrap(); - let qqu = vec![qb_t(), qb_t(), usize_t()]; - let qq = TypeRow::from(vec![qb_t(); 2]); - let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); - let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); - let [q1, q2, u] = entry.input_wires_arr(); - let (inner, inner_pred) = { - let mut inner = entry - .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) - .unwrap(); - let mut entry = inner.entry_builder(vec![qq.clone()], type_row![]).unwrap(); - let [q1, q2] = entry.input_wires_arr(); - let [pred] = entry - .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) - .unwrap() - .outputs_arr(); - let entry = entry.finish_with_outputs(pred, []).unwrap(); - inner.branch(&entry, 0, &inner.exit_block()).unwrap(); - (inner.finish_sub_container().unwrap(), pred.node()) - }; - let [q1, q2] = inner.outputs_arr(); - let [entry_pred] = entry - .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) - .unwrap() - .outputs_arr(); - let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); - - let loop_b = { - let mut loop_b = outer - .block_builder(qq.clone(), [qb_t().into(), usize_t().into()], qb_t().into()) - .unwrap(); - let [q1, q2] = loop_b.input_wires_arr(); - // u here is `dom` edge from entry block - let [pred] = loop_b - .add_dataflow_op(tst_op, [q1, u]) - .unwrap() - .outputs_arr(); - loop_b.finish_with_outputs(pred, [q2]).unwrap() - }; - outer.branch(&entry, 0, &loop_b).unwrap(); - outer.branch(&loop_b, 0, &loop_b).unwrap(); - - let (tail_b, tail_pred) = { - let uq = TypeRow::from(vec![usize_t(), qb_t()]); - let mut tail_b = outer - .block_builder(uq.clone(), vec![uq.clone()], type_row![]) - .unwrap(); - let [u, q] = tail_b.input_wires_arr(); - let [br] = tail_b - .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) - .unwrap() - .outputs_arr(); - (tail_b.finish_with_outputs(br, []).unwrap(), br.node()) - }; - outer.branch(&loop_b, 1, &tail_b).unwrap(); - outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); - let mut h = outer.finish_hugr().unwrap(); - assert_eq!( - h.get_parent(h.get_parent(inner_pred).unwrap()), - Some(inner.node()) - ); - assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); - assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); - - let res = NormalizeCFGPass::default().run(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - HashMap::from([ - (inner.node(), NormalizeCFGResult::CFGToDFG), - ( - h.entrypoint(), - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: true - } - ) - ]) - ); - // Now contains only one CFG with one BB (self-loop) - assert_eq!( - h.nodes() - .filter(|n| h.get_optype(*n).is_cfg()) - .exactly_one() - .ok(), - Some(h.entrypoint()) - ); - let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); - assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); - // Inner CFG is now a DFG (and still sibling of entry_pred)... - assert_eq!(h.get_parent(inner_pred), Some(inner.node())); - assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); - assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); - // Predicates lifted appropriately... - for n in [entry_pred.node(), tail_pred.node()] { - let parent = h.get_parent(n).unwrap(); - assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); - assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); - } - // ...and followed by UnpackTuple's - for n in [inner_pred, entry_pred.node(), tail_pred.node()] { - let [unpack] = h.output_neighbours(n).collect_array().unwrap(); - assert!( - h.get_optype(unpack) - .as_extension_op() - .and_then(|e| UnpackTuple::from_extension_op(e).ok()) - .is_some() - ); - } - } - - fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { - h.children(n) - .map(|n| match h.get_optype(n) { - OpType::ExtensionOp(e) => e.unqualified_id().to_string(), - op => format!("{:?}", op.tag()), - }) - .collect() - } -} diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs new file mode 100644 index 0000000000..df88b9d90d --- /dev/null +++ b/hugr-passes/src/normalize_cfg.rs @@ -0,0 +1,879 @@ +//! Merge BBs along control-flow edges where the source BB has no other successors +//! and the target BB has no other predecessors. +use std::collections::HashMap; + +use hugr_core::extension::prelude::UnpackTuple; +use hugr_core::hugr::views::RootChecked; +use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; +use hugr_core::types::{EdgeKind, Signature, TypeRow}; +use itertools::Itertools; + +use hugr_core::ops::handle::CfgID; +use hugr_core::ops::{CFG, DFG, DataflowParent, ExitBlock, OpTag, OpType}; +use hugr_core::{Direction, HugrView, OutgoingPort, PortIndex}; + +use crate::ComposablePass; +#[expect(deprecated)] // refactor/inline merge_basic_blocks, or just hide it +use crate::merge_bbs::merge_basic_blocks; + +/// Errors from [normalize_cfg] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +pub enum NormalizeCFGError { + /// The requested node was not a CFG + #[error("Requested node was not a CFG but {_0}")] + NotCFG(OpTag), +} + +/// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NormalizeCFGResult { + /// The entire [`CFG`] was converted into a [`DFG`]. + /// + /// The entrypoint node id is preserved after the conversion, but now identifies + /// the new [`DFG`]. + CFGToDFG, + /// The CFG was preserved, but the entry or exit blocks may have changed. + #[allow(missing_docs)] + CFGPreserved { + entry_changed: bool, + exit_changed: bool, + }, +} + +/// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. +#[derive(Clone, Debug)] +pub struct NormalizeCFGPass { + cfgs: Vec, +} + +impl Default for NormalizeCFGPass { + fn default() -> Self { + Self { cfgs: vec![] } + } +} + +impl NormalizeCFGPass { + /// Allows mutating the set of CFG nodes that will be normalized. + /// + /// If empty (the default), all (non-strict) descendants of the [HugrView::entrypoint] + /// will be normalized. + pub fn cfgs(&mut self) -> &mut Vec { + &mut self.cfgs + } +} + +impl ComposablePass for NormalizeCFGPass { + type Error = NormalizeCFGError; + + /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG + type Result = HashMap; + + fn run(&self, hugr: &mut H) -> Result { + let cfgs = if self.cfgs.is_empty() { + let mut v = hugr + .entry_descendants() + .filter(|n| hugr.get_optype(*n).is_cfg()) + .collect::>(); + // Process inner CFGs first, in case they are removed + // (if they are in an unreachable block when the Entry node has only the Exit as successor) + v.reverse(); + v + } else { + self.cfgs.clone() + }; + let mut results = HashMap::new(); + for cfg in cfgs { + let res = normalize_cfg(&mut hugr.with_entrypoint_mut(cfg))?; + results.insert(cfg, res); + } + Ok(results) + } +} + +/// Normalize a CFG in a Hugr: +/// * Merge consecutive basic blocks i.e. where a BB has only a single successor which +/// has no predecessors +/// * If the entry block has only one successor, and no predecessors, then move its contents +/// outside/before CFG. +/// * (Similarly) if the exit block has only one predecessor, then move contents +/// outside/after CFG. +/// * If that predecessor is the entry block, then convert the CFG to a DFG. +/// This will be reported via [NormalizeCFGResult::CFGToDFG] +/// +/// +/// # Errors +/// +/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG +#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter +pub fn normalize_cfg(mut h: &mut H) -> Result { + let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) + .map_err(|e| match e { + HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), + _ => unreachable!(), + })?; + merge_basic_blocks(checked); + let cfg_node = h.entrypoint(); + + // Further normalizations with effects outside the CFG + let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); + let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); + let cfg_parent = h.get_parent(cfg_node).unwrap(); + // 1. If the entry block has only one successor, and no predecessors, then move contents + // outside/before CFG. + // (Note if the entry block has multiple successors, but no predecessors, we *could* move + // its contents outside/before the CFG, but would need to keep an empty/identity entry + // block - we do not do this here.) + let mut entry_changed = false; + #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` + if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { + if succ == exit { + // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore + assert_eq!( + &Signature::new( + entry_blk.inputs.clone(), + entry_blk.successor_input(0).unwrap() + ), + h.signature(cfg_node).unwrap().as_ref() + ); + // 1a. Turn the CFG into a DFG containing only what was in the entry block + // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive + let children_to_remove: Vec<_> = h.children(cfg_node).skip(1).collect(); + for blk in children_to_remove { + h.remove_subtree(blk); + } + while let Some(ch) = h.first_child(entry) { + h.set_parent(ch, cfg_node); + } + h.remove_node(entry); + let cfg_ty = h.optype_mut(cfg_node); + let OpType::CFG(CFG { signature }) = std::mem::take(cfg_ty) else { + unreachable!() // Checked at entry + }; + let result_tys = signature.output.clone(); + *cfg_ty = OpType::DFG(DFG { signature }); + unpack_before_output(h, h.get_io(cfg_node).unwrap()[1], result_tys); + return Ok(NormalizeCFGResult::CFGToDFG); + } else if h.input_neighbours(entry).count() == 0 { + // 1b. Move entry block outside/before the CFG into a DFG; its successor becomes the entry block. + let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); + let dfg = h.add_node_with_parent( + cfg_parent, + DFG { + signature: Signature::new(entry_blk.inputs.clone(), new_cfg_inputs.clone()), + }, + ); + let [_, entry_output] = h.get_io(entry).unwrap(); + while let Some(n) = h.first_child(entry) { + h.set_parent(n, dfg); + } + h.move_before_sibling(succ, entry); + h.remove_node(entry); + + unpack_before_output(h, entry_output, new_cfg_inputs.clone()); + + // Inputs to CFG go directly to DFG + for inp in h.node_inputs(cfg_node).collect::>() { + for src in h.linked_outputs(cfg_node, inp).collect::>() { + h.connect(src.0, src.1, dfg, inp.index()); + } + h.disconnect(cfg_node, inp); + } + + // Update input ports + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { + panic!() + }; + let inputs_to_add = + new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; + cfg_ty.signature.input = new_cfg_inputs; + h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); + + // Wire outputs of DFG directly to CFG + for src in h.node_outputs(dfg).collect::>() { + h.connect(dfg, src, cfg_node, src.index()); + } + entry_changed = true; + } + } + // 2. If the exit node has a single predecessor and that predecessor has no other successors... + let mut exit_changed = false; + if let Some(pred) = h + .input_neighbours(exit) + .exactly_one() + .ok() + .filter(|pred| h.output_neighbours(*pred).count() == 1) + { + // Code in that predecessor can be moved outside (into a new DFG after the CFG), + // and the predecessor deleted + let [_, output] = h.get_io(pred).unwrap(); + let pred_blk = h.get_optype(pred).as_dataflow_block().unwrap(); + let new_cfg_outs = pred_blk.inner_signature().into_owned().input; + + // new CFG result type and exit block + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { + panic!() + }; + let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); + h.add_ports( + cfg_node, + Direction::Outgoing, + new_cfg_outs.len() as isize - result_tys.len() as isize, + ); + + *h.optype_mut(pred) = ExitBlock { + cfg_outputs: new_cfg_outs.clone(), + } + .into(); + debug_assert_eq!(h.num_ports(pred, Direction::Outgoing), 1); + h.set_num_ports(pred, 1, 0); + + h.move_before_sibling(pred, exit); + h.remove_node(exit); + // Move contents into new DFG + let dfg = h.add_node_with_parent( + cfg_parent, + DFG { + signature: Signature::new(new_cfg_outs, result_tys.clone()), + }, + ); + while let Some(n) = h.first_child(pred) { + h.set_parent(n, dfg); + } + unpack_before_output(h, output, result_tys); + + // Move output edges. + for p in h.node_outputs(cfg_node).collect_vec() { + let tgts = h.linked_inputs(cfg_node, p).collect_vec(); + h.disconnect(cfg_node, p); + for tgt in tgts { + h.connect(dfg, p, tgt.0, tgt.1) + } + } + for p in h.node_inputs(dfg).collect_vec() { + h.connect(cfg_node, p.index(), dfg, p); + } + exit_changed = true; + } + Ok(NormalizeCFGResult::CFGPreserved { + entry_changed, + exit_changed, + }) +} + +type NodePorts = Vec<(N, OutgoingPort)>; +/// Remove all input wires to `n` and return them in two groups: +/// the [EdgeKind::Value] inputs, and the [EdgeKind::StateOrder] inputs +fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { + let mut values = vec![]; + let mut orders = vec![]; + for p in h.node_inputs(n).collect_vec() { + let srcs = h.linked_outputs(n, p).collect_vec(); + h.disconnect(n, p); + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(_)) => { + assert_eq!(srcs.len(), 1); + values.extend(srcs); + } + Some(EdgeKind::StateOrder) => { + assert_eq!(orders, []); + orders.extend(srcs); + } + k => panic!("Unexpected port kind: {:?}", k), + } + } + (values, orders) +} + +fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(ty)) => ty.as_sum().unwrap().as_tuple().unwrap().clone(), + p => panic!("Expected Value port not {:?}", p), + } + .try_into() + .unwrap() +} + +/// Unpack the first `value_srcs`; wire the unpacked elements and remaining `value_srcs` into +/// consecutive ports of `dst`. Finally wire `order_srcs` all to the order input of `dst`. +pub(crate) fn wire_unpack_first( + h: &mut H, + value_srcs: impl IntoIterator, + order_srcs: impl IntoIterator, + dst: H::Node, +) { + let parent = h.get_parent(dst).unwrap(); + let mut srcs = value_srcs.into_iter(); + let src_to_unpack = srcs.next().unwrap(); + let tuple_tys = tuple_elems(h, src_to_unpack.0, src_to_unpack.1); + let tuple_len = tuple_tys.len(); + let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); + h.connect(src_to_unpack.0, src_to_unpack.1, unp, 0); + + for i in 0..tuple_len { + h.connect(unp, i, dst, i); + } + assert_eq!( + h.get_optype(dst).other_port_kind(Direction::Incoming), + Some(EdgeKind::StateOrder) + ); + let order_tgt = h.get_optype(dst).other_input_port().unwrap(); + for (i, (src, src_p)) in srcs.enumerate() { + assert!(i + tuple_len < order_tgt.index()); + h.connect(src, src_p, dst, i + tuple_len); + } + for (src, src_p) in order_srcs { + h.connect(src, src_p, dst, order_tgt); + } +} + +/// Unpack the first input to specified [Output] node and shuffle all the rest along +fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: TypeRow) { + let (values, orders) = take_inputs(h, output_node); + let OpType::Output(ou) = h.optype_mut(output_node) else { + panic!() + }; + let ports_to_add = new_types.len() as isize - ou.types.len() as isize; + ou.types = new_types; + h.add_ports(output_node, Direction::Incoming, ports_to_add); + wire_unpack_first(h, values, orders, output_node); +} + +#[cfg(test)] +mod test { + use std::collections::{HashMap, HashSet}; + use std::sync::Arc; + + use hugr_core::extension::simple_op::MakeExtensionOp; + use itertools::Itertools; + use rstest::rstest; + + #[expect(deprecated)] + // when merge_basic_blocks removed/hidden, consider testing normalize_cfgs directly + use super::{NormalizeCFGPass, NormalizeCFGResult, merge_basic_blocks, normalize_cfg}; + use crate::ComposablePass; + use hugr_core::builder::{ + CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, + }; + use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; + use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; + use hugr_core::ops::{constant::Value, handle::NodeHandle}; + use hugr_core::types::{Signature, Type, TypeRow}; + use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; + + const_extension_ids! { + const EXT_ID: ExtensionId = "TestExt"; + } + + fn extension() -> Arc { + Extension::new_arc( + EXT_ID, + hugr_core::extension::Version::new(0, 1, 0), + |ext, extension_ref| { + ext.add_op( + "Test".into(), + String::new(), + Signature::new( + vec![qb_t(), usize_t()], + TypeRow::from(vec![Type::new_sum(vec![vec![qb_t()], vec![usize_t()]])]), + ), + extension_ref, + ) + .unwrap(); + }, + ) + } + + #[rstest] + #[case(true)] + #[case(false)] + fn merge_bbs_in_loop(#[case] self_loop: bool) -> Result<(), Box> { + /* self_loop==False: + -> Noop1 -----> Test -> Exit -> Noop1AndTest --> Exit + | | => / \ + \-<- Noop2 <-/ \-<- Noop2 <-/ + (Noop2 -> Noop1 cannot be merged because Noop1 is the entry node) + + self_loop==True: + -> Noop --> Test -> Exit -> NoopAndTest --> Exit + | | => / \ + \--<--<--/ \--<-----<--/ + */ + + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + let mut no_b1 = h.simple_entry_builder(loop_variants.clone(), 1)?; + let n = no_b1.add_dataflow_op(Noop::new(qb_t()), no_b1.input_wires())?; + let br = no_b1.add_load_value(Value::unary_unit_sum()); + let no_b1 = no_b1.finish_with_outputs(br, n.outputs())?; + let mut test_block = h.block_builder( + loop_variants.clone(), + vec![loop_variants.clone(), exit_types], + type_row![], + )?; + let [test_input] = test_block.input_wires_arr(); + let usize_cst = test_block.add_load_value(ConstUsize::new(1)); + let [tst] = test_block + .add_dataflow_op(tst_op, [test_input, usize_cst])? + .outputs_arr(); + let test_block = test_block.finish_with_outputs(tst, [])?; + let loop_backedge_target = if self_loop { + no_b1 + } else { + let mut no_b2 = h.simple_block_builder(endo_sig(loop_variants), 1)?; + let n = no_b2.add_dataflow_op(Noop::new(qb_t()), no_b2.input_wires())?; + let br = no_b2.add_load_value(Value::unary_unit_sum()); + let nid = no_b2.finish_with_outputs(br, n.outputs())?; + h.branch(&nid, 0, &no_b1)?; + nid + }; + h.branch(&no_b1, 0, &test_block)?; + h.branch(&test_block, 0, &loop_backedge_target)?; + h.branch(&test_block, 1, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let r = h.entrypoint(); + #[expect(deprecated)] + // when merge_basic_blocks removed/hidden, consider testing normalize_cfgs directly + merge_basic_blocks(&mut h); + h.validate().unwrap(); + assert_eq!(r, h.entrypoint()); + assert!(matches!(h.get_optype(r), OpType::CFG(_))); + let [entry, exit] = h + .children(r) + .take(2) + .collect::>() + .try_into() + .unwrap(); + // Check the Noop('s) is/are in the right block(s) + let nops = h + .entry_descendants() + .filter(|n| h.get_optype(*n).cast::().is_some()); + let (entry_nop, expected_backedge_target) = if self_loop { + assert_eq!(h.children(r).count(), 2); + (nops.exactly_one().ok().unwrap(), entry) + } else { + let [_, _, no_b2] = h.children(r).collect::>().try_into().unwrap(); + let mut nops = nops.collect::>(); + let entry_nop_idx = nops + .iter() + .position(|n| h.get_parent(*n) == Some(entry)) + .unwrap(); + let entry_nop = nops[entry_nop_idx]; + nops.remove(entry_nop_idx); + let [n_op2] = nops.try_into().unwrap(); + assert_eq!(h.get_parent(n_op2), Some(no_b2)); + (entry_nop, no_b2) + }; + assert_eq!(h.get_parent(entry_nop), Some(entry)); + assert_eq!( + h.output_neighbours(entry).collect::>(), + HashSet::from([expected_backedge_target, exit]) + ); + // And the Noop in the entry block is consumed by the custom Test op + let tst = find_unique( + h.entry_descendants(), + |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), + ); + assert_eq!(h.get_parent(tst), Some(entry)); + assert_eq!( + h.output_neighbours(entry_nop).collect::>(), + vec![tst] + ); + Ok(()) + } + + #[test] + fn elide_triple_with_permute() -> Result<(), Box> { + // Blocks are just BB1 -> BB2 -> BB3 --> Exit. + // Should be merged into one BB (we don't check that specifically) + // and then the whole CFG elided. + let e = extension(); + let tst_op: OpType = e.instantiate_extension_op("Test", &[])?.into(); + let [res_t] = tst_op + .dataflow_signature() + .unwrap() + .into_owned() + .output + .into_owned() + .try_into() + .unwrap(); + let mut h = CFGBuilder::new(inout_sig(qb_t(), res_t.clone()))?; + let mut bb1 = h.simple_entry_builder(vec![usize_t(), qb_t()].into(), 1)?; + let [inw] = bb1.input_wires_arr(); + let load_cst = bb1.add_load_value(ConstUsize::new(1)); + let pred = bb1.add_load_value(Value::unary_unit_sum()); + let bb1 = bb1.finish_with_outputs(pred, [load_cst, inw])?; + + let mut bb2 = h.block_builder( + vec![usize_t(), qb_t()].into(), + vec![type_row![]], + vec![qb_t(), usize_t()].into(), + )?; + let [u, q] = bb2.input_wires_arr(); + let pred = bb2.add_load_value(Value::unary_unit_sum()); + let bb2 = bb2.finish_with_outputs(pred, [q, u])?; + + let mut bb3 = h.block_builder( + vec![qb_t(), usize_t()].into(), + vec![type_row![]], + res_t.clone().into(), + )?; + let [q, u] = bb3.input_wires_arr(); + let tst = bb3.add_dataflow_op(tst_op, [q, u])?; + let pred = bb3.add_load_value(Value::unary_unit_sum()); + let bb3 = bb3.finish_with_outputs(pred, tst.outputs())?; + // Now add control-flow edges between basic blocks + h.branch(&bb1, 0, &bb2)?; + h.branch(&bb2, 0, &bb3)?; + h.branch(&bb3, 0, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let res = normalize_cfg(&mut h); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); + h.validate()?; + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!( + h.entry_descendants().find(|n| matches!( + h.get_optype(*n), + OpType::DataflowBlock(_) | OpType::CFG(_) | OpType::ExitBlock(_) + )), + None + ); + let tst = find_unique( + h.entry_descendants(), + |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), + ); + assert_eq!(h.get_parent(tst), Some(h.entrypoint())); + + let inp = find_unique(h.entry_descendants(), |n| { + matches!(h.get_optype(*n), OpType::Input(_)) + }); + let mut tst_inputs = h.input_neighbours(tst).collect::>(); + tst_inputs.remove(tst_inputs.iter().find_position(|n| **n == inp).unwrap().0); + let [other_input] = tst_inputs.try_into().unwrap(); + assert_eq!( + h.get_optype(other_input), + &(LoadConstant { + datatype: usize_t() + } + .into()) + ); + Ok(()) + } + + fn find_unique(items: impl Iterator, pred: impl Fn(&T) -> bool) -> T { + items.filter(pred).exactly_one().ok().unwrap() + } + + #[rstest] + fn elide_cfg() { + let ext = extension(); + let op = ext.instantiate_extension_op("Test", []).unwrap(); + let out_ty = op.signature().output().clone(); + let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); + let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); + let op_res = entry + .add_dataflow_op(op.clone(), entry.input_wires()) + .unwrap(); + let predicate = entry.add_load_value(Value::unary_unit_sum()); + let entry = entry + .finish_with_outputs(predicate, op_res.outputs()) + .unwrap(); + cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); + + // Add an (unreachable) block before the entry + let Signature { input, output } = op.signature().as_ref().clone(); + for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { + let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); + let inp = extra.input_wires(); + let branch = extra.add_load_value(Value::unary_unit_sum()); + let extra = extra.finish_with_outputs(branch, inp).unwrap(); + cfg.branch(&extra, 0, &dest).unwrap(); + } + + let mut h = cfg.finish_hugr().unwrap(); + + let func = h.children(h.module_root()).exactly_one().ok().unwrap(); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); + let mut dfb_children = child_tags_ext_ids(&h, entry.node()); + + let res = normalize_cfg(&mut h); + h.validate().unwrap(); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); + dfb_children.push("UnpackTuple".to_string()); + assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); + } + + #[test] + fn entry_before_loop() -> Result<(), Box> { + /* -> Noop --> Test -> Exit -> Test --> Exit + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(qb_t(), usize_t()))?; + let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let entry = nop_b.finish_with_outputs(br, n.outputs())?; + + let mut loop_b = h.block_builder( + loop_variants.clone(), + [loop_variants, exit_types], + type_row![], + )?; + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&entry, 0, &loop_)?; + h.branch(&loop_, 0, &loop_)?; + h.branch(&loop_, 1, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: false + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + let func_children = child_tags_ext_ids(&h, func); + assert_eq!( + func_children.into_iter().sorted().collect_vec(), + ["Cfg", "Dfg", "Input", "Output",] + ); + let [dfg] = h + .children(func) + .filter(|n| h.get_optype(*n).is_dfg()) + .collect_array() + .unwrap(); + assert_eq!( + child_tags_ext_ids(&h, dfg) + .into_iter() + .sorted() + .collect_vec(), + [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] + ); + Ok(()) + } + + #[test] + fn loop_before_exit() -> Result<(), Box> { + /* -> Test -> Noop -> Exit -> Test --> Exit (then Noop) + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + + let mut loop_b = h.entry_builder(vec![loop_variants, exit_types.clone()], type_row![])?; + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&loop_, 0, &loop_)?; + + let mut nop_b = h.simple_block_builder(endo_sig(exit_types), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(usize_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let tail = nop_b.finish_with_outputs(br, n.outputs())?; + + h.branch(&loop_, 1, &tail)?; + h.branch(&tail, 0, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: false, + exit_changed: true + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + assert_eq!( + h.children(func) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] + ); + + let dfg = h.children(func).last().unwrap(); + let dfg_children = child_tags_ext_ids(&h, dfg); + assert_eq!( + dfg_children.into_iter().sorted().collect_vec(), + [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] + ); + + Ok(()) + } + + #[test] + fn nested_cfgs_pass() { + // --> Entry --> Loop --> Tail --> EXIT + // | / \ + // (E->X) \<-/ + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", []).unwrap(); + let qqu = vec![qb_t(), qb_t(), usize_t()]; + let qq = TypeRow::from(vec![qb_t(); 2]); + let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); + let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2, u] = entry.input_wires_arr(); + let (inner, inner_pred) = { + let mut inner = entry + .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) + .unwrap(); + let mut entry = inner.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2] = entry.input_wires_arr(); + let [pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(pred, []).unwrap(); + inner.branch(&entry, 0, &inner.exit_block()).unwrap(); + (inner.finish_sub_container().unwrap(), pred.node()) + }; + let [q1, q2] = inner.outputs_arr(); + let [entry_pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); + + let loop_b = { + let mut loop_b = outer + .block_builder(qq.clone(), [qb_t().into(), usize_t().into()], qb_t().into()) + .unwrap(); + let [q1, q2] = loop_b.input_wires_arr(); + // u here is `dom` edge from entry block + let [pred] = loop_b + .add_dataflow_op(tst_op, [q1, u]) + .unwrap() + .outputs_arr(); + loop_b.finish_with_outputs(pred, [q2]).unwrap() + }; + outer.branch(&entry, 0, &loop_b).unwrap(); + outer.branch(&loop_b, 0, &loop_b).unwrap(); + + let (tail_b, tail_pred) = { + let uq = TypeRow::from(vec![usize_t(), qb_t()]); + let mut tail_b = outer + .block_builder(uq.clone(), vec![uq.clone()], type_row![]) + .unwrap(); + let [u, q] = tail_b.input_wires_arr(); + let [br] = tail_b + .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) + .unwrap() + .outputs_arr(); + (tail_b.finish_with_outputs(br, []).unwrap(), br.node()) + }; + outer.branch(&loop_b, 1, &tail_b).unwrap(); + outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); + let mut h = outer.finish_hugr().unwrap(); + assert_eq!( + h.get_parent(h.get_parent(inner_pred).unwrap()), + Some(inner.node()) + ); + assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); + assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); + + let res = NormalizeCFGPass::default().run(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + HashMap::from([ + (inner.node(), NormalizeCFGResult::CFGToDFG), + ( + h.entrypoint(), + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: true + } + ) + ]) + ); + // Now contains only one CFG with one BB (self-loop) + assert_eq!( + h.nodes() + .filter(|n| h.get_optype(*n).is_cfg()) + .exactly_one() + .ok(), + Some(h.entrypoint()) + ); + let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); + assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); + // Inner CFG is now a DFG (and still sibling of entry_pred)... + assert_eq!(h.get_parent(inner_pred), Some(inner.node())); + assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); + // Predicates lifted appropriately... + for n in [entry_pred.node(), tail_pred.node()] { + let parent = h.get_parent(n).unwrap(); + assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); + } + // ...and followed by UnpackTuple's + for n in [inner_pred, entry_pred.node(), tail_pred.node()] { + let [unpack] = h.output_neighbours(n).collect_array().unwrap(); + assert!( + h.get_optype(unpack) + .as_extension_op() + .and_then(|e| UnpackTuple::from_extension_op(e).ok()) + .is_some() + ); + } + } + + fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { + h.children(n) + .map(|n| match h.get_optype(n) { + OpType::ExtensionOp(e) => e.unqualified_id().to_string(), + op => format!("{:?}", op.tag()), + }) + .collect() + } +} From c2af7cb44a5dc47215274c209ec390bac8fabe6e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 09:14:12 +0100 Subject: [PATCH 38/49] Revert "Split apart normalize_cfg.rs, taking all tests with it" This reverts commit 3050976227088f10a4990f4cafad9a06fb921d38. --- hugr-passes/src/lib.rs | 2 - hugr-passes/src/merge_bbs.rs | 880 ++++++++++++++++++++++++++++++- hugr-passes/src/normalize_cfg.rs | 879 ------------------------------ 3 files changed, 871 insertions(+), 890 deletions(-) delete mode 100644 hugr-passes/src/normalize_cfg.rs diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 0a3dae4691..6e97f3422e 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -17,10 +17,8 @@ pub use inline_funcs::inline_acyclic; pub mod linearize_array; pub use linearize_array::LinearizeArrayPass; pub mod lower; -#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] pub mod merge_bbs; mod monomorphize; -pub mod normalize_cfg; pub mod untuple; pub use monomorphize::{MonomorphizePass, mangle_name, monomorphize}; diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/merge_bbs.rs index 60395cbe3f..78ae296c4b 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/merge_bbs.rs @@ -2,17 +2,21 @@ //! and the target BB has no other predecessors. use std::collections::HashMap; -use hugr_core::hugr::hugrmut::HugrMut; -use hugr_core::hugr::views::RootCheckable; +use hugr_core::extension::prelude::UnpackTuple; +use hugr_core::hugr::views::{RootCheckable, RootChecked}; +use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; +use hugr_core::types::{EdgeKind, Signature, TypeRow}; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{DFG, DataflowBlock, DataflowParent, Input, Output}; -use hugr_core::{Hugr, HugrView, Node}; +use hugr_core::ops::{ + CFG, DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output, +}; +use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; -use crate::normalize_cfg::wire_unpack_first; +use crate::ComposablePass; /// Merge any basic blocks that are direct children of the specified CFG /// i.e. where a basic block B has a single successor B' whose only predecessor @@ -23,7 +27,7 @@ use crate::normalize_cfg::wire_unpack_first; /// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG -#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] // Note: as a first step, just hide this +#[deprecated(note = "Use normalize_cfg")] // Note: as a first step, just hide this pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) where H: 'h + HugrMut, @@ -46,7 +50,6 @@ where // - the exit block, nodes in n should move *outside* the CFG - a separate pass. continue; } - #[expect(deprecated)] // undeprecate when hidden let (rep, merge_bb, dfgs) = mk_rep(cfg, n, succ); let node_map = cfg.apply_patch(rep).unwrap(); let merged_bb = *node_map.get(&merge_bb).unwrap(); @@ -58,6 +61,250 @@ where } } +/// Errors from [normalize_cfg] +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +pub enum NormalizeCFGError { + /// The requested node was not a CFG + #[error("Requested node was not a CFG but {_0}")] + NotCFG(OpTag), +} + +/// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NormalizeCFGResult { + /// The entire [`CFG`] was converted into a [`DFG`]. + /// + /// The entrypoint node id is preserved after the conversion, but now identifies + /// the new [`DFG`]. + CFGToDFG, + /// The CFG was preserved, but the entry or exit blocks may have changed. + #[allow(missing_docs)] + CFGPreserved { + entry_changed: bool, + exit_changed: bool, + }, +} + +/// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. +#[derive(Clone, Debug)] +pub struct NormalizeCFGPass { + cfgs: Vec, +} + +impl Default for NormalizeCFGPass { + fn default() -> Self { + Self { cfgs: vec![] } + } +} + +impl NormalizeCFGPass { + /// Allows mutating the set of CFG nodes that will be normalized. + /// + /// If empty (the default), all (non-strict) descendants of the [HugrView::entrypoint] + /// will be normalized. + pub fn cfgs(&mut self) -> &mut Vec { + &mut self.cfgs + } +} + +impl ComposablePass for NormalizeCFGPass { + type Error = NormalizeCFGError; + + /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG + type Result = HashMap; + + fn run(&self, hugr: &mut H) -> Result { + let cfgs = if self.cfgs.is_empty() { + let mut v = hugr + .entry_descendants() + .filter(|n| hugr.get_optype(*n).is_cfg()) + .collect::>(); + // Process inner CFGs first, in case they are removed + // (if they are in an unreachable block when the Entry node has only the Exit as successor) + v.reverse(); + v + } else { + self.cfgs.clone() + }; + let mut results = HashMap::new(); + for cfg in cfgs { + let res = normalize_cfg(&mut hugr.with_entrypoint_mut(cfg))?; + results.insert(cfg, res); + } + Ok(results) + } +} + +/// Normalize a CFG in a Hugr: +/// * Merge consecutive basic blocks i.e. where a BB has only a single successor which +/// has no predecessors +/// * If the entry block has only one successor, and no predecessors, then move its contents +/// outside/before CFG. +/// * (Similarly) if the exit block has only one predecessor, then move contents +/// outside/after CFG. +/// * If that predecessor is the entry block, then convert the CFG to a DFG. +/// This will be reported via [NormalizeCFGResult::CFGToDFG] +/// +/// +/// # Errors +/// +/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG +#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter +pub fn normalize_cfg(mut h: &mut H) -> Result { + let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) + .map_err(|e| match e { + HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), + _ => unreachable!(), + })?; + merge_basic_blocks(checked); + let cfg_node = h.entrypoint(); + + // Further normalizations with effects outside the CFG + let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); + let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); + let cfg_parent = h.get_parent(cfg_node).unwrap(); + // 1. If the entry block has only one successor, and no predecessors, then move contents + // outside/before CFG. + // (Note if the entry block has multiple successors, but no predecessors, we *could* move + // its contents outside/before the CFG, but would need to keep an empty/identity entry + // block - we do not do this here.) + let mut entry_changed = false; + #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` + if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { + if succ == exit { + // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore + assert_eq!( + &Signature::new( + entry_blk.inputs.clone(), + entry_blk.successor_input(0).unwrap() + ), + h.signature(cfg_node).unwrap().as_ref() + ); + // 1a. Turn the CFG into a DFG containing only what was in the entry block + // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive + let children_to_remove: Vec<_> = h.children(cfg_node).skip(1).collect(); + for blk in children_to_remove { + h.remove_subtree(blk); + } + while let Some(ch) = h.first_child(entry) { + h.set_parent(ch, cfg_node); + } + h.remove_node(entry); + let cfg_ty = h.optype_mut(cfg_node); + let OpType::CFG(CFG { signature }) = std::mem::take(cfg_ty) else { + panic!() + }; + let result_tys = signature.output.clone(); + *cfg_ty = OpType::DFG(DFG { signature }); + unpack_before_output(h, h.get_io(cfg_node).unwrap()[1], result_tys); + return Ok(NormalizeCFGResult::CFGToDFG); + } else if h.input_neighbours(entry).count() == 0 { + // 1b. Move entry block outside/before the CFG into a DFG; its successor becomes the entry block. + let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); + let dfg = h.add_node_with_parent( + cfg_parent, + DFG { + signature: Signature::new(entry_blk.inputs.clone(), new_cfg_inputs.clone()), + }, + ); + let [_, entry_output] = h.get_io(entry).unwrap(); + while let Some(n) = h.first_child(entry) { + h.set_parent(n, dfg); + } + h.move_before_sibling(succ, entry); + h.remove_node(entry); + + unpack_before_output(h, entry_output, new_cfg_inputs.clone()); + + // Inputs to CFG go directly to DFG + for inp in h.node_inputs(cfg_node).collect::>() { + for src in h.linked_outputs(cfg_node, inp).collect::>() { + h.connect(src.0, src.1, dfg, inp.index()); + } + h.disconnect(cfg_node, inp); + } + + // Update input ports + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { + panic!() + }; + let inputs_to_add = + new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; + cfg_ty.signature.input = new_cfg_inputs; + h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); + + // Wire outputs of DFG directly to CFG + for src in h.node_outputs(dfg).collect::>() { + h.connect(dfg, src, cfg_node, src.index()); + } + entry_changed = true; + } + } + // 2. If the exit node has a single predecessor and that predecessor has no other successors... + let mut exit_changed = false; + if let Some(pred) = h + .input_neighbours(exit) + .exactly_one() + .ok() + .filter(|pred| h.output_neighbours(*pred).count() == 1) + { + // Code in that predecessor can be moved outside (into a new DFG after the CFG), + // and the predecessor deleted + let [_, output] = h.get_io(pred).unwrap(); + let pred_blk = h.get_optype(pred).as_dataflow_block().unwrap(); + let new_cfg_outs = pred_blk.inner_signature().into_owned().input; + + // new CFG result type and exit block + let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { + panic!() + }; + let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); + h.add_ports( + cfg_node, + Direction::Outgoing, + new_cfg_outs.len() as isize - result_tys.len() as isize, + ); + + *h.optype_mut(pred) = ExitBlock { + cfg_outputs: new_cfg_outs.clone(), + } + .into(); + debug_assert_eq!(h.num_ports(pred, Direction::Outgoing), 1); + h.set_num_ports(pred, 1, 0); + + h.move_before_sibling(pred, exit); + h.remove_node(exit); + // Move contents into new DFG + let dfg = h.add_node_with_parent( + cfg_parent, + DFG { + signature: Signature::new(new_cfg_outs, result_tys.clone()), + }, + ); + while let Some(n) = h.first_child(pred) { + h.set_parent(n, dfg); + } + unpack_before_output(h, output, result_tys); + + // Move output edges. + for p in h.node_outputs(cfg_node).collect_vec() { + let tgts = h.linked_inputs(cfg_node, p).collect_vec(); + h.disconnect(cfg_node, p); + for tgt in tgts { + h.connect(dfg, p, tgt.0, tgt.1) + } + } + for p in h.node_inputs(dfg).collect_vec() { + h.connect(cfg_node, p.index(), dfg, p); + } + exit_changed = true; + } + Ok(NormalizeCFGResult::CFGPreserved { + entry_changed, + exit_changed, + }) +} + fn mk_rep( cfg: &H, pred: H::Node, @@ -118,9 +365,8 @@ fn mk_rep( .collect::>(); let dfg_order_out = replacement.get_optype(dfg1).other_output_port().unwrap(); - // Do not add Order edges between DFGs unless there are no value edges let order_srcs = (dfg1_outs.is_empty()).then_some((dfg1, dfg_order_out)); - + // Do not add Order edges between DFGs unless there are no value edges wire_unpack_first(&mut replacement, dfg1_outs, order_srcs, dfg2); // If there are edges from succ back to pred, we cannot do these via the mu_inp/out/new @@ -165,3 +411,619 @@ fn mk_rep( }; (rep, merged, [dfg1, dfg2]) } + +type NodePorts = Vec<(N, OutgoingPort)>; +/// Remove all input wires to `n` and return them in two groups: +/// the [EdgeKind::Value] inputs, and the [EdgeKind::StateOrder] inputs +fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { + let mut values = vec![]; + let mut orders = vec![]; + for p in h.node_inputs(n).collect_vec() { + let srcs = h.linked_outputs(n, p).collect_vec(); + h.disconnect(n, p); + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(_)) => { + assert_eq!(srcs.len(), 1); + values.extend(srcs); + } + Some(EdgeKind::StateOrder) => { + assert_eq!(orders, []); + orders.extend(srcs); + } + k => panic!("Unexpected port kind: {:?}", k), + } + } + (values, orders) +} + +fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { + match h.get_optype(n).port_kind(p) { + Some(EdgeKind::Value(ty)) => ty.as_sum().unwrap().as_tuple().unwrap().clone(), + p => panic!("Expected Value port not {:?}", p), + } + .try_into() + .unwrap() +} + +/// Unpack the first `value_srcs`; wire the unpacked elements and remaining `value_srcs` into +/// consecutive ports of `dst`. Finally wire `order_srcs` all to the order input of `dst`. +fn wire_unpack_first( + h: &mut H, + value_srcs: impl IntoIterator, + order_srcs: impl IntoIterator, + dst: H::Node, +) { + let parent = h.get_parent(dst).unwrap(); + let mut srcs = value_srcs.into_iter(); + let src_to_unpack = srcs.next().unwrap(); + let tuple_tys = tuple_elems(h, src_to_unpack.0, src_to_unpack.1); + let tuple_len = tuple_tys.len(); + let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); + h.connect(src_to_unpack.0, src_to_unpack.1, unp, 0); + + for i in 0..tuple_len { + h.connect(unp, i, dst, i); + } + assert_eq!( + h.get_optype(dst).other_port_kind(Direction::Incoming), + Some(EdgeKind::StateOrder) + ); + let order_tgt = h.get_optype(dst).other_input_port().unwrap(); + for (i, (src, src_p)) in srcs.enumerate() { + assert!(i + tuple_len < order_tgt.index()); + h.connect(src, src_p, dst, i + tuple_len); + } + for (src, src_p) in order_srcs { + h.connect(src, src_p, dst, order_tgt); + } +} + +/// Unpack the first input to specified [Output] node and shuffle all the rest along +fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: TypeRow) { + let (values, orders) = take_inputs(h, output_node); + let OpType::Output(ou) = h.optype_mut(output_node) else { + panic!() + }; + let ports_to_add = new_types.len() as isize - ou.types.len() as isize; + ou.types = new_types; + h.add_ports(output_node, Direction::Incoming, ports_to_add); + wire_unpack_first(h, values, orders, output_node); +} + +#[cfg(test)] +#[expect(deprecated)] // remove tests of merge_bbs, or just hide the latter +mod test { + use std::collections::{HashMap, HashSet}; + use std::sync::Arc; + + use hugr_core::extension::simple_op::MakeExtensionOp; + use itertools::Itertools; + use rstest::rstest; + + use hugr_core::builder::{ + CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, + }; + use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; + use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; + use hugr_core::ops::{constant::Value, handle::NodeHandle}; + use hugr_core::types::{Signature, Type, TypeRow}; + use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; + + use super::{NormalizeCFGPass, NormalizeCFGResult, merge_basic_blocks, normalize_cfg}; + use crate::ComposablePass; + + const_extension_ids! { + const EXT_ID: ExtensionId = "TestExt"; + } + + fn extension() -> Arc { + Extension::new_arc( + EXT_ID, + hugr_core::extension::Version::new(0, 1, 0), + |ext, extension_ref| { + ext.add_op( + "Test".into(), + String::new(), + Signature::new( + vec![qb_t(), usize_t()], + TypeRow::from(vec![Type::new_sum(vec![vec![qb_t()], vec![usize_t()]])]), + ), + extension_ref, + ) + .unwrap(); + }, + ) + } + + #[rstest] + #[case(true)] + #[case(false)] + fn merge_bbs_in_loop(#[case] self_loop: bool) -> Result<(), Box> { + /* self_loop==False: + -> Noop1 -----> Test -> Exit -> Noop1AndTest --> Exit + | | => / \ + \-<- Noop2 <-/ \-<- Noop2 <-/ + (Noop2 -> Noop1 cannot be merged because Noop1 is the entry node) + + self_loop==True: + -> Noop --> Test -> Exit -> NoopAndTest --> Exit + | | => / \ + \--<--<--/ \--<-----<--/ + */ + + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + let mut no_b1 = h.simple_entry_builder(loop_variants.clone(), 1)?; + let n = no_b1.add_dataflow_op(Noop::new(qb_t()), no_b1.input_wires())?; + let br = no_b1.add_load_value(Value::unary_unit_sum()); + let no_b1 = no_b1.finish_with_outputs(br, n.outputs())?; + let mut test_block = h.block_builder( + loop_variants.clone(), + vec![loop_variants.clone(), exit_types], + type_row![], + )?; + let [test_input] = test_block.input_wires_arr(); + let usize_cst = test_block.add_load_value(ConstUsize::new(1)); + let [tst] = test_block + .add_dataflow_op(tst_op, [test_input, usize_cst])? + .outputs_arr(); + let test_block = test_block.finish_with_outputs(tst, [])?; + let loop_backedge_target = if self_loop { + no_b1 + } else { + let mut no_b2 = h.simple_block_builder(endo_sig(loop_variants), 1)?; + let n = no_b2.add_dataflow_op(Noop::new(qb_t()), no_b2.input_wires())?; + let br = no_b2.add_load_value(Value::unary_unit_sum()); + let nid = no_b2.finish_with_outputs(br, n.outputs())?; + h.branch(&nid, 0, &no_b1)?; + nid + }; + h.branch(&no_b1, 0, &test_block)?; + h.branch(&test_block, 0, &loop_backedge_target)?; + h.branch(&test_block, 1, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let r = h.entrypoint(); + merge_basic_blocks(&mut h); + h.validate().unwrap(); + assert_eq!(r, h.entrypoint()); + assert!(matches!(h.get_optype(r), OpType::CFG(_))); + let [entry, exit] = h + .children(r) + .take(2) + .collect::>() + .try_into() + .unwrap(); + // Check the Noop('s) is/are in the right block(s) + let nops = h + .entry_descendants() + .filter(|n| h.get_optype(*n).cast::().is_some()); + let (entry_nop, expected_backedge_target) = if self_loop { + assert_eq!(h.children(r).count(), 2); + (nops.exactly_one().ok().unwrap(), entry) + } else { + let [_, _, no_b2] = h.children(r).collect::>().try_into().unwrap(); + let mut nops = nops.collect::>(); + let entry_nop_idx = nops + .iter() + .position(|n| h.get_parent(*n) == Some(entry)) + .unwrap(); + let entry_nop = nops[entry_nop_idx]; + nops.remove(entry_nop_idx); + let [n_op2] = nops.try_into().unwrap(); + assert_eq!(h.get_parent(n_op2), Some(no_b2)); + (entry_nop, no_b2) + }; + assert_eq!(h.get_parent(entry_nop), Some(entry)); + assert_eq!( + h.output_neighbours(entry).collect::>(), + HashSet::from([expected_backedge_target, exit]) + ); + // And the Noop in the entry block is consumed by the custom Test op + let tst = find_unique( + h.entry_descendants(), + |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), + ); + assert_eq!(h.get_parent(tst), Some(entry)); + assert_eq!( + h.output_neighbours(entry_nop).collect::>(), + vec![tst] + ); + Ok(()) + } + + #[test] + fn elide_triple_with_permute() -> Result<(), Box> { + // Blocks are just BB1 -> BB2 -> BB3 --> Exit. + // Should be merged into one BB (we don't check that specifically) + // and then the whole CFG elided. + let e = extension(); + let tst_op: OpType = e.instantiate_extension_op("Test", &[])?.into(); + let [res_t] = tst_op + .dataflow_signature() + .unwrap() + .into_owned() + .output + .into_owned() + .try_into() + .unwrap(); + let mut h = CFGBuilder::new(inout_sig(qb_t(), res_t.clone()))?; + let mut bb1 = h.simple_entry_builder(vec![usize_t(), qb_t()].into(), 1)?; + let [inw] = bb1.input_wires_arr(); + let load_cst = bb1.add_load_value(ConstUsize::new(1)); + let pred = bb1.add_load_value(Value::unary_unit_sum()); + let bb1 = bb1.finish_with_outputs(pred, [load_cst, inw])?; + + let mut bb2 = h.block_builder( + vec![usize_t(), qb_t()].into(), + vec![type_row![]], + vec![qb_t(), usize_t()].into(), + )?; + let [u, q] = bb2.input_wires_arr(); + let pred = bb2.add_load_value(Value::unary_unit_sum()); + let bb2 = bb2.finish_with_outputs(pred, [q, u])?; + + let mut bb3 = h.block_builder( + vec![qb_t(), usize_t()].into(), + vec![type_row![]], + res_t.clone().into(), + )?; + let [q, u] = bb3.input_wires_arr(); + let tst = bb3.add_dataflow_op(tst_op, [q, u])?; + let pred = bb3.add_load_value(Value::unary_unit_sum()); + let bb3 = bb3.finish_with_outputs(pred, tst.outputs())?; + // Now add control-flow edges between basic blocks + h.branch(&bb1, 0, &bb2)?; + h.branch(&bb2, 0, &bb3)?; + h.branch(&bb3, 0, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let res = normalize_cfg(&mut h); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); + h.validate()?; + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!( + h.entry_descendants().find(|n| matches!( + h.get_optype(*n), + OpType::DataflowBlock(_) | OpType::CFG(_) | OpType::ExitBlock(_) + )), + None + ); + let tst = find_unique( + h.entry_descendants(), + |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), + ); + assert_eq!(h.get_parent(tst), Some(h.entrypoint())); + + let inp = find_unique(h.entry_descendants(), |n| { + matches!(h.get_optype(*n), OpType::Input(_)) + }); + let mut tst_inputs = h.input_neighbours(tst).collect::>(); + tst_inputs.remove(tst_inputs.iter().find_position(|n| **n == inp).unwrap().0); + let [other_input] = tst_inputs.try_into().unwrap(); + assert_eq!( + h.get_optype(other_input), + &(LoadConstant { + datatype: usize_t() + } + .into()) + ); + Ok(()) + } + + fn find_unique(items: impl Iterator, pred: impl Fn(&T) -> bool) -> T { + items.filter(pred).exactly_one().ok().unwrap() + } + + #[rstest] + fn elide_cfg() { + let ext = extension(); + let op = ext.instantiate_extension_op("Test", []).unwrap(); + let out_ty = op.signature().output().clone(); + let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); + let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); + let op_res = entry + .add_dataflow_op(op.clone(), entry.input_wires()) + .unwrap(); + let predicate = entry.add_load_value(Value::unary_unit_sum()); + let entry = entry + .finish_with_outputs(predicate, op_res.outputs()) + .unwrap(); + cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); + + // Add an (unreachable) block before the entry + let Signature { input, output } = op.signature().as_ref().clone(); + for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { + let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); + let inp = extra.input_wires(); + let branch = extra.add_load_value(Value::unary_unit_sum()); + let extra = extra.finish_with_outputs(branch, inp).unwrap(); + cfg.branch(&extra, 0, &dest).unwrap(); + } + + let mut h = cfg.finish_hugr().unwrap(); + + let func = h.children(h.module_root()).exactly_one().ok().unwrap(); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); + let mut dfb_children = child_tags_ext_ids(&h, entry.node()); + + let res = normalize_cfg(&mut h); + h.validate().unwrap(); + assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); + assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); + assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); + dfb_children.push("UnpackTuple".to_string()); + assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); + } + + #[test] + fn entry_before_loop() -> Result<(), Box> { + /* -> Noop --> Test -> Exit -> Test --> Exit + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(qb_t(), usize_t()))?; + let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let entry = nop_b.finish_with_outputs(br, n.outputs())?; + + let mut loop_b = h.block_builder( + loop_variants.clone(), + [loop_variants, exit_types], + type_row![], + )?; + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&entry, 0, &loop_)?; + h.branch(&loop_, 0, &loop_)?; + h.branch(&loop_, 1, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: false + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + let func_children = child_tags_ext_ids(&h, func); + assert_eq!( + func_children.into_iter().sorted().collect_vec(), + ["Cfg", "Dfg", "Input", "Output",] + ); + let [dfg] = h + .children(func) + .filter(|n| h.get_optype(*n).is_dfg()) + .collect_array() + .unwrap(); + assert_eq!( + child_tags_ext_ids(&h, dfg) + .into_iter() + .sorted() + .collect_vec(), + [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] + ); + Ok(()) + } + + #[test] + fn loop_before_exit() -> Result<(), Box> { + /* -> Test -> Noop -> Exit -> Test --> Exit (then Noop) + | | => | | + \<-/ \<-/ + */ + let loop_variants: TypeRow = vec![qb_t()].into(); + let exit_types: TypeRow = vec![usize_t()].into(); + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", [])?; + let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; + + let mut loop_b = h.entry_builder(vec![loop_variants, exit_types.clone()], type_row![])?; + let [qb] = loop_b.input_wires_arr(); + let usz = loop_b.add_load_value(ConstUsize::new(3)); + let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); + let loop_ = loop_b.finish_with_outputs(tst, [])?; + h.branch(&loop_, 0, &loop_)?; + + let mut nop_b = h.simple_block_builder(endo_sig(exit_types), 1)?; + let n = nop_b.add_dataflow_op(Noop::new(usize_t()), nop_b.input_wires())?; + let br = nop_b.add_load_value(Value::unary_unit_sum()); + let tail = nop_b.finish_with_outputs(br, n.outputs())?; + + h.branch(&loop_, 1, &tail)?; + h.branch(&tail, 0, &h.exit_block())?; + + let mut h = h.finish_hugr()?; + let res = normalize_cfg(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + NormalizeCFGResult::CFGPreserved { + entry_changed: false, + exit_changed: true + } + ); + assert_eq!( + h.children(h.entrypoint()) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::DataflowBlock, OpTag::BasicBlockExit] + ); + let func = h.get_parent(h.entrypoint()).unwrap(); + assert_eq!( + h.children(func) + .map(|n| h.get_optype(n).tag()) + .collect_vec(), + [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] + ); + + let dfg = h.children(func).last().unwrap(); + let dfg_children = child_tags_ext_ids(&h, dfg); + assert_eq!( + dfg_children.into_iter().sorted().collect_vec(), + [ + "Const", + "Input", + "LoadConst", + "Noop", + "Output", + "UnpackTuple" + ] + ); + + Ok(()) + } + + #[test] + fn nested_cfgs_pass() { + // --> Entry --> Loop --> Tail --> EXIT + // | / \ + // (E->X) \<-/ + let e = extension(); + let tst_op = e.instantiate_extension_op("Test", []).unwrap(); + let qqu = vec![qb_t(), qb_t(), usize_t()]; + let qq = TypeRow::from(vec![qb_t(); 2]); + let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); + let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2, u] = entry.input_wires_arr(); + let (inner, inner_pred) = { + let mut inner = entry + .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) + .unwrap(); + let mut entry = inner.entry_builder(vec![qq.clone()], type_row![]).unwrap(); + let [q1, q2] = entry.input_wires_arr(); + let [pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(pred, []).unwrap(); + inner.branch(&entry, 0, &inner.exit_block()).unwrap(); + (inner.finish_sub_container().unwrap(), pred.node()) + }; + let [q1, q2] = inner.outputs_arr(); + let [entry_pred] = entry + .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) + .unwrap() + .outputs_arr(); + let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); + + let loop_b = { + let mut loop_b = outer + .block_builder(qq.clone(), [qb_t().into(), usize_t().into()], qb_t().into()) + .unwrap(); + let [q1, q2] = loop_b.input_wires_arr(); + // u here is `dom` edge from entry block + let [pred] = loop_b + .add_dataflow_op(tst_op, [q1, u]) + .unwrap() + .outputs_arr(); + loop_b.finish_with_outputs(pred, [q2]).unwrap() + }; + outer.branch(&entry, 0, &loop_b).unwrap(); + outer.branch(&loop_b, 0, &loop_b).unwrap(); + + let (tail_b, tail_pred) = { + let uq = TypeRow::from(vec![usize_t(), qb_t()]); + let mut tail_b = outer + .block_builder(uq.clone(), vec![uq.clone()], type_row![]) + .unwrap(); + let [u, q] = tail_b.input_wires_arr(); + let [br] = tail_b + .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) + .unwrap() + .outputs_arr(); + (tail_b.finish_with_outputs(br, []).unwrap(), br.node()) + }; + outer.branch(&loop_b, 1, &tail_b).unwrap(); + outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); + let mut h = outer.finish_hugr().unwrap(); + assert_eq!( + h.get_parent(h.get_parent(inner_pred).unwrap()), + Some(inner.node()) + ); + assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); + assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); + + let res = NormalizeCFGPass::default().run(&mut h).unwrap(); + h.validate().unwrap(); + assert_eq!( + res, + HashMap::from([ + (inner.node(), NormalizeCFGResult::CFGToDFG), + ( + h.entrypoint(), + NormalizeCFGResult::CFGPreserved { + entry_changed: true, + exit_changed: true + } + ) + ]) + ); + // Now contains only one CFG with one BB (self-loop) + assert_eq!( + h.nodes() + .filter(|n| h.get_optype(*n).is_cfg()) + .exactly_one() + .ok(), + Some(h.entrypoint()) + ); + let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); + assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); + // Inner CFG is now a DFG (and still sibling of entry_pred)... + assert_eq!(h.get_parent(inner_pred), Some(inner.node())); + assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); + // Predicates lifted appropriately... + for n in [entry_pred.node(), tail_pred.node()] { + let parent = h.get_parent(n).unwrap(); + assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); + assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); + } + // ...and followed by UnpackTuple's + for n in [inner_pred, entry_pred.node(), tail_pred.node()] { + let [unpack] = h.output_neighbours(n).collect_array().unwrap(); + assert!( + h.get_optype(unpack) + .as_extension_op() + .and_then(|e| UnpackTuple::from_extension_op(e).ok()) + .is_some() + ); + } + } + + fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { + h.children(n) + .map(|n| match h.get_optype(n) { + OpType::ExtensionOp(e) => e.unqualified_id().to_string(), + op => format!("{:?}", op.tag()), + }) + .collect() + } +} diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs deleted file mode 100644 index df88b9d90d..0000000000 --- a/hugr-passes/src/normalize_cfg.rs +++ /dev/null @@ -1,879 +0,0 @@ -//! Merge BBs along control-flow edges where the source BB has no other successors -//! and the target BB has no other predecessors. -use std::collections::HashMap; - -use hugr_core::extension::prelude::UnpackTuple; -use hugr_core::hugr::views::RootChecked; -use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; -use hugr_core::types::{EdgeKind, Signature, TypeRow}; -use itertools::Itertools; - -use hugr_core::ops::handle::CfgID; -use hugr_core::ops::{CFG, DFG, DataflowParent, ExitBlock, OpTag, OpType}; -use hugr_core::{Direction, HugrView, OutgoingPort, PortIndex}; - -use crate::ComposablePass; -#[expect(deprecated)] // refactor/inline merge_basic_blocks, or just hide it -use crate::merge_bbs::merge_basic_blocks; - -/// Errors from [normalize_cfg] -#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] -pub enum NormalizeCFGError { - /// The requested node was not a CFG - #[error("Requested node was not a CFG but {_0}")] - NotCFG(OpTag), -} - -/// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NormalizeCFGResult { - /// The entire [`CFG`] was converted into a [`DFG`]. - /// - /// The entrypoint node id is preserved after the conversion, but now identifies - /// the new [`DFG`]. - CFGToDFG, - /// The CFG was preserved, but the entry or exit blocks may have changed. - #[allow(missing_docs)] - CFGPreserved { - entry_changed: bool, - exit_changed: bool, - }, -} - -/// A [ComposablePass] that normalizes CFGs (i.e. [normalize_cfg]) in a Hugr. -#[derive(Clone, Debug)] -pub struct NormalizeCFGPass { - cfgs: Vec, -} - -impl Default for NormalizeCFGPass { - fn default() -> Self { - Self { cfgs: vec![] } - } -} - -impl NormalizeCFGPass { - /// Allows mutating the set of CFG nodes that will be normalized. - /// - /// If empty (the default), all (non-strict) descendants of the [HugrView::entrypoint] - /// will be normalized. - pub fn cfgs(&mut self) -> &mut Vec { - &mut self.cfgs - } -} - -impl ComposablePass for NormalizeCFGPass { - type Error = NormalizeCFGError; - - /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG - type Result = HashMap; - - fn run(&self, hugr: &mut H) -> Result { - let cfgs = if self.cfgs.is_empty() { - let mut v = hugr - .entry_descendants() - .filter(|n| hugr.get_optype(*n).is_cfg()) - .collect::>(); - // Process inner CFGs first, in case they are removed - // (if they are in an unreachable block when the Entry node has only the Exit as successor) - v.reverse(); - v - } else { - self.cfgs.clone() - }; - let mut results = HashMap::new(); - for cfg in cfgs { - let res = normalize_cfg(&mut hugr.with_entrypoint_mut(cfg))?; - results.insert(cfg, res); - } - Ok(results) - } -} - -/// Normalize a CFG in a Hugr: -/// * Merge consecutive basic blocks i.e. where a BB has only a single successor which -/// has no predecessors -/// * If the entry block has only one successor, and no predecessors, then move its contents -/// outside/before CFG. -/// * (Similarly) if the exit block has only one predecessor, then move contents -/// outside/after CFG. -/// * If that predecessor is the entry block, then convert the CFG to a DFG. -/// This will be reported via [NormalizeCFGResult::CFGToDFG] -/// -/// -/// # Errors -/// -/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG -#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter -pub fn normalize_cfg(mut h: &mut H) -> Result { - let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) - .map_err(|e| match e { - HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), - _ => unreachable!(), - })?; - merge_basic_blocks(checked); - let cfg_node = h.entrypoint(); - - // Further normalizations with effects outside the CFG - let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); - let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); - let cfg_parent = h.get_parent(cfg_node).unwrap(); - // 1. If the entry block has only one successor, and no predecessors, then move contents - // outside/before CFG. - // (Note if the entry block has multiple successors, but no predecessors, we *could* move - // its contents outside/before the CFG, but would need to keep an empty/identity entry - // block - we do not do this here.) - let mut entry_changed = false; - #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` - if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { - if succ == exit { - // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore - assert_eq!( - &Signature::new( - entry_blk.inputs.clone(), - entry_blk.successor_input(0).unwrap() - ), - h.signature(cfg_node).unwrap().as_ref() - ); - // 1a. Turn the CFG into a DFG containing only what was in the entry block - // Annoying here - "while let Some(blk) = cfg.children(...).skip(1).next()" keeps iterator alive - let children_to_remove: Vec<_> = h.children(cfg_node).skip(1).collect(); - for blk in children_to_remove { - h.remove_subtree(blk); - } - while let Some(ch) = h.first_child(entry) { - h.set_parent(ch, cfg_node); - } - h.remove_node(entry); - let cfg_ty = h.optype_mut(cfg_node); - let OpType::CFG(CFG { signature }) = std::mem::take(cfg_ty) else { - unreachable!() // Checked at entry - }; - let result_tys = signature.output.clone(); - *cfg_ty = OpType::DFG(DFG { signature }); - unpack_before_output(h, h.get_io(cfg_node).unwrap()[1], result_tys); - return Ok(NormalizeCFGResult::CFGToDFG); - } else if h.input_neighbours(entry).count() == 0 { - // 1b. Move entry block outside/before the CFG into a DFG; its successor becomes the entry block. - let new_cfg_inputs = entry_blk.successor_input(0).unwrap(); - let dfg = h.add_node_with_parent( - cfg_parent, - DFG { - signature: Signature::new(entry_blk.inputs.clone(), new_cfg_inputs.clone()), - }, - ); - let [_, entry_output] = h.get_io(entry).unwrap(); - while let Some(n) = h.first_child(entry) { - h.set_parent(n, dfg); - } - h.move_before_sibling(succ, entry); - h.remove_node(entry); - - unpack_before_output(h, entry_output, new_cfg_inputs.clone()); - - // Inputs to CFG go directly to DFG - for inp in h.node_inputs(cfg_node).collect::>() { - for src in h.linked_outputs(cfg_node, inp).collect::>() { - h.connect(src.0, src.1, dfg, inp.index()); - } - h.disconnect(cfg_node, inp); - } - - // Update input ports - let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { - panic!() - }; - let inputs_to_add = - new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; - cfg_ty.signature.input = new_cfg_inputs; - h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); - - // Wire outputs of DFG directly to CFG - for src in h.node_outputs(dfg).collect::>() { - h.connect(dfg, src, cfg_node, src.index()); - } - entry_changed = true; - } - } - // 2. If the exit node has a single predecessor and that predecessor has no other successors... - let mut exit_changed = false; - if let Some(pred) = h - .input_neighbours(exit) - .exactly_one() - .ok() - .filter(|pred| h.output_neighbours(*pred).count() == 1) - { - // Code in that predecessor can be moved outside (into a new DFG after the CFG), - // and the predecessor deleted - let [_, output] = h.get_io(pred).unwrap(); - let pred_blk = h.get_optype(pred).as_dataflow_block().unwrap(); - let new_cfg_outs = pred_blk.inner_signature().into_owned().input; - - // new CFG result type and exit block - let OpType::CFG(cfg_ty) = h.optype_mut(cfg_node) else { - panic!() - }; - let result_tys = std::mem::replace(&mut cfg_ty.signature.output, new_cfg_outs.clone()); - h.add_ports( - cfg_node, - Direction::Outgoing, - new_cfg_outs.len() as isize - result_tys.len() as isize, - ); - - *h.optype_mut(pred) = ExitBlock { - cfg_outputs: new_cfg_outs.clone(), - } - .into(); - debug_assert_eq!(h.num_ports(pred, Direction::Outgoing), 1); - h.set_num_ports(pred, 1, 0); - - h.move_before_sibling(pred, exit); - h.remove_node(exit); - // Move contents into new DFG - let dfg = h.add_node_with_parent( - cfg_parent, - DFG { - signature: Signature::new(new_cfg_outs, result_tys.clone()), - }, - ); - while let Some(n) = h.first_child(pred) { - h.set_parent(n, dfg); - } - unpack_before_output(h, output, result_tys); - - // Move output edges. - for p in h.node_outputs(cfg_node).collect_vec() { - let tgts = h.linked_inputs(cfg_node, p).collect_vec(); - h.disconnect(cfg_node, p); - for tgt in tgts { - h.connect(dfg, p, tgt.0, tgt.1) - } - } - for p in h.node_inputs(dfg).collect_vec() { - h.connect(cfg_node, p.index(), dfg, p); - } - exit_changed = true; - } - Ok(NormalizeCFGResult::CFGPreserved { - entry_changed, - exit_changed, - }) -} - -type NodePorts = Vec<(N, OutgoingPort)>; -/// Remove all input wires to `n` and return them in two groups: -/// the [EdgeKind::Value] inputs, and the [EdgeKind::StateOrder] inputs -fn take_inputs(h: &mut H, n: H::Node) -> (NodePorts, NodePorts) { - let mut values = vec![]; - let mut orders = vec![]; - for p in h.node_inputs(n).collect_vec() { - let srcs = h.linked_outputs(n, p).collect_vec(); - h.disconnect(n, p); - match h.get_optype(n).port_kind(p) { - Some(EdgeKind::Value(_)) => { - assert_eq!(srcs.len(), 1); - values.extend(srcs); - } - Some(EdgeKind::StateOrder) => { - assert_eq!(orders, []); - orders.extend(srcs); - } - k => panic!("Unexpected port kind: {:?}", k), - } - } - (values, orders) -} - -fn tuple_elems(h: &H, n: H::Node, p: OutgoingPort) -> TypeRow { - match h.get_optype(n).port_kind(p) { - Some(EdgeKind::Value(ty)) => ty.as_sum().unwrap().as_tuple().unwrap().clone(), - p => panic!("Expected Value port not {:?}", p), - } - .try_into() - .unwrap() -} - -/// Unpack the first `value_srcs`; wire the unpacked elements and remaining `value_srcs` into -/// consecutive ports of `dst`. Finally wire `order_srcs` all to the order input of `dst`. -pub(crate) fn wire_unpack_first( - h: &mut H, - value_srcs: impl IntoIterator, - order_srcs: impl IntoIterator, - dst: H::Node, -) { - let parent = h.get_parent(dst).unwrap(); - let mut srcs = value_srcs.into_iter(); - let src_to_unpack = srcs.next().unwrap(); - let tuple_tys = tuple_elems(h, src_to_unpack.0, src_to_unpack.1); - let tuple_len = tuple_tys.len(); - let unp = h.add_node_with_parent(parent, UnpackTuple::new(tuple_tys)); - h.connect(src_to_unpack.0, src_to_unpack.1, unp, 0); - - for i in 0..tuple_len { - h.connect(unp, i, dst, i); - } - assert_eq!( - h.get_optype(dst).other_port_kind(Direction::Incoming), - Some(EdgeKind::StateOrder) - ); - let order_tgt = h.get_optype(dst).other_input_port().unwrap(); - for (i, (src, src_p)) in srcs.enumerate() { - assert!(i + tuple_len < order_tgt.index()); - h.connect(src, src_p, dst, i + tuple_len); - } - for (src, src_p) in order_srcs { - h.connect(src, src_p, dst, order_tgt); - } -} - -/// Unpack the first input to specified [Output] node and shuffle all the rest along -fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: TypeRow) { - let (values, orders) = take_inputs(h, output_node); - let OpType::Output(ou) = h.optype_mut(output_node) else { - panic!() - }; - let ports_to_add = new_types.len() as isize - ou.types.len() as isize; - ou.types = new_types; - h.add_ports(output_node, Direction::Incoming, ports_to_add); - wire_unpack_first(h, values, orders, output_node); -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::sync::Arc; - - use hugr_core::extension::simple_op::MakeExtensionOp; - use itertools::Itertools; - use rstest::rstest; - - #[expect(deprecated)] - // when merge_basic_blocks removed/hidden, consider testing normalize_cfgs directly - use super::{NormalizeCFGPass, NormalizeCFGResult, merge_basic_blocks, normalize_cfg}; - use crate::ComposablePass; - use hugr_core::builder::{ - CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, - }; - use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; - use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; - use hugr_core::ops::{constant::Value, handle::NodeHandle}; - use hugr_core::types::{Signature, Type, TypeRow}; - use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; - - const_extension_ids! { - const EXT_ID: ExtensionId = "TestExt"; - } - - fn extension() -> Arc { - Extension::new_arc( - EXT_ID, - hugr_core::extension::Version::new(0, 1, 0), - |ext, extension_ref| { - ext.add_op( - "Test".into(), - String::new(), - Signature::new( - vec![qb_t(), usize_t()], - TypeRow::from(vec![Type::new_sum(vec![vec![qb_t()], vec![usize_t()]])]), - ), - extension_ref, - ) - .unwrap(); - }, - ) - } - - #[rstest] - #[case(true)] - #[case(false)] - fn merge_bbs_in_loop(#[case] self_loop: bool) -> Result<(), Box> { - /* self_loop==False: - -> Noop1 -----> Test -> Exit -> Noop1AndTest --> Exit - | | => / \ - \-<- Noop2 <-/ \-<- Noop2 <-/ - (Noop2 -> Noop1 cannot be merged because Noop1 is the entry node) - - self_loop==True: - -> Noop --> Test -> Exit -> NoopAndTest --> Exit - | | => / \ - \--<--<--/ \--<-----<--/ - */ - - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; - let mut no_b1 = h.simple_entry_builder(loop_variants.clone(), 1)?; - let n = no_b1.add_dataflow_op(Noop::new(qb_t()), no_b1.input_wires())?; - let br = no_b1.add_load_value(Value::unary_unit_sum()); - let no_b1 = no_b1.finish_with_outputs(br, n.outputs())?; - let mut test_block = h.block_builder( - loop_variants.clone(), - vec![loop_variants.clone(), exit_types], - type_row![], - )?; - let [test_input] = test_block.input_wires_arr(); - let usize_cst = test_block.add_load_value(ConstUsize::new(1)); - let [tst] = test_block - .add_dataflow_op(tst_op, [test_input, usize_cst])? - .outputs_arr(); - let test_block = test_block.finish_with_outputs(tst, [])?; - let loop_backedge_target = if self_loop { - no_b1 - } else { - let mut no_b2 = h.simple_block_builder(endo_sig(loop_variants), 1)?; - let n = no_b2.add_dataflow_op(Noop::new(qb_t()), no_b2.input_wires())?; - let br = no_b2.add_load_value(Value::unary_unit_sum()); - let nid = no_b2.finish_with_outputs(br, n.outputs())?; - h.branch(&nid, 0, &no_b1)?; - nid - }; - h.branch(&no_b1, 0, &test_block)?; - h.branch(&test_block, 0, &loop_backedge_target)?; - h.branch(&test_block, 1, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let r = h.entrypoint(); - #[expect(deprecated)] - // when merge_basic_blocks removed/hidden, consider testing normalize_cfgs directly - merge_basic_blocks(&mut h); - h.validate().unwrap(); - assert_eq!(r, h.entrypoint()); - assert!(matches!(h.get_optype(r), OpType::CFG(_))); - let [entry, exit] = h - .children(r) - .take(2) - .collect::>() - .try_into() - .unwrap(); - // Check the Noop('s) is/are in the right block(s) - let nops = h - .entry_descendants() - .filter(|n| h.get_optype(*n).cast::().is_some()); - let (entry_nop, expected_backedge_target) = if self_loop { - assert_eq!(h.children(r).count(), 2); - (nops.exactly_one().ok().unwrap(), entry) - } else { - let [_, _, no_b2] = h.children(r).collect::>().try_into().unwrap(); - let mut nops = nops.collect::>(); - let entry_nop_idx = nops - .iter() - .position(|n| h.get_parent(*n) == Some(entry)) - .unwrap(); - let entry_nop = nops[entry_nop_idx]; - nops.remove(entry_nop_idx); - let [n_op2] = nops.try_into().unwrap(); - assert_eq!(h.get_parent(n_op2), Some(no_b2)); - (entry_nop, no_b2) - }; - assert_eq!(h.get_parent(entry_nop), Some(entry)); - assert_eq!( - h.output_neighbours(entry).collect::>(), - HashSet::from([expected_backedge_target, exit]) - ); - // And the Noop in the entry block is consumed by the custom Test op - let tst = find_unique( - h.entry_descendants(), - |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), - ); - assert_eq!(h.get_parent(tst), Some(entry)); - assert_eq!( - h.output_neighbours(entry_nop).collect::>(), - vec![tst] - ); - Ok(()) - } - - #[test] - fn elide_triple_with_permute() -> Result<(), Box> { - // Blocks are just BB1 -> BB2 -> BB3 --> Exit. - // Should be merged into one BB (we don't check that specifically) - // and then the whole CFG elided. - let e = extension(); - let tst_op: OpType = e.instantiate_extension_op("Test", &[])?.into(); - let [res_t] = tst_op - .dataflow_signature() - .unwrap() - .into_owned() - .output - .into_owned() - .try_into() - .unwrap(); - let mut h = CFGBuilder::new(inout_sig(qb_t(), res_t.clone()))?; - let mut bb1 = h.simple_entry_builder(vec![usize_t(), qb_t()].into(), 1)?; - let [inw] = bb1.input_wires_arr(); - let load_cst = bb1.add_load_value(ConstUsize::new(1)); - let pred = bb1.add_load_value(Value::unary_unit_sum()); - let bb1 = bb1.finish_with_outputs(pred, [load_cst, inw])?; - - let mut bb2 = h.block_builder( - vec![usize_t(), qb_t()].into(), - vec![type_row![]], - vec![qb_t(), usize_t()].into(), - )?; - let [u, q] = bb2.input_wires_arr(); - let pred = bb2.add_load_value(Value::unary_unit_sum()); - let bb2 = bb2.finish_with_outputs(pred, [q, u])?; - - let mut bb3 = h.block_builder( - vec![qb_t(), usize_t()].into(), - vec![type_row![]], - res_t.clone().into(), - )?; - let [q, u] = bb3.input_wires_arr(); - let tst = bb3.add_dataflow_op(tst_op, [q, u])?; - let pred = bb3.add_load_value(Value::unary_unit_sum()); - let bb3 = bb3.finish_with_outputs(pred, tst.outputs())?; - // Now add control-flow edges between basic blocks - h.branch(&bb1, 0, &bb2)?; - h.branch(&bb2, 0, &bb3)?; - h.branch(&bb3, 0, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let res = normalize_cfg(&mut h); - assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - h.validate()?; - assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!( - h.entry_descendants().find(|n| matches!( - h.get_optype(*n), - OpType::DataflowBlock(_) | OpType::CFG(_) | OpType::ExitBlock(_) - )), - None - ); - let tst = find_unique( - h.entry_descendants(), - |n| matches!(h.get_optype(*n), OpType::ExtensionOp(c) if c.def().extension_id() != &PRELUDE_ID), - ); - assert_eq!(h.get_parent(tst), Some(h.entrypoint())); - - let inp = find_unique(h.entry_descendants(), |n| { - matches!(h.get_optype(*n), OpType::Input(_)) - }); - let mut tst_inputs = h.input_neighbours(tst).collect::>(); - tst_inputs.remove(tst_inputs.iter().find_position(|n| **n == inp).unwrap().0); - let [other_input] = tst_inputs.try_into().unwrap(); - assert_eq!( - h.get_optype(other_input), - &(LoadConstant { - datatype: usize_t() - } - .into()) - ); - Ok(()) - } - - fn find_unique(items: impl Iterator, pred: impl Fn(&T) -> bool) -> T { - items.filter(pred).exactly_one().ok().unwrap() - } - - #[rstest] - fn elide_cfg() { - let ext = extension(); - let op = ext.instantiate_extension_op("Test", []).unwrap(); - let out_ty = op.signature().output().clone(); - let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); - let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); - let op_res = entry - .add_dataflow_op(op.clone(), entry.input_wires()) - .unwrap(); - let predicate = entry.add_load_value(Value::unary_unit_sum()); - let entry = entry - .finish_with_outputs(predicate, op_res.outputs()) - .unwrap(); - cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); - - // Add an (unreachable) block before the entry - let Signature { input, output } = op.signature().as_ref().clone(); - for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { - let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); - let inp = extra.input_wires(); - let branch = extra.add_load_value(Value::unary_unit_sum()); - let extra = extra.finish_with_outputs(branch, inp).unwrap(); - cfg.branch(&extra, 0, &dest).unwrap(); - } - - let mut h = cfg.finish_hugr().unwrap(); - - let func = h.children(h.module_root()).exactly_one().ok().unwrap(); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); - let mut dfb_children = child_tags_ext_ids(&h, entry.node()); - - let res = normalize_cfg(&mut h); - h.validate().unwrap(); - assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); - dfb_children.push("UnpackTuple".to_string()); - assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); - } - - #[test] - fn entry_before_loop() -> Result<(), Box> { - /* -> Noop --> Test -> Exit -> Test --> Exit - | | => | | - \<-/ \<-/ - */ - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(qb_t(), usize_t()))?; - let mut nop_b = h.simple_entry_builder(loop_variants.clone(), 1)?; - let n = nop_b.add_dataflow_op(Noop::new(qb_t()), nop_b.input_wires())?; - let br = nop_b.add_load_value(Value::unary_unit_sum()); - let entry = nop_b.finish_with_outputs(br, n.outputs())?; - - let mut loop_b = h.block_builder( - loop_variants.clone(), - [loop_variants, exit_types], - type_row![], - )?; - let [qb] = loop_b.input_wires_arr(); - let usz = loop_b.add_load_value(ConstUsize::new(3)); - let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); - let loop_ = loop_b.finish_with_outputs(tst, [])?; - h.branch(&entry, 0, &loop_)?; - h.branch(&loop_, 0, &loop_)?; - h.branch(&loop_, 1, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - - let res = normalize_cfg(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: false - } - ); - assert_eq!( - h.children(h.entrypoint()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::DataflowBlock, OpTag::BasicBlockExit] - ); - let func = h.get_parent(h.entrypoint()).unwrap(); - let func_children = child_tags_ext_ids(&h, func); - assert_eq!( - func_children.into_iter().sorted().collect_vec(), - ["Cfg", "Dfg", "Input", "Output",] - ); - let [dfg] = h - .children(func) - .filter(|n| h.get_optype(*n).is_dfg()) - .collect_array() - .unwrap(); - assert_eq!( - child_tags_ext_ids(&h, dfg) - .into_iter() - .sorted() - .collect_vec(), - [ - "Const", - "Input", - "LoadConst", - "Noop", - "Output", - "UnpackTuple" - ] - ); - Ok(()) - } - - #[test] - fn loop_before_exit() -> Result<(), Box> { - /* -> Test -> Noop -> Exit -> Test --> Exit (then Noop) - | | => | | - \<-/ \<-/ - */ - let loop_variants: TypeRow = vec![qb_t()].into(); - let exit_types: TypeRow = vec![usize_t()].into(); - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", [])?; - let mut h = CFGBuilder::new(inout_sig(loop_variants.clone(), exit_types.clone()))?; - - let mut loop_b = h.entry_builder(vec![loop_variants, exit_types.clone()], type_row![])?; - let [qb] = loop_b.input_wires_arr(); - let usz = loop_b.add_load_value(ConstUsize::new(3)); - let [tst] = loop_b.add_dataflow_op(tst_op, [qb, usz])?.outputs_arr(); - let loop_ = loop_b.finish_with_outputs(tst, [])?; - h.branch(&loop_, 0, &loop_)?; - - let mut nop_b = h.simple_block_builder(endo_sig(exit_types), 1)?; - let n = nop_b.add_dataflow_op(Noop::new(usize_t()), nop_b.input_wires())?; - let br = nop_b.add_load_value(Value::unary_unit_sum()); - let tail = nop_b.finish_with_outputs(br, n.outputs())?; - - h.branch(&loop_, 1, &tail)?; - h.branch(&tail, 0, &h.exit_block())?; - - let mut h = h.finish_hugr()?; - let res = normalize_cfg(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: false, - exit_changed: true - } - ); - assert_eq!( - h.children(h.entrypoint()) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::DataflowBlock, OpTag::BasicBlockExit] - ); - let func = h.get_parent(h.entrypoint()).unwrap(); - assert_eq!( - h.children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] - ); - - let dfg = h.children(func).last().unwrap(); - let dfg_children = child_tags_ext_ids(&h, dfg); - assert_eq!( - dfg_children.into_iter().sorted().collect_vec(), - [ - "Const", - "Input", - "LoadConst", - "Noop", - "Output", - "UnpackTuple" - ] - ); - - Ok(()) - } - - #[test] - fn nested_cfgs_pass() { - // --> Entry --> Loop --> Tail --> EXIT - // | / \ - // (E->X) \<-/ - let e = extension(); - let tst_op = e.instantiate_extension_op("Test", []).unwrap(); - let qqu = vec![qb_t(), qb_t(), usize_t()]; - let qq = TypeRow::from(vec![qb_t(); 2]); - let mut outer = CFGBuilder::new(inout_sig(qqu.clone(), vec![usize_t(), qb_t()])).unwrap(); - let mut entry = outer.entry_builder(vec![qq.clone()], type_row![]).unwrap(); - let [q1, q2, u] = entry.input_wires_arr(); - let (inner, inner_pred) = { - let mut inner = entry - .cfg_builder([(qb_t(), q1), (qb_t(), q2)], qq.clone()) - .unwrap(); - let mut entry = inner.entry_builder(vec![qq.clone()], type_row![]).unwrap(); - let [q1, q2] = entry.input_wires_arr(); - let [pred] = entry - .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) - .unwrap() - .outputs_arr(); - let entry = entry.finish_with_outputs(pred, []).unwrap(); - inner.branch(&entry, 0, &inner.exit_block()).unwrap(); - (inner.finish_sub_container().unwrap(), pred.node()) - }; - let [q1, q2] = inner.outputs_arr(); - let [entry_pred] = entry - .add_dataflow_op(Tag::new(0, vec![qq.clone()]), [q1, q2]) - .unwrap() - .outputs_arr(); - let entry = entry.finish_with_outputs(entry_pred, []).unwrap(); - - let loop_b = { - let mut loop_b = outer - .block_builder(qq.clone(), [qb_t().into(), usize_t().into()], qb_t().into()) - .unwrap(); - let [q1, q2] = loop_b.input_wires_arr(); - // u here is `dom` edge from entry block - let [pred] = loop_b - .add_dataflow_op(tst_op, [q1, u]) - .unwrap() - .outputs_arr(); - loop_b.finish_with_outputs(pred, [q2]).unwrap() - }; - outer.branch(&entry, 0, &loop_b).unwrap(); - outer.branch(&loop_b, 0, &loop_b).unwrap(); - - let (tail_b, tail_pred) = { - let uq = TypeRow::from(vec![usize_t(), qb_t()]); - let mut tail_b = outer - .block_builder(uq.clone(), vec![uq.clone()], type_row![]) - .unwrap(); - let [u, q] = tail_b.input_wires_arr(); - let [br] = tail_b - .add_dataflow_op(Tag::new(0, vec![uq.clone()]), [u, q]) - .unwrap() - .outputs_arr(); - (tail_b.finish_with_outputs(br, []).unwrap(), br.node()) - }; - outer.branch(&loop_b, 1, &tail_b).unwrap(); - outer.branch(&tail_b, 0, &outer.exit_block()).unwrap(); - let mut h = outer.finish_hugr().unwrap(); - assert_eq!( - h.get_parent(h.get_parent(inner_pred).unwrap()), - Some(inner.node()) - ); - assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); - assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); - - let res = NormalizeCFGPass::default().run(&mut h).unwrap(); - h.validate().unwrap(); - assert_eq!( - res, - HashMap::from([ - (inner.node(), NormalizeCFGResult::CFGToDFG), - ( - h.entrypoint(), - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: true - } - ) - ]) - ); - // Now contains only one CFG with one BB (self-loop) - assert_eq!( - h.nodes() - .filter(|n| h.get_optype(*n).is_cfg()) - .exactly_one() - .ok(), - Some(h.entrypoint()) - ); - let [entry, exit] = h.children(h.entrypoint()).collect_array().unwrap(); - assert_eq!(h.output_neighbours(entry).collect_vec(), [entry, exit]); - // Inner CFG is now a DFG (and still sibling of entry_pred)... - assert_eq!(h.get_parent(inner_pred), Some(inner.node())); - assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); - assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); - // Predicates lifted appropriately... - for n in [entry_pred.node(), tail_pred.node()] { - let parent = h.get_parent(n).unwrap(); - assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); - assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); - } - // ...and followed by UnpackTuple's - for n in [inner_pred, entry_pred.node(), tail_pred.node()] { - let [unpack] = h.output_neighbours(n).collect_array().unwrap(); - assert!( - h.get_optype(unpack) - .as_extension_op() - .and_then(|e| UnpackTuple::from_extension_op(e).ok()) - .is_some() - ); - } - } - - fn child_tags_ext_ids(h: &H, n: H::Node) -> Vec { - h.children(n) - .map(|n| match h.get_optype(n) { - OpType::ExtensionOp(e) => e.unqualified_id().to_string(), - op => format!("{:?}", op.tag()), - }) - .collect() - } -} From 9d617b9269d4d320f1d04360db44981eb9d6e835 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 09:20:58 +0100 Subject: [PATCH 39/49] Rename module, add deprecated mod merge_bbs with just re-export --- hugr-passes/src/lib.rs | 9 ++++++++- hugr-passes/src/{merge_bbs.rs => normalize_cfg.rs} | 8 ++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) rename hugr-passes/src/{merge_bbs.rs => normalize_cfg.rs} (99%) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 6e97f3422e..107daa1d73 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -17,10 +17,17 @@ pub use inline_funcs::inline_acyclic; pub mod linearize_array; pub use linearize_array::LinearizeArrayPass; pub mod lower; -pub mod merge_bbs; +pub mod normalize_cfg; mod monomorphize; pub mod untuple; +/// Merge basic blocks. Subset of [normalize_cfg], use the latter. +#[deprecated(note = "Use normalize_cfg", since="0.15.1")] +pub mod merge_bbs { + #[expect(deprecated)] // remove this + pub use super::normalize_cfg::merge_basic_blocks; +} + pub use monomorphize::{MonomorphizePass, mangle_name, monomorphize}; pub mod replace_types; pub use replace_types::ReplaceTypes; diff --git a/hugr-passes/src/merge_bbs.rs b/hugr-passes/src/normalize_cfg.rs similarity index 99% rename from hugr-passes/src/merge_bbs.rs rename to hugr-passes/src/normalize_cfg.rs index 78ae296c4b..f13e515d60 100644 --- a/hugr-passes/src/merge_bbs.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -1,5 +1,9 @@ -//! Merge BBs along control-flow edges where the source BB has no other successors -//! and the target BB has no other predecessors. +//! CFG normalizations. +//! +//! * Merge BBs along control-flow edges where the source BB has no other successors +//! and the target BB has no other predecessors. +//! * Move entry/last-before-exit blocks outside of CFG when possible. +//! * Convert whole CFG to DFG if straight-line control-flow use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; From ec0e2f655a9ed412c27a8c238aec6639aa1e8983 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 09:23:32 +0100 Subject: [PATCH 40/49] fmt --- hugr-passes/src/lib.rs | 4 ++-- hugr-passes/src/normalize_cfg.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 107daa1d73..6ab2b2889c 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -17,12 +17,12 @@ pub use inline_funcs::inline_acyclic; pub mod linearize_array; pub use linearize_array::LinearizeArrayPass; pub mod lower; -pub mod normalize_cfg; mod monomorphize; +pub mod normalize_cfg; pub mod untuple; /// Merge basic blocks. Subset of [normalize_cfg], use the latter. -#[deprecated(note = "Use normalize_cfg", since="0.15.1")] +#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] pub mod merge_bbs { #[expect(deprecated)] // remove this pub use super::normalize_cfg::merge_basic_blocks; diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index f13e515d60..2b63781162 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -1,5 +1,5 @@ //! CFG normalizations. -//! +//! //! * Merge BBs along control-flow edges where the source BB has no other successors //! and the target BB has no other predecessors. //! * Move entry/last-before-exit blocks outside of CFG when possible. From dedd861ea2483cdf65e45850e4a8b048cadad11f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 10:17:33 +0100 Subject: [PATCH 41/49] common up optype_mut's --- hugr-passes/src/normalize_cfg.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index 2b63781162..60a2ceaeef 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -162,7 +162,12 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(h: &'a mut H, n: H::Node) -> &'a mut CFG { + match h.optype_mut(n) { + OpType::CFG(cfg) => cfg, + _ => unreachable!(), // Checked at entry to normalize_cfg + } + }; // Further normalizations with effects outside the CFG let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); @@ -194,12 +199,9 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(mut h: &mut H) -> Result(mut h: &mut H) -> Result Date: Tue, 30 Sep 2025 12:07:43 +0100 Subject: [PATCH 42/49] check Entry has no preds even if succ == exit; remove test --- hugr-passes/src/normalize_cfg.rs | 129 +++++++++++-------------------- 1 file changed, 44 insertions(+), 85 deletions(-) diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index 60a2ceaeef..4afc20f171 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -167,21 +167,24 @@ pub fn normalize_cfg(mut h: &mut H) -> Result cfg, _ => unreachable!(), // Checked at entry to normalize_cfg } - }; + } // Further normalizations with effects outside the CFG let [entry, exit] = h.children(cfg_node).take(2).collect_array().unwrap(); let entry_blk = h.get_optype(entry).as_dataflow_block().unwrap(); let cfg_parent = h.get_parent(cfg_node).unwrap(); - // 1. If the entry block has only one successor, and no predecessors, then move contents - // outside/before CFG. - // (Note if the entry block has multiple successors, but no predecessors, we *could* move - // its contents outside/before the CFG, but would need to keep an empty/identity entry - // block - we do not do this here.) + // 1. If the entry block has no predecessors, then we can move contents outside/before the CFG. + // However, we only do this if the Entry block has just one successor (i.e. we can remove + // the entry block altogether) - an extension would be to do this in other cases, preserving + // the Entry block as an empty branch. let mut entry_changed = false; - #[allow(clippy::match_result_ok)] // let Ok(...) without .ok() borrows `h` - if let Some(succ) = h.output_neighbours(entry).exactly_one().ok() { + if let Some(succ) = h + .output_neighbours(entry) + .exactly_one() + .ok() + .filter(|_| h.input_neighbours(entry).next().is_none()) + { if succ == exit { - // Any (basic-block) predecessors of `entry` are unreachable, so allow/ignore + // 1a. Turn the CFG into a DFG containing only what was in the entry block assert_eq!( &Signature::new( entry_blk.inputs.clone(), @@ -189,7 +192,6 @@ pub fn normalize_cfg(mut h: &mut H) -> Result = h.children(cfg_node).skip(1).collect(); for blk in children_to_remove { @@ -204,45 +206,43 @@ pub fn normalize_cfg(mut h: &mut H) -> Result>() { - for src in h.linked_outputs(cfg_node, inp).collect::>() { - h.connect(src.0, src.1, dfg, inp.index()); - } - h.disconnect(cfg_node, inp); + // Inputs to CFG go directly to DFG + for inp in h.node_inputs(cfg_node).collect::>() { + for src in h.linked_outputs(cfg_node, inp).collect::>() { + h.connect(src.0, src.1, dfg, inp.index()); } + h.disconnect(cfg_node, inp); + } - // Update input ports - let cfg_ty = cfg_ty_mut(h, cfg_node); - let inputs_to_add = - new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; - cfg_ty.signature.input = new_cfg_inputs; - h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); + // Update input ports + let cfg_ty = cfg_ty_mut(h, cfg_node); + let inputs_to_add = new_cfg_inputs.len() as isize - cfg_ty.signature.input.len() as isize; + cfg_ty.signature.input = new_cfg_inputs; + h.add_ports(cfg_node, Direction::Incoming, inputs_to_add); - // Wire outputs of DFG directly to CFG - for src in h.node_outputs(dfg).collect::>() { - h.connect(dfg, src, cfg_node, src.index()); - } - entry_changed = true; + // Wire outputs of DFG directly to CFG + for src in h.node_outputs(dfg).collect::>() { + h.connect(dfg, src, cfg_node, src.index()); } + entry_changed = true; } // 2. If the exit node has a single predecessor and that predecessor has no other successors... let mut exit_changed = false; @@ -506,7 +506,7 @@ mod test { CFGBuilder, Dataflow, HugrBuilder, SubContainer, endo_sig, inout_sig, }; use hugr_core::extension::prelude::{ConstUsize, Noop, PRELUDE_ID, UnpackTuple, qb_t, usize_t}; - use hugr_core::ops::{DataflowOpTrait, LoadConstant, OpTag, OpTrait, OpType, Tag}; + use hugr_core::ops::{LoadConstant, OpTag, OpTrait, OpType, Tag}; use hugr_core::ops::{constant::Value, handle::NodeHandle}; use hugr_core::types::{Signature, Type, TypeRow}; use hugr_core::{Extension, HugrView, const_extension_ids, type_row}; @@ -720,47 +720,6 @@ mod test { items.filter(pred).exactly_one().ok().unwrap() } - #[rstest] - fn elide_cfg() { - let ext = extension(); - let op = ext.instantiate_extension_op("Test", []).unwrap(); - let out_ty = op.signature().output().clone(); - let mut cfg = CFGBuilder::new(op.signature().into_owned()).unwrap(); - let mut entry = cfg.simple_entry_builder(out_ty, 1).unwrap(); - let op_res = entry - .add_dataflow_op(op.clone(), entry.input_wires()) - .unwrap(); - let predicate = entry.add_load_value(Value::unary_unit_sum()); - let entry = entry - .finish_with_outputs(predicate, op_res.outputs()) - .unwrap(); - cfg.branch(&entry, 0, &cfg.exit_block()).unwrap(); - - // Add an (unreachable) block before the entry - let Signature { input, output } = op.signature().as_ref().clone(); - for (ty, dest) in [(input, entry), (output, cfg.exit_block())] { - let mut extra = cfg.simple_block_builder(endo_sig(ty), 1).unwrap(); - let inp = extra.input_wires(); - let branch = extra.add_load_value(Value::unary_unit_sum()); - let extra = extra.finish_with_outputs(branch, inp).unwrap(); - cfg.branch(&extra, 0, &dest).unwrap(); - } - - let mut h = cfg.finish_hugr().unwrap(); - - let func = h.children(h.module_root()).exactly_one().ok().unwrap(); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Cfg"]); - let mut dfb_children = child_tags_ext_ids(&h, entry.node()); - - let res = normalize_cfg(&mut h); - h.validate().unwrap(); - assert_eq!(res, Ok(NormalizeCFGResult::CFGToDFG)); - assert_eq!(h.entrypoint_optype().tag(), OpTag::Dfg); - assert_eq!(child_tags_ext_ids(&h, func), ["Input", "Output", "Dfg"]); - dfb_children.push("UnpackTuple".to_string()); - assert_eq!(child_tags_ext_ids(&h, h.entrypoint()), dfb_children); - } - #[test] fn entry_before_loop() -> Result<(), Box> { /* -> Noop --> Test -> Exit -> Test --> Exit From 476d27647996991e32fa5b08bbc71c5b4a80caec Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 12:28:26 +0100 Subject: [PATCH 43/49] clippy: lifetimes needless after all? --- hugr-passes/src/normalize_cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index 4afc20f171..68797aa76d 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -162,7 +162,7 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(h: &'a mut H, n: H::Node) -> &'a mut CFG { + fn cfg_ty_mut(h: &mut H, n: H::Node) -> &mut CFG { match h.optype_mut(n) { OpType::CFG(cfg) => cfg, _ => unreachable!(), // Checked at entry to normalize_cfg From 4006171089875b32f1c80191d151cd9974006fca Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 12:51:20 +0100 Subject: [PATCH 44/49] fixup! check Entry --- hugr-passes/src/normalize_cfg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index 68797aa76d..4af0bd4f48 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -123,8 +123,8 @@ impl ComposablePass for NormalizeCFGPass { .entry_descendants() .filter(|n| hugr.get_optype(*n).is_cfg()) .collect::>(); - // Process inner CFGs first, in case they are removed - // (if they are in an unreachable block when the Entry node has only the Exit as successor) + // Process inner CFGs first, in case they are removed (if they are in a completely + // disconnected block when the Entry node has only the Exit as successor). v.reverse(); v } else { From d92aa2502bd143d71aeff6c24ed69b5188e72327 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 12:51:34 +0100 Subject: [PATCH 45/49] Report preserved exit_dfg/entry_dfg --- hugr-passes/src/normalize_cfg.rs | 111 ++++++++++++++++--------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfg.rs index 4af0bd4f48..5ab0fc061c 100644 --- a/hugr-passes/src/normalize_cfg.rs +++ b/hugr-passes/src/normalize_cfg.rs @@ -74,18 +74,21 @@ pub enum NormalizeCFGError { } /// Result from [normalize_cfg], i.e. a report of what changes were made to the Hugr. +/// +/// `N` is the type of the nodes in the Hugr. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum NormalizeCFGResult { +pub enum NormalizeCFGResult { /// The entire [`CFG`] was converted into a [`DFG`]. /// /// The entrypoint node id is preserved after the conversion, but now identifies /// the new [`DFG`]. CFGToDFG, /// The CFG was preserved, but the entry or exit blocks may have changed. - #[allow(missing_docs)] CFGPreserved { - entry_changed: bool, - exit_changed: bool, + /// If `Some`, the new [DFG] containing what was previously in the entry block + entry_dfg: Option, + /// If `Some`, the new [DFG] containing what was previously in the last block before the exit + exit_dfg: Option, }, } @@ -115,7 +118,7 @@ impl ComposablePass for NormalizeCFGPass { type Error = NormalizeCFGError; /// For each CFG node that was normalized, the [NormalizeCFGResult] for that CFG - type Result = HashMap; + type Result = HashMap>; fn run(&self, hugr: &mut H) -> Result { let cfgs = if self.cfgs.is_empty() { @@ -154,7 +157,9 @@ impl ComposablePass for NormalizeCFGPass { /// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG #[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter -pub fn normalize_cfg(mut h: &mut H) -> Result { +pub fn normalize_cfg( + mut h: &mut H, +) -> Result, NormalizeCFGError> { let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) .map_err(|e| match e { HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), @@ -176,7 +181,7 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(mut h: &mut H) -> Result>() { h.connect(dfg, src, cfg_node, src.index()); } - entry_changed = true; + entry_dfg = Some(dfg); } // 2. If the exit node has a single predecessor and that predecessor has no other successors... - let mut exit_changed = false; + let mut exit_dfg = None; if let Some(pred) = h .input_neighbours(exit) .exactly_one() @@ -299,11 +304,11 @@ pub fn normalize_cfg(mut h: &mut H) -> Result(h: &mut H, output_node: H::Node, new_types: #[cfg(test)] #[expect(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { - use std::collections::{HashMap, HashSet}; + use std::collections::HashSet; use std::sync::Arc; use hugr_core::extension::simple_op::MakeExtensionOp; @@ -753,13 +758,13 @@ mod test { let res = normalize_cfg(&mut h).unwrap(); h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: false - } - ); + let NormalizeCFGResult::CFGPreserved { + entry_dfg: Some(dfg), + exit_dfg: None, + } = res + else { + panic!("Unexpected result"); + }; assert_eq!( h.children(h.entrypoint()) .map(|n| h.get_optype(n).tag()) @@ -772,11 +777,12 @@ mod test { func_children.into_iter().sorted().collect_vec(), ["Cfg", "Dfg", "Input", "Output",] ); - let [dfg] = h - .children(func) - .filter(|n| h.get_optype(*n).is_dfg()) - .collect_array() - .unwrap(); + assert_eq!( + h.children(func) + .filter(|n| h.get_optype(*n).is_dfg()) + .collect_vec(), + [dfg] + ); assert_eq!( child_tags_ext_ids(&h, dfg) .into_iter() @@ -824,13 +830,13 @@ mod test { let mut h = h.finish_hugr()?; let res = normalize_cfg(&mut h).unwrap(); h.validate().unwrap(); - assert_eq!( - res, - NormalizeCFGResult::CFGPreserved { - entry_changed: false, - exit_changed: true - } - ); + let NormalizeCFGResult::CFGPreserved { + entry_dfg: None, + exit_dfg: Some(dfg), + } = res + else { + panic!("Unexpected result"); + }; assert_eq!( h.children(h.entrypoint()) .map(|n| h.get_optype(n).tag()) @@ -839,16 +845,16 @@ mod test { ); let func = h.get_parent(h.entrypoint()).unwrap(); assert_eq!( - h.children(func) - .map(|n| h.get_optype(n).tag()) - .collect_vec(), - [OpTag::Input, OpTag::Output, OpTag::Cfg, OpTag::Dfg] + child_tags_ext_ids(&h, func), + ["Input", "Output", "Cfg", "Dfg"] ); - let dfg = h.children(func).last().unwrap(); - let dfg_children = child_tags_ext_ids(&h, dfg); + assert_eq!(h.children(func).last(), Some(dfg)); assert_eq!( - dfg_children.into_iter().sorted().collect_vec(), + child_tags_ext_ids(&h, dfg) + .into_iter() + .sorted() + .collect_vec(), [ "Const", "Input", @@ -932,21 +938,20 @@ mod test { assert_eq!(h.get_parent(entry_pred.node()), Some(entry.node())); assert_eq!(h.get_parent(tail_pred.node()), Some(tail_b.node())); - let res = NormalizeCFGPass::default().run(&mut h).unwrap(); + let mut res = NormalizeCFGPass::default().run(&mut h).unwrap(); h.validate().unwrap(); assert_eq!( - res, - HashMap::from([ - (inner.node(), NormalizeCFGResult::CFGToDFG), - ( - h.entrypoint(), - NormalizeCFGResult::CFGPreserved { - entry_changed: true, - exit_changed: true - } - ) - ]) + res.remove(&inner.node()), + Some(NormalizeCFGResult::CFGToDFG) ); + let Some(NormalizeCFGResult::CFGPreserved { + entry_dfg: Some(entry_dfg), + exit_dfg: Some(tail_dfg), + }) = res.remove(&h.entrypoint()) + else { + panic!("Unexpected result") + }; + assert!(res.is_empty()); // Now contains only one CFG with one BB (self-loop) assert_eq!( h.nodes() @@ -962,8 +967,8 @@ mod test { assert_eq!(h.get_optype(inner.node()).tag(), OpTag::Dfg); assert_eq!(h.get_parent(inner.node()), h.get_parent(entry_pred.node())); // Predicates lifted appropriately... - for n in [entry_pred.node(), tail_pred.node()] { - let parent = h.get_parent(n).unwrap(); + for (n, parent) in [(entry_pred.node(), entry_dfg), (tail_pred.node(), tail_dfg)] { + assert_eq!(h.get_parent(n), Some(parent)); assert_eq!(h.get_optype(parent).tag(), OpTag::Dfg); assert_eq!(h.get_parent(parent), h.get_parent(h.entrypoint())); } From e8582f6238ec7f561ba4e9bf3d99250b8e416c51 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 13:19:27 +0100 Subject: [PATCH 46/49] rename module normalize_cfg{=>s} --- hugr-passes/src/lib.rs | 8 ++++---- hugr-passes/src/{normalize_cfg.rs => normalize_cfgs.rs} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename hugr-passes/src/{normalize_cfg.rs => normalize_cfgs.rs} (100%) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 6ab2b2889c..57def8778a 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -18,14 +18,14 @@ pub mod linearize_array; pub use linearize_array::LinearizeArrayPass; pub mod lower; mod monomorphize; -pub mod normalize_cfg; +pub mod normalize_cfgs; pub mod untuple; -/// Merge basic blocks. Subset of [normalize_cfg], use the latter. -#[deprecated(note = "Use normalize_cfg", since = "0.15.1")] +/// Merge basic blocks. Subset of [normalize_cfgs], use the latter. +#[deprecated(note = "Use normalize_cfgs", since = "0.15.1")] pub mod merge_bbs { #[expect(deprecated)] // remove this - pub use super::normalize_cfg::merge_basic_blocks; + pub use super::normalize_cfgs::merge_basic_blocks; } pub use monomorphize::{MonomorphizePass, mangle_name, monomorphize}; diff --git a/hugr-passes/src/normalize_cfg.rs b/hugr-passes/src/normalize_cfgs.rs similarity index 100% rename from hugr-passes/src/normalize_cfg.rs rename to hugr-passes/src/normalize_cfgs.rs From c41411cd994481c9f1619defb1293977c0444cb7 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 13:20:21 +0100 Subject: [PATCH 47/49] report num_merged by separating merge_basic_blocks --- hugr-passes/src/lib.rs | 21 ++++++++++-- hugr-passes/src/normalize_cfgs.rs | 56 +++++++++++++++---------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 57def8778a..e8b6aa865f 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -24,8 +24,25 @@ pub mod untuple; /// Merge basic blocks. Subset of [normalize_cfgs], use the latter. #[deprecated(note = "Use normalize_cfgs", since = "0.15.1")] pub mod merge_bbs { - #[expect(deprecated)] // remove this - pub use super::normalize_cfgs::merge_basic_blocks; + use hugr_core::hugr::{hugrmut::HugrMut, views::RootCheckable}; + use hugr_core::ops::handle::CfgID; + + /// Merge any basic blocks that are direct children of the specified CFG + /// i.e. where a basic block B has a single successor B' whose only predecessor + /// is B, B and B' can be combined. + /// + /// # Panics + /// + /// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] + /// + /// [OpType::CFG]: hugr_core::ops::OpType::CFG + #[deprecated(note = "Use version in normalize_cfgs", since = "0.15.1")] + pub fn merge_basic_blocks<'h, H: 'h + HugrMut>( + cfg: impl RootCheckable<&'h mut H, CfgID>, + ) { + let checked = cfg.try_into_checked().expect("Hugr must be a CFG region"); + super::normalize_cfgs::merge_basic_blocks(checked.into_hugr()).unwrap(); + } } pub use monomorphize::{MonomorphizePass, mangle_name, monomorphize}; diff --git a/hugr-passes/src/normalize_cfgs.rs b/hugr-passes/src/normalize_cfgs.rs index 5ab0fc061c..5bc26702c2 100644 --- a/hugr-passes/src/normalize_cfgs.rs +++ b/hugr-passes/src/normalize_cfgs.rs @@ -7,38 +7,33 @@ use std::collections::HashMap; use hugr_core::extension::prelude::UnpackTuple; -use hugr_core::hugr::views::{RootCheckable, RootChecked}; -use hugr_core::hugr::{HugrError, hugrmut::HugrMut}; +use hugr_core::hugr::hugrmut::HugrMut; use hugr_core::types::{EdgeKind, Signature, TypeRow}; use itertools::Itertools; use hugr_core::hugr::patch::inline_dfg::InlineDFG; use hugr_core::hugr::patch::replace::{NewEdgeKind, NewEdgeSpec, Replacement}; -use hugr_core::ops::handle::CfgID; use hugr_core::ops::{ - CFG, DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpType, Output, + CFG, DFG, DataflowBlock, DataflowParent, ExitBlock, Input, OpTag, OpTrait, OpType, Output, }; use hugr_core::{Direction, Hugr, HugrView, Node, OutgoingPort, PortIndex}; use crate::ComposablePass; -/// Merge any basic blocks that are direct children of the specified CFG -/// i.e. where a basic block B has a single successor B' whose only predecessor -/// is B, B and B' can be combined. +/// Merge any basic blocks that are direct children of the specified [`CFG`]-entrypoint +/// Hugr. /// -/// # Panics +/// That is, where a basic block B has a single successor B' whose only predecessor +/// is B, B and B' can be combined. /// -/// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] +/// Returns the number of merged blocks. /// -/// [OpType::CFG]: hugr_core::ops::OpType::CFG -#[deprecated(note = "Use normalize_cfg")] // Note: as a first step, just hide this -pub fn merge_basic_blocks<'h, H>(cfg: impl RootCheckable<&'h mut H, CfgID>) -where - H: 'h + HugrMut, -{ - let checked = cfg.try_into_checked().expect("Hugr must be a CFG region"); - let cfg = checked.into_hugr(); - +/// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG +pub fn merge_basic_blocks(cfg: &mut H) -> Result { + if !cfg.entrypoint_optype().is_cfg() { + return Err(NormalizeCFGError::NotCFG(cfg.entrypoint_optype().tag())); + } + let mut num_merged = 0; let mut worklist = cfg.children(cfg.entrypoint()).collect::>(); while let Some(n) = worklist.pop() { // Consider merging n with its successor @@ -54,6 +49,7 @@ where // - the exit block, nodes in n should move *outside* the CFG - a separate pass. continue; } + num_merged += 1; let (rep, merge_bb, dfgs) = mk_rep(cfg, n, succ); let node_map = cfg.apply_patch(rep).unwrap(); let merged_bb = *node_map.get(&merge_bb).unwrap(); @@ -63,6 +59,7 @@ where } worklist.push(merged_bb); } + Ok(num_merged) } /// Errors from [normalize_cfg] @@ -87,8 +84,11 @@ pub enum NormalizeCFGResult { CFGPreserved { /// If `Some`, the new [DFG] containing what was previously in the entry block entry_dfg: Option, - /// If `Some`, the new [DFG] containing what was previously in the last block before the exit + /// If `Some`, the new [DFG] of what was previously in the last block before the exit exit_dfg: Option, + /// The number of basic blocks merged together. + /// Does not include any lifted to become DFGs outside. + num_merged: usize, }, } @@ -156,16 +156,10 @@ impl ComposablePass for NormalizeCFGPass { /// # Errors /// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG -#[expect(deprecated)] // inline/combine/refactor with merge_bbs, or just hide latter pub fn normalize_cfg( - mut h: &mut H, + h: &mut H, ) -> Result, NormalizeCFGError> { - let checked: RootChecked<_, CfgID> = RootChecked::<_, CfgID>::try_new(&mut h) - .map_err(|e| match e { - HugrError::InvalidTag { actual, .. } => NormalizeCFGError::NotCFG(actual), - _ => unreachable!(), - })?; - merge_basic_blocks(checked); + let num_merged = merge_basic_blocks(h)?; let cfg_node = h.entrypoint(); fn cfg_ty_mut(h: &mut H, n: H::Node) -> &mut CFG { match h.optype_mut(n) { @@ -309,6 +303,7 @@ pub fn normalize_cfg( Ok(NormalizeCFGResult::CFGPreserved { entry_dfg, exit_dfg, + num_merged, }) } @@ -498,7 +493,6 @@ fn unpack_before_output(h: &mut H, output_node: H::Node, new_types: } #[cfg(test)] -#[expect(deprecated)] // remove tests of merge_bbs, or just hide the latter mod test { use std::collections::HashSet; use std::sync::Arc; @@ -594,7 +588,8 @@ mod test { let mut h = h.finish_hugr()?; let r = h.entrypoint(); - merge_basic_blocks(&mut h); + let num_merged = merge_basic_blocks(&mut h)?; + assert_eq!(num_merged, 1); h.validate().unwrap(); assert_eq!(r, h.entrypoint()); assert!(matches!(h.get_optype(r), OpType::CFG(_))); @@ -761,6 +756,7 @@ mod test { let NormalizeCFGResult::CFGPreserved { entry_dfg: Some(dfg), exit_dfg: None, + num_merged: 0, } = res else { panic!("Unexpected result"); @@ -833,6 +829,7 @@ mod test { let NormalizeCFGResult::CFGPreserved { entry_dfg: None, exit_dfg: Some(dfg), + num_merged: 0, } = res else { panic!("Unexpected result"); @@ -947,6 +944,7 @@ mod test { let Some(NormalizeCFGResult::CFGPreserved { entry_dfg: Some(entry_dfg), exit_dfg: Some(tail_dfg), + num_merged: 0, }) = res.remove(&h.entrypoint()) else { panic!("Unexpected result") From 2f92e90169287bbcb821f1fcb4eea4f833bbeed0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 13:35:15 +0100 Subject: [PATCH 48/49] docs --- hugr-passes/src/lib.rs | 2 +- hugr-passes/src/normalize_cfgs.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index e8b6aa865f..bb101511ac 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -33,7 +33,7 @@ pub mod merge_bbs { /// /// # Panics /// - /// If the [HugrView::entrypoint] of `cfg` is not an [OpType::CFG] + /// If the `entrypoint` of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG #[deprecated(note = "Use version in normalize_cfgs", since = "0.15.1")] diff --git a/hugr-passes/src/normalize_cfgs.rs b/hugr-passes/src/normalize_cfgs.rs index 5bc26702c2..7599f6928b 100644 --- a/hugr-passes/src/normalize_cfgs.rs +++ b/hugr-passes/src/normalize_cfgs.rs @@ -28,6 +28,8 @@ use crate::ComposablePass; /// /// Returns the number of merged blocks. /// +/// # Errors +/// /// [NormalizeCFGError::NotCFG] If the entrypoint is not a CFG pub fn merge_basic_blocks(cfg: &mut H) -> Result { if !cfg.entrypoint_optype().is_cfg() { @@ -146,12 +148,11 @@ impl ComposablePass for NormalizeCFGPass { /// * Merge consecutive basic blocks i.e. where a BB has only a single successor which /// has no predecessors /// * If the entry block has only one successor, and no predecessors, then move its contents -/// outside/before CFG. -/// * (Similarly) if the exit block has only one predecessor, then move contents -/// outside/after CFG. -/// * If that predecessor is the entry block, then convert the CFG to a DFG. +/// into a DFG outside/before CFG. +/// * If that successor is the exit block, then convert the entire CFG to a DFG. /// This will be reported via [NormalizeCFGResult::CFGToDFG] -/// +/// * (Similarly) if the exit block has only one predecessor, then move contents into a DFG +/// outside/after CFG. /// /// # Errors /// From 34491eb00dee044500cbd08e67dc7caf79f44949 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 30 Sep 2025 14:25:58 +0100 Subject: [PATCH 49/49] Ooops, version number --- hugr-passes/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index bb101511ac..2a0cbb01a5 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -22,7 +22,7 @@ pub mod normalize_cfgs; pub mod untuple; /// Merge basic blocks. Subset of [normalize_cfgs], use the latter. -#[deprecated(note = "Use normalize_cfgs", since = "0.15.1")] +#[deprecated(note = "Use normalize_cfgs", since = "0.23.0")] pub mod merge_bbs { use hugr_core::hugr::{hugrmut::HugrMut, views::RootCheckable}; use hugr_core::ops::handle::CfgID; @@ -36,7 +36,7 @@ pub mod merge_bbs { /// If the `entrypoint` of `cfg` is not an [OpType::CFG] /// /// [OpType::CFG]: hugr_core::ops::OpType::CFG - #[deprecated(note = "Use version in normalize_cfgs", since = "0.15.1")] + #[deprecated(note = "Use version in normalize_cfgs", since = "0.23.0")] pub fn merge_basic_blocks<'h, H: 'h + HugrMut>( cfg: impl RootCheckable<&'h mut H, CfgID>, ) {