]>
Commit | Line | Data |
---|---|---|
0531ce1d XL |
1 | //! Implementation of the `#[assert_instr]` macro |
2 | //! | |
416331ca | 3 | //! This macro is used when testing the `stdarch` crate and is used to generate |
0531ce1d XL |
4 | //! test cases to assert that functions do indeed contain the instructions that |
5 | //! we're expecting them to contain. | |
6 | //! | |
7 | //! The procedural macro here is relatively simple, it simply appends a | |
8 | //! `#[test]` function to the original token stream which asserts that the | |
9 | //! function itself contains the relevant instruction. | |
cdc7bbd5 | 10 | #![deny(rust_2018_idioms)] |
0531ce1d | 11 | |
0531ce1d XL |
12 | #[macro_use] |
13 | extern crate quote; | |
0531ce1d XL |
14 | |
15 | use proc_macro2::TokenStream; | |
0731742a | 16 | use quote::ToTokens; |
0531ce1d XL |
17 | |
18 | #[proc_macro_attribute] | |
19 | pub fn assert_instr( | |
0731742a XL |
20 | attr: proc_macro::TokenStream, |
21 | item: proc_macro::TokenStream, | |
0531ce1d | 22 | ) -> proc_macro::TokenStream { |
0731742a XL |
23 | let invoc = match syn::parse::<Invoc>(attr) { |
24 | Ok(s) => s, | |
25 | Err(e) => return e.to_compile_error().into(), | |
26 | }; | |
27 | let item = match syn::parse::<syn::Item>(item) { | |
28 | Ok(s) => s, | |
29 | Err(e) => return e.to_compile_error().into(), | |
30 | }; | |
0531ce1d XL |
31 | let func = match item { |
32 | syn::Item::Fn(ref f) => f, | |
33 | _ => panic!("must be attached to a function"), | |
34 | }; | |
35 | ||
36 | let instr = &invoc.instr; | |
74b04a01 | 37 | let name = &func.sig.ident; |
83c7162d XL |
38 | |
39 | // Disable assert_instr for x86 targets compiled with avx enabled, which | |
8faf50e0 XL |
40 | // causes LLVM to generate different intrinsics that the ones we are |
41 | // testing for. | |
416331ca XL |
42 | let disable_assert_instr = std::env::var("STDARCH_DISABLE_ASSERT_INSTR").is_ok(); |
43 | ||
44 | // If instruction tests are disabled avoid emitting this shim at all, just | |
45 | // return the original item without our attribute. | |
46 | if !cfg!(optimized) || disable_assert_instr { | |
47 | return (quote! { #item }).into(); | |
48 | } | |
83c7162d | 49 | |
83c7162d | 50 | let instr_str = instr |
83c7162d | 51 | .replace('.', "_") |
0731742a XL |
52 | .replace('/', "_") |
53 | .replace(':', "_") | |
48663c56 | 54 | .replace(char::is_whitespace, ""); |
0731742a | 55 | let assert_name = syn::Ident::new(&format!("assert_{}_{}", name, instr_str), name.span()); |
416331ca XL |
56 | // These name has to be unique enough for us to find it in the disassembly later on: |
57 | let shim_name = syn::Ident::new( | |
58 | &format!("stdarch_test_shim_{}_{}", name, instr_str), | |
59 | name.span(), | |
60 | ); | |
0531ce1d XL |
61 | let mut inputs = Vec::new(); |
62 | let mut input_vals = Vec::new(); | |
17df50a5 | 63 | let mut const_vals = Vec::new(); |
74b04a01 XL |
64 | let ret = &func.sig.output; |
65 | for arg in func.sig.inputs.iter() { | |
0531ce1d | 66 | let capture = match *arg { |
74b04a01 | 67 | syn::FnArg::Typed(ref c) => c, |
8faf50e0 XL |
68 | ref v => panic!( |
69 | "arguments must not have patterns: `{:?}`", | |
70 | v.clone().into_token_stream() | |
71 | ), | |
0531ce1d | 72 | }; |
74b04a01 | 73 | let ident = match *capture.pat { |
0531ce1d XL |
74 | syn::Pat::Ident(ref i) => &i.ident, |
75 | _ => panic!("must have bare arguments"), | |
76 | }; | |
74b04a01 XL |
77 | if let Some(&(_, ref tokens)) = invoc.args.iter().find(|a| *ident == a.0) { |
78 | input_vals.push(quote! { #tokens }); | |
0731742a XL |
79 | } else { |
80 | inputs.push(capture); | |
81 | input_vals.push(quote! { #ident }); | |
82 | } | |
0531ce1d | 83 | } |
17df50a5 XL |
84 | for arg in func.sig.generics.params.iter() { |
85 | let c = match *arg { | |
86 | syn::GenericParam::Const(ref c) => c, | |
87 | ref v => panic!( | |
88 | "only const generics are allowed: `{:?}`", | |
89 | v.clone().into_token_stream() | |
90 | ), | |
91 | }; | |
92 | if let Some(&(_, ref tokens)) = invoc.args.iter().find(|a| c.ident == a.0) { | |
93 | const_vals.push(quote! { #tokens }); | |
94 | } else { | |
95 | panic!("const generics must have a value for tests"); | |
96 | } | |
97 | } | |
0531ce1d | 98 | |
8faf50e0 XL |
99 | let attrs = func |
100 | .attrs | |
0531ce1d XL |
101 | .iter() |
102 | .filter(|attr| { | |
103 | attr.path | |
104 | .segments | |
105 | .first() | |
83c7162d | 106 | .expect("attr.path.segments.first() failed") |
0531ce1d | 107 | .ident |
8faf50e0 | 108 | .to_string() |
0531ce1d | 109 | .starts_with("target") |
0731742a XL |
110 | }) |
111 | .collect::<Vec<_>>(); | |
0531ce1d XL |
112 | let attrs = Append(&attrs); |
113 | ||
114 | // Use an ABI on Windows that passes SIMD values in registers, like what | |
115 | // happens on Unix (I think?) by default. | |
116 | let abi = if cfg!(windows) { | |
94222f64 XL |
117 | let target = std::env::var("TARGET").unwrap(); |
118 | if target.contains("x86_64") { | |
119 | syn::LitStr::new("sysv64", proc_macro2::Span::call_site()) | |
120 | } else { | |
121 | syn::LitStr::new("vectorcall", proc_macro2::Span::call_site()) | |
122 | } | |
0531ce1d XL |
123 | } else { |
124 | syn::LitStr::new("C", proc_macro2::Span::call_site()) | |
125 | }; | |
8faf50e0 | 126 | let shim_name_str = format!("{}{}", shim_name, assert_name); |
0531ce1d XL |
127 | let to_test = quote! { |
128 | #attrs | |
9fa01778 | 129 | #[no_mangle] |
416331ca XL |
130 | #[inline(never)] |
131 | pub unsafe extern #abi fn #shim_name(#(#inputs),*) #ret { | |
8faf50e0 XL |
132 | // The compiler in optimized mode by default runs a pass called |
133 | // "mergefunc" where it'll merge functions that look identical. | |
134 | // Turns out some intrinsics produce identical code and they're | |
135 | // folded together, meaning that one just jumps to another. This | |
136 | // messes up our inspection of the disassembly of this function and | |
137 | // we're not a huge fan of that. | |
138 | // | |
139 | // To thwart this pass and prevent functions from being merged we | |
140 | // generate some code that's hopefully very tight in terms of | |
141 | // codegen but is otherwise unique to prevent code from being | |
142 | // folded. | |
3dfed10e XL |
143 | // |
144 | // This is avoided on Wasm32 right now since these functions aren't | |
145 | // inlined which breaks our tests since each intrinsic looks like it | |
146 | // calls functions. Turns out functions aren't similar enough to get | |
147 | // merged on wasm32 anyway. This bug is tracked at | |
148 | // rust-lang/rust#74320. | |
149 | #[cfg(not(target_arch = "wasm32"))] | |
416331ca XL |
150 | ::stdarch_test::_DONT_DEDUP.store( |
151 | std::mem::transmute(#shim_name_str.as_bytes().as_ptr()), | |
152 | std::sync::atomic::Ordering::Relaxed, | |
153 | ); | |
17df50a5 | 154 | #name::<#(#const_vals),*>(#(#input_vals),*) |
0531ce1d XL |
155 | } |
156 | }; | |
157 | ||
74b04a01 | 158 | let tokens: TokenStream = quote! { |
3dfed10e | 159 | #[test] |
0531ce1d | 160 | #[allow(non_snake_case)] |
0531ce1d XL |
161 | fn #assert_name() { |
162 | #to_test | |
163 | ||
416331ca | 164 | ::stdarch_test::assert(#shim_name as usize, |
0531ce1d | 165 | stringify!(#shim_name), |
0bf4aa26 | 166 | #instr); |
0531ce1d | 167 | } |
0731742a | 168 | }; |
0531ce1d | 169 | |
74b04a01 | 170 | let tokens: TokenStream = quote! { |
0531ce1d | 171 | #item |
74b04a01 | 172 | #tokens |
0731742a | 173 | }; |
74b04a01 | 174 | tokens.into() |
0531ce1d XL |
175 | } |
176 | ||
177 | struct Invoc { | |
0bf4aa26 | 178 | instr: String, |
0531ce1d XL |
179 | args: Vec<(syn::Ident, syn::Expr)>, |
180 | } | |
181 | ||
0bf4aa26 | 182 | impl syn::parse::Parse for Invoc { |
cdc7bbd5 | 183 | fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> { |
0731742a | 184 | use syn::{ext::IdentExt, Token}; |
0bf4aa26 | 185 | |
0731742a XL |
186 | let mut instr = String::new(); |
187 | while !input.is_empty() { | |
188 | if input.parse::<Token![,]>().is_ok() { | |
189 | break; | |
190 | } | |
191 | if let Ok(ident) = syn::Ident::parse_any(input) { | |
192 | instr.push_str(&ident.to_string()); | |
193 | continue; | |
194 | } | |
195 | if input.parse::<Token![.]>().is_ok() { | |
fc512014 | 196 | instr.push('.'); |
0731742a XL |
197 | continue; |
198 | } | |
199 | if let Ok(s) = input.parse::<syn::LitStr>() { | |
200 | instr.push_str(&s.value()); | |
201 | continue; | |
202 | } | |
203 | println!("{:?}", input.cursor().token_stream()); | |
204 | return Err(input.error("expected an instruction")); | |
205 | } | |
9fa01778 | 206 | if instr.is_empty() { |
0731742a XL |
207 | return Err(input.error("expected an instruction before comma")); |
208 | } | |
0bf4aa26 | 209 | let mut args = Vec::new(); |
0731742a | 210 | while !input.is_empty() { |
0bf4aa26 XL |
211 | let name = input.parse::<syn::Ident>()?; |
212 | input.parse::<Token![=]>()?; | |
213 | let expr = input.parse::<syn::Expr>()?; | |
214 | args.push((name, expr)); | |
0731742a XL |
215 | |
216 | if input.parse::<Token![,]>().is_err() { | |
217 | if !input.is_empty() { | |
218 | return Err(input.error("extra tokens at end")); | |
219 | } | |
220 | break; | |
221 | } | |
0bf4aa26 | 222 | } |
0731742a | 223 | Ok(Self { instr, args }) |
0bf4aa26 | 224 | } |
0531ce1d XL |
225 | } |
226 | ||
227 | struct Append<T>(T); | |
228 | ||
229 | impl<T> quote::ToTokens for Append<T> | |
230 | where | |
231 | T: Clone + IntoIterator, | |
232 | T::Item: quote::ToTokens, | |
233 | { | |
8faf50e0 | 234 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { |
0531ce1d XL |
235 | for item in self.0.clone() { |
236 | item.to_tokens(tokens); | |
237 | } | |
238 | } | |
239 | } |