1use std::hash::{DefaultHasher, Hash, Hasher};
8
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{Block, Expr, ItemFn, parse_macro_input, parse_quote, visit_mut::VisitMut};
12
13fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Path {
15 if attr.is_empty() {
16 syn::parse_quote!(::tessera_ui)
18 } else {
19 syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
21 }
22}
23
24fn register_node_tokens(crate_path: &syn::Path, fn_name: &syn::Ident) -> proc_macro2::TokenStream {
26 quote! {
27 {
28 use #crate_path::ComponentNode;
29 use #crate_path::layout::DefaultLayoutSpec;
30 use #crate_path::runtime::TesseraRuntime;
31
32 TesseraRuntime::with_mut(|runtime| {
33 runtime.component_tree.add_node(
34 ComponentNode {
35 fn_name: stringify!(#fn_name).to_string(),
36 logic_id: __tessera_logic_id,
37 instance_key: 0,
38 input_handler_fn: None,
39 layout_spec: Box::new(DefaultLayoutSpec::default()),
40 }
41 )
42 })
43 }
44 }
45}
46
47fn layout_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
49 quote! {
50 #[allow(clippy::needless_pass_by_value)]
51 fn layout<S>(spec: S)
52 where
53 S: #crate_path::layout::LayoutSpec,
54 {
55 use #crate_path::runtime::TesseraRuntime;
56
57 TesseraRuntime::with_mut(|runtime| runtime.set_current_layout_spec(spec));
58 }
59 }
60}
61
62fn input_handler_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
64 quote! {
65 #[allow(clippy::needless_pass_by_value)]
66 fn input_handler<F>(fun: F)
67 where
68 F: Fn(#crate_path::InputHandlerInput) + Send + Sync + 'static,
69 {
70 use #crate_path::InputHandlerFn;
71 use #crate_path::runtime::TesseraRuntime;
72
73 TesseraRuntime::with_mut(|runtime| {
74 runtime
75 .component_tree
76 .current_node_mut()
77 .unwrap()
78 .input_handler_fn = Some(Box::new(fun) as Box<InputHandlerFn>)
79 });
80 }
81 }
82}
83
84fn on_minimize_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
86 quote! {
87 let on_minimize = {
88 use #crate_path::runtime::TesseraRuntime;
89 |fun: Box<dyn Fn(bool) + Send + Sync + 'static>| {
90 TesseraRuntime::with_mut(|runtime| runtime.on_minimize(fun));
91 }
92 };
93 }
94}
95
96fn on_close_inject_tokens(crate_path: &syn::Path) -> proc_macro2::TokenStream {
98 quote! {
99 let on_close = {
100 use #crate_path::runtime::TesseraRuntime;
101 |fun: Box<dyn Fn() + Send + Sync + 'static>| {
102 TesseraRuntime::with_mut(|runtime| runtime.on_close(fun));
103 }
104 };
105 }
106}
107
108fn logic_id_tokens(fn_name: &syn::Ident) -> proc_macro2::TokenStream {
111 quote! {
112 {
113 use std::hash::{Hash, Hasher};
114 let mut hasher = std::collections::hash_map::DefaultHasher::new();
115 module_path!().hash(&mut hasher);
116 stringify!(#fn_name).hash(&mut hasher);
117 hasher.finish()
118 }
119 }
120}
121
122struct ControlFlowInstrumenter {
123 counter: usize,
125 seed: u64,
127}
128
129impl ControlFlowInstrumenter {
130 fn new(seed: u64) -> Self {
131 Self { counter: 0, seed }
132 }
133
134 fn next_group_id(&mut self) -> u64 {
136 let mut hasher = DefaultHasher::new();
137 self.seed.hash(&mut hasher);
138 self.counter.hash(&mut hasher);
139 self.counter += 1;
140 hasher.finish()
141 }
142
143 fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
149 self.visit_expr_mut(expr);
152 let group_id = self.next_group_id();
153 let original_expr = &expr;
155 let new_expr: Expr = parse_quote! {
156 {
157 let _group_guard = ::tessera_ui::runtime::GroupGuard::new(#group_id);
158 #original_expr
159 }
160 };
161 *expr = new_expr;
162 }
163
164 fn wrap_block_in_group(&mut self, block: &mut Block) {
166 self.visit_block_mut(block);
168
169 let group_id = self.next_group_id();
170 let original_stmts = &block.stmts;
171
172 let new_block: Block = parse_quote! {
173 {
174 let _group_guard = ::tessera_ui::runtime::GroupGuard::new(#group_id);
175 #(#original_stmts)*
176 }
177 };
178
179 *block = new_block;
180 }
181}
182
183impl VisitMut for ControlFlowInstrumenter {
184 fn visit_expr_if_mut(&mut self, i: &mut syn::ExprIf) {
185 self.visit_expr_mut(&mut i.cond);
186 self.wrap_block_in_group(&mut i.then_branch);
187 if let Some((_, else_branch)) = &mut i.else_branch {
188 match &mut **else_branch {
189 Expr::Block(block_expr) => {
190 self.wrap_block_in_group(&mut block_expr.block);
191 }
192 Expr::If(_) => {
193 self.visit_expr_mut(else_branch);
194 }
195 _ => {
196 self.wrap_expr_in_group(else_branch);
197 }
198 }
199 }
200 }
201
202 fn visit_expr_match_mut(&mut self, m: &mut syn::ExprMatch) {
203 self.visit_expr_mut(&mut m.expr);
204 for arm in &mut m.arms {
205 self.wrap_expr_in_group(&mut arm.body);
206 }
207 }
208
209 fn visit_expr_for_loop_mut(&mut self, f: &mut syn::ExprForLoop) {
210 self.visit_expr_mut(&mut f.expr);
211 self.wrap_block_in_group(&mut f.body);
212 }
213
214 fn visit_expr_while_mut(&mut self, w: &mut syn::ExprWhile) {
215 self.visit_expr_mut(&mut w.cond);
216 self.wrap_block_in_group(&mut w.body);
217 }
218
219 fn visit_expr_loop_mut(&mut self, l: &mut syn::ExprLoop) {
220 self.wrap_block_in_group(&mut l.body);
221 }
222}
223
224#[proc_macro_attribute]
245pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
246 let crate_path: syn::Path = parse_crate_path(attr);
247
248 let mut input_fn = parse_macro_input!(item as ItemFn);
250 let fn_name = &input_fn.sig.ident;
251 let fn_vis = &input_fn.vis;
252 let fn_attrs = &input_fn.attrs;
253 let fn_sig = &input_fn.sig;
254
255 let mut hasher = DefaultHasher::new();
258 input_fn.sig.ident.to_string().hash(&mut hasher);
259 let seed = hasher.finish();
260
261 let mut instrumenter = ControlFlowInstrumenter::new(seed);
263 instrumenter.visit_block_mut(&mut input_fn.block);
264 let fn_block = &input_fn.block;
265
266 let register_tokens = register_node_tokens(&crate_path, fn_name);
268 let layout_tokens = layout_inject_tokens(&crate_path);
269 let state_tokens = input_handler_inject_tokens(&crate_path);
270 let on_minimize_tokens = on_minimize_inject_tokens(&crate_path);
271 let on_close_tokens = on_close_inject_tokens(&crate_path);
272 let logic_id_tokens = logic_id_tokens(fn_name);
273
274 let expanded = quote! {
276 #(#fn_attrs)*
277 #fn_vis #fn_sig {
278 let __tessera_logic_id: u64 = #logic_id_tokens;
279 let __tessera_phase_guard = {
280 use #crate_path::runtime::{RuntimePhase, push_phase};
281 push_phase(RuntimePhase::Build)
282 };
283 let __tessera_fn_name: &str = stringify!(#fn_name);
284 let __tessera_node_id = #register_tokens;
285
286 let _component_scope_guard = {
288 struct ComponentScopeGuard;
289 impl Drop for ComponentScopeGuard {
290 fn drop(&mut self) {
291 use #crate_path::runtime::TesseraRuntime;
292 TesseraRuntime::with_mut(|runtime| runtime.component_tree.pop_node());
293 }
294 }
295 ComponentScopeGuard
296 };
297
298 let _node_ctx_guard = {
300 use #crate_path::runtime::push_current_node;
301 push_current_node(__tessera_node_id, __tessera_logic_id, __tessera_fn_name)
302 };
303
304 let __tessera_instance_key: u64 = #crate_path::runtime::current_instance_key();
305 {
306 use #crate_path::runtime::TesseraRuntime;
307 TesseraRuntime::with_mut(|runtime| {
308 runtime.set_current_instance_key(__tessera_instance_key);
309 });
310 }
311 let _trace_guard = {
312 struct TraceGuard;
313 impl Drop for TraceGuard {
314 fn drop(&mut self) {
315 #crate_path::runtime::trace_end();
316 }
317 }
318 #crate_path::runtime::trace_begin(__tessera_instance_key);
319 TraceGuard
320 };
321
322 #layout_tokens
324 #state_tokens
325 #on_minimize_tokens
326 #on_close_tokens
327
328 #fn_block
330 }
331 };
332
333 TokenStream::from(expanded)
334}
335
336#[cfg(feature = "shard")]
398#[proc_macro_attribute]
399pub fn shard(attr: TokenStream, input: TokenStream) -> TokenStream {
400 use heck::ToUpperCamelCase;
401 use syn::Pat;
402
403 let crate_path: syn::Path = if attr.is_empty() {
404 syn::parse_quote!(::tessera_ui)
405 } else {
406 syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
407 };
408
409 let mut func = parse_macro_input!(input as ItemFn);
410
411 let mut state_param = None;
415 let mut state_lifecycle: Option<proc_macro2::TokenStream> = None;
416 let mut new_inputs = syn::punctuated::Punctuated::new();
417 for arg in func.sig.inputs.iter() {
418 if let syn::FnArg::Typed(pat_type) = arg {
419 let mut is_state = false;
421 let mut lifecycle_override: Option<proc_macro2::TokenStream> = None;
422 for attr in &pat_type.attrs {
423 if attr.path().is_ident("state") {
424 is_state = true;
425 if let Ok(arg_ident) = attr.parse_args::<syn::Ident>() {
427 let s = arg_ident.to_string().to_lowercase();
428 if s == "app" || s == "application" {
429 lifecycle_override = Some(
430 quote! { #crate_path::tessera_shard::ShardStateLifeCycle::Application },
431 );
432 } else if s == "shard" {
433 lifecycle_override = Some(
434 quote! { #crate_path::tessera_shard::ShardStateLifeCycle::Shard },
435 );
436 } else {
437 panic!(
438 "Unsupported #[state(...)] argument in #[shard]: expected `app` or `shard`"
439 );
440 }
441 }
442 }
443 }
444 if is_state {
445 if state_param.is_some() {
446 panic!(
447 "#[shard] function must have at most one parameter marked with #[state]."
448 );
449 }
450 state_param = Some(pat_type.clone());
451 state_lifecycle = lifecycle_override;
452 continue;
453 }
454 }
455 new_inputs.push(arg.clone());
456 }
457 func.sig.inputs = new_inputs;
458
459 let (state_name, state_type) = if let Some(state_param) = state_param {
460 let name = match *state_param.pat {
461 Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
462 _ => panic!(
463 "Unsupported parameter pattern in #[shard] function. Please use a simple identifier like `state`."
464 ),
465 };
466 (Some(name), Some(state_param.ty))
467 } else {
468 (None, None)
469 };
470
471 let func_body = func.block;
472 let func_name_str = func.sig.ident.to_string();
473
474 let func_attrs = &func.attrs;
475 let func_vis = &func.vis;
476 let func_sig_modified = &func.sig;
477
478 let func_name = func.sig.ident.clone();
480 let struct_name = syn::Ident::new(
481 &format!("{}Destination", func_name_str.to_upper_camel_case()),
482 func_name.span(),
483 );
484
485 let dest_fields = func.sig.inputs.iter().map(|arg| match arg {
487 syn::FnArg::Typed(pat_type) => {
488 let ident = match *pat_type.pat {
489 syn::Pat::Ident(ref pat_ident) => &pat_ident.ident,
490 _ => panic!("Unsupported parameter pattern in #[shard] function."),
491 };
492 let ty = &pat_type.ty;
493 quote! { pub #ident: #ty }
494 }
495 _ => panic!("Unsupported parameter type in #[shard] function."),
496 });
497
498 let param_idents: Vec<_> = func
500 .sig
501 .inputs
502 .iter()
503 .map(|arg| match arg {
504 syn::FnArg::Typed(pat_type) => match *pat_type.pat {
505 syn::Pat::Ident(ref pat_ident) => pat_ident.ident.clone(),
506 _ => panic!("Unsupported parameter pattern in #[shard] function."),
507 },
508 _ => panic!("Unsupported parameter type in #[shard] function."),
509 })
510 .collect();
511
512 let lifecycle_method_tokens = if let Some(lc) = state_lifecycle.clone() {
513 quote! {
514 fn life_cycle(&self) -> #crate_path::tessera_shard::ShardStateLifeCycle {
515 #lc
516 }
517 }
518 } else {
519 quote! {}
521 };
522
523 let expanded = {
524 let exec_args = param_idents
526 .iter()
527 .map(|ident| quote! { self.#ident.clone() });
528
529 if let Some(state_type) = state_type {
530 let state_name = state_name.as_ref().unwrap();
531 quote! {
532 #func_vis struct #struct_name {
533 #(#dest_fields),*
534 }
535
536 impl #crate_path::tessera_shard::router::RouterDestination for #struct_name {
537 fn exec_component(&self) {
538 #func_name(
539 #(
540 #exec_args
541 ),*
542 );
543 }
544
545 fn shard_id(&self) -> &'static str {
546 concat!(module_path!(), "::", #func_name_str)
547 }
548
549 #lifecycle_method_tokens
550 }
551
552 #(#func_attrs)*
553 #func_vis #func_sig_modified {
554 const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
556
557 unsafe {
559 #crate_path::tessera_shard::ShardRegistry::get().init_or_get::<#state_type, _, _>(
560 SHARD_ID,
561 |#state_name| {
562 #func_body
563 },
564 )
565 }
566 }
567 }
568 } else {
569 quote! {
570 #func_vis struct #struct_name {
571 #(#dest_fields),*
572 }
573
574 impl #crate_path::tessera_shard::router::RouterDestination for #struct_name {
575 fn exec_component(&self) {
576 #func_name(
577 #(
578 #exec_args
579 ),*
580 );
581 }
582
583 fn shard_id(&self) -> &'static str {
584 concat!(module_path!(), "::", #func_name_str)
585 }
586
587 #lifecycle_method_tokens
588 }
589
590 #(#func_attrs)*
591 #func_vis #func_sig_modified {
592 #func_body
593 }
594 }
595 }
596 };
597
598 TokenStream::from(expanded)
599}