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<X, V>` 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 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 10:20:22 +00:00
parent 37c0d0852d
commit fc94487d0b
3 changed files with 46 additions and 9 deletions

View File

@@ -980,6 +980,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<TokenStream2> = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect();
let NodeFields {
@@ -1088,7 +1094,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 => {
@@ -1517,7 +1523,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
@@ -1719,7 +1725,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
let vis = &parsed.vis;
// The input index rides along: an async kernel's param type depends on the
// field's value binding, which is indexed off the node's inputs.
let kernel_indexed: Vec<(usize, &&ParsedField)> = regular_fields.iter().enumerate().filter(|(_, field)| !injected_name(&field.pat_ident.ident)).collect();
let kernel_indexed: Vec<(usize, &&ParsedField)> = regular_fields.iter().enumerate().filter(|(_, field)| !kernel_omits(field)).collect();
let kernel_fields: Vec<&&ParsedField> = kernel_indexed.iter().map(|(_, field)| *field).collect();
// A bare `Attr<M>` in the return type cannot elide its lifetime, so the
// kernel gets a fresh one; reference-valued writes name their real
@@ -2010,12 +2016,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),

View File

@@ -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<Name0>, value: V) -> (f64, Attr<'e, Named<Name0, V::Row>>) {
(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<usize> = 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<String> = 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(

View File

@@ -376,8 +376,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<Name0>,
/// The attribute name, folded into the layout when the graph compiles.
name: Named<Name0>,
#[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec<NodeId>, String)] value: V,
) -> Result<(T, Attr<'e, Named<Name0, V::Row>>), Interrupt> {
let parked = value.park(ctx.arena()).ok_or(GraphError {