1mod 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#[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 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, ¶ms));
91 quote!(#function #call_macro).into()
92 }
93 Err(error) => error.to_compile_error().into(),
94 }
95}
96
97#[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
108fn 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
147fn 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
212struct 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 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 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 = ¶m.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| ¶m.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
348fn 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 fn wrap(&mut self, block: &mut Block) {
411 let (key, guard) = self.next();
412 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 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 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 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 fn visit_item_mut(&mut self, _item: &mut syn::Item) {}
471}
472
473fn guard_name(ordinal: u32) -> Ident {
475 Ident::new(&format!("__compose_group_{ordinal}"), Span::call_site())
476}
477
478fn 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}