1#![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
24struct 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
78fn 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
121struct 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
161struct 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 let predicate = if input.peek(Token![,]) {
174 input.parse::<Token![,]>()?;
175
176 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 let input = parse_macro_input!(item as ItemFn);
255
256 let attrs = input.attrs;
258 let vis = input.vis;
259 let mut sig = input.sig;
260 let block = input.block;
261
262 sig.asyncness
265 .take()
266 .expect("test_async macro can only be used with async functions");
267
268 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 let input = parse_macro_input!(item as ItemFn);
283
284 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 let attrs = input.attrs;
305 let vis = input.vis;
306 let sig = input.sig;
307 let block = input.block;
308
309 let expanded = quote! {
311 #[test]
312 #(#attrs)*
313 #vis #sig {
314 use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
315
316 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 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 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 let log_level = if attr.is_empty() {
381 quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG }
383 } else {
384 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 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 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 let mut outer_sig = sig;
417 outer_sig.inputs.clear();
418
419 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), };
430
431 let expanded = quote! {
432 #(#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
473struct 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 let SelectInput { branches } = parse_macro_input!(input as SelectInput);
513
514 let mut select_branches = Vec::new();
516 for Branch {
517 pattern,
518 future,
519 body,
520 } in branches.into_iter()
521 {
522 let branch_code = quote! {
524 #pattern = #future => #body,
525 };
526 select_branches.push(branch_code);
527 }
528
529 quote! {
531 {
532 ::commonware_macros::__reexport::tokio::select! {
533 biased;
534 #(#select_branches)*
535 }
536 }
537 }
538 .into()
539}
540
541struct 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 let context: Expr = input.parse()?;
556 input.parse::<Token![,]>()?;
557
558 let start_expr = if input.peek(Ident) {
560 let ident: Ident = input.fork().parse()?;
561 if ident == "on_start" {
562 input.parse::<Ident>()?; 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 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 let shutdown_expr: Expr = input.parse()?;
586
587 input.parse::<Token![,]>()?;
589
590 let mut branches = Vec::new();
593 while !input.is_empty() {
594 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 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 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 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 b.else_body.as_ref().map_or_else(
702 || quote! { #pattern = #future => #body, },
704 |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 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 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 #[allow(unreachable_code)]
747 break;
748 },
749 #(#branch_tokens)*
750 }
751
752 #on_end_tokens
753 }
754 }
755 }
756 .into()
757}