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}