mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
* 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
1290 lines
46 KiB
Rust
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(®ular.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, ¶m.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),
|
|
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 = ®ular_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!(®ular_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
|
|
}
|