mod analyze;
mod link;
mod program;
mod runtime_cache;
pub use analyze::{analyze, analyze_program, Analysis, DiskSources, Options, Probe, Sources};
pub use program::{package_of, Program, Root, RootKind, Roots};
pub use runtime_cache::{set_embedded_runtime, EmbeddedRuntime};
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use nova_codegen_cranelift::CompiledProgram;
use nova_diagnostics::{render, Diagnostic, FileDb, FileId, Severity, Span};
use nova_hir as hir;
use nova_hir::Ty;
use nova_pm::PackageId;
use nova_resolver::{Builtin, Def, DefId, DefKind, Definitions, ModuleId, ModuleSource, TestFn};
use program::Loaded;
pub enum Outcome<T> {
Ok(T),
Failed {
errors: usize,
},
}
pub fn check_file(path: &Path) -> Result<Outcome<()>> {
check_program(Program::for_file(path))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Checked {
pub errors: usize,
pub warnings: usize,
}
pub fn check_program(program: Program) -> Result<Outcome<()>> {
let checked = check_program_counted(program)?;
if checked.errors > 0 {
Ok(Outcome::Failed {
errors: checked.errors,
})
} else {
Ok(Outcome::Ok(()))
}
}
pub fn check_program_counted(program: Program) -> Result<Checked> {
let runs = program.runs;
let mut ctx = FrontendContext::new(program)?;
if let Some((module, _tests, _fresh_def_id)) = ctx.check(false)? {
if runs {
if let Err(diags) = nova_mir::lower_module(&module) {
ctx.render(&diags);
}
}
}
Ok(Checked {
errors: ctx.errors,
warnings: ctx.warnings,
})
}
pub fn compile_file(path: &Path) -> Result<Outcome<CompiledProgram>> {
compile_program(Program::for_file(path))
}
pub fn compile_program(program: Program) -> Result<Outcome<CompiledProgram>> {
let mir = match lower_to_mir(program)? {
Outcome::Ok(mir) => mir,
Outcome::Failed { errors } => return Ok(Outcome::Failed { errors }),
};
match nova_codegen_cranelift::compile_jit(&mir) {
Ok(program) => Ok(Outcome::Ok(program)),
Err(e) => {
if let Some(sym) = e.downcast_ref::<nova_codegen_cranelift::UnresolvedExternSymbol>() {
let diag = Diagnostic::error(
"E0902",
format!(
"{sym} at run time; an `extern` function must be provided by the \
C runtime or an already-loaded library (a `nova build` executable \
resolves it at link time instead)"
),
);
render::emit_all(&FileDb::new(), &[diag]);
return Ok(Outcome::Failed { errors: 1 });
}
Err(e).context("internal codegen error (this is a compiler bug)")
}
}
}
pub fn build_file(path: &Path, output: &Path) -> Result<Outcome<PathBuf>> {
build_program(Program::for_file(path), output)
}
pub fn build_program(program: Program, output: &Path) -> Result<Outcome<PathBuf>> {
let mir = match lower_to_mir(program)? {
Outcome::Ok(mir) => mir,
Outcome::Failed { errors } => return Ok(Outcome::Failed { errors }),
};
let bytes = nova_codegen_cranelift::compile_object(&mir)
.context("internal codegen error (this is a compiler bug)")?;
let obj_ext = if cfg!(windows) { "obj" } else { "o" };
let obj_path = intermediate(output, obj_ext);
std::fs::write(&obj_path, bytes)
.with_context(|| format!("failed to write {}", obj_path.display()))?;
let linked = link::link_executable(&obj_path, output);
let _ = std::fs::remove_file(&obj_path);
linked?;
Ok(Outcome::Ok(output.to_path_buf()))
}
fn intermediate(output: &Path, ext: &str) -> PathBuf {
let mut name = output
.file_name()
.map(|n| n.to_os_string())
.unwrap_or_else(|| std::ffi::OsString::from("out"));
name.push(".nova.");
name.push(ext);
output.with_file_name(name)
}
pub fn build_file_release(path: &Path, output: &Path) -> Result<Outcome<PathBuf>> {
build_program_release(Program::for_file(path), output)
}
pub fn build_program_release(program: Program, output: &Path) -> Result<Outcome<PathBuf>> {
let mir = match lower_to_mir(program)? {
Outcome::Ok(mir) => mir,
Outcome::Failed { errors } => return Ok(Outcome::Failed { errors }),
};
let ir = nova_codegen_llvm::compile_ir(&mir)
.context("internal LLVM IR generation error (this is a compiler bug)")?;
let ll_path = intermediate(output, "ll");
std::fs::write(&ll_path, ir.as_bytes())
.with_context(|| format!("failed to write {}", ll_path.display()))?;
let obj_ext = if cfg!(windows) { "obj" } else { "o" };
let obj_path = intermediate(output, obj_ext);
let result = link::compile_ir_to_object(&ll_path, &obj_path)
.and_then(|()| link::link_executable(&obj_path, output));
let _ = std::fs::remove_file(&obj_path);
match result {
Ok(()) => {
let _ = std::fs::remove_file(&ll_path);
Ok(Outcome::Ok(output.to_path_buf()))
}
Err(e) => Err(e.context(format!(
"release build failed; generated LLVM IR left at {}",
ll_path.display()
))),
}
}
const SHADOWED_USER_MAIN_NAME: &str = "main.shadowed_by_nova_test";
const NOT_THE_ENTRY_MAIN: &str = "main.not_the_entry";
pub const DOC_TEST_FUNCTION: &str = "nova_doctest";
fn keep_the_entry_main(module: &mut hir::Module, definitions: &Definitions) {
for function in &mut module.functions {
if function.name != "main" {
continue;
}
let def = definitions.defs().get(function.def_id.0 as usize);
if let Some(Def {
kind: DefKind::Fn { item_index },
..
}) = def
{
if definitions.module_of(*item_index) != ModuleId(0) {
function.name = NOT_THE_ENTRY_MAIN.to_string();
}
}
}
}
fn name_tests(tests: &mut [TestFn], definitions: &Definitions, modules: &[Loaded]) {
for test in tests {
let def = definitions.defs().get(test.def_id.0 as usize);
let Some(Def {
kind: DefKind::Fn { item_index },
..
}) = def
else {
continue;
};
let module = definitions.module_of(*item_index).0 as usize;
if let Some(module) = modules.get(module).filter(|m| m.test_file) {
test.name = format!(
"{}::{}",
FrontendContext::module_name(&module.path),
test.name
);
}
}
}
pub fn build_test_binary(path: &Path) -> Result<(PathBuf, Vec<TestFn>)> {
build_test_program(Program::for_file(path))
}
pub type MapDiagnostic<'a> = dyn Fn(&mut FileDb, &mut Diagnostic) -> bool + 'a;
pub fn std_source(name: &str) -> Option<&'static str> {
let short = name.strip_prefix("<std/")?.strip_suffix('>')?;
nova_resolver::STD_MODULES
.iter()
.chain(std::iter::once(&nova_resolver::STD_TEST_MODULE))
.find(|(module, _)| module.strip_prefix("$std.") == Some(short))
.map(|&(_, text)| text)
}
pub fn build_test_program(program: Program) -> Result<(PathBuf, Vec<TestFn>)> {
build_test_program_with(program, &DiskSources, None)
}
pub fn build_test_program_with(
program: Program,
sources: &dyn Sources,
map: Option<&MapDiagnostic>,
) -> Result<(PathBuf, Vec<TestFn>)> {
let entry = program
.roots
.first()
.map(|root| root.path.clone())
.unwrap_or_default();
let mut ctx = FrontendContext::with(program, sources, map)?;
let Some((mut module, tests, fresh_def_id)) = ctx.check(true)? else {
anyhow::bail!(
"could not compile due to {} previous error{}",
ctx.errors,
if ctx.errors == 1 { "" } else { "s" }
);
};
for f in &mut module.functions {
if f.name == "main" {
f.name = SHADOWED_USER_MAIN_NAME.to_string();
}
}
module
.functions
.push(synthesize_test_main(fresh_def_id, &tests));
let mir = match nova_mir::lower_module(&module) {
Ok(mir) => mir,
Err(diags) => {
ctx.render(&diags);
anyhow::bail!(
"could not compile due to {} previous error{}",
ctx.errors,
if ctx.errors == 1 { "" } else { "s" }
);
}
};
let bytes = nova_codegen_cranelift::compile_object(&mir)
.context("internal codegen error (this is a compiler bug)")?;
let dir = std::env::temp_dir()
.join("nova-test-bin")
.join(path_fingerprint(&entry));
std::fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
let output = dir.join(format!(
"{}{}",
FrontendContext::module_name(&entry),
std::env::consts::EXE_SUFFIX
));
let obj_ext = if cfg!(windows) { "obj" } else { "o" };
let obj_path = intermediate(&output, obj_ext);
std::fs::write(&obj_path, bytes)
.with_context(|| format!("failed to write {}", obj_path.display()))?;
let linked = link::link_executable(&obj_path, &output);
let _ = std::fs::remove_file(&obj_path);
linked?;
Ok((output, tests))
}
fn path_fingerprint(path: &Path) -> String {
let canon = path.canonicalize().unwrap_or_else(|_| {
std::env::current_dir()
.map(|dir| dir.join(path))
.unwrap_or_else(|_| path.to_path_buf())
});
let mut hasher = std::collections::hash_map::DefaultHasher::new();
canon.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
fn dummy_span() -> Span {
Span::point(0, FileId::DUMMY)
}
fn expr(kind: hir::ExprKind, ty: Ty) -> hir::Expr {
hir::Expr {
kind,
ty,
span: dummy_span(),
}
}
fn synthesize_test_main(main_id: DefId, tests: &[TestFn]) -> hir::Function {
let sel = hir::LocalId(0);
let mut stmts = vec![expr(
hir::ExprKind::Let {
local: sel,
init: Box::new(expr(
hir::ExprKind::Call {
func: hir::Callee::Builtin(Builtin::TestSelector),
type_args: Vec::new(),
args: Vec::new(),
},
Ty::Int,
)),
},
Ty::Unit,
)];
for (i, t) in tests.iter().enumerate() {
let cond = expr(
hir::ExprKind::Binary {
op: hir::BinOp::Eq,
lhs: Box::new(expr(hir::ExprKind::Local(sel), Ty::Int)),
rhs: Box::new(expr(hir::ExprKind::IntLit(i as i64), Ty::Int)),
},
Ty::Bool,
);
let call = expr(
hir::ExprKind::Call {
func: hir::Callee::Def(t.def_id),
type_args: Vec::new(),
args: Vec::new(),
},
Ty::Unit,
);
stmts.push(expr(
hir::ExprKind::If {
cond: Box::new(cond),
then: Box::new(call),
else_: None,
},
Ty::Unit,
));
}
let println = |s: String| {
expr(
hir::ExprKind::Call {
func: hir::Callee::Builtin(Builtin::Println),
type_args: Vec::new(),
args: vec![expr(hir::ExprKind::StrLit(s), Ty::String)],
},
Ty::Unit,
)
};
let mut inventory = vec![println(tests.len().to_string())];
inventory.extend(tests.iter().map(|t| println(t.name.clone())));
let sel_is_negative = expr(
hir::ExprKind::Binary {
op: hir::BinOp::Lt,
lhs: Box::new(expr(hir::ExprKind::Local(sel), Ty::Int)),
rhs: Box::new(expr(hir::ExprKind::IntLit(0), Ty::Int)),
},
Ty::Bool,
);
stmts.push(expr(
hir::ExprKind::If {
cond: Box::new(sel_is_negative),
then: Box::new(expr(
hir::ExprKind::Block {
stmts: inventory,
trailing: None,
},
Ty::Unit,
)),
else_: None,
},
Ty::Unit,
));
hir::Function {
def_id: main_id,
name: "main".to_string(),
generics: 0,
bounds: Vec::new(),
takes_env: false,
capture_count: 0,
params: 0,
locals: vec![hir::Local {
name: "sel".to_string(),
ty: Ty::Int,
is_mut: false,
span: dummy_span(),
}],
ret_ty: Ty::Unit,
is_async: false,
body: expr(
hir::ExprKind::Block {
stmts,
trailing: None,
},
Ty::Unit,
),
span: dummy_span(),
}
}
fn lower_to_mir(program: Program) -> Result<Outcome<nova_mir::Module>> {
let mut ctx = FrontendContext::new(program)?;
let Some((module, _tests, _fresh_def_id)) = ctx.check(false)? else {
return Ok(Outcome::Failed { errors: ctx.errors });
};
match nova_mir::lower_module(&module) {
Ok(mir) => Ok(Outcome::Ok(mir)),
Err(diags) => {
ctx.render(&diags);
Ok(Outcome::Failed { errors: ctx.errors })
}
}
}
pub fn run_file(path: &Path, args: Vec<String>) -> Result<Outcome<()>> {
run_program(Program::for_file(path), args)
}
pub fn run_program(program: Program, args: Vec<String>) -> Result<Outcome<()>> {
match compile_program(program)? {
Outcome::Ok(program) => {
program.run_with_args(args);
Ok(Outcome::Ok(()))
}
Outcome::Failed { errors } => Ok(Outcome::Failed { errors }),
}
}
struct FrontendContext<'a> {
db: FileDb,
graph: Option<nova_pm::Graph>,
roots: Vec<Root>,
errors: usize,
warnings: usize,
registry_files: HashSet<FileId>,
sources: &'a dyn Sources,
map: Option<&'a MapDiagnostic<'a>>,
}
impl<'a> FrontendContext<'a> {
fn new(program: Program) -> Result<Self> {
FrontendContext::with(program, &DiskSources, None)
}
fn with(
program: Program,
sources: &'a dyn Sources,
map: Option<&'a MapDiagnostic<'a>>,
) -> Result<Self> {
let Program {
db,
graph,
roots,
diagnostics,
..
} = program;
let mut ctx = FrontendContext {
db,
graph,
roots,
errors: 0,
warnings: 0,
registry_files: HashSet::new(),
sources,
map,
};
ctx.render(&diagnostics);
if ctx.errors == 0 {
let entry = ctx
.entry()
.context("nothing to compile: the package has no source file for this")?;
ctx.sources
.read(entry)
.with_context(|| format!("failed to open {}", entry.display()))?;
}
Ok(ctx)
}
fn entry(&self) -> Option<&Path> {
self.roots.first().map(|root| root.path.as_path())
}
fn render(&mut self, diagnostics: &[Diagnostic]) {
let mut shown: Vec<Diagnostic> = diagnostics
.iter()
.filter(|d| !program::a_dependencys_warning(d, &self.registry_files))
.cloned()
.collect();
if let Some(map) = self.map {
let db = &mut self.db;
shown.retain_mut(|d| map(db, d) || d.severity == Severity::Error);
}
self.errors += shown
.iter()
.filter(|d| d.severity == Severity::Error)
.count();
self.warnings += shown
.iter()
.filter(|d| d.severity == Severity::Warning)
.count();
if !shown.is_empty() {
render::emit_all(&self.db, &shown);
}
}
fn module_name(path: &Path) -> String {
path.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "main".to_string())
}
fn load_modules(&mut self) -> Result<Vec<Loaded>> {
let entry = self.entry().map(Path::to_path_buf).unwrap_or_default();
let load =
program::load_program(self.graph.as_ref(), &self.roots, self.sources, &mut self.db)
.with_context(|| format!("failed to read {}", entry.display()))?;
self.registry_files = program::registry_files(self.graph.as_ref(), &load.modules);
self.render(&load.diagnostics);
Ok(load.modules)
}
fn check(
&mut self,
with_test_module: bool,
) -> Result<Option<(nova_hir::Module, Vec<TestFn>, DefId)>> {
if self.errors > 0 {
return Ok(None);
}
let mut modules = self.load_modules()?;
if self.errors > 0 {
return Ok(None);
}
strip_test_functions(&mut modules, with_test_module);
let std_files: Vec<FileId> = nova_resolver::STD_MODULES
.iter()
.map(|&(name, src)| {
let short = name.strip_prefix("$std.").unwrap_or(name);
self.db.add(format!("<std/{short}>"), src)
})
.collect();
let extra_std = with_test_module.then(|| {
let (name, src) = nova_resolver::STD_TEST_MODULE;
let short = name.strip_prefix("$std.").unwrap_or(name);
let file_id = self.db.add(format!("<std/{short}>"), src);
(nova_resolver::STD_TEST_MODULE, file_id)
});
let sources: Vec<ModuleSource> = modules
.iter()
.map(|m| ModuleSource {
name: m.name.clone(),
file: &m.ast,
imports: m.imports.clone(),
text: self.db.get_source(m.file),
package: m.package.map(|p| p.0),
importable: m.importable.clone(),
})
.collect();
let resolved = nova_resolver::resolve_program(&sources, &std_files, extra_std);
self.render(&resolved.diagnostics);
if self.errors > 0 {
return Ok(None);
}
let mut tests = resolved.tests;
name_tests(&mut tests, &resolved.definitions, &modules);
let checked = nova_typeck::check_with(
&resolved.file,
&resolved.definitions,
&nova_typeck::CheckOptions {
sources: Some(&self.db),
..Default::default()
},
);
self.render(&checked.diagnostics);
if self.errors > 0 {
return Ok(None);
}
let mut module = checked.module;
keep_the_entry_main(&mut module, &resolved.definitions);
let fresh_def_id = DefId(
module
.functions
.iter()
.map(|f| f.def_id.0 + 1)
.fold(resolved.definitions.defs().len() as u32, u32::max),
);
Ok(Some((module, tests, fresh_def_id)))
}
}
fn strip_test_functions(modules: &mut [Loaded], keep_root_tests: bool) {
let doc_tests = modules.iter().any(|m| m.doc);
for module in modules.iter_mut() {
let root = matches!(module.package, None | Some(PackageId(0)));
let doc = module.doc;
module.ast.items.retain(|item| match &item.value {
nova_ast::Item::Function(f) if f.attrs.iter().any(|a| a.name.value == "test") => {
keep_root_tests
&& if doc_tests {
doc && f.name.value == DOC_TEST_FUNCTION
} else {
root
}
}
_ => true,
});
}
}
#[cfg(test)]
mod doc_test_builds {
use super::*;
struct Memory;
impl Sources for Memory {
fn read(&self, _: &Path) -> std::io::Result<String> {
Ok("fn main() {}\n".to_string())
}
}
#[test]
fn a_map_hides_the_warnings_it_rejects_and_never_an_error() {
let map = |_: &mut FileDb, _: &mut Diagnostic| false;
let program = Program::loose(Path::new("<nova driver: memory>/main.nova"));
let mut ctx = FrontendContext::with(program, &Memory, Some(&map)).expect("the entry reads");
ctx.render(&[
Diagnostic::warning("W0000", "a warning"),
Diagnostic::error("E0000", "an error"),
]);
assert_eq!((ctx.errors, ctx.warnings), (1, 0));
}
#[test]
fn a_path_no_file_holds_is_fingerprinted_from_the_current_directory() {
let relative = Path::new("<nova driver: no such dir>").join("doc0.nova");
let absolute = std::env::current_dir()
.expect("a current directory")
.join(&relative);
assert_eq!(path_fingerprint(&relative), path_fingerprint(&absolute));
}
#[test]
fn std_source_gives_a_std_modules_text_by_its_name_in_the_database() {
assert_eq!(
std_source("<std/core>"),
Some(nova_resolver::STD_MODULES[0].1)
);
assert_eq!(
std_source("<std/time>"),
Some(nova_resolver::STD_MODULES[10].1)
);
assert_eq!(
std_source("<std/test>"),
Some(nova_resolver::STD_TEST_MODULE.1)
);
assert_eq!(std_source("<std/nope>"), None);
assert_eq!(std_source("std/time/lib.nova"), None);
assert_eq!(std_source("src/lib.nova"), None);
}
}
#[cfg(test)]
mod async_end_to_end {
use nova_diagnostics::FileId;
use nova_mir::{Function, MirTy, Module, RtFunc, Stmt, Temp, Terminator};
const PROBE_TASK_ID: i64 = 0;
thread_local! {
static PROBED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
enum Arg {
Int(i64),
Float(f64),
}
fn run_async_fn(src: &str, name: &str, args: &[Arg]) -> i64 {
run_async_fn_with(src, name, args, &|_| {})
}
fn run_async_fn_with(src: &str, name: &str, args: &[Arg], fixup: &dyn Fn(&mut Module)) -> i64 {
assert!(
!PROBED.with(|p| p.replace(true)),
"one probe per test: see PROBE_TASK_ID"
);
let (tokens, lex_errors) = nova_lexer::lex(src, FileId::DUMMY);
assert!(lex_errors.is_empty(), "lex: {lex_errors:?}");
let (ast, parse_errors) = nova_parser::parse(&tokens, FileId::DUMMY);
assert!(parse_errors.is_empty(), "parse: {parse_errors:?}");
let resolved = nova_resolver::resolve(&ast.expect("no 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 mut mir = nova_mir::lower_module(&checked.module).expect("MIR lowering");
let prefix = format!("{name}.");
let wrapper = mir
.functions
.iter()
.map(|f| f.name.clone())
.find(|n| n.starts_with(&prefix) && !n.ends_with("$poll"))
.unwrap_or_else(|| {
panic!(
"no wrapper for `{name}`; have {:?}",
mir.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
)
});
assert!(
mir.functions
.iter()
.any(|f| f.name == format!("{wrapper}$poll")),
"the transform must also have emitted `{wrapper}$poll`"
);
fixup(&mut mir);
replace_main(&mut mir, &wrapper, args);
let program = nova_codegen_cranelift::compile_jit(&mir).expect("JIT compile");
program.run();
nova_runtime::task::output_bits(PROBE_TASK_ID)
}
fn replace_main(mir: &mut Module, wrapper: &str, args: &[Arg]) {
let mut temps: Vec<MirTy> = Vec::new();
let mut stmts: Vec<Stmt> = Vec::new();
fn push(temps: &mut Vec<MirTy>, ty: MirTy) -> Temp {
let t = Temp(temps.len() as u32);
temps.push(ty);
t
}
let mut arg_temps = Vec::new();
for a in args {
match a {
Arg::Int(v) => {
let t = push(&mut temps, MirTy::I64);
stmts.push(Stmt::ConstInt(t, *v));
arg_temps.push(t);
}
Arg::Float(v) => {
let t = push(&mut temps, MirTy::F64);
stmts.push(Stmt::ConstFloat(t, *v));
arg_temps.push(t);
}
}
}
for spawn in [true, false] {
let fut = push(&mut temps, MirTy::Ptr);
stmts.push(Stmt::Call {
dst: Some(fut),
callee: wrapper.to_string(),
args: arg_temps.clone(),
});
let status = push(&mut temps, MirTy::I64);
stmts.push(Stmt::CallRuntime {
dst: Some(status),
func: if spawn {
RtFunc::TaskSpawn
} else {
RtFunc::TaskBlockOn
},
args: vec![fut],
});
}
let main = mir
.functions
.iter_mut()
.find(|f| f.name == "main")
.expect("`main` was lowered");
*main = Function {
name: "main".to_string(),
params: 0,
takes_env: false,
capture_count: 0,
temps,
ret: MirTy::Unit,
is_async: false,
blocks: vec![nova_mir::Block {
stmts,
term: Terminator::Return(None),
}],
};
}
#[test]
fn an_await_free_async_fn_returning_int_runs_and_produces_its_value() {
let out = run_async_fn(
"async fn f() -> Int { 40 + 2 }\nfn main() { let x = f() }",
"f",
&[],
);
assert_eq!(out, 42);
}
#[test]
fn an_await_free_async_fn_returning_float_runs_and_produces_its_value() {
let out = run_async_fn(
"async fn f() -> Float { 1.5 }\nfn main() { let x = f() }",
"f",
&[],
);
assert_eq!(
out as u64,
1.5f64.to_bits(),
"the output slot must hold 1.5 as an f64 bit pattern, not an \
integer 1 or a truncation: got {out:#x}, want {:#x}",
1.5f64.to_bits()
);
}
#[test]
fn mix64_agrees_with_the_runtime_finalizer_over_keys_of_both_signs() {
let out = run_async_fn(
"async fn fold() -> Int {\n\
\x20 let mut acc = 0\n\
\x20 let mut k = -64\n\
\x20 while k < 64 {\n\
\x20 acc = acc ^ (k).hash()\n\
\x20 k = k + 1\n\
\x20 }\n\
\x20 acc\n\
}\n\
fn main() { let x = fold() }",
"fold",
&[],
);
let seed = nova_runtime::nova_rt_int_hash_seed() as u64;
let want = (-64i64..64).fold(0i64, |acc, k| {
acc ^ nova_runtime::splitmix64_finalize((k as u64) ^ seed) as i64
});
assert_eq!(
out, want,
"Nova's `mix64` and the runtime's `splitmix64_finalize` disagree \
somewhere in the keys -64..63. Both are the splitmix64 finalizer \
and the records say so; one of them has drifted. The single-key \
probe names a specific input."
);
}
#[test]
fn mix64_agrees_with_the_runtime_finalizer_at_a_single_key() {
let out = run_async_fn(
"async fn one() -> Int { (-1).hash() }\n\
fn main() { let x = one() }",
"one",
&[],
);
let seed = nova_runtime::nova_rt_int_hash_seed() as u64;
let want = nova_runtime::splitmix64_finalize((-1i64 as u64) ^ seed) as i64;
assert_eq!(
out, want,
"`(-1).hash()` in Nova and `splitmix64_finalize(-1 ^ seed)` in the \
runtime must agree: got {out:#x}, want {want:#x}"
);
}
#[test]
fn an_async_fns_parameters_reach_its_body_through_the_state_object() {
let out = run_async_fn(
"async fn add(a: Float, b: Float) -> Float { a + b }\n\
fn main() { let x = add(1.0, 2.0) }",
"add",
&[Arg::Float(1.5), Arg::Float(2.25)],
);
assert_eq!(
out as u64,
3.75f64.to_bits(),
"1.5 + 2.25 must reach the body; a zeroed parameter slot yields \
0.0. Commutative, so `parameter_slots_are_seeded_in_order` \
covers ordering: got {out:#x}"
);
}
#[test]
fn parameter_slots_are_seeded_in_order() {
let out = run_async_fn(
"async fn sub(a: Float, b: Float) -> Float { a - b }\n\
fn main() { let x = sub(1.0, 2.0) }",
"sub",
&[Arg::Float(10.0), Arg::Float(2.5)],
);
assert_eq!(
out as u64,
7.5f64.to_bits(),
"10.0 - 2.5 = 7.5; reversed slots would give -7.5: got {out:#x}"
);
}
#[test]
fn a_loop_accumulator_survives_the_spill_across_blocks() {
let out = run_async_fn(
"async fn total(n: Int) -> Int {\n \
let mut i = 0\n \
let mut t = 0\n \
while i < n { t = t + i\n i = i + 1 }\n \
t\n\
}\n\
fn main() { let x = total(1) }",
"total",
&[Arg::Int(10)],
);
assert_eq!(out, 45, "0+1+..+9 = 45");
}
#[test]
fn a_heap_value_allocated_inside_the_poll_fn_round_trips_through_a_state_slot() {
let out = run_async_fn(
"async fn f(n: Int) -> Bool { let s = \"ab${n}cd\"
s == \"ab7cd\" }
fn main() { let x = f(1) }",
"f",
&[Arg::Int(7)],
);
assert_eq!(
out & 0xff, 1,
"the interpolated string must equal \"ab7cd\"; a parameter that did not reach the body would build \"ab0cd\" and give 0: got {out:#x}"
);
}
fn replace_poll_with_a_pending_stub(mir: &mut Module, name: &str, pends: i64, value: f64) {
let prefix = format!("{name}.");
let poll = mir
.functions
.iter_mut()
.find(|f| f.name.starts_with(&prefix) && f.name.ends_with("$poll"))
.unwrap_or_else(|| panic!("no `$poll` for `{name}`"));
let mut temps = vec![MirTy::Ptr, MirTy::Ptr];
fn push(temps: &mut Vec<MirTy>, ty: MirTy) -> Temp {
let t = Temp(temps.len() as u32);
temps.push(ty);
t
}
let state = Temp(0);
let tag = push(&mut temps, MirTy::I64);
let dispatch = nova_mir::Block {
stmts: vec![Stmt::RecordField {
dst: tag,
record: state,
index: nova_mir::STATE_SLOT_TAG,
ty: MirTy::I64,
}],
term: Terminator::Switch {
disc: tag,
arms: (0..pends)
.map(|k| (k, nova_mir::BlockId(k as u32 + 1)))
.collect(),
default: nova_mir::BlockId(pends as u32 + 1),
},
};
let mut blocks = vec![dispatch];
for k in 0..pends {
let next = push(&mut temps, MirTy::I64);
let pending = push(&mut temps, MirTy::I64);
blocks.push(nova_mir::Block {
stmts: vec![
Stmt::ConstInt(next, k + 1),
Stmt::SetField {
record: state,
index: nova_mir::STATE_SLOT_TAG,
value: next,
ty: MirTy::I64,
},
Stmt::ConstInt(pending, nova_mir::POLL_PENDING),
],
term: Terminator::Return(Some(pending)),
});
}
let out = push(&mut temps, MirTy::F64);
let ready = push(&mut temps, MirTy::I64);
blocks.push(nova_mir::Block {
stmts: vec![
Stmt::ConstFloat(out, value),
Stmt::SetField {
record: state,
index: nova_mir::STATE_SLOT_OUTPUT,
value: out,
ty: MirTy::F64,
},
Stmt::ConstInt(ready, nova_mir::POLL_READY),
],
term: Terminator::Return(Some(ready)),
});
*poll = Function {
name: poll.name.clone(),
params: 1,
takes_env: true,
capture_count: 0,
temps,
ret: MirTy::I64,
is_async: false,
blocks,
};
}
#[test]
fn an_await_on_a_twice_pending_future_resumes_at_the_same_await() {
let out = run_async_fn_with(
"async fn g() -> Float { 0.0 }\n\
async fn f() -> Float { g().await + 1.0 }\n\
fn main() { let x = f() }",
"f",
&[],
&|mir| replace_poll_with_a_pending_stub(mir, "g", 2, 2.5),
);
assert_eq!(
out as u64,
3.5f64.to_bits(),
"2.5 + 1.0 must come back from the resumed await; an output slot read \
before the inner future wrote it gives 1.0: got {out:#x}, want {:#x}",
3.5f64.to_bits()
);
}
#[test]
fn an_await_inside_a_loop_suspends_and_resumes_on_every_iteration() {
let out = run_async_fn_with(
"async fn half() -> Float { 0.0 }\n\
async fn total(n: Int) -> Float {\n \
let mut i = 0\n \
let mut t = 0.0\n \
while i < n { t = t + half().await\n i = i + 1 }\n \
t\n\
}\n\
fn main() { let x = total(1) }",
"total",
&[Arg::Int(4)],
&|mir| replace_poll_with_a_pending_stub(mir, "half", 1, 0.5),
);
assert_eq!(
out as u64,
2.0f64.to_bits(),
"four iterations of 0.5 must accumulate to 2.0 across four \
suspensions: got {out:#x}, want {:#x}",
2.0f64.to_bits()
);
}
#[test]
fn an_await_on_an_already_ready_future_falls_through_to_its_continuation() {
let out = run_async_fn(
"async fn g(x: Float) -> Float { x * 2.0 }\n\
async fn f(a: Float) -> Float { g(a).await + 0.25 }\n\
fn main() { let x = f(1.0) }",
"f",
&[Arg::Float(1.5)],
);
assert_eq!(
out as u64,
3.25f64.to_bits(),
"1.5 * 2.0 + 0.25 = 3.25; the awaited value must be the inner \
future's own output, not its argument or zero: got {out:#x}, want {:#x}",
3.25f64.to_bits()
);
}
#[test]
fn two_awaits_in_one_body_each_get_their_own_resume_state() {
let out = run_async_fn(
"async fn g(x: Float) -> Float { x }\n\
async fn f() -> Float { g(4.0).await - g(2.5).await }\n\
fn main() { let x = f() }",
"f",
&[],
);
assert_eq!(
out as u64,
1.5f64.to_bits(),
"4.0 - 2.5 = 1.5: got {out:#x}, want {:#x}",
1.5f64.to_bits()
);
}
#[test]
fn an_await_inside_a_match_arm_runs_the_arm_it_selected() {
let out = run_async_fn(
"type Choice = | Left | Right\n\
async fn g(x: Float) -> Float { x }\n\
async fn f(a: Float) -> Float {\n \
let c: Choice = Left\n \
match c { Left => g(a).await + 1.0, Right => 0.0, }\n\
}\n\
fn main() { let x = f(1.0) }",
"f",
&[Arg::Float(2.5)],
);
assert_eq!(
out as u64,
3.5f64.to_bits(),
"the `Left` arm awaits and adds 1.0, so 2.5 gives 3.5; the `Right` \
arm would give 0.0: got {out:#x}, want {:#x}",
3.5f64.to_bits()
);
}
#[test]
fn a_unit_output_await_still_polls_its_future_and_continues() {
let out = run_async_fn(
"async fn g() { }\n\
async fn f(a: Float) -> Float { g().await\n a + 1.0 }\n\
fn main() { let x = f(1.0) }",
"f",
&[Arg::Float(0.5)],
);
assert_eq!(
out as u64,
1.5f64.to_bits(),
"0.5 + 1.0 = 1.5 must run after the unit await: got {out:#x}, want {:#x}",
1.5f64.to_bits()
);
}
#[test]
fn a_unit_returning_async_fn_completes_and_reads_a_valid_output_slot() {
let out = run_async_fn("async fn f() { }\nfn main() { let x = f() }", "f", &[]);
assert_eq!(out, 0, "the explicit zero the transform stores for unit");
}
}