hugr_core/
export.rs

1//! Exporting HUGR graphs to their `hugr-model` representation.
2use crate::Visibility;
3use crate::extension::ExtensionRegistry;
4use crate::hugr::internal::HugrInternals;
5use crate::types::type_param::Term;
6use crate::{
7    Direction, Hugr, HugrView, IncomingPort, Node, NodeIndex as _, Port,
8    extension::{ExtensionId, OpDef, SignatureFunc},
9    hugr::IdentList,
10    ops::{
11        DataflowBlock, DataflowOpTrait, OpName, OpTrait, OpType, Value, constant::CustomSerialized,
12    },
13    std_extensions::{
14        arithmetic::{float_types::ConstF64, int_types::ConstInt},
15        collections::array::ArrayValue,
16    },
17    types::{
18        CustomType, EdgeKind, FuncTypeBase, MaybeRV, PolyFuncTypeBase, RowVariable, SumType,
19        TypeBase, TypeBound, TypeEnum, type_param::TermVar, type_row::TypeRowBase,
20    },
21};
22
23use fxhash::{FxBuildHasher, FxHashMap};
24use hugr_model::v0::bumpalo;
25use hugr_model::v0::{
26    self as model,
27    bumpalo::{Bump, collections::String as BumpString, collections::Vec as BumpVec},
28    table,
29};
30use petgraph::unionfind::UnionFind;
31use smol_str::ToSmolStr;
32use std::fmt::Write;
33
34/// Exports a deconstructed `Package` to its representation in the model.
35pub fn export_package<'a, 'h: 'a>(
36    hugrs: impl IntoIterator<Item = &'h Hugr>,
37    _extensions: &ExtensionRegistry,
38    bump: &'a Bump,
39) -> table::Package<'a> {
40    let modules = hugrs
41        .into_iter()
42        .map(|module| export_hugr(module, bump))
43        .collect();
44    table::Package { modules }
45}
46
47/// Export a [`Hugr`] graph to its representation in the model.
48pub fn export_hugr<'a>(hugr: &'a Hugr, bump: &'a Bump) -> table::Module<'a> {
49    let mut ctx = Context::new(hugr, bump);
50    ctx.export_root();
51    ctx.module
52}
53
54/// State for converting a HUGR graph to its representation in the model.
55struct Context<'a> {
56    /// The HUGR graph to convert.
57    hugr: &'a Hugr,
58    /// The module that is being built.
59    module: table::Module<'a>,
60    /// The arena in which the model is allocated.
61    bump: &'a Bump,
62    /// Stores the terms that we have already seen to avoid duplicates.
63    term_map: FxHashMap<table::Term<'a>, table::TermId>,
64
65    /// The current scope for local variables.
66    ///
67    /// This is set to the id of the smallest enclosing node that defines a polymorphic type.
68    /// We use this when exporting local variables in terms.
69    local_scope: Option<table::NodeId>,
70
71    /// Constraints to be added to the local scope.
72    ///
73    /// When exporting a node that defines a polymorphic type, we use this field
74    /// to collect the constraints that need to be added to that polymorphic
75    /// type. Currently this is used to record `nonlinear` constraints on uses
76    /// of `TypeParam::Type` with a `TypeBound::Copyable` bound.
77    local_constraints: Vec<table::TermId>,
78
79    /// Mapping from extension operations to their declarations.
80    decl_operations: FxHashMap<(ExtensionId, OpName), table::NodeId>,
81
82    /// Auxiliary structure for tracking the links between ports.
83    links: Links,
84
85    /// The symbol table tracking symbols that are currently in scope.
86    symbols: model::scope::SymbolTable<'a>,
87
88    /// Mapping from implicit imports to their node ids.
89    implicit_imports: FxHashMap<&'a str, table::NodeId>,
90
91    /// Map from node ids in the [`Hugr`] to the corresponding node ids in the model.
92    node_to_id: FxHashMap<Node, table::NodeId>,
93
94    /// Mapping from node ids in the [`Hugr`] to the corresponding model nodes.
95    id_to_node: FxHashMap<table::NodeId, Node>,
96    // TODO: Once this module matures, we should consider adding an auxiliary structure
97    // that ensures that the `node_to_id` and `id_to_node` maps stay in sync.
98}
99
100const NO_VIS: Option<model::Visibility> = None;
101
102impl<'a> Context<'a> {
103    pub fn new(hugr: &'a Hugr, bump: &'a Bump) -> Self {
104        let mut module = table::Module::default();
105        module.nodes.reserve(hugr.num_nodes());
106        let links = Links::new(hugr);
107
108        Self {
109            hugr,
110            module,
111            bump,
112            links,
113            term_map: FxHashMap::default(),
114            local_scope: None,
115            decl_operations: FxHashMap::default(),
116            local_constraints: Vec::new(),
117            symbols: model::scope::SymbolTable::default(),
118            implicit_imports: FxHashMap::default(),
119            node_to_id: FxHashMap::default(),
120            id_to_node: FxHashMap::default(),
121        }
122    }
123
124    /// Exports the root module of the HUGR graph.
125    pub fn export_root(&mut self) {
126        self.module.root = self.module.insert_region(table::Region::default());
127        self.symbols.enter(self.module.root);
128        self.links.enter(self.module.root);
129
130        let hugr_children = self.hugr.children(self.hugr.module_root());
131        let mut children = Vec::with_capacity(hugr_children.size_hint().0);
132
133        for child in hugr_children.clone() {
134            if let Some(child_id) = self.export_node_shallow(child) {
135                children.push(child_id);
136            }
137        }
138
139        for child in &children {
140            self.export_node_deep(*child);
141        }
142
143        let mut all_children = BumpVec::with_capacity_in(
144            children.len() + self.decl_operations.len() + self.implicit_imports.len(),
145            self.bump,
146        );
147
148        all_children.extend(self.implicit_imports.drain().map(|(_, id)| id));
149        all_children.extend(self.decl_operations.values().copied());
150        all_children.extend(children);
151
152        let mut meta = Vec::new();
153        self.export_node_json_metadata(self.hugr.module_root(), &mut meta);
154
155        let (links, ports) = self.links.exit();
156        self.symbols.exit();
157
158        self.module.regions[self.module.root.index()] = table::Region {
159            kind: model::RegionKind::Module,
160            sources: &[],
161            targets: &[],
162            children: all_children.into_bump_slice(),
163            meta: self.bump.alloc_slice_copy(&meta),
164            signature: None,
165            scope: Some(table::RegionScope { links, ports }),
166        };
167    }
168
169    pub fn make_ports(
170        &mut self,
171        node: Node,
172        direction: Direction,
173        num_ports: usize,
174    ) -> &'a [table::LinkIndex] {
175        let ports = self.hugr.node_ports(node, direction);
176        let mut links = BumpVec::with_capacity_in(ports.size_hint().0, self.bump);
177
178        for port in ports.take(num_ports) {
179            links.push(self.links.use_link(node, port));
180        }
181
182        links.into_bump_slice()
183    }
184
185    pub fn make_term(&mut self, term: table::Term<'a>) -> table::TermId {
186        // There is a canonical id for wildcard terms.
187        if term == table::Term::Wildcard {
188            return table::TermId::default();
189        }
190
191        // We can omit a prefix of wildcard terms for symbol applications.
192        let term = match term {
193            table::Term::Apply(symbol, args) => {
194                let prefix = args.iter().take_while(|arg| !arg.is_valid()).count();
195                table::Term::Apply(symbol, &args[prefix..])
196            }
197            term => term,
198        };
199
200        *self
201            .term_map
202            .entry(term.clone())
203            .or_insert_with(|| self.module.insert_term(term))
204    }
205
206    pub fn make_qualified_name(
207        &mut self,
208        extension: &ExtensionId,
209        name: impl AsRef<str>,
210    ) -> &'a str {
211        let capacity = extension.len() + name.as_ref().len() + 1;
212        let mut output = BumpString::with_capacity_in(capacity, self.bump);
213        let _ = write!(&mut output, "{}.{}", extension, name.as_ref());
214        output.into_bump_str()
215    }
216
217    pub fn make_named_global_ref(
218        &mut self,
219        extension: &IdentList,
220        name: impl AsRef<str>,
221    ) -> table::NodeId {
222        let symbol = self.make_qualified_name(extension, name);
223        self.resolve_symbol(symbol)
224    }
225
226    /// Get the node that declares or defines the function associated with the given
227    /// node via the static input. Returns `None` if the node is not connected to a function.
228    fn connected_function(&self, node: Node) -> Option<Node> {
229        let func_node = self.hugr.static_source(node)?;
230
231        match self.hugr.get_optype(func_node) {
232            OpType::FuncDecl(_) => Some(func_node),
233            OpType::FuncDefn(_) => Some(func_node),
234            _ => None,
235        }
236    }
237
238    fn with_local_scope<T>(&mut self, node: table::NodeId, f: impl FnOnce(&mut Self) -> T) -> T {
239        let prev_local_scope = self.local_scope.replace(node);
240        let prev_local_constraints = std::mem::take(&mut self.local_constraints);
241        let result = f(self);
242        self.local_scope = prev_local_scope;
243        self.local_constraints = prev_local_constraints;
244        result
245    }
246
247    fn export_node_shallow(&mut self, node: Node) -> Option<table::NodeId> {
248        let optype = self.hugr.get_optype(node);
249
250        // We skip nodes that are not exported as nodes in the model.
251        if let OpType::Const(_)
252        | OpType::Input(_)
253        | OpType::Output(_)
254        | OpType::ExitBlock(_)
255        | OpType::Case(_) = optype
256        {
257            return None;
258        }
259
260        let node_id = self.module.insert_node(table::Node::default());
261        self.node_to_id.insert(node, node_id);
262        self.id_to_node.insert(node_id, node);
263
264        // We record the name of the symbol defined by the node, if any.
265        let symbol = match optype {
266            OpType::FuncDefn(_) | OpType::FuncDecl(_) => {
267                // Functions aren't exported using their core name but with a mangled
268                // name derived from their id. The function's core name will be recorded
269                // using `core.title` metadata.
270                Some(self.mangled_name(node))
271            }
272            OpType::AliasDecl(alias_decl) => Some(alias_decl.name.as_str()),
273            OpType::AliasDefn(alias_defn) => Some(alias_defn.name.as_str()),
274            _ => None,
275        };
276
277        if let Some(symbol) = symbol {
278            self.symbols
279                .insert(symbol, node_id)
280                .expect("duplicate symbol");
281        }
282
283        Some(node_id)
284    }
285
286    fn export_node_deep(&mut self, node_id: table::NodeId) {
287        // We insert a dummy node with the invalid operation at this point to reserve
288        // the node id. This is necessary to establish the correct node id for the
289        // local scope introduced by some operations. We will overwrite this node later.
290        let mut regions: &[_] = &[];
291        let mut meta = Vec::new();
292
293        let node = self.id_to_node[&node_id];
294        let optype = self.hugr.get_optype(node);
295
296        let operation = match optype {
297            OpType::Module(_) => todo!("this should be an error"),
298
299            OpType::Input(_) => {
300                panic!("input nodes should have been handled by the region export")
301            }
302
303            OpType::Output(_) => {
304                panic!("output nodes should have been handled by the region export")
305            }
306
307            OpType::DFG(_) => {
308                regions = self.bump.alloc_slice_copy(&[self.export_dfg(
309                    node,
310                    model::ScopeClosure::Open,
311                    false,
312                    false,
313                )]);
314                table::Operation::Dfg
315            }
316
317            OpType::CFG(_) => {
318                regions = self
319                    .bump
320                    .alloc_slice_copy(&[self.export_cfg(node, model::ScopeClosure::Open)]);
321                table::Operation::Cfg
322            }
323
324            OpType::ExitBlock(_) => {
325                panic!("exit blocks should have been handled by the region export")
326            }
327
328            OpType::Case(_) => {
329                todo!("case nodes should have been handled by the region export")
330            }
331
332            OpType::DataflowBlock(_) => {
333                regions = self.bump.alloc_slice_copy(&[self.export_dfg(
334                    node,
335                    model::ScopeClosure::Open,
336                    false,
337                    false,
338                )]);
339                table::Operation::Block
340            }
341
342            OpType::FuncDefn(func) => self.with_local_scope(node_id, |this| {
343                let symbol_name = this.export_func_name(node, &mut meta);
344
345                let symbol = this.export_poly_func_type(
346                    symbol_name,
347                    Some(func.visibility().clone().into()),
348                    func.signature(),
349                );
350                regions = this.bump.alloc_slice_copy(&[this.export_dfg(
351                    node,
352                    model::ScopeClosure::Closed,
353                    false,
354                    false,
355                )]);
356                table::Operation::DefineFunc(symbol)
357            }),
358
359            OpType::FuncDecl(func) => self.with_local_scope(node_id, |this| {
360                let symbol_name = this.export_func_name(node, &mut meta);
361
362                let symbol = this.export_poly_func_type(
363                    symbol_name,
364                    Some(func.visibility().clone().into()),
365                    func.signature(),
366                );
367                table::Operation::DeclareFunc(symbol)
368            }),
369
370            OpType::AliasDecl(alias) => self.with_local_scope(node_id, |this| {
371                // TODO: We should support aliases with different types and with parameters
372                let signature = this.make_term_apply(model::CORE_TYPE, &[]);
373                let symbol = this.bump.alloc(table::Symbol {
374                    visibility: &NO_VIS, // not spec'd in hugr-core
375                    name: &alias.name,
376                    params: &[],
377                    constraints: &[],
378                    signature,
379                });
380                table::Operation::DeclareAlias(symbol)
381            }),
382
383            OpType::AliasDefn(alias) => self.with_local_scope(node_id, |this| {
384                let value = this.export_type(&alias.definition);
385                // TODO: We should support aliases with different types and with parameters
386                let signature = this.make_term_apply(model::CORE_TYPE, &[]);
387                let symbol = this.bump.alloc(table::Symbol {
388                    visibility: &NO_VIS, // not spec'd in hugr-core
389                    name: &alias.name,
390                    params: &[],
391                    constraints: &[],
392                    signature,
393                });
394                table::Operation::DefineAlias(symbol, value)
395            }),
396
397            OpType::Call(call) => {
398                // TODO: If the node is not connected to a function, we should do better than panic.
399                let node = self.connected_function(node).unwrap();
400                let symbol = self.node_to_id[&node];
401                let mut args = BumpVec::new_in(self.bump);
402                args.extend(call.type_args.iter().map(|arg| self.export_term(arg, None)));
403                let args = args.into_bump_slice();
404                let func = self.make_term(table::Term::Apply(symbol, args));
405
406                // TODO PERFORMANCE: Avoid exporting the signature here again.
407                let signature = call.signature();
408                let inputs = self.export_type_row(&signature.input);
409                let outputs = self.export_type_row(&signature.output);
410                let operation = self.make_term_apply(model::CORE_CALL, &[inputs, outputs, func]);
411                table::Operation::Custom(operation)
412            }
413
414            OpType::LoadFunction(load) => {
415                let node = self.connected_function(node).unwrap();
416                let symbol = self.node_to_id[&node];
417                let mut args = BumpVec::new_in(self.bump);
418                args.extend(load.type_args.iter().map(|arg| self.export_term(arg, None)));
419                let args = args.into_bump_slice();
420                let func = self.make_term(table::Term::Apply(symbol, args));
421                let runtime_type = self.make_term(table::Term::Wildcard);
422                let operation = self.make_term_apply(model::CORE_LOAD_CONST, &[runtime_type, func]);
423                table::Operation::Custom(operation)
424            }
425
426            OpType::Const(_) => {
427                unreachable!("const nodes are filtered out by `export_node_shallow`")
428            }
429
430            OpType::LoadConstant(_) => {
431                // TODO: If the node is not connected to a constant, we should do better than panic.
432                let const_node = self.hugr.static_source(node).unwrap();
433                let const_node_op = self.hugr.get_optype(const_node);
434
435                let OpType::Const(const_node_data) = const_node_op else {
436                    panic!("expected `LoadConstant` node to be connected to a `Const` node");
437                };
438
439                // TODO: Share the constant value between all nodes that load it.
440
441                let runtime_type = self.make_term(table::Term::Wildcard);
442                let value = self.export_value(&const_node_data.value);
443                let operation =
444                    self.make_term_apply(model::CORE_LOAD_CONST, &[runtime_type, value]);
445                table::Operation::Custom(operation)
446            }
447
448            OpType::CallIndirect(call) => {
449                let inputs = self.export_type_row(&call.signature.input);
450                let outputs = self.export_type_row(&call.signature.output);
451                let operation = self.make_term_apply(model::CORE_CALL_INDIRECT, &[inputs, outputs]);
452                table::Operation::Custom(operation)
453            }
454
455            OpType::Tag(tag) => {
456                let variants = self.make_term(table::Term::Wildcard);
457                let types = self.make_term(table::Term::Wildcard);
458                let tag = self.make_term(model::Literal::Nat(tag.tag as u64).into());
459                let operation = self.make_term_apply(model::CORE_MAKE_ADT, &[variants, types, tag]);
460                table::Operation::Custom(operation)
461            }
462
463            OpType::TailLoop(_) => {
464                regions = self.bump.alloc_slice_copy(&[self.export_dfg(
465                    node,
466                    model::ScopeClosure::Open,
467                    false,
468                    false,
469                )]);
470                table::Operation::TailLoop
471            }
472
473            OpType::Conditional(_) => {
474                regions = self.export_conditional_regions(node);
475                table::Operation::Conditional
476            }
477
478            OpType::ExtensionOp(op) => {
479                let node = self.export_opdef(op.def());
480                let params = self
481                    .bump
482                    .alloc_slice_fill_iter(op.args().iter().map(|arg| self.export_term(arg, None)));
483                let operation = self.make_term(table::Term::Apply(node, params));
484                table::Operation::Custom(operation)
485            }
486
487            OpType::OpaqueOp(op) => {
488                let node = self.make_named_global_ref(op.extension(), op.unqualified_id());
489                let params = self
490                    .bump
491                    .alloc_slice_fill_iter(op.args().iter().map(|arg| self.export_term(arg, None)));
492                let operation = self.make_term(table::Term::Apply(node, params));
493                table::Operation::Custom(operation)
494            }
495        };
496
497        let (signature, num_inputs, num_outputs) = match optype {
498            OpType::DataflowBlock(block) => {
499                let signature = self.export_block_signature(block);
500                (Some(signature), 1, block.sum_rows.len())
501            }
502
503            // PERFORMANCE: As it stands, `OpType::dataflow_signature` copies and/or allocates.
504            // That might not seem like a big deal, but it's a significant portion of the time spent
505            // when exporting. However it is not trivial to change this at the moment.
506            _ => match &optype.dataflow_signature() {
507                Some(signature) => {
508                    let num_inputs = signature.input_types().len();
509                    let num_outputs = signature.output_types().len();
510                    let signature = self.export_func_type(signature);
511                    (Some(signature), num_inputs, num_outputs)
512                }
513                None => (None, 0, 0),
514            },
515        };
516
517        let inputs = self.make_ports(node, Direction::Incoming, num_inputs);
518        let outputs = self.make_ports(node, Direction::Outgoing, num_outputs);
519
520        self.export_node_json_metadata(node, &mut meta);
521        self.export_node_order_metadata(node, &mut meta);
522        self.export_node_entrypoint_metadata(node, &mut meta);
523        let meta = self.bump.alloc_slice_copy(&meta);
524
525        self.module.nodes[node_id.index()] = table::Node {
526            operation,
527            inputs,
528            outputs,
529            regions,
530            meta,
531            signature,
532        };
533    }
534
535    /// Export an `OpDef` as an operation declaration.
536    ///
537    /// Operations that allow a declarative form are exported as a reference to
538    /// an operation declaration node, and this node is reused for all instances
539    /// of the operation. The node is added to the `decl_operations` map so that
540    /// at the end of the export, the operation declaration nodes can be added
541    /// to the module as children of the module region.
542    pub fn export_opdef(&mut self, opdef: &OpDef) -> table::NodeId {
543        use std::collections::hash_map::Entry;
544
545        let poly_func_type = match opdef.signature_func() {
546            SignatureFunc::PolyFuncType(poly_func_type) => poly_func_type,
547            _ => return self.make_named_global_ref(opdef.extension_id(), opdef.name()),
548        };
549
550        let key = (opdef.extension_id().clone(), opdef.name().clone());
551        let entry = self.decl_operations.entry(key);
552
553        let node = match entry {
554            Entry::Occupied(occupied_entry) => return *occupied_entry.get(),
555            Entry::Vacant(vacant_entry) => {
556                *vacant_entry.insert(self.module.insert_node(table::Node::default()))
557            }
558        };
559
560        let symbol = self.with_local_scope(node, |this| {
561            let name = this.make_qualified_name(opdef.extension_id(), opdef.name());
562            this.export_poly_func_type(name, None, poly_func_type)
563        });
564
565        let meta = {
566            let description = Some(opdef.description()).filter(|d| !d.is_empty());
567            let meta_len = opdef.iter_misc().len() + usize::from(description.is_some());
568            let mut meta = BumpVec::with_capacity_in(meta_len, self.bump);
569
570            if let Some(description) = description {
571                let value = self.make_term(model::Literal::Str(description.into()).into());
572                meta.push(self.make_term_apply(model::CORE_META_DESCRIPTION, &[value]));
573            }
574
575            for (name, value) in opdef.iter_misc() {
576                meta.push(self.make_json_meta(name, value));
577            }
578
579            self.bump.alloc_slice_copy(&meta)
580        };
581
582        let node_data = self.module.get_node_mut(node).unwrap();
583        node_data.operation = table::Operation::DeclareOperation(symbol);
584        node_data.meta = meta;
585
586        node
587    }
588
589    /// Export the signature of a `DataflowBlock`. Here we can't use `OpType::dataflow_signature`
590    /// like for the other nodes since the ports are control flow ports.
591    pub fn export_block_signature(&mut self, block: &DataflowBlock) -> table::TermId {
592        let inputs = {
593            let inputs = self.export_type_row(&block.inputs);
594            self.make_term(table::Term::List(
595                self.bump.alloc_slice_copy(&[table::SeqPart::Item(inputs)]),
596            ))
597        };
598
599        let tail = self.export_type_row(&block.other_outputs);
600
601        let outputs = {
602            let mut outputs = BumpVec::with_capacity_in(block.sum_rows.len(), self.bump);
603            for sum_row in &block.sum_rows {
604                let variant = self.export_type_row_with_tail(sum_row, Some(tail));
605                outputs.push(table::SeqPart::Item(variant));
606            }
607            self.make_term(table::Term::List(outputs.into_bump_slice()))
608        };
609
610        self.make_term_apply(model::CORE_CTRL, &[inputs, outputs])
611    }
612
613    /// Creates a data flow region from the given node's children.
614    ///
615    /// `Input` and `Output` nodes are used to determine the source and target ports of the region.
616    pub fn export_dfg(
617        &mut self,
618        node: Node,
619        closure: model::ScopeClosure,
620        export_json_meta: bool,
621        export_entrypoint_meta: bool,
622    ) -> table::RegionId {
623        let region = self.module.insert_region(table::Region::default());
624
625        self.symbols.enter(region);
626        if closure == model::ScopeClosure::Closed {
627            self.links.enter(region);
628        }
629
630        let mut sources: &[_] = &[];
631        let mut targets: &[_] = &[];
632        let mut input_types = None;
633        let mut output_types = None;
634
635        let mut meta = Vec::new();
636
637        if export_json_meta {
638            self.export_node_json_metadata(node, &mut meta);
639        }
640        if export_entrypoint_meta {
641            self.export_node_entrypoint_metadata(node, &mut meta);
642        }
643
644        let children = self.hugr.children(node);
645        let mut region_children = BumpVec::with_capacity_in(children.size_hint().0 - 2, self.bump);
646
647        for child in children {
648            match self.hugr.get_optype(child) {
649                OpType::Input(input) => {
650                    sources = self.make_ports(child, Direction::Outgoing, input.types.len());
651                    input_types = Some(&input.types);
652
653                    if has_order_edges(self.hugr, child) {
654                        let key = self.make_term(model::Literal::Nat(child.index() as u64).into());
655                        meta.push(self.make_term_apply(model::ORDER_HINT_INPUT_KEY, &[key]));
656                    }
657                }
658                OpType::Output(output) => {
659                    targets = self.make_ports(child, Direction::Incoming, output.types.len());
660                    output_types = Some(&output.types);
661
662                    if has_order_edges(self.hugr, child) {
663                        let key = self.make_term(model::Literal::Nat(child.index() as u64).into());
664                        meta.push(self.make_term_apply(model::ORDER_HINT_OUTPUT_KEY, &[key]));
665                    }
666                }
667                _ => {
668                    if let Some(child_id) = self.export_node_shallow(child) {
669                        region_children.push(child_id);
670                    }
671                }
672            }
673
674            // Record all order edges that originate from this node in metadata.
675            let successors = self
676                .hugr
677                .get_optype(child)
678                .other_output_port()
679                .into_iter()
680                .flat_map(|port| self.hugr.linked_inputs(child, port))
681                .map(|(successor, _)| successor);
682
683            for successor in successors {
684                let a = self.make_term(model::Literal::Nat(child.index() as u64).into());
685                let b = self.make_term(model::Literal::Nat(successor.index() as u64).into());
686                meta.push(self.make_term_apply(model::ORDER_HINT_ORDER, &[a, b]));
687            }
688        }
689
690        for child_id in &region_children {
691            self.export_node_deep(*child_id);
692        }
693
694        let signature = {
695            let inputs = self.export_type_row(input_types.unwrap());
696            let outputs = self.export_type_row(output_types.unwrap());
697            Some(self.make_term_apply(model::CORE_FN, &[inputs, outputs]))
698        };
699
700        let scope = match closure {
701            model::ScopeClosure::Closed => {
702                let (links, ports) = self.links.exit();
703                Some(table::RegionScope { links, ports })
704            }
705            model::ScopeClosure::Open => None,
706        };
707        self.symbols.exit();
708
709        self.module.regions[region.index()] = table::Region {
710            kind: model::RegionKind::DataFlow,
711            sources,
712            targets,
713            children: region_children.into_bump_slice(),
714            meta: self.bump.alloc_slice_copy(&meta),
715            signature,
716            scope,
717        };
718
719        region
720    }
721
722    /// Creates a control flow region from the given node's children.
723    pub fn export_cfg(&mut self, node: Node, closure: model::ScopeClosure) -> table::RegionId {
724        let region = self.module.insert_region(table::Region::default());
725        self.symbols.enter(region);
726
727        if closure == model::ScopeClosure::Closed {
728            self.links.enter(region);
729        }
730
731        let mut source = None;
732        let mut targets: &[_] = &[];
733
734        let mut meta = Vec::new();
735        self.export_node_json_metadata(node, &mut meta);
736        self.export_node_entrypoint_metadata(node, &mut meta);
737
738        let children = self.hugr.children(node);
739        let mut region_children = BumpVec::with_capacity_in(children.size_hint().0 - 1, self.bump);
740
741        for child in children {
742            if let OpType::ExitBlock(_) = self.hugr.get_optype(child) {
743                targets = self.make_ports(child, Direction::Incoming, 1);
744            } else {
745                if let Some(child_id) = self.export_node_shallow(child) {
746                    region_children.push(child_id);
747                }
748
749                if source.is_none() {
750                    source = Some(self.links.use_link(child, IncomingPort::from(0)));
751                }
752            }
753        }
754
755        for child_id in &region_children {
756            self.export_node_deep(*child_id);
757        }
758
759        // Get the signature of the control flow region.
760        let signature = {
761            let node_signature = self.hugr.signature(node).unwrap();
762
763            let inputs = {
764                let types = self.export_type_row(node_signature.input());
765                self.make_term(table::Term::List(
766                    self.bump.alloc_slice_copy(&[table::SeqPart::Item(types)]),
767                ))
768            };
769
770            let outputs = {
771                let types = self.export_type_row(node_signature.output());
772                self.make_term(table::Term::List(
773                    self.bump.alloc_slice_copy(&[table::SeqPart::Item(types)]),
774                ))
775            };
776
777            Some(self.make_term_apply(model::CORE_CTRL, &[inputs, outputs]))
778        };
779
780        let scope = match closure {
781            model::ScopeClosure::Closed => {
782                let (links, ports) = self.links.exit();
783                Some(table::RegionScope { links, ports })
784            }
785            model::ScopeClosure::Open => None,
786        };
787        self.symbols.exit();
788
789        self.module.regions[region.index()] = table::Region {
790            kind: model::RegionKind::ControlFlow,
791            sources: self.bump.alloc_slice_copy(&[source.unwrap()]),
792            targets,
793            children: region_children.into_bump_slice(),
794            meta: self.bump.alloc_slice_copy(&meta),
795            signature,
796            scope,
797        };
798
799        region
800    }
801
802    /// Export the `Case` node children of a `Conditional` node as data flow regions.
803    pub fn export_conditional_regions(&mut self, node: Node) -> &'a [table::RegionId] {
804        let children = self.hugr.children(node);
805        let mut regions = BumpVec::with_capacity_in(children.size_hint().0, self.bump);
806
807        for child in children {
808            let OpType::Case(_) = self.hugr.get_optype(child) else {
809                panic!("expected a `Case` node as a child of a `Conditional` node");
810            };
811
812            regions.push(self.export_dfg(child, model::ScopeClosure::Open, true, true));
813        }
814
815        regions.into_bump_slice()
816    }
817
818    /// Exports a polymorphic function type.
819    pub fn export_poly_func_type<RV: MaybeRV>(
820        &mut self,
821        name: &'a str,
822        visibility: Option<model::Visibility>,
823        t: &PolyFuncTypeBase<RV>,
824    ) -> &'a table::Symbol<'a> {
825        let mut params = BumpVec::with_capacity_in(t.params().len(), self.bump);
826        let scope = self
827            .local_scope
828            .expect("exporting poly func type outside of local scope");
829        let visibility = self.bump.alloc(visibility);
830        for (i, param) in t.params().iter().enumerate() {
831            let name = self.bump.alloc_str(&i.to_string());
832            let r#type = self.export_term(param, Some((scope, i as _)));
833            let param = table::Param { name, r#type };
834            params.push(param);
835        }
836
837        let constraints = self.bump.alloc_slice_copy(&self.local_constraints);
838        let body = self.export_func_type(t.body());
839
840        self.bump.alloc(table::Symbol {
841            visibility,
842            name,
843            params: params.into_bump_slice(),
844            constraints,
845            signature: body,
846        })
847    }
848
849    pub fn export_type<RV: MaybeRV>(&mut self, t: &TypeBase<RV>) -> table::TermId {
850        self.export_type_enum(t.as_type_enum())
851    }
852
853    pub fn export_type_enum<RV: MaybeRV>(&mut self, t: &TypeEnum<RV>) -> table::TermId {
854        match t {
855            TypeEnum::Extension(ext) => self.export_custom_type(ext),
856            TypeEnum::Alias(alias) => {
857                let symbol = self.resolve_symbol(self.bump.alloc_str(alias.name()));
858                self.make_term(table::Term::Apply(symbol, &[]))
859            }
860            TypeEnum::Function(func) => self.export_func_type(func),
861            TypeEnum::Variable(index, _) => {
862                let node = self.local_scope.expect("local variable out of scope");
863                self.make_term(table::Term::Var(table::VarId(node, *index as _)))
864            }
865            TypeEnum::RowVar(rv) => self.export_row_var(rv.as_rv()),
866            TypeEnum::Sum(sum) => self.export_sum_type(sum),
867        }
868    }
869
870    pub fn export_func_type<RV: MaybeRV>(&mut self, t: &FuncTypeBase<RV>) -> table::TermId {
871        let inputs = self.export_type_row(t.input());
872        let outputs = self.export_type_row(t.output());
873        self.make_term_apply(model::CORE_FN, &[inputs, outputs])
874    }
875
876    pub fn export_custom_type(&mut self, t: &CustomType) -> table::TermId {
877        let symbol = self.make_named_global_ref(t.extension(), t.name());
878
879        let args = self
880            .bump
881            .alloc_slice_fill_iter(t.args().iter().map(|p| self.export_term(p, None)));
882        let term = table::Term::Apply(symbol, args);
883        self.make_term(term)
884    }
885
886    pub fn export_type_arg_var(&mut self, var: &TermVar) -> table::TermId {
887        let node = self.local_scope.expect("local variable out of scope");
888        self.make_term(table::Term::Var(table::VarId(node, var.index() as _)))
889    }
890
891    pub fn export_row_var(&mut self, t: &RowVariable) -> table::TermId {
892        let node = self.local_scope.expect("local variable out of scope");
893        self.make_term(table::Term::Var(table::VarId(node, t.0 as _)))
894    }
895
896    pub fn export_sum_variants(&mut self, t: &SumType) -> table::TermId {
897        match t {
898            SumType::Unit { size } => {
899                let parts = self.bump.alloc_slice_fill_iter(
900                    (0..*size)
901                        .map(|_| table::SeqPart::Item(self.make_term(table::Term::List(&[])))),
902                );
903                self.make_term(table::Term::List(parts))
904            }
905            SumType::General { rows } => {
906                let parts = self.bump.alloc_slice_fill_iter(
907                    rows.iter()
908                        .map(|row| table::SeqPart::Item(self.export_type_row(row))),
909                );
910                self.make_term(table::Term::List(parts))
911            }
912        }
913    }
914
915    pub fn export_sum_type(&mut self, t: &SumType) -> table::TermId {
916        let variants = self.export_sum_variants(t);
917        self.make_term_apply(model::CORE_ADT, &[variants])
918    }
919
920    #[inline]
921    pub fn export_type_row<RV: MaybeRV>(&mut self, row: &TypeRowBase<RV>) -> table::TermId {
922        self.export_type_row_with_tail(row, None)
923    }
924
925    pub fn export_type_row_with_tail<RV: MaybeRV>(
926        &mut self,
927        row: &TypeRowBase<RV>,
928        tail: Option<table::TermId>,
929    ) -> table::TermId {
930        let mut parts =
931            BumpVec::with_capacity_in(row.len() + usize::from(tail.is_some()), self.bump);
932
933        for t in row.iter() {
934            match t.as_type_enum() {
935                TypeEnum::RowVar(var) => {
936                    parts.push(table::SeqPart::Splice(self.export_row_var(var.as_rv())));
937                }
938                _ => {
939                    parts.push(table::SeqPart::Item(self.export_type(t)));
940                }
941            }
942        }
943
944        if let Some(tail) = tail {
945            parts.push(table::SeqPart::Splice(tail));
946        }
947
948        let parts = parts.into_bump_slice();
949        self.make_term(table::Term::List(parts))
950    }
951
952    /// Exports a term.
953    ///
954    /// The `var` argument is set when the term being exported is the
955    /// type of a parameter to a polymorphic definition. In that case we can
956    /// generate a `nonlinear` constraint for the type of runtime types marked as
957    /// `TypeBound::Copyable`.
958    pub fn export_term(
959        &mut self,
960        t: &Term,
961        var: Option<(table::NodeId, table::VarIndex)>,
962    ) -> table::TermId {
963        match t {
964            Term::RuntimeType(b) => {
965                if let (Some((node, index)), TypeBound::Copyable) = (var, b) {
966                    let term = self.make_term(table::Term::Var(table::VarId(node, index)));
967                    let non_linear = self.make_term_apply(model::CORE_NON_LINEAR, &[term]);
968                    self.local_constraints.push(non_linear);
969                }
970
971                self.make_term_apply(model::CORE_TYPE, &[])
972            }
973            Term::BoundedNatType(_) => self.make_term_apply(model::CORE_NAT_TYPE, &[]),
974            Term::StringType => self.make_term_apply(model::CORE_STR_TYPE, &[]),
975            Term::BytesType => self.make_term_apply(model::CORE_BYTES_TYPE, &[]),
976            Term::FloatType => self.make_term_apply(model::CORE_FLOAT_TYPE, &[]),
977            Term::ListType(item_type) => {
978                let item_type = self.export_term(item_type, None);
979                self.make_term_apply(model::CORE_LIST_TYPE, &[item_type])
980            }
981            Term::TupleType(item_types) => {
982                let item_types = self.export_term(item_types, None);
983                self.make_term_apply(model::CORE_TUPLE_TYPE, &[item_types])
984            }
985            Term::Runtime(ty) => self.export_type(ty),
986            Term::BoundedNat(value) => self.make_term(model::Literal::Nat(*value).into()),
987            Term::String(value) => self.make_term(model::Literal::Str(value.into()).into()),
988            Term::Float(value) => self.make_term(model::Literal::Float(*value).into()),
989            Term::Bytes(value) => self.make_term(model::Literal::Bytes(value.clone()).into()),
990            Term::List(elems) => {
991                let parts = self.bump.alloc_slice_fill_iter(
992                    elems
993                        .iter()
994                        .map(|elem| table::SeqPart::Item(self.export_term(elem, None))),
995                );
996                self.make_term(table::Term::List(parts))
997            }
998            Term::ListConcat(lists) => {
999                let parts = self.bump.alloc_slice_fill_iter(
1000                    lists
1001                        .iter()
1002                        .map(|elem| table::SeqPart::Splice(self.export_term(elem, None))),
1003                );
1004                self.make_term(table::Term::List(parts))
1005            }
1006            Term::Tuple(elems) => {
1007                let parts = self.bump.alloc_slice_fill_iter(
1008                    elems
1009                        .iter()
1010                        .map(|elem| table::SeqPart::Item(self.export_term(elem, None))),
1011                );
1012                self.make_term(table::Term::Tuple(parts))
1013            }
1014            Term::TupleConcat(tuples) => {
1015                let parts = self.bump.alloc_slice_fill_iter(
1016                    tuples
1017                        .iter()
1018                        .map(|elem| table::SeqPart::Splice(self.export_term(elem, None))),
1019                );
1020                self.make_term(table::Term::Tuple(parts))
1021            }
1022            Term::Variable(v) => self.export_type_arg_var(v),
1023            Term::StaticType => self.make_term_apply(model::CORE_STATIC, &[]),
1024            Term::ConstType(ty) => {
1025                let ty = self.export_type(ty);
1026                self.make_term_apply(model::CORE_CONST, &[ty])
1027            }
1028        }
1029    }
1030
1031    fn export_value(&mut self, value: &'a Value) -> table::TermId {
1032        match value {
1033            Value::Extension { e } => {
1034                // NOTE: We have special cased arrays, integers, and floats for now.
1035                // TODO: Allow arbitrary extension values to be exported as terms.
1036
1037                if let Some(array) = e.value().downcast_ref::<ArrayValue>() {
1038                    let len = self
1039                        .make_term(model::Literal::Nat(array.get_contents().len() as u64).into());
1040                    let element_type = self.export_type(array.get_element_type());
1041                    let mut contents =
1042                        BumpVec::with_capacity_in(array.get_contents().len(), self.bump);
1043
1044                    for element in array.get_contents() {
1045                        contents.push(table::SeqPart::Item(self.export_value(element)));
1046                    }
1047
1048                    let contents = self.make_term(table::Term::List(contents.into_bump_slice()));
1049
1050                    let symbol = self.resolve_symbol(ArrayValue::CTR_NAME);
1051                    let args = self.bump.alloc_slice_copy(&[len, element_type, contents]);
1052                    return self.make_term(table::Term::Apply(symbol, args));
1053                }
1054
1055                if let Some(v) = e.value().downcast_ref::<ConstInt>() {
1056                    let bitwidth =
1057                        self.make_term(model::Literal::Nat(u64::from(v.log_width())).into());
1058                    let literal = self.make_term(model::Literal::Nat(v.value_u()).into());
1059
1060                    let symbol = self.resolve_symbol(ConstInt::CTR_NAME);
1061                    let args = self.bump.alloc_slice_copy(&[bitwidth, literal]);
1062                    return self.make_term(table::Term::Apply(symbol, args));
1063                }
1064
1065                if let Some(v) = e.value().downcast_ref::<ConstF64>() {
1066                    let literal = self.make_term(model::Literal::Float(v.value().into()).into());
1067                    let symbol = self.resolve_symbol(ConstF64::CTR_NAME);
1068                    let args = self.bump.alloc_slice_copy(&[literal]);
1069                    return self.make_term(table::Term::Apply(symbol, args));
1070                }
1071
1072                let json = match e.value().downcast_ref::<CustomSerialized>() {
1073                    Some(custom) => serde_json::to_string(custom.value()).unwrap(),
1074                    None => serde_json::to_string(e.value())
1075                        .expect("custom extension values should be serializable"),
1076                };
1077
1078                let json = self.make_term(model::Literal::Str(json.into()).into());
1079                let runtime_type = self.export_type(&e.get_type());
1080                let args = self.bump.alloc_slice_copy(&[runtime_type, json]);
1081                let symbol = self.resolve_symbol(model::COMPAT_CONST_JSON);
1082                self.make_term(table::Term::Apply(symbol, args))
1083            }
1084
1085            Value::Function { hugr } => {
1086                let outer_hugr = std::mem::replace(&mut self.hugr, hugr);
1087                let outer_node_to_id = std::mem::take(&mut self.node_to_id);
1088
1089                let region = match hugr.entrypoint_optype() {
1090                    OpType::DFG(_) => {
1091                        self.export_dfg(hugr.entrypoint(), model::ScopeClosure::Closed, true, true)
1092                    }
1093                    _ => panic!("Value::Function root must be a DFG"),
1094                };
1095
1096                self.node_to_id = outer_node_to_id;
1097                self.hugr = outer_hugr;
1098
1099                self.make_term(table::Term::Func(region))
1100            }
1101
1102            Value::Sum(sum) => {
1103                let variants = self.export_sum_variants(&sum.sum_type);
1104                let types = self.make_term(table::Term::Wildcard);
1105                let tag = self.make_term(model::Literal::Nat(sum.tag as u64).into());
1106
1107                let values = {
1108                    let mut values = BumpVec::with_capacity_in(sum.values.len(), self.bump);
1109
1110                    for value in &sum.values {
1111                        values.push(table::SeqPart::Item(self.export_value(value)));
1112                    }
1113
1114                    self.make_term(table::Term::Tuple(values.into_bump_slice()))
1115                };
1116
1117                self.make_term_apply(model::CORE_CONST_ADT, &[variants, types, tag, values])
1118            }
1119        }
1120    }
1121
1122    fn export_node_json_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1123        let metadata_map = self.hugr.node_metadata_map(node);
1124        meta.reserve(metadata_map.len());
1125
1126        for (name, value) in metadata_map {
1127            meta.push(self.make_json_meta(name, value));
1128        }
1129    }
1130
1131    fn export_node_order_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1132        if has_order_edges(self.hugr, node) {
1133            let key = self.make_term(model::Literal::Nat(node.index() as u64).into());
1134            meta.push(self.make_term_apply(model::ORDER_HINT_KEY, &[key]));
1135        }
1136    }
1137
1138    fn export_node_entrypoint_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1139        if self.hugr.entrypoint() == node {
1140            meta.push(self.make_term_apply(model::CORE_ENTRYPOINT, &[]));
1141        }
1142    }
1143
1144    /// Used when exporting function definitions or declarations. When the
1145    /// function is public, its symbol name will be the core name. For private
1146    /// functions, the symbol name is derived from the node id and the core name
1147    /// is exported as `core.title` metadata.
1148    ///
1149    /// This is a hack, necessary due to core names for functions being
1150    /// non-functional. Once functions have a "link name", that should be used as the symbol name here.
1151    fn export_func_name(&mut self, node: Node, meta: &mut Vec<table::TermId>) -> &'a str {
1152        let (name, vis) = match self.hugr.get_optype(node) {
1153            OpType::FuncDefn(func_defn) => (func_defn.func_name(), func_defn.visibility()),
1154            OpType::FuncDecl(func_decl) => (func_decl.func_name(), func_decl.visibility()),
1155            _ => panic!(
1156                "`export_func_name` is only supposed to be used on function declarations and definitions"
1157            ),
1158        };
1159
1160        match vis {
1161            Visibility::Public => name,
1162            Visibility::Private => {
1163                let literal =
1164                    self.make_term(table::Term::Literal(model::Literal::Str(name.to_smolstr())));
1165                meta.push(self.make_term_apply(model::CORE_TITLE, &[literal]));
1166                self.mangled_name(node)
1167            }
1168        }
1169    }
1170
1171    pub fn make_json_meta(&mut self, name: &str, value: &serde_json::Value) -> table::TermId {
1172        let value = serde_json::to_string(value).expect("json values are always serializable");
1173        let value = self.make_term(model::Literal::Str(value.into()).into());
1174        let name = self.make_term(model::Literal::Str(name.into()).into());
1175        self.make_term_apply(model::COMPAT_META_JSON, &[name, value])
1176    }
1177
1178    fn resolve_symbol(&mut self, name: &'a str) -> table::NodeId {
1179        let result = self.symbols.resolve(name);
1180
1181        match result {
1182            Ok(node) => node,
1183            Err(_) => *self.implicit_imports.entry(name).or_insert_with(|| {
1184                self.module.insert_node(table::Node {
1185                    operation: table::Operation::Import { name },
1186                    ..table::Node::default()
1187                })
1188            }),
1189        }
1190    }
1191
1192    fn make_term_apply(&mut self, name: &'a str, args: &[table::TermId]) -> table::TermId {
1193        let symbol = self.resolve_symbol(name);
1194        let args = self.bump.alloc_slice_copy(args);
1195        self.make_term(table::Term::Apply(symbol, args))
1196    }
1197
1198    /// Creates a mangled name for a particular node.
1199    fn mangled_name(&self, node: Node) -> &'a str {
1200        bumpalo::format!(in &self.bump, "_{}", node.index()).into_bump_str()
1201    }
1202}
1203
1204type FxIndexSet<T> = indexmap::IndexSet<T, FxBuildHasher>;
1205
1206/// Data structure for translating the edges between ports in the `Hugr` graph
1207/// into the hypergraph representation used by `hugr_model`.
1208struct Links {
1209    /// Scoping helper that keeps track of the current nesting of regions
1210    /// and translates the group of connected ports into a link index.
1211    scope: model::scope::LinkTable<u32>,
1212
1213    /// A mapping from each port to the group of connected ports it belongs to.
1214    groups: FxHashMap<(Node, Port), u32>,
1215}
1216
1217impl Links {
1218    /// Create the `Links` data structure from a `Hugr` graph by recording the
1219    /// connectivity of the ports.
1220    pub fn new(hugr: &Hugr) -> Self {
1221        let scope = model::scope::LinkTable::new();
1222
1223        // We collect all ports that are in the hugr into an index set so that
1224        // we have an association between the port and a numeric index.
1225        let node_ports: FxIndexSet<(Node, Port)> = hugr
1226            .nodes()
1227            .flat_map(|node| hugr.all_node_ports(node).map(move |port| (node, port)))
1228            .collect();
1229
1230        // We then use a union-find data structure to group together all ports that are connected.
1231        let mut uf = UnionFind::<u32>::new(node_ports.len());
1232
1233        for (i, (node, port)) in node_ports.iter().enumerate() {
1234            if let Ok(port) = port.as_incoming() {
1235                for (other_node, other_port) in hugr.linked_outputs(*node, port) {
1236                    let other_port = Port::from(other_port);
1237                    let j = node_ports.get_index_of(&(other_node, other_port)).unwrap();
1238                    uf.union(i as u32, j as u32);
1239                }
1240            }
1241        }
1242
1243        // We then collect the association between the port and the group of connected ports it belongs to.
1244        let groups = node_ports
1245            .into_iter()
1246            .enumerate()
1247            .map(|(i, node_port)| (node_port, uf.find(i as u32)))
1248            .collect();
1249
1250        Self { scope, groups }
1251    }
1252
1253    /// Enter an isolated region.
1254    pub fn enter(&mut self, region: table::RegionId) {
1255        self.scope.enter(region);
1256    }
1257
1258    /// Leave an isolated region, returning the number of links and ports in the region.
1259    ///
1260    /// # Panics
1261    ///
1262    /// Panics if there is no remaining open scope to exit.
1263    pub fn exit(&mut self) -> (u32, u32) {
1264        self.scope.exit()
1265    }
1266
1267    /// Obtain the link index for a node and port.
1268    ///
1269    /// # Panics
1270    ///
1271    /// Panics if the port does not exist in the [`Hugr`] that was passed to `[Self::new]`.
1272    pub fn use_link(&mut self, node: Node, port: impl Into<Port>) -> table::LinkIndex {
1273        let port = port.into();
1274        let group = self.groups[&(node, port)];
1275        self.scope.use_link(group)
1276    }
1277}
1278
1279/// Returns `true` if a node has any incident order edges.
1280fn has_order_edges(hugr: &Hugr, node: Node) -> bool {
1281    let optype = hugr.get_optype(node);
1282    Direction::BOTH
1283        .iter()
1284        .filter(|dir| optype.other_port_kind(**dir) == Some(EdgeKind::StateOrder))
1285        .filter_map(|dir| optype.other_port(*dir))
1286        .flat_map(|port| hugr.linked_ports(node, port))
1287        .next()
1288        .is_some()
1289}
1290
1291#[cfg(test)]
1292mod test {
1293    use rstest::{fixture, rstest};
1294
1295    use crate::{
1296        Hugr,
1297        builder::{Dataflow, DataflowSubContainer},
1298        extension::prelude::qb_t,
1299        types::Signature,
1300        utils::test_quantum_extension::{cx_gate, h_gate},
1301    };
1302
1303    #[fixture]
1304    fn test_simple_circuit() -> Hugr {
1305        crate::builder::test::build_main(
1306            Signature::new_endo(vec![qb_t(), qb_t()]).into(),
1307            |mut f_build| {
1308                let wires: Vec<_> = f_build.input_wires().collect();
1309                let mut linear = f_build.as_circuit(wires);
1310
1311                assert_eq!(linear.n_wires(), 2);
1312
1313                linear
1314                    .append(h_gate(), [0])?
1315                    .append(cx_gate(), [0, 1])?
1316                    .append(cx_gate(), [1, 0])?;
1317
1318                let outs = linear.finish();
1319                f_build.finish_with_outputs(outs)
1320            },
1321        )
1322        .unwrap()
1323    }
1324
1325    #[rstest]
1326    #[case(test_simple_circuit())]
1327    fn test_export(#[case] hugr: Hugr) {
1328        use hugr_model::v0::bumpalo::Bump;
1329        let bump = Bump::new();
1330        let _model = super::export_hugr(&hugr, &bump);
1331    }
1332}