tessera_macros/
lib.rs

1//! # Tessera Macros
2//!
3//! This crate provides procedural macros for the Tessera UI framework.
4//! The main export is the `#[tessera]` attribute macro, which transforms
5//! regular Rust functions into Tessera UI components.
6
7use 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
13/// Helper: parse crate path from attribute TokenStream
14fn parse_crate_path(attr: proc_macro::TokenStream) -> syn::Path {
15    if attr.is_empty() {
16        // Default to `tessera_ui` if no path is provided
17        syn::parse_quote!(::tessera_ui)
18    } else {
19        // Parse the provided path, e.g., `crate` or `tessera_ui`
20        syn::parse(attr).expect("Expected a valid path like `crate` or `tessera_ui`")
21    }
22}
23
24/// Helper: tokens to register a component node
25fn 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
47/// Helper: tokens to inject `layout`
48fn 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
62/// Helper: tokens to inject `input_handler`
63fn 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
84/// Helper: tokens to inject `on_minimize`
85fn 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
96/// Helper: tokens to inject `on_close`
97fn 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
108/// Helper: tokens to compute a stable logic id based on module path + function
109/// name.
110fn 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 to generate unique IDs in current function
124    counter: usize,
125    /// seed to prevent ID collisions across functions
126    seed: u64,
127}
128
129impl ControlFlowInstrumenter {
130    fn new(seed: u64) -> Self {
131        Self { counter: 0, seed }
132    }
133
134    /// Generate the next unique group ID
135    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    /// Wrap an expression in a GroupGuard block
144    ///
145    /// Before transform: expr
146    /// After transform: { let _group_guard =
147    /// ::tessera_ui::runtime::GroupGuard::new(#id); expr }
148    fn wrap_expr_in_group(&mut self, expr: &mut Expr) {
149        // Recursively visit sub-expressions (depth-first) to ensure nested structures
150        // are wrapped
151        self.visit_expr_mut(expr);
152        let group_id = self.next_group_id();
153        // Use fully-qualified path ::tessera_ui to avoid relying on a crate alias
154        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    /// Wrap a block in a GroupGuard block
165    fn wrap_block_in_group(&mut self, block: &mut Block) {
166        // Recursively instrument nested expressions before wrapping the block
167        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/// Transforms a regular Rust function into a Tessera UI component.
225///
226/// # Usage
227///
228/// Annotate a free function (no captured self) with `#[tessera]`. You may then
229/// (optionally) call any of the injected helpers exactly once (last call wins
230/// if repeated).
231///
232/// # Parameters
233///
234/// * Attribute arguments are currently unused; pass nothing or `#[tessera]`.
235///
236/// # When NOT to Use
237///
238/// * For function that should not be a ui component.
239///
240/// # See Also
241///
242/// * [`#[shard]`](crate::shard) for navigation‑aware components with injectable
243///   shard state.
244#[proc_macro_attribute]
245pub fn tessera(attr: TokenStream, item: TokenStream) -> TokenStream {
246    let crate_path: syn::Path = parse_crate_path(attr);
247
248    // Parse the input function that will be transformed into a component
249    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    // Generate a stable hash seed based on function name in order to avoid ID
256    // collisions
257    let mut hasher = DefaultHasher::new();
258    input_fn.sig.ident.to_string().hash(&mut hasher);
259    let seed = hasher.finish();
260
261    // Modify the function body to instrument control flow with GroupGuard
262    let mut instrumenter = ControlFlowInstrumenter::new(seed);
263    instrumenter.visit_block_mut(&mut input_fn.block);
264    let fn_block = &input_fn.block;
265
266    // Prepare token fragments using helpers to keep function small and readable
267    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    // Generate the transformed function with Tessera runtime integration
275    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            // Inject guard to pop component node on function exit
287            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            // Track current node for control-flow instrumentation
299            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            // Inject helper tokens
323            #layout_tokens
324            #state_tokens
325            #on_minimize_tokens
326            #on_close_tokens
327
328            // Execute user's function body
329            #fn_block
330        }
331    };
332
333    TokenStream::from(expanded)
334}
335
336/// Transforms a function into a *shard component* that can be navigated to via
337/// the routing system and (optionally) provided with a lazily‑initialized
338/// per‑shard state.
339///
340/// # Features
341///
342/// * Generates a `StructNameDestination` (UpperCamelCase + `Destination`)
343///   implementing `tessera_shard::router::RouterDestination`
344/// * (Optional) Injects a single `#[state]` parameter whose type:
345///   - Must implement `Default + Send + Sync + 'static`
346///   - Is constructed (or reused) and passed to your function body
347/// * Produces a stable shard ID: `module_path!()::function_name`
348///
349/// # Lifecycle
350///
351/// Controlled by the generated destination (via `#[state(...)]`).
352/// * Default: `Shard` – state is removed when the destination is `pop()`‑ed
353/// * Override: `#[state(app)]` (or `#[state(application)]`) – persist for the
354///   entire application
355///
356/// When `pop()` is called and the destination lifecycle is `Shard`, the
357/// registry entry is removed, freeing the state.
358///
359/// # Parameter Transformation
360///
361/// * At most one parameter may be annotated with `#[state]`.
362/// * That parameter is removed from the *generated* function signature and
363///   supplied implicitly.
364/// * All other parameters remain explicit and become public fields on the
365///   generated `*Destination` struct.
366///
367/// # Generated Destination (Conceptual)
368///
369/// ```rust,ignore
370/// struct ProfilePageDestination { /* non-state params as public fields */ }
371/// impl RouterDestination for ProfilePageDestination {
372///     fn exec_component(&self) { profile_page(/* fields */); }
373///     fn shard_id(&self) -> &'static str { "<module>::profile_page" }
374/// }
375/// ```
376///
377/// # Limitations
378///
379/// * No support for multiple `#[state]` params (compile panic if violated)
380/// * Do not manually implement `RouterDestination` for these pages; rely on
381///   generation
382///
383/// # See Also
384///
385/// * Routing helpers: `tessera_ui::router::{push, pop, router_root}`
386/// * Shard state registry: `tessera_shard::ShardRegistry`
387///
388/// # Safety
389///
390/// Internally uses an unsafe cast inside the registry to recover `Arc<T>` from
391/// `Arc<dyn ShardState>`; this is encapsulated and not exposed.
392///
393/// # Errors / Panics
394///
395/// * Panics at compile time if multiple `#[state]` parameters are used or
396///   unsupported pattern forms are encountered.
397#[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    // Handle #[state] parameters, ensuring it's unique and removing it from the
412    // signature Also parse optional lifecycle argument: #[state(app)] or
413    // #[state(shard)]
414    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            // Detect #[state] and parse optional argument
420            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                    // Try parse an optional argument: #[state(app)] / #[state(shard)]
426                    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    // Generate struct name for the new RouterDestination
479    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    // Generate fields for the new struct that will implement `RouterDestination`
486    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    // Only keep the parameters that are not marked with #[state]
499    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        // Default is `Shard` per RouterDestination trait; no override needed.
520        quote! {}
521    };
522
523    let expanded = {
524        // `exec_component` only passes struct fields (unmarked parameters).
525        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                    // Generate a stable unique ID at the call site
555                    const SHARD_ID: &str = concat!(module_path!(), "::", #func_name_str);
556
557                    // Call the global registry and pass the original function body as a closure
558                    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}