llvm-in-rust 0.1.0

Top-level facade crate for the LLVM-in-Rust compiler pipeline.
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};

struct Config {
    input: PathBuf,
    predicate: String,
    output: PathBuf,
    evidence_dir: Option<PathBuf>,
    log: Option<PathBuf>,
    bucket_signature: Option<String>,
}

struct ReductionStats {
    attempts: usize,
    removals: usize,
}

fn usage() {
    eprintln!(
        "Usage: llvm-ir-min --input <file.ll> --predicate <cmd with {{input}}> \
         [--output <min.ll>] [--evidence-dir <dir>] [--log <reducer.log>] \
         [--bucket-signature <text>]"
    );
}

fn parse_args() -> Result<Config, String> {
    let mut input: Option<PathBuf> = None;
    let mut predicate: Option<String> = None;
    let mut output: Option<PathBuf> = None;
    let mut evidence_dir: Option<PathBuf> = None;
    let mut log: Option<PathBuf> = None;
    let mut bucket_signature: Option<String> = None;

    let mut it = env::args().skip(1);
    while let Some(arg) = it.next() {
        match arg.as_str() {
            "--input" => input = it.next().map(PathBuf::from),
            "--predicate" => predicate = it.next(),
            "--output" => output = it.next().map(PathBuf::from),
            "--evidence-dir" => evidence_dir = it.next().map(PathBuf::from),
            "--log" => log = it.next().map(PathBuf::from),
            "--bucket-signature" => bucket_signature = it.next(),
            "-h" | "--help" => {
                usage();
                std::process::exit(0);
            }
            _ => return Err(format!("unknown argument: {arg}")),
        }
    }

    let input = input.ok_or("missing --input".to_string())?;
    let predicate = predicate.ok_or("missing --predicate".to_string())?;
    let output = output.unwrap_or_else(|| {
        evidence_dir
            .as_ref()
            .map(|dir| dir.join("minimized.ll"))
            .unwrap_or_else(|| PathBuf::from("minimized.ll"))
    });
    let log = log.or_else(|| evidence_dir.as_ref().map(|dir| dir.join("reducer.log")));

    Ok(Config {
        input,
        predicate,
        output,
        evidence_dir,
        log,
        bucket_signature,
    })
}

fn run_predicate(cmd_tpl: &str, input_path: &Path) -> Result<bool, String> {
    let input = input_path
        .to_str()
        .ok_or("input path is not valid UTF-8".to_string())?;
    let cmd = cmd_tpl.replace("{{input}}", input);
    let status = Command::new("sh")
        .arg("-c")
        .arg(&cmd)
        .status()
        .map_err(|e| format!("failed to run predicate: {e}"))?;
    Ok(!status.success())
}

fn has_ir_shape(text: &str) -> bool {
    text.lines().any(|l| l.trim_start().starts_with("define "))
}

fn minimize_lines(
    original: &str,
    predicate: &str,
    work_file: &Path,
    log: &mut Vec<String>,
) -> Result<(String, ReductionStats), String> {
    let mut lines: Vec<String> = original.lines().map(|s| s.to_string()).collect();
    let mut changed = true;
    let mut stats = ReductionStats {
        attempts: 0,
        removals: 0,
    };

    while changed {
        changed = false;
        let mut i = 0usize;
        while i < lines.len() {
            let mut trial = lines.clone();
            let removed = trial.remove(i);
            let trial_text = format!("{}\n", trial.join("\n"));
            if !has_ir_shape(&trial_text) {
                log.push(format!("skip line {i}: removing it loses IR shape"));
                i += 1;
                continue;
            }
            stats.attempts += 1;
            fs::write(work_file, &trial_text).map_err(|e| format!("write trial file failed: {e}"))?;
            if run_predicate(predicate, work_file)? {
                log.push(format!("remove line {i}: {removed}"));
                lines = trial;
                changed = true;
                stats.removals += 1;
            } else {
                log.push(format!("keep line {i}: predicate no longer fails"));
                i += 1;
            }
        }
    }

    Ok((format!("{}\n", lines.join("\n")), stats))
}

