1 //! Test command for running CLIF files and verifying their results
2 //!
3 //! The `run` test command compiles each function on the host machine and executes it
4 
5 use crate::function_runner::{CompiledTestFile, TestFileCompiler};
6 use crate::runone::FileUpdate;
7 use crate::subtest::{Context, SubTest};
8 use anyhow::Context as _;
9 use cranelift_codegen::data_value::DataValue;
10 use cranelift_codegen::ir::Type;
11 use cranelift_codegen::isa::{OwnedTargetIsa, TargetIsa};
12 use cranelift_codegen::settings::{Configurable, Flags};
13 use cranelift_codegen::{ir, settings};
14 use cranelift_reader::TestCommand;
15 use cranelift_reader::{TestFile, parse_run_command};
16 use log::{info, trace};
17 use std::borrow::Cow;
18 use target_lexicon::Architecture;
19 
20 struct TestRun;
21 
subtest(parsed: &TestCommand) -> anyhow::Result<Box<dyn SubTest>>22 pub fn subtest(parsed: &TestCommand) -> anyhow::Result<Box<dyn SubTest>> {
23     assert_eq!(parsed.command, "run");
24     if !parsed.options.is_empty() {
25         anyhow::bail!("No options allowed on {parsed}");
26     }
27     Ok(Box::new(TestRun))
28 }
29 
30 /// Builds a [TargetIsa] for the current host.
31 ///
32 /// ISA Flags can be overridden by passing [Value]'s via `isa_flags`.
build_host_isa( infer_native_flags: bool, flags: settings::Flags, isa_flags: Vec<settings::Value>, ) -> anyhow::Result<OwnedTargetIsa>33 fn build_host_isa(
34     infer_native_flags: bool,
35     flags: settings::Flags,
36     isa_flags: Vec<settings::Value>,
37 ) -> anyhow::Result<OwnedTargetIsa> {
38     let mut builder = cranelift_native::builder_with_options(infer_native_flags)
39         .map_err(|e| anyhow::Error::msg(e))?;
40 
41     // Copy ISA Flags
42     for value in isa_flags {
43         builder.set(value.name, &value.value_string())?;
44     }
45 
46     let isa = builder.finish(flags)?;
47     Ok(isa)
48 }
49 
50 /// Checks if the host's ISA is compatible with the one requested by the test.
is_isa_compatible( file_path: &str, host: Option<&dyn TargetIsa>, requested: &dyn TargetIsa, ) -> Result<(), String>51 fn is_isa_compatible(
52     file_path: &str,
53     host: Option<&dyn TargetIsa>,
54     requested: &dyn TargetIsa,
55 ) -> Result<(), String> {
56     let host_triple = match host {
57         Some(host) => host.triple().clone(),
58         None => target_lexicon::Triple::host(),
59     };
60     // If this test requests to run on a completely different
61     // architecture than the host platform then we skip it entirely,
62     // since we won't be able to natively execute machine code.
63     let host_arch = host_triple.architecture;
64     let requested_arch = requested.triple().architecture;
65 
66     match (host_arch, requested_arch) {
67         // If the host matches the requested target, then that's all good.
68         (host, requested) if host == requested => {}
69 
70         // Allow minor differences in risc-v targets.
71         (Architecture::Riscv64(_), Architecture::Riscv64(_)) => {}
72 
73         // Any host can run pulley so long as the pointer width and endianness
74         // match.
75         (
76             _,
77             Architecture::Pulley32
78             | Architecture::Pulley64
79             | Architecture::Pulley32be
80             | Architecture::Pulley64be,
81         ) if host_triple.pointer_width() == requested.triple().pointer_width()
82             && host_triple.endianness() == requested.triple().endianness() => {}
83 
84         _ => {
85             return Err(format!(
86                 "skipped {file_path}: host can't run {requested_arch:?} programs"
87             ));
88         }
89     }
90 
91     // We need to check that the requested ISA does not have any flags that
92     // we can't natively support on the host.
93     let requested_flags = requested.isa_flags();
94     for req_value in requested_flags {
95         // pointer_width for pulley already validated above
96         if req_value.name == "pointer_width" {
97             continue;
98         }
99         let requested = match req_value.as_bool() {
100             Some(requested) => requested,
101             None => unimplemented!("ISA flag {} of kind {:?}", req_value.name, req_value.kind()),
102         };
103         let host_isa_flags = match host {
104             Some(host) => host.isa_flags(),
105             None => {
106                 return Err(format!(
107                     "host not available on this platform for isa-specific flag"
108                 ));
109             }
110         };
111         let available_in_host = host_isa_flags
112             .iter()
113             .find(|val| val.name == req_value.name)
114             .and_then(|val| val.as_bool())
115             .unwrap_or(false);
116 
117         if !requested || available_in_host {
118             continue;
119         }
120 
121         // The AArch64 feature `sign_return_address` is supported on all AArch64
122         // hosts, regardless of whether `cranelift-native` infers it or not. The
123         // instructions emitted with this feature enabled are interpreted as
124         // "hint" noop instructions on CPUs which don't support address
125         // authentication.
126         //
127         // Note that at this time `cranelift-native` will only enable
128         // `sign_return_address` for macOS (notably not Linux) because of a
129         // historical bug in libunwind which causes pointer address signing,
130         // when run on hardware that supports it, so segfault during unwinding.
131         if req_value.name == "sign_return_address" && matches!(host_arch, Architecture::Aarch64(_))
132         {
133             continue;
134         }
135 
136         return Err(format!(
137             "skipped {}: host does not support ISA flag {}",
138             file_path, req_value.name
139         ));
140     }
141 
142     Ok(())
143 }
144 
compile_testfile( testfile: &TestFile, flags: &Flags, isa: &dyn TargetIsa, ) -> anyhow::Result<CompiledTestFile>145 fn compile_testfile(
146     testfile: &TestFile,
147     flags: &Flags,
148     isa: &dyn TargetIsa,
149 ) -> anyhow::Result<CompiledTestFile> {
150     let isa = match isa.triple().architecture {
151         // Convert `&dyn TargetIsa` to `OwnedTargetIsa` by re-making the ISA and
152         // applying pulley flags/etc.
153         Architecture::Pulley32
154         | Architecture::Pulley64
155         | Architecture::Pulley32be
156         | Architecture::Pulley64be => {
157             let mut builder = cranelift_codegen::isa::lookup(isa.triple().clone())?;
158             for value in isa.isa_flags() {
159                 builder.set(value.name, &value.value_string()).unwrap();
160             }
161             builder.finish(flags.clone())?
162         }
163 
164         // We can't use the requested ISA directly since it does not contain info
165         // about the operating system / calling convention / etc..
166         //
167         // Copy the requested ISA flags into the host ISA and use that.
168         _ => build_host_isa(false, flags.clone(), isa.isa_flags()).unwrap(),
169     };
170 
171     let mut tfc = TestFileCompiler::new(isa);
172     tfc.add_testfile(testfile)?;
173     Ok(tfc.compile()?)
174 }
175 
run_test( testfile: &CompiledTestFile, func: &ir::Function, context: &Context, ) -> anyhow::Result<()>176 fn run_test(
177     testfile: &CompiledTestFile,
178     func: &ir::Function,
179     context: &Context,
180 ) -> anyhow::Result<()> {
181     for comment in context.details.comments.iter() {
182         if let Some(command) = parse_run_command(comment.text, &func.signature)? {
183             trace!("Parsed run command: {command}");
184 
185             command
186                 .run(|_, run_args| {
187                     let (_ctx_struct, _vmctx_ptr) =
188                         build_vmctx_struct(context.isa.unwrap().pointer_type());
189 
190                     let mut args = Vec::with_capacity(run_args.len());
191                     args.extend_from_slice(run_args);
192 
193                     let trampoline = testfile.get_trampoline(func).unwrap();
194                     Ok(trampoline.call(&testfile, &args))
195                 })
196                 .map_err(|s| anyhow::anyhow!("{s}"))?;
197         }
198     }
199     Ok(())
200 }
201 
202 impl SubTest for TestRun {
name(&self) -> &'static str203     fn name(&self) -> &'static str {
204         "run"
205     }
206 
is_mutating(&self) -> bool207     fn is_mutating(&self) -> bool {
208         false
209     }
210 
needs_isa(&self) -> bool211     fn needs_isa(&self) -> bool {
212         true
213     }
214 
215     /// Runs the entire subtest for a given target, invokes [Self::run] for running
216     /// individual tests.
run_target<'a>( &self, testfile: &TestFile, file_update: &mut FileUpdate, file_path: &'a str, flags: &'a Flags, isa: Option<&'a dyn TargetIsa>, ) -> anyhow::Result<()>217     fn run_target<'a>(
218         &self,
219         testfile: &TestFile,
220         file_update: &mut FileUpdate,
221         file_path: &'a str,
222         flags: &'a Flags,
223         isa: Option<&'a dyn TargetIsa>,
224     ) -> anyhow::Result<()> {
225         // Disable runtests with pinned reg enabled.
226         // We've had some abi issues that the trampoline isn't quite ready for.
227         if flags.enable_pinned_reg() {
228             return Err(anyhow::anyhow!(
229                 [
230                     "Cannot run runtests with pinned_reg enabled.",
231                     "See https://github.com/bytecodealliance/wasmtime/issues/4376 for more info"
232                 ]
233                 .join("\n")
234             ));
235         }
236 
237         // Check that the host machine can run this test case (i.e. has all extensions)
238         let host_isa = build_host_isa(true, flags.clone(), vec![]).ok();
239         if let Err(e) = is_isa_compatible(file_path, host_isa.as_deref(), isa.unwrap()) {
240             log::info!("{e}");
241             return Ok(());
242         }
243 
244         let compiled_testfile = compile_testfile(&testfile, flags, isa.unwrap())?;
245 
246         for (func, details) in &testfile.functions {
247             info!(
248                 "Test: {}({}) {}",
249                 self.name(),
250                 func.name,
251                 isa.map_or("-", TargetIsa::name)
252             );
253 
254             let context = Context {
255                 preamble_comments: &testfile.preamble_comments,
256                 details,
257                 flags,
258                 isa,
259                 file_path: file_path.as_ref(),
260                 file_update,
261             };
262 
263             run_test(&compiled_testfile, &func, &context).context(self.name())?;
264         }
265 
266         Ok(())
267     }
268 
run(&self, _func: Cow<ir::Function>, _context: &Context) -> anyhow::Result<()>269     fn run(&self, _func: Cow<ir::Function>, _context: &Context) -> anyhow::Result<()> {
270         unreachable!()
271     }
272 }
273 
274 /// Build a VMContext struct with the layout described in docs/testing.md.
build_vmctx_struct(ptr_ty: Type) -> (Vec<u64>, DataValue)275 pub fn build_vmctx_struct(ptr_ty: Type) -> (Vec<u64>, DataValue) {
276     let context_struct: Vec<u64> = Vec::new();
277 
278     let ptr = context_struct.as_ptr() as usize as i128;
279     let ptr_dv =
280         DataValue::from_integer(ptr, ptr_ty).expect("Failed to cast pointer to native target size");
281 
282     // Return all these to make sure we don't deallocate the heaps too early
283     (context_struct, ptr_dv)
284 }
285