1 //! CLI tool to interpret Cranelift IR files. 2 3 use crate::utils::iterate_files; 4 use clap::Parser; 5 use cranelift_interpreter::environment::FunctionStore; 6 use cranelift_interpreter::interpreter::{Interpreter, InterpreterState}; 7 use cranelift_interpreter::step::ControlFlow; 8 use cranelift_reader::{ParseError, ParseOptions, parse_run_command, parse_test}; 9 use std::path::PathBuf; 10 use std::{fs, io}; 11 use thiserror::Error; 12 13 /// Interpret clif code 14 #[derive(Parser)] 15 pub struct Options { 16 /// Specify an input file to be used. Use '-' for stdin. 17 #[arg(required = true)] 18 files: Vec<PathBuf>, 19 20 /// Be more verbose 21 #[arg(short, long)] 22 verbose: bool, 23 } 24 25 /// Run files through the Cranelift interpreter, interpreting any functions with annotations. 26 pub fn run(options: &Options) -> anyhow::Result<()> { 27 let mut total = 0; 28 let mut errors = 0; 29 for file in iterate_files(&options.files) { 30 total += 1; 31 let runner = FileInterpreter::from_path(file)?; 32 match runner.run() { 33 Ok(_) => { 34 if options.verbose { 35 println!("{}", runner.path()); 36 } 37 } 38 Err(e) => { 39 if options.verbose { 40 println!("{}: {}", runner.path(), e.to_string()); 41 } 42 errors += 1; 43 } 44 } 45 } 46 47 if options.verbose { 48 match total { 49 0 => println!("0 files"), 50 1 => println!("1 file"), 51 n => println!("{n} files"), 52 } 53 } 54 55 match errors { 56 0 => Ok(()), 57 1 => anyhow::bail!("1 failure"), 58 n => anyhow::bail!("{n} failures"), 59 } 60 } 61 62 /// Contains CLIF code that can be executed with [FileInterpreter::run]. 63 pub struct FileInterpreter { 64 path: Option<PathBuf>, 65 contents: String, 66 } 67 68 impl FileInterpreter { 69 /// Construct a file runner from a CLIF file path. 70 pub fn from_path(path: impl Into<PathBuf>) -> Result<Self, io::Error> { 71 let path = path.into(); 72 log::trace!("New file runner from path: {}:", path.to_string_lossy()); 73 let contents = fs::read_to_string(&path)?; 74 Ok(Self { 75 path: Some(path), 76 contents, 77 }) 78 } 79 80 /// Construct a file runner from a CLIF code string. Currently only used for testing. 81 #[cfg(test)] 82 pub fn from_inline_code(contents: String) -> Self { 83 log::trace!("New file runner from inline code: {}:", &contents[..20]); 84 Self { 85 path: None, 86 contents, 87 } 88 } 89 90 /// Return the path of the file runner or `[inline code]`. 91 pub fn path(&self) -> String { 92 match self.path { 93 None => "[inline code]".to_string(), 94 Some(ref p) => p.to_string_lossy().to_string(), 95 } 96 } 97 98 /// Run the file; this searches for annotations like `; run: %fn0(42)` or 99 /// `; test: %fn0(42) == 2` and executes them, performing any test comparisons if necessary. 100 pub fn run(&self) -> Result<(), FileInterpreterFailure> { 101 // parse file 102 let test = parse_test(&self.contents, ParseOptions::default()) 103 .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))?; 104 105 // collect functions 106 let mut env = FunctionStore::default(); 107 let mut commands = vec![]; 108 for (func, details) in test.functions.iter() { 109 for comment in &details.comments { 110 if let Some(command) = parse_run_command(comment.text, &func.signature) 111 .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))? 112 { 113 commands.push(command); 114 } 115 } 116 // Note: func.name may truncate the function name 117 env.add(func.name.to_string(), func); 118 } 119 120 // Run assertion commands 121 for command in commands { 122 command 123 .run(|func_name, args| { 124 // Because we have stored function names with a leading %, we need to re-add it. 125 let func_name = &format!("%{func_name}"); 126 let state = InterpreterState::default().with_function_store(env.clone()); 127 match Interpreter::new(state).call_by_name(func_name, args) { 128 Ok(ControlFlow::Return(results)) => Ok(results.to_vec()), 129 Ok(ControlFlow::Trap(trap)) => Err(trap.to_string()), 130 Ok(_) => panic!("Unexpected returned control flow--this is likely a bug."), 131 Err(t) => Err(t.to_string()), 132 } 133 }) 134 .map_err(|s| FileInterpreterFailure::FailedExecution(s))?; 135 } 136 137 Ok(()) 138 } 139 } 140 141 /// Possible sources of failure in this file. 142 #[derive(Error, Debug)] 143 pub enum FileInterpreterFailure { 144 #[error("failure reading file")] 145 Io(#[from] io::Error), 146 #[error("failure parsing file {0}: {1}")] 147 ParsingClif(String, ParseError), 148 #[error("failed to run function: {0}")] 149 FailedExecution(String), 150 } 151 152 #[cfg(test)] 153 mod test { 154 use super::*; 155 156 #[test] 157 fn nop() { 158 let code = String::from( 159 " 160 function %test() -> i8 { 161 block0: 162 nop 163 v1 = iconst.i8 -1 164 v2 = iconst.i8 42 165 return v1 166 } 167 ; run: %test() == -1 168 ", 169 ); 170 FileInterpreter::from_inline_code(code).run().unwrap() 171 } 172 173 #[test] 174 fn filetests() { 175 run(&Options { 176 files: vec![PathBuf::from("../filetests/filetests/interpreter")], 177 verbose: true, 178 }) 179 .unwrap() 180 } 181 } 182