Files
Graphite/node-graph/node-macro/src/codegen.rs
Keavon Chambers 2f24459344 Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes (#4397)
* Rework Gradient into a newtype of List<Color> with optional position and midpoint attributes

* Fix Vello stopless-gradient fallback coverage, empty legacy gradient tables, the node docs gradient swatch, NaN position elision, and wired setter input overwrites
2026-08-03 04:05:02 -07:00

1290 lines
46 KiB
Rust

use crate::parsing::*;
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, format_ident, quote, quote_spanned};
use std::sync::atomic::AtomicU64;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{Error, Expr, ExprPath, Ident, PatIdent, Token, WhereClause, WherePredicate, parse_quote};
static NODE_ID: AtomicU64 = AtomicU64::new(0);
pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
let ParsedNodeFn {
vis,
attributes,
fn_name,
struct_name,
mod_name,
fn_generics,
where_clause,
input,
output_type,
is_async,
fields,
body,
description,
..
} = parsed;
let core_types = crate_ident.gcore()?;
let category = attributes
.category
.as_ref()
.expect("The 'category' attribute is required and should be checked during parsing, but was not found during codegen");
let mod_name = format_ident!("_{}_mod", mod_name);
let display_name = match &attributes.display_name.as_ref() {
Some(lit) => lit.value(),
None => struct_name.to_string().to_case(Case::Title),
};
let struct_name = format_ident!("{}Node", struct_name);
// Separate data fields from regular fields
let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields.iter().partition(|f| f.is_data_field);
// The primary's position among the regular fields, which `#[scope]` fields may precede (`#[data]` fields are already partitioned out)
let primary_regular_index = regular_fields.iter().position(|field| !field.is_environment());
// Extract function generics used by data fields
let data_field_generics: Vec<_> = fn_generics
.iter()
.filter(|generic| {
let generic_ident = match generic {
syn::GenericParam::Type(type_param) => &type_param.ident,
_ => return false,
};
// Check if this generic is used in any data field type
data_fields
.iter()
.any(|field| field.ty.regular().is_some_and(|regular| type_contains_ident(&regular.ty, generic_ident)))
})
.cloned()
.collect();
// Node generics for regular fields (Node0, Node1, ...)
let node_generics: Vec<Ident> = regular_fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
// An `Item<T>` primary input declares the node as an element-wise rank-0 kernel over element type `T`
let primary_element = primary_item_element(parsed);
let element_wise = primary_element.is_some();
let mapped_variant = generates_mapped_variant(parsed);
let list_content_variant = generates_list_content_variant(parsed);
// Extract just the idents from data_field_generics for struct type parameters
let data_field_generic_idents: Vec<Ident> = data_field_generics
.iter()
.filter_map(|gp| match gp {
syn::GenericParam::Type(tp) => Some(tp.ident.clone()),
_ => None,
})
.collect();
// Combined struct type parameters: data field generic idents (T, U, ...) + node generics (Node0, Node1, ...)
// For struct type instantiation: MemoizeNode<T, Node0>
let struct_type_params: Vec<Ident> = data_field_generic_idents.iter().cloned().chain(node_generics.iter().cloned()).collect();
// Combined struct generic parameters with bounds for struct definition
// struct MemoizeNode<T: Clone, Node0>
let struct_generic_params: Vec<TokenStream2> = data_field_generics.iter().map(|gp| quote!(#gp)).chain(node_generics.iter().map(|id| quote!(#id))).collect();
let input_ident = &input.pat_ident;
let context_features = &input.context_features;
// Regular field idents and names (for function parameters)
let field_idents: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident).collect();
let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect();
let regular_field_names: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
let data_field_names: Vec<_> = data_fields.iter().map(|f| &f.pat_ident.ident).collect();
// Only regular fields have input names/descriptions (for UI)
let input_names: Vec<_> = regular_fields
.iter()
.map(|f| &f.name)
.zip(regular_field_names.iter())
.map(|zipped| match zipped {
(Some(name), _) => name.value(),
(_, name) => name.to_string().to_case(Case::Title),
})
.collect();
let input_hidden = regular_field_names.iter().map(|name| name.to_string().starts_with('_')).collect::<Vec<_>>();
let input_descriptions: Vec<_> = regular_fields.iter().map(|f| &f.description).collect();
// Generate struct fields: data fields (concrete types) + regular fields (generic types)
let data_field_defs = data_fields.iter().map(|field| {
let name = &field.pat_ident.ident;
let Some(RegularParsedField { ty, .. }) = field.ty.regular() else {
unreachable!("Data fields cannot be lazy `Node` inputs")
};
quote! { pub(super) #name: #ty }
});
let regular_field_defs = regular_field_names.iter().zip(node_generics.iter()).map(|(name, r#gen)| {
quote! { pub(super) #name: #r#gen }
});
let struct_fields: Vec<_> = data_field_defs.chain(regular_field_defs).collect();
let mut future_idents = Vec::new();
// Data fields get passed as references to the underlying function
let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect();
let data_field_types: Vec<_> = data_fields
.iter()
.map(|field| {
let Some(RegularParsedField { ty, .. }) = field.ty.regular() else {
unreachable!("Data fields cannot be lazy `Node` inputs")
};
let ty = ty.clone();
quote!(&#ty)
})
.collect();
// Regular fields have types passed to the function
let field_types: Vec<_> = regular_fields
.iter()
.map(|field| match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_type, input_type, .. }) => match parsed.is_async {
true => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = impl core::future::Future<Output=#output_type>>),
false => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = #output_type>),
},
value => value.regular().expect("a non-node field is a value field").ty.clone(),
})
.collect();
// Only regular fields have UI metadata (data fields are internal state)
let widget_override: Vec<_> = regular_fields
.iter()
.map(|field| match &field.widget_override {
ParsedWidgetOverride::None => quote!(RegistryWidgetOverride::None),
ParsedWidgetOverride::Hidden => quote!(RegistryWidgetOverride::Hidden),
ParsedWidgetOverride::String(lit_str) => quote!(RegistryWidgetOverride::String(#lit_str)),
ParsedWidgetOverride::Custom(lit_str) => quote!(RegistryWidgetOverride::Custom(#lit_str)),
})
.collect();
let value_sources: Vec<_> = regular_fields
.iter()
.map(|field| match field.ty.regular() {
Some(RegularParsedField { value_source, .. }) => match value_source {
ParsedValueSource::Default(data) => {
// Check if the data is a string literal by parsing the token stream
let data_str = data.to_string();
if data_str.starts_with('"') && data_str.ends_with('"') && data_str.len() >= 2 {
quote!(RegistryValueSource::Default(#data))
} else {
quote!(RegistryValueSource::Default(stringify!(#data)))
}
}
ParsedValueSource::Scope(data) => {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }) = data.as_ref() {
quote!(RegistryValueSource::Scope(#data))
} else {
quote!(RegistryValueSource::Scope(#data.as_static_str()))
}
}
_ => quote!(RegistryValueSource::None),
},
None => quote!(RegistryValueSource::None),
})
.collect();
let default_colors: Vec<_> = regular_fields
.iter()
.map(|field| match field.ty.regular() {
Some(RegularParsedField {
value_source: ParsedValueSource::Default(data),
..
}) => match color_constant_paths(data) {
Some(paths) => quote!(Some(&[#(#paths),*])),
None => quote!(None),
},
_ => quote!(None),
})
.collect();
let default_types: Vec<_> = regular_fields
.iter()
.enumerate()
.map(|(index, field)| {
let Some(RegularParsedField { implementations, value_source, .. }) = field.ty.regular() else {
return quote!(None);
};
match implementations.first() {
// A primary's scalar `#[default]` parses as a bare element (unranked, promoted at resolution); without one it defaults to an empty List
Some(implementation_ty) if Some(index) == primary_regular_index && element_wise => match value_source {
ParsedValueSource::Default(_) => quote!(Some(concrete!(#implementation_ty))),
_ => quote!(Some(#core_types::list!(#implementation_ty))),
},
// A ranked implementation row emits structurally, keeping the element's TypeId which name-parsing in `normalize_rank` cannot recover
Some(implementation_ty) => {
if let Some(element) = peel_list(implementation_ty) {
quote!(Some(#core_types::list!(#element)))
} else if let Some(element) = peel_item(implementation_ty) {
quote!(Some(#core_types::item!(#element, #element)))
} else {
quote!(Some(concrete!(#implementation_ty)))
}
}
// A concrete ranked `Item<T>` param's scalar `#[default]` parses as a bare `T` literal (unranked, promoted at resolution);
// without one it keeps the structural `Type::Item` wire type with the element's alias on its descriptor (so the rank-0 Properties widget still dispatches, e.g. `Progression`), and `node_inputs` peels to `T` if no `Item` type default exists
None => match &field.ty {
ParsedFieldType::Item {
field: RegularParsedField { value_source, .. },
element,
} if !fn_generics
.iter()
.any(|generic| matches!(generic, syn::GenericParam::Type(param) if type_contains_ident(element, &param.ident))) =>
{
// The fn's lifetimes are elided since the metadata registration fn declares none of them
let element = substitute_lifetimes(element.clone(), "_");
match value_source {
ParsedValueSource::Default(_) => quote!(Some(concrete!(#element))),
_ => quote!(Some(#core_types::item!(#element, #element))),
}
}
_ => quote!(None),
},
}
})
.collect();
let bound_values = |select: fn(&RegularParsedField) -> &Option<NumberBound>| -> Vec<_> {
regular_fields
.iter()
.map(|field| match field.ty.regular() {
Some(regular) => select(regular).as_ref().map_or(quote!(None), |bound| quote!(Some(#bound))),
None => quote!(None),
})
.collect()
};
let number_soft_min_values = bound_values(|field| &field.number_soft_min);
let number_soft_max_values = bound_values(|field| &field.number_soft_max);
let number_hard_min_values = bound_values(|field| &field.number_hard_min);
let number_hard_max_values = bound_values(|field| &field.number_hard_max);
let number_mode_range_values: Vec<_> = regular_fields
.iter()
.map(|field| match field.ty.regular() {
Some(RegularParsedField { number_mode_range, .. }) => quote!(#number_mode_range),
None => quote!(false),
})
.collect();
let number_display_decimal_places: Vec<_> = regular_fields
.iter()
.map(|field| field.number_display_decimal_places.as_ref().map_or(quote!(None), |i| quote!(Some(#i))))
.collect();
let number_step: Vec<_> = regular_fields.iter().map(|field| field.number_step.as_ref().map_or(quote!(None), |i| quote!(Some(#i)))).collect();
let unit_suffix: Vec<_> = regular_fields.iter().map(|field| field.unit.as_ref().map_or(quote!(None), |i| quote!(Some(#i)))).collect();
let exposed: Vec<_> = regular_fields
.iter()
.map(|field| match field.ty.regular() {
Some(RegularParsedField { exposed, .. }) => quote!(#exposed),
None => quote!(true),
})
.collect();
// Only eval regular fields (data fields are accessed directly as self.field_name)
let eval_args = regular_fields.iter().map(|field| {
let name = &field.pat_ident.ident;
match &field.ty {
ParsedFieldType::Node { .. } => {
quote! { let #name = &self.#name; }
}
_ => {
quote! { let #name = self.#name.eval(__input.clone()).await; }
}
}
});
// Only regular fields can have min/max constraints
let min_max_args = regular_fields.iter().map(|field| match field.ty.regular() {
Some(RegularParsedField { number_hard_min, number_hard_max, .. }) => {
let name = &field.pat_ident.ident;
let mut tokens = quote!();
if let Some(min) = number_hard_min {
tokens.extend(quote_spanned! {min.span()=>
let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min);
});
}
if let Some(max) = number_hard_max {
tokens.extend(quote_spanned! {max.span()=>
let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);
});
}
tokens
}
None => quote!(),
});
let all_implementation_types = fields.iter().flat_map(|field| match &field.ty {
ParsedFieldType::Node(NodeParsedField { implementations, .. }) => implementations
.iter()
.flat_map(|implementation| [implementation.input.clone(), implementation.output.clone()])
.collect::<Vec<_>>(),
value => value.regular().map_or_else(Vec::new, |regular| regular.implementations.iter().cloned().collect()),
});
let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
let input_type = &parsed.input.ty;
// Add Clampable bounds for fields with hard bounds, applying to each variant's evaluated wire type
let build_clampable_clauses = |primary_wire: Option<WireWrapper>| -> Vec<TokenStream2> {
regular_fields
.iter()
.filter_map(|field| {
let RegularParsedField {
ty, number_hard_min, number_hard_max, ..
} = field.ty.regular()?;
if number_hard_min.is_none() && number_hard_max.is_none() {
return None;
}
let ty = match (&field.ty, primary_wire) {
(ParsedFieldType::Item { element, .. }, Some(wrap)) if !field.is_environment() => wrap.apply(core_types, element),
_ => ty.clone(),
};
Some(quote!(#ty: #core_types::misc::Clampable))
})
.collect()
};
future_idents.extend((0..regular_fields.len()).map(|id| format_ident!("F{}", id)));
// Builds every field's where-clause bounds, wrapping each ranked connector's wire type for the element-wise variants
// while environment fields keep their declared type. `list_content` additionally lifts a lazy primary connector's
// `Item<E>` output to `List<E>` for the list-content variant.
let build_field_clauses = |primary_wire: Option<WireWrapper>, list_content: bool| -> Vec<TokenStream2> {
regular_fields
.iter()
.zip(node_generics.iter())
.zip(future_idents.iter())
.enumerate()
.map(|(index, ((field, name), fut_ident))| match &field.ty {
ParsedFieldType::Node(NodeParsedField {
input_type,
output_type,
output_element,
..
}) => {
if !*is_async {
unreachable!("Found node which takes an impl Node<> input but is not async")
}
let output_type = if list_content && Some(index) == primary_regular_index {
let element_ty = output_element.clone().unwrap_or_else(|| output_type.clone());
WireWrapper::List.apply(core_types, &element_ty)
} else {
output_type.clone()
};
quote!(
#fut_ident: core::future::Future<Output = #output_type> + #core_types::WasmNotSend + 'n,
#name: #core_types::Node<'n, #input_type, Output = #fut_ident > + #core_types::WasmNotSync
)
}
value => {
let declared = &value.regular().expect("a non-node field is a value field").ty;
let ty = match (value, primary_wire) {
// An `Item<T>`-declared connector contributes its element type to the wire wrapping
(ParsedFieldType::Item { element, .. }, Some(wrap)) if !field.is_environment() => wrap.apply(core_types, element),
_ => declared.clone(),
};
let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all");
quote!(
#fut_ident: core::future::Future<Output = #ty> + #core_types::WasmNotSend + 'n,
for<'all> #all_lifetime_ty: #core_types::WasmNotSend,
#name: #core_types::Node<'n, #input_type, Output = #fut_ident> + #core_types::WasmNotSync
)
}
})
.collect()
};
let where_clause = where_clause.clone().unwrap_or(WhereClause {
where_token: Token![where](output_type.span()),
predicates: Default::default(),
});
let make_struct_where_clause = |field_clauses: Vec<TokenStream2>, clampable_clauses: Vec<TokenStream2>, extra_clauses: Vec<TokenStream2>| {
let mut struct_where_clause = where_clause.clone();
let extra_where: Punctuated<WherePredicate, Comma> = parse_quote!(
#(#field_clauses,)*
#(#clampable_clauses,)*
#(#extra_clauses,)*
#output_type: 'n,
);
struct_where_clause.predicates.extend(extra_where);
struct_where_clause
};
let primary_wire = element_wise.then_some(WireWrapper::Item);
let struct_where_clause = make_struct_where_clause(build_field_clauses(primary_wire, false), build_clampable_clauses(primary_wire), Vec::new());
// The mapped variant clones bare parameters and clones ranked connectors' items per frame slot, so both need Clone
let param_clone_clauses: Vec<TokenStream2> = regular_fields
.iter()
.filter_map(|field| {
let RegularParsedField { ty, .. } = field.ty.regular()?;
Some(match &field.ty {
ParsedFieldType::Item { element, .. } => quote!(#element: Clone),
_ => quote!(#ty: Clone),
})
})
.collect();
let mapped_struct_where_clause = mapped_variant.then(|| {
make_struct_where_clause(
build_field_clauses(Some(WireWrapper::List), false),
build_clampable_clauses(Some(WireWrapper::List)),
param_clone_clauses.clone(),
)
});
// The list-content variant clones each content slot and feeds it in behind a shared reference held across the kernel's await,
// so the primary connector's element type needs `Clone` (to clone the slot) and `Sync` (so `&PrecomputedItemNode` is `Send`)
let list_content_struct_where_clause = list_content_variant.then(|| {
let mut extra_clauses = param_clone_clauses.clone();
if let Some(content_element) = &primary_element {
extra_clauses.push(quote!(#content_element: Clone + #core_types::WasmNotSync));
}
make_struct_where_clause(build_field_clauses(Some(WireWrapper::List), true), build_clampable_clauses(Some(WireWrapper::List)), extra_clauses)
});
// Only regular fields are parameters to new()
let new_args: Vec<_> = node_generics
.iter()
.zip(regular_field_names.iter())
.map(|(r#gen, name)| {
quote! { #name: #r#gen }
})
.collect();
// Initialize data fields with Default, regular fields with parameters
let data_inits = data_field_names.iter().map(|name| {
quote! { #name: Default::default() }
});
let regular_inits = regular_field_names.iter().map(|name| {
quote! { #name }
});
let all_field_inits: Vec<_> = data_inits.chain(regular_inits).collect();
let async_keyword = is_async.then(|| quote!(async));
let await_keyword = is_async.then(|| quote!(.await));
// Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone
let struct_derives = if data_fields.is_empty() {
quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)])
} else {
quote!(#[derive(Debug, Clone)])
};
// Generate serialize method if serialize attribute is specified
let serialize_impl = if let Some(serialize_fn) = &parsed.attributes.serialize {
let data_field_refs = data_field_names.iter().map(|name| quote!(&self.#name));
quote! {
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
#serialize_fn(#(#data_field_refs),*)
}
}
} else {
quote!()
};
let eval_prelude = quote! {
use #core_types::misc::Clampable;
#(#eval_args)*
#(#min_max_args)*
};
let eval_impl = quote! {
type Output = #core_types::registry::DynFuture<'n, #output_type>;
#[inline]
fn eval(&'n self, __input: #input_type) -> Self::Output {
Box::pin(async move {
#eval_prelude
self::#fn_name(__input #(, &self.#data_field_names)* #(, #regular_field_names)*) #await_keyword
})
}
#serialize_impl
};
// The mapped variant zips every ranked connector by frame slot (longest-list, last-element repeats), broadcasting bare and environment parameters by clone
let mapped_eval_impl = mapped_variant.then(|| {
let ranked_names: Vec<_> = regular_fields
.iter()
.filter(|field| !field.is_environment() && matches!(&field.ty, ParsedFieldType::Item { .. }))
.map(|field| &field.pat_ident.ident)
.collect();
let per_slot_args: Vec<_> = regular_fields
.iter()
.map(|field| {
let name = &field.pat_ident.ident;
match &field.ty {
ParsedFieldType::Node(_) => quote!(#name),
ParsedFieldType::Item { .. } if !field.is_environment() => quote! {
#name.clone_item(__slot_index.min(#name.len() - 1)).expect("A zip slot index is always within bounds")
},
_ => quote!(#name.clone()),
}
})
.collect();
// The frame index is stamped onto each slot's context so lazy connectors can generate uniquely per slot.
// A `()` generator frames purely over its param values (no stamp), so its kernel needs no context-extraction bounds.
let has_lazy_connectors = regular_fields.iter().any(|field| matches!(&field.ty, ParsedFieldType::Node(_)));
let slot_context = match has_lazy_connectors {
true => quote!(#core_types::OwnedContextImpl::from(__input.clone()).with_index(__slot_index).into_context()),
false => quote!(__input.clone()),
};
// An expander kernel (returning `List<U>`) flat-maps under the frame per the rank-2 force-flatten rule; a map kernel pushes one item per slot
let (mapped_output_type, initial_output, collect_result) = match parsed.output_element.as_ref() {
Some(element_ty) => (
quote!(#core_types::list::List<#element_ty>),
quote!(#core_types::list::List::with_capacity(__frame_length)),
quote!(__output.push(__result);),
),
None => (quote!(#output_type), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)),
};
quote! {
type Output = #core_types::registry::DynFuture<'n, #mapped_output_type>;
#[inline]
#[allow(clippy::clone_on_copy)]
fn eval(&'n self, __input: #input_type) -> Self::Output {
Box::pin(async move {
#eval_prelude
let __frame_length = [#(#ranked_names.len()),*].into_iter().max().unwrap_or(0);
if [#(#ranked_names.len()),*].into_iter().any(|length| length == 0) {
return #core_types::list::List::new();
}
let mut __output = #initial_output;
for __slot_index in 0..__frame_length {
let __slot_context = #slot_context;
let __result = self::#fn_name(__slot_context #(, &self.#data_field_names)* #(, #per_slot_args)*) #await_keyword;
#collect_result
}
__output
})
}
#serialize_impl
}
});
// The list-content variant evaluates a lazy primary's whole content `List` once, then feeds each slot into the kernel as a
// precomputed stub (ambient footprint, no index stamp), zipping ranked params by slot with the frame taken from the content length.
let list_content_eval_impl = list_content_variant.then(|| {
let primary_index = primary_regular_index.expect("A list-content variant always has a lazy primary");
let primary_name = &regular_fields[primary_index].pat_ident.ident;
let ranked_names: Vec<_> = regular_fields
.iter()
.filter(|field| !field.is_environment() && matches!(&field.ty, ParsedFieldType::Item { .. }))
.map(|field| &field.pat_ident.ident)
.collect();
let per_slot_args: Vec<_> = regular_fields
.iter()
.enumerate()
.map(|(index, field)| {
let name = &field.pat_ident.ident;
if index == primary_index {
return quote!(&__stub);
}
match &field.ty {
ParsedFieldType::Node(_) => quote!(#name),
ParsedFieldType::Item { .. } if !field.is_environment() => quote! {
#name.clone_item(__slot_index.min(#name.len() - 1)).expect("A zip slot index is always within bounds")
},
_ => quote!(#name.clone()),
}
})
.collect();
let (list_content_output_type, initial_output, collect_result) = match parsed.output_element.as_ref() {
Some(element_ty) => (
quote!(#core_types::list::List<#element_ty>),
quote!(#core_types::list::List::with_capacity(__frame_length)),
quote!(__output.push(__result);),
),
None => (quote!(#output_type), quote!(#core_types::list::List::new()), quote!(__output.extend(__result);)),
};
let empty_param_check = (!ranked_names.is_empty()).then(|| {
quote! {
if [#(#ranked_names.len()),*].into_iter().any(|__length| __length == 0) {
return #core_types::list::List::new();
}
}
});
quote! {
type Output = #core_types::registry::DynFuture<'n, #list_content_output_type>;
#[inline]
#[allow(clippy::clone_on_copy)]
fn eval(&'n self, __input: #input_type) -> Self::Output {
Box::pin(async move {
#eval_prelude
let __content = #primary_name.eval(#core_types::OwnedContextImpl::from(__input.clone()).into_context()) #await_keyword;
let __frame_length = __content.len();
#empty_param_check
let mut __output = #initial_output;
for __slot_index in 0..__frame_length {
let __stub = #core_types::value::PrecomputedItemNode::new(
__content.clone_item(__slot_index).expect("A content slot index is always within bounds")
);
let __result = self::#fn_name(__input.clone() #(, &self.#data_field_names)* #(, #per_slot_args)*) #await_keyword;
#collect_result
}
__output
})
}
#serialize_impl
}
});
let identifier = format_ident!("{}_proto_ident", fn_name);
let identifier_path = match parsed.attributes.path.as_ref() {
Some(path) => {
let path = path.to_token_stream().to_string().replace(' ', "");
quote!(#path)
}
None => quote!(std::module_path!()),
};
let mapped_struct_name = format_ident!("{}Mapped", struct_name);
let list_content_struct_name = format_ident!("{}ListContent", struct_name);
let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &mapped_struct_name, &list_content_struct_name, &identifier)?;
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
let memoize_flag = attributes.memoize;
let inject_scope_flag = attributes.inject_scope;
let cfg = crate::shader_nodes::modify_cfg(attributes);
let node_input_accessor = generate_node_input_references(parsed, &field_idents, core_types, &identifier, &cfg);
let ShaderTokens { shader_entry_point, gpu_node } = attributes.shader_node.as_ref().map(|n| n.codegen(crate_ident, parsed)).unwrap_or(Ok(ShaderTokens::default()))?;
let mapped_node_impl = match (&mapped_struct_where_clause, &mapped_eval_impl) {
(Some(mapped_where_clause), Some(mapped_eval)) => quote! {
#cfg
#[automatically_derived]
impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#mapped_struct_name<#(#struct_type_params,)*>
#mapped_where_clause
{
#mapped_eval
}
},
_ => quote!(),
};
let list_content_node_impl = match (&list_content_struct_where_clause, &list_content_eval_impl) {
(Some(list_content_where_clause), Some(list_content_eval)) => quote! {
#cfg
#[automatically_derived]
impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#list_content_struct_name<#(#struct_type_params,)*>
#list_content_where_clause
{
#list_content_eval
}
},
_ => quote!(),
};
let mapped_struct_def = mapped_variant.then(|| {
quote! {
#struct_derives
pub struct #mapped_struct_name<#(#struct_generic_params,)*> {
#(#struct_fields,)*
}
#[automatically_derived]
impl<'n, #(#struct_generic_params,)*> #mapped_struct_name<#(#struct_type_params,)*>
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)*) -> Self {
Self {
#(#all_field_inits,)*
}
}
}
}
});
let list_content_struct_def = list_content_variant.then(|| {
quote! {
#struct_derives
pub struct #list_content_struct_name<#(#struct_generic_params,)*> {
#(#struct_fields,)*
}
#[automatically_derived]
impl<'n, #(#struct_generic_params,)*> #list_content_struct_name<#(#struct_type_params,)*>
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)*) -> Self {
Self {
#(#all_field_inits,)*
}
}
}
}
});
let mapped_struct_export = mapped_variant.then(|| {
quote! {
#cfg
#[doc(hidden)]
pub use #mod_name::#mapped_struct_name;
}
});
let list_content_struct_export = list_content_variant.then(|| {
quote! {
#cfg
#[doc(hidden)]
pub use #mod_name::#list_content_struct_name;
}
});
let display_name_header = format!("# {display_name}");
let mut description_doc_attrs = vec![quote!(#[doc = #display_name_header]), quote!(#[doc = ""])];
description_doc_attrs.extend(description.lines().map(|line| quote!(#[doc = #line])));
// Add parameter list to doc comment
if !input_names.is_empty() {
description_doc_attrs.push(quote!(#[doc = ""]));
description_doc_attrs.push(quote!(#[doc = "## Parameters"]));
for (name, desc) in input_names.iter().zip(input_descriptions.iter()) {
if desc.is_empty() {
let header = format!("- **{name}**");
description_doc_attrs.push(quote!(#[doc = #header]));
} else {
let first_line = desc.lines().next().unwrap_or("");
let header = format!("- **{name}**: {first_line}");
description_doc_attrs.push(quote!(#[doc = #header]));
for line in desc.lines().skip(1) {
let continuation = format!(" {line}");
description_doc_attrs.push(quote!(#[doc = #continuation]));
}
}
}
}
Ok(quote! {
#(#description_doc_attrs)*
#[inline]
#[allow(clippy::too_many_arguments)]
#vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #data_field_idents: #data_field_types)* #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#cfg
#[automatically_derived]
impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_type_params,)*>
#struct_where_clause
{
#eval_impl
}
#mapped_node_impl
#list_content_node_impl
#cfg
const fn #identifier() -> #core_types::ProtoNodeIdentifier {
#core_types::ProtoNodeIdentifier::new(std::concat!(#identifier_path, "::", std::stringify!(#struct_name)))
}
#cfg
#[doc(inline)]
pub use #mod_name::#struct_name;
#mapped_struct_export
#list_content_struct_export
#[doc(hidden)]
#node_input_accessor
#cfg
#[doc(hidden)]
#[allow(clippy::module_inception)]
mod #mod_name {
use super::*;
use #core_types as gcore;
use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO, ContextFeature};
use gcore::value::ClonedNode;
use gcore::ops::TypeNode;
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
use gcore::ctor::ctor;
// Use the types specified in the implementation
static #import_name: core::marker::PhantomData<(#(#all_implementation_types,)*)> = core::marker::PhantomData;
#struct_derives
pub struct #struct_name<#(#struct_generic_params,)*> {
#(#struct_fields,)*
}
#[automatically_derived]
impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*>
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)*) -> Self {
Self {
#(#all_field_inits,)*
}
}
}
#mapped_struct_def
#list_content_struct_def
#register_node_impl
#[cfg_attr(not(target_family = "wasm"), ctor)]
fn register_metadata() {
let metadata = NodeMetadata {
display_name: #display_name,
category: #category,
description: #description,
properties: #properties,
context_features: vec![#(ContextFeature::#context_features,)*],
memoize: #memoize_flag,
inject_scope: #inject_scope_flag,
fields: vec![
#(
FieldMetadata {
name: #input_names,
widget_override: #widget_override,
description: #input_descriptions,
hidden: #input_hidden,
exposed: #exposed,
value_source: #value_sources,
default_colors: #default_colors,
default_type: #default_types,
number_soft_min: #number_soft_min_values,
number_soft_max: #number_soft_max_values,
number_hard_min: #number_hard_min_values,
number_hard_max: #number_hard_max_values,
number_mode_range: #number_mode_range_values,
number_display_decimal_places: #number_display_decimal_places,
number_step: #number_step,
unit: #unit_suffix,
},
)*
],
};
NODE_METADATA.lock().unwrap().insert(#identifier(), metadata);
}
}
#shader_entry_point
#gpu_node
})
}
/// The `Color::*` constant paths making up a default expression, when it consists solely of them (the form used by color and gradient parameter defaults).
fn color_constant_paths(tokens: &TokenStream2) -> Option<Vec<ExprPath>> {
use syn::parse::Parser;
let expressions = Punctuated::<Expr, Token![,]>::parse_terminated.parse2(tokens.clone()).ok()?;
if expressions.is_empty() {
return None;
}
expressions
.into_iter()
.map(|expression| {
let Expr::Path(path) = expression else { return None };
let segments = &path.path.segments;
let is_color_constant = path.qself.is_none() && segments.len() == 2 && segments[0].ident == "Color" && segments.iter().all(|segment| segment.arguments.is_none());
is_color_constant.then_some(path)
})
.collect()
}
/// Generates the per-parameter symbol types used to reference this node's inputs.
fn generate_node_input_references(parsed: &ParsedNodeFn, 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 {
for (input_index, input_ident) in field_idents.iter().enumerate() {
// 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));
// Every parameter gets a plain unit struct: the symbol used across the codebase to name this input
generated_input_accessor.push(quote! {
pub struct #struct_name;
impl #core_types::NodeParameter for #struct_name {
const NODE_IDENTIFIER: #core_types::ProtoNodeIdentifier = #inputs_module_name::IDENTIFIER;
const INDEX: usize = #input_index;
}
});
}
}
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)*
}
}
}
/// The wire container a generated node variant is registered with, wrapping the kernel's primary input element type.
#[derive(Clone, Copy, PartialEq)]
enum WireWrapper {
Item,
List,
}
impl WireWrapper {
fn apply(self, core_types: &TokenStream2, ty: &syn::Type) -> syn::Type {
match self {
WireWrapper::Item => parse_quote!(#core_types::list::Item<#ty>),
WireWrapper::List => parse_quote!(#core_types::list::List<#ty>),
}
}
}
/// The variant of a node registered under one identifier: `Plain` for non-element-wise nodes, and the three element-wise wire
/// shapes (`Item` content and params, `Mapped` `List` params, `ListContent` `List` content for a lazy primary).
#[derive(Clone, Copy, PartialEq)]
enum RegisterVariant {
Plain,
Item,
Mapped,
ListContent,
}
impl RegisterVariant {
/// The wrapper applied to ranked eager params for this variant.
fn param_wrap(self) -> Option<WireWrapper> {
match self {
RegisterVariant::Item => Some(WireWrapper::Item),
RegisterVariant::Mapped | RegisterVariant::ListContent => Some(WireWrapper::List),
RegisterVariant::Plain => None,
}
}
}
/// Returns the element type of the node's primary input if it is declared `Item<T>` (directly, or as a lazy
/// connector's `Output = Item<T>`), which marks the node as element-wise.
fn primary_item_element(parsed: &ParsedNodeFn) -> Option<syn::Type> {
// Manually registered nodes control their own variants
if parsed.attributes.skip_impl {
return None;
}
let (_, field) = parsed.primary_input_field()?;
match &field.ty {
ParsedFieldType::Node(NodeParsedField { output_element, .. }) => output_element.clone(),
ParsedFieldType::Item { element, .. } => Some(element.clone()),
_ => None,
}
}
/// Whether the element-wise node gets a mapped `List` wire variant: an eager primary is its own frame source, while a lazy primary (or a `()` generator) draws the frame from its ranked eager params, so it needs at least one.
fn generates_mapped_variant(parsed: &ParsedNodeFn) -> bool {
// A `()` generator frames over its ranked params exactly like a lazy primary, but generating fresh content per slot instead of transforming an upstream item
if is_generator_frame(parsed) {
return true;
}
if primary_item_element(parsed).is_none() {
return false;
}
let mut input_fields = parsed.fields.iter().filter(|field| !field.is_environment());
match input_fields.next().map(|field| &field.ty) {
Some(ParsedFieldType::Node(_)) => input_fields.any(|field| matches!(&field.ty, ParsedFieldType::Item { .. })),
_ => true,
}
}
/// Whether the node is a `()`-primary generator that frames over its ranked params: a unit primary with at least one ranked (`Item<T>`) param.
fn is_generator_frame(parsed: &ParsedNodeFn) -> bool {
if parsed.attributes.skip_impl {
return false;
}
let Some((primary_index, primary)) = parsed.primary_input_field() else { return false };
let Some(RegularParsedField { ty, .. }) = primary.ty.regular() else { return false };
if !is_unit_type(ty) {
return false;
}
parsed
.fields
.iter()
.enumerate()
.any(|(index, field)| index != primary_index && !field.is_environment() && matches!(&field.ty, ParsedFieldType::Item { .. }))
}
/// Whether the type is the unit type `()`, which marks a generator with no primary input.
fn is_unit_type(ty: &syn::Type) -> bool {
matches!(ty, syn::Type::Tuple(tuple) if tuple.elems.is_empty())
}
/// Whether the element-wise node gets a list-content `List` wire variant: only a lazy primary connector qualifies, since it draws its frame from the whole content `List` (an eager primary already maps over `List` content via the mapped variant).
fn generates_list_content_variant(parsed: &ParsedNodeFn) -> bool {
primary_item_element(parsed).is_some() && matches!(parsed.primary_input_field().map(|(_, field)| &field.ty), Some(ParsedFieldType::Node(_)))
}
fn generate_register_node_impl(
parsed: &ParsedNodeFn,
field_names: &[&Ident],
struct_name: &Ident,
mapped_struct_name: &Ident,
list_content_struct_name: &Ident,
identifier: &Ident,
) -> Result<TokenStream2, Error> {
// On native, `register_node` and `register_metadata` run automatically via `#[ctor]`.
// On Wasm, `ctor` isn't available, so this `extern "C"` fn is invoked from JS to register the same way.
// `skip_impl` nodes don't generate a `register_node`, so the shim calls only `register_metadata` for them.
let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name);
let register_node_call = if parsed.attributes.skip_impl { quote!() } else { quote!(register_node();) };
let wasm_shim = quote! {
#[cfg(target_family = "wasm")]
#[unsafe(no_mangle)]
extern "C" fn #registry_name() {
#register_node_call
register_metadata();
}
};
if parsed.attributes.skip_impl {
return Ok(wasm_shim);
}
let mut constructors = Vec::new();
let unit = parse_quote!(gcore::Context);
let regular_fields: Vec<_> = parsed.fields.iter().filter(|f| !f.is_data_field).collect();
let primary_regular_index = regular_fields.iter().position(|field| !field.is_environment());
let parameter_types: Vec<_> = regular_fields
.iter()
.map(|field| {
match &field.ty {
ParsedFieldType::Node(NodeParsedField {
implementations,
input_type,
output_type,
..
}) => {
if !implementations.is_empty() {
implementations.iter().map(|impl_| (&impl_.input, &impl_.output)).collect()
} else {
vec![(input_type, output_type)]
}
}
value => {
let RegularParsedField { implementations, ty, .. } = value.regular().expect("a non-node field is a value field");
if !implementations.is_empty() {
implementations.iter().map(|ty| (&unit, ty)).collect()
} else {
vec![(&unit, ty)]
}
}
}
.into_iter()
.map(|(input, out)| (substitute_lifetimes(input.clone(), "_"), substitute_lifetimes(out.clone(), "_")))
.collect::<Vec<_>>()
})
.collect();
let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max();
// Element-wise nodes register a variant per wire shape per implementations row; all other nodes register one
let gcore = quote!(gcore);
let variants: Vec<RegisterVariant> = if primary_item_element(parsed).is_some() {
let mut variants = vec![RegisterVariant::Item];
if generates_mapped_variant(parsed) {
variants.push(RegisterVariant::Mapped);
}
if generates_list_content_variant(parsed) {
variants.push(RegisterVariant::ListContent);
}
variants
} else if is_generator_frame(parsed) {
// A `()` generator registers an Item form (single generation) plus a mapped form framed over its ranked params
vec![RegisterVariant::Item, RegisterVariant::Mapped]
} else {
vec![RegisterVariant::Plain]
};
for i in 0..max_implementations.unwrap_or(0) {
for &variant in &variants {
let mut temp_constructors = Vec::new();
let mut temp_node_io = Vec::new();
let mut panic_node_types = Vec::new();
for (j, types) in parameter_types.iter().enumerate() {
let field_name = field_names[j];
let (input_type, output_type) = &types[i.min(types.len() - 1)];
// Rankedness comes from the field's declared type; its #[implementations(...)] entries are bare element types
let field_is_ranked = !regular_fields[j].is_environment() && matches!(&regular_fields[j].ty, ParsedFieldType::Item { .. });
let is_lazy_primary = Some(j) == primary_regular_index && matches!(regular_fields[j].ty, ParsedFieldType::Node { .. });
// The list-content variant lifts its lazy primary connector's `Item<E>` content to `List<E>`; ranked params follow the variant's param wrap.
// A ranked signature is spelled with bare `Item<...>`/`List<...>` tokens, which `fn_type_fut!` matches syntactically to construct the structural form.
let (output_type, signature_type) = if is_lazy_primary && variant == RegisterVariant::ListContent {
let element_ty = peel_item(output_type).unwrap_or_else(|| output_type.clone());
(WireWrapper::List.apply(&gcore, &element_ty), Some(quote!(List<#element_ty>)))
} else {
match (field_is_ranked, variant.param_wrap()) {
(true, Some(wrap)) => {
let element_ty = peel_item(output_type).unwrap_or_else(|| output_type.clone());
let signature = match wrap {
WireWrapper::List => quote!(List<#element_ty>),
WireWrapper::Item => quote!(Item<#element_ty>),
};
(wrap.apply(&gcore, &element_ty), Some(signature))
}
_ => (output_type.clone(), None),
}
};
let signature_type = signature_type.unwrap_or_else(|| quote!(#output_type));
let node = matches!(regular_fields[j].ty, ParsedFieldType::Node { .. });
let downcast_node = quote!(
let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone());
);
if node && !parsed.is_async {
return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters"));
}
temp_constructors.push(downcast_node);
temp_node_io.push(quote!(fn_type_fut!(#input_type, #signature_type, alias: #signature_type)));
panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>));
}
let input_type = match parsed.input.implementations.is_empty() {
true => parsed.input.ty.clone(),
false => parsed.input.implementations[i.min(parsed.input.implementations.len() - 1)].clone(),
};
let variant_struct_name = match variant {
RegisterVariant::Mapped => mapped_struct_name,
RegisterVariant::ListContent => list_content_struct_name,
RegisterVariant::Item | RegisterVariant::Plain => struct_name,
};
constructors.push(quote!(
(
|args| {
Box::pin(async move {
#(#temp_constructors;)*
let node = #variant_struct_name::new(#(#field_names,)*);
// try polling futures
let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node);
Box::new(any) as TypeErasedBox<'_>
})
}, {
let node = #variant_struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*);
let params = vec![#(#temp_node_io,)*];
let mut node_io = NodeIO::<'_, #input_type>::to_async_node_io(&node, params);
node_io
}
)
));
}
}
let native = quote! {
#[cfg_attr(not(target_family = "wasm"), ctor)]
fn register_node() {
let mut registry = NODE_REGISTRY.lock().unwrap();
registry.insert(
#identifier(),
vec![
#(#constructors,)*
]
);
}
};
if cfg!(feature = "disable-registration") {
return Ok(native);
}
Ok(quote! {
#native
#wasm_shim
})
}
use crate::crate_ident::CrateIdent;
use crate::shader_nodes::{ShaderCodegen, ShaderTokens};
use syn::visit_mut::VisitMut;
use syn::{GenericArgument, Lifetime, Type};
struct LifetimeReplacer(&'static str);
impl VisitMut for LifetimeReplacer {
fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
lifetime.ident = Ident::new(self.0, lifetime.ident.span());
}
fn visit_type_mut(&mut self, ty: &mut Type) {
match ty {
Type::Reference(type_reference) => {
if let Some(lifetime) = &mut type_reference.lifetime {
self.visit_lifetime_mut(lifetime);
}
self.visit_type_mut(&mut type_reference.elem);
}
_ => syn::visit_mut::visit_type_mut(self, ty),
}
}
fn visit_generic_argument_mut(&mut self, arg: &mut GenericArgument) {
if let GenericArgument::Lifetime(lifetime) = arg {
self.visit_lifetime_mut(lifetime);
} else {
syn::visit_mut::visit_generic_argument_mut(self, arg);
}
}
}
#[must_use]
fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type {
LifetimeReplacer(lifetime).visit_type_mut(&mut ty);
ty
}
/// Check if a type contains a reference to a specific identifier (e.g., a generic type parameter)
fn type_contains_ident(ty: &Type, ident: &Ident) -> bool {
struct IdentChecker<'a> {
target: &'a Ident,
found: bool,
}
impl<'a, 'ast> syn::visit::Visit<'ast> for IdentChecker<'a> {
fn visit_ident(&mut self, i: &'ast Ident) {
if i == self.target {
self.found = true;
}
}
}
let mut checker = IdentChecker { target: ident, found: false };
syn::visit::visit_type(&mut checker, ty);
checker.found
}