use super::*;
pub fn run_discovery(inputs: &ProofLowerInputs) -> Vec<LawDiscovery> {
let mut reports = Vec::new();
for item in inputs.entry_items {
let TopLevel::Verify(vb) = item else {
continue;
};
let VerifyKind::Law(law) = &vb.kind else {
continue;
};
let cone = LawProofCone::compute(law, &vb.fn_name, inputs);
let cone_fn_count = cone.pure_fns().len();
let mut binders = Vec::new();
let mut conjectures = Vec::new();
let mut term_count = 0;
let mut terms_truncated = false;
let mut conjectures_truncated = false;
let skipped_large_cone = cone_fn_count > MAX_CONE_FNS;
if !skipped_large_cone {
binders = variable_context(&cone);
let ops = operations(&cone, &binders);
let (terms, tt) = enumerate_terms(&binders, &ops, MAX_TERM_SIZE);
let (conj, ct) = conjectures_from_terms(&terms, &binders);
term_count = terms.len();
terms_truncated = tt;
conjectures = conj;
conjectures_truncated = ct;
conjectures.extend(homomorphism_conjectures(
&cone,
&binders,
inputs,
&vb.fn_name,
));
}
reports.push(LawDiscovery {
subject_fn: vb.fn_name.clone(),
law_name: law.name.clone(),
cone_fns: cone.pure_fns().iter().map(|fd| fd.name.clone()).collect(),
cone_types: cone
.types()
.iter()
.map(|td| crate::codegen::common::type_def_name(td).to_string())
.collect(),
binders,
proved: Vec::new(),
stats: DiscoveryStats {
cone_fn_count,
term_count,
conjecture_count: conjectures.len(),
terms_truncated,
conjectures_truncated,
skipped_large_cone,
vm_filtered: false,
candidates_refuted: 0,
max_term_size: MAX_TERM_SIZE,
},
conjectures,
});
}
reports
}
fn homomorphism_conjectures(
cone: &LawProofCone,
binders: &[Binder],
inputs: &ProofLowerInputs,
subject_fn: &str,
) -> Vec<Conjecture> {
use crate::codegen::proof_recognize::{detect_canonical_peano, is_canonical_add};
use crate::codegen::recursion::detect::single_list_structural_param_index;
let mut fns: Vec<&crate::ast::FnDef> = cone.pure_fns().to_vec();
if let Some(subj) = inputs.find_fn_def_by_call_name(subject_fn)
&& !fns.iter().any(|f| f.name == subj.name)
{
fns.push(subj);
}
let fns = &fns;
let add = fns.iter().find_map(|&fd| {
let p = inputs
.find_type_def(fd.return_type.trim())
.and_then(detect_canonical_peano)?;
is_canonical_add(fd, &p).then(|| (fd.name.clone(), p))
});
let Some((add_name, peano)) = add else {
return Vec::new();
};
let render_of = |ann: &str| render_type(&crate::codegen::common::parse_type_annotation(ann));
let bidx = |ann: &str| -> Vec<usize> {
let want = render_of(ann);
binders
.iter()
.enumerate()
.filter(|(_, b)| render_type(&b.ty) == want)
.map(|(i, _)| i)
.collect()
};
let mut out = Vec::new();
for &g in fns {
if g.name == add_name {
continue;
}
match inputs
.find_type_def(g.return_type.trim())
.and_then(detect_canonical_peano)
{
Some(gp) if gp.type_name == peano.type_name => {}
_ => continue,
}
let Some(list_idx) = single_list_structural_param_index(g) else {
continue;
};
let list_bs = bidx(&g.params[list_idx].1);
if list_bs.len() < 2 {
continue;
}
let (a, b) = (list_bs[0], list_bs[1]);
let mut fixed: BTreeMap<usize, usize> = BTreeMap::new();
let mut ok = true;
for (j, (_, ann)) in g.params.iter().enumerate() {
if j == list_idx {
continue;
}
match bidx(ann).first() {
Some(&bi) => {
fixed.insert(j, bi);
}
None => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
let g_at = |list_val: TermNode| TermNode::App {
callee: g.name.clone(),
args: (0..g.params.len())
.map(|j| {
if j == list_idx {
list_val.clone()
} else {
TermNode::Var(fixed[&j])
}
})
.collect(),
};
let concat = TermNode::App {
callee: "List.concat".to_string(),
args: vec![TermNode::Var(a), TermNode::Var(b)],
};
out.push(Conjecture {
lhs: g_at(concat),
rhs: TermNode::App {
callee: add_name.clone(),
args: vec![g_at(TermNode::Var(a)), g_at(TermNode::Var(b))],
},
ty: crate::codegen::common::parse_type_annotation(&g.return_type),
});
}
out
}
fn variable_context(cone: &LawProofCone) -> Vec<Binder> {
let mut param_types: BTreeMap<String, Type> = BTreeMap::new();
for fd in cone.pure_fns() {
for (_param_name, annotation) in &fd.params {
let ty = crate::codegen::common::parse_type_annotation(annotation);
if ty == Type::Invalid {
continue;
}
param_types.entry(render_type(&ty)).or_insert(ty);
}
}
let mut binders = Vec::new();
for ty in param_types.values() {
for _ in 0..MAX_VARS_PER_TYPE {
binders.push(Binder {
name: format!("x{}", binders.len()),
ty: ty.clone(),
});
}
}
binders
}
fn operations(cone: &LawProofCone, binders: &[Binder]) -> Vec<Op> {
let mut ops = Vec::new();
for fd in cone.pure_fns() {
let params: Vec<Type> = fd
.params
.iter()
.map(|(_, ann)| crate::codegen::common::parse_type_annotation(ann))
.collect();
let ret = crate::codegen::common::parse_type_annotation(&fd.return_type);
if ret == Type::Invalid || params.contains(&Type::Invalid) {
continue;
}
ops.push(Op {
callee: fd.name.clone(),
params,
ret,
});
}
let mut elem_types: BTreeMap<String, Type> = BTreeMap::new();
for b in binders {
collect_list_elem_types(&b.ty, &mut elem_types);
}
for op in &ops {
for p in &op.params {
collect_list_elem_types(p, &mut elem_types);
}
collect_list_elem_types(&op.ret, &mut elem_types);
}
for elem in elem_types.values() {
let list_ty = Type::List(Box::new(elem.clone()));
ops.push(Op {
callee: "List.concat".to_string(),
params: vec![list_ty.clone(), list_ty.clone()],
ret: list_ty,
});
}
ops
}
fn collect_list_elem_types(ty: &Type, out: &mut BTreeMap<String, Type>) {
match ty {
Type::List(elem) | Type::Vector(elem) => {
out.entry(render_type(elem))
.or_insert_with(|| (**elem).clone());
collect_list_elem_types(elem, out);
}
Type::Option(inner) => collect_list_elem_types(inner, out),
Type::Result(a, b) | Type::Map(a, b) => {
collect_list_elem_types(a, out);
collect_list_elem_types(b, out);
}
Type::Tuple(items) => {
for t in items {
collect_list_elem_types(t, out);
}
}
Type::Fn(args, ret, _) => {
for a in args {
collect_list_elem_types(a, out);
}
collect_list_elem_types(ret, out);
}
_ => {}
}
}
fn enumerate_terms(binders: &[Binder], ops: &[Op], max_size: usize) -> (Vec<EnumTerm>, bool) {
let mut terms: Vec<EnumTerm> = Vec::new();
let mut by_size: Vec<Vec<usize>> = vec![Vec::new(); max_size + 1];
let mut seen: HashSet<String> = HashSet::new();
let mut truncated = false;
for (i, b) in binders.iter().enumerate() {
let node = TermNode::Var(i);
if seen.insert(node.render(binders)) {
by_size[1].push(terms.len());
terms.push(EnumTerm {
node,
ty: b.ty.clone(),
});
}
}
'sizes: for size in 2..=max_size {
for op in ops {
let arity = op.params.len();
if arity == 0 || arity > size - 1 {
continue;
}
for comp in compositions(size - 1, arity) {
let mut pools: Vec<Vec<usize>> = Vec::with_capacity(arity);
let mut any_empty = false;
for (j, &part) in comp.iter().enumerate() {
let pool: Vec<usize> = by_size[part]
.iter()
.copied()
.filter(|&id| terms[id].ty == op.params[j])
.collect();
if pool.is_empty() {
any_empty = true;
break;
}
pools.push(pool);
}
if any_empty {
continue;
}
let (combos, combos_capped) = cartesian(&pools, CARTESIAN_CAP);
if combos_capped {
truncated = true;
}
for combo in combos {
if terms.len() >= MAX_TERMS {
truncated = true;
break 'sizes;
}
let args: Vec<TermNode> =
combo.iter().map(|&id| terms[id].node.clone()).collect();
let node = TermNode::App {
callee: op.callee.clone(),
args,
};
let rendered = node.render(binders);
if seen.insert(rendered) {
by_size[size].push(terms.len());
terms.push(EnumTerm {
node,
ty: op.ret.clone(),
});
}
}
}
}
}
(terms, truncated)
}
fn conjectures_from_terms(terms: &[EnumTerm], binders: &[Binder]) -> (Vec<Conjecture>, bool) {
let mut buckets: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for (id, t) in terms.iter().enumerate() {
buckets.entry(render_type(&t.ty)).or_default().push(id);
}
for ids in buckets.values_mut() {
ids.sort_by_key(|&id| (terms[id].node.size(), terms[id].node.render(binders)));
}
let mut out = Vec::new();
let mut seen_pairs: HashSet<(String, String)> = HashSet::new();
let mut truncated = false;
let mut pairs_examined = 0usize;
'buckets: for ids in buckets.values() {
for a in 0..ids.len() {
for b in (a + 1)..ids.len() {
pairs_examined += 1;
if pairs_examined >= MAX_PAIRS_EXAMINED {
truncated = true;
break 'buckets;
}
let lt = &terms[ids[a]];
let rt = &terms[ids[b]];
let mut lv = BTreeSet::new();
lt.node.free_vars(&mut lv);
let mut rv = BTreeSet::new();
rt.node.free_vars(&mut rv);
if lv != rv {
continue;
}
let lr = lt.node.render(binders);
let rr = rt.node.render(binders);
if lr == rr {
continue;
}
let pair = if lr < rr {
(lr.clone(), rr.clone())
} else {
(rr.clone(), lr.clone())
};
if !seen_pairs.insert(pair) {
continue;
}
if out.len() >= MAX_CONJECTURES {
truncated = true;
break 'buckets;
}
out.push(Conjecture {
lhs: lt.node.clone(),
rhs: rt.node.clone(),
ty: lt.ty.clone(),
});
}
}
}
(out, truncated)
}
fn compositions(total: usize, parts: usize) -> Vec<Vec<usize>> {
if parts == 0 {
return if total == 0 { vec![vec![]] } else { vec![] };
}
if parts == 1 {
return if total >= 1 {
vec![vec![total]]
} else {
vec![]
};
}
let mut out = Vec::new();
for first in 1..=total.saturating_sub(parts - 1) {
for mut rest in compositions(total - first, parts - 1) {
let mut v = vec![first];
v.append(&mut rest);
out.push(v);
}
}
out
}
fn cartesian(pools: &[Vec<usize>], cap: usize) -> (Vec<Vec<usize>>, bool) {
let mut capped = false;
let mut acc: Vec<Vec<usize>> = vec![Vec::new()];
for pool in pools {
let mut next: Vec<Vec<usize>> = Vec::new();
'fill: for prefix in &acc {
for &id in pool {
let mut v = prefix.clone();
v.push(id);
next.push(v);
if next.len() >= cap {
capped = true;
break 'fill;
}
}
}
acc = next;
}
(acc, capped)
}
pub(super) fn render_type(ty: &Type) -> String {
match ty {
Type::Int => "Int".to_string(),
Type::Float => "Float".to_string(),
Type::Str => "String".to_string(),
Type::Bool => "Bool".to_string(),
Type::Unit => "Unit".to_string(),
Type::Result(a, b) => format!("Result<{}, {}>", render_type(a), render_type(b)),
Type::Option(a) => format!("Option<{}>", render_type(a)),
Type::List(a) => format!("List<{}>", render_type(a)),
Type::Vector(a) => format!("Vector<{}>", render_type(a)),
Type::Map(a, b) => format!("Map<{}, {}>", render_type(a), render_type(b)),
Type::Tuple(items) => format!(
"({})",
items.iter().map(render_type).collect::<Vec<_>>().join(", ")
),
Type::Fn(args, ret, _) => format!(
"({}) -> {}",
args.iter().map(render_type).collect::<Vec<_>>().join(", "),
render_type(ret)
),
Type::Named { name, .. } => name.clone(),
Type::Var(n) => n.clone(),
Type::Invalid => "<invalid>".to_string(),
}
}