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}