hypothalamus 0.6.0

An optimizing Brainfuck AOT compiler with an LLVM IR backend
Documentation
use std::env;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;

const OUTPUT_TAIL_BYTES: usize = 4096;

#[derive(Debug)]
pub(crate) struct CapturedToolFailure {
    pub(crate) status: String,
    pub(crate) stdout: String,
    pub(crate) stderr: String,
}

pub(crate) fn run_captured(mut command: Command) -> io::Result<Option<CapturedToolFailure>> {
    let output = command.output()?;
    if output.status.success() {
        return Ok(None);
    }

    Ok(Some(CapturedToolFailure {
        status: output.status.to_string(),
        stdout: output_tail(&output.stdout),
        stderr: output_tail(&output.stderr),
    }))
}

pub(crate) fn find_tool(
    override_path: Option<&Path>,
    name: &str,
    fallback_dir: Option<&Path>,
) -> Option<PathBuf> {
    if let Some(path) = override_path {
        return Some(path.to_path_buf());
    }

    find_on_path(name).or_else(|| {
        let fallback_dir = fallback_dir?;
        let path = fallback_dir.join(name);
        path.is_file().then_some(path)
    })
}

pub(crate) fn find_on_path(name: &str) -> Option<PathBuf> {
    env::var_os("PATH").and_then(|path| {
        env::split_paths(&path)
            .map(|dir| dir.join(name))
            .find(|path| path.is_file())
    })
}

pub(crate) fn find_sibling_tool(driver: &str, candidates: &[&str]) -> Option<PathBuf> {
    let driver_path = resolve_command_path(driver)?;
    let dir = driver_path.parent()?;

    candidates.iter().find_map(|candidate| {
        let path = dir.join(candidate);
        if path.is_file() {
            return Some(path);
        }

        #[cfg(windows)]
        {
            let path = dir.join(format!("{candidate}.exe"));
            if path.is_file() {
                return Some(path);
            }
        }

        None
    })
}

pub(crate) fn resolve_command_path(command: &str) -> Option<PathBuf> {
    let path = Path::new(command);
    if path.components().count() > 1 && path.is_file() {
        return Some(path.to_path_buf());
    }

    find_on_path(command)
}

fn output_tail(bytes: &[u8]) -> String {
    let tail_start = bytes.len().saturating_sub(OUTPUT_TAIL_BYTES);
    let mut output = String::new();

    if tail_start > 0 {
        output.push_str("[truncated]\n");
    }
    output.push_str(&String::from_utf8_lossy(&bytes[tail_start..]));
    output
}