From 5b02edfd9aff11b9b086cc685f237e150d4ade7d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 10 Aug 2026 16:15:31 +0000 Subject: [PATCH] Split the node macro codegen into classify, entries, and metadata modules --- node-graph/node-macro/src/codegen.rs | 1413 +---------------- node-graph/node-macro/src/codegen/classify.rs | 715 +++++++++ node-graph/node-macro/src/codegen/entries.rs | 519 ++++++ node-graph/node-macro/src/codegen/metadata.rs | 173 ++ 4 files changed, 1417 insertions(+), 1403 deletions(-) create mode 100644 node-graph/node-macro/src/codegen/classify.rs create mode 100644 node-graph/node-macro/src/codegen/entries.rs create mode 100644 node-graph/node-macro/src/codegen/metadata.rs diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index f6d3aa4a07..db8faee94a 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -6,7 +6,17 @@ use quote::{ToTokens, format_ident, quote}; use std::sync::atomic::AtomicU64; 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}; + +mod classify; +mod entries; +mod metadata; +pub(crate) use classify::*; +use entries::entries_tokens; +use metadata::generate_node_input_references; + static NODE_ID: AtomicU64 = AtomicU64::new(0); pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { @@ -625,180 +635,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }) } -/// Generates strongly typed utilites to access inputs -fn generate_node_input_references( - parsed: &ParsedNodeFn, - fn_generics: &[crate::GenericParam], - field_idents: &[&PatIdent], - core_types: &TokenStream2, - identifier: &Ident, - cfg: &TokenStream2, -) -> TokenStream2 { - let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake)); - - let mut generated_input_accessor = Vec::new(); - if !parsed.attributes.skip_impl { - 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() { - let mut ty = match &parsed_input.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, - } - .clone(); - - // We only want the necessary generics. - let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty); - // TODO: figure out a better name that doesn't conflict with so many types - let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal)); - let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter()); - - // Only create structs with phantom data where necessary. - generated_input_accessor.push(if phantom_data_declerations.is_empty() { - quote! { - pub struct #struct_name; - } - } else { - quote! { - pub struct #struct_name <#(#used),*>{ - #(#phantom_data_declerations,)* - } - } - }); - generated_input_accessor.push(quote! { - impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { - const INDEX: usize = #input_index; - fn identifier() -> #core_types::ProtoNodeIdentifier { - #inputs_module_name::IDENTIFIER.clone() - } - type Result = #ty; - } - }) - } - } - - quote! { - #cfg - pub mod #inputs_module_name { - use super::*; - - /// The `ProtoNodeIdentifier` of this node without any generics attached to it - pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier(); - #(#generated_input_accessor)* - } - } -} - -/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors. -fn generate_phantom_data<'a>(fn_generics: impl Iterator) -> (Vec, Vec) { - let mut phantom_data_declerations = Vec::new(); - let mut fn_generic_params = Vec::new(); - - for fn_generic_param in fn_generics { - let field_name = format_ident!("phantom_{}", phantom_data_declerations.len()); - - match fn_generic_param { - crate::GenericParam::Lifetime(lifetime_param) => { - let lifetime = &lifetime_param.lifetime; - - fn_generic_params.push(quote! {#lifetime}); - phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>}) - } - crate::GenericParam::Type(type_param) => { - let generic_name = &type_param.ident; - - fn_generic_params.push(quote! {#generic_name}); - phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>}); - } - _ => {} - } - } - (fn_generic_params, phantom_data_declerations) -} - -use crate::shader_nodes::{ShaderCodegen, ShaderTokens}; -use syn::visit_mut::VisitMut; - -/// Get only the necessary generics. -struct FilterUsedGenerics { - all: Vec, - used: Vec, -} - -impl VisitMut for FilterUsedGenerics { - fn visit_lifetime_mut(&mut self, used_lifetime: &mut Lifetime) { - for (generic, used) in self.all.iter().zip(self.used.iter_mut()) { - let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue }; - if used_lifetime == &lifetime_param.lifetime { - *used = true; - } - } - } - - fn visit_path_mut(&mut self, path: &mut syn::Path) { - for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() { - let crate::GenericParam::Type(type_param) = generic else { continue }; - if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident { - *used = true; - // Sometimes the generics conflict with the type name so we rename the generics. - path.segments[0].ident = format_ident!("G{index}"); - } - } - for mut el in Punctuated::pairs_mut(&mut path.segments) { - self.visit_path_segment_mut(el.value_mut()); - } - } -} - -impl FilterUsedGenerics { - fn new(fn_generics: &[crate::GenericParam]) -> (Vec, Self) { - let mut all_possible_generics = fn_generics.to_vec(); - // The 'n lifetime may also be needed; we must add it in - all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site())))); - - let modified = all_possible_generics - .iter() - .cloned() - .enumerate() - .map(|(index, mut generic)| { - let crate::GenericParam::Type(type_param) = &mut generic else { return generic }; - // Sometimes the generics conflict with the type name so we rename the generics. - type_param.ident = format_ident!("G{index}"); - generic - }) - .collect::>(); - - let generic_collector = Self { - used: vec![false; all_possible_generics.len()], - all: all_possible_generics, - }; - - (modified, generic_collector) - } - - fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator { - modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value) - } - - fn filter_unnecessary_generics(&mut self, modified: &mut Vec, ty: &mut Type) -> Vec { - self.used.fill(false); - - // Find out which generics are necessary to support the node input - self.visit_type_mut(ty); - - // Sometimes generics may reference other generics. This is a non-optimal way of dealing with that. - for _ in 0..=self.all.len() { - for (index, item) in modified.iter_mut().enumerate() { - if self.used[index] { - self.visit_generic_param_mut(item); - } - } - } - - self.used(&*modified).cloned().collect() - } -} - /// Check if a type contains a reference to a specific identifier (e.g., a generic type parameter) pub(crate) fn type_contains_ident(ty: &Type, ident: &Ident) -> bool { struct IdentChecker<'a> { @@ -2118,1232 +1954,3 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ..Default::default() }) } - -/// How a record node's primary input lowers. -#[derive(Clone)] -pub(crate) enum RecordCarrier { - /// `_: ()`: no carrier edge, the kernel writes a fresh record. - None, - /// An unbounded generic returned in the element position: the element - /// bytes carry through the copy plan and the kernel sees `ElToken`. - Token(Ident), - /// An element type read at offset 0, monomorphized per its - /// implementations list where generic. - Read(Type), -} - -/// The record io of a node fn: how the carrier lowers, the element write, -/// and the markers written and removed. Present exactly when the signature -/// declares attribute reads or writes in a shape the record tier supports; -/// malformed record io is reported by validation and generates no node impl. -#[derive(Clone)] -pub(crate) struct RecordShape { - pub(crate) carrier: RecordCarrier, - pub(crate) element_write: Option, - pub(crate) write_markers: Vec, - pub(crate) removes: Vec, -} - -impl RecordShape { - pub(crate) fn skips_carrier(&self) -> bool { - matches!(self.carrier, RecordCarrier::None) - } - - pub(crate) fn carries_element(&self) -> bool { - self.element_write.is_none() - } -} - -/// The record-tier lowering a node fn resolves to. Exactly one class per node, -/// computed once by [`analyze`]; every downstream fragment reads the class -/// instead of recomputing the classification predicates. -pub(crate) enum Class { - RecordIo(RecordShape), - Routing(RoutingIo), - Flip { carrier: bool }, - Opaque, -} - -/// The effect/return axis of a node's kernel, resolved once from the signature. -/// Orthogonal to [`Class`]: it selects the eval tail (finish / merge / spawn) -/// and the kernel signature wrapping across every class. -#[derive(Clone, Copy, PartialEq)] -pub(crate) enum Dialect { - Sync, - Interrupt, - Poll, - AsyncFn, - Future, - FutureInterrupt, -} - -pub(crate) fn dialect(parsed: &ParsedNodeFn) -> Dialect { - if parsed.is_async { - return Dialect::AsyncFn; - } - match kernel_kind(&parsed.output_type) { - KernelKind::Plain => Dialect::Sync, - KernelKind::Interrupt(_) => Dialect::Interrupt, - KernelKind::Poll(_) => Dialect::Poll, - KernelKind::Future(_) => Dialect::Future, - KernelKind::FutureInterrupt(_) => Dialect::FutureInterrupt, - } -} - -/// The result of classifying a node fn. A node with no supported lowering -/// (an async node with lazy inputs, malformed record io, or a signature no -/// class accepts) yields `None` and generates a struct and metadata but no -/// `Node` impl. -pub(crate) struct NodeModel { - pub(crate) class: Class, - pub(crate) dialect: Dialect, -} - -pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option { - if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { - return None; - } - let class = if let Some(shape) = record_shape(parsed) { - Class::RecordIo(shape) - } else if has_record_io(parsed) { - return None; - } else if let Some(routing) = routing_io(parsed) { - Class::Routing(routing) - } else if record_flip(parsed) { - Class::Flip { carrier: flip_carrier(parsed) } - } else if record_opaque(parsed) { - Class::Opaque - } else { - return None; - }; - Some(NodeModel { class, dialect: dialect(parsed) }) -} - -/// The per-field binding role, resolved once per regular field from the node -/// class and field shape. Drives the eval bindings and the lazy-edge input -/// types. The `lend` and `reads` axes stay field properties the value arms of -/// `kernel_params`/`call_args`/`value_args` consult, since they cross roles. -#[derive(Clone, Copy)] -pub(crate) enum InputRole { - RecordCarrier, - FlipCarrier, - ReadingSecondary, - LendBorrow, - RecordValue, - PlainValue, - DeriveRoutingSource, - OpaqueRecordEdge, - FlipRawLazyEdge, - FlipLazy, - RawLazy, - Lazy, -} - -impl InputRole { - /// A role that copies an element out of a record edge, so its record frame - /// must be reclaimed after the read. The step lowering wraps such a bind in - /// `mark`/`rewind`, making the stack discipline structural rather than - /// hand-threaded through each read-out arm. - fn reads_out(self) -> bool { - matches!(self, InputRole::ReadingSecondary | InputRole::RecordValue) - } -} - -/// The tail form of a node's eval, selected from its class and dialect: forward -/// the kernel's own record, assemble a record (io or flip carrier), or spawn a -/// source and lift its completion. -#[derive(Clone, Copy)] -enum Tail { - Forward, - Record, - Flip, - SpawnAsyncFn, - SpawnFuture, -} - -/// One statement group of a node's `eval` body, lowered in order: the input -/// binds first (one per input), then the numeric clamps, then the tail that -/// assembles the output record and closes the dialect. -enum EvalStep<'a> { - Bind(usize, &'a ParsedField), - Clamp(&'a ParsedField), - Tail(Tail), -} - -/// Whether the signature declares record-tier attribute io: value-input reads -/// 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(_))); - 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(_))) -} - -/// The value inputs of a routing node (every regular field that is not a -/// routing source), with their indices into the regular fields. -pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], routing: &RoutingIo) -> 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(&routing.generic)), - ParsedFieldType::Node(_) => false, - }) - .map(|(index, _)| index) - .collect() -} - -/// The lazy inputs declaring attribute reads, with their indices into the -/// unit-skipped regular fields. -pub(crate) fn lazy_read_fields<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a ParsedField)> { - regular_fields - .iter() - .enumerate() - .filter(|(_, field)| matches!(field.ty, ParsedFieldType::Node(_)) && !field.attribute_reads.is_empty()) - .map(|(index, field)| (index, *field)) - .collect() -} - -/// The indices (into the unit-skipped regular fields) of value inputs whose -/// reads resolve against their own wire rather than the carrier's. -pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], shape: &RecordShape) -> Vec { - regular_fields - .iter() - .enumerate() - .filter(|(index, field)| !field.attribute_reads.is_empty() && (shape.skips_carrier() || *index != 0)) - .map(|(index, _)| index) - .collect() -} - -/// Every attribute read in field order with the owning field's index, flat so -/// read slots are numbered across inputs. -pub(crate) fn field_reads<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a AttributeRead)> { - regular_fields - .iter() - .enumerate() - .flat_map(|(index, field)| field.attribute_reads.iter().map(move |read| (index, read))) - .collect() -} - -/// Substitutes bare generic idents with their row-assigned types. -fn substitute_ident_types(ty: &Type, assignments: &[(Ident, Type)]) -> Type { - struct Subst<'a> { - assignments: &'a [(Ident, Type)], - } - - impl VisitMut for Subst<'_> { - fn visit_type_mut(&mut self, ty: &mut Type) { - if let Type::Path(path) = ty - && path.qself.is_none() - && let Some(ident) = path.path.get_ident() - && let Some((_, replacement)) = self.assignments.iter().find(|(generic, _)| generic == ident) - { - *ty = replacement.clone(); - return; - } - syn::visit_mut::visit_type_mut(self, ty); - } - } - - let mut ty = ty.clone(); - Subst { assignments }.visit_type_mut(&mut ty); - ty -} - -/// Replaces the routing generic in a derive-routing kernel's return type with -/// the routing record value, since the kernel's edges rebind to '__record. -fn substitute_routing_record(output: &Type, generic: &Ident, core_types: &TokenStream2) -> Type { - struct Subst<'a> { - generic: &'a Ident, - replacement: Type, - } - - impl VisitMut for Subst<'_> { - fn visit_type_mut(&mut self, ty: &mut Type) { - if let Type::Path(path) = ty - && path.qself.is_none() - && path.path.get_ident() == Some(self.generic) - { - *ty = self.replacement.clone(); - return; - } - syn::visit_mut::visit_type_mut(self, ty); - } - } - - let mut ty = output.clone(); - let mut subst = Subst { - generic, - replacement: syn::parse_quote!(#core_types::record::RecordValue<'__record>), - }; - subst.visit_type_mut(&mut ty); - ty -} - -fn inject_attr_lifetimes(output: &Type) -> Option { - struct Injector { - changed: bool, - } - - impl VisitMut for Injector { - fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) { - if segment.ident == "Attr" - && let PathArguments::AngleBracketed(args) = &mut segment.arguments - && !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_))) - { - args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__attr", proc_macro2::Span::call_site()))); - self.changed = true; - } - syn::visit_mut::visit_path_segment_mut(self, segment); - } - } - - let mut ty = output.clone(); - let mut injector = Injector { changed: false }; - injector.visit_type_mut(&mut ty); - injector.changed.then_some(ty) -} - -pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool { - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - parsed - .fn_generics - .iter() - .any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident))) -} - -pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option { - let ident = bare_ident(ty)?.clone(); - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - parsed - .fn_generics - .iter() - .find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?; - if let Some(where_clause) = &parsed.where_clause - && tokens_contain_ident(where_clause.to_token_stream(), &ident) - { - return None; - } - Some(ident) -} - -pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { - let value = match kernel_kind(&parsed.output_type) { - KernelKind::Plain => parsed.output_type.clone(), - KernelKind::Interrupt(inner) => inner, - _ => 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(_))); - if !has_reads && writes.is_none() { - return None; - } - if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { - return None; - } - let reads_well_placed = parsed.fields.iter().all(|field| { - field.attribute_reads.is_empty() || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) - }); - if !reads_well_placed { - return None; - } - let carrier_field = parsed.fields.first()?; - if carrier_field.is_data_field { - return None; - } - let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else { - return None; - }; - let carrier = match ty { - Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None, - ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() { - Some(token) => RecordCarrier::Token(token), - None => { - if contains_open_generic(parsed, ty) { - return None; - } - RecordCarrier::Read(ty.clone()) - } - }, - }; - let (element, write_markers, removes) = match writes { - Some(RecordWrites { element, markers, removes }) => (element, markers, removes), - None => (value, Vec::new(), Vec::new()), - }; - let element_write = match &carrier { - RecordCarrier::Token(token) => match bare_ident(&element) { - Some(ident) if ident == token => None, - _ => return None, - }, - _ => { - if contains_open_generic(parsed, &element) { - return None; - } - Some(element) - } - }; - if matches!(carrier, RecordCarrier::None) && !removes.is_empty() { - return None; - } - Some(RecordShape { - carrier, - element_write, - write_markers, - removes, - }) -} - -pub(crate) fn is_poll_kernel(output: &Type) -> bool { - matches!(kernel_kind(output), KernelKind::Poll(_)) -} - -/// A routing family: an unbounded generic shared by lazy inputs (and -/// optionally the first parameter) and returned whole, instantiated at -/// `RecordValue` so opaque records flow through the kernel. Detected only -/// when the family's fields carry no implementations lists, so the existing -/// per-type row spelling keeps its meaning. -#[derive(Clone)] -pub(crate) struct RoutingIo { - pub(crate) generic: Ident, -} - -/// Whether a flipped node's primary input is a carrier: the first parameter -/// after the context, when it is an owned or lent value input. A carrier's -/// fields pass through to the output; every production layout is element-only -/// until attribute adoption, so the copy plan is empty and behavior is -/// unchanged. Async kernels carry fields per eval around the slot (only the -/// element crosses the future boundary), so their carrier must be owned: the -/// future captures the element by value. -pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool { - if !record_flip(parsed) { - return false; - } - let Some(first) = parsed.fields.first() else { return false }; - if first.is_data_field { - return false; - } - let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &first.ty else { - return false; - }; - if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) { - return false; - } - let async_kernel = parsed.is_async || matches!(kernel_kind(&parsed.output_type), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)); - !(async_kernel && lend.is_some()) -} - -/// Whether a plain node's lowering flips onto record wires: sync, -/// fully-concrete value-input nodes in this cut; batch, shader, async, lend, -/// lazy, and generic nodes keep the plain lowering until their record forms -/// land. -pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { - if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() { - return false; - } - // Shader nodes flip like any value node: the kernel doubles as the - // shader body on the spirv target, but the struct and Node impl are - // std-gated, so the record machinery never reaches the shader build. - if parsed.attributes.batch.is_some() || parsed.attributes.plain { - return false; - } - if type_disqualifies(&slot_value_type(&parsed.output_type)) { - return false; - } - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - for param in &parsed.fn_generics { - match param { - GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_ident.as_ref() => {} - // Registry rows assign a generic by unifying a field's type with - // the row's, so a generic without an extractable position keeps - // the plain lowering. A `skip_impl` node's rows are hand-written - // with explicit types, so no extractable position is needed. - GenericParam::Type(type_param) => { - let extractable = parsed.fields.iter().filter(|field| !field.is_data_field).any(|field| { - let ty = match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, - }; - generic_extractable(ty, &type_param.ident) - }); - if !extractable && !parsed.attributes.skip_impl { - return false; - } - } - GenericParam::Lifetime(_) | GenericParam::Const(_) => return false, - } - } - true -} - -/// Whether unifying a value of `field_ty`'s shape can bind `generic`: the -/// generic sits bare or under path type arguments, the shapes -/// [`generic_assignment`] walks. -fn generic_extractable(field_ty: &Type, generic: &Ident) -> bool { - match field_ty { - Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic) => true, - Type::Path(path) => path.path.segments.iter().any(|segment| match &segment.arguments { - PathArguments::AngleBracketed(args) => args.args.iter().any(|argument| match argument { - GenericArgument::Type(inner) => generic_extractable(inner, generic), - _ => false, - }), - _ => false, - }), - _ => false, - } -} - -/// Binds `generic` by unifying `field_ty` against `row_ty`: where the field -/// names the generic, the row's corresponding subtree is the assignment. -fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident) -> Option { - if matches!(field_ty, Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic)) { - return Some(row_ty.clone()); - } - let (Type::Path(field_path), Type::Path(row_path)) = (field_ty, row_ty) else { - return None; - }; - let field_segment = field_path.path.segments.last()?; - let row_segment = row_path.path.segments.last()?; - let (PathArguments::AngleBracketed(field_args), PathArguments::AngleBracketed(row_args)) = (&field_segment.arguments, &row_segment.arguments) else { - return None; - }; - field_args.args.iter().zip(row_args.args.iter()).find_map(|(field_arg, row_arg)| match (field_arg, row_arg) { - (GenericArgument::Type(field_inner), GenericArgument::Type(row_inner)) => generic_assignment(field_inner, row_inner, generic), - _ => None, - }) -} - -pub(crate) fn is_record_value(ty: &Type) -> bool { - matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RecordValue")) -} - -/// Whether a kernel operates on whole records: it names `RecordValue` in its -/// output, receives raw record edges paired with the node's layout, and -/// takes on the record APIs' unsafe contracts itself. -pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool { - is_record_value(&slot_value_type(&parsed.output_type)) -} - -pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { - if has_record_io(parsed) || parsed.is_async { - return None; - } - if !matches!(kernel_kind(&parsed.output_type), KernelKind::Plain | KernelKind::Interrupt(_)) { - return None; - } - let value = slot_value_type(&parsed.output_type); - let Type::Path(path) = &value else { return None }; - let ident = path.path.get_ident()?.clone(); - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - parsed - .fn_generics - .iter() - .find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?; - if let Some(where_clause) = &parsed.where_clause - && tokens_contain_ident(where_clause.to_token_stream(), &ident) - { - return None; - } - let mut sources = 0; - for (index, field) in parsed.fields.iter().enumerate() { - match &field.ty { - ParsedFieldType::Node(NodeParsedField { - output_type, - input_type, - implementations, - }) => { - if bare_ident(output_type) == Some(&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() { - return None; - } - sources += 1; - } else if type_contains_ident(output_type, &ident) || type_contains_ident(input_type, &ident) { - return None; - } - } - ParsedFieldType::Regular(RegularParsedField { ty, implementations, lend, .. }) => { - if bare_ident(ty) == Some(&ident) { - if index != 0 || field.is_data_field || !implementations.is_empty() || lend.is_some() { - return None; - } - sources += 1; - } else if type_contains_ident(ty, &ident) { - return None; - } - } - } - } - (sources > 0).then(|| RoutingIo { generic: ident }) -} - -pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> { - let Type::Path(path) = ty else { return None }; - path.path.get_ident() -} - -fn tokens_contain_ident(tokens: TokenStream2, ident: &Ident) -> bool { - tokens.into_iter().any(|token| match token { - proc_macro2::TokenTree::Ident(candidate) => &candidate == ident, - proc_macro2::TokenTree::Group(group) => tokens_contain_ident(group.stream(), ident), - _ => false, - }) -} - -pub(crate) fn slot_value_type(output: &Type) -> Type { - match kernel_kind(output) { - KernelKind::Plain => output.clone(), - KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, - KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => match kernel_kind(&payload) { - KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, - _ => payload, - }, - } -} - -pub(crate) fn is_source_kernel(output: &Type) -> bool { - matches!(kernel_kind(output), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)) -} - -enum KernelKind { - Plain, - Interrupt(Type), - Poll(Type), - Future(Type), - FutureInterrupt(Type), -} - -fn source_future_payload(segment: &syn::PathSegment) -> Type { - let PathArguments::AngleBracketed(args) = &segment.arguments else { - return syn::parse_quote!(()); - }; - args.args - .iter() - .find_map(|argument| match argument { - GenericArgument::Type(ty) => Some(ty.clone()), - _ => None, - }) - .unwrap_or_else(|| syn::parse_quote!(())) -} - -fn kernel_kind(output: &Type) -> KernelKind { - let plain = || KernelKind::Plain; - let Type::Path(path) = output else { return plain() }; - let Some(segment) = path.path.segments.last() else { return plain() }; - match segment.ident.to_string().as_str() { - "GPoll" => { - let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; - let inner = args.args.iter().find_map(|argument| match argument { - GenericArgument::Type(ty) => Some(ty.clone()), - _ => None, - }); - inner.map(KernelKind::Poll).unwrap_or_else(plain) - } - "SourceFuture" => KernelKind::Future(source_future_payload(segment)), - "Result" => { - let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; - let mut types = args.args.iter().filter_map(|argument| match argument { - GenericArgument::Type(ty) => Some(ty), - _ => None, - }); - let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else { - return plain(); - }; - if error_path.path.segments.last().is_none_or(|segment| segment.ident != "Interrupt") { - return plain(); - } - if let Type::Path(inner_path) = inner - && let Some(inner_segment) = inner_path.path.segments.last() - && inner_segment.ident == "SourceFuture" - { - return KernelKind::FutureInterrupt(source_future_payload(inner_segment)); - } - KernelKind::Interrupt(inner.clone()) - } - _ => plain(), - } -} - -fn context_param(parsed: &ParsedNodeFn) -> Option<&TypeParam> { - let Type::Path(path) = &parsed.input.ty else { - return None; - }; - let ident = path.path.get_ident()?; - parsed.fn_generics.iter().find_map(|param| match param { - GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param), - _ => None, - }) -} - -fn type_disqualifies(ty: &Type) -> bool { - struct Disqualifier { - found: bool, - } - - impl<'ast> Visit<'ast> for Disqualifier { - fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) { - self.found = true; - } - - fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) { - self.found = true; - } - - fn visit_lifetime(&mut self, _: &'ast Lifetime) { - self.found = true; - } - } - - let mut visitor = Disqualifier { found: false }; - visitor.visit_type(ty); - visitor.found -} - -fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 { - let TypeParamBound::Trait(trait_bound) = bound else { - return quote!(#bound); - }; - let Some(segment) = trait_bound.path.segments.last() else { - return quote!(#bound); - }; - if segment.ident != "ExtractArena" { - return quote!(#bound); - } - let PathArguments::AngleBracketed(args) = &segment.arguments else { - return quote!(#bound); - }; - if args.args.len() != 1 { - return quote!(#bound); - } - let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else { - return quote!(#bound); - }; - quote!(#core_types::context::ExtractArena) -} - -fn entries_tokens(parsed: &ParsedNodeFn, class: &Class, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 { - if !data_field_generic_idents.is_empty() { - return quote!(); - } - match class { - Class::RecordIo(_) => record_entries_tokens(parsed, struct_name, regular_fields), - Class::Routing(_) => routing_entries_tokens(parsed, struct_name, regular_fields), - Class::Flip { .. } => flip_entries_tokens(parsed, struct_name, regular_fields), - Class::Opaque => record_opaque_entries_tokens(parsed, struct_name, regular_fields), - } -} - -/// The registry rows of a flipped plain node: every wire is a record wire, -/// inputs resolve their layouts off the claimed handles, and the output is an -/// element-only record of the kernel's return type. -fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { - let Some(rows) = implementation_rows(parsed, regular_fields) else { - return quote!(); - }; - let rows: Vec<&Vec> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect(); - if rows.is_empty() { - return quote!(); - } - let output = slot_value_type(&parsed.output_type); - - let field_type = |field: &ParsedField| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(), - }; - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - let generic_positions: Option> = parsed - .fn_generics - .iter() - .filter_map(|param| match param { - GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), - _ => None, - }) - .map(|generic| { - regular_fields - .iter() - .position(|field| generic_extractable(&field_type(field), generic)) - .map(|index| (generic.clone(), index)) - }) - .collect(); - let Some(generic_positions) = generic_positions else { - return quote!(); - }; - - let fn_name = &parsed.fn_name; - let entries_name = format_ident!("{}_entries", fn_name); - let arity = regular_fields.len(); - let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); - let node_underscores: Vec = regular_fields.iter().map(|_| quote!(_)).collect(); - - // Shorthand associated types in the output only resolve against the - // generics' bounds, so rows name the output through a bounded alias. Only - // output-reaching generics (directly or through a kept bound) may appear: - // an unused alias parameter is an error. - let candidate_params: Vec<&GenericParam> = parsed - .fn_generics - .iter() - .filter(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref())) - .collect(); - let param_ident = |param: &&GenericParam| match param { - GenericParam::Type(type_param) => type_param.ident.clone(), - _ => unreachable!("candidates are type parameters"), - }; - let mut kept: Vec = candidate_params.iter().map(|param| type_contains_ident(&output, ¶m_ident(param))).collect(); - loop { - let mut grew = false; - for index in 0..candidate_params.len() { - if kept[index] { - continue; - } - let ident = param_ident(&candidate_params[index]); - let mentioned = candidate_params.iter().zip(&kept).any(|(param, kept)| { - *kept - && match param { - GenericParam::Type(type_param) => type_param.bounds.iter().any(|bound| { - let bound: Type = syn::parse_quote!(dyn #bound); - type_contains_ident(&bound, &ident) - }), - _ => false, - } - }); - if mentioned { - kept[index] = true; - grew = true; - } - } - if !grew { - break; - } - } - let alias_params: Vec<&GenericParam> = candidate_params.iter().zip(&kept).filter(|(_, kept)| **kept).map(|(param, _)| *param).collect(); - let alias_param_idents: Vec = alias_params.iter().map(|param| param_ident(param)).collect(); - let alias_param_tokens: Vec = alias_params.iter().map(|param| quote!(#param)).collect(); - let output_alias = format_ident!("__{}_output", fn_name); - let alias_def = match alias_param_tokens.is_empty() { - true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;), - false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;), - }; - - let entries = rows.iter().filter_map(|row| { - let assignments: Vec<(Ident, Type)> = generic_positions - .iter() - .map(|(generic, index)| generic_assignment(&field_type(regular_fields[*index]), &row[*index], generic).map(|assigned| (generic.clone(), assigned))) - .collect::>()?; - if type_disqualifies(&substitute_ident_types(&output, &assignments)) { - return None; - } - let assignment_types: Vec = assignments.iter().map(|(_, ty)| quote!(#ty)).collect(); - let alias_arguments: Vec = assignments - .iter() - .filter(|(generic, _)| alias_param_idents.contains(generic)) - .map(|(_, ty)| quote!(#ty)) - .collect(); - let row_output = match alias_arguments.is_empty() { - true => quote!(#output_alias), - false => quote!(#output_alias<#(#alias_arguments),*>), - }; - let assignment_types = assignment_types.iter(); - let turbofish = quote!(::<#(#node_underscores,)* #(#assignment_types,)*>); - let input_types = row.iter().map(|ty| quote!(gcore::registry::record_edge_type::<#ty>())); - let downcasts = names.iter().zip(row.iter()).enumerate().map(|(index, (name, ty))| { - let handle = format_ident!("__handle_{index}"); - let layout = format_ident!("__layout_{index}"); - quote! { - let #handle = inputs.next().unwrap(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = #handle.downcast_record::<#ty>()?; - } - }); - let layout_args = (0..arity).map(|index| { - let layout = format_ident!("__layout_{index}"); - quote!(&#layout,) - }); - Some(quote! { - gcore::registry::RegistryEntry { - io: gcore::registry::NodeIOTypes::new( - gcore::concrete!(gcore::context::ContextImpl<'static>), - gcore::registry::record_type::<#row_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)* - let __node = #struct_name #turbofish::new(#(#names,)* #(#layout_args)*); - Ok(gcore::registry::EdgeHandle::new_record::<#row_output>(::std::sync::Arc::new(__node) as ::std::sync::Arc)) - }, - } - }) - }); - let entries: Vec = entries.collect(); - if entries.is_empty() { - return quote!(); - } - - quote! { - pub fn #entries_name() -> ::std::vec::Vec { - #alias_def - vec![#(#entries),*] - } - } -} - -/// The registry row of a routing node: one instance covers every element, -/// sources claim generic record edges, and the constructor wraps each source -/// in its union translation and stores the union as the node's layout. -fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { - let Some(routing) = routing_io(parsed) else { - return quote!(); - }; - let is_source = |field: &ParsedField| { - let ty = match &field.ty { - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, - }; - matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic)) - }; - let values_concrete = regular_fields.iter().filter(|field| !is_source(field)).all(|field| { - let (ty, lend) = match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), - }; - !contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) - }); - if !values_concrete { - return quote!(); - } - - let fn_name = &parsed.fn_name; - let entries_name = format_ident!("{}_entries", fn_name); - let arity = regular_fields.len(); - let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); - let token_name = routing.generic.to_string(); - - let input_types = regular_fields.iter().map(|field| { - if is_source(field) { - return quote!(gcore::registry::generic_record_edge_type(#token_name)); - } - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::record_edge_type::<#ty>()), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), - } - }); - let source_layouts: Vec = regular_fields - .iter() - .enumerate() - .filter(|(_, field)| is_source(field)) - .map(|(index, _)| format_ident!("__layout_{index}")) - .collect(); - let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { - let name = &field.pat_ident.ident; - if is_source(field) { - let layout = format_ident!("__layout_{index}"); - let handle = format_ident!("__handle_{index}"); - let ty = format_ident!("__ty_{index}"); - return quote! { - let #handle = inputs.next().unwrap(); - let #ty = #handle.ty().clone(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = #handle.downcast_erased::(#ty.clone())?; - }; - } - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => { - let handle = format_ident!("__handle_{index}"); - let layout = format_ident!("__in_layout_{index}"); - quote! { - let #handle = inputs.next().unwrap(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = #handle.downcast_record::<#ty>()?; - } - } - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), - } - }); - let value_layout_args = regular_fields - .iter() - .enumerate() - .filter(|(_, field)| !is_source(field) && matches!(field.ty, ParsedFieldType::Regular(_))) - .map(|(index, _)| { - let layout = format_ident!("__in_layout_{index}"); - quote!(&#layout,) - }); - let source_wraps = regular_fields.iter().enumerate().filter(|(_, field)| is_source(field)).map(|(index, field)| { - let name = &field.pat_ident.ident; - let layout = format_ident!("__layout_{index}"); - quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);) - }); - let first_source_ty = regular_fields - .iter() - .enumerate() - .find(|(_, field)| is_source(field)) - .map(|(index, _)| format_ident!("__ty_{index}")) - .expect("routing nodes have a source"); - - quote! { - pub fn #entries_name() -> ::std::vec::Vec { - vec![gcore::registry::RegistryEntry { - io: gcore::registry::NodeIOTypes::new( - gcore::concrete!(gcore::context::ContextImpl<'static>), - gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#token_name)))), - 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)* - let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); - #(#source_wraps)* - let __node = #struct_name::new(#(#names,)* &__union, #(#value_layout_args)*); - Ok(gcore::registry::EdgeHandle::new_erased( - ::std::sync::Arc::new(__node) as ::std::sync::Arc, - #first_source_ty, - )) - }, - }] - } - } -} - -fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { - let is_record = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type)); - let values_concrete = regular_fields.iter().filter(|field| !is_record(field)).all(|field| { - let (ty, lend) = match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), - }; - !contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) - }); - if !values_concrete { - return quote!(); - } - - let fn_name = &parsed.fn_name; - let entries_name = format_ident!("{}_entries", fn_name); - let arity = regular_fields.len(); - let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); - - let input_types = regular_fields.iter().map(|field| { - if is_record(field) { - return quote!(gcore::registry::generic_record_edge_type("T")); - } - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(gcore::registry::edge_type::<#ty>()), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::edge_type::<#ty>()), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), - } - }); - let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { - let name = &field.pat_ident.ident; - if is_record(field) { - let layout = format_ident!("__layout_{index}"); - let handle = format_ident!("__handle_{index}"); - let ty = format_ident!("__ty_{index}"); - return quote! { - let #handle = inputs.next().unwrap(); - let #ty = #handle.ty().clone(); - let Some(#layout) = #handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = #handle.downcast_erased::(#ty.clone())?; - }; - } - match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), - } - }); - let first_record = regular_fields.iter().position(|field| is_record(field)).expect("record-opaque nodes have a record input"); - let record_layout = format_ident!("__layout_{first_record}"); - let record_ty = format_ident!("__ty_{first_record}"); - - quote! { - pub fn #entries_name() -> ::std::vec::Vec { - vec![gcore::registry::RegistryEntry { - io: gcore::registry::NodeIOTypes::new( - gcore::concrete!(gcore::context::ContextImpl<'static>), - gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))), - 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)* - let __node = #struct_name::new(#(#names,)* &#record_layout); - Ok(gcore::registry::EdgeHandle::new_erased( - ::std::sync::Arc::new(__node) as ::std::sync::Arc, - #record_ty, - )) - }, - }] - } - } -} - -fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { - let Some(shape) = record_shape(parsed) else { - return quote!(); - }; - let carrier_in_fields = !shape.skips_carrier(); - let values_concrete = regular_fields.iter().skip(carrier_in_fields as usize).all(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => !contains_open_generic(parsed, ty) && (lend.is_some() || !type_disqualifies(ty)), - _ => false, - }); - if !values_concrete { - return quote!(); - } - - let fn_name = &parsed.fn_name; - let entries_name = format_ident!("{}_entries", fn_name); - let arity = regular_fields.len(); - let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); - let reading_secondaries = reading_secondary_indices(regular_fields, &shape); - - let input_types = regular_fields.iter().enumerate().map(|(index, field)| { - let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { - unreachable!("record nodes take no lazy inputs") - }; - if carrier_in_fields && index == 0 { - return match &shape.carrier { - RecordCarrier::Token(token) => { - let name = token.to_string(); - quote!(gcore::registry::generic_record_edge_type(#name)) - } - RecordCarrier::Read(carrier_ty) => quote!(gcore::registry::record_edge_type::<#carrier_ty>()), - RecordCarrier::None => unreachable!(), - }; - } - match field.attribute_reads.is_empty() { - true => quote!(gcore::registry::edge_type::<#ty>()), - false => quote!(gcore::registry::record_edge_type::<#ty>()), - } - }); - let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { - let name = &field.pat_ident.ident; - let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { - unreachable!("record nodes take no lazy inputs") - }; - if carrier_in_fields && index == 0 { - return quote! { - let __carrier_handle = inputs.next().unwrap(); - let __carrier_ty = __carrier_handle.ty().clone(); - let Some(__carrier_layout) = __carrier_handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = __carrier_handle.downcast_erased::(__carrier_ty.clone())?; - }; - } - if !field.attribute_reads.is_empty() { - let layout_local = format_ident!("__in_layout_{index}"); - return quote! { - let __in_handle = inputs.next().unwrap(); - let __in_ty = __in_handle.ty().clone(); - let Some(#layout_local) = __in_handle.layout().cloned() else { - return Err(gcore::registry::ConstructionError::MissingLayout); - }; - let #name = __in_handle.downcast_erased::(__in_ty)?; - }; - } - quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;) - }); - let wire_layout_arg = carrier_in_fields.then(|| quote!(&__carrier_layout,)).into_iter(); - let input_layout_args = reading_secondaries.iter().map(|index| { - let layout_local = format_ident!("__in_layout_{index}"); - quote!(&#layout_local,) - }); - let (io_output, construct_output) = match (&shape.carrier, &shape.element_write) { - (RecordCarrier::Token(token), _) => { - let name = token.to_string(); - ( - 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, - __carrier_ty, - ))), - ) - } - (_, Some(element)) => ( - quote!(gcore::registry::record_type::<#element>()), - quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))), - ), - (_, None) => unreachable!("non-token record nodes write an element"), - }; - - quote! { - pub fn #entries_name() -> ::std::vec::Vec { - vec![gcore::registry::RegistryEntry { - 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)* - let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)* #(#input_layout_args)*); - #construct_output - }, - }] - } - } -} - -fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option>> { - let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); - let open_generics: Vec<&Ident> = parsed - .fn_generics - .iter() - .filter_map(|param| match param { - GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), - _ => None, - }) - .collect(); - - let candidates: Vec> = regular_fields - .iter() - .map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() { - false => Some(implementations.iter().cloned().collect()), - true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]), - }, - ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() { - false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()), - true => open_generics - .iter() - .all(|generic| !crate::codegen::type_contains_ident(output_type, generic)) - .then(|| vec![output_type.clone()]), - }, - }) - .collect::>()?; - - let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1); - Some((0..row_count).map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()).collect()) -} diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs new file mode 100644 index 0000000000..d1c3ca97ad --- /dev/null +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -0,0 +1,715 @@ +use super::*; + +/// How a record node's primary input lowers. +#[derive(Clone)] +pub(crate) enum RecordCarrier { + /// `_: ()`: no carrier edge, the kernel writes a fresh record. + None, + /// An unbounded generic returned in the element position: the element + /// bytes carry through the copy plan and the kernel sees `ElToken`. + Token(Ident), + /// An element type read at offset 0, monomorphized per its + /// implementations list where generic. + Read(Type), +} + +/// The record io of a node fn: how the carrier lowers, the element write, +/// and the markers written and removed. Present exactly when the signature +/// declares attribute reads or writes in a shape the record tier supports; +/// malformed record io is reported by validation and generates no node impl. +#[derive(Clone)] +pub(crate) struct RecordShape { + pub(crate) carrier: RecordCarrier, + pub(crate) element_write: Option, + pub(crate) write_markers: Vec, + pub(crate) removes: Vec, +} + +impl RecordShape { + pub(crate) fn skips_carrier(&self) -> bool { + matches!(self.carrier, RecordCarrier::None) + } + + pub(crate) fn carries_element(&self) -> bool { + self.element_write.is_none() + } +} + +/// The record-tier lowering a node fn resolves to. Exactly one class per node, +/// computed once by [`analyze`]; every downstream fragment reads the class +/// instead of recomputing the classification predicates. +pub(crate) enum Class { + RecordIo(RecordShape), + Routing(RoutingIo), + Flip { carrier: bool }, + Opaque, +} + +/// The effect/return axis of a node's kernel, resolved once from the signature. +/// Orthogonal to [`Class`]: it selects the eval tail (finish / merge / spawn) +/// and the kernel signature wrapping across every class. +#[derive(Clone, Copy, PartialEq)] +pub(crate) enum Dialect { + Sync, + Interrupt, + Poll, + AsyncFn, + Future, + FutureInterrupt, +} + +pub(crate) fn dialect(parsed: &ParsedNodeFn) -> Dialect { + if parsed.is_async { + return Dialect::AsyncFn; + } + match kernel_kind(&parsed.output_type) { + KernelKind::Plain => Dialect::Sync, + KernelKind::Interrupt(_) => Dialect::Interrupt, + KernelKind::Poll(_) => Dialect::Poll, + KernelKind::Future(_) => Dialect::Future, + KernelKind::FutureInterrupt(_) => Dialect::FutureInterrupt, + } +} + +/// The result of classifying a node fn. A node with no supported lowering +/// (an async node with lazy inputs, malformed record io, or a signature no +/// class accepts) yields `None` and generates a struct and metadata but no +/// `Node` impl. +pub(crate) struct NodeModel { + pub(crate) class: Class, + pub(crate) dialect: Dialect, +} + +pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option { + if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { + return None; + } + let class = if let Some(shape) = record_shape(parsed) { + Class::RecordIo(shape) + } else if has_record_io(parsed) { + return None; + } else if let Some(routing) = routing_io(parsed) { + Class::Routing(routing) + } else if record_flip(parsed) { + Class::Flip { carrier: flip_carrier(parsed) } + } else if record_opaque(parsed) { + Class::Opaque + } else { + return None; + }; + Some(NodeModel { class, dialect: dialect(parsed) }) +} + +/// The per-field binding role, resolved once per regular field from the node +/// class and field shape. Drives the eval bindings and the lazy-edge input +/// types. The `lend` and `reads` axes stay field properties the value arms of +/// `kernel_params`/`call_args`/`value_args` consult, since they cross roles. +#[derive(Clone, Copy)] +pub(crate) enum InputRole { + RecordCarrier, + FlipCarrier, + ReadingSecondary, + LendBorrow, + RecordValue, + PlainValue, + DeriveRoutingSource, + OpaqueRecordEdge, + FlipRawLazyEdge, + FlipLazy, + RawLazy, + Lazy, +} + +impl InputRole { + /// A role that copies an element out of a record edge, so its record frame + /// must be reclaimed after the read. The step lowering wraps such a bind in + /// `mark`/`rewind`, making the stack discipline structural rather than + /// hand-threaded through each read-out arm. + pub(crate) fn reads_out(self) -> bool { + matches!(self, InputRole::ReadingSecondary | InputRole::RecordValue) + } +} + +/// The tail form of a node's eval, selected from its class and dialect: forward +/// the kernel's own record, assemble a record (io or flip carrier), or spawn a +/// source and lift its completion. +#[derive(Clone, Copy)] +pub(crate) enum Tail { + Forward, + Record, + Flip, + SpawnAsyncFn, + SpawnFuture, +} + +/// One statement group of a node's `eval` body, lowered in order: the input +/// binds first (one per input), then the numeric clamps, then the tail that +/// assembles the output record and closes the dialect. +pub(crate) enum EvalStep<'a> { + Bind(usize, &'a ParsedField), + Clamp(&'a ParsedField), + Tail(Tail), +} + +/// Whether the signature declares record-tier attribute io: value-input reads +/// 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(_))); + 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(_))) +} + +/// The value inputs of a routing node (every regular field that is not a +/// routing source), with their indices into the regular fields. +pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], routing: &RoutingIo) -> 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(&routing.generic)), + ParsedFieldType::Node(_) => false, + }) + .map(|(index, _)| index) + .collect() +} + +/// The lazy inputs declaring attribute reads, with their indices into the +/// unit-skipped regular fields. +pub(crate) fn lazy_read_fields<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a ParsedField)> { + regular_fields + .iter() + .enumerate() + .filter(|(_, field)| matches!(field.ty, ParsedFieldType::Node(_)) && !field.attribute_reads.is_empty()) + .map(|(index, field)| (index, *field)) + .collect() +} + +/// The indices (into the unit-skipped regular fields) of value inputs whose +/// reads resolve against their own wire rather than the carrier's. +pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], shape: &RecordShape) -> Vec { + regular_fields + .iter() + .enumerate() + .filter(|(index, field)| !field.attribute_reads.is_empty() && (shape.skips_carrier() || *index != 0)) + .map(|(index, _)| index) + .collect() +} + +/// Every attribute read in field order with the owning field's index, flat so +/// read slots are numbered across inputs. +pub(crate) fn field_reads<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a AttributeRead)> { + regular_fields + .iter() + .enumerate() + .flat_map(|(index, field)| field.attribute_reads.iter().map(move |read| (index, read))) + .collect() +} + +/// Substitutes bare generic idents with their row-assigned types. +pub(crate) fn substitute_ident_types(ty: &Type, assignments: &[(Ident, Type)]) -> Type { + struct Subst<'a> { + assignments: &'a [(Ident, Type)], + } + + impl VisitMut for Subst<'_> { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::Path(path) = ty + && path.qself.is_none() + && let Some(ident) = path.path.get_ident() + && let Some((_, replacement)) = self.assignments.iter().find(|(generic, _)| generic == ident) + { + *ty = replacement.clone(); + return; + } + syn::visit_mut::visit_type_mut(self, ty); + } + } + + let mut ty = ty.clone(); + Subst { assignments }.visit_type_mut(&mut ty); + ty +} + +/// Replaces the routing generic in a derive-routing kernel's return type with +/// the routing record value, since the kernel's edges rebind to '__record. +pub(crate) fn substitute_routing_record(output: &Type, generic: &Ident, core_types: &TokenStream2) -> Type { + struct Subst<'a> { + generic: &'a Ident, + replacement: Type, + } + + impl VisitMut for Subst<'_> { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::Path(path) = ty + && path.qself.is_none() + && path.path.get_ident() == Some(self.generic) + { + *ty = self.replacement.clone(); + return; + } + syn::visit_mut::visit_type_mut(self, ty); + } + } + + let mut ty = output.clone(); + let mut subst = Subst { + generic, + replacement: syn::parse_quote!(#core_types::record::RecordValue<'__record>), + }; + subst.visit_type_mut(&mut ty); + ty +} + +pub(crate) fn inject_attr_lifetimes(output: &Type) -> Option { + struct Injector { + changed: bool, + } + + impl VisitMut for Injector { + fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) { + if segment.ident == "Attr" + && let PathArguments::AngleBracketed(args) = &mut segment.arguments + && !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_))) + { + args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__attr", proc_macro2::Span::call_site()))); + self.changed = true; + } + syn::visit_mut::visit_path_segment_mut(self, segment); + } + } + + let mut ty = output.clone(); + let mut injector = Injector { changed: false }; + injector.visit_type_mut(&mut ty); + injector.changed.then_some(ty) +} + +pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + parsed + .fn_generics + .iter() + .any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident))) +} + +pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option { + let ident = bare_ident(ty)?.clone(); + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + parsed + .fn_generics + .iter() + .find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?; + if let Some(where_clause) = &parsed.where_clause + && tokens_contain_ident(where_clause.to_token_stream(), &ident) + { + return None; + } + Some(ident) +} + +pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { + let value = match kernel_kind(&parsed.output_type) { + KernelKind::Plain => parsed.output_type.clone(), + KernelKind::Interrupt(inner) => inner, + _ => 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(_))); + if !has_reads && writes.is_none() { + return None; + } + if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { + return None; + } + let reads_well_placed = parsed.fields.iter().all(|field| { + field.attribute_reads.is_empty() || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) + }); + if !reads_well_placed { + return None; + } + let carrier_field = parsed.fields.first()?; + if carrier_field.is_data_field { + return None; + } + let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else { + return None; + }; + let carrier = match ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None, + ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() { + Some(token) => RecordCarrier::Token(token), + None => { + if contains_open_generic(parsed, ty) { + return None; + } + RecordCarrier::Read(ty.clone()) + } + }, + }; + let (element, write_markers, removes) = match writes { + Some(RecordWrites { element, markers, removes }) => (element, markers, removes), + None => (value, Vec::new(), Vec::new()), + }; + let element_write = match &carrier { + RecordCarrier::Token(token) => match bare_ident(&element) { + Some(ident) if ident == token => None, + _ => return None, + }, + _ => { + if contains_open_generic(parsed, &element) { + return None; + } + Some(element) + } + }; + if matches!(carrier, RecordCarrier::None) && !removes.is_empty() { + return None; + } + Some(RecordShape { + carrier, + element_write, + write_markers, + removes, + }) +} + +pub(crate) fn is_poll_kernel(output: &Type) -> bool { + matches!(kernel_kind(output), KernelKind::Poll(_)) +} + +/// A routing family: an unbounded generic shared by lazy inputs (and +/// optionally the first parameter) and returned whole, instantiated at +/// `RecordValue` so opaque records flow through the kernel. Detected only +/// when the family's fields carry no implementations lists, so the existing +/// per-type row spelling keeps its meaning. +#[derive(Clone)] +pub(crate) struct RoutingIo { + pub(crate) generic: Ident, +} + +/// Whether a flipped node's primary input is a carrier: the first parameter +/// after the context, when it is an owned or lent value input. A carrier's +/// fields pass through to the output; every production layout is element-only +/// until attribute adoption, so the copy plan is empty and behavior is +/// unchanged. Async kernels carry fields per eval around the slot (only the +/// element crosses the future boundary), so their carrier must be owned: the +/// future captures the element by value. +pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool { + if !record_flip(parsed) { + return false; + } + let Some(first) = parsed.fields.first() else { return false }; + if first.is_data_field { + return false; + } + let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &first.ty else { + return false; + }; + if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) { + return false; + } + let async_kernel = parsed.is_async || matches!(kernel_kind(&parsed.output_type), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)); + !(async_kernel && lend.is_some()) +} + +/// Whether a plain node's lowering flips onto record wires: sync, +/// fully-concrete value-input nodes in this cut; batch, shader, async, lend, +/// lazy, and generic nodes keep the plain lowering until their record forms +/// land. +pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { + if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() { + return false; + } + // Shader nodes flip like any value node: the kernel doubles as the + // shader body on the spirv target, but the struct and Node impl are + // std-gated, so the record machinery never reaches the shader build. + if parsed.attributes.batch.is_some() || parsed.attributes.plain { + return false; + } + if type_disqualifies(&slot_value_type(&parsed.output_type)) { + return false; + } + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + for param in &parsed.fn_generics { + match param { + GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_ident.as_ref() => {} + // Registry rows assign a generic by unifying a field's type with + // the row's, so a generic without an extractable position keeps + // the plain lowering. A `skip_impl` node's rows are hand-written + // with explicit types, so no extractable position is needed. + GenericParam::Type(type_param) => { + let extractable = parsed.fields.iter().filter(|field| !field.is_data_field).any(|field| { + let ty = match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, + }; + generic_extractable(ty, &type_param.ident) + }); + if !extractable && !parsed.attributes.skip_impl { + return false; + } + } + GenericParam::Lifetime(_) | GenericParam::Const(_) => return false, + } + } + true +} + +/// Whether unifying a value of `field_ty`'s shape can bind `generic`: the +/// generic sits bare or under path type arguments, the shapes +/// [`generic_assignment`] walks. +pub(crate) fn generic_extractable(field_ty: &Type, generic: &Ident) -> bool { + match field_ty { + Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic) => true, + Type::Path(path) => path.path.segments.iter().any(|segment| match &segment.arguments { + PathArguments::AngleBracketed(args) => args.args.iter().any(|argument| match argument { + GenericArgument::Type(inner) => generic_extractable(inner, generic), + _ => false, + }), + _ => false, + }), + _ => false, + } +} + +/// Binds `generic` by unifying `field_ty` against `row_ty`: where the field +/// names the generic, the row's corresponding subtree is the assignment. +pub(crate) fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident) -> Option { + if matches!(field_ty, Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic)) { + return Some(row_ty.clone()); + } + let (Type::Path(field_path), Type::Path(row_path)) = (field_ty, row_ty) else { + return None; + }; + let field_segment = field_path.path.segments.last()?; + let row_segment = row_path.path.segments.last()?; + let (PathArguments::AngleBracketed(field_args), PathArguments::AngleBracketed(row_args)) = (&field_segment.arguments, &row_segment.arguments) else { + return None; + }; + field_args.args.iter().zip(row_args.args.iter()).find_map(|(field_arg, row_arg)| match (field_arg, row_arg) { + (GenericArgument::Type(field_inner), GenericArgument::Type(row_inner)) => generic_assignment(field_inner, row_inner, generic), + _ => None, + }) +} + +pub(crate) fn is_record_value(ty: &Type) -> bool { + matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RecordValue")) +} + +/// Whether a kernel operates on whole records: it names `RecordValue` in its +/// output, receives raw record edges paired with the node's layout, and +/// takes on the record APIs' unsafe contracts itself. +pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool { + is_record_value(&slot_value_type(&parsed.output_type)) +} + +pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { + if has_record_io(parsed) || parsed.is_async { + return None; + } + if !matches!(kernel_kind(&parsed.output_type), KernelKind::Plain | KernelKind::Interrupt(_)) { + return None; + } + let value = slot_value_type(&parsed.output_type); + let Type::Path(path) = &value else { return None }; + let ident = path.path.get_ident()?.clone(); + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + parsed + .fn_generics + .iter() + .find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?; + if let Some(where_clause) = &parsed.where_clause + && tokens_contain_ident(where_clause.to_token_stream(), &ident) + { + return None; + } + let mut sources = 0; + for (index, field) in parsed.fields.iter().enumerate() { + match &field.ty { + ParsedFieldType::Node(NodeParsedField { + output_type, + input_type, + implementations, + }) => { + if bare_ident(output_type) == Some(&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() { + return None; + } + sources += 1; + } else if type_contains_ident(output_type, &ident) || type_contains_ident(input_type, &ident) { + return None; + } + } + ParsedFieldType::Regular(RegularParsedField { ty, implementations, lend, .. }) => { + if bare_ident(ty) == Some(&ident) { + if index != 0 || field.is_data_field || !implementations.is_empty() || lend.is_some() { + return None; + } + sources += 1; + } else if type_contains_ident(ty, &ident) { + return None; + } + } + } + } + (sources > 0).then(|| RoutingIo { generic: ident }) +} + +pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> { + let Type::Path(path) = ty else { return None }; + path.path.get_ident() +} + +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, + proc_macro2::TokenTree::Group(group) => tokens_contain_ident(group.stream(), ident), + _ => false, + }) +} + +pub(crate) fn slot_value_type(output: &Type) -> Type { + match kernel_kind(output) { + KernelKind::Plain => output.clone(), + KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, + KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => match kernel_kind(&payload) { + KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner, + _ => payload, + }, + } +} + +pub(crate) fn is_source_kernel(output: &Type) -> bool { + matches!(kernel_kind(output), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)) +} + +pub(crate) enum KernelKind { + Plain, + Interrupt(Type), + Poll(Type), + Future(Type), + FutureInterrupt(Type), +} + +pub(crate) fn source_future_payload(segment: &syn::PathSegment) -> Type { + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return syn::parse_quote!(()); + }; + args.args + .iter() + .find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }) + .unwrap_or_else(|| syn::parse_quote!(())) +} + +pub(crate) fn kernel_kind(output: &Type) -> KernelKind { + let plain = || KernelKind::Plain; + let Type::Path(path) = output else { return plain() }; + let Some(segment) = path.path.segments.last() else { return plain() }; + match segment.ident.to_string().as_str() { + "GPoll" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let inner = args.args.iter().find_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty.clone()), + _ => None, + }); + inner.map(KernelKind::Poll).unwrap_or_else(plain) + } + "SourceFuture" => KernelKind::Future(source_future_payload(segment)), + "Result" => { + let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() }; + let mut types = args.args.iter().filter_map(|argument| match argument { + GenericArgument::Type(ty) => Some(ty), + _ => None, + }); + let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else { + return plain(); + }; + if error_path.path.segments.last().is_none_or(|segment| segment.ident != "Interrupt") { + return plain(); + } + if let Type::Path(inner_path) = inner + && let Some(inner_segment) = inner_path.path.segments.last() + && inner_segment.ident == "SourceFuture" + { + return KernelKind::FutureInterrupt(source_future_payload(inner_segment)); + } + KernelKind::Interrupt(inner.clone()) + } + _ => plain(), + } +} + +pub(crate) fn context_param(parsed: &ParsedNodeFn) -> Option<&TypeParam> { + let Type::Path(path) = &parsed.input.ty else { + return None; + }; + let ident = path.path.get_ident()?; + parsed.fn_generics.iter().find_map(|param| match param { + GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param), + _ => None, + }) +} + +pub(crate) fn type_disqualifies(ty: &Type) -> bool { + struct Disqualifier { + found: bool, + } + + impl<'ast> Visit<'ast> for Disqualifier { + fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) { + self.found = true; + } + + fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) { + self.found = true; + } + + fn visit_lifetime(&mut self, _: &'ast Lifetime) { + self.found = true; + } + } + + let mut visitor = Disqualifier { found: false }; + visitor.visit_type(ty); + visitor.found +} + +pub(crate) fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 { + let TypeParamBound::Trait(trait_bound) = bound else { + return quote!(#bound); + }; + let Some(segment) = trait_bound.path.segments.last() else { + return quote!(#bound); + }; + if segment.ident != "ExtractArena" { + return quote!(#bound); + } + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return quote!(#bound); + }; + if args.args.len() != 1 { + return quote!(#bound); + } + let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else { + return quote!(#bound); + }; + quote!(#core_types::context::ExtractArena) +} diff --git a/node-graph/node-macro/src/codegen/entries.rs b/node-graph/node-macro/src/codegen/entries.rs new file mode 100644 index 0000000000..5621417051 --- /dev/null +++ b/node-graph/node-macro/src/codegen/entries.rs @@ -0,0 +1,519 @@ +use super::*; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{GenericParam, Ident, Type}; + +pub(crate) fn entries_tokens(parsed: &ParsedNodeFn, class: &Class, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 { + if !data_field_generic_idents.is_empty() { + return quote!(); + } + match class { + Class::RecordIo(_) => record_entries_tokens(parsed, struct_name, regular_fields), + Class::Routing(_) => routing_entries_tokens(parsed, struct_name, regular_fields), + Class::Flip { .. } => flip_entries_tokens(parsed, struct_name, regular_fields), + Class::Opaque => record_opaque_entries_tokens(parsed, struct_name, regular_fields), + } +} + +/// The registry rows of a flipped plain node: every wire is a record wire, +/// inputs resolve their layouts off the claimed handles, and the output is an +/// element-only record of the kernel's return type. +fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { + let Some(rows) = implementation_rows(parsed, regular_fields) else { + return quote!(); + }; + let rows: Vec<&Vec> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect(); + if rows.is_empty() { + return quote!(); + } + let output = slot_value_type(&parsed.output_type); + + let field_type = |field: &ParsedField| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(), + }; + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + let generic_positions: Option> = parsed + .fn_generics + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), + _ => None, + }) + .map(|generic| { + regular_fields + .iter() + .position(|field| generic_extractable(&field_type(field), generic)) + .map(|index| (generic.clone(), index)) + }) + .collect(); + let Some(generic_positions) = generic_positions else { + return quote!(); + }; + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let node_underscores: Vec = regular_fields.iter().map(|_| quote!(_)).collect(); + + // Shorthand associated types in the output only resolve against the + // generics' bounds, so rows name the output through a bounded alias. Only + // output-reaching generics (directly or through a kept bound) may appear: + // an unused alias parameter is an error. + let candidate_params: Vec<&GenericParam> = parsed + .fn_generics + .iter() + .filter(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref())) + .collect(); + let param_ident = |param: &&GenericParam| match param { + GenericParam::Type(type_param) => type_param.ident.clone(), + _ => unreachable!("candidates are type parameters"), + }; + let mut kept: Vec = candidate_params.iter().map(|param| type_contains_ident(&output, ¶m_ident(param))).collect(); + loop { + let mut grew = false; + for index in 0..candidate_params.len() { + if kept[index] { + continue; + } + let ident = param_ident(&candidate_params[index]); + let mentioned = candidate_params.iter().zip(&kept).any(|(param, kept)| { + *kept + && match param { + GenericParam::Type(type_param) => type_param.bounds.iter().any(|bound| { + let bound: Type = syn::parse_quote!(dyn #bound); + type_contains_ident(&bound, &ident) + }), + _ => false, + } + }); + if mentioned { + kept[index] = true; + grew = true; + } + } + if !grew { + break; + } + } + let alias_params: Vec<&GenericParam> = candidate_params.iter().zip(&kept).filter(|(_, kept)| **kept).map(|(param, _)| *param).collect(); + let alias_param_idents: Vec = alias_params.iter().map(|param| param_ident(param)).collect(); + let alias_param_tokens: Vec = alias_params.iter().map(|param| quote!(#param)).collect(); + let output_alias = format_ident!("__{}_output", fn_name); + let alias_def = match alias_param_tokens.is_empty() { + true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;), + false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;), + }; + + let entries = rows.iter().filter_map(|row| { + let assignments: Vec<(Ident, Type)> = generic_positions + .iter() + .map(|(generic, index)| generic_assignment(&field_type(regular_fields[*index]), &row[*index], generic).map(|assigned| (generic.clone(), assigned))) + .collect::>()?; + if type_disqualifies(&substitute_ident_types(&output, &assignments)) { + return None; + } + let assignment_types: Vec = assignments.iter().map(|(_, ty)| quote!(#ty)).collect(); + let alias_arguments: Vec = assignments + .iter() + .filter(|(generic, _)| alias_param_idents.contains(generic)) + .map(|(_, ty)| quote!(#ty)) + .collect(); + let row_output = match alias_arguments.is_empty() { + true => quote!(#output_alias), + false => quote!(#output_alias<#(#alias_arguments),*>), + }; + let assignment_types = assignment_types.iter(); + let turbofish = quote!(::<#(#node_underscores,)* #(#assignment_types,)*>); + let input_types = row.iter().map(|ty| quote!(gcore::registry::record_edge_type::<#ty>())); + let downcasts = names.iter().zip(row.iter()).enumerate().map(|(index, (name, ty))| { + let handle = format_ident!("__handle_{index}"); + let layout = format_ident!("__layout_{index}"); + quote! { + let #handle = inputs.next().unwrap(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_record::<#ty>()?; + } + }); + let layout_args = (0..arity).map(|index| { + let layout = format_ident!("__layout_{index}"); + quote!(&#layout,) + }); + Some(quote! { + gcore::registry::RegistryEntry { + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + gcore::registry::record_type::<#row_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)* + let __node = #struct_name #turbofish::new(#(#names,)* #(#layout_args)*); + Ok(gcore::registry::EdgeHandle::new_record::<#row_output>(::std::sync::Arc::new(__node) as ::std::sync::Arc)) + }, + } + }) + }); + let entries: Vec = entries.collect(); + if entries.is_empty() { + return quote!(); + } + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + #alias_def + vec![#(#entries),*] + } + } +} + +/// The registry row of a routing node: one instance covers every element, +/// sources claim generic record edges, and the constructor wraps each source +/// in its union translation and stores the union as the node's layout. +fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { + let Some(routing) = routing_io(parsed) else { + return quote!(); + }; + let is_source = |field: &ParsedField| { + let ty = match &field.ty { + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, + }; + matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic)) + }; + let values_concrete = regular_fields.iter().filter(|field| !is_source(field)).all(|field| { + let (ty, lend) = match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), + }; + !contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) + }); + if !values_concrete { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let token_name = routing.generic.to_string(); + + let input_types = regular_fields.iter().map(|field| { + if is_source(field) { + return quote!(gcore::registry::generic_record_edge_type(#token_name)); + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::record_edge_type::<#ty>()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), + } + }); + let source_layouts: Vec = regular_fields + .iter() + .enumerate() + .filter(|(_, field)| is_source(field)) + .map(|(index, _)| format_ident!("__layout_{index}")) + .collect(); + let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + if is_source(field) { + let layout = format_ident!("__layout_{index}"); + let handle = format_ident!("__handle_{index}"); + let ty = format_ident!("__ty_{index}"); + return quote! { + let #handle = inputs.next().unwrap(); + let #ty = #handle.ty().clone(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_erased::(#ty.clone())?; + }; + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => { + let handle = format_ident!("__handle_{index}"); + let layout = format_ident!("__in_layout_{index}"); + quote! { + let #handle = inputs.next().unwrap(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_record::<#ty>()?; + } + } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), + } + }); + let value_layout_args = regular_fields + .iter() + .enumerate() + .filter(|(_, field)| !is_source(field) && matches!(field.ty, ParsedFieldType::Regular(_))) + .map(|(index, _)| { + let layout = format_ident!("__in_layout_{index}"); + quote!(&#layout,) + }); + let source_wraps = regular_fields.iter().enumerate().filter(|(_, field)| is_source(field)).map(|(index, field)| { + let name = &field.pat_ident.ident; + let layout = format_ident!("__layout_{index}"); + quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);) + }); + let first_source_ty = regular_fields + .iter() + .enumerate() + .find(|(_, field)| is_source(field)) + .map(|(index, _)| format_ident!("__ty_{index}")) + .expect("routing nodes have a source"); + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![gcore::registry::RegistryEntry { + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#token_name)))), + 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)* + let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); + #(#source_wraps)* + let __node = #struct_name::new(#(#names,)* &__union, #(#value_layout_args)*); + Ok(gcore::registry::EdgeHandle::new_erased( + ::std::sync::Arc::new(__node) as ::std::sync::Arc, + #first_source_ty, + )) + }, + }] + } + } +} + +fn record_opaque_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { + let is_record = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if is_record_value(output_type)); + let values_concrete = regular_fields.iter().filter(|field| !is_record(field)).all(|field| { + let (ty, lend) = match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => (ty, lend.is_some()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => (output_type, false), + }; + !contains_open_generic(parsed, ty) && (lend || !type_disqualifies(ty)) + }); + if !values_concrete { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + + let input_types = regular_fields.iter().map(|field| { + if is_record(field) { + return quote!(gcore::registry::generic_record_edge_type("T")); + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(gcore::registry::edge_type::<#ty>()), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::edge_type::<#ty>()), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), + } + }); + let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + if is_record(field) { + let layout = format_ident!("__layout_{index}"); + let handle = format_ident!("__handle_{index}"); + let ty = format_ident!("__ty_{index}"); + return quote! { + let #handle = inputs.next().unwrap(); + let #ty = #handle.ty().clone(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_erased::(#ty.clone())?; + }; + } + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), + } + }); + let first_record = regular_fields.iter().position(|field| is_record(field)).expect("record-opaque nodes have a record input"); + let record_layout = format_ident!("__layout_{first_record}"); + let record_ty = format_ident!("__ty_{first_record}"); + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![gcore::registry::RegistryEntry { + io: gcore::registry::NodeIOTypes::new( + gcore::concrete!(gcore::context::ContextImpl<'static>), + gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed("T")))), + 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)* + let __node = #struct_name::new(#(#names,)* &#record_layout); + Ok(gcore::registry::EdgeHandle::new_erased( + ::std::sync::Arc::new(__node) as ::std::sync::Arc, + #record_ty, + )) + }, + }] + } + } +} + +fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 { + let Some(shape) = record_shape(parsed) else { + return quote!(); + }; + let carrier_in_fields = !shape.skips_carrier(); + let values_concrete = regular_fields.iter().skip(carrier_in_fields as usize).all(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => !contains_open_generic(parsed, ty) && (lend.is_some() || !type_disqualifies(ty)), + _ => false, + }); + if !values_concrete { + return quote!(); + } + + let fn_name = &parsed.fn_name; + let entries_name = format_ident!("{}_entries", fn_name); + let arity = regular_fields.len(); + let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let reading_secondaries = reading_secondary_indices(regular_fields, &shape); + + let input_types = regular_fields.iter().enumerate().map(|(index, field)| { + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("record nodes take no lazy inputs") + }; + if carrier_in_fields && index == 0 { + return match &shape.carrier { + RecordCarrier::Token(token) => { + let name = token.to_string(); + quote!(gcore::registry::generic_record_edge_type(#name)) + } + RecordCarrier::Read(carrier_ty) => quote!(gcore::registry::record_edge_type::<#carrier_ty>()), + RecordCarrier::None => unreachable!(), + }; + } + match field.attribute_reads.is_empty() { + true => quote!(gcore::registry::edge_type::<#ty>()), + false => quote!(gcore::registry::record_edge_type::<#ty>()), + } + }); + let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { + let name = &field.pat_ident.ident; + let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { + unreachable!("record nodes take no lazy inputs") + }; + if carrier_in_fields && index == 0 { + return quote! { + let __carrier_handle = inputs.next().unwrap(); + let __carrier_ty = __carrier_handle.ty().clone(); + let Some(__carrier_layout) = __carrier_handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = __carrier_handle.downcast_erased::(__carrier_ty.clone())?; + }; + } + if !field.attribute_reads.is_empty() { + let layout_local = format_ident!("__in_layout_{index}"); + return quote! { + let __in_handle = inputs.next().unwrap(); + let __in_ty = __in_handle.ty().clone(); + let Some(#layout_local) = __in_handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = __in_handle.downcast_erased::(__in_ty)?; + }; + } + quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;) + }); + let wire_layout_arg = carrier_in_fields.then(|| quote!(&__carrier_layout,)).into_iter(); + let input_layout_args = reading_secondaries.iter().map(|index| { + let layout_local = format_ident!("__in_layout_{index}"); + quote!(&#layout_local,) + }); + let (io_output, construct_output) = match (&shape.carrier, &shape.element_write) { + (RecordCarrier::Token(token), _) => { + let name = token.to_string(); + ( + 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, + __carrier_ty, + ))), + ) + } + (_, Some(element)) => ( + quote!(gcore::registry::record_type::<#element>()), + quote!(Ok(gcore::registry::EdgeHandle::new_record::<#element>(::std::sync::Arc::new(__node)))), + ), + (_, None) => unreachable!("non-token record nodes write an element"), + }; + + quote! { + pub fn #entries_name() -> ::std::vec::Vec { + vec![gcore::registry::RegistryEntry { + 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)* + let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)* #(#input_layout_args)*); + #construct_output + }, + }] + } + } +} + +fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option>> { + let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone()); + let open_generics: Vec<&Ident> = parsed + .fn_generics + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident), + _ => None, + }) + .collect(); + + let candidates: Vec> = regular_fields + .iter() + .map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().cloned().collect()), + true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]), + }, + ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() { + false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()), + true => open_generics + .iter() + .all(|generic| !crate::codegen::type_contains_ident(output_type, generic)) + .then(|| vec![output_type.clone()]), + }, + }) + .collect::>()?; + + let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1); + Some((0..row_count).map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()).collect()) +} diff --git a/node-graph/node-macro/src/codegen/metadata.rs b/node-graph/node-macro/src/codegen/metadata.rs new file mode 100644 index 0000000000..5e8444ed1a --- /dev/null +++ b/node-graph/node-macro/src/codegen/metadata.rs @@ -0,0 +1,173 @@ +use super::*; + +/// Generates strongly typed utilites to access inputs +pub(crate) fn generate_node_input_references( + parsed: &ParsedNodeFn, + fn_generics: &[crate::GenericParam], + field_idents: &[&PatIdent], + core_types: &TokenStream2, + identifier: &Ident, + cfg: &TokenStream2, +) -> TokenStream2 { + let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake)); + + let mut generated_input_accessor = Vec::new(); + if !parsed.attributes.skip_impl { + 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() { + let mut ty = match &parsed_input.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty, + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type, + } + .clone(); + + // We only want the necessary generics. + let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty); + // TODO: figure out a better name that doesn't conflict with so many types + let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal)); + let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter()); + + // Only create structs with phantom data where necessary. + generated_input_accessor.push(if phantom_data_declerations.is_empty() { + quote! { + pub struct #struct_name; + } + } else { + quote! { + pub struct #struct_name <#(#used),*>{ + #(#phantom_data_declerations,)* + } + } + }); + generated_input_accessor.push(quote! { + impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> { + const INDEX: usize = #input_index; + fn identifier() -> #core_types::ProtoNodeIdentifier { + #inputs_module_name::IDENTIFIER.clone() + } + type Result = #ty; + } + }) + } + } + + quote! { + #cfg + pub mod #inputs_module_name { + use super::*; + + /// The `ProtoNodeIdentifier` of this node without any generics attached to it + pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier(); + #(#generated_input_accessor)* + } + } +} + +/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors. +pub(crate) fn generate_phantom_data<'a>(fn_generics: impl Iterator) -> (Vec, Vec) { + let mut phantom_data_declerations = Vec::new(); + let mut fn_generic_params = Vec::new(); + + for fn_generic_param in fn_generics { + let field_name = format_ident!("phantom_{}", phantom_data_declerations.len()); + + match fn_generic_param { + crate::GenericParam::Lifetime(lifetime_param) => { + let lifetime = &lifetime_param.lifetime; + + fn_generic_params.push(quote! {#lifetime}); + phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>}) + } + crate::GenericParam::Type(type_param) => { + let generic_name = &type_param.ident; + + fn_generic_params.push(quote! {#generic_name}); + phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>}); + } + _ => {} + } + } + (fn_generic_params, phantom_data_declerations) +} + + +/// Get only the necessary generics. +struct FilterUsedGenerics { + all: Vec, + used: Vec, +} + +impl VisitMut for FilterUsedGenerics { + fn visit_lifetime_mut(&mut self, used_lifetime: &mut Lifetime) { + for (generic, used) in self.all.iter().zip(self.used.iter_mut()) { + let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue }; + if used_lifetime == &lifetime_param.lifetime { + *used = true; + } + } + } + + fn visit_path_mut(&mut self, path: &mut syn::Path) { + for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() { + let crate::GenericParam::Type(type_param) = generic else { continue }; + if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident { + *used = true; + // Sometimes the generics conflict with the type name so we rename the generics. + path.segments[0].ident = format_ident!("G{index}"); + } + } + for mut el in Punctuated::pairs_mut(&mut path.segments) { + self.visit_path_segment_mut(el.value_mut()); + } + } +} + +impl FilterUsedGenerics { + fn new(fn_generics: &[crate::GenericParam]) -> (Vec, Self) { + let mut all_possible_generics = fn_generics.to_vec(); + // The 'n lifetime may also be needed; we must add it in + all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site())))); + + let modified = all_possible_generics + .iter() + .cloned() + .enumerate() + .map(|(index, mut generic)| { + let crate::GenericParam::Type(type_param) = &mut generic else { return generic }; + // Sometimes the generics conflict with the type name so we rename the generics. + type_param.ident = format_ident!("G{index}"); + generic + }) + .collect::>(); + + let generic_collector = Self { + used: vec![false; all_possible_generics.len()], + all: all_possible_generics, + }; + + (modified, generic_collector) + } + + fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator { + modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value) + } + + fn filter_unnecessary_generics(&mut self, modified: &mut Vec, ty: &mut Type) -> Vec { + self.used.fill(false); + + // Find out which generics are necessary to support the node input + self.visit_type_mut(ty); + + // Sometimes generics may reference other generics. This is a non-optimal way of dealing with that. + for _ in 0..=self.all.len() { + for (index, item) in modified.iter_mut().enumerate() { + if self.used[index] { + self.visit_generic_param_mut(item); + } + } + } + + self.used(&*modified).cloned().collect() + } +}