hugr_core/
export.rs

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