Skip to main content

commonware_macros_impl/
lib.rs

1//! Proc-macro implementation for `commonware-macros`.
2//!
3//! This is an internal crate. Use [`commonware-macros`](https://docs.rs/commonware-macros)
4//! instead.
5
6#![doc(
7    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
8    html_favicon_url = "https://commonware.xyz/favicon.ico"
9)]
10
11use crate::nextest::configured_test_groups;
12use proc_macro::TokenStream;
13use proc_macro_crate::{FoundCrate, crate_name};
14use proc_macro2::Span;
15use quote::{format_ident, quote};
16use syn::{
17    Error, Expr, Ident, ItemFn, LitInt, LitStr, Pat, Token, Visibility, braced,
18    parse::{Parse, ParseStream, Result},
19    parse_macro_input,
20};
21
22mod nextest;
23
24/// Stability level input that accepts either a literal integer (0-4) or a named constant
25/// (ALPHA, BETA, GAMMA, DELTA, EPSILON).
26struct StabilityLevel {
27    value: u8,
28}
29
30impl Parse for StabilityLevel {
31    fn parse(input: ParseStream<'_>) -> Result<Self> {
32        let lookahead = input.lookahead1();
33        if lookahead.peek(LitInt) {
34            let lit: LitInt = input.parse()?;
35            let value: u8 = lit
36                .base10_parse()
37                .map_err(|_| Error::new(lit.span(), "stability level must be 0, 1, 2, 3, or 4"))?;
38            if value > 4 {
39                return Err(Error::new(
40                    lit.span(),
41                    "stability level must be 0, 1, 2, 3, or 4",
42                ));
43            }
44            Ok(Self { value })
45        } else if lookahead.peek(Ident) {
46            let ident: Ident = input.parse()?;
47            let value = match ident.to_string().as_str() {
48                "ALPHA" => 0,
49                "BETA" => 1,
50                "GAMMA" => 2,
51                "DELTA" => 3,
52                "EPSILON" => 4,
53                _ => {
54                    return Err(Error::new(
55                        ident.span(),
56                        "expected stability level: ALPHA, BETA, GAMMA, DELTA, EPSILON, or 0-4",
57                    ));
58                }
59            };
60            Ok(Self { value })
61        } else {
62            Err(lookahead.error())
63        }
64    }
65}
66
67fn level_name(level: u8) -> &'static str {
68    match level {
69        0 => "ALPHA",
70        1 => "BETA",
71        2 => "GAMMA",
72        3 => "DELTA",
73        4 => "EPSILON",
74        _ => unreachable!(),
75    }
76}
77
78/// Generates cfg identifiers that should exclude an item at the given stability level.
79///
80/// The stability system works by excluding items when building at higher stability levels.
81/// For example, an item marked `#[stability(BETA)]` (level 1) should be excluded when
82/// building with `--cfg commonware_stability_GAMMA` (level 2) or higher.
83///
84/// This function returns identifiers for all levels above the given level, plus `RESERVED`.
85/// The generated `#[cfg(not(any(...)))]` attribute ensures the item is included only when
86/// none of the higher-level cfgs are set.
87///
88/// ```text
89/// Level 0 (ALPHA)   -> excludes at: BETA, GAMMA, DELTA, EPSILON, RESERVED
90/// Level 1 (BETA)    -> excludes at: GAMMA, DELTA, EPSILON, RESERVED
91/// Level 2 (GAMMA)   -> excludes at: DELTA, EPSILON, RESERVED
92/// Level 3 (DELTA)   -> excludes at: EPSILON, RESERVED
93/// Level 4 (EPSILON) -> excludes at: RESERVED
94/// ```
95///
96/// `RESERVED` is a special level used by `scripts/find_unstable_public.sh` to exclude ALL
97/// stability-marked items, leaving only unmarked public items visible in rustdoc output.
98fn exclusion_cfg_names(level: u8) -> Vec<proc_macro2::Ident> {
99    let mut names: Vec<_> = ((level + 1)..=4)
100        .map(|l| format_ident!("commonware_stability_{}", level_name(l)))
101        .collect();
102
103    names.push(format_ident!("commonware_stability_RESERVED"));
104    names
105}
106
107#[proc_macro_attribute]
108pub fn stability(attr: TokenStream, item: TokenStream) -> TokenStream {
109    let level = parse_macro_input!(attr as StabilityLevel);
110    let exclude_names = exclusion_cfg_names(level.value);
111
112    let item2: proc_macro2::TokenStream = item.into();
113    let expanded = quote! {
114        #[cfg(not(any(#(#exclude_names),*)))]
115        #item2
116    };
117
118    TokenStream::from(expanded)
119}
120
121/// Input for the `stability_mod!` macro: `level, visibility mod name`
122struct StabilityModInput {
123    level: StabilityLevel,
124    visibility: Visibility,
125    name: Ident,
126}
127
128impl Parse for StabilityModInput {
129    fn parse(input: ParseStream<'_>) -> Result<Self> {
130        let level: StabilityLevel = input.parse()?;
131        input.parse::<Token![,]>()?;
132        let visibility: Visibility = input.parse()?;
133        input.parse::<Token![mod]>()?;
134        let name: Ident = input.parse()?;
135        Ok(Self {
136            level,
137            visibility,
138            name,
139        })
140    }
141}
142
143#[proc_macro]
144pub fn stability_mod(input: TokenStream) -> TokenStream {
145    let StabilityModInput {
146        level,
147        visibility,
148        name,
149    } = parse_macro_input!(input as StabilityModInput);
150
151    let exclude_names = exclusion_cfg_names(level.value);
152
153    let expanded = quote! {
154        #[cfg(not(any(#(#exclude_names),*)))]
155        #visibility mod #name;
156    };
157
158    TokenStream::from(expanded)
159}
160
161/// Input for the `stability_scope!` macro: `level [, cfg(predicate)] { items... }`
162struct StabilityScopeInput {
163    level: StabilityLevel,
164    predicate: Option<syn::Meta>,
165    items: Vec<syn::Item>,
166}
167
168impl Parse for StabilityScopeInput {
169    fn parse(input: ParseStream<'_>) -> Result<Self> {
170        let level: StabilityLevel = input.parse()?;
171
172        // Check for optional cfg predicate
173        let predicate = if input.peek(Token![,]) {
174            input.parse::<Token![,]>()?;
175
176            // Parse `cfg(...)` - expect the literal identifier "cfg" followed by parenthesized content
177            let cfg_ident: Ident = input.parse()?;
178            if cfg_ident != "cfg" {
179                return Err(Error::new(cfg_ident.span(), "expected `cfg`"));
180            }
181            let cfg_content;
182            syn::parenthesized!(cfg_content in input);
183            Some(cfg_content.parse()?)
184        } else {
185            None
186        };
187
188        let content;
189        braced!(content in input);
190
191        let mut items = Vec::new();
192        while !content.is_empty() {
193            items.push(content.parse()?);
194        }
195
196        Ok(Self {
197            level,
198            predicate,
199            items,
200        })
201    }
202}
203
204#[proc_macro]
205pub fn stability_scope(input: TokenStream) -> TokenStream {
206    let StabilityScopeInput {
207        level,
208        predicate,
209        items,
210    } = parse_macro_input!(input as StabilityScopeInput);
211
212    let exclude_names = exclusion_cfg_names(level.value);
213
214    let cfg_attr = predicate.map_or_else(
215        || quote! { #[cfg(not(any(#(#exclude_names),*)))] },
216        |pred| quote! { #[cfg(all(#pred, not(any(#(#exclude_names),*))))] },
217    );
218
219    let expanded_items: Vec<_> = items
220        .into_iter()
221        .map(|item| {
222            quote! {
223                #cfg_attr
224                #item
225            }
226        })
227        .collect();
228
229    let expanded = quote! {
230        #(#expanded_items)*
231    };
232
233    TokenStream::from(expanded)
234}
235
236#[proc_macro_attribute]
237pub fn boxed(_: TokenStream, item: TokenStream) -> TokenStream {
238    let mut input = parse_macro_input!(item as ItemFn);
239    if input.sig.asyncness.is_none() {
240        return Error::new_spanned(&input.sig, "#[boxed] can only be used with async functions")
241            .to_compile_error()
242            .into();
243    }
244
245    let block = input.block;
246    input.block = syn::parse_quote!({ ::std::boxed::Box::pin(async move #block).await });
247
248    quote!(#input).into()
249}
250
251#[proc_macro_attribute]
252pub fn test_async(_: TokenStream, item: TokenStream) -> TokenStream {
253    // Parse the input tokens into a syntax tree
254    let input = parse_macro_input!(item as ItemFn);
255
256    // Extract function components
257    let attrs = input.attrs;
258    let vis = input.vis;
259    let mut sig = input.sig;
260    let block = input.block;
261
262    // Remove 'async' from the function signature (#[test] only
263    // accepts sync functions)
264    sig.asyncness
265        .take()
266        .expect("test_async macro can only be used with async functions");
267
268    // Generate output tokens
269    let expanded = quote! {
270        #[test]
271        #(#attrs)*
272        #vis #sig {
273            futures::executor::block_on(async #block);
274        }
275    };
276    TokenStream::from(expanded)
277}
278
279#[proc_macro_attribute]
280pub fn test_traced(attr: TokenStream, item: TokenStream) -> TokenStream {
281    // Parse the input tokens into a syntax tree
282    let input = parse_macro_input!(item as ItemFn);
283
284    // Parse the attribute argument for default log level
285    let default_level = if attr.is_empty() {
286        "debug".to_string()
287    } else {
288        let level_str = parse_macro_input!(attr as LitStr);
289        let level_ident = level_str.value().to_lowercase();
290        match level_ident.as_str() {
291            "trace" | "debug" | "info" | "warn" | "error" => level_ident,
292            _ => {
293                return Error::new_spanned(
294                    level_str,
295                    "Invalid log level. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR.",
296                )
297                .to_compile_error()
298                .into();
299            }
300        }
301    };
302
303    // Extract function components
304    let attrs = input.attrs;
305    let vis = input.vis;
306    let sig = input.sig;
307    let block = input.block;
308
309    // Generate output tokens
310    let expanded = quote! {
311        #[test]
312        #(#attrs)*
313        #vis #sig {
314            use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
315
316            // Use RUST_LOG if set, otherwise fall back to the macro's default level
317            let filter = EnvFilter::try_from_default_env()
318                .unwrap_or_else(|_| EnvFilter::new(#default_level));
319            let subscriber = tracing_subscriber::Registry::default()
320                .with(
321                    tracing_subscriber::fmt::layer()
322                        .with_test_writer()
323                        .with_line_number(true)
324                )
325                .with(filter);
326            let dispatcher = tracing::Dispatch::new(subscriber);
327
328            // Set the subscriber for the scope of the test
329            tracing::dispatcher::with_default(&dispatcher, || {
330                #block
331            });
332        }
333    };
334    TokenStream::from(expanded)
335}
336
337#[proc_macro_attribute]
338pub fn test_group(attr: TokenStream, item: TokenStream) -> TokenStream {
339    if attr.is_empty() {
340        return Error::new(
341            Span::call_site(),
342            "test_group requires a string literal filter group name",
343        )
344        .to_compile_error()
345        .into();
346    }
347
348    let mut input = parse_macro_input!(item as ItemFn);
349    let group_literal = parse_macro_input!(attr as LitStr);
350
351    let group = match nextest::sanitize_group_literal(&group_literal) {
352        Ok(group) => group,
353        Err(err) => return err.to_compile_error().into(),
354    };
355    let groups = match configured_test_groups() {
356        Ok(groups) => groups,
357        Err(_) => {
358            // Don't fail the compilation if the file isn't found; just return the original input.
359            return TokenStream::from(quote!(#input));
360        }
361    };
362
363    if let Err(err) = nextest::ensure_group_known(groups, &group, group_literal.span()) {
364        return err.to_compile_error().into();
365    }
366
367    let original_name = input.sig.ident.to_string();
368    let new_ident = Ident::new(&format!("{original_name}_{group}_"), input.sig.ident.span());
369
370    input.sig.ident = new_ident;
371
372    TokenStream::from(quote!(#input))
373}
374
375#[proc_macro_attribute]
376pub fn test_collect_traces(attr: TokenStream, item: TokenStream) -> TokenStream {
377    let input = parse_macro_input!(item as ItemFn);
378
379    // Parse the attribute argument for log level
380    let log_level = if attr.is_empty() {
381        // Default log level is DEBUG
382        quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG }
383    } else {
384        // Parse the attribute as a string literal
385        let level_str = parse_macro_input!(attr as LitStr);
386        let level_ident = level_str.value().to_uppercase();
387        match level_ident.as_str() {
388            "TRACE" => quote! { ::tracing_subscriber::filter::LevelFilter::TRACE },
389            "DEBUG" => quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG },
390            "INFO" => quote! { ::tracing_subscriber::filter::LevelFilter::INFO },
391            "WARN" => quote! { ::tracing_subscriber::filter::LevelFilter::WARN },
392            "ERROR" => quote! { ::tracing_subscriber::filter::LevelFilter::ERROR },
393            _ => {
394                // Return a compile error for invalid log levels
395                return Error::new_spanned(
396                    level_str,
397                    "Invalid log level. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR.",
398                )
399                .to_compile_error()
400                .into();
401            }
402        }
403    };
404
405    let attrs = input.attrs;
406    let vis = input.vis;
407    let sig = input.sig;
408    let block = input.block;
409
410    // Create the signature of the inner function that takes the TraceStorage.
411    let inner_ident = format_ident!("__{}_inner_traced", sig.ident);
412    let mut inner_sig = sig.clone();
413    inner_sig.ident = inner_ident.clone();
414
415    // Create the signature of the outer test function.
416    let mut outer_sig = sig;
417    outer_sig.inputs.clear();
418
419    // Detect the path of the `commonware-runtime` crate. If it has been renamed or
420    // this macro is being used within the `commonware-runtime` crate itself, adjust
421    // the path accordingly.
422    let rt_path = match crate_name("commonware-runtime") {
423        Ok(FoundCrate::Itself) => quote!(crate),
424        Ok(FoundCrate::Name(name)) => {
425            let ident = syn::Ident::new(&name, Span::call_site());
426            quote!(#ident)
427        }
428        Err(_) => quote!(::commonware_runtime), // fallback
429    };
430
431    let expanded = quote! {
432        // Inner test function runs the actual test logic, accepting the TraceStorage
433        // created by the harness.
434        #(#attrs)*
435        #vis #inner_sig #block
436
437        #[test]
438        #vis #outer_sig {
439            use ::tracing_subscriber::{Layer, fmt, Registry, layer::SubscriberExt, util::SubscriberInitExt};
440            use ::tracing::{Dispatch, dispatcher};
441            use #rt_path::telemetry::traces::collector::{CollectingLayer, TraceStorage};
442
443            let trace_store = TraceStorage::default();
444            let collecting_layer = CollectingLayer::new(trace_store.clone());
445
446            let fmt_layer = fmt::layer()
447                .with_test_writer()
448                .with_line_number(true)
449                .with_span_events(fmt::format::FmtSpan::CLOSE)
450                .with_filter(#log_level);
451
452            let subscriber = Registry::default().with(collecting_layer).with(fmt_layer);
453            let dispatcher = Dispatch::new(subscriber);
454            dispatcher::with_default(&dispatcher, || {
455                #inner_ident(trace_store);
456            });
457        }
458    };
459
460    TokenStream::from(expanded)
461}
462
463struct SelectInput {
464    branches: Vec<Branch>,
465}
466
467struct Branch {
468    pattern: Pat,
469    future: Expr,
470    body: Expr,
471}
472
473/// Branch for [select_loop!] with optional `else` clause for `Some` patterns.
474struct SelectLoopBranch {
475    pattern: Pat,
476    future: Expr,
477    else_body: Option<Expr>,
478    body: Expr,
479}
480
481impl Parse for SelectInput {
482    fn parse(input: ParseStream<'_>) -> Result<Self> {
483        let mut branches = Vec::new();
484
485        while !input.is_empty() {
486            let pattern = Pat::parse_single(input)?;
487            input.parse::<Token![=]>()?;
488            let future: Expr = input.parse()?;
489            input.parse::<Token![=>]>()?;
490            let body: Expr = input.parse()?;
491
492            branches.push(Branch {
493                pattern,
494                future,
495                body,
496            });
497
498            if input.peek(Token![,]) {
499                input.parse::<Token![,]>()?;
500            } else {
501                break;
502            }
503        }
504
505        Ok(Self { branches })
506    }
507}
508
509#[proc_macro]
510pub fn select(input: TokenStream) -> TokenStream {
511    // Parse the input tokens
512    let SelectInput { branches } = parse_macro_input!(input as SelectInput);
513
514    // Generate code from provided statements
515    let mut select_branches = Vec::new();
516    for Branch {
517        pattern,
518        future,
519        body,
520    } in branches.into_iter()
521    {
522        // Generate branch for `select!` macro
523        let branch_code = quote! {
524            #pattern = #future => #body,
525        };
526        select_branches.push(branch_code);
527    }
528
529    // Generate the final output code
530    quote! {
531        {
532            ::commonware_macros::__reexport::tokio::select! {
533                biased;
534                #(#select_branches)*
535            }
536        }
537    }
538    .into()
539}
540
541/// Input for [select_loop!].
542///
543/// Parses: `context, [on_start => expr,] on_stopped => expr, branches... [, on_end => expr]`
544struct SelectLoopInput {
545    context: Expr,
546    start_expr: Option<Expr>,
547    shutdown_expr: Expr,
548    branches: Vec<SelectLoopBranch>,
549    end_expr: Option<Expr>,
550}
551
552impl Parse for SelectLoopInput {
553    fn parse(input: ParseStream<'_>) -> Result<Self> {
554        // Parse context expression
555        let context: Expr = input.parse()?;
556        input.parse::<Token![,]>()?;
557
558        // Check for optional `on_start =>`
559        let start_expr = if input.peek(Ident) {
560            let ident: Ident = input.fork().parse()?;
561            if ident == "on_start" {
562                input.parse::<Ident>()?; // consume the ident
563                input.parse::<Token![=>]>()?;
564                let expr: Expr = input.parse()?;
565                input.parse::<Token![,]>()?;
566                Some(expr)
567            } else {
568                None
569            }
570        } else {
571            None
572        };
573
574        // Parse `on_stopped =>`
575        let on_stopped_ident: Ident = input.parse()?;
576        if on_stopped_ident != "on_stopped" {
577            return Err(Error::new(
578                on_stopped_ident.span(),
579                "expected `on_stopped` keyword",
580            ));
581        }
582        input.parse::<Token![=>]>()?;
583
584        // Parse shutdown expression
585        let shutdown_expr: Expr = input.parse()?;
586
587        // Parse comma after shutdown expression
588        input.parse::<Token![,]>()?;
589
590        // Parse branches directly (no surrounding braces)
591        // Stop when we see `on_end` or reach end of input
592        let mut branches = Vec::new();
593        while !input.is_empty() {
594            // Check if next token is `on_end`
595            if input.peek(Ident) {
596                let ident: Ident = input.fork().parse()?;
597                if ident == "on_end" {
598                    break;
599                }
600            }
601
602            let pattern = Pat::parse_single(input)?;
603            input.parse::<Token![=]>()?;
604            let future: Expr = input.parse()?;
605
606            // Parse optional else clause: `else expr`
607            let else_body = if input.peek(Token![else]) {
608                input.parse::<Token![else]>()?;
609                Some(input.parse::<Expr>()?)
610            } else {
611                None
612            };
613
614            input.parse::<Token![=>]>()?;
615            let body: Expr = input.parse()?;
616
617            branches.push(SelectLoopBranch {
618                pattern,
619                future,
620                else_body,
621                body,
622            });
623
624            if input.peek(Token![,]) {
625                input.parse::<Token![,]>()?;
626            } else {
627                break;
628            }
629        }
630
631        // Check for optional `on_end =>`
632        let end_expr = if !input.is_empty() && input.peek(Ident) {
633            let ident: Ident = input.parse()?;
634            if ident == "on_end" {
635                input.parse::<Token![=>]>()?;
636                let expr: Expr = input.parse()?;
637                if input.peek(Token![,]) {
638                    input.parse::<Token![,]>()?;
639                }
640                Some(expr)
641            } else {
642                return Err(Error::new(ident.span(), "expected `on_end` keyword"));
643            }
644        } else {
645            None
646        };
647
648        Ok(Self {
649            context,
650            start_expr,
651            shutdown_expr,
652            branches,
653            end_expr,
654        })
655    }
656}
657
658#[proc_macro]
659pub fn select_loop(input: TokenStream) -> TokenStream {
660    let SelectLoopInput {
661        context,
662        start_expr,
663        shutdown_expr,
664        branches,
665        end_expr,
666    } = parse_macro_input!(input as SelectLoopInput);
667
668    fn is_irrefutable(pat: &Pat) -> bool {
669        match pat {
670            Pat::Wild(_) | Pat::Rest(_) => true,
671            Pat::Ident(i) => i.subpat.as_ref().is_none_or(|(_, p)| is_irrefutable(p)),
672            Pat::Type(t) => is_irrefutable(&t.pat),
673            Pat::Tuple(t) => t.elems.iter().all(is_irrefutable),
674            Pat::Reference(r) => is_irrefutable(&r.pat),
675            Pat::Paren(p) => is_irrefutable(&p.pat),
676            _ => false,
677        }
678    }
679
680    for b in &branches {
681        if b.else_body.is_none() && !is_irrefutable(&b.pattern) {
682            return Error::new_spanned(
683                &b.pattern,
684                "refutable patterns require an else clause: \
685                 `Some(msg) = future else break => { ... }`",
686            )
687            .to_compile_error()
688            .into();
689        }
690    }
691
692    // Convert branches to tokens for the inner select!
693    let branch_tokens: Vec<_> = branches
694        .iter()
695        .map(|b| {
696            let pattern = &b.pattern;
697            let future = &b.future;
698            let body = &b.body;
699
700            // If else clause is present, use let-else to unwrap
701            b.else_body.as_ref().map_or_else(
702                // No else: normal pattern binding (already validated as irrefutable)
703                || quote! { #pattern = #future => #body, },
704                // With else: use let-else for refutable patterns
705                |else_expr| {
706                    quote! {
707                        __select_result = #future => {
708                            let #pattern = __select_result else { #else_expr };
709                            #body
710                        },
711                    }
712                },
713            )
714        })
715        .collect();
716
717    // Helper to convert an expression to tokens, inlining block contents
718    // to preserve variable scope
719    fn expr_to_tokens(expr: &Expr) -> proc_macro2::TokenStream {
720        match expr {
721            Expr::Block(block) => {
722                let stmts = &block.block.stmts;
723                quote! { #(#stmts)* }
724            }
725            other => quote! { #other; },
726        }
727    }
728
729    // Generate on_start and on_end tokens if present
730    let on_start_tokens = start_expr.as_ref().map(expr_to_tokens);
731    let on_end_tokens = end_expr.as_ref().map(expr_to_tokens);
732    let shutdown_tokens = expr_to_tokens(&shutdown_expr);
733
734    quote! {
735        {
736            let mut shutdown = #context.stopped();
737            loop {
738                #on_start_tokens
739
740                commonware_macros::select! {
741                    _ = &mut shutdown => {
742                        #shutdown_tokens
743
744                        // Break the loop after handling shutdown. Some implementations
745                        // may divert control flow themselves, so this may be unused.
746                        #[allow(unreachable_code)]
747                        break;
748                    },
749                    #(#branch_tokens)*
750                }
751
752                #on_end_tokens
753            }
754        }
755    }
756    .into()
757}