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