Skip to main content

intarsia_build/
lib.rs

1//! Build-time helpers for compiling ISLE DSL files to Rust code.
2//!
3//! This crate provides utilities for [Cargo build scripts] to compile
4//! [ISLE] (Instruction Selection Lowering Expressions) domain-specific language
5//! files into Rust code. It uses the [`cranelift-isle`] compiler internally.
6//!
7//! [ISLE]: https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/isle/docs/language-reference.md
8//! [Cargo build scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
9//! [`cranelift-isle`]: https://docs.rs/cranelift-isle/
10//!
11//! # Example
12//!
13//! In your `Cargo.toml`:
14//! ```toml
15//! [dependencies]
16//! intarsia = "0.1"
17//!
18//! [build-dependencies]
19//! intarsia-build = "*"
20//! ```
21//!
22//! In your `build.rs`:
23//! ```no_run
24//! fn main() {
25//!     intarsia_build::compile_isle_auto().unwrap();
26//! }
27//! ```
28
29use std::error::Error;
30use std::fs;
31use std::path::{Path, PathBuf};
32
33/// Automatically discover and compile ISLE files in a conventional location.
34///
35/// This function looks for an `isle/` directory in the current directory
36/// and compiles all `.isle` files found there. The generated Rust code
37/// is written to the same `isle/` directory with a `.rs` extension.
38///
39/// This is a convenience wrapper around [`compile_isle_dir`] with a fixed path.
40///
41/// # Directory Structure
42///
43/// ```text
44/// your_project/
45///   ├── build.rs          (calls this function)
46///   ├── Cargo.toml
47///   └── src/
48///       ├── main.rs
49///       └── isle/
50///         ├── rules.isle    (your ISLE file)
51///         └── rules.rs      (generated - git ignore this)
52/// ```
53///
54/// # Example
55///
56/// ```no_run
57/// // build.rs
58/// fn main() {
59///     intarsia::build::compile_isle_auto().unwrap();
60/// }
61/// ```
62///
63/// # Errors
64///
65/// Returns an error if:
66/// - The `isle/` directory doesn't exist
67/// - No `.isle` files are found
68/// - ISLE compilation fails (see [`cranelift_isle::error::Errors`])
69/// - File I/O fails (see [`std::io::Error`])
70///
71/// [`cranelift_isle::error::Errors`]: https://docs.rs/cranelift-isle/latest/cranelift_isle/error/struct.Errors.html
72pub fn compile_isle_auto() -> Result<(), Box<dyn Error>> {
73    compile_isle_dir("src/isle")
74}
75
76/// Compile all ISLE files in a specified directory.
77///
78/// This function finds all `.isle` files in the given directory,
79/// compiles them using the ISLE compiler from [`cranelift-isle`], and writes
80/// the generated Rust code to `.rs` files in the same directory.
81///
82/// [`cranelift-isle`]: https://docs.rs/cranelift-isle/
83///
84/// # Arguments
85///
86/// * `isle_dir` - Path to directory containing `.isle` files (relative to current dir)
87///
88/// # Generated Files
89///
90/// For each `name.isle` file, generates `name.rs` in the same directory.
91/// The generated files should be added to `.gitignore`.
92///
93/// # Example
94///
95/// ```no_run
96/// // build.rs
97/// fn main() {
98///     // Compile ISLE files in examples/optimizer/isle/
99///     intarsia_build::compile_isle_dir("examples/optimizer/isle").unwrap();
100/// }
101/// ```
102///
103/// # Errors
104///
105/// Returns an error if:
106/// - The directory doesn't exist
107/// - No `.isle` files are found
108/// - ISLE compilation fails (see [`cranelift_isle::error::Errors`])
109/// - File I/O fails (see [`std::io::Error`])
110///
111/// [`cranelift_isle::error::Errors`]: https://docs.rs/cranelift-isle/latest/cranelift_isle/error/struct.Errors.html
112pub fn compile_isle_dir(isle_dir: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
113    let isle_dir = isle_dir.as_ref();
114
115    // Check if directory exists
116    if !isle_dir.exists() {
117        return Err(format!("ISLE directory not found: {}", isle_dir.display()).into());
118    }
119
120    if !isle_dir.is_dir() {
121        return Err(format!("Not a directory: {}", isle_dir.display()).into());
122    }
123
124    // Find all .isle files in the directory
125    let isle_files: Vec<PathBuf> = fs::read_dir(isle_dir)?
126        .filter_map(|entry| {
127            let entry = entry.ok()?;
128            let path = entry.path();
129            if path.extension()? == "isle" {
130                Some(path)
131            } else {
132                None
133            }
134        })
135        .collect();
136
137    if isle_files.is_empty() {
138        println!(
139            "cargo:warning=No .isle files found in {}",
140            isle_dir.display()
141        );
142        return Ok(());
143    }
144
145    // Compile each .isle file
146    for isle_file in isle_files {
147        compile_isle_file(&isle_file)?;
148    }
149
150    Ok(())
151}
152
153/// Compile a single ISLE file to Rust code.
154///
155/// This is a lower-level function that compiles a single ISLE file using
156/// [`cranelift_isle::compile::from_files`]. The generated Rust code is written
157/// to a `.rs` file in the same directory as the input file.
158///
159/// [`cranelift_isle::compile::from_files`]: https://docs.rs/cranelift-isle/latest/cranelift_isle/compile/fn.from_files.html
160///
161/// # Arguments
162///
163/// * `isle_file` - Path to the `.isle` file to compile
164///
165/// # Example
166///
167/// ```no_run
168/// // build.rs
169/// fn main() {
170///     intarsia_build::compile_isle_file("isle/rules.isle").unwrap();
171///     intarsia_build::compile_isle_file("isle/custom.isle").unwrap();
172/// }
173/// ```
174///
175/// # Errors
176///
177/// Returns an error if:
178/// - The file doesn't exist
179/// - ISLE compilation fails (see [`cranelift_isle::error::Errors`])
180/// - File I/O fails (see [`std::io::Error`])
181///
182/// [`cranelift_isle::error::Errors`]: https://docs.rs/cranelift-isle/latest/cranelift_isle/error/struct.Errors.html
183pub fn compile_isle_file(isle_file: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
184    let isle_file = isle_file.as_ref();
185
186    // Validate input file
187    if !isle_file.exists() {
188        return Err(format!("ISLE file not found: {}", isle_file.display()).into());
189    }
190
191    let file_name = isle_file
192        .file_name()
193        .ok_or("Invalid file name")?
194        .to_str()
195        .ok_or("Non-UTF8 file name")?;
196
197    // Set up cargo rerun-if-changed
198    println!("cargo:rerun-if-changed={}", isle_file.display());
199    println!("cargo:warning=Compiling ISLE file: {}", file_name);
200
201    // Output the generated Rust code to same directory with .rs extension
202    let output_file = isle_file.with_extension("rs");
203
204    // Compile the ISLE file
205    let code =
206        cranelift_isle::compile::from_files(vec![isle_file.to_path_buf()], &Default::default())
207            .map_err(|e| format!("ISLE compilation failed for {}: {:?}", file_name, e))?;
208
209    fs::write(&output_file, code)?;
210    println!("cargo:warning=Generated: {}", output_file.display());
211
212    Ok(())
213}
214
215/// Compile multiple specific ISLE files.
216///
217/// This is a convenience function for compiling a list of ISLE files.
218/// It calls [`compile_isle_file`] for each file in the provided slice.
219///
220/// # Example
221///
222/// ```no_run
223/// // build.rs
224/// fn main() {
225///     intarsia_build::compile_isle_files(&[
226///         "isle/rules.isle",
227///         "isle/cost.isle",
228///     ]).unwrap();
229/// }
230/// ```
231pub fn compile_isle_files(isle_files: &[impl AsRef<Path>]) -> Result<(), Box<dyn Error>> {
232    for isle_file in isle_files {
233        compile_isle_file(isle_file)?;
234    }
235    Ok(())
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_compile_isle_dir_not_exists() {
244        let result = compile_isle_dir("nonexistent_directory");
245        assert!(result.is_err());
246    }
247}