Skip to content

Commit 4bc7f65

Browse files
authored
feat: Names of private functions become core.title metadata. (#2448)
This PR changes the way that function names are exported/imported between `hugr-core`/`hugr-py` on the one side and `hugr-model` on the other. When a function is public, the exported `hugr-model` symbol name will match the `hugr-core` name. For private functions, the `hugr-model` exported symbol name will be `_{node_id}` and the `hugr-core` name is preserved via `core.title` metadata. On import, the `hugr-core` name is the title metadata if it is present; otherwise it is the `hugr-model` symbol name.
1 parent 3943499 commit 4bc7f65

15 files changed

+148
-43
lines changed

hugr-core/src/export.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Exporting HUGR graphs to their `hugr-model` representation.
2+
use crate::Visibility;
23
use crate::extension::ExtensionRegistry;
34
use crate::hugr::internal::HugrInternals;
45
use crate::types::type_param::Term;
@@ -20,13 +21,14 @@ use crate::{
2021
};
2122

2223
use fxhash::{FxBuildHasher, FxHashMap};
23-
use hugr_model::v0::Visibility;
24+
use hugr_model::v0::bumpalo;
2425
use hugr_model::v0::{
2526
self as model,
2627
bumpalo::{Bump, collections::String as BumpString, collections::Vec as BumpVec},
2728
table,
2829
};
2930
use petgraph::unionfind::UnionFind;
31+
use smol_str::ToSmolStr;
3032
use std::fmt::Write;
3133

3234
/// Exports a deconstructed `Package` to its representation in the model.
@@ -95,7 +97,7 @@ struct Context<'a> {
9597
// that ensures that the `node_to_id` and `id_to_node` maps stay in sync.
9698
}
9799

98-
const NO_VIS: Option<Visibility> = None;
100+
const NO_VIS: Option<model::Visibility> = None;
99101

