use std::collections::{BTreeMap, BTreeSet, HashSet};
use crate::ast::{TopLevel, VerifyKind};
use crate::codegen::proof_lower::{LawProofCone, ProofLowerInputs};
use crate::nan_value::{NanValue, NanValueConvert};
use crate::types::Type;
use crate::value::Value;
const MAX_VARS_PER_TYPE: usize = 2;
const MAX_TERM_SIZE: usize = 5;
const MAX_TERMS: usize = 20_000;
const MAX_CONJECTURES: usize = 20_000;
const MAX_PAIRS_EXAMINED: usize = 2_000_000;
const CARTESIAN_CAP: usize = 4_000;
const MAX_CONE_FNS: usize = 24;
#[derive(Debug, Clone)]
pub struct Binder {
pub name: String,
pub ty: Type,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TermNode {
Var(usize),
App { callee: String, args: Vec<TermNode> },
}
impl TermNode {
fn size(&self) -> usize {
match self {
TermNode::Var(_) => 1,
TermNode::App { args, .. } => 1 + args.iter().map(TermNode::size).sum::<usize>(),
}
}
fn render(&self, binders: &[Binder]) -> String {
match self {
TermNode::Var(i) => binders
.get(*i)
.map(|b| b.name.clone())
.unwrap_or_else(|| format!("?{i}")),
TermNode::App { callee, args } => {
let rendered: Vec<String> = args.iter().map(|a| a.render(binders)).collect();
format!("{callee}({})", rendered.join(", "))
}
}
}
fn free_vars(&self, out: &mut BTreeSet<usize>) {
match self {
TermNode::Var(i) => {
out.insert(*i);
}
TermNode::App { args, .. } => {
for a in args {
a.free_vars(out);
}
}
}
}
}
#[derive(Debug, Clone)]
struct Op {
callee: String,
params: Vec<Type>,
ret: Type,
}
#[derive(Debug, Clone)]
struct EnumTerm {
node: TermNode,
ty: Type,
}
#[derive(Debug, Clone)]
pub struct Conjecture {
pub lhs: TermNode,
pub rhs: TermNode,
pub ty: Type,
}
impl Conjecture {
pub fn render(&self, binders: &[Binder]) -> String {
format!(
"{} == {}",
self.lhs.render(binders),
self.rhs.render(binders)
)
}
}
#[derive(Debug, Clone)]
pub struct DiscoveryStats {
pub cone_fn_count: usize,
pub term_count: usize,
pub conjecture_count: usize,
pub terms_truncated: bool,
pub conjectures_truncated: bool,
pub skipped_large_cone: bool,
pub vm_filtered: bool,
pub candidates_refuted: usize,
pub max_term_size: usize,
}
#[derive(Debug, Clone)]
pub struct LawDiscovery {
pub subject_fn: String,
pub law_name: String,
pub cone_fns: Vec<String>,
pub cone_types: Vec<String>,
pub binders: Vec<Binder>,
pub conjectures: Vec<Conjecture>,
pub proved: Vec<String>,
pub stats: DiscoveryStats,
}
mod bricks;
mod committed;
mod enumerate;
mod render;
mod vm_filter;
pub use bricks::structural_lemma_groups;
pub use committed::{
CommittedLemma, SimpDirection, apply_simp_over_lemma_pins, forbidden_token_in_lemma,
mentioned_fns, parse_committed_lemmas, plan_simp_over_lemma_pins, simp_entries,
simp_orientation,
};
pub use enumerate::run_discovery;
pub use render::{
discovery_surface_hash, lean_lemma_theorem, rank_candidate_indices, render_report,
};
pub use vm_filter::vm_filter;
use enumerate::render_type;
#[cfg(test)]
use bricks::detect_encoders;
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::ModuleInfo;
use std::collections::HashSet;
const SRC: &str = r#"
record Run
char: String
count: Int
fn repeat(c: String, n: Int) -> List<String>
[c]
fn expandRun(r: Run) -> List<String>
repeat(r.char, r.count)
fn decode(runs: List<Run>) -> List<String>
match runs
[] -> []
[run, ..rest] -> List.concat(expandRun(run), decode(rest))
fn encode(xs: List<String>) -> List<Run>
[]
verify encode law roundtrip
given xs: List<String> = [[], ["a"]]
decode(encode(xs)) => xs
"#;
fn with_inputs<R>(src: &str, f: impl FnOnce(&ProofLowerInputs) -> R) -> R {
let mut lexer = crate::lexer::Lexer::new(src);
let tokens = lexer.tokenize().expect("lex");
let mut items = crate::parser::Parser::new(tokens).parse().expect("parse");
crate::ir::pipeline::tco(&mut items);
crate::ir::pipeline::resolve(&mut items);
let symbols = crate::ir::SymbolTable::build(&items, &[]);
let resolved = crate::ir::hir::resolve_program(&symbols, &items);
let resolved_fns: Vec<&crate::ir::hir::ResolvedFnDef> = resolved
.iter()
.filter_map(|t| match t {
crate::ir::hir::ResolvedTopLevel::FnDef(fd) => Some(fd),
_ => None,
})
.collect();
let shape =
crate::analysis::shape::analyze_program_with_modules(&resolved_fns, &items, &[]);
let prefixes: HashSet<String> = HashSet::new();
let recursive: HashSet<crate::ir::FnId> = HashSet::new();
let no_modules: &[ModuleInfo] = &[];
let inputs = ProofLowerInputs {
entry_items: &items,
dep_modules: no_modules,
module_prefixes: &prefixes,
recursive_fns: &recursive,
symbol_table: &symbols,
program_shape: Some(&shape),
};
f(&inputs)
}
fn discover(src: &str) -> Vec<LawDiscovery> {
with_inputs(src, |inputs| {
let mut reports = run_discovery(inputs);
vm_filter(&mut reports, inputs);
reports
})
}
fn rle_source() -> String {
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/rle.av"))
.expect("read rle.av")
}
fn tally_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/tally.av"
))
.expect("read tally.av")
}
fn drain_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/drain.av"
))
.expect("read drain.av")
}
fn scale_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/scale.av"
))
.expect("read scale.av")
}
fn twofield_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/twofield.av"
))
.expect("read twofield.av")
}
fn sparse_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/sparse.av"
))
.expect("read sparse.av")
}
fn sum_acc_source() -> String {
std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/data/sum_acc.av"
))
.expect("read sum_acc.av")
}
fn encoder_roles(src: &str) -> Vec<String> {
with_inputs(src, |inputs| {
detect_encoders(inputs)
.into_iter()
.map(|e| {
format!(
"{}/{}/{}/{}/{}/{}/{}",
e.wrapper, e.inverse, e.loop_fn, e.finish, e.step, e.expand, e.var
)
})
.collect()
})
}
fn is_self_concat_identity(c: &Conjecture) -> bool {
fn oriented(l: &TermNode, r: &TermNode) -> bool {
let TermNode::Var(x) = l else { return false };
let TermNode::App { callee, args } = r else {
return false;
};
callee == "List.concat"
&& args.len() == 2
&& matches!((&args[0], &args[1]), (TermNode::Var(a), TermNode::Var(b)) if a == x && b == x)
}
oriented(&c.lhs, &c.rhs) || oriented(&c.rhs, &c.lhs)
}
fn is_decode_append(c: &Conjecture) -> bool {
fn oriented(l: &TermNode, r: &TermNode) -> bool {
let TermNode::App {
callee: lc,
args: la,
} = l
else {
return false;
};
if lc != "decode" || la.len() != 1 {
return false;
}
let TermNode::App {
callee: cc,
args: ca,
} = &la[0]
else {
return false;
};
if cc != "List.concat" || ca.len() != 2 {
return false;
}
let (TermNode::Var(a), TermNode::Var(b)) = (&ca[0], &ca[1]) else {
return false;
};
if a == b {
return false;
}
let TermNode::App {
callee: rc,
args: ra,
} = r
else {
return false;
};
if rc != "List.concat" || ra.len() != 2 {
return false;
}
let (
TermNode::App {
callee: d1,
args: r1,
},
TermNode::App {
callee: d2,
args: r2,
},
) = (&ra[0], &ra[1])
else {
return false;
};
if d1 != "decode" || d2 != "decode" || r1.len() != 1 || r2.len() != 1 {
return false;
}
matches!((&r1[0], &r2[0]), (TermNode::Var(a2), TermNode::Var(b2)) if a2 == a && b2 == b)
}
oriented(&c.lhs, &c.rhs) || oriented(&c.rhs, &c.lhs)
}
#[test]
fn cone_excludes_subject_and_closes_over_pure_helpers() {
let reports = discover(SRC);
assert_eq!(reports.len(), 1);
let r = &reports[0];
assert_eq!(r.subject_fn, "encode");
assert_eq!(r.law_name, "roundtrip");
assert_eq!(r.cone_fns, vec!["decode", "expandRun", "repeat"]);
}
#[test]
fn cone_types_resolve_adts_from_signatures() {
let r = &discover(SRC)[0];
assert_eq!(r.cone_types, vec!["Run"]);
}
#[test]
fn enumerator_rediscovers_decode_append() {
let r = &discover(SRC)[0];
assert!(
r.conjectures.iter().any(is_decode_append),
"decode_append candidate not found among {} conjectures",
r.conjectures.len()
);
assert!(!r.stats.terms_truncated, "term enumeration truncated");
assert!(
!r.stats.conjectures_truncated,
"conjecture generation truncated"
);
}
#[test]
fn enumerator_rediscovers_decode_append_on_real_rle() {
let src =
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/rle.av"))
.expect("read rle.av");
let reports = discover(&src);
let roundtrip = reports
.iter()
.find(|r| r.law_name == "roundtrip")
.expect("roundtrip law");
assert_eq!(
roundtrip.cone_fns,
vec![
"decode",
"encodeFold",
"encodeLoop",
"expandRun",
"flushAcc",
"repeat"
]
);
assert!(
roundtrip.conjectures.iter().any(is_decode_append),
"decode_append candidate not found among {} conjectures on real rle.av",
roundtrip.conjectures.len()
);
assert!(!roundtrip.stats.terms_truncated && !roundtrip.stats.conjectures_truncated);
}
#[test]
fn vm_filter_refutes_false_keeps_decode_append() {
let r = &discover(SRC)[0];
assert!(r.stats.vm_filtered, "VM-filter did not run");
assert!(
r.stats.candidates_refuted > 0,
"VM-filter refuted nothing — oracle likely failed to compile"
);
assert!(
r.conjectures.iter().any(is_decode_append),
"decode_append did not survive the VM-filter"
);
assert!(
!r.conjectures.iter().any(is_self_concat_identity),
"false self-concat identity survived the VM-filter"
);
}
#[test]
fn lean_theorem_renders_decode_append() {
let r = &discover(SRC)[0];
let c = r
.conjectures
.iter()
.find(|c| is_decode_append(c))
.expect("decode_append survives");
let thm = lean_lemma_theorem(c, &r.binders, "L").expect("template applies");
assert!(thm.contains("theorem L "), "{thm}");
assert!(thm.contains(": List Run)"), "{thm}");
assert!(thm.contains("decode (") && thm.contains("++"), "{thm}");
assert!(thm.contains("induction "), "{thm}");
assert!(thm.contains("| nil => first | (simp [decode]"), "{thm}");
assert!(thm.contains("List.append_assoc"), "{thm}");
assert!(thm.contains("ih"), "{thm}");
assert!(
thm.contains("omega"),
"ladder must include the omega branch: {thm}"
);
assert!(
thm.contains("split <;>"),
"ladder must include the inner-match split branch: {thm}"
);
assert!(
!thm.contains("sorry"),
"discovered-lemma tactic must never carry a sorry fallback: {thm}"
);
}
#[test]
fn ranking_puts_homomorphism_first() {
let r = &discover(SRC)[0];
let ranked = rank_candidate_indices(r);
let first = &r.conjectures[ranked[0]];
assert!(
is_decode_append(first),
"expected decode_append ranked first, got {}",
first.render(&r.binders)
);
}
const COUNT_HOMO_SRC: &str = r#"
type Nat
Z
S(Nat)
fn eqNat(x: Nat, y: Nat) -> Bool
match x
Nat.Z -> match y
Nat.Z -> true
Nat.S(z) -> false
Nat.S(x2) -> match y
Nat.Z -> false
Nat.S(y2) -> eqNat(x2, y2)
fn count(x: Nat, y: List<Nat>) -> Nat
match y
[] -> Nat.Z
[z, ..ys] -> match eqNat(x, z)
true -> Nat.S(count(x, ys))
false -> count(x, ys)
fn plus(x: Nat, y: Nat) -> Nat
match x
Nat.Z -> y
Nat.S(z) -> Nat.S(plus(z, y))
fn appendNat(xs: List<Nat>, ys: List<Nat>) -> List<Nat>
List.concat(xs, ys)
verify count law countPlusConcat
given n: Nat = [Nat.Z, Nat.S(Nat.Z)]
given xs: List<Nat> = [[], [Nat.Z]]
given ys: List<Nat> = [[], [Nat.S(Nat.Z)]]
plus(count(n, xs), count(n, ys)) => count(n, appendNat(xs, ys))
"#;
#[test]
fn structural_homomorphism_conjectured_for_count_fold() {
let r = &discover(COUNT_HOMO_SRC)[0];
let found = r.conjectures.iter().any(|c| {
let s = c.render(&r.binders);
s.contains("List.concat(") && s.contains("plus(count(")
});
assert!(
found,
"count→plus homomorphism not conjectured/surviving; survivors:\n{}",
r.conjectures
.iter()
.map(|c| c.render(&r.binders))
.collect::<Vec<_>>()
.join("\n")
);
let ranked = rank_candidate_indices(r);
let top = r.conjectures[ranked[0]].render(&r.binders);
assert!(
top.contains("List.concat(") && top.contains("plus(count("),
"count homomorphism must rank first, got {top}"
);
}
#[test]
fn structural_conjecturer_on_real_rle() {
let groups = with_inputs(&rle_source(), structural_lemma_groups);
assert_eq!(
groups.len(),
1,
"expected only the relational chain (standalone bricks deduped)"
);
let all: String = groups.iter().flatten().map(|(_, t)| t.as_str()).collect();
assert!(
all.contains("repeat' char (n + 1) = repeat' char n ++ [char]"),
"{all}"
);
assert!(
all.contains("(hn : 0 <= n)") && all.contains("natAbs"),
"{all}"
);
assert!(all.contains("_count_nonneg"), "{all}");
assert!(all.contains("0 <= (encodeFold acc char).count"), "{all}");
assert!(
all.contains("unfold encodeFold") && all.contains("split <;>"),
"{all}"
);
assert_eq!(
all.matches("0 <= (encodeFold acc char).count").count(),
1,
"count_nonneg duplicated — dedup regressed"
);
}
#[test]
fn monotone_field_generalizes_beyond_rle_shape() {
let groups = with_inputs(&tally_source(), structural_lemma_groups);
let all: String = groups.iter().flatten().map(|(_, t)| t.as_str()).collect();
assert!(all.contains("0 <= (tallyStep acc x).seen"), "{all}");
assert!(all.contains("unfold tallyStep"), "{all}");
assert!(!all.contains("acc.current"), "{all}");
}
#[test]
fn bounded_step_handles_decreasing_accumulator() {
let groups = with_inputs(&drain_source(), structural_lemma_groups);
let all: String = groups.iter().flatten().map(|(_, t)| t.as_str()).collect();
assert!(!all.contains("0 <= (tick acc x).n"), "{all}");
assert!(all.contains("acc.n - 1 <= (tick acc x).n"), "{all}");
assert!(all.contains("(tick acc x).n <= acc.n + 1"), "{all}");
assert!(
all.contains("unfold tick") && all.contains("split <;>"),
"{all}"
);
}
#[test]
fn nonneg_covers_multiplicative_scaling() {
let groups = with_inputs(&scale_source(), structural_lemma_groups);
let all: String = groups.iter().flatten().map(|(_, t)| t.as_str()).collect();
assert!(all.contains("0 <= (grow acc x).level"), "{all}");
assert!(all.contains("unfold grow"), "{all}");
}
#[test]
fn detect_encoder_recognizes_rle_and_sparse() {
assert!(
encoder_roles(&rle_source())
.contains(&"encode/decode/encodeLoop/flushAcc/encodeFold/expandRun/xs".to_string()),
"rle roles: {:?}",
encoder_roles(&rle_source())
);
assert!(
encoder_roles(&sparse_source()).contains(
&"encodeSparse/decodeSparse/sparseLoop/flushSparse/sparseStep/expandTok/xs"
.to_string()
),
"sparse roles: {:?}",
encoder_roles(&sparse_source())
);
}
#[test]
fn shape_classifies_fold_wrappers() {
use crate::analysis::shape::ModulePattern;
let wrappers = |src: &str| -> Vec<(String, String)> {
with_inputs(src, |inputs| {
inputs
.program_shape
.map(|s| {
s.patterns
.iter()
.filter_map(|p| match p {
ModulePattern::WrapperOverRecursion {
wrapper_fn,
inner_fn,
..
} => Some((wrapper_fn.clone(), inner_fn.clone())),
_ => None,
})
.collect()
})
.unwrap_or_default()
})
};
assert!(
wrappers(&rle_source()).contains(&("encode".to_string(), "encodeLoop".to_string())),
"rle: {:?}",
wrappers(&rle_source())
);
assert!(
wrappers(&sparse_source())
.contains(&("encodeSparse".to_string(), "sparseLoop".to_string())),
"sparse: {:?}",
wrappers(&sparse_source())
);
assert!(
wrappers(&sum_acc_source()).contains(&("sum".to_string(), "sumTR".to_string())),
"sum_acc: {:?}",
wrappers(&sum_acc_source())
);
}
#[test]
fn monoidal_spec_equivalence_emitted_for_sum_acc() {
let all: String = with_inputs(&sum_acc_source(), structural_lemma_groups)
.iter()
.flatten()
.map(|(_, t)| t.as_str())
.collect();
assert!(all.contains("sum xs = sumDirect xs"), "{all}");
assert!(
all.contains("sumTR list acc = acc + sumDirect list"),
"{all}"
);
assert!(all.contains("induction list with"), "{all}");
assert!(
all.contains("rw [ih (acc + h)]") && all.contains("omega"),
"{all}"
);
assert!(!all.contains("flush_fold_step"), "{all}");
assert!(!all.contains("inv_append"), "{all}");
}
#[test]
fn relational_chain_emitted_for_rle_and_sparse() {
let rle: String = with_inputs(&rle_source(), structural_lemma_groups)
.iter()
.flatten()
.map(|(_, t)| t.as_str())
.collect();
assert!(rle.contains("decode (encode xs) = xs"), "{rle}");
assert!(
rle.contains("decode (flushAcc (encodeFold acc x)) = decode (flushAcc acc) ++ [x]"),
"{rle}"
);
assert!(rle.contains("unfold encodeFold flushAcc"), "{rle}");
assert!(
rle.contains("{ runs := [], current := \"\", count := 0 }"),
"{rle}"
);
let sparse: String = with_inputs(&sparse_source(), structural_lemma_groups)
.iter()
.flatten()
.map(|(_, t)| t.as_str())
.collect();
assert!(
sparse.contains("decodeSparse (encodeSparse xs) = xs"),
"{sparse}"
);
assert!(sparse.contains("repeat0 1 = [0]"), "{sparse}");
assert!(sparse.contains("unfold sparseStep flushSparse"), "{sparse}");
assert!(sparse.contains("{ out := [], pending := 0 }"), "{sparse}");
let crux = "split <;> (try split) <;> (try split) <;>";
assert!(rle.contains(crux) && sparse.contains(crux));
}
#[test]
fn multi_int_fields_each_get_a_lemma() {
let groups = with_inputs(&twofield_source(), structural_lemma_groups);
let all: String = groups.iter().flatten().map(|(_, t)| t.as_str()).collect();
assert!(all.contains("0 <= (meterStep acc x).seen"), "{all}");
assert!(
all.contains("acc.budget - 1 <= (meterStep acc x).budget"),
"{all}"
);
assert!(
all.contains("(meterStep acc x).budget <= acc.budget - 1"),
"{all}"
);
}
}