Skip to main content

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}