Skip to main content

compose_rust_macros/
lib.rs

1//! `#[composable]`: what the Compose compiler plugin does to a function, done from outside
2//! the compiler.
3//!
4//! The plugin does three things to a `@Composable` function, and this does the same three
5//! to a Rust one:
6//!
7//! 1. **It gives the call a group**, keyed by where the function is declared. A slot table
8//!    stores what a call site remembered at a position, and the group is what turns a
9//!    position into an identity: two calls of the same function in one parent are told
10//!    apart by order, and everything the function remembers belongs to its group alone.
11//! 2. **It gives every branch a group of its own**: each arm of an `if`, an `else` and a
12//!    `match`. Without one, the state a branch remembered is handed to whichever call
13//!    occupies its position after the branch stops being taken. Both values are often the
14//!    same type, so nothing complains, and the screen silently shows one widget's state in
15//!    another.
16//!
17//!    Loop bodies get no group, as in Compose. Each pass of a loop calls the same call
18//!    sites again, and the runtime tells the passes apart by order, which is what a list
19//!    that only grows and shrinks at the end needs. A list whose items move needs to say
20//!    which item is which, and that is `key(id, || ..)`: a group per iteration here would
21//!    sit between the loop and the key and pin every item to its position.
22//! 3. **It makes the function skippable and restartable.** The parameters are compared
23//!    with the ones the group was last run with, and when they are all equal and nothing
24//!    the function read has changed, its body is not run at all. When a state it read
25//!    changes, the runtime runs this function again on its own, with the parameters it
26//!    last had, without running whatever called it.
27//!
28//! A group closes when a guard bound at the top of its block is dropped, not when a
29//! closure wrapping the block returns. A local with a destructor is dropped after the
30//! block's tail expression has been evaluated, so the group closes in the right place on
31//! an ordinary exit, and closes anyway on `continue`, `break`, `return`, `?` and an
32//! unwind. Wrapping the body in a closure would make the first two compile errors and give
33//! `return` a different meaning.
34//!
35//! What can be compared and stored is decided from the parameter's type as written. A
36//! parameter whose type borrows (other than `&'static`), names a generic parameter, or is
37//! `impl Trait` cannot be kept past the call, so a function with one is never skipped and
38//! is re-run by its caller rather than on its own. A parameter that can be kept but is not
39//! `Clone + PartialEq` is found out at compile time by method resolution and treated the
40//! same way. Nothing here needs the function's author to say which is which.
41
42use proc_macro::TokenStream;
43use proc_macro2::Span;
44use quote::{format_ident, quote};
45use syn::visit::{self, Visit};
46use syn::visit_mut::{self, VisitMut};
47use syn::{
48    Block, Expr, FnArg, GenericParam, Ident, ItemFn, Pat, ReturnType, Type, parse_macro_input,
49    parse_quote,
50};
51
52/// Turns a function into a composable.
53///
54/// ```ignore
55/// #[composable]
56/// fn Greeting(name: String) {
57///     let clicks = remember(|| mutable_state_of(0));
58///     Text(format!("Hello, {name}: {}", clicks.get()));
59///     Button("Again").on_click(move || clicks.update(|count| *count += 1));
60/// }
61/// ```
62///
63/// `#[composable(nonrestartable)]` keeps the groups and the skipping but never re-runs the
64/// function on its own: a state it reads re-runs its caller instead. That is what an
65/// associated function needs, because the runtime re-runs a function by calling it by its
66/// bare name.
67#[proc_macro_attribute]
68pub fn composable(attribute: TokenStream, item: TokenStream) -> TokenStream {
69    let options = match parse_options(attribute.into()) {
70        Ok(options) => options,
71        Err(error) => return error.to_compile_error().into(),
72    };
73    let mut function = parse_macro_input!(item as ItemFn);
74    match expand(&mut function, &options) {
75        Ok(()) => quote!(#function).into(),
76        Err(error) => error.to_compile_error().into(),
77    }
78}
79
80struct Options {
81    restartable: bool,
82}
83
84fn parse_options(attribute: proc_macro2::TokenStream) -> syn::Result<Options> {
85    let mut options = Options { restartable: true };
86    if attribute.is_empty() {
87        return Ok(options);
88    }
89    let ident: Ident = syn::parse2(attribute)?;
90    if ident == "nonrestartable" {
91        options.restartable = false;
92        Ok(options)
93    } else {
94        Err(syn::Error::new(
95            ident.span(),
96            "#[composable] takes no argument except `nonrestartable`",
97        ))
98    }
99}
100
101/// One parameter the runtime may compare and keep.
102struct Param {
103    ident: Ident,
104    storable: bool,
105}
106
107fn expand(function: &mut ItemFn, options: &Options) -> syn::Result<()> {
108    if let Some(asyncness) = function.sig.asyncness {
109        return Err(syn::Error::new(
110            asyncness.span,
111            "a composable cannot be async: it runs to completion inside one composition. \
112             Start asynchronous work with LaunchedEffect or remember_coroutine_scope",
113        ));
114    }
115    let name = function.sig.ident.clone();
116    let path = name.to_string();
117
118    // Branch groups first, so the function's own group wraps them.
119    let mut inserter = Inserter {
120        path: path.clone(),
121        ordinal: 0,
122    };
123    inserter.visit_block_mut(&mut function.block);
124
125    let generic_types: Vec<Ident> = function
126        .sig
127        .generics
128        .params
129        .iter()
130        .filter_map(|param| match param {
131            GenericParam::Type(param) => Some(param.ident.clone()),
132            _ => None,
133        })
134        .collect();
135    let has_generics = !function.sig.generics.params.is_empty();
136
137    let mut params = Vec::new();
138    let mut params_ok = true;
139    for input in &function.sig.inputs {
140        match input {
141            FnArg::Receiver(_) => params_ok = false,
142            FnArg::Typed(typed) => match &*typed.pat {
143                Pat::Ident(pattern) if pattern.by_ref.is_none() && pattern.subpat.is_none() => {
144                    params.push(Param {
145                        ident: pattern.ident.clone(),
146                        storable: storable(&typed.ty, &generic_types),
147                    });
148                }
149                _ => params_ok = false,
150            },
151        }
152    }
153
154    let unit_return = match &function.sig.output {
155        ReturnType::Default => true,
156        ReturnType::Type(_, ty) => matches!(&**ty, Type::Tuple(tuple) if tuple.elems.is_empty()),
157    };
158    // A parameter that cannot be kept cannot be compared next time, so a function with
159    // one runs whenever its caller does, as an unstable parameter makes a Compose function
160    // run.
161    let skippable = unit_return && params_ok && params.iter().all(|param| param.storable);
162    let restartable = skippable && options.restartable && !has_generics;
163
164    let private = quote!(::compose_rust::runtime::__private);
165    let key = key_expression(&path, u32::MAX);
166    let body = &function.block;
167
168    let skip = if skippable {
169        let comparisons = params.iter().enumerate().map(|(index, param)| {
170            let ident = &param.ident;
171            quote! {
172                __compose_changed |= (&#private::Param(&#ident))
173                    .__compose_changed(&__compose_scope, #index);
174            }
175        });
176        let restart = if restartable && params.is_empty() {
177            quote! {
178                if __compose_scope.wants_restart() {
179                    __compose_scope.set_restart(move || {
180                        #name();
181                    });
182                }
183            }
184        } else if restartable {
185            let captured: Vec<Ident> = (0..params.len())
186                .map(|index| format_ident!("__compose_param_{}", index))
187                .collect();
188            let idents = params.iter().map(|param| &param.ident);
189            let clones = captured.iter().map(|captured| {
190                quote! {
191                    (&#private::Param(&#captured))
192                        .__compose_clone()
193                        .expect("a parameter that was cloned once clones again")
194                }
195            });
196            quote! {
197                if __compose_scope.wants_restart() {
198                    if let (#(::core::option::Option::Some(#captured),)*) =
199                        (#((&#private::Param(&#idents)).__compose_clone(),)*)
200                    {
201                        __compose_scope.set_restart(move || {
202                            #name(#(#clones),*);
203                        });
204                    }
205                }
206            }
207        } else {
208            quote! {}
209        };
210        quote! {
211            {
212                #[allow(unused_mut)]
213                let mut __compose_changed = __compose_scope.must_run();
214                #(#comparisons)*
215                if !__compose_changed {
216                    __compose_scope.skip();
217                    return;
218                }
219                #restart
220            }
221        }
222    } else {
223        quote! {}
224    };
225
226    function.block = parse_quote!({
227        #[allow(unused_imports)]
228        use #private::{StableParam as _, UnstableParam as _};
229        let __compose_scope = #private::start_restart_group(#key);
230        #skip
231        #body
232    });
233    function.attrs.push(parse_quote!(#[allow(non_snake_case)]));
234    Ok(())
235}
236
237/// Whether a parameter of this type, as written, can be kept after the call returns.
238///
239/// Only the syntax is available here, so the answer is conservative: anything that could
240/// borrow, anything generic and anything opaque is treated as not storable, and the
241/// function is then run every time its caller runs rather than wrongly skipped.
242fn storable(ty: &Type, generics: &[Ident]) -> bool {
243    struct Check<'a> {
244        generics: &'a [Ident],
245        ok: bool,
246    }
247    impl<'ast> Visit<'ast> for Check<'_> {
248        fn visit_type_reference(&mut self, reference: &'ast syn::TypeReference) {
249            match &reference.lifetime {
250                Some(lifetime) if lifetime.ident == "static" => {}
251                _ => self.ok = false,
252            }
253            visit::visit_type_reference(self, reference);
254        }
255        fn visit_lifetime(&mut self, lifetime: &'ast syn::Lifetime) {
256            if lifetime.ident != "static" {
257                self.ok = false;
258            }
259        }
260        fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) {
261            self.ok = false;
262        }
263        fn visit_type_infer(&mut self, _: &'ast syn::TypeInfer) {
264            self.ok = false;
265        }
266        fn visit_type_path(&mut self, path: &'ast syn::TypePath) {
267            if path.qself.is_some() {
268                self.ok = false;
269            }
270            if let Some(first) = path.path.segments.first() {
271                if first.ident == "Self" || self.generics.contains(&first.ident) {
272                    self.ok = false;
273                }
274            }
275            visit::visit_type_path(self, path);
276        }
277        fn visit_type_ptr(&mut self, _: &'ast syn::TypePtr) {
278            self.ok = false;
279        }
280    }
281    let mut check = Check { generics, ok: true };
282    check.visit_type(ty);
283    check.ok
284}
285
286struct Inserter {
287    path: String,
288    ordinal: u32,
289}
290
291impl Inserter {
292    fn next(&mut self) -> (proc_macro2::TokenStream, Ident) {
293        let key = key_expression(&self.path, self.ordinal);
294        self.ordinal += 1;
295        (key, guard_name(self.ordinal))
296    }
297
298    /// Wraps a block so that what it remembers belongs to this branch and no other.
299    fn wrap(&mut self, block: &mut Block) {
300        let (key, guard) = self.next();
301        // The branch's own statements, spliced in after the guard rather than nested as a
302        // block of their own: a nested block around a tail value is braces the compiler
303        // warns about, and the warning would point at the user's code.
304        let statements = &block.stmts;
305        *block = parse_quote!({
306            let #guard = ::compose_rust::runtime::__private::group(#key);
307            #(#statements)*
308        });
309    }
310}
311
312impl VisitMut for Inserter {
313    fn visit_expr_mut(&mut self, expr: &mut Expr) {
314        // Descend first, so an inner branch is numbered and wrapped before the outer one
315        // closes over it.
316        visit_mut::visit_expr_mut(self, expr);
317
318        match expr {
319            Expr::If(conditional) => {
320                self.wrap(&mut conditional.then_branch);
321                if let Some((_, otherwise)) = conditional.else_branch.as_mut() {
322                    // An `else if` is another `Expr::If`, already wrapped by the descent
323                    // above. Only a plain `else` block needs one here.
324                    if let Expr::Block(block) = otherwise.as_mut() {
325                        self.wrap(&mut block.block);
326                    }
327                }
328            }
329            Expr::Match(matched) => {
330                for arm in &mut matched.arms {
331                    let (key, guard) = self.next();
332                    let body = arm.body.clone();
333                    arm.body = match *body {
334                        // A block arm's statements go after the guard, for the reason
335                        // `wrap` gives.
336                        Expr::Block(syn::ExprBlock {
337                            ref attrs,
338                            label: None,
339                            ref block,
340                        }) if attrs.is_empty() => {
341                            let statements = &block.stmts;
342                            parse_quote!({
343                                let #guard = ::compose_rust::runtime::__private::group(#key);
344                                #(#statements)*
345                            })
346                        }
347                        body => parse_quote!({
348                            let #guard = ::compose_rust::runtime::__private::group(#key);
349                            #body
350                        }),
351                    };
352                }
353            }
354            _ => {}
355        }
356    }
357
358    /// Bodies of nested items are somebody else's composition, or not a composition at all.
359    fn visit_item_mut(&mut self, _item: &mut syn::Item) {}
360}
361
362/// A distinct name per group, so nesting does not shadow an outer guard and drop it early.
363fn guard_name(ordinal: u32) -> Ident {
364    Ident::new(&format!("__compose_group_{ordinal}"), Span::call_site())
365}
366
367/// A call site's key, computed at compile time from the function's path and the macro's
368/// own counter within its body.
369fn key_expression(path: &str, ordinal: u32) -> proc_macro2::TokenStream {
370    quote!({
371        const __COMPOSE_KEY: u64 = ::compose_rust::runtime::__private::call_site(
372            concat!(module_path!(), "::", #path),
373            #ordinal,
374        );
375        __COMPOSE_KEY
376    })
377}