Skip to main content

hugr_passes/
replace_types.rs

1#![allow(clippy::type_complexity)]
2//! Replace types with other types across the Hugr. See [`ReplaceTypes`] and [Linearizer].
3//!
4use std::borrow::Cow;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use handlers::list_const;
9use hugr_core::hugr::linking::{HugrLinking, NameLinkingPolicy, OnMultiDefn};
10use hugr_core::std_extensions::collections::array::array_type_def;
11use hugr_core::std_extensions::collections::list::list_type_def;
12use itertools::Either;
13use thiserror::Error;
14
15use hugr_core::builder::{
16    BuildError, BuildHandle, Container, Dataflow, DataflowHugr, FunctionBuilder, HugrBuilder,
17};
18use hugr_core::extension::{ExtensionId, OpDef, SignatureError, TypeDef};
19use hugr_core::hugr::hugrmut::HugrMut;
20use hugr_core::ops::constant::{OpaqueValue, Sum};
21use hugr_core::ops::handle::{DataflowOpID, FuncID, NodeHandle};
22use hugr_core::ops::{
23    AliasDefn, CFG, Call, CallIndirect, Case, Conditional, Const, DFG, DataflowBlock, ExitBlock,
24    ExtensionOp, Input, LoadConstant, LoadFunction, OpTrait, OpType, Output, Tag, TailLoop, Value,
25};
26use hugr_core::types::{
27    ConstTypeError, CustomType, Signature, Transformable, Type, TypeArg, TypeEnum, TypeRow,
28    TypeTransformer,
29};
30use hugr_core::{Direction, Hugr, HugrView, Node, PortIndex, Visibility, Wire};
31
32use crate::composable::WithScope;
33use crate::{ComposablePass, PassScope};
34
35mod linearize;
36pub use linearize::{CallbackHandler, DelegatingLinearizer, LinearizeError, Linearizer};
37
38/// A recipe for creating a dataflow Node - as a new child of a [`DataflowParent`]
39/// or in order to replace an existing node.
40///
41/// [`DataflowParent`]: hugr_core::ops::OpTag::DataflowParent
42#[derive(Clone, Debug, PartialEq)]
43#[non_exhaustive]
44#[deprecated(
45    note = "`hugr-passes` is deprecated. Use tket::passes instead",
46    since = "0.26.2"
47)]
48pub enum NodeTemplate {
49    /// A single node - so if replacing an existing node, change only the op.
50    ///
51    /// An error will be raised if the new op has a static inport; use [Self::LinkedHugr] instead.
52    SingleOp(OpType),
53    /// Defines a sub-Hugr whose entrypoint-subtree to insert, the entrypoint (which must be
54    /// a [CFG], [Conditional], [DFG] or [`TailLoop`]) becoming (/replacing) the desired Node.
55    // Not a FuncDefn, nor Case/DataflowBlock
56    /// Note 1. `CompoundOp` will be of limited use for polymorphic ops (before [monomorphization])
57    /// because the new subtree will not be able to use type variables present in the
58    /// parent Hugr or previous op.
59    ///
60    /// Edges incoming to the entrypoint subtree will be disconnected.
61    ///
62    /// It is **recommended** to use [Self::LinkedHugr] instead.
63    ///
64    /// [monomorphization]: crate::MonomorphizePass
65    CompoundOp(Box<Hugr>),
66    /// Defines a sub-Hugr to insert, whose entrypoint becomes (or replaces) the desired Node.
67    /// Other children of the Hugr reachable from the entrypoint will also be inserted
68    /// according to the specified linking policy.
69    LinkedHugr(Box<Hugr>, NameLinkingPolicy),
70    /// A Call to an existing function.
71    #[deprecated(
72        note = "Use LinkedHugr with Call entrypoint and FuncDecl",
73        since = "0.24.4"
74    )]
75    Call(Node, Vec<TypeArg>),
76}
77
78impl NodeTemplate {
79    /// Creates a [Self::LinkedHugr] from the given Hugr with the default linking policy.
80    pub fn linked_hugr(h: impl Into<Hugr>) -> Self {
81        NodeTemplate::LinkedHugr(Box::new(h.into()), NameLinkingPolicy::default())
82    }
83
84    /// Creates a [`NodeTemplate::LinkedHugr`] creating a call node to the given
85    /// function definition or declaration.
86    ///
87    /// Returns a [`BuildError::UnexpectedType`] if the given Hugr does not have
88    /// a function definition or declaration as entrypoint.
89    pub fn call_to_function(
90        func_def: Hugr,
91        type_args: &[TypeArg],
92    ) -> Result<NodeTemplate, BuildError> {
93        // Create a replacement hugr for the op nodes: Add a `call` node in the `func_def` hugr and set it as entrypoint.
94        let func_op = func_def.entrypoint_optype();
95        let func_signature = match func_op {
96            OpType::FuncDecl(decl) => decl.signature().clone(),
97            OpType::FuncDefn(defn) => defn.signature().clone(),
98            _ => {
99                return Err(BuildError::UnexpectedType {
100                    node: func_def.entrypoint(),
101                    op_desc: "function definition or declaration",
102                });
103            }
104        }
105        .instantiate(type_args)?;
106
107        // Build a new hugr and insert the function definition into it
108        let mut b = FunctionBuilder::new_vis("", func_signature, Visibility::Private).unwrap();
109        let func_id = FuncID::<true>::from(
110            b.module_root_builder()
111                .add_hugr(func_def)
112                .inserted_entrypoint,
113        );
114
115        // Build a call to the function in the new separate function.
116        let call = b.call(&func_id, type_args, b.input_wires()).unwrap();
117        let mut call_hugr = b.finish_hugr_with_outputs(call.outputs()).unwrap();
118        call_hugr.set_entrypoint(call.node());
119
120        Ok(NodeTemplate::LinkedHugr(
121            Box::new(call_hugr),
122            NameLinkingPolicy::default().on_multiple_defn(OnMultiDefn::UseTarget),
123        ))
124    }
125
126    /// Adds this instance to the specified [`HugrMut`] as a new node or subtree under a
127    /// given parent, returning the unique new child (of that parent) thus created
128    ///
129    /// # Panics
130    ///
131    /// * If `parent` is not in the `hugr`
132    ///
133    /// # Errors
134    ///
135    /// * If `self` is a [`Self::Call`] and the target Node either
136    ///    * is neither a [`FuncDefn`] nor a [`FuncDecl`]
137    ///    * has a [`signature`] which the type-args of the [`Self::Call`] do not match
138    ///
139    /// [`signature`]: hugr_core::types::PolyFuncType
140    /// [`FuncDecl`]: hugr_core::ops::FuncDecl
141    /// [`FuncDefn`]: hugr_core::ops::FuncDefn
142    pub fn add_hugr(
143        self,
144        hugr: &mut impl HugrMut<Node = Node>,
145        parent: Node,
146    ) -> Result<Node, BuildError> {
147        match self {
148            NodeTemplate::SingleOp(op_type) => Ok(hugr.add_node_with_parent(parent, op_type)),
149            NodeTemplate::CompoundOp(new_h) => {
150                Ok(hugr.insert_hugr(parent, *new_h).inserted_entrypoint)
151            }
152            NodeTemplate::LinkedHugr(h, pol) => {
153                Ok(hugr.insert_link_hugr(parent, *h, &pol)?.inserted_entrypoint)
154            }
155            #[expect(deprecated)] // remove together
156            NodeTemplate::Call(target, type_args) => {
157                let c = call(hugr, target, type_args)?;
158                let tgt_port = c.called_function_port();
159                let n = hugr.add_node_with_parent(parent, c);
160                hugr.connect(target, 0, n, tgt_port);
161                Ok(n)
162            }
163        }
164    }
165
166    /// Adds this instance to the specified [Dataflow] builder as a new node or subtree
167    pub fn add(
168        self,
169        dfb: &mut impl Dataflow,
170        inputs: impl IntoIterator<Item = Wire>,
171    ) -> Result<BuildHandle<DataflowOpID>, BuildError> {
172        match self {
173            NodeTemplate::SingleOp(opty) => dfb.add_dataflow_op(opty, inputs),
174            NodeTemplate::CompoundOp(h) => dfb.add_hugr_with_wires(*h, inputs),
175            NodeTemplate::LinkedHugr(h, pol) => dfb.add_link_hugr_with_wires(*h, &pol, inputs),
176            #[expect(deprecated)] // remove together
177            // Really we should check whether func points at a FuncDecl or FuncDefn and create
178            // the appropriate variety of FuncID but it doesn't matter for the purpose of making a Call.
179            NodeTemplate::Call(func, type_args) => {
180                if !dfb.hugr().contains_node(func) {
181                    return Err(BuildError::NodeNotFound { node: func });
182                }
183                dfb.call(&FuncID::<true>::from(func), &type_args, inputs)
184            }
185        }
186    }
187
188    fn replace<H: HugrMut<Node = Node>>(
189        self,
190        hugr: &mut H,
191        n: Node,
192        rt: &ReplaceTypes,
193        opts: &ReplacementOptions,
194    ) -> Result<(), ReplaceTypesError> {
195        let ef = |e| ReplaceTypesError::AddTemplateError(n, Box::new(e));
196        assert_eq!(hugr.children(n).count(), 0);
197        let (new_optype, static_source, static_inport) = match self {
198            NodeTemplate::SingleOp(op_type) => {
199                if op_type.static_input_port().is_some() {
200                    return Err(ef(BuildError::UnexpectedType {
201                        node: n,
202                        op_desc: "Replacement SingleOp without static input",
203                    }));
204                }
205                (op_type, None, None)
206            }
207            NodeTemplate::CompoundOp(new_h) => {
208                let root = new_h.entrypoint_optype();
209                if !matches!(
210                    root,
211                    OpType::CFG(_) | OpType::DFG(_) | OpType::Conditional(_) | OpType::TailLoop(_)
212                )
213                //if !root.is_container() || !root.dataflow_signature().is_some() // Using explicit list as per docs
214                {
215                    return Err(ef(BuildError::UnexpectedType {
216                        node: n,
217                        op_desc: "Replacement CompoundOp not a container/dataflow node",
218                    }));
219                }
220                assert!(root.static_input_port().is_none());
221                let new_entrypoint = hugr.insert_hugr(n, *new_h).inserted_entrypoint;
222                let children = hugr.children(new_entrypoint).collect::<Vec<_>>();
223                let root_opty = hugr.remove_node(new_entrypoint);
224                for ch in children {
225                    hugr.set_parent(ch, n);
226                }
227                (root_opty, None, None)
228            }
229            NodeTemplate::LinkedHugr(mut h, pol) => {
230                // We have to recursively process any children that *might* be linked in,
231                // before linking, as otherwise they'll signature conflict with other,
232                // already-recursively-processed, functions with which they might be linked.
233                let mut containing_func = h.entrypoint();
234                while let Some(parent) = h.get_parent(containing_func)
235                    && !h.get_optype(parent).is_module()
236                {
237                    containing_func = parent;
238                }
239
240                for ch in h.children(h.module_root()).collect::<Vec<_>>() {
241                    if ch != containing_func {
242                        rt.process_subtree_opts(&mut h, ch, opts)?;
243                    }
244                }
245                let new_entrypoint = hugr
246                    .insert_link_hugr(n, *h, &pol)
247                    .map_err(|e| ef(BuildError::from(e)))?
248                    .inserted_entrypoint;
249                let children = hugr.children(new_entrypoint).collect::<Vec<_>>();
250                let static_source = hugr.static_source(new_entrypoint);
251                let root_opty = hugr.remove_node(new_entrypoint);
252                let static_inport = root_opty.static_input_port();
253                for ch in children {
254                    hugr.set_parent(ch, n);
255                }
256                (root_opty, static_source, static_inport)
257            }
258            #[expect(deprecated)] // remove together
259            NodeTemplate::Call(func, type_args) => {
260                let c = call(hugr, func, type_args).map_err(ef)?;
261                let called_func_port = c.called_function_port();
262                (c.into(), Some(func), Some(called_func_port))
263            }
264        };
265        *hugr.optype_mut(n) = new_optype;
266        if let Some(static_inport) = static_inport {
267            hugr.insert_ports(n, Direction::Incoming, static_inport.index(), 1);
268            if let Some(static_source) = static_source {
269                hugr.connect(static_source, 0, n, static_inport);
270            }
271        }
272        rt.process_subtree_opts(hugr, n, opts)?;
273        Ok(())
274    }
275
276    fn check_signature(
277        &self,
278        inputs: &TypeRow,
279        outputs: &TypeRow,
280    ) -> Result<(), Option<Signature>> {
281        let sig = match self {
282            NodeTemplate::SingleOp(op_type) => op_type,
283            NodeTemplate::CompoundOp(hugr) => hugr.entrypoint_optype(),
284            NodeTemplate::LinkedHugr(hugr, _) => hugr.entrypoint_optype(),
285            #[expect(deprecated)] // remove together, perhaps refactor to return just Signature
286            NodeTemplate::Call(_, _) => return Ok(()), // no way to tell
287        }
288        .dataflow_signature();
289        if sig.as_deref().map(Signature::io) == Some((inputs, outputs)) {
290            Ok(())
291        } else {
292            Err(sig.map(Cow::into_owned))
293        }
294    }
295}
296
297fn call<H: HugrView<Node = Node>>(
298    h: &H,
299    func: Node,
300    type_args: Vec<TypeArg>,
301) -> Result<Call, BuildError> {
302    let func_sig = match h.get_optype(func) {
303        OpType::FuncDecl(fd) => fd.signature().clone(),
304        OpType::FuncDefn(fd) => fd.signature().clone(),
305        _ => {
306            return Err(BuildError::UnexpectedType {
307                node: func,
308                op_desc: "func defn/decl",
309            });
310        }
311    };
312    Ok(Call::try_new(func_sig, type_args)?)
313}
314
315/// Options for how a replacement (op or type) is processed.
316///
317/// May be specified by
318/// [ReplaceTypes::replace_op_with], [ReplaceTypes::replace_parametrized_op_with],
319/// [ReplaceTypes::replace_type_opts] or [ReplaceTypes::replace_parametrized_type_opts].
320/// Otherwise (the default), replacements are inserted as given (without further processing).
321#[derive(Clone, Default, PartialEq, Eq)] // More derives might inhibit future extension
322#[deprecated(
323    note = "`hugr-passes` is deprecated. Use tket::passes instead",
324    since = "0.26.2"
325)]
326pub struct ReplacementOptions {
327    process_recursive: bool,
328    linearize_unchanged: bool,
329}
330
331impl ReplacementOptions {
332    fn recursive() -> Self {
333        Self {
334            process_recursive: true,
335            linearize_unchanged: false,
336        }
337    }
338
339    /// Specifies whether all nodes within the replacement should have their
340    /// output ports linearized.
341    pub fn with_linearization(mut self, lin: bool) -> Self {
342        self.linearize_unchanged = lin;
343        self
344    }
345}
346
347/// A *lowering* [ComposablePass] that replaces types, ops and constants, i.e. changing
348/// node signatures/interfaces.
349///
350/// The struct configures what types, ops, and constants should be replaced with what,
351/// and may be applied to a Hugr via [`Self::run`].
352///
353/// Parametrized types and ops will be reparameterized taking into account the
354/// replacements, but any ops taking/returning the replaced types *not* as a result of
355/// parametrization, will also need to be replaced - see [`Self::replace_op`].
356/// Similarly [Const]s.
357///
358/// Types that are [Copyable](hugr_core::types::TypeBound::Copyable) may also be replaced
359/// with types that are not, see [Linearizer].
360///
361/// Note that although this pass may be used before [monomorphization], there are some
362/// limitations (that do not apply if done after [monomorphization]):
363/// * [`NodeTemplate::CompoundOp`] only works for operations that do not use type variables
364/// * "Overrides" of specific instantiations of polymorphic types will not be detected if
365///   the instantiations are created inside polymorphic functions. For example, suppose
366///   we [`Self::replace_type`] type `A` with `X`, [`Self::replace_parametrized_type`]
367///   container `MyList` with `List`, and [`Self::replace_type`] `MyList<A>` with
368///   `SpecialListOfXs`. If a function `foo` polymorphic over a type variable `T` dealing
369///   with `MyList<T>`s, that is called with type argument `A`, then `foo<T>` will be
370///   updated to deal with `List<T>`s and the call `foo<A>` updated to `foo<X>`, but this
371///   will still result in using `List<X>` rather than `SpecialListOfXs`. (However this
372///   would be fine *after* [monomorphization]: the monomorphic definition of `foo_A`
373///   would use `SpecialListOfXs`.)
374/// * See also limitations noted for [Linearizer].
375///
376/// [monomorphization]: crate::MonomorphizePass
377#[derive(Clone)]
378#[deprecated(
379    note = "`hugr-passes` is deprecated. Use tket::passes instead",
380    since = "0.26.2"
381)]
382pub struct ReplaceTypes {
383    type_map: HashMap<CustomType, (Type, ReplacementOptions)>,
384    param_types:
385        HashMap<ParametricType, (Arc<dyn Fn(&[TypeArg]) -> Option<Type>>, ReplacementOptions)>,
386    linearize: DelegatingLinearizer,
387    op_map: HashMap<OpHashWrapper, (NodeTemplate, ReplacementOptions)>,
388    param_ops: HashMap<
389        ParametricOp,
390        (
391            Arc<
392                dyn Fn(
393                    &[TypeArg],
394                    &ReplaceTypes,
395                ) -> Result<Option<NodeTemplate>, ReplaceTypesError>,
396            >,
397            ReplacementOptions,
398        ),
399    >,
400    consts: HashMap<
401        CustomType,
402        Arc<dyn Fn(&OpaqueValue, &ReplaceTypes) -> Result<Value, ReplaceTypesError>>,
403    >,
404    param_consts: HashMap<
405        ParametricType,
406        Arc<dyn Fn(&OpaqueValue, &ReplaceTypes) -> Result<Option<Value>, ReplaceTypesError>>,
407    >,
408    scope: Either<PassScope, Vec<Node>>,
409}
410
411impl Default for ReplaceTypes {
412    fn default() -> Self {
413        let mut res = Self::new_empty();
414        res.linearize = DelegatingLinearizer::default();
415        res.replace_consts_parametrized(array_type_def(), handlers::array_const);
416        res.replace_consts_parametrized(list_type_def(), list_const);
417        res
418    }
419}
420
421impl TypeTransformer for ReplaceTypes {
422    type Err = ReplaceTypesError;
423
424    fn apply_custom(&self, ct: &CustomType) -> Result<Option<Type>, Self::Err> {
425        let mut ty_and_opts = None;
426        if let Some(res) = self.type_map.get(ct) {
427            ty_and_opts = Some(res.clone())
428        } else if let Some((dest_fn, opts)) = self.param_types.get(&ct.into()) {
429            // `ct` has not had args transformed
430            let mut nargs = ct.args().to_vec();
431            // We don't care if `nargs` are changed, we're just calling `dest_fn`
432            nargs
433                .iter_mut()
434                .try_for_each(|ta| ta.transform(self).map(|_ch| ()))?;
435            ty_and_opts = dest_fn(&nargs).map(|ty| (ty, opts.clone()))
436        };
437        let Some((mut ty, opts)) = ty_and_opts else {
438            return Ok(None);
439        };
440        if opts.process_recursive {
441            ty.transform(self)?;
442        }
443        Ok(Some(ty))
444    }
445}
446
447/// An error produced by the [`ReplaceTypes`] pass
448#[derive(Debug, Error, PartialEq)]
449#[non_exhaustive]
450#[expect(missing_docs)]
451#[deprecated(
452    note = "`hugr-passes` is deprecated. Use tket::passes instead",
453    since = "0.26.2"
454)]
455pub enum ReplaceTypesError {
456    #[error(transparent)]
457    SignatureError(#[from] SignatureError),
458    #[error(transparent)]
459    ConstError(#[from] ConstTypeError),
460    #[error(transparent)]
461    LinearizeError(#[from] LinearizeError),
462    #[error("Replacement op for {0} could not be added because {1}")]
463    AddTemplateError(Node, Box<BuildError>),
464}
465
466impl ReplaceTypes {
467    /// Makes a new instance. Unlike [`Self::default`], this does not understand
468    /// any extension types, even those in the prelude.
469    #[must_use]
470    pub fn new_empty() -> Self {
471        Self {
472            type_map: Default::default(),
473            param_types: Default::default(),
474            linearize: DelegatingLinearizer::new_empty(),
475            op_map: Default::default(),
476            param_ops: Default::default(),
477            consts: Default::default(),
478            param_consts: Default::default(),
479            // Not really clear what "preserve" means for a pass that changes signatures,
480            // but default to running on whole hugr not just entrypoint.
481            scope: Either::Left(PassScope::default()),
482        }
483    }
484
485    /// Configures this instance to replace occurrences of type `src` with `dest`.
486    #[deprecated(note = "Use set_replace_type", since = "0.25.0")]
487    pub fn replace_type(&mut self, src: CustomType, dest: Type) {
488        #[expect(deprecated)] // remove together
489        self.replace_type_opts(src, dest, ReplacementOptions::default())
490    }
491
492    /// Configures this instance to replace occurrences of type `src` with `dest`.
493    ///
494    /// `dest` will be recursively transformed by this [ReplaceTypes] before replacement.
495    /// (Cases where a type should be replaced by a type containing an instance of
496    /// the first type, must be handled by two separate [ReplaceTypes]'s via a temporary
497    /// type. )
498    ///
499    /// Note that if `src` is an instance of a *parametrized* [`TypeDef`], this takes
500    /// precedence over [`Self::replace_parametrized_type`] where the `src`s overlap. Thus, this
501    /// should only be used on already-*[monomorphize](crate::MonomorphizePass)d* Hugrs, as
502    /// substitution (parametric polymorphism) happening later will not respect this replacement.
503    ///
504    /// If there are any [`LoadConstant`]s of this type, callers should also call [`Self::replace_consts`]
505    /// (or [`Self::replace_consts_parametrized`]) as the [`LoadConstant`]s will be reparameterized
506    /// (and this will break the edge from [Const] to [`LoadConstant`]).
507    ///
508    /// Note that if `src` is Copyable and `dest` is Linear, then (besides linearity violations)
509    /// [`SignatureError`] will be raised if this leads to an impossible type e.g. ArrayOfCopyables(src).
510    /// (This can be overridden by an additional [`Self::replace_type`].)
511    pub fn set_replace_type(&mut self, src: CustomType, dest: Type) {
512        // We could check that 'dest' is copyable, 'src' is linear, or relevant copy and
513        // discard functions are registered with the linearizer; but since we can't check
514        // that for parametrized types, we'll be consistent and not check here either.
515        self.type_map
516            .insert(src, (dest, ReplacementOptions::recursive()));
517    }
518
519    /// Configures this instance to replace occurrences of type `src` with `dest`,
520    /// according to the given `ReplacementOptions`.
521    #[deprecated(note = "Use set_replace_type", since = "0.25.0")]
522    pub fn replace_type_opts(&mut self, src: CustomType, dest: Type, opts: ReplacementOptions) {
523        self.type_map.insert(src, (dest, opts));
524    }
525
526    /// Configures this instance to change occurrences of a parametrized type `src`
527    /// via a callback that builds the replacement type given the [`TypeArg`]s.
528    #[deprecated(note = "Use set_replace_parametrized_type", since = "0.25.0")]
529    pub fn replace_parametrized_type(
530        &mut self,
531        src: &TypeDef,
532        dest_fn: impl Fn(&[TypeArg]) -> Option<Type> + 'static,
533    ) {
534        #[expect(deprecated)] // remove together
535        self.replace_parametrized_type_opts(src, dest_fn, ReplacementOptions::default())
536    }
537
538    /// Configures this instance to change occurrences of a parametrized type `src`
539    /// via a callback that builds the replacement type given the [`TypeArg`]s.
540    ///
541    /// Note that the `TypeArgs` will already have been updated (e.g. they may not
542    /// fit the bounds of the original type). The callback may return `None` to indicate
543    /// no change (in which case the supplied `TypeArgs` will be given to `src`).
544    /// The returned type will also be subject to recursive processing by this [ReplaceTypes].
545    /// (Cases where a type should be replaced by a type containing an instance of
546    /// the first type, must be handled by two separate [ReplaceTypes]'s via a temporary
547    /// type. )
548    ///
549    /// If there are any [`LoadConstant`]s of any of these types, callers should also call
550    /// [`Self::replace_consts_parametrized`] (or [`Self::replace_consts`]) as the
551    /// [`LoadConstant`]s will be reparameterized (and this will break the edge from [Const] to
552    /// [`LoadConstant`]).
553    /// See [Self::set_replace_type] for more details (including recursion).
554    pub fn set_replace_parametrized_type(
555        &mut self,
556        src: &TypeDef,
557        dest_fn: impl Fn(&[TypeArg]) -> Option<Type> + 'static,
558    ) {
559        // No way to check that dest_fn never produces a linear type.
560        // We could require copy/discard-generators if src is Copyable, or *might be*
561        // (depending on arguments - i.e. if src's TypeDefBound is anything other than
562        // `TypeDefBound::Explicit(TypeBound::Copyable)`) but that seems an annoying
563        // overapproximation. Moreover, these depend upon the *return type* of the Fn.
564        // It would be too awkward to require:
565        // dest_fn: impl Fn(&TypeArg) -> (Type,
566        //                                Fn(&Linearizer) -> NodeTemplate, // copy
567        //                                Fn(&Linearizer) -> NodeTemplate)` // discard
568        self.param_types.insert(
569            src.into(),
570            (Arc::new(dest_fn), ReplacementOptions::recursive()),
571        );
572    }
573
574    /// Configures this instance to change occurrences of a parametrized type `src`
575    /// via a callback that builds the replacement type given the [`TypeArg`]s,
576    /// and using the given [ReplacementOptions].
577    #[deprecated(note = "Use set_replace_parametrized_type", since = "0.25.0")]
578    pub fn replace_parametrized_type_opts(
579        &mut self,
580        src: &TypeDef,
581        dest_fn: impl Fn(&[TypeArg]) -> Option<Type> + 'static,
582        opts: ReplacementOptions,
583    ) {
584        self.param_types
585            .insert(src.into(), (Arc::new(dest_fn), opts));
586    }
587
588    /// Allows to configure how to deal with types/wires that were `Copyable`
589    /// but have become linear as a result of type-changing.
590    #[deprecated(note = "Use get_linearizer or linearizer_mut", since = "0.25.0")]
591    pub fn linearizer(&mut self) -> &mut DelegatingLinearizer {
592        &mut self.linearize
593    }
594
595    /// Allows to configure how to deal with types/wires that were [Copyable]
596    /// but have become linear as a result of type-changing. Specifically,
597    /// the [Linearizer] is used whenever lowering produces an outport which both
598    /// * has a non-[Copyable] type - perhaps a direct substitution, or perhaps e.g.
599    ///   as a result of changing the element type of a collection such as an [`array`]
600    /// * has other than one connected inport,
601    ///
602    /// [Copyable]: hugr_core::types::TypeBound::Copyable
603    /// [`array`]: hugr_core::std_extensions::collections::array::array_type
604    pub fn linearizer_mut(&mut self) -> &mut DelegatingLinearizer {
605        &mut self.linearize
606    }
607
608    /// Allows use of the linearizer (e.g. in a callback passed to
609    /// [Self::set_replace_parametrized_op])
610    pub fn get_linearizer(&self) -> &impl Linearizer {
611        &self.linearize
612    }
613
614    /// Configures this instance to change occurrences of `src` to `dest`.
615    #[deprecated(note = "Use set_replace_op", since = "0.25.0")]
616    pub fn replace_op(&mut self, src: &ExtensionOp, dest: NodeTemplate) {
617        #[expect(deprecated)] // remove together
618        self.replace_op_with(src, dest, ReplacementOptions::default())
619    }
620
621    /// Configures this instance to change occurrences of `src` to `dest`.
622    ///
623    /// The RHS will be recursively processed by this [ReplaceTypes].
624    /// (Cases where an op should be replaced by a container including an
625    /// instance of the same op, must be handled by two separate [ReplaceTypes]'s
626    /// via a temporary op.)
627    ///
628    /// Note that if `src` is an instance of a *parametrized* [`OpDef`], this takes
629    /// precedence over [`Self::set_replace_parametrized_op`] where the `src`s overlap.
630    /// Thus, this method should only be used for already-*[monomorphize](crate::MonomorphizePass)d*
631    /// Hugrs, as substitution (parametric polymorphism) happening later will not respect
632    /// this replacement.
633    pub fn set_replace_op(&mut self, src: &ExtensionOp, dest: NodeTemplate) {
634        self.op_map.insert(
635            OpHashWrapper::from(src),
636            (dest, ReplacementOptions::recursive()),
637        );
638    }
639
640    /// Configures this instance to change occurrences of `src` to `dest`.
641    #[deprecated(note = "Use set_replace_op", since = "0.25.0")]
642    pub fn replace_op_with(
643        &mut self,
644        src: &ExtensionOp,
645        dest: NodeTemplate,
646        opts: ReplacementOptions,
647    ) {
648        self.op_map.insert(OpHashWrapper::from(src), (dest, opts));
649    }
650
651    /// Configures this instance to change occurrences of a parametrized op `src`
652    /// via a callback that builds the replacement type given the [`TypeArg`]s.
653    #[deprecated(note = "Use set_replace_parametrized_op", since = "0.25.0")]
654    pub fn replace_parametrized_op(
655        &mut self,
656        src: &OpDef,
657        dest_fn: impl Fn(&[TypeArg]) -> Option<NodeTemplate> + 'static,
658    ) {
659        #[expect(deprecated)] // remove together
660        self.replace_parametrized_op_with(src, dest_fn, ReplacementOptions::default())
661    }
662
663    /// Configures this instance to change occurrences of a parametrized op `src`
664    /// via a callback that builds the replacement type given the [`TypeArg`]s.
665    /// Note that the `TypeArgs` will already have been updated (e.g. they may not
666    /// fit the bounds of the original op); and the returned [NodeTemplate] will be
667    /// recursively processed by this [ReplaceTypes]. (Cases where an op should be
668    /// replaced by a container including an instance of the same op, must be handled
669    /// by two separate [ReplaceTypes]'s via a temporary op.)
670    ///
671    /// If the Callback returns None, the new typeargs will be applied to the original op.
672    pub fn set_replace_parametrized_op(
673        &mut self,
674        src: &OpDef,
675        dest_fn: impl Fn(&[TypeArg], &ReplaceTypes) -> Result<Option<NodeTemplate>, ReplaceTypesError>
676        + 'static,
677    ) {
678        self.param_ops.insert(
679            src.into(),
680            (Arc::new(dest_fn), ReplacementOptions::recursive()),
681        );
682    }
683
684    /// Configures this instance to change occurrences of a parametrized op `src`
685    /// via a callback that builds the replacement type given the [`TypeArg`]s.
686    #[deprecated(note = "Use set_replace_parametrized_op", since = "0.25.0")]
687    pub fn replace_parametrized_op_with(
688        &mut self,
689        src: &OpDef,
690        dest_fn: impl Fn(&[TypeArg]) -> Option<NodeTemplate> + 'static,
691        opts: ReplacementOptions,
692    ) {
693        self.param_ops.insert(
694            src.into(),
695            (Arc::new(move |args, _| Ok(dest_fn(args))), opts),
696        );
697    }
698
699    /// Configures this instance to change [Const]s of type `src_ty`, using
700    /// a callback that is passed the value of the constant (of that type).
701    ///
702    /// Note that if `src_ty` is an instance of a *parametrized* [`TypeDef`],
703    /// this takes precedence over [`Self::replace_consts_parametrized`] where
704    /// the `src_ty`s overlap.
705    pub fn replace_consts(
706        &mut self,
707        src_ty: CustomType,
708        const_fn: impl Fn(&OpaqueValue, &ReplaceTypes) -> Result<Value, ReplaceTypesError> + 'static,
709    ) {
710        self.consts.insert(src_ty, Arc::new(const_fn));
711    }
712
713    /// Configures this instance to change [Const]s of all types that are instances
714    /// of a parametrized typedef `src_ty`, using a callback that is passed the
715    /// value of the constant (the [`OpaqueValue`] contains the [`TypeArg`]s). The
716    /// callback may return `None` to indicate no change to the constant.
717    pub fn replace_consts_parametrized(
718        &mut self,
719        src_ty: &TypeDef,
720        const_fn: impl Fn(&OpaqueValue, &ReplaceTypes) -> Result<Option<Value>, ReplaceTypesError>
721        + 'static,
722    ) {
723        self.param_consts.insert(src_ty.into(), Arc::new(const_fn));
724    }
725
726    /// Set the regions of the Hugr to which this pass should be applied.
727    ///
728    /// If not set, the pass is applied to the whole Hugr.
729    /// Each call overwrites any previous calls to `set_regions` and/or [Self::with_scope].
730    pub fn set_regions(&mut self, regions: impl IntoIterator<Item = Node>) {
731        self.scope = Either::Right(regions.into_iter().collect());
732    }
733
734    fn process_subtree_opts(
735        &self,
736        hugr: &mut impl HugrMut<Node = Node>,
737        root: Node,
738        opts: &ReplacementOptions,
739    ) -> Result<(), ReplaceTypesError> {
740        if opts.process_recursive {
741            self.change_subtree(hugr, root, opts.linearize_unchanged)?;
742            // change_subtree does not linearize its root, just as change_node
743            // does not linearize the node it's called on; our caller does.
744        } else if opts.linearize_unchanged {
745            let mut descs = hugr.descendants(root);
746            assert_eq!(descs.next(), Some(root));
747            for n in descs.collect::<Vec<_>>() {
748                self.linearize_outputs(hugr, n)?;
749            }
750        }
751        Ok(())
752    }
753
754    fn change_subtree(
755        &self,
756        hugr: &mut impl HugrMut<Node = Node>,
757        root: Node,
758        linearize_unchanged_ops: bool,
759    ) -> Result<bool, ReplaceTypesError> {
760        let mut descs = hugr.descendants(root).collect::<Vec<_>>().into_iter();
761        assert_eq!(descs.next(), Some(root));
762        let mut changed = self.change_node(hugr, root)?;
763        // Do not linearize the root's outputs - that's done by the caller if appropriate,
764        // as any copy/discard ops would be *outside* the root
765        for n in descs {
766            if self.change_node(hugr, n)? {
767                changed = true;
768            } else if !linearize_unchanged_ops {
769                continue;
770            }
771            self.linearize_outputs(hugr, n)?;
772        }
773        Ok(changed)
774    }
775
776    fn change_node(
777        &self,
778        hugr: &mut impl HugrMut<Node = Node>,
779        n: Node,
780    ) -> Result<bool, ReplaceTypesError> {
781        match hugr.optype_mut(n) {
782            OpType::FuncDefn(fd) => fd.signature_mut().body_mut().transform(self),
783            OpType::FuncDecl(fd) => fd.signature_mut().body_mut().transform(self),
784            OpType::LoadConstant(LoadConstant { datatype: ty })
785            | OpType::AliasDefn(AliasDefn { definition: ty, .. }) => ty.transform(self),
786
787            OpType::ExitBlock(ExitBlock { cfg_outputs: types })
788            | OpType::Input(Input { types })
789            | OpType::Output(Output { types }) => types.transform(self),
790            OpType::LoadFunction(LoadFunction {
791                func_sig,
792                type_args,
793                instantiation,
794            })
795            | OpType::Call(Call {
796                func_sig,
797                type_args,
798                instantiation,
799            }) => {
800                let change = func_sig.body_mut().transform(self)? | type_args.transform(self)?;
801                if change {
802                    let new_inst = func_sig
803                        .instantiate(type_args)
804                        .map_err(ReplaceTypesError::SignatureError)?;
805                    *instantiation = new_inst;
806                }
807                Ok(change)
808            }
809            OpType::Case(Case { signature })
810            | OpType::CFG(CFG { signature })
811            | OpType::DFG(DFG { signature })
812            | OpType::CallIndirect(CallIndirect { signature }) => signature.transform(self),
813            OpType::Tag(Tag { variants, .. }) => variants.transform(self),
814            OpType::Conditional(Conditional {
815                other_inputs: row1,
816                outputs: row2,
817                sum_rows,
818                ..
819            })
820            | OpType::DataflowBlock(DataflowBlock {
821                inputs: row1,
822                other_outputs: row2,
823                sum_rows,
824                ..
825            }) => Ok(row1.transform(self)? | row2.transform(self)? | sum_rows.transform(self)?),
826            OpType::TailLoop(TailLoop {
827                just_inputs,
828                just_outputs,
829                rest,
830                ..
831            }) => Ok(just_inputs.transform(self)?
832                | just_outputs.transform(self)?
833                | rest.transform(self)?),
834
835            OpType::Const(Const { value, .. }) => self.change_value(value),
836            OpType::ExtensionOp(ext_op) => Ok({
837                let def = ext_op.def_arc();
838                let mut changed = false;
839                let replacement = match self.op_map.get(&OpHashWrapper::from(&*ext_op)) {
840                    r @ Some(_) => r.cloned(),
841                    None => {
842                        let mut args = ext_op.args().to_vec();
843                        changed = args.transform(self)?;
844                        let r2 = match self.param_ops.get(&def.as_ref().into()) {
845                            None => None,
846                            Some((rep_fn, opts)) => {
847                                rep_fn(&args, self)?.map(|nt| (nt, opts.clone()))
848                            }
849                        };
850                        if r2.is_none() && changed {
851                            *ext_op = ExtensionOp::new(def.clone(), args)?;
852                        }
853                        r2
854                    }
855                };
856                if let Some((replacement, opts)) = replacement {
857                    replacement.replace(hugr, n, self, &opts)?;
858                    true
859                } else {
860                    changed
861                }
862            }),
863
864            OpType::OpaqueOp(_) => panic!("OpaqueOp should not be in a Hugr"),
865
866            OpType::AliasDecl(_) | OpType::Module(_) => Ok(false),
867            _ => todo!(),
868        }
869    }
870
871    /// Modifies the specified Value in-place according to current configuration.
872    /// Returns whether the value has changed (conservative over-approximation).
873    pub fn change_value(&self, value: &mut Value) -> Result<bool, ReplaceTypesError> {
874        match value {
875            Value::Sum(Sum {
876                values, sum_type, ..
877            }) => {
878                let mut any_change = false;
879                for value in values {
880                    any_change |= self.change_value(value)?;
881                }
882                any_change |= sum_type.transform(self)?;
883                Ok(any_change)
884            }
885            Value::Extension { e } => Ok({
886                let new_const = match e.get_type().as_type_enum() {
887                    TypeEnum::Extension(exty) => match self.consts.get(exty) {
888                        Some(const_fn) => Some(const_fn(e, self)),
889                        None => self
890                            .param_consts
891                            .get(&exty.into())
892                            .and_then(|const_fn| const_fn(e, self).transpose()),
893                    },
894                    _ => None,
895                };
896                if let Some(new_const) = new_const {
897                    *value = new_const?;
898                    true
899                } else {
900                    false
901                }
902            }),
903        }
904    }
905
906    fn linearize_outputs<H: HugrMut<Node = Node>>(
907        &self,
908        hugr: &mut H,
909        n: H::Node,
910    ) -> Result<(), LinearizeError> {
911        if let Some(new_sig) = hugr.get_optype(n).dataflow_signature() {
912            let new_sig = new_sig.into_owned();
913            for outp in new_sig.output_ports() {
914                if !new_sig.out_port_type(outp).unwrap().copyable() {
915                    let targets = hugr.linked_inputs(n, outp).collect::<Vec<_>>();
916                    if targets.len() != 1 {
917                        hugr.disconnect(n, outp);
918                        let src = Wire::new(n, outp);
919                        self.linearize.insert_copy_discard(hugr, src, &targets)?;
920                    }
921                }
922            }
923        }
924        Ok(())
925    }
926}
927
928impl<H: HugrMut<Node = Node>> ComposablePass<H> for ReplaceTypes {
929    type Error = ReplaceTypesError;
930    type Result = bool;
931
932    fn run(&self, hugr: &mut H) -> Result<bool, ReplaceTypesError> {
933        let temp: Vec<Node>; // keep alive
934        let regions = match &self.scope {
935            Either::Left(scope) => {
936                temp = Vec::from_iter(scope.root(hugr));
937                &temp
938            }
939            Either::Right(regs) => regs,
940        };
941        let mut changed = false;
942        for region_root in regions {
943            changed |= self.change_subtree(hugr, *region_root, false)?;
944        }
945        Ok(changed)
946    }
947}
948
949impl WithScope for ReplaceTypes {
950    /// Sets the scope within which the pass will operate. Note that this pass ignores
951    /// * [PassScope::preserve_interface], as this is a lowering pass: its purpose is to
952    ///   change node signatures.
953    /// * [PassScope::recursive], as non-recursion generally leads to invalid Hugrs.
954    ///
955    /// Hence, really only the [PassScope::root] affects the pass.
956    fn with_scope(mut self, scope: impl Into<PassScope>) -> Self {
957        self.scope = Either::Left(scope.into());
958        self
959    }
960}
961
962#[deprecated(
963    note = "`hugr-passes` is deprecated. Use tket::passes instead",
964    since = "0.26.2"
965)]
966pub mod handlers;
967
968#[derive(Clone, Hash, PartialEq, Eq)]
969struct OpHashWrapper {
970    op_name: String, // Only because SmolStr not in hugr-passes yet
971    args: Vec<TypeArg>,
972}
973
974impl From<&ExtensionOp> for OpHashWrapper {
975    fn from(op: &ExtensionOp) -> Self {
976        Self {
977            op_name: op.qualified_id().to_string(),
978            args: op.args().to_vec(),
979        }
980    }
981}
982
983#[derive(Clone, Debug, PartialEq, Eq, Hash)]
984struct ParametricType(ExtensionId, String);
985
986impl From<&TypeDef> for ParametricType {
987    fn from(value: &TypeDef) -> Self {
988        Self(value.extension_id().clone(), value.name().to_string())
989    }
990}
991
992impl From<&CustomType> for ParametricType {
993    fn from(value: &CustomType) -> Self {
994        Self(value.extension().clone(), value.name().to_string())
995    }
996}
997
998// Separate from above for clarity
999#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1000struct ParametricOp(ExtensionId, String);
1001
1002impl From<&OpDef> for ParametricOp {
1003    fn from(value: &OpDef) -> Self {
1004        Self(value.extension_id().clone(), value.name().to_string())
1005    }
1006}
1007
1008#[cfg(test)]
1009mod test {
1010    use std::sync::Arc;
1011
1012    use crate::replace_types::handlers::generic_array_const;
1013    use hugr_core::builder::{
1014        BuildError, Container, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer,
1015        FunctionBuilder, HugrBuilder, ModuleBuilder, SubContainer, TailLoopBuilder, endo_sig,
1016        inout_sig,
1017    };
1018    use hugr_core::extension::SignatureError;
1019    use hugr_core::extension::prelude::{
1020        ConstUsize, Noop, UnwrapBuilder, bool_t, option_type, qb_t, usize_t,
1021    };
1022    use hugr_core::extension::simple_op::{MakeOpDef, MakeRegisteredOp};
1023    use hugr_core::extension::{TypeDefBound, Version, simple_op::MakeExtensionOp};
1024    use hugr_core::hugr::{IdentList, ValidationError, hugrmut::HugrMut};
1025    use hugr_core::ops::constant::{CustomConst, OpaqueValue};
1026    use hugr_core::ops::{self, ExtensionOp, OpTrait, OpType, Tag, Value, handle::NodeHandle};
1027    use hugr_core::std_extensions::arithmetic::conversions::ConvertOpDef;
1028    use hugr_core::std_extensions::arithmetic::int_types::{ConstInt, INT_TYPES};
1029    use hugr_core::std_extensions::collections::array::{
1030        self, Array, ArrayKind, ArrayOpDef, GenericArrayValue, array_type, array_type_def,
1031    };
1032    use hugr_core::std_extensions::collections::borrow_array::{
1033        BArrayValue, BorrowArray, borrow_array_type,
1034    };
1035    use hugr_core::std_extensions::collections::list::{
1036        ListOp, ListOpInst, ListValue, list_type, list_type_def,
1037    };
1038    use hugr_core::types::{
1039        EdgeKind, PolyFuncType, Signature, SumType, Term, Type, TypeArg, TypeBound, TypeRow,
1040    };
1041    use hugr_core::{Direction, Extension, HugrView, Port, Visibility, type_row};
1042    use itertools::Itertools;
1043    use rstest::rstest;
1044
1045    use crate::{ComposablePass, mangle_name};
1046
1047    use super::{NodeTemplate, ReplaceTypes, handlers::list_const};
1048
1049    const PACKED_VEC: &str = "PackedVec";
1050    const READ: &str = "read";
1051
1052    fn i64_t() -> Type {
1053        INT_TYPES[6].clone()
1054    }
1055
1056    fn read_op(ext: &Arc<Extension>, t: Type) -> ExtensionOp {
1057        ExtensionOp::new(ext.get_op(READ).unwrap().clone(), [t.into()]).unwrap()
1058    }
1059
1060    fn just_elem_type(args: &[TypeArg]) -> &Type {
1061        let [TypeArg::Runtime(ty)] = args else {
1062            panic!("Expected just elem type")
1063        };
1064        ty
1065    }
1066
1067    fn ext() -> Arc<Extension> {
1068        Extension::new_arc(
1069            IdentList::new("TestExt").unwrap(),
1070            Version::new(0, 0, 1),
1071            |ext, w| {
1072                let pv_of_var = ext
1073                    .add_type(
1074                        PACKED_VEC.into(),
1075                        vec![TypeBound::Linear.into()],
1076                        String::new(),
1077                        TypeDefBound::from_params(vec![0]),
1078                        w,
1079                    )
1080                    .unwrap()
1081                    .instantiate(vec![Type::new_var_use(0, TypeBound::Copyable).into()])
1082                    .unwrap();
1083                ext.add_op(
1084                    READ.into(),
1085                    String::new(),
1086                    PolyFuncType::new(
1087                        vec![TypeBound::Copyable.into()],
1088                        Signature::new(
1089                            vec![pv_of_var.into(), i64_t()],
1090                            [Type::new_var_use(0, TypeBound::Linear)],
1091                        ),
1092                    ),
1093                    w,
1094                )
1095                .unwrap();
1096                ext.add_op(
1097                    "lowered_read_bool".into(),
1098                    String::new(),
1099                    Signature::new(vec![i64_t(); 2], [bool_t()]),
1100                    w,
1101                )
1102                .unwrap();
1103            },
1104        )
1105    }
1106
1107    fn lowered_read<T: Container + Dataflow>(
1108        elem_ty: Type,
1109        new: impl Fn(Signature) -> Result<T, BuildError>,
1110    ) -> T {
1111        let mut dfb = new(Signature::new(
1112            [list_type(elem_ty.clone()), i64_t()],
1113            [elem_ty.clone()],
1114        ))
1115        .unwrap();
1116        let [val, idx] = dfb.input_wires_arr();
1117        let [idx] = dfb
1118            .add_dataflow_op(ConvertOpDef::itousize.without_log_width(), [idx])
1119            .unwrap()
1120            .outputs_arr();
1121        let [opt] = dfb
1122            .add_dataflow_op(
1123                ListOp::get
1124                    .with_type(elem_ty.clone())
1125                    .to_extension_op()
1126                    .unwrap(),
1127                [val, idx],
1128            )
1129            .unwrap()
1130            .outputs_arr();
1131        let [res] = dfb
1132            .build_unwrap_sum(1, option_type([Type::from(elem_ty)]), opt)
1133            .unwrap();
1134        dfb.set_outputs([res]).unwrap();
1135        dfb
1136    }
1137
1138    fn lowerer(ext: &Arc<Extension>) -> ReplaceTypes {
1139        let pv = ext.get_type(PACKED_VEC).unwrap();
1140        let mut lw = ReplaceTypes::default();
1141        lw.set_replace_type(pv.instantiate([bool_t().into()]).unwrap(), i64_t());
1142        lw.set_replace_parametrized_type(
1143            pv,
1144            Box::new(|args: &[TypeArg]| Some(list_type(just_elem_type(args).clone()))),
1145        );
1146        lw.set_replace_op(
1147            &read_op(ext, bool_t()),
1148            NodeTemplate::SingleOp(
1149                ExtensionOp::new(ext.get_op("lowered_read_bool").unwrap().clone(), [])
1150                    .unwrap()
1151                    .into(),
1152            ),
1153        );
1154        lw.set_replace_parametrized_op(ext.get_op(READ).unwrap().as_ref(), |type_args, _| {
1155            Ok(Some(NodeTemplate::CompoundOp(Box::new(
1156                lowered_read(just_elem_type(type_args).clone(), DFGBuilder::new)
1157                    .finish_hugr()
1158                    .unwrap(),
1159            ))))
1160        });
1161        lw
1162    }
1163
1164    #[test]
1165    fn module_func_cfg_call() {
1166        let ext = ext();
1167        let coln = ext.get_type(PACKED_VEC).unwrap();
1168        let c_int = Type::from(coln.instantiate([i64_t().into()]).unwrap());
1169        let c_bool = Type::from(coln.instantiate([bool_t().into()]).unwrap());
1170        let mut mb = ModuleBuilder::new();
1171        let sig = Signature::new_endo([Type::new_var_use(0, TypeBound::Linear)]);
1172        let fb = mb
1173            .define_function("id", PolyFuncType::new([TypeBound::Linear.into()], sig))
1174            .unwrap();
1175        let inps = fb.input_wires();
1176        let id = fb.finish_with_outputs(inps).unwrap();
1177
1178        let sig = Signature::new([i64_t(), c_int.clone(), c_bool.clone()], [bool_t()]);
1179        let mut fb = mb.define_function("main", sig).unwrap();
1180        let [idx, indices, bools] = fb.input_wires_arr();
1181        let [indices] = fb
1182            .call(id.handle(), &[c_int.into()], [indices])
1183            .unwrap()
1184            .outputs_arr();
1185        let [idx2] = fb
1186            .add_dataflow_op(read_op(&ext, i64_t()), [indices, idx])
1187            .unwrap()
1188            .outputs_arr();
1189        let mut cfg = fb
1190            .cfg_builder(
1191                [(i64_t(), idx2), (c_bool.clone(), bools)],
1192                [bool_t()].into(),
1193            )
1194            .unwrap();
1195        let mut entry = cfg.entry_builder([[bool_t()].into()], type_row![]).unwrap();
1196        let [idx2, bools] = entry.input_wires_arr();
1197        let [bools] = entry
1198            .call(id.handle(), &[c_bool.into()], [bools])
1199            .unwrap()
1200            .outputs_arr();
1201        let bool_read_op = entry
1202            .add_dataflow_op(read_op(&ext, bool_t()), [bools, idx2])
1203            .unwrap();
1204        let [tagged] = entry
1205            .add_dataflow_op(
1206                OpType::Tag(Tag::new(0, vec![[bool_t()].into()])),
1207                bool_read_op.outputs(),
1208            )
1209            .unwrap()
1210            .outputs_arr();
1211        let entry = entry.finish_with_outputs(tagged, []).unwrap();
1212        cfg.branch(&entry, 0, &cfg.exit_block()).unwrap();
1213        let cfg = cfg.finish_sub_container().unwrap();
1214        fb.finish_with_outputs(cfg.outputs()).unwrap();
1215        let mut h = mb.finish_hugr().unwrap();
1216
1217        assert!(lowerer(&ext).run(&mut h).unwrap());
1218
1219        let ext_ops = h
1220            .entry_descendants()
1221            .filter_map(|n| h.get_optype(n).as_extension_op());
1222        assert_eq!(
1223            ext_ops
1224                .map(hugr_core::ops::ExtensionOp::unqualified_id)
1225                .sorted()
1226                .collect_vec(),
1227            ["get", "itousize", "lowered_read_bool", "panic",]
1228        );
1229    }
1230
1231    #[test]
1232    fn dfg_conditional_case() {
1233        let ext = ext();
1234        let coln = ext.get_type(PACKED_VEC).unwrap();
1235        let pv = |t: Type| Type::new_extension(coln.instantiate([t.into()]).unwrap());
1236        let sum_rows = [[pv(pv(bool_t())), i64_t()].into(), [pv(i64_t())].into()];
1237        let mut dfb = DFGBuilder::new(inout_sig(
1238            vec![Type::new_sum(sum_rows.clone()), pv(bool_t()), pv(i64_t())],
1239            vec![pv(bool_t()), pv(i64_t())],
1240        ))
1241        .unwrap();
1242        let [sum, vb, vi] = dfb.input_wires_arr();
1243        let mut cb = dfb
1244            .conditional_builder(
1245                (sum_rows, sum),
1246                [(pv(bool_t()), vb), (pv(i64_t()), vi)],
1247                vec![pv(bool_t()), pv(i64_t())].into(),
1248            )
1249            .unwrap();
1250        let mut case0 = cb.case_builder(0).unwrap();
1251        let [vvb, i, _, vi0] = case0.input_wires_arr();
1252        let [vb0] = case0
1253            .add_dataflow_op(read_op(&ext, pv(bool_t())), [vvb, i])
1254            .unwrap()
1255            .outputs_arr();
1256        case0.finish_with_outputs([vb0, vi0]).unwrap();
1257
1258        let case1 = cb.case_builder(1).unwrap();
1259        let [vi, vb1, _vi1] = case1.input_wires_arr();
1260        case1.finish_with_outputs([vb1, vi]).unwrap();
1261        let cond = cb.finish_sub_container().unwrap();
1262        let mut h = dfb.finish_hugr_with_outputs(cond.outputs()).unwrap();
1263
1264        lowerer(&ext).run(&mut h).unwrap();
1265
1266        let ext_ops = h
1267            .entry_descendants()
1268            .filter_map(|n| h.get_optype(n).as_extension_op())
1269            .collect_vec();
1270        assert_eq!(
1271            ext_ops
1272                .iter()
1273                .map(|x| x.unqualified_id())
1274                .sorted()
1275                .collect_vec(),
1276            ["get", "itousize", "panic"]
1277        );
1278        // The PackedVec<PackedVec<bool>> becomes a list<i64>
1279        let array_gets = ext_ops
1280            .into_iter()
1281            .filter_map(|e| ListOpInst::from_extension_op(e).ok())
1282            .collect_vec();
1283        assert_eq!(array_gets, [ListOp::get.with_type(i64_t())]);
1284    }
1285
1286    #[test]
1287    fn loop_const() {
1288        let cu = |u| ConstUsize::new(u).into();
1289        let mut tl = TailLoopBuilder::new(
1290            [list_type(usize_t())],
1291            [list_type(bool_t())],
1292            [list_type(usize_t())],
1293        )
1294        .unwrap();
1295        let [_, bools] = tl.input_wires_arr();
1296        let st = SumType::new(vec![[list_type(usize_t())]; 2]);
1297        let pred = tl.add_load_value(
1298            Value::sum(
1299                0,
1300                [ListValue::new(usize_t(), [cu(1), cu(3), cu(3), cu(7)]).into()],
1301                st,
1302            )
1303            .unwrap(),
1304        );
1305        tl.set_outputs(pred, [bools]).unwrap();
1306        let backup = tl.finish_hugr().unwrap();
1307
1308        let mut lowerer = ReplaceTypes::default();
1309
1310        // 1. Lower List<T> to BArray<10, T> UNLESS T is usize_t() or i64_t
1311        lowerer.set_replace_parametrized_type(list_type_def(), |args| {
1312            let ty = just_elem_type(args);
1313            (![usize_t(), i64_t()].contains(ty)).then_some(borrow_array_type(10, ty.clone()))
1314        });
1315        {
1316            let mut h = backup.clone();
1317            assert_eq!(lowerer.run(&mut h), Ok(true));
1318            let sig = h.signature(h.entrypoint()).unwrap();
1319            assert_eq!(
1320                sig.input(),
1321                &TypeRow::from(vec![list_type(usize_t()), borrow_array_type(10, bool_t())])
1322            );
1323            assert_eq!(sig.input(), sig.output());
1324        }
1325
1326        // 2. Now we'll also change usize's to i64_t's
1327        let usize_custom_t = usize_t().as_extension().unwrap().clone();
1328        lowerer.set_replace_type(usize_custom_t.clone(), i64_t());
1329        lowerer.replace_consts(usize_custom_t, |opaq, _| {
1330            Ok(ConstInt::new_u(
1331                6,
1332                opaq.value().downcast_ref::<ConstUsize>().unwrap().value(),
1333            )
1334            .unwrap()
1335            .into())
1336        });
1337        {
1338            let mut h = backup.clone();
1339            assert_eq!(lowerer.run(&mut h), Ok(true));
1340            let sig = h.signature(h.entrypoint()).unwrap();
1341            assert_eq!(
1342                sig.input(),
1343                &TypeRow::from(vec![list_type(i64_t()), borrow_array_type(10, bool_t())])
1344            );
1345            assert_eq!(sig.input(), sig.output());
1346            // This will have to update inside the Const
1347            let cst = h
1348                .entry_descendants()
1349                .filter_map(|n| h.get_optype(n).as_const())
1350                .exactly_one()
1351                .ok()
1352                .unwrap();
1353            assert_eq!(cst.get_type(), Type::new_sum(vec![[list_type(i64_t())]; 2]));
1354        }
1355
1356        // 3. Lower all List<T> to BArray<4,T>
1357        let mut h = backup;
1358        lowerer.set_replace_parametrized_type(
1359            list_type_def(),
1360            Box::new(|args: &[TypeArg]| Some(borrow_array_type(4, just_elem_type(args).clone()))),
1361        );
1362        lowerer.replace_consts_parametrized(list_type_def(), |opaq, repl| {
1363            // First recursively transform the contents
1364            let Some(Value::Extension { e: opaq }) = list_const(opaq, repl)? else {
1365                panic!("Expected list value to stay a list value");
1366            };
1367            let lv = opaq.value().downcast_ref::<ListValue>().unwrap();
1368
1369            Ok(Some(
1370                BArrayValue::new(lv.get_element_type().clone(), lv.get_contents().to_vec()).into(),
1371            ))
1372        });
1373        lowerer.run(&mut h).unwrap();
1374
1375        assert_eq!(
1376            h.get_optype(pred.node())
1377                .as_load_constant()
1378                .map(hugr_core::ops::LoadConstant::constant_type),
1379            Some(&Type::new_sum(vec![
1380                [Type::from(borrow_array_type(
1381                    4,
1382                    i64_t()
1383                ))];
1384                2
1385            ]))
1386        );
1387    }
1388
1389    #[test]
1390    fn partial_replace() {
1391        let e = Extension::new_arc(
1392            IdentList::new_unchecked("NoBoundsCheck"),
1393            Version::new(0, 0, 0),
1394            |e, w| {
1395                let params = vec![TypeBound::Linear.into()];
1396                let tv = Type::new_var_use(0, TypeBound::Linear);
1397                let list_of_var = list_type(tv.clone());
1398                e.add_op(
1399                    READ.into(),
1400                    "Like List::get but without the option".to_string(),
1401                    PolyFuncType::new(params, Signature::new([list_of_var, usize_t()], [tv])),
1402                    w,
1403                )
1404                .unwrap();
1405            },
1406        );
1407        fn option_contents(ty: &Type) -> Option<Type> {
1408            let row = ty.as_sum()?.get_variant(1).unwrap().clone();
1409            let elem = row.into_owned().into_iter().exactly_one().unwrap();
1410            Some(elem.try_into_type().unwrap())
1411        }
1412        let i32_t = || INT_TYPES[5].clone();
1413        let opt_i32 = Type::from(option_type([i32_t()]));
1414        let i32_custom_t = i32_t().as_extension().unwrap().clone();
1415        let mut dfb = DFGBuilder::new(inout_sig(
1416            vec![list_type(i32_t()), list_type(opt_i32.clone())],
1417            vec![i32_t(), opt_i32.clone()],
1418        ))
1419        .unwrap();
1420        let [l_i, l_oi] = dfb.input_wires_arr();
1421        let idx = dfb.add_load_value(ConstUsize::new(2));
1422        let [i] = dfb
1423            .add_dataflow_op(read_op(&e, i32_t()), [l_i, idx])
1424            .unwrap()
1425            .outputs_arr();
1426        let [oi] = dfb
1427            .add_dataflow_op(read_op(&e, opt_i32.clone()), [l_oi, idx])
1428            .unwrap()
1429            .outputs_arr();
1430        let mut h = dfb.finish_hugr_with_outputs([i, oi]).unwrap();
1431
1432        let mut lowerer = ReplaceTypes::default();
1433        lowerer.set_replace_type(i32_custom_t, qb_t());
1434        // Lower list<option<x>> to list<x>
1435        lowerer.set_replace_parametrized_type(list_type_def(), |args| {
1436            option_contents(just_elem_type(args)).map(list_type)
1437        });
1438        // and read<option<x>> to get<x> - the latter has the expected option<x> return type
1439        lowerer.set_replace_parametrized_op(e.get_op(READ).unwrap().as_ref(), |args, _| {
1440            Ok(option_contents(just_elem_type(args)).map(|elem| {
1441                NodeTemplate::SingleOp(
1442                    ListOp::get
1443                        .with_type(elem)
1444                        .to_extension_op()
1445                        .unwrap()
1446                        .into(),
1447                )
1448            }))
1449        });
1450        assert!(lowerer.run(&mut h).unwrap());
1451        // list<usz>      -> read<usz>      -> usz just becomes list<qb> -> read<qb> -> qb
1452        // list<opt<usz>> -> read<opt<usz>> -> opt<usz> becomes list<qb> -> get<qb>  -> opt<qb>
1453        assert_eq!(
1454            h.entrypoint_optype().dataflow_signature().unwrap().io(),
1455            (
1456                &vec![list_type(qb_t()); 2].into(),
1457                &vec![qb_t(), option_type([qb_t()]).into()].into()
1458            )
1459        );
1460        assert_eq!(
1461            h.entry_descendants()
1462                .filter_map(|n| h.get_optype(n).as_extension_op())
1463                .map(hugr_core::ops::ExtensionOp::qualified_id)
1464                .sorted()
1465                .collect_vec(),
1466            ["NoBoundsCheck.read", "collections.list.get"]
1467        );
1468    }
1469
1470    #[rstest]
1471    #[case(&[], Array)]
1472    #[case(&[3], Array)]
1473    #[case(&[5,7,11,13,17,19], BorrowArray)]
1474    fn array_const<AK: ArrayKind>(#[case] vals: &[u64], #[case] _kind: AK)
1475    where
1476        GenericArrayValue<AK>: CustomConst,
1477    {
1478        let mut dfb =
1479            DFGBuilder::new(inout_sig(type_row![], [AK::ty(vals.len() as _, usize_t())])).unwrap();
1480        let c = dfb.add_load_value(GenericArrayValue::<AK>::new(
1481            usize_t(),
1482            vals.iter().map(|u| ConstUsize::new(*u).into()),
1483        ));
1484        let backup = dfb.finish_hugr_with_outputs([c]).unwrap();
1485
1486        let mut repl = ReplaceTypes::new_empty();
1487        let usize_custom_t = usize_t().as_extension().unwrap().clone();
1488        repl.set_replace_type(usize_custom_t.clone(), INT_TYPES[6].clone());
1489        repl.replace_consts(usize_custom_t, |cst: &OpaqueValue, _| {
1490            let cu = cst.value().downcast_ref::<ConstUsize>().unwrap();
1491            Ok(ConstInt::new_u(6, cu.value())?.into())
1492        });
1493
1494        let mut h = backup.clone();
1495        repl.run(&mut h).unwrap(); // No validation here
1496        assert!(
1497            matches!(h.validate(), Err(ValidationError::IncompatiblePorts {from, to, ..})
1498             if backup.get_optype(from).is_const() && to == c.node())
1499        );
1500        repl.replace_consts_parametrized(AK::type_def(), generic_array_const::<AK>);
1501        let mut h = backup;
1502        repl.run(&mut h).unwrap();
1503        h.validate().unwrap();
1504    }
1505
1506    #[rstest]
1507    fn op_to_call_polymorphic(#[values(true, false)] use_linking: bool) {
1508        // Note the resulting Hugr has a polymorphic lowered_read function, which would
1509        // mean (re)running monomorphization *after* ReplaceTypes; usually we would expect
1510        // monomorphization to happen first so that ReplaceTypes can act upon the concrete types.
1511        let e = ext();
1512        let pv = e.get_type(PACKED_VEC).unwrap();
1513        let inner = pv.instantiate([usize_t().into()]).unwrap();
1514        let outer = pv
1515            .instantiate([Type::new_extension(inner.clone()).into()])
1516            .unwrap();
1517        let mut dfb = DFGBuilder::new(inout_sig([outer.into(), i64_t()], [usize_t()])).unwrap();
1518        let read_func = dfb
1519            .module_root_builder()
1520            .add_hugr(
1521                lowered_read(Type::new_var_use(0, TypeBound::Copyable), |sig| {
1522                    FunctionBuilder::new_vis(
1523                        "lowered_read",
1524                        PolyFuncType::new([TypeBound::Copyable.into()], sig),
1525                        Visibility::Public,
1526                    )
1527                })
1528                .finish_hugr()
1529                .unwrap(),
1530            )
1531            .inserted_entrypoint;
1532        let [outer, idx] = dfb.input_wires_arr();
1533        let [inner] = dfb
1534            .add_dataflow_op(read_op(&e, inner.clone().into()), [outer, idx])
1535            .unwrap()
1536            .outputs_arr();
1537        let res = dfb
1538            .add_dataflow_op(read_op(&e, usize_t()), [inner, idx])
1539            .unwrap();
1540        let mut h = dfb.finish_hugr_with_outputs(res.outputs()).unwrap();
1541        let read_poly = h
1542            .get_optype(read_func)
1543            .as_func_defn()
1544            .unwrap()
1545            .signature()
1546            .clone();
1547
1548        let mut lw = lowerer(&e);
1549        lw.set_replace_parametrized_op(e.get_op(READ).unwrap().as_ref(), move |args, _| {
1550            Ok(Some(if use_linking {
1551                let mut decl_b = ModuleBuilder::new();
1552                let decl_node = decl_b.declare("lowered_read", read_poly.clone()).unwrap();
1553                let mut decl_hugr = decl_b.finish_hugr().unwrap();
1554                decl_hugr.set_entrypoint(decl_node.node());
1555
1556                NodeTemplate::call_to_function(decl_hugr, args).unwrap()
1557            } else {
1558                #[expect(deprecated)] // remove use_linking==false case
1559                NodeTemplate::Call(read_func, args.to_owned())
1560            }))
1561        });
1562        lw.run(&mut h).unwrap();
1563        h.validate().unwrap();
1564
1565        assert_eq!(h.output_neighbours(read_func).count(), 2);
1566        assert_eq!(
1567            h.entry_descendants()
1568                .find(|n| h.get_optype(*n).is_extension_op()),
1569            None
1570        );
1571        assert_eq!(h.children(h.module_root()).count(), 2); // main + lowered_read
1572    }
1573
1574    #[rstest]
1575    fn op_to_call_monomorphic(#[values(false, true)] i64_to_usize: bool) {
1576        let e = ext();
1577        let pv = e.get_type(PACKED_VEC).unwrap();
1578        let inner = pv.instantiate([usize_t().into()]).unwrap();
1579        let outer = pv
1580            .instantiate([Type::new_extension(inner.clone()).into()])
1581            .unwrap();
1582        let read_outer = read_op(&e, inner.clone().into());
1583        let mut dfb = DFGBuilder::new(inout_sig(
1584            vec![outer.into(), inner.clone().into(), i64_t()],
1585            vec![usize_t(); 2],
1586        ))
1587        .unwrap();
1588
1589        let [outer, inner, idx] = dfb.input_wires_arr();
1590        let res1 = dfb
1591            .add_dataflow_op(read_op(&e, usize_t()), [inner, idx])
1592            .unwrap();
1593        let [inner] = dfb
1594            .add_dataflow_op(read_outer, [outer, idx])
1595            .unwrap()
1596            .outputs_arr();
1597        let res2 = dfb
1598            .add_dataflow_op(read_op(&e, usize_t()), [inner, idx])
1599            .unwrap();
1600        let mut h = dfb
1601            .finish_hugr_with_outputs(res1.outputs().chain(res2.outputs()))
1602            .unwrap();
1603
1604        let mut lw = lowerer(&e);
1605        lw.set_replace_parametrized_op(e.get_op(READ).unwrap().as_ref(), move |args, _| {
1606            Ok(Some({
1607                let [Term::Runtime(ty)] = args else {
1608                    return Err(SignatureError::InvalidTypeArgs.into());
1609                };
1610
1611                let defn_hugr = lowered_read(ty.clone(), |sig| {
1612                    FunctionBuilder::new_vis(
1613                        mangle_name("lowered_read", args),
1614                        sig,
1615                        Visibility::Public,
1616                    )
1617                })
1618                .finish_hugr()
1619                .unwrap();
1620
1621                NodeTemplate::call_to_function(defn_hugr, &[]).unwrap()
1622            }))
1623        });
1624        if i64_to_usize {
1625            lw.set_replace_type(i64_t().as_extension().unwrap().clone(), usize_t());
1626            lw.set_replace_op(
1627                &ConvertOpDef::itousize
1628                    .without_log_width()
1629                    .to_extension_op()
1630                    .unwrap(),
1631                NodeTemplate::SingleOp(Noop::new(usize_t()).into()),
1632            );
1633        }
1634        lw.run(&mut h).unwrap();
1635        h.validate().unwrap();
1636
1637        assert_eq!(
1638            h.entry_descendants()
1639                .find(|n| h.get_optype(*n).is_extension_op()),
1640            None
1641        );
1642        assert_eq!(h.children(h.module_root()).count(), 3); // main + lowered_read
1643        for n in h.children(h.module_root()) {
1644            let fd = h.get_optype(n).as_func_defn().unwrap();
1645            let expected_uses_and_vis = if fd.func_name() == "main" {
1646                (0, Visibility::Private)
1647            } else {
1648                let is_array = !fd.signature().body().output[0]
1649                    .as_extension()
1650                    .unwrap()
1651                    .args()
1652                    .is_empty();
1653                (2 - (is_array as usize), Visibility::Public)
1654            };
1655            assert_eq!(h.output_neighbours(n).count(), expected_uses_and_vis.0);
1656            assert_eq!(fd.visibility(), &expected_uses_and_vis.1);
1657        }
1658    }
1659
1660    #[test]
1661    fn regions() {
1662        let ext = ext();
1663        let coln = ext.get_type(PACKED_VEC).unwrap();
1664        let c_u = Type::new_extension(coln.instantiate(&[usize_t().into()]).unwrap());
1665        let mut h = {
1666            let db = DFGBuilder::new(endo_sig([c_u.clone()])).unwrap();
1667            let inps = db.input_wires();
1668            db.finish_hugr_with_outputs(inps)
1669        }
1670        .unwrap();
1671        let mut lowerer = lowerer(&ext);
1672
1673        {
1674            let backup = h.clone();
1675            lowerer.set_regions(vec![]);
1676            assert!(!lowerer.run(&mut h).unwrap());
1677            assert_eq!(h, backup);
1678        }
1679
1680        let ep = h.entrypoint();
1681        lowerer.set_regions(vec![h.entrypoint()]);
1682        assert!(lowerer.run(&mut h).unwrap());
1683        let v_u = list_type(usize_t());
1684        assert_eq!(h.signature(ep).unwrap().as_ref(), &endo_sig([v_u.clone()]));
1685        assert_eq!(h.num_nodes(), h.num_nodes());
1686        let [f_in, _] = h.get_io(h.get_parent(ep).unwrap()).unwrap();
1687        assert_eq!(
1688            h.validate(),
1689            Err(ValidationError::IncompatiblePorts {
1690                from: f_in,
1691                from_port: Port::new(Direction::Outgoing, 0),
1692                to: ep,
1693                to_port: Port::new(Direction::Incoming, 0),
1694                from_kind: Box::new(EdgeKind::Value(c_u)),
1695                to_kind: Box::new(EdgeKind::Value(v_u))
1696            })
1697        );
1698    }
1699
1700    #[test]
1701    fn compositionality() {
1702        let ext = ext();
1703        let mut lowerer = lowerer(&ext);
1704        // Replace std Array's with 64 elements with PackedVec's
1705        let ext2 = ext.clone();
1706        lowerer.set_replace_parametrized_type(array_type_def(), move |args| {
1707            let [sz, ty] = args else {
1708                panic!("Expected two args to array")
1709            };
1710            (sz == &Term::BoundedNat(64)).then_some(
1711                ext2.get_type(PACKED_VEC)
1712                    .unwrap()
1713                    .instantiate([ty.clone()])
1714                    .unwrap()
1715                    .into(),
1716            )
1717        });
1718
1719        // Replacement of `get` is complex because we need to wrap result of read into a Some
1720        let ext = ext.clone();
1721        lowerer.set_replace_parametrized_op(
1722            array::EXTENSION
1723                .get_op(ArrayOpDef::get.opdef_id().as_str())
1724                .unwrap()
1725                .as_ref(),
1726            move |args, _| {
1727                let [sz, Term::Runtime(ty)] = args else {
1728                    panic!("Expected two args to array-get")
1729                };
1730                if sz != &Term::BoundedNat(64) {
1731                    return Ok(None);
1732                }
1733                let pv = ext
1734                    .get_type(PACKED_VEC)
1735                    .unwrap()
1736                    .instantiate([ty.clone().into()])
1737                    .unwrap();
1738
1739                let mut dfb = DFGBuilder::new(Signature::new(
1740                    vec![pv.clone().into(), usize_t()],
1741                    vec![option_type([ty.clone()]).into(), pv.into()],
1742                ))
1743                .unwrap();
1744                let [pvec, idx] = dfb.input_wires_arr();
1745                let [idx] = dfb
1746                    .add_dataflow_op(ConvertOpDef::ifromusize.without_log_width(), [idx])
1747                    .unwrap()
1748                    .outputs_arr();
1749                let [elem] = dfb
1750                    .add_dataflow_op(read_op(&ext, ty.clone()), [pvec, idx])
1751                    .unwrap()
1752                    .outputs_arr();
1753                let [wrapped_elem] = dfb
1754                    .add_dataflow_op(
1755                        ops::Tag::new(1, vec![type_row![], [ty.clone()].into()]),
1756                        [elem],
1757                    )
1758                    .unwrap()
1759                    .outputs_arr();
1760                Ok(Some(NodeTemplate::CompoundOp(Box::new(
1761                    dfb.finish_hugr_with_outputs([wrapped_elem, pvec]).unwrap(),
1762                ))))
1763            },
1764        );
1765
1766        // Arrays of 64 bools should thus be transformed into PackedVec<bool> and then to int64s
1767        // Arrays of 64 non-bools should thus become PackedVec<T> and thus List<T>
1768        let a64 = |t| array_type(64, t);
1769        let opt = |t| Type::from(option_type([t]));
1770        let mut dfb = DFGBuilder::new(Signature::new(
1771            vec![a64(bool_t()), a64(usize_t())],
1772            vec![opt(bool_t()), a64(bool_t()), opt(usize_t()), a64(usize_t())],
1773        ))
1774        .unwrap();
1775        let [bools, usizes] = dfb.input_wires_arr();
1776        let idx = dfb.add_load_value(ConstUsize::new(5));
1777        let [b, bools] = dfb
1778            .add_dataflow_op(ArrayOpDef::get.to_concrete(bool_t(), 64), [bools, idx])
1779            .unwrap()
1780            .outputs_arr();
1781        let [u, usizes] = dfb
1782            .add_dataflow_op(ArrayOpDef::get.to_concrete(usize_t(), 64), [usizes, idx])
1783            .unwrap()
1784            .outputs_arr();
1785        let mut h = dfb.finish_hugr_with_outputs([b, bools, u, usizes]).unwrap();
1786
1787        lowerer.run(&mut h).unwrap();
1788
1789        h.validate().unwrap();
1790    }
1791}