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_macro2::Span;
14use proc_macro_crate::{crate_name, FoundCrate};
15use quote::{format_ident, quote};
16use syn::{
17    braced,
18    parse::{Parse, ParseStream, Result},
19    parse_macro_input, Error, Expr, Ident, ItemFn, LitInt, LitStr, Pat, Token, Visibility,
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                        .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
325                )
326                .with(filter);
327            let dispatcher = tracing::Dispatch::new(subscriber);
328
329            // Set the subscriber for the scope of the test
330            tracing::dispatcher::with_default(&dispatcher, || {
331                #block
332            });
333        }
334    };
335    TokenStream::from(expanded)
336}
337
338#[proc_macro_attribute]
339pub fn test_group(attr: TokenStream, item: TokenStream) -> TokenStream {
340    if attr.is_empty() {
341        return Error::new(
342            Span::call_site(),
343            "test_group requires a string literal filter group name",
344        )
345        .to_compile_error()
346        .into();
347    }
348
349    let mut input = parse_macro_input!(item as ItemFn);
350    let group_literal = parse_macro_input!(attr as LitStr);
351
352    let group = match nextest::sanitize_group_literal(&group_literal) {
353        Ok(group) => group,
354        Err(err) => return err.to_compile_error().into(),
355    };
356    let groups = match configured_test_groups() {
357        Ok(groups) => groups,
358        Err(_) => {
359            // Don't fail the compilation if the file isn't found; just return the original input.
360            return TokenStream::from(quote!(#input));
361        }
362    };
363
364    if let Err(err) = nextest::ensure_group_known(groups, &group, group_literal.span()) {
365        return err.to_compile_error().into();
366    }
367
368    let original_name = input.sig.ident.to_string();
369    let new_ident = Ident::new(&format!("{original_name}_{group}_"), input.sig.ident.span());
370
371    input.sig.ident = new_ident;
372
373    TokenStream::from(quote!(#input))
374}
375
376#[proc_macro_attribute]
377pub fn test_collect_traces(attr: TokenStream, item: TokenStream) -> TokenStream {
378    let input = parse_macro_input!(item as ItemFn);
379
380    // Parse the attribute argument for log level
381    let log_level = if attr.is_empty() {
382        // Default log level is DEBUG
383        quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG }
384    } else {
385        // Parse the attribute as a string literal
386        let level_str = parse_macro_input!(attr as LitStr);
387        let level_ident = level_str.value().to_uppercase();
388        match level_ident.as_str() {
389            "TRACE" => quote! { ::tracing_subscriber::filter::LevelFilter::TRACE },
390            "DEBUG" => quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG },
391            "INFO" => quote! { ::tracing_subscriber::filter::LevelFilter::INFO },
392            "WARN" => quote! { ::tracing_subscriber::filter::LevelFilter::WARN },
393            "ERROR" => quote! { ::tracing_subscriber::filter::LevelFilter::ERROR },
394            _ => {
395                // Return a compile error for invalid log levels
396                return Error::new_spanned(
397                    level_str,
398                    "Invalid log level. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR.",
399                )
400                .to_compile_error()
401                .into();
402            }
403        }
404    };
405
406    let attrs = input.attrs;
407    let vis = input.vis;
408    let sig = input.sig;
409    let block = input.block;
410
411    // Create the signature of the inner function that takes the TraceStorage.
412    let inner_ident = format_ident!("__{}_inner_traced", sig.ident);
413    let mut inner_sig = sig.clone();
414    inner_sig.ident = inner_ident.clone();
415
416    // Create the signature of the outer test function.
417    let mut outer_sig = sig;
418    outer_sig.inputs.clear();
419
420    // Detect the path of the `commonware-runtime` crate. If it has been renamed or
421    // this macro is being used within the `commonware-runtime` crate itself, adjust
422    // the path accordingly.
423    let rt_path = match crate_name("commonware-runtime") {
424        Ok(FoundCrate::Itself) => quote!(crate),
425        Ok(FoundCrate::Name(name)) => {
426            let ident = syn::Ident::new(&name, Span::call_site());
427            quote!(#ident)
428        }
429        Err(_) => quote!(::commonware_runtime), // fallback
430    };
431
432    let expanded = quote! {
433        // Inner test function runs the actual test logic, accepting the TraceStorage
434        // created by the harness.
435        #(#attrs)*
436        #vis #inner_sig #block
437
438        #[test]
439        #vis #outer_sig {
440            use ::tracing_subscriber::{Layer, fmt, Registry, layer::SubscriberExt, util::SubscriberInitExt};
441            use ::tracing::{Dispatch, dispatcher};
442            use #rt_path::telemetry::traces::collector::{CollectingLayer, TraceStorage};
443
444            let trace_store = TraceStorage::default();
445            let collecting_layer = CollectingLayer::new(trace_store.clone());
446
447            let fmt_layer = fmt::layer()
448                .with_test_writer()
449                .with_line_number(true)
450                .with_span_events(fmt::format::FmtSpan::CLOSE)
451                .with_filter(#log_level);
452
453            let subscriber = Registry::default().with(collecting_layer).with(fmt_layer);
454            let dispatcher = Dispatch::new(subscriber);
455            dispatcher::with_default(&dispatcher, || {
456                #inner_ident(trace_store);
457            });
458        }
459    };
460
461    TokenStream::from(expanded)
462}
463
464struct SelectInput {
465    branches: Vec<Branch>,
466}
467
468struct Branch {
469    pattern: Pat,
470    future: Expr,
471    body: Expr,
472}
473
474/// Branch for [select_loop!] with optional `else` clause for `Some` patterns.
475struct SelectLoopBranch {
476    pattern: Pat,
477    future: Expr,
478    else_body: Option<Expr>,
479    body: Expr,
480}
481
482impl Parse for SelectInput {
483    fn parse(input: ParseStream<'_>) -> Result<Self> {
484        let mut branches = Vec::new();
485
486        while !input.is_empty() {
487            let pattern = Pat::parse_single(input)?;
488            input.parse::<Token![=]>()?;
489            let future: Expr = input.parse()?;
490            input.parse::<Token![=>]>()?;
491            let body: Expr = input.parse()?;
492
493            branches.push(Branch {
494                pattern,
495                future,
496                body,
497            });
498
499            if input.peek(Token![,]) {
500                input.parse::<Token![,]>()?;
501            } else {
502                break;
503            }
504        }
505
506        Ok(Self { branches })
507    }
508}
509
510#[proc_macro]
511pub fn select(input: TokenStream) -> TokenStream {
512    // Parse the input tokens
513    let SelectInput { branches } = parse_macro_input!(input as SelectInput);
514
515    // Generate code from provided statements
516    let mut select_branches = Vec::new();
517    for Branch {
518        pattern,
519        future,
520        body,
521    } in branches.into_iter()
522    {
523        // Generate branch for `select!` macro
524        let branch_code = quote! {
525            #pattern = #future => #body,
526        };
527        select_branches.push(branch_code);
528    }
529
530    // Generate the final output code
531    quote! {
532        {
533            ::commonware_macros::__reexport::tokio::select! {
534                biased;
535                #(#select_branches)*
536            }
537        }
538    }
539    .into()
540}
541
542/// Input for [select_loop!].
543///
544/// Parses: `context, [on_start => expr,] on_stopped => expr, branches... [, on_end => expr]`
545struct SelectLoopInput {
546    context: Expr,
547    start_expr: Option<Expr>,
548    shutdown_expr: Expr,
549    branches: Vec<SelectLoopBranch>,
550    end_expr: Option<Expr>,
551}
552
553impl Parse for SelectLoopInput {
554    fn parse(input: ParseStream<'_>) -> Result<Self> {
555        // Parse context expression
556        let context: Expr = input.parse()?;
557        input.parse::<Token![,]>()?;
558
559        // Check for optional `on_start =>`
560        let start_expr = if input.peek(Ident) {
561            let ident: Ident = input.fork().parse()?;
562            if ident == "on_start" {
563                input.parse::<Ident>()?; // consume the ident
564                input.parse::<Token![=>]>()?;
565                let expr: Expr = input.parse()?;
566                input.parse::<Token![,]>()?;
567                Some(expr)
568            } else {
569                None
570            }
571        } else {
572            None
573        };
574
575        // Parse `on_stopped =>`
576        let on_stopped_ident: Ident = input.parse()?;
577        if on_stopped_ident != "on_stopped" {
578            return Err(Error::new(
579                on_stopped_ident.span(),
580                "expected `on_stopped` keyword",
581            ));
582        }
583        input.parse::<Token![=>]>()?;
584
585        // Parse shutdown expression
586        let shutdown_expr: Expr = input.parse()?;
587
588        // Parse comma after shutdown expression
589        input.parse::<Token![,]>()?;
590
591        // Parse branches directly (no surrounding braces)
592        // Stop when we see `on_end` or reach end of input
593        let mut branches = Vec::new();
594        while !input.is_empty() {
595            // Check if next token is `on_end`
596            if input.peek(Ident) {
597                let ident: Ident = input.fork().parse()?;
598                if ident == "on_end" {
599                    break;
600                }
601            }
602
603            let pattern = Pat::parse_single(input)?;
604            input.parse::<Token![=]>()?;
605            let future: Expr = input.parse()?;
606
607            // Parse optional else clause: `else expr`
608            let else_body = if input.peek(Token![else]) {
609                input.parse::<Token![else]>()?;
610                Some(input.parse::<Expr>()?)
611            } else {
612                None
613            };
614
615            input.parse::<Token![=>]>()?;
616            let body: Expr = input.parse()?;
617
618            branches.push(SelectLoopBranch {
619                pattern,
620                future,
621                else_body,
622                body,
623            });
624
625            if input.peek(Token![,]) {
626                input.parse::<Token![,]>()?;
627            } else {
628                break;
629            }
630        }
631
632        // Check for optional `on_end =>`
633        let end_expr = if !input.is_empty() && input.peek(Ident) {
634            let ident: Ident = input.parse()?;
635            if ident == "on_end" {
636                input.parse::<Token![=>]>()?;
637                let expr: Expr = input.parse()?;
638                if input.peek(Token![,]) {
639                    input.parse::<Token![,]>()?;
640                }
641                Some(expr)
642            } else {
643                return Err(Error::new(ident.span(), "expected `on_end` keyword"));
644            }
645        } else {
646            None
647        };
648
649        Ok(Self {
650            context,
651            start_expr,
652            shutdown_expr,
653            branches,
654            end_expr,
655        })
656    }
657}
658
659#[proc_macro]
660pub fn select_loop(input: TokenStream) -> TokenStream {
661    let SelectLoopInput {
662        context,
663        start_expr,
664        shutdown_expr,
665        branches,
666        end_expr,
667    } = parse_macro_input!(input as SelectLoopInput);
668
669    fn is_irrefutable(pat: &Pat) -> bool {
670        match pat {
671            Pat::Wild(_) | Pat::Rest(_) => true,
672            Pat::Ident(i) => i.subpat.as_ref().is_none_or(|(_, p)| is_irrefutable(p)),
673            Pat::Type(t) => is_irrefutable(&t.pat),
674            Pat::Tuple(t) => t.elems.iter().all(is_irrefutable),
675            Pat::Reference(r) => is_irrefutable(&r.pat),
676            Pat::Paren(p) => is_irrefutable(&p.pat),
677            _ => false,
678        }
679    }
680
681    for b in &branches {
682        if b.else_body.is_none() && !is_irrefutable(&b.pattern) {
683            return Error::new_spanned(
684                &b.pattern,
685                "refutable patterns require an else clause: \
686                 `Some(msg) = future else break => { ... }`",
687            )
688            .to_compile_error()
689            .into();
690        }
691    }
692
693    // Convert branches to tokens for the inner select!
694    let branch_tokens: Vec<_> = branches
695        .iter()
696        .map(|b| {
697            let pattern = &b.pattern;
698            let future = &b.future;
699            let body = &b.body;
700
701            // If else clause is present, use let-else to unwrap
702            b.else_body.as_ref().map_or_else(
703                // No else: normal pattern binding (already validated as irrefutable)
704                || quote! { #pattern = #future => #body, },
705                // With else: use let-else for refutable patterns
706                |else_expr| {
707                    quote! {
708                        __select_result = #future => {
709                            let #pattern = __select_result else { #else_expr };
710                            #body
711                        },
712                    }
713                },
714            )
715        })
716        .collect();
717
718    // Helper to convert an expression to tokens, inlining block contents
719    // to preserve variable scope
720    fn expr_to_tokens(expr: &Expr) -> proc_macro2::TokenStream {
721        match expr {
722            Expr::Block(block) => {
723                let stmts = &block.block.stmts;
724                quote! { #(#stmts)* }
725            }
726            other => quote! { #other; },
727        }
728    }
729
730    // Generate on_start and on_end tokens if present
731    let on_start_tokens = start_expr.as_ref().map(expr_to_tokens);
732    let on_end_tokens = end_expr.as_ref().map(expr_to_tokens);
733    let shutdown_tokens = expr_to_tokens(&shutdown_expr);
734
735    quote! {
736        {
737            let mut shutdown = #context.stopped();
738            loop {
739                #on_start_tokens
740
741                commonware_macros::select! {
742                    _ = &mut shutdown => {
743                        #shutdown_tokens
744
745                        // Break the loop after handling shutdown. Some implementations
746                        // may divert control flow themselves, so this may be unused.
747                        #[allow(unreachable_code)]
748                        break;
749                    },
750                    #(#branch_tokens)*
751                }
752
753                #on_end_tokens
754            }
755        }
756    }
757    .into()
758}