compose_rust_macros/
lib.rs1use 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#[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
101struct 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 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 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 = ¶m.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| ¶m.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
237fn 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 fn wrap(&mut self, block: &mut Block) {
300 let (key, guard) = self.next();
301 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 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 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 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 fn visit_item_mut(&mut self, _item: &mut syn::Item) {}
360}
361
362fn guard_name(ordinal: u32) -> Ident {
364 Ident::new(&format!("__compose_group_{ordinal}"), Span::call_site())
365}
366
367fn 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}