Skip to content

Commit d2969e0

Browse files
committed
feat(macros): Implement the ref_signal! macro and update the signal! macro.
- Added the `ref_signal!` macro to create a signal that returns a reference. - Updated the `signal!` macro to return a value directly (requires the type to implement `Copy`). - Modified relevant test files to adapt to the new signal macro.
1 parent 1135dea commit d2969e0

File tree

3 files changed

+91
-25
lines changed

3 files changed

+91
-25
lines changed

macros/src/lib.rs

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ use syn::{Ident, ItemFn, ItemStatic, ReturnType, parse_macro_input};
55
/// Wraps a `static mut` variable as a reactive signal (similar to a property)
66
/// with getter and setter functions.
77
///
8-
/// The `signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
8+
/// The `ref_signal!` macro transforms a `static mut` variable into a `reactive_cache::Signal`,
99
/// and automatically generates:
10-
/// 1. A `_get()` function to read the value.
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.
1112
/// 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+
/// Unlike `signal!`, `ref_signal!` does **not** generate a same-named function that directly returns the value.
1315
///
1416
/// # Requirements
1517
///
@@ -19,16 +21,14 @@ use syn::{Ident, ItemFn, ItemStatic, ReturnType, parse_macro_input};
1921
/// # Examples
2022
///
2123
/// ```rust
22-
/// use std::cell::Cell;
23-
/// use reactive_macros::signal;
24+
/// use reactive_macros::ref_signal;
2425
///
25-
/// signal!(static mut A: i32 = 10;);
26+
/// ref_signal!(static mut A: String = "hello".to_string(););
2627
///
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
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
3232
/// ```
3333
///
3434
/// # SAFETY
@@ -51,7 +51,7 @@ use syn::{Ident, ItemFn, ItemStatic, ReturnType, parse_macro_input};
5151
/// halting problem. Therefore, you must ensure manually that effects do not
5252
/// update signals within their own dependency chain.
5353
#[proc_macro]
54-
pub fn signal(input: TokenStream) -> TokenStream {
54+
pub fn ref_signal(input: TokenStream) -> TokenStream {
5555
let item = parse_macro_input!(input as ItemStatic);
5656

5757
let attrs = &item.attrs;
@@ -78,7 +78,6 @@ pub fn signal(input: TokenStream) -> TokenStream {
7878
let ident_p = format_ident!("_{}", ident.to_string().to_uppercase());
7979
let ident_get = format_ident!("{}_get", ident);
8080
let ident_set = format_ident!("{}_set", ident);
81-
let ident_fn = format_ident!("{}", ident);
8281

8382
let lazy_ty = quote! { reactive_cache::Lazy<reactive_cache::Signal<#ty>> };
8483
let expr = quote! { reactive_cache::Lazy::new(|| reactive_cache::Signal::new(Some(#expr))) };
@@ -96,10 +95,77 @@ pub fn signal(input: TokenStream) -> TokenStream {
9695
pub fn #ident_set(value: #ty) -> bool {
9796
unsafe { #ident_p.set(value) }
9897
}
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 + Default`.
118+
///
119+
/// # Examples
120+
///
121+
/// ```rust
122+
/// use reactive_macros::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]
153+
pub 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+
reactive_macros::ref_signal!(#input_clone);
99165

100166
#[allow(non_snake_case)]
101-
pub fn #ident_fn() -> std::cell::Ref<'static, #ty> {
102-
#ident_get()
167+
pub fn #ident_fn() -> #ty {
168+
*unsafe { #ident_p.get() }
103169
}
104170
};
105171

macros/tests/dep.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use std::cell::Cell;
44

5-
use reactive_macros::{memo, signal};
5+
use reactive_macros::{memo, ref_signal, signal};
66

77
thread_local! {
88
static SOURCE_A_CALLED: Cell<bool> = const { Cell::new(false) };
@@ -16,20 +16,20 @@ signal!(
1616
static mut A: i32 = 10;
1717
);
1818

19-
signal!(
19+
ref_signal!(
2020
static mut B: String = 5.to_string();
2121
);
2222

2323
pub fn source_a() -> i32 {
2424
SOURCE_A_CALLED.set(true);
2525

26-
*A_get()
26+
A()
2727
}
2828

2929
pub fn source_b() -> i32 {
3030
SOURCE_B_CALLED.set(true);
3131

32-
B().parse::<i32>().unwrap()
32+
B_get().parse::<i32>().unwrap()
3333
}
3434

3535
#[memo]

macros/tests/effect.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::{cell::Cell, rc::Rc};
44

55
use reactive_cache::effect;
6-
use reactive_macros::{memo, signal};
6+
use reactive_macros::{memo, ref_signal, signal};
77

88
static mut SOURCE_A_CALLED: i32 = 0;
99
static mut SOURCE_B_CALLED: i32 = 0;
@@ -16,20 +16,20 @@ signal!(
1616
static mut A: i32 = 10;
1717
);
1818

19-
signal!(
19+
ref_signal!(
2020
static mut B: String = 5.to_string();
2121
);
2222

2323
pub fn source_a() -> i32 {
2424
unsafe { SOURCE_A_CALLED += 1 };
2525

26-
*A_get()
26+
A()
2727
}
2828

2929
pub fn source_b() -> i32 {
3030
unsafe { SOURCE_B_CALLED += 1 };
3131

32-
B().parse::<i32>().unwrap()
32+
B_get().parse::<i32>().unwrap()
3333
}
3434

3535
#[memo]
@@ -140,10 +140,10 @@ fn switch_effect_test() {
140140
let b_rst_clone = b_rst.clone();
141141
let _ = Rc::into_raw(effect!(
142142
move || {
143-
match *SWITCH_A() {
143+
match SWITCH_A() {
144144
true => {}
145145
false => {
146-
b_rst_clone.set(*SWITCH_B());
146+
b_rst_clone.set(*SWITCH_B_get());
147147
}
148148
}
149149
},

0 commit comments

Comments
 (0)