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 `ref_signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
9/// and automatically generates:
10/// 1. A `_get()` function that returns a reference to the value, allowing read access.
11/// - This reference behaves like a normal immutable reference for most purposes.
12/// 2. A `_set(value)` function to write the value (returns `true` if changed).
13///
14/// Unlike `signal!`, `ref_signal!` does **not** generate a same-named function that directly returns the value.
15///
16/// # Requirements
17///
18/// - The macro currently supports only `static mut` variables.
19/// - The variable type must implement `Eq`.
20///
21/// # Examples
22///
23/// ```rust
24/// use reactive_macros::ref_signal;
25///
26/// ref_signal!(static mut A: String = "hello".to_string(););
27///
28/// assert_eq!(&*A_get(), "hello");
29/// assert!(A_set("signal".to_string()));
30/// assert_eq!(&*A_get(), "signal");
31/// assert!(!A_set("signal".to_string())); // 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 ref_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
82 let lazy_ty = quote! { reactive_cache::Lazy<std::rc::Rc<reactive_cache::Signal<#ty>>> };
83 let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Signal::new(#expr)) };
84
85 let expanded = quote! {
86 #(#attrs)*
87 #vis #static_token #mutability #ident_p #colon_token #lazy_ty #eq_token #expr #semi_token
88
89 #[allow(non_snake_case)]
90 pub fn #ident_get() -> std::cell::Ref<'static, #ty> {
91 unsafe { #ident_p.get() }
92 }
93
94 #[allow(non_snake_case)]
95 pub fn #ident_set(value: #ty) -> bool {
96 unsafe { #ident_p.set(value) }
97 }
98 };
99
100 expanded.into()
101}
102
103/// Wraps a `static mut` variable as a reactive signal (similar to a property)
104/// with getter and setter functions.
105///
106/// The `signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
107/// and automatically generates:
108/// 1. A `_get()` function that returns a reference to the value, allowing read access.
109/// - This reference behaves like a normal immutable reference for most purposes.
110/// 2. A `_set(value)` function to write the value (returns `true` if changed).
111/// 3. A function with the same name as the variable that directly returns the value
112/// by dereferencing the underlying variable. This requires the type to implement `Copy`.
113///
114/// # Requirements
115///
116/// - The macro currently supports only `static mut` variables.
117/// - The variable type must implement `Eq`.
118///
119/// # Examples
120///
121/// ```rust
122/// use reactive_macros::{ref_signal, signal};
123///
124/// signal!(static mut A: i32 = 10;);
125///
126/// assert_eq!(A(), 10); // returns value directly (requires Copy)
127/// assert_eq!(*A_get(), 10); // returns a reference to the value
128/// assert!(A_set(20));
129/// assert_eq!(A(), 20);
130/// assert!(!A_set(20)); // No change
131/// ```
132///
133/// # SAFETY
134///
135/// This macro wraps `static mut` variables internally, so it **is not thread-safe**.
136/// It should be used only in single-threaded contexts.
137///
138/// # Warning
139///
140/// **Do not set any signal that is part of the same effect chain.**
141///
142/// Effects automatically run whenever one of their dependent signals changes.
143/// If an effect modifies a signal that it (directly or indirectly) observes,
144/// it creates a circular dependency. This can lead to:
145/// - an infinite loop of updates, or
146/// - conflicting updates that the system cannot resolve.
147///
148/// In the general case, it is impossible to automatically determine whether
149/// such an effect will ever terminate—this is essentially a version of the
150/// halting problem. Therefore, you must ensure manually that effects do not
151/// update signals within their own dependency chain.
152#[proc_macro]
153pub fn signal(input: TokenStream) -> TokenStream {
154 let input_clone: proc_macro2::TokenStream = input.clone().into();
155
156 let item = parse_macro_input!(input as ItemStatic);
157 let ident = &item.ident;
158 let ty = &item.ty;
159
160 let ident_p = format_ident!("_{}", ident.to_string().to_uppercase());
161 let ident_fn = format_ident!("{}", ident);
162
163 let expanded = quote! {
164 ref_signal!(#input_clone);
165
166 #[allow(non_snake_case)]
167 pub fn #ident_fn() -> #ty {
168 *unsafe { #ident_p.get() }
169 }
170 };
171
172 expanded.into()
173}
174
175/// Turns a zero-argument function into a memoized, reactive computation.
176///
177/// The `#[memo]` attribute macro transforms a function into a static
178/// `reactive_cache::Memo`, which:
179/// 1. Computes the value the first time the function is called.
180/// 2. Caches the result for future calls.
181/// 3. Automatically tracks reactive dependencies if used inside `Signal` or other reactive contexts.
182///
183/// # Requirements
184///
185/// - The function must have **no parameters**.
186/// - The function must return a value (`-> T`), which must implement `Clone`.
187///
188/// # Examples
189///
190/// ```rust
191/// use reactive_macros::memo;
192///
193/// #[memo]
194/// pub fn get_number() -> i32 {
195/// // The first call sets INVOKED to true
196/// static mut INVOKED: bool = false;
197/// assert!(!unsafe { INVOKED });
198/// unsafe { INVOKED = true };
199///
200/// 42
201/// }
202///
203/// fn main() {
204/// // First call computes and caches the value
205/// assert_eq!(get_number(), 42);
206/// // Subsequent calls return the cached value without re-running the block
207/// assert_eq!(get_number(), 42);
208/// }
209/// ```
210///
211/// # SAFETY
212///
213/// This macro uses a `static mut` internally, so it **is not thread-safe**.
214/// It is intended for single-threaded usage only. Accessing the memo from
215/// multiple threads concurrently can cause undefined behavior.
216#[proc_macro_attribute]
217pub fn memo(_attr: TokenStream, item: TokenStream) -> TokenStream {
218 let func = parse_macro_input!(item as ItemFn);
219
220 let vis = &func.vis;
221 let sig = &func.sig;
222 let block = &func.block;
223 let ident = &func.sig.ident;
224
225 let output_ty = match &sig.output {
226 ReturnType::Type(_, ty) => ty.clone(),
227 _ => {
228 return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
229 .to_compile_error()
230 .into();
231 }
232 };
233
234 if !sig.inputs.is_empty() {
235 return syn::Error::new_spanned(
236 &sig.inputs,
237 "The memo macro can only be used with `get` function without any parameters.",
238 )
239 .to_compile_error()
240 .into();
241 }
242
243 let ident = format_ident!("{}", ident.to_string().to_uppercase());
244 let ty = quote! { reactive_cache::Lazy<std::rc::Rc<reactive_cache::Memo<#output_ty>>> };
245 let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Memo::new(|| #block)) };
246
247 let expanded = quote! {
248 static mut #ident: #ty = #expr;
249
250 #vis #sig
251 where #output_ty: Clone
252 {
253 unsafe { (*#ident).get() }
254 }
255 };
256
257 expanded.into()
258}
259
260/// Evaluates a zero-argument function and optionally reports when the value changes.
261///
262/// The `#[evaluate(print_fn)]` attribute macro transforms a function into a reactive
263/// evaluator that:
264/// 1. Computes the function result on each call.
265/// 2. Compares it with the previously computed value.
266/// 3. If the value is unchanged, calls the specified print function with a message.
267///
268/// # Requirements
269///
270/// - The function must have **no parameters**.
271/// - The function must return a value (`-> T`), which must implement `Eq + Clone`.
272/// - The print function (e.g., `print`) must be a callable accepting a `String`.
273///
274/// # Examples
275///
276/// ```rust
277/// use reactive_macros::evaluate;
278///
279/// fn print(msg: String) {
280/// println!("{}", msg);
281/// }
282///
283/// #[evaluate(print)]
284/// pub fn get_number() -> i32 {
285/// 42
286/// }
287///
288/// fn main() {
289/// // First call computes the value
290/// assert_eq!(get_number(), 42);
291/// // Second call compares with previous; prints message since value didn't change
292/// assert_eq!(get_number(), 42);
293/// }
294/// ```
295///
296/// # SAFETY
297///
298/// This macro uses a `static mut` internally to store the previous value,
299/// so it **is not thread-safe**. It should only be used in single-threaded contexts.
300#[proc_macro_attribute]
301pub fn evaluate(attr: TokenStream, item: TokenStream) -> TokenStream {
302 let print = parse_macro_input!(attr as Ident);
303 let func = parse_macro_input!(item as ItemFn);
304
305 let vis = &func.vis;
306 let sig = &func.sig;
307 let block = &func.block;
308 let ident = &func.sig.ident;
309
310 let output_ty = match &sig.output {
311 ReturnType::Type(_, ty) => ty.clone(),
312 _ => {
313 return syn::Error::new_spanned(&sig.output, "Functions must have a return value")
314 .to_compile_error()
315 .into();
316 }
317 };
318
319 if !sig.inputs.is_empty() {
320 return syn::Error::new_spanned(
321 &sig.inputs,
322 "The memo macro can only be used with `get` function without any parameters.",
323 )
324 .to_compile_error()
325 .into();
326 }
327
328 let option_ty = quote! { Option<#output_ty> };
329 let ident = ident.to_string();
330
331 let expanded = quote! {
332 #vis #sig
333 where #output_ty: Eq + Clone
334 {
335 let new: #output_ty = (|| #block)();
336
337 static mut VALUE: #option_ty = None;
338 if let Some(old) = unsafe { VALUE } && old == new {
339 #print(format!("Evaluate: {} not changed, still {:?}\n", #ident, new));
340 }
341 unsafe { VALUE = Some(new.clone()) };
342
343 new
344 }
345 };
346
347 expanded.into()
348}