clif_util/
interpret.rs

1//! CLI tool to interpret Cranelift IR files.
2
3use crate::utils::iterate_files;
4use clap::Parser;
5use cranelift_interpreter::environment::FunctionStore;
6use cranelift_interpreter::interpreter::{Interpreter, InterpreterState};
7use cranelift_interpreter::step::ControlFlow;
8use cranelift_reader::{parse_run_command, parse_test, ParseError, ParseOptions};
9use std::path::PathBuf;
10use std::{fs, io};
11use thiserror::Error;
12
13/// Interpret clif code
14#[derive(Parser)]
15pub 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.
26pub 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!("{} failures", n),
59    }
60}
61
62/// Contains CLIF code that can be executed with [FileInterpreter::run].
63pub struct FileInterpreter {
64    path: Option<PathBuf>,
65    contents: String,
66}
67
68impl 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(_) => panic!("Unexpected returned control flow--this is likely a bug."),
130                        Err(t) => Err(t.to_string()),
131                    }
132                })
133                .map_err(|s| FileInterpreterFailure::FailedExecution(s))?;
134        }
135
136        Ok(())
137    }
138}
139
140/// Possible sources of failure in this file.
141#[derive(Error, Debug)]
142pub enum FileInterpreterFailure {
143    #[error("failure reading file")]
144    Io(#[from] io::Error),
145    #[error("failure parsing file {0}: {1}")]
146    ParsingClif(String, ParseError),
147    #[error("failed to run function: {0}")]
148    FailedExecution(String),
149}
150
151#[cfg(test)]
152mod test {
153    use super::*;
154
155    #[test]
156    fn nop() {
157        let code = String::from(
158            "
159            function %test() -> i8 {
160            block0:
161                nop
162                v1 = iconst.i8 -1
163                v2 = iconst.i8 42
164                return v1
165            }
166            ; run: %test() == -1
167            ",
168        );
169        FileInterpreter::from_inline_code(code).run().unwrap()
170    }
171
172    #[test]
173    fn filetests() {
174        run(&Options {
175            files: vec![PathBuf::from("../filetests/filetests/interpreter")],
176            verbose: true,
177        })
178        .unwrap()
179    }
180}