//! Implementation of the `#[assert_instr]` macro //! //! This macro is used when testing the `stdsimd` crate and is used to generate //! test cases to assert that functions do indeed contain the instructions that //! we're expecting them to contain. //! //! The procedural macro here is relatively simple, it simply appends a //! `#[test]` function to the original token stream which asserts that the //! function itself contains the relevant instruction. extern crate proc_macro; extern crate proc_macro2; #[macro_use] extern crate quote; extern crate syn; use proc_macro2::TokenStream; use quote::ToTokens; #[proc_macro_attribute] pub fn assert_instr( attr: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let invoc = syn::parse::(attr).expect("expected #[assert_instr(instr, a = b, ...)]"); let item = syn::parse::(item).expect("must be attached to an item"); let func = match item { syn::Item::Fn(ref f) => f, _ => panic!("must be attached to a function"), }; let instr = &invoc.instr; let name = &func.ident; // Disable assert_instr for x86 targets compiled with avx enabled, which // causes LLVM to generate different intrinsics that the ones we are // testing for. let disable_assert_instr = std::env::var("STDSIMD_DISABLE_ASSERT_INSTR").is_ok(); let instr_str = instr .replace('.', "_") .replace(|c: char| c.is_whitespace(), ""); let assert_name = syn::Ident::new(&format!("assert_{}_{}", name, instr_str), name.span()); let shim_name = syn::Ident::new(&format!("{}_shim", name), name.span()); let mut inputs = Vec::new(); let mut input_vals = Vec::new(); let ret = &func.decl.output; for arg in func.decl.inputs.iter() { let capture = match *arg { syn::FnArg::Captured(ref c) => c, ref v => panic!( "arguments must not have patterns: `{:?}`", v.clone().into_token_stream() ), }; let ident = match capture.pat { syn::Pat::Ident(ref i) => &i.ident, _ => panic!("must have bare arguments"), }; if let Some(&(_, ref tts)) = invoc.args.iter().find(|a| *ident == a.0) { input_vals.push(quote! { #tts }); } else { inputs.push(capture); input_vals.push(quote! { #ident }); } } let attrs = func .attrs .iter() .filter(|attr| { attr.path .segments .first() .expect("attr.path.segments.first() failed") .value() .ident .to_string() .starts_with("target") }) .collect::>(); let attrs = Append(&attrs); // Use an ABI on Windows that passes SIMD values in registers, like what // happens on Unix (I think?) by default. let abi = if cfg!(windows) { syn::LitStr::new("vectorcall", proc_macro2::Span::call_site()) } else { syn::LitStr::new("C", proc_macro2::Span::call_site()) }; let shim_name_str = format!("{}{}", shim_name, assert_name); let to_test = quote! { #attrs unsafe extern #abi fn #shim_name(#(#inputs),*) #ret { // The compiler in optimized mode by default runs a pass called // "mergefunc" where it'll merge functions that look identical. // Turns out some intrinsics produce identical code and they're // folded together, meaning that one just jumps to another. This // messes up our inspection of the disassembly of this function and // we're not a huge fan of that. // // To thwart this pass and prevent functions from being merged we // generate some code that's hopefully very tight in terms of // codegen but is otherwise unique to prevent code from being // folded. ::stdsimd_test::_DONT_DEDUP = #shim_name_str; #name(#(#input_vals),*) } }; // If instruction tests are disabled avoid emitting this shim at all, just // return the original item without our attribute. if !cfg!(optimized) || disable_assert_instr { return (quote! { #item }).into(); } let tts: TokenStream = quote! { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] #[allow(non_snake_case)] fn #assert_name() { #to_test ::stdsimd_test::assert(#shim_name as usize, stringify!(#shim_name), #instr); } }; // why? necessary now to get tests to work? let tts: TokenStream = tts.to_string().parse().expect("cannot parse tokenstream"); let tts: TokenStream = quote! { #item #tts }; tts.into() } struct Invoc { instr: String, args: Vec<(syn::Ident, syn::Expr)>, } impl syn::parse::Parse for Invoc { fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { use syn::Token; let instr = match input.parse::() { Ok(s) => s.to_string(), Err(_) => input.parse::()?.value(), }; let mut args = Vec::new(); while input.parse::().is_ok() { let name = input.parse::()?; input.parse::()?; let expr = input.parse::()?; args.push((name, expr)); } Ok(Self { instr, args }) } } struct Append(T); impl quote::ToTokens for Append where T: Clone + IntoIterator, T::Item: quote::ToTokens, { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { for item in self.0.clone() { item.to_tokens(tokens); } } }