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() -> #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() -> #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}