hypothalamus 0.6.0

An optimizing Brainfuck AOT compiler with an LLVM IR backend
Documentation
use hypothalamus::DEFAULT_TAPE_SIZE;
use hypothalamus::bf;
use hypothalamus::llvm::{self, LlvmOptions, Runtime};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};

const INTERPRETER_BF: &[u8] = include_bytes!("../examples/interpreter.bf");

fn interpreter_llvm() -> String {
    let ops = bf::parse(INTERPRETER_BF).expect("owned interpreter should parse");
    llvm::generate_module(
        &ops,
        &LlvmOptions {
            tape_size: DEFAULT_TAPE_SIZE,
            target_triple: None,
            source_filename: Some("examples/interpreter.bf".to_string()),
            bounds_check: false,
            runtime: Runtime::Hosted,
        },
    )
    .expect("owned interpreter should lower to LLVM IR")
}

#[test]
fn owned_interpreter_lowers_to_llvm() {
    let ir = interpreter_llvm();

    assert!(ir.contains("define i32 @main()"));
    assert!(ir.contains("@putchar"));
    assert!(ir.contains("loop_check_"));
}

#[test]
fn owned_interpreter_runs_smoke_programs_when_clang_is_available() {
    if !clang_available() {
        eprintln!("skipping owned interpreter execution smoke test: clang is not available");
        return;
    }

    let temp_dir = TestTempDir::new("owned-interpreter");

    let ll_path = temp_dir.path().join("interpreter.ll");
    let exe_path = temp_dir.path().join("interpreter");
    fs::write(&ll_path, interpreter_llvm()).expect("write interpreter LLVM IR");

    let status = Command::new("clang")
        .arg("-O3")
        .arg(&ll_path)
        .arg("-o")
        .arg(&exe_path)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .expect("run clang");

    if !status.success() {
        eprintln!("skipping owned interpreter execution smoke test: clang failed");
        return;
    }

    assert_eq!(run_interpreter(&exe_path, b",+.!A"), b"B");
    assert_eq!(run_interpreter(&exe_path, b"++[>++[>++<-]<-]>>.!"), &[8]);
}

fn clang_available() -> bool {
    Command::new("clang")
        .arg("--version")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

fn run_interpreter(exe_path: &Path, input: &[u8]) -> Vec<u8> {
    let mut child = Command::new(exe_path)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect("run owned interpreter");

    {
        let stdin = child.stdin.as_mut().expect("interpreter stdin");
        use std::io::Write;
        stdin.write_all(input).expect("write interpreter input");
    }

    let output = child.wait_with_output().expect("read interpreter output");
    assert!(output.status.success(), "interpreter should exit cleanly");
    output.stdout
}

struct TestTempDir {
    path: PathBuf,
}

impl TestTempDir {
    fn new(label: &str) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system clock should be after UNIX epoch")
            .as_nanos();
        let path = std::env::temp_dir().join(format!(
            "hypothalamus-{label}-{}-{timestamp}",
            std::process::id()
        ));
        fs::create_dir_all(&path).expect("create test temp dir");
        Self { path }
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

impl Drop for TestTempDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}