Refactor the node macro and simply most of the node implementations (#1942)

* Add support structure for new node macro to gcore

* Fix compile issues and code generation

* Implement new node_fn macro

* Implement property translation

* Fix NodeIO type generation

* Start translating math nodes

* Move node implementation to outer scope to allow usage of local imports

* Add expose attribute to allow controlling the parameter exposure

* Add rust analyzer support for #[implementations] attribute

* Migrate logic nodes

* Handle where clause properly

* Implement argument ident pattern preservation

* Implement adjustment layer mapping

* Fix node registry types

* Fix module paths

* Improve demo artwork comptibility

* Improve macro error reporting

* Fix handling of impl node implementations

* Fix nodeio type computation

* Fix opacity node and graph type resolution

* Fix loading of demo artworks

* Fix eslint

* Fix typo in macro test

* Remove node definitions for Adjustment Nodes

* Fix type alias property generation and make adjustments footprint aware

* Convert vector nodes

* Implement path overrides

* Fix stroke node

* Fix painted dreams

* Implement experimental type level specialization

* Fix poisson disk sampling -> all demo artworks should work again

* Port text node + make node macro more robust by implementing lifetime substitution

* Fix vector node tests

* Fix red dress demo + ci

* Fix clippy warnings

* Code review

* Fix primary input issues

* Improve math nodes and audit others

* Set no_properties when no automatic properties are derived

* Port vector generator nodes (could not derive all definitions yet)

* Various QA changes and add min/max/mode_range to number parameters

* Add min and max for f64 and u32

* Convert gpu nodes and clean up unused nodes

* Partially port transform node

* Allow implementations on call arg

* Port path modify node

* Start porting graphic element nodes

* Transform nodes in graphic_element.rs

* Port brush node

* Port nodes in wasm_executior

* Rename node macro

* Fix formatting

* Fix Mandelbrot node

* Formatting

* Fix Load Image and Load Resource nodes, add scope input to node macro

* Remove unnecessary underscores

* Begin attemping to make nodes resolution-aware

* Infer a generic manual compositon type on generic call arg

* Various fixes and work towards merging

* Final changes for merge!

* Fix tests, probably

* More free line removals!

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2024-09-20 12:50:30 +02:00
committed by GitHub
parent ca0d102296
commit e352c7fa71
92 changed files with 4255 additions and 7275 deletions

View File

@@ -0,0 +1,433 @@
use std::sync::atomic::AtomicU64;
use crate::parsing::*;
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_crate::FoundCrate;
use quote::{format_ident, quote};
use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, Ident, Token, WhereClause, WherePredicate};
static NODE_ID: AtomicU64 = AtomicU64::new(0);
pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
let ParsedNodeFn {
attributes,
fn_name,
struct_name,
mod_name,
fn_generics,
where_clause,
input,
output_type,
is_async,
fields,
body,
crate_name: graphene_core_crate,
..
} = parsed;
let category = &attributes.category.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
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);
let struct_generics: Vec<Ident> = fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
let input_ident = &input.pat_ident;
let input_type = &input.ty;
let field_idents: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { pat_ident, .. } | ParsedField::Node { pat_ident, .. } => pat_ident,
})
.collect();
let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect();
let input_names: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { name, .. } | ParsedField::Node { name, .. } => name,
})
.zip(field_names.iter())
.map(|zipped| match zipped {
(Some(name), _) => name.value(),
(_, name) => name.to_string().to_case(convert_case::Case::Title),
})
.collect();
let struct_fields = field_names.iter().zip(struct_generics.iter()).map(|(name, gen)| {
quote! { pub(super) #name: #gen }
});
let graphene_core = match graphene_core_crate {
FoundCrate::Itself => quote!(crate),
FoundCrate::Name(name) => {
let ident = Ident::new(name, proc_macro2::Span::call_site());
quote!( #ident )
}
};
let field_types: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { ty, .. } => ty.clone(),
ParsedField::Node { output_type, input_type, .. } => match parsed.is_async {
true => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output: core::future::Future<Output=#output_type> + #graphene_core::WasmNotSend>),
false => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output = #output_type>),
},
})
.collect();
let value_sources: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { value_source, .. } => match value_source {
ValueSource::Default(data) => quote!(ValueSource::Default(stringify!(#data))),
ValueSource::Scope(data) => quote!(ValueSource::Scope(#data)),
_ => quote!(ValueSource::None),
},
_ => quote!(ValueSource::None),
})
.collect();
let number_min_values: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { number_min: Some(number_min), .. } => quote!(Some(#number_min)),
_ => quote!(None),
})
.collect();
let number_max_values: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { number_max: Some(number_max), .. } => quote!(Some(#number_max)),
_ => quote!(None),
})
.collect();
let number_mode_range_values: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular {
number_mode_range: Some(number_mode_range),
..
} => quote!(Some(#number_mode_range)),
_ => quote!(None),
})
.collect();
let exposed: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { exposed, .. } => quote!(#exposed),
_ => quote!(true),
})
.collect();
let eval_args = fields.iter().map(|field| match field {
ParsedField::Regular { pat_ident, .. } => {
let name = &pat_ident.ident;
quote! { let #name = self.#name.eval(()); }
}
ParsedField::Node { pat_ident, .. } => {
let name = &pat_ident.ident;
quote! { let #name = &self.#name; }
}
});
let all_implementation_types = fields.iter().flat_map(|field| match field {
ParsedField::Regular { implementations, .. } => implementations.into_iter().cloned().collect::<Vec<_>>(),
ParsedField::Node { implementations, .. } => implementations.into_iter().map(|tuple| syn::Type::Tuple(tuple.clone())).collect(),
});
let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
let mut clauses = Vec::new();
for (field, name) in fields.iter().zip(struct_generics.iter()) {
clauses.push(match (field, *is_async) {
(ParsedField::Regular { ty, .. }, _) => quote!(#name: #graphene_core::Node<'n, (), Output = #ty> ),
(ParsedField::Node { input_type, output_type, .. }, false) => {
quote!(for<'all_input> #name: #graphene_core::Node<'all_input, #input_type, Output = #output_type> + #graphene_core::WasmNotSync)
}
(ParsedField::Node { input_type, output_type, .. }, true) => {
quote!(for<'all_input> #name: #graphene_core::Node<'all_input, #input_type, Output: core::future::Future<Output = #output_type> + #graphene_core::WasmNotSend> + #graphene_core::WasmNotSync)
}
});
}
let where_clause = where_clause.clone().unwrap_or(WhereClause {
where_token: Token![where](output_type.span()),
predicates: Default::default(),
});
let mut struct_where_clause = where_clause.clone();
let extra_where: Punctuated<WherePredicate, Comma> = parse_quote!(
#(#clauses,)*
#output_type: 'n,
);
struct_where_clause.predicates.extend(extra_where);
let new_args = struct_generics.iter().zip(field_names.iter()).map(|(gen, name)| {
quote! { #name: #gen }
});
let async_keyword = is_async.then(|| quote!(async));
let eval_impl = if *is_async {
quote! {
type Output = #graphene_core::registry::DynFuture<'n, #output_type>;
#[inline]
fn eval(&'n self, __input: #input_type) -> Self::Output {
#(#eval_args)*
Box::pin(self::#fn_name(__input #(, #field_names)*))
}
}
} else {
quote! {
type Output = #output_type;
#[inline]
fn eval(&'n self, __input: #input_type) -> Self::Output {
#(#eval_args)*
self::#fn_name(__input #(, #field_names)*)
}
}
};
let path = match parsed.attributes.path {
Some(ref path) => quote!(stringify!(#path).replace(' ', "")),
None => quote!(std::module_path!().rsplit_once("::").unwrap().0),
};
let identifier = quote!(format!("{}::{}", #path, stringify!(#struct_name)));
let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?;
Ok(quote! {
/// Underlying implementation for [#struct_name]
#[inline]
#[allow(clippy::too_many_arguments)]
#async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
#[automatically_derived]
impl<'n, #(#fn_generics,)* #(#struct_generics,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>
#struct_where_clause
{
#eval_impl
}
#[doc(inline)]
pub use #mod_name::#struct_name;
#[doc(hidden)]
mod #mod_name {
use super::*;
use #graphene_core as gcore;
use gcore::{Node, NodeIOTypes, concrete, fn_type, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
use gcore::value::ClonedNode;
use gcore::ops::TypeNode;
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, ValueSource};
use gcore::ctor::ctor;
// Use the types specified in the implementation
#[cfg(__never_compiled)]
static _IMPORTS: core::marker::PhantomData<#(#all_implementation_types,)*> = core::marker::PhantomData;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct #struct_name<#(#struct_generics,)*> {
#(#struct_fields,)*
}
#[automatically_derived]
impl<'n, #(#struct_generics,)*> #struct_name<#(#struct_generics,)*>
{
#[allow(clippy::too_many_arguments)]
pub fn new(#(#new_args,)*) -> Self {
Self {
#(#field_names,)*
}
}
}
#register_node_impl
#[cfg_attr(not(target_arch = "wasm32"), ctor)]
fn register_metadata() {
let metadata = NodeMetadata {
display_name: #display_name,
category: #category,
fields: vec![
#(
FieldMetadata {
name: #input_names,
exposed: #exposed,
value_source: #value_sources,
number_min: #number_min_values,
number_max: #number_max_values,
number_mode_range: #number_mode_range_values,
},
)*
],
};
NODE_METADATA.lock().unwrap().insert(#identifier, metadata);
}
}
})
}
fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], struct_name: &Ident, identifier: &TokenStream2) -> Result<TokenStream2, syn::Error> {
if parsed.attributes.skip_impl {
return Ok(quote!());
}
let mut constructors = Vec::new();
let unit = parse_quote!(());
let parameter_types: Vec<_> = parsed
.fields
.iter()
.map(|field| {
match field {
ParsedField::Regular { implementations, ty, .. } => {
if !implementations.is_empty() {
implementations.into_iter().map(|ty| (&unit, ty, false)).collect()
} else {
vec![(&unit, ty, false)]
}
}
ParsedField::Node {
implementations,
output_type,
input_type,
..
} => {
if !implementations.is_empty() {
implementations.into_iter().map(|tup| (&tup.elems[0], &tup.elems[1], true)).collect()
} else {
vec![(input_type, output_type, true)]
}
}
}
.into_iter()
.map(|(input, out, node)| (substitute_lifetimes(input.clone()), substitute_lifetimes(out.clone()), node))
.collect::<Vec<_>>()
})
.collect();
let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max();
let future_node = (!parsed.is_async).then(|| quote!(let node = gcore::registry::FutureWrapperNode::new(node);));
for i in 0..max_implementations.unwrap_or(0) {
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, impl_node) = &types[i.min(types.len() - 1)];
let node = matches!(parsed.fields[j], ParsedField::Node { .. });
let downcast_node = quote!(
let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone());
);
temp_constructors.push(if node {
if !parsed.is_async {
return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters"));
}
downcast_node
} else {
quote!(
#downcast_node
let #field_name = #field_name.eval(()).await;
let #field_name = ClonedNode::new(#field_name);
let #field_name: TypeNode<_, #input_type, #output_type> = TypeNode::new(#field_name);
// try polling futures
)
});
temp_node_io.push(quote!(fn_type!(#input_type, #output_type, alias: #output_type)));
match parsed.is_async && *impl_node {
true => panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>)),
false => panic_node_types.push(quote!(#input_type, #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 node_io = if parsed.is_async { quote!(to_async_node_io) } else { quote!(to_node_io) };
constructors.push(quote!(
(
|args| {
Box::pin(async move {
#(#temp_constructors;)*
let node = #struct_name::new(#(#field_names,)*);
// try polling futures
#future_node
let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node);
Box::new(any) as TypeErasedBox<'_>
})
}, {
let node = #struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*);
let params = vec![#(#temp_node_io,)*];
let mut node_io = NodeIO::<'_, #input_type>::#node_io(&node, params);
node_io
}
)
));
}
let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name);
Ok(quote! {
#[cfg_attr(not(target_arch = "wasm32"), ctor)]
fn register_node() {
let mut registry = NODE_REGISTRY.lock().unwrap();
registry.insert(
#identifier,
vec![
#(#constructors,)*
]
);
}
#[cfg(target_arch = "wasm32")]
#[no_mangle]
extern "C" fn #registry_name() {
register_node();
register_metadata();
}
})
}
use syn::{visit_mut::VisitMut, GenericArgument, Lifetime, Type};
struct LifetimeReplacer;
impl VisitMut for LifetimeReplacer {
fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
lifetime.ident = syn::Ident::new("_", 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) -> Type {
LifetimeReplacer.visit_type_mut(&mut ty);
ty
}

View File

@@ -6,6 +6,9 @@ use syn::{
PathSegment, PredicateType, ReturnType, Token, TraitBound, Type, TypeImplTrait, TypeParam, TypeParamBound, TypeTuple, WhereClause, WherePredicate,
};
mod codegen;
mod parsing;
/// A macro used to construct a proto node implementation from the given struct and the decorated function.
///
/// This works by generating two `impl` blocks for the given struct:
@@ -86,7 +89,7 @@ use syn::{
///
/// When a `let` declaration is generated automatically, this is called **automatic composition**. When opting out, this is called **manual composition**.
#[proc_macro_attribute]
pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn old_node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
let node_impl = node_impl_proxy(attr.clone(), item.clone());
@@ -99,15 +102,21 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
new_constructor
}
#[proc_macro_attribute]
pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
parsing::new_node_fn(attr.into(), item.into()).into()
}
/// Attaches an `impl TheGivenStruct` block to the node struct, containing a `new` constructor method. This is almost always called by the combined [`node_fn`] macro instead of using this one, however it can be used separately if needed. See that macro's documentation for more information.
#[proc_macro_attribute]
pub fn node_new(attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn old_node_new(attr: TokenStream, item: TokenStream) -> TokenStream {
node_new_impl(attr, item)
}
/// Attaches an `impl Node for TheGivenStruct` block to the node struct, containing an implementation of the node's `eval` method for a certain type signature. This can be called with multiple separate functions each having different type signatures. The [`node_fn`] macro calls this macro as well as defining a `new` constructor method on the node struct, which is a necessary part of defining a proto node; therefore you will most likely call that macro on the first decorated function and this macro on any additional decorated functions to provide additional type signatures for the proto node. See that macro's documentation for more information.
#[proc_macro_attribute]
pub fn node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
pub fn old_node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
node_impl_proxy(attr, item)
}

View File

@@ -0,0 +1,872 @@
use convert_case::{Case, Casing};
use indoc::indoc;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, ToTokens};
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Attribute, Error, ExprTuple, FnArg, GenericParam, Ident, ItemFn, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeTuple, WhereClause};
use crate::codegen::generate_node_code;
#[derive(Debug)]
pub(crate) struct ParsedNodeFn {
pub(crate) attributes: NodeFnAttributes,
pub(crate) fn_name: Ident,
pub(crate) struct_name: Ident,
pub(crate) mod_name: Ident,
pub(crate) fn_generics: Vec<GenericParam>,
pub(crate) where_clause: Option<WhereClause>,
pub(crate) input: Input,
pub(crate) output_type: Type,
pub(crate) is_async: bool,
pub(crate) fields: Vec<ParsedField>,
pub(crate) body: TokenStream2,
pub(crate) crate_name: proc_macro_crate::FoundCrate,
}
#[derive(Debug, Default)]
pub(crate) struct NodeFnAttributes {
pub(crate) category: Option<LitStr>,
pub(crate) display_name: Option<LitStr>,
pub(crate) path: Option<Path>,
pub(crate) skip_impl: bool,
// Add more attributes as needed
}
#[derive(Debug, Default)]
pub enum ValueSource {
#[default]
None,
Default(TokenStream2),
Scope(LitStr),
}
#[derive(Debug)]
pub(crate) enum ParsedField {
Regular {
pat_ident: PatIdent,
name: Option<LitStr>,
ty: Type,
exposed: bool,
value_source: ValueSource,
number_min: Option<LitFloat>,
number_max: Option<LitFloat>,
number_mode_range: Option<ExprTuple>,
implementations: Punctuated<Type, Comma>,
},
Node {
pat_ident: PatIdent,
name: Option<LitStr>,
input_type: Type,
output_type: Type,
implementations: Punctuated<TypeTuple, Comma>,
},
}
#[derive(Debug)]
pub(crate) struct Input {
pub(crate) pat_ident: PatIdent,
pub(crate) ty: Type,
pub(crate) implementations: Punctuated<Type, Comma>,
}
impl Parse for NodeFnAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut category = None;
let mut display_name = None;
let mut path = None;
let mut skip_impl = false;
let content = input;
// let content;
// syn::parenthesized!(content in input);
let nested = content.call(Punctuated::<Meta, Comma>::parse_terminated)?;
for meta in nested {
match meta {
Meta::List(meta) if meta.path.is_ident("category") => {
if category.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'category' attributes are not allowed"));
}
let lit: LitStr = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a string literal for 'category', e.g., category(\"Value\")"))?;
category = Some(lit);
}
Meta::List(meta) if meta.path.is_ident("name") => {
if display_name.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'name' attributes are not allowed"));
}
let parsed_name: LitStr = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a string for 'name', e.g., name(\"Memoize\")"))?;
display_name = Some(parsed_name);
}
Meta::List(meta) if meta.path.is_ident("path") => {
if path.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'path' attributes are not allowed"));
}
let parsed_path: Path = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'path', e.g., path(crate::MemoizeNode)"))?;
path = Some(parsed_path);
}
Meta::Path(path) if path.is_ident("skip_impl") => {
if skip_impl {
return Err(Error::new_spanned(path, "Multiple 'skip_impl' attributes are not allowed"));
}
skip_impl = true;
}
_ => {
return Err(Error::new_spanned(
meta,
indoc!(
r#"
Unsupported attribute in `node`.
Supported attributes are 'category', 'path' and 'name'.
Example usage:
#[node_macro::node(category("Value"), name("Test Node"))]
"#
),
));
}
}
}
Ok(NodeFnAttributes {
category,
display_name,
path,
skip_impl,
})
}
}
fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNodeFn> {
let attributes = syn::parse2::<NodeFnAttributes>(attr.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node_fn attributes: {}", e)))?;
let input_fn = syn::parse2::<ItemFn>(item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse function: {}. Make sure it's a valid Rust function.", e)))?;
let fn_name = input_fn.sig.ident.clone();
let struct_name = format_ident!("{}", fn_name.to_string().to_case(Case::Pascal));
let mod_name = fn_name.clone();
let fn_generics = input_fn.sig.generics.params.into_iter().collect();
let is_async = input_fn.sig.asyncness.is_some();
let (input, fields) = parse_inputs(&input_fn.sig.inputs)?;
let output_type = parse_output(&input_fn.sig.output)?;
let where_clause = input_fn.sig.generics.where_clause;
let body = input_fn.block.to_token_stream();
let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| {
Error::new(
proc_macro2::Span::call_site(),
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
)
})?;
Ok(ParsedNodeFn {
attributes,
fn_name,
struct_name,
mod_name,
fn_generics,
input,
output_type,
is_async,
fields,
where_clause,
body,
crate_name,
})
}
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>)> {
let mut fields = Vec::new();
let mut input = None;
for (index, arg) in inputs.iter().enumerate() {
if let FnArg::Typed(PatType { pat, ty, attrs, .. }) = arg {
// Call argument
if index == 0 {
if extract_attribute(attrs, "default").is_some() {
return Err(Error::new_spanned(&attrs[0], "Call argument cannot be given a default value".to_string()));
}
if extract_attribute(attrs, "expose").is_some() {
return Err(Error::new_spanned(&attrs[0], "Call argument cannot be exposed".to_string()));
}
let pat_ident = match (**pat).clone() {
Pat::Ident(pat_ident) => pat_ident,
Pat::Wild(wild) => PatIdent {
attrs: wild.attrs,
by_ref: None,
mutability: None,
ident: wild.underscore_token.into(),
subpat: None,
},
_ => continue,
};
let implementations = extract_attribute(attrs, "implementations")
.map(|attr| parse_implementations(attr, &pat_ident.ident))
.transpose()?
.unwrap_or_default();
input = Some(Input {
pat_ident,
ty: (**ty).clone(),
implementations,
});
} else if let Pat::Ident(pat_ident) = &**pat {
let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?;
fields.push(field);
} else {
return Err(Error::new_spanned(pat, "Expected a simple identifier for the field name"));
}
} else {
return Err(Error::new_spanned(arg, "Expected a typed argument (e.g., `x: i32`)"));
}
}
let input = input.ok_or_else(|| Error::new_spanned(inputs, "Expected at least one input argument. The first argument should be the node input type."))?;
Ok((input, fields))
}
fn parse_implementations<T: Parse>(attr: &Attribute, name: &Ident) -> syn::Result<Punctuated<T, Comma>> {
let content: TokenStream2 = attr
.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid implementations for argument '{}': {}", name, e)))?;
let parser = Punctuated::<T, Comma>::parse_terminated;
parser
.parse2(content)
.map_err(|e| Error::new_spanned(attr, format!("Failed to parse implementations for argument '{}': {}", name, e)))
}
fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Result<ParsedField> {
let ident = &pat_ident.ident;
let default_value = extract_attribute(attrs, "default")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid `default` value for argument '{}': {}", ident, e)))
})
.transpose()?;
let scope = extract_attribute(attrs, "scope")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid `scope` value for argument '{}': {}", ident, e)))
})
.transpose()?;
let name = extract_attribute(attrs, "name")
.map(|attr| attr.parse_args().map_err(|e| Error::new_spanned(attr, format!("Invalid `name` value for argument '{}': {}", ident, e))))
.transpose()?;
let exposed = extract_attribute(attrs, "expose").is_some();
let value_source = match (default_value, scope) {
(Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")),
(Some(default_value), _) => ValueSource::Default(default_value),
(_, Some(scope)) => ValueSource::Scope(scope),
_ => ValueSource::None,
};
let number_min = extract_attribute(attrs, "min")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `min` value for argument '{}': {}", ident, e)))
})
.transpose()?;
let number_max = extract_attribute(attrs, "max")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid numerical `max` value for argument '{}': {}", ident, e)))
})
.transpose()?;
let number_mode_range = extract_attribute(attrs, "range")
.map(|attr| {
attr.parse_args::<ExprTuple>().map_err(|e| {
Error::new_spanned(
attr,
format!(
"Invalid `range` tuple of min and max range slider values for argument '{}': {}\nUSAGE EXAMPLE: #[range((0., 100.))]",
ident, e
),
)
})
})
.transpose()?;
if let Some(range) = &number_mode_range {
if range.elems.len() != 2 {
return Err(Error::new_spanned(range, "Expected a tuple of two values for `range` for the min and max, respectively"));
}
}
let implementations = extract_attribute(attrs, "implementations")
.map(|attr| parse_implementations(attr, ident))
.transpose()?
.unwrap_or_default();
let (is_node, node_input_type, node_output_type) = parse_node_type(&ty);
if is_node {
let (input_type, output_type) = node_input_type
.zip(node_output_type)
.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input, Output = OutputType>`"))?;
if !matches!(&value_source, ValueSource::None) {
return Err(Error::new_spanned(&ty, "No default values for `impl Node` allowed"));
}
let implementations = extract_attribute(attrs, "implementations")
.map(|attr| parse_implementations(attr, ident))
.transpose()?
.unwrap_or_default();
Ok(ParsedField::Node {
pat_ident,
name,
input_type,
output_type,
implementations,
})
} else {
Ok(ParsedField::Regular {
pat_ident,
name,
exposed,
number_min,
number_max,
number_mode_range,
ty,
value_source,
implementations,
})
}
}
fn parse_node_type(ty: &Type) -> (bool, Option<Type>, Option<Type>) {
if let Type::ImplTrait(impl_trait) = ty {
for bound in &impl_trait.bounds {
if let syn::TypeParamBound::Trait(trait_bound) = bound {
if trait_bound.path.segments.last().map_or(false, |seg| seg.ident == "Node") {
if let syn::PathArguments::AngleBracketed(args) = &trait_bound.path.segments.last().unwrap().arguments {
let input_type = args.args.iter().find_map(|arg| if let syn::GenericArgument::Type(ty) = arg { Some(ty.clone()) } else { None });
let output_type = args.args.iter().find_map(|arg| {
if let syn::GenericArgument::AssocType(assoc_type) = arg {
if assoc_type.ident == "Output" {
Some(assoc_type.ty.clone())
} else {
None
}
} else {
None
}
});
return (true, input_type, output_type);
}
}
}
}
}
(false, None, None)
}
fn parse_output(output: &ReturnType) -> syn::Result<Type> {
match output {
ReturnType::Default => Ok(syn::parse_quote!(())),
ReturnType::Type(_, ty) => Ok((**ty).clone()),
}
}
fn extract_attribute<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
attrs.iter().find(|attr| attr.path().is_ident(name))
}
// Modify the new_node_fn function to use the code generation
pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> TokenStream2 {
match parse_node_fn(attr, item.clone()).and_then(|x| generate_node_code(&x)) {
Ok(parsed) => {
/*let generated_code = generate_node_code(&parsed);
// panic!("{}", generated_code.to_string());
quote! {
// #item
#generated_code
}*/
parsed
}
Err(e) => {
// Return the error as a compile error
Error::new(e.span(), format!("Failed to parse node function: {}", e)).to_compile_error()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::Span;
use proc_macro_crate::FoundCrate;
use quote::quote;
use syn::parse_quote;
fn pat_ident(name: &str) -> PatIdent {
PatIdent {
attrs: Vec::new(),
by_ref: None,
mutability: None,
ident: Ident::new(name, Span::call_site()),
subpat: None,
}
}
fn assert_parsed_node_fn(parsed: &ParsedNodeFn, expected: &ParsedNodeFn) {
assert_eq!(parsed.fn_name, expected.fn_name);
assert_eq!(parsed.struct_name, expected.struct_name);
assert_eq!(parsed.mod_name, expected.mod_name);
assert_eq!(parsed.is_async, expected.is_async);
assert_eq!(format!("{:?}", parsed.input), format!("{:?}", expected.input));
assert_eq!(format!("{:?}", parsed.output_type), format!("{:?}", expected.output_type));
assert_eq!(parsed.attributes.category, expected.attributes.category);
assert_eq!(parsed.attributes.display_name, expected.attributes.display_name);
assert_eq!(parsed.attributes.path, expected.attributes.path);
assert_eq!(parsed.attributes.skip_impl, expected.attributes.skip_impl);
assert_eq!(parsed.fields.len(), expected.fields.len());
for (parsed_field, expected_field) in parsed.fields.iter().zip(expected.fields.iter()) {
match (parsed_field, expected_field) {
(
ParsedField::Regular {
pat_ident: p_name,
ty: p_ty,
exposed: p_exp,
value_source: p_default,
..
},
ParsedField::Regular {
pat_ident: e_name,
ty: e_ty,
exposed: e_exp,
value_source: e_default,
..
},
) => {
assert_eq!(p_name, e_name);
assert_eq!(p_exp, e_exp);
match (p_default, e_default) {
(ValueSource::None, ValueSource::None) => {}
(ValueSource::Default(p), ValueSource::Default(e)) => {
assert_eq!(p.to_token_stream().to_string(), e.to_token_stream().to_string());
}
(ValueSource::Scope(p), ValueSource::Scope(e)) => {
assert_eq!(p.value(), e.value());
}
_ => panic!("Mismatched default values"),
}
assert_eq!(format!("{:?}", p_ty), format!("{:?}", e_ty));
}
(
ParsedField::Node {
pat_ident: p_name,
input_type: p_input,
output_type: p_output,
..
},
ParsedField::Node {
pat_ident: e_name,
input_type: e_input,
output_type: e_output,
..
},
) => {
assert_eq!(p_name, e_name);
assert_eq!(format!("{:?}", p_input), format!("{:?}", e_input));
assert_eq!(format!("{:?}", p_output), format!("{:?}", e_output));
}
_ => panic!("Mismatched field types"),
}
}
}
#[test]
fn test_basic_node() {
let attr = quote!(category("Math: Arithmetic"), path(graphene_core::TestNode), skip_impl);
let input = quote!(
fn add(a: f64, b: f64) -> f64 {
a + b
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")),
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: true,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
mod_name: Ident::new("add", Span::call_site()),
fn_generics: vec![],
where_clause: None,
input: Input {
pat_ident: pat_ident("a"),
ty: parse_quote!(f64),
implementations: Punctuated::new(),
},
output_type: parse_quote!(f64),
is_async: false,
fields: vec![ParsedField::Regular {
pat_ident: pat_ident("b"),
name: None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
implementations: Punctuated::new(),
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_node_with_impl_node() {
let attr = quote!(category("General"));
let input = quote!(
fn transform<T: 'static>(footprint: Footprint, transform_target: impl Node<Footprint, Output = T>, translate: DVec2) -> T {
// Implementation details...
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("General")),
display_name: None,
path: None,
skip_impl: false,
},
fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()),
mod_name: Ident::new("transform", Span::call_site()),
fn_generics: vec![parse_quote!(T: 'static)],
where_clause: None,
input: Input {
pat_ident: pat_ident("footprint"),
ty: parse_quote!(Footprint),
implementations: Punctuated::new(),
},
output_type: parse_quote!(T),
is_async: false,
fields: vec![
ParsedField::Node {
pat_ident: pat_ident("transform_target"),
name: None,
input_type: parse_quote!(Footprint),
output_type: parse_quote!(T),
implementations: Punctuated::new(),
},
ParsedField::Regular {
pat_ident: pat_ident("translate"),
name: None,
ty: parse_quote!(DVec2),
exposed: false,
value_source: ValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
implementations: Punctuated::new(),
},
],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_node_with_default_values() {
let attr = quote!(category("Vector: Shape"));
let input = quote!(
fn circle(_: (), #[default(50.)] radius: f64) -> VectorData {
// Implementation details...
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("Vector: Shape")),
display_name: None,
path: None,
skip_impl: false,
},
fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()),
mod_name: Ident::new("circle", Span::call_site()),
fn_generics: vec![],
where_clause: None,
input: Input {
pat_ident: pat_ident("_"),
ty: parse_quote!(()),
implementations: Punctuated::new(),
},
output_type: parse_quote!(VectorData),
is_async: false,
fields: vec![ParsedField::Regular {
pat_ident: pat_ident("radius"),
name: None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::Default(quote!(50.)),
number_min: None,
number_max: None,
number_mode_range: None,
implementations: Punctuated::new(),
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_node_with_implementations() {
let attr = quote!(category("Raster: Adjustment"));
let input = quote!(
fn levels<P: Pixel>(image: ImageFrame<P>, #[implementations(f32, f64)] shadows: f64) -> ImageFrame<P> {
// Implementation details...
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("Raster: Adjustment")),
display_name: None,
path: None,
skip_impl: false,
},
fn_name: Ident::new("levels", Span::call_site()),
struct_name: Ident::new("Levels", Span::call_site()),
mod_name: Ident::new("levels", Span::call_site()),
fn_generics: vec![parse_quote!(P: Pixel)],
where_clause: None,
input: Input {
pat_ident: pat_ident("image"),
ty: parse_quote!(ImageFrame<P>),
implementations: Punctuated::new(),
},
output_type: parse_quote!(ImageFrame<P>),
is_async: false,
fields: vec![ParsedField::Regular {
pat_ident: pat_ident("shadows"),
name: None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
implementations: {
let mut p = Punctuated::new();
p.push(parse_quote!(f32));
p.push(parse_quote!(f64));
p
},
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_number_min_max_range_mode() {
let attr = quote!(category("Math: Arithmetic"), path(graphene_core::TestNode));
let input = quote!(
fn add(
a: f64,
#[range((0., 100.))]
#[min(-500.)]
#[max(500.)]
b: f64,
) -> f64 {
a + b
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("Math: Arithmetic")),
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: false,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
mod_name: Ident::new("add", Span::call_site()),
fn_generics: vec![],
where_clause: None,
input: Input {
pat_ident: pat_ident("a"),
ty: parse_quote!(f64),
implementations: Punctuated::new(),
},
output_type: parse_quote!(f64),
is_async: false,
fields: vec![ParsedField::Regular {
pat_ident: pat_ident("b"),
name: None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
number_min: Some(parse_quote!(-500.)),
number_max: Some(parse_quote!(500.)),
number_mode_range: Some(parse_quote!((0., 100.))),
implementations: Punctuated::new(),
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_async_node() {
let attr = quote!(category("IO"));
let input = quote!(
async fn load_image(api: &WasmEditorApi, #[expose] path: String) -> ImageFrame<Color> {
// Implementation details...
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("IO")),
display_name: None,
path: None,
skip_impl: false,
},
fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()),
mod_name: Ident::new("load_image", Span::call_site()),
fn_generics: vec![],
where_clause: None,
input: Input {
pat_ident: pat_ident("api"),
ty: parse_quote!(&WasmEditorApi),
implementations: Punctuated::new(),
},
output_type: parse_quote!(ImageFrame<Color>),
is_async: true,
fields: vec![ParsedField::Regular {
pat_ident: pat_ident("path"),
name: None,
ty: parse_quote!(String),
exposed: true,
value_source: ValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
implementations: Punctuated::new(),
}],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
fn test_node_with_custom_name() {
let attr = quote!(category("Custom"), name("CustomNode2"));
let input = quote!(
fn custom_node(input: i32) -> i32 {
input * 2
}
);
let parsed = parse_node_fn(attr, input).unwrap();
let expected = ParsedNodeFn {
attributes: NodeFnAttributes {
category: Some(parse_quote!("Custom")),
display_name: Some(parse_quote!("CustomNode2")),
path: None,
skip_impl: false,
},
fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()),
mod_name: Ident::new("custom_node", Span::call_site()),
fn_generics: vec![],
where_clause: None,
input: Input {
pat_ident: pat_ident("input"),
ty: parse_quote!(i32),
implementations: Punctuated::new(),
},
output_type: parse_quote!(i32),
is_async: false,
fields: vec![],
body: TokenStream2::new(),
crate_name: FoundCrate::Itself,
};
assert_parsed_node_fn(&parsed, &expected);
}
#[test]
#[should_panic(expected = "Multiple 'category' attributes are not allowed")]
fn test_multiple_categories() {
let attr = quote!(category("Math: Arithmetic"), category("General"));
let input = quote!(
fn add(a: i32, b: i32) -> i32 {
a + b
}
);
parse_node_fn(attr, input).unwrap();
}
#[test]
#[should_panic(expected = "Call argument cannot be given a default value")]
fn test_default_value_for_first_arg() {
let attr = quote!(category("Invalid"));
let input = quote!(
fn invalid_node(#[default(())] node: impl Node<(), Output = i32>) -> i32 {
node.eval(())
}
);
parse_node_fn(attr, input).unwrap();
}
#[test]
#[should_panic(expected = "No default values for `impl Node` allowed")]
fn test_default_value_for_impl_node() {
let attr = quote!(category("Invalid"));
let input = quote!(
fn invalid_node(_: (), #[default(())] node: impl Node<(), Output = i32>) -> i32 {
node.eval(())
}
);
parse_node_fn(attr, input).unwrap();
}
#[test]
#[should_panic(expected = "Unsupported attribute in `node`")]
fn test_unsupported_attribute() {
let attr = quote!(unsupported("Value"));
let input = quote!(
fn test_node(input: i32) -> i32 {
input
}
);
parse_node_fn(attr, input).unwrap();
}
}