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 global signal.
6///
7/// The `signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
8/// and generates a **function with the same name as the variable** that returns a
9/// `&'static Rc<Signal<T>>`. You can then call `.get()` to read the value or `.set(value)` to update it.
10///
11/// # Requirements
12///
13/// - Supports only `static mut` variables.
14/// - Type `T` must implement `Eq`.
15///
16/// # Examples
17///
18/// ```rust
19/// use reactive_cache::prelude::*;
20/// use reactive_macros::signal;
21///
22/// signal!(static mut A: i32 = 10;);
23///
24/// assert_eq!(*A().get(), 10);
25/// assert!(A().set(20));
26/// assert_eq!(*A().get(), 20);
27/// assert!(!A().set(20)); // No change
28///
29/// signal!(static mut B: String = "hello".to_string(););
30///
31/// assert_eq!(*B().get(), "hello");
32/// assert!(B().set("world".to_string()));
33/// assert_eq!(*B().get(), "world");
34/// ```
35///
36/// # SAFETY
37///
38/// This macro wraps `static mut` variables internally, so it **is not thread-safe**.
39/// It should be used only in single-threaded contexts.
40///
41/// # Warning
42///
43/// **Do not set any signal that is part of the same effect chain.**
44///
45/// Effects automatically run whenever one of their dependent signals changes.
46/// If an effect modifies a signal that it (directly or indirectly) observes,
47/// it creates a circular dependency. This can lead to:
48/// - an infinite loop of updates, or
49/// - conflicting updates that the system cannot resolve.
50///
51/// In the general case, it is impossible to automatically determine whether
52/// such an effect will ever terminate—this is essentially a version of the
53/// halting problem. Therefore, you must ensure manually that effects do not
54/// update signals within their own dependency chain.
55#[proc_macro]
56pub fn signal(input: TokenStream) -> TokenStream {
57    let item = parse_macro_input!(input as ItemStatic);
58
59    let vis = &item.vis;
60    let ident = &item.ident;
61    let ty = &item.ty;
62    let expr = &item.expr;
63
64    let lazy_ty = quote! { reactive_cache::Lazy<std::rc::Rc<reactive_cache::Signal<#ty>>> };
65    let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Signal::new(#expr)) };
66
67    let expanded = quote! {
68        #[allow(non_snake_case)]
69        #vis fn #ident() -> &'static std::rc::Rc<reactive_cache::Signal<#ty>> {
70            static mut #ident: #lazy_ty = #expr;
71            unsafe { &*#ident }
72        }
73    };
74
75    expanded.into()
76}
77
78/// Turns a zero-argument function into a memoized, reactive computation.
79///
80/// The `#[memo]` attribute macro transforms a function into a static
81/// `reactive_cache::Memo`, which:
82/// 1. Computes the value the first time the function is called.
83/// 2. Caches the result for future calls.
84/// 3. Automatically tracks reactive dependencies if used inside `Signal` or other reactive contexts.
85///
86/// # Requirements
87///
88/// - The function must have **no parameters**.
89/// - The function must return a value (`-> T`), which must implement `Clone`.
90///
91/// # Examples
92///
93/// ```rust
94/// use reactive_cache::prelude::*;
95/// use reactive_macros::memo;
96///
97/// #[memo]
98/// pub fn get_number() -> i32 {
99///     // The first call sets INVOKED to true
100///     static mut INVOKED: bool = false;
101///     assert!(!unsafe { INVOKED });
102///     unsafe { INVOKED = true };
103///
104///     42
105/// }
106///
107/// #[memo]
108/// pub fn get_string() -> String {
109///     "Hello, World!".to_string()
110/// }
111///
112/// fn main() {
113///     // First call computes and caches the value
114///     assert_eq!(get_number(), 42);
115///     // Subsequent calls return the cached value without re-running the block
116///     assert_eq!(get_number(), 42);
117///
118///     assert_eq!(get_string(), "Hello, World!");
119/// }
120/// ```
121///
122/// # SAFETY
123///
124/// This macro uses a `static mut` internally, so it **is not thread-safe**.
125/// It is intended for single-threaded usage only. Accessing the memo from
126/// multiple threads concurrently can cause undefined behavior.
127#[proc_macro_attribute]
128pub fn memo(_attr: TokenStream, item: TokenStream) -> TokenStream {
129    let func = parse_macro_input!(item as ItemFn);
130
131    let vis = &func.vis;
132    let sig = &func.sig;
133    let block = &func.block;
134    let ident = &func.sig.ident;
135
136    let output_ty = match &sig.output {
137        ReturnType::Type(_, ty) => ty.clone(),
138        _ => {
139            return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
140                .to_compile_error()
141                .into();
142        }
143    };
144
145    if !sig.inputs.is_empty() {
146        return syn::Error::new_spanned(
147            &sig.inputs,
148            "The memo macro can only be used with `get` function without any parameters.",
149        )
150        .to_compile_error()
151        .into();
152    }
153
154    let ident = format_ident!("{}", ident.to_string().to_uppercase());
155    let ty = quote! { reactive_cache::Lazy<std::rc::Rc<reactive_cache::Memo<#output_ty>>> };
156    let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Memo::new(|| #block)) };
157
158    let expanded = quote! {
159        #vis #sig {
160            static mut #ident: #ty = #expr;
161            unsafe { #ident.get() }
162        }
163    };
164
165    expanded.into()
166}
167
168/// Evaluates a zero-argument function and optionally reports when the value changes.
169///
170/// The `#[evaluate(print_fn)]` attribute macro transforms a function into a reactive
171/// evaluator that:
172/// 1. Computes the function result on each call.
173/// 2. Compares it with the previously computed value.
174/// 3. If the value is unchanged, calls the specified print function with a message.
175///
176/// # Requirements
177///
178/// - The function must have **no parameters**.
179/// - The function must return a value (`-> T`), which must implement `Eq + Clone`.
180/// - The print function (e.g., `print`) must be a callable accepting a `String`.
181///
182/// # Examples
183///
184/// ```rust
185/// use reactive_cache::prelude::*;
186/// use reactive_macros::evaluate;
187///
188/// fn print(msg: String) {
189///     println!("{}", msg);
190/// }
191///
192/// #[evaluate(print)]
193/// pub fn get_number() -> i32 {
194///     42
195/// }
196///
197/// fn main() {
198///     // First call computes the value
199///     assert_eq!(get_number(), 42);
200///     // Second call compares with previous; prints message since value didn't change
201///     assert_eq!(get_number(), 42);
202/// }
203/// ```
204///
205/// # SAFETY
206///
207/// This macro uses a `static mut` internally to store the previous value,
208/// so it **is not thread-safe**. It should only be used in single-threaded contexts.
209#[proc_macro_attribute]
210pub fn evaluate(attr: TokenStream, item: TokenStream) -> TokenStream {
211    let print = parse_macro_input!(attr as Ident);
212    let func = parse_macro_input!(item as ItemFn);
213
214    let vis = &func.vis;
215    let sig = &func.sig;
216    let block = &func.block;
217    let ident = &func.sig.ident;
218
219    let output_ty = match &sig.output {
220        ReturnType::Type(_, ty) => ty.clone(),
221        _ => {
222            return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
223                .to_compile_error()
224                .into();
225        }
226    };
227
228    if !sig.inputs.is_empty() {
229        return syn::Error::new_spanned(
230            &sig.inputs,
231            "The memo macro can only be used with `get` function without any parameters.",
232        )
233        .to_compile_error()
234        .into();
235    }
236
237    let option_ty = quote! { Option<#output_ty> };
238    let ident = ident.to_string();
239
240    let expanded = quote! {
241        #vis #sig
242        where #output_ty: Eq + Clone
243        {
244            let new: #output_ty = (|| #block)();
245
246            static mut VALUE: #option_ty = None;
247            if let Some(old) = unsafe { VALUE } && old == new {
248                #print(format!("Evaluate: {} not changed, still {:?}\n", #ident, new));
249            }
250            unsafe { VALUE = Some(new.clone()) };
251
252            new
253        }
254    };
255
256    expanded.into()
257}