use super::*;
pub fn lean_lemma_theorem(c: &Conjecture, binders: &[Binder], name: &str) -> Option<String> {
let mut fvs = BTreeSet::new();
c.lhs.free_vars(&mut fvs);
c.rhs.free_vars(&mut fvs);
let induct = fvs
.iter()
.copied()
.find(|&i| matches!(binders.get(i).map(|b| &b.ty), Some(Type::List(_))))?;
let binder_decls: Vec<String> = fvs
.iter()
.filter_map(|&i| binders.get(i))
.map(|b| {
format!(
"({} : {})",
b.name,
crate::codegen::lean::type_to_lean(&b.ty)
)
})
.collect();
let mut fns = BTreeSet::new();
collect_user_fns(&c.lhs, &mut fns);
collect_user_fns(&c.rhs, &mut fns);
let unfolds: Vec<String> = fns
.iter()
.map(|f| crate::codegen::lean::aver_name_to_lean(f))
.collect();
let lhs = term_to_lean(&c.lhs, binders);
let rhs = term_to_lean(&c.rhs, binders);
let iv = &binders[induct].name;
let nil_defs = unfolds.join(", ");
let cons_defs = {
let mut v = unfolds.clone();
v.push("List.append_assoc".to_string());
v.join(", ")
};
let nil_split = if unfolds.is_empty() {
"List.cons_append".to_string()
} else {
format!("{nil_defs}, List.cons_append")
};
let cons_split = format!("{cons_defs}, List.cons_append");
Some(format!(
"theorem {name} {binders} : {lhs} = {rhs} := by\n \
induction {iv} with\n \
| nil => first | (simp [{nil_defs}]; done) | (simp [{nil_defs}]; omega) | (simp only [{nil_split}]; split <;> simp_all [{nil_defs}] <;> omega)\n \
| cons head tail ih => first | (simp_all [{cons_defs}]; done) | (simp_all [{cons_defs}]; omega) | (simp only [{cons_split}]; split <;> simp_all [{cons_defs}] <;> omega)\n",
binders = binder_decls.join(" "),
))
}
fn term_to_lean(node: &TermNode, binders: &[Binder]) -> String {
match node {
TermNode::Var(i) => binders
.get(*i)
.map(|b| b.name.clone())
.unwrap_or_else(|| format!("x{i}")),
TermNode::App { callee, args } => {
if callee == "List.concat" && args.len() == 2 {
return format!(
"({} ++ {})",
term_to_lean(&args[0], binders),
term_to_lean(&args[1], binders)
);
}
let lean_fn = crate::codegen::lean::aver_name_to_lean(callee);
let rendered: Vec<String> = args.iter().map(|a| term_to_lean(a, binders)).collect();
format!("({} {})", lean_fn, rendered.join(" "))
}
}
}
fn collect_user_fns(node: &TermNode, out: &mut BTreeSet<String>) {
if let TermNode::App { callee, args } = node {
if callee != "List.concat" {
out.insert(callee.clone());
}
for a in args {
collect_user_fns(a, out);
}
}
}
fn is_homomorphism(c: &Conjecture) -> bool {
fn var(t: &TermNode) -> Option<usize> {
if let TermNode::Var(i) = t {
Some(*i)
} else {
None
}
}
fn oriented(l: &TermNode, r: &TermNode) -> bool {
let TermNode::App {
callee: g,
args: largs,
} = l
else {
return false;
};
if g == "List.concat" || largs.is_empty() {
return false;
}
let mut pos: Option<usize> = None;
let mut ab: Option<(usize, usize)> = None;
for (i, arg) in largs.iter().enumerate() {
if let TermNode::App {
callee: cc,
args: ca,
} = arg
&& cc == "List.concat"
&& ca.len() == 2
&& let (Some(a), Some(b)) = (var(&ca[0]), var(&ca[1]))
&& a != b
{
if pos.is_some() {
return false; }
pos = Some(i);
ab = Some((a, b));
} else if var(arg).is_none() {
return false; }
}
let (Some(pos), Some((a, b))) = (pos, ab) else {
return false;
};
let TermNode::App {
callee: _op,
args: ra,
} = r
else {
return false;
};
if ra.len() != 2 {
return false;
}
let side_ok = |side: &TermNode, repl: usize| -> bool {
let TermNode::App {
callee: gg,
args: gargs,
} = side
else {
return false;
};
gg == g
&& gargs.len() == largs.len()
&& gargs.iter().enumerate().all(|(i, t)| {
if i == pos {
var(t) == Some(repl)
} else {
var(t).is_some() && var(t) == var(&largs[i])
}
})
};
side_ok(&ra[0], a) && side_ok(&ra[1], b)
}
oriented(&c.lhs, &c.rhs) || oriented(&c.rhs, &c.lhs)
}
pub fn discovery_surface_hash(inputs: &ProofLowerInputs) -> String {
let mut sigs: Vec<String> = inputs
.pure_fns()
.iter()
.map(|fd| {
let params: Vec<String> = fd.params.iter().map(|(n, t)| format!("{n}:{t}")).collect();
format!("{}({})->{}", fd.name, params.join(","), fd.return_type)
})
.collect();
sigs.sort();
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in sigs.join(";").bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
pub fn rank_candidate_indices(report: &LawDiscovery) -> Vec<usize> {
let mut idx: Vec<usize> = (0..report.conjectures.len()).collect();
idx.sort_by_key(|&i| {
let c = &report.conjectures[i];
let homo = if is_homomorphism(c) { 0 } else { 1 };
(homo, c.lhs.size() + c.rhs.size(), c.render(&report.binders))
});
idx
}
pub fn render_report(reports: &[LawDiscovery]) -> String {
const SAMPLE: usize = 12;
let mut out = String::new();
if reports.is_empty() {
out.push_str("lemma discovery: no `verify ... law` blocks found\n");
return out;
}
out.push_str(&format!(
"lemma discovery (skeleton): {} law(s)\n",
reports.len()
));
for r in reports {
out.push_str(&format!("\n• verify {} law {}\n", r.subject_fn, r.law_name));
out.push_str(&format!(" cone fns: [{}]\n", r.cone_fns.join(", ")));
out.push_str(&format!(" cone types: [{}]\n", r.cone_types.join(", ")));
if r.stats.skipped_large_cone {
out.push_str(&format!(
" cone too large ({} fns > {}) — enumeration skipped (size-{} discovery not meaningful at this scope)\n",
r.stats.cone_fn_count, MAX_CONE_FNS, r.stats.max_term_size
));
continue;
}
let legend: Vec<String> = r
.binders
.iter()
.map(|b| format!("{}: {}", b.name, render_type(&b.ty)))
.collect();
out.push_str(&format!(" variables: [{}]\n", legend.join(", ")));
out.push_str(&format!(
" enumerated {} terms (size ≤ {}{}), {} candidate equations{}\n",
r.stats.term_count,
r.stats.max_term_size,
if r.stats.terms_truncated {
", TRUNCATED"
} else {
""
},
r.stats.conjecture_count,
if r.stats.conjectures_truncated {
" (TRUNCATED)"
} else {
""
},
));
if r.stats.vm_filtered {
out.push_str(&format!(
" VM-filter: {} survived, {} refuted on sample data\n",
r.conjectures.len(),
r.stats.candidates_refuted
));
}
let shown = r.conjectures.len().min(SAMPLE);
let label = if r.stats.vm_filtered {
"survivors"
} else {
"candidates"
};
out.push_str(&format!(" {label} (showing {shown}):\n"));
for c in r.conjectures.iter().take(SAMPLE) {
out.push_str(&format!(" {}\n", c.render(&r.binders)));
}
if r.conjectures.len() > SAMPLE {
out.push_str(&format!(
" … and {} more\n",
r.conjectures.len() - SAMPLE
));
}
if !r.proved.is_empty() {
out.push_str(&format!(
" PROVED (Lean, kernel-checked): {}\n",
r.proved.len()
));
for p in &r.proved {
out.push_str(&format!(" ✓ {p}\n"));
}
}
}
out
}