1use std::hash::{DefaultHasher, Hash, Hasher};
8
9use proc_macro::TokenStream;
10use quote::{format_ident, quote};
11use syn::{
12 Block, Expr, FnArg, GenericArgument, Ident, ItemFn, Pat, Path, PathArguments, Token, Type,
13 parse::Parse, parse_macro_input, parse_quote, visit_mut::VisitMut,
14};
15
16fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Result<syn::Path> {
18 if attr.is_empty() {
19 Ok(syn::parse_quote!(::tessera_ui))
21 } else {
22 let tokens: proc_macro2::TokenStream = attr.clone().into();
24 syn::parse(attr).map_err(|_| {
25 syn::Error::new_spanned(tokens, "expected a crate path like `crate` or `tessera_ui`")
26 })
27 }
28}
29
30#[derive(Clone, Copy, Debug, Default)]
31struct SetterAttrConfig {
32 skip: bool,
33 into: bool,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37enum PropHelperKind {
38 Callback,
39 CallbackWith,
40 RenderSlot,
41 RenderSlotWith,
42}
43
44#[derive(Clone, Copy, Debug, Default)]
45struct PropFieldAttrConfig {
46 skip_eq: bool,
47}
48
49fn parse_setter_attr(attrs: &[syn::Attribute]) -> syn::Result<SetterAttrConfig> {
50 let mut config = SetterAttrConfig::default();
51 for attr in attrs {
52 if !attr.path().is_ident("prop") {
53 continue;
54 }
55
56 match &attr.meta {
57 syn::Meta::Path(_) => {}
58 syn::Meta::List(_) => {
59 attr.parse_nested_meta(|meta| {
60 if meta.path.is_ident("skip") {
61 return Err(meta.error("unsupported setter option `skip`"));
62 }
63 if meta.path.is_ident("skip_setter") {
64 config.skip = true;
65 return Ok(());
66 }
67 if meta.path.is_ident("into") {
68 config.into = true;
69 return Ok(());
70 }
71 Ok(())
72 })?;
73 }
74 syn::Meta::NameValue(_) => {
75 return Err(syn::Error::new_spanned(
76 attr,
77 "unsupported #[prop = ...] form; expected #[prop(...)]",
78 ));
79 }
80 }
81 }
82 Ok(config)
83}
84
85fn parse_prop_field_attr(attrs: &[syn::Attribute]) -> syn::Result<PropFieldAttrConfig> {
86 let mut config = PropFieldAttrConfig::default();
87 for attr in attrs {
88 if !attr.path().is_ident("prop") {
89 continue;
90 }
91 attr.parse_nested_meta(|meta| {
92 if meta.path.is_ident("skip_eq") {
93 if config.skip_eq {
94 return Err(meta.error("duplicate `skip_eq` in #[prop(...)]"));
95 }
96 config.skip_eq = true;
97 return Ok(());
98 }
99 if meta.path.is_ident("skip") {
100 return Err(meta.error("unsupported field option `skip`"));
101 }
102 if meta.path.is_ident("skip_setter") || meta.path.is_ident("into") {
103 return Ok(());
104 }
105 if meta.path.is_ident("crate_path") {
106 return Err(meta.error(
107 "container option in field #[prop(...)]; `crate_path` is only valid on structs",
108 ));
109 }
110
111 Err(meta.error(
112 "unsupported field #[prop(...)] option; expected setter options (`skip_setter`/`into`) or compare option (`skip_eq`)",
113 ))
114 })?;
115 }
116 Ok(config)
117}
118
119fn option_inner_type(ty: &Type) -> Option<Type> {
120 let Type::Path(type_path) = ty else {
121 return None;
122 };
123 let segment = type_path.path.segments.last()?;
124 if segment.ident != "Option" {
125 return None;
126 }
127 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
128 return None;
129 };
130 arguments.args.iter().find_map(|arg| match arg {
131 GenericArgument::Type(inner) => Some(inner.clone()),
132 _ => None,
133 })
134}
135
136fn parse_functor_signature(ty: &Type, type_name: &str) -> Option<(Type, Type)> {
137 let Type::Path(type_path) = ty else {
138 return None;
139 };
140 let segment = type_path.path.segments.last()?;
141 if segment.ident != type_name {
142 return None;
143 }
144 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
145 return None;
146 };
147 let mut types = arguments.args.iter().filter_map(|arg| match arg {
148 GenericArgument::Type(ty) => Some(ty.clone()),
149 _ => None,
150 });
151 let arg = types.next()?;
152 let ret = types.next().unwrap_or_else(|| parse_quote!(()));
153 Some((arg, ret))
154}
155
156fn is_unit_type(ty: &Type) -> bool {
157 matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
158}
159
160fn type_last_segment_ident(ty: &Type) -> Option<&Ident> {
161 let Type::Path(type_path) = ty else {
162 return None;
163 };
164 type_path.path.segments.last().map(|segment| &segment.ident)
165}
166
167fn infer_prop_helper_kind(ty: &Type) -> Option<PropHelperKind> {
168 let value_ty = option_inner_type(ty).unwrap_or_else(|| ty.clone());
169 let ident = type_last_segment_ident(&value_ty)?;
170 if ident == "Callback" {
171 return Some(PropHelperKind::Callback);
172 }
173 if ident == "CallbackWith" {
174 return Some(PropHelperKind::CallbackWith);
175 }
176 if ident == "RenderSlot" {
177 return Some(PropHelperKind::RenderSlot);
178 }
179 if ident == "RenderSlotWith" {
180 return Some(PropHelperKind::RenderSlotWith);
181 }
182 None
183}
184
185fn is_arc_type(ty: &Type) -> bool {
186 type_last_segment_ident(ty).is_some_and(|ident| ident == "Arc")
187}
188
189fn is_rc_type(ty: &Type) -> bool {
190 type_last_segment_ident(ty).is_some_and(|ident| ident == "Rc")
191}
192
193fn field_compare_expr(field_ident: &Ident, ty: &Type) -> proc_macro2::TokenStream {
194 if is_arc_type(ty) {
195 return quote! { ::std::sync::Arc::ptr_eq(&self.#field_ident, &other.#field_ident) };
196 }
197 if is_rc_type(ty) {
198 return quote! { ::std::rc::Rc::ptr_eq(&self.#field_ident, &other.#field_ident) };
199 }
200 if let Some(inner_ty) = option_inner_type(ty) {
201 if is_arc_type(&inner_ty) {
202 return quote! {
203 match (&self.#field_ident, &other.#field_ident) {
204 (Some(lhs), Some(rhs)) => ::std::sync::Arc::ptr_eq(lhs, rhs),
205 (None, None) => true,
206 _ => false,
207 }
208 };
209 }
210 if is_rc_type(&inner_ty) {
211 return quote! {
212 match (&self.#field_ident, &other.#field_ident) {
213 (Some(lhs), Some(rhs)) => ::std::rc::Rc::ptr_eq(lhs, rhs),
214 (None, None) => true,
215 _ => false,
216 }
217 };
218 }
219 }
220 quote! { self.#field_ident == other.#field_ident }
221}
222
223#[cfg(feature = "shard")]
224#[derive(Default)]
225struct ShardMacroArgs {
226 crate_path: Option<Path>,
227 state_type: Option<Type>,
228 lifecycle: Option<Ident>,
229}
230
231#[cfg(feature = "shard")]
232struct ShardParam {
233 ident: Ident,
234 ty: Type,
235 is_router: bool,
236}
237
238#[cfg(feature = "shard")]
239fn parse_shard_params(sig: &syn::Signature) -> syn::Result<Vec<ShardParam>> {
240 let mut params = Vec::with_capacity(sig.inputs.len());
241 for arg in &sig.inputs {
242 match arg {
243 FnArg::Receiver(receiver) => {
244 return Err(syn::Error::new_spanned(
245 receiver,
246 "#[shard] does not support methods; use a free function",
247 ));
248 }
249 FnArg::Typed(pat_type) => {
250 let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
251 return Err(syn::Error::new_spanned(
252 &pat_type.pat,
253 "#[shard] parameters must be simple named bindings like `foo: T`",
254 ));
255 };
256 let is_router = pat_type
257 .attrs
258 .iter()
259 .any(|attr| attr.path().is_ident("router"));
260 params.push(ShardParam {
261 ident: pat_ident.ident.clone(),
262 ty: (*pat_type.ty).clone(),
263 is_router,
264 });
265 }
266 }
267 }
268 Ok(params)
269}
270
271#[cfg(feature = "shard")]
272fn is_state_router_controller_type(ty: &Type) -> bool {
273 let Type::Path(type_path) = ty else {
274 return false;
275 };
276 let Some(state_segment) = type_path.path.segments.last() else {
277 return false;
278 };
279 if state_segment.ident != "State" {
280 return false;
281 }
282 let syn::PathArguments::AngleBracketed(args) = &state_segment.arguments else {
283 return false;
284 };
285 let Some(syn::GenericArgument::Type(Type::Path(controller_path))) = args.args.first() else {
286 return false;
287 };
288 controller_path
289 .path
290 .segments
291 .last()
292 .is_some_and(|segment| segment.ident == "RouterController")
293}
294
295#[cfg(feature = "shard")]
296impl Parse for ShardMacroArgs {
297 fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
298 let mut args = ShardMacroArgs::default();
299 while !input.is_empty() {
300 let key: Ident = input.parse()?;
301 input.parse::<Token![=]>()?;
302 match key.to_string().as_str() {
303 "crate_path" => {
304 if args.crate_path.is_some() {
305 return Err(syn::Error::new(
306 key.span(),
307 "duplicate `crate_path` argument",
308 ));
309 }
310 args.crate_path = Some(input.parse::<Path>()?);
311 }
312 "state" => {
313 if args.state_type.is_some() {
314 return Err(syn::Error::new(key.span(), "duplicate `state` argument"));
315 }
316 args.state_type = Some(input.parse::<Type>()?);
317 }
318 "lifecycle" => {
319 if args.lifecycle.is_some() {
320 return Err(syn::Error::new(
321 key.span(),
322 "duplicate `lifecycle` argument",
323 ));
324 }
325 args.lifecycle = Some(input.parse::<Ident>()?);
326 }
327 _ => {
328 return Err(syn::Error::new(
329 key.span(),
330 "unsupported #[shard(...)] argument; expected `state`, `lifecycle`, or `crate_path`",
331 ));
332 }
333 }
334
335 if input.peek(Token![,]) {
336 input.parse::<Token![,]>()?;
337 }
338 }
339 Ok(args)
340 }
341}
342
343fn register_node_tokens(crate_path: &syn::Path, fn_name: &syn::Ident) -> proc_macro2::TokenStream {
345 quote! {
346 #crate_path::__private::register_component_node(
347 stringify!(#fn_name),
348 __tessera_component_type_id,
349 )
350 }
351}
352
353enum ComponentPropSignature {
356 Unit,
357 Params(Vec<PropFieldSpec>),
358}
359
360fn strict_prop_signature(sig: &syn::Signature) -> Result<ComponentPropSignature, syn::Error> {
361 if sig.inputs.is_empty() {
362 return Ok(ComponentPropSignature::Unit);
363 }
364
365 let mut fields = Vec::new();
366 for arg in &sig.inputs {
367 let FnArg::Typed(arg) = arg else {
368 return Err(syn::Error::new_spanned(
369 arg,
370 "#[tessera] methods are not supported; use free functions with named parameters",
371 ));
372 };
373
374 let Pat::Ident(pat_ident) = arg.pat.as_ref() else {
375 return Err(syn::Error::new_spanned(
376 &arg.pat,
377 "component parameters must be named identifiers",
378 ));
379 };
380
381 if pat_ident.by_ref.is_some() || pat_ident.mutability.is_some() {
382 return Err(syn::Error::new_spanned(
383 pat_ident,
384 "component parameters must be plain named bindings like `foo: T`",
385 ));
386 }
387
388 if matches!(arg.ty.as_ref(), Type::Reference(_)) {
389 return Err(syn::Error::new_spanned(
390 &arg.ty,
391 "component parameters must be owned types; borrowed parameter types are not supported",
392 ));
393 }
394
395 let field_setter_attr = parse_setter_attr(&arg.attrs)?;
396 let prop_attr = parse_prop_field_attr(&arg.attrs)?;
397 let default_expr = parse_component_default_attr(&arg.attrs)?;
398 fields.push(PropFieldSpec {
399 ident: pat_ident.ident.clone(),
400 ty: (*arg.ty).clone(),
401 setter: field_setter_attr,
402 helper: infer_prop_helper_kind(arg.ty.as_ref()),
403 skip_eq: prop_attr.skip_eq,
404 default_expr,
405 });
406 }
407
408 Ok(ComponentPropSignature::Params(fields))
409}
410
411fn replay_register_tokens(
413 crate_path: &syn::Path,
414 runner_name: &syn::Ident,
415 prop_type: &syn::Type,
416 props_expr: proc_macro2::TokenStream,
417) -> proc_macro2::TokenStream {
418 quote! {
419 let __tessera_component_reused = {
420 let __tessera_runner =
421 #crate_path::__private::make_component_runner::<#prop_type>(#runner_name);
422 #crate_path::__private::set_current_component_replay(__tessera_runner, #props_expr)
423 };
424 }
425}
426
427fn prop_assert_tokens(crate_path: &syn::Path, prop_type: &syn::Type) -> proc_macro2::TokenStream {
429 quote! {
430 {
431 fn __tessera_assert_prop<T: #crate_path::__private::Prop>() {}
432 __tessera_assert_prop::<#prop_type>();
433 }
434 }
435}
436
437struct PropFieldSpec {
438 ident: Ident,
439 ty: Type,
440 setter: SetterAttrConfig,
441 helper: Option<PropHelperKind>,
442 skip_eq: bool,
443 default_expr: Option<Expr>,
444}
445
446fn parse_component_default_attr(attrs: &[syn::Attribute]) -> syn::Result<Option<Expr>> {
447 let mut default_expr = None;
448 for attr in attrs {
449 if !attr.path().is_ident("default") {
450 continue;
451 }
452
453 if default_expr.is_some() {
454 return Err(syn::Error::new_spanned(
455 attr,
456 "duplicate #[default(...)] on component parameter",
457 ));
458 }
459
460 default_expr = Some(attr.parse_args::<Expr>()?);
461 }
462
463 Ok(default_expr)
464}
465
466fn generate_default_setter_method_for_path(
467 field: &PropFieldSpec,
468 field_path: &proc_macro2::TokenStream,
469 set_flag_path: Option<&proc_macro2::TokenStream>,
470) -> syn::Result<Option<proc_macro2::TokenStream>> {
471 if field.setter.skip {
472 return Ok(None);
473 }
474
475 let ident = &field.ident;
476 let method_doc = format!("Set `{ident}`.");
477 let field_ty = &field.ty;
478 let set_flag_stmt = set_flag_path.map(|path| quote!(self.#path = true;));
479 if let Some(inner_ty) = option_inner_type(field_ty) {
480 let method = if field.setter.into {
481 quote! {
482 #[doc = #method_doc]
483 pub fn #ident(mut self, #ident: impl Into<#inner_ty>) -> Self {
484 self.#field_path = Some(#ident.into());
485 #set_flag_stmt
486 self
487 }
488 }
489 } else {
490 quote! {
491 #[doc = #method_doc]
492 pub fn #ident(mut self, #ident: #inner_ty) -> Self {
493 self.#field_path = Some(#ident);
494 #set_flag_stmt
495 self
496 }
497 }
498 };
499 return Ok(Some(method));
500 }
501
502 let method = if field.setter.into {
503 quote! {
504 #[doc = #method_doc]
505 pub fn #ident(mut self, #ident: impl Into<#field_ty>) -> Self {
506 self.#field_path = #ident.into();
507 #set_flag_stmt
508 self
509 }
510 }
511 } else {
512 quote! {
513 #[doc = #method_doc]
514 pub fn #ident(mut self, #ident: #field_ty) -> Self {
515 self.#field_path = #ident;
516 #set_flag_stmt
517 self
518 }
519 }
520 };
521 Ok(Some(method))
522}
523
524fn generate_helper_setter_methods(
525 field: &PropFieldSpec,
526 helper: PropHelperKind,
527 crate_path: &Path,
528 field_path: &proc_macro2::TokenStream,
529 set_flag_path: Option<&proc_macro2::TokenStream>,
530) -> syn::Result<proc_macro2::TokenStream> {
531 let ident = &field.ident;
532 let shared_ident = format_ident!("{}_shared", ident);
533 let helper_doc = format!("Set `{ident}` from a closure.");
534 let shared_doc = format!("Set `{ident}` from a shared handle.");
535 let (value_ty, wrap_some) = match option_inner_type(&field.ty) {
536 Some(inner) => (inner, true),
537 None => (field.ty.clone(), false),
538 };
539 let set_flag_stmt = set_flag_path.map(|path| quote!(self.#path = true;));
540
541 match helper {
542 PropHelperKind::Callback => {
543 let matches_type = matches!(
544 &value_ty,
545 Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Callback")
546 );
547 if !matches_type {
548 return Err(syn::Error::new_spanned(
549 &field.ty,
550 "`#[prop(callback)]` requires `Callback` or `Option<Callback>`",
551 ));
552 }
553
554 let closure_assign = if wrap_some {
555 quote! { Some(#crate_path::Callback::new(#ident)) }
556 } else {
557 quote! { #crate_path::Callback::new(#ident) }
558 };
559 let shared_assign = if wrap_some {
560 quote! { Some(#ident.into()) }
561 } else {
562 quote! { #ident.into() }
563 };
564
565 Ok(quote! {
566 #[doc = #helper_doc]
567 pub fn #ident<F>(mut self, #ident: F) -> Self
568 where
569 F: Fn() + Send + Sync + 'static,
570 {
571 self.#field_path = #closure_assign;
572 #set_flag_stmt
573 self
574 }
575
576 #[doc = #shared_doc]
577 pub fn #shared_ident(mut self, #ident: impl Into<#crate_path::Callback>) -> Self {
578 self.#field_path = #shared_assign;
579 #set_flag_stmt
580 self
581 }
582 })
583 }
584 PropHelperKind::CallbackWith => {
585 let Some((arg_ty, ret_ty)) = parse_functor_signature(&value_ty, "CallbackWith") else {
586 return Err(syn::Error::new_spanned(
587 &field.ty,
588 "`#[prop(callback_with)]` requires `CallbackWith<T, R>` or `Option<CallbackWith<T, R>>`",
589 ));
590 };
591
592 let closure_assign = if is_unit_type(&arg_ty) {
593 if wrap_some {
594 quote! { Some(#crate_path::CallbackWith::new(move |()| #ident())) }
595 } else {
596 quote! { #crate_path::CallbackWith::new(move |()| #ident()) }
597 }
598 } else if wrap_some {
599 quote! { Some(#crate_path::CallbackWith::new(#ident)) }
600 } else {
601 quote! { #crate_path::CallbackWith::new(#ident) }
602 };
603 let shared_assign = if wrap_some {
604 quote! { Some(#ident.into()) }
605 } else {
606 quote! { #ident.into() }
607 };
608
609 let callback_bound = if is_unit_type(&arg_ty) {
610 quote! { F: Fn() -> #ret_ty + Send + Sync + 'static }
611 } else {
612 quote! { F: Fn(#arg_ty) -> #ret_ty + Send + Sync + 'static }
613 };
614
615 Ok(quote! {
616 #[doc = #helper_doc]
617 pub fn #ident<F>(mut self, #ident: F) -> Self
618 where
619 #callback_bound,
620 {
621 self.#field_path = #closure_assign;
622 #set_flag_stmt
623 self
624 }
625
626 #[doc = #shared_doc]
627 pub fn #shared_ident(
628 mut self,
629 #ident: impl Into<#crate_path::CallbackWith<#arg_ty, #ret_ty>>,
630 ) -> Self {
631 self.#field_path = #shared_assign;
632 #set_flag_stmt
633 self
634 }
635 })
636 }
637 PropHelperKind::RenderSlot => {
638 let matches_type = matches!(
639 &value_ty,
640 Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "RenderSlot")
641 );
642 if !matches_type {
643 return Err(syn::Error::new_spanned(
644 &field.ty,
645 "`#[prop(render_slot)]` requires `RenderSlot` or `Option<RenderSlot>`",
646 ));
647 }
648
649 let closure_assign = if wrap_some {
650 quote! { Some(#crate_path::RenderSlot::new(#ident)) }
651 } else {
652 quote! { #crate_path::RenderSlot::new(#ident) }
653 };
654 let shared_assign = if wrap_some {
655 quote! { Some(#ident.into()) }
656 } else {
657 quote! { #ident.into() }
658 };
659
660 Ok(quote! {
661 #[doc = #helper_doc]
662 pub fn #ident<F>(mut self, #ident: F) -> Self
663 where
664 F: Fn() + Send + Sync + 'static,
665 {
666 self.#field_path = #closure_assign;
667 #set_flag_stmt
668 self
669 }
670
671 #[doc = #shared_doc]
672 pub fn #shared_ident(mut self, #ident: impl Into<#crate_path::RenderSlot>) -> Self {
673 self.#field_path = #shared_assign;
674 #set_flag_stmt
675 self
676 }
677 })
678 }
679 PropHelperKind::RenderSlotWith => {
680 let Some((arg_ty, ret_ty)) = parse_functor_signature(&value_ty, "RenderSlotWith")
681 else {
682 return Err(syn::Error::new_spanned(
683 &field.ty,
684 "`#[prop(render_slot_with)]` requires `RenderSlotWith<T, R>` or `Option<RenderSlotWith<T, R>>`",
685 ));
686 };
687
688 let closure_assign = if is_unit_type(&arg_ty) {
689 if wrap_some {
690 quote! { Some(#crate_path::RenderSlotWith::new(move |()| #ident())) }
691 } else {
692 quote! { #crate_path::RenderSlotWith::new(move |()| #ident()) }
693 }
694 } else if wrap_some {
695 quote! { Some(#crate_path::RenderSlotWith::new(#ident)) }
696 } else {
697 quote! { #crate_path::RenderSlotWith::new(#ident) }
698 };
699 let shared_assign = if wrap_some {
700 quote! { Some(#ident.into()) }
701 } else {
702 quote! { #ident.into() }
703 };
704
705 let callback_bound = if is_unit_type(&arg_ty) {
706 quote! { F: Fn() -> #ret_ty + Send + Sync + 'static }
707 } else {
708 quote! { F: Fn(#arg_ty) -> #ret_ty + Send + Sync + 'static }
709 };
710
711 Ok(quote! {
712 #[doc = #helper_doc]
713 pub fn #ident<F>(mut self, #ident: F) -> Self
714 where
715 #callback_bound,
716 {
717 self.#field_path = #closure_assign;
718 #set_flag_stmt
719 self
720 }
721
722 #[doc = #shared_doc]
723 pub fn #shared_ident(
724 mut self,
725 #ident: impl Into<#crate_path::RenderSlotWith<#arg_ty, #ret_ty>>,
726 ) -> Self {
727 self.#field_path = #shared_assign;
728 #set_flag_stmt
729 self
730 }
731 })
732 }
733 }
734}
735
736fn pascal_case_ident(base: &Ident, suffix: &str) -> Ident {
738 let mut output = String::new();
739 for segment in base
740 .to_string()
741 .split('_')
742 .filter(|segment| !segment.is_empty())
743 {
744 let mut chars = segment.chars();
745 if let Some(first) = chars.next() {
746 output.extend(first.to_uppercase());
747 output.push_str(chars.as_str());
748 }
749 }
750 output.push_str(suffix);
751 format_ident!("{output}")
752}
753
754fn hidden_props_ident(fn_name: &Ident) -> Ident {
755 format_ident!("__Tessera{}Props", pascal_case_ident(fn_name, ""))
756}
757
758fn builder_ident(fn_name: &Ident) -> Ident {
759 pascal_case_ident(fn_name, "Builder")
760}
761
762fn hidden_component_impl_ident(fn_name: &Ident) -> Ident {
763 format_ident!("__tessera_{}_impl", fn_name)
764}
765
766fn generate_prop_like_impls(
767 type_name: &Ident,
768 fields: &[PropFieldSpec],
769 crate_path: &Path,
770) -> proc_macro2::TokenStream {
771 let compare_fields: Vec<_> = fields
772 .iter()
773 .filter(|field| !field.skip_eq)
774 .map(|field| field_compare_expr(&field.ident, &field.ty))
775 .collect();
776 let prop_eq_expr = if compare_fields.is_empty() {
777 quote! { true }
778 } else {
779 quote! { true #(&& #compare_fields)* }
780 };
781
782 quote! {
783 impl ::core::cmp::PartialEq for #type_name {
784 fn eq(&self, other: &Self) -> bool {
785 #prop_eq_expr
786 }
787 }
788
789 impl #crate_path::__private::Prop for #type_name {
790 fn prop_eq(&self, other: &Self) -> bool {
791 <Self as ::core::cmp::PartialEq>::eq(self, other)
792 }
793 }
794 }
795}
796
797fn generate_builder_methods(
798 fields: &[PropFieldSpec],
799 crate_path: &Path,
800) -> syn::Result<Vec<proc_macro2::TokenStream>> {
801 let mut methods = Vec::new();
802 for field in fields {
803 let ident = &field.ident;
804 let field_path = quote!(props.#ident);
805 let set_flag_ident = format_ident!("__tessera_set_{}", ident);
806 let set_flag_path = quote!(props.#set_flag_ident);
807 if let Some(helper) = field.helper {
808 methods.push(generate_helper_setter_methods(
809 field,
810 helper,
811 crate_path,
812 &field_path,
813 Some(&set_flag_path),
814 )?);
815 } else if let Some(method) =
816 generate_default_setter_method_for_path(field, &field_path, Some(&set_flag_path))?
817 {
818 methods.push(method);
819 }
820 }
821 Ok(methods)
822}
823
824fn component_type_id_tokens(fn_name: &syn::Ident) -> proc_macro2::TokenStream {
827 quote! {
828 {
829 use std::hash::{Hash, Hasher};
830 let mut hasher = std::collections::hash_map::DefaultHasher::new();
831 module_path!().hash(&mut hasher);
832 stringify!(#fn_name).hash(&mut hasher);
833 hasher.finish()
834 }
835 }
836}
837
838struct ControlFlowInstrumenter {
839 counter: usize,
841 seed: u64,
843}
844
845impl ControlFlowInstrumenter {
846 fn new(seed: u64) -> Self {
847 Self { counter: 0, seed }
848 }
849
850 fn next_group_id(&mut self) -> u64 {
852 let mut hasher = DefaultHasher::new();
853 self.seed.hash(&mut hasher);
854 self.counter.hash(&mut hasher);
855 self.counter += 1;
856 hasher.finish()
857 }
858
859 fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
865 self.visit_expr_mut(expr);
868 let group_id = self.next_group_id();
869 let original_expr = &expr;
871 let new_expr: Expr = parse_quote! {
872 {
873 let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
874 #original_expr
875 }
876 };
877 *expr = new_expr;
878 }
879
880 fn wrap_block_in_group(&mut self, block: &mut Block) {
882 self.visit_block_mut(block);
884
885 let group_id = self.next_group_id();
886 let original_stmts = &block.stmts;
887
888 let new_block: Block = parse_quote! {
889 {
890 let _group_guard = ::tessera_ui::__private::GroupGuard::new(#group_id);
891 #(#original_stmts)*
892 }
893 };
894
895 *block = new_block;
896 }
897
898 fn wrap_block_in_path_group(&mut self, block: &mut Block) {
900 self.visit_block_mut(block);
901
902 let group_id = self.next_group_id();
903 let original_stmts = &block.stmts;
904
905 let new_block: Block = parse_quote! {
906 {
907 let _group_guard = ::tessera_ui::__private::PathGroupGuard::new(#group_id);
908 #(#original_stmts)*
909 }
910 };
911
912 *block = new_block;
913 }
914}
915
916impl VisitMut for ControlFlowInstrumenter {
917 fn visit_expr_if_mut(&mut self, i: &mut syn::ExprIf) {
918 self.visit_expr_mut(&mut i.cond);
919 self.wrap_block_in_group(&mut i.then_branch);
920 if let Some((_, else_branch)) = &mut i.else_branch {
921 match &mut **else_branch {
922 Expr::Block(block_expr) => {
923 self.wrap_block_in_group(&mut block_expr.block);
924 }
925 Expr::If(_) => {
926 self.visit_expr_mut(else_branch);
927 }
928 _ => {
929 self.wrap_expr_in_group(else_branch);
930 }
931 }
932 }
933 }
934
935 fn visit_expr_match_mut(&mut self, m: &mut syn::ExprMatch) {
936 self.visit_expr_mut(&mut m.expr);
937 for arm in &mut m.arms {
938 self.wrap_expr_in_group(&mut arm.body);
939 }
940 }
941
942 fn visit_expr_for_loop_mut(&mut self, f: &mut syn::ExprForLoop) {
943 self.visit_expr_mut(&mut f.expr);
944 self.wrap_block_in_path_group(&mut f.body);
945 }
946
947 fn visit_expr_while_mut(&mut self, w: &mut syn::ExprWhile) {
948 self.visit_expr_mut(&mut w.cond);
949 self.wrap_block_in_path_group(&mut w.body);
950 }
951
952 fn visit_expr_loop_mut(&mut self, l: &mut syn::ExprLoop) {
953 self.wrap_block_in_path_group(&mut l.body);
954 }
955}
956
957#[proc_macro_attribute]
981pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
982 let crate_path: syn::Path = match parse_crate_path(attr) {
983 Ok(path) => path,
984 Err(err) => return err.to_compile_error().into(),
985 };
986
987 let mut input_fn = parse_macro_input!(item as ItemFn);
989 if let Some(conflicting_attr) = input_fn
990 .attrs
991 .iter()
992 .find(|attr| attr.path().is_ident("shard"))
993 {
994 return syn::Error::new_spanned(
995 conflicting_attr,
996 "#[tessera] and #[shard] cannot be combined on the same function; use #[shard] only",
997 )
998 .to_compile_error()
999 .into();
1000 }
1001
1002 if input_fn.sig.constness.is_some()
1003 || input_fn.sig.asyncness.is_some()
1004 || input_fn.sig.unsafety.is_some()
1005 || input_fn.sig.abi.is_some()
1006 || input_fn.sig.variadic.is_some()
1007 || !input_fn.sig.generics.params.is_empty()
1008 || input_fn.sig.generics.where_clause.is_some()
1009 {
1010 return syn::Error::new_spanned(
1011 &input_fn.sig,
1012 "#[tessera] components must be plain, non-generic free functions",
1013 )
1014 .to_compile_error()
1015 .into();
1016 }
1017 if !matches!(input_fn.sig.output, syn::ReturnType::Default) {
1018 return syn::Error::new_spanned(
1019 &input_fn.sig.output,
1020 "#[tessera] components must not return a value",
1021 )
1022 .to_compile_error()
1023 .into();
1024 }
1025
1026 let fn_name = &input_fn.sig.ident;
1027 let fn_vis = &input_fn.vis;
1028 let fn_attrs = &input_fn.attrs;
1029 let prop_signature = match strict_prop_signature(&input_fn.sig) {
1030 Ok(v) => v,
1031 Err(err) => return err.to_compile_error().into(),
1032 };
1033
1034 let mut hasher = DefaultHasher::new();
1037 input_fn.sig.ident.to_string().hash(&mut hasher);
1038 let seed = hasher.finish();
1039
1040 let mut instrumenter = ControlFlowInstrumenter::new(seed);
1042 instrumenter.visit_block_mut(&mut input_fn.block);
1043 let fn_block = &input_fn.block;
1044
1045 let register_tokens = register_node_tokens(&crate_path, fn_name);
1047 let component_type_id_tokens = component_type_id_tokens(fn_name);
1048 let expanded = match &prop_signature {
1049 ComponentPropSignature::Unit => {
1050 let fn_sig = &input_fn.sig;
1051 let prop_assert_tokens = prop_assert_tokens(&crate_path, &syn::parse_quote!(()));
1052 let unit_runner_ident = format_ident!("__tessera_{}_unit_runner", fn_name);
1053 let replay_tokens = replay_register_tokens(
1054 &crate_path,
1055 &unit_runner_ident,
1056 &syn::parse_quote!(()),
1057 quote!(&__tessera_unit_props),
1058 );
1059
1060 quote! {
1061 fn #unit_runner_ident(_props: &()) {
1062 #fn_name();
1063 }
1064
1065 #(#fn_attrs)*
1066 #fn_vis #fn_sig {
1067 let __tessera_component_type_id: u64 = #component_type_id_tokens;
1068 let __tessera_phase_guard = {
1069 use #crate_path::__private::{RuntimePhase, push_phase};
1070 push_phase(RuntimePhase::Build)
1071 };
1072 let __tessera_fn_name: &str = stringify!(#fn_name);
1073 let __tessera_node_id = #register_tokens;
1074
1075 let _node_ctx_guard = {
1076 use #crate_path::__private::push_current_node;
1077 push_current_node(
1078 __tessera_node_id,
1079 __tessera_component_type_id,
1080 __tessera_fn_name,
1081 )
1082 };
1083
1084 let __tessera_instance_key: u64 = #crate_path::__private::current_instance_key();
1085 let __tessera_instance_logic_id: u64 =
1086 #crate_path::__private::current_instance_logic_id();
1087 let _instance_ctx_guard = {
1088 use #crate_path::__private::push_current_component_instance_key;
1089 push_current_component_instance_key(__tessera_instance_key)
1090 };
1091 let _component_scope_guard = {
1092 struct ComponentScopeGuard;
1093 impl Drop for ComponentScopeGuard {
1094 fn drop(&mut self) {
1095 #crate_path::__private::finish_component_node();
1096 }
1097 }
1098 ComponentScopeGuard
1099 };
1100 #crate_path::__private::set_current_node_identity(
1101 __tessera_instance_key,
1102 __tessera_instance_logic_id,
1103 );
1104 #prop_assert_tokens
1105 #crate_path::__private::record_current_context_snapshot_for(__tessera_instance_key);
1106 let __tessera_unit_props = ();
1107 #replay_tokens
1108 if __tessera_component_reused {
1109 return;
1110 }
1111 #fn_block
1112 }
1113 }
1114 }
1115 ComponentPropSignature::Params(fields) => {
1116 let props_ident = hidden_props_ident(fn_name);
1117 let builder_ident = builder_ident(fn_name);
1118 let impl_ident = hidden_component_impl_ident(fn_name);
1119 let set_flag_idents: Vec<_> = fields
1120 .iter()
1121 .map(|field| format_ident!("__tessera_set_{}", field.ident))
1122 .collect();
1123 let field_defs: Vec<_> = fields
1124 .iter()
1125 .map(|field| {
1126 let ident = &field.ident;
1127 let ty = &field.ty;
1128 quote!(#ident: #ty)
1129 })
1130 .collect();
1131 let set_flag_defs: Vec<_> = set_flag_idents
1132 .iter()
1133 .map(|ident| quote!(#ident: bool))
1134 .collect();
1135 let field_resolutions: Vec<_> = fields
1136 .iter()
1137 .zip(set_flag_idents.iter())
1138 .map(|(field, set_flag_ident)| {
1139 let ident = &field.ident;
1140 let ty = &field.ty;
1141 if let Some(default_expr) = &field.default_expr {
1142 quote! {
1143 let #ident: #ty = if __tessera_props.#set_flag_ident {
1144 __tessera_props.#ident.clone()
1145 } else {
1146 #default_expr
1147 };
1148 }
1149 } else {
1150 quote!(let #ident: #ty = __tessera_props.#ident.clone();)
1151 }
1152 })
1153 .collect();
1154 let prop_impl_tokens = generate_prop_like_impls(&props_ident, fields, &crate_path);
1155 let builder_methods = match generate_builder_methods(fields, &crate_path) {
1156 Ok(methods) => methods,
1157 Err(err) => return err.to_compile_error().into(),
1158 };
1159 let prop_assert_tokens =
1160 prop_assert_tokens(&crate_path, &syn::parse_quote!(#props_ident));
1161 let replay_tokens = replay_register_tokens(
1162 &crate_path,
1163 &impl_ident,
1164 &syn::parse_quote!(#props_ident),
1165 quote!(__tessera_props),
1166 );
1167
1168 quote! {
1169 #[derive(Clone, Default)]
1170 struct #props_ident {
1171 #(#field_defs,)*
1172 #(#set_flag_defs,)*
1173 }
1174
1175 #prop_impl_tokens
1176
1177 #[derive(Default)]
1178 #[doc = concat!("Builder returned by [`", stringify!(#fn_name), "`].")]
1179 #fn_vis struct #builder_ident {
1180 props: #props_ident,
1181 }
1182
1183 impl #builder_ident {
1184 #(#builder_methods)*
1185 }
1186
1187 impl Drop for #builder_ident {
1188 fn drop(&mut self) {
1189 #impl_ident(&self.props);
1190 }
1191 }
1192
1193 #(#fn_attrs)*
1194 #fn_vis fn #fn_name() -> #builder_ident {
1195 #builder_ident::default()
1196 }
1197
1198 fn #impl_ident(__tessera_props: &#props_ident) {
1199 let __tessera_component_type_id: u64 = #component_type_id_tokens;
1200 let __tessera_phase_guard = {
1201 use #crate_path::__private::{RuntimePhase, push_phase};
1202 push_phase(RuntimePhase::Build)
1203 };
1204 let __tessera_fn_name: &str = stringify!(#fn_name);
1205 let __tessera_node_id = #register_tokens;
1206
1207 let _node_ctx_guard = {
1208 use #crate_path::__private::push_current_node;
1209 push_current_node(
1210 __tessera_node_id,
1211 __tessera_component_type_id,
1212 __tessera_fn_name,
1213 )
1214 };
1215
1216 let __tessera_instance_key: u64 = #crate_path::__private::current_instance_key();
1217 let __tessera_instance_logic_id: u64 =
1218 #crate_path::__private::current_instance_logic_id();
1219 let _instance_ctx_guard = {
1220 use #crate_path::__private::push_current_component_instance_key;
1221 push_current_component_instance_key(__tessera_instance_key)
1222 };
1223 let _component_scope_guard = {
1224 struct ComponentScopeGuard;
1225 impl Drop for ComponentScopeGuard {
1226 fn drop(&mut self) {
1227 #crate_path::__private::finish_component_node();
1228 }
1229 }
1230 ComponentScopeGuard
1231 };
1232 #crate_path::__private::set_current_node_identity(
1233 __tessera_instance_key,
1234 __tessera_instance_logic_id,
1235 );
1236 #prop_assert_tokens
1237 #crate_path::__private::record_current_context_snapshot_for(__tessera_instance_key);
1238 #replay_tokens
1239 if __tessera_component_reused {
1240 return;
1241 }
1242 #(#field_resolutions)*
1243 #fn_block
1244 }
1245 }
1246 }
1247 };
1248
1249 TokenStream::from(expanded)
1250}
1251
1252#[proc_macro_attribute]
1258pub fn entry(attr: TokenStream, item: TokenStream) -> TokenStream {
1259 let crate_path: syn::Path = match parse_crate_path(attr) {
1260 Ok(path) => path,
1261 Err(err) => return err.to_compile_error().into(),
1262 };
1263 let mut input_fn = parse_macro_input!(item as ItemFn);
1264
1265 if !input_fn.sig.inputs.is_empty() {
1266 return syn::Error::new_spanned(
1267 &input_fn.sig.inputs,
1268 "entry functions must not accept arguments",
1269 )
1270 .to_compile_error()
1271 .into();
1272 }
1273
1274 input_fn.attrs.retain(|attr| !attr.path().is_ident("entry"));
1275 let fn_name = &input_fn.sig.ident;
1276
1277 let expanded = quote! {
1278 #input_fn
1279
1280 #[cfg(target_os = "android")]
1281 #[unsafe(no_mangle)]
1282 fn android_main(android_app: #crate_path::winit::platform::android::activity::AndroidApp) {
1283 if let Err(err) = #fn_name().run_android(android_app) {
1284 eprintln!("App failed to run: {err}");
1285 }
1286 }
1287 };
1288
1289 expanded.into()
1290}
1291
1292#[cfg(feature = "shard")]
1347#[proc_macro_attribute]
1348pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
1349 use heck::ToUpperCamelCase;
1350 let shard_args = if attr.is_empty() {
1351 ShardMacroArgs::default()
1352 } else {
1353 match syn::parse::<ShardMacroArgs>(attr) {
1354 Ok(value) => value,
1355 Err(err) => return err.to_compile_error().into(),
1356 }
1357 };
1358
1359 let crate_path: syn::Path = shard_args
1360 .crate_path
1361 .unwrap_or_else(|| syn::parse_quote!(::tessera_ui));
1362
1363 let func = parse_macro_input!(input as ItemFn);
1364 if let Some(conflicting_attr) = func
1365 .attrs
1366 .iter()
1367 .find(|attr| attr.path().is_ident("tessera"))
1368 {
1369 return syn::Error::new_spanned(
1370 conflicting_attr,
1371 "#[shard] already defines a component boundary; do not combine #[shard] with #[tessera]",
1372 )
1373 .to_compile_error()
1374 .into();
1375 }
1376
1377 let shard_params = match parse_shard_params(&func.sig) {
1378 Ok(params) => params,
1379 Err(err) => return err.to_compile_error().into(),
1380 };
1381 let router_params: Vec<_> = shard_params
1382 .iter()
1383 .filter(|param| param.is_router)
1384 .collect();
1385 if router_params.len() > 1 {
1386 return syn::Error::new_spanned(
1387 &func.sig.inputs,
1388 "#[shard] supports at most one `#[router]` parameter",
1389 )
1390 .to_compile_error()
1391 .into();
1392 }
1393 if let Some(router_param) = router_params.first()
1394 && !is_state_router_controller_type(&router_param.ty)
1395 {
1396 return syn::Error::new_spanned(
1397 &router_param.ty,
1398 "`#[router]` parameters must have type `State<RouterController>`",
1399 )
1400 .to_compile_error()
1401 .into();
1402 }
1403
1404 let state_type = shard_args.state_type;
1405 let state_lifecycle_tokens = match shard_args.lifecycle {
1406 Some(lifecycle) => {
1407 let lifecycle_name = lifecycle.to_string().to_lowercase();
1408 if state_type.is_none() {
1409 return syn::Error::new_spanned(
1410 lifecycle,
1411 "`lifecycle` requires `state` in #[shard(...)]",
1412 )
1413 .to_compile_error()
1414 .into();
1415 }
1416 if lifecycle_name == "scope" {
1417 quote! { #crate_path::router::ShardStateLifeCycle::Scope }
1418 } else if lifecycle_name == "shard" {
1419 quote! { #crate_path::router::ShardStateLifeCycle::Shard }
1420 } else {
1421 return syn::Error::new_spanned(
1422 lifecycle,
1423 "unsupported `lifecycle` in #[shard(...)]: expected `scope` or `shard`",
1424 )
1425 .to_compile_error()
1426 .into();
1427 }
1428 }
1429 None => quote! { #crate_path::router::ShardStateLifeCycle::Shard },
1430 };
1431
1432 let func_body = func.block;
1433 let func_name_str = func.sig.ident.to_string();
1434
1435 let func_attrs = &func.attrs;
1436 let func_vis = &func.vis;
1437 let router_binding =
1438 router_params
1439 .first()
1440 .map_or_else(proc_macro2::TokenStream::new, |param| {
1441 let router_ident = ¶m.ident;
1442 quote! {
1443 let #router_ident = #router_ident
1444 .expect("`#[router]` injection is only available inside shard_home");
1445 }
1446 });
1447 let mut func_sig_modified = func.sig.clone();
1448 for input in &mut func_sig_modified.inputs {
1449 let FnArg::Typed(pat_type) = input else {
1450 continue;
1451 };
1452 let is_router = pat_type
1453 .attrs
1454 .iter()
1455 .any(|attr| attr.path().is_ident("router"));
1456 if !is_router {
1457 continue;
1458 }
1459 let original_ty = (*pat_type.ty).clone();
1460 *pat_type.ty = parse_quote!(::core::option::Option<#original_ty>);
1461 pat_type
1462 .attrs
1463 .retain(|attr| !attr.path().is_ident("router"));
1464 pat_type.attrs.push(syn::parse_quote!(#[prop(skip_setter)]));
1465 pat_type.attrs.push(
1466 syn::parse_quote!(#[default(Some(#crate_path::__private::current_router_controller()))]),
1467 );
1468 }
1469
1470 let func_name = func.sig.ident.clone();
1472 let struct_name = syn::Ident::new(
1473 &format!("{}Destination", func_name_str.to_upper_camel_case()),
1474 func_name.span(),
1475 );
1476 let dest_fields = shard_params
1478 .iter()
1479 .filter(|param| !param.is_router)
1480 .map(|param| {
1481 let ident = ¶m.ident;
1482 let ty = ¶m.ty;
1483 quote! { pub #ident: #ty }
1484 });
1485
1486 let expanded = {
1488 let destination_builder_setters: Vec<_> = shard_params
1489 .iter()
1490 .filter(|param| !param.is_router)
1491 .map(|param| {
1492 let ident = ¶m.ident;
1493 if option_inner_type(¶m.ty).is_some() {
1494 quote! {
1495 if let Some(value) = self.#ident.clone() {
1496 __tessera_builder = __tessera_builder.#ident(value);
1497 }
1498 }
1499 } else {
1500 quote! {
1501 __tessera_builder = __tessera_builder.#ident(self.#ident.clone());
1502 }
1503 }
1504 })
1505 .collect();
1506
1507 if let Some(state_type) = state_type {
1508 quote! {
1509 #func_vis struct #struct_name {
1510 #(#dest_fields),*
1511 }
1512
1513 impl #crate_path::router::RouterDestination for #struct_name {
1514 fn exec_component(&self) {
1515 let mut __tessera_builder = #func_name();
1516 #(#destination_builder_setters)*
1517 drop(__tessera_builder);
1518 }
1519
1520 fn shard_id(&self) -> &'static str {
1521 concat!(module_path!(), "::", #func_name_str)
1522 }
1523 }
1524
1525 #(#func_attrs)*
1526 #[#crate_path::tessera(#crate_path)]
1527 #func_vis #func_sig_modified {
1528 const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
1529 #router_binding
1530 #crate_path::__private::with_current_router_shard_state::<#state_type, _, _>(
1531 SHARD_ID,
1532 #state_lifecycle_tokens,
1533 |state| {
1534 #func_body
1535 },
1536 )
1537 }
1538 }
1539 } else {
1540 quote! {
1541 #func_vis struct #struct_name {
1542 #(#dest_fields),*
1543 }
1544
1545 impl #crate_path::router::RouterDestination for #struct_name {
1546 fn exec_component(&self) {
1547 let mut __tessera_builder = #func_name();
1548 #(#destination_builder_setters)*
1549 drop(__tessera_builder);
1550 }
1551
1552 fn shard_id(&self) -> &'static str {
1553 concat!(module_path!(), "::", #func_name_str)
1554 }
1555 }
1556
1557 #(#func_attrs)*
1558 #[#crate_path::tessera(#crate_path)]
1559 #func_vis #func_sig_modified {
1560 #router_binding
1561 #func_body
1562 }
1563 }
1564 }
1565 };
1566
1567 TokenStream::from(expanded)
1568}