reactive_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{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! { reactive_cache::Lazy<reactive_cache::Signal<#ty>> };
84    let expr = quote! { reactive_cache::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 = quote! { reactive_cache::Lazy<reactive_cache::Memo<#output_ty, fn() -> #output_ty>> };
179    let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Memo::new(|| #block)) };
180
181    let expanded = quote! {
182        static mut #ident: #ty = #expr;
183
184        #vis #sig
185        where #output_ty: Clone
186        {
187            unsafe { (*#ident).get() }
188        }
189    };
190
191    expanded.into()
192}
193
194/// Evaluates a zero-argument function and optionally reports when the value changes.
195///
196/// The `#[evaluate(print_fn)]` attribute macro transforms a function into a reactive
197/// evaluator that:
198/// 1. Computes the function result on each call.
199/// 2. Compares it with the previously computed value.
200/// 3. If the value is unchanged, calls the specified print function with a message.
201///
202/// # Requirements
203///
204/// - The function must have **no parameters**.
205/// - The function must return a value (`-> T`), which must implement `Eq + Clone`.
206/// - The print function (e.g., `print`) must be a callable accepting a `String`.
207///
208/// # Examples
209///
210/// ```rust
211/// use reactive_macros::evaluate;
212///
213/// fn print(msg: String) {
214///     println!("{}", msg);
215/// }
216///
217/// #[evaluate(print)]
218/// pub fn get_number() -> i32 {
219///     42
220/// }
221///
222/// fn main() {
223///     // First call computes the value
224///     assert_eq!(get_number(), 42);
225///     // Second call compares with previous; prints message since value didn't change
226///     assert_eq!(get_number(), 42);
227/// }
228/// ```
229///
230/// # SAFETY
231///
232/// This macro uses a `static mut` internally to store the previous value,
233/// so it **is not thread-safe**. It should only be used in single-threaded contexts.
234#[proc_macro_attribute]
235pub fn evaluate(attr: TokenStream, item: TokenStream) -> TokenStream {
236    let print = parse_macro_input!(attr as Ident);
237    let func = parse_macro_input!(item as ItemFn);
238
239    let vis = &func.vis;
240    let sig = &func.sig;
241    let block = &func.block;
242    let ident = &func.sig.ident;
243
244    let output_ty = match &sig.output {
245        ReturnType::Type(_, ty) => ty.clone(),
246        _ => {
247            return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
248                .to_compile_error()
249                .into();
250        }
251    };
252
253    if !sig.inputs.is_empty() {
254        return syn::Error::new_spanned(
255            &sig.inputs,
256            "The memo macro can only be used with `get` function without any parameters.",
257        )
258        .to_compile_error()
259        .into();
260    }
261
262    let option_ty = quote! { Option<#output_ty> };
263    let ident = ident.to_string();
264
265    let expanded = quote! {
266        #vis #sig
267        where #output_ty: Eq + Clone
268        {
269            let new: #output_ty = (|| #block)();
270
271            static mut VALUE: #option_ty = None;
272            if let Some(old) = unsafe { VALUE } && old == new {
273                #print(format!("Evaluate: {} not changed, still {:?}\n", #ident, new));
274            }
275            unsafe { VALUE = Some(new.clone()) };
276
277            new
278        }
279    };
280
281    expanded.into()
282}