From 6f5f6a5113bcdec8f802c2c4b4b656f5da769eda Mon Sep 17 00:00:00 2001 From: Lukas Heidemann Date: Thu, 17 Jul 2025 13:44:39 +0100 Subject: [PATCH] Names of private functions become `core.title` metadata. --- hugr-core/src/export.rs | 66 +++++++++++++++---- hugr-core/src/import.rs | 32 ++++++++- .../snapshots/model__roundtrip_call.snap | 4 +- .../tests/snapshots/model__roundtrip_cfg.snap | 2 +- .../snapshots/model__roundtrip_cond.snap | 2 +- .../model__roundtrip_constraints.snap | 11 ++-- .../model__roundtrip_entrypoint.snap | 2 +- .../snapshots/model__roundtrip_loop.snap | 5 +- .../snapshots/model__roundtrip_params.snap | 13 ++-- hugr-model/src/v0/mod.rs | 12 ++++ hugr-model/tests/fixtures/model-call.edn | 4 +- hugr-model/tests/fixtures/model-cfg.edn | 2 +- hugr-model/tests/fixtures/model-cond.edn | 2 +- .../tests/fixtures/model-entrypoint.edn | 2 +- hugr-py/src/hugr/model/export.py | 32 ++++++--- 15 files changed, 148 insertions(+), 43 deletions(-) diff --git a/hugr-core/src/export.rs b/hugr-core/src/export.rs index 704285c168..e42515c355 100644 --- a/hugr-core/src/export.rs +++ b/hugr-core/src/export.rs @@ -1,4 +1,5 @@ //! Exporting HUGR graphs to their `hugr-model` representation. +use crate::Visibility; use crate::extension::ExtensionRegistry; use crate::hugr::internal::HugrInternals; use crate::types::type_param::Term; @@ -20,13 +21,14 @@ use crate::{ }; use fxhash::{FxBuildHasher, FxHashMap}; -use hugr_model::v0::Visibility; +use hugr_model::v0::bumpalo; use hugr_model::v0::{ self as model, bumpalo::{Bump, collections::String as BumpString, collections::Vec as BumpVec}, table, }; use petgraph::unionfind::UnionFind; +use smol_str::ToSmolStr; use std::fmt::Write; /// Exports a deconstructed `Package` to its representation in the model. @@ -95,7 +97,7 @@ struct Context<'a> { // that ensures that the `node_to_id` and `id_to_node` maps stay in sync. } -const NO_VIS: Option = None; +const NO_VIS: Option = None; impl<'a> Context<'a> { pub fn new(hugr: &'a Hugr, bump: &'a Bump) -> Self { @@ -261,8 +263,12 @@ impl<'a> Context<'a> { // We record the name of the symbol defined by the node, if any. let symbol = match optype { - OpType::FuncDefn(func_defn) => Some(func_defn.func_name().as_str()), - OpType::FuncDecl(func_decl) => Some(func_decl.func_name().as_str()), + OpType::FuncDefn(_) | OpType::FuncDecl(_) => { + // Functions aren't exported using their core name but with a mangled + // name derived from their id. The function's core name will be recorded + // using `core.title` metadata. + Some(self.mangled_name(node)) + } OpType::AliasDecl(alias_decl) => Some(alias_decl.name.as_str()), OpType::AliasDefn(alias_defn) => Some(alias_defn.name.as_str()), _ => None, @@ -282,6 +288,7 @@ impl<'a> Context<'a> { // the node id. This is necessary to establish the correct node id for the // local scope introduced by some operations. We will overwrite this node later. let mut regions: &[_] = &[]; + let mut meta = Vec::new(); let node = self.id_to_node[&node_id]; let optype = self.hugr.get_optype(node); @@ -331,8 +338,10 @@ impl<'a> Context<'a> { } OpType::FuncDefn(func) => self.with_local_scope(node_id, |this| { + let symbol_name = this.export_func_name(node, &mut meta); + let symbol = this.export_poly_func_type( - func.func_name(), + symbol_name, Some(func.visibility().clone().into()), func.signature(), ); @@ -345,8 +354,10 @@ impl<'a> Context<'a> { }), OpType::FuncDecl(func) => self.with_local_scope(node_id, |this| { + let symbol_name = this.export_func_name(node, &mut meta); + let symbol = this.export_poly_func_type( - func.func_name(), + symbol_name, Some(func.visibility().clone().into()), func.signature(), ); @@ -502,12 +513,9 @@ impl<'a> Context<'a> { let inputs = self.make_ports(node, Direction::Incoming, num_inputs); let outputs = self.make_ports(node, Direction::Outgoing, num_outputs); - let meta = { - let mut meta = Vec::new(); - self.export_node_json_metadata(node, &mut meta); - self.export_node_order_metadata(node, &mut meta); - self.bump.alloc_slice_copy(&meta) - }; + self.export_node_json_metadata(node, &mut meta); + self.export_node_order_metadata(node, &mut meta); + let meta = self.bump.alloc_slice_copy(&meta); self.module.nodes[node_id.index()] = table::Node { operation, @@ -803,7 +811,7 @@ impl<'a> Context<'a> { pub fn export_poly_func_type( &mut self, name: &'a str, - visibility: Option, + visibility: Option, t: &PolyFuncTypeBase, ) -> &'a table::Symbol<'a> { let mut params = BumpVec::with_capacity_in(t.params().len(), self.bump); @@ -1121,6 +1129,33 @@ impl<'a> Context<'a> { } } + /// Used when exporting function definitions or declarations. When the + /// function is public, its symbol name will be the core name. For private + /// functions, the symbol name is derived from the node id and the core name + /// is exported as `core.title` metadata. + /// + /// This is a hack, necessary due to core names for functions being + /// non-functional. Once functions have a "link name", that should be used as the symbol name here. + fn export_func_name(&mut self, node: Node, meta: &mut Vec) -> &'a str { + let (name, vis) = match self.hugr.get_optype(node) { + OpType::FuncDefn(func_defn) => (func_defn.func_name(), func_defn.visibility()), + OpType::FuncDecl(func_decl) => (func_decl.func_name(), func_decl.visibility()), + _ => panic!( + "`export_func_name` is only supposed to be used on function declarations and definitions" + ), + }; + + match vis { + Visibility::Public => name, + Visibility::Private => { + let literal = + self.make_term(table::Term::Literal(model::Literal::Str(name.to_smolstr()))); + meta.push(self.make_term_apply(model::CORE_TITLE, &[literal])); + self.mangled_name(node) + } + } + } + pub fn make_json_meta(&mut self, name: &str, value: &serde_json::Value) -> table::TermId { let value = serde_json::to_string(value).expect("json values are always serializable"); let value = self.make_term(model::Literal::Str(value.into()).into()); @@ -1147,6 +1182,11 @@ impl<'a> Context<'a> { let args = self.bump.alloc_slice_copy(args); self.make_term(table::Term::Apply(symbol, args)) } + + /// Creates a mangled name for a particular node. + fn mangled_name(&self, node: Node) -> &'a str { + bumpalo::format!(in &self.bump, "_{}", node.index()).into_bump_str() + } } type FxIndexSet = indexmap::IndexSet; diff --git a/hugr-core/src/import.rs b/hugr-core/src/import.rs index 087bace7d5..1a9a3a67f7 100644 --- a/hugr-core/src/import.rs +++ b/hugr-core/src/import.rs @@ -970,8 +970,10 @@ impl<'a> Context<'a> { "No visibility for FuncDefn".to_string(), ))?; self.import_poly_func_type(node_id, *symbol, |ctx, signature| { + let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name); + let optype = - OpType::FuncDefn(FuncDefn::new_vis(symbol.name, signature, visibility.into())); + OpType::FuncDefn(FuncDefn::new_vis(func_name, signature, visibility.into())); let node = ctx.make_node(node_id, optype, parent)?; @@ -999,8 +1001,10 @@ impl<'a> Context<'a> { "No visibility for FuncDecl".to_string(), ))?; self.import_poly_func_type(node_id, *symbol, |ctx, signature| { + let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name); + let optype = - OpType::FuncDecl(FuncDecl::new_vis(symbol.name, signature, visibility.into())); + OpType::FuncDecl(FuncDecl::new_vis(func_name, signature, visibility.into())); let node = ctx.make_node(node_id, optype, parent)?; Ok(node) }) @@ -1817,6 +1821,30 @@ impl<'a> Context<'a> { N )) } + + /// Searches for `core.title` metadata on the given node. + fn import_title_metadata( + &self, + node_id: table::NodeId, + ) -> Result, ImportError> { + let node_data = self.get_node(node_id)?; + for meta in node_data.meta { + let Some([name]) = self.match_symbol(*meta, model::CORE_TITLE)? else { + continue; + }; + + let table::Term::Literal(model::Literal::Str(name)) = self.get_term(name)? else { + return Err(error_invalid!( + "`{}` metadata expected a string literal as argument", + model::CORE_TITLE + )); + }; + + return Ok(Some(name.as_str())); + } + + Ok(None) + } } /// Information about a local variable. diff --git a/hugr-core/tests/snapshots/model__roundtrip_call.snap b/hugr-core/tests/snapshots/model__roundtrip_call.snap index dd6ed80449..8681cf372c 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_call.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_call.snap @@ -24,7 +24,7 @@ expression: ast (meta (compat.meta_json "title" "\"Callee\""))) (define-func - private + public example.caller (core.fn [arithmetic.int.types.int] [arithmetic.int.types.int]) (meta @@ -43,7 +43,7 @@ expression: ast (core.fn [arithmetic.int.types.int] [arithmetic.int.types.int]))))) (define-func - private + public example.load (core.fn [] [(core.fn [arithmetic.int.types.int] [arithmetic.int.types.int])]) (dfg [] [%0] diff --git a/hugr-core/tests/snapshots/model__roundtrip_cfg.snap b/hugr-core/tests/snapshots/model__roundtrip_cfg.snap index 934423d3aa..da2fe4851f 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_cfg.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_cfg.snap @@ -16,7 +16,7 @@ expression: ast (import core.adt) -(define-func private example.cfg_loop (param ?0 core.type) (core.fn [?0] [?0]) +(define-func public example.cfg_loop (param ?0 core.type) (core.fn [?0] [?0]) (dfg [%0] [%1] (signature (core.fn [?0] [?0])) (cfg [%0] [%1] diff --git a/hugr-core/tests/snapshots/model__roundtrip_cond.snap b/hugr-core/tests/snapshots/model__roundtrip_cond.snap index b29f755705..c51323db5c 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_cond.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_cond.snap @@ -25,7 +25,7 @@ expression: ast "negation modulo 2^N (signed and unsigned versions are the same op)"))) (define-func - private + public example.cond (core.fn [(core.adt [[] []]) (arithmetic.int.types.int 6)] diff --git a/hugr-core/tests/snapshots/model__roundtrip_constraints.snap b/hugr-core/tests/snapshots/model__roundtrip_constraints.snap index fbe935cab0..2c50e73489 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_constraints.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_constraints.snap @@ -10,19 +10,22 @@ expression: ast (import core.nat) -(import core.type) - (import core.nonlinear) +(import core.type) + (import core.fn) +(import core.title) + (declare-func private - array.replicate + _1 (param ?0 core.nat) (param ?1 core.type) (where (core.nonlinear ?1)) - (core.fn [?1] [(collections.array.array ?0 ?1)])) + (core.fn [?1] [(collections.array.array ?0 ?1)]) + (meta (core.title "array.replicate"))) (declare-func public diff --git a/hugr-core/tests/snapshots/model__roundtrip_entrypoint.snap b/hugr-core/tests/snapshots/model__roundtrip_entrypoint.snap index 040c10f508..d88842ca4d 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_entrypoint.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_entrypoint.snap @@ -19,7 +19,7 @@ expression: ast (import core.entrypoint) -(define-func private wrapper_dfg (core.fn [] []) +(define-func public wrapper_dfg (core.fn [] []) (dfg (signature (core.fn [] [])) (meta core.entrypoint))) (mod) diff --git a/hugr-core/tests/snapshots/model__roundtrip_loop.snap b/hugr-core/tests/snapshots/model__roundtrip_loop.snap index bdbb088e60..e6991516f3 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_loop.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_loop.snap @@ -14,7 +14,10 @@ expression: ast (import core.adt) -(define-func private example.loop (param ?0 core.type) (core.fn [?0] [?0]) +(import core.title) + +(define-func private _1 (param ?0 core.type) (core.fn [?0] [?0]) + (meta (core.title "example.loop")) (dfg [%0] [%1] (signature (core.fn [?0] [?0])) (tail-loop [%0] [%1] diff --git a/hugr-core/tests/snapshots/model__roundtrip_params.snap b/hugr-core/tests/snapshots/model__roundtrip_params.snap index eab6045e57..c5c5eac95b 100644 --- a/hugr-core/tests/snapshots/model__roundtrip_params.snap +++ b/hugr-core/tests/snapshots/model__roundtrip_params.snap @@ -6,20 +6,22 @@ expression: ast (mod) -(import core.bytes) - -(import core.nat) - (import core.call) (import core.type) +(import core.bytes) + +(import core.nat) + (import core.fn) (import core.str) (import core.float) +(import core.title) + (define-func public example.swap @@ -37,7 +39,8 @@ expression: ast (param ?3 core.float) (core.fn [] [])) -(define-func private example.call_literals (core.fn [] []) +(define-func private _5 (core.fn [] []) + (meta (core.title "example.call_literals")) (dfg (signature (core.fn [] [])) ((core.call diff --git a/hugr-model/src/v0/mod.rs b/hugr-model/src/v0/mod.rs index 16d3b67d12..c74201a910 100644 --- a/hugr-model/src/v0/mod.rs +++ b/hugr-model/src/v0/mod.rs @@ -322,6 +322,18 @@ pub const ORDER_HINT_OUTPUT_KEY: &str = "core.order_hint.output_key"; /// - **Result:** `core.meta` pub const ORDER_HINT_ORDER: &str = "core.order_hint.order"; +/// Metadata constructor for symbol titles. +/// +/// The names of functions in `hugr-core` are currently not used for symbol +/// resolution, but rather serve as a short description of the function. +/// As such, there is no requirement for uniqueness or formatting. +/// This metadata can be used to preserve that name when serializing through +/// `hugr-model`. +/// +/// - **Parameter:** `?title: core.str` +/// - **Result:** `core.meta` +pub const CORE_TITLE: &str = "core.title"; + pub mod ast; pub mod binary; pub mod scope; diff --git a/hugr-model/tests/fixtures/model-call.edn b/hugr-model/tests/fixtures/model-call.edn index 42b7fb24a8..8fa25feaf6 100644 --- a/hugr-model/tests/fixtures/model-call.edn +++ b/hugr-model/tests/fixtures/model-call.edn @@ -8,7 +8,7 @@ (meta (compat.meta_json "title" "\"Callee\"")) (meta (compat.meta_json "description" "\"This is a function declaration.\""))) -(define-func private example.caller +(define-func public example.caller (core.fn [arithmetic.int.types.int] [arithmetic.int.types.int]) (meta (compat.meta_json "title" "\"Caller\"")) (meta (compat.meta_json "description" "\"This defines a function that calls the function which we declared earlier.\"")) @@ -17,7 +17,7 @@ ((core.call _ _ example.callee) [%3] [%4] (signature (core.fn [arithmetic.int.types.int] [arithmetic.int.types.int]))))) -(define-func private +(define-func public example.load (core.fn [] [(core.fn [arithmetic.int.types.int] [arithmetic.int.types.int])]) (dfg diff --git a/hugr-model/tests/fixtures/model-cfg.edn b/hugr-model/tests/fixtures/model-cfg.edn index 14a9a610fa..bfce67854b 100644 --- a/hugr-model/tests/fixtures/model-cfg.edn +++ b/hugr-model/tests/fixtures/model-cfg.edn @@ -2,7 +2,7 @@ (mod) -(define-func private example.cfg_loop +(define-func public example.cfg_loop (param ?a core.type) (core.fn [?a] [?a]) (dfg [%0] [%1] diff --git a/hugr-model/tests/fixtures/model-cond.edn b/hugr-model/tests/fixtures/model-cond.edn index 05905c1a5e..9f49446d6d 100644 --- a/hugr-model/tests/fixtures/model-cond.edn +++ b/hugr-model/tests/fixtures/model-cond.edn @@ -2,7 +2,7 @@ (mod) -(define-func private +(define-func public example.cond (core.fn [(core.adt [[] []]) (arithmetic.int.types.int 6)] diff --git a/hugr-model/tests/fixtures/model-entrypoint.edn b/hugr-model/tests/fixtures/model-entrypoint.edn index f75d8f7a5c..fb70d10309 100644 --- a/hugr-model/tests/fixtures/model-entrypoint.edn +++ b/hugr-model/tests/fixtures/model-entrypoint.edn @@ -10,7 +10,7 @@ (mod) -(define-func private wrapper_dfg +(define-func public wrapper_dfg (core.fn [] []) (dfg [] [] (signature (core.fn [] [])) diff --git a/hugr-py/src/hugr/model/export.py b/hugr-py/src/hugr/model/export.py index cde0c21dba..3f99519f5b 100644 --- a/hugr-py/src/hugr/model/export.py +++ b/hugr-py/src/hugr/model/export.py @@ -150,21 +150,28 @@ def export_node( ) case FuncDefn() as op: - name = _mangle_name(node, op.f_name) + name = _mangle_name(node, op.f_name, op.visibility) symbol = self.export_symbol( name, op.visibility, op.signature.params, op.signature.body ) region = self.export_region_dfg(node) + if op.visibility == "Private": + meta.append(model.Apply("core.title", [model.Literal(op.f_name)])) + return model.Node( operation=model.DefineFunc(symbol), regions=[region], meta=meta ) case FuncDecl() as op: - name = _mangle_name(node, op.f_name) + name = _mangle_name(node, op.f_name, op.visibility) symbol = self.export_symbol( name, op.visibility, op.signature.params, op.signature.body ) + + if op.visibility == "Private": + meta.append(model.Apply("core.title", [model.Literal(op.f_name)])) + return model.Node(operation=model.DeclareFunc(symbol), meta=meta) case AliasDecl() as op: @@ -584,7 +591,7 @@ def export_symbol( ) def find_func_input(self, node: Node) -> str | None: - """Find the name of the function that a node is connected to, if any.""" + """Find the symbol name of the function that a node is connected to, if any.""" try: func_node = next( out_port.node @@ -598,12 +605,14 @@ def find_func_input(self, node: Node) -> str | None: match self.hugr[func_node].op: case FuncDecl() as func_op: name = func_op.f_name + visibility = func_op.visibility case FuncDefn() as func_op: name = func_op.f_name + visibility = func_op.visibility case _: return None - return _mangle_name(func_node, name) + return _mangle_name(func_node, name, visibility) def find_const_input(self, node: Node) -> model.Term | None: """Find and export the constant that a node is connected to, if any.""" @@ -624,10 +633,17 @@ def find_const_input(self, node: Node) -> model.Term | None: return None -def _mangle_name(node: Node, name: str) -> str: - # Until we come to an agreement on the uniqueness of names, we mangle the names - # by adding the node id. - return f"_{name}_{node.idx}" +def _mangle_name(node: Node, name: str, visibility: Visibility) -> str: + match visibility: + case "Private": + # Until we come to an agreement on the uniqueness of names, + # we mangle the names by replacing id with the node id. + return f"_{node.idx}" + case "Public": + return name + case _: + error = f"Unexpected visibility {visibility}" + raise ValueError(error) T = TypeVar("T")