hypothalamus 0.4.0

An optimizing Brainfuck AOT compiler with an LLVM IR backend
Documentation
use hypothalamus::DEFAULT_TAPE_SIZE;
use hypothalamus::bf;
use hypothalamus::driver::{CompilerConfig, EmitKind, compile_to_llvm, compile_with_tools};
use hypothalamus::llvm::{self, LlvmOptions};
use hypothalamus::target::TargetProfile;
use hypothalamus::targets::gba;
use std::fs;
use std::process::{Command, Stdio};

fn compile_example(source: &[u8], name: &str) -> String {
    let ops = bf::parse(source).expect("example should parse");
    llvm::generate_module(
        &ops,
        &LlvmOptions {
            tape_size: DEFAULT_TAPE_SIZE,
            target_triple: None,
            source_filename: Some(name.to_string()),
            bounds_check: false,
            runtime: llvm::Runtime::Hosted,
        },
    )
    .expect("example should lower to LLVM IR")
}

#[test]
fn hello_example_lowers_to_llvm() {
    let ir = compile_example(include_bytes!("../examples/hello.bf"), "examples/hello.bf");
    assert!(ir.contains("define i32 @main()"));
}

#[test]
fn scan_loop_fixture_lowers_to_explicit_scan() {
    let ir = compile_example(b"+[>]", "scan-loop.bf");
    assert!(ir.contains("define i32 @main()"));
    assert!(ir.contains("scan_check_"));
}

#[test]
fn multiply_transfer_fixture_lowers_to_straight_line_ir() {
    let ir = compile_example(b"+++++[->+++>++<<]>>.", "multiply-transfer.bf");
    assert!(ir.contains("define i32 @main()"));
    assert!(ir.contains("mul i8"));
    assert!(!ir.contains("loop_check_"));
}

#[test]
fn target_presets_emit_expected_llvm_runtime() {
    let mut config = CompilerConfig::for_target("examples/hello.bf", TargetProfile::resolve("gba"));
    config.emit = EmitKind::LlvmIr;

    let ir = compile_to_llvm(&config).expect("GBA target should lower to LLVM IR");

    assert!(ir.contains("target triple = \"thumbv4t-none-eabi\""));
    assert!(ir.contains("define void @bf_main()"));
    assert!(ir.contains("declare void @bf_putchar(i8)"));
    assert!(!ir.contains("define i32 @main()"));
}

#[test]
fn freestanding_targets_emit_objects_when_clang_supports_them() {
    for target_name in ["i386-none", "gba"] {
        let target = TargetProfile::resolve(target_name);
        let Some(triple) = target.llvm_triple() else {
            continue;
        };
        if !clang_supports_target(triple, target.clang_args()) {
            eprintln!("skipping {target_name} object smoke test: clang does not support {triple}");
            continue;
        }

        let temp_dir = std::env::temp_dir().join(format!(
            "hypothalamus-target-smoke-{}-{target_name}",
            std::process::id()
        ));
        fs::create_dir_all(&temp_dir).expect("create target smoke temp dir");
        let output = temp_dir.join("hello.o");

        let mut config = CompilerConfig::for_target("examples/hello.bf", target);
        config.emit = EmitKind::Object;
        config.output = Some(output.clone());

        compile_with_tools(&config).expect("target object smoke should compile");
        assert!(output.exists());
    }
}

#[test]
fn gba_target_builds_rom_when_tools_are_available() {
    if gba::find_gba_tool(None, "arm-none-eabi-gcc").is_none()
        || gba::find_gba_tool(None, "arm-none-eabi-objcopy").is_none()
    {
        eprintln!("skipping GBA ROM smoke test: devkitARM tools are not available");
        return;
    }

    let temp_dir =
        std::env::temp_dir().join(format!("hypothalamus-gba-rom-smoke-{}", std::process::id()));
    fs::create_dir_all(&temp_dir).expect("create GBA ROM temp dir");
    let output = temp_dir.join("hello.gba");

    let mut config = CompilerConfig::for_target("examples/hello.bf", TargetProfile::resolve("gba"));
    config.output = Some(output.clone());

    compile_with_tools(&config).expect("GBA ROM smoke should compile");

    let rom = fs::read(output).expect("read generated GBA ROM");
    assert!(gba::has_valid_header(&rom));
    assert!(rom.len() > gba::HEADER_SIZE);
    assert_eq!(&rom[0xA0..0xAC], gba::ROM_TITLE);
    assert_eq!(&rom[0xAC..0xB0], gba::GAME_CODE);
}

fn clang_supports_target(triple: &str, clang_args: &[String]) -> bool {
    let temp_dir = std::env::temp_dir().join(format!(
        "hypothalamus-clang-target-check-{}",
        std::process::id()
    ));
    if fs::create_dir_all(&temp_dir).is_err() {
        return false;
    }
    let output = temp_dir.join("check.o");

    let mut child = match Command::new("clang")
        .arg(format!("--target={triple}"))
        .args(clang_args)
        .arg("-x")
        .arg("c")
        .arg("-c")
        .arg("-")
        .arg("-o")
        .arg(output)
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(child) => child,
        Err(_) => return false,
    };

    {
        let Some(stdin) = child.stdin.as_mut() else {
            return false;
        };
        use std::io::Write;
        if stdin.write_all(b"void f(void) {}\n").is_err() {
            return false;
        }
    }

    child.wait().map(|status| status.success()).unwrap_or(false)
}