Skip to main content

hugr_passes/replace_types/
linearize.rs

1use std::{collections::HashMap, sync::Arc};
2
3use hugr_core::builder::{
4    BuildError, ConditionalBuilder, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer,
5    HugrBuilder, inout_sig,
6};
7use hugr_core::extension::{SignatureError, TypeDef};
8use hugr_core::std_extensions::collections::array::array_type_def;
9use hugr_core::std_extensions::collections::borrow_array::borrow_array_type_def;
10use hugr_core::types::{CustomType, Signature, Type, TypeArg, TypeEnum, TypeRow};
11use hugr_core::{HugrView, IncomingPort, Node, Wire, hugr::hugrmut::HugrMut, ops::Tag};
12use itertools::Itertools;
13
14use super::handlers::{copy_discard_array, copy_discard_borrow_array};
15use super::{NodeTemplate, ParametricType};
16
17/// Trait for things that know how to wire up linear outports to other than one
18/// target.
19///
20/// Used to restore Hugr validity when a [`ReplaceTypes`](super::ReplaceTypes)
21/// results in types of such outports changing from [Copyable] to linear (i.e.
22/// [`hugr_core::types::TypeBound::Linear`]).
23///
24/// Note that this is not really effective before [monomorphization]: if a
25/// function polymorphic over a [Copyable] becomes called with a
26/// non-Copyable type argument, [Linearizer] cannot insert copy/discard
27/// operations for such a case. However, following [monomorphization], there
28/// would be a specific instantiation of the function for the
29/// type-that-becomes-linear, into which copy/discard can be inserted.
30///
31/// [monomorphization]: crate::MonomorphizePass
32/// [Copyable]: hugr_core::types::TypeBound::Copyable
33#[deprecated(
34    note = "`hugr-passes` is deprecated. Use tket::passes instead",
35    since = "0.26.2"
36)]
37pub trait Linearizer {
38    /// Insert copy or discard operations (as appropriate) enough to wire `src`
39    /// up to all `targets`.
40    ///
41    /// The default implementation
42    /// * if `targets.len() == 1`, wires `src` to the unique target
43    /// * otherwise, makes a single call to [`Self::copy_discard_op`], inserts that op,
44    ///   and wires its outputs 1:1 to each target
45    ///
46    /// # Errors
47    ///
48    /// Most variants of [`LinearizeError`] can be raised, specifically including
49    /// [`LinearizeError::CopyableType`] if the type is [Copyable], in which case the Hugr
50    /// will be unchanged.
51    ///
52    /// [Copyable]: hugr_core::types::TypeBound::Copyable
53    ///
54    /// # Panics
55    ///
56    /// if `src` is not a valid Wire (does not identify a dataflow out-port)
57    fn insert_copy_discard(
58        &self,
59        hugr: &mut impl HugrMut<Node = Node>,
60        src: Wire,
61        targets: &[(Node, IncomingPort)],
62    ) -> Result<(), LinearizeError> {
63        let (tgt_node, tgt_inport) = if targets.len() == 1 {
64            *targets.first().unwrap()
65        } else {
66            // Fail fast if the edges are nonlocal.
67            let src_parent = hugr
68                .get_parent(src.node())
69                .expect("Root node cannot have out edges");
70            if let Some((tgt, tgt_parent)) = targets.iter().find_map(|(tgt, _)| {
71                let tgt_parent = hugr
72                    .get_parent(*tgt)
73                    .expect("Root node cannot have incoming edges");
74                (tgt_parent != src_parent).then_some((*tgt, tgt_parent))
75            }) {
76                return Err(LinearizeError::NoLinearNonLocalEdges {
77                    src: src.node(),
78                    src_parent,
79                    tgt,
80                    tgt_parent,
81                });
82            }
83            let sig = hugr.signature(src.node()).unwrap();
84            let typ = sig.port_type(src.source()).unwrap().clone();
85            let copy_discard_op = self
86                .copy_discard_op(&typ, targets.len())?
87                .add_hugr(hugr, src_parent)
88                .map_err(|e| LinearizeError::NestedTemplateError(Box::new(typ), Box::new(e)))?;
89            for (n, (tgt_node, tgt_port)) in targets.iter().enumerate() {
90                hugr.connect(copy_discard_op, n, *tgt_node, *tgt_port);
91            }
92            (copy_discard_op, 0.into())
93        };
94        hugr.connect(src.node(), src.source(), tgt_node, tgt_inport);
95        Ok(())
96    }
97
98    /// Gets an [`NodeTemplate`] for copying or discarding a value of type `typ`, i.e.
99    /// a recipe for a node with one input of that type and the specified number of
100    /// outports.
101    ///
102    /// Implementations are free to panic if `num_outports == 1`, such calls should never
103    /// occur as source/target can be directly wired without any node/op being required.
104    fn copy_discard_op(
105        &self,
106        typ: &Type,
107        num_outports: usize,
108    ) -> Result<NodeTemplate, LinearizeError>;
109}
110
111/// A configuration for implementing [Linearizer] by delegating to
112/// type-specific callbacks, and by  composing them in order to handle compound types
113/// such as [`TypeEnum::Sum`]s.
114#[derive(Clone)]
115#[deprecated(
116    note = "`hugr-passes` is deprecated. Use tket::passes instead",
117    since = "0.26.2"
118)]
119pub struct DelegatingLinearizer {
120    // Keyed by lowered type, as only needed when there is an op outputting such
121    copy_discard: HashMap<CustomType, (NodeTemplate, NodeTemplate)>,
122    // Copy/discard of parametric types handled by a function that receives the new/lowered type.
123    // We do not allow overriding copy/discard of non-extension types, but that
124    // can be achieved by *firstly* lowering to a custom linear type, with copy/discard
125    // inserted; *secondly* by lowering that to the desired non-extension linear type,
126    // including lowering of the copy/discard operations to...whatever.
127    copy_discard_parametric: HashMap<
128        ParametricType,
129        Arc<
130            dyn Fn(&[TypeArg], usize, &CallbackHandler<'_>) -> Result<NodeTemplate, LinearizeError>,
131        >,
132    >,
133}
134
135impl Default for DelegatingLinearizer {
136    fn default() -> Self {
137        let mut res = Self::new_empty();
138        res.register_callback(array_type_def(), copy_discard_array);
139        res.register_callback(borrow_array_type_def(), copy_discard_borrow_array);
140        res
141    }
142}
143
144/// Implementation of [Linearizer] passed to callbacks, (e.g.) so that callbacks for
145/// handling collection types can use it to generate copy/discards of elements.
146// (Note, this is its own type just to give a bit of room for future expansion,
147// rather than passing a &DelegatingLinearizer directly)
148#[deprecated(
149    note = "`hugr-passes` is deprecated. Use tket::passes instead",
150    since = "0.26.2"
151)]
152pub struct CallbackHandler<'a>(&'a DelegatingLinearizer);
153
154#[derive(Clone, Debug, thiserror::Error, PartialEq)]
155#[expect(missing_docs)]
156#[non_exhaustive]
157#[deprecated(
158    note = "`hugr-passes` is deprecated. Use tket::passes instead",
159    since = "0.26.2"
160)]
161pub enum LinearizeError {
162    #[error("Need copy/discard op for {_0}")]
163    NeedCopyDiscard(Box<Type>),
164    #[error("Copy/discard op for {typ} with {num_outports} outputs had wrong signature {sig:?}")]
165    WrongSignature {
166        typ: Box<Type>,
167        num_outports: usize,
168        sig: Option<Box<Signature>>,
169    },
170    #[error(
171        "Cannot add nonlocal edge for linear type from {src} (with parent {src_parent}) to {tgt} (with parent {tgt_parent}).
172  Try using LocalizeEdges pass first."
173    )]
174    NoLinearNonLocalEdges {
175        src: Node,
176        src_parent: Node,
177        tgt: Node,
178        tgt_parent: Node,
179    },
180    /// `SignatureError`'s can happen when converting nested types e.g. Sums
181    #[error(transparent)]
182    SignatureError(#[from] SignatureError),
183    /// We cannot linearize (insert copy and discard functions) for
184    /// [Variable](TypeEnum::Variable)s, [Row variables](TypeEnum::RowVar),
185    /// or [Alias](TypeEnum::Alias)es.
186    #[error("Cannot linearize type {_0}")]
187    UnsupportedType(Box<Type>),
188    /// Neither does linearization make sense for copyable types
189    #[error("Type {_0} is copyable")]
190    CopyableType(Box<Type>),
191    /// Error may be returned by a callback for e.g. a container because it could
192    /// not generate a [`NodeTemplate`] because of a problem with an element
193    #[error("Could not generate NodeTemplate for contained type {0} because {1}")]
194    NestedTemplateError(Box<Type>, Box<BuildError>),
195}
196
197impl DelegatingLinearizer {
198    /// Makes a new instance. Unlike [`Self::default`], this does not understand
199    /// any extension types, even those in the prelude.
200    #[must_use]
201    pub fn new_empty() -> Self {
202        Self {
203            copy_discard: Default::default(),
204            copy_discard_parametric: Default::default(),
205        }
206    }
207
208    /// Configures this instance that the specified monomorphic type can be copied and/or
209    /// discarded via the provided [`NodeTemplate`]s - directly or as part of a compound type
210    /// e.g. [`TypeEnum::Sum`].
211    /// `copy` should have exactly one inport, of type `src`, and two outports, of same type;
212    /// `discard` should have exactly one inport, of type 'src', and no outports.
213    ///
214    /// # Errors
215    ///
216    /// * [`LinearizeError::CopyableType`] If `typ` is
217    ///   [Copyable](hugr_core::types::TypeBound::Copyable)
218    /// * [`LinearizeError::WrongSignature`] if `copy` or `discard` do not have the expected
219    ///   inputs or outputs (for [`NodeTemplate::SingleOp`] and [`NodeTemplate::CompoundOp`]
220    ///   only: the signature for a [`NodeTemplate::Call`] cannot be checked until it is used
221    ///   in a Hugr).
222    pub fn register_simple(
223        &mut self,
224        cty: CustomType,
225        copy: NodeTemplate,
226        discard: NodeTemplate,
227    ) -> Result<(), LinearizeError> {
228        let typ = Type::new_extension(cty.clone());
229        if typ.copyable() {
230            return Err(LinearizeError::CopyableType(Box::new(typ)));
231        }
232        check_sig(&copy, &typ, 2)?;
233        check_sig(&discard, &typ, 0)?;
234        self.copy_discard.insert(cty, (copy, discard));
235        Ok(())
236    }
237
238    /// Configures this instance that instances of the specified [`TypeDef`] (perhaps
239    /// polymorphic) can be copied and/or discarded by using the provided callback
240    /// to generate a [`NodeTemplate`] for an appropriate copy/discard operation.
241    ///
242    /// The callback is given
243    /// * the type arguments (as appropriate for the [`TypeDef`], so perhaps empty)
244    /// * the desired number of outports (this will never be 1)
245    /// * A [`CallbackHandler`] that the callback can use it to generate
246    ///   `copy`/`discard` ops for other types (e.g. the elements of a collection),
247    ///   as part of an [`NodeTemplate::CompoundOp`].
248    ///
249    /// Note that [`Self::register_simple`] takes precedence when the `src` types overlap.
250    pub fn register_callback(
251        &mut self,
252        src: &TypeDef,
253        copy_discard_fn: impl Fn(
254            &[TypeArg],
255            usize,
256            &CallbackHandler<'_>,
257        ) -> Result<NodeTemplate, LinearizeError>
258        + 'static,
259    ) {
260        // We could look for `src`s TypeDefBound being explicit Copyable, otherwise
261        // it depends on the arguments. Since there is no method to get the TypeDefBound
262        // from a TypeDef, leaving this for now.
263        self.copy_discard_parametric
264            .insert(src.into(), Arc::new(copy_discard_fn));
265    }
266}
267
268fn check_sig(tmpl: &NodeTemplate, typ: &Type, num_outports: usize) -> Result<(), LinearizeError> {
269    tmpl.check_signature(
270        &[typ.clone()].into(),
271        &vec![typ.clone(); num_outports].into(),
272    )
273    .map_err(|sig| LinearizeError::WrongSignature {
274        typ: Box::new(typ.clone()),
275        num_outports,
276        sig: sig.map(Box::new),
277    })
278}
279
280impl Linearizer for DelegatingLinearizer {
281    fn copy_discard_op(
282        &self,
283        typ: &Type,
284        num_outports: usize,
285    ) -> Result<NodeTemplate, LinearizeError> {
286        if typ.copyable() {
287            return Err(LinearizeError::CopyableType(Box::new(typ.clone())));
288        }
289        assert!(num_outports != 1);
290
291        match typ.as_type_enum() {
292            TypeEnum::Sum(sum_type) => {
293                let variants = sum_type
294                    .variants()
295                    .map(|trv| trv.clone().try_into())
296                    .collect::<Result<Vec<TypeRow>, _>>()?;
297                let mut cb = ConditionalBuilder::new(
298                    variants.clone(),
299                    vec![],
300                    vec![sum_type.clone().into(); num_outports],
301                )
302                .unwrap();
303                for (tag, variant) in variants.iter().enumerate() {
304                    let mut case_b = cb.case_builder(tag).unwrap();
305                    let mut elems_for_copy = vec![vec![]; num_outports];
306                    for (inp, ty) in case_b.input_wires().zip_eq(variant.iter()) {
307                        let inp_copies = if ty.copyable() {
308                            std::iter::repeat_n(inp, num_outports).collect::<Vec<_>>()
309                        } else {
310                            self.copy_discard_op(ty, num_outports)?
311                                .add(&mut case_b, [inp])
312                                .unwrap()
313                                .outputs()
314                                .collect()
315                        };
316                        for (src, elems) in inp_copies.into_iter().zip_eq(elems_for_copy.iter_mut())
317                        {
318                            elems.push(src);
319                        }
320                    }
321                    let t = Tag::new(tag, variants.clone());
322                    let outputs = elems_for_copy
323                        .into_iter()
324                        .map(|elems| {
325                            let [copy] = case_b
326                                .add_dataflow_op(t.clone(), elems)
327                                .unwrap()
328                                .outputs_arr();
329                            copy
330                        })
331                        .collect::<Vec<_>>(); // must collect to end borrow of `case_b` by closure
332                    case_b.finish_with_outputs(outputs).unwrap();
333                }
334                Ok(NodeTemplate::CompoundOp(Box::new(
335                    cb.finish_hugr().unwrap(),
336                )))
337            }
338            TypeEnum::Extension(cty) => {
339                if let Some((copy, discard)) = self.copy_discard.get(cty) {
340                    Ok(if num_outports == 0 {
341                        discard.clone()
342                    } else {
343                        let mut dfb = DFGBuilder::new(inout_sig(
344                            [typ.clone()],
345                            vec![typ.clone(); num_outports],
346                        ))
347                        .unwrap();
348                        let [mut src] = dfb.input_wires_arr();
349                        let mut outputs = vec![];
350                        for _ in 0..num_outports - 1 {
351                            let [out0, out1] =
352                                copy.clone().add(&mut dfb, [src]).unwrap().outputs_arr();
353                            outputs.push(out0);
354                            src = out1;
355                        }
356                        outputs.push(src);
357                        NodeTemplate::CompoundOp(Box::new(
358                            dfb.finish_hugr_with_outputs(outputs).unwrap(),
359                        ))
360                    })
361                } else {
362                    let copy_discard_fn = self
363                        .copy_discard_parametric
364                        .get(&cty.into())
365                        .ok_or_else(|| LinearizeError::NeedCopyDiscard(Box::new(typ.clone())))?;
366                    let tmpl = copy_discard_fn(cty.args(), num_outports, &CallbackHandler(self))?;
367                    check_sig(&tmpl, typ, num_outports)?;
368                    Ok(tmpl)
369                }
370            }
371            TypeEnum::Function(_) => panic!("Ruled out above as copyable"),
372            _ => Err(LinearizeError::UnsupportedType(Box::new(typ.clone()))),
373        }
374    }
375}
376
377impl Linearizer for CallbackHandler<'_> {
378    fn copy_discard_op(
379        &self,
380        typ: &Type,
381        num_outports: usize,
382    ) -> Result<NodeTemplate, LinearizeError> {
383        self.0.copy_discard_op(typ, num_outports)
384    }
385}
386
387#[cfg(test)]
388mod test {
389    use std::collections::HashMap;
390    use std::sync::Arc;
391
392    use hugr_core::builder::{
393        Container, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder, inout_sig,
394    };
395
396    use hugr_core::Visibility;
397    use hugr_core::extension::prelude::{option_type, qb_t, usize_t};
398    use hugr_core::extension::{
399        CustomSignatureFunc, OpDef, SignatureError, SignatureFunc, TypeDefBound, Version,
400    };
401    use hugr_core::hugr::ValidationError;
402    use hugr_core::hugr::hugrmut::HugrMut;
403    use hugr_core::ops::handle::NodeHandle;
404    use hugr_core::ops::{DataflowOpTrait, ExtensionOp, OpName, OpType};
405    use hugr_core::std_extensions::arithmetic::int_types::INT_TYPES;
406    use hugr_core::std_extensions::collections::array::array_type;
407    use hugr_core::std_extensions::collections::borrow_array::{BArrayOpDef, borrow_array_type};
408    use hugr_core::types::type_param::TypeParam;
409    use hugr_core::types::{
410        FuncValueType, PolyFuncTypeRV, Signature, Type, TypeArg, TypeBound, TypeRow,
411    };
412    use hugr_core::{Extension, Hugr, HugrView, Node, hugr::IdentList};
413    use itertools::Itertools;
414    use rstest::rstest;
415
416    use crate::replace_types::handlers::{DISCARD_TO_UNIT_PREFIX, MAKE_NONE_PREFIX, UNWRAP_PREFIX};
417    use crate::replace_types::{LinearizeError, Linearizer, NodeTemplate, ReplaceTypesError};
418    use crate::{ComposablePass, ReplaceTypes};
419
420    const LIN_T: &str = "Lin";
421    const COPY_T: &str = "Copy";
422
423    struct NWayCopySigFn(Type);
424    impl CustomSignatureFunc for NWayCopySigFn {
425        fn compute_signature<'o, 'a: 'o>(
426            &'a self,
427            arg_values: &[TypeArg],
428            _def: &'o OpDef,
429        ) -> Result<PolyFuncTypeRV, SignatureError> {
430            let [TypeArg::BoundedNat(n)] = arg_values else {
431                panic!()
432            };
433            let outs = vec![self.0.clone(); *n as usize];
434            Ok(FuncValueType::new([self.0.clone()], outs).into())
435        }
436
437        fn static_params(&self) -> &[TypeParam] {
438            const JUST_NAT: &[TypeParam] = &[TypeParam::max_nat_type()];
439            JUST_NAT
440        }
441    }
442
443    fn ext_lowerer() -> (Arc<Extension>, ReplaceTypes) {
444        // Extension with a linear type, an n-way parametric copy op, and a discard op
445        let e = Extension::new_arc(
446            IdentList::new_unchecked("TestExt"),
447            Version::new(0, 0, 0),
448            |e, w| {
449                let lin = Type::new_extension(
450                    e.add_type(LIN_T.into(), vec![], String::new(), TypeDefBound::any(), w)
451                        .unwrap()
452                        .instantiate([])
453                        .unwrap(),
454                );
455                e.add_type(
456                    COPY_T.into(),
457                    vec![],
458                    String::new(),
459                    TypeDefBound::copyable(),
460                    w,
461                )
462                .unwrap()
463                .instantiate([])
464                .unwrap();
465                e.add_op(
466                    "discard".into(),
467                    String::new(),
468                    Signature::new([lin.clone()], []),
469                    w,
470                )
471                .unwrap();
472                e.add_op(
473                    "copy".into(),
474                    String::new(),
475                    SignatureFunc::CustomFunc(Box::new(NWayCopySigFn(lin))),
476                    w,
477                )
478                .unwrap();
479            },
480        );
481
482        let lin_custom_t = e.get_type(LIN_T).unwrap().instantiate([]).unwrap();
483
484        // Configure to lower usize_t to the linear type above, using a 2-way copy only
485        let copy_op = ExtensionOp::new(e.get_op("copy").unwrap().clone(), [2.into()]).unwrap();
486        let discard_op = ExtensionOp::new(e.get_op("discard").unwrap().clone(), []).unwrap();
487        let mut lowerer = ReplaceTypes::default();
488        let usize_custom_t = usize_t().as_extension().unwrap().clone();
489        lowerer.set_replace_type(usize_custom_t, Type::new_extension(lin_custom_t.clone()));
490        lowerer
491            .linearizer_mut()
492            .register_simple(
493                lin_custom_t,
494                NodeTemplate::SingleOp(copy_op.into()),
495                NodeTemplate::SingleOp(discard_op.into()),
496            )
497            .unwrap();
498        (e, lowerer)
499    }
500
501    #[test]
502    fn single_values() {
503        let (_e, lowerer) = ext_lowerer();
504        // Build Hugr - uses first input three times, discards second input (both usize)
505        let mut outer = DFGBuilder::new(inout_sig(
506            vec![usize_t(); 2],
507            vec![usize_t(), borrow_array_type(2, usize_t())],
508        ))
509        .unwrap();
510        let [inp, _] = outer.input_wires_arr();
511        let new_array = outer
512            .add_dataflow_op(BArrayOpDef::new_array.to_concrete(usize_t(), 2), [inp, inp])
513            .unwrap();
514        let [arr] = new_array.outputs_arr();
515        let mut h = outer.finish_hugr_with_outputs([inp, arr]).unwrap();
516
517        assert!(lowerer.run(&mut h).unwrap());
518
519        let ext_ops = h
520            .entry_descendants()
521            .filter_map(|n| h.get_optype(n).as_extension_op());
522        let mut counts = HashMap::<OpName, u32>::new();
523        for e in ext_ops {
524            *counts.entry(e.qualified_id()).or_default() += 1;
525        }
526        assert_eq!(
527            counts,
528            HashMap::from([
529                ("TestExt.copy".into(), 2),
530                ("TestExt.discard".into(), 1),
531                ("collections.borrow_arr.new_array".into(), 1)
532            ])
533        );
534    }
535
536    fn copy_n_discard_one(ty: Type, n: usize) -> (Hugr, Node) {
537        let mut outer = DFGBuilder::new(inout_sig([ty.clone()], vec![ty.clone(); n - 1])).unwrap();
538        let [inp] = outer.input_wires_arr();
539        let inner = outer
540            .dfg_builder(inout_sig([ty], []), [inp])
541            .unwrap()
542            .finish_with_outputs([])
543            .unwrap();
544        let h = outer.finish_hugr_with_outputs(vec![inp; n - 1]).unwrap();
545        (h, inner.node())
546    }
547
548    #[rstest]
549    fn sums_2way_copy(#[values(2, 3, 4)] num_copies: usize) {
550        let (mut h, inner) = copy_n_discard_one(option_type([usize_t()]).into(), num_copies);
551
552        let (e, lowerer) = ext_lowerer();
553        assert!(lowerer.run(&mut h).unwrap());
554
555        let lin_t = Type::from(e.get_type(LIN_T).unwrap().instantiate([]).unwrap());
556        let sum_ty: Type = option_type([lin_t.clone()]).into();
557        let count_tags = |n| h.children(n).filter(|n| h.get_optype(*n).is_tag()).count();
558
559        // Check we've inserted one Conditional into outer (for copy) and inner (for discard)...
560        for (dfg, num_tags, expected_ext_ops) in [
561            (inner.node(), 0, vec!["TestExt.discard"]),
562            (
563                h.entrypoint(),
564                num_copies,
565                vec!["TestExt.copy"; num_copies - 1],
566            ), // 2 copy nodes -> 3 outputs, etc.
567        ] {
568            let [(cond_node, cond)] = h
569                .children(dfg)
570                .filter_map(|n| h.get_optype(n).as_conditional().map(|c| (n, c)))
571                .collect_array()
572                .unwrap();
573            assert_eq!(
574                cond.signature().output(),
575                &TypeRow::from(vec![sum_ty.clone(); num_tags])
576            );
577            let [case0, case1] = h.children(cond_node).collect_array().unwrap();
578            // first is for empty variant
579            assert_eq!(h.children(case0).count(), 2 + num_tags); // Input, Output
580            assert_eq!(count_tags(case0), num_tags);
581
582            // second is for variant of a LIN_T
583            assert_eq!(h.children(case1).count(), 3 + num_tags); // Input, Output, copy/discard
584            assert_eq!(count_tags(case1), num_tags);
585            let ext_ops = h
586                .descendants(case1)
587                .filter_map(|n| {
588                    h.get_optype(n)
589                        .as_extension_op()
590                        .map(ExtensionOp::qualified_id)
591                })
592                .collect_vec();
593            assert_eq!(ext_ops, expected_ext_ops);
594        }
595    }
596
597    #[rstest]
598    fn sum_nway_copy(#[values(2, 5, 9)] num_copies: usize) {
599        let i8_t = || INT_TYPES[3].clone();
600        let sum_ty = Type::new_sum([vec![i8_t()], vec![usize_t(); 2]]);
601
602        let (mut h, inner) = copy_n_discard_one(sum_ty, num_copies);
603        let (e, _) = ext_lowerer();
604        let mut lowerer = ReplaceTypes::default();
605        let lin_t_def = e.get_type(LIN_T).unwrap();
606        lowerer.set_replace_type(
607            usize_t().as_extension().unwrap().clone(),
608            lin_t_def.instantiate([]).unwrap().into(),
609        );
610        let opdef = e.get_op("copy").unwrap();
611        let opdef2 = opdef.clone();
612        lowerer
613            .linearizer_mut()
614            .register_callback(lin_t_def, move |args, num_outs, _| {
615                assert!(args.is_empty());
616                Ok(NodeTemplate::SingleOp(
617                    ExtensionOp::new(opdef2.clone(), [(num_outs as u64).into()])
618                        .unwrap()
619                        .into(),
620                ))
621            });
622        assert!(lowerer.run(&mut h).unwrap());
623
624        let lin_t = Type::from(e.get_type(LIN_T).unwrap().instantiate([]).unwrap());
625        let sum_ty = Type::new_sum([vec![i8_t()], vec![lin_t.clone(); 2]]);
626        let count_tags = |n| h.children(n).filter(|n| h.get_optype(*n).is_tag()).count();
627
628        // Check we've inserted one Conditional into outer (for copy) and inner (for discard)...
629        for (dfg, num_tags) in [(inner.node(), 0), (h.entrypoint(), num_copies)] {
630            let [cond] = h
631                .children(dfg)
632                .filter(|n| h.get_optype(*n).is_conditional())
633                .collect_array()
634                .unwrap();
635            let [case0, case1] = h.children(cond).collect_array().unwrap();
636            let out_row = vec![sum_ty.clone(); num_tags].into();
637            // first is for empty variant - the only input is Copyable so can be directly wired or ignored
638            assert_eq!(h.children(case0).count(), 2 + num_tags); // Input, Output
639            assert_eq!(count_tags(case0), num_tags);
640            let case0 = h.get_optype(case0).as_case().unwrap();
641            assert_eq!(case0.signature.io(), (&vec![i8_t()].into(), &out_row));
642
643            // second is for variant of two elements
644            assert_eq!(h.children(case1).count(), 4 + num_tags); // Input, Output, two leaf copies/discards:
645            assert_eq!(count_tags(case1), num_tags);
646            let ext_ops = h
647                .children(case1)
648                .filter_map(|n| h.get_optype(n).as_extension_op())
649                .collect_vec();
650            let expected_op = ExtensionOp::new(opdef.clone(), [(num_tags as u64).into()]).unwrap();
651            assert_eq!(ext_ops, vec![&expected_op; 2]);
652
653            let case1 = h.get_optype(case1).as_case().unwrap();
654            assert_eq!(
655                case1.signature.io(),
656                (&vec![lin_t.clone(); 2].into(), &out_row)
657            );
658        }
659    }
660
661    #[test]
662    fn bad_sig() {
663        // Change usize to QB_T
664        let (ext, _) = ext_lowerer();
665        let lin_ct = ext.get_type(LIN_T).unwrap().instantiate([]).unwrap();
666        let lin_t = Type::from(lin_ct.clone());
667        let copy3 = OpType::from(
668            ExtensionOp::new(ext.get_op("copy").unwrap().clone(), [3.into()]).unwrap(),
669        );
670        let copy2 = ExtensionOp::new(ext.get_op("copy").unwrap().clone(), [2.into()]).unwrap();
671        let discard = ExtensionOp::new(ext.get_op("discard").unwrap().clone(), []).unwrap();
672        let mut replacer = ReplaceTypes::default();
673        replacer.set_replace_type(usize_t().as_extension().unwrap().clone(), lin_t.clone());
674
675        let bad_copy = replacer.linearizer_mut().register_simple(
676            lin_ct.clone(),
677            NodeTemplate::SingleOp(copy3.clone()),
678            NodeTemplate::SingleOp(discard.clone().into()),
679        );
680        let sig3 = Some(Signature::new([lin_t.clone()], vec![lin_t.clone(); 3]));
681        assert_eq!(
682            bad_copy,
683            Err(LinearizeError::WrongSignature {
684                typ: Box::new(lin_t.clone()),
685                num_outports: 2,
686                sig: sig3.clone().map(Box::new)
687            })
688        );
689
690        let bad_discard = replacer.linearizer_mut().register_simple(
691            lin_ct.clone(),
692            NodeTemplate::SingleOp(copy2.into()),
693            NodeTemplate::SingleOp(copy3.clone()),
694        );
695
696        assert_eq!(
697            bad_discard,
698            Err(LinearizeError::WrongSignature {
699                typ: Box::new(lin_t.clone()),
700                num_outports: 0,
701                sig: sig3.clone().map(Box::new)
702            })
703        );
704
705        // Try parametrized instead, but this version always returns 3 outports
706        replacer
707            .linearizer_mut()
708            .register_callback(ext.get_type(LIN_T).unwrap(), move |_args, _, _| {
709                Ok(NodeTemplate::SingleOp(copy3.clone()))
710            });
711
712        // A hugr that copies a usize
713        let dfb = DFGBuilder::new(inout_sig([usize_t()], vec![usize_t(); 2])).unwrap();
714        let [inp] = dfb.input_wires_arr();
715        let mut h = dfb.finish_hugr_with_outputs([inp, inp]).unwrap();
716
717        assert_eq!(
718            replacer.run(&mut h),
719            Err(ReplaceTypesError::LinearizeError(
720                LinearizeError::WrongSignature {
721                    typ: Box::new(lin_t.clone()),
722                    num_outports: 2,
723                    sig: sig3.clone().map(Box::new)
724                }
725            ))
726        );
727    }
728
729    #[rstest]
730    fn call_in_array(#[values(true, false)] use_linking: bool) {
731        let (e, _) = ext_lowerer();
732        let lin_ct = e.get_type(LIN_T).unwrap().instantiate([]).unwrap();
733        let lin_t: Type = lin_ct.clone().into();
734
735        // A simple Hugr that discards a usize_t, with a "drop" function
736        let mut dfb = DFGBuilder::new(inout_sig([usize_t()], [])).unwrap();
737        let discard_fn = {
738            let mut mb = dfb.module_root_builder();
739            let mut fb = mb
740                .define_function_vis(
741                    "drop",
742                    Signature::new([lin_t.clone()], []),
743                    Visibility::Public,
744                )
745                .unwrap();
746            let ins = fb.input_wires();
747            fb.add_dataflow_op(
748                ExtensionOp::new(e.get_op("discard").unwrap().clone(), []).unwrap(),
749                ins,
750            )
751            .unwrap();
752            fb.finish_with_outputs([]).unwrap()
753        }
754        .node();
755        let backup = dfb.finish_hugr().unwrap();
756
757        let mut lower_discard_to_call = ReplaceTypes::default();
758        if use_linking {
759            lower_discard_to_call
760                .linearizer_mut()
761                .register_simple(
762                    lin_ct.clone(),
763                    NodeTemplate::CompoundOp(Box::new({
764                        // Not a valid Hugr, but won't be used
765                        std::mem::take(
766                            DFGBuilder::new(inout_sig([lin_t.clone()], vec![lin_t.clone(); 2]))
767                                .unwrap()
768                                .hugr_mut(),
769                        )
770                    })),
771                    NodeTemplate::linked_hugr({
772                        let mut dfb = DFGBuilder::new(inout_sig([lin_t.clone()], [])).unwrap();
773                        let drop_fn = dfb
774                            .module_root_builder()
775                            .declare("drop", inout_sig([lin_t.clone()], []).into())
776                            .unwrap();
777                        let ins = dfb.input_wires();
778                        let call = dfb.call(&drop_fn, &[], ins).unwrap();
779                        dfb.finish_hugr_with_outputs(call.outputs()).unwrap()
780                    }),
781                )
782                .unwrap();
783        } else {
784            #[expect(deprecated)] // Remove use_linking==false case along with NodeTemplate::Call
785            lower_discard_to_call
786                .linearizer_mut()
787                .register_simple(
788                    lin_ct.clone(),
789                    NodeTemplate::Call(backup.entrypoint(), vec![]), // Arbitrary, unused
790                    NodeTemplate::Call(discard_fn, vec![]),
791                )
792                .unwrap();
793        };
794        // Ok to lower usize_t to lin_t and call that function
795        {
796            let mut lowerer = lower_discard_to_call.clone();
797            lowerer.set_replace_type(usize_t().as_extension().unwrap().clone(), lin_t.clone());
798            let mut h = backup.clone();
799            lowerer.run(&mut h).unwrap();
800            assert_eq!(h.output_neighbours(discard_fn).count(), 1);
801        }
802
803        // Now lower usize_t to array<lin_t>
804        lower_discard_to_call.set_replace_type(
805            usize_t().as_extension().unwrap().clone(),
806            array_type(4, lin_ct.into()),
807        );
808        let mut h = backup.clone();
809        let r = lower_discard_to_call.run(&mut h);
810        if use_linking {
811            r.unwrap();
812            h.validate().unwrap();
813        } else {
814            // Without linking, the Call node to the function discarding the array<lin_t> is
815            // inside a nested Hugr (hidden here, built by the array linearization helper)
816            // that does not define "drop".
817            // So, we might expect a LinearizeError in building that nested Hugr.
818            // However, by (bad) luck the target Node of the call identifies,
819            // in the nested Hugr, the Lin->() function being built, which makes
820            // a legal Hugr (the unit outport can have zero edges).
821            // Of course this would loop forever at runtime!
822            r.unwrap();
823            h.validate().unwrap();
824            let disc = h
825                .children(h.module_root())
826                .find(|n| {
827                    h.get_optype(*n)
828                        .as_func_defn()
829                        .is_some_and(|fd| fd.func_name().contains(DISCARD_TO_UNIT_PREFIX))
830                })
831                .unwrap();
832            let call = h
833                .descendants(disc)
834                .filter(|n| h.get_optype(*n).is_call())
835                .exactly_one()
836                .ok()
837                .unwrap();
838            assert_eq!(h.static_source(call), Some(disc)); // Ooops.
839        }
840    }
841
842    #[test]
843    fn use_in_op_callback() {
844        let (e, mut lowerer) = ext_lowerer();
845        let drop_ext = Extension::new_arc(
846            IdentList::new_unchecked("DropExt"),
847            Version::new(0, 0, 0),
848            |e, w| {
849                e.add_op(
850                    "drop".into(),
851                    String::new(),
852                    PolyFuncTypeRV::new(
853                        [TypeBound::Linear.into()], // It won't *lower* for any type tho!
854                        Signature::new([Type::new_var_use(0, TypeBound::Linear)], vec![]),
855                    ),
856                    w,
857                )
858                .unwrap();
859            },
860        );
861        let drop_op = drop_ext.get_op("drop").unwrap();
862        lowerer.set_replace_parametrized_op(drop_op, |args, rt| {
863            let [TypeArg::Runtime(ty)] = args else {
864                panic!("Expected just one type")
865            };
866            Ok(Some(rt.get_linearizer().copy_discard_op(ty, 0)?))
867        });
868
869        let build_hugr = |ty: Type| {
870            let mut dfb = DFGBuilder::new(Signature::new([ty.clone()], [])).unwrap();
871            let [inp] = dfb.input_wires_arr();
872            let drop_op = drop_ext
873                .instantiate_extension_op("drop", [ty.into()])
874                .unwrap();
875            dfb.add_dataflow_op(drop_op, [inp]).unwrap();
876            dfb.finish_hugr().unwrap()
877        };
878        // We can drop a tuple of 2* lin_t
879        let lin_t = Type::from(e.get_type(LIN_T).unwrap().instantiate([]).unwrap());
880        let mut h = build_hugr(Type::new_tuple(vec![lin_t.clone(); 2]));
881        lowerer.run(&mut h).unwrap();
882        h.validate().unwrap();
883        let mut exts = h.nodes().filter_map(|n| h.get_optype(n).as_extension_op());
884        assert_eq!(exts.clone().count(), 2);
885        assert!(exts.all(|eo| eo.qualified_id() == "TestExt.discard"));
886
887        // We can drop a borrow array of lin_t
888        let mut h = build_hugr(borrow_array_type(4, lin_t));
889        lowerer.run(&mut h).unwrap();
890        h.validate().unwrap();
891        let mut exts = h.nodes().filter_map(|n| h.get_optype(n).as_extension_op());
892        assert!(exts.any(|eo| eo.qualified_id() == "collections.borrow_arr.discard_all_borrowed"));
893
894        // We can drop a borrow array of usize
895        let mut h = build_hugr(borrow_array_type(4, usize_t()));
896        lowerer.run(&mut h).unwrap();
897        h.validate().unwrap();
898        let mut exts = h.nodes().filter_map(|n| h.get_optype(n).as_extension_op());
899        assert!(exts.any(|eo| eo.qualified_id() == "collections.borrow_arr.discard_all_borrowed"));
900
901        // We cannot drop a qubit
902        let mut h = build_hugr(qb_t());
903        assert_eq!(
904            lowerer.run(&mut h).unwrap_err(),
905            ReplaceTypesError::LinearizeError(LinearizeError::NeedCopyDiscard(Box::new(qb_t())))
906        );
907
908        // We cannot drop an array of qubits
909        let mut h = build_hugr(borrow_array_type(4, qb_t()));
910        assert_eq!(
911            lowerer.run(&mut h).unwrap_err(),
912            ReplaceTypesError::LinearizeError(LinearizeError::NeedCopyDiscard(Box::new(qb_t())))
913        );
914    }
915
916    #[rstest]
917    #[case([borrow_array_type(2, usize_t())])]
918    #[case([borrow_array_type(2, usize_t()), borrow_array_type(4, usize_t())])]
919    fn test_copy_borrow_array<const N: usize>(#[case] tys: [Type; N]) {
920        // Build invalid Hugr that treats element of `tys` as copyable
921        let (inp, out, mut h) = {
922            let mut dfb = DFGBuilder::new(Signature::new(
923                Vec::from_iter(tys.clone()),
924                tys.clone().into_iter().chain(tys.clone()).collect_vec(),
925            ))
926            .unwrap();
927            (dfb.input(), dfb.output(), std::mem::take(dfb.hugr_mut()))
928        };
929        for (n, _) in tys.iter().enumerate() {
930            h.connect(inp.node(), n, out.node(), n);
931            h.connect(inp.node(), n, out.node(), n + tys.len());
932        }
933        assert!(matches!(
934            h.validate(),
935            Err(ValidationError::TooManyConnections { .. })
936        ));
937        let (_e, lowerer) = ext_lowerer();
938        lowerer.run(&mut h).unwrap();
939        h.validate().unwrap();
940        for prefix in [UNWRAP_PREFIX, MAKE_NONE_PREFIX] {
941            assert_eq!(
942                h.children(h.module_root())
943                    .filter(|n| match h.get_optype(*n) {
944                        OpType::FuncDecl(_) => panic!("Unexpected FuncDecl"),
945                        OpType::FuncDefn(fd) => fd.func_name().contains(prefix),
946                        _ => false,
947                    })
948                    .count(),
949                1,
950                "Found multiple {prefix} funcs"
951            );
952        }
953    }
954}