Skip to main content

tessera_macros/
lib.rs

1//! # Tessera Macros
2//!
3//! This crate provides procedural macros for the Tessera UI framework.
4//! The main export is the `#[tessera]` attribute macro, which transforms
5//! regular Rust functions into Tessera UI components.
6#![deny(
7    missing_docs,
8    clippy::unwrap_used,
9    rustdoc::broken_intra_doc_links,
10    rustdoc::invalid_rust_codeblocks,
11    rustdoc::invalid_html_tags
12)]
13
14use std::hash::{DefaultHasher, Hash, Hasher};
15
16use proc_macro::TokenStream;
17use quote::{format_ident, quote};
18use syn::{
19    Block, Expr, FnArg, GenericArgument, GenericParam, Generics, Ident, ItemFn, Pat, Path,
20    PathArguments, Token, Type, parse::Parse, parse_macro_input, parse_quote, visit_mut::VisitMut,
21};
22
23/// Helper: parse crate path from attribute TokenStream
24fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Result<syn::Path> {
25    if attr.is_empty() {
26        // Default to `tessera_ui` if no path is provided
27        Ok(syn::parse_quote!(::tessera_ui))
28    } else {
29        // Parse the provided path, e.g., `crate` or `tessera_ui`
30        let tokens: proc_macro2::TokenStream = attr.clone().into();
31        syn::parse(attr).map_err(|_| {
32            syn::Error::new_spanned(tokens, "expected a crate path like `crate` or `tessera_ui`")
33        })
34    }
35}
36
37#[derive(Clone, Copy, Debug, Default)]
38struct SetterAttrConfig {
39    skip: bool,
40    into: bool,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44enum PropHelperKind {
45    Callback,
46    CallbackWith,
47    RenderSlot,
48    RenderSlotWith,
49}
50
51#[derive(Clone, Copy, Debug, Default)]
52struct PropFieldAttrConfig {
53    skip_eq: bool,
54}
55
56fn parse_setter_attr(attrs: &[syn::Attribute]) -> syn::Result<SetterAttrConfig> {
57    let mut config = SetterAttrConfig::default();
58    for attr in attrs {
59        if !attr.path().is_ident("prop") {
60            continue;
61        }
62
63        match &attr.meta {
64            syn::Meta::Path(_) => {}
65            syn::Meta::List(_) => {
66                attr.parse_nested_meta(|meta| {
67                    if meta.path.is_ident("skip") {
68                        return Err(meta.error("unsupported setter option `skip`"));
69                    }
70                    if meta.path.is_ident("skip_setter") {
71                        config.skip = true;
72                        return Ok(());
73                    }
74                    if meta.path.is_ident("into") {
75                        config.into = true;
76                        return Ok(());
77                    }
78                    Ok(())
79                })?;
80            }
81            syn::Meta::NameValue(_) => {
82                return Err(syn::Error::new_spanned(
83                    attr,
84                    "unsupported #[prop = ...] form; expected #[prop(...)]",
85                ));
86            }
87        }
88    }
89    Ok(config)
90}
91
92fn parse_prop_field_attr(attrs: &[syn::Attribute]) -> syn::Result<PropFieldAttrConfig> {
93    let mut config = PropFieldAttrConfig::default();
94    for attr in attrs {
95        if !attr.path().is_ident("prop") {
96            continue;
97        }
98        attr.parse_nested_meta(|meta| {
99            if meta.path.is_ident("skip_eq") {
100                if config.skip_eq {
101                    return Err(meta.error("duplicate `skip_eq` in #[prop(...)]"));
102                }
103                config.skip_eq = true;
104                return Ok(());
105            }
106            if meta.path.is_ident("skip") {
107                return Err(meta.error("unsupported field option `skip`"));
108            }
109            if meta.path.is_ident("skip_setter") || meta.path.is_ident("into") {
110                return Ok(());
111            }
112            if meta.path.is_ident("crate_path") {
113                return Err(meta.error(
114                    "container option in field #[prop(...)]; `crate_path` is only valid on structs",
115                ));
116            }
117
118            Err(meta.error(
119                "unsupported field #[prop(...)] option; expected setter options (`skip_setter`/`into`) or compare option (`skip_eq`)",
120            ))
121        })?;
122    }
123    Ok(config)
124}
125
126fn option_inner_type(ty: &Type) -> Option<Type> {
127    let Type::Path(type_path) = ty else {
128        return None;
129    };
130    let segment = type_path.path.segments.last()?;
131    if segment.ident != "Option" {
132        return None;
133    }
134    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
135        return None;
136    };
137    arguments.args.iter().find_map(|arg| match arg {
138        GenericArgument::Type(inner) => Some(inner.clone()),
139        _ => None,
140    })
141}
142
143fn parse_functor_signature(ty: &Type, type_name: &str) -> Option<(Type, Type)> {
144    let Type::Path(type_path) = ty else {
145        return None;
146    };
147    let segment = type_path.path.segments.last()?;
148    if segment.ident != type_name {
149        return None;
150    }
151    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
152        return None;
153    };
154    let mut types = arguments.args.iter().filter_map(|arg| match arg {
155        GenericArgument::Type(ty) => Some(ty.clone()),
156        _ => None,
157    });
158    let arg = types.next()?;
159    let ret = types.next().unwrap_or_else(|| parse_quote!(()));
160    Some((arg, ret))
161}
162
163fn is_unit_type(ty: &Type) -> bool {
164    matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
165}
166
167fn type_last_segment_ident(ty: &Type) -> Option<&Ident> {
168    let Type::Path(type_path) = ty else {
169        return None;
170    };
171    type_path.path.segments.last().map(|segment| &segment.ident)
172}
173
174fn infer_prop_helper_kind(ty: &Type) -> Option<PropHelperKind> {
175    let value_ty = option_inner_type(ty).unwrap_or_else(|| ty.clone());
176    let ident = type_last_segment_ident(&value_ty)?;
177    if ident == "Callback" {
178        return Some(PropHelperKind::Callback);
179    }
180    if ident == "CallbackWith" {
181        return Some(PropHelperKind::CallbackWith);
182    }
183    if ident == "RenderSlot" {
184        return Some(PropHelperKind::RenderSlot);
185    }
186    if ident == "RenderSlotWith" {
187        return Some(PropHelperKind::RenderSlotWith);
188    }
189    None
190}
191
192fn is_arc_type(ty: &Type) -> bool {
193    type_last_segment_ident(ty).is_some_and(|ident| ident == "Arc")
194}
195
196fn is_rc_type(ty: &Type) -> bool {
197    type_last_segment_ident(ty).is_some_and(|ident| ident == "Rc")
198}
199
200fn field_compare_expr(field_ident: &Ident, ty: &Type) -> proc_macro2::TokenStream {
201    if is_arc_type(ty) {
202        return quote! { ::std::sync::Arc::ptr_eq(&self.#field_ident, &other.#field_ident) };
203    }
204    if is_rc_type(ty) {
205        return quote! { ::std::rc::Rc::ptr_eq(&self.#field_ident, &other.#field_ident) };
206    }
207    if let Some(inner_ty) = option_inner_type(ty) {
208        if is_arc_type(&inner_ty) {
209            return quote! {
210                match (&self.#field_ident, &other.#field_ident) {
211                    (Some(lhs), Some(rhs)) => ::std::sync::Arc::ptr_eq(lhs, rhs),
212                    (None, None) => true,
213                    _ => false,
214                }
215            };
216        }
217        if is_rc_type(&inner_ty) {
218            return quote! {
219                match (&self.#field_ident, &other.#field_ident) {
220                    (Some(lhs), Some(rhs)) => ::std::rc::Rc::ptr_eq(lhs, rhs),
221                    (None, None) => true,
222                    _ => false,
223                }
224            };
225        }
226    }
227    quote! { self.#field_ident == other.#field_ident }
228}
229
230#[cfg(feature = "shard")]
231#[derive(Default)]
232struct ShardMacroArgs {
233    crate_path: Option<Path>,
234    shard_crate_path: Option<Path>,
235    state_type: Option<Type>,
236    lifecycle: Option<Ident>,
237}
238
239#[cfg(feature = "shard")]
240struct ShardParam {
241    ident: Ident,
242    ty: Type,
243    is_router: bool,
244}
245
246#[cfg(feature = "shard")]
247fn parse_shard_params(sig: &syn::Signature) -> syn::Result<Vec<ShardParam>> {
248    let mut params = Vec::with_capacity(sig.inputs.len());
249    for arg in &sig.inputs {
250        match arg {
251            FnArg::Receiver(receiver) => {
252                return Err(syn::Error::new_spanned(
253                    receiver,
254                    "#[shard] does not support methods; use a free function",
255                ));
256            }
257            FnArg::Typed(pat_type) => {
258                let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
259                    return Err(syn::Error::new_spanned(
260                        &pat_type.pat,
261                        "#[shard] parameters must be simple named bindings like `foo: T`",
262                    ));
263                };
264                let is_router = pat_type
265                    .attrs
266                    .iter()
267                    .any(|attr| attr.path().is_ident("router"));
268                params.push(ShardParam {
269                    ident: pat_ident.ident.clone(),
270                    ty: (*pat_type.ty).clone(),
271                    is_router,
272                });
273            }
274        }
275    }
276    Ok(params)
277}
278
279#[cfg(feature = "shard")]
280fn is_state_router_controller_type(ty: &Type) -> bool {
281    let Type::Path(type_path) = ty else {
282        return false;
283    };
284    let Some(state_segment) = type_path.path.segments.last() else {
285        return false;
286    };
287    if state_segment.ident != "State" {
288        return false;
289    }
290    let syn::PathArguments::AngleBracketed(args) = &state_segment.arguments else {
291        return false;
292    };
293    let Some(syn::GenericArgument::Type(Type::Path(controller_path))) = args.args.first() else {
294        return false;
295    };
296    controller_path
297        .path
298        .segments
299        .last()
300        .is_some_and(|segment| segment.ident == "RouterController")
301}
302
303#[cfg(feature = "shard")]
304impl Parse for ShardMacroArgs {
305    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
306        let mut args = ShardMacroArgs::default();
307        while !input.is_empty() {
308            let key: Ident = input.parse()?;
309            input.parse::<Token![=]>()?;
310            match key.to_string().as_str() {
311                "crate_path" => {
312                    if args.crate_path.is_some() {
313                        return Err(syn::Error::new(
314                            key.span(),
315                            "duplicate `crate_path` argument",
316                        ));
317                    }
318                    args.crate_path = Some(input.parse::<Path>()?);
319                }
320                "shard_crate_path" => {
321                    if args.shard_crate_path.is_some() {
322                        return Err(syn::Error::new(
323                            key.span(),
324                            "duplicate `shard_crate_path` argument",
325                        ));
326                    }
327                    args.shard_crate_path = Some(input.parse::<Path>()?);
328                }
329                "state" => {
330                    if args.state_type.is_some() {
331                        return Err(syn::Error::new(key.span(), "duplicate `state` argument"));
332                    }
333                    args.state_type = Some(input.parse::<Type>()?);
334                }
335                "lifecycle" => {
336                    if args.lifecycle.is_some() {
337                        return Err(syn::Error::new(
338                            key.span(),
339                            "duplicate `lifecycle` argument",
340                        ));
341                    }
342                    args.lifecycle = Some(input.parse::<Ident>()?);
343                }
344                _ => {
345                    return Err(syn::Error::new(
346                        key.span(),
347                        "unsupported #[shard(...)] argument; expected `state`, `lifecycle`, `crate_path`, or `shard_crate_path`",
348                    ));
349                }
350            }
351
352            if input.peek(Token![,]) {
353                input.parse::<Token![,]>()?;
354            }
355        }
356        Ok(args)
357    }
358}
359
360/// Helper: tokens to register a component node
361fn register_node_tokens(crate_path: &syn::Path, fn_name: &syn::Ident) -> proc_macro2::TokenStream {
362    quote! {
363        #crate_path::__private::register_component_node(
364            stringify!(#fn_name),
365            __tessera_component_type_id,
366        )
367    }
368}
369
370/// Parse and validate strict component props signature:
371/// `fn component()` or `fn component(foo: T, bar: U)`.
372enum ComponentPropSignature {
373    Unit,
374    Params(Vec<PropFieldSpec>),
375}
376
377fn strict_prop_signature(sig: &syn::Signature) -> Result<ComponentPropSignature, syn::Error> {
378    if sig.inputs.is_empty() {
379        return Ok(ComponentPropSignature::Unit);
380    }
381
382    let mut fields = Vec::new();
383    for arg in &sig.inputs {
384        let FnArg::Typed(arg) = arg else {
385            return Err(syn::Error::new_spanned(
386                arg,
387                "#[tessera] methods are not supported; use free functions with named parameters",
388            ));
389        };
390
391        let Pat::Ident(pat_ident) = arg.pat.as_ref() else {
392            return Err(syn::Error::new_spanned(
393                &arg.pat,
394                "component parameters must be named identifiers",
395            ));
396        };
397
398        if pat_ident.by_ref.is_some() || pat_ident.mutability.is_some() {
399            return Err(syn::Error::new_spanned(
400                pat_ident,
401                "component parameters must be plain named bindings like `foo: T`",
402            ));
403        }
404
405        if matches!(arg.ty.as_ref(), Type::Reference(_)) {
406            return Err(syn::Error::new_spanned(
407                &arg.ty,
408                "component parameters must be owned types; borrowed parameter types are not supported",
409            ));
410        }
411
412        let field_setter_attr = parse_setter_attr(&arg.attrs)?;
413        let prop_attr = parse_prop_field_attr(&arg.attrs)?;
414        fields.push(PropFieldSpec {
415            ident: pat_ident.ident.clone(),
416            ty: (*arg.ty).clone(),
417            setter: field_setter_attr,
418            helper: infer_prop_helper_kind(arg.ty.as_ref()),
419            skip_eq: prop_attr.skip_eq,
420        });
421    }
422
423    Ok(ComponentPropSignature::Params(fields))
424}
425
426/// Helper: tokens to attach replay metadata to the current component node.
427fn replay_register_tokens(
428    crate_path: &syn::Path,
429    runner_name: &syn::Ident,
430    prop_type: &syn::Type,
431    props_expr: proc_macro2::TokenStream,
432) -> proc_macro2::TokenStream {
433    quote! {
434        let __tessera_component_reused = {
435            let __tessera_runner =
436                #crate_path::__private::make_component_runner::<#prop_type>(#runner_name);
437            #crate_path::__private::set_current_component_replay(__tessera_runner, #props_expr)
438        };
439    }
440}
441
442/// Helper: compile-time assertion that component props implement `Prop`.
443fn prop_assert_tokens(crate_path: &syn::Path, prop_type: &syn::Type) -> proc_macro2::TokenStream {
444    quote! {
445        {
446            fn __tessera_assert_prop<T: #crate_path::__private::Prop>() {}
447            __tessera_assert_prop::<#prop_type>();
448        }
449    }
450}
451
452struct PropFieldSpec {
453    ident: Ident,
454    ty: Type,
455    setter: SetterAttrConfig,
456    helper: Option<PropHelperKind>,
457    skip_eq: bool,
458}
459
460fn is_required_component_field(field: &PropFieldSpec) -> bool {
461    option_inner_type(&field.ty).is_none()
462}
463
464fn stored_value_ty(field_ty: &Type) -> Type {
465    option_inner_type(field_ty).unwrap_or_else(|| field_ty.clone())
466}
467
468fn stored_field_ty(field_ty: &Type) -> Type {
469    let value_ty = stored_value_ty(field_ty);
470    parse_quote!(::core::option::Option<#value_ty>)
471}
472
473fn generate_default_setter_method_for_path(
474    field: &PropFieldSpec,
475    field_path: &proc_macro2::TokenStream,
476) -> syn::Result<Option<proc_macro2::TokenStream>> {
477    if field.setter.skip {
478        return Ok(None);
479    }
480
481    let ident = &field.ident;
482    let method_doc = format!("Set `{ident}`.");
483    let field_ty = &field.ty;
484    if let Some(inner_ty) = option_inner_type(field_ty) {
485        let method = if field.setter.into {
486            quote! {
487                #[doc = #method_doc]
488                pub fn #ident(mut self, #ident: impl Into<#inner_ty>) -> Self {
489                    self.#field_path = Some(#ident.into());
490                    self
491                }
492            }
493        } else {
494            quote! {
495                #[doc = #method_doc]
496                pub fn #ident(mut self, #ident: #inner_ty) -> Self {
497                    self.#field_path = Some(#ident);
498                    self
499                }
500            }
501        };
502        return Ok(Some(method));
503    }
504
505    let method = if field.setter.into {
506        quote! {
507            #[doc = #method_doc]
508            pub fn #ident(mut self, #ident: impl Into<#field_ty>) -> Self {
509                self.#field_path = Some(#ident.into());
510                self
511            }
512        }
513    } else {
514        quote! {
515            #[doc = #method_doc]
516            pub fn #ident(mut self, #ident: #field_ty) -> Self {
517                self.#field_path = Some(#ident);
518                self
519            }
520        }
521    };
522    Ok(Some(method))
523}
524
525fn generate_optional_setter_method_for_path(
526    field: &PropFieldSpec,
527    field_path: &proc_macro2::TokenStream,
528) -> Option<proc_macro2::TokenStream> {
529    if field.setter.skip {
530        return None;
531    }
532
533    let inner_ty = option_inner_type(&field.ty)?;
534    let ident = &field.ident;
535    let optional_ident = format_ident!("{}_optional", ident);
536    let method_doc = format!("Set `{ident}` from an optional value.");
537
538    Some(quote! {
539        #[doc = #method_doc]
540        pub fn #optional_ident(mut self, #ident: ::core::option::Option<#inner_ty>) -> Self {
541            self.#field_path = #ident;
542            self
543        }
544    })
545}
546
547fn generate_helper_setter_methods(
548    field: &PropFieldSpec,
549    helper: PropHelperKind,
550    crate_path: &Path,
551    field_path: &proc_macro2::TokenStream,
552) -> syn::Result<proc_macro2::TokenStream> {
553    let ident = &field.ident;
554    let shared_ident = format_ident!("{}_shared", ident);
555    let helper_doc = format!("Set `{ident}` from a closure.");
556    let shared_doc = format!("Set `{ident}` from a shared handle.");
557    let value_ty = stored_value_ty(&field.ty);
558
559    match helper {
560        PropHelperKind::Callback => {
561            let matches_type = matches!(
562                &value_ty,
563                Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Callback")
564            );
565            if !matches_type {
566                return Err(syn::Error::new_spanned(
567                    &field.ty,
568                    "`#[prop(callback)]` requires `Callback` or `Option<Callback>`",
569                ));
570            }
571
572            let closure_assign = quote! { Some(#crate_path::Callback::new(#ident)) };
573            let shared_assign = quote! { Some(#ident.into()) };
574
575            Ok(quote! {
576                #[doc = #helper_doc]
577                pub fn #ident<F>(mut self, #ident: F) -> Self
578                where
579                    F: Fn() + Send + Sync + 'static,
580                {
581                    self.#field_path = #closure_assign;
582                    self
583                }
584
585                #[doc = #shared_doc]
586                pub fn #shared_ident(mut self, #ident: impl Into<#crate_path::Callback>) -> Self {
587                    self.#field_path = #shared_assign;
588                    self
589                }
590            })
591        }
592        PropHelperKind::CallbackWith => {
593            let Some((arg_ty, ret_ty)) = parse_functor_signature(&value_ty, "CallbackWith") else {
594                return Err(syn::Error::new_spanned(
595                    &field.ty,
596                    "`#[prop(callback_with)]` requires `CallbackWith<T, R>` or `Option<CallbackWith<T, R>>`",
597                ));
598            };
599
600            let closure_assign = if is_unit_type(&arg_ty) {
601                quote! { Some(#crate_path::CallbackWith::new(move |()| #ident())) }
602            } else {
603                quote! { Some(#crate_path::CallbackWith::new(#ident)) }
604            };
605            let shared_assign = quote! { Some(#ident.into()) };
606
607            let callback_bound = if is_unit_type(&arg_ty) {
608                quote! { F: Fn() -> #ret_ty + Send + Sync + 'static }
609            } else {
610                quote! { F: Fn(#arg_ty) -> #ret_ty + Send + Sync + 'static }
611            };
612
613            Ok(quote! {
614                #[doc = #helper_doc]
615                pub fn #ident<F>(mut self, #ident: F) -> Self
616                where
617                    #callback_bound,
618                {
619                    self.#field_path = #closure_assign;
620                    self
621                }
622
623                #[doc = #shared_doc]
624                pub fn #shared_ident(
625                    mut self,
626                    #ident: impl Into<#crate_path::CallbackWith<#arg_ty, #ret_ty>>,
627                ) -> Self {
628                    self.#field_path = #shared_assign;
629                    self
630                }
631            })
632        }
633        PropHelperKind::RenderSlot => {
634            let matches_type = matches!(
635                &value_ty,
636                Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RenderSlot")
637            );
638            if !matches_type {
639                return Err(syn::Error::new_spanned(
640                    &field.ty,
641                    "`#[prop(render_slot)]` requires `RenderSlot` or `Option<RenderSlot>`",
642                ));
643            }
644
645            let closure_assign = quote! { Some(#crate_path::RenderSlot::new(#ident)) };
646            let shared_assign = quote! { Some(#ident.into()) };
647
648            Ok(quote! {
649                #[doc = #helper_doc]
650                pub fn #ident<F>(mut self, #ident: F) -> Self
651                where
652                    F: Fn() + Send + Sync + 'static,
653                {
654                    self.#field_path = #closure_assign;
655                    self
656                }
657
658                #[doc = #shared_doc]
659                pub fn #shared_ident(mut self, #ident: impl Into<#crate_path::RenderSlot>) -> Self {
660                    self.#field_path = #shared_assign;
661                    self
662                }
663            })
664        }
665        PropHelperKind::RenderSlotWith => {
666            let Some((arg_ty, _ret_ty)) = parse_functor_signature(&value_ty, "RenderSlotWith")
667            else {
668                return Err(syn::Error::new_spanned(
669                    &field.ty,
670                    "`#[prop(render_slot_with)]` requires `RenderSlotWith<T>` or `Option<RenderSlotWith<T>>`",
671                ));
672            };
673
674            let closure_assign = if is_unit_type(&arg_ty) {
675                quote! { Some(#crate_path::RenderSlotWith::new(move |()| #ident())) }
676            } else {
677                quote! { Some(#crate_path::RenderSlotWith::new(#ident)) }
678            };
679            let shared_assign = quote! { Some(#ident.into()) };
680
681            let callback_bound = if is_unit_type(&arg_ty) {
682                quote! { F: Fn() + Send + Sync + 'static }
683            } else {
684                quote! { F: Fn(#arg_ty) + Send + Sync + 'static }
685            };
686
687            Ok(quote! {
688                #[doc = #helper_doc]
689                pub fn #ident<F>(mut self, #ident: F) -> Self
690                where
691                    #callback_bound,
692                {
693                    self.#field_path = #closure_assign;
694                    self
695                }
696
697                #[doc = #shared_doc]
698                pub fn #shared_ident(
699                    mut self,
700                    #ident: impl Into<#crate_path::RenderSlotWith<#arg_ty>>,
701                ) -> Self {
702                    self.#field_path = #shared_assign;
703                    self
704                }
705            })
706        }
707    }
708}
709
710fn generate_constructor_param_and_assignment(
711    field: &PropFieldSpec,
712    crate_path: &Path,
713) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
714    let ident = &field.ident;
715    let field_ty = &field.ty;
716    let value_ty = stored_value_ty(field_ty);
717    let helper_uses_into = matches!(
718        field.helper,
719        Some(
720            PropHelperKind::Callback
721                | PropHelperKind::CallbackWith
722                | PropHelperKind::RenderSlot
723                | PropHelperKind::RenderSlotWith
724        )
725    );
726
727    if let Some(helper) = field.helper {
728        match helper {
729            PropHelperKind::Callback => {
730                let matches_type = matches!(
731                    &value_ty,
732                    Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Callback")
733                );
734                if !matches_type {
735                    return Err(syn::Error::new_spanned(
736                        &field.ty,
737                        "`#[prop(callback)]` requires `Callback` or `Option<Callback>`",
738                    ));
739                }
740            }
741            PropHelperKind::CallbackWith => {
742                let Some((arg_ty, ret_ty)) = parse_functor_signature(&value_ty, "CallbackWith")
743                else {
744                    return Err(syn::Error::new_spanned(
745                        &field.ty,
746                        "`#[prop(callback_with)]` requires `CallbackWith<T, R>` or `Option<CallbackWith<T, R>>`",
747                    ));
748                };
749
750                if is_unit_type(&arg_ty) {
751                    let param = quote! {
752                        #ident: impl Fn() -> #ret_ty + Send + Sync + 'static
753                    };
754                    let assignment = quote! {
755                        __tessera_builder.props.#ident =
756                            Some(#crate_path::CallbackWith::new(move |()| #ident()));
757                    };
758                    return Ok((param, assignment));
759                }
760            }
761            PropHelperKind::RenderSlot => {
762                let matches_type = matches!(
763                    &value_ty,
764                    Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RenderSlot")
765                );
766                if !matches_type {
767                    return Err(syn::Error::new_spanned(
768                        &field.ty,
769                        "`#[prop(render_slot)]` requires `RenderSlot` or `Option<RenderSlot>`",
770                    ));
771                }
772            }
773            PropHelperKind::RenderSlotWith => {
774                let Some((arg_ty, _)) = parse_functor_signature(&value_ty, "RenderSlotWith") else {
775                    return Err(syn::Error::new_spanned(
776                        &field.ty,
777                        "`#[prop(render_slot_with)]` requires `RenderSlotWith<T>` or `Option<RenderSlotWith<T>>`",
778                    ));
779                };
780
781                if is_unit_type(&arg_ty) {
782                    let param = quote! {
783                        #ident: impl Fn() + Send + Sync + 'static
784                    };
785                    let assignment = quote! {
786                        __tessera_builder.props.#ident =
787                            Some(#crate_path::RenderSlotWith::new(move |()| #ident()));
788                    };
789                    return Ok((param, assignment));
790                }
791            }
792        }
793    }
794
795    let constructor_param = if field.setter.into || helper_uses_into {
796        quote!(#ident: impl Into<#field_ty>)
797    } else {
798        quote!(#ident: #field_ty)
799    };
800    let constructor_assignment = if field.setter.into || helper_uses_into {
801        quote! {
802            __tessera_builder.props.#ident = Some(#ident.into());
803        }
804    } else {
805        quote! {
806            __tessera_builder.props.#ident = Some(#ident);
807        }
808    };
809
810    Ok((constructor_param, constructor_assignment))
811}
812
813/// Automatically converts a struct into component props and
814fn pascal_case_ident(base: &Ident, suffix: &str) -> Ident {
815    let mut output = String::new();
816    for segment in base
817        .to_string()
818        .split('_')
819        .filter(|segment| !segment.is_empty())
820    {
821        let mut chars = segment.chars();
822        if let Some(first) = chars.next() {
823            output.extend(first.to_uppercase());
824            output.push_str(chars.as_str());
825        }
826    }
827    output.push_str(suffix);
828    format_ident!("{output}")
829}
830
831fn hidden_props_ident(fn_name: &Ident) -> Ident {
832    format_ident!("__Tessera{}Props", pascal_case_ident(fn_name, ""))
833}
834
835fn builder_ident(fn_name: &Ident) -> Ident {
836    pascal_case_ident(fn_name, "Builder")
837}
838
839fn hidden_component_impl_ident(fn_name: &Ident) -> Ident {
840    format_ident!("__tessera_{}_impl", fn_name)
841}
842
843fn hidden_const_marker_ident(fn_name: &Ident, const_ident: &Ident) -> Ident {
844    format_ident!(
845        "__Tessera{}ConstMarker{}",
846        pascal_case_ident(fn_name, ""),
847        pascal_case_ident(const_ident, "")
848    )
849}
850
851fn validate_component_generics(generics: &Generics) -> syn::Result<()> {
852    for param in &generics.params {
853        if let GenericParam::Lifetime(param) = param {
854            return Err(syn::Error::new_spanned(
855                param,
856                "#[tessera] components do not support lifetime generics; use owned props instead",
857            ));
858        }
859    }
860    Ok(())
861}
862
863fn component_turbofish_tokens(generics: &Generics) -> proc_macro2::TokenStream {
864    let generic_args: Vec<_> = generics
865        .params
866        .iter()
867        .map(|param| match param {
868            GenericParam::Type(param) => {
869                let ident = &param.ident;
870                quote!(#ident)
871            }
872            GenericParam::Const(param) => {
873                let ident = &param.ident;
874                quote!(#ident)
875            }
876            GenericParam::Lifetime(_) => unreachable!("lifetime generics are rejected earlier"),
877        })
878        .collect();
879    if generic_args.is_empty() {
880        quote!()
881    } else {
882        quote!(::<#(#generic_args),*>)
883    }
884}
885
886fn generic_marker_tokens(
887    fn_name: &Ident,
888    generics: &Generics,
889) -> (
890    Vec<proc_macro2::TokenStream>,
891    Option<proc_macro2::TokenStream>,
892) {
893    let mut const_marker_defs = Vec::new();
894    let mut marker_parts = Vec::new();
895
896    for param in &generics.params {
897        match param {
898            GenericParam::Type(param) => {
899                let ident = &param.ident;
900                marker_parts.push(quote!(#ident));
901            }
902            GenericParam::Const(param) => {
903                let ident = &param.ident;
904                let ty = &param.ty;
905                let marker_ident = hidden_const_marker_ident(fn_name, ident);
906                const_marker_defs.push(quote! {
907                    struct #marker_ident<const #ident: #ty>;
908                });
909                marker_parts.push(quote!(#marker_ident<#ident>));
910            }
911            GenericParam::Lifetime(_) => unreachable!("lifetime generics are rejected earlier"),
912        }
913    }
914
915    if marker_parts.is_empty() {
916        return (const_marker_defs, None);
917    }
918
919    let marker_ty = if marker_parts.len() == 1 {
920        let marker_part = &marker_parts[0];
921        quote!(#marker_part)
922    } else {
923        quote!((#(#marker_parts,)*))
924    };
925
926    (
927        const_marker_defs,
928        Some(quote! {
929            __tessera_generic_marker: ::core::marker::PhantomData<#marker_ty>
930        }),
931    )
932}
933
934fn generate_props_clone_default_impls(
935    type_name: &Ident,
936    generics: &Generics,
937    field_idents: &[Ident],
938    has_generic_marker: bool,
939) -> proc_macro2::TokenStream {
940    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
941    let clone_fields: Vec<_> = field_idents
942        .iter()
943        .map(|ident| quote!(#ident: self.#ident.clone()))
944        .collect();
945    let default_fields: Vec<_> = field_idents
946        .iter()
947        .map(|ident| quote!(#ident: ::core::default::Default::default()))
948        .collect();
949    let clone_marker = has_generic_marker.then(|| {
950        quote! {
951            __tessera_generic_marker: ::core::marker::PhantomData,
952        }
953    });
954    let default_marker = has_generic_marker.then(|| {
955        quote! {
956            __tessera_generic_marker: ::core::marker::PhantomData,
957        }
958    });
959
960    quote! {
961        impl #impl_generics ::core::clone::Clone for #type_name #ty_generics #where_clause {
962            fn clone(&self) -> Self {
963                Self {
964                    #(#clone_fields,)*
965                    #clone_marker
966                }
967            }
968        }
969
970        impl #impl_generics ::core::default::Default for #type_name #ty_generics #where_clause {
971            fn default() -> Self {
972                Self {
973                    #(#default_fields,)*
974                    #default_marker
975                }
976            }
977        }
978    }
979}
980
981fn generate_prop_like_impls(
982    type_name: &Ident,
983    generics: &Generics,
984    fields: &[PropFieldSpec],
985    crate_path: &Path,
986) -> proc_macro2::TokenStream {
987    let compare_fields: Vec<_> = fields
988        .iter()
989        .filter(|field| !field.skip_eq)
990        .map(|field| field_compare_expr(&field.ident, &stored_field_ty(&field.ty)))
991        .collect();
992    let prop_eq_expr = if compare_fields.is_empty() {
993        quote! { true }
994    } else {
995        quote! { true #(&& #compare_fields)* }
996    };
997    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
998
999    quote! {
1000        impl #impl_generics ::core::cmp::PartialEq for #type_name #ty_generics #where_clause {
1001            fn eq(&self, other: &Self) -> bool {
1002                #prop_eq_expr
1003            }
1004        }
1005
1006        impl #impl_generics #crate_path::__private::Prop for #type_name #ty_generics #where_clause {
1007            fn prop_eq(&self, other: &Self) -> bool {
1008                <Self as ::core::cmp::PartialEq>::eq(self, other)
1009            }
1010        }
1011    }
1012}
1013
1014fn generate_builder_methods(
1015    fields: &[PropFieldSpec],
1016    crate_path: &Path,
1017) -> syn::Result<Vec<proc_macro2::TokenStream>> {
1018    let mut methods = Vec::new();
1019    for field in fields {
1020        if is_required_component_field(field) {
1021            continue;
1022        }
1023        let ident = &field.ident;
1024        let field_path = quote!(props.#ident);
1025        if let Some(helper) = field.helper {
1026            methods.push(generate_helper_setter_methods(
1027                field,
1028                helper,
1029                crate_path,
1030                &field_path,
1031            )?);
1032        } else if let Some(method) = generate_default_setter_method_for_path(field, &field_path)? {
1033            methods.push(method);
1034        }
1035        if let Some(method) = generate_optional_setter_method_for_path(field, &field_path) {
1036            methods.push(method);
1037        }
1038    }
1039    Ok(methods)
1040}
1041
1042/// Helper: tokens to compute a stable component type id based on module path +
1043/// function name.
1044fn component_type_id_tokens(
1045    fn_name: &syn::Ident,
1046    prop_type: &syn::Type,
1047) -> proc_macro2::TokenStream {
1048    quote! {
1049        {
1050            use std::{any::TypeId, hash::{Hash, Hasher}};
1051            let mut hasher = std::collections::hash_map::DefaultHasher::new();
1052            module_path!().hash(&mut hasher);
1053            stringify!(#fn_name).hash(&mut hasher);
1054            TypeId::of::<#prop_type>().hash(&mut hasher);
1055            hasher.finish()
1056        }
1057    }
1058}
1059
1060struct ControlFlowInstrumenter {
1061    /// counter to generate unique IDs in current function
1062    counter: usize,
1063    /// seed to prevent ID collisions across functions
1064    seed: u64,
1065}
1066
1067impl ControlFlowInstrumenter {
1068    fn new(seed: u64) -> Self {
1069        Self { counter: 0, seed }
1070    }
1071
1072    /// Generate the next unique group ID
1073    fn next_group_id(&mut self) -> u64 {
1074        let mut hasher = DefaultHasher::new();
1075        self.seed.hash(&mut hasher);
1076        self.counter.hash(&mut hasher);
1077        self.counter += 1;
1078        hasher.finish()
1079    }
1080
1081    /// Wrap an expression in a GroupGuard block
1082    ///
1083    /// Before transform: expr
1084    /// After transform: { let _group_guard =
1085    /// ::tessera_ui::__private::GroupGuard::new(#id); expr }
1086    fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
1087        // Recursively visit sub-expressions (depth-first) to ensure nested structures
1088        // are wrapped
1089        self.visit_expr_mut(expr);
1090        let group_id = self.next_group_id();
1091        // Use fully-qualified path ::tessera_ui to avoid relying on a crate alias
1092        let original_expr = &expr;
1093        let new_expr: Expr = parse_quote! {
1094            {
1095                let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
1096                #original_expr
1097            }
1098        };
1099        *expr = new_expr;
1100    }
1101
1102    /// Wrap a block in a GroupGuard block
1103    fn wrap_block_in_group(&mut self, block: &mut Block) {
1104        // Recursively instrument nested expressions before wrapping the block
1105        self.visit_block_mut(block);
1106
1107        let group_id = self.next_group_id();
1108        let original_stmts = &block.stmts;
1109
1110        let new_block: Block = parse_quote! {
1111            {
1112                let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
1113                #(#original_stmts)*
1114            }
1115        };
1116
1117        *block = new_block;
1118    }
1119
1120    /// Wrap a block in a path-only group block.
1121    fn wrap_block_in_path_group(&mut self, block: &mut Block) {
1122        self.visit_block_mut(block);
1123
1124        let group_id = self.next_group_id();
1125        let original_stmts = &block.stmts;
1126
1127        let new_block: Block = parse_quote! {
1128            {
1129                let _group_guard = ::tessera_ui::__private::PathGroupGuard::new(#group_id);
1130                #(#original_stmts)*
1131            }
1132        };
1133
1134        *block = new_block;
1135    }
1136}
1137
1138impl VisitMut for ControlFlowInstrumenter {
1139    fn visit_expr_if_mut(&mut self, i: &mut syn::ExprIf) {
1140        self.visit_expr_mut(&mut i.cond);
1141        self.wrap_block_in_group(&mut i.then_branch);
1142        if let Some((_, else_branch)) = &mut i.else_branch {
1143            match &mut **else_branch {
1144                Expr::Block(block_expr) => {
1145                    self.wrap_block_in_group(&mut block_expr.block);
1146                }
1147                Expr::If(_) => {
1148                    self.visit_expr_mut(else_branch);
1149                }
1150                _ => {
1151                    self.wrap_expr_in_group(else_branch);
1152                }
1153            }
1154        }
1155    }
1156
1157    fn visit_expr_match_mut(&mut self, m: &mut syn::ExprMatch) {
1158        self.visit_expr_mut(&mut m.expr);
1159        for arm in &mut m.arms {
1160            self.wrap_expr_in_group(&mut arm.body);
1161        }
1162    }
1163
1164    fn visit_expr_for_loop_mut(&mut self, f: &mut syn::ExprForLoop) {
1165        self.visit_expr_mut(&mut f.expr);
1166        self.wrap_block_in_path_group(&mut f.body);
1167    }
1168
1169    fn visit_expr_while_mut(&mut self, w: &mut syn::ExprWhile) {
1170        self.visit_expr_mut(&mut w.cond);
1171        self.wrap_block_in_path_group(&mut w.body);
1172    }
1173
1174    fn visit_expr_loop_mut(&mut self, l: &mut syn::ExprLoop) {
1175        self.wrap_block_in_path_group(&mut l.body);
1176    }
1177}
1178
1179/// Transforms a regular Rust function into a Tessera UI component.
1180///
1181/// # Usage
1182///
1183/// Annotate a plain free function with `#[tessera]`.
1184///
1185/// The macro turns the function into a Tessera component entrypoint. Public
1186/// components use the generated builder syntax, while the original function
1187/// body runs inside Tessera's build/replay context.
1188///
1189/// # Parameters
1190///
1191/// * Attribute arguments select the Tessera crate path. Use `#[tessera]` for
1192///   normal external authoring, or `#[tessera(crate)]` inside Tessera crates.
1193///
1194/// # When NOT to Use
1195///
1196/// * For functions that should not participate in the component tree.
1197///
1198/// # See Also
1199///
1200/// * [`#[shard]`](crate::shard) for navigation‑aware components with injectable
1201///   shard state.
1202#[proc_macro_attribute]
1203pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
1204    let crate_path: syn::Path = match parse_crate_path(attr) {
1205        Ok(path) => path,
1206        Err(err) => return err.to_compile_error().into(),
1207    };
1208
1209    // Parse the input function that will be transformed into a component
1210    let mut input_fn = parse_macro_input!(item as ItemFn);
1211    if let Some(conflicting_attr) = input_fn
1212        .attrs
1213        .iter()
1214        .find(|attr| attr.path().is_ident("shard"))
1215    {
1216        return syn::Error::new_spanned(
1217            conflicting_attr,
1218            "#[tessera] and #[shard] cannot be combined on the same function; use #[shard] only",
1219        )
1220        .to_compile_error()
1221        .into();
1222    }
1223
1224    if input_fn.sig.constness.is_some()
1225        || input_fn.sig.asyncness.is_some()
1226        || input_fn.sig.unsafety.is_some()
1227        || input_fn.sig.abi.is_some()
1228        || input_fn.sig.variadic.is_some()
1229    {
1230        return syn::Error::new_spanned(
1231            &input_fn.sig,
1232            "#[tessera] components must be plain free functions",
1233        )
1234        .to_compile_error()
1235        .into();
1236    }
1237    if !matches!(input_fn.sig.output, syn::ReturnType::Default) {
1238        return syn::Error::new_spanned(
1239            &input_fn.sig.output,
1240            "#[tessera] components must not return a value",
1241        )
1242        .to_compile_error()
1243        .into();
1244    }
1245
1246    let fn_name = &input_fn.sig.ident;
1247    let fn_vis = &input_fn.vis;
1248    let fn_attrs = &input_fn.attrs;
1249    if let Err(err) = validate_component_generics(&input_fn.sig.generics) {
1250        return err.to_compile_error().into();
1251    }
1252    let prop_signature = match strict_prop_signature(&input_fn.sig) {
1253        Ok(v) => v,
1254        Err(err) => return err.to_compile_error().into(),
1255    };
1256
1257    // Generate a stable hash seed based on function name in order to avoid ID
1258    // collisions
1259    let mut hasher = DefaultHasher::new();
1260    input_fn.sig.ident.to_string().hash(&mut hasher);
1261    let seed = hasher.finish();
1262
1263    // Modify the function body to instrument control flow with GroupGuard
1264    let mut instrumenter = ControlFlowInstrumenter::new(seed);
1265    instrumenter.visit_block_mut(&mut input_fn.block);
1266    let fn_block = &input_fn.block;
1267    let generics = &input_fn.sig.generics;
1268    let mut item_generics = generics.clone();
1269    item_generics.where_clause = None;
1270    let where_clause = &generics.where_clause;
1271    let (impl_generics, ty_generics, _) = generics.split_for_impl();
1272    let component_turbofish = component_turbofish_tokens(generics);
1273
1274    // Prepare token fragments using helpers to keep function small and readable
1275    let register_tokens = register_node_tokens(&crate_path, fn_name);
1276    let expanded = match &prop_signature {
1277        ComponentPropSignature::Unit => {
1278            let props_ident = hidden_props_ident(fn_name);
1279            let fn_sig = &input_fn.sig;
1280            let unit_runner_ident = format_ident!("__tessera_{}_unit_runner", fn_name);
1281            let (const_marker_defs, marker_field_def) = generic_marker_tokens(fn_name, generics);
1282            let has_generic_marker = marker_field_def.is_some();
1283            let prop_type: syn::Type = syn::parse_quote!(#props_ident #ty_generics);
1284            let component_type_id_tokens = component_type_id_tokens(fn_name, &prop_type);
1285            let props_clone_default_impls =
1286                generate_props_clone_default_impls(&props_ident, generics, &[], has_generic_marker);
1287            let prop_impl_tokens =
1288                generate_prop_like_impls(&props_ident, generics, &[], &crate_path);
1289            let prop_assert_tokens = prop_assert_tokens(&crate_path, &prop_type);
1290            let replay_tokens = replay_register_tokens(
1291                &crate_path,
1292                &unit_runner_ident,
1293                &prop_type,
1294                quote!(&__tessera_unit_props),
1295            );
1296            let marker_field_def = marker_field_def
1297                .map(|field| quote!(#field,))
1298                .unwrap_or_default();
1299
1300            quote! {
1301                #(#const_marker_defs)*
1302
1303                struct #props_ident #item_generics #where_clause {
1304                    #marker_field_def
1305                }
1306
1307                #props_clone_default_impls
1308                #prop_impl_tokens
1309
1310                fn #unit_runner_ident #item_generics(_props: &#props_ident #ty_generics) #where_clause {
1311                    #fn_name #component_turbofish ();
1312                }
1313
1314                #(#fn_attrs)*
1315                #fn_vis #fn_sig {
1316                    let __tessera_component_type_id: u64 = #component_type_id_tokens;
1317                    let __tessera_phase_guard = {
1318                        use #crate_path::__private::{RuntimePhase, push_phase};
1319                        push_phase(RuntimePhase::Build)
1320                    };
1321                    let __tessera_fn_name: &str = stringify!(#fn_name);
1322                    let __tessera_node_id = #register_tokens;
1323
1324                    let _node_ctx_guard = {
1325                        use #crate_path::__private::push_current_node;
1326                        push_current_node(
1327                            __tessera_node_id,
1328                            __tessera_component_type_id,
1329                            __tessera_fn_name,
1330                        )
1331                    };
1332
1333                    let __tessera_instance_key: u64 = #crate_path::__private::current_instance_key();
1334                    let __tessera_instance_logic_id: u64 =
1335                        #crate_path::__private::current_instance_logic_id();
1336                    let _instance_ctx_guard = {
1337                        use #crate_path::__private::push_current_component_instance_key;
1338                        push_current_component_instance_key(__tessera_instance_key)
1339                    };
1340                    let _component_scope_guard = {
1341                        struct ComponentScopeGuard;
1342                        impl Drop for ComponentScopeGuard {
1343                            fn drop(&mut self) {
1344                                #crate_path::__private::finish_component_node();
1345                            }
1346                        }
1347                        ComponentScopeGuard
1348                    };
1349                    #crate_path::__private::set_current_node_identity(
1350                        __tessera_instance_key,
1351                        __tessera_instance_logic_id,
1352                    );
1353                    #prop_assert_tokens
1354                    #crate_path::__private::record_current_context_snapshot_for(__tessera_instance_key);
1355                    let __tessera_unit_props: #prop_type = ::core::default::Default::default();
1356                    #replay_tokens
1357                    if __tessera_component_reused {
1358                        return;
1359                    }
1360                    #fn_block
1361                }
1362            }
1363        }
1364        ComponentPropSignature::Params(fields) => {
1365            let props_ident = hidden_props_ident(fn_name);
1366            let builder_ident = builder_ident(fn_name);
1367            let impl_ident = hidden_component_impl_ident(fn_name);
1368            let (const_marker_defs, marker_field_def) = generic_marker_tokens(fn_name, generics);
1369            let has_generic_marker = marker_field_def.is_some();
1370            let prop_type: syn::Type = syn::parse_quote!(#props_ident #ty_generics);
1371            let component_type_id_tokens = component_type_id_tokens(fn_name, &prop_type);
1372            let field_idents: Vec<_> = fields.iter().map(|field| field.ident.clone()).collect();
1373            let required_fields: Vec<_> = fields
1374                .iter()
1375                .filter(|field| is_required_component_field(field))
1376                .collect();
1377            let constructor_params_and_assignments: Vec<_> = match required_fields
1378                .iter()
1379                .map(|field| generate_constructor_param_and_assignment(field, &crate_path))
1380                .collect::<syn::Result<Vec<_>>>()
1381            {
1382                Ok(entries) => entries,
1383                Err(err) => return err.to_compile_error().into(),
1384            };
1385            let constructor_params: Vec<_> = constructor_params_and_assignments
1386                .iter()
1387                .map(|(param, _)| param.clone())
1388                .collect();
1389            let constructor_assignments: Vec<_> = constructor_params_and_assignments
1390                .iter()
1391                .map(|(_, assignment)| assignment.clone())
1392                .collect();
1393            let field_defs: Vec<_> = fields
1394                .iter()
1395                .map(|field| {
1396                    let ident = &field.ident;
1397                    let ty = stored_field_ty(&field.ty);
1398                    quote!(#ident: #ty)
1399                })
1400                .collect();
1401            let field_resolutions: Vec<_> = fields
1402                .iter()
1403                .map(|field| {
1404                    let ident = &field.ident;
1405                    let ty = &field.ty;
1406                    if option_inner_type(ty).is_some() {
1407                        return quote! {
1408                            let #ident: #ty = __tessera_props.#ident.clone();
1409                        };
1410                    }
1411
1412                    let missing_prop_message = format!(
1413                        "missing required prop `{}` for component `{}`",
1414                        ident, fn_name
1415                    );
1416                    quote! {
1417                        let #ident: #ty = __tessera_props.#ident.clone().unwrap_or_else(|| {
1418                            panic!(#missing_prop_message)
1419                        });
1420                    }
1421                })
1422                .collect();
1423            let prop_impl_tokens =
1424                generate_prop_like_impls(&props_ident, generics, fields, &crate_path);
1425            let builder_methods = match generate_builder_methods(fields, &crate_path) {
1426                Ok(methods) => methods,
1427                Err(err) => return err.to_compile_error().into(),
1428            };
1429            let prop_assert_tokens = prop_assert_tokens(&crate_path, &prop_type);
1430            let replay_tokens = replay_register_tokens(
1431                &crate_path,
1432                &impl_ident,
1433                &prop_type,
1434                quote!(__tessera_props),
1435            );
1436            let marker_field_def = marker_field_def
1437                .map(|field| quote!(#field,))
1438                .unwrap_or_default();
1439            let props_clone_default_impls = generate_props_clone_default_impls(
1440                &props_ident,
1441                generics,
1442                &field_idents,
1443                has_generic_marker,
1444            );
1445
1446            quote! {
1447                #(#const_marker_defs)*
1448
1449                struct #props_ident #item_generics #where_clause {
1450                    #(#field_defs,)*
1451                    #marker_field_def
1452                }
1453
1454                #props_clone_default_impls
1455                #prop_impl_tokens
1456
1457                #[doc = concat!("Builder returned by [`", stringify!(#fn_name), "`].")]
1458                #fn_vis struct #builder_ident #item_generics #where_clause {
1459                    props: #props_ident #ty_generics,
1460                }
1461
1462                impl #impl_generics ::core::default::Default for #builder_ident #ty_generics #where_clause {
1463                    fn default() -> Self {
1464                        Self {
1465                            props: ::core::default::Default::default(),
1466                        }
1467                    }
1468                }
1469
1470                impl #impl_generics #builder_ident #ty_generics #where_clause {
1471                    #(#builder_methods)*
1472                }
1473
1474                impl #impl_generics Drop for #builder_ident #ty_generics #where_clause {
1475                    fn drop(&mut self) {
1476                        #impl_ident(&self.props);
1477                    }
1478                }
1479
1480                #(#fn_attrs)*
1481                #fn_vis fn #fn_name #item_generics(
1482                    #(#constructor_params),*
1483                ) -> #builder_ident #ty_generics #where_clause {
1484                    let mut __tessera_builder = #builder_ident::default();
1485                    #(#constructor_assignments)*
1486                    __tessera_builder
1487                }
1488
1489                fn #impl_ident #item_generics(__tessera_props: &#props_ident #ty_generics) #where_clause {
1490                    let __tessera_component_type_id: u64 = #component_type_id_tokens;
1491                    let __tessera_phase_guard = {
1492                        use #crate_path::__private::{RuntimePhase, push_phase};
1493                        push_phase(RuntimePhase::Build)
1494                    };
1495                    let __tessera_fn_name: &str = stringify!(#fn_name);
1496                    let __tessera_node_id = #register_tokens;
1497
1498                    let _node_ctx_guard = {
1499                        use #crate_path::__private::push_current_node;
1500                        push_current_node(
1501                            __tessera_node_id,
1502                            __tessera_component_type_id,
1503                            __tessera_fn_name,
1504                        )
1505                    };
1506
1507                    let __tessera_instance_key: u64 = #crate_path::__private::current_instance_key();
1508                    let __tessera_instance_logic_id: u64 =
1509                        #crate_path::__private::current_instance_logic_id();
1510                    let _instance_ctx_guard = {
1511                        use #crate_path::__private::push_current_component_instance_key;
1512                        push_current_component_instance_key(__tessera_instance_key)
1513                    };
1514                    let _component_scope_guard = {
1515                        struct ComponentScopeGuard;
1516                        impl Drop for ComponentScopeGuard {
1517                            fn drop(&mut self) {
1518                                #crate_path::__private::finish_component_node();
1519                            }
1520                        }
1521                        ComponentScopeGuard
1522                    };
1523                    #crate_path::__private::set_current_node_identity(
1524                        __tessera_instance_key,
1525                        __tessera_instance_logic_id,
1526                    );
1527                    #prop_assert_tokens
1528                    #crate_path::__private::record_current_context_snapshot_for(__tessera_instance_key);
1529                    #replay_tokens
1530                    if __tessera_component_reused {
1531                        return;
1532                    }
1533                    #(#field_resolutions)*
1534                    #fn_block
1535                }
1536            }
1537        }
1538    };
1539
1540    TokenStream::from(expanded)
1541}
1542
1543/// Generates platform-specific entry points from a shared `run` function.
1544///
1545/// # Usage
1546///
1547/// Annotate a public zero-argument function that returns
1548/// `tessera_ui::EntryPoint`.
1549#[proc_macro_attribute]
1550pub fn entry(attr: TokenStream, item: TokenStream) -> TokenStream {
1551    let crate_path: syn::Path = match parse_crate_path(attr) {
1552        Ok(path) => path,
1553        Err(err) => return err.to_compile_error().into(),
1554    };
1555    let mut input_fn = parse_macro_input!(item as ItemFn);
1556
1557    if !input_fn.sig.inputs.is_empty() {
1558        return syn::Error::new_spanned(
1559            &input_fn.sig.inputs,
1560            "entry functions must not accept arguments",
1561        )
1562        .to_compile_error()
1563        .into();
1564    }
1565
1566    input_fn.attrs.retain(|attr| !attr.path().is_ident("entry"));
1567    let fn_name = &input_fn.sig.ident;
1568
1569    let expanded = quote! {
1570        #input_fn
1571
1572        #[cfg(target_os = "android")]
1573        #[unsafe(no_mangle)]
1574        fn android_main(android_app: #crate_path::winit::platform::android::activity::AndroidApp) {
1575            if let Err(err) = #fn_name().run_android(android_app) {
1576                eprintln!("App failed to run: {err}");
1577            }
1578        }
1579    };
1580
1581    expanded.into()
1582}
1583
1584/// Transforms a function into a *shard component* that can be navigated to via
1585/// the routing system and (optionally) provided with a lazily‑initialized
1586/// per‑shard state.
1587///
1588/// # Features
1589///
1590/// * Generates a `StructNameDestination` (UpperCamelCase + `Destination`)
1591///   implementing `tessera_shard::router::RouterDestination`
1592/// * Optional state injection via `#[shard(state = T)]`, where `T`:
1593///   - Must implement `Default + Send + Sync + 'static`
1594///   - Is constructed (or reused) and exposed as local variable `state` with
1595///     type `tessera_shard::ShardState<T>`
1596/// * Produces a stable shard ID: `module_path!()::function_name`
1597///
1598/// # Lifecycle
1599///
1600/// Controlled by the `lifecycle` shard attribute argument.
1601/// * Default: `Shard` – state is removed when the destination is `pop()`‑ed
1602/// * Override: `#[shard(lifecycle = scope)]` to persist for the lifetime of the
1603///   current router controller hosted by `shard_home`
1604///
1605/// Route-scoped state is removed on route pop/clear. Scope-scoped state is
1606/// removed when the hosting `shard_home` is dropped.
1607///
1608/// # Parameter Transformation
1609///
1610/// * Function parameters are treated as explicit destination props.
1611/// * When `state = T` is configured, shard state is injected as local variable
1612///   `state` and does not appear in the function signature.
1613///
1614/// # Generated Destination (Conceptual)
1615///
1616/// ```rust,ignore
1617/// struct ProfilePageDestination { /* non-state params as public fields */ }
1618/// impl RouterDestination for ProfilePageDestination {
1619///     fn exec_component(&self) { profile_page(/* fields */); }
1620///     fn destination_id() -> &'static str { "<module>::profile_page" }
1621/// }
1622/// ```
1623///
1624/// # Limitations
1625///
1626/// * Do not manually implement `RouterDestination` for these pages; rely on
1627///   generation
1628///
1629/// # See Also
1630///
1631/// * Routing helper: `tessera_shard::shard_home`
1632/// * Router controller internals: `tessera_shard::RouterController`
1633///
1634/// # Errors
1635///
1636/// Emits a compile error if unsupported `lifecycle` is provided, or if
1637/// `lifecycle` is used without `state`.
1638#[cfg(feature = "shard")]
1639#[proc_macro_attribute]
1640pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
1641    use heck::ToUpperCamelCase;
1642    let shard_args = if attr.is_empty() {
1643        ShardMacroArgs::default()
1644    } else {
1645        match syn::parse::<ShardMacroArgs>(attr) {
1646            Ok(value) => value,
1647            Err(err) => return err.to_compile_error().into(),
1648        }
1649    };
1650
1651    let ui_crate_path: syn::Path = shard_args
1652        .crate_path
1653        .unwrap_or_else(|| syn::parse_quote!(::tessera_ui));
1654    let shard_crate_path: syn::Path = shard_args
1655        .shard_crate_path
1656        .unwrap_or_else(|| syn::parse_quote!(::tessera_shard));
1657
1658    let func = parse_macro_input!(input as ItemFn);
1659    if let Some(conflicting_attr) = func
1660        .attrs
1661        .iter()
1662        .find(|attr| attr.path().is_ident("tessera"))
1663    {
1664        return syn::Error::new_spanned(
1665            conflicting_attr,
1666            "#[shard] already defines a component boundary; do not combine #[shard] with #[tessera]",
1667        )
1668        .to_compile_error()
1669        .into();
1670    }
1671
1672    let shard_params = match parse_shard_params(&func.sig) {
1673        Ok(params) => params,
1674        Err(err) => return err.to_compile_error().into(),
1675    };
1676    let router_params: Vec<_> = shard_params
1677        .iter()
1678        .filter(|param| param.is_router)
1679        .collect();
1680    if router_params.len() > 1 {
1681        return syn::Error::new_spanned(
1682            &func.sig.inputs,
1683            "#[shard] supports at most one `#[router]` parameter",
1684        )
1685        .to_compile_error()
1686        .into();
1687    }
1688    if let Some(router_param) = router_params.first()
1689        && !is_state_router_controller_type(&router_param.ty)
1690    {
1691        return syn::Error::new_spanned(
1692            &router_param.ty,
1693            "`#[router]` parameters must have type `State<RouterController>`",
1694        )
1695        .to_compile_error()
1696        .into();
1697    }
1698
1699    let state_type = shard_args.state_type;
1700    let state_lifecycle_tokens = match shard_args.lifecycle {
1701        Some(lifecycle) => {
1702            let lifecycle_name = lifecycle.to_string().to_lowercase();
1703            if state_type.is_none() {
1704                return syn::Error::new_spanned(
1705                    lifecycle,
1706                    "`lifecycle` requires `state` in #[shard(...)]",
1707                )
1708                .to_compile_error()
1709                .into();
1710            }
1711            if lifecycle_name == "scope" {
1712                quote! { #shard_crate_path::router::ShardStateLifeCycle::Scope }
1713            } else if lifecycle_name == "shard" {
1714                quote! { #shard_crate_path::router::ShardStateLifeCycle::Shard }
1715            } else {
1716                return syn::Error::new_spanned(
1717                    lifecycle,
1718                    "unsupported `lifecycle` in #[shard(...)]: expected `scope` or `shard`",
1719                )
1720                .to_compile_error()
1721                .into();
1722            }
1723        }
1724        None => quote! { #shard_crate_path::router::ShardStateLifeCycle::Shard },
1725    };
1726
1727    let func_body = func.block;
1728    let func_name_str = func.sig.ident.to_string();
1729
1730    let func_attrs = &func.attrs;
1731    let func_vis = &func.vis;
1732    let router_controller_resolution = quote! {
1733        #shard_crate_path::__private::current_router_controller!()
1734    };
1735
1736    let router_binding =
1737        router_params
1738            .first()
1739            .map_or_else(proc_macro2::TokenStream::new, |param| {
1740                let router_ident = &param.ident;
1741                quote! {
1742                    let #router_ident = #router_ident
1743                        .unwrap_or_else(|| #router_controller_resolution);
1744                }
1745            });
1746    let mut func_sig_modified = func.sig.clone();
1747    for input in &mut func_sig_modified.inputs {
1748        let FnArg::Typed(pat_type) = input else {
1749            continue;
1750        };
1751        let is_router = pat_type
1752            .attrs
1753            .iter()
1754            .any(|attr| attr.path().is_ident("router"));
1755        if !is_router {
1756            continue;
1757        }
1758        let original_ty = (*pat_type.ty).clone();
1759        *pat_type.ty = parse_quote!(::core::option::Option<#original_ty>);
1760        pat_type
1761            .attrs
1762            .retain(|attr| !attr.path().is_ident("router"));
1763        pat_type.attrs.push(syn::parse_quote!(#[prop(skip_setter)]));
1764    }
1765
1766    // Generate struct name for the new RouterDestination
1767    let func_name = func.sig.ident.clone();
1768    let struct_name = syn::Ident::new(
1769        &format!("{}Destination", func_name_str.to_upper_camel_case()),
1770        func_name.span(),
1771    );
1772    // Generate fields for the new struct that will implement `RouterDestination`
1773    let dest_fields = shard_params
1774        .iter()
1775        .filter(|param| !param.is_router)
1776        .map(|param| {
1777            let ident = &param.ident;
1778            let ty = &param.ty;
1779            quote! { pub #ident: #ty }
1780        });
1781
1782    // Keep all explicit function parameters as destination props.
1783    let expanded = {
1784        let destination_builder_setters: Vec<_> = shard_params
1785            .iter()
1786            .filter(|param| !param.is_router)
1787            .map(|param| {
1788                let ident = &param.ident;
1789                if option_inner_type(&param.ty).is_some() {
1790                    quote! {
1791                        if let Some(value) = self.#ident.clone() {
1792                            __tessera_builder = __tessera_builder.#ident(value);
1793                        }
1794                    }
1795                } else {
1796                    quote! {
1797                        __tessera_builder = __tessera_builder.#ident(self.#ident.clone());
1798                    }
1799                }
1800            })
1801            .collect();
1802
1803        if let Some(state_type) = state_type {
1804            quote! {
1805                #[derive(Clone, PartialEq)]
1806                #func_vis struct #struct_name {
1807                    #(#dest_fields),*
1808                }
1809
1810                impl #shard_crate_path::router::RouterDestination for #struct_name {
1811                    fn exec_component(&self) {
1812                        let mut __tessera_builder = #func_name();
1813                        #(#destination_builder_setters)*
1814                        drop(__tessera_builder);
1815                    }
1816
1817                    fn destination_id() -> &'static str {
1818                        concat!(module_path!(), "::", #func_name_str)
1819                    }
1820                }
1821
1822                #(#func_attrs)*
1823                #[#ui_crate_path::tessera(#ui_crate_path)]
1824                #func_vis #func_sig_modified {
1825                    const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
1826                    let __router_controller = #router_controller_resolution;
1827                    #router_binding
1828                    #shard_crate_path::__private::with_current_router_shard_state::<#state_type, _, _>(
1829                        SHARD_ID,
1830                        #state_lifecycle_tokens,
1831                        __router_controller,
1832                        |state| {
1833                            #func_body
1834                        },
1835                    )
1836                }
1837            }
1838        } else {
1839            quote! {
1840                #[derive(Clone, PartialEq)]
1841                #func_vis struct #struct_name {
1842                    #(#dest_fields),*
1843                }
1844
1845                impl #shard_crate_path::router::RouterDestination for #struct_name {
1846                    fn exec_component(&self) {
1847                        let mut __tessera_builder = #func_name();
1848                        #(#destination_builder_setters)*
1849                        drop(__tessera_builder);
1850                    }
1851
1852                    fn destination_id() -> &'static str {
1853                        concat!(module_path!(), "::", #func_name_str)
1854                    }
1855                }
1856
1857                #(#func_attrs)*
1858                #[#ui_crate_path::tessera(#ui_crate_path)]
1859                #func_vis #func_sig_modified {
1860                    #router_binding
1861                    #func_body
1862                }
1863            }
1864        }
1865    };
1866
1867    TokenStream::from(expanded)
1868}