use nibli_engine::NibliEngine;
use std::process::ExitCode;
use std::time::{Duration, Instant};
const CORPUS: &str = include_str!("../../../utopia.nibli");
const NAF_TRUE_QUERY: &str = "reward(Esa).";
const NAF_FALSE_QUERY: &str = "reward(Bela).";
const LOOKUP_QUERY: &str = "teaches(Esa, Fin).";
fn run_once(materialization: bool) -> Result<(Duration, Duration, Duration, Duration), String> {
let t_start = Instant::now();
let engine = NibliEngine::new();
engine.set_materialization(materialization);
let mut asserted = 0u32;
for (line_num, line) in CORPUS.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
engine
.assert_text(trimmed)
.map_err(|e| format!("utopia.nibli line {}: {e:?}", line_num + 1))?;
asserted += 1;
}
if asserted == 0 {
return Err("empty corpus".into());
}
let t_load = t_start.elapsed();
let t0 = Instant::now();
let r = engine
.query_holds(NAF_TRUE_QUERY)
.map_err(|e| format!("{e:?}"))?;
let t_naf_true = t0.elapsed();
if !r.is_true() {
return Err(format!("{NAF_TRUE_QUERY}: expected TRUE, got {r:?}"));
}
let t0 = Instant::now();
let r = engine
.query_holds(NAF_FALSE_QUERY)
.map_err(|e| format!("{e:?}"))?;
let t_naf_false = t0.elapsed();
if !r.is_false() {
return Err(format!("{NAF_FALSE_QUERY}: expected FALSE, got {r:?}"));
}
let t0 = Instant::now();
let r = engine
.query_holds(LOOKUP_QUERY)
.map_err(|e| format!("{e:?}"))?;
let t_lookup = t0.elapsed();
if !r.is_true() {
return Err(format!("{LOOKUP_QUERY}: expected TRUE, got {r:?}"));
}
Ok((t_load, t_naf_true, t_naf_false, t_lookup))
}
fn stats(mut xs: Vec<Duration>) -> (Duration, Duration, Duration) {
xs.sort();
let median = xs[xs.len() / 2];
(xs[0], median, *xs.last().unwrap())
}
fn fmt(d: Duration) -> String {
let ms = d.as_secs_f64() * 1000.0;
if ms < 10.0 {
format!("{ms:.1} ms")
} else {
format!("{ms:.0} ms")
}
}
fn main() -> ExitCode {
let profile = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
if cfg!(debug_assertions) {
eprintln!(
"WARNING: debug build — these figures are NOT quotable. \
Run `just bench-naf` (release profile)."
);
}
let runs: usize = std::env::var("NIBLI_BENCH_RUNS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let materialization = std::env::var("NIBLI_MATERIALIZE").ok().as_deref() != Some("0");
if let Err(e) = run_once(materialization) {
eprintln!("bench-naf: sequence failed: {e}");
return ExitCode::FAILURE;
}
let mut loads = Vec::with_capacity(runs);
let mut naf_trues = Vec::with_capacity(runs);
let mut naf_falses = Vec::with_capacity(runs);
let mut lookups = Vec::with_capacity(runs);
for _ in 0..runs {
match run_once(materialization) {
Ok((l, t, f, k)) => {
loads.push(l);
naf_trues.push(t);
naf_falses.push(f);
lookups.push(k);
}
Err(e) => {
eprintln!("bench-naf: sequence failed: {e}");
return ExitCode::FAILURE;
}
}
}
println!(
"nibli-bench-naf — native in-process engine (nibli-engine), {profile} profile, \
{runs} runs (fresh engine per run, 1 untimed warm-up)"
);
println!(
" materialisation: {}",
if materialization {
"ON (stratum-ordered; NAF answers by lookup)"
} else {
"OFF via NIBLI_MATERIALIZE=0 (every NAF re-proves its positive)"
}
);
println!(" corpus: utopia.nibli (all verdicts asserted every run)");
for (label, xs) in [
("utopia.nibli load", loads),
("naf-true reward(Esa)", naf_trues),
("naf-false reward(Bela)", naf_falses),
("lookup teaches(..)", lookups),
] {
let (min, med, max) = stats(xs);
println!(
" {label:<22} min {:>8} median {:>8} max {:>8}",
fmt(min),
fmt(med),
fmt(max)
);
}
println!(" (naf-true is the worst case: `~false(Esa)` has no witness, so the");
println!(" search for `false(Esa)` is exhaustive over the void rule's domain)");
ExitCode::SUCCESS
}