From ff3c44385cc521db704ded259bf07b92c33299c3 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 21 Aug 2019 09:03:09 -0700 Subject: [PATCH] Add `test run` to cranelift-filetests to allow executing CLIF (#890) * Add ability to run CLIF IR using `clif-util run [-v] {file}` and add `test run` to cranelift-filetests to allow executing CLIF This re-factors the compile/execute parts to a FunctionRunner that is shared between cranelift-filetests and clif-util. CLIF can be now be run using `clif-util run` as well as during `clif-util test` for files with a `test run` header. As before, only functions suffixed with a `run` comment are executed. The `run: fn(...) == ...` expression syntax is left for a subsequent change. --- cranelift/Cargo.toml | 1 + cranelift/codegen/src/binemit/memorysink.rs | 10 ++ cranelift/codegen/src/binemit/mod.rs | 3 +- cranelift/filetests/Cargo.toml | 5 +- .../filetests/isa/x86/run-const.clif | 11 ++ cranelift/filetests/src/function_runner.rs | 118 ++++++++++++++++++ cranelift/filetests/src/lib.rs | 4 + cranelift/filetests/src/test_run.rs | 46 +++++++ cranelift/src/clif-util.rs | 16 +++ cranelift/src/run.rs | 118 ++++++++++++++++++ 10 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 cranelift/filetests/filetests/isa/x86/run-const.clif create mode 100644 cranelift/filetests/src/function_runner.rs create mode 100644 cranelift/filetests/src/test_run.rs create mode 100644 cranelift/src/run.rs diff --git a/cranelift/Cargo.toml b/cranelift/Cargo.toml index eb25cb2a65..a8f85c30b7 100644 --- a/cranelift/Cargo.toml +++ b/cranelift/Cargo.toml @@ -42,6 +42,7 @@ target-lexicon = "0.4.0" pretty_env_logger = "0.3.0" file-per-thread-logger = "0.1.2" indicatif = "0.11.0" +walkdir = "2.2" [features] default = ["disas", "wasm", "cranelift-codegen/all-arch"] diff --git a/cranelift/codegen/src/binemit/memorysink.rs b/cranelift/codegen/src/binemit/memorysink.rs index b543d3e89c..49c519f6b9 100644 --- a/cranelift/codegen/src/binemit/memorysink.rs +++ b/cranelift/codegen/src/binemit/memorysink.rs @@ -162,6 +162,16 @@ impl<'a> CodeSink for MemoryCodeSink<'a> { } } +/// A `RelocSink` implementation that does nothing, which is convenient when +/// compiling code that does not relocate anything. +pub struct NullRelocSink {} + +impl RelocSink for NullRelocSink { + fn reloc_ebb(&mut self, _: u32, _: Reloc, _: u32) {} + fn reloc_external(&mut self, _: u32, _: Reloc, _: &ExternalName, _: i64) {} + fn reloc_jt(&mut self, _: u32, _: Reloc, _: JumpTable) {} +} + /// A `TrapSink` implementation that does nothing, which is convenient when /// compiling code that does not rely on trapping semantics. pub struct NullTrapSink {} diff --git a/cranelift/codegen/src/binemit/mod.rs b/cranelift/codegen/src/binemit/mod.rs index d6b72553bb..996f2e4494 100644 --- a/cranelift/codegen/src/binemit/mod.rs +++ b/cranelift/codegen/src/binemit/mod.rs @@ -9,7 +9,8 @@ mod shrink; mod stackmap; pub use self::memorysink::{ - MemoryCodeSink, NullStackmapSink, NullTrapSink, RelocSink, StackmapSink, TrapSink, + MemoryCodeSink, NullRelocSink, NullStackmapSink, NullTrapSink, RelocSink, StackmapSink, + TrapSink, }; pub use self::relaxation::relax_branches; pub use self::shrink::shrink_instructions; diff --git a/cranelift/filetests/Cargo.toml b/cranelift/filetests/Cargo.toml index fd8b309bb5..f5d3220ed9 100644 --- a/cranelift/filetests/Cargo.toml +++ b/cranelift/filetests/Cargo.toml @@ -11,9 +11,12 @@ edition = "2018" [dependencies] cranelift-codegen = { path = "../cranelift-codegen", version = "0.40.0", features = ["testing_hooks"] } +cranelift-native = { path = "../cranelift-native", version = "0.40.0" } cranelift-reader = { path = "../cranelift-reader", version = "0.40.0" } cranelift-preopt = { path = "../cranelift-preopt", version = "0.40.0" } file-per-thread-logger = "0.1.2" filecheck = "0.4.0" -num_cpus = "1.8.0" log = "0.4.6" +mmap = "0.1.1" +num_cpus = "1.8.0" +region = "2.1.2" diff --git a/cranelift/filetests/filetests/isa/x86/run-const.clif b/cranelift/filetests/filetests/isa/x86/run-const.clif new file mode 100644 index 0000000000..1ac5062e49 --- /dev/null +++ b/cranelift/filetests/filetests/isa/x86/run-const.clif @@ -0,0 +1,11 @@ +test run + +function %test_compare_i32() -> b1 { +ebb0: + v0 = iconst.i32 42 + v1 = iconst.i32 42 + v2 = icmp eq v0, v1 + return v2 +} + +; run diff --git a/cranelift/filetests/src/function_runner.rs b/cranelift/filetests/src/function_runner.rs new file mode 100644 index 0000000000..dee7320384 --- /dev/null +++ b/cranelift/filetests/src/function_runner.rs @@ -0,0 +1,118 @@ +use core::mem; +use cranelift_codegen::binemit::{NullRelocSink, NullStackmapSink, NullTrapSink}; +use cranelift_codegen::ir::Function; +use cranelift_codegen::isa::{CallConv, TargetIsa}; +use cranelift_codegen::{settings, Context}; +use cranelift_native::builder as host_isa_builder; +use mmap::{MapOption, MemoryMap}; +use region; +use region::Protection; + +/// Run a function on a host +pub struct FunctionRunner { + function: Function, + isa: Box, +} + +impl FunctionRunner { + /// Build a function runner from a function and the ISA to run on (must be the host machine's ISA) + pub fn new(function: Function, isa: Box) -> Self { + FunctionRunner { function, isa } + } + + /// Build a function runner using the host machine's ISA and the passed flags + pub fn with_host_isa(function: Function, flags: settings::Flags) -> Self { + let builder = host_isa_builder().expect("Unable to build a TargetIsa for the current host"); + let isa = builder.finish(flags); + FunctionRunner::new(function, isa) + } + + /// Build a function runner using the host machine's ISA and the default flags for this ISA + pub fn with_default_host_isa(function: Function) -> Self { + let flags = settings::Flags::new(settings::builder()); + FunctionRunner::with_host_isa(function, flags) + } + + /// Compile and execute a single function, expecting a boolean to be returned; a 'true' value is + /// interpreted as a successful test execution and mapped to Ok whereas a 'false' value is + /// interpreted as a failed test and mapped to Err. + pub fn run(&self) -> Result<(), String> { + let func = self.function.clone(); + if !(func.signature.params.is_empty() + && func.signature.returns.len() == 1 + && func.signature.returns.first().unwrap().value_type.is_bool()) + { + return Err(String::from( + "Functions must have a signature like: () -> boolean", + )); + } + + if func.signature.call_conv != self.isa.default_call_conv() + && func.signature.call_conv != CallConv::Fast + { + // ideally we wouldn't have to also check for Fast here but currently there is no way to inform the filetest parser that we would like to use a default other than Fast + return Err(String::from( + "Functions only run on the host's default calling convention; remove the specified calling convention in the function signature to use the host's default.", + )); + } + + // set up the context + let mut context = Context::new(); + context.func = func; + + // compile and encode the result to machine code + let relocs = &mut NullRelocSink {}; + let traps = &mut NullTrapSink {}; + let stackmaps = &mut NullStackmapSink {}; + let code_info = context + .compile(self.isa.as_ref()) + .map_err(|e| e.to_string())?; + let code_page = MemoryMap::new(code_info.total_size as usize, &[MapOption::MapWritable]) + .map_err(|e| e.to_string())?; + let callable_fn: fn() -> bool = unsafe { + context.emit_to_memory( + self.isa.as_ref(), + code_page.data(), + relocs, + traps, + stackmaps, + ); + region::protect(code_page.data(), code_page.len(), Protection::ReadExecute) + .map_err(|e| e.to_string())?; + mem::transmute(code_page.data()) + }; + + // execute + match callable_fn() { + true => Ok(()), + false => Err(format!("Failed: {}", context.func.name.to_string())), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use cranelift_reader::parse_test; + + #[test] + fn nop() { + let code = String::from( + "function %test() -> b8 system_v { + ebb0: + nop + v1 = bconst.b8 true + return v1 + }", + ); + + // extract function + let test_file = parse_test(code.as_str(), None, None).unwrap(); + assert_eq!(1, test_file.functions.len()); + let function = test_file.functions[0].0.clone(); + + // execute function + let runner = FunctionRunner::with_default_host_isa(function); + runner.run().unwrap() // will panic if execution fails + } +} diff --git a/cranelift/filetests/src/lib.rs b/cranelift/filetests/src/lib.rs index 6099ce3726..08f02afbf5 100644 --- a/cranelift/filetests/src/lib.rs +++ b/cranelift/filetests/src/lib.rs @@ -23,6 +23,7 @@ ) )] +pub use crate::function_runner::FunctionRunner; use crate::runner::TestRunner; use cranelift_codegen::timing; use cranelift_reader::TestCommand; @@ -30,6 +31,7 @@ use std::path::Path; use std::time; mod concurrent; +mod function_runner; mod match_directive; mod runner; mod runone; @@ -46,6 +48,7 @@ mod test_postopt; mod test_preopt; mod test_print_cfg; mod test_regalloc; +mod test_run; mod test_safepoint; mod test_shrink; mod test_simple_gvn; @@ -124,6 +127,7 @@ fn new_subtest(parsed: &TestCommand) -> subtest::SubtestResult test_simple_preopt::subtest(parsed), "print-cfg" => test_print_cfg::subtest(parsed), "regalloc" => test_regalloc::subtest(parsed), + "run" => test_run::subtest(parsed), "shrink" => test_shrink::subtest(parsed), "simple-gvn" => test_simple_gvn::subtest(parsed), "verifier" => test_verifier::subtest(parsed), diff --git a/cranelift/filetests/src/test_run.rs b/cranelift/filetests/src/test_run.rs new file mode 100644 index 0000000000..6e34bfebfa --- /dev/null +++ b/cranelift/filetests/src/test_run.rs @@ -0,0 +1,46 @@ +//! Test command for running CLIF files and verifying their results +//! +//! The `run` test command compiles each function on the host machine and executes it + +use crate::function_runner::FunctionRunner; +use crate::subtest::{Context, SubTest, SubtestResult}; +use cranelift_codegen; +use cranelift_codegen::ir; +use cranelift_reader::TestCommand; +use std::borrow::Cow; + +struct TestRun; + +pub fn subtest(parsed: &TestCommand) -> SubtestResult> { + assert_eq!(parsed.command, "run"); + if !parsed.options.is_empty() { + Err(format!("No options allowed on {}", parsed)) + } else { + Ok(Box::new(TestRun)) + } +} + +impl SubTest for TestRun { + fn name(&self) -> &'static str { + "run" + } + + fn is_mutating(&self) -> bool { + false + } + + fn needs_isa(&self) -> bool { + false + } + + fn run(&self, func: Cow, context: &Context) -> SubtestResult<()> { + for comment in context.details.comments.iter() { + if comment.text.contains("run") { + let runner = + FunctionRunner::with_host_isa(func.clone().into_owned(), context.flags.clone()); + runner.run()? + } + } + Ok(()) + } +} diff --git a/cranelift/src/clif-util.rs b/cranelift/src/clif-util.rs index a182889059..51bac2a533 100755 --- a/cranelift/src/clif-util.rs +++ b/cranelift/src/clif-util.rs @@ -33,6 +33,7 @@ mod cat; mod compile; mod disasm; mod print_cfg; +mod run; mod utils; /// A command either succeeds or fails with an error message. @@ -162,6 +163,13 @@ fn main() { .arg(add_input_file_arg()) .arg(add_debug_flag()), ) + .subcommand( + SubCommand::with_name("run") + .about("Execute CLIF code and verify with test expressions") + .arg(add_verbose_flag()) + .arg(add_input_file_arg()) + .arg(add_debug_flag()), + ) .subcommand( SubCommand::with_name("cat") .about("Outputs .clif file") @@ -224,6 +232,14 @@ fn main() { ) .map(|_time| ()) } + ("run", Some(rest_cmd)) => { + handle_debug_flag(rest_cmd.is_present("debug")); + run::run( + get_vec(rest_cmd.values_of("file")), + rest_cmd.is_present("verbose"), + ) + .map(|_time| ()) + } ("pass", Some(rest_cmd)) => { handle_debug_flag(rest_cmd.is_present("debug")); diff --git a/cranelift/src/run.rs b/cranelift/src/run.rs new file mode 100644 index 0000000000..a6c460083f --- /dev/null +++ b/cranelift/src/run.rs @@ -0,0 +1,118 @@ +//! CLI tool to compile Cranelift IR files to native code in memory and execute them. + +use crate::utils::read_to_string; +use cranelift_codegen::isa::TargetIsa; +use cranelift_filetests::FunctionRunner; +use cranelift_native::builder as host_isa_builder; +use cranelift_reader::{parse_test, Details, IsaSpec}; +use std::path::PathBuf; +use walkdir::WalkDir; + +pub fn run(files: Vec, flag_print: bool) -> Result<(), String> { + let mut total = 0; + let mut errors = 0; + for file in iterate_files(files) { + total += 1; + match run_single_file(&file) { + Ok(_) => { + if flag_print { + println!("{}", file.to_string_lossy()); + } + } + Err(e) => { + if flag_print { + println!("{}: {}", file.to_string_lossy(), e); + } + errors += 1; + } + } + } + + if flag_print { + match total { + 0 => println!("0 files"), + 1 => println!("1 file"), + n => println!("{} files", n), + } + } + + match errors { + 0 => Ok(()), + 1 => Err(String::from("1 failure")), + n => Err(format!("{} failures", n)), + } +} + +/// Iterate over all of the files passed as arguments, recursively iterating through directories +fn iterate_files(files: Vec) -> impl Iterator { + files + .into_iter() + .flat_map(WalkDir::new) + .filter(|f| match f { + Ok(d) => { + // filter out hidden files (starting with .) + !d.file_name().to_str().map_or(false, |s| s.starts_with(".")) + // filter out directories + && !d.file_type().is_dir() + } + Err(e) => { + println!("Unable to read file: {}", e); + false + } + }) + .map(|f| { + f.expect("This should not happen: we have already filtered out the errors") + .into_path() + }) +} + +/// Run all functions in a file that are succeeded by "run:" comments +fn run_single_file(path: &PathBuf) -> Result<(), String> { + let file_contents = read_to_string(&path).map_err(|e| e.to_string())?; + run_file_contents(file_contents) +} + +/// Main body of `run_single_file` separated for testing +fn run_file_contents(file_contents: String) -> Result<(), String> { + let test_file = parse_test(&file_contents, None, None).map_err(|e| e.to_string())?; + for (func, Details { comments, .. }) in test_file.functions { + if comments.iter().any(|c| c.text.contains("run")) { + let isa = create_target_isa(&test_file.isa_spec)?; + FunctionRunner::new(func, isa).run()? + } + } + Ok(()) +} + +/// Build an ISA based on the current machine running this code (the host) +fn create_target_isa(isa_spec: &IsaSpec) -> Result, String> { + if let IsaSpec::None(flags) = isa_spec { + // build an ISA for the current machine + let builder = host_isa_builder()?; + Ok(builder.finish(flags.clone())) + } else { + Err(String::from("A target ISA was specified in the file but should not have been--only the host ISA can be used for running CLIF files"))? + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn nop() { + let code = String::from( + " + function %test() -> b8 system_v { + ebb0: + nop + v1 = bconst.b8 true + return v1 + } + + ; run + ", + ); + run_file_contents(code).unwrap() + } +}