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.
This commit is contained in:
committed by
Benjamin Bouvier
parent
276bb5e26d
commit
ff3c44385c
@@ -42,6 +42,7 @@ target-lexicon = "0.4.0"
|
|||||||
pretty_env_logger = "0.3.0"
|
pretty_env_logger = "0.3.0"
|
||||||
file-per-thread-logger = "0.1.2"
|
file-per-thread-logger = "0.1.2"
|
||||||
indicatif = "0.11.0"
|
indicatif = "0.11.0"
|
||||||
|
walkdir = "2.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["disas", "wasm", "cranelift-codegen/all-arch"]
|
default = ["disas", "wasm", "cranelift-codegen/all-arch"]
|
||||||
|
|||||||
@@ -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
|
/// A `TrapSink` implementation that does nothing, which is convenient when
|
||||||
/// compiling code that does not rely on trapping semantics.
|
/// compiling code that does not rely on trapping semantics.
|
||||||
pub struct NullTrapSink {}
|
pub struct NullTrapSink {}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ mod shrink;
|
|||||||
mod stackmap;
|
mod stackmap;
|
||||||
|
|
||||||
pub use self::memorysink::{
|
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::relaxation::relax_branches;
|
||||||
pub use self::shrink::shrink_instructions;
|
pub use self::shrink::shrink_instructions;
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cranelift-codegen = { path = "../cranelift-codegen", version = "0.40.0", features = ["testing_hooks"] }
|
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-reader = { path = "../cranelift-reader", version = "0.40.0" }
|
||||||
cranelift-preopt = { path = "../cranelift-preopt", version = "0.40.0" }
|
cranelift-preopt = { path = "../cranelift-preopt", version = "0.40.0" }
|
||||||
file-per-thread-logger = "0.1.2"
|
file-per-thread-logger = "0.1.2"
|
||||||
filecheck = "0.4.0"
|
filecheck = "0.4.0"
|
||||||
num_cpus = "1.8.0"
|
|
||||||
log = "0.4.6"
|
log = "0.4.6"
|
||||||
|
mmap = "0.1.1"
|
||||||
|
num_cpus = "1.8.0"
|
||||||
|
region = "2.1.2"
|
||||||
|
|||||||
11
cranelift/filetests/filetests/isa/x86/run-const.clif
Normal file
11
cranelift/filetests/filetests/isa/x86/run-const.clif
Normal file
@@ -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
|
||||||
118
cranelift/filetests/src/function_runner.rs
Normal file
118
cranelift/filetests/src/function_runner.rs
Normal file
@@ -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<dyn TargetIsa>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<dyn TargetIsa>) -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
pub use crate::function_runner::FunctionRunner;
|
||||||
use crate::runner::TestRunner;
|
use crate::runner::TestRunner;
|
||||||
use cranelift_codegen::timing;
|
use cranelift_codegen::timing;
|
||||||
use cranelift_reader::TestCommand;
|
use cranelift_reader::TestCommand;
|
||||||
@@ -30,6 +31,7 @@ use std::path::Path;
|
|||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
mod concurrent;
|
mod concurrent;
|
||||||
|
mod function_runner;
|
||||||
mod match_directive;
|
mod match_directive;
|
||||||
mod runner;
|
mod runner;
|
||||||
mod runone;
|
mod runone;
|
||||||
@@ -46,6 +48,7 @@ mod test_postopt;
|
|||||||
mod test_preopt;
|
mod test_preopt;
|
||||||
mod test_print_cfg;
|
mod test_print_cfg;
|
||||||
mod test_regalloc;
|
mod test_regalloc;
|
||||||
|
mod test_run;
|
||||||
mod test_safepoint;
|
mod test_safepoint;
|
||||||
mod test_shrink;
|
mod test_shrink;
|
||||||
mod test_simple_gvn;
|
mod test_simple_gvn;
|
||||||
@@ -124,6 +127,7 @@ fn new_subtest(parsed: &TestCommand) -> subtest::SubtestResult<Box<dyn subtest::
|
|||||||
"simple_preopt" => test_simple_preopt::subtest(parsed),
|
"simple_preopt" => test_simple_preopt::subtest(parsed),
|
||||||
"print-cfg" => test_print_cfg::subtest(parsed),
|
"print-cfg" => test_print_cfg::subtest(parsed),
|
||||||
"regalloc" => test_regalloc::subtest(parsed),
|
"regalloc" => test_regalloc::subtest(parsed),
|
||||||
|
"run" => test_run::subtest(parsed),
|
||||||
"shrink" => test_shrink::subtest(parsed),
|
"shrink" => test_shrink::subtest(parsed),
|
||||||
"simple-gvn" => test_simple_gvn::subtest(parsed),
|
"simple-gvn" => test_simple_gvn::subtest(parsed),
|
||||||
"verifier" => test_verifier::subtest(parsed),
|
"verifier" => test_verifier::subtest(parsed),
|
||||||
|
|||||||
46
cranelift/filetests/src/test_run.rs
Normal file
46
cranelift/filetests/src/test_run.rs
Normal file
@@ -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<Box<dyn SubTest>> {
|
||||||
|
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<ir::Function>, 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ mod cat;
|
|||||||
mod compile;
|
mod compile;
|
||||||
mod disasm;
|
mod disasm;
|
||||||
mod print_cfg;
|
mod print_cfg;
|
||||||
|
mod run;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
/// A command either succeeds or fails with an error message.
|
/// A command either succeeds or fails with an error message.
|
||||||
@@ -162,6 +163,13 @@ fn main() {
|
|||||||
.arg(add_input_file_arg())
|
.arg(add_input_file_arg())
|
||||||
.arg(add_debug_flag()),
|
.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(
|
||||||
SubCommand::with_name("cat")
|
SubCommand::with_name("cat")
|
||||||
.about("Outputs .clif file")
|
.about("Outputs .clif file")
|
||||||
@@ -224,6 +232,14 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.map(|_time| ())
|
.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)) => {
|
("pass", Some(rest_cmd)) => {
|
||||||
handle_debug_flag(rest_cmd.is_present("debug"));
|
handle_debug_flag(rest_cmd.is_present("debug"));
|
||||||
|
|
||||||
|
|||||||
118
cranelift/src/run.rs
Normal file
118
cranelift/src/run.rs
Normal file
@@ -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<String>, 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<String>) -> impl Iterator<Item = PathBuf> {
|
||||||
|
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<Box<dyn TargetIsa>, 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user