Migrate memo nodes to node macro and make implementing other persistent nodes easier (#3552)

* Add #[data] and #[serialize] attributes to node macro

- Add #[data] attribute for struct fields that aren't node parameters
  - Data fields are initialized with Default::default()
  - Passed as references to the underlying function
  - Excluded from registry metadata (internal state)
  - Generic types in data fields allowed without #[implementations]

- Add #[serialize] attribute for custom Node::serialize() implementation
  - Receives references to all data fields
  - Generates serialize() method in Node trait impl

- Conditional derives based on data field presence
  - With data fields: Debug, Clone only
  - Without data fields: Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash

* Refactor Memo and Monitor Node to use node macro

* Move Complex type into type alias

* Fix format

* Update node-graph/nodes/gcore/src/memo.rs

Co-authored-by: Keavon Chambers <keavon@keavon.com>

* Update node-graph/nodes/gcore/src/memo.rs

Co-authored-by: Keavon Chambers <keavon@keavon.com>

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2026-01-05 22:28:02 +01:00
committed by GitHub
parent 8f25eb6ca4
commit fafc687d84
5 changed files with 249 additions and 115 deletions

View File

@@ -50,6 +50,8 @@ pub(crate) struct NodeFnAttributes {
pub(crate) cfg: Option<TokenStream2>,
/// if this node should get a gpu implementation, defaults to None
pub(crate) shader_node: Option<ShaderNodeType>,
/// Custom serialization function path (e.g., "my_module::custom_serialize")
pub(crate) serialize: Option<Path>,
// Add more attributes as needed
}
@@ -112,6 +114,7 @@ pub struct ParsedField {
pub number_display_decimal_places: Option<LitInt>,
pub number_step: Option<LitFloat>,
pub unit: Option<LitStr>,
pub is_data_field: bool,
}
#[derive(Clone, Debug)]
@@ -201,6 +204,7 @@ impl Parse for NodeFnAttributes {
let mut properties_string = None;
let mut cfg = None;
let mut shader_node = None;
let mut serialize = None;
let content = input;
// let content;
@@ -270,13 +274,23 @@ impl Parse for NodeFnAttributes {
let meta = meta.require_list()?;
shader_node = Some(syn::parse2(meta.tokens.to_token_stream())?);
}
"serialize" => {
let meta = meta.require_list()?;
if serialize.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'serialize' attributes are not allowed"));
}
let parsed_path: Path = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'serialize', e.g., serialize(my_module::custom_serialize)"))?;
serialize = Some(parsed_path);
}
_ => {
return Err(Error::new_spanned(
meta,
indoc!(
r#"
Unsupported attribute in `node`.
Supported attributes are 'category', 'path' 'name', 'skip_impl', 'cfg' and 'properties'.
Supported attributes are 'category', 'path', 'name', 'skip_impl', 'cfg', 'properties', 'serialize', and 'shader_node'.
Example usage:
#[node_macro::node(category("Value"), name("Test Node"))]
@@ -295,6 +309,7 @@ impl Parse for NodeFnAttributes {
properties_string,
cfg,
shader_node,
serialize,
})
}
}
@@ -467,6 +482,9 @@ fn parse_node_implementations<T: Parse>(attr: &Attribute, name: &Ident) -> syn::
fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Result<ParsedField> {
let ident = &pat_ident.ident;
// Check if this is a data field (struct field, not a parameter)
let is_data_field = extract_attribute(attrs, "data").is_some();
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()?;
@@ -489,6 +507,25 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
let exposed = extract_attribute(attrs, "expose").is_some();
// Validate data field attributes
if is_data_field {
if default_value.is_some() {
return Err(Error::new_spanned(
&pat_ident,
"Data fields (#[data]) cannot have #[default] attribute. They are automatically initialized with Default::default()",
));
}
if scope.is_some() {
return Err(Error::new_spanned(&pat_ident, "Data fields (#[data]) cannot have #[scope] attribute"));
}
if exposed {
return Err(Error::new_spanned(
&pat_ident,
"Data fields (#[data]) cannot be exposed (#[expose]). They are internal state, not node parameters",
));
}
}
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), _) => ParsedValueSource::Default(default_value),
@@ -586,6 +623,14 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
.fold(String::new(), |acc, b| acc + &b + "\n");
if is_node {
// Data fields cannot be impl Node types
if is_data_field {
return Err(Error::new_spanned(
&ty,
"Data fields (#[data]) cannot be of type `impl Node`. Data fields must be concrete types that implement Default",
));
}
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>`"))?;
@@ -610,6 +655,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
number_display_decimal_places,
number_step,
unit,
is_data_field,
})
} else {
let implementations = extract_attribute(attrs, "implementations")
@@ -636,6 +682,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
number_display_decimal_places,
number_step,
unit,
is_data_field,
})
}
}
@@ -826,6 +873,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@@ -860,6 +908,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
}],
body: TokenStream2::new(),
description: String::from("Multi\nLine\n"),
@@ -892,6 +941,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()),
@@ -920,6 +970,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
},
ParsedField {
pat_ident: pat_ident("translate"),
@@ -941,6 +992,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
},
],
body: TokenStream2::new(),
@@ -971,6 +1023,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()),
@@ -1005,6 +1058,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
}],
body: TokenStream2::new(),
description: "Test\n".into(),
@@ -1033,6 +1087,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("levels", Span::call_site()),
struct_name: Ident::new("Levels", Span::call_site()),
@@ -1072,6 +1127,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
}],
body: TokenStream2::new(),
description: String::new(),
@@ -1107,6 +1163,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@@ -1141,6 +1198,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
}],
body: TokenStream2::new(),
description: String::new(),
@@ -1169,6 +1227,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()),
@@ -1203,6 +1262,7 @@ mod tests {
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
}],
body: TokenStream2::new(),
description: String::new(),
@@ -1231,6 +1291,7 @@ mod tests {
properties_string: None,
cfg: None,
shader_node: None,
serialize: None,
},
fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()),