Skip to main content

runite_proc_macros/
lib.rs

1//! Procedural macros consumed through `runite`.
2//!
3//! This crate provides the implementations for [`#[runite::main]`](main) and
4//! [`#[runite::test]`](test), the attribute macros re-exported by the `runite`
5//! crate. It is an implementation detail and is not intended to be used
6//! directly; depend on `runite` and invoke the macros as `#[runite::main]` /
7//! `#[runite::test]` instead.
8
9#![deny(missing_docs)]
10
11use proc_macro::TokenStream;
12use proc_macro2::TokenStream as TokenStream2;
13use quote::{format_ident, quote};
14use syn::parse::{Parse, ParseStream};
15use syn::{Error, ItemFn, LitStr, Path, Token, parse_macro_input, parse_quote};
16
17/// Marks `fn main` as the runite entry point.
18///
19/// Works for both synchronous and `async` entry points. An `async fn main` has
20/// its future driven to completion with `runite::block_on`, so the program
21/// ends when `main`'s future resolves (like `std`'s `main`, any still-running
22/// background tasks are abandoned) and the function's return value is honored:
23/// an `async fn main() -> Result<…>` that returns `Err` reports a non-zero exit
24/// status through [`std::process::Termination`], instead of silently exiting 0.
25/// A synchronous `fn main` runs its body, drives the event loop to drain any
26/// tasks it spawned via `runite::run`, then returns its value.
27///
28/// To use a renamed `runite` dependency, pass the path:
29/// `#[runite::main(crate = "my_runite")]`.
30#[proc_macro_attribute]
31pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
32    expand(attr, item, EntryKind::Main)
33}
34
35/// Marks an `async fn` as a runite-driven test.
36///
37/// Generates a `#[test]` wrapper that drives the test's future to completion
38/// with `runite::block_on`. The test function may return anything that
39/// implements [`std::process::Termination`] (for example `Result<(), E>` so the
40/// body can use `?`). Test attributes such as `#[ignore]` and `#[should_panic]`
41/// placed below `#[runite::test]` are forwarded to the generated test.
42///
43/// To use a renamed `runite` dependency, pass the path:
44/// `#[runite::test(crate = "my_runite")]`.
45#[proc_macro_attribute]
46pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
47    expand(attr, item, EntryKind::Test)
48}
49
50#[derive(Clone, Copy, PartialEq, Eq)]
51enum EntryKind {
52    Main,
53    Test,
54}
55
56impl EntryKind {
57    fn noun(self) -> &'static str {
58        match self {
59            EntryKind::Main => "entry",
60            EntryKind::Test => "test",
61        }
62    }
63}
64
65/// Parsed attribute arguments: an optional `crate = "path"` override for the
66/// `runite` crate path (to support renamed dependencies).
67struct EntryArgs {
68    crate_path: Path,
69}
70
71impl Parse for EntryArgs {
72    fn parse(input: ParseStream) -> syn::Result<Self> {
73        let mut crate_path: Path = parse_quote!(::runite);
74        if input.is_empty() {
75            return Ok(Self { crate_path });
76        }
77
78        // `crate` is a keyword, so parse it as one rather than as an identifier.
79        if !input.peek(Token![crate]) {
80            return Err(input.error("runite entry attributes accept only `crate = \"...\"`"));
81        }
82        input.parse::<Token![crate]>()?;
83        input.parse::<Token![=]>()?;
84        let value: LitStr = input.parse()?;
85        crate_path = value.parse()?;
86
87        if !input.is_empty() {
88            return Err(input.error("unexpected trailing tokens after `crate = \"...\"`"));
89        }
90        Ok(Self { crate_path })
91    }
92}
93
94fn expand(attr: TokenStream, item: TokenStream, kind: EntryKind) -> TokenStream {
95    let args = parse_macro_input!(attr as EntryArgs);
96    let function = parse_macro_input!(item as ItemFn);
97    match validate(&function, kind) {
98        Ok(()) => generate(function, args.crate_path, kind).into(),
99        Err(error) => error.to_compile_error().into(),
100    }
101}
102
103fn validate(function: &ItemFn, kind: EntryKind) -> syn::Result<()> {
104    let signature = &function.sig;
105
106    if kind == EntryKind::Main && signature.ident != "main" {
107        return Err(Error::new_spanned(
108            &signature.ident,
109            "runite entry attribute must be attached to a function named `main`",
110        ));
111    }
112
113    let noun = kind.noun();
114    if !signature.inputs.is_empty() {
115        return Err(Error::new_spanned(
116            &signature.inputs,
117            format!("runite {noun} functions cannot take parameters"),
118        ));
119    }
120    if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() {
121        return Err(Error::new_spanned(
122            &signature.generics,
123            format!("runite {noun} functions cannot be generic"),
124        ));
125    }
126    if signature.constness.is_some() {
127        return Err(Error::new_spanned(
128            signature.fn_token,
129            format!("runite {noun} functions cannot be const"),
130        ));
131    }
132    if signature.unsafety.is_some() {
133        return Err(Error::new_spanned(
134            signature.fn_token,
135            format!("runite {noun} functions cannot be unsafe"),
136        ));
137    }
138    if signature.abi.is_some() {
139        return Err(Error::new_spanned(
140            &signature.abi,
141            format!("runite {noun} functions cannot declare an ABI"),
142        ));
143    }
144    if signature.variadic.is_some() {
145        return Err(Error::new_spanned(
146            &signature.variadic,
147            format!("runite {noun} functions cannot be variadic"),
148        ));
149    }
150
151    Ok(())
152}
153
154fn generate(function: ItemFn, crate_path: Path, kind: EntryKind) -> TokenStream2 {
155    let is_async = function.sig.asyncness.is_some();
156    let original_name = function.sig.ident.clone();
157    let output = function.sig.output.clone();
158    let implementation_name = format_ident!("__runite_runtime_internal_{}", original_name);
159
160    let mut implementation = function;
161    implementation.sig.ident = implementation_name.clone();
162
163    // For tests, hoist the user's attributes (e.g. `#[ignore]`,
164    // `#[should_panic]`) onto the generated `#[test]` wrapper where they belong,
165    // rather than leaving them on the inner implementation function.
166    let wrapper_attrs = match kind {
167        EntryKind::Test => std::mem::take(&mut implementation.attrs),
168        EntryKind::Main => Vec::new(),
169    };
170
171    // `async` bodies are driven to completion and their value returned; sync
172    // bodies run inline, then the loop is drained so spawned tasks execute, then
173    // the value is returned. Both preserve the original return type so that
174    // `Termination` (e.g. `Result`) governs the process exit / test outcome.
175    let drive = if is_async {
176        quote! { #crate_path::block_on(#implementation_name()) }
177    } else {
178        quote! {
179            let __runite_output = #implementation_name();
180            #crate_path::run();
181            __runite_output
182        }
183    };
184
185    let test_attr = match kind {
186        // Use the fully-qualified built-in `test` attribute so the expansion is
187        // robust even if `test` is shadowed at the call site.
188        EntryKind::Test => quote! { #[::core::prelude::v1::test] },
189        EntryKind::Main => quote! {},
190    };
191
192    quote! {
193        #implementation
194
195        #(#wrapper_attrs)*
196        #test_attr
197        fn #original_name() #output {
198            #drive
199        }
200    }
201}