reactive_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{Expr, Ident, ItemFn, ItemStatic, ReturnType, parse_macro_input};
4
5/// Wraps a `static mut` variable as a reactive signal (similar to a property)
6/// with getter and setter functions.
7///
8/// The `signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
9/// and automatically generates:
10/// 1. A `_get()` function to read the value.
11/// 2. A `_set(value)` function to write the value (returns `true` if changed).
12/// 3. A function with the same name as the variable to simplify access (calls `_get()`).
13///
14/// # Requirements
15///
16/// - The macro currently supports only `static mut` variables.
17/// - The variable type must implement `Eq + Default`.
18///
19/// # Examples
20///
21/// ```rust
22/// use std::cell::Cell;
23/// use reactive_macros::signal;
24///
25/// signal!(static mut A: i32 = 10;);
26///
27/// assert_eq!(*A(), 10);
28/// assert_eq!(*A_get(), 10);
29/// assert!(A_set(20));
30/// assert_eq!(*A(), 20);
31/// assert!(!A_set(20)); // No change
32/// ```
33///
34/// # SAFETY
35///
36/// This macro wraps `static mut` variables internally, so it **is not thread-safe**.
37/// It should be used only in single-threaded contexts.
38///
39/// # Warning
40///
41/// **Do not set any signal that is part of the same effect chain.**
42///
43/// Effects automatically run whenever one of their dependent signals changes.
44/// If an effect modifies a signal that it (directly or indirectly) observes,
45/// it creates a circular dependency. This can lead to:
46/// - an infinite loop of updates, or
47/// - conflicting updates that the system cannot resolve.
48///
49/// In the general case, it is impossible to automatically determine whether
50/// such an effect will ever terminate—this is essentially a version of the
51/// halting problem. Therefore, you must ensure manually that effects do not
52/// update signals within their own dependency chain.
53#[proc_macro]
54pub fn signal(input: TokenStream) -> TokenStream {
55    let item = parse_macro_input!(input as ItemStatic);
56
57    let attrs = &item.attrs;
58    let vis = &item.vis;
59    let static_token = &item.static_token;
60    let _mutability = &item.mutability;
61    let ident = &item.ident;
62    let colon_token = &item.colon_token;
63    let ty = &item.ty;
64    let eq_token = &item.eq_token;
65    let expr = &item.expr;
66    let semi_token = &item.semi_token;
67
68    let mutability = match &item.mutability {
69        syn::StaticMutability::Mut(_) => quote! { mut },
70        syn::StaticMutability::None => quote! {},
71        _ => {
72            return syn::Error::new_spanned(&item.mutability, "Mutability not supported")
73                .to_compile_error()
74                .into();
75        }
76    };
77
78    let ident_p = format_ident!("_{}", ident.to_string().to_uppercase());
79    let ident_get = format_ident!("{}_get", ident);
80    let ident_set = format_ident!("{}_set", ident);
81    let ident_fn = format_ident!("{}", ident);
82
83    let lazy_ty = quote! { once_cell::unsync::Lazy<reactive_cache::Signal<#ty>> };
84    let expr = quote! { once_cell::unsync::Lazy::new(|| reactive_cache::Signal::new(Some(#expr))) };
85
86    let expanded = quote! {
87        #(#attrs)*
88        #vis #static_token #mutability #ident_p #colon_token #lazy_ty #eq_token #expr #semi_token
89
90        #[allow(non_snake_case)]
91        pub fn #ident_get() -> std::cell::Ref<'static, #ty> {
92            unsafe { #ident_p.get() }
93        }
94
95        #[allow(non_snake_case)]
96        pub fn #ident_set(value: #ty) -> bool {
97            unsafe { #ident_p.set(value) }
98        }
99
100        #[allow(non_snake_case)]
101        pub fn #ident_fn() -> std::cell::Ref<'static, #ty> {
102            #ident_get()
103        }
104    };
105
106    expanded.into()
107}
108
109/// Turns a zero-argument function into a memoized, reactive computation.
110///
111/// The `#[memo]` attribute macro transforms a function into a static
112/// `reactive_cache::Memo`, which:
113/// 1. Computes the value the first time the function is called.
114/// 2. Caches the result for future calls.
115/// 3. Automatically tracks reactive dependencies if used inside `Signal` or other reactive contexts.
116///
117/// # Requirements
118///
119/// - The function must have **no parameters**.
120/// - The function must return a value (`-> T`), which must implement `Clone`.
121///
122/// # Examples
123///
124/// ```rust
125/// use reactive_macros::memo;
126///
127/// #[memo]
128/// pub fn get_number() -> i32 {
129///     // The first call sets INVOKED to true
130///     static mut INVOKED: bool = false;
131///     assert!(!unsafe { INVOKED });
132///     unsafe { INVOKED = true };
133///
134///     42
135/// }
136///
137/// fn main() {
138///     // First call computes and caches the value
139///     assert_eq!(get_number(), 42);
140///     // Subsequent calls return the cached value without re-running the block
141///     assert_eq!(get_number(), 42);
142/// }
143/// ```
144///
145/// # SAFETY
146///
147/// This macro uses a `static mut` internally, so it **is not thread-safe**.
148/// It is intended for single-threaded usage only. Accessing the memo from
149/// multiple threads concurrently can cause undefined behavior.
150#[proc_macro_attribute]
151pub fn memo(_attr: TokenStream, item: TokenStream) -> TokenStream {
152    let func = parse_macro_input!(item as ItemFn);
153
154    let vis = &func.vis;
155    let sig = &func.sig;
156    let block = &func.block;
157    let ident = &func.sig.ident;
158
159    let output_ty = match &sig.output {
160        ReturnType::Type(_, ty) => ty.clone(),
161        _ => {
162            return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
163                .to_compile_error()
164                .into();
165        }
166    };
167
168    if !sig.inputs.is_empty() {
169        return syn::Error::new_spanned(
170            &sig.inputs,
171            "The memo macro can only be used with `get` function without any parameters.",
172        )
173        .to_compile_error()
174        .into();
175    }
176
177    let ident = format_ident!("{}", ident.to_string().to_uppercase());
178    let ty =
179        quote! { once_cell::unsync::Lazy<reactive_cache::Memo<#output_ty, fn() -> #output_ty>> };
180    let expr = quote! { once_cell::unsync::Lazy::new(|| reactive_cache::Memo::new(|| #block)) };
181
182    let expanded = quote! {
183        static mut #ident: #ty = #expr;
184
185        #vis #sig
186        where #output_ty: Clone
187        {
188            unsafe { (*#ident).get() }
189        }
190    };
191
192    expanded.into()
193}
194
195/// Creates a reactive effect from a closure or function pointer.
196///
197/// The `effect!` procedural macro is a convenient wrapper around `reactive_cache::Effect::new`.
198/// It allows you to quickly register a reactive effect that automatically tracks
199/// dependencies and re-runs when they change.
200///
201/// # Requirements
202///
203/// - The argument must be either:
204///   1. A closure (e.g., `|| { ... }`), or  
205///   2. A function pointer / function name with zero arguments.
206/// - The closure or function must return `()` (no return value required).
207///
208/// # Examples
209///
210/// ```rust
211/// use std::{cell::Cell, rc::Rc};
212/// use reactive_macros::{effect, signal};
213///
214/// signal!(static mut A: i32 = 1;);
215///
216/// // Track effect runs
217/// let counter = Rc::new(Cell::new(0));
218/// let counter_clone = counter.clone();
219///
220/// let e = effect!(move || {
221///     let _ = A();           // reading the signal
222///     counter_clone.set(counter_clone.get() + 1); // increment effect counter
223/// });
224///
225/// let ptr = Rc::into_raw(e); // actively leak to avoid implicitly dropping the effect
226///
227/// // Effect runs immediately upon creation
228/// assert_eq!(counter.get(), 1);
229///
230/// // Changing A triggers the effect again
231/// assert!(A_set(10));
232/// assert_eq!(counter.get(), 2);
233///
234/// // Setting the same value does NOT trigger the effect
235/// assert!(!A_set(10));
236/// assert_eq!(counter.get(), 2);
237/// ```
238///
239/// # SAFETY
240///
241/// The macro internally uses `reactive_cache::Effect`, which relies on
242/// `static` tracking and is **not thread-safe**. Only use in single-threaded contexts.
243///
244/// # Warning
245///
246/// **Do not set any signal that is part of the same effect chain.**
247///
248/// Effects automatically run whenever one of their dependent signals changes.
249/// If an effect modifies a signal that it (directly or indirectly) observes,
250/// it creates a circular dependency. This can lead to:
251/// - an infinite loop of updates, or
252/// - conflicting updates that the system cannot resolve.
253///
254/// In the general case, it is impossible to automatically determine whether
255/// such an effect will ever terminate—this is essentially a version of the
256/// halting problem. Therefore, you must ensure manually that effects do not
257/// update signals within their own dependency chain.
258#[proc_macro]
259pub fn effect(input: TokenStream) -> TokenStream {
260    let expr = parse_macro_input!(input as Expr);
261
262    let expanded = match expr {
263        Expr::Path(path) if path.path.get_ident().is_some() => {
264            let ident = path.path.get_ident().unwrap();
265            quote! {
266                reactive_cache::Effect::new(#ident)
267            }
268        }
269        Expr::Closure(closure) => {
270            quote! {
271                reactive_cache::Effect::new(#closure)
272            }
273        }
274        _ => {
275            return syn::Error::new_spanned(&expr, "Expected a variable name or a closure")
276                .to_compile_error()
277                .into();
278        }
279    };
280
281    expanded.into()
282}
283
284/// Evaluates a zero-argument function and optionally reports when the value changes.
285///
286/// The `#[evaluate(print_fn)]` attribute macro transforms a function into a reactive
287/// evaluator that:
288/// 1. Computes the function result on each call.
289/// 2. Compares it with the previously computed value.
290/// 3. If the value is unchanged, calls the specified print function with a message.
291///
292/// # Requirements
293///
294/// - The function must have **no parameters**.
295/// - The function must return a value (`-> T`), which must implement `Eq + Clone`.
296/// - The print function (e.g., `print`) must be a callable accepting a `String`.
297///
298/// # Examples
299///
300/// ```rust
301/// use reactive_macros::evaluate;
302///
303/// fn print(msg: String) {
304///     println!("{}", msg);
305/// }
306///
307/// #[evaluate(print)]
308/// pub fn get_number() -> i32 {
309///     42
310/// }
311///
312/// fn main() {
313///     // First call computes the value
314///     assert_eq!(get_number(), 42);
315///     // Second call compares with previous; prints message since value didn't change
316///     assert_eq!(get_number(), 42);
317/// }
318/// ```
319///
320/// # SAFETY
321///
322/// This macro uses a `static mut` internally to store the previous value,
323/// so it **is not thread-safe**. It should only be used in single-threaded contexts.
324#[proc_macro_attribute]
325pub fn evaluate(attr: TokenStream, item: TokenStream) -> TokenStream {
326    let print = parse_macro_input!(attr as Ident);
327    let func = parse_macro_input!(item as ItemFn);
328
329    let vis = &func.vis;
330    let sig = &func.sig;
331    let block = &func.block;
332    let ident = &func.sig.ident;
333
334    let output_ty = match &sig.output {
335        ReturnType::Type(_, ty) => ty.clone(),
336        _ => {
337            return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
338                .to_compile_error()
339                .into();
340        }
341    };
342
343    if !sig.inputs.is_empty() {
344        return syn::Error::new_spanned(
345            &sig.inputs,
346            "The memo macro can only be used with `get` function without any parameters.",
347        )
348        .to_compile_error()
349        .into();
350    }
351
352    let option_ty = quote! { Option<#output_ty> };
353    let ident = ident.to_string();
354
355    let expanded = quote! {
356        #vis #sig
357        where #output_ty: Eq + Clone
358        {
359            let new: #output_ty = (|| #block)();
360
361            static mut VALUE: #option_ty = None;
362            if let Some(old) = unsafe { VALUE } && old == new {
363                #print(format!("Evaluate: {} not changed, still {:?}\n", #ident, new));
364            }
365            unsafe { VALUE = Some(new.clone()) };
366
367            new
368        }
369    };
370
371    expanded.into()
372}