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    Direction, Hugr, HugrView, IncomingPort, Node, NodeIndex as _, Port,
6    extension::{ExtensionId, OpDef, SignatureFunc},
7    hugr::IdentList,
8    ops::{
9        DataflowBlock, DataflowOpTrait, OpName, OpTrait, OpType, Value, constant::CustomSerialized,
10    },
11    std_extensions::{
12        arithmetic::{float_types::ConstF64, int_types::ConstInt},
13        collections::array::ArrayValue,
14    },
15    types::{
16        CustomType, EdgeKind, FuncTypeBase, MaybeRV, PolyFuncTypeBase, RowVariable, SumType,
17        TypeArg, TypeBase, TypeBound, TypeEnum, TypeRow,
18        type_param::{TypeArgVariable, TypeParam},
19        type_row::TypeRowBase,
20    },
21};
22
23use fxhash::{FxBuildHasher, FxHashMap};
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
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 mut meta = Vec::new();
149        self.export_node_json_metadata(self.hugr.module_root(), &mut meta);
150
151        let (links, ports) = self.links.exit();
152        self.symbols.exit();
153
154        self.module.regions[self.module.root.index()] = table::Region {
155            kind: model::RegionKind::Module,
156            sources: &[],
157            targets: &[],
158            children: all_children.into_bump_slice(),
159            meta: self.bump.alloc_slice_copy(&meta),
160            signature: None,
161            scope: Some(table::RegionScope { links, ports }),
162        };
163    }
164
165    pub fn make_ports(
166        &mut self,
167        node: Node,
168        direction: Direction,
169        num_ports: usize,
170    ) -> &'a [table::LinkIndex] {
171        let ports = self.hugr.node_ports(node, direction);
172        let mut links = BumpVec::with_capacity_in(ports.size_hint().0, self.bump);
173
174        for port in ports.take(num_ports) {
175            links.push(self.links.use_link(node, port));
176        }
177
178        links.into_bump_slice()
179    }
180
181    pub fn make_term(&mut self, term: table::Term<'a>) -> table::TermId {
182        // There is a canonical id for wildcard terms.
183        if term == table::Term::Wildcard {
184            return table::TermId::default();
185        }
186
187        // We can omit a prefix of wildcard terms for symbol applications.
188        let term = match term {
189            table::Term::Apply(symbol, args) => {
190                let prefix = args.iter().take_while(|arg| !arg.is_valid()).count();
191                table::Term::Apply(symbol, &args[prefix..])
192            }
193            term => term,
194        };
195
196        *self
197            .term_map
198            .entry(term.clone())
199            .or_insert_with(|| self.module.insert_term(term))
200    }
201
202    pub fn make_qualified_name(
203        &mut self,
204        extension: &ExtensionId,
205        name: impl AsRef<str>,
206    ) -> &'a str {
207        let capacity = extension.len() + name.as_ref().len() + 1;
208        let mut output = BumpString::with_capacity_in(capacity, self.bump);
209        let _ = write!(&mut output, "{}.{}", extension, name.as_ref());
210        output.into_bump_str()
211    }
212
213    pub fn make_named_global_ref(
214        &mut self,
215        extension: &IdentList,
216        name: impl AsRef<str>,
217    ) -> table::NodeId {
218        let symbol = self.make_qualified_name(extension, name);
219        self.resolve_symbol(symbol)
220    }
221
222    /// Get the node that declares or defines the function associated with the given
223    /// node via the static input. Returns `None` if the node is not connected to a function.
224    fn connected_function(&self, node: Node) -> Option<Node> {
225        let func_node = self.hugr.static_source(node)?;
226
227        match self.hugr.get_optype(func_node) {
228            OpType::FuncDecl(_) => Some(func_node),
229            OpType::FuncDefn(_) => Some(func_node),
230            _ => None,
231        }
232    }
233
234    /// Get the name of a function definition or declaration node. Returns `None` if not
235    /// one of those operations.
236    fn get_func_name(&self, func_node: Node) -> Option<&'a str> {
237        match self.hugr.get_optype(func_node) {
238            OpType::FuncDecl(func_decl) => Some(func_decl.func_name()),
239            OpType::FuncDefn(func_defn) => Some(func_defn.func_name()),
240            _ => None,
241        }
242    }
243
244    fn with_local_scope<T>(&mut self, node: table::NodeId, f: impl FnOnce(&mut Self) -> T) -> T {
245        let prev_local_scope = self.local_scope.replace(node);
246        let prev_local_constraints = std::mem::take(&mut self.local_constraints);
247        let result = f(self);
248        self.local_scope = prev_local_scope;
249        self.local_constraints = prev_local_constraints;
250        result
251    }
252
253    fn export_node_shallow(&mut self, node: Node) -> Option<table::NodeId> {
254        let optype = self.hugr.get_optype(node);
255
256        // We skip nodes that are not exported as nodes in the model.
257        if let OpType::Const(_)
258        | OpType::Input(_)
259        | OpType::Output(_)
260        | OpType::ExitBlock(_)
261        | OpType::Case(_) = optype
262        {
263            return None;
264        }
265
266        let node_id = self.module.insert_node(table::Node::default());
267        self.node_to_id.insert(node, node_id);
268        self.id_to_node.insert(node_id, node);
269
270        // We record the name of the symbol defined by the node, if any.
271        let symbol = match optype {
272            OpType::FuncDefn(func_defn) => Some(func_defn.func_name().as_str()),
273            OpType::FuncDecl(func_decl) => Some(func_decl.func_name().as_str()),
274            OpType::AliasDecl(alias_decl) => Some(alias_decl.name.as_str()),
275            OpType::AliasDefn(alias_defn) => Some(alias_defn.name.as_str()),
276            _ => None,
277        };
278
279        if let Some(symbol) = symbol {
280            self.symbols
281                .insert(symbol, node_id)
282                .expect("duplicate symbol");
283        }
284
285        Some(node_id)
286    }
287
288    fn export_node_deep(&mut self, node_id: table::NodeId) {
289        // We insert a dummy node with the invalid operation at this point to reserve
290        // the node id. This is necessary to establish the correct node id for the
291        // local scope introduced by some operations. We will overwrite this node later.
292        let mut regions: &[_] = &[];
293
294        let node = self.id_to_node[&node_id];
295        let optype = self.hugr.get_optype(node);
296
297        let operation = match optype {
298            OpType::Module(_) => todo!("this should be an error"),
299
300            OpType::Input(_) => {
301                panic!("input nodes should have been handled by the region export")
302            }
303
304            OpType::Output(_) => {
305                panic!("output nodes should have been handled by the region export")
306            }
307
308            OpType::DFG(_) => {
309                regions = self.bump.alloc_slice_copy(&[self.export_dfg(
310                    node,
311                    model::ScopeClosure::Open,
312                    false,
313                )]);
314                table::Operation::Dfg
315            }
316
317            OpType::CFG(_) => {
318                regions = self
319                    .bump
320                    .alloc_slice_copy(&[self.export_cfg(node, model::ScopeClosure::Open)]);
321                table::Operation::Cfg
322            }
323
324            OpType::ExitBlock(_) => {
325                panic!("exit blocks should have been handled by the region export")
326            }
327
328            OpType::Case(_) => {
329                todo!("case nodes should have been handled by the region export")
330            }
331
332            OpType::DataflowBlock(_) => {
333                regions = self.bump.alloc_slice_copy(&[self.export_dfg(
334                    node,
335                    model::ScopeClosure::Open,
336                    false,
337                )]);
338                table::Operation::Block
339            }
340
341            OpType::FuncDefn(func) => self.with_local_scope(node_id, |this| {
342                let name = this.get_func_name(node).unwrap();
343                let symbol = this.export_poly_func_type(name, func.signature());
344                regions = this.bump.alloc_slice_copy(&[this.export_dfg(
345                    node,
346                    model::ScopeClosure::Closed,
347                    false,
348                )]);
349                table::Operation::DefineFunc(symbol)
350            }),
351
352            OpType::FuncDecl(func) => self.with_local_scope(node_id, |this| {
353                let name = this.get_func_name(node).unwrap();
354                let symbol = this.export_poly_func_type(name, func.signature());
355                table::Operation::DeclareFunc(symbol)
356            }),
357
358            OpType::AliasDecl(alias) => self.with_local_scope(node_id, |this| {
359                // TODO: We should support aliases with different types and with parameters
360                let signature = this.make_term_apply(model::CORE_TYPE, &[]);
361                let symbol = this.bump.alloc(table::Symbol {
362                    name: &alias.name,
363                    params: &[],
364                    constraints: &[],
365                    signature,
366                });
367                table::Operation::DeclareAlias(symbol)
368            }),
369
370            OpType::AliasDefn(alias) => self.with_local_scope(node_id, |this| {
371                let value = this.export_type(&alias.definition);
372                // TODO: We should support aliases with different types and with parameters
373                let signature = this.make_term_apply(model::CORE_TYPE, &[]);
374                let symbol = this.bump.alloc(table::Symbol {
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_type_arg(arg)));
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_type_arg(arg)));
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_type_arg(arg)));
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_type_arg(arg)));
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, 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            let inputs = self.make_term_apply(model::CORE_CTRL, &[inputs]);
582            self.make_term(table::Term::List(
583                self.bump.alloc_slice_copy(&[table::SeqPart::Item(inputs)]),
584            ))
585        };
586
587        let tail = self.export_type_row(&block.other_outputs);
588
589        let outputs = {
590            let mut outputs = BumpVec::with_capacity_in(block.sum_rows.len(), self.bump);
591            for sum_row in &block.sum_rows {
592                let variant = self.export_type_row_with_tail(sum_row, Some(tail));
593                let control = self.make_term_apply(model::CORE_CTRL, &[variant]);
594                outputs.push(table::SeqPart::Item(control));
595            }
596            self.make_term(table::Term::List(outputs.into_bump_slice()))
597        };
598
599        self.make_term_apply(model::CORE_FN, &[inputs, outputs])
600    }
601
602    /// Creates a data flow region from the given node's children.
603    ///
604    /// `Input` and `Output` nodes are used to determine the source and target ports of the region.
605    pub fn export_dfg(
606        &mut self,
607        node: Node,
608        closure: model::ScopeClosure,
609        export_json_meta: bool,
610    ) -> table::RegionId {
611        let region = self.module.insert_region(table::Region::default());
612
613        self.symbols.enter(region);
614        if closure == model::ScopeClosure::Closed {
615            self.links.enter(region);
616        }
617
618        let mut sources: &[_] = &[];
619        let mut targets: &[_] = &[];
620        let mut input_types = None;
621        let mut output_types = None;
622
623        let mut meta = Vec::new();
624
625        if export_json_meta {
626            self.export_node_json_metadata(node, &mut meta);
627        }
628        self.export_node_entrypoint_metadata(node, &mut meta);
629
630        let children = self.hugr.children(node);
631        let mut region_children = BumpVec::with_capacity_in(children.size_hint().0 - 2, self.bump);
632
633        let mut output_node = None;
634
635        for child in children {
636            match self.hugr.get_optype(child) {
637                OpType::Input(input) => {
638                    sources = self.make_ports(child, Direction::Outgoing, input.types.len());
639                    input_types = Some(&input.types);
640                }
641                OpType::Output(output) => {
642                    targets = self.make_ports(child, Direction::Incoming, output.types.len());
643                    output_types = Some(&output.types);
644                    output_node = Some(child);
645                }
646                child_optype => {
647                    if let Some(child_id) = self.export_node_shallow(child) {
648                        region_children.push(child_id);
649
650                        // Record all order edges that originate from this node in metadata.
651                        let successors = child_optype
652                            .other_output_port()
653                            .into_iter()
654                            .flat_map(|port| self.hugr.linked_inputs(child, port))
655                            .map(|(successor, _)| successor)
656                            .filter(|successor| Some(*successor) != output_node);
657
658                        for successor in successors {
659                            let a =
660                                self.make_term(model::Literal::Nat(child.index() as u64).into());
661                            let b = self
662                                .make_term(model::Literal::Nat(successor.index() as u64).into());
663                            meta.push(self.make_term_apply(model::ORDER_HINT_ORDER, &[a, b]));
664                        }
665                    }
666                }
667            }
668        }
669
670        for child_id in &region_children {
671            self.export_node_deep(*child_id);
672        }
673
674        let signature = {
675            let inputs = self.export_type_row(input_types.unwrap());
676            let outputs = self.export_type_row(output_types.unwrap());
677            Some(self.make_term_apply(model::CORE_FN, &[inputs, outputs]))
678        };
679
680        let scope = match closure {
681            model::ScopeClosure::Closed => {
682                let (links, ports) = self.links.exit();
683                Some(table::RegionScope { links, ports })
684            }
685            model::ScopeClosure::Open => None,
686        };
687        self.symbols.exit();
688
689        self.module.regions[region.index()] = table::Region {
690            kind: model::RegionKind::DataFlow,
691            sources,
692            targets,
693            children: region_children.into_bump_slice(),
694            meta: self.bump.alloc_slice_copy(&meta),
695            signature,
696            scope,
697        };
698
699        region
700    }
701
702    /// Creates a control flow region from the given node's children.
703    pub fn export_cfg(&mut self, node: Node, closure: model::ScopeClosure) -> table::RegionId {
704        let region = self.module.insert_region(table::Region::default());
705        self.symbols.enter(region);
706
707        if closure == model::ScopeClosure::Closed {
708            self.links.enter(region);
709        }
710
711        let mut source = None;
712        let mut targets: &[_] = &[];
713
714        let mut meta = Vec::new();
715        self.export_node_json_metadata(node, &mut meta);
716        self.export_node_entrypoint_metadata(node, &mut meta);
717
718        let children = self.hugr.children(node);
719        let mut region_children = BumpVec::with_capacity_in(children.size_hint().0 - 1, self.bump);
720
721        for child in children {
722            if let OpType::ExitBlock(_) = self.hugr.get_optype(child) {
723                targets = self.make_ports(child, Direction::Incoming, 1);
724            } else {
725                if let Some(child_id) = self.export_node_shallow(child) {
726                    region_children.push(child_id);
727                }
728
729                if source.is_none() {
730                    source = Some(self.links.use_link(child, IncomingPort::from(0)));
731                }
732            }
733        }
734
735        for child_id in &region_children {
736            self.export_node_deep(*child_id);
737        }
738
739        // Get the signature of the control flow region.
740        let signature = {
741            let node_signature = self.hugr.signature(node).unwrap();
742
743            let mut wrap_ctrl = |types: &TypeRow| {
744                let types = self.export_type_row(types);
745                let types_ctrl = self.make_term_apply(model::CORE_CTRL, &[types]);
746                self.make_term(table::Term::List(
747                    self.bump
748                        .alloc_slice_copy(&[table::SeqPart::Item(types_ctrl)]),
749                ))
750            };
751
752            let inputs = wrap_ctrl(node_signature.input());
753            let outputs = wrap_ctrl(node_signature.output());
754            Some(self.make_term_apply(model::CORE_FN, &[inputs, outputs]))
755        };
756
757        let scope = match closure {
758            model::ScopeClosure::Closed => {
759                let (links, ports) = self.links.exit();
760                Some(table::RegionScope { links, ports })
761            }
762            model::ScopeClosure::Open => None,
763        };
764        self.symbols.exit();
765
766        self.module.regions[region.index()] = table::Region {
767            kind: model::RegionKind::ControlFlow,
768            sources: self.bump.alloc_slice_copy(&[source.unwrap()]),
769            targets,
770            children: region_children.into_bump_slice(),
771            meta: self.bump.alloc_slice_copy(&meta),
772            signature,
773            scope,
774        };
775
776        region
777    }
778
779    /// Export the `Case` node children of a `Conditional` node as data flow regions.
780    pub fn export_conditional_regions(&mut self, node: Node) -> &'a [table::RegionId] {
781        let children = self.hugr.children(node);
782        let mut regions = BumpVec::with_capacity_in(children.size_hint().0, self.bump);
783
784        for child in children {
785            let OpType::Case(_) = self.hugr.get_optype(child) else {
786                panic!("expected a `Case` node as a child of a `Conditional` node");
787            };
788
789            regions.push(self.export_dfg(child, model::ScopeClosure::Open, true));
790        }
791
792        regions.into_bump_slice()
793    }
794
795    /// Exports a polymorphic function type.
796    pub fn export_poly_func_type<RV: MaybeRV>(
797        &mut self,
798        name: &'a str,
799        t: &PolyFuncTypeBase<RV>,
800    ) -> &'a table::Symbol<'a> {
801        let mut params = BumpVec::with_capacity_in(t.params().len(), self.bump);
802        let scope = self
803            .local_scope
804            .expect("exporting poly func type outside of local scope");
805
806        for (i, param) in t.params().iter().enumerate() {
807            let name = self.bump.alloc_str(&i.to_string());
808            let r#type = self.export_type_param(param, Some((scope, i as _)));
809            let param = table::Param { name, r#type };
810            params.push(param);
811        }
812
813        let constraints = self.bump.alloc_slice_copy(&self.local_constraints);
814        let body = self.export_func_type(t.body());
815
816        self.bump.alloc(table::Symbol {
817            name,
818            params: params.into_bump_slice(),
819            constraints,
820            signature: body,
821        })
822    }
823
824    pub fn export_type<RV: MaybeRV>(&mut self, t: &TypeBase<RV>) -> table::TermId {
825        self.export_type_enum(t.as_type_enum())
826    }
827
828    pub fn export_type_enum<RV: MaybeRV>(&mut self, t: &TypeEnum<RV>) -> table::TermId {
829        match t {
830            TypeEnum::Extension(ext) => self.export_custom_type(ext),
831            TypeEnum::Alias(alias) => {
832                let symbol = self.resolve_symbol(self.bump.alloc_str(alias.name()));
833                self.make_term(table::Term::Apply(symbol, &[]))
834            }
835            TypeEnum::Function(func) => self.export_func_type(func),
836            TypeEnum::Variable(index, _) => {
837                let node = self.local_scope.expect("local variable out of scope");
838                self.make_term(table::Term::Var(table::VarId(node, *index as _)))
839            }
840            TypeEnum::RowVar(rv) => self.export_row_var(rv.as_rv()),
841            TypeEnum::Sum(sum) => self.export_sum_type(sum),
842        }
843    }
844
845    pub fn export_func_type<RV: MaybeRV>(&mut self, t: &FuncTypeBase<RV>) -> table::TermId {
846        let inputs = self.export_type_row(t.input());
847        let outputs = self.export_type_row(t.output());
848        self.make_term_apply(model::CORE_FN, &[inputs, outputs])
849    }
850
851    pub fn export_custom_type(&mut self, t: &CustomType) -> table::TermId {
852        let symbol = self.make_named_global_ref(t.extension(), t.name());
853
854        let args = self
855            .bump
856            .alloc_slice_fill_iter(t.args().iter().map(|p| self.export_type_arg(p)));
857        let term = table::Term::Apply(symbol, args);
858        self.make_term(term)
859    }
860
861    pub fn export_type_arg(&mut self, t: &TypeArg) -> table::TermId {
862        match t {
863            TypeArg::Type { ty } => self.export_type(ty),
864            TypeArg::BoundedNat { n } => self.make_term(model::Literal::Nat(*n).into()),
865            TypeArg::String { arg } => self.make_term(model::Literal::Str(arg.into()).into()),
866            TypeArg::Sequence { elems } => {
867                // For now we assume that the sequence is meant to be a list.
868                let parts = self.bump.alloc_slice_fill_iter(
869                    elems
870                        .iter()
871                        .map(|elem| table::SeqPart::Item(self.export_type_arg(elem))),
872                );
873                self.make_term(table::Term::List(parts))
874            }
875            TypeArg::Variable { v } => self.export_type_arg_var(v),
876        }
877    }
878
879    pub fn export_type_arg_var(&mut self, var: &TypeArgVariable) -> table::TermId {
880        let node = self.local_scope.expect("local variable out of scope");
881        self.make_term(table::Term::Var(table::VarId(node, var.index() as _)))
882    }
883
884    pub fn export_row_var(&mut self, t: &RowVariable) -> table::TermId {
885        let node = self.local_scope.expect("local variable out of scope");
886        self.make_term(table::Term::Var(table::VarId(node, t.0 as _)))
887    }
888
889    pub fn export_sum_variants(&mut self, t: &SumType) -> table::TermId {
890        match t {
891            SumType::Unit { size } => {
892                let parts = self.bump.alloc_slice_fill_iter(
893                    (0..*size)
894                        .map(|_| table::SeqPart::Item(self.make_term(table::Term::List(&[])))),
895                );
896                self.make_term(table::Term::List(parts))
897            }
898            SumType::General { rows } => {
899                let parts = self.bump.alloc_slice_fill_iter(
900                    rows.iter()
901                        .map(|row| table::SeqPart::Item(self.export_type_row(row))),
902                );
903                self.make_term(table::Term::List(parts))
904            }
905        }
906    }
907
908    pub fn export_sum_type(&mut self, t: &SumType) -> table::TermId {
909        let variants = self.export_sum_variants(t);
910        self.make_term_apply(model::CORE_ADT, &[variants])
911    }
912
913    #[inline]
914    pub fn export_type_row<RV: MaybeRV>(&mut self, row: &TypeRowBase<RV>) -> table::TermId {
915        self.export_type_row_with_tail(row, None)
916    }
917
918    pub fn export_type_row_with_tail<RV: MaybeRV>(
919        &mut self,
920        row: &TypeRowBase<RV>,
921        tail: Option<table::TermId>,
922    ) -> table::TermId {
923        let mut parts =
924            BumpVec::with_capacity_in(row.len() + usize::from(tail.is_some()), self.bump);
925
926        for t in row.iter() {
927            match t.as_type_enum() {
928                TypeEnum::RowVar(var) => {
929                    parts.push(table::SeqPart::Splice(self.export_row_var(var.as_rv())));
930                }
931                _ => {
932                    parts.push(table::SeqPart::Item(self.export_type(t)));
933                }
934            }
935        }
936
937        if let Some(tail) = tail {
938            parts.push(table::SeqPart::Splice(tail));
939        }
940
941        let parts = parts.into_bump_slice();
942        self.make_term(table::Term::List(parts))
943    }
944
945    /// Exports a `TypeParam` to a term.
946    ///
947    /// The `var` argument is set when the type parameter being exported is the
948    /// type of a parameter to a polymorphic definition. In that case we can
949    /// generate a `nonlinear` constraint for the type of runtime types marked as
950    /// `TypeBound::Copyable`.
951    pub fn export_type_param(
952        &mut self,
953        t: &TypeParam,
954        var: Option<(table::NodeId, table::VarIndex)>,
955    ) -> table::TermId {
956        match t {
957            TypeParam::Type { b } => {
958                if let (Some((node, index)), TypeBound::Copyable) = (var, b) {
959                    let term = self.make_term(table::Term::Var(table::VarId(node, index)));
960                    let non_linear = self.make_term_apply(model::CORE_NON_LINEAR, &[term]);
961                    self.local_constraints.push(non_linear);
962                }
963
964                self.make_term_apply(model::CORE_TYPE, &[])
965            }
966            // This ignores the bound on the natural for now.
967            TypeParam::BoundedNat { .. } => self.make_term_apply(model::CORE_NAT_TYPE, &[]),
968            TypeParam::String => self.make_term_apply(model::CORE_STR_TYPE, &[]),
969            TypeParam::List { param } => {
970                let item_type = self.export_type_param(param, None);
971                self.make_term_apply(model::CORE_LIST_TYPE, &[item_type])
972            }
973            TypeParam::Tuple { params } => {
974                let parts = self.bump.alloc_slice_fill_iter(
975                    params
976                        .iter()
977                        .map(|param| table::SeqPart::Item(self.export_type_param(param, None))),
978                );
979                let types = self.make_term(table::Term::List(parts));
980                self.make_term_apply(model::CORE_TUPLE_TYPE, &[types])
981            }
982        }
983    }
984
985    fn export_value(&mut self, value: &'a Value) -> table::TermId {
986        match value {
987            Value::Extension { e } => {
988                // NOTE: We have special cased arrays, integers, and floats for now.
989                // TODO: Allow arbitrary extension values to be exported as terms.
990
991                if let Some(array) = e.value().downcast_ref::<ArrayValue>() {
992                    let len = self
993                        .make_term(model::Literal::Nat(array.get_contents().len() as u64).into());
994                    let element_type = self.export_type(array.get_element_type());
995                    let mut contents =
996                        BumpVec::with_capacity_in(array.get_contents().len(), self.bump);
997
998                    for element in array.get_contents() {
999                        contents.push(table::SeqPart::Item(self.export_value(element)));
1000                    }
1001
1002                    let contents = self.make_term(table::Term::List(contents.into_bump_slice()));
1003
1004                    let symbol = self.resolve_symbol(ArrayValue::CTR_NAME);
1005                    let args = self.bump.alloc_slice_copy(&[len, element_type, contents]);
1006                    return self.make_term(table::Term::Apply(symbol, args));
1007                }
1008
1009                if let Some(v) = e.value().downcast_ref::<ConstInt>() {
1010                    let bitwidth =
1011                        self.make_term(model::Literal::Nat(u64::from(v.log_width())).into());
1012                    let literal = self.make_term(model::Literal::Nat(v.value_u()).into());
1013
1014                    let symbol = self.resolve_symbol(ConstInt::CTR_NAME);
1015                    let args = self.bump.alloc_slice_copy(&[bitwidth, literal]);
1016                    return self.make_term(table::Term::Apply(symbol, args));
1017                }
1018
1019                if let Some(v) = e.value().downcast_ref::<ConstF64>() {
1020                    let literal = self.make_term(model::Literal::Float(v.value().into()).into());
1021                    let symbol = self.resolve_symbol(ConstF64::CTR_NAME);
1022                    let args = self.bump.alloc_slice_copy(&[literal]);
1023                    return self.make_term(table::Term::Apply(symbol, args));
1024                }
1025
1026                let json = match e.value().downcast_ref::<CustomSerialized>() {
1027                    Some(custom) => serde_json::to_string(custom.value()).unwrap(),
1028                    None => serde_json::to_string(e.value())
1029                        .expect("custom extension values should be serializable"),
1030                };
1031
1032                let json = self.make_term(model::Literal::Str(json.into()).into());
1033                let runtime_type = self.export_type(&e.get_type());
1034                let args = self.bump.alloc_slice_copy(&[runtime_type, json]);
1035                let symbol = self.resolve_symbol(model::COMPAT_CONST_JSON);
1036                self.make_term(table::Term::Apply(symbol, args))
1037            }
1038
1039            Value::Function { hugr } => {
1040                let outer_hugr = std::mem::replace(&mut self.hugr, hugr);
1041                let outer_node_to_id = std::mem::take(&mut self.node_to_id);
1042
1043                let region = match hugr.entrypoint_optype() {
1044                    OpType::DFG(_) => {
1045                        self.export_dfg(hugr.entrypoint(), model::ScopeClosure::Closed, true)
1046                    }
1047                    _ => panic!("Value::Function root must be a DFG"),
1048                };
1049
1050                self.node_to_id = outer_node_to_id;
1051                self.hugr = outer_hugr;
1052
1053                self.make_term(table::Term::Func(region))
1054            }
1055
1056            Value::Sum(sum) => {
1057                let variants = self.export_sum_variants(&sum.sum_type);
1058                let types = self.make_term(table::Term::Wildcard);
1059                let tag = self.make_term(model::Literal::Nat(sum.tag as u64).into());
1060
1061                let values = {
1062                    let mut values = BumpVec::with_capacity_in(sum.values.len(), self.bump);
1063
1064                    for value in &sum.values {
1065                        values.push(table::SeqPart::Item(self.export_value(value)));
1066                    }
1067
1068                    self.make_term(table::Term::Tuple(values.into_bump_slice()))
1069                };
1070
1071                self.make_term_apply(model::CORE_CONST_ADT, &[variants, types, tag, values])
1072            }
1073        }
1074    }
1075
1076    fn export_node_json_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1077        let metadata_map = self.hugr.node_metadata_map(node);
1078        meta.reserve(metadata_map.len());
1079
1080        for (name, value) in metadata_map {
1081            meta.push(self.make_json_meta(name, value));
1082        }
1083    }
1084
1085    fn export_node_order_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1086        fn is_relevant_node(hugr: &Hugr, node: Node) -> bool {
1087            let optype = hugr.get_optype(node);
1088            !optype.is_input() && !optype.is_output()
1089        }
1090
1091        let optype = self.hugr.get_optype(node);
1092
1093        let has_order_edges = Direction::BOTH
1094            .iter()
1095            .filter(|dir| optype.other_port_kind(**dir) == Some(EdgeKind::StateOrder))
1096            .filter_map(|dir| optype.other_port(*dir))
1097            .flat_map(|port| self.hugr.linked_ports(node, port))
1098            .any(|(other, _)| is_relevant_node(self.hugr, other));
1099
1100        if has_order_edges {
1101            let key = self.make_term(model::Literal::Nat(node.index() as u64).into());
1102            meta.push(self.make_term_apply(model::ORDER_HINT_KEY, &[key]));
1103        }
1104    }
1105
1106    fn export_node_entrypoint_metadata(&mut self, node: Node, meta: &mut Vec<table::TermId>) {
1107        if self.hugr.entrypoint() == node {
1108            meta.push(self.make_term_apply(model::CORE_ENTRYPOINT, &[]));
1109        }
1110    }
1111
1112    pub fn make_json_meta(&mut self, name: &str, value: &serde_json::Value) -> table::TermId {
1113        let value = serde_json::to_string(value).expect("json values are always serializable");
1114        let value = self.make_term(model::Literal::Str(value.into()).into());
1115        let name = self.make_term(model::Literal::Str(name.into()).into());
1116        self.make_term_apply(model::COMPAT_META_JSON, &[name, value])
1117    }
1118
1119    fn resolve_symbol(&mut self, name: &'a str) -> table::NodeId {
1120        let result = self.symbols.resolve(name);
1121
1122        match result {
1123            Ok(node) => node,
1124            Err(_) => *self.implicit_imports.entry(name).or_insert_with(|| {
1125                self.module.insert_node(table::Node {
1126                    operation: table::Operation::Import { name },
1127                    ..table::Node::default()
1128                })
1129            }),
1130        }
1131    }
1132
1133    fn make_term_apply(&mut self, name: &'a str, args: &[table::TermId]) -> table::TermId {
1134        let symbol = self.resolve_symbol(name);
1135        let args = self.bump.alloc_slice_copy(args);
1136        self.make_term(table::Term::Apply(symbol, args))
1137    }
1138}
1139
1140type FxIndexSet<T> = indexmap::IndexSet<T, FxBuildHasher>;
1141
1142/// Data structure for translating the edges between ports in the `Hugr` graph
1143/// into the hypergraph representation used by `hugr_model`.
1144struct Links {
1145    /// Scoping helper that keeps track of the current nesting of regions
1146    /// and translates the group of connected ports into a link index.
1147    scope: model::scope::LinkTable<u32>,
1148
1149    /// A mapping from each port to the group of connected ports it belongs to.
1150    groups: FxHashMap<(Node, Port), u32>,
1151}
1152
1153impl Links {
1154    /// Create the `Links` data structure from a `Hugr` graph by recording the
1155    /// connectivity of the ports.
1156    pub fn new(hugr: &Hugr) -> Self {
1157        let scope = model::scope::LinkTable::new();
1158
1159        // We collect all ports that are in the hugr into an index set so that
1160        // we have an association between the port and a numeric index.
1161        let node_ports: FxIndexSet<(Node, Port)> = hugr
1162            .nodes()
1163            .flat_map(|node| hugr.all_node_ports(node).map(move |port| (node, port)))
1164            .collect();
1165
1166        // We then use a union-find data structure to group together all ports that are connected.
1167        let mut uf = UnionFind::<u32>::new(node_ports.len());
1168
1169        for (i, (node, port)) in node_ports.iter().enumerate() {
1170            if let Ok(port) = port.as_incoming() {
1171                for (other_node, other_port) in hugr.linked_outputs(*node, port) {
1172                    let other_port = Port::from(other_port);
1173                    let j = node_ports.get_index_of(&(other_node, other_port)).unwrap();
1174                    uf.union(i as u32, j as u32);
1175                }
1176            }
1177        }
1178
1179        // We then collect the association between the port and the group of connected ports it belongs to.
1180        let groups = node_ports
1181            .into_iter()
1182            .enumerate()
1183            .map(|(i, node_port)| (node_port, uf.find(i as u32)))
1184            .collect();
1185
1186        Self { scope, groups }
1187    }
1188
1189    /// Enter an isolated region.
1190    pub fn enter(&mut self, region: table::RegionId) {
1191        self.scope.enter(region);
1192    }
1193
1194    /// Leave an isolated region, returning the number of links and ports in the region.
1195    ///
1196    /// # Panics
1197    ///
1198    /// Panics if there is no remaining open scope to exit.
1199    pub fn exit(&mut self) -> (u32, u32) {
1200        self.scope.exit()
1201    }
1202
1203    /// Obtain the link index for a node and port.
1204    ///
1205    /// # Panics
1206    ///
1207    /// Panics if the port does not exist in the [`Hugr`] that was passed to `[Self::new]`.
1208    pub fn use_link(&mut self, node: Node, port: impl Into<Port>) -> table::LinkIndex {
1209        let port = port.into();
1210        let group = self.groups[&(node, port)];
1211        self.scope.use_link(group)
1212    }
1213}
1214
1215#[cfg(test)]
1216mod test {
1217    use rstest::{fixture, rstest};
1218
1219    use crate::{
1220        Hugr,
1221        builder::{Dataflow, DataflowSubContainer},
1222        extension::prelude::qb_t,
1223        types::Signature,
1224        utils::test_quantum_extension::{cx_gate, h_gate},
1225    };
1226
1227    #[fixture]
1228    fn test_simple_circuit() -> Hugr {
1229        crate::builder::test::build_main(
1230            Signature::new_endo(vec![qb_t(), qb_t()]).into(),
1231            |mut f_build| {
1232                let wires: Vec<_> = f_build.input_wires().collect();
1233                let mut linear = f_build.as_circuit(wires);
1234
1235                assert_eq!(linear.n_wires(), 2);
1236
1237                linear
1238                    .append(h_gate(), [0])?
1239                    .append(cx_gate(), [0, 1])?
1240                    .append(cx_gate(), [1, 0])?;
1241
1242                let outs = linear.finish();
1243                f_build.finish_with_outputs(outs)
1244            },
1245        )
1246        .unwrap()
1247    }
1248
1249    #[rstest]
1250    #[case(test_simple_circuit())]
1251    fn test_export(#[case] hugr: Hugr) {
1252        use hugr_model::v0::bumpalo::Bump;
1253        let bump = Bump::new();
1254        let _model = super::export_hugr(&hugr, &bump);
1255    }
1256}