use parse::{Attrs, MacroOpts};
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, Signature, parse_macro_input};
mod parse;
const NO_SYNC_ERR: &str = "The vexide entrypoint must be marked `async`.";
const NO_UNSAFE_ERR: &str = "The vexide entrypoint must be not marked `unsafe`.";
const WRONG_ARGS_ERR: &str = "The vexide entrypoint must take a single parameter of type `vexide_devices::peripherals::Peripherals`";
fn verify_function_sig(sig: &Signature) -> Result<(), syn::Error> {
let mut error = None;
if sig.asyncness.is_none() {
let message = syn::Error::new_spanned(sig, NO_SYNC_ERR);
error.replace(message);
}
if sig.unsafety.is_some() {
let message = syn::Error::new_spanned(sig, NO_UNSAFE_ERR);
match error {
Some(ref mut e) => e.combine(message),
None => {
error.replace(message);
}
}
}
if sig.inputs.len() != 1 {
let message = syn::Error::new_spanned(sig, WRONG_ARGS_ERR);
match error {
Some(ref mut e) => e.combine(message),
None => {
error.replace(message);
}
}
}
match error {
Some(e) => Err(e),
None => Ok(()),
}
}
fn make_code_sig(opts: MacroOpts) -> proc_macro2::TokenStream {
let sig = if let Some(code_sig) = opts.code_sig {
quote! { #code_sig }
} else {
quote! { ::vexide::program::CodeSignature::new(
::vexide::program::ProgramType::User,
::vexide::program::ProgramOwner::Partner,
::vexide::program::ProgramOptions::empty(),
) }
};
quote! {
#[cfg_attr(target_os = "vexos", unsafe(link_section = ".code_signature"))]
#[used] #[unsafe(no_mangle)]
static __VEXIDE_CODE_SIGNATURE: ::vexide::program::CodeSignature = #sig;
}
}
fn make_entrypoint(inner: &ItemFn, opts: MacroOpts) -> proc_macro2::TokenStream {
match verify_function_sig(&inner.sig) {
Ok(()) => {}
Err(e) => return e.to_compile_error(),
}
let inner_ident = inner.sig.ident.clone();
let ret_type = match &inner.sig.output {
syn::ReturnType::Default => quote! { () },
syn::ReturnType::Type(_, ty) => quote! { #ty },
};
let banner_theme = if let Some(theme) = opts.banner_theme {
quote! { #theme }
} else {
quote! { ::vexide::startup::banner::themes::THEME_DEFAULT }
};
let banner_print = if opts.banner_enabled {
quote! {
::vexide::startup::banner::print(#banner_theme);
}
} else {
quote! {}
};
quote! {
fn main() -> #ret_type {
unsafe {
::vexide::startup::startup();
}
#banner_print
#inner
::vexide::runtime::block_on(
#inner_ident(::vexide::peripherals::Peripherals::take().unwrap())
)
}
}
}
#[proc_macro_attribute]
pub fn main(attrs: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let opts = MacroOpts::from(parse_macro_input!(attrs as Attrs));
let entrypoint = make_entrypoint(&item, opts.clone());
let code_signature = make_code_sig(opts);
quote! {
const _: () = {
#code_signature
};
#entrypoint
}
.into()
}
#[proc_macro_attribute]
#[doc(hidden)]
pub fn main_fail(_args: TokenStream, _item: TokenStream) -> TokenStream {
syn::Error::new(
proc_macro2::Span::call_site(),
"The #[vexide::main] macro requires the `core`, `async`, `startup`, and `devices` features to be enabled.",
)
.to_compile_error()
.into()
}
#[proc_macro_attribute]
pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
if input.sig.asyncness.is_none() {
return syn::Error::new_spanned(input.sig.fn_token, "#[vexide::test] requires an async fn")
.to_compile_error()
.into();
}
let vis = &input.vis;
let ident = &input.sig.ident;
let inputs = &input.sig.inputs;
let block = &input.block;
quote! {
#[::core::prelude::v1::test]
#vis fn #ident() {
async fn #ident(#inputs) #block
::vexide::runtime::block_on(
#ident(unsafe { ::vexide::peripherals::Peripherals::steal() })
)
}
}
.into()
}
#[proc_macro_attribute]
#[doc(hidden)]
pub fn test_fail(_args: TokenStream, _item: TokenStream) -> TokenStream {
syn::Error::new(
proc_macro2::Span::call_site(),
"The #[vexide::test] macro requires the `core`, `async`, `startup`, and `devices` features to be enabled.",
)
.to_compile_error()
.into()
}
#[cfg(test)]
mod test {
use quote::quote;
use syn::{Ident, ItemFn};
use super::{make_code_sig, make_entrypoint};
use crate::{MacroOpts, NO_SYNC_ERR, NO_UNSAFE_ERR, WRONG_ARGS_ERR};
#[test]
fn wraps_main_fn() {
let source = quote! {
async fn main(_peripherals: Peripherals) {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let output = make_entrypoint(&input, MacroOpts::default());
assert_eq!(
output.to_string(),
quote! {
fn main() -> () {
unsafe {
::vexide::startup::startup();
}
::vexide::startup::banner::print(::vexide::startup::banner::themes::THEME_DEFAULT);
#source
::vexide::runtime::block_on(
main(::vexide::peripherals::Peripherals::take().unwrap())
)
}
}
.to_string()
);
}
#[test]
fn toggles_banner_using_parsed_opts() {
let source = quote! {
async fn main(_peripherals: Peripherals) {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let entrypoint = make_entrypoint(
&input,
MacroOpts {
banner_enabled: false,
banner_theme: None,
code_sig: None,
},
);
assert!(!entrypoint.to_string().contains("banner"));
let entrypoint = make_entrypoint(
&input,
MacroOpts {
banner_enabled: true,
banner_theme: None,
code_sig: None,
},
);
assert!(entrypoint.to_string().contains("banner"));
}
#[test]
fn uses_custom_code_sig_from_parsed_opts() {
let code_sig = make_code_sig(MacroOpts {
banner_enabled: false,
banner_theme: None,
code_sig: Some(Ident::new(
"__custom_code_sig_ident__",
proc_macro2::Span::call_site(),
)),
});
assert!(code_sig.to_string().contains(
"static __VEXIDE_CODE_SIGNATURE : :: vexide :: program :: CodeSignature = __custom_code_sig_ident__ ;"
));
}
#[test]
fn requires_async() {
let source = quote! {
fn main(_peripherals: Peripherals) {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let output = make_entrypoint(&input, MacroOpts::default());
assert!(output.to_string().contains(NO_SYNC_ERR));
}
#[test]
fn requires_safe() {
let source = quote! {
async unsafe fn main(_peripherals: Peripherals) {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let output = make_entrypoint(&input, MacroOpts::default());
assert!(output.to_string().contains(NO_UNSAFE_ERR));
}
#[test]
fn disallows_0_args() {
let source = quote! {
async fn main() {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let output = make_entrypoint(&input, MacroOpts::default());
assert!(output.to_string().contains(WRONG_ARGS_ERR));
}
#[test]
fn disallows_2_args() {
let source = quote! {
async fn main(_peripherals: Peripherals, _other: Peripherals) {
println!("Hello, world!");
}
};
let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
let output = make_entrypoint(&input, MacroOpts::default());
assert!(output.to_string().contains(WRONG_ARGS_ERR));
}
}