diff --git a/hugr-passes/src/composable.rs b/hugr-passes/src/composable.rs index 35f3a5e5e..7b5fc3248 100644 --- a/hugr-passes/src/composable.rs +++ b/hugr-passes/src/composable.rs @@ -307,8 +307,8 @@ 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}; @@ -416,7 +416,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(); diff --git a/hugr-passes/src/dead_funcs.rs b/hugr-passes/src/dead_funcs.rs index a77c19fe9..1aafef298 100644 --- a/hugr-passes/src/dead_funcs.rs +++ b/hugr-passes/src/dead_funcs.rs @@ -3,15 +3,16 @@ use std::collections::HashSet; use hugr_core::{ - HugrView, Node, + HugrView, Node, Visibility, hugr::hugrmut::HugrMut, module_graph::{ModuleGraph, StaticNode}, ops::{OpTag, OpTrait}, }; +use itertools::Either; use petgraph::visit::{Dfs, Walker}; use crate::{ - ComposablePass, + ComposablePass, PassScope, composable::{ValidatePassError, validate_if_test}, }; @@ -48,23 +49,41 @@ fn reachable_funcs<'a, H: HugrView>( }) } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] /// A configuration for the Dead Function Removal pass. pub struct RemoveDeadFuncsPass { - entry_points: Vec, + entry_points: Either, PassScope>, +} + +impl Default for RemoveDeadFuncsPass { + fn default() -> Self { + Self { + entry_points: Either::Left(Vec::new()), + } + } } impl RemoveDeadFuncsPass { + #[deprecated(note = "Use RemoveDeadFuncsPass::with_scope")] /// Adds new entry points - these must be [`FuncDefn`] nodes /// that are children of the [`Module`] at the root of the Hugr. /// + /// Overrides any [PassScope] set by a call to [Self::with_scope]. + /// /// [`FuncDefn`]: hugr_core::ops::OpType::FuncDefn /// [`Module`]: hugr_core::ops::OpType::Module pub fn with_module_entry_points( mut self, entry_points: impl IntoIterator, ) -> Self { - self.entry_points.extend(entry_points); + let v = match self.entry_points { + Either::Left(ref mut v) => v, + Either::Right(_) => { + self.entry_points = Either::Left(Vec::new()); + self.entry_points.as_mut().unwrap_left() + } + }; + v.extend(entry_points); self } } @@ -72,17 +91,51 @@ impl RemoveDeadFuncsPass { impl> ComposablePass for RemoveDeadFuncsPass { type Error = RemoveDeadFuncsError; type Result = (); + + /// Overrides any entrypoints set by a call to [Self::with_module_entry_points]. + fn with_scope(mut self, scope: &PassScope) -> Self { + self.entry_points = Either::Right(scope.clone()); + self + } + fn run(&self, hugr: &mut H) -> Result<(), RemoveDeadFuncsError> { let mut entry_points = Vec::new(); - for &n in self.entry_points.iter() { - if !hugr.get_optype(n).is_func_defn() { - return Err(RemoveDeadFuncsError::InvalidEntryPoint { node: n }); + match &self.entry_points { + Either::Left(ep) => { + for &n in ep { + if !hugr.get_optype(n).is_func_defn() { + return Err(RemoveDeadFuncsError::InvalidEntryPoint { node: n }); + } + debug_assert_eq!(hugr.get_parent(n), Some(hugr.module_root())); + entry_points.push(n); + } + if hugr.entrypoint() != hugr.module_root() { + entry_points.push(hugr.entrypoint()) + } + } + Either::Right(PassScope::EntrypointFlat | PassScope::EntrypointRecursive) => { + // If the entrypoint is the module root, not allowed to touch anything. + // Otherwise, we must keep the entrypoint (and can touch only inside it). + return Ok(()); + } + Either::Right(PassScope::PreserveAll) => return Ok(()), // Optimize whole Hugr but keep all functions + Either::Right(PassScope::PreservePublic) => { + for n in hugr.children(hugr.module_root()) { + if let Some(fd) = hugr.get_optype(n).as_func_defn() + && fd.visibility() == &Visibility::Public { + entry_points.push(n); + } + } + if hugr.entrypoint() != hugr.module_root() { + entry_points.push(hugr.entrypoint()); + } + } + Either::Right(PassScope::PreserveEntrypoint) => { + if hugr.entrypoint() == hugr.module_root() { + return Ok(()); + }; + entry_points.push(hugr.entrypoint()) } - debug_assert_eq!(hugr.get_parent(n), Some(hugr.module_root())); - entry_points.push(n); - } - if hugr.entrypoint() != hugr.module_root() { - entry_points.push(hugr.entrypoint()) } let mut reachable = @@ -124,6 +177,8 @@ impl> ComposablePass for RemoveDeadFuncsPass { /// [`FuncDefn`]: hugr_core::ops::OpType::FuncDefn /// [`LoadFunction`]: hugr_core::ops::OpType::LoadFunction /// [`Module`]: hugr_core::ops::OpType::Module +#[deprecated] +#[expect(deprecated)] pub fn remove_dead_funcs( h: &mut impl HugrMut, entry_points: impl IntoIterator, @@ -134,11 +189,28 @@ pub fn remove_dead_funcs( ) } +/// Deletes from the Hugr any functions that are not used by either [`Call`] or +/// [`LoadFunction`] nodes in reachable parts, according to the specified [PassScope]. +// TODO: after removing the deprecated `remove_dead_funcs`, rename this over it +pub fn remove_dead_funcs_scoped>( + h: &mut H, + scope: &PassScope, +) -> Result<(), ValidatePassError> { + validate_if_test( + >::with_scope( + RemoveDeadFuncsPass::default(), + scope, + ), + h, + ) +} + #[cfg(test)] mod test { use std::collections::HashMap; use hugr_core::ops::handle::NodeHandle; + use hugr_core::{Hugr, Visibility}; use itertools::Itertools; use rstest::rstest; @@ -146,44 +218,61 @@ mod test { use hugr_core::hugr::hugrmut::HugrMut; use hugr_core::{HugrView, extension::prelude::usize_t, types::Signature}; - use super::remove_dead_funcs; + use crate::PassScope; + use crate::dead_funcs::remove_dead_funcs_scoped; + + fn hugr(use_entrypoint: bool) -> Hugr { + let mut hb = ModuleBuilder::new(); + let o2 = hb + .define_function("from_pub", Signature::new_endo(usize_t())) + .unwrap(); + let o2inp = o2.input_wires(); + let o2 = o2.finish_with_outputs(o2inp).unwrap(); + let mut o1 = hb + .define_function_vis( + "pubfunc", + Signature::new_endo(usize_t()), + Visibility::Public, + ) + .unwrap(); + + let o1c = o1.call(o2.handle(), &[], o1.input_wires()).unwrap(); + o1.finish_with_outputs(o1c.outputs()).unwrap(); + + let fm = hb + .define_function("from_main", Signature::new_endo(usize_t())) + .unwrap(); + let f_inp = fm.input_wires(); + let fm = fm.finish_with_outputs(f_inp).unwrap(); + let mut m = hb + .define_function("main", Signature::new_endo(usize_t())) + .unwrap(); + let m_in = m.input_wires(); + let mut dfb = m.dfg_builder(Signature::new_endo(usize_t()), m_in).unwrap(); + let c = dfb.call(fm.handle(), &[], dfb.input_wires()).unwrap(); + let dfg = dfb.finish_with_outputs(c.outputs()).unwrap(); + m.finish_with_outputs(dfg.outputs()).unwrap(); + let mut h = hb.finish_hugr().unwrap(); + if use_entrypoint { + h.set_entrypoint(dfg.node()); + } + h + } #[rstest] #[case(false, [], vec![])] // No entry_points removes everything! #[case(true, [], vec!["from_main", "main"])] #[case(false, ["main"], vec!["from_main", "main"])] #[case(false, ["from_main"], vec!["from_main"])] - #[case(false, ["other1"], vec!["other1", "other2"])] - #[case(true, ["other2"], vec!["from_main", "main", "other2"])] - #[case(false, ["other1", "other2"], vec!["other1", "other2"])] + #[case(false, ["pubfunc"], vec!["from_pub", "pubfunc"])] + #[case(true, ["from_pub"], vec!["from_main", "from_pub", "main"])] + #[case(false, ["from_pub", "pubfunc"], vec!["from_pub", "pubfunc"])] fn remove_dead_funcs_entry_points( #[case] use_hugr_entrypoint: bool, #[case] entry_points: impl IntoIterator, #[case] retained_funcs: Vec<&'static str>, ) -> Result<(), Box> { - let mut hb = ModuleBuilder::new(); - let o2 = hb.define_function("other2", Signature::new_endo(usize_t()))?; - let o2inp = o2.input_wires(); - let o2 = o2.finish_with_outputs(o2inp)?; - let mut o1 = hb.define_function("other1", Signature::new_endo(usize_t()))?; - - let o1c = o1.call(o2.handle(), &[], o1.input_wires())?; - o1.finish_with_outputs(o1c.outputs())?; - - let fm = hb.define_function("from_main", Signature::new_endo(usize_t()))?; - let f_inp = fm.input_wires(); - let fm = fm.finish_with_outputs(f_inp)?; - let mut m = hb.define_function("main", Signature::new_endo(usize_t()))?; - let m_in = m.input_wires(); - let mut dfg = m.dfg_builder(Signature::new_endo(usize_t()), m_in)?; - let c = dfg.call(fm.handle(), &[], dfg.input_wires())?; - let dfg = dfg.finish_with_outputs(c.outputs()).unwrap(); - m.finish_with_outputs(dfg.outputs())?; - - let mut hugr = hb.finish_hugr()?; - if use_hugr_entrypoint { - hugr.set_entrypoint(dfg.node()); - } + let mut hugr = hugr(use_hugr_entrypoint); let avail_funcs = hugr .children(hugr.module_root()) @@ -194,7 +283,8 @@ mod test { }) .collect::>(); - remove_dead_funcs( + #[expect(deprecated)] + super::remove_dead_funcs( &mut hugr, entry_points .into_iter() @@ -215,4 +305,31 @@ mod test { assert_eq!(remaining_funcs, retained_funcs); Ok(()) } + + #[rstest] + #[case(PassScope::PreserveAll, false, vec!["from_main", "from_pub", "main", "pubfunc"])] + #[case(PassScope::EntrypointFlat, true, vec!["from_main", "from_pub", "main", "pubfunc"])] + #[case(PassScope::EntrypointRecursive, false, vec!["from_main", "from_pub", "main", "pubfunc"])] + #[case(PassScope::PreservePublic, true, vec!["from_main", "from_pub", "main", "pubfunc"])] + #[case(PassScope::PreservePublic, false, vec!["from_pub", "pubfunc"])] + #[case(PassScope::PreserveEntrypoint, true, vec!["from_main", "main"])] + fn remove_dead_funcs_scope( + #[case] scope: PassScope, + #[case] use_entrypoint: bool, + #[case] retained_funcs: Vec<&'static str>, + ) { + let mut hugr = hugr(use_entrypoint); + remove_dead_funcs_scoped(&mut hugr, &scope).unwrap(); + + let remaining_funcs = hugr + .nodes() + .filter_map(|n| { + hugr.get_optype(n) + .as_func_defn() + .map(|fd| fd.func_name().as_str()) + }) + .sorted() + .collect_vec(); + assert_eq!(remaining_funcs, retained_funcs); + } } diff --git a/hugr-passes/src/lib.rs b/hugr-passes/src/lib.rs index 6f4ae1ac1..239b77646 100644 --- a/hugr-passes/src/lib.rs +++ b/hugr-passes/src/lib.rs @@ -24,7 +24,10 @@ pub use composable::{ComposablePass, InScope, PassScope}; // Pass re-exports pub use dead_code::DeadCodeElimPass; -pub use dead_funcs::{RemoveDeadFuncsError, RemoveDeadFuncsPass, remove_dead_funcs}; +#[deprecated(note = "Use RemoveDeadFuncsPass instead")] +#[expect(deprecated)] // Remove together +pub use dead_funcs::remove_dead_funcs; +pub use dead_funcs::{RemoveDeadFuncsError, RemoveDeadFuncsPass, remove_dead_funcs_scoped}; pub use force_order::{force_order, force_order_by_key}; pub use inline_funcs::inline_acyclic; pub use lower::{lower_ops, replace_many_ops}; diff --git a/hugr-passes/src/monomorphize.rs b/hugr-passes/src/monomorphize.rs index 5ce486dde..3fb07c27e 100644 --- a/hugr-passes/src/monomorphize.rs +++ b/hugr-passes/src/monomorphize.rs @@ -281,13 +281,14 @@ mod test { HugrBuilder, ModuleBuilder, }; use hugr_core::extension::prelude::{ConstUsize, UnpackTuple, UnwrapBuilder, usize_t}; - use hugr_core::ops::handle::{FuncID, NodeHandle}; + use hugr_core::ops::handle::FuncID; use hugr_core::ops::{CallIndirect, DataflowOpTrait as _, FuncDefn, Tag}; use hugr_core::types::{PolyFuncType, Signature, Type, TypeArg, TypeBound, TypeEnum}; - use hugr_core::{Hugr, HugrView, Node}; + use hugr_core::{Hugr, HugrView, Node, Visibility}; use rstest::rstest; - use crate::{monomorphize, remove_dead_funcs}; + use crate::dead_funcs::remove_dead_funcs_scoped; + use crate::{PassScope, monomorphize}; use super::{is_polymorphic, mangle_name}; @@ -349,9 +350,13 @@ mod test { let trip = fb.add_dataflow_op(tag, [elem1, elem2, elem])?; fb.finish_with_outputs(trip.outputs())? }; - let mn = { + { let outs = vec![triple_type(usize_t()), triple_type(pair_type(usize_t()))]; - let mut fb = mb.define_function("main", Signature::new(usize_t(), outs))?; + let mut fb = mb.define_function_vis( + "main", + Signature::new(usize_t(), outs), + Visibility::Public, + )?; let [elem] = fb.input_wires_arr(); let [res1] = fb .call(tr.handle(), &[usize_t().into()], [elem])? @@ -359,7 +364,7 @@ mod test { let pair = fb.call(db.handle(), &[usize_t().into()], [elem])?; let pty = pair_type(usize_t()).into(); let [res2] = fb.call(tr.handle(), &[pty], pair.outputs())?.outputs_arr(); - fb.finish_with_outputs([res1, res2])? + fb.finish_with_outputs([res1, res2])?; }; let mut hugr = mb.finish_hugr()?; assert_eq!( @@ -394,7 +399,7 @@ mod test { assert_eq!(mono2, mono); // Idempotent let mut nopoly = mono; - remove_dead_funcs(&mut nopoly, [mn.node()])?; + remove_dead_funcs_scoped(&mut nopoly, &PassScope::PreservePublic)?; let mut funcs = list_funcs(&nopoly); assert!(funcs.values().all(|(_, fd)| !is_polymorphic(fd))); @@ -621,7 +626,7 @@ mod test { }; monomorphize(&mut hugr).unwrap(); - remove_dead_funcs(&mut hugr, []).unwrap(); + remove_dead_funcs_scoped(&mut hugr, &PassScope::PreservePublic).unwrap(); let funcs = list_funcs(&hugr); assert!(funcs.values().all(|(_, fd)| !is_polymorphic(fd))); diff --git a/hugr-passes/src/untuple.rs b/hugr-passes/src/untuple.rs index 27a1f1f05..67297165f 100644 --- a/hugr-passes/src/untuple.rs +++ b/hugr-passes/src/untuple.rs @@ -11,22 +11,30 @@ use hugr_core::hugr::views::sibling_subgraph::TopoConvexChecker; use hugr_core::ops::{OpTrait, OpType}; use hugr_core::types::Type; use hugr_core::{HugrView, Node, SimpleReplacement}; -use itertools::Itertools; +use itertools::{Either, Itertools}; -use crate::ComposablePass; +use crate::{ComposablePass, PassScope}; /// Configuration enum for the untuple rewrite pass. /// /// Indicates whether the pattern match should traverse the HUGR recursively. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[deprecated(note = "Use PassScope instead")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UntupleRecursive { /// Traverse the HUGR recursively, i.e. consider the entire subtree Recursive, /// Do not traverse the HUGR recursively, i.e. consider only the sibling subgraph - #[default] NonRecursive, } +#[expect(deprecated)] // Remove along with UntupleRecursive +#[expect(clippy::derivable_impls)] // derive(Default) generates deprecation warning +impl Default for UntupleRecursive { + fn default() -> Self { + UntupleRecursive::NonRecursive + } +} + /// A pass that removes unnecessary `MakeTuple` operations immediately followed /// by `UnpackTuple`s. /// @@ -44,12 +52,29 @@ pub enum UntupleRecursive { /// /// - Order edges are not supported yet. The pass currently panics if it encounters /// a pack/unpack pair with connected order edges. See . -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct UntuplePass { - /// Whether to traverse the HUGR recursively. - recursive: UntupleRecursive, - /// Parent node under which to operate; None indicates the Hugr root - parent: Option, + /// Either a [PassScope] controlling which parts of the Hugr to process; + /// or a flag for recursiveness, and the parent node under which to operate + /// (None indicating the Hugr root) + #[expect(deprecated)] // remove Right half and just use PassScope + scope: Either)>, +} + +impl Default for UntuplePass { + fn default() -> Self { + // TODO Move to PassScope::Default() when UntupleRecursive is removed + Self { + scope: Either::Right((Default::default(), Default::default())), + } + } +} + +#[expect(deprecated)] // Remove along with UntupleRecursive +impl From for bool { + fn from(value: UntupleRecursive) -> Self { + value == UntupleRecursive::Recursive + } } #[derive(Debug, derive_more::Display, derive_more::Error, derive_more::From)] @@ -68,59 +93,127 @@ pub struct UntupleResult { } impl UntuplePass { - /// Create a new untuple pass with the given configuration. + /// Create a new untuple pass with the given recursiveness and that + /// will run on the entrypoint region/subtree. #[must_use] + #[deprecated(note = "Use new_scoped or default instead")] + #[expect(deprecated)] // Remove along with UntupleRecursive pub fn new(recursive: UntupleRecursive) -> Self { Self { - recursive, - parent: None, + scope: Either::Right((recursive, None)), + } + } + + /// Create a new untuple pass with the given configuration + #[must_use] + pub fn new_scoped(scope: PassScope) -> Self { + Self { + scope: Either::Left(scope), } } /// Sets the parent node to optimize (overwrites any previous setting) + /// + /// If the pass was previously configured by [Self::with_scope] then + /// implicitly `[Self::set_recursive]`'s with thee [PassScope::recursive] + #[deprecated(note = "Use with_scope instead")] + #[expect(deprecated)] // Remove along with UntupleRecursive pub fn set_parent(mut self, parent: impl Into>) -> Self { - self.parent = parent.into(); + match &mut self.scope { + Either::Left(p) => { + let rec = if p.recursive() { + UntupleRecursive::Recursive + } else { + UntupleRecursive::NonRecursive + }; + self.scope = Either::Right((rec, parent.into())) + } + Either::Right((_, p)) => *p = parent.into(), + }; self } /// Sets whether the pass should traverse the HUGR recursively. + /// + /// If the pass was last configured via [Self::with_scope], overrides that, + /// with `set_parent` of default `None`. #[must_use] + #[deprecated(note = "Use with_scope")] + #[expect(deprecated)] // Remove along with UntupleRecursive pub fn recursive(mut self, recursive: UntupleRecursive) -> Self { - self.recursive = recursive; + let parent = self.scope.right().and_then(|(_, p)| p); + self.scope = Either::Right((recursive, parent)); self } /// Find tuple pack operations followed by tuple unpack operations + /// beneath a specified parent and according to this instance's recursiveness + /// ([Self::recursive] or [Self::with_scope] + [PassScope::recursive]) /// and generate rewrites to remove them. /// /// The returned rewrites are guaranteed to be independent of each other. /// /// Returns an iterator over the rewrites. + #[deprecated(note = "Use all_rewrites")] pub fn find_rewrites( &self, hugr: &H, parent: H::Node, ) -> Vec> { - let mut res = Vec::new(); - let mut children_queue = VecDeque::new(); - children_queue.push_back(parent); - - // Required to create SimpleReplacements. - let mut convex_checker: Option> = None; - - while let Some(parent) = children_queue.pop_front() { - for node in hugr.children(parent) { - let op = hugr.get_optype(node); - if let Some(rw) = make_rewrite(hugr, &mut convex_checker, node, op) { - res.push(rw); - } - if self.recursive == UntupleRecursive::Recursive && op.is_container() { - children_queue.push_back(node); - } + let recursive = match &self.scope { + Either::Left(scope) => scope.recursive(), + Either::Right((rec, _)) => (*rec).into(), + }; + find_rewrites(hugr, parent, recursive) + } + + /// Find tuple pack operations followed by tuple unpack operations + /// and generate rewrites to remove them. + /// + /// The returned rewrites are guaranteed to be independent of each other. + /// + /// Returns an iterator over the rewrites. + pub fn all_rewrites>( + &self, + hugr: &H, + ) -> Vec> { + let (recursive, parent) = match &self.scope { + Either::Left(scope) => { + let Some(root) = scope.root(hugr) else { + return vec![]; + }; + (scope.recursive(), root) + } + Either::Right((rec, parent)) => ((*rec).into(), parent.unwrap_or(hugr.entrypoint())), + }; + find_rewrites(hugr, parent, recursive) + } +} + +fn find_rewrites( + hugr: &H, + parent: H::Node, + recursive: bool, +) -> Vec> { + let mut res = Vec::new(); + let mut children_queue = VecDeque::new(); + children_queue.push_back(parent); + + // Required to create SimpleReplacements. + let mut convex_checker: Option> = None; + + while let Some(parent) = children_queue.pop_front() { + for node in hugr.children(parent) { + let op = hugr.get_optype(node); + if let Some(rw) = make_rewrite(hugr, &mut convex_checker, node, op) { + res.push(rw); + } + if recursive && op.is_container() { + children_queue.push_back(node); } } - res } + res } impl> ComposablePass for UntuplePass { @@ -128,7 +221,7 @@ impl> ComposablePass for UntuplePass { type Result = UntupleResult; fn run(&self, hugr: &mut H) -> Result { - let rewrites = self.find_rewrites(hugr, self.parent.unwrap_or(hugr.entrypoint())); + let rewrites = self.all_rewrites(hugr); let rewrites_applied = rewrites.len(); // The rewrites are independent, so we can always apply them all. for rewrite in rewrites { @@ -136,6 +229,12 @@ impl> ComposablePass for UntuplePass { } Ok(UntupleResult { rewrites_applied }) } + + /// Overrides any [Self::set_parent] or [Self::recursive] + fn with_scope(mut self, scope: &crate::PassScope) -> Self { + self.scope = Either::Left(scope.clone()); + self + } } /// Returns true if the given optype is a `MakeTuple` operation. @@ -399,14 +498,16 @@ mod test { #[case] mut hugr: Hugr, #[case] expected_rewrites: usize, #[case] remaining_nodes: usize, + #[values(true, false)] use_scope: bool, ) { - let pass = UntuplePass::default().recursive(UntupleRecursive::NonRecursive); - let parent = hugr.entrypoint(); - let res = pass - .set_parent(parent) - .run(&mut hugr) - .unwrap_or_else(|e| panic!("{e}")); + let pass = if use_scope { + UntuplePass::new_scoped(PassScope::EntrypointFlat) + } else { + #[expect(deprecated)] // Remove use_scope==false case along with UntupleRecursive + UntuplePass::default().set_parent(parent) + }; + let res = pass.run(&mut hugr).unwrap_or_else(|e| panic!("{e}")); assert_eq!(res.rewrites_applied, expected_rewrites); assert_eq!(hugr.children(parent).count(), remaining_nodes); }