100102
impl<'a> Context<'a> {
101103
pub fn new(hugr: &'a Hugr, bump: &'a Bump) -> Self {
@@ -261,8 +263,12 @@ impl<'a> Context<'a> {
261263

262264
// We record the name of the symbol defined by the node, if any.
263265
let symbol = match optype {
264-
OpType::FuncDefn(func_defn) => Some(func_defn.func_name().as_str()),
265-
OpType::FuncDecl(func_decl) => Some(func_decl.func_name().as_str()),
266+
OpType::FuncDefn(_) | OpType::FuncDecl(_) => {
267+
// Functions aren't exported using their core name but with a mangled
268+
// name derived from their id. The function's core name will be recorded
269+
// using `core.title` metadata.
270+
Some(self.mangled_name(node))
271+
}
266272
OpType::AliasDecl(alias_decl) => Some(alias_decl.name.as_str()),
267273
OpType::AliasDefn(alias_defn) => Some(alias_defn.name.as_str()),
268274
_ => None,
@@ -282,6 +288,7 @@ impl<'a> Context<'a> {
282288
// the node id. This is necessary to establish the correct node id for the
283289
// local scope introduced by some operations. We will overwrite this node later.
284290
let mut regions: &[_] = &[];
291+
let mut meta = Vec::new();
285292

286293
let node = self.id_to_node[&node_id];
287294
let optype = self.hugr.get_optype(node);
@@ -331,8 +338,10 @@ impl<'a> Context<'a> {
331338
}
332339

333340
OpType::FuncDefn(func) => self.with_local_scope(node_id, |this| {
341+
let symbol_name = this.export_func_name(node, &mut meta);
342+
334343
let symbol = this.export_poly_func_type(
335-
func.func_name(),
344+
symbol_name,
336345
Some(func.visibility().clone().into()),
337346
func.signature(),
338347
);
@@ -345,8 +354,10 @@ impl<'a> Context<'a> {
345354
}),
346355

347356
OpType::FuncDecl(func) => self.with_local_scope(node_id, |this| {
357+
let symbol_name = this.export_func_name(node, &mut meta);
358+
348359
let symbol = this.export_poly_func_type(
349-
func.func_name(),
360+
symbol_name,
350361
Some(func.visibility().clone().into()),
351362
func.signature(),
352363
);
@@ -502,12 +513,9 @@ impl<'a> Context<'a> {
502513
let inputs = self.make_ports(node, Direction::Incoming, num_inputs);
503514
let outputs = self.make_ports(node, Direction::Outgoing, num_outputs);
504515

505-
let meta = {
506-
let mut meta = Vec::new();
507-
self.export_node_json_metadata(node, &mut meta);
508-
self.export_node_order_metadata(node, &mut meta);
509-
self.bump.alloc_slice_copy(&meta)
510-
};
516+
self.export_node_json_metadata(node, &mut meta);
517+
self.export_node_order_metadata(node, &mut meta);
518+
let meta = self.bump.alloc_slice_copy(&meta);
511519

512520
self.module.nodes[node_id.index()] = table::Node {
513521
operation,
@@ -803,7 +811,7 @@ impl<'a> Context<'a> {
803811
pub fn export_poly_func_type<RV: MaybeRV>(
804812
&mut self,
805813
name: &'a str,
806-
visibility: Option<Visibility>,
814+
visibility: Option<model::Visibility>,
807815
t: &PolyFuncTypeBase<RV>,
808816
) -> &'a table::Symbol<'a> {
809817
let mut params = BumpVec::with_capacity_in(t.params().len(), self.bump);
@@ -1121,6 +1129,33 @@ impl<'a> Context<'a> {
11211129
}
11221130
}
11231131

1132+
/// Used when exporting function definitions or declarations. When the
1133+
/// function is public, its symbol name will be the core name. For private
1134+
/// functions, the symbol name is derived from the node id and the core name
1135+
/// is exported as `core.title` metadata.
1136+
///
1137+
/// This is a hack, necessary due to core names for functions being
1138+
/// non-functional. Once functions have a "link name", that should be used as the symbol name here.
1139+
fn export_func_name(&mut self, node: Node, meta: &mut Vec<table::TermId>) -> &'a str {
1140+
let (name, vis) = match self.hugr.get_optype(node) {
1141+
OpType::FuncDefn(func_defn) => (func_defn.func_name(), func_defn.visibility()),
1142+
OpType::FuncDecl(func_decl) => (func_decl.func_name(), func_decl.visibility()),
1143+
_ => panic!(
1144+
"`export_func_name` is only supposed to be used on function declarations and definitions"
1145+
),
1146+
};
1147+
1148+
match vis {
1149+
Visibility::Public => name,
1150+
Visibility::Private => {
1151+
let literal =
1152+
self.make_term(table::Term::Literal(model::Literal::Str(name.to_smolstr())));
1153+
meta.push(self.make_term_apply(model::CORE_TITLE, &[literal]));
1154+
self.mangled_name(node)
1155+
}
1156+
}
1157+
}
1158+
11241159
pub fn make_json_meta(&mut self, name: &str, value: &serde_json::Value) -> table::TermId {
11251160
let value = serde_json::to_string(value).expect("json values are always serializable");
11261161
let value = self.make_term(model::Literal::Str(value.into()).into());
@@ -1147,6 +1182,11 @@ impl<'a> Context<'a> {
11471182
let args = self.bump.alloc_slice_copy(args);
11481183
self.make_term(table::Term::Apply(symbol, args))
11491184
}
1185+
1186+
/// Creates a mangled name for a particular node.
1187+
fn mangled_name(&self, node: Node) -> &'a str {
1188+
bumpalo::format!(in &self.bump, "_{}", node.index()).into_bump_str()
1189+
}
11501190
}
11511191

11521192
type FxIndexSet<T> = indexmap::IndexSet<T, FxBuildHasher>;

hugr-core/src/import.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -970,8 +970,10 @@ impl<'a> Context<'a> {
970970
"No visibility for FuncDefn".to_string(),
971971
))?;
972972
self.import_poly_func_type(node_id, *symbol, |ctx, signature| {
973+
let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name);
974+
973975
let optype =
974-
OpType::FuncDefn(FuncDefn::new_vis(symbol.name, signature, visibility.into()));
976+
OpType::FuncDefn(FuncDefn::new_vis(func_name, signature, visibility.into()));
975977

976978
let node = ctx.make_node(node_id, optype, parent)?;
977979

@@ -999,8 +1001,10 @@ impl<'a> Context<'a> {
9991001
"No visibility for FuncDecl".to_string(),
10001002
))?;
10011003
self.import_poly_func_type(node_id, *symbol, |ctx, signature| {
1004+
let func_name = ctx.import_title_metadata(node_id)?.unwrap_or(symbol.name);
1005+
10021006
let optype =
1003-
OpType::FuncDecl(FuncDecl::new_vis(symbol.name, signature, visibility.into()));
1007+
OpType::FuncDecl(FuncDecl::new_vis(func_name, signature, visibility.into()));
10041008
let node = ctx.make_node(node_id, optype, parent)?;
10051009
Ok(node)
10061010
})
@@ -1817,6 +1821,30 @@ impl<'a> Context<'a> {
18171821
N
18181822
))
18191823
}
1824+
1825+
/// Searches for `core.title` metadata on the given node.
1826+
fn import_title_metadata(
1827+
&self,
1828+
node_id: table::NodeId,
1829+
) -> Result<Option<&'a str>, ImportError> {
1830+
let node_data = self.get_node(node_id)?;
1831+
for meta in node_data.meta {
1832+
let Some([name]) = self.match_symbol(*meta, model::CORE_TITLE)? else {
1833+
continue;
1834+
};
1835+
1836+
let table::Term::Literal(model::Literal::Str(name)) = self.get_term(name)? else {
1837+
return Err(error_invalid!(
1838+
"`{}` metadata expected a string literal as argument",
1839+
model::CORE_TITLE
1840+
));
1841+
};
1842+
1843+
return Ok(Some(name.as_str()));
1844+
}
1845+
1846+
Ok(None)
1847+
}
18201848
}
18211849

18221850
/// Information about a local variable.

hugr-core/tests/snapshots/model__roundtrip_call.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ expression: ast
2424
(meta (compat.meta_json "title" "\"Callee\"")))
2525

2626
(define-func
27-
private
27+
public
2828
example.caller
2929
(core.fn [arithmetic.int.types.int] [arithmetic.int.types.int])
3030
(meta
@@ -43,7 +43,7 @@ expression: ast
4343
(core.fn [arithmetic.int.types.int] [arithmetic.int.types.int])))))
4444

4545
(define-func
46-
private
46+
public
4747
example.load
4848
(core.fn [] [(core.fn [arithmetic.int.types.int] [arithmetic.int.types.int])])
4949
(dfg [] [%0]

hugr-core/tests/snapshots/model__roundtrip_cfg.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ expression: ast
1616

1717
(import core.adt)
1818

19-
(define-func private example.cfg_loop (param ?0 core.type) (core.fn [?0] [?0])
19+
(define-func public example.cfg_loop (param ?0 core.type) (core.fn [?0] [?0])
2020
(dfg [%0] [%1]
2121
(signature (core.fn [?0] [?0]))
2222
(cfg [%0] [%1]

hugr-core/tests/snapshots/model__roundtrip_cond.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ expression: ast
2525
"negation modulo 2^N (signed and unsigned versions are the same op)")))
2626

2727
(define-func
28-
private
28+
public
2929
example.cond
3030
(core.fn
3131
[(core.adt [[] []]) (arithmetic.int.types.int 6)]

hugr-core/tests/snapshots/model__roundtrip_constraints.snap

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ expression: ast
1010

1111
(import core.nat)
1212

13-
(import core.type)
14-
1513
(import core.nonlinear)
1614

15+
(import core.type)
16+
1717
(import core.fn)
1818

19+
(import core.title)
20+
1921
(declare-func
2022
private
21-
array.replicate
23+
_1
2224
(param ?0 core.nat)
2325
(param ?1 core.type)
2426
(where (core.nonlinear ?1))
25-
(core.fn [?1] [(collections.array.array ?0 ?1)]))
27+
(core.fn [?1] [(collections.array.array ?0 ?1)])
28+
(meta (core.title "array.replicate")))
2629

2730
(declare-func
2831
public

hugr-core/tests/snapshots/model__roundtrip_entrypoint.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ expression: ast
1919

2020
(import core.entrypoint)
2121

22-
(define-func private wrapper_dfg (core.fn [] [])
22+
(define-func public wrapper_dfg (core.fn [] [])
2323
(dfg (signature (core.fn [] [])) (meta core.entrypoint)))
2424

2525
(mod)

hugr-core/tests/snapshots/model__roundtrip_loop.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ expression: ast
1414

1515
(import core.adt)
1616

17-
(define-func private example.loop (param ?0 core.type) (core.fn [?0] [?0])
17+
(import core.title)
18+
19+
(define-func private _1 (param ?0 core.type) (core.fn [?0] [?0])
20+
(meta (core.title "example.loop"))
1821
(dfg [%0] [%1]
1922
(signature (core.fn [?0] [?0]))
2023
(tail-loop [%0] [%1]

hugr-core/tests/snapshots/model__roundtrip_params.snap

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ expression: ast
66

77
(mod)
88

9-
(import core.bytes)
10-
11-
(import core.nat)
12-
139
(import core.call)
1410

1511
(import core.type)
1612

13+
(import core.bytes)
14+
15+
(import core.nat)
16+
1717
(import core.fn)
1818

1919
(import core.str)
2020

2121
(import core.float)
2222

23+
(import core.title)
24+
2325
(define-func
2426
public
2527
example.swap
@@ -37,7 +39,8 @@ expression: ast
3739
(param ?3 core.float)
3840
(core.fn [] []))
3941

40-
(define-func private example.call_literals (core.fn [] [])
42+
(define-func private _5 (core.fn [] [])
43+
(meta (core.title "example.call_literals"))
4144
(dfg
4245
(signature (core.fn [] []))
4346
((core.call

hugr-model/src/v0/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,18 @@ pub const ORDER_HINT_OUTPUT_KEY: &str = "core.order_hint.output_key";
322322
/// - **Result:** `core.meta`
323323
pub const ORDER_HINT_ORDER: &str = "core.order_hint.order";
324324

325+
/// Metadata constructor for symbol titles.
326+
///
327+
/// The names of functions in `hugr-core` are currently not used for symbol
328+
/// resolution, but rather serve as a short description of the function.
329+
/// As such, there is no requirement for uniqueness or formatting.
330+
/// This metadata can be used to preserve that name when serializing through
331+
/// `hugr-model`.
332+
///
333+
/// - **Parameter:** `?title: core.str`
334+
/// - **Result:** `core.meta`
335+
pub const CORE_TITLE: &str = "core.title";
336+
325337
pub mod ast;
326338
pub mod binary;
327339
pub mod scope;

0 commit comments

Comments
 (0)