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::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
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()"));
let mut config =
CompilerConfig::for_target("examples/hello.bf", TargetProfile::resolve("nds-arm9"));
config.emit = EmitKind::LlvmIr;
let ir = compile_to_llvm(&config).expect("DS ARM9 target should lower to LLVM IR");
assert!(ir.contains("target triple = \"armv5te-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", "nds-arm9", "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 = TestTempDir::new(&format!("target-smoke-{target_name}"));
let output = temp_dir.path().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 = TestTempDir::new("gba-rom-smoke");
let output = temp_dir.path().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 Some(temp_dir) = TestTempDir::try_new("clang-target-check") else {
return false;
};
let output = temp_dir.path().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)
}
struct TestTempDir {
path: PathBuf,
}
impl TestTempDir {
fn new(label: &str) -> Self {
Self::try_new(label).expect("create test temp dir")
}
fn try_new(label: &str) -> Option<Self> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()?
.as_nanos();
let path = std::env::temp_dir().join(format!(
"hypothalamus-{label}-{}-{timestamp}",
std::process::id()
));
fs::create_dir_all(&path).ok()?;
Some(Self { path })
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestTempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}