use nova_codegen_llvm::compile_ir;
use nova_diagnostics::FileId;
fn ir_for(src: &str) -> String {
let file_id = FileId::DUMMY;
let (tokens, lex_errors) = nova_lexer::lex(src, file_id);
assert!(lex_errors.is_empty(), "lex: {lex_errors:?}");
let (ast, parse_errors) = nova_parser::parse(&tokens, file_id);
assert!(parse_errors.is_empty(), "parse: {parse_errors:?}");
let ast = ast.expect("ast");
let resolved = nova_resolver::resolve(&ast);
assert!(
resolved.diagnostics.is_empty(),
"resolve: {:?}",
resolved.diagnostics
);
let checked = nova_typeck::check(&resolved.file, &resolved.definitions);
assert!(
checked.diagnostics.is_empty(),
"typeck: {:?}",
checked.diagnostics
);
let mir = nova_mir::lower_module(&checked.module).expect("mir lowering");
compile_ir(&mir).expect("ir emission")
}
fn assert_well_formed(ir: &str) {
assert_eq!(
ir.matches('{').count(),
ir.matches('}').count(),
"unbalanced braces:\n{ir}"
);
let terms = ["br ", "ret ", "switch ", "unreachable"];
let lines: Vec<&str> = ir.lines().map(|l| l.trim()).collect();
for (i, line) in lines.iter().enumerate() {
if *line != "}" {
continue;
}
let prev = lines[..i].iter().rev().find(|l| !l.is_empty());
if let Some(prev) = prev {
assert!(
terms.iter().any(|k| prev.starts_with(k)),
"block does not end in a terminator (`{prev}`):\n{ir}"
);
}
}
}
#[test]
fn hello_emits_entry_and_c_wrapper() {
let ir = ir_for("fn main() { println(\"hi\") }");
assert!(ir.contains("define void @\"nova_main\"()"), "{ir}");
assert!(
ir.contains("define i32 @main(i32 %argc, ptr %argv)"),
"{ir}"
);
assert!(ir.contains("call void @nova_main()"), "{ir}");
assert!(ir.contains("declare void @nova_rt_println(ptr)"), "{ir}");
assert!(ir.contains("call void @nova_rt_println"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn string_literal_becomes_a_global_and_str_new() {
let ir = ir_for("fn main() { println(\"hi\") }");
assert!(
ir.contains("private unnamed_addr constant [2 x i8] c\"hi\""),
"{ir}"
);
assert!(ir.contains("call ptr @nova_rt_str_new(ptr"), "{ir}");
}
#[test]
fn recursion_and_branch_lower() {
let ir = ir_for(
"fn fib(n: Int) -> Int { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\n\
fn main() { println(\"${fib(5)}\") }",
);
assert!(ir.contains("define i64 @\"fib."), "{ir}");
assert!(ir.contains("call i64 @\"fib."), "{ir}");
assert!(ir.contains("icmp sle i64"), "{ir}");
assert!(ir.contains("br i1"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn match_lowers_to_switch_with_trap_default() {
let ir = ir_for(
"type Shape = | Circle(Int) | Rect(Int, Int) | Empty\n\
fn area(s: Shape) -> Int { match s { Circle(r) => r, Rect(w, h) => w, Empty => 0 } }\n\
fn main() { println(\"${area(Empty)}\") }",
);
assert!(ir.contains("switch i64"), "{ir}");
assert!(ir.contains("call ptr @nova_rt_alloc"), "{ir}");
assert!(ir.contains("call void @llvm.trap()"), "{ir}");
assert!(ir.contains("unreachable"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn bool_match_switches_on_i8_not_i64() {
let ir = ir_for(
"fn classify(b: Bool) -> Int { match b { true => 1, false => 0 } }\n\
fn main() { println(\"${classify(true)}\") }",
);
assert!(ir.contains("switch i8 "), "expected i8 switch:\n{ir}");
assert!(
!ir.contains("switch i64 "),
"a bool match must not switch on i64:\n{ir}"
);
assert_well_formed(&ir);
}
#[test]
fn sum_match_still_switches_on_i64() {
let ir = ir_for(
"type E = | A | B | C\n\
fn f(e: E) -> Int { match e { A => 0, B => 1, C => 2 } }\n\
fn main() { println(\"${f(B)}\") }",
);
assert!(ir.contains("switch i64 "), "expected i64 sum switch:\n{ir}");
assert_well_formed(&ir);
}
#[test]
fn floats_use_hex_constants_and_fp_ops() {
let ir = ir_for(
"fn main() {\n\
let a = 3.5\n let b = 2.0\n let c = a * b - 1.0\n\
println(\"${c > 5.0}\")\n\
}",
);
assert!(ir.contains("double 0x400C000000000000"), "{ir}");
assert!(ir.contains("fmul double"), "{ir}");
assert!(ir.contains("fsub double"), "{ir}");
assert!(ir.contains("fcmp ogt double"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn arrays_emit_bounds_check_and_element_addressing() {
let ir = ir_for(
"fn main() {\n\
let mut xs = [1, 2, 3]\n xs[0] = xs[1]\n\
println(\"${xs.len()} ${xs[2]}\")\n\
}",
);
assert!(ir.contains("call void @nova_rt_check_bounds"), "{ir}");
assert!(ir.contains("mul i64"), "{ir}");
assert!(ir.contains("getelementptr inbounds i8, ptr"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn closures_emit_fat_pointer_and_indirect_call() {
let ir = ir_for(
"fn main() {\n\
let base = 10\n let f = |n| n + base\n\
println(\"${f(5)}\")\n\
}",
);
assert!(ir.contains("call ptr @nova_rt_alloc(i64 16)"), "{ir}");
assert!(ir.contains("store ptr @\""), "{ir}");
assert!(ir.contains("(ptr "), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn records_emit_field_addressing() {
let ir = ir_for(
"record Point { x: Int, y: Int }\n\
fn main() { let p = Point { x: 3, y: 4 }\n println(\"${p.x}\") }",
);
assert!(ir.contains("call ptr @nova_rt_alloc"), "{ir}");
assert!(ir.contains("getelementptr inbounds i8, ptr"), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn field_assignment_emits_store_at_field_offset() {
let ir = ir_for(
"record Point { x: Int, y: Int }\n\
fn main() {\n\
let mut p = Point { x: 3, y: 4 }\n\
p.y = 5\n\
println(\"${p.y}\")\n\
}",
);
let body: Vec<&str> = ir
.lines()
.map(str::trim)
.skip_while(|l| !l.starts_with("define void @\"nova_main\"("))
.take_while(|l| *l != "}")
.collect();
assert!(!body.is_empty(), "no @nova_main body in:\n{ir}");
let alloc_reg = body
.iter()
.find_map(|l| l.strip_suffix(" = call ptr @nova_rt_alloc(i64 16)"))
.unwrap_or_else(|| panic!("no 16-byte record allocation in:\n{ir}"));
let reloaded_field1: Vec<&str> = body
.iter()
.filter_map(|l| {
let (dst, rest) = l.split_once(" = getelementptr inbounds i8, ptr ")?;
let base = rest.strip_suffix(", i64 8")?;
(base != alloc_reg).then_some(dst)
})
.collect();
assert!(
!reloaded_field1.is_empty(),
"no field-1 address off a reloaded record pointer in:\n{ir}"
);
assert!(
reloaded_field1.iter().any(|addr| body
.iter()
.any(|l| l.starts_with("store i64 ") && l.ends_with(&format!("ptr {addr}")))),
"expected an i64 store through a reloaded field-1 address in:\n{ir}"
);
assert_well_formed(&ir);
}
#[test]
fn repeat_array_emits_runtime_length_alloc() {
let ir = ir_for("fn main() { let n = 3\n let a = [7; n]\n println(\"${a[0]}\") }");
let body: Vec<&str> = ir.lines().map(|l| l.trim()).collect();
let defining = |reg: &str, op: &str| -> String {
let prefix = format!("{reg} = {op} ");
body.iter()
.find(|l| l.starts_with(&prefix))
.unwrap_or_else(|| panic!("expected `{prefix}…` in:\n{ir}"))
.to_string()
};
let scaled_operand = |line: &str, op: &str| -> String {
line.split_once(&format!(" = {op} "))
.and_then(|(_, rhs)| rhs.strip_suffix(", 8"))
.unwrap_or_else(|| panic!("expected `{op} <reg>, 8`, got `{line}` in:\n{ir}"))
.to_string()
};
let call = body
.iter()
.find(|l| l.contains(" = call ptr @nova_rt_alloc(i64 "))
.unwrap_or_else(|| panic!("expected an `@nova_rt_alloc(i64 …)` call in:\n{ir}"));
let alloc_dst = call
.split(" = ")
.next()
.expect("the alloc call line has a destination register");
let size_reg = call
.rsplit_once("(i64 ")
.and_then(|(_, rest)| rest.strip_suffix(')'))
.unwrap_or_else(|| panic!("expected a register size argument in `{call}`"));
let add = defining(size_reg, "add i64");
let bytes_reg = scaled_operand(&add, "add i64");
let mul = defining(&bytes_reg, "mul i64");
let len_reg = scaled_operand(&mul, "mul i64");
let len_store = format!("store i64 {len_reg}, ptr {alloc_dst}");
assert!(
body.contains(&len_store.as_str()),
"expected `{len_store}` (the length stored at offset 0 through the \
allocation's own pointer) in:\n{ir}"
);
assert!(
body.iter().any(|l| l.starts_with("br i1 ")),
"expected the fill loop's conditional branch in:\n{ir}"
);
assert!(
ir.contains("call void @nova_rt_panic_str("),
"expected the negative-length guard's panic call in:\n{ir}"
);
assert_well_formed(&ir);
}
#[test]
fn panic_emits_declaration_and_call() {
let ir = ir_for("fn main() { panic(\"boom\") }");
assert!(ir.contains("declare void @nova_rt_panic_str(ptr)"), "{ir}");
assert!(ir.contains("call void @nova_rt_panic_str("), "{ir}");
assert_well_formed(&ir);
}
#[test]
fn every_rt_func_is_declared_with_its_real_signature() {
fn llty(ty: nova_mir::MirTy) -> &'static str {
match ty {
nova_mir::MirTy::I64 => "i64",
nova_mir::MirTy::F64 => "double",
nova_mir::MirTy::I8 => "i8",
nova_mir::MirTy::Ptr => "ptr",
nova_mir::MirTy::Unit => "void",
}
}
let ir = ir_for("fn main() {}");
for rt in nova_mir::RtFunc::ALL {
let (params, ret) = rt.signature();
let params: Vec<&str> = params.iter().map(|&p| llty(p)).collect();
let expected = format!(
"declare {} @{}({})",
llty(ret),
rt.symbol(),
params.join(", ")
);
assert!(
ir.lines().any(|l| l == expected),
"missing declaration for RtFunc::{rt:?}: expected line `{expected}` in IR:\n{ir}"
);
}
}
#[test]
fn an_await_free_async_fn_emits_a_poll_fn_and_a_wrapper() {
let ir = ir_for("async fn f() -> Float { 1.5 }\nfn main() { let x = f() }");
assert_well_formed(&ir);
let poll = ir
.lines()
.find(|l| l.starts_with("define ") && l.contains("$poll\""))
.unwrap_or_else(|| panic!("no `$poll` definition:\n{ir}"));
assert!(
poll.starts_with("define i64 @\"") && poll.ends_with("(ptr %p0, ptr %p1) {"),
"the poll fn must be `(state, task_ctx) -> i64`, got `{poll}`"
);
let symbol = poll
.trim_start_matches("define i64 @\"")
.split('"')
.next()
.expect("a quoted symbol");
let wrapper_symbol = symbol.trim_end_matches("$poll");
assert!(
ir.contains(&format!("define ptr @\"{wrapper_symbol}\"() {{")),
"the original symbol must survive as a `() -> ptr` wrapper:\n{ir}"
);
assert!(
ir.contains(&format!("store ptr @\"{symbol}\", ptr ")),
"word 0 of the future must be the poll fn's address:\n{ir}"
);
let poll_body = ir
.split(poll)
.nth(1)
.expect("text after the poll definition")
.split("\n}")
.next()
.expect("the poll fn's body")
.to_string();
let bits = format!("0x{:016X}", 1.5f64.to_bits());
assert!(
poll_body.contains(&bits),
"the body's constant ({bits}) must be inside the poll fn:{poll_body}"
);
let out_offset = nova_mir::STATE_SLOT_OUTPUT as i64 * 8;
let lines: Vec<&str> = poll_body.lines().map(str::trim).collect();
let out_store = lines
.windows(2)
.find(|w| w[0].ends_with(&format!("i64 {out_offset}")) && w[1].starts_with("store "))
.map(|w| w[1])
.unwrap_or_else(|| panic!("no store at byte offset {out_offset}:{poll_body}"));
assert!(
out_store.starts_with("store double %"),
"the output store must carry the f64 class, not the i64 status class: \
`{out_store}`"
);
assert!(
poll_body.contains(&format!("store i64 {}, ptr ", nova_mir::POLL_READY)),
"the poll fn must set its status to the POLL_READY constant:{poll_body}"
);
assert!(
lines.last().is_some_and(|l| l.starts_with("ret i64 ")),
"the poll fn must return an i64 status:{poll_body}"
);
}
fn poll_body_of(ir: &str, name: &str) -> String {
let prefix = format!("define i64 @\"{name}.");
let header = ir
.lines()
.find(|l| l.starts_with(&prefix) && l.contains("$poll\""))
.unwrap_or_else(|| panic!("no `$poll` definition for `{name}`:\n{ir}"));
ir.split(header)
.nth(1)
.expect("text after the definition")
.split("\n}")
.next()
.expect("the poll fn's body")
.to_string()
}
fn blocks_of(body: &str) -> std::collections::HashMap<String, Vec<String>> {
let mut out: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
let mut current = String::new();
for line in body.lines() {
let t = line.trim();
if let Some(label) = t.strip_suffix(':') {
if !label.is_empty() && !label.contains(' ') {
current = label.to_string();
out.entry(current.clone()).or_default();
continue;
}
}
if !current.is_empty() && !t.is_empty() {
out.entry(current.clone()).or_default().push(t.to_string());
}
}
out
}
fn parse_switch(line: &str) -> (String, Vec<(i64, String)>) {
let (head, arms) = line.split_once('[').expect("a switch's arm list");
let default = head
.rsplit_once("label %")
.expect("a switch default label")
.1
.trim()
.to_string();
let arms = arms
.trim_end()
.trim_end_matches(']')
.split("i64 ")
.filter_map(|a| {
let (v, l) = a.split_once(", label %")?;
Some((v.trim().parse::<i64>().ok()?, l.trim().to_string()))
})
.collect();
(default, arms)
}
#[test]
fn an_async_fn_containing_await_emits_a_resume_dispatch_and_an_indirect_poll() {
let ir = ir_for(
"async fn g() -> Float { 1.5 }\n\
async fn f() -> Float { g().await + 1.0 }\n\
fn main() { let x = f() }",
);
assert_well_formed(&ir);
let body = poll_body_of(&ir, "f");
let blocks = blocks_of(&body);
let switches: Vec<&String> = body
.lines()
.map(str::trim)
.filter(|l| l.starts_with("switch i64 "))
.map(|l| {
blocks
.values()
.flatten()
.find(|s| s.as_str() == l)
.expect("every switch line belongs to a block")
})
.collect();
assert_eq!(
switches.len(),
2,
"one resume dispatch plus one status switch per await:{body}"
);
let (dispatch_default, dispatch_arms) = parse_switch(switches[0]);
let mut values: Vec<i64> = dispatch_arms.iter().map(|(v, _)| *v).collect();
values.sort_unstable();
assert_eq!(
values,
vec![0, 1],
"the entry state plus one resume tag: `{}`",
switches[0]
);
let distinct: std::collections::HashSet<&String> =
dispatch_arms.iter().map(|(_, l)| l).collect();
assert_eq!(
distinct.len(),
2,
"the resume states must be distinct blocks: `{}`",
switches[0]
);
let tag_offset = nova_mir::STATE_SLOT_TAG as i64 * 8;
let entry = blocks
.get("bb0")
.unwrap_or_else(|| panic!("no `bb0` (the dispatch):{body}"));
assert!(
entry
.windows(2)
.any(
|w| w[0].ends_with(&format!("i64 {tag_offset}")) && w[1].starts_with("load i64")
|| w[0].ends_with(&format!("i64 {tag_offset}")) && w[1].contains("= load i64")
),
"the dispatch must switch on an i64 loaded from byte offset {tag_offset}, \
the tag slot: {entry:?}"
);
let (status_default, status_arms) = parse_switch(switches[1]);
assert_eq!(
dispatch_default, status_default,
"both switches send an unrecognized discriminant to one shared block: \
`{}` vs `{}`",
switches[0], switches[1]
);
let bad = blocks
.get(&status_default)
.unwrap_or_else(|| panic!("no block `{status_default}`:{body}"));
assert!(
bad.iter().any(|l| l.contains("@llvm.trap()")) && bad.iter().any(|l| l == "unreachable"),
"the shared default must trap: {bad:?}"
);
assert!(
body.lines()
.map(str::trim)
.any(|l| l.contains("= call i64 %") && l.matches("ptr %").count() == 2),
"the await must be an indirect `(ptr, ptr) -> i64` call:{body}"
);
let pending = status_arms
.iter()
.find_map(|(v, l)| (*v == nova_mir::POLL_PENDING).then_some(l))
.unwrap_or_else(|| panic!("no PENDING arm: `{}`", switches[1]));
let suspend = blocks
.get(pending)
.unwrap_or_else(|| panic!("no block `{pending}`:{body}"));
assert!(
suspend
.windows(2)
.any(|w| w[0].ends_with(&format!("i64 {tag_offset}")) && w[1].starts_with("store i64")),
"the suspend must store a tag at byte offset {tag_offset}: {suspend:?}"
);
assert!(
suspend.iter().any(
|l| l == &format!("store i64 {}, ptr %t6.slot", nova_mir::POLL_PENDING)
|| l.starts_with(&format!("store i64 {}, ptr %t", nova_mir::POLL_PENDING))
),
"the suspend must materialize the POLL_PENDING constant: {suspend:?}"
);
assert!(
suspend.last().is_some_and(|l| l.starts_with("ret i64 ")),
"the suspend must return a status: {suspend:?}"
);
}