diff --git a/Cargo.lock b/Cargo.lock index 9b920dfae2..52c8ef1b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,6 +536,7 @@ dependencies = [ "cranelift-faerie", "cranelift-filetests", "cranelift-frontend", + "cranelift-interpreter", "cranelift-module", "cranelift-native", "cranelift-object", @@ -547,10 +548,12 @@ dependencies = [ "file-per-thread-logger", "filecheck", "indicatif", + "log", "pretty_env_logger", "serde", "target-lexicon", "term", + "thiserror", "walkdir", "wat", ] diff --git a/cranelift/Cargo.toml b/cranelift/Cargo.toml index fef8b439cb..f2e4ea9607 100644 --- a/cranelift/Cargo.toml +++ b/cranelift/Cargo.toml @@ -17,6 +17,7 @@ path = "src/clif-util.rs" cfg-if = "0.1" cranelift-codegen = { path = "codegen", version = "0.63.0" } cranelift-entity = { path = "entity", version = "0.63.0" } +cranelift-interpreter = { path = "interpreter", version = "*" } cranelift-reader = { path = "reader", version = "0.63.0" } cranelift-frontend = { path = "frontend", version = "0.63.0" } cranelift-serde = { path = "serde", version = "0.63.0", optional = true } @@ -31,6 +32,7 @@ cranelift-preopt = { path = "preopt", version = "0.63.0" } cranelift = { path = "umbrella", version = "0.63.0" } filecheck = "0.5.0" clap = "2.32.0" +log = "0.4.8" serde = "1.0.8" term = "0.6.1" capstone = { version = "0.6.0", optional = true } @@ -39,6 +41,7 @@ target-lexicon = "0.10" pretty_env_logger = "0.4.0" file-per-thread-logger = "0.1.2" indicatif = "0.13.0" +thiserror = "1.0.15" walkdir = "2.2" [features] diff --git a/cranelift/src/clif-util.rs b/cranelift/src/clif-util.rs index 4066ef0fda..2a8f37664b 100755 --- a/cranelift/src/clif-util.rs +++ b/cranelift/src/clif-util.rs @@ -24,6 +24,7 @@ mod bugpoint; mod cat; mod compile; mod disasm; +mod interpret; mod print_cfg; mod run; mod utils; @@ -179,6 +180,13 @@ fn main() { .arg(add_input_file_arg()) .arg(add_debug_flag()), ) + .subcommand( + SubCommand::with_name("interpret") + .about("Interpret CLIF code") + .arg(add_verbose_flag()) + .arg(add_input_file_arg()) + .arg(add_debug_flag()), + ) .subcommand( SubCommand::with_name("cat") .about("Outputs .clif file") @@ -239,6 +247,14 @@ fn main() { ) .map(|_time| ()) } + ("interpret", Some(rest_cmd)) => { + handle_debug_flag(rest_cmd.is_present("debug")); + interpret::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/interpret.rs b/cranelift/src/interpret.rs new file mode 100644 index 0000000000..efd0b22a6d --- /dev/null +++ b/cranelift/src/interpret.rs @@ -0,0 +1,163 @@ +//! CLI tool to interpret Cranelift IR files. + +use crate::utils::iterate_files; +use cranelift_interpreter::environment::Environment; +use cranelift_interpreter::interpreter::{ControlFlow, Interpreter}; +use cranelift_reader::{parse_run_command, parse_test, ParseError, ParseOptions}; +use log::debug; +use std::path::PathBuf; +use std::{fs, io}; +use thiserror::Error; + +/// Run files through the Cranelift interpreter, interpreting any functions with annotations. +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; + let runner = FileInterpreter::from_path(file).map_err(|e| e.to_string())?; + match runner.run() { + Ok(_) => { + if flag_print { + println!("{}", runner.path()); + } + } + Err(e) => { + if flag_print { + println!("{}: {}", runner.path(), e.to_string()); + } + 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)), + } +} + +/// Contains CLIF code that can be executed with [FileInterpreter::run]. +pub struct FileInterpreter { + path: Option, + contents: String, +} + +impl FileInterpreter { + /// Construct a file runner from a CLIF file path. + pub fn from_path(path: impl Into) -> Result { + let path = path.into(); + debug!("New file runner from path: {}:", path.to_string_lossy()); + let contents = fs::read_to_string(&path)?; + Ok(Self { + path: Some(path), + contents, + }) + } + + /// Construct a file runner from a CLIF code string. Currently only used for testing. + #[cfg(test)] + pub fn from_inline_code(contents: String) -> Self { + debug!("New file runner from inline code: {}:", &contents[..20]); + Self { + path: None, + contents, + } + } + + /// Return the path of the file runner or `[inline code]`. + pub fn path(&self) -> String { + match self.path { + None => "[inline code]".to_string(), + Some(ref p) => p.to_string_lossy().to_string(), + } + } + + /// Run the file; this searches for annotations like `; run: %fn0(42)` or + /// `; test: %fn0(42) == 2` and executes them, performing any test comparisons if necessary. + pub fn run(&self) -> Result<(), FileInterpreterFailure> { + // parse file + let test = parse_test(&self.contents, ParseOptions::default()) + .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))?; + + // collect functions + let mut env = Environment::default(); + let mut commands = vec![]; + for (func, details) in test.functions.into_iter() { + for comment in details.comments { + if let Some(command) = parse_run_command(comment.text, &func.signature) + .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))? + { + commands.push(command); + } + } + // Note: func.name may truncate the function name + env.add(func.name.to_string(), func); + } + + // Run assertion commands + let interpreter = Interpreter::new(env); + for command in commands { + command + .run(|func_name, args| { + // Because we have stored function names with a leading %, we need to re-add it. + let func_name = &format!("%{}", func_name); + match interpreter.call_by_name(func_name, args) { + Ok(ControlFlow::Return(results)) => Ok(results), + Ok(_) => panic!("Unexpected returned control flow--this is likely a bug."), + Err(t) => Err(t.to_string()), + } + }) + .map_err(|s| FileInterpreterFailure::FailedExecution(s))?; + } + + Ok(()) + } +} + +/// Possible sources of failure in this file. +#[derive(Error, Debug)] +pub enum FileInterpreterFailure { + #[error("failure reading file")] + Io(#[from] io::Error), + #[error("failure parsing file {0}: {1}")] + ParsingClif(String, ParseError), + #[error("failed to run function: {0}")] + FailedExecution(String), +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn nop() { + let code = String::from( + " + function %test() -> b8 { + block0: + nop + v1 = bconst.b8 true + v2 = iconst.i8 42 + return v1 + } + ; run: %test() == true + ", + ); + FileInterpreter::from_inline_code(code).run().unwrap() + } + + #[test] + fn filetests() { + run(vec!["../filetests/filetests/interpreter".to_string()], true).unwrap() + } +}