From d1d50ec469be81aee0975b504ace589df692725d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Tue, 18 Aug 2026 11:02:25 +0000 Subject: [PATCH] Classify routing sources through IList nesting --- node-graph/node-macro/src/codegen.rs | 4 ++-- node-graph/node-macro/src/codegen/classify.rs | 16 ++++++++++++---- node-graph/node-macro/src/codegen/ir.rs | 4 ++-- node-graph/node-macro/src/codegen/metadata.rs | 3 ++- node-graph/node-macro/src/validation.rs | 3 ++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 9ca1f37389..5c2838a6ee 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -867,7 +867,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, }; - if matches!((&routing_generic, source_ty), (Some(generic), Type::Path(path)) if path.path.get_ident() == Some(generic)) { + if routing_generic.as_ref().is_some_and(|generic| crate::codegen::classify::routing_source_output(source_ty, generic)) { let source_generic = format_ident!("__Source{index}"); generics.push(quote! { #source_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>> @@ -922,7 +922,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn false => quote!(#core_types::node::Node<#ctx_ident, Output = #output_type>), }; - let routing_source = |ty: &Type| matches!((&routing_generic, ty), (Some(generic), Type::Path(path)) if path.path.get_ident() == Some(generic)); + let routing_source = |ty: &Type| routing_generic.as_ref().is_some_and(|generic| crate::codegen::classify::routing_source_output(ty, generic)); let lazy_read_out = |field: &ParsedField, output_type: &Type| { let attr_tys = field.attribute_reads.iter().map(|read| { diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs index 885d8982df..d18200f9b2 100644 --- a/node-graph/node-macro/src/codegen/classify.rs +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -110,14 +110,15 @@ pub(crate) fn has_lazy_reads(parsed: &ParsedNodeFn) -> bool { .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_))) } -/// The value inputs of a routing node (every regular field that is not a -/// routing source), with their indices into the regular fields. +/// The value inputs of a routing node (every regular field that is neither a +/// routing source nor a ranked whole-list input), with their indices into the +/// regular fields. pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], generic: &Ident) -> Vec { regular_fields .iter() .enumerate() .filter(|(_, field)| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(generic)), + ParsedFieldType::Regular(RegularParsedField { ty, list_levels, .. }) => *list_levels == 0 && !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(generic)), ParsedFieldType::Node(_) => false, }) .map(|(index, _)| index) @@ -514,7 +515,7 @@ pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { input_type, implementations, }) => { - if bare_ident(output_type) == Some(&ident) { + if routing_source_output(output_type, &ident) { // A source forwards its whole record opaquely; declared // reads contradict that and are rejected by validation. if !implementations.is_empty() || type_contains_ident(input_type, &ident) || !field.attribute_reads.is_empty() { @@ -545,6 +546,13 @@ pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> { path.path.get_ident() } +/// Whether a lazy input's declared output is the routing generic, at any +/// `IList` nesting: the nesting is rank depth, not a distinct row type. +pub(crate) fn routing_source_output(output_type: &Type, generic: &Ident) -> bool { + let (stripped, _) = crate::codegen::ir::strip_ilist(output_type); + bare_ident(&stripped) == Some(generic) +} + pub(crate) fn tokens_contain_ident(tokens: TokenStream2, ident: &Ident) -> bool { tokens.into_iter().any(|token| match token { proc_macro2::TokenTree::Ident(candidate) => &candidate == ident, diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index 5a06a5550b..a66bccbcbd 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -77,7 +77,7 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) -> fn subject(index: usize, field: &ParsedField, carrier_subject: bool, routing: Option<&RoutingIo>) -> bool { match &field.ty { ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { - is_record_value(output_type) || routing.is_some_and(|routing| bare_ident(output_type) == Some(&routing.generic)) || (index == 0 && carrier_subject) + is_record_value(output_type) || routing.is_some_and(|routing| crate::codegen::classify::routing_source_output(output_type, &routing.generic)) || (index == 0 && carrier_subject) } ParsedFieldType::Regular(RegularParsedField { ty, .. }) => routing.is_some_and(|routing| bare_ident(ty) == Some(&routing.generic)) || (index == 0 && carrier_subject), } @@ -653,7 +653,7 @@ mod tests { let carrier_flip = flip && flip_carrier(parsed); let derives = ctx_derives(parsed); let generic = routing_generic(parsed); - let routing_source = |ty: &Type| generic.as_ref().is_some_and(|generic| bare_ident(ty) == Some(generic)); + let routing_source = |ty: &Type| generic.as_ref().is_some_and(|generic| crate::codegen::classify::routing_source_output(ty, generic)); match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => { if record && !skips_carrier && index == 0 { diff --git a/node-graph/node-macro/src/codegen/metadata.rs b/node-graph/node-macro/src/codegen/metadata.rs index 67d04834f5..b02aa925f8 100644 --- a/node-graph/node-macro/src/codegen/metadata.rs +++ b/node-graph/node-macro/src/codegen/metadata.rs @@ -16,9 +16,10 @@ pub(crate) fn generate_node_input_references( let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics); for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() { + // `IList` nesting is rank metadata, not part of the value type. let mut ty = match &parsed_input.ty { ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => crate::codegen::ir::strip_ilist(output_type).0, }; // We only want the necessary generics. diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index ca472d36ed..a62cfe0bac 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -437,7 +437,8 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) { _ => None, }; let opaque_record_generic = |ty: &Type| { - let ident = match ty { + let (stripped, _) = crate::codegen::ir::strip_ilist(ty); + let ident = match &stripped { Type::Path(path) => path.path.get_ident(), _ => None, };