1use proc_macro::TokenStream;
2use proc_macro_crate::{FoundCrate, crate_name};
3use proc_macro2::{Span, TokenStream as TokenStream2};
4use quote::quote;
5use syn::{FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Type, parse_macro_input};
6
7mod branch_groups;
8
9fn is_fn_like_type(ty: &Type) -> bool {
10 match ty {
11 Type::ImplTrait(impl_trait) => impl_trait.bounds.iter().any(|bound| {
12 if let syn::TypeParamBound::Trait(trait_bound) = bound {
13 let path = &trait_bound.path;
14 if let Some(segment) = path.segments.last() {
15 let ident_str = segment.ident.to_string();
16 return ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce";
17 }
18 }
19 false
20 }),
21 Type::Path(type_path) => {
22 if let Some(segment) = type_path.path.segments.last()
23 && segment.ident == "Box"
24 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
25 && let Some(syn::GenericArgument::Type(Type::TraitObject(trait_obj))) =
26 args.args.first()
27 {
28 return trait_obj.bounds.iter().any(|bound| {
29 if let syn::TypeParamBound::Trait(trait_bound) = bound {
30 let path = &trait_bound.path;
31 if let Some(segment) = path.segments.last() {
32 let ident_str = segment.ident.to_string();
33 return ident_str == "FnMut"
34 || ident_str == "Fn"
35 || ident_str == "FnOnce";
36 }
37 }
38 false
39 });
40 }
41 false
42 }
43 Type::BareFn(_) => true,
44 _ => false,
45 }
46}
47
48fn is_generic_fn_like(ty: &Type, generics: &syn::Generics) -> bool {
49 let type_ident = match ty {
50 Type::Path(type_path) if type_path.path.segments.len() == 1 => {
51 &type_path.path.segments[0].ident
52 }
53 _ => return false,
54 };
55
56 for param in &generics.params {
57 if let syn::GenericParam::Type(type_param) = param
58 && type_param.ident == *type_ident
59 {
60 for bound in &type_param.bounds {
61 if let syn::TypeParamBound::Trait(trait_bound) = bound
62 && let Some(segment) = trait_bound.path.segments.last()
63 {
64 let ident_str = segment.ident.to_string();
65 if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
66 return true;
67 }
68 }
69 }
70 }
71 }
72
73 if let Some(where_clause) = &generics.where_clause {
74 for predicate in &where_clause.predicates {
75 if let syn::WherePredicate::Type(pred) = predicate
76 && let Type::Path(bounded_type) = &pred.bounded_ty
77 && bounded_type.path.segments.len() == 1
78 && bounded_type.path.segments[0].ident == *type_ident
79 {
80 for bound in &pred.bounds {
81 if let syn::TypeParamBound::Trait(trait_bound) = bound
82 && let Some(segment) = trait_bound.path.segments.last()
83 {
84 let ident_str = segment.ident.to_string();
85 if ident_str == "FnMut" || ident_str == "Fn" || ident_str == "FnOnce" {
86 return true;
87 }
88 }
89 }
90 }
91 }
92 }
93
94 false
95}
96
97fn is_fn_param(ty: &Type, generics: &syn::Generics) -> bool {
98 is_fn_like_type(ty) || is_generic_fn_like(ty, generics)
99}
100
101fn is_zero_arg_fn_impl_trait(ty: &Type) -> bool {
102 if let Type::ImplTrait(impl_trait) = ty {
103 impl_trait.bounds.iter().any(|bound| {
104 if let syn::TypeParamBound::Trait(trait_bound) = bound
105 && let Some(segment) = trait_bound.path.segments.last()
106 {
107 let ident_str = segment.ident.to_string();
108 if (ident_str == "Fn" || ident_str == "FnMut")
109 && let syn::PathArguments::Parenthesized(args) = &segment.arguments
110 {
111 return args.inputs.is_empty();
112 }
113 }
114 false
115 })
116 } else {
117 false
118 }
119}
120
121fn type_bare_generic_ident(ty: &Type) -> Option<&Ident> {
122 match ty {
123 Type::Path(type_path)
124 if type_path.qself.is_none()
125 && type_path.path.segments.len() == 1
126 && type_path.path.segments[0].arguments.is_none() =>
127 {
128 Some(&type_path.path.segments[0].ident)
129 }
130 _ => None,
131 }
132}
133
134fn stream_mentions_ident(tokens: &TokenStream2, name: &str) -> bool {
135 tokens.clone().into_iter().any(|tt| match tt {
136 proc_macro2::TokenTree::Ident(ident) => ident == name,
137 proc_macro2::TokenTree::Group(group) => stream_mentions_ident(&group.stream(), name),
138 _ => false,
139 })
140}
141
142fn filter_generics(
143 generics: &syn::Generics,
144 strip: &std::collections::HashSet<String>,
145) -> syn::Generics {
146 let mut filtered = generics.clone();
147 filtered.params = filtered
148 .params
149 .into_iter()
150 .filter(|param| match param {
151 syn::GenericParam::Type(type_param) => !strip.contains(&type_param.ident.to_string()),
152 _ => true,
153 })
154 .collect();
155 if let Some(where_clause) = &mut filtered.where_clause {
156 where_clause.predicates = where_clause
157 .predicates
158 .clone()
159 .into_iter()
160 .filter(|predicate| {
161 if let syn::WherePredicate::Type(pred) = predicate
162 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
163 {
164 return !strip.contains(&ident.to_string());
165 }
166 true
167 })
168 .collect();
169 if where_clause.predicates.is_empty() {
170 filtered.where_clause = None;
171 }
172 }
173 filtered
174}
175
176fn is_node_id_return(ty: &Type) -> bool {
177 matches!(
178 ty,
179 Type::Path(type_path)
180 if type_path
181 .path
182 .segments
183 .last()
184 .is_some_and(|segment| segment.ident == "NodeId")
185 )
186}
187
188fn core_crate_path() -> TokenStream2 {
189 let crate_name = crate_name("cranpose")
190 .ok()
191 .or_else(|| crate_name("cranpose-core").ok());
192
193 match crate_name {
194 Some(FoundCrate::Itself) => quote!(crate),
195 Some(FoundCrate::Name(name)) => {
196 let ident = Ident::new(&name, Span::call_site());
197 quote!(#ident)
198 }
199 None => quote!(cranpose_core),
200 }
201}
202
203fn definition_key_stmt(core_path: &TokenStream2, caller_key_ident: &Ident) -> TokenStream2 {
204 quote! {
205 let #caller_key_ident = #core_path::composable_identity_key({
206 struct __CranposeDefinitionMarker;
207 static __CRANPOSE_DEFINITION_KEY: ::std::sync::OnceLock<#core_path::Key> =
208 ::std::sync::OnceLock::new();
209 #core_path::cached_composable_definition_key(
210 &__CRANPOSE_DEFINITION_KEY,
211 file!(),
212 line!(),
213 column!(),
214 ::std::any::TypeId::of::<__CranposeDefinitionMarker>(),
215 )
216 });
217 }
218}
219
220#[proc_macro_attribute]
221pub fn composable(attr: TokenStream, item: TokenStream) -> TokenStream {
222 let attr_tokens = TokenStream2::from(attr);
223 let mut enable_skip = true;
224 let core_path = core_crate_path();
225 if !attr_tokens.is_empty() {
226 match syn::parse2::<Ident>(attr_tokens) {
227 Ok(ident) if ident == "no_skip" => enable_skip = false,
228 Ok(other) => {
229 return syn::Error::new_spanned(other, "unsupported composable attribute")
230 .to_compile_error()
231 .into();
232 }
233 Err(err) => {
234 return err.to_compile_error().into();
235 }
236 }
237 }
238
239 let mut func = parse_macro_input!(item as ItemFn);
240
241 struct ParamInfo {
242 ident: Ident,
243 pat: Box<Pat>,
244 ty: Type,
245 pat_is_mut: bool,
246 is_impl_trait: bool,
247 }
248
249 let mut param_info: Vec<ParamInfo> = Vec::new();
250
251 for (index, arg) in func.sig.inputs.iter_mut().enumerate() {
252 if let FnArg::Typed(PatType { pat, ty, .. }) = arg {
253 if let Some(reserved) = find_reserved_pattern_ident(pat) {
254 let name = reserved.to_string();
255 return syn::Error::new(
256 reserved.span(),
257 format!("`{name}` is reserved by #[composable]"),
258 )
259 .to_compile_error()
260 .into();
261 }
262 let pat_is_mut = matches!(
263 pat.as_ref(),
264 Pat::Ident(pat_ident) if pat_ident.mutability.is_some()
265 );
266 let is_impl_trait = matches!(**ty, Type::ImplTrait(_));
267
268 if is_impl_trait {
269 let original_pat: Box<Pat> = pat.clone();
270 if let Pat::Ident(pat_ident) = &**pat {
271 param_info.push(ParamInfo {
272 ident: pat_ident.ident.clone(),
273 pat: original_pat,
274 ty: ty.as_ref().clone(),
275 pat_is_mut,
276 is_impl_trait: true,
277 });
278 } else {
279 param_info.push(ParamInfo {
280 ident: Ident::new(&format!("__arg{}", index), Span::mixed_site()),
281 pat: original_pat,
282 ty: ty.as_ref().clone(),
283 pat_is_mut,
284 is_impl_trait: true,
285 });
286 }
287 } else {
288 let ident = Ident::new(&format!("__arg{}", index), Span::mixed_site());
289 let original_pat: Box<Pat> = pat.clone();
290 **pat = syn::parse_quote! { #ident };
291 param_info.push(ParamInfo {
292 ident,
293 pat: original_pat,
294 ty: ty.as_ref().clone(),
295 pat_is_mut,
296 is_impl_trait: false,
297 });
298 }
299 }
300 }
301
302 branch_groups::inject_branch_groups(&core_path, &mut func.block);
303 let has_rust_abi = match &func.sig.abi {
304 None => true,
305 Some(abi) => abi.name.as_ref().is_some_and(|name| name.value() == "Rust"),
306 };
307 if has_rust_abi {
308 func.attrs.push(syn::parse_quote!(#[track_caller]));
309 }
310
311 let scope_label_ident = func.sig.ident.clone();
312 let original_block = func.block.clone();
313 let helper_block = original_block.clone();
314 let recompose_block = original_block.clone();
315 let composer_ident = Ident::new("__composer", Span::mixed_site());
316 let outer_composer_ident = Ident::new("__outer_composer", Span::mixed_site());
317 let caller_key_ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
318 let current_scope_ident = Ident::new("__current_scope", Span::mixed_site());
319 let result_slot_index_ident = Ident::new("__result_slot_index", Span::mixed_site());
320 let has_previous_ident = Ident::new("__has_previous", Span::mixed_site());
321 let result_ident = Ident::new("__result", Span::mixed_site());
322 let value_ident = Ident::new("__value", Span::mixed_site());
323 let key_expr = quote! { #caller_key_ident };
324 let caller_key_stmt = definition_key_stmt(&core_path, &caller_key_ident);
325
326 let rebinds_for_no_skip: Vec<_> = param_info
327 .iter()
328 .map(|info| {
329 let ident = &info.ident;
330 let pat = &info.pat;
331 quote! { let #pat = #ident; }
332 })
333 .collect();
334
335 let return_ty: syn::Type = match &func.sig.output {
336 ReturnType::Default => syn::parse_quote! { () },
337 ReturnType::Type(_, ty) => ty.as_ref().clone(),
338 };
339 let returns_unit = match &func.sig.output {
340 ReturnType::Default => true,
341 ReturnType::Type(_, ty) => {
342 matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty())
343 }
344 };
345 let invalidate_return_consumer = if returns_unit || is_node_id_return(&return_ty) {
346 quote! {}
347 } else {
348 quote! { #composer_ident.__invalidate_return_consumer_scope(); }
349 };
350 let _helper_ident = Ident::new(
351 &format!("__cranpose_impl_{}", func.sig.ident),
352 Span::mixed_site(),
353 );
354 let generics = func.sig.generics.clone();
355 let (_impl_generics, _ty_generics, _where_clause) = generics.split_for_impl();
356
357 let _helper_inputs: Vec<TokenStream2> = param_info
358 .iter()
359 .map(|info| {
360 let ident = &info.ident;
361 let ty = &info.ty;
362 quote! { #ident: #ty }
363 })
364 .collect();
365
366 let has_unhandled_impl_trait = param_info
367 .iter()
368 .any(|info| info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty));
369
370 if enable_skip && !has_unhandled_impl_trait {
371 let helper_ident = Ident::new(
372 &format!("__cranpose_impl_{}", func.sig.ident),
373 Span::mixed_site(),
374 );
375 let generics = func.sig.generics.clone();
376
377 let param_erased: Vec<bool> = param_info
378 .iter()
379 .map(|info| {
380 (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
381 || (!info.is_impl_trait
382 && type_bare_generic_ident(&info.ty).is_some()
383 && is_generic_fn_like(&info.ty, &generics))
384 })
385 .collect();
386
387 let mut strippable: std::collections::HashSet<String> = param_info
388 .iter()
389 .zip(¶m_erased)
390 .filter(|(info, erased)| **erased && !info.is_impl_trait)
391 .filter_map(|(info, _)| type_bare_generic_ident(&info.ty))
392 .map(Ident::to_string)
393 .collect();
394 loop {
395 use quote::ToTokens;
396 let mut used_elsewhere: Vec<TokenStream2> = Vec::new();
397 for (info, erased) in param_info.iter().zip(¶m_erased) {
398 if !*erased {
399 used_elsewhere.push(info.ty.to_token_stream());
400 }
401 }
402 used_elsewhere.push(return_ty.to_token_stream());
403 for param in &generics.params {
404 match param {
405 syn::GenericParam::Type(type_param) => {
406 if !strippable.contains(&type_param.ident.to_string()) {
407 used_elsewhere.push(type_param.bounds.to_token_stream());
408 if let Some(default) = &type_param.default {
409 used_elsewhere.push(default.to_token_stream());
410 }
411 }
412 }
413 syn::GenericParam::Const(const_param) => {
414 used_elsewhere.push(const_param.ty.to_token_stream());
415 }
416 syn::GenericParam::Lifetime(_) => {}
417 }
418 }
419 if let Some(where_clause) = &generics.where_clause {
420 for predicate in &where_clause.predicates {
421 if let syn::WherePredicate::Type(pred) = predicate
422 && let Some(ident) = type_bare_generic_ident(&pred.bounded_ty)
423 && strippable.contains(&ident.to_string())
424 {
425 continue;
426 }
427 used_elsewhere.push(predicate.to_token_stream());
428 }
429 }
430 let before = strippable.len();
431 strippable.retain(|name| {
432 !used_elsewhere
433 .iter()
434 .any(|tokens| stream_mentions_ident(tokens, name))
435 });
436 if strippable.len() == before {
437 break;
438 }
439 }
440
441 let helper_generics = filter_generics(&generics, &strippable);
442 let (impl_generics, ty_generics, where_clause) = helper_generics.split_for_impl();
443 let ty_generics_turbofish = ty_generics.as_turbofish();
444
445 let helper_inputs: Vec<TokenStream2> = param_info
446 .iter()
447 .zip(¶m_erased)
448 .filter_map(|(info, erased)| {
449 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
450 None
451 } else if *erased {
452 let ident = &info.ident;
453 Some(quote! { #ident: ::std::boxed::Box<dyn ::core::ops::FnMut() + 'static> })
454 } else {
455 let ident = &info.ident;
456 let ty = &info.ty;
457 Some(quote! { #ident: #ty })
458 }
459 })
460 .collect();
461
462 let param_state_slots: Vec<Ident> = (0..param_info.len())
463 .map(|index| Ident::new(&format!("__param_state_slot{}", index), Span::mixed_site()))
464 .collect();
465
466 let param_setup: Vec<TokenStream2> = param_info
467 .iter()
468 .zip(param_state_slots.iter())
469 .zip(¶m_erased)
470 .map(|((info, slot_ident), erased)| {
471 if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
472 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
473 {
474 let ident = &info.ident;
475 let update = if *erased {
476 quote! { holder.update_boxed(#ident); }
477 } else {
478 quote! { holder.update(#ident); }
479 };
480 quote! {
481 let #slot_ident = #composer_ident
482 .__use_param_slot(|| #core_path::CallbackHolder::new());
483 #composer_ident.with_slot_value::<#core_path::CallbackHolder, _>(
484 #slot_ident,
485 |holder| {
486 #update
487 },
488 );
489 __changed = true;
490 }
491 } else if info.is_impl_trait {
492 quote! { __changed = true; }
493 } else {
494 let ident = &info.ident;
495 let ty = &info.ty;
496 quote! {
497 let #slot_ident = #composer_ident
498 .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
499 if #composer_ident.with_slot_value_mut::<#core_path::ParamState<#ty>, _>(
500 #slot_ident,
501 |state| state.update(&#ident),
502 )
503 {
504 __changed = true;
505 }
506 }
507 }
508 })
509 .collect();
510
511 let param_setup_recompose: Vec<TokenStream2> = param_info
512 .iter()
513 .zip(param_state_slots.iter())
514 .map(|(info, slot_ident)| {
515 if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
516 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
517 {
518 quote! {
519 let #slot_ident = #composer_ident
520 .__use_param_slot(|| #core_path::CallbackHolder::new());
521 }
522 } else if info.is_impl_trait {
523 quote! {}
524 } else {
525 let ty = &info.ty;
526 quote! {
527 let #slot_ident = #composer_ident
528 .__use_param_slot(|| #core_path::ParamState::<#ty>::default());
529 }
530 }
531 })
532 .collect();
533
534 let rebinds: Vec<TokenStream2> = param_info
535 .iter()
536 .zip(param_state_slots.iter())
537 .map(|(info, slot_ident)| {
538 if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
539 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
540 {
541 let pat = &info.pat;
542 let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
543 if can_add_mut && !info.pat_is_mut {
544 quote! {
545 #[allow(unused_mut)]
546 let mut #pat = #composer_ident
547 .with_slot_value::<#core_path::CallbackHolder, _>(
548 #slot_ident,
549 |holder| holder.clone_rc(),
550 );
551 }
552 } else {
553 quote! {
554 #[allow(unused_mut)]
555 let #pat = #composer_ident
556 .with_slot_value::<#core_path::CallbackHolder, _>(
557 #slot_ident,
558 |holder| holder.clone_rc(),
559 );
560 }
561 }
562 } else if info.is_impl_trait {
563 quote! {}
564 } else {
565 let pat = &info.pat;
566 let ident = &info.ident;
567 quote! {
568 let #pat = #ident;
569 }
570 }
571 })
572 .collect();
573
574 let rebinds_for_recompose: Vec<TokenStream2> = param_info
575 .iter()
576 .zip(param_state_slots.iter())
577 .map(|(info, slot_ident)| {
578 if (info.is_impl_trait && is_zero_arg_fn_impl_trait(&info.ty))
579 || (!info.is_impl_trait && is_fn_param(&info.ty, &generics))
580 {
581 let pat = &info.pat;
582 let can_add_mut = matches!(pat.as_ref(), Pat::Ident(_));
583 if can_add_mut && !info.pat_is_mut {
584 quote! {
585 #[allow(unused_mut)]
586 let mut #pat = #composer_ident
587 .with_slot_value::<#core_path::CallbackHolder, _>(
588 #slot_ident,
589 |holder| holder.clone_rc(),
590 );
591 }
592 } else {
593 quote! {
594 #[allow(unused_mut)]
595 let #pat = #composer_ident
596 .with_slot_value::<#core_path::CallbackHolder, _>(
597 #slot_ident,
598 |holder| holder.clone_rc(),
599 );
600 }
601 }
602 } else if info.is_impl_trait {
603 quote! {}
604 } else {
605 let pat = &info.pat;
606 let ty = &info.ty;
607 quote! {
608 let #pat = #composer_ident
609 .with_slot_value::<#core_path::ParamState<#ty>, _>(
610 #slot_ident,
611 |state| {
612 state
613 .value()
614 .expect("composable parameter missing for recomposition")
615 },
616 );
617 }
618 }
619 })
620 .collect();
621
622 let recompose_fn_ident = Ident::new(
623 &format!("__cranpose_recompose_{}", func.sig.ident),
624 Span::mixed_site(),
625 );
626
627 let recompose_setter = quote! {
628 {
629 #composer_ident.set_recompose_callback(move |
630 #composer_ident: &#core_path::Composer|
631 {
632 let _ = #recompose_fn_ident #ty_generics_turbofish (
633 #composer_ident
634 );
635 });
636 }
637 };
638
639 let helper_body = if returns_unit {
640 quote! {
641 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
642 let #current_scope_ident = #composer_ident
643 .current_recompose_scope()
644 .expect("missing recompose scope");
645 let mut __changed = #current_scope_ident.should_recompose();
646 #(#param_setup)*
647 #recompose_setter
648 if !__changed && #current_scope_ident.has_composed_once() {
649 #composer_ident.skip_current_group();
650 return;
651 }
652 #(#rebinds)*
653 #helper_block
654 }
655 } else {
656 quote! {
657 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
658 let #current_scope_ident = #composer_ident
659 .current_recompose_scope()
660 .expect("missing recompose scope");
661 let mut __changed = #current_scope_ident.should_recompose();
662 #(#param_setup)*
663 #recompose_setter
664 let #result_slot_index_ident = #composer_ident
665 .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
666 let #has_previous_ident = #composer_ident
667 .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
668 #result_slot_index_ident,
669 |slot| slot.get().is_some(),
670 );
671 if !__changed && #has_previous_ident {
672 #composer_ident.skip_current_group();
673 let #result_ident = #composer_ident
674 .with_slot_value::<#core_path::ReturnSlot<#return_ty>, _>(
675 #result_slot_index_ident,
676 |slot| {
677 slot.get()
678 .expect("composable return value missing during skip")
679 },
680 );
681 return #result_ident;
682 }
683 let #value_ident: #return_ty = {
684 #(#rebinds)*
685 #helper_block
686 };
687 #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
688 #result_slot_index_ident,
689 |slot| {
690 slot.store(#value_ident.clone());
691 },
692 );
693 #value_ident
694 }
695 };
696
697 let recompose_fn_body = if returns_unit {
698 quote! {
699 #(#param_setup_recompose)*
700 #(#rebinds_for_recompose)*
701 #recompose_block
702 #recompose_setter
703 }
704 } else {
705 quote! {
706 #(#param_setup_recompose)*
707 let #result_slot_index_ident = #composer_ident
708 .__use_return_slot(|| #core_path::ReturnSlot::<#return_ty>::default());
709 #(#rebinds_for_recompose)*
710 let #value_ident: #return_ty = {
711 #recompose_block
712 };
713 #composer_ident.with_slot_value_mut::<#core_path::ReturnSlot<#return_ty>, _>(
714 #result_slot_index_ident,
715 |slot| {
716 slot.store(#value_ident.clone());
717 },
718 );
719 #recompose_setter
720 #invalidate_return_consumer
721 #value_ident
722 }
723 };
724
725 let recompose_fn = quote! {
726 #[allow(non_snake_case)]
727 fn #recompose_fn_ident #impl_generics (
728 #composer_ident: &#core_path::Composer
729 ) -> #return_ty #where_clause {
730 #recompose_fn_body
731 }
732 };
733
734 let helper_fn = quote! {
735 #[allow(non_snake_case, clippy::too_many_arguments)]
736 fn #helper_ident #impl_generics (
737 #composer_ident: &#core_path::Composer
738 #(, #helper_inputs)*
739 ) -> #return_ty #where_clause {
740 #helper_body
741 }
742 };
743
744 let wrapper_args: Vec<TokenStream2> = param_info
745 .iter()
746 .zip(¶m_erased)
747 .filter_map(|(info, erased)| {
748 if info.is_impl_trait && !is_zero_arg_fn_impl_trait(&info.ty) {
749 None
750 } else if *erased {
751 let ident = &info.ident;
752 Some(quote! { ::std::boxed::Box::new(#ident) })
753 } else {
754 let ident = &info.ident;
755 Some(quote! { #ident })
756 }
757 })
758 .collect();
759
760 let wrapped = quote!({
761 #caller_key_stmt
762 #core_path::with_current_composer(|#composer_ident: &#core_path::Composer| {
763 #composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
764 #helper_ident(#composer_ident #(, #wrapper_args)*)
765 })
766 })
767 });
768 *func.block = syn::parse2(wrapped).expect("failed to build block");
769 TokenStream::from(quote! {
770 #recompose_fn
771 #helper_fn
772 #func
773 })
774 } else {
775 let wrapped = quote!({
776 #caller_key_stmt
777 #core_path::with_current_composer(|#outer_composer_ident: &#core_path::Composer| {
778 #outer_composer_ident.with_group(#key_expr, |#composer_ident: &#core_path::Composer| {
779 #core_path::debug_label_current_scope(stringify!(#scope_label_ident));
780 #(#rebinds_for_no_skip)*
781 #original_block
782 })
783 })
784 });
785 *func.block = syn::parse2(wrapped).expect("failed to build block");
786 TokenStream::from(quote! { #func })
787 }
788}
789
790fn find_reserved_pattern_ident(pat: &Pat) -> Option<&Ident> {
791 use syn::visit::Visit;
792
793 struct Scan<'ast> {
794 found: Option<&'ast Ident>,
795 }
796 impl<'ast> syn::visit::Visit<'ast> for Scan<'ast> {
797 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
798 if self.found.is_none() {
799 let name = node.ident.to_string();
800 if name == "__composer" || name.starts_with("__cranpose") {
801 self.found = Some(&node.ident);
802 }
803 }
804 syn::visit::visit_pat_ident(self, node);
805 }
806 }
807 let mut scan = Scan { found: None };
808 scan.visit_pat(pat);
809 scan.found
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn definition_key_does_not_monomorphise_the_once_lock_initializer() {
818 let core_path = quote!(::cranpose_core);
819 let ident = Ident::new("__cranpose_caller_key", Span::mixed_site());
820 let tokens = definition_key_stmt(&core_path, &ident).to_string();
821
822 assert!(
823 tokens.contains("cached_composable_definition_key"),
824 "the definition key must be latched through the outlined core \
825 helper, got: {tokens}"
826 );
827 assert!(
828 !tokens.contains("get_or_init"),
829 "no initializer closure may reach the expansion site, got: {tokens}"
830 );
831 }
832}