#![deny(missing_docs)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::parse::{Parse, ParseStream};
use syn::{Error, ItemFn, LitStr, Path, Token, parse_macro_input, parse_quote};
#[proc_macro_attribute]
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
expand(attr, item, EntryKind::Main)
}
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
expand(attr, item, EntryKind::Test)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EntryKind {
Main,
Test,
}
impl EntryKind {
fn noun(self) -> &'static str {
match self {
EntryKind::Main => "entry",
EntryKind::Test => "test",
}
}
}
struct EntryArgs {
crate_path: Path,
}
impl Parse for EntryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut crate_path: Path = parse_quote!(::runite);
if input.is_empty() {
return Ok(Self { crate_path });
}
if !input.peek(Token![crate]) {
return Err(input.error("runite entry attributes accept only `crate = \"...\"`"));
}
input.parse::<Token![crate]>()?;
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
crate_path = value.parse()?;
if !input.is_empty() {
return Err(input.error("unexpected trailing tokens after `crate = \"...\"`"));
}
Ok(Self { crate_path })
}
}
fn expand(attr: TokenStream, item: TokenStream, kind: EntryKind) -> TokenStream {
let args = parse_macro_input!(attr as EntryArgs);
let function = parse_macro_input!(item as ItemFn);
match validate(&function, kind) {
Ok(()) => generate(function, args.crate_path, kind).into(),
Err(error) => error.to_compile_error().into(),
}
}
fn validate(function: &ItemFn, kind: EntryKind) -> syn::Result<()> {
let signature = &function.sig;
if kind == EntryKind::Main && signature.ident != "main" {
return Err(Error::new_spanned(
&signature.ident,
"runite entry attribute must be attached to a function named `main`",
));
}
let noun = kind.noun();
if !signature.inputs.is_empty() {
return Err(Error::new_spanned(
&signature.inputs,
format!("runite {noun} functions cannot take parameters"),
));
}
if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() {
return Err(Error::new_spanned(
&signature.generics,
format!("runite {noun} functions cannot be generic"),
));
}
if signature.constness.is_some() {
return Err(Error::new_spanned(
signature.fn_token,
format!("runite {noun} functions cannot be const"),
));
}
if signature.unsafety.is_some() {
return Err(Error::new_spanned(
signature.fn_token,
format!("runite {noun} functions cannot be unsafe"),
));
}
if signature.abi.is_some() {
return Err(Error::new_spanned(
&signature.abi,
format!("runite {noun} functions cannot declare an ABI"),
));
}
if signature.variadic.is_some() {
return Err(Error::new_spanned(
&signature.variadic,
format!("runite {noun} functions cannot be variadic"),
));
}
Ok(())
}
fn generate(function: ItemFn, crate_path: Path, kind: EntryKind) -> TokenStream2 {
let is_async = function.sig.asyncness.is_some();
let original_name = function.sig.ident.clone();
let output = function.sig.output.clone();
let implementation_name = format_ident!("__runite_runtime_internal_{}", original_name);
let mut implementation = function;
implementation.sig.ident = implementation_name.clone();
let wrapper_attrs = match kind {
EntryKind::Test => std::mem::take(&mut implementation.attrs),
EntryKind::Main => Vec::new(),
};
let drive = if is_async {
quote! { #crate_path::block_on(#implementation_name()) }
} else {
quote! {
let __runite_output = #implementation_name();
#crate_path::run();
__runite_output
}
};
let test_attr = match kind {
EntryKind::Test => quote! { #[::core::prelude::v1::test] },
EntryKind::Main => quote! {},
};
quote! {
#implementation
#(#wrapper_attrs)*
#test_attr
fn #original_name() #output {
#drive
}
}
}