1#![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
23fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Result<syn::Path> {
25 if attr.is_empty() {
26 Ok(syn::parse_quote!(::tessera_ui))
28 } else {
29 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
360fn 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
370enum 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
426fn 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
442fn 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
813fn 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 = ¶m.ident;
870 quote!(#ident)
871 }
872 GenericParam::Const(param) => {
873 let ident = ¶m.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 = ¶m.ident;
900 marker_parts.push(quote!(#ident));
901 }
902 GenericParam::Const(param) => {
903 let ident = ¶m.ident;
904 let ty = ¶m.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
1042fn 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: usize,
1063 seed: u64,
1065}
1066
1067impl ControlFlowInstrumenter {
1068 fn new(seed: u64) -> Self {
1069 Self { counter: 0, seed }
1070 }
1071
1072 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 fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
1087 self.visit_expr_mut(expr);
1090 let group_id = self.next_group_id();
1091 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 fn wrap_block_in_group(&mut self, block: &mut Block) {
1104 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 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#[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 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 let mut hasher = DefaultHasher::new();
1260 input_fn.sig.ident.to_string().hash(&mut hasher);
1261 let seed = hasher.finish();
1262
1263 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 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#[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#[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 = ¶m.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 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 let dest_fields = shard_params
1774 .iter()
1775 .filter(|param| !param.is_router)
1776 .map(|param| {
1777 let ident = ¶m.ident;
1778 let ty = ¶m.ty;
1779 quote! { pub #ident: #ty }
1780 });
1781
1782 let expanded = {
1784 let destination_builder_setters: Vec<_> = shard_params
1785 .iter()
1786 .filter(|param| !param.is_router)
1787 .map(|param| {
1788 let ident = ¶m.ident;
1789 if option_inner_type(¶m.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}