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
42mod call_macro;
43
44use call_macro::{CallParam, implicit_default, normalise_default};
45use proc_macro::TokenStream;
46use proc_macro2::Span;
47use quote::{format_ident, quote};
48use syn::visit::{self, Visit};
49use syn::visit_mut::{self, VisitMut};
50use syn::{
51    Block, Expr, FnArg, GenericParam, Ident, ItemFn, Pat, ReturnType, Type, parse_macro_input,
52    parse_quote,
53};
54
55/// Turns a function into a composable.
56///
57/// ```ignore
58/// #[Composable]
59/// fn Greeting(name: String) {
60///     let clicks = remember(|| mutable_state_of(0));
61///     Text(format!("Hello, {name}: {}", clicks.get()));
62///     Button("Again").on_click(move || clicks.update(|count| *count += 1));
63/// }
64/// ```
65///
66/// `#[Composable(nonrestartable)]` keeps the groups and the skipping but never re-runs the
67/// function on its own: a state it reads re-runs its caller instead. That is what an
68/// associated function needs, because the runtime re-runs a function by calling it by its
69/// bare name.
70#[proc_macro_attribute]
71#[allow(non_snake_case)]
72pub fn Composable(attribute: TokenStream, item: TokenStream) -> TokenStream {
73    let options = match parse_options(attribute.into()) {
74        Ok(options) => options,
75        Err(error) => return error.to_compile_error().into(),
76    };
77    let mut function = parse_macro_input!(item as ItemFn);
78    let defaults = match take_defaults(&mut function) {
79        Ok(defaults) => defaults,
80        Err(error) => return error.to_compile_error().into(),
81    };
82    // A function that re-runs itself by bare name is an associated function, and a method
83    // has no name of its own to share with a macro, so only the restartable form gets one.
84    let call_params = options
85        .restartable
86        .then(|| call_params(&function, defaults))
87        .flatten();
88    match expand(&mut function, &options) {
89        Ok(()) => {
90            let call_macro = call_params.map(|params| call_macro::generate(&function, &params));
91            quote!(#function #call_macro).into()
92        }
93        Err(error) => error.to_compile_error().into(),
94    }
95}
96
97/// The expansion of every generated call macro. Hidden: it is the generated
98/// `macro_rules!`'s way of reading its arguments with the caller's spans, not an API.
99#[doc(hidden)]
100#[proc_macro]
101pub fn __compose_call_check(input: TokenStream) -> TokenStream {
102    match call_macro::check(input.into()) {
103        Ok(tokens) => tokens.into(),
104        Err(error) => error.to_compile_error().into(),
105    }
106}
107
108/// Removes the `#[default(expr)]` attributes from the parameters and returns the
109/// expressions, one entry per parameter.
110fn take_defaults(function: &mut ItemFn) -> syn::Result<Vec<Option<Expr>>> {
111    let mut defaults = Vec::new();
112    for input in &mut function.sig.inputs {
113        let attrs = match input {
114            FnArg::Receiver(receiver) => &mut receiver.attrs,
115            FnArg::Typed(typed) => &mut typed.attrs,
116        };
117        let mut default = None;
118        let mut kept = Vec::new();
119        for attribute in attrs.drain(..) {
120            if !attribute.path().is_ident("default") {
121                kept.push(attribute);
122                continue;
123            }
124            if default.is_some() {
125                return Err(syn::Error::new_spanned(
126                    attribute,
127                    "a parameter takes one #[default(..)]",
128                ));
129            }
130            default = Some(attribute.parse_args::<Expr>().map_err(|error| {
131                syn::Error::new(
132                    error.span(),
133                    "#[default(..)] takes the default value as an expression, \
134                     like #[default(false)]",
135                )
136            })?);
137        }
138        *attrs = kept;
139        if default.is_some() && matches!(input, FnArg::Receiver(_)) {
140            return Err(syn::Error::new_spanned(input, "self has no default"));
141        }
142        defaults.push(default);
143    }
144    Ok(defaults)
145}
146
147/// The parameters as the call macro needs them, or `None` for a function the macro cannot
148/// name: one with a receiver, a pattern instead of a name, or `Self` in a type.
149fn call_params(function: &ItemFn, defaults: Vec<Option<Expr>>) -> Option<Vec<CallParam>> {
150    let mut params = Vec::new();
151    for (input, default) in function.sig.inputs.iter().zip(defaults) {
152        let FnArg::Typed(typed) = input else {
153            return None;
154        };
155        let Pat::Ident(pattern) = &*typed.pat else {
156            return None;
157        };
158        if pattern.by_ref.is_some() || pattern.subpat.is_some() || mentions_self(&typed.ty) {
159            return None;
160        }
161        let ident = pattern.ident.clone();
162        let ty = (*typed.ty).clone();
163        let default = match default {
164            Some(expr) => Some(normalise_default(quote!(#expr))),
165            None => implicit_default(&ident, &ty),
166        };
167        params.push(CallParam { ident, ty, default });
168    }
169    Some(params)
170}
171
172fn mentions_self(ty: &Type) -> bool {
173    struct Find(bool);
174    impl<'ast> Visit<'ast> for Find {
175        fn visit_path(&mut self, path: &'ast syn::Path) {
176            if path
177                .segments
178                .first()
179                .is_some_and(|first| first.ident == "Self")
180            {
181                self.0 = true;
182            }
183            visit::visit_path(self, path);
184        }
185    }
186    let mut find = Find(false);
187    find.visit_type(ty);
188    find.0
189}
190
191struct Options {
192    restartable: bool,
193}
194
195fn parse_options(attribute: proc_macro2::TokenStream) -> syn::Result<Options> {
196    let mut options = Options { restartable: true };
197    if attribute.is_empty() {
198        return Ok(options);
199    }
200    let ident: Ident = syn::parse2(attribute)?;
201    if ident == "nonrestartable" {
202        options.restartable = false;
203        Ok(options)
204    } else {
205        Err(syn::Error::new(
206            ident.span(),
207            "#[Composable] takes no argument except `nonrestartable`",
208        ))
209    }
210}
211
212/// One parameter the runtime may compare and keep.
213struct Param {
214    ident: Ident,
215    storable: bool,
216}
217
218fn expand(function: &mut ItemFn, options: &Options) -> syn::Result<()> {
219    if let Some(asyncness) = function.sig.asyncness {
220        return Err(syn::Error::new(
221            asyncness.span,
222            "a composable cannot be async: it runs to completion inside one composition. \
223             Start asynchronous work with LaunchedEffect or remember_coroutine_scope",
224        ));
225    }
226    let name = function.sig.ident.clone();
227    let path = name.to_string();
228
229    // Branch groups first, so the function's own group wraps them.
230    let mut inserter = Inserter {
231        path: path.clone(),
232        ordinal: 0,
233    };
234    inserter.visit_block_mut(&mut function.block);
235
236    let generic_types: Vec<Ident> = function
237        .sig
238        .generics
239        .params
240        .iter()
241        .filter_map(|param| match param {
242            GenericParam::Type(param) => Some(param.ident.clone()),
243            _ => None,
244        })
245        .collect();
246    let has_generics = !function.sig.generics.params.is_empty();
247
248    let mut params = Vec::new();
249    let mut params_ok = true;
250    for input in &function.sig.inputs {
251        match input {
252            FnArg::Receiver(_) => params_ok = false,
253            FnArg::Typed(typed) => match &*typed.pat {
254                Pat::Ident(pattern) if pattern.by_ref.is_none() && pattern.subpat.is_none() => {
255                    params.push(Param {
256                        ident: pattern.ident.clone(),
257                        storable: storable(&typed.ty, &generic_types),
258                    });
259                }
260                _ => params_ok = false,
261            },
262        }
263    }
264
265    let unit_return = match &function.sig.output {
266        ReturnType::Default => true,
267        ReturnType::Type(_, ty) => matches!(&**ty, Type::Tuple(tuple) if tuple.elems.is_empty()),
268    };
269    // A parameter that cannot be kept cannot be compared next time, so a function with
270    // one runs whenever its caller does, as an unstable parameter makes a Compose function
271    // run.
272    let skippable = unit_return && params_ok && params.iter().all(|param| param.storable);
273    let restartable = skippable && options.restartable && !has_generics;
274
275    let private = quote!(::compose_rust::runtime::__private);
276    let key = key_expression(&path, u32::MAX);
277    let body = &function.block;
278
279    let skip = if skippable {
280        let comparisons = params.iter().enumerate().map(|(index, param)| {
281            let ident = &param.ident;
282            quote! {
283                __compose_changed |= (&#private::Param(&#ident))
284                    .__compose_changed(&__compose_scope, #index);
285            }
286        });
287        let restart = if restartable && params.is_empty() {
288            quote! {
289                if __compose_scope.wants_restart() {
290                    __compose_scope.set_restart(move || {
291                        #name();
292                    });
293                }
294            }
295        } else if restartable {
296            let captured: Vec<Ident> = (0..params.len())
297                .map(|index| format_ident!("__compose_param_{}", index))
298                .collect();
299            let idents = params.iter().map(|param| &param.ident);
300            let clones = captured.iter().map(|captured| {
301                quote! {
302                    (&#private::Param(&#captured))
303                        .__compose_clone()
304                        .expect("a parameter that was cloned once clones again")
305                }
306            });
307            quote! {
308                if __compose_scope.wants_restart() {
309                    if let (#(::core::option::Option::Some(#captured),)*) =
310                        (#((&#private::Param(&#idents)).__compose_clone(),)*)
311                    {
312                        __compose_scope.set_restart(move || {
313                            #name(#(#clones),*);
314                        });
315                    }
316                }
317            }
318        } else {
319            quote! {}
320        };
321        quote! {
322            {
323                #[allow(unused_mut)]
324                let mut __compose_changed = __compose_scope.must_run();
325                #(#comparisons)*
326                if !__compose_changed {
327                    __compose_scope.skip();
328                    return;
329                }
330                #restart
331            }
332        }
333    } else {
334        quote! {}
335    };
336
337    function.block = parse_quote!({
338        #[allow(unused_imports)]
339        use #private::{StableParam as _, UnstableParam as _};
340        let __compose_scope = #private::start_restart_group(#key);
341        #skip
342        #body
343    });
344    function.attrs.push(parse_quote!(#[allow(non_snake_case)]));
345    Ok(())
346}
347
348/// Whether a parameter of this type, as written, can be kept after the call returns.
349///
350/// Only the syntax is available here, so the answer is conservative: anything that could
351/// borrow, anything generic and anything opaque is treated as not storable, and the
352/// function is then run every time its caller runs rather than wrongly skipped.
353fn storable(ty: &Type, generics: &[Ident]) -> bool {
354    struct Check<'a> {
355        generics: &'a [Ident],
356        ok: bool,
357    }
358    impl<'ast> Visit<'ast> for Check<'_> {
359        fn visit_type_reference(&mut self, reference: &'ast syn::TypeReference) {
360            match &reference.lifetime {
361                Some(lifetime) if lifetime.ident == "static" => {}
362                _ => self.ok = false,
363            }
364            visit::visit_type_reference(self, reference);
365        }
366        fn visit_lifetime(&mut self, lifetime: &'ast syn::Lifetime) {
367            if lifetime.ident != "static" {
368                self.ok = false;
369            }
370        }
371        fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) {
372            self.ok = false;
373        }
374        fn visit_type_infer(&mut self, _: &'ast syn::TypeInfer) {
375            self.ok = false;
376        }
377        fn visit_type_path(&mut self, path: &'ast syn::TypePath) {
378            if path.qself.is_some() {
379                self.ok = false;
380            }
381            if let Some(first) = path.path.segments.first() {
382                if first.ident == "Self" || self.generics.contains(&first.ident) {
383                    self.ok = false;
384                }
385            }
386            visit::visit_type_path(self, path);
387        }
388        fn visit_type_ptr(&mut self, _: &'ast syn::TypePtr) {
389            self.ok = false;
390        }
391    }
392    let mut check = Check { generics, ok: true };
393    check.visit_type(ty);
394    check.ok
395}
396
397struct Inserter {
398    path: String,
399    ordinal: u32,
400}
401
402impl Inserter {
403    fn next(&mut self) -> (proc_macro2::TokenStream, Ident) {
404        let key = key_expression(&self.path, self.ordinal);
405        self.ordinal += 1;
406        (key, guard_name(self.ordinal))
407    }
408
409    /// Wraps a block so that what it remembers belongs to this branch and no other.
410    fn wrap(&mut self, block: &mut Block) {
411        let (key, guard) = self.next();
412        // The branch's own statements, spliced in after the guard rather than nested as a
413        // block of their own: a nested block around a tail value is braces the compiler
414        // warns about, and the warning would point at the user's code.
415        let statements = &block.stmts;
416        *block = parse_quote!({
417            let #guard = ::compose_rust::runtime::__private::group(#key);
418            #(#statements)*
419        });
420    }
421}
422
423impl VisitMut for Inserter {
424    fn visit_expr_mut(&mut self, expr: &mut Expr) {
425        // Descend first, so an inner branch is numbered and wrapped before the outer one
426        // closes over it.
427        visit_mut::visit_expr_mut(self, expr);
428
429        match expr {
430            Expr::If(conditional) => {
431                self.wrap(&mut conditional.then_branch);
432                if let Some((_, otherwise)) = conditional.else_branch.as_mut() {
433                    // An `else if` is another `Expr::If`, already wrapped by the descent
434                    // above. Only a plain `else` block needs one here.
435                    if let Expr::Block(block) = otherwise.as_mut() {
436                        self.wrap(&mut block.block);
437                    }
438                }
439            }
440            Expr::Match(matched) => {
441                for arm in &mut matched.arms {
442                    let (key, guard) = self.next();
443                    let body = arm.body.clone();
444                    arm.body = match *body {
445                        // A block arm's statements go after the guard, for the reason
446                        // `wrap` gives.
447                        Expr::Block(syn::ExprBlock {
448                            ref attrs,
449                            label: None,
450                            ref block,
451                        }) if attrs.is_empty() => {
452                            let statements = &block.stmts;
453                            parse_quote!({
454                                let #guard = ::compose_rust::runtime::__private::group(#key);
455                                #(#statements)*
456                            })
457                        }
458                        body => parse_quote!({
459                            let #guard = ::compose_rust::runtime::__private::group(#key);
460                            #body
461                        }),
462                    };
463                }
464            }
465            _ => {}
466        }
467    }
468
469    /// Bodies of nested items are somebody else's composition, or not a composition at all.
470    fn visit_item_mut(&mut self, _item: &mut syn::Item) {}
471}
472
473/// A distinct name per group, so nesting does not shadow an outer guard and drop it early.
474fn guard_name(ordinal: u32) -> Ident {
475    Ident::new(&format!("__compose_group_{ordinal}"), Span::call_site())
476}
477
478/// A call site's key, computed at compile time from the function's path and the macro's
479/// own counter within its body.
480fn key_expression(path: &str, ordinal: u32) -> proc_macro2::TokenStream {
481    quote!({
482        const __COMPOSE_KEY: u64 = ::compose_rust::runtime::__private::call_site(
483            concat!(module_path!(), "::", #path),
484            #ordinal,
485        );
486        __COMPOSE_KEY
487    })
488}