Skip to content
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1f05f19
feat: Define pass application scopes
aborgna-q Dec 19, 2025
aba6477
Stub implementation for all passes
aborgna-q Dec 19, 2025
3e0c6ac
Python definition for PassScope
aborgna-q Dec 19, 2025
02d2886
Add default with_scope impl, make this non-breaking
aborgna-q Dec 22, 2025
e3a63b4
Move scope to a submodule of ::composable
aborgna-q Dec 22, 2025
0b730bf
wip
acl-cqc Jan 23, 2026
e71125c
All=>Preserve (make global), add PreserveEntrypoint, enum InScope
acl-cqc Jan 23, 2026
11a643e
docs, re-export InScope
acl-cqc Jan 23, 2026
62bd044
(experimental) Add Scoped...breaking
acl-cqc Jan 23, 2026
ce4c551
Revert "(experimental) Add Scoped...breaking"
acl-cqc Jan 23, 2026
293ef0f
Update Dead Function Removal
acl-cqc Jan 19, 2026
075e2b6
Update UntuplePass
acl-cqc Jan 23, 2026
236c08c
ReplaceTypes....ok if we don't respect PassScope::preserve_interface …
acl-cqc Feb 4, 2026
b16b72a
feat: normalize_cfgs (use Either for explicit CFG list)
acl-cqc Feb 11, 2026
84b33c7
feat: add PassScope to DeadCodeElim (alongside entry_points)
acl-cqc Feb 11, 2026
d7eca10
feat: ConstFold (add Scope alongside inputs)
acl-cqc Feb 11, 2026
51bfb59
Add PassScope::from_entrypoint
acl-cqc Feb 11, 2026
3e6997c
clippy
acl-cqc Feb 11, 2026
b9cac77
docs, and deprecation note
acl-cqc Feb 11, 2026
f22c27e
DCE: Skip first descendant
acl-cqc Feb 11, 2026
81f2cac
ConstFold/DCE store Option<PassScope> allowing old entrypoint-subtree…
acl-cqc Feb 11, 2026
c1a1898
note idempotency in composablepass/::then
acl-cqc Feb 18, 2026
90172c5
redefine, make tests pass
acl-cqc Feb 19, 2026
d678210
test renaming
acl-cqc Feb 19, 2026
d6eba57
docs, re-export
acl-cqc Feb 19, 2026
3295c8e
Merge remote-tracking branch 'origin/main' into ab/pass-scopes
acl-cqc Feb 20, 2026
f98093d
Various doc improvements, and module root is PreserveInterface
acl-cqc Feb 20, 2026
14c5b9b
reimplement in python (no tests)
acl-cqc Feb 21, 2026
a3e07c9
PassScopeBase has regions/in_scope meths+dedup docs, metaclass fun
acl-cqc Feb 22, 2026
540dab7
scope.rs: unless it is set to --> unless the entrypoint is
acl-cqc Feb 26, 2026
a6ebb96
doc re. EntrypointFlat
acl-cqc Feb 26, 2026
85a094d
with_scope takes impl Into<PassScope>
acl-cqc Feb 26, 2026
834edd9
update python docs to mirror Rust
acl-cqc Feb 26, 2026
22e951a
fmt-py
acl-cqc Feb 26, 2026
6a4332d
python: don't preserve all non-function module-children
acl-cqc Feb 26, 2026
968c12e
Remove note to reviewers
acl-cqc Feb 26, 2026
332bc84
remove from_entrypoint, for next PR
acl-cqc Feb 26, 2026
1c38f50
python tests, extend builder to allow setting visibility
acl-cqc Feb 26, 2026
6b05449
test_hug -> test_hugr
acl-cqc Feb 27, 2026
db7d78f
Merge remote-tracking branch 'origin/main' into ab/pass-scopes
acl-cqc Feb 27, 2026
756b310
Revert "remove from_entrypoint, for next PR"
acl-cqc Feb 27, 2026
470a970
Merge branch 'ab/pass-scopes' into acl/pass-scopes (non-compiling)
acl-cqc Feb 27, 2026
134784a
Merge remote-tracking branch 'origin/main' into acl/pass-scopes
acl-cqc Mar 3, 2026
b9e6bfb
pass updates...revert behaviour change to constant_fold_pass + deprecate
acl-cqc Mar 3, 2026
7180479
const_fold simplify scope_entrypoints, skip module_root; DCE preserve…
acl-cqc Mar 5, 2026
75a2d14
Remove PassScope::from_entrypoint, not used.
acl-cqc Mar 5, 2026
3c152af
dead_code: no need to skip first descendant now we are marking parents
acl-cqc Mar 5, 2026
17e878c
skip redundant chain(root)
acl-cqc Mar 5, 2026
89a5a17
Specify since="0.25.7" on deprecations
acl-cqc Mar 6, 2026
3dd5187
fmt those deprecated's
acl-cqc Mar 6, 2026
1866af0
remove_dead_funcs_scoped
acl-cqc Mar 6, 2026
4bb84f8
Deprecate monomorphize; add run_validating inside test
acl-cqc Mar 6, 2026
7a03521
extend dead_code comment
acl-cqc Mar 6, 2026
1e2b9d5
Comments re. ReplaceTypes
acl-cqc Mar 6, 2026
e3c9eee
Avoid calling monomorphize;extend/expect deprecation
acl-cqc Mar 6, 2026
c8c6e76
Ooops, fix semicolon + fmt
acl-cqc Mar 6, 2026
82cfd0e
Remove UntuplePass::new_scoped
acl-cqc Mar 6, 2026
79641a5
update docs with_scope*_internal*
acl-cqc Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions hugr-passes/src/composable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ impl<
}
}

