From 41fa0ba95562c7d7208192f1bb36ae9f4b597cf4 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 9 Sep 2026 10:20:22 +0000 Subject: [PATCH] Keep the name placeholder out of the kernel entirely A name input declares where a placeholder's name is wired and nothing more. The name is spent resolving the layout when the graph compiles, so the kernel neither declares it nor is passed it; the return type's `Named` is the only tie between the write slot and the folded name. The wire is untouched: the input keeps its declared position and crosses as constant text, so a document's input order is unchanged. The placeholder stays a concrete token, so it adds no generic for the node to carry and none to go unconstrained. Co-Authored-By: Claude Fable 5 --- node-graph/node-macro/src/codegen.rs | 17 ++++++++----- node-graph/node-macro/src/codegen/ir.rs | 34 +++++++++++++++++++++++++ node-graph/nodes/graphic/src/graphic.rs | 4 +-- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 8bbdb6ca77..395257ff19 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -995,6 +995,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let output_type = &parsed.output_type; let raw_lazy = matches!(*model, Dialect::Poll); let injected_name = |ident: &Ident| async_source && (ident == "_runtime" || ident == "_source"); + // A name input declares where a placeholder's name is wired and nothing + // more: the name is spent resolving the layout when the graph compiles, so + // it reaches neither the kernel's parameters nor its call. The wire input + // stays, since the fold reads the constant off it. + let kernel_omits = + |field: &ParsedField| injected_name(&field.pat_ident.ident) || matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { name_source: Some(_), .. })); let where_predicates: Vec = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect(); let NodeFields { @@ -1103,7 +1109,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); quote!((#value_param #(, #read_pats)*): (#value_ty #(, #read_tys)*)) }; - let kernel_params = regular_fields.iter().enumerate().filter(|(_, field)| !injected_name(&field.pat_ident.ident)).map(|(index, field)| { + let kernel_params = regular_fields.iter().enumerate().filter(|(_, field)| !kernel_omits(field)).map(|(index, field)| { let pat = &field.pat_ident; match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, .. }) if ir::materialized_levels(&node, index) > 0 => { @@ -1532,7 +1538,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (!tokens.is_empty()).then_some(tokens) }; - let call_args = regular_fields.iter().enumerate().filter(|(_, field)| !injected_name(&field.pat_ident.ident)).map(|(index, field)| { + let call_args = regular_fields.iter().enumerate().filter(|(_, field)| !kernel_omits(field)).map(|(index, field)| { let name = &field.pat_ident.ident; match &field.ty { // A lend param binds an owned input; the kernel borrows the @@ -1732,7 +1738,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let fn_where = &parsed.where_clause; let body = if level_delta > 0 { rewrite_emit(&parsed.body) } else { parsed.body.clone() }; let vis = &parsed.vis; - let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect(); + let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !kernel_omits(field)).collect(); // A bare `Attr` in the return type cannot elide its lifetime, so the // kernel gets a fresh one; reference-valued writes name their real // lifetime explicitly and pass through untouched. An async source's value @@ -2016,12 +2022,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Some(tuple_arg(regular_fields[0], quote!(#core_types::record::ElToken))) } .into_iter(); - let value_args = regular_fields.iter().skip(if skips_carrier { 0 } else { 1 }).map(|field| { + let value_args = regular_fields.iter().skip(if skips_carrier { 0 } else { 1 }).filter(|field| !kernel_omits(field)).map(|field| { let name = &field.pat_ident.ident; match &field.ty { - // A name input's text is spent when the graph compiles, so the - // kernel takes the bare placeholder, not the wired string. - ParsedFieldType::Regular(RegularParsedField { name_source: Some(_), .. }) => quote!(::core::default::Default::default()), // A lend param binds an owned input; the kernel borrows the // evaluated value. ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => quote!(&#name), diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 3a2dab824d..2f47b1909e 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -859,6 +859,40 @@ mod tests { ); } + #[test] + fn a_name_input_is_omitted_from_the_kernel_but_kept_on_the_wire() { + let mut parsed = crate::parsing::parse_node_fn( + quote!(category("")), + quote!( + fn tag<'e, V: WireValue>(ctx: impl Ctx + ExtractArena<'e>, content: f64, name: Named, value: V) -> (f64, Attr<'e, Named>) { + (content, Attr(value)) + } + ), + ) + .unwrap(); + parsed.replace_impl_trait_in_input(); + let node = build(&parsed); + + // The name keeps its wired position, so a document's input order is + // untouched, while the kernel neither declares nor is passed it. + assert_eq!(node.inputs.len(), 3, "the wire keeps content, name and value"); + assert!(node.inputs[1].name_source.is_some(), "the name sits at its declared position"); + let named: Vec = node.inputs.iter().enumerate().filter(|(_, input)| input.name_source.is_some()).map(|(index, _)| index).collect(); + assert_eq!(named, vec![1], "exactly one input names a placeholder"); + + // A name input's wire type is plain text, which is what the fold reads. + let ParsedFieldType::Regular(RegularParsedField { ty, name_source, .. }) = &parsed.fields[1].ty else { + panic!("the name is a regular input"); + }; + assert!(name_source.is_some(), "the parameter declares a placeholder"); + assert_eq!(quote!(#ty).to_string(), "String", "the name crosses as constant text"); + + // The placeholder is a concrete token, so it adds no generic for the + // struct to carry and none to go unconstrained. + let generics: Vec = node.generics.iter().map(|generic| generic.ident.to_string()).collect(); + assert_eq!(generics, vec!["V".to_string()], "only the value generic rides the node"); + } + #[test] fn bridge_record_remove() { assert_bridge( diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 30bf2c50ae..58eb3e6a1b 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -379,8 +379,8 @@ pub fn stamp_layer_path<'e, T>(ctx: impl Ctx + ExtractArena<'e>, element: T, pat pub fn write_attribute<'e, T, V: WireValue>( ctx: impl Ctx + ExtractArena<'e>, content: T, - /// The attribute name, which the compiler folds and the kernel never reads. - _name: Named, + /// The attribute name, folded into the layout when the graph compiles. + name: Named, #[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec, String)] value: V, ) -> Result<(T, Attr<'e, Named>), Interrupt> { let parked = value.park(ctx.arena()).ok_or(GraphError {