mod call_macro;
use call_macro::{CallParam, implicit_default, normalise_default};
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::visit::{self, Visit};
use syn::visit_mut::{self, VisitMut};
use syn::{
Block, Expr, FnArg, GenericParam, Ident, ItemFn, Pat, ReturnType, Type, parse_macro_input,
parse_quote,
};
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Composable(attribute: TokenStream, item: TokenStream) -> TokenStream {
let options = match parse_options(attribute.into()) {
Ok(options) => options,
Err(error) => return error.to_compile_error().into(),
};
let mut function = parse_macro_input!(item as ItemFn);
let defaults = match take_defaults(&mut function) {
Ok(defaults) => defaults,
Err(error) => return error.to_compile_error().into(),
};
let call_params = options
.restartable
.then(|| call_params(&function, defaults))
.flatten();
match expand(&mut function, &options) {
Ok(()) => {
let call_macro = call_params.map(|params| call_macro::generate(&function, ¶ms));
quote!(#function #call_macro).into()
}
Err(error) => error.to_compile_error().into(),
}
}
#[doc(hidden)]
#[proc_macro]
pub fn __compose_call_check(input: TokenStream) -> TokenStream {
match call_macro::check(input.into()) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn take_defaults(function: &mut ItemFn) -> syn::Result<Vec<Option<Expr>>> {
let mut defaults = Vec::new();
for input in &mut function.sig.inputs {
let attrs = match input {
FnArg::Receiver(receiver) => &mut receiver.attrs,
FnArg::Typed(typed) => &mut typed.attrs,
};
let mut default = None;
let mut kept = Vec::new();
for attribute in attrs.drain(..) {
if !attribute.path().is_ident("default") {
kept.push(attribute);
continue;
}
if default.is_some() {
return Err(syn::Error::new_spanned(
attribute,
"a parameter takes one #[default(..)]",
));
}
default = Some(attribute.parse_args::<Expr>().map_err(|error| {
syn::Error::new(
error.span(),
"#[default(..)] takes the default value as an expression, \
like #[default(false)]",
)
})?);
}
*attrs = kept;
if default.is_some() && matches!(input, FnArg::Receiver(_)) {
return Err(syn::Error::new_spanned(input, "self has no default"));
}
defaults.push(default);
}
Ok(defaults)
}
fn call_params(function: &ItemFn, defaults: Vec<Option<Expr>>) -> Option<Vec<CallParam>> {
let mut params = Vec::new();
for (input, default) in function.sig.inputs.iter().zip(defaults) {
let FnArg::Typed(typed) = input else {
return None;
};
let Pat::Ident(pattern) = &*typed.pat else {
return None;
};
if pattern.by_ref.is_some() || pattern.subpat.is_some() || mentions_self(&typed.ty) {
return None;
}
let ident = pattern.ident.clone();
let ty = (*typed.ty).clone();
let default = match default {
Some(expr) => Some(normalise_default(quote!(#expr))),
None => implicit_default(&ident, &ty),
};
params.push(CallParam { ident, ty, default });
}
Some(params)
}
fn mentions_self(ty: &Type) -> bool {
struct Find(bool);
impl<'ast> Visit<'ast> for Find {
fn visit_path(&mut self, path: &'ast syn::Path) {
if path
.segments
.first()
.is_some_and(|first| first.ident == "Self")
{
self.0 = true;
}
visit::visit_path(self, path);
}
}
let mut find = Find(false);
find.visit_type(ty);
find.0
}
struct Options {
restartable: bool,
}
fn parse_options(attribute: proc_macro2::TokenStream) -> syn::Result<Options> {
let mut options = Options { restartable: true };
if attribute.is_empty() {
return Ok(options);
}
let ident: Ident = syn::parse2(attribute)?;
if ident == "nonrestartable" {
options.restartable = false;
Ok(options)
} else {
Err(syn::Error::new(
ident.span(),
"#[Composable] takes no argument except `nonrestartable`",
))
}
}
struct Param {
ident: Ident,
storable: bool,
}
fn expand(function: &mut ItemFn, options: &Options) -> syn::Result<()> {
if let Some(asyncness) = function.sig.asyncness {
return Err(syn::Error::new(
asyncness.span,
"a composable cannot be async: it runs to completion inside one composition. \
Start asynchronous work with LaunchedEffect or remember_coroutine_scope",
));
}
let name = function.sig.ident.clone();
let path = name.to_string();
let mut inserter = Inserter {
path: path.clone(),
ordinal: 0,
};
inserter.visit_block_mut(&mut function.block);
let generic_types: Vec<Ident> = function
.sig
.generics
.params
.iter()
.filter_map(|param| match param {
GenericParam::Type(param) => Some(param.ident.clone()),
_ => None,
})
.collect();
let has_generics = !function.sig.generics.params.is_empty();
let mut params = Vec::new();
let mut params_ok = true;
for input in &function.sig.inputs {
match input {
FnArg::Receiver(_) => params_ok = false,
FnArg::Typed(typed) => match &*typed.pat {
Pat::Ident(pattern) if pattern.by_ref.is_none() && pattern.subpat.is_none() => {
params.push(Param {
ident: pattern.ident.clone(),
storable: storable(&typed.ty, &generic_types),
});
}
_ => params_ok = false,
},
}
}
let unit_return = match &function.sig.output {
ReturnType::Default => true,
ReturnType::Type(_, ty) => matches!(&**ty, Type::Tuple(tuple) if tuple.elems.is_empty()),
};
let skippable = unit_return && params_ok && params.iter().all(|param| param.storable);
let restartable = skippable && options.restartable && !has_generics;
let private = quote!(::compose_rust::runtime::__private);
let key = key_expression(&path, u32::MAX);
let body = &function.block;
let skip = if skippable {
let comparisons = params.iter().enumerate().map(|(index, param)| {
let ident = ¶m.ident;
quote! {
__compose_changed |= (&#private::Param(&#ident))
.__compose_changed(&__compose_scope, #index);
}
});
let restart = if restartable && params.is_empty() {
quote! {
if __compose_scope.wants_restart() {
__compose_scope.set_restart(move || {
#name();
});
}
}
} else if restartable {
let captured: Vec<Ident> = (0..params.len())
.map(|index| format_ident!("__compose_param_{}", index))
.collect();
let idents = params.iter().map(|param| ¶m.ident);
let clones = captured.iter().map(|captured| {
quote! {
(&#private::Param(&#captured))
.__compose_clone()
.expect("a parameter that was cloned once clones again")
}
});
quote! {
if __compose_scope.wants_restart() {
if let (#(::core::option::Option::Some(#captured),)*) =
(#((&#private::Param(&#idents)).__compose_clone(),)*)
{
__compose_scope.set_restart(move || {
#name(#(#clones),*);
});
}
}
}
} else {
quote! {}
};
quote! {
{
#[allow(unused_mut)]
let mut __compose_changed = __compose_scope.must_run();
#(#comparisons)*
if !__compose_changed {
__compose_scope.skip();
return;
}
#restart
}
}
} else {
quote! {}
};
function.block = parse_quote!({
#[allow(unused_imports)]
use #private::{StableParam as _, UnstableParam as _};
let __compose_scope = #private::start_restart_group(#key);
#skip
#body
});
function.attrs.push(parse_quote!(#[allow(non_snake_case)]));
Ok(())
}
fn storable(ty: &Type, generics: &[Ident]) -> bool {
struct Check<'a> {
generics: &'a [Ident],
ok: bool,
}
impl<'ast> Visit<'ast> for Check<'_> {
fn visit_type_reference(&mut self, reference: &'ast syn::TypeReference) {
match &reference.lifetime {
Some(lifetime) if lifetime.ident == "static" => {}
_ => self.ok = false,
}
visit::visit_type_reference(self, reference);
}
fn visit_lifetime(&mut self, lifetime: &'ast syn::Lifetime) {
if lifetime.ident != "static" {
self.ok = false;
}
}
fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) {
self.ok = false;
}
fn visit_type_infer(&mut self, _: &'ast syn::TypeInfer) {
self.ok = false;
}
fn visit_type_path(&mut self, path: &'ast syn::TypePath) {
if path.qself.is_some() {
self.ok = false;
}
if let Some(first) = path.path.segments.first() {
if first.ident == "Self" || self.generics.contains(&first.ident) {
self.ok = false;
}
}
visit::visit_type_path(self, path);
}
fn visit_type_ptr(&mut self, _: &'ast syn::TypePtr) {
self.ok = false;
}
}
let mut check = Check { generics, ok: true };
check.visit_type(ty);
check.ok
}
struct Inserter {
path: String,
ordinal: u32,
}
impl Inserter {
fn next(&mut self) -> (proc_macro2::TokenStream, Ident) {
let key = key_expression(&self.path, self.ordinal);
self.ordinal += 1;
(key, guard_name(self.ordinal))
}
fn wrap(&mut self, block: &mut Block) {
let (key, guard) = self.next();
let statements = &block.stmts;
*block = parse_quote!({
let #guard = ::compose_rust::runtime::__private::group(#key);
#(#statements)*
});
}
}
impl VisitMut for Inserter {
fn visit_expr_mut(&mut self, expr: &mut Expr) {
visit_mut::visit_expr_mut(self, expr);
match expr {
Expr::If(conditional) => {
self.wrap(&mut conditional.then_branch);
if let Some((_, otherwise)) = conditional.else_branch.as_mut() {
if let Expr::Block(block) = otherwise.as_mut() {
self.wrap(&mut block.block);
}
}
}
Expr::Match(matched) => {
for arm in &mut matched.arms {
let (key, guard) = self.next();
let body = arm.body.clone();
arm.body = match *body {
Expr::Block(syn::ExprBlock {
ref attrs,
label: None,
ref block,
}) if attrs.is_empty() => {
let statements = &block.stmts;
parse_quote!({
let #guard = ::compose_rust::runtime::__private::group(#key);
#(#statements)*
})
}
body => parse_quote!({
let #guard = ::compose_rust::runtime::__private::group(#key);
#body
}),
};
}
}
_ => {}
}
}
fn visit_item_mut(&mut self, _item: &mut syn::Item) {}
}
fn guard_name(ordinal: u32) -> Ident {
Ident::new(&format!("__compose_group_{ordinal}"), Span::call_site())
}
fn key_expression(path: &str, ordinal: u32) -> proc_macro2::TokenStream {
quote!({
const __COMPOSE_KEY: u64 = ::compose_rust::runtime::__private::call_site(
concat!(module_path!(), "::", #path),
#ordinal,
);
__COMPOSE_KEY
})
}