use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use super::Report;
pub const COMPARATOR: &str = "scripts/Comparator.lean";
pub const CHALLENGE_DIR: &str = "Challenge";
pub const CHALLENGE_NS: &str = "PvlChallenge";
pub const SORRY_AXIOM: &str = "sorryAx";
#[must_use]
pub fn challenge_decl(solution: &str) -> String {
format!("{CHALLENGE_NS}.{solution}")
}
#[must_use]
pub fn solution_of(challenge: &str) -> Option<&str> {
challenge
.strip_prefix(CHALLENGE_NS)
.and_then(|s| s.strip_prefix('.'))
.filter(|s| !s.is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct Row {
pub name: String,
pub challenge_type_hash: Option<String>,
pub solution_type_hash: Option<String>,
#[serde(default)]
pub defeq_instances: Option<bool>,
pub axioms: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Closure {
pub closed: usize,
pub total: usize,
}
pub fn parse_rows(ndjson: &str) -> Result<Vec<Row>, String> {
ndjson
.lines()
.enumerate()
.filter(|(_, l)| !l.trim().is_empty())
.map(|(i, l)| {
serde_json::from_str::<Row>(l)
.map_err(|e| format!("comparator output line {}: {e}: {l}", i + 1))
})
.collect()
}
#[must_use]
pub fn challenge_files(lean_dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(lean_dir.join(CHALLENGE_DIR)) else {
return Vec::new();
};
let mut out: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().and_then(|e| e.to_str()) == Some("lean"))
.filter_map(|p| p.file_name().map(|n| Path::new(CHALLENGE_DIR).join(n)))
.collect();
out.sort();
out
}
const ROOT_DECL: &str = "theorem _root_.";
#[must_use]
pub fn roots_in(text: &str) -> BTreeSet<String> {
text.lines()
.filter_map(|l| l.trim_start().strip_prefix(ROOT_DECL))
.filter_map(|rest| {
let end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
let decl = &rest[..end];
let decl = decl.find(".{").map_or(decl, |i| &decl[..i]);
solution_of(decl).map(str::to_string)
})
.collect()
}
pub fn expected_roots(lean_dir: &Path, files: &[PathBuf]) -> Result<BTreeSet<String>, String> {
let mut out = BTreeSet::new();
for f in files {
let p = lean_dir.join(f);
let text = std::fs::read_to_string(&p).map_err(|e| format!("{}: {e}", p.display()))?;
out.extend(roots_in(&text));
}
Ok(out)
}
pub fn cross_check(rows: &[Row], roots: &BTreeSet<String>, c: &mut Closure, r: &mut Report) {
let names: BTreeSet<&str> = rows.iter().map(|row| row.name.as_str()).collect();
let mut missing = 0;
for root in roots.iter().filter(|n| !names.contains(n.as_str())) {
missing += 1;
r.fail(format!(
"MISSING-ROW {root} -- {} is declared in a Challenge file but the comparator emitted no row for it",
challenge_decl(root)
));
}
for n in names.iter().filter(|n| !roots.contains(**n)) {
r.fail(format!(
"UNEXPECTED-ROW {n} -- the comparator reported {} but no Challenge file declares it",
challenge_decl(n)
));
}
c.total = names.len() + missing;
r.lines.push(format!(
"COMPARATOR rows cross-checked: {} row(s), {} declared root(s)",
names.len(),
roots.len()
));
}
pub fn judge_rows(rows: &[Row], r: &mut Report) -> Closure {
let mut seen = BTreeSet::new();
let mut closed = 0;
for row in rows {
let n = &row.name;
let ch = challenge_decl(n);
if !seen.insert(n.as_str()) {
r.fail(format!(
"DUPLICATE {ch} -- the comparator reported it twice"
));
continue;
}
match judge_row(row, &ch) {
Err(fail) => {
r.fail(fail);
continue;
}
Ok(Some(line)) => r.lines.push(line),
Ok(None) => {}
}
closed += 1;
}
let c = Closure {
closed,
total: rows.len(),
};
r.lines.push(format!(
"COMPARATOR {}/{} challenge(s) closed",
c.closed, c.total
));
if rows.is_empty() && !r.reject {
r.decline = Some("comparator: 0 challenge rows -- nothing was compared".to_string());
}
c
}
fn judge_row(row: &Row, ch: &str) -> Result<Option<String>, String> {
let n = &row.name;
let (Some(c), sol) = (&row.challenge_type_hash, &row.solution_type_hash) else {
return Err(format!("MALFORMED {ch} -- no challenge_type_hash"));
};
let Some(s) = sol else {
return Err(format!(
"MISSING-ROOT {n} -- {ch} pins a theorem that does not exist"
));
};
let Some(axioms) = &row.axioms else {
return Err(format!(
"MALFORMED {ch} -- a solution with no axioms list: its sorry-freedom was never measured"
));
};
if c != s && row.defeq_instances != Some(true) {
return Err(format!(
"MISMATCH {n} -- it proves a different statement than {ch} pins (challenge {c}, solution {s})"
));
}
if axioms.iter().any(|a| a == SORRY_AXIOM) {
return Err(format!(
"SORRY {n} -- the solution rests on {SORRY_AXIOM}: it closes nothing"
));
}
Ok((c != s).then(|| {
format!(
"MATCH(instances) {n} -- defeq to {ch} at .instances transparency (challenge {c}, solution {s})"
)
}))
}
#[cfg(test)]
#[path = "comparator_tests.rs"]
mod tests;