fn write_evidence_package(config: &Config, original: &str, minimized: &str, log_text: &str) -> Result<(), String> {
    let Some(dir) = &config.evidence_dir else {
        return Ok(());
    };

    fs::create_dir_all(dir).map_err(|e| format!("failed to create evidence dir {}: {e}", dir.display()))?;
    fs::write(dir.join("original.ll"), original)
        .map_err(|e| format!("failed to write original.ll: {e}"))?;
    fs::write(dir.join("minimized.ll"), minimized)
        .map_err(|e| format!("failed to write minimized.ll: {e}"))?;
    fs::write(dir.join("reducer.log"), log_text)
        .map_err(|e| format!("failed to write reducer.log: {e}"))?;
    fs::write(
        dir.join("repro.sh"),
        format!("#!/usr/bin/env sh\nset -eu\n{}\n", config.predicate.replace("{{input}}", "${1:-minimized.ll}")),
    )
    .map_err(|e| format!("failed to write repro.sh: {e}"))?;
    let _ = Command::new("chmod").arg("+x").arg(dir.join("repro.sh")).status();

    let manifest = format!(
        "input={}\noutput={}\nbucket_signature={}\noriginal_bytes={}\nminimized_bytes={}\n",
        config.input.display(),
        config.output.display(),
        config.bucket_signature.as_deref().unwrap_or("unbucketed"),
        original.len(),
        minimized.len()
    );
    fs::write(dir.join("manifest.txt"), manifest)
        .map_err(|e| format!("failed to write manifest.txt: {e}"))?;
    Ok(())
}

fn main() -> ExitCode {
    let config = match parse_args() {
        Ok(v) => v,
        Err(e) => {
            eprintln!("error: {e}");
            usage();
            return ExitCode::from(2);
        }
    };

    let original = match fs::read_to_string(&config.input) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("error: failed to read {}: {e}", config.input.display());
            return ExitCode::from(1);
        }
    };

    if !has_ir_shape(&original) {
        eprintln!("error: input does not look like LLVM IR (no `define` found)");
        return ExitCode::from(1);
    }

    if let Err(e) = run_predicate(&config.predicate, &config.input).and_then(|failing| {
        if failing {
            Ok(())
        } else {
            Err("predicate does not fail on original input".to_string())
        }
    }) {
        eprintln!("error: {e}");
        return ExitCode::from(1);
    }

    let tmp = config.output.with_extension("tmp.ll");
    let mut log = vec![format!("input: {}", config.input.display())];
    if let Some(signature) = &config.bucket_signature {
        log.push(format!("bucket_signature: {signature}"));
    }
    let (minimized, stats) = match minimize_lines(&original, &config.predicate, &tmp, &mut log) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("error: minimization failed: {e}");
            let _ = fs::remove_file(&tmp);
            return ExitCode::from(1);
        }
    };
    let _ = fs::remove_file(&tmp);

    if let Some(parent) = config.output.parent() {
        if !parent.as_os_str().is_empty() {
            if let Err(e) = fs::create_dir_all(parent) {
                eprintln!("error: failed to create {}: {e}", parent.display());
                return ExitCode::from(1);
            }
        }
    }
    if let Err(e) = fs::write(&config.output, &minimized) {
        eprintln!("error: failed to write {}: {e}", config.output.display());
        return ExitCode::from(1);
    }

    log.push(format!("attempts: {}", stats.attempts));
    log.push(format!("removals: {}", stats.removals));
    log.push(format!("original_bytes: {}", original.len()));
    log.push(format!("minimized_bytes: {}", minimized.len()));
    let log_text = format!("{}\n", log.join("\n"));

    if let Some(log_path) = &config.log {
        if let Some(parent) = log_path.parent() {
            if !parent.as_os_str().is_empty() {
                if let Err(e) = fs::create_dir_all(parent) {
                    eprintln!("error: failed to create {}: {e}", parent.display());
                    return ExitCode::from(1);
                }
            }
        }
        if let Err(e) = fs::write(log_path, &log_text) {
            eprintln!("error: failed to write {}: {e}", log_path.display());
            return ExitCode::from(1);
        }
    }

    if let Err(e) = write_evidence_package(&config, &original, &minimized, &log_text) {
        eprintln!("error: {e}");
        return ExitCode::from(1);
    }

    println!(
        "minimized {} -> {} ({} -> {} bytes, {} removals)",
        config.input.display(),
        config.output.display(),
        original.len(),
        minimized.len(),
        stats.removals
    );

    ExitCode::SUCCESS
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn keeps_define_shape() {
        assert!(has_ir_shape("define i32 @main() {\nret i32 0\n}"));
        assert!(!has_ir_shape("; comment only"));
    }
}