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