diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index c32cd11c0a..69fb62b9c9 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -42,12 +42,10 @@ pub const ATTR_LETTER_TILT: &str = crate::attribute::LetterTilt::NAME; // Implicit attribute defaults // =========================== -/// Overrides the type's default value for certain attributes. +/// The census-declared default for `key`; a mismatched value type degrades +/// to the column type's own default in `push_repeated`. fn implicit_default_value(key: &str) -> Option> { - match key { - ATTR_OPACITY | ATTR_OPACITY_FILL => Some(Box::new(1_f64)), - _ => None, - } + crate::attribute::ATTRIBUTE_REGISTRY.lock().unwrap().get(key).map(|info| (info.default)()) } /// Appends `count` copies of `key`'s implicit default to `attribute` (see [`implicit_default_value`]). diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index f66ee6f0a7..426c7c1e53 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1,5 +1,6 @@ use crate::crate_ident::CrateIdent; use crate::parsing::*; +use crate::shader_nodes::{ShaderCodegen, ShaderTokens}; use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, format_ident, quote}; @@ -8,7 +9,6 @@ use syn::punctuated::Punctuated; use syn::visit::Visit; use syn::visit_mut::VisitMut; use syn::{GenericArgument, GenericParam, Ident, Lifetime, PatIdent, PathArguments, Type, TypeParam, TypeParamBound}; -use crate::shader_nodes::{ShaderCodegen, ShaderTokens}; pub(crate) mod classify; mod entries; @@ -124,9 +124,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let carried_generics: Vec<&syn::GenericParam> = fn_generics .iter() .filter(|param| match param { - syn::GenericParam::Type(tp) => { - Some(&tp.ident) != ctx_ident_for_flip.as_ref() && !data_field_generic_idents.contains(&tp.ident) && (flip || ranked_carries(&tp.ident)) - } + syn::GenericParam::Type(tp) => Some(&tp.ident) != ctx_ident_for_flip.as_ref() && !data_field_generic_idents.contains(&tp.ident) && (flip || ranked_carries(&tp.ident)), _ => false, }) .collect(); @@ -202,6 +200,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let slot = format_ident!("__in_{index}"); quote!(pub(super) #slot: gcore::record::Layout) })); + state.extend(crate::codegen::ir::element_lazy_indices(&struct_regular_fields, &node).into_iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(pub(super) #slot: gcore::record::Layout) + })); let total_reads: usize = struct_regular_fields.iter().map(|field| field.attribute_reads.len()).sum(); state.extend((0..total_reads).map(|index| { let slot = format_ident!("__read_{index}"); @@ -214,10 +216,14 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn state } else if routing_generic.is_some() { let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)]; - state.extend(routing_value_indices(&struct_regular_fields, routing_generic.as_ref().expect("guarded by the arm")).into_iter().map(|index| { - let slot = format_ident!("__in_{index}"); - quote!(pub(super) #slot: gcore::record::Layout) - })); + state.extend( + routing_value_indices(&struct_regular_fields, routing_generic.as_ref().expect("guarded by the arm")) + .into_iter() + .map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(pub(super) #slot: gcore::record::Layout) + }), + ); state } else if opaque { vec![quote!(pub(super) __layout: gcore::record::Layout)] @@ -398,10 +404,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // offsets from the carrier layout; `new` cannot fill that state. let routing_layout_param = (routing_generic.is_some() || opaque).then(|| quote!(__layout: &gcore::record::Layout,)).into_iter(); let routing_layout_init = (routing_generic.is_some() || opaque).then(|| quote!(__layout: __layout.clone(),)).into_iter(); - let routing_value_layouts: Vec = routing_generic - .as_ref() - .map(|generic| routing_value_indices(&struct_regular_fields, generic)) - .unwrap_or_default(); + let routing_value_layouts: Vec = routing_generic.as_ref().map(|generic| routing_value_indices(&struct_regular_fields, generic)).unwrap_or_default(); let routing_in_params = routing_value_layouts.iter().map(|index| { let slot = format_ident!("__in_{index}"); quote!(#slot: &gcore::record::Layout,) @@ -854,9 +857,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .fn_generics .iter() .filter(|param| match param { - GenericParam::Type(type_param) => { - Some(&type_param.ident) != routing_generic.as_ref() && Some(&type_param.ident) != record_token.as_ref() - } + GenericParam::Type(type_param) => Some(&type_param.ident) != routing_generic.as_ref() && Some(&type_param.ident) != record_token.as_ref(), _ => true, }) .map(&generic_tokens) @@ -932,6 +933,24 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn generics.insert(0, quote!('__record)); } } + if record_io { + let mut kernel_lazy = false; + for (index, field) in regular_fields.iter().enumerate() { + if matches!(&field.ty, ParsedFieldType::Node(_)) && matches!(crate::codegen::ir::lazy_binding(&node, index), LazyBinding::Element) { + kernel_lazy = true; + let source_generic = format_ident!("__Source{index}"); + let derived_extra = derives + .then(|| quote!(+ for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>)) + .into_iter(); + generics.push(quote! { + #source_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::record::RecordValue<'__record>> #(#derived_extra)* + }); + } + } + if kernel_lazy && !(derive_routing || (lazy_carrier && derives)) { + generics.insert(0, quote!('__record)); + } + } if opaque { for (index, field) in regular_fields.iter().enumerate() { if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty { @@ -975,48 +994,44 @@ 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 pat = &field.pat_ident; - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) if ir::materialized_levels(&node, index) > 0 => { - quote!(#pat: #core_types::node::List<'_, #ty>) - } - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { - let source_generic = format_ident!("__Source{index}"); - match (ir::lazy_binding(&node, index), raw_lazy) { - (LazyBinding::DeriveRouting, _) => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>), - (LazyBinding::DeriveCarrier, _) => { - let out = lazy_read_out(field, output_type); - quote!(#pat: #core_types::record::DerivedLazyInput<'_, '__record, #out, #source_generic>) - } - (LazyBinding::OpaqueRecord, _) => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>), - (LazyBinding::Element, true) => { - let out = lazy_read_out(field, output_type); - quote!(#pat: &#core_types::record::ElementEdge<'_, #out, #source_generic>) - } - (LazyBinding::Element, false) => { - let out = lazy_read_out(field, output_type); - quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>) - } - (LazyBinding::Plain, true) => { - let bound = lazy_bound(output_type); - quote!(#pat: &impl #bound) - } - (LazyBinding::Plain, false) => { - let bound = lazy_bound(output_type); - quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) - } + let kernel_params = regular_fields.iter().enumerate().filter(|(_, field)| !injected_name(&field.pat_ident.ident)).map(|(index, field)| { + let pat = &field.pat_ident; + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if ir::materialized_levels(&node, index) > 0 => { + quote!(#pat: #core_types::node::List<'_, #ty>) + } + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + let source_generic = format_ident!("__Source{index}"); + match (ir::lazy_binding(&node, index), raw_lazy) { + (LazyBinding::DeriveRouting, _) => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>), + (LazyBinding::DeriveCarrier, _) => { + let out = lazy_read_out(field, output_type); + quote!(#pat: #core_types::record::DerivedLazyInput<'_, '__record, #out, #source_generic>) + } + (LazyBinding::OpaqueRecord, _) => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>), + (LazyBinding::Element, true) => { + let out = lazy_read_out(field, output_type); + quote!(#pat: &#core_types::record::ElementEdge<'_, #out, #source_generic>) + } + (LazyBinding::Element, false) => { + let out = lazy_read_out(field, output_type); + quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>) + } + (LazyBinding::Plain, true) => { + let bound = lazy_bound(output_type); + quote!(#pat: &impl #bound) + } + (LazyBinding::Plain, false) => { + let bound = lazy_bound(output_type); + quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) } } } - }); + } + }); let record_value_ty: Type = syn::parse_quote!(#core_types::record::RecordValue<'__record>); let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| match &field.ty { @@ -1041,6 +1056,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Regular(_) if record_io && !field.attribute_reads.is_empty() => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } + // An element-consuming lazy secondary rides a record edge, derivable + // when the kernel evaluates it at derived contexts. + ParsedFieldType::Node(_) if record_io && matches!(ir::lazy_binding(&node, index), LazyBinding::Element) => match derives { + true => quote! { + #node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>, + #node_generic: for<'__derived> #core_types::record::RecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>> + }, + false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>), + }, ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } @@ -2027,8 +2051,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .enumerate() .filter(|(index, field)| { matches!(field.ty, ParsedFieldType::Regular(_)) - && hoists(*index) - && matches!(ir::value_binding(&node, *index), ValueBinding::Plain | ValueBinding::ReadingSecondary | ValueBinding::RecordElement) + && hoists(*index) && matches!(ir::value_binding(&node, *index), ValueBinding::Plain | ValueBinding::ReadingSecondary | ValueBinding::RecordElement) }) .map(|(_, field)| { let name = &field.pat_ident.ident; @@ -2128,10 +2151,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // A reading secondary input's element copies out of its record, as // does a concrete carrier read. if record_io { - bounds.extend(reading_secondary_indices(®ular_fields, skips_carrier).into_iter().filter_map(|index| match ®ular_fields[index].ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), - _ => None, - })); + bounds.extend( + reading_secondary_indices(®ular_fields, skips_carrier) + .into_iter() + .filter_map(|index| match ®ular_fields[index].ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), + _ => None, + }), + ); if let Some(ty) = carrier_read_ty { bounds.push(quote!(#ty: ::core::clone::Clone)); } @@ -2289,12 +2316,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }; let reading_secondaries = reading_secondary_indices(®ular_fields, skips_carrier); + // The layout slots the constructor fills: reading secondaries plus the + // element-consuming lazy inputs, in field order to match the entries. + let layout_slots: Vec = { + let mut slots = reading_secondaries.clone(); + slots.extend(crate::codegen::ir::element_lazy_indices(®ular_fields, &node)); + slots.sort_unstable(); + slots + }; let edge_args = regular_fields.iter().zip(&node_generics).map(|(field, generic)| { let name = &field.pat_ident.ident; quote!(#name: #generic) }); let carrier_layout_param = (!skips_carrier).then(|| quote!(__carrier_layout: &#core_types::record::Layout,)).into_iter(); - let input_layout_params = reading_secondaries.iter().map(|index| { + let input_layout_params = layout_slots.iter().map(|index| { let slot = format_ident!("__in_{index}"); quote!(#slot: &#core_types::record::Layout,) }); @@ -2313,7 +2348,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#name,) }); let carrier_init = (!skips_carrier).then(|| quote!(__carrier: __carrier_layout.clone(),)).into_iter(); - let input_layout_inits = reading_secondaries.iter().map(|index| { + let input_layout_inits = layout_slots.iter().map(|index| { let slot = format_ident!("__in_{index}"); quote!(#slot: #slot.clone(),) }); diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs index d18200f9b2..e8bea27abb 100644 --- a/node-graph/node-macro/src/codegen/classify.rs +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -96,18 +96,12 @@ pub(crate) enum EvalStep<'a> { /// or return-tuple writes. Reads on lazy inputs belong to the record lowering /// of the flip class instead. pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool { - let value_reads = parsed - .fields - .iter() - .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); + let value_reads = parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); value_reads || record_writes(&slot_value_type(&parsed.output_type)).is_some() } pub(crate) fn has_lazy_reads(parsed: &ParsedNodeFn) -> bool { - parsed - .fields - .iter() - .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_))) + parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_))) } /// The value inputs of a routing node (every regular field that is neither a @@ -266,10 +260,7 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { _ => return None, }; let writes = record_writes(&value); - let has_reads = parsed - .fields - .iter() - .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); + let has_reads = parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); if !has_reads && writes.is_none() { return None; } @@ -283,13 +274,17 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { // A first-field lazy carrier: the kernel evaluates the derived content // itself and returns its opaque row token beside the write set. let lazy_carrier = matches!(&carrier_field.ty, ParsedFieldType::Node(_)); - if parsed.fields.iter().skip(lazy_carrier as usize).any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { + // Lazy secondaries are consumed as plain elements; raw record edges and + // ranked outputs have no element binding here. + let unsupported_lazy_secondary = |field: &ParsedField| match &field.ty { + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(), + ParsedFieldType::Regular(_) => false, + }; + if parsed.fields.iter().skip(lazy_carrier as usize).any(|field| unsupported_lazy_secondary(field)) { return None; } let reads_well_placed = parsed.fields.iter().enumerate().all(|(index, field)| { - field.attribute_reads.is_empty() - || (lazy_carrier && index == 0) - || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) + field.attribute_reads.is_empty() || (lazy_carrier && index == 0) || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) }); if !reads_well_placed { return None; diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs index bb913bb7e5..1825c97285 100644 --- a/node-graph/node-macro/src/codegen/entries.rs +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -227,6 +227,13 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields }; } match &field.ty { + // An element-consuming lazy secondary of a record node rides a + // record edge with a layout slot, like a reading secondary. + ParsedFieldType::Node(NodeParsedField { output_type, .. }) + if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && matches!(ir::lazy_binding(&node, index), ir::LazyBinding::Element) => + { + SlotKind::Value(output_type.clone()) + } ParsedFieldType::Node(NodeParsedField { output_type, .. }) => SlotKind::Lazy(output_type.clone()), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) { ir::ValueBinding::Materialized => SlotKind::Ranked(ty.clone()), @@ -261,9 +268,7 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields .collect(); let ranked_source = |generic: &Ident| { regular_fields.iter().position(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, list_levels, implementations, .. }) => { - *list_levels > 0 && !implementations.is_empty() && generic_extractable(ty, generic) - } + ParsedFieldType::Regular(RegularParsedField { ty, list_levels, implementations, .. }) => *list_levels > 0 && !implementations.is_empty() && generic_extractable(ty, generic), _ => false, }) }; @@ -297,166 +302,171 @@ fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields let arity = regular_fields.len(); let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); - let entries: Vec = row_assignments.iter().filter_map(|assignments| { - // A row whose assignments did not all solve cannot instantiate the struct. - if assignments.len() != carried.len() { - return None; - } - let slots: Vec = slots + let entries: Vec = row_assignments .iter() - .map(|slot| match slot { - SlotKind::BaseGeneric(name) => SlotKind::BaseGeneric(name.clone()), - SlotKind::BaseConcrete(ty) => SlotKind::BaseConcrete(substitute_ident_types(ty, assignments)), - SlotKind::Value(ty) => SlotKind::Value(substitute_ident_types(ty, assignments)), - SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_ident_types(ty, assignments)), - SlotKind::Ranked(ty) => SlotKind::Ranked(substitute_ident_types(ty, assignments)), - SlotKind::Plain(ty) => SlotKind::Plain(substitute_ident_types(ty, assignments)), - SlotKind::Lazy(ty) => SlotKind::Lazy(substitute_ident_types(ty, assignments)), - }) - .collect(); + .filter_map(|assignments| { + // A row whose assignments did not all solve cannot instantiate the struct. + if assignments.len() != carried.len() { + return None; + } + let slots: Vec = slots + .iter() + .map(|slot| match slot { + SlotKind::BaseGeneric(name) => SlotKind::BaseGeneric(name.clone()), + SlotKind::BaseConcrete(ty) => SlotKind::BaseConcrete(substitute_ident_types(ty, assignments)), + SlotKind::Value(ty) => SlotKind::Value(substitute_ident_types(ty, assignments)), + SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_ident_types(ty, assignments)), + SlotKind::Ranked(ty) => SlotKind::Ranked(substitute_ident_types(ty, assignments)), + SlotKind::Plain(ty) => SlotKind::Plain(substitute_ident_types(ty, assignments)), + SlotKind::Lazy(ty) => SlotKind::Lazy(substitute_ident_types(ty, assignments)), + }) + .collect(); - // Every non-base value/plain/lazy input must be concrete. - let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot { - SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true, - SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => { - !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)) - } - }); - if !values_concrete { - return None; - } - - let input_types = slots.iter().map(|slot| match slot { - SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_edge_type(#name)), - SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => quote!(gcore::registry::record_edge_type::<#ty>()), - SlotKind::Plain(ty) | SlotKind::Lazy(ty) => quote!(gcore::registry::edge_type::<#ty>()), - }); - - let downcasts = names.iter().zip(&slots).enumerate().map(|(index, (name, slot))| { - let handle = format_ident!("__handle_{index}"); - let layout = format_ident!("__layout_{index}"); - let ty = format_ident!("__ty_{index}"); - match slot { - SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => quote! { - let #handle = inputs.next().unwrap(); - let #ty = #handle.ty().clone(); - let #layout = #handle.layout().clone(); - let #name = #handle.downcast_erased::(#ty.clone())?; - }, - SlotKind::Value(value_ty) => quote! { - let #handle = inputs.next().unwrap(); - let #layout = #handle.layout().clone(); - let #name = #handle.downcast_record::<#value_ty>()?; - }, - SlotKind::Extracted(value_ty) => quote! { - let #handle = inputs.next().unwrap(); - let #layout = #handle.layout().clone(); - let #name = gcore::record::RecordExtract::<#value_ty, _>::new(#handle.downcast_record::<#value_ty>()?, &#layout); - }, - SlotKind::Ranked(value_ty) => quote! { - let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?; - }, - SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;), - } - }); - - let base_indices: Vec = slots.iter().enumerate().filter(|(_, slot)| slot.is_base()).map(|(index, _)| index).collect(); - let value_indices: Vec = slots.iter().enumerate().filter(|(_, slot)| matches!(slot, SlotKind::Value(_))).map(|(index, _)| index).collect(); - let value_layout_args: Vec = value_indices - .iter() - .map(|index| { - let layout = format_ident!("__layout_{index}"); - quote!(&#layout,) - }) - .collect(); - - let carried_meta = || { - let meta = ir::layout_meta_tokens(&node, quote!(gcore::record::ElementSpec::Carried), &core_types); - quote!(Some(#meta)) - }; - - // The output wire and node wrap follow the output element: a concrete (or - // row-assigned) element is a typed record; a generic or opaque element is - // an erased record carrying the first base slot's runtime type. - let output_element = match &node.output.shape.element { - ir::Element::Concrete(element) => Some(substitute_ident_types(element, assignments)), - ir::Element::Generic(ident) => assignments.iter().find(|(generic, _)| generic == ident).map(|(_, ty)| ty.clone()), - ir::Element::Opaque => None, - }; - let (io_output, wrap) = match &output_element { - Some(element) => ( - quote!(gcore::registry::record_type::<#element>()), - quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))), - ), - None => { - let name = match &node.output.shape.element { - ir::Element::Generic(ident) => ident.to_string(), - _ => "T".to_string(), - }; - let base_ty = format_ident!("__ty_{}", base_indices[0]); - ( - quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#name))))), - quote!(Ok(gcore::registry::EdgeHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc, #base_ty))), - ) - } - }; - - let (prelude, new_layout_args, layout_meta) = match ir::node_kind(&node) { - ir::NodeKind::RecordIo => { - let carrier_arg = (node.inputs.first().is_some_and(|input| input.subject) && ir::materialized_levels(&node, 0) == 0).then(|| quote!(&__layout_0,)); - let layout_meta_fn = format_ident!("{}_layout_meta", fn_name); - (quote!(), quote!(#carrier_arg #(#value_layout_args)*), quote!(Some(self::#layout_meta_fn()))) - } - ir::NodeKind::Routing => { - let source_layouts = base_indices.iter().map(|index| format_ident!("__layout_{index}")); - let source_wraps = base_indices.iter().map(|index| { - let name = names[*index]; - let layout = format_ident!("__layout_{index}"); - quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);) - }); - let prelude = quote! { - let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); - #(#source_wraps)* - }; - (prelude, quote!(&__union, #(#value_layout_args)*), carried_meta()) - } - ir::NodeKind::Opaque => { - let record_layout = format_ident!("__layout_{}", base_indices[0]); - (quote!(), quote!(&#record_layout), carried_meta()) - } - ir::NodeKind::Flip => unreachable!("flip has its own multi-row emitter"), - }; - - // A carried generic instantiates through the struct's trailing phantom - // parameters, so the constructor names the row's types after one inferred - // slot per input field. - let turbofish = (!carried.is_empty()).then(|| { - let underscores = (0..arity).map(|_| quote!(_)); - let carried_types = carried.iter().filter_map(|(generic, _)| assignments.iter().find(|(ident, _)| ident == generic).map(|(_, ty)| quote!(#ty))); - quote!(::<#(#underscores,)* #(#carried_types,)*>) - }); - - Some(quote! { - gcore::registry::RegistryEntry { - layout_meta: #layout_meta, - io: gcore::registry::NodeIOTypes::new( - gcore::concrete!(gcore::context::ContextImpl<'static>), - #io_output, - vec![#(#input_types),*], - ), - constructor: |inputs| { - if inputs.len() != #arity { - return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() }); + // Every non-base value/plain/lazy input must be concrete. + let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot { + SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true, + SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) | SlotKind::Plain(ty) | SlotKind::Lazy(ty) => { + !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)) } - let mut inputs = inputs.into_iter(); - #(#downcasts)* - #prelude - let __node = #struct_name #turbofish::new(#(#names,)* #new_layout_args); - #wrap - }, - } - }) - }).collect(); + }); + if !values_concrete { + return None; + } + + let input_types = slots.iter().map(|slot| match slot { + SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_edge_type(#name)), + SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => quote!(gcore::registry::record_edge_type::<#ty>()), + SlotKind::Plain(ty) | SlotKind::Lazy(ty) => quote!(gcore::registry::edge_type::<#ty>()), + }); + + let downcasts = names.iter().zip(&slots).enumerate().map(|(index, (name, slot))| { + let handle = format_ident!("__handle_{index}"); + let layout = format_ident!("__layout_{index}"); + let ty = format_ident!("__ty_{index}"); + match slot { + SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => quote! { + let #handle = inputs.next().unwrap(); + let #ty = #handle.ty().clone(); + let #layout = #handle.layout().clone(); + let #name = #handle.downcast_erased::(#ty.clone())?; + }, + SlotKind::Value(value_ty) => quote! { + let #handle = inputs.next().unwrap(); + let #layout = #handle.layout().clone(); + let #name = #handle.downcast_record::<#value_ty>()?; + }, + SlotKind::Extracted(value_ty) => quote! { + let #handle = inputs.next().unwrap(); + let #layout = #handle.layout().clone(); + let #name = gcore::record::RecordExtract::<#value_ty, _>::new(#handle.downcast_record::<#value_ty>()?, &#layout); + }, + SlotKind::Ranked(value_ty) => quote! { + let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?; + }, + SlotKind::Plain(value_ty) | SlotKind::Lazy(value_ty) => quote!(let #name = inputs.next().unwrap().downcast::<#value_ty>()?;), + } + }); + + let base_indices: Vec = slots.iter().enumerate().filter(|(_, slot)| slot.is_base()).map(|(index, _)| index).collect(); + let value_indices: Vec = slots.iter().enumerate().filter(|(_, slot)| matches!(slot, SlotKind::Value(_))).map(|(index, _)| index).collect(); + let value_layout_args: Vec = value_indices + .iter() + .map(|index| { + let layout = format_ident!("__layout_{index}"); + quote!(&#layout,) + }) + .collect(); + + let carried_meta = || { + let meta = ir::layout_meta_tokens(&node, quote!(gcore::record::ElementSpec::Carried), &core_types); + quote!(Some(#meta)) + }; + + // The output wire and node wrap follow the output element: a concrete (or + // row-assigned) element is a typed record; a generic or opaque element is + // an erased record carrying the first base slot's runtime type. + let output_element = match &node.output.shape.element { + ir::Element::Concrete(element) => Some(substitute_ident_types(element, assignments)), + ir::Element::Generic(ident) => assignments.iter().find(|(generic, _)| generic == ident).map(|(_, ty)| ty.clone()), + ir::Element::Opaque => None, + }; + let (io_output, wrap) = match &output_element { + Some(element) => ( + quote!(gcore::registry::record_type::<#element>()), + quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))), + ), + None => { + let name = match &node.output.shape.element { + ir::Element::Generic(ident) => ident.to_string(), + _ => "T".to_string(), + }; + let base_ty = format_ident!("__ty_{}", base_indices[0]); + ( + quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#name))))), + quote!(Ok(gcore::registry::EdgeHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc, #base_ty))), + ) + } + }; + + let (prelude, new_layout_args, layout_meta) = match ir::node_kind(&node) { + ir::NodeKind::RecordIo => { + let carrier_arg = (node.inputs.first().is_some_and(|input| input.subject) && ir::materialized_levels(&node, 0) == 0).then(|| quote!(&__layout_0,)); + let layout_meta_fn = format_ident!("{}_layout_meta", fn_name); + (quote!(), quote!(#carrier_arg #(#value_layout_args)*), quote!(Some(self::#layout_meta_fn()))) + } + ir::NodeKind::Routing => { + let source_layouts = base_indices.iter().map(|index| format_ident!("__layout_{index}")); + let source_wraps = base_indices.iter().map(|index| { + let name = names[*index]; + let layout = format_ident!("__layout_{index}"); + quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);) + }); + let prelude = quote! { + let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); + #(#source_wraps)* + }; + (prelude, quote!(&__union, #(#value_layout_args)*), carried_meta()) + } + ir::NodeKind::Opaque => { + let record_layout = format_ident!("__layout_{}", base_indices[0]); + (quote!(), quote!(&#record_layout), carried_meta()) + } + ir::NodeKind::Flip => unreachable!("flip has its own multi-row emitter"), + }; + + // A carried generic instantiates through the struct's trailing phantom + // parameters, so the constructor names the row's types after one inferred + // slot per input field. + let turbofish = (!carried.is_empty()).then(|| { + let underscores = (0..arity).map(|_| quote!(_)); + let carried_types = carried + .iter() + .filter_map(|(generic, _)| assignments.iter().find(|(ident, _)| ident == generic).map(|(_, ty)| quote!(#ty))); + quote!(::<#(#underscores,)* #(#carried_types,)*>) + }); + + Some(quote! { + gcore::registry::RegistryEntry { + layout_meta: #layout_meta, + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + #io_output, + vec![#(#input_types),*], + ), + constructor: |inputs| { + if inputs.len() != #arity { + return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + #(#downcasts)* + #prelude + let __node = #struct_name #turbofish::new(#(#names,)* #new_layout_args); + #wrap + }, + } + }) + }) + .collect(); if entries.is_empty() { return quote!(); diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index e732404e93..b014f51b77 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -109,7 +109,12 @@ fn monomorphizations(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: & }; let positions: Option> = generics .iter() - .map(|generic| fields.iter().position(|&field| generic_extractable(field_element_type(field), generic)).map(|index| (generic.clone(), index))) + .map(|generic| { + fields + .iter() + .position(|&field| generic_extractable(field_element_type(field), generic)) + .map(|index| (generic.clone(), index)) + }) .collect(); let Some(positions) = positions else { return Vec::new(); @@ -145,7 +150,13 @@ fn item_shape(element: &Type, depth: u8, reads: &[AttributeRead], generics: &[Id ItemShape { element: element_of(element, generics), depth, - attrs: reads.iter().map(|read| LevelAttr { marker: read.marker.clone(), level: 0 }).collect(), + attrs: reads + .iter() + .map(|read| LevelAttr { + marker: read.marker.clone(), + level: 0, + }) + .collect(), } } @@ -330,6 +341,20 @@ impl ValueBinding { } } +/// A record node's lazy inputs consumed as plain elements: their record edges +/// need a layout slot at wiring, like the reading secondaries. +pub(crate) fn element_lazy_indices(regular_fields: &[&ParsedField], node: &Node) -> Vec { + if !matches!(node_kind(node), NodeKind::RecordIo) { + return Vec::new(); + } + regular_fields + .iter() + .enumerate() + .filter(|(index, field)| matches!(field.ty, ParsedFieldType::Node(_)) && matches!(lazy_binding(node, *index), LazyBinding::Element)) + .map(|(index, _)| index) + .collect() +} + #[derive(Clone, Copy)] pub(crate) enum NodeKind { Flip, @@ -356,7 +381,10 @@ fn is_routing(node: &Node) -> bool { let Element::Generic(output) = &node.output.shape.element else { return false }; node.monomorphizations.is_empty() && node.generics.iter().any(|generic| &generic.ident == output && generic.bounds.is_empty()) - && node.inputs.iter().any(|input| input.subject && matches!(&input.shape.element, Element::Generic(generic) if generic == output)) + && node + .inputs + .iter() + .any(|input| input.subject && matches!(&input.shape.element, Element::Generic(generic) if generic == output)) } fn has_attr_io(node: &Node) -> bool { @@ -401,7 +429,7 @@ pub(crate) fn lazy_binding(node: &Node, index: usize) -> LazyBinding { LazyBinding::DeriveRouting } else if node.derives && matches!(kind, NodeKind::RecordIo) && input.subject { LazyBinding::DeriveCarrier - } else if matches!(kind, NodeKind::Flip) { + } else if matches!(kind, NodeKind::Flip) || (matches!(kind, NodeKind::RecordIo) && !input.subject) { LazyBinding::Element } else if matches!(input.shape.element, Element::Opaque) { LazyBinding::OpaqueRecord @@ -577,7 +605,9 @@ mod tests { delta: 0, } } else if kinds.opaque { - let record = fields.iter().position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type))); + let record = fields + .iter() + .position(|field| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type))); Facts { sources: record.into_iter().collect(), carried: true, @@ -588,7 +618,12 @@ mod tests { } else if kinds.routing { let generic = routing_generic(parsed).expect("routing has a generic"); Facts { - sources: fields.iter().enumerate().filter(|(_, field)| bare_ident(&source_ty(field)) == Some(&generic)).map(|(index, _)| index).collect(), + sources: fields + .iter() + .enumerate() + .filter(|(_, field)| bare_ident(&source_ty(field)) == Some(&generic)) + .map(|(index, _)| index) + .collect(), carried: true, writes: vec![], removes: vec![], @@ -617,7 +652,14 @@ mod tests { #[test] fn bridge_flip_concrete() { - assert_bridge(quote!(category("")), quote!(fn negate(_: impl Ctx, x: f64) -> f64 { -x })); + assert_bridge( + quote!(category("")), + quote!( + fn negate(_: impl Ctx, x: f64) -> f64 { + -x + } + ), + ); } #[test] @@ -632,17 +674,38 @@ mod tests { #[test] fn bridge_record_write() { - assert_bridge(quote!(category("")), quote!(fn set_opacity(_: impl Ctx, val: f64) -> (f64, Attr) { (val, Attr(1.)) })); + assert_bridge( + quote!(category("")), + quote!( + fn set_opacity(_: impl Ctx, val: f64) -> (f64, Attr) { + (val, Attr(1.)) + } + ), + ); } #[test] fn bridge_record_remove() { - assert_bridge(quote!(category("")), quote!(fn strip(_: impl Ctx, val: f64) -> (f64, RemoveAttr) { (val, RemoveAttr) })); + assert_bridge( + quote!(category("")), + quote!( + fn strip(_: impl Ctx, val: f64) -> (f64, RemoveAttr) { + (val, RemoveAttr) + } + ), + ); } #[test] fn bridge_record_fresh() { - assert_bridge(quote!(category("")), quote!(fn make(_: impl Ctx, _: (), fill: f64) -> (f64, Attr) { (fill, Attr(1.)) })); + assert_bridge( + quote!(category("")), + quote!( + fn make(_: impl Ctx, _: (), fill: f64) -> (f64, Attr) { + (fill, Attr(1.)) + } + ), + ); } #[test] @@ -724,7 +787,7 @@ mod tests { match &field.ty { ParsedFieldType::Regular(_) => match value_binding(node, index) { ValueBinding::Carrier => "carrier", - ValueBinding::Materialized => "materialized", + ValueBinding::Materialized => "materialized", ValueBinding::Lend => "lend", ValueBinding::ReadingSecondary => "reading", ValueBinding::RecordElement => "record", @@ -767,57 +830,109 @@ mod tests { assert_eq!(actual_kind, expected_kind, "node_kind of {}", parsed.fn_name); let fields: Vec<&ParsedField> = parsed.fields.iter().filter(|field| !field.is_data_field).collect(); for (index, field) in fields.iter().enumerate() { - assert_eq!( - ir_label(&node, index, field, raw), - reference_label(&parsed, raw, index, field), - "field {index} of {}", - parsed.fn_name - ); + assert_eq!(ir_label(&node, index, field, raw), reference_label(&parsed, raw, index, field), "field {index} of {}", parsed.fn_name); } } #[test] fn bindings_flip() { - assert_bindings(quote!(category("")), quote!(fn negate(_: impl Ctx, x: f64) -> f64 { -x })); - assert_bindings(quote!(category("")), quote!(fn add2(_: impl Ctx, a: f64, b: f64) -> f64 { a + b })); + assert_bindings( + quote!(category("")), + quote!( + fn negate(_: impl Ctx, x: f64) -> f64 { + -x + } + ), + ); + assert_bindings( + quote!(category("")), + quote!( + fn add2(_: impl Ctx, a: f64, b: f64) -> f64 { + a + b + } + ), + ); } #[test] fn bindings_lend() { - assert_bindings(quote!(category("")), quote!(fn borrow(_: impl Ctx, prim: f64, other: &f64) -> f64 { prim + *other })); + assert_bindings( + quote!(category("")), + quote!( + fn borrow(_: impl Ctx, prim: f64, other: &f64) -> f64 { + prim + *other + } + ), + ); } #[test] fn bindings_reading_secondary() { - assert_bindings(quote!(category("")), quote!(fn read_op(_: impl Ctx, carrier: f64, (other, op): (f64, Attr)) -> f64 { carrier + other })); + assert_bindings( + quote!(category("")), + quote!( + fn read_op(_: impl Ctx, carrier: f64, (other, op): (f64, Attr)) -> f64 { + carrier + other + } + ), + ); } #[test] fn bindings_flip_lazy() { - assert_bindings(quote!(category("")), quote!(fn apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> f64 { inner.eval(()) })); + assert_bindings( + quote!(category("")), + quote!( + fn apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> f64 { + inner.eval(()) + } + ), + ); } #[test] fn bindings_flip_lazy_reads() { assert_bindings( quote!(category("")), - quote!(fn apply_reads(_: impl Ctx, carrier: f64, inner: impl Node<(), Output = (f64, Attr)>) -> f64 { carrier + inner.eval(()).0 }), + quote!( + fn apply_reads(_: impl Ctx, carrier: f64, inner: impl Node<(), Output = (f64, Attr)>) -> f64 { + carrier + inner.eval(()).0 + } + ), ); } #[test] fn bindings_flip_raw() { - assert_bindings(quote!(category("")), quote!(fn poll_apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> GPoll { inner.eval(()) })); + assert_bindings( + quote!(category("")), + quote!( + fn poll_apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> GPoll { + inner.eval(()) + } + ), + ); } #[test] fn bindings_skip_impl_generic() { // A bounded generic forwarded whole (passthrough) flips, not routes. - assert_bindings(quote!(category(""), skip_impl), quote!(fn passthrough(_: impl Ctx, content: T) -> T { content })); + assert_bindings( + quote!(category(""), skip_impl), + quote!( + fn passthrough(_: impl Ctx, content: T) -> T { + content + } + ), + ); // A generic transformed into a different output type flips. assert_bindings( quote!(category(""), skip_impl), - quote!(fn into_ty, O: Send>(_: impl Ctx, value: T, #[data] _out: PhantomData) -> O { value.into() }), + quote!( + fn into_ty, O: Send>(_: impl Ctx, value: T, #[data] _out: PhantomData) -> O { + value.into() + } + ), ); } @@ -825,20 +940,35 @@ mod tests { fn bindings_routing() { assert_bindings( quote!(category("")), - quote!(fn switch(_: impl Ctx, condition: bool, off: impl Node<(), Output = T>, on: impl Node<(), Output = T>) -> T { if condition { on.eval(()) } else { off.eval(()) } }), + quote!( + fn switch(_: impl Ctx, condition: bool, off: impl Node<(), Output = T>, on: impl Node<(), Output = T>) -> T { + if condition { on.eval(()) } else { off.eval(()) } + } + ), ); } #[test] fn bindings_derive_routing() { - assert_bindings(quote!(category("")), quote!(fn ctx_mod(_: impl Ctx + DeriveCtx, inner: impl Node<(), Output = T>) -> T { inner.eval(()) })); + assert_bindings( + quote!(category("")), + quote!( + fn ctx_mod(_: impl Ctx + DeriveCtx, inner: impl Node<(), Output = T>) -> T { + inner.eval(()) + } + ), + ); } #[test] fn bindings_opaque() { assert_bindings( quote!(category("")), - quote!(fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { content.eval(()) }), + quote!( + fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { + content.eval(()) + } + ), ); } @@ -846,7 +976,11 @@ mod tests { fn creator_ilist_return_pushes_a_level() { let mut parsed = parse_node_fn( quote!(category(""), extent(repeat_extent)), - quote!(fn repeat(_: impl Ctx, (element, transform): (T, Attr), count: u32) -> IList<(T, Attr)> { emit(element, Attr(count as f64)) }), + quote!( + fn repeat(_: impl Ctx, (element, transform): (T, Attr), count: u32) -> IList<(T, Attr)> { + emit(element, Attr(count as f64)) + } + ), ) .unwrap(); parsed.replace_impl_trait_in_input(); @@ -862,7 +996,15 @@ mod tests { #[test] fn reducer_ilist_input_collapses_a_level() { - let mut parsed = parse_node_fn(quote!(category("")), quote!(fn sum(_: impl Ctx, items: IList) -> f64 { items.into_iter().sum() })).unwrap(); + let mut parsed = parse_node_fn( + quote!(category("")), + quote!( + fn sum(_: impl Ctx, items: IList) -> f64 { + items.into_iter().sum() + } + ), + ) + .unwrap(); parsed.replace_impl_trait_in_input(); let node = build(&parsed); // The `IList` input is a depth-1 subject; the scalar output collapses it. diff --git a/node-graph/node-macro/src/codegen/metadata.rs b/node-graph/node-macro/src/codegen/metadata.rs index b02aa925f8..01e331b6af 100644 --- a/node-graph/node-macro/src/codegen/metadata.rs +++ b/node-graph/node-macro/src/codegen/metadata.rs @@ -91,7 +91,6 @@ pub(crate) fn generate_phantom_data<'a>(fn_generics: impl Iterator, diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index fddf7ea182..291aad8dca 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -7,8 +7,8 @@ use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::{Comma, RArrow}; use syn::{ - AttrStyle, Attribute, Error, Expr, FnArg, GenericArgument, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, PathArguments, ReturnType, - TraitBound, Type, TypeImplTrait, TypeParam, TypeParamBound, Visibility, WhereClause, parse_quote, + AttrStyle, Attribute, Error, Expr, FnArg, GenericArgument, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, PathArguments, ReturnType, TraitBound, + Type, TypeImplTrait, TypeParam, TypeParamBound, Visibility, WhereClause, parse_quote, }; use crate::codegen::generate_node_code; @@ -585,7 +585,9 @@ impl Parse for NodeFnAttributes { if extent_raw.is_some() { return Err(Error::new_spanned(meta, "Multiple 'extent_raw' attributes are not allowed")); } - let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent_raw', e.g., extent_raw(my_extent)"))?; + let parsed_path: Path = meta + .parse_args() + .map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent_raw', e.g., extent_raw(my_extent)"))?; extent_raw = Some(parsed_path); } // Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop. @@ -858,10 +860,7 @@ fn parse_read_tuple(pat_tuple: &syn::PatTuple, ty: &Type, attrs: &[Attribute], i let Pat::Ident(pat_ident) = pat else { return Err(Error::new_spanned(pat, "Expected a simple identifier for the attribute read")); }; - Ok(AttributeRead { - pat_ident: pat_ident.clone(), - marker, - }) + Ok(AttributeRead { pat_ident: pat_ident.clone(), marker }) }) .collect::>()?; let mut field = parse_field(value_ident.clone(), value_ty.clone(), attrs).map_err(|e| Error::new_spanned(&value_ident, format!("Failed to parse argument '{}': {}", value_ident.ident, e)))?; @@ -1539,7 +1538,7 @@ mod tests { description: String::new(), widget_override: ParsedWidgetOverride::None, ty: ParsedFieldType::Regular(RegularParsedField { - lend: None, + lend: None, list_levels: 0, ty: parse_quote!(DVec2), exposed: false, diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index a62cfe0bac..961a689fb4 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -40,10 +40,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) { } let writes = record_writes(&value); - let has_reads = parsed - .fields - .iter() - .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); + let has_reads = parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); if !has_reads && writes.is_none() { return; } @@ -56,8 +53,14 @@ fn validate_record_io(parsed: &ParsedNodeFn) { } for field in parsed.fields.iter().skip(1) { - if matches!(field.ty, ParsedFieldType::Node(_)) { - emit_error!(field.pat_ident.span(), "record nodes take no lazy inputs yet"); + if let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty { + // Lazy secondaries are consumed as plain elements through the wire. + if crate::codegen::classify::is_record_value(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 { + emit_error!(field.pat_ident.span(), "a record node's lazy inputs consume plain elements, not record or ranked wires"); + } + if !field.attribute_reads.is_empty() { + emit_error!(field.pat_ident.span(), "attribute reads on a record node's lazy inputs are not supported yet"); + } } } for (index, field) in parsed.fields.iter().enumerate() { @@ -91,10 +94,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) { } let Some(carrier) = parsed.fields.first() else { - emit_error!( - parsed.fn_name.span(), - "attribute io needs a primary input as the first parameter after the context (`_: ()` for none)" - ); + emit_error!(parsed.fn_name.span(), "attribute io needs a primary input as the first parameter after the context (`_: ()` for none)"); return; }; let lazy_carrier = matches!(&crate::codegen::record_shape(parsed), Some(shape) if matches!(shape.carrier, crate::codegen::RecordCarrier::LazyToken)); @@ -111,10 +111,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) { return; }; if lazy_carrier && !crate::codegen::ir::build(parsed).derives { - emit_error!( - parsed.input.pat_ident.span(), - "a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`" - ); + emit_error!(parsed.input.pat_ident.span(), "a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`"); return; } @@ -128,11 +125,7 @@ fn validate_record_io(parsed: &ParsedNodeFn) { match &token { Some(token) => { if !matches!(crate::codegen::bare_ident(element), Some(ident) if ident == token) { - emit_error!( - parsed.output_type.span(), - "a generic element passes through unchanged: return `{}` in the first tuple position", - token - ); + emit_error!(parsed.output_type.span(), "a generic element passes through unchanged: return `{}` in the first tuple position", token); } } None => { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index d4548bdd56..daea2dba56 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1687,11 +1687,32 @@ fn path_is_closed( // Converts with the write-attribute family: the record form needs a lazy // value input on a record node, which the macro does not accept yet, and it -// shares that family's per-item index convention. -#[node_macro::node(category("Vector"), path(graphene_core::vector))] -fn map_points(ctx: impl Ctx + DeriveCtx, content: List, mapped: impl Node, Output = DVec2>) -> Result, Interrupt> { +/// Sets each anchor point's position to the value the mapped input produces, with the point's +/// index and current position provided via context. +#[node_macro::node(category("Vector"), path(graphene_core::vector), extent(map_points_extent))] +fn map_points<'e>( + ctx: impl Ctx + DeriveCtx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, + mapped: impl Node, Output = DVec2>, +) -> Result< + IList<( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, Fill>, + Attr<'e, StrokeAttr>, + Attr<'e, BlendModeAttr>, + Attr<'e, Opacity>, + Attr<'e, OpacityFill>, + Attr<'e, ClippingMask>, + Attr<'e, EditorLayerPath>, + Attr<'e, EditorMergedLayers>, + )>, + Interrupt, +> { + // The pushed copy keeps the legacy convention: the running point index + // across all rows rides as a promotion for the mapped input. let spilled = ctx.index_head(); - let mut content = content; + let mut content = legacy_vector_list_of(content); let mut index = 0; for vector in content.iter_element_values_mut() { @@ -1702,7 +1723,11 @@ fn map_points(ctx: impl Ctx + DeriveCtx, content: List, mapped: impl Nod } } - Ok(content) + emit_legacy_lane(ctx.arena(), content, ctx.innermost_index() as usize) +} + +fn map_points_extent(content: ListIn<'_, Vector>, _mapped: ExtentIn<'_>, level: LevelIn) -> GPoll { + subject_counts_extent(content, level) } fn flatten_path_core<'e>(