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.
run(options: &Options) -> anyhow::Result<()>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.
from_path(path: impl Into<PathBuf>) -> Result<Self, io::Error>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)]
from_inline_code(contents: String) -> Self82     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]`.
path(&self) -> String91     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.
run(&self) -> Result<(), FileInterpreterFailure>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]
nop()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]
filetests()174     fn filetests() {
175         run(&Options {
176             files: vec![PathBuf::from("../filetests/filetests/interpreter")],
177             verbose: true,
178         })
179         .unwrap()
180     }
181 }
182