use regex::Regex; use std::{ env, fs::File, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}, }; const CMD_IRRT_CLANG: &str = "clang-irrt"; const CMD_IRRT_CLANG_TEST: &str = "clang-irrt-test"; const CMD_IRRT_LLVM_AS: &str = "llvm-as-irrt"; fn get_out_dir() -> PathBuf { PathBuf::from(env::var("OUT_DIR").unwrap()) } fn get_irrt_dir() -> &'static Path { Path::new("irrt") } /// Compile `irrt.cpp` for use in `src/codegen` fn compile_irrt_cpp() { let out_dir = get_out_dir(); let irrt_dir = get_irrt_dir(); /* * HACK: Sadly, clang doesn't let us emit generic LLVM bitcode. * Compiling for WASM32 and filtering the output with regex is the closest we can get. */ let irrt_cpp_path = irrt_dir.join("irrt.cpp"); let flags: &[&str] = &[ "--target=wasm32", "-x", "c++", "-fno-discard-value-names", "-fno-exceptions", "-fno-rtti", match env::var("PROFILE").as_deref() { Ok("debug") => "-O0", Ok("release") => "-O3", flavor => panic!("Unknown or missing build flavor {flavor:?}"), }, "-emit-llvm", "-S", "-Wall", "-Wextra", "-Werror=return-type", "-o", "-", irrt_cpp_path.to_str().unwrap(), ]; // Tell Cargo to rerun if any file under `irrt_dir` (recursive) changes println!("cargo:rerun-if-changed={}", irrt_dir.to_str().unwrap()); // Compile IRRT and capture the LLVM IR output let output = Command::new(CMD_IRRT_CLANG) .args(flags) .output() .map(|o| { assert!(o.status.success(), "{}", std::str::from_utf8(&o.stderr).unwrap()); o }) .unwrap(); // https://github.com/rust-lang/regex/issues/244 let output = std::str::from_utf8(&output.stdout).unwrap().replace("\r\n", "\n"); let mut filtered_output = String::with_capacity(output.len()); // Filter out irrelevant IR // // Regex: // - `(?ms:^define.*?\}$)` captures LLVM `define` blocks // - `(?m:^declare.*?$)` captures LLVM `declare` lines let regex_filter = Regex::new(r"(?ms:^define.*?\}$)|(?m:^declare.*?$)").unwrap(); for f in regex_filter.captures_iter(&output) { assert_eq!(f.len(), 1); filtered_output.push_str(&f[0]); filtered_output.push('\n'); } let filtered_output = Regex::new("(#\\d+)|(, *![0-9A-Za-z.]+)|(![0-9A-Za-z.]+)|(!\".*?\")") .unwrap() .replace_all(&filtered_output, ""); // For debugging // Doing `DEBUG_DUMP_IRRT=1 cargo build -p nac3core` dumps the LLVM IR generated const DEBUG_DUMP_IRRT: &str = "DEBUG_DUMP_IRRT"; println!("cargo:rerun-if-env-changed={DEBUG_DUMP_IRRT}"); if env::var(DEBUG_DUMP_IRRT).is_ok() { let mut file = File::create(out_dir.join("irrt.ll")).unwrap(); file.write_all(output.as_bytes()).unwrap(); let mut file = File::create(out_dir.join("irrt-filtered.ll")).unwrap(); file.write_all(filtered_output.as_bytes()).unwrap(); } // Assemble the emitted and filtered IR to .bc // That .bc will be integrated into nac3core's codegen let mut llvm_as = Command::new(CMD_IRRT_LLVM_AS) .stdin(Stdio::piped()) .arg("-o") .arg(out_dir.join("irrt.bc")) .spawn() .unwrap(); llvm_as.stdin.as_mut().unwrap().write_all(filtered_output.as_bytes()).unwrap(); assert!(llvm_as.wait().unwrap().success()); } /// Compile `irrt_test.cpp` for testing fn compile_irrt_test_cpp() { let out_dir = get_out_dir(); let irrt_dir = get_irrt_dir(); let exe_path = out_dir.join("irrt_test.out"); // Output path of the compiled test executable let irrt_test_cpp_path = irrt_dir.join("irrt_test.cpp"); let flags: &[&str] = &[ irrt_test_cpp_path.to_str().unwrap(), "-x", "c++", "-I", irrt_dir.to_str().unwrap(), "-g", "-fno-discard-value-names", "-O0", "-Wall", "-Wextra", "-Werror=return-type", "-lm", // for `tgamma()`, `lgamma()` "-o", exe_path.to_str().unwrap(), ]; Command::new(CMD_IRRT_CLANG_TEST) .args(flags) .output() .map(|o| { assert!(o.status.success(), "{}", std::str::from_utf8(&o.stderr).unwrap()); o }) .unwrap(); println!("cargo:rerun-if-changed={}", irrt_dir.to_str().unwrap()); } fn main() { compile_irrt_cpp(); // https://github.com/rust-lang/cargo/issues/2549 // `cargo test -F test` to also build `irrt_test.cpp if cfg!(feature = "test") { compile_irrt_test_cpp(); } }