mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
1284 lines
46 KiB
Rust
1284 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
|
|
|
|
}
|
|
)
|
|
));
|
|
}
|
|
}
|
|
Ok(quote! {
|
|
#[cfg_attr(not(target_family = "wasm"), ctor)]
|
|
fn register_node() {
|
|
let mut registry = NODE_REGISTRY.lock().unwrap();
|
|
registry.insert(
|
|
#identifier(),
|
|
vec![
|
|
#(#constructors,)*
|
|
]
|
|
);
|
|
}
|
|
|
|
#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
|
|
}
|