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}