// Note remove when deprecated constant_fold_pass / remove_dead_funcs are removed
pub(crate) fn validate_if_test<P: ComposablePass<H>, H: HugrMut>(
pass: P,
hugr: &mut H,
Expand All @@ -319,7 +320,7 @@ pub(crate) fn validate_if_test<P: ComposablePass<H>, H: HugrMut>(
}

#[cfg(test)]
mod test {
pub(crate) mod test {
use hugr_core::ops::Value;
use itertools::{Either, Itertools};

Expand All @@ -335,11 +336,18 @@ mod test {

use crate::const_fold::{ConstFoldError, ConstantFoldPass};
use crate::dead_code::DeadCodeElimError;
use crate::untuple::{UntupleRecursive, UntupleResult};
use crate::{DeadCodeElimPass, ReplaceTypes, UntuplePass};
use crate::untuple::UntupleResult;
use crate::{DeadCodeElimPass, PassScope, ReplaceTypes, UntuplePass};

use super::{ComposablePass, IfThen, ValidatePassError, ValidatingPass, validate_if_test};

pub(crate) fn run_validating<P: ComposablePass<H>, H: HugrMut>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, this could be outside test, even public; it's a simple-enough utility function

pass: P,
hugr: &mut H,
) -> Result<P::Result, ValidatePassError<H::Node, P::Error>> {
ValidatingPass::new(pass).run(hugr)
}

#[test]
fn test_then() {
let mut mb = ModuleBuilder::new();
Expand Down Expand Up @@ -444,7 +452,7 @@ mod test {
fb.finish_hugr_with_outputs(untup.outputs()).unwrap()
};

let untup = UntuplePass::new(UntupleRecursive::Recursive);
let untup = UntuplePass::new_scoped(PassScope::EntrypointRecursive);
{
// Change usize_t to INT_TYPES[6], and if that did anything (it will!), then Untuple
let mut repl = ReplaceTypes::default();
Expand All @@ -453,7 +461,7 @@ mod test {
let ifthen = IfThen::<Either<_, _>, _, _, _>::new(repl, untup.clone());

let mut h = h.clone();
let r = validate_if_test(ifthen, &mut h).unwrap();
let r = run_validating(ifthen, &mut h).unwrap();
assert_eq!(
r,
Some(UntupleResult {
Expand All @@ -470,7 +478,7 @@ mod test {
repl.set_replace_type(i32_custom_t, INT_TYPES[6].clone());
let ifthen = IfThen::<Either<_, _>, _, _, _>::new(repl, untup);
let mut h = h;
let r = validate_if_test(ifthen, &mut h).unwrap();
let r = run_validating(ifthen, &mut h).unwrap();
assert_eq!(r, None);
assert_eq!(h.children(h.entrypoint()).count(), 4);
let mktup = h
Expand Down
46 changes: 37 additions & 9 deletions hugr-passes/src/const_fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ use crate::dataflow::{
partial_from_const,
};
use crate::dead_code::{DeadCodeElimError, DeadCodeElimPass, PreserveNode};
use crate::{ComposablePass, composable::validate_if_test};
use crate::{ComposablePass, PassScope, composable::validate_if_test};

#[derive(Debug, Clone, Default)]
/// A configuration for the Constant Folding pass.
pub struct ConstantFoldPass {
allow_increase_termination: bool,
scope: Option<PassScope>,
/// Each outer key Node must be either:
/// - a `FuncDefn` child of the root, if the root is a module; or
/// - the root, if the root is not a Module
/// - the entrypoint, if the entrypoint is not a Module
inputs: HashMap<Node, HashMap<IncomingPort, Value>>,
}

Expand Down Expand Up @@ -67,8 +68,8 @@ impl ConstantFoldPass {
}

/// Specifies a number of external inputs to an entry point of the Hugr.
/// In normal use, for Module-rooted Hugrs, `node` is a `FuncDefn` child of the root;
/// or for non-Module-rooted Hugrs, `node` is the root of the Hugr. (This is not
/// In normal use, for Module-entrypoint Hugrs, `node` is a `FuncDefn` child of the module;
/// or for non-Module-entrypoint Hugrs, `node` is the entrypoint of the Hugr. (This is not
/// enforced, but it must be a container and not a module itself.)
///
/// Multiple calls for the same entry-point combine their values, with later
Expand Down Expand Up @@ -100,6 +101,13 @@ impl<H: HugrMut<Node = Node> + 'static> ComposablePass<H> for ConstantFoldPass {
/// [`ConstFoldError::InvalidEntryPoint`] if an entry-point added by [`Self::with_inputs`]
/// was of an invalid [`OpType`]
fn run(&self, hugr: &mut H) -> Result<(), ConstFoldError> {
let Some(root) = self
.scope
.as_ref()
.map_or(Some(hugr.entrypoint()), |sc| sc.root(hugr))
else {
return Ok(()); // Scope says do nothing
};
let fresh_node = Node::from(portgraph::NodeIndex::new(
hugr.nodes().max().map_or(0, |n| n.index() + 1),
));
Expand All @@ -122,15 +130,24 @@ impl<H: HugrMut<Node = Node> + 'static> ComposablePass<H> for ConstantFoldPass {
.map_err(|op| ConstFoldError::InvalidEntryPoint { node: n, op })?;
}

for node in self.scope.iter().flat_map(|sc| sc.preserve_interface(hugr)) {
if node == hugr.module_root() || self.inputs.contains_key(&node) {
// Cannot prepopulate inputs for module-root; do not `join` with inputs explicitly specified.
continue;
}
const NO_INPUTS: [(IncomingPort, PartialValue<ValueHandle>); 0] = [];
m.prepopulate_inputs(node, NO_INPUTS)
.map_err(|op| ConstFoldError::InvalidEntryPoint { node, op })?;
}

let results = m.run(ConstFoldContext, []);
let mb_root_inp = hugr.get_io(hugr.entrypoint()).map(|[i, _]| i);

let wires_to_break = hugr
.entry_descendants()
.descendants(root)
.flat_map(|n| hugr.node_inputs(n).map(move |ip| (n, ip)))
.filter(|(n, ip)| {
*n != hugr.entrypoint()
&& matches!(hugr.get_optype(*n).port_kind(*ip), Some(EdgeKind::Value(_)))
*n != root && matches!(hugr.get_optype(*n).port_kind(*ip), Some(EdgeKind::Value(_)))
})
.filter_map(|(n, ip)| {
let (src, outp) = hugr.single_linked_output(n, ip).unwrap();
Expand Down Expand Up @@ -165,8 +182,13 @@ impl<H: HugrMut<Node = Node> + 'static> ComposablePass<H> for ConstantFoldPass {
hugr.connect(lcst, OutgoingPort::from(0), n, inport);
}
// Eliminate dead code not required for the same entry points.
DeadCodeElimPass::<H>::default()
.with_entry_points(self.inputs.keys().copied())
let dce = self
.scope
.as_ref()
.map_or(DeadCodeElimPass::<H>::default(), |scope| {
DeadCodeElimPass::<H>::default().with_scope_internal(scope.clone())
});
dce.with_entry_points(self.inputs.keys().copied())
.set_preserve_callback(if self.allow_increase_termination {
Arc::new(|_, _| PreserveNode::CanRemoveIgnoringChildren)
} else {
Expand All @@ -186,13 +208,19 @@ impl<H: HugrMut<Node = Node> + 'static> ComposablePass<H> for ConstantFoldPass {
})?;
Ok(())
}

fn with_scope_internal(mut self, scope: impl Into<PassScope>) -> Self {
self.scope = Some(scope.into());
self
}
}

/// Exhaustively apply constant folding to a HUGR.
/// If the Hugr's entrypoint is its [`Module`], assumes all [`FuncDefn`] children are reachable.
///
/// [`FuncDefn`]: hugr_core::ops::OpType::FuncDefn
/// [`Module`]: hugr_core::ops::OpType::Module
#[deprecated(note = "Use ConstantFoldPass with a PassScope", since = "0.25.7")]
pub fn constant_fold_pass<H: HugrMut<Node = Node> + 'static>(mut h: impl AsMut<H>) {
let h = h.as_mut();
let c = ConstantFoldPass::default();
Expand Down
21 changes: 16 additions & 5 deletions hugr-passes/src/const_fold/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::{
sync::LazyLock,
};

use hugr_core::ops::Const;
use hugr_core::ops::handle::NodeHandle;
use hugr_core::{Visibility, ops::Const};
use itertools::Itertools;
use rstest::rstest;

Expand All @@ -30,10 +30,20 @@ use hugr_core::std_extensions::logic::LogicOp;
use hugr_core::types::{Signature, SumType, Type, TypeBound, TypeRow, TypeRowRV};
use hugr_core::{Hugr, HugrView, IncomingPort, Node, type_row};

use crate::ComposablePass as _;
use crate::dataflow::{DFContext, PartialValue, partial_from_const};
use crate::{ComposablePass as _, composable::ValidatingPass};
use crate::{
PassScope,
composable::WithScope,
dataflow::{DFContext, PartialValue, partial_from_const},
};

use super::{ConstFoldContext, ConstantFoldPass, ValueHandle};

use super::{ConstFoldContext, ConstantFoldPass, ValueHandle, constant_fold_pass};
fn constant_fold_pass(h: &mut (impl HugrMut<Node = Node> + 'static)) {
// the default ConstantFoldPass has no scope, i.e. preserving legacy behavior
let c = ConstantFoldPass::default().with_scope(PassScope::default());
ValidatingPass::new(c).run(h).unwrap();
}

#[rstest]
#[case(ConstInt::new_u(4, 2).unwrap(), true)]
Expand Down Expand Up @@ -1592,9 +1602,10 @@ fn test_module() -> Result<(), Box<dyn std::error::Error>> {
let c17 = mb.add_constant(Value::from(ConstInt::new_u(5, 17)?));
let ad1 = mb.add_alias_declare("unused", TypeBound::Linear)?;
let ad2 = mb.add_alias_def("unused2", INT_TYPES[3].clone())?;
let mut main = mb.define_function(
let mut main = mb.define_function_vis(
"main",
Signature::new(type_row![], vec![INT_TYPES[5].clone(); 2]),
Visibility::Public,
)?;
let lc7 = main.load_const(&c7);
let lc17 = main.load_const(&c17);
Expand Down
41 changes: 33 additions & 8 deletions hugr-passes/src/dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::{Debug, Display, Formatter};
use std::sync::Arc;

use crate::ComposablePass;
use crate::{ComposablePass, PassScope};

/// Configuration for Dead Code Elimination pass
#[derive(Clone)]
pub struct DeadCodeElimPass<H: HugrView> {
/// Nodes that are definitely needed - e.g. `FuncDefns`, but could be anything.
/// Hugr Root is assumed to be an entry point even if not mentioned here.
entry_points: Vec<H::Node>,
/// If None, use entrypoint-subtree (even if module root)
scope: Option<PassScope>,
/// Callback identifying nodes that must be preserved even if their
/// results are not used. Defaults to [`PreserveNode::default_for`].
preserve_callback: Arc<PreserveCallback<H>>,
Expand All @@ -23,6 +25,8 @@ impl<H: HugrView + 'static> Default for DeadCodeElimPass<H> {
fn default() -> Self {
Self {
entry_points: Default::default(),
// Preserve pre-PassScope behaviour of affecting entrypoint subtree only:
scope: None,
preserve_callback: Arc::new(PreserveNode::default_for),
}
}
Expand All @@ -36,11 +40,13 @@ impl<H: HugrView> Debug for DeadCodeElimPass<H> {
#[derive(Debug)]
struct DCEDebug<'a, N> {
entry_points: &'a Vec<N>,
scope: &'a Option<PassScope>,
}

Debug::fmt(
&DCEDebug {
entry_points: &self.entry_points,
scope: &self.scope,
},
f,
)
Expand Down Expand Up @@ -97,11 +103,11 @@ impl<H: HugrView> DeadCodeElimPass<H> {
self
}

/// Mark some nodes as entry points to the Hugr, i.e. so we cannot eliminate any code
/// used to evaluate these nodes.
/// [`HugrView::entrypoint`] is assumed to be an entry point;
/// for Module roots the client will want to mark some of the `FuncDefn` children
/// as entry points too.
/// Mark some nodes as starting points for analysis, i.e. so we cannot eliminate any code
/// used to evaluate these nodes. (E.g. nodes at which we may start executing the Hugr.)
///
/// Other starting points are added according to the [PassScope].
// TODO should we deprecate this? i.e. require use of PreserveCallback / Hugr edges?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, deprecating this would make it impossible to support ConstantFoldPass::with_inputs which I think is quite a nice feature for debugging. (Perhaps following deprecation both could be made crate-private instead of removing?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would crate-private be enough for testing?
Do we not want it for e.g. testing things in tket?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah possibly. But maybe the answer to that is to move it into tket rather than being its own crate?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Soon™️
Quantinuum/tket2#1414

(We'll first move the hugr-passes crate as-is to the other repo, and probably move it inside tket afterwards)

pub fn with_entry_points(mut self, entry_points: impl IntoIterator<Item = H::Node>) -> Self {
self.entry_points.extend(entry_points);
self
Expand All @@ -111,14 +117,21 @@ impl<H: HugrView> DeadCodeElimPass<H> {
let mut must_preserve = HashMap::new();
let mut needed = HashSet::new();
let mut q = VecDeque::from_iter(self.entry_points.iter().copied());
q.push_front(h.entrypoint());

match &self.scope {
None => q.push_back(h.entrypoint()),
Some(scope) => q.extend(scope.preserve_interface(h)),
};
while let Some(n) = q.pop_front() {
if !h.contains_node(n) {
return Err(DeadCodeElimError::NodeNotFound(n));
}
if !needed.insert(n) {
continue;
}
// Ensure no orphans. We could remove more from parent, but would require transforming
// (e.g. removing individual Output ports) not just deleting, so don't.
q.extend(h.get_parent(n));
for (i, ch) in h.children(n).enumerate() {
if self.must_preserve(h, &mut must_preserve, ch)
|| match h.get_optype(ch) {
Expand Down Expand Up @@ -181,16 +194,28 @@ impl<H: HugrMut> ComposablePass<H> for DeadCodeElimPass<H> {
type Result = ();

fn run(&self, hugr: &mut H) -> Result<(), Self::Error> {
let root = match &self.scope {
None => hugr.entrypoint(),
Some(scope) => match scope.root(hugr) {
Some(root) => root,
None => return Ok(()),
},
};
let needed = self.find_needed_nodes(&*hugr)?;
let remove = hugr
.entry_descendants()
.descendants(root)
.filter(|n| !needed.contains(n))
.collect::<Vec<_>>();
for n in remove {
hugr.remove_node(n);
}
Ok(())
}

fn with_scope_internal(mut self, scope: impl Into<PassScope>) -> Self {
self.scope = Some(scope.into());
self
}
}
#[cfg(test)]
mod test {
Expand Down
Loading
Loading