clif_util/
run.rs

1//! CLI tool to compile Cranelift IR files to native code in memory and execute them.
2
3use crate::utils::{iterate_files, read_to_string};
4use anyhow::Result;
5use clap::Parser;
6use cranelift_codegen::isa::{CallConv, OwnedTargetIsa};
7use cranelift_filetests::TestFileCompiler;
8use cranelift_native::builder as host_isa_builder;
9use cranelift_reader::{parse_run_command, parse_test, Details, IsaSpec, ParseOptions};
10use std::path::{Path, PathBuf};
11use target_lexicon::{Triple, HOST};
12
13/// Execute clif code and verify with test expressions
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
25pub fn run(options: &Options) -> Result<()> {
26    let stdin_exist = options
27        .files
28        .iter()
29        .find(|file| *file == Path::new("-"))
30        .is_some();
31    let filtered_files = options
32        .files
33        .iter()
34        .cloned()
35        .filter(|file| *file != Path::new("-"))
36        .collect::<Vec<_>>();
37    let mut total = 0;
38    let mut errors = 0;
39    let mut special_files: Vec<PathBuf> = vec![];
40    if stdin_exist {
41        special_files.push("-".into());
42    }
43    for file in iterate_files(&filtered_files).chain(special_files) {
44        total += 1;
45        match run_single_file(&file) {
46            Ok(_) => {
47                if options.verbose {
48                    println!("{}", file.to_string_lossy());
49                }
50            }
51            Err(e) => {
52                if options.verbose {
53                    println!("{}: {}", file.to_string_lossy(), e);
54                }
55                errors += 1;
56            }
57        }
58    }
59
60    if options.verbose {
61        match total {
62            0 => println!("0 files"),
63            1 => println!("1 file"),
64            n => println!("{n} files"),
65        }
66    }
67
68    match errors {
69        0 => Ok(()),
70        1 => anyhow::bail!("1 failure"),
71        n => anyhow::bail!("{} failures", n),
72    }
73}
74
75/// Run all functions in a file that are succeeded by "run:" comments
76fn run_single_file(path: &PathBuf) -> Result<()> {
77    let file_contents = read_to_string(&path)?;
78    run_file_contents(file_contents)
79}
80
81/// Main body of `run_single_file` separated for testing
82fn run_file_contents(file_contents: String) -> Result<()> {
83    let options = ParseOptions {
84        default_calling_convention: CallConv::triple_default(&Triple::host()), // use the host's default calling convention
85        ..ParseOptions::default()
86    };
87    let test_file = parse_test(&file_contents, options)?;
88    let isa = create_target_isa(&test_file.isa_spec)?;
89    let mut tfc = TestFileCompiler::new(isa);
90    tfc.add_testfile(&test_file)?;
91    let compiled = tfc.compile()?;
92
93    for (func, Details { comments, .. }) in test_file.functions {
94        for comment in comments {
95            if let Some(command) = parse_run_command(comment.text, &func.signature)? {
96                let trampoline = compiled.get_trampoline(&func).unwrap();
97
98                command
99                    .run(|_, args| Ok(trampoline.call(args)))
100                    .map_err(|s| anyhow::anyhow!("{}", s))?;
101            }
102        }
103    }
104    Ok(())
105}
106
107/// Build an ISA based on the current machine running this code (the host)
108fn create_target_isa(isa_spec: &IsaSpec) -> Result<OwnedTargetIsa> {
109    let builder = host_isa_builder().map_err(|s| anyhow::anyhow!("{}", s))?;
110    match *isa_spec {
111        IsaSpec::None(ref flags) => {
112            // build an ISA for the current machine
113            Ok(builder.finish(flags.clone())?)
114        }
115        IsaSpec::Some(ref isas) => {
116            for isa in isas {
117                if isa.triple().architecture == HOST.architecture {
118                    return Ok(builder.finish(isa.flags().clone())?);
119                }
120            }
121            anyhow::bail!(
122                "The target ISA specified in the file is not compatible with the host ISA"
123            )
124        }
125    }
126}
127
128#[cfg(test)]
129mod test {
130    use super::*;
131
132    #[test]
133    fn nop() {
134        if cranelift_native::builder().is_err() {
135            return;
136        }
137        let code = String::from(
138            "
139            function %test() -> i8 {
140            block0:
141                nop
142                v1 = iconst.i8 -1
143                return v1
144            }
145            ; run
146            ",
147        );
148        run_file_contents(code).unwrap()
149    }
150}