use nova_diagnostics::FileId;
use nova_lexer::lex;
use nova_mir::lower_module;
use nova_parser::parse;
use nova_resolver::resolve;
use nova_typeck::check;
fn mir_for(src: &str) -> nova_mir::Module {
let file_id = FileId::DUMMY;
let (tokens, lex_errors) = lex(src, file_id);
assert!(lex_errors.is_empty(), "lex errors: {lex_errors:?}");
let (ast, parse_errors) = parse(&tokens, file_id);
assert!(parse_errors.is_empty(), "parse errors: {parse_errors:?}");
let ast = ast.expect("no AST");
let resolved = resolve(&ast);
assert!(
resolved.diagnostics.is_empty(),
"resolve: {:?}",
resolved.diagnostics
);
let checked = check(&resolved.file, &resolved.definitions);
assert!(
checked.diagnostics.is_empty(),
"typeck: {:?}",
checked.diagnostics
);
lower_module(&checked.module).expect("MIR lowering failed")
}
fn function_names(mir: &nova_mir::Module) -> Vec<&str> {
mir.functions.iter().map(|f| f.name.as_str()).collect()
}
fn stmts_of<'m>(mir: &'m nova_mir::Module, name: &str) -> Vec<&'m nova_mir::Stmt> {
let instance = format!("{name}.");
let f = mir
.functions
.iter()
.find(|f| f.name == name || f.name.starts_with(&instance))
.unwrap_or_else(|| panic!("no function `{name}`: {:?}", function_names(mir)));
f.blocks.iter().flat_map(|b| &b.stmts).collect()
}
fn count_rt(stmts: &[&nova_mir::Stmt], want: nova_mir::RtFunc) -> usize {
stmts
.iter()
.filter(|s| matches!(s, nova_mir::Stmt::CallRuntime { func, .. } if *func == want))
.count()
}
#[test]
fn an_interpolation_of_three_or_more_parts_lowers_to_one_nary_concat() {
let mir = mir_for(
"fn s(a: Int, x: String, y: String) -> String { \"a=${a} x=${x} y=${y}\" }\n\
fn main() { println(s(1, \"p\", \"q\")) }",
);
let stmts = stmts_of(&mir, "s");
assert_eq!(
count_rt(&stmts, nova_mir::RtFunc::StrConcatN),
1,
"{stmts:?}"
);
assert_eq!(
count_rt(&stmts, nova_mir::RtFunc::StrConcat),
0,
"{stmts:?}"
);
assert_eq!(
stmts
.iter()
.filter(|s| matches!(s, nova_mir::Stmt::MakeArray { .. }))
.count(),
1,
"{stmts:?}"
);
}
#[test]
fn an_interpolation_of_two_parts_keeps_the_pairwise_concat() {
let mir = mir_for(
"fn s(x: String, y: String) -> String { \"${x}${y}\" }\n\
fn main() { println(s(\"p\", \"q\")) }",
);
let stmts = stmts_of(&mir, "s");
assert_eq!(
count_rt(&stmts, nova_mir::RtFunc::StrConcat),
1,
"{stmts:?}"
);
assert_eq!(
count_rt(&stmts, nova_mir::RtFunc::StrConcatN),
0,
"{stmts:?}"
);
}
#[test]
fn an_await_inside_a_three_part_interpolation_suspends_and_lowers_to_one_nary_concat() {
let mir = mir_for(
"async fn g() -> Int { 1 }\n\
async fn f() -> String { \"a${g().await}b\" }\n\
fn main() { let x = f() }",
);
assert_eq!(
resume_states(&mir, "f"),
2,
"an `.await` inside a three-part interpolation suspends"
);
let all: Vec<&nova_mir::Stmt> = mir
.functions
.iter()
.filter(|f| f.name.starts_with("f."))
.flat_map(|f| f.blocks.iter().flat_map(|b| &b.stmts))
.collect();
assert_eq!(count_rt(&all, nova_mir::RtFunc::StrConcatN), 1, "{all:?}");
assert_eq!(count_rt(&all, nova_mir::RtFunc::StrConcat), 0, "{all:?}");
}
#[test]
fn hello_world_lowers() {
let mir = mir_for("fn main() { println(\"hi\") }");
assert_eq!(function_names(&mir), vec!["main"]);
}
#[test]
fn fibonacci_lowers() {
let mir = mir_for(
"fn fib(n: Int) -> Int {\n\
if n <= 1 { n } else { fib(n - 1) + fib(n - 2) }\n\
}\n\
fn main() { println(\"${fib(10)}\") }",
);
let names = function_names(&mir);
assert!(names.contains(&"main"));
assert!(
names.iter().any(|n| n.starts_with("fib.")),
"names: {names:?}"
);
}
#[test]
fn generics_monomorphize_per_instance() {
let mir = mir_for(
"fn identity<T>(x: T) -> T { x }\n\
fn main() {\n\
let n = identity(1)\n\
let s = identity(\"s\")\n\
println(\"${n}${s}\")\n\
}",
);
let names = function_names(&mir);
assert!(
names
.iter()
.any(|n| n.starts_with("identity.") && n.ends_with("$i")),
"names: {names:?}"
);
assert!(
names
.iter()
.any(|n| n.starts_with("identity.") && n.ends_with("$s")),
"names: {names:?}"
);
assert_eq!(mir.functions.len(), 3);
}
#[test]
fn match_on_enum_lowers_to_switch() {
let mir = mir_for(
"type Shape = | Circle(Int) | Rect(Int, Int) | Empty\n\
fn area(s: Shape) -> Int {\n\
match s { Circle(r) => 3 * r * r, Rect(w, h) => w * h, Empty => 0, }\n\
}\n\
fn main() { println(\"${area(Circle(10))}\") }",
);
let area = mir
.functions
.iter()
.find(|f| f.name.starts_with("area."))
.expect("area lowered");
let has_switch = area
.blocks
.iter()
.any(|b| matches!(b.term, nova_mir::Terminator::Switch { .. }));
assert!(has_switch, "match should lower to a Switch terminator");
}
#[test]
fn records_lower_to_make_and_field() {
let mir = mir_for(
"record Point { x: Int, y: Int }\n\
fn main() {\n\
let p = Point { x: 3, y: 4 }\n\
println(\"${p.x}\")\n\
}",
);
let main = mir
.functions
.iter()
.find(|f| f.name == "main")
.expect("main");
let has_make = main.blocks.iter().any(|b| {
b.stmts
.iter()
.any(|s| matches!(s, nova_mir::Stmt::MakeRecord { .. }))
});
let has_field = main.blocks.iter().any(|b| {
b.stmts
.iter()
.any(|s| matches!(s, nova_mir::Stmt::RecordField { .. }))
});
assert!(has_make, "record literal should lower to MakeRecord");
assert!(has_field, "field access should lower to RecordField");
}
#[test]
fn field_assignment_lowers_to_set_field() {
let mir = mir_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 main = mir
.functions
.iter()
.find(|f| f.name == "main")
.expect("main");
let stmts: Vec<&nova_mir::Stmt> = main.blocks.iter().flat_map(|b| b.stmts.iter()).collect();
let sets: Vec<&nova_mir::Stmt> = stmts
.iter()
.copied()
.filter(|s| matches!(s, nova_mir::Stmt::SetField { .. }))
.collect();
assert_eq!(sets.len(), 1, "one field store, got {sets:?}");
let nova_mir::Stmt::SetField { index, ty, .. } = sets[0] else {
unreachable!("filtered to SetField")
};
assert_eq!(*index, 1);
assert_eq!(*ty, nova_mir::MirTy::I64);
}
#[test]
fn generic_record_monomorphizes_field_types() {
let mir = mir_for(
"record Box<T> { value: T }\n\
fn main() {\n\
let a = Box { value: 1 }\n\
let b = Box { value: \"hi\" }\n\
println(\"${a.value} ${b.value}\")\n\
}",
);
assert!(mir.functions.iter().any(|f| f.name == "main"));
}
#[test]
fn unreferenced_functions_are_not_emitted() {
let mir = mir_for("fn unused() { }\nfn main() { }");
assert_eq!(function_names(&mir), vec!["main"]);
}
#[test]
fn std_core_types_used_without_methods_emit_no_symbols() {
let mir = mir_for(
"fn main() {\n\
let none_val: Option<Int> = None\n\
let some_val = Some(1)\n\
let ok_val: Result<Int, String> = Ok(2)\n\
let err_val: Result<Int, String> = Err(\"e\")\n\
match some_val {\n\
Some(_) => println(\"some\"),\n\
None => println(\"none\"),\n\
}\n\
}",
);
let names = function_names(&mir);
assert_eq!(
names,
vec!["main"],
"constructing std/core types without calling their methods leaked a \
symbol into the module: {names:?}"
);
}
#[test]
fn std_collections_unused_emit_no_symbols() {
const MARKERS: [&str; 4] = ["Vec_T", "Map_K_V", "Set_T", "mix64"];
let mir = mir_for(
"record P { v: Int }\n\
impl P { fn scaled(self, k: Int) -> Int { self.v * k } }\n\
fn first<T>(xs: [T], fallback: T) -> T {\n\
if xs.len() == 0 { fallback } else { xs[0] }\n\
}\n\
fn main() {\n\
let n = 3\n\
let a = [7; n]\n\
let p = P { v: first(a, 0) + a.len() }\n\
let o: Option<Int> = Some(p.scaled(2))\n\
match o { Some(x) => println(\"${x}\"), None => println(\"none\"), }\n\
}",
);
let names = function_names(&mir);
for marker in MARKERS {
assert!(
!names.iter().any(|n| n.contains(marker)),
"a program that touches no collection emitted a `{marker}` symbol — \
std/collections is no longer pruned and every Nova binary just \
grew: {names:?}"
);
}
let used = mir_for(
"fn main() {\n\
let mut v = Vec::new()\n\
v.push(1)\n\
let mut m = Map::new()\n\
let _ = m.insert(1, 2)\n\
let mut s = Set::new()\n\
let _ = s.insert(3)\n\
println(\"${v.len()} ${m.len()} ${s.len()}\")\n\
}",
);
let used_names = function_names(&used);
for marker in MARKERS {
assert!(
used_names.iter().any(|n| n.contains(marker)),
"marker `{marker}` matches no symbol even when the collections are \
used, so the pruning assertion above proves nothing: {used_names:?}"
);
}
}
#[test]
fn trait_method_dispatches_to_impl() {
let mir = mir_for(
"record P { v: Int }\n\
trait Show { fn name(self) -> String }\n\
impl Show for P { fn name(self) -> String { \"p\" } }\n\
fn label<T: Show>(x: T) -> String { x.name() }\n\
fn main() { println(label(P { v: 1 })) }",
);
assert!(
mir.functions.iter().any(|f| f.name.contains("name")),
"impl method should be monomorphized: {:?}",
function_names(&mir)
);
}
#[test]
fn generic_impl_method_monomorphizes_per_element_type() {
let mir = mir_for(
"record Box<T> { value: T }\n\
impl<T> Box<T> { fn get(self) -> T { self.value } }\n\
fn main() {\n\
let a = Box { value: 1 }\n\
let b = Box { value: \"s\" }\n\
println(\"${a.get()} ${b.get()}\")\n\
}",
);
let names = function_names(&mir);
assert!(
names.iter().filter(|n| n.contains("get")).count() >= 2,
"expected two `get` instances, got {names:?}"
);
}
#[test]
fn generic_trait_impl_dispatches_to_instance() {
let mir = mir_for(
"record Box<T> { value: T }\n\
trait Tag { fn tag(self) -> String }\n\
impl<T> Tag for Box<T> { fn tag(self) -> String { \"b\" } }\n\
fn main() { let b = Box { value: 1 }\n println(b.tag()) }",
);
assert!(
mir.functions.iter().any(|f| f.name.contains("tag")),
"impl method should be monomorphized: {:?}",
function_names(&mir)
);
}
#[test]
fn conditional_generic_impl_unsatisfied_reports_e0013() {
let codes = diagnostics_for(
"trait Show { fn show(self) -> String }\n\
record Wrap<T> { inner: T }\n\
impl Show for Int { fn show(self) -> String { \"i\" } }\n\
impl<T: Show> Show for Wrap<T> { fn show(self) -> String { self.inner.show() } }\n\
fn present<X: Show>(x: X) -> String { x.show() }\n\
fn main() { println(present(Wrap { inner: true })) }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn method_generic_monomorphizes_per_instance() {
let mir = mir_for(
"record Box<T> { value: T }\n\
impl<T> Box<T> {\n\
fn map<U>(self, f: fn(T) -> U) -> Box<U> { Box { value: f(self.value) } }\n\
}\n\
fn twice(n: Int) -> Int { n * 2 }\n\
fn label(n: Int) -> String { \"${n}\" }\n\
fn main() {\n\
let a = Box { value: 5 }\n\
let b = a.map(twice)\n\
let c = a.map(label)\n\
println(\"${b.value} ${c.value}\")\n\
}",
);
let names = function_names(&mir);
assert!(
names.iter().filter(|n| n.contains("map")).count() >= 2,
"expected two `map` instances, got {names:?}"
);
}
#[test]
fn method_generic_bound_unsatisfied_reports_e0013() {
let codes = diagnostics_for(
"record Box<T> { value: T }\n\
trait Show { fn show(self) -> String }\n\
impl Show for Int { fn show(self) -> String { \"i\" } }\n\
impl<T> Box<T> { fn tag<U: Show>(self, x: U) -> String { x.show() } }\n\
fn main() { let a = Box { value: 1 }\n println(a.tag(true)) }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn where_clause_bound_unsatisfied_reports_e0013() {
let codes = diagnostics_for(
"trait Show { fn show(self) -> String }\n\
impl Show for Int { fn show(self) -> String { \"i\" } }\n\
fn label<T>(x: T) -> String where T: Show { x.show() }\n\
fn main() { println(label(true)) }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn generic_trait_method_monomorphizes_per_instance() {
let mir = mir_for(
"trait Mapper { fn raw(self) -> Int\n \
fn remap<U>(self, f: fn(Int) -> U) -> U { f(self.raw()) } }\n\
record C { n: Int }\n\
impl Mapper for C { fn raw(self) -> Int { self.n } }\n\
fn dbl(n: Int) -> Int { n * 2 }\n\
fn lbl(n: Int) -> String { \"${n}\" }\n\
fn main() { let c = C { n: 1 }\n println(\"${c.remap(dbl)} ${c.remap(lbl)}\") }",
);
let names = function_names(&mir);
assert!(
names.iter().filter(|n| n.contains("remap")).count() >= 2,
"expected two `remap` instances, got {names:?}"
);
}
#[test]
fn generic_trait_method_bound_unsatisfied_reports_e0013() {
let codes = diagnostics_for(
"trait Show { fn show(self) -> String }\n\
impl Show for Int { fn show(self) -> String { \"i\" } }\n\
trait Tagger { fn tag<U: Show>(self, x: U) -> String }\n\
record T { v: Int }\n\
impl Tagger for T { fn tag<U: Show>(self, x: U) -> String { x.show() } }\n\
fn main() { let t = T { v: 1 }\n println(t.tag(true)) }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn repeated_param_trait_impl_mismatch_reports_e0013() {
let codes = diagnostics_for(
"record Pair<A, B> { first: A, second: B }\n\
trait Same { fn same(self) -> Int }\n\
impl<T> Same for Pair<T, T> { fn same(self) -> Int { 1 } }\n\
fn use_it<X: Same>(x: X) -> Int { x.same() }\n\
fn main() { let p = Pair { first: 1, second: \"x\" }\n println(\"${use_it(p)}\") }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
fn deep_wrap_program(core: &str, depth: usize) -> String {
let mut inner = core.to_string();
for _ in 0..depth {
inner = format!("Wrap {{ inner: {inner} }}");
}
format!(
"trait Display {{ fn fmt(self) -> String }}\n\
record Wrap<T> {{ inner: T }}\n\
impl Display for Int {{ fn fmt(self) -> String {{ \"i\" }} }}\n\
impl<T: Display> Display for Wrap<T> {{ fn fmt(self) -> String {{ \"w\" }} }}\n\
fn describe<T: Display>(x: T) -> String {{ x.fmt() }}\n\
fn main() {{ let w = {inner}\n println(describe(w)) }}"
)
}
#[test]
fn deeply_nested_unsatisfiable_bound_is_rejected() {
let codes = diagnostics_for(&deep_wrap_program("true", 20));
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn deeply_nested_satisfiable_bound_is_accepted() {
let codes = diagnostics_for(&deep_wrap_program("0", 20));
assert!(codes.is_empty(), "unexpected diagnostics: {codes:?}");
}
#[test]
fn non_overlapping_concrete_impls_lower_to_distinct_functions() {
let mir = mir_for(
"record Pair<A, B> { first: A, second: B }\n\
trait Foo { fn foo(self) -> String }\n\
impl Foo for Pair<Int, Bool> { fn foo(self) -> String { \"b\" } }\n\
impl Foo for Pair<Int, Int> { fn foo(self) -> String { \"i\" } }\n\
fn main() {\n\
let pb = Pair { first: 1, second: true }\n\
let pii = Pair { first: 1, second: 2 }\n\
println(pb.foo())\n\
println(pii.foo())\n\
}",
);
let foo_fns = mir
.functions
.iter()
.filter(|f| f.name.contains("foo"))
.count();
assert_eq!(
foo_fns,
2,
"both concrete impl methods must be emitted as distinct functions: {:?}",
function_names(&mir)
);
}
#[test]
fn trait_associated_function_through_bound_lowers_without_a_receiver() {
let mir = mir_for(
"trait Zero { fn zero() -> Self }\n\
impl Zero for Int { fn zero() -> Int { 0 } }\n\
fn make<T: Zero>() -> T { T::zero() }\n\
fn main() { let n: Int = make()\n println(\"${n}\") }",
);
let names = function_names(&mir);
assert!(
names.iter().any(|n| n.contains("zero")),
"the Int impl's `zero` must be monomorphized: {names:?}"
);
let mut zero_calls = 0;
for f in &mir.functions {
for b in &f.blocks {
for s in &b.stmts {
if let nova_mir::Stmt::Call { callee, args, .. } = s {
if callee.contains("zero") {
zero_calls += 1;
assert!(
args.is_empty(),
"`{callee}` is receiver-less but was called with \
{} argument(s) from `{}`",
args.len(),
f.name
);
}
}
}
}
}
assert_eq!(zero_calls, 1, "expected exactly one call to `zero`");
}
fn diagnostics_for(src: &str) -> Vec<String> {
let file_id = FileId::DUMMY;
let (tokens, _) = lex(src, file_id);
let (ast, _) = parse(&tokens, file_id);
let ast = ast.expect("no AST");
let resolved = resolve(&ast);
let checked = check(&resolved.file, &resolved.definitions);
match lower_module(&checked.module) {
Ok(_) => Vec::new(),
Err(diags) => diags.into_iter().map(|d| d.code).collect(),
}
}
#[test]
fn unsatisfied_trait_bound_reports_e0013() {
let codes = diagnostics_for(
"record Q { v: Int }\n\
trait Show { fn name(self) -> String }\n\
fn label<T: Show>(x: T) -> String { \"x\" }\n\
fn main() { println(label(Q { v: 1 })) }",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn map_key_without_hash_reports_e0013() {
let codes = diagnostics_for(
"record Unhashable { v: Int }\n\
fn main() {\n\
let mut m = Map::new()\n\
let k = Unhashable { v: 1 }\n\
let p = m.insert(k, 1)\n\
println(\"${m.len()}\")\n\
}",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn fn_as_value_lowers_to_closure_and_indirect_call() {
let mir = mir_for(
"fn double(n: Int) -> Int { n * 2 }\n\
fn apply_twice<T>(f: fn(T) -> T, x: T) -> T { f(f(x)) }\n\
fn main() { println(\"${apply_twice(double, 5)}\") }",
);
let names = function_names(&mir);
assert!(
names.iter().any(|n| n.starts_with("double.")),
"names: {names:?}"
);
assert!(
names
.iter()
.any(|n| n.starts_with("apply_twice.") && n.ends_with("$i")),
"names: {names:?}"
);
let main = mir.functions.iter().find(|f| f.name == "main").unwrap();
let has_make = main.blocks.iter().any(|b| {
b.stmts
.iter()
.any(|s| matches!(s, nova_mir::Stmt::MakeClosure { .. }))
});
assert!(has_make, "bare-fn value should lower to MakeClosure");
let apply = mir
.functions
.iter()
.find(|f| f.name.starts_with("apply_twice.") && f.name.ends_with("$i"))
.expect("instance exists");
let has_indirect = apply.blocks.iter().any(|b| {
b.stmts
.iter()
.any(|s| matches!(s, nova_mir::Stmt::CallIndirect { .. }))
});
assert!(has_indirect, "call through fn param should be indirect");
}
#[test]
fn arrays_lower_with_bounds_check() {
let mir = mir_for(
"fn main() {\n\
let mut xs = [1, 2, 3]\n\
xs[0] = xs[1]\n\
println(\"${xs.len()} ${xs[2]}\")\n\
}",
);
use nova_mir::{RtFunc, Stmt};
let main = mir.functions.iter().find(|f| f.name == "main").unwrap();
let stmts: Vec<&Stmt> = main.blocks.iter().flat_map(|b| b.stmts.iter()).collect();
assert!(
stmts.iter().any(|s| matches!(s, Stmt::MakeArray { .. })),
"MakeArray"
);
assert!(
stmts.iter().any(|s| matches!(s, Stmt::ArrayGet { .. })),
"ArrayGet"
);
assert!(
stmts.iter().any(|s| matches!(s, Stmt::ArraySet { .. })),
"ArraySet"
);
assert!(
stmts.iter().any(|s| matches!(s, Stmt::ArrayLen { .. })),
"ArrayLen"
);
assert!(
stmts.iter().any(|s| matches!(
s,
Stmt::CallRuntime {
func: RtFunc::CheckBounds,
..
}
)),
"bounds check"
);
}
#[test]
fn repeat_array_lowers_to_alloc_plus_fill_loop() {
use nova_mir::{RtFunc, Stmt};
let mir = mir_for("fn main() { let n = 3\n let a = [7; n]\n println(\"${a[0]}\") }");
let main = mir
.functions
.iter()
.find(|f| f.name == "main")
.expect("main exists");
let stmts: Vec<&Stmt> = main.blocks.iter().flat_map(|b| b.stmts.iter()).collect();
assert!(
stmts.iter().any(|s| matches!(s, Stmt::ArrayAlloc { .. })),
"expected an ArrayAlloc"
);
assert!(
stmts.iter().any(|s| matches!(s, Stmt::ArraySet { .. })),
"expected the fill loop's ArraySet"
);
assert!(
main.blocks.len() > 1,
"expected a loop, got {} block(s)",
main.blocks.len()
);
assert!(
stmts.iter().any(|s| matches!(
s,
Stmt::CallRuntime {
func: RtFunc::Panic,
..
}
)),
"expected the negative-length guard's panic call"
);
}
#[test]
fn repeat_array_guards_both_ends_of_the_length_range() {
use nova_mir::{Stmt, MAX_ARRAY_LEN};
let mir = mir_for("fn main() { let n = 3\n let a = [7; n]\n println(\"${a[0]}\") }");
let main = mir
.functions
.iter()
.find(|f| f.name == "main")
.expect("main exists");
let stmts: Vec<&Stmt> = main.blocks.iter().flat_map(|b| b.stmts.iter()).collect();
let messages: Vec<&str> = stmts
.iter()
.filter_map(|s| match s {
Stmt::ConstStr(_, m) => Some(m.as_str()),
_ => None,
})
.collect();
assert!(
messages.contains(&"array length must not be negative"),
"expected the negative-length message, got {messages:?}"
);
assert!(
messages.contains(&"array length is too large"),
"expected the overlong-length message, got {messages:?}"
);
assert_eq!(MAX_ARRAY_LEN, 1_152_921_504_606_846_974);
assert!(
stmts
.iter()
.any(|s| matches!(s, Stmt::ConstInt(_, v) if *v == MAX_ARRAY_LEN)),
"expected the guard to materialize MAX_ARRAY_LEN"
);
}
#[test]
fn static_array_literal_still_lowers_without_a_loop() {
use nova_mir::Stmt;
let mir = mir_for("fn main() { let a = [1, 2, 3]\n println(\"${a[0]}\") }");
let main = mir
.functions
.iter()
.find(|f| f.name == "main")
.expect("main exists");
let stmts: Vec<&Stmt> = main.blocks.iter().flat_map(|b| b.stmts.iter()).collect();
assert!(
stmts.iter().any(|s| matches!(s, Stmt::MakeArray { .. })),
"expected a MakeArray"
);
assert!(
!stmts.iter().any(|s| matches!(s, Stmt::ArrayAlloc { .. })),
"a static literal should not use the runtime-length ArrayAlloc"
);
assert_eq!(main.blocks.len(), 1, "a static literal needs no branching");
}
#[test]
fn break_and_continue_lower_to_gotos() {
let mir = mir_for(
"fn main() {\n\
let mut i = 0\n\
while i < 10 {\n\
i = i + 1\n\
if i == 3 { continue }\n\
if i == 7 { break }\n\
}\n\
println(\"${i}\")\n\
}",
);
let main = mir.functions.iter().find(|f| f.name == "main").unwrap();
assert!(
main.blocks.len() >= 6,
"expected several blocks for break/continue, got {}",
main.blocks.len()
);
}
#[test]
fn closure_lowers_to_env_taking_function() {
let mir = mir_for(
"fn main() {\n\
let base = 10\n\
let f = |n| n + base\n\
println(\"${f(5)}\")\n\
}",
);
let closure = mir
.functions
.iter()
.find(|f| f.takes_env && f.capture_count == 1)
.expect("a closure with one capture was lifted");
let loads_capture = closure.blocks.iter().any(|b| {
b.stmts
.iter()
.any(|s| matches!(s, nova_mir::Stmt::RecordField { .. }))
});
assert!(loads_capture, "closure should load its capture from env");
}
#[test]
fn supertrait_derived_bound_is_discharged() {
let mir = mir_for(
"trait A { fn a(self) -> Int }\n\
trait B: A { fn b(self) -> Int }\n\
record R { v: Int }\n\
impl A for R { fn a(self) -> Int { 1 } }\n\
impl B for R { fn b(self) -> Int { 2 } }\n\
fn sum<T: B>(x: T) -> Int { x.a() + x.b() }\n\
fn main() { let r = R { v: 0 }\n println(\"${sum(r)}\") }",
);
let names = function_names(&mir);
assert!(
names.iter().any(|n| n.contains("A.a")),
"the supertrait method must be monomorphized: {names:?}"
);
assert!(
names.iter().any(|n| n.contains("B.b")),
"the subtrait method must be monomorphized: {names:?}"
);
}
#[test]
fn conditional_impl_discharges_derived_supertrait_bound() {
let mir = mir_for(
"trait A { fn a(self) -> Int }\n\
trait B: A { fn b(self) -> Int }\n\
record R { v: Int }\n\
record W<T> { inner: T }\n\
impl A for R { fn a(self) -> Int { 1 } }\n\
impl B for R { fn b(self) -> Int { 2 } }\n\
impl<T: B> A for W<T> { fn a(self) -> Int { self.inner.a() } }\n\
impl<T: B> B for W<T> { fn b(self) -> Int { self.inner.b() } }\n\
fn sum<U: B>(x: U) -> Int { x.a() + x.b() }\n\
fn main() {\n\
let w = W { inner: R { v: 0 } }\n\
println(\"${sum(w)}\")\n\
}",
);
let names = function_names(&mir);
assert!(
names.iter().filter(|n| n.contains("A.a")).count() >= 2,
"`a` should be monomorphized for both `W<R>` and `R`: {names:?}"
);
}
#[test]
fn supertrait_impl_with_a_narrower_bound_reports_e0013() {
let codes = diagnostics_for(
"trait Show { fn show(self) -> String }\n\
trait A { fn a(self) -> Int }\n\
trait B: A { fn b(self) -> Int }\n\
record W<T> { inner: T }\n\
impl Show for Int { fn show(self) -> String { \"i\" } }\n\
impl<T: Show> A for W<T> { fn a(self) -> Int { 1 } }\n\
impl<T> B for W<T> { fn b(self) -> Int { 2 } }\n\
fn sum<U: B>(x: U) -> Int { x.b() }\n\
fn main() {\n\
let w = W { inner: true }\n\
println(\"${sum(w)}\")\n\
}",
);
assert!(codes.contains(&"E0013".to_string()), "codes: {codes:?}");
}
#[test]
fn supertrait_default_body_dispatches_at_monomorphization() {
let mir = mir_for(
"trait A { fn a(self) -> Int }\n\
trait B: A { fn b(self) -> Int { self.a() + 1 } }\n\
record R { v: Int }\n\
impl A for R { fn a(self) -> Int { 1 } }\n\
impl B for R { }\n\
fn main() { let r = R { v: 0 }\n println(\"${r.b()}\") }",
);
let names = function_names(&mir);
assert!(
names.iter().any(|n| n.contains("A.a")),
"the supertrait method called from the default body must be \
monomorphized: {names:?}"
);
}
#[test]
fn hash_builtins_lower_to_a_runtime_call_and_a_move() {
use nova_mir::{RtFunc, Stmt};
let mir = mir_for("fn main() { println(\"${('a').hash()} ${(\"s\").hash()}\") }");
let find = |prefix: &str| {
mir.functions
.iter()
.find(|f| f.name.starts_with(prefix))
.unwrap_or_else(|| panic!("no `{prefix}*`: {:?}", function_names(&mir)))
};
let rt_calls = |f: &nova_mir::Function| -> Vec<RtFunc> {
f.blocks
.iter()
.flat_map(|b| &b.stmts)
.filter_map(|s| match s {
Stmt::CallRuntime { func, .. } => Some(*func),
_ => None,
})
.collect()
};
assert_eq!(
rt_calls(find("String.Hash.hash")),
vec![RtFunc::StrHash],
"`impl Hash for String` reaches the runtime exactly once"
);
let char_hash = find("Char.Hash.hash");
assert_eq!(
rt_calls(char_hash),
vec![RtFunc::IntHashSeed],
"`impl Hash for Char` reaches the runtime only for the seed draw"
);
assert!(
char_hash
.blocks
.iter()
.flat_map(|b| &b.stmts)
.any(|s| matches!(s, Stmt::Copy { .. })),
"`char_to_int` lowers to a `Copy`"
);
}
#[test]
fn http_parse_request_lowers_to_a_runtime_call() {
use nova_mir::{RtFunc, Stmt};
use nova_resolver::{resolve_program, ModuleSource, STD_MODULES};
let wrapper_src =
"pub fn lower_tests_http_parse_request_stub(buf: Bytes) -> [Int] { http_parse_request(buf) }\n";
let file_id = FileId::DUMMY;
let (tokens, lex_errors) = lex(
"fn main() {\n\
let buf = bytes_from_string(\"x\")\n\
let offsets = lower_tests_http_parse_request_stub(buf)\n\
println(\"${offsets.len()}\")\n\
}",
file_id,
);
assert!(lex_errors.is_empty(), "lex errors: {lex_errors:?}");
let (ast, parse_errors) = parse(&tokens, file_id);
assert!(parse_errors.is_empty(), "parse errors: {parse_errors:?}");
let ast = ast.expect("no AST");
let sources = [ModuleSource::new("main", &ast)];
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let resolved = resolve_program(
&sources,
&std_files,
Some((("$test.http_stub", wrapper_src), FileId::DUMMY)),
);
assert!(
resolved.diagnostics.is_empty(),
"resolve: {:?}",
resolved.diagnostics
);
let checked = check(&resolved.file, &resolved.definitions);
assert!(
checked.diagnostics.is_empty(),
"typeck: {:?}",
checked.diagnostics
);
let mir = lower_module(&checked.module).expect("MIR lowering failed");
let find = |prefix: &str| {
mir.functions
.iter()
.find(|f| f.name.starts_with(prefix))
.unwrap_or_else(|| panic!("no `{prefix}*`: {:?}", function_names(&mir)))
};
let rt_calls = |f: &nova_mir::Function| -> Vec<RtFunc> {
f.blocks
.iter()
.flat_map(|b| &b.stmts)
.filter_map(|s| match s {
Stmt::CallRuntime { func, .. } => Some(*func),
_ => None,
})
.collect()
};
assert_eq!(
rt_calls(find("lower_tests_http_parse_request_stub")),
vec![RtFunc::HttpParseRequest],
"a Nova call to `http_parse_request` reaches `RtFunc::HttpParseRequest` exactly once"
);
}
#[test]
fn crypto_builtins_reach_their_runtime_functions() {
use nova_mir::{RtFunc, Stmt};
use nova_resolver::{resolve_program, ModuleSource, STD_MODULES};
fn assert_stub_reaches(
wrapper_key: &str,
wrapper_src: &str,
main_src: &str,
stub_prefix: &str,
expected: RtFunc,
) {
let file_id = FileId::DUMMY;
let (tokens, lex_errors) = lex(main_src, file_id);
assert!(lex_errors.is_empty(), "lex errors: {lex_errors:?}");
let (ast, parse_errors) = parse(&tokens, file_id);
assert!(parse_errors.is_empty(), "parse errors: {parse_errors:?}");
let ast = ast.expect("no AST");
let sources = [ModuleSource::new("main", &ast)];
let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
let resolved = resolve_program(
&sources,
&std_files,
Some(((wrapper_key, wrapper_src), FileId::DUMMY)),
);
assert!(
resolved.diagnostics.is_empty(),
"resolve: {:?}",
resolved.diagnostics
);
let checked = check(&resolved.file, &resolved.definitions);
assert!(
checked.diagnostics.is_empty(),
"typeck: {:?}",
checked.diagnostics
);
let mir = lower_module(&checked.module).expect("MIR lowering failed");
let names: Vec<&str> = mir.functions.iter().map(|f| f.name.as_str()).collect();
let f = mir
.functions
.iter()
.find(|f| f.name.starts_with(stub_prefix))
.unwrap_or_else(|| panic!("no `{stub_prefix}*`: {names:?}"));
let rt_calls: Vec<RtFunc> = f
.blocks
.iter()
.flat_map(|b| &b.stmts)
.filter_map(|s| match s {
Stmt::CallRuntime { func, .. } => Some(*func),
_ => None,
})
.collect();
assert_eq!(
rt_calls,
vec![expected],
"a Nova call to `{stub_prefix}` reaches `{expected:?}` exactly once"
);
}
assert_stub_reaches(
"$test.crypto_hash_stub",
"pub fn lower_tests_crypto_hash_stub(op: Int, key: Bytes, data: Bytes, tag: Bytes) -> Int { crypto_hash(op, key, data, tag) }\n",
"fn main() {\n\
let key = bytes_from_ints([])\n\
let data = bytes_from_ints([])\n\
let tag = bytes_from_ints([])\n\
let _ = lower_tests_crypto_hash_stub(0, key, data, tag)\n\
}",
"lower_tests_crypto_hash_stub",
RtFunc::CryptoHash,
);
assert_stub_reaches(
"$test.crypto_random_bytes_stub",
"pub fn lower_tests_crypto_random_bytes_stub(n: Int) -> Int { crypto_random_bytes(n) }\n",
"fn main() { let _ = lower_tests_crypto_random_bytes_stub(8) }",
"lower_tests_crypto_random_bytes_stub",
RtFunc::CryptoRandomBytes,
);
assert_stub_reaches(
"$test.crypto_random_int_stub",
"pub fn lower_tests_crypto_random_int_stub(min: Int, max: Int) -> Int { crypto_random_int(min, max) }\n",
"fn main() { let _ = lower_tests_crypto_random_int_stub(0, 9) }",
"lower_tests_crypto_random_int_stub",
RtFunc::CryptoRandomInt,
);
}
fn resume_states(mir: &nova_mir::Module, name: &str) -> usize {
let prefix = format!("{name}.");
let poll = mir
.functions
.iter()
.find(|f| f.name.starts_with(&prefix) && f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no `$poll` for `{name}`: {:?}", function_names(mir)));
match &poll.blocks[0].term {
nova_mir::Terminator::Switch { arms, .. } => arms.len(),
_ => 1,
}
}
#[test]
fn a_reachable_async_fn_containing_await_becomes_a_resumable_poll_fn() {
let mir = mir_for(
"async fn f() -> Float { 1.5 }\n\
async fn g() -> Float { f().await }\n\
fn main() { let x = g() }",
);
let names = function_names(&mir);
for name in ["f", "g"] {
let wrapper = mir
.functions
.iter()
.find(|f| f.name.starts_with(&format!("{name}.")) && !f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no wrapper for `{name}`: {names:?}"));
assert_eq!(
wrapper.ret,
nova_mir::MirTy::Ptr,
"`{name}`'s original symbol must return a future"
);
}
assert_eq!(
resume_states(&mir, "g"),
2,
"one await means an entry state plus one resume state: {names:?}"
);
assert_eq!(
resume_states(&mir, "f"),
1,
"`f` is await-free, so it needs no dispatch: {names:?}"
);
}
#[test]
fn an_async_fn_containing_await_produces_no_diagnostics() {
let diags = diagnostics_for(
"async fn f() -> Float { 1.5 }\n\
async fn g() -> Float { f().await }\n\
fn main() { let x = g() }",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn an_await_buried_in_any_expression_form_still_splits_the_body() {
for (body, states) in [
("if true { g().await } else { 0 }", 2),
("let mut i = 0\n while i < 1 { i = g().await }\n i", 2),
("g().await + 1", 2),
("match g().await { _ => 0 }", 2),
("return g().await", 2),
("let a = [g().await, 2]\n a[0]", 2),
("g().await + g().await", 3),
] {
let src = format!(
"async fn g() -> Int {{ 1 }}\n\
async fn f() -> Int {{ {body} }}\n\
fn main() {{ let x = f() }}"
);
let mir = mir_for(&src);
assert_eq!(
resume_states(&mir, "f"),
states,
"a buried `.await` must still split the body: {src}"
);
}
let mir = mir_for(
"async fn g() -> Int { 1 }\n\
async fn f() -> String { \"${g().await}\" }\n\
fn main() { let x = f() }",
);
assert_eq!(
resume_states(&mir, "f"),
2,
"an `.await` inside a string interpolation suspends too"
);
}
#[test]
fn an_await_inside_a_match_arm_splits_a_block_reached_through_a_switch() {
let mir = mir_for(
"type Choice = | Left | Right\n\
async fn g() -> Int { 1 }\n\
async fn f(c: Choice) -> Int { match c { Left => g().await, Right => 0, } }\n\
fn main() { let x = f(Left) }",
);
let prefix = "f.";
let poll = mir
.functions
.iter()
.find(|f| f.name.starts_with(prefix) && f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no `$poll` for `f`: {:?}", function_names(&mir)));
assert_eq!(
resume_states(&mir, "f"),
2,
"the arm's `.await` must still split the body"
);
let body_switches = poll
.blocks
.iter()
.skip(1)
.filter(|b| matches!(b.term, nova_mir::Terminator::Switch { .. }))
.count();
assert!(
body_switches >= 2,
"expected the match's own tag Switch alongside the await's status \
Switch, found {body_switches}: {:?}",
poll.blocks
);
}
#[test]
fn an_await_free_async_fn_is_transformed_at_float() {
let mir = mir_for("async fn f() -> Float { 1.5 }\nfn main() { let x = f() }");
let names = function_names(&mir);
let wrapper = mir
.functions
.iter()
.find(|f| f.name.starts_with("f.") && !f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no wrapper for `f`: {names:?}"));
assert_eq!(
wrapper.ret,
nova_mir::MirTy::Ptr,
"the original symbol must return a future, not the body's Float"
);
let poll = mir
.functions
.iter()
.find(|f| f.name == format!("{}$poll", wrapper.name))
.unwrap_or_else(|| panic!("no `$poll` for `f`: {names:?}"));
assert_eq!(
poll.ret,
nova_mir::MirTy::I64,
"the poll fn returns a status, so the Float never crosses an i64 return"
);
assert!(poll.takes_env, "the poll fn's env is its state object");
assert_eq!(poll.params, 1, "poll's one real parameter is task_ctx");
}
#[test]
fn an_await_free_async_fn_produces_no_diagnostics() {
let diags = diagnostics_for("async fn f() -> Float { 1.5 }\nfn main() { let x = f() }");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unreached_async_fn_compiles_cleanly() {
let mir = mir_for("async fn f() -> Int { 1 }\nfn main() { }");
assert_eq!(function_names(&mir), vec!["main"]);
}
#[test]
fn a_reachable_async_inherent_method_is_transformed() {
let mir = mir_for(
"record W { v: Int }\n\
impl W { async fn get(self) -> Int { self.v } }\n\
fn main() { let w = W { v: 1 }\n let y = w.get() }",
);
let names = function_names(&mir);
let wrapper = mir
.functions
.iter()
.find(|f| f.name.starts_with("W.get.") && !f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no wrapper for `W.get`: {names:?}"));
assert_eq!(wrapper.ret, nova_mir::MirTy::Ptr);
assert_eq!(
wrapper.params, 1,
"the wrapper keeps `self` as its own parameter, since an async fn's \
arguments arrive at the wrapper and not at poll"
);
assert!(
names.contains(&format!("{}$poll", wrapper.name).as_str()),
"no `$poll` for `W.get`: {names:?}"
);
}
#[test]
fn a_reachable_generic_async_fn_instantiation_is_transformed() {
let mir = mir_for("async fn id<T>(x: T) -> T { x }\nfn main() { let y = id(1.5) }");
let names = function_names(&mir);
let wrapper = mir
.functions
.iter()
.find(|f| f.name.starts_with("id.") && !f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no wrapper for `id`: {names:?}"));
assert!(
wrapper.name.contains("$f"),
"the wrapper keeps the instantiation's mangled name, so two \
instantiations cannot collide on one symbol: {}",
wrapper.name
);
assert_eq!(wrapper.ret, nova_mir::MirTy::Ptr);
let poll = mir
.functions
.iter()
.find(|f| f.name == format!("{}$poll", wrapper.name))
.unwrap_or_else(|| panic!("no `$poll` for `id`: {names:?}"));
assert_eq!(poll.ret, nova_mir::MirTy::I64);
assert_eq!(
poll.temps[..2],
[nova_mir::MirTy::Ptr, nova_mir::MirTy::Ptr],
"the instantiated poll fn still has PollFn's `(state, task_ctx)` ABI, \
not the Float parameter its pre-transform self had at temp 0"
);
}
#[test]
fn block_on_reads_its_output_from_the_state_object_at_the_outputs_own_class() {
let mir = mir_for(
"async fn half(x: Float) -> Float { x / 2.0 }\n\
fn main() { println(\"${block_on(half(7.0))}\") }",
);
let names = function_names(&mir);
let block_on = mir
.functions
.iter()
.find(|f| f.name.starts_with("block_on."))
.unwrap_or_else(|| panic!("no `block_on` instance: {names:?}"));
let stmts: Vec<&nova_mir::Stmt> = block_on.blocks.iter().flat_map(|b| &b.stmts).collect();
assert!(
stmts.iter().any(|s| matches!(
s,
nova_mir::Stmt::CallRuntime {
func: nova_mir::RtFunc::TaskBlockOn,
dst: None,
..
}
)),
"`block_on` must drive the executor and discard its `i64` return: {stmts:?}"
);
assert!(
!stmts.iter().any(|s| matches!(
s,
nova_mir::Stmt::CallRuntime {
func: nova_mir::RtFunc::TaskTakeOutput,
..
}
)),
"the output must not also be taken through the runtime, which would \
unroot the state object this instance then reads: {stmts:?}"
);
let loads: Vec<(nova_mir::Temp, nova_mir::Temp, u32, nova_mir::MirTy)> = stmts
.iter()
.filter_map(|s| match s {
nova_mir::Stmt::RecordField {
dst,
record,
index,
ty,
} => Some((*dst, *record, *index, *ty)),
_ => None,
})
.collect();
assert_eq!(
loads.len(),
2,
"exactly two loads: the future's state word, then that state's output \
slot: {stmts:?}"
);
assert_eq!(
loads[0].3,
nova_mir::MirTy::Ptr,
"the state object's address is a pointer: {stmts:?}"
);
assert_eq!(
loads[1].3,
nova_mir::MirTy::F64,
"the output must be loaded AS an f64, not through the executor's i64: \
{stmts:?}"
);
assert_eq!(
loads[1].1, loads[0].0,
"the output load must go through the state object the first load \
produced, not straight off the future: {stmts:?}"
);
}
#[test]
fn an_async_main_is_wrapped_in_a_shim_that_drives_it() {
let file_id = FileId::DUMMY;
let src = "async fn main() { println(\"hi\") }";
let (tokens, _) = lex(src, file_id);
let (ast, _) = parse(&tokens, file_id);
let ast = ast.expect("no AST");
let resolved = resolve(&ast);
let checked = check(&resolved.file, &resolved.definitions);
assert!(
checked.diagnostics.is_empty(),
"typeck should accept this program: {:?}",
checked.diagnostics
);
let mir = lower_module(&checked.module).unwrap_or_else(|diags| {
panic!(
"an `async fn main` must lower, got {:?}",
diags.iter().map(|d| &d.code).collect::<Vec<_>>()
)
});
let names = function_names(&mir);
let main = mir
.functions
.iter()
.find(|f| f.name == "main")
.unwrap_or_else(|| panic!("no `main`: {names:?}"));
assert_eq!(
main.ret,
nova_mir::MirTy::Unit,
"`main` must be the shim, not the future-returning wrapper: {names:?}"
);
let drives: Vec<&str> = main
.blocks
.iter()
.flat_map(|b| &b.stmts)
.filter_map(|s| match s {
nova_mir::Stmt::CallRuntime { func, .. } => Some(func.symbol()),
nova_mir::Stmt::Call { callee, .. } => Some(callee.as_str()),
_ => None,
})
.collect();
assert!(
drives.contains(&"nova_rt_task_block_on"),
"the shim must hand the future to the executor: {drives:?}"
);
let wrapper = drives
.iter()
.find(|s| s.starts_with("main."))
.unwrap_or_else(|| panic!("the shim must call the mangled wrapper: {drives:?}"));
assert!(
mir.functions.iter().any(|f| f.name == **wrapper),
"the wrapper the shim calls must exist: {names:?}"
);
assert!(
mir.functions
.iter()
.any(|f| f.name == format!("{wrapper}$poll")),
"the user's body must still have been split into a poll fn: {names:?}"
);
}