hugr_core/
import.rs

1//! Importing HUGR graphs from their `hugr-model` representation.
2//!
3//! **Warning**: This module is still under development and is expected to change.
4//! It is included in the library to allow for early experimentation, and for
5//! the core and model to converge incrementally.
6use std::sync::Arc;
7
8use crate::{
9    Direction, Hugr, HugrView, Node, Port,
10    extension::{
11        ExtensionId, ExtensionRegistry, SignatureError, resolution::ExtensionResolutionError,
12    },
13    hugr::{HugrMut, NodeMetadata},
14    ops::{
15        AliasDecl, AliasDefn, CFG, Call, CallIndirect, Case, Conditional, Const, DFG,
16        DataflowBlock, ExitBlock, FuncDecl, FuncDefn, Input, LoadConstant, LoadFunction, OpType,
17        OpaqueOp, Output, Tag, TailLoop, Value,
18        constant::{CustomConst, CustomSerialized, OpaqueValue},
19    },
20    package::Package,
21    std_extensions::{
22        arithmetic::{float_types::ConstF64, int_types::ConstInt},
23        collections::array::ArrayValue,
24    },
25    types::{
26        CustomType, FuncTypeBase, MaybeRV, PolyFuncType, PolyFuncTypeBase, RowVariable, Signature,
27        Term, Type, TypeArg, TypeBase, TypeBound, TypeEnum, TypeName, TypeRow,
28        type_param::{SeqPart, TypeParam},
29        type_row::TypeRowBase,
30    },
31};
32use hugr_model::v0::table;
33use hugr_model::v0::{self as model};
34use itertools::{Either, Itertools};
35use rustc_hash::FxHashMap;
36use smol_str::{SmolStr, ToSmolStr};
37use thiserror::Error;
38
39fn gen_str(generator: &Option<String>) -> String {
40    match generator {
41        Some(g) => format!(" generated by {g}"),
42        None => String::new(),
43    }
44}
45
46/// An error that can occur during import.
47#[derive(Debug, Clone, Error)]
48#[error("failed to import hugr{}", gen_str(&self.generator))]
49pub struct ImportError {
50    #[source]
51    inner: ImportErrorInner,
52    generator: Option<String>,
53}
54
55const UNSUPPORTED_HINT: &str = concat!(
56    "Hint: The import process encountered a `hugr-model` feature ",
57    "that is currently unsupported in `hugr-core`. ",
58    "As `hugr-core` evolves towards the same expressivity as `hugr-model` ",
59    "we expect that errors of this kind will be removed.",
60);
61
62const UNINFERRED_HINT: &str = concat!(
63    "Hint: The import process encountered implicit information in the `hugr-model` ",
64    "package that it can not yet fill in. ",
65    "Such implicit information can be signatures for nodes and regions, ",
66    "wildcard terms or symbol applications with fewer arguments than the symbol has parameters. ",
67    "Until more comprehensive inference is implemented, `hugr-model` packages will need to be very explicit. ",
68    "To avoid this error, make sure to include as much information as possible when generating packages."
69);
70
71/// Error hint explaining the concept of a closed tuple.
72const CLOSED_TUPLE_HINT: &str = concat!(
73    "Hint: A tuple is closed if all of its items are known. ",
74    "Closed tuples can contain spliced subtuples, as long as they can be recursively flattened ",
75    "into a tuple that only contains individual items."
76);
77
78/// Error hint explaining the concept of a closed list.
79const CLOSED_LIST_HINT: &str = concat!(
80    "Hint: A list is closed if all of its items are known. ",
81    "Closed lists can contain spliced sublists, as long as they can be recursively flattened ",
82    "into a list that only contains individual items."
83);
84
85#[derive(Debug, Clone, Error)]
86enum ImportErrorInner {
87    /// The model contains a feature that is not supported by the importer yet.
88    /// Errors of this kind are expected to be removed as the model format and
89    /// the core HUGR representation converge.
90    #[error("Unsupported: {0}\n{hint}", hint = UNSUPPORTED_HINT)]
91    Unsupported(String),
92
93    /// The model contains implicit information that has not yet been inferred.
94    /// This includes wildcards and application of functions with implicit parameters.
95    #[error("Uninferred implicit: {0}\n{hint}", hint = UNINFERRED_HINT)]
96    Uninferred(String),
97
98    /// The model is not well-formed.
99    #[error("{0}")]
100    Invalid(String),
101
102    /// An error with additional context.
103    #[error("import failed in context: {1}")]
104    Context(#[source] Box<ImportErrorInner>, String),
105
106    /// A signature mismatch was detected during import.
107    #[error("signature error")]
108    Signature(#[from] SignatureError),
109
110    /// An error relating to the loaded extension registry.
111    #[error("extension error")]
112    Extension(#[from] ExtensionError),
113
114    /// Incorrect order hints.
115    #[error("incorrect order hint")]
116    OrderHint(#[from] OrderHintError),
117
118    /// Extension resolution.
119    #[error("extension resolution error")]
120    ExtensionResolution(#[from] ExtensionResolutionError),
121}
122
123#[derive(Debug, Clone, Error)]
124enum ExtensionError {
125    /// An extension is missing.
126    #[error("Importing the hugr requires extension {missing_ext}, which was not found in the registry. The available extensions are: [{}]",
127            available.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(", "))]
128    Missing {
129        /// The missing extension.
130        missing_ext: ExtensionId,
131        /// The available extensions in the registry.
132        available: Vec<ExtensionId>,
133    },
134
135    /// An extension type is missing.
136    #[error(
137        "Importing the hugr requires extension {ext} to have a type named {name}, but it was not found."
138    )]
139    MissingType {
140        /// The extension that is missing the type.
141        ext: ExtensionId,
142        /// The name of the missing type.
143        name: TypeName,
144    },
145}
146
147/// Import error caused by incorrect order hints.
148#[derive(Debug, Clone, Error)]
149enum OrderHintError {
150    /// Duplicate order hint key in the same region.
151    #[error("duplicate order hint key {0}")]
152    DuplicateKey(table::RegionId, u64),
153    /// Order hint including a key not defined in the region.
154    #[error("order hint with unknown key {0}")]
155    UnknownKey(u64),
156    /// Order hint involving a node with no order port.
157    #[error("order hint on node with no order port: {0}")]
158    NoOrderPort(table::NodeId),
159}
160
161/// Helper macro to create an `ImportErrorInner::Unsupported` error with a formatted message.
162macro_rules! error_unsupported {
163    ($($e:expr),*) => { ImportErrorInner::Unsupported(format!($($e),*)) }
164}
165
166/// Helper macro to create an `ImportErrorInner::Uninferred` error with a formatted message.
167macro_rules! error_uninferred {
168    ($($e:expr),*) => { ImportErrorInner::Uninferred(format!($($e),*)) }
169}
170
171/// Helper macro to create an `ImportErrorInner::Invalid` error with a formatted message.
172macro_rules! error_invalid {
173    ($($e:expr),*) => { ImportErrorInner::Invalid(format!($($e),*)) }
174}
175
176/// Helper macro to create an `ImportErrorInner::Context` error with a formatted message.
177macro_rules! error_context {
178    ($err:expr, $($e:expr),*) => {
179        {
180            ImportErrorInner::Context(Box::new($err), format!($($e),*))
181        }
182    }
183}
184
185/// Import a [`Package`] from the model representation
186/// of the modules and any included extensions.
187pub fn import_package(
188    package: &table::Package,
189    packaged_extensions: ExtensionRegistry,
190    loaded_extensions: &ExtensionRegistry,
191) -> Result<Package, ImportError> {
192    let mut registry = loaded_extensions.clone();
193    registry.extend(&packaged_extensions);
194    let modules = package
195        .modules
196        .iter()
197        .map(|module| import_hugr(module, &registry))
198        .collect::<Result<Vec<_>, _>>()?;
199
200    // This does not panic since the import already requires a module root.
201    let mut package = Package::new(modules);
202    package.extensions = packaged_extensions;
203    Ok(package)
204}
205
206/// Get the name of the generator from the metadata of the module.
207/// If no generator is found, `None` is returned.
208fn get_generator(ctx: &Context<'_>) -> Option<String> {
209    ctx.module
210        .get_region(ctx.module.root)
211        .map(|r| r.meta.iter())
212        .into_iter()
213        .flatten()
214        .find_map(|meta| {
215            let (name, json_val) = ctx.decode_json_meta(*meta).ok()??;
216
217            (name == crate::envelope::GENERATOR_KEY)
218                .then_some(crate::envelope::format_generator(&json_val))
219        })
220}
221
222/// Import a [`Hugr`] module from its model representation.
223pub fn import_hugr(
224    module: &table::Module,
225    extensions: &ExtensionRegistry,
226) -> Result<Hugr, ImportError> {
227    // TODO: Module should know about the number of edges, so that we can use a vector here.
228    // For now we use a hashmap, which will be slower.
229    let mut ctx = Context {
230        module,
231        hugr: Hugr::new(),
232        link_ports: FxHashMap::default(),
233        static_edges: Vec::new(),
234        extensions,
235        nodes: FxHashMap::default(),
236        local_vars: FxHashMap::default(),
237        custom_name_cache: FxHashMap::default(),
238        region_scope: table::RegionId::default(),
239    };
240
241    let import_steps: [fn(&mut Context) -> _; 3] = [
242        |ctx| ctx.import_root(),
243        |ctx| ctx.link_ports(),
244        |ctx| ctx.link_static_ports(),
245    ];
246
247    for step in import_steps {
248        if let Err(e) = step(&mut ctx) {
249            return Err(ImportError {
250                inner: e,
251                generator: get_generator(&ctx),
252            });
253        }
254    }
255    ctx.hugr
256        .resolve_extension_defs(extensions)
257        .map_err(|e| ImportError {
258            inner: ImportErrorInner::ExtensionResolution(e),
259            generator: get_generator(&ctx),
260        })?;
261    Ok(ctx.hugr)
262}
263
264struct Context<'a> {
265    /// The module being imported.
266    module: &'a table::Module<'a>,
267
268    /// The HUGR graph being constructed.
269    hugr: Hugr,
270
271    /// The ports that are part of each link. This is used to connect the ports at the end of the
272    /// import process.
273    link_ports: FxHashMap<(table::RegionId, table::LinkIndex), Vec<(Node, Port)>>,
274
275    /// Pairs of nodes that should be connected by a static edge.
276    /// These are collected during the import process and connected at the end.
277    static_edges: Vec<(table::NodeId, table::NodeId)>,
278
279    /// The ambient extension registry to use for importing.
280    extensions: &'a ExtensionRegistry,
281
282    /// A map from `NodeId` to the imported `Node`.
283    nodes: FxHashMap<table::NodeId, Node>,
284
285    local_vars: FxHashMap<table::VarId, LocalVar>,
286
287    custom_name_cache: FxHashMap<&'a str, (ExtensionId, SmolStr)>,
288
289    region_scope: table::RegionId,
290}
291
292impl<'a> Context<'a> {
293    /// Get the signature of the node with the given `NodeId`.
294    fn get_node_signature(&mut self, node: table::NodeId) -> Result<Signature, ImportErrorInner> {
295        let node_data = self.get_node(node)?;
296        let signature = node_data
297            .signature
298            .ok_or_else(|| error_uninferred!("node signature"))?;
299        self.import_func_type(signature)
300    }
301
302    /// Get the node with the given `NodeId`, or return an error if it does not exist.
303    #[inline]
304    fn get_node(&self, node_id: table::NodeId) -> Result<&'a table::Node<'a>, ImportErrorInner> {
305        self.module
306            .get_node(node_id)
307            .ok_or_else(|| error_invalid!("unknown node {}", node_id))
308    }
309
310    /// Get the term with the given `TermId`, or return an error if it does not exist.
311    #[inline]
312    fn get_term(&self, term_id: table::TermId) -> Result<&'a table::Term<'a>, ImportErrorInner> {
313        self.module
314            .get_term(term_id)
315            .ok_or_else(|| error_invalid!("unknown term {}", term_id))
316    }
317
318    /// Get the region with the given `RegionId`, or return an error if it does not exist.
319    #[inline]
320    fn get_region(
321        &self,
322        region_id: table::RegionId,
323    ) -> Result<&'a table::Region<'a>, ImportErrorInner> {
324        self.module
325            .get_region(region_id)
326            .ok_or_else(|| error_invalid!("unknown region {}", region_id))
327    }
328
329    fn make_node(
330        &mut self,
331        node_id: table::NodeId,
332        op: OpType,
333        parent: Node,
334    ) -> Result<Node, ImportErrorInner> {
335        let node = self.hugr.add_node_with_parent(parent, op);
336        self.nodes.insert(node_id, node);
337
338        let node_data = self.get_node(node_id)?;
339        self.record_links(node, Direction::Incoming, node_data.inputs);
340        self.record_links(node, Direction::Outgoing, node_data.outputs);
341
342        for meta_item in node_data.meta {
343            self.import_node_metadata(node, *meta_item)
344                .map_err(|err| error_context!(err, "node metadata"))?;
345        }
346
347        Ok(node)
348    }
349
350    fn import_node_metadata(
351        &mut self,
352        node: Node,
353        meta_item: table::TermId,
354    ) -> Result<(), ImportErrorInner> {
355        // Import the JSON metadata
356        if let Some((name, json_value)) = self.decode_json_meta(meta_item)? {
357            self.hugr.set_metadata(node, name, json_value);
358        }
359
360        // Set the entrypoint
361        if let Some([]) = self.match_symbol(meta_item, model::CORE_ENTRYPOINT)? {
362            self.hugr.set_entrypoint(node);
363        }
364
365        Ok(())
366    }
367
368    fn decode_json_meta(
369        &self,
370        meta_item: table::TermId,
371    ) -> Result<Option<(SmolStr, serde_json::Value)>, ImportErrorInner> {
372        Ok(
373            if let Some([name_arg, json_arg]) =
374                self.match_symbol(meta_item, model::COMPAT_META_JSON)?
375            {
376                let table::Term::Literal(model::Literal::Str(name)) = self.get_term(name_arg)?
377                else {
378                    return Err(error_invalid!(
379                        "`{}` expects a string literal as its first argument",
380                        model::COMPAT_META_JSON
381                    ));
382                };
383
384                let table::Term::Literal(model::Literal::Str(json_str)) =
385                    self.get_term(json_arg)?
386                else {
387                    return Err(error_invalid!(
388                        "`{}` expects a string literal as its second argument",
389                        model::COMPAT_CONST_JSON
390                    ));
391                };
392
393                let json_value: NodeMetadata = serde_json::from_str(json_str).map_err(|_| {
394                    error_invalid!(
395                        "failed to parse JSON string for `{}` metadata",
396                        model::COMPAT_CONST_JSON
397                    )
398                })?;
399                Some((name.to_owned(), json_value))
400            } else {
401                None
402            },
403        )
404    }
405
406    /// Associate links with the ports of the given node in the given direction.
407    fn record_links(&mut self, node: Node, direction: Direction, links: &'a [table::LinkIndex]) {
408        let optype = self.hugr.get_optype(node);
409        // NOTE: `OpType::port_count` copies the signature, which significantly slows down the import.
410        debug_assert!(links.len() <= optype.port_count(direction));
411
412        for (link, port) in links.iter().zip(self.hugr.node_ports(node, direction)) {
413            self.link_ports
414                .entry((self.region_scope, *link))
415                .or_default()
416                .push((node, port));
417        }
418    }
419
420    /// Link up the ports in the hugr graph, according to the connectivity information that
421    /// has been gathered in the `link_ports` map.
422    fn link_ports(&mut self) -> Result<(), ImportErrorInner> {
423        // For each edge, we group the ports by their direction. We reuse the `inputs` and
424        // `outputs` vectors to avoid unnecessary allocations.
425        let mut inputs = Vec::new();
426        let mut outputs = Vec::new();
427
428        for (link_id, link_ports) in std::mem::take(&mut self.link_ports) {
429            // Skip the edge if it doesn't have any ports.
430            if link_ports.is_empty() {
431                continue;
432            }
433
434            for (node, port) in link_ports {
435                match port.as_directed() {
436                    Either::Left(input) => inputs.push((node, input)),
437                    Either::Right(output) => outputs.push((node, output)),
438                }
439            }
440
441            match (inputs.as_slice(), outputs.as_slice()) {
442                ([], []) => {
443                    unreachable!();
444                }
445                (_, [output]) => {
446                    for (node, port) in &inputs {
447                        self.hugr.connect(output.0, output.1, *node, *port);
448                    }
449                }
450                ([input], _) => {
451                    for (node, port) in &outputs {
452                        self.hugr.connect(*node, *port, input.0, input.1);
453                    }
454                }
455                _ => {
456                    return Err(error_unsupported!(
457                        concat!(
458                            "Link {:?} would require a hyperedge because it connects more ",
459                            "than one input port with more than one output port."
460                        ),
461                        link_id
462                    ));
463                }
464            }
465
466            inputs.clear();
467            outputs.clear();
468        }
469
470        Ok(())
471    }
472
473    /// Connects static ports according to the connections in [`self.static_edges`].
474    fn link_static_ports(&mut self) -> Result<(), ImportErrorInner> {
475        for (src_id, dst_id) in std::mem::take(&mut self.static_edges) {
476            // None of these lookups should fail given how we constructed `static_edges`.
477            let src = self.nodes[&src_id];
478            let dst = self.nodes[&dst_id];
479            let src_port = self.hugr.get_optype(src).static_output_port().unwrap();
480            let dst_port = self.hugr.get_optype(dst).static_input_port().unwrap();
481            self.hugr.connect(src, src_port, dst, dst_port);
482        }
483
484        Ok(())
485    }
486
487    fn get_symbol_name(&self, node_id: table::NodeId) -> Result<&'a str, ImportErrorInner> {
488        let node_data = self.get_node(node_id)?;
489        let name = node_data
490            .operation
491            .symbol()
492            .ok_or_else(|| error_invalid!("node {} is expected to be a symbol", node_id))?;
493        Ok(name)
494    }
495
496    fn get_func_signature(
497        &mut self,
498        func_node: table::NodeId,
499    ) -> Result<PolyFuncType, ImportErrorInner> {
500        let symbol = match self.get_node(func_node)?.operation {
501            table::Operation::DefineFunc(symbol) => symbol,
502            table::Operation::DeclareFunc(symbol) => symbol,
503            _ => {
504                return Err(error_invalid!(
505                    "node {} is expected to be a function declaration or definition",
506                    func_node
507                ));
508            }
509        };
510
511        self.import_poly_func_type(func_node, *symbol, |_, signature| Ok(signature))
512    }
513
514    /// Import the root region of the module.
515    fn import_root(&mut self) -> Result<(), ImportErrorInner> {
516        self.region_scope = self.module.root;
517        let region_data = self.get_region(self.module.root)?;
518
519        for node in region_data.children {
520            self.import_node(*node, self.hugr.module_root())?;
521        }
522
523        for meta_item in region_data.meta {
524            self.import_node_metadata(self.hugr.module_root(), *meta_item)?;
525        }
526
527        Ok(())
528    }
529
530    fn import_node(
531        &mut self,
532        node_id: table::NodeId,
533        parent: Node,
534    ) -> Result<Option<Node>, ImportErrorInner> {
535        let node_data = self.get_node(node_id)?;
536
537        let result = match node_data.operation {
538            table::Operation::Invalid => {
539                return Err(error_invalid!(concat!(
540                    "Tried to import an `invalid` operation.\n",
541                    "The `invalid` operation in `hugr-model` is a placeholder indicating a missing operation. ",
542                    "It currently has no equivalent in `hugr-core`."
543                )));
544            }
545
546            table::Operation::Dfg => Some(
547                self.import_node_dfg(node_id, parent, node_data)
548                    .map_err(|err| error_context!(err, "`dfg` node with id {}", node_id))?,
549            ),
550
551            table::Operation::Cfg => Some(
552                self.import_node_cfg(node_id, parent, node_data)
553                    .map_err(|err| error_context!(err, "`cfg` node with id {}", node_id))?,
554            ),
555
556            table::Operation::Block => Some(
557                self.import_node_block(node_id, parent)
558                    .map_err(|err| error_context!(err, "`block` node with id {}", node_id))?,
559            ),
560
561            table::Operation::DefineFunc(symbol) => Some(
562                self.import_node_define_func(node_id, symbol, node_data, parent)
563                    .map_err(|err| error_context!(err, "`define-func` node with id {}", node_id))?,
564            ),
565
566            table::Operation::DeclareFunc(symbol) => Some(
567                self.import_node_declare_func(node_id, symbol, parent)
568                    .map_err(|err| {
569                        error_context!(err, "`declare-func` node with id {}", node_id)
570                    })?,
571            ),
572
573            table::Operation::TailLoop => Some(
574                self.import_tail_loop(node_id, parent)
575                    .map_err(|err| error_context!(err, "`tail-loop` node with id {}", node_id))?,
576            ),
577
578            table::Operation::Conditional => Some(
579                self.import_conditional(node_id, parent)
580                    .map_err(|err| error_context!(err, "`cond` node with id {}", node_id))?,
581            ),
582
583            table::Operation::Custom(operation) => Some(
584                self.import_node_custom(node_id, operation, node_data, parent)
585                    .map_err(|err| error_context!(err, "custom node with id {}", node_id))?,
586            ),
587
588            table::Operation::DefineAlias(symbol, value) => Some(
589                self.import_node_define_alias(node_id, symbol, value, parent)
590                    .map_err(|err| {
591                        error_context!(err, "`define-alias` node with id {}", node_id)
592                    })?,
593            ),
594
595            table::Operation::DeclareAlias(symbol) => Some(
596                self.import_node_declare_alias(node_id, symbol, parent)
597                    .map_err(|err| {
598                        error_context!(err, "`declare-alias` node with id {}", node_id)
599                    })?,
600            ),
601
602            table::Operation::Import { .. } => None,
603
604            table::Operation::DeclareConstructor { .. } => None,
605            table::Operation::DeclareOperation { .. } => None,
606        };
607
608        Ok(result)
609    }
610
611    fn import_node_dfg(
612        &mut self,
613        node_id: table::NodeId,
614        parent: Node,
615        node_data: &'a table::Node<'a>,
616    ) -> Result<Node, ImportErrorInner> {
617        let signature = self
618            .get_node_signature(node_id)
619            .map_err(|err| error_context!(err, "node signature"))?;
620
621        let optype = OpType::DFG(DFG { signature });
622        let node = self.make_node(node_id, optype, parent)?;
623
624        let [region] = node_data.regions else {
625            return Err(error_invalid!("dfg region expects a single region"));
626        };
627
628        self.import_dfg_region(*region, node)?;
629        Ok(node)
630    }
631
632    fn import_node_cfg(
633        &mut self,
634        node_id: table::NodeId,
635        parent: Node,
636        node_data: &'a table::Node<'a>,
637    ) -> Result<Node, ImportErrorInner> {
638        let signature = self
639            .get_node_signature(node_id)
640            .map_err(|err| error_context!(err, "node signature"))?;
641
642        let optype = OpType::CFG(CFG { signature });
643        let node = self.make_node(node_id, optype, parent)?;
644
645        let [region] = node_data.regions else {
646            return Err(error_invalid!("cfg nodes expect a single region"));
647        };
648
649        self.import_cfg_region(*region, node)?;
650        Ok(node)
651    }
652
653    fn import_dfg_region(
654        &mut self,
655        region: table::RegionId,
656        node: Node,
657    ) -> Result<(), ImportErrorInner> {
658        let region_data = self.get_region(region)?;
659
660        let prev_region = self.region_scope;
661        if region_data.scope.is_some() {
662            self.region_scope = region;
663        }
664
665        if region_data.kind != model::RegionKind::DataFlow {
666            return Err(error_invalid!("expected dfg region"));
667        }
668
669        let signature = self
670            .import_func_type(
671                region_data
672                    .signature
673                    .ok_or_else(|| error_uninferred!("region signature"))?,
674            )
675            .map_err(|err| error_context!(err, "signature of dfg region with id {}", region))?;
676
677        // Create the input and output nodes
678        let input = self.hugr.add_node_with_parent(
679            node,
680            OpType::Input(Input {
681                types: signature.input,
682            }),
683        );
684        let output = self.hugr.add_node_with_parent(
685            node,
686            OpType::Output(Output {
687                types: signature.output,
688            }),
689        );
690
691        // Make sure that the ports of the input/output nodes are connected correctly
692        self.record_links(input, Direction::Outgoing, region_data.sources);
693        self.record_links(output, Direction::Incoming, region_data.targets);
694
695        for child in region_data.children {
696            self.import_node(*child, node)?;
697        }
698
699        self.create_order_edges(region, input, output)?;
700
701        for meta_item in region_data.meta {
702            self.import_node_metadata(node, *meta_item)?;
703        }
704
705        self.region_scope = prev_region;
706
707        Ok(())
708    }
709
710    /// Create order edges between nodes of a dataflow region based on order hint metadata.
711    ///
712    /// This method assumes that the nodes for the children of the region have already been imported.
713    fn create_order_edges(
714        &mut self,
715        region_id: table::RegionId,
716        input: Node,
717        output: Node,
718    ) -> Result<(), ImportErrorInner> {
719        let region_data = self.get_region(region_id)?;
720        debug_assert_eq!(region_data.kind, model::RegionKind::DataFlow);
721
722        // Collect order hint keys
723        // PERFORMANCE: It might be worthwhile to reuse the map to avoid allocations.
724        let mut order_keys = FxHashMap::<u64, Node>::default();
725
726        for child_id in region_data.children {
727            let child_data = self.get_node(*child_id)?;
728
729            for meta_id in child_data.meta {
730                let Some([key]) = self.match_symbol(*meta_id, model::ORDER_HINT_KEY)? else {
731                    continue;
732                };
733
734                let table::Term::Literal(model::Literal::Nat(key)) = self.get_term(key)? else {
735                    continue;
736                };
737
738                // NOTE: The lookups here are expected to succeed since we only
739                // process the order metadata after we have imported the nodes.
740                let child_node = self.nodes[child_id];
741                let child_optype = self.hugr.get_optype(child_node);
742
743                // Check that the node has order ports.
744                // NOTE: This assumes that a node has an input order port iff it has an output one.
745                if child_optype.other_output_port().is_none() {
746                    return Err(OrderHintError::NoOrderPort(*child_id).into());
747                }
748
749                if order_keys.insert(*key, child_node).is_some() {
750                    return Err(OrderHintError::DuplicateKey(region_id, *key).into());
751                }
752            }
753        }
754
755        // Collect the order hint keys for the input and output nodes
756        for meta_id in region_data.meta {
757            if let Some([key]) = self.match_symbol(*meta_id, model::ORDER_HINT_INPUT_KEY)? {
758                let table::Term::Literal(model::Literal::Nat(key)) = self.get_term(key)? else {
759                    continue;
760                };
761
762                if order_keys.insert(*key, input).is_some() {
763                    return Err(OrderHintError::DuplicateKey(region_id, *key).into());
764                }
765            }
766
767            if let Some([key]) = self.match_symbol(*meta_id, model::ORDER_HINT_OUTPUT_KEY)? {
768                let table::Term::Literal(model::Literal::Nat(key)) = self.get_term(key)? else {
769                    continue;
770                };
771
772                if order_keys.insert(*key, output).is_some() {
773                    return Err(OrderHintError::DuplicateKey(region_id, *key).into());
774                }
775            }
776        }
777
778        // Insert order edges
779        for meta_id in region_data.meta {
780            let Some([a, b]) = self.match_symbol(*meta_id, model::ORDER_HINT_ORDER)? else {
781                continue;
782            };
783
784            let table::Term::Literal(model::Literal::Nat(a)) = self.get_term(a)? else {
785                continue;
786            };
787
788            let table::Term::Literal(model::Literal::Nat(b)) = self.get_term(b)? else {
789                continue;
790            };
791
792            let a = order_keys.get(a).ok_or(OrderHintError::UnknownKey(*a))?;
793            let b = order_keys.get(b).ok_or(OrderHintError::UnknownKey(*b))?;
794
795            // NOTE: The unwrap here must succeed:
796            // - For all ordinary nodes we checked that they have an order port.
797            // - Input and output nodes always have an order port.
798            let a_port = self.hugr.get_optype(*a).other_output_port().unwrap();
799            let b_port = self.hugr.get_optype(*b).other_input_port().unwrap();
800
801            self.hugr.connect(*a, a_port, *b, b_port);
802        }
803
804        Ok(())
805    }
806
807    /// Imports a closed list whose first element is an ADT with a closed list
808    /// of variants. The variants and the remaining elements of the list are
809    /// returned as type rows.
810    fn import_adt_and_rest(
811        &mut self,
812        list: table::TermId,
813    ) -> Result<(Vec<TypeRow>, TypeRow), ImportErrorInner> {
814        let items = self.import_closed_list(list)?;
815
816        let Some((first, rest)) = items.split_first() else {
817            return Err(error_invalid!("expected list to have at least one element"));
818        };
819
820        let sum_rows: Vec<_> = {
821            let [variants] = self.expect_symbol(*first, model::CORE_ADT)?;
822            self.import_type_rows(variants)?
823        };
824
825        let rest = rest
826            .iter()
827            .map(|term| self.import_type(*term))
828            .collect::<Result<Vec<_>, _>>()?
829            .into();
830
831        Ok((sum_rows, rest))
832    }
833
834    fn import_tail_loop(
835        &mut self,
836        node_id: table::NodeId,
837        parent: Node,
838    ) -> Result<Node, ImportErrorInner> {
839        let node_data = self.get_node(node_id)?;
840        debug_assert_eq!(node_data.operation, table::Operation::TailLoop);
841
842        let [region] = node_data.regions else {
843            return Err(error_invalid!(
844                "loop node {} expects a single region",
845                node_id
846            ));
847        };
848
849        let region_data = self.get_region(*region)?;
850
851        let (just_inputs, just_outputs, rest) = (|| {
852            let [_, region_outputs] = self.get_func_type(
853                region_data
854                    .signature
855                    .ok_or_else(|| error_uninferred!("region signature"))?,
856            )?;
857            let (sum_rows, rest) = self.import_adt_and_rest(region_outputs)?;
858
859            if sum_rows.len() != 2 {
860                return Err(error_invalid!(
861                    "Loop nodes expect their first target to be an ADT with two variants."
862                ));
863            }
864
865            let mut sum_rows = sum_rows.into_iter();
866            let just_inputs = sum_rows.next().unwrap();
867            let just_outputs = sum_rows.next().unwrap();
868
869            Ok((just_inputs, just_outputs, rest))
870        })()
871        .map_err(|err| error_context!(err, "region signature"))?;
872
873        let optype = OpType::TailLoop(TailLoop {
874            just_inputs,
875            just_outputs,
876            rest,
877        });
878
879        let node = self.make_node(node_id, optype, parent)?;
880
881        self.import_dfg_region(*region, node)?;
882        Ok(node)
883    }
884
885    fn import_conditional(
886        &mut self,
887        node_id: table::NodeId,
888        parent: Node,
889    ) -> Result<Node, ImportErrorInner> {
890        let node_data = self.get_node(node_id)?;
891        debug_assert_eq!(node_data.operation, table::Operation::Conditional);
892
893        let (sum_rows, other_inputs, outputs) = (|| {
894            let [inputs, outputs] = self.get_func_type(
895                node_data
896                    .signature
897                    .ok_or_else(|| error_uninferred!("node signature"))?,
898            )?;
899            let (sum_rows, other_inputs) = self.import_adt_and_rest(inputs)?;
900            let outputs = self.import_type_row(outputs)?;
901
902            Ok((sum_rows, other_inputs, outputs))
903        })()
904        .map_err(|err| error_context!(err, "node signature"))?;
905
906        let optype = OpType::Conditional(Conditional {
907            sum_rows,
908            other_inputs,
909            outputs,
910        });
911
912        let node = self.make_node(node_id, optype, parent)?;
913
914        for region in node_data.regions {
915            let region_data = self.get_region(*region)?;
916            let signature = self.import_func_type(
917                region_data
918                    .signature
919                    .ok_or_else(|| error_uninferred!("region signature"))?,
920            )?;
921
922            let case_node = self
923                .hugr
924                .add_node_with_parent(node, OpType::Case(Case { signature }));
925
926            self.import_dfg_region(*region, case_node)?;
927        }
928
929        Ok(node)
930    }
931
932    /// Imports a control flow region.
933    ///
934    /// The `hugr-model` and `hugr-core` representations of control flow are
935    /// slightly different, and so this method needs to perform some conversion.
936    ///
937    /// In `hugr-core` the first node in the region is a [`BasicBlock`] that by
938    /// virtue of its position is the designated entry block. The second node in
939    /// the region is an [`ExitBlock`]. The [`ExitBlock`] is analogous to an
940    /// [`Output`] node in a dataflow graph, while there is no direct control flow
941    /// equivalent to [`Input`] nodes.
942    ///
943    /// In `hugr-model` control flow regions have a single source and target port,
944    /// respectively, mirroring data flow regions. The region's source port needs
945    /// to be connected to either the input port of a block in the region or to the
946    /// target port.
947    fn import_cfg_region(
948        &mut self,
949        region: table::RegionId,
950        node: Node,
951    ) -> Result<(), ImportErrorInner> {
952        let region_data = self.get_region(region)?;
953
954        if region_data.kind != model::RegionKind::ControlFlow {
955            return Err(error_invalid!("expected cfg region"));
956        }
957
958        let prev_region = self.region_scope;
959        if region_data.scope.is_some() {
960            self.region_scope = region;
961        }
962
963        let region_target_types = (|| {
964            let [_, region_targets] = self.get_ctrl_type(
965                region_data
966                    .signature
967                    .ok_or_else(|| error_uninferred!("region signature"))?,
968            )?;
969
970            self.import_closed_list(region_targets)
971        })()
972        .map_err(|err| error_context!(err, "signature of cfg region with id {}", region))?;
973
974        // Identify the entry node of the control flow region by looking for
975        // a block whose input is linked to the sole source port of the CFG region.
976        let entry_node = 'find_entry: {
977            let [entry_link] = region_data.sources else {
978                return Err(error_invalid!("cfg region expects a single source"));
979            };
980
981            for child in region_data.children {
982                let child_data = self.get_node(*child)?;
983                let is_entry = child_data.inputs.iter().any(|link| link == entry_link);
984
985                if is_entry {
986                    break 'find_entry *child;
987                }
988            }
989
990            // TODO: We should allow for the case in which control flows
991            // directly from the source to the target of the region. This is
992            // currently not allowed in hugr core directly, but may be simulated
993            // by constructing an empty entry block.
994            return Err(error_invalid!("cfg region without entry node"));
995        };
996
997        // The entry node in core control flow regions is identified by being
998        // the first child node of the CFG node. We therefore import the entry node first.
999        self.import_node(entry_node, node)?;
1000
1001        // Create the exit node for the control flow region. This always needs
1002        // to be second in the node list.
1003        {
1004            let cfg_outputs = {
1005                let [target_types] = region_target_types.as_slice() else {
1006                    return Err(error_invalid!("cfg region expects a single target"));
1007                };
1008
1009                self.import_type_row(*target_types)?
1010            };
1011
1012            let exit = self
1013                .hugr
1014                .add_node_with_parent(node, OpType::ExitBlock(ExitBlock { cfg_outputs }));
1015            self.record_links(exit, Direction::Incoming, region_data.targets);
1016        }
1017
1018        // Finally we import all other nodes.
1019        for child in region_data.children {
1020            if *child != entry_node {
1021                self.import_node(*child, node)?;
1022            }
1023        }
1024
1025        for meta_item in region_data.meta {
1026            self.import_node_metadata(node, *meta_item)
1027                .map_err(|err| error_context!(err, "node metadata"))?;
1028        }
1029
1030        self.region_scope = prev_region;
1031
1032        Ok(())
1033    }
1034
1035    fn import_node_block(
1036        &mut self,
1037        node_id: table::NodeId,
1038        parent: Node,
1039    ) -> Result<Node, ImportErrorInner> {
1040        let node_data = self.get_node(node_id)?;
1041        debug_assert_eq!(node_data.operation, table::Operation::Block);
1042
1043        let [region] = node_data.regions else {
1044            return Err(error_invalid!("basic block expects a single region"));
1045        };
1046        let region_data = self.get_region(*region)?;
1047        let [inputs, outputs] = self.get_func_type(
1048            region_data
1049                .signature
1050                .ok_or_else(|| error_uninferred!("region signature"))?,
1051        )?;
1052        let inputs = self.import_type_row(inputs)?;
1053        let (sum_rows, other_outputs) = self.import_adt_and_rest(outputs)?;
1054
1055        let optype = OpType::DataflowBlock(DataflowBlock {
1056            inputs,
1057            other_outputs,
1058            sum_rows,
1059        });
1060        let node = self.make_node(node_id, optype, parent)?;
1061
1062        self.import_dfg_region(*region, node).map_err(|err| {
1063            error_context!(err, "block body defined by region with id {}", *region)
1064        })?;
1065        Ok(node)
1066    }
1067
1068    fn import_node_define_func(
1069        &mut self,
1070        node_id: table::NodeId,
1071        symbol: &'a table::Symbol<'a>,
1072        node_data: &'a table::Node<'a>,
1073        parent: Node,
1074    ) -> Result<Node, ImportErrorInner> {
1075        let visibility = symbol.visibility.clone().ok_or(ImportErrorInner::Invalid(
1076            "No visibility for FuncDefn".to_string(),
1077        ))?;
1078        self.import_poly_func_type(node_id, *symbol, |ctx, signature| {
1079            let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name);
1080
1081            let optype =
1082                OpType::FuncDefn(FuncDefn::new_vis(func_name, signature, visibility.into()));
1083
1084            let node = ctx.make_node(node_id, optype, parent)?;
1085
1086            let [region] = node_data.regions else {
1087                return Err(error_invalid!(
1088                    "function definition nodes expect a single region"
1089                ));
1090            };
1091
1092            ctx.import_dfg_region(*region, node).map_err(|err| {
1093                error_context!(err, "function body defined by region with id {}", *region)
1094            })?;
1095
1096            Ok(node)
1097        })
1098    }
1099
1100    fn import_node_declare_func(
1101        &mut self,
1102        node_id: table::NodeId,
1103        symbol: &'a table::Symbol<'a>,
1104        parent: Node,
1105    ) -> Result<Node, ImportErrorInner> {
1106        let visibility = symbol.visibility.clone().ok_or(ImportErrorInner::Invalid(
1107            "No visibility for FuncDecl".to_string(),
1108        ))?;
1109        self.import_poly_func_type(node_id, *symbol, |ctx, signature| {
1110            let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name);
1111
1112            let optype =
1113                OpType::FuncDecl(FuncDecl::new_vis(func_name, signature, visibility.into()));
1114            let node = ctx.make_node(node_id, optype, parent)?;
1115            Ok(node)
1116        })
1117    }
1118
1119    /// Import a node with a custom operation.
1120    ///
1121    /// A custom operation in `hugr-model` referred to by a symbol application
1122    /// term. The name of the symbol specifies the name of the custom operation,
1123    /// and the arguments supplied to the symbol are the arguments to be passed
1124    /// to the custom operation. This method imports the custom operations as
1125    /// [`OpaqueOp`]s. The [`OpaqueOp`]s are then resolved later against the
1126    /// [`ExtensionRegistry`].
1127    ///
1128    /// Some operations that needed to be builtins in `hugr-core` are custom
1129    /// operations in `hugr-model`. This method detects these and converts them
1130    /// to the corresponding `hugr-core` builtins.
1131    fn import_node_custom(
1132        &mut self,
1133        node_id: table::NodeId,
1134        operation: table::TermId,
1135        node_data: &'a table::Node<'a>,
1136        parent: Node,
1137    ) -> Result<Node, ImportErrorInner> {
1138        if let Some([inputs, outputs]) = self.match_symbol(operation, model::CORE_CALL_INDIRECT)? {
1139            let inputs = self.import_type_row(inputs)?;
1140            let outputs = self.import_type_row(outputs)?;
1141            let signature = Signature::new(inputs, outputs);
1142            let optype = OpType::CallIndirect(CallIndirect { signature });
1143            let node = self.make_node(node_id, optype, parent)?;
1144            return Ok(node);
1145        }
1146
1147        if let Some([_, _, func]) = self.match_symbol(operation, model::CORE_CALL)? {
1148            let (symbol, args) = match self.get_term(func)? {
1149                table::Term::Apply(symbol, args) => (symbol, args),
1150                table::Term::Var(_) => {
1151                    // TODO: Allow calling functions that are passed as variables.
1152                    //
1153                    // This would be necessary to allow functions which take
1154                    // other functions as static parameters and then call them.
1155                    // See #2301.
1156                    return Err(error_unsupported!(
1157                        "`{}` does not yet support function variables.",
1158                        model::CORE_CALL
1159                    ));
1160                }
1161                table::Term::Func(_) => {
1162                    // TODO: Allow importing and calling anonymous functions.
1163                    //
1164                    // This could be implemented in `hugr-core` by lifting the anonymous function
1165                    // into a function to be added into the containing module and then calling that
1166                    // function. See #2559.
1167                    return Err(error_unsupported!(
1168                        "`{}` does not yet support anonymous functions.",
1169                        model::CORE_CALL
1170                    ));
1171                }
1172                _ => {
1173                    return Err(error_invalid!(
1174                        concat!(
1175                            "Expected a function to be passed to `{}`.\n",
1176                            "Currently this is restricted to symbols that refer to functions."
1177                        ),
1178                        model::CORE_CALL
1179                    ));
1180                }
1181            };
1182
1183            let func_sig = self.get_func_signature(*symbol)?;
1184
1185            let type_args = args
1186                .iter()
1187                .map(|term| self.import_term(*term))
1188                .collect::<Result<Vec<TypeArg>, _>>()?;
1189
1190            self.static_edges.push((*symbol, node_id));
1191            let optype = OpType::Call(
1192                Call::try_new(func_sig, type_args).map_err(ImportErrorInner::Signature)?,
1193            );
1194
1195            let node = self.make_node(node_id, optype, parent)?;
1196            return Ok(node);
1197        }
1198
1199        if let Some([_, value]) = self.match_symbol(operation, model::CORE_LOAD_CONST)? {
1200            // If the constant refers directly to a function, import this as the `LoadFunc` operation.
1201            if let table::Term::Apply(symbol, args) = self.get_term(value)? {
1202                let func_node_data = self.get_node(*symbol)?;
1203
1204                if let table::Operation::DefineFunc(_) | table::Operation::DeclareFunc(_) =
1205                    func_node_data.operation
1206                {
1207                    let func_sig = self.get_func_signature(*symbol)?;
1208                    let type_args = args
1209                        .iter()
1210                        .map(|term| self.import_term(*term))
1211                        .collect::<Result<Vec<TypeArg>, _>>()?;
1212
1213                    self.static_edges.push((*symbol, node_id));
1214
1215                    let optype = OpType::LoadFunction(
1216                        LoadFunction::try_new(func_sig, type_args)
1217                            .map_err(ImportErrorInner::Signature)?,
1218                    );
1219
1220                    let node = self.make_node(node_id, optype, parent)?;
1221                    return Ok(node);
1222                }
1223            }
1224
1225            // Otherwise use const nodes
1226            let signature = node_data
1227                .signature
1228                .ok_or_else(|| error_uninferred!("node signature"))?;
1229            let [_, outputs] = self.get_func_type(signature)?;
1230            let outputs = self.import_closed_list(outputs)?;
1231            let output = outputs.first().ok_or_else(|| {
1232                error_invalid!("`{}` expects a single output", model::CORE_LOAD_CONST)
1233            })?;
1234            let datatype = self.import_type(*output)?;
1235
1236            let imported_value = self.import_value(value, *output)?;
1237
1238            let load_const_node = self.make_node(
1239                node_id,
1240                OpType::LoadConstant(LoadConstant {
1241                    datatype: datatype.clone(),
1242                }),
1243                parent,
1244            )?;
1245
1246            let const_node = self
1247                .hugr
1248                .add_node_with_parent(parent, OpType::Const(Const::new(imported_value)));
1249
1250            self.hugr.connect(const_node, 0, load_const_node, 0);
1251
1252            return Ok(load_const_node);
1253        }
1254
1255        if let Some([_, _, tag]) = self.match_symbol(operation, model::CORE_MAKE_ADT)? {
1256            let tag = match self.get_term(tag)? {
1257                table::Term::Literal(model::Literal::Nat(tag)) => tag,
1258                table::Term::Var(_) => {
1259                    return Err(error_unsupported!(
1260                        concat!(
1261                            "`{}` does not yet support passing a variable as the tag.\n",
1262                            "The `hugr-core` builtin `Tag` operation expects a concrete value for the tag. ",
1263                            "Therefore we must insist on a tag given as a natural number literal on import.",
1264                        ),
1265                        model::CORE_MAKE_ADT
1266                    ));
1267                }
1268                _ => {
1269                    return Err(error_invalid!(
1270                        "`{}` expects a nat literal tag",
1271                        model::CORE_MAKE_ADT
1272                    ));
1273                }
1274            };
1275
1276            let signature = node_data
1277                .signature
1278                .ok_or_else(|| error_uninferred!("node signature"))?;
1279            let [_, outputs] = self.get_func_type(signature)?;
1280            let (variants, _) = self.import_adt_and_rest(outputs)?;
1281            let node = self.make_node(
1282                node_id,
1283                OpType::Tag(Tag {
1284                    variants,
1285                    tag: *tag as usize,
1286                }),
1287                parent,
1288            )?;
1289            return Ok(node);
1290        }
1291
1292        let table::Term::Apply(node, params) = self.get_term(operation)? else {
1293            return Err(error_invalid!(
1294                "custom operations expect a symbol application referencing an operation"
1295            ));
1296        };
1297        let name = self.get_symbol_name(*node)?;
1298        let args = params
1299            .iter()
1300            .map(|param| self.import_term(*param))
1301            .collect::<Result<Vec<_>, _>>()?;
1302        let (extension, name) = self.import_custom_name(name)?;
1303        let signature = self.get_node_signature(node_id)?;
1304
1305        // TODO: Currently we do not have the description or any other metadata for
1306        // the custom op. This will improve with declarative extensions being able
1307        // to declare operations as a node, in which case the description will be attached
1308        // to that node as metadata.
1309
1310        let optype = OpType::OpaqueOp(OpaqueOp::new(extension, name, args, signature));
1311        self.make_node(node_id, optype, parent)
1312    }
1313
1314    fn import_node_define_alias(
1315        &mut self,
1316        node_id: table::NodeId,
1317        symbol: &'a table::Symbol<'a>,
1318        value: table::TermId,
1319        parent: Node,
1320    ) -> Result<Node, ImportErrorInner> {
1321        if !symbol.params.is_empty() {
1322            return Err(error_unsupported!(
1323                "parameters or constraints in alias definition"
1324            ));
1325        }
1326
1327        let optype = OpType::AliasDefn(AliasDefn {
1328            name: symbol.name.to_smolstr(),
1329            definition: self.import_type(value)?,
1330        });
1331
1332        let node = self.make_node(node_id, optype, parent)?;
1333        Ok(node)
1334    }
1335
1336    fn import_node_declare_alias(
1337        &mut self,
1338        node_id: table::NodeId,
1339        symbol: &'a table::Symbol<'a>,
1340        parent: Node,
1341    ) -> Result<Node, ImportErrorInner> {
1342        if !symbol.params.is_empty() {
1343            return Err(error_unsupported!(
1344                "parameters or constraints in alias declaration"
1345            ));
1346        }
1347
1348        let optype = OpType::AliasDecl(AliasDecl {
1349            name: symbol.name.to_smolstr(),
1350            bound: TypeBound::Copyable,
1351        });
1352
1353        let node = self.make_node(node_id, optype, parent)?;
1354        Ok(node)
1355    }
1356
1357    fn import_poly_func_type<RV: MaybeRV, T>(
1358        &mut self,
1359        node: table::NodeId,
1360        symbol: table::Symbol<'a>,
1361        in_scope: impl FnOnce(&mut Self, PolyFuncTypeBase<RV>) -> Result<T, ImportErrorInner>,
1362    ) -> Result<T, ImportErrorInner> {
1363        (|| {
1364            let mut imported_params = Vec::with_capacity(symbol.params.len());
1365
1366            for (index, param) in symbol.params.iter().enumerate() {
1367                self.local_vars
1368                    .insert(table::VarId(node, index as _), LocalVar::new(param.r#type));
1369            }
1370
1371            for constraint in symbol.constraints {
1372                if let Some([term]) = self.match_symbol(*constraint, model::CORE_NON_LINEAR)? {
1373                    let table::Term::Var(var) = self.get_term(term)? else {
1374                        return Err(error_unsupported!(
1375                            "constraint on term that is not a variable"
1376                        ));
1377                    };
1378
1379                    self.local_vars
1380                        .get_mut(var)
1381                        .ok_or_else(|| error_invalid!("unknown variable {}", var))?
1382                        .bound = TypeBound::Copyable;
1383                } else {
1384                    return Err(error_unsupported!(
1385                        concat!(
1386                            "Constraints other than `{}` can not yet be imported.\n",
1387                            "`hugr-core` does not have support for arbitrary constraints yet, ",
1388                            "instead relying on operation-specific Rust code to compute and ",
1389                            "validate signatures of custom operations."
1390                        ),
1391                        model::CORE_NON_LINEAR
1392                    ));
1393                }
1394            }
1395
1396            for (index, param) in symbol.params.iter().enumerate() {
1397                let bound = self.local_vars[&table::VarId(node, index as _)].bound;
1398                imported_params.push(
1399                    self.import_term_with_bound(param.r#type, bound)
1400                        .map_err(|err| error_context!(err, "type of parameter `{}`", param.name))?,
1401                );
1402            }
1403
1404            let body = self.import_func_type::<RV>(symbol.signature)?;
1405            in_scope(self, PolyFuncTypeBase::new(imported_params, body))
1406        })()
1407        .map_err(|err| error_context!(err, "symbol `{}` defined by node {}", symbol.name, node))
1408    }
1409
1410    /// Import a [`Term`] from a term that represents a static type or value.
1411    fn import_term(&mut self, term_id: table::TermId) -> Result<Term, ImportErrorInner> {
1412        self.import_term_with_bound(term_id, TypeBound::Linear)
1413    }
1414
1415    fn import_term_with_bound(
1416        &mut self,
1417        term_id: table::TermId,
1418        bound: TypeBound,
1419    ) -> Result<Term, ImportErrorInner> {
1420        (|| {
1421            if let Some([]) = self.match_symbol(term_id, model::CORE_STR_TYPE)? {
1422                return Ok(Term::StringType);
1423            }
1424
1425            if let Some([]) = self.match_symbol(term_id, model::CORE_NAT_TYPE)? {
1426                return Ok(Term::max_nat_type());
1427            }
1428
1429            if let Some([]) = self.match_symbol(term_id, model::CORE_BYTES_TYPE)? {
1430                return Ok(Term::BytesType);
1431            }
1432
1433            if let Some([]) = self.match_symbol(term_id, model::CORE_FLOAT_TYPE)? {
1434                return Ok(Term::FloatType);
1435            }
1436
1437            if let Some([]) = self.match_symbol(term_id, model::CORE_TYPE)? {
1438                return Ok(TypeParam::RuntimeType(bound));
1439            }
1440
1441            if let Some([]) = self.match_symbol(term_id, model::CORE_CONSTRAINT)? {
1442                return Err(error_unsupported!("`{}`", model::CORE_CONSTRAINT));
1443            }
1444
1445            if let Some([]) = self.match_symbol(term_id, model::CORE_STATIC)? {
1446                return Ok(Term::StaticType);
1447            }
1448
1449            if let Some([ty]) = self.match_symbol(term_id, model::CORE_CONST)? {
1450                let ty = self
1451                    .import_type(ty)
1452                    .map_err(|err| error_context!(err, "type of a constant"))?;
1453                return Ok(TypeParam::new_const(ty));
1454            }
1455
1456            if let Some([item_type]) = self.match_symbol(term_id, model::CORE_LIST_TYPE)? {
1457                // At present `hugr-model` has no way to express that the item
1458                // type of a list must be copyable. Therefore we import it as `Any`.
1459                let item_type = self
1460                    .import_term(item_type)
1461                    .map_err(|err| error_context!(err, "item type of list type"))?;
1462                return Ok(TypeParam::new_list_type(item_type));
1463            }
1464
1465            if let Some([item_types]) = self.match_symbol(term_id, model::CORE_TUPLE_TYPE)? {
1466                // At present `hugr-model` has no way to express that the item
1467                // types of a tuple must be copyable. Therefore we import it as `Any`.
1468                let item_types = self
1469                    .import_term(item_types)
1470                    .map_err(|err| error_context!(err, "item types of tuple type"))?;
1471                return Ok(TypeParam::new_tuple_type(item_types));
1472            }
1473
1474            match self.get_term(term_id)? {
1475                table::Term::Wildcard => Err(error_uninferred!("wildcard")),
1476
1477                table::Term::Var(var) => {
1478                    let var_info = self
1479                        .local_vars
1480                        .get(var)
1481                        .ok_or_else(|| error_invalid!("unknown variable {}", var))?;
1482                    let decl = self.import_term_with_bound(var_info.r#type, var_info.bound)?;
1483                    Ok(Term::new_var_use(var.1 as _, decl))
1484                }
1485
1486                table::Term::List(parts) => {
1487                    // PERFORMANCE: Can we do this without the additional allocation?
1488                    let parts: Vec<_> = parts
1489                        .iter()
1490                        .map(|part| self.import_seq_part(part))
1491                        .collect::<Result<_, _>>()
1492                        .map_err(|err| error_context!(err, "list parts"))?;
1493                    Ok(TypeArg::new_list_from_parts(parts))
1494                }
1495
1496                table::Term::Tuple(parts) => {
1497                    // PERFORMANCE: Can we do this without the additional allocation?
1498                    let parts: Vec<_> = parts
1499                        .iter()
1500                        .map(|part| self.import_seq_part(part))
1501                        .try_collect()
1502                        .map_err(|err| error_context!(err, "tuple parts"))?;
1503                    Ok(TypeArg::new_tuple_from_parts(parts))
1504                }
1505
1506                table::Term::Literal(model::Literal::Str(value)) => {
1507                    Ok(Term::String(value.to_string()))
1508                }
1509
1510                table::Term::Literal(model::Literal::Nat(value)) => Ok(Term::BoundedNat(*value)),
1511
1512                table::Term::Literal(model::Literal::Bytes(value)) => {
1513                    Ok(Term::Bytes(value.clone()))
1514                }
1515                table::Term::Literal(model::Literal::Float(value)) => Ok(Term::Float(*value)),
1516                table::Term::Func { .. } => Err(error_unsupported!("function constant")),
1517
1518                table::Term::Apply { .. } => {
1519                    let ty: Type = self.import_type(term_id)?;
1520                    Ok(ty.into())
1521                }
1522            }
1523        })()
1524        .map_err(|err| error_context!(err, "term {}", term_id))
1525    }
1526
1527    fn import_seq_part(
1528        &mut self,
1529        seq_part: &'a table::SeqPart,
1530    ) -> Result<SeqPart<TypeArg>, ImportErrorInner> {
1531        Ok(match seq_part {
1532            table::SeqPart::Item(term_id) => SeqPart::Item(self.import_term(*term_id)?),
1533            table::SeqPart::Splice(term_id) => SeqPart::Splice(self.import_term(*term_id)?),
1534        })
1535    }
1536
1537    /// Import a `Type` from a term that represents a runtime type.
1538    fn import_type<RV: MaybeRV>(
1539        &mut self,
1540        term_id: table::TermId,
1541    ) -> Result<TypeBase<RV>, ImportErrorInner> {
1542        (|| {
1543            if let Some([_, _]) = self.match_symbol(term_id, model::CORE_FN)? {
1544                let func_type = self.import_func_type::<RowVariable>(term_id)?;
1545                return Ok(TypeBase::new_function(func_type));
1546            }
1547
1548            if let Some([variants]) = self.match_symbol(term_id, model::CORE_ADT)? {
1549                let variants = (|| {
1550                    self.import_closed_list(variants)?
1551                        .iter()
1552                        .map(|variant| self.import_type_row::<RowVariable>(*variant))
1553                        .collect::<Result<Vec<_>, _>>()
1554                })()
1555                .map_err(|err| error_context!(err, "adt variants"))?;
1556
1557                return Ok(TypeBase::new_sum(variants));
1558            }
1559
1560            match self.get_term(term_id)? {
1561                table::Term::Wildcard => Err(error_uninferred!("wildcard")),
1562
1563                table::Term::Apply(symbol, args) => {
1564                    let name = self.get_symbol_name(*symbol)?;
1565
1566                    let args = args
1567                        .iter()
1568                        .map(|arg| self.import_term(*arg))
1569                        .collect::<Result<Vec<_>, _>>()
1570                        .map_err(|err| {
1571                            error_context!(err, "type argument of custom type `{}`", name)
1572                        })?;
1573
1574                    let (extension, id) = self.import_custom_name(name)?;
1575
1576                    let extension_ref =
1577                        self.extensions
1578                            .get(&extension)
1579                            .ok_or_else(|| ExtensionError::Missing {
1580                                missing_ext: extension.clone(),
1581                                available: self.extensions.ids().cloned().collect(),
1582                            })?;
1583
1584                    let ext_type =
1585                        extension_ref
1586                            .get_type(&id)
1587                            .ok_or_else(|| ExtensionError::MissingType {
1588                                ext: extension.clone(),
1589                                name: id.clone(),
1590                            })?;
1591
1592                    let bound = ext_type.bound(&args);
1593
1594                    Ok(TypeBase::new_extension(CustomType::new(
1595                        id,
1596                        args,
1597                        extension,
1598                        bound,
1599                        &Arc::downgrade(extension_ref),
1600                    )))
1601                }
1602
1603                table::Term::Var(var @ table::VarId(_, index)) => {
1604                    let local_var = self
1605                        .local_vars
1606                        .get(var)
1607                        .ok_or(error_invalid!("unknown var {}", var))?;
1608                    Ok(TypeBase::new_var_use(*index as _, local_var.bound))
1609                }
1610
1611                // The following terms are not runtime types, but the core `Type` only contains runtime types.
1612                // We therefore report a type error here.
1613                table::Term::Literal(_)
1614                | table::Term::List { .. }
1615                | table::Term::Tuple { .. }
1616                | table::Term::Func { .. } => Err(error_invalid!("expected a runtime type")),
1617            }
1618        })()
1619        .map_err(|err| error_context!(err, "term {} as `Type`", term_id))
1620    }
1621
1622    fn get_func_type(
1623        &mut self,
1624        term_id: table::TermId,
1625    ) -> Result<[table::TermId; 2], ImportErrorInner> {
1626        self.match_symbol(term_id, model::CORE_FN)?
1627            .ok_or(error_invalid!("expected a function type"))
1628    }
1629
1630    fn get_ctrl_type(
1631        &mut self,
1632        term_id: table::TermId,
1633    ) -> Result<[table::TermId; 2], ImportErrorInner> {
1634        self.match_symbol(term_id, model::CORE_CTRL)?
1635            .ok_or(error_invalid!("expected a control type"))
1636    }
1637
1638    /// Import a [`Signature`] or [`FuncValueType`].
1639    ///
1640    /// When importing a [`Signature`] the lists of input and output types need
1641    /// to be closed. In constrast [`FuncValueType`] admits importing open lists
1642    /// of input and output types via "row variables".
1643    ///
1644    /// Function types are not special-cased in `hugr-model` but are represented
1645    /// via the `core.fn` term constructor.
1646    fn import_func_type<RV: MaybeRV>(
1647        &mut self,
1648        term_id: table::TermId,
1649    ) -> Result<FuncTypeBase<RV>, ImportErrorInner> {
1650        (|| {
1651            let [inputs, outputs] = self.get_func_type(term_id)?;
1652            let inputs = self
1653                .import_type_row(inputs)
1654                .map_err(|err| error_context!(err, "function inputs"))?;
1655            let outputs = self
1656                .import_type_row(outputs)
1657                .map_err(|err| error_context!(err, "function outputs"))?;
1658            Ok(FuncTypeBase::new(inputs, outputs))
1659        })()
1660        .map_err(|err| error_context!(err, "function type"))
1661    }
1662
1663    /// Import a closed list as a vector of term ids.
1664    ///
1665    /// This method supports list terms that contain spliced sublists as long as
1666    /// the list can be recursively flattened to only contain individual items.
1667    ///
1668    /// To allow for IR constructions that are parameterised by static
1669    /// parameters, open lists with spliced variables should be supported where
1670    /// possible. Closed lists might be required in some places of the IR that
1671    /// are not supposed to be parameterised with variables or where such
1672    /// parameterisation is not yet supported by the `hugr-core` structures that
1673    /// we are importing into.
1674    fn import_closed_list(
1675        &mut self,
1676        term_id: table::TermId,
1677    ) -> Result<Vec<table::TermId>, ImportErrorInner> {
1678        fn import_into(
1679            ctx: &mut Context,
1680            term_id: table::TermId,
1681            types: &mut Vec<table::TermId>,
1682        ) -> Result<(), ImportErrorInner> {
1683            match ctx.get_term(term_id)? {
1684                table::Term::List(parts) => {
1685                    types.reserve(parts.len());
1686
1687                    for part in *parts {
1688                        match part {
1689                            table::SeqPart::Item(term_id) => {
1690                                types.push(*term_id);
1691                            }
1692                            table::SeqPart::Splice(term_id) => {
1693                                import_into(ctx, *term_id, types)?;
1694                            }
1695                        }
1696                    }
1697                }
1698                _ => {
1699                    return Err(error_invalid!(
1700                        "Expected a closed list.\n{}",
1701                        CLOSED_LIST_HINT
1702                    ));
1703                }
1704            }
1705
1706            Ok(())
1707        }
1708
1709        let mut types = Vec::new();
1710        import_into(self, term_id, &mut types)?;
1711        Ok(types)
1712    }
1713
1714    /// Import a closed tuple as a vector of term ids.
1715    ///
1716    /// This is the tuple version of [`Self::import_closed_list`].
1717    fn import_closed_tuple(
1718        &mut self,
1719        term_id: table::TermId,
1720    ) -> Result<Vec<table::TermId>, ImportErrorInner> {
1721        fn import_into(
1722            ctx: &mut Context,
1723            term_id: table::TermId,
1724            types: &mut Vec<table::TermId>,
1725        ) -> Result<(), ImportErrorInner> {
1726            match ctx.get_term(term_id)? {
1727                table::Term::Tuple(parts) => {
1728                    types.reserve(parts.len());
1729
1730                    for part in *parts {
1731                        match part {
1732                            table::SeqPart::Item(term_id) => {
1733                                types.push(*term_id);
1734                            }
1735                            table::SeqPart::Splice(term_id) => {
1736                                import_into(ctx, *term_id, types)?;
1737                            }
1738                        }
1739                    }
1740                }
1741                _ => {
1742                    return Err(error_invalid!(
1743                        "Expected a closed tuple term.\n{}",
1744                        CLOSED_TUPLE_HINT
1745                    ));
1746                }
1747            }
1748
1749            Ok(())
1750        }
1751
1752        let mut types = Vec::new();
1753        import_into(self, term_id, &mut types)?;
1754        Ok(types)
1755    }
1756
1757    /// Imports a list of lists as a vector of type rows.
1758    ///
1759    /// See [`Self::import_type_row`].
1760    fn import_type_rows<RV: MaybeRV>(
1761        &mut self,
1762        term_id: table::TermId,
1763    ) -> Result<Vec<TypeRowBase<RV>>, ImportErrorInner> {
1764        self.import_closed_list(term_id)?
1765            .into_iter()
1766            .map(|term_id| self.import_type_row::<RV>(term_id))
1767            .collect()
1768    }
1769
1770    /// Imports a list as a type row.
1771    ///
1772    /// This method works to produce a [`TypeRow`] or a [`TypeRowRV`], depending
1773    /// on the `RV` type argument. For [`TypeRow`] a closed list is expected.
1774    /// For [`TypeRowRV`] we import spliced variables as row variables.
1775    fn import_type_row<RV: MaybeRV>(
1776        &mut self,
1777        term_id: table::TermId,
1778    ) -> Result<TypeRowBase<RV>, ImportErrorInner> {
1779        fn import_into<RV: MaybeRV>(
1780            ctx: &mut Context,
1781            term_id: table::TermId,
1782            types: &mut Vec<TypeBase<RV>>,
1783        ) -> Result<(), ImportErrorInner> {
1784            match ctx.get_term(term_id)? {
1785                table::Term::List(parts) => {
1786                    types.reserve(parts.len());
1787
1788                    for item in *parts {
1789                        match item {
1790                            table::SeqPart::Item(term_id) => {
1791                                types.push(ctx.import_type::<RV>(*term_id)?);
1792                            }
1793                            table::SeqPart::Splice(term_id) => {
1794                                import_into(ctx, *term_id, types)?;
1795                            }
1796                        }
1797                    }
1798                }
1799                table::Term::Var(table::VarId(_, index)) => {
1800                    let var = RV::try_from_rv(RowVariable(*index as _, TypeBound::Linear))
1801                        .map_err(|_| {
1802                            error_invalid!("Expected a closed list.\n{}", CLOSED_LIST_HINT)
1803                        })?;
1804                    types.push(TypeBase::new(TypeEnum::RowVar(var)));
1805                }
1806                _ => return Err(error_invalid!("expected a list")),
1807            }
1808
1809            Ok(())
1810        }
1811
1812        let mut types = Vec::new();
1813        import_into(self, term_id, &mut types)?;
1814        Ok(types.into())
1815    }
1816
1817    fn import_custom_name(
1818        &mut self,
1819        symbol: &'a str,
1820    ) -> Result<(ExtensionId, SmolStr), ImportErrorInner> {
1821        use std::collections::hash_map::Entry;
1822        match self.custom_name_cache.entry(symbol) {
1823            Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
1824            Entry::Vacant(vacant_entry) => {
1825                let qualified_name = ExtensionId::new(symbol)
1826                    .map_err(|_| error_invalid!("`{}` is not a valid symbol name", symbol))?;
1827
1828                let (extension, id) = qualified_name
1829                    .split_last()
1830                    .ok_or_else(|| error_invalid!("`{}` is not a valid symbol name", symbol))?;
1831
1832                vacant_entry.insert((extension.clone(), id.clone()));
1833                Ok((extension, id))
1834            }
1835        }
1836    }
1837
1838    /// Import a constant term as a [`Value`].
1839    ///
1840    /// This method supports the JSON compatibility constants and a small selection of built in
1841    /// constant constructors. It is a compatibility shim until constants can be represented as
1842    /// terms in `hugr-core` at which point this method will become redundant.
1843    fn import_value(
1844        &mut self,
1845        term_id: table::TermId,
1846        type_id: table::TermId,
1847    ) -> Result<Value, ImportErrorInner> {
1848        let term_data = self.get_term(term_id)?;
1849
1850        // NOTE: We have special cased arrays, integers, and floats for now.
1851        // TODO: Allow arbitrary extension values to be imported from terms.
1852
1853        if let Some([runtime_type, json]) = self.match_symbol(term_id, model::COMPAT_CONST_JSON)? {
1854            let table::Term::Literal(model::Literal::Str(json)) = self.get_term(json)? else {
1855                return Err(error_invalid!(
1856                    "`{}` expects a string literal",
1857                    model::COMPAT_CONST_JSON
1858                ));
1859            };
1860
1861            // We attempt to deserialize as the custom const directly.
1862            // This might fail due to the custom const struct not being included when
1863            // this code was compiled; in that case, we fall back to the serialized form.
1864            let value: Option<Box<dyn CustomConst>> = serde_json::from_str(json).ok();
1865
1866            if let Some(value) = value {
1867                let opaque_value = OpaqueValue::from(value);
1868                return Ok(Value::Extension { e: opaque_value });
1869            } else {
1870                let runtime_type = self.import_type(runtime_type)?;
1871                let value: serde_json::Value = serde_json::from_str(json).map_err(|_| {
1872                    error_invalid!(
1873                        "unable to parse JSON string for `{}`",
1874                        model::COMPAT_CONST_JSON
1875                    )
1876                })?;
1877                let custom_const = CustomSerialized::new(runtime_type, value);
1878                let opaque_value = OpaqueValue::new(custom_const);
1879                return Ok(Value::Extension { e: opaque_value });
1880            }
1881        }
1882
1883        if let Some([_, element_type_term, contents]) =
1884            self.match_symbol(term_id, ArrayValue::CTR_NAME)?
1885        {
1886            let element_type = self.import_type(element_type_term)?;
1887            let contents = self.import_closed_list(contents)?;
1888            let contents = contents
1889                .iter()
1890                .map(|item| self.import_value(*item, element_type_term))
1891                .collect::<Result<Vec<_>, _>>()?;
1892            return Ok(ArrayValue::new(element_type, contents).into());
1893        }
1894
1895        if let Some([bitwidth, value]) = self.match_symbol(term_id, ConstInt::CTR_NAME)? {
1896            let bitwidth = {
1897                let table::Term::Literal(model::Literal::Nat(bitwidth)) =
1898                    self.get_term(bitwidth)?
1899                else {
1900                    return Err(error_invalid!(
1901                        "`{}` expects a nat literal in its `bitwidth` argument",
1902                        ConstInt::CTR_NAME
1903                    ));
1904                };
1905                if *bitwidth > 6 {
1906                    return Err(error_invalid!(
1907                        "`{}` expects the bitwidth to be at most 6, got {}",
1908                        ConstInt::CTR_NAME,
1909                        bitwidth
1910                    ));
1911                }
1912                *bitwidth as u8
1913            };
1914
1915            let value = {
1916                let table::Term::Literal(model::Literal::Nat(value)) = self.get_term(value)? else {
1917                    return Err(error_invalid!(
1918                        "`{}` expects a nat literal value",
1919                        ConstInt::CTR_NAME
1920                    ));
1921                };
1922                *value
1923            };
1924
1925            return Ok(ConstInt::new_u(bitwidth, value)
1926                .map_err(|_| error_invalid!("failed to create int constant"))?
1927                .into());
1928        }
1929
1930        if let Some([value]) = self.match_symbol(term_id, ConstF64::CTR_NAME)? {
1931            let table::Term::Literal(model::Literal::Float(value)) = self.get_term(value)? else {
1932                return Err(error_invalid!(
1933                    "`{}` expects a float literal value",
1934                    ConstF64::CTR_NAME
1935                ));
1936            };
1937
1938            return Ok(ConstF64::new(value.into_inner()).into());
1939        }
1940
1941        if let Some([_, _, tag, values]) = self.match_symbol(term_id, model::CORE_CONST_ADT)? {
1942            let [variants] = self.expect_symbol(type_id, model::CORE_ADT)?;
1943            let values = self.import_closed_tuple(values)?;
1944            let variants = self.import_closed_list(variants)?;
1945
1946            let table::Term::Literal(model::Literal::Nat(tag)) = self.get_term(tag)? else {
1947                return Err(error_invalid!(
1948                    "`{}` expects a nat literal tag",
1949                    model::CORE_ADT
1950                ));
1951            };
1952
1953            let variant = variants.get(*tag as usize).ok_or(error_invalid!(
1954                "the tag of a `{}` must be a valid index into the list of variants",
1955                model::CORE_CONST_ADT
1956            ))?;
1957
1958            let variant = self.import_closed_list(*variant)?;
1959
1960            let items = values
1961                .iter()
1962                .zip(variant.iter())
1963                .map(|(value, typ)| self.import_value(*value, *typ))
1964                .collect::<Result<Vec<_>, _>>()?;
1965
1966            let typ = {
1967                // TODO: Import as a `SumType` directly and avoid the copy.
1968                let typ: Type = self.import_type(type_id)?;
1969                match typ.as_type_enum() {
1970                    TypeEnum::Sum(sum) => sum.clone(),
1971                    _ => unreachable!(),
1972                }
1973            };
1974
1975            return Ok(Value::sum(*tag as _, items, typ).unwrap());
1976        }
1977
1978        match term_data {
1979            table::Term::Wildcard => Err(error_uninferred!("wildcard")),
1980            table::Term::Var(_) => Err(error_unsupported!(concat!(
1981                "Constant value containing a variable.\n",
1982                "The constant system in `hugr-core` is not set up yet to support ",
1983                "constants that depend on variables.",
1984            ))),
1985
1986            table::Term::Apply(symbol, _) => {
1987                let symbol_name = self.get_symbol_name(*symbol)?;
1988                Err(error_unsupported!(
1989                    concat!(
1990                        "Unknown custom constant constructor `{}`.\n",
1991                        "Importing constants from `hugr-model` to `hugr-core` currently only supports a small ",
1992                        "and hard-coded list of constant constructors. To support JSON encoded constants ",
1993                        "use the constant constructor `{}`."
1994                    ),
1995                    symbol_name,
1996                    model::COMPAT_CONST_JSON
1997                ))
1998                // TODO: This should ultimately include the following cases:
1999                // - function definitions
2000                // - custom constructors for values
2001            }
2002
2003            table::Term::List { .. } | table::Term::Tuple(_) | table::Term::Literal(_) => {
2004                Err(error_invalid!("expected constant"))
2005            }
2006
2007            table::Term::Func { .. } => Err(error_unsupported!("Constant function value.")),
2008        }
2009    }
2010
2011    /// Check if a term is an application of a symbol with the given name. If
2012    /// so, return the arguments of the application.
2013    ///
2014    /// We allow the match even if the symbol is applied to fewer arguments than
2015    /// expected. In that case, the arguments are considered implicit and the
2016    /// return array is padded with wildcard terms at the beginning. The match
2017    /// fails if the symbol is applied to more arguments than expected.
2018    ///
2019    /// # Errors
2020    ///
2021    /// An error is returned in the following cases:
2022    ///
2023    /// - The term id does not exist in the module to be imported.
2024    /// - The term is a symbol application but the node id in the application does not refer to a symbol.
2025    ///
2026    /// Failed matches return `Ok(None)` instead of an error so that this method can be used
2027    /// in sequence to probe for applications of different symbol constructors.
2028    fn match_symbol<const N: usize>(
2029        &self,
2030        term_id: table::TermId,
2031        name: &str,
2032    ) -> Result<Option<[table::TermId; N]>, ImportErrorInner> {
2033        let term = self.get_term(term_id)?;
2034
2035        // TODO: Follow alias chains?
2036
2037        let table::Term::Apply(symbol, args) = term else {
2038            return Ok(None);
2039        };
2040
2041        if name != self.get_symbol_name(*symbol)? {
2042            return Ok(None);
2043        }
2044
2045        if args.len() > N {
2046            return Ok(None);
2047        }
2048
2049        let result = std::array::from_fn(|i| {
2050            (i + args.len())
2051                .checked_sub(N)
2052                .map(|i| args[i])
2053                .unwrap_or_default()
2054        });
2055
2056        Ok(Some(result))
2057    }
2058
2059    /// Expects a term to be an application of a symbol with the given name and
2060    /// returns the arguments of the application.
2061    ///
2062    /// See [`Self::match_symbol`].
2063    ///
2064    /// # Errors
2065    ///
2066    /// In addition to the error cases described in [`Self::match_symbol`], this
2067    /// method also returns an error when the match failed.
2068    fn expect_symbol<const N: usize>(
2069        &self,
2070        term_id: table::TermId,
2071        name: &str,
2072    ) -> Result<[table::TermId; N], ImportErrorInner> {
2073        self.match_symbol(term_id, name)?.ok_or(error_invalid!(
2074            "Expected symbol `{}` with arity {}.",
2075            name,
2076            N
2077        ))
2078    }
2079
2080    /// Searches for `core.title` metadata on the given node.
2081    ///
2082    /// The `core.title` metadata is used as the `hugr-core` name for private function symbols.
2083    /// This is necessary as a compatibility shim to bridge between the different concepts of name
2084    /// in `hugr-core` and `hugr-model`: In `hugr-model` the name of a symbol uniquely identifies
2085    /// that symbol within a module, while in `hugr-core` the name started out as procedurally
2086    /// irrelevant debug metadata. With linking, the `hugr-core` name has become significant,
2087    /// but only for public functions.
2088    fn import_title_metadata(
2089        &self,
2090        node_id: table::NodeId,
2091    ) -> Result<Option<&'a str>, ImportErrorInner> {
2092        let node_data = self.get_node(node_id)?;
2093        for meta in node_data.meta {
2094            let Some([name]) = self.match_symbol(*meta, model::CORE_TITLE)? else {
2095                continue;
2096            };
2097
2098            let table::Term::Literal(model::Literal::Str(name)) = self.get_term(name)? else {
2099                return Err(error_invalid!(
2100                    "`{}` metadata expected a string literal as argument",
2101                    model::CORE_TITLE
2102                ));
2103            };
2104
2105            return Ok(Some(name.as_str()));
2106        }
2107
2108        Ok(None)
2109    }
2110}
2111
2112/// Information about a local variable.
2113#[derive(Debug, Clone, Copy)]
2114struct LocalVar {
2115    /// The type of the variable.
2116    r#type: table::TermId,
2117    /// The type bound of the variable.
2118    bound: TypeBound,
2119}
2120
2121impl LocalVar {
2122    pub fn new(r#type: table::TermId) -> Self {
2123        Self {
2124            r#type,
2125            bound: TypeBound::Linear,
2126        }
2127    }
2128}