hugr_core/
export.rs

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