mirror of
https://github.com/rust-lang/rust.git
synced 2025-10-21 12:28:29 +00:00
261 lines
8.5 KiB
Rust
261 lines
8.5 KiB
Rust
//! Implementation of the `#[assert_instr]` macro
|
|
//!
|
|
//! This macro is used when testing the `stdarch` 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.
|
|
#![deny(rust_2018_idioms)]
|
|
|
|
#[macro_use]
|
|
extern crate quote;
|
|
|
|
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 = match syn::parse::<Invoc>(attr) {
|
|
Ok(s) => s,
|
|
Err(e) => return e.to_compile_error().into(),
|
|
};
|
|
let item = match syn::parse::<syn::Item>(item) {
|
|
Ok(s) => s,
|
|
Err(e) => return e.to_compile_error().into(),
|
|
};
|
|
let func = match item {
|
|
syn::Item::Fn(ref f) => f,
|
|
_ => panic!("must be attached to a function"),
|
|
};
|
|
|
|
let instr = &invoc.instr;
|
|
let name = &func.sig.ident;
|
|
let maybe_allow_deprecated = if func
|
|
.attrs
|
|
.iter()
|
|
.any(|attr| attr.path().is_ident("deprecated"))
|
|
{
|
|
quote! { #[allow(deprecated)] }
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// 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("STDARCH_DISABLE_ASSERT_INSTR").is_ok();
|
|
|
|
// Disable dedup guard. Only works if the LLVM MergeFunctions pass is disabled, e.g.
|
|
// with `-Z merge-functions=disabled` in RUSTFLAGS.
|
|
let disable_dedup_guard = std::env::var("STDARCH_DISABLE_DEDUP_GUARD").is_ok();
|
|
|
|
// 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 instr_str = instr
|
|
.replace(['.', '/', ':'], "_")
|
|
.replace(char::is_whitespace, "");
|
|
let assert_name = syn::Ident::new(&format!("assert_{name}_{instr_str}"), name.span());
|
|
// These name has to be unique enough for us to find it in the disassembly later on:
|
|
let shim_name = syn::Ident::new(
|
|
&format!("stdarch_test_shim_{name}_{instr_str}"),
|
|
name.span(),
|
|
);
|
|
let shim_name_ptr = syn::Ident::new(
|
|
&format!("stdarch_test_shim_{name}_{instr_str}_ptr").to_ascii_uppercase(),
|
|
name.span(),
|
|
);
|
|
let mut inputs = Vec::new();
|
|
let mut input_vals = Vec::new();
|
|
let mut const_vals = Vec::new();
|
|
let ret = &func.sig.output;
|
|
for arg in func.sig.inputs.iter() {
|
|
let capture = match *arg {
|
|
syn::FnArg::Typed(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((_, tokens)) = invoc.args.iter().find(|a| *ident == a.0) {
|
|
input_vals.push(quote! { #tokens });
|
|
} else {
|
|
inputs.push(capture);
|
|
input_vals.push(quote! { #ident });
|
|
}
|
|
}
|
|
for arg in func.sig.generics.params.iter() {
|
|
let c = match *arg {
|
|
syn::GenericParam::Const(ref c) => c,
|
|
ref v => panic!(
|
|
"only const generics are allowed: `{:?}`",
|
|
v.clone().into_token_stream()
|
|
),
|
|
};
|
|
if let Some((_, tokens)) = invoc.args.iter().find(|a| c.ident == a.0) {
|
|
const_vals.push(quote! { #tokens });
|
|
} else {
|
|
panic!("const generics must have a value for tests");
|
|
}
|
|
}
|
|
|
|
let attrs = func
|
|
.attrs
|
|
.iter()
|
|
.filter(|attr| {
|
|
attr.path()
|
|
.segments
|
|
.first()
|
|
.expect("attr.path.segments.first() failed")
|
|
.ident
|
|
.to_string()
|
|
.starts_with("target")
|
|
})
|
|
.collect::<Vec<_>>();
|
|
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) {
|
|
let target = std::env::var("TARGET").unwrap();
|
|
if target.contains("x86_64") {
|
|
syn::LitStr::new("sysv64", proc_macro2::Span::call_site())
|
|
} else {
|
|
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 = if disable_dedup_guard {
|
|
quote! {
|
|
#attrs
|
|
#maybe_allow_deprecated
|
|
#[no_mangle]
|
|
#[inline(never)]
|
|
pub unsafe extern #abi fn #shim_name(#(#inputs),*) #ret {
|
|
#name::<#(#const_vals),*>(#(#input_vals),*)
|
|
}
|
|
}
|
|
} else {
|
|
quote! {
|
|
|
|
const #shim_name_ptr : *const u8 = #shim_name_str.as_ptr();
|
|
|
|
#attrs
|
|
#maybe_allow_deprecated
|
|
#[no_mangle]
|
|
#[inline(never)]
|
|
pub 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.
|
|
::stdarch_test::_DONT_DEDUP = #shim_name_ptr;
|
|
#name::<#(#const_vals),*>(#(#input_vals),*)
|
|
}
|
|
}
|
|
};
|
|
|
|
let tokens: TokenStream = quote! {
|
|
#[test]
|
|
#[allow(non_snake_case)]
|
|
fn #assert_name() {
|
|
#to_test
|
|
|
|
::stdarch_test::assert(#shim_name as usize,
|
|
stringify!(#shim_name),
|
|
#instr);
|
|
}
|
|
};
|
|
|
|
let tokens: TokenStream = quote! {
|
|
#item
|
|
#tokens
|
|
};
|
|
tokens.into()
|
|
}
|
|
|
|
struct Invoc {
|
|
instr: String,
|
|
args: Vec<(syn::Ident, syn::Expr)>,
|
|
}
|
|
|
|
impl syn::parse::Parse for Invoc {
|
|
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
|
|
use syn::{ext::IdentExt, Token};
|
|
|
|
let mut instr = String::new();
|
|
while !input.is_empty() {
|
|
if input.parse::<Token![,]>().is_ok() {
|
|
break;
|
|
}
|
|
if let Ok(ident) = syn::Ident::parse_any(input) {
|
|
instr.push_str(&ident.to_string());
|
|
continue;
|
|
}
|
|
if input.parse::<Token![.]>().is_ok() {
|
|
instr.push('.');
|
|
continue;
|
|
}
|
|
if let Ok(s) = input.parse::<syn::LitStr>() {
|
|
instr.push_str(&s.value());
|
|
continue;
|
|
}
|
|
println!("{:?}", input.cursor().token_stream());
|
|
return Err(input.error("expected an instruction"));
|
|
}
|
|
if instr.is_empty() {
|
|
return Err(input.error("expected an instruction before comma"));
|
|
}
|
|
let mut args = Vec::new();
|
|
while !input.is_empty() {
|
|
let name = input.parse::<syn::Ident>()?;
|
|
input.parse::<Token![=]>()?;
|
|
let expr = input.parse::<syn::Expr>()?;
|
|
args.push((name, expr));
|
|
|
|
if input.parse::<Token![,]>().is_err() {
|
|
if !input.is_empty() {
|
|
return Err(input.error("extra tokens at end"));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
Ok(Self { instr, args })
|
|
}
|
|
}
|
|
|
|
struct Append<T>(T);
|
|
|
|
impl<T> quote::ToTokens for Append<T>
|
|
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);
|
|
}
|
|
}
|
|
}
|