wingfoil_derive/lib.rs
1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 Ident, ImplItem, ImplItemFn, ItemImpl, Token, Type, bracketed,
5 parse::{Parse, ParseStream},
6 parse_macro_input,
7 punctuated::Punctuated,
8};
9
10struct NodeArgs {
11 active: Vec<Ident>,
12 passive: Vec<Ident>,
13 output: Option<(Ident, Type)>,
14}
15
16impl Parse for NodeArgs {
17 fn parse(input: ParseStream) -> syn::Result<Self> {
18 let mut active: Option<Vec<Ident>> = None;
19 let mut passive: Option<Vec<Ident>> = None;
20 let mut output: Option<(Ident, Type)> = None;
21
22 while !input.is_empty() {
23 let key: Ident = input.parse()?;
24 input.parse::<Token![=]>()?;
25
26 match key.to_string().as_str() {
27 "active" => {
28 if active.is_some() {
29 return Err(syn::Error::new(key.span(), "duplicate key `active`"));
30 }
31 let content;
32 bracketed!(content in input);
33 let list = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
34 active = Some(list.into_iter().collect());
35 }
36 "passive" => {
37 if passive.is_some() {
38 return Err(syn::Error::new(key.span(), "duplicate key `passive`"));
39 }
40 let content;
41 bracketed!(content in input);
42 let list = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
43 passive = Some(list.into_iter().collect());
44 }
45 "output" => {
46 if output.is_some() {
47 return Err(syn::Error::new(key.span(), "duplicate key `output`"));
48 }
49 let field: Ident = input.parse()?;
50 input.parse::<Token![:]>()?;
51 let ty: Type = input.parse()?;
52 output = Some((field, ty));
53 }
54 _ => {
55 return Err(syn::Error::new(
56 key.span(),
57 format!("unknown key `{key}`; expected `active`, `passive`, or `output`"),
58 ));
59 }
60 }
61
62 if input.peek(Token![,]) {
63 input.parse::<Token![,]>()?;
64 }
65 }
66
67 Ok(NodeArgs {
68 active: active.unwrap_or_default(),
69 passive: passive.unwrap_or_default(),
70 output,
71 })
72 }
73}
74
75/// Attribute macro that reduces boilerplate in [`MutableNode`] implementations.
76///
77/// Place `#[node(...)]` on an `impl MutableNode for MyType` block. It can:
78///
79/// - Inject `fn upstreams()` from `active = [field1, field2]` and/or `passive = [field3]`
80/// - Emit a separate `impl StreamPeekRef<T>` from `output = field_name: FieldType`
81///
82/// Fields listed as `active` or `passive` must implement `AsUpstreamNodes`
83/// (`Rc<dyn Node>`, `Rc<dyn Stream<T>>`, or `Vec` of either).
84///
85/// If neither `active` nor `passive` is specified, no `upstreams()` is injected —
86/// the default `UpStreams::none()` from [`MutableNode`] is used (source node), or you
87/// can write `upstreams()` manually in the impl block for complex cases (e.g. `Dep<T>`).
88///
89/// # Examples
90///
91/// ```rust,ignore
92/// // Transform node: active upstream, stream output
93/// #[node(active = [upstream], output = value: OUT)]
94/// impl<IN, OUT: Element> MutableNode for MapStream<IN, OUT> {
95/// fn cycle(&mut self, _state: &mut GraphState) -> anyhow::Result<bool> {
96/// self.value = (self.func)(self.upstream.peek_value());
97/// Ok(true)
98/// }
99/// }
100///
101/// // Sink node: active upstream, no output
102/// #[node(active = [upstream])]
103/// impl<IN> MutableNode for ConsumerNode<IN> {
104/// fn cycle(&mut self, state: &mut GraphState) -> anyhow::Result<bool> { ... }
105/// }
106///
107/// // Source node with output (upstreams default to none)
108/// #[node(output = value: T)]
109/// impl<T: Element> MutableNode for ConstantStream<T> {
110/// fn cycle(&mut self, _state: &mut GraphState) -> anyhow::Result<bool> { Ok(true) }
111/// }
112///
113/// // Mixed: passive + active upstreams, output
114/// #[node(passive = [upstream], active = [trigger], output = value: T)]
115/// impl<T: Element> MutableNode for SampleStream<T> { ... }
116///
117/// // Complex upstreams (Dep<T> etc.) — write upstreams() manually, use #[node] for output only
118/// #[node(output = value: OUT)]
119/// impl<IN1: 'static, IN2: 'static, OUT: Element> MutableNode for BiMapStream<IN1, IN2, OUT> {
120/// fn cycle(&mut self, ...) -> anyhow::Result<bool> { ... }
121/// fn upstreams(&self) -> UpStreams { /* Dep<T> logic */ }
122/// }
123/// ```
124#[proc_macro_attribute]
125pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
126 let args = parse_macro_input!(attr as NodeArgs);
127 let mut impl_block = parse_macro_input!(item as ItemImpl);
128
129 let self_ty = impl_block.self_ty.clone();
130 let (impl_generics, _, where_clause) = impl_block.generics.split_for_impl();
131
132 // Inject fn upstreams() if active/passive fields are specified.
133 if !args.active.is_empty() || !args.passive.is_empty() {
134 let active_fields = &args.active;
135 let passive_fields = &args.passive;
136
137 let upstreams_fn: ImplItemFn = syn::parse_quote! {
138 fn upstreams(&self) -> UpStreams {
139 let mut active: ::std::vec::Vec<::std::rc::Rc<dyn Node>> = ::std::vec::Vec::new();
140 let mut passive: ::std::vec::Vec<::std::rc::Rc<dyn Node>> = ::std::vec::Vec::new();
141 #(active.extend(AsUpstreamNodes::as_upstream_nodes(&self.#active_fields));)*
142 #(passive.extend(AsUpstreamNodes::as_upstream_nodes(&self.#passive_fields));)*
143 UpStreams::new(active, passive)
144 }
145 };
146 impl_block.items.push(ImplItem::Fn(upstreams_fn));
147 }
148
149 // Emit a StreamPeekRef impl if output is specified.
150 let peek_ref_impl = args.output.map(|(field, ty)| {
151 quote! {
152 impl #impl_generics StreamPeekRef<#ty> for #self_ty #where_clause {
153 fn peek_ref(&self) -> &#ty {
154 &self.#field
155 }
156 }
157 }
158 });
159
160 quote! {
161 #impl_block
162 #peek_ref_impl
163 }
164 .into()
165}