use std::collections::BTreeSet;
use super::super::expr::aver_name_to_lean;
use super::super::shared::to_lower_first;
use super::AutoProof;
use super::shared::law_simp_defs;
use crate::ast::{TypeDef, TypeVariant, VerifyBlock, VerifyLaw};
use crate::codegen::CodegenContext;
fn lean_rev_support(
ops: &[crate::codegen::proof_recognize::RevOp],
law_uid: &str,
) -> (Vec<String>, Vec<String>) {
let mut support = Vec::new();
let mut simp_extra = Vec::new();
for op in ops {
let r = aver_name_to_lean(&op.rev);
let a = aver_name_to_lean(&op.append);
let nilr = format!("{law_uid}_{a}_nilR");
let assoc = format!("{law_uid}_{a}_assoc");
let dist = format!("{law_uid}_{r}_revDist");
support.push(format!(
"theorem {nilr} : ∀ (xa : List Int), {a} xa [] = xa := by\n intro xa; induction xa with\n | nil => simp [{a}]\n | cons h t ih => simp [{a}, ih]"
));
support.push(format!(
"theorem {assoc} : ∀ (xa xb xc : List Int), {a} ({a} xa xb) xc = {a} xa ({a} xb xc) := by\n intro xa xb xc; induction xa with\n | nil => simp [{a}]\n | cons h t ih => simp [{a}, ih]"
));
support.push(format!(
"theorem {dist} : ∀ (xa xb : List Int), {r} ({a} xa xb) = {a} ({r} xb) ({r} xa) := by\n intro xa xb; induction xa with\n | nil => simp [{r}, {a}, {nilr}]\n | cons h t ih => simp [{r}, {a}, ih, {assoc}]"
));
simp_extra.push(a);
simp_extra.push(dist);
}
(support, simp_extra)
}
fn lean_nat_lift_support(
law: &VerifyLaw,
ctx: &CodegenContext,
law_uid: &str,
extra_fns: &BTreeSet<String>,
) -> (Vec<String>, Vec<String>, BTreeSet<String>) {
use crate::codegen::proof_recognize::{NatArithKind, NatCompareKind};
let mut support = Vec::new();
let mut simp_extra = Vec::new();
let mut bridged_fns: BTreeSet<String> = BTreeSet::new();
let mut arith = crate::codegen::proof_recognize::collect_nat_arith_ops_in_law(law, ctx);
for op in crate::codegen::proof_recognize::collect_nat_arith_ops_for_names(extra_fns, ctx) {
if !arith.iter().any(|o| o.fn_name == op.fn_name) {
arith.push(op);
}
}
let add_bridge_name = arith
.iter()
.find(|op| op.kind == NatArithKind::Add)
.map(|op| format!("{law_uid}_{}_isNatAdd", aver_name_to_lean(&op.fn_name)));
let mut has_mul = false;
let ordered = arith
.iter()
.filter(|o| o.kind != NatArithKind::Mul)
.chain(arith.iter().filter(|o| o.kind == NatArithKind::Mul));
for op in ordered {
let f = aver_name_to_lean(&op.fn_name);
bridged_fns.insert(f.clone());
match op.kind {
NatArithKind::Add => {
let name = format!("{law_uid}_{f}_isNatAdd");
support.push(format!(
"theorem {name} : ∀ a b, {f} a b = a + b := by\n intro a b\n induction a with\n | zero => first | (simp [{f}]; done) | (simp [{f}]; omega) | sorry\n | succ k ih => first | (simp [{f}, ih]; done) | (simp [{f}, ih]; omega) | sorry"
));
simp_extra.push(name);
}
NatArithKind::Sub => {
let name = format!("{law_uid}_{f}_isNatSub");
support.push(format!(
"theorem {name} : ∀ a b, {f} a b = a - b := by\n intro a b\n induction a generalizing b with\n | zero => first | (simp [{f}]; done) | (simp [{f}]; omega) | sorry\n | succ k ih => cases b with\n | zero => first | (simp [{f}]; done) | (simp [{f}]; omega) | sorry\n | succ j => first | (simp [{f}, ih]; done) | (simp [{f}, ih]; omega) | sorry"
));
simp_extra.push(name);
}
NatArithKind::Mul => {
let Some(add_name) = &add_bridge_name else {
continue;
};
let name = format!("{law_uid}_{f}_isNatMul");
support.push(format!(
"theorem {name} : ∀ a b, {f} a b = a * b := by\n intro a b\n induction a with\n | zero => first | (simp [{f}]; done) | (simp [{f}]; omega) | sorry\n | succ k ih => first | (simp only [{f}, {add_name}, ih, Nat.succ_mul, Nat.add_comm]) | sorry"
));
simp_extra.push(name);
has_mul = true;
}
}
}
let mut compare = crate::codegen::proof_recognize::collect_nat_compare_ops_in_law(law, ctx);
for op in crate::codegen::proof_recognize::collect_nat_compare_ops_for_names(extra_fns, ctx) {
if !compare.iter().any(|o| o.fn_name == op.fn_name) {
compare.push(op);
}
}
for op in compare {
let f = aver_name_to_lean(&op.fn_name);
bridged_fns.insert(f.clone());
match op.kind {
NatCompareKind::Le => {
let name = format!("{law_uid}_{f}_isNatLe");
support.push(format!(
"theorem {name} : ∀ a b, ({f} a b = true) = (a ≤ b) := by\n intro a b\n induction a generalizing b with\n | zero => first | (simp [{f}]) | sorry\n | succ k ih => cases b with\n | zero => first | (simp [{f}]) | sorry\n | succ w => first | (simp [{f}, ih]) | sorry"
));
simp_extra.push(name);
}
NatCompareKind::Lt => {
let name = format!("{law_uid}_{f}_isNatLt");
support.push(format!(
"theorem {name} : ∀ a b, ({f} a b = true) = (a < b) := by\n intro a b\n induction b generalizing a with\n | zero => cases a <;> first | (simp [{f}]) | sorry\n | succ k ih => cases a <;> first | (simp [{f}, ih]) | sorry"
));
simp_extra.push(name);
}
}
}
if has_mul {
for lemma in [
"Nat.mul_add",
"Nat.add_mul",
"Nat.mul_assoc",
"Nat.succ_mul",
"Nat.mul_succ",
"Nat.mul_one",
"Nat.one_mul",
"Nat.mul_zero",
"Nat.zero_mul",
] {
simp_extra.push(lemma.to_string());
}
}
(support, simp_extra, bridged_fns)
}
fn discovered_lemma_source_fns(ctx: &CodegenContext, names: &[String]) -> BTreeSet<String> {
use std::collections::BTreeMap;
if names.is_empty() {
return BTreeSet::new();
}
let lean_index: BTreeMap<String, String> = ctx
.modules
.iter()
.flat_map(|m| m.fn_defs.iter())
.chain(ctx.fn_defs.iter())
.filter(|fd| crate::codegen::common::is_pure_fn(fd))
.map(|fd| (aver_name_to_lean(&fd.name), fd.name.clone()))
.collect();
ctx.discovered_lemmas
.iter()
.filter(|l| names.contains(&l.name))
.flat_map(|l| crate::codegen::lemma_discovery::mentioned_fns(&l.text, &lean_index))
.collect()
}
fn discovered_simp_entries(ctx: &CodegenContext, names: &[String]) -> Vec<String> {
if names.is_empty() {
return Vec::new();
}
let program_fns: BTreeSet<String> = ctx
.modules
.iter()
.flat_map(|m| m.fn_defs.iter())
.chain(ctx.fn_defs.iter())
.filter(|fd| crate::codegen::common::is_pure_fn(fd))
.map(|fd| aver_name_to_lean(&fd.name))
.collect();
let pinned: Vec<&crate::codegen::lemma_discovery::CommittedLemma> = ctx
.discovered_lemmas
.iter()
.filter(|l| names.contains(&l.name))
.collect();
crate::codegen::lemma_discovery::simp_entries(&pinned, &program_fns)
}
fn discovered_support_lines(
ctx: &CodegenContext,
vb: &VerifyBlock,
law: &VerifyLaw,
names: &[String],
) -> Vec<String> {
if names.is_empty() {
return Vec::new();
}
let Some(fn_id) = ctx
.symbol_table
.fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name))
else {
return Vec::new();
};
let mut out = Vec::new();
for name in names {
let first_user = ctx.proof_ir.law_theorems.iter().find(|t| {
matches!(&t.strategy,
crate::ir::ProofStrategy::SimpOverLemmas(ns) if ns.contains(name))
});
let this_law_is_first =
first_user.is_some_and(|t| t.fn_id == fn_id && t.law_name == law.name);
if this_law_is_first
&& let Some(lemma) = ctx.discovered_lemmas.iter().find(|l| &l.name == name)
{
out.push(lemma.text.clone());
}
}
out
}
fn program_fn_lean_names(ctx: &CodegenContext) -> BTreeSet<String> {
ctx.modules
.iter()
.flat_map(|m| m.fn_defs.iter())
.chain(ctx.fn_defs.iter())
.filter(|fd| crate::codegen::common::is_pure_fn(fd))
.map(|fd| aver_name_to_lean(&fd.name))
.collect()
}
fn earlier_law_lemmas(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
) -> Vec<crate::codegen::lemma_discovery::CommittedLemma> {
use crate::ast::{TopLevel, VerifyKind};
let inputs = crate::codegen::proof_lower::ProofLowerInputs::from_ctx(ctx);
let cone = crate::codegen::proof_lower::LawProofCone::compute(law, &vb.fn_name, &inputs);
let mut scope: BTreeSet<String> = cone
.pure_fns()
.iter()
.map(|fd| aver_name_to_lean(&fd.name))
.collect();
let subject = aver_name_to_lean(&vb.fn_name);
scope.insert(subject.clone());
let program_index: std::collections::BTreeMap<String, String> = program_fn_lean_names(ctx)
.into_iter()
.map(|l| (l.clone(), l))
.collect();
let mut out = Vec::new();
for item in &ctx.items {
let TopLevel::Verify(prev) = item else {
continue;
};
if prev.line == vb.line && prev.fn_name == vb.fn_name {
break;
}
let VerifyKind::Law(prev_law) = &prev.kind else {
continue;
};
let Some((name, stmt)) =
crate::codegen::lean::toplevel::law_as_lemma_statement(prev, prev_law, ctx)
else {
continue;
};
let text = format!("theorem {name} : {stmt} := by");
let mentions = crate::codegen::lemma_discovery::mentioned_fns(&text, &program_index);
if mentions.is_empty() {
continue;
}
let prev_subject = aver_name_to_lean(&prev.fn_name);
if mentions.is_subset(&scope)
|| (mentions.contains(&subject) && scope.contains(&prev_subject))
{
out.push(crate::codegen::lemma_discovery::CommittedLemma::reference(
name, text,
));
}
}
out
}
fn fastpath_simp_entries(
ctx: &CodegenContext,
committed_names: &[String],
siblings: &[crate::codegen::lemma_discovery::CommittedLemma],
) -> Vec<String> {
let program_fns = program_fn_lean_names(ctx);
let mut pool: Vec<&crate::codegen::lemma_discovery::CommittedLemma> = ctx
.discovered_lemmas
.iter()
.filter(|l| committed_names.contains(&l.name))
.collect();
pool.extend(siblings.iter());
if pool.is_empty() {
return Vec::new();
}
crate::codegen::lemma_discovery::simp_entries(&pool, &program_fns)
}
fn feedback_source_fns(
ctx: &CodegenContext,
committed_names: &[String],
siblings: &[crate::codegen::lemma_discovery::CommittedLemma],
) -> BTreeSet<String> {
let lean_to_source: std::collections::BTreeMap<String, String> = ctx
.modules
.iter()
.flat_map(|m| m.fn_defs.iter())
.chain(ctx.fn_defs.iter())
.filter(|fd| crate::codegen::common::is_pure_fn(fd))
.map(|fd| (aver_name_to_lean(&fd.name), fd.name.clone()))
.collect();
let mut out = discovered_lemma_source_fns(ctx, committed_names);
for s in siblings {
out.extend(crate::codegen::lemma_discovery::mentioned_fns(
&s.text,
&lean_to_source,
));
}
out
}
enum VariantKind {
Leaf,
DirectRec,
IndirectRec,
}
fn classify_variant(variant: &TypeVariant, type_name: &str) -> VariantKind {
let mut has_indirect = false;
for field in &variant.fields {
if field.trim() == type_name {
return VariantKind::DirectRec;
}
if field_type_contains_indirect(field, type_name) {
has_indirect = true;
}
}
if has_indirect {
VariantKind::IndirectRec
} else {
VariantKind::Leaf
}
}
fn field_type_contains_indirect(field_type: &str, type_name: &str) -> bool {
if field_type.trim() == type_name {
return false;
}
field_type.contains(&format!("<{}", type_name))
|| field_type.contains(&format!("{}>", type_name))
|| field_type.contains(&format!(", {}", type_name))
|| field_type.contains(&format!("{},", type_name))
}
fn find_sum_type<'a>(
ctx: &'a CodegenContext,
name: &str,
) -> Option<(&'a String, &'a Vec<TypeVariant>)> {
ctx.modules
.iter()
.flat_map(|m| m.type_defs.iter())
.chain(ctx.type_defs.iter())
.find_map(|td| match td {
TypeDef::Sum {
name: n, variants, ..
} if n == name => Some((n, variants)),
_ => None,
})
}
fn is_recursive_sum(type_name: &str, variants: &[TypeVariant]) -> bool {
variants
.iter()
.any(|variant| variants_fields_contain_type(&variant.fields, type_name))
}
fn variants_fields_contain_type(fields: &[String], type_name: &str) -> bool {
fields.iter().any(|field| {
field.trim() == type_name
|| field.contains(&format!("<{}", type_name))
|| field.contains(&format!("{}>", type_name))
|| field.contains(&format!(", {}", type_name))
|| field.contains(&format!("{},", type_name))
})
}
fn find_induction_target<'a>(
law: &'a VerifyLaw,
ctx: &CodegenContext,
) -> Option<(usize, &'a str, &'a str)> {
for (index, given) in law.givens.iter().enumerate() {
if let Some((_, variants)) = find_sum_type(ctx, &given.type_name)
&& is_recursive_sum(&given.type_name, variants)
{
return Some((index, &given.name, &given.type_name));
}
}
None
}
fn has_indirect_variants(variants: &[TypeVariant], type_name: &str) -> bool {
variants.iter().any(|variant| {
matches!(
classify_variant(variant, type_name),
VariantKind::IndirectRec
)
})
}
fn premise_intro_names(law: &VerifyLaw, intro_names: &[String]) -> Vec<String> {
let mut names = Vec::new();
if law.when.is_some() {
names.extend(intro_names.iter().map(|name| format!("h_{name}")));
names.push("h_when".to_string());
}
names
}
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_structural_induction_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
_theorem_base: &str,
_quant_params: &str,
_theorem_prop: &str,
discovered: &[String],
) -> Option<AutoProof> {
if law.when.is_some() {
return None;
}
let verified_recurses_on_list = ctx
.fn_def_by_name(&vb.fn_name, ctx.active_module_scope().as_deref())
.is_some_and(|fd| {
crate::codegen::recursion::detect::single_list_structural_param_index(fd).is_some()
});
let list_target = find_list_induction_target(law);
let sum_target = find_induction_target(law, ctx);
if verified_recurses_on_list && let Some(target_idx) = list_target {
return emit_list_induction(vb, law, ctx, intro_names, target_idx, discovered);
}
if let Some(proof) = emit_both_args_peeling_law(vb, law, ctx, intro_names) {
return Some(proof);
}
if let Some((target_idx, _target_name, type_name)) = sum_target {
let (_, variants) = find_sum_type(ctx, type_name)?;
if has_indirect_variants(variants, type_name) {
return None;
}
return emit_simple_induction(
vb,
law,
ctx,
intro_names,
target_idx,
type_name,
variants,
discovered,
);
}
if let Some(target_idx) = list_target {
return emit_list_induction(vb, law, ctx, intro_names, target_idx, discovered);
}
None
}
fn find_list_induction_target(law: &VerifyLaw) -> Option<usize> {
law.givens
.iter()
.position(|given| given.type_name.trim().starts_with("List<"))
}
enum MapFoldQuery {
Get,
Has,
}
struct MapFoldHomomorphism {
query: MapFoldQuery,
key_lean: String,
}
fn call_dotted_name(expr: &crate::ast::Spanned<crate::ast::Expr>) -> Option<String> {
use crate::ast::Expr;
match &expr.node {
Expr::FnCall(callee, _) => super::shared::expr_dotted_name(callee),
_ => None,
}
}
fn map_query_over_fold(
expr: &crate::ast::Spanned<crate::ast::Expr>,
fold_fn: &str,
list_given: &str,
given_names: &[String],
) -> Option<String> {
use crate::ast::Expr;
let Expr::FnCall(_, args) = &expr.node else {
return None;
};
if args.len() != 2 {
return None;
}
let Expr::FnCall(map_callee, map_args) = &args[0].node else {
return None;
};
if super::shared::expr_dotted_name(map_callee).as_deref() != Some(fold_fn) {
return None;
}
if map_args.len() != 1
|| !matches!(&map_args[0].node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == list_given)
{
return None;
}
let key = match &args[1].node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
_ => return None,
};
if key == list_given || !given_names.iter().any(|g| g == &key) {
return None;
}
Some(key)
}
fn recognize_map_fold_homomorphism(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
target_idx: usize,
) -> Option<MapFoldHomomorphism> {
use crate::ast::Expr;
let fd = ctx.fn_def_by_name(&vb.fn_name, ctx.active_module_scope().as_deref())?;
crate::codegen::recursion::detect::single_list_structural_param_index(fd)?;
if !fold_step_updates_map(fd, ctx) {
return None;
}
let list_given = &law.givens.get(target_idx)?.name;
let given_names: Vec<String> = law.givens.iter().map(|g| g.name.clone()).collect();
let recognize_side = |side: &crate::ast::Spanned<Expr>| -> Option<(MapFoldQuery, String)> {
match call_dotted_name(side).as_deref() {
Some("Option.withDefault") | Some("Map.getD") => {
let Expr::FnCall(_, args) = &side.node else {
return None;
};
let inner = args.first()?;
if call_dotted_name(inner).as_deref() != Some("Map.get") {
return None;
}
let key = map_query_over_fold(inner, &vb.fn_name, list_given, &given_names)?;
Some((MapFoldQuery::Get, key))
}
Some("Map.get") => {
let key = map_query_over_fold(side, &vb.fn_name, list_given, &given_names)?;
Some((MapFoldQuery::Get, key))
}
Some("Map.has") => {
let key = map_query_over_fold(side, &vb.fn_name, list_given, &given_names)?;
Some((MapFoldQuery::Has, key))
}
_ => None,
}
};
let (query, key) = recognize_side(&law.lhs).or_else(|| recognize_side(&law.rhs))?;
let key_lean = intro_names
.get(given_names.iter().position(|g| g == &key)?)?
.clone();
Some(MapFoldHomomorphism { query, key_lean })
}
fn fold_step_updates_map(fd: &crate::ast::FnDef, ctx: &CodegenContext) -> bool {
fn expr_calls_map_set(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::Expr;
match &expr.node {
Expr::FnCall(callee, args) => {
super::shared::expr_dotted_name(callee).as_deref() == Some("Map.set")
|| args.iter().any(expr_calls_map_set)
|| expr_calls_map_set(callee)
}
Expr::Attr(base, _) => expr_calls_map_set(base),
Expr::BinOp(_, l, r) => expr_calls_map_set(l) || expr_calls_map_set(r),
Expr::Neg(inner) | Expr::ErrorProp(inner) => expr_calls_map_set(inner),
Expr::Match { subject, arms } => {
expr_calls_map_set(subject) || arms.iter().any(|a| expr_calls_map_set(&a.body))
}
Expr::Constructor(_, inner) => inner.as_deref().is_some_and(expr_calls_map_set),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(expr_calls_map_set)
}
_ => false,
}
}
fn body_calls_map_set(fd: &crate::ast::FnDef) -> bool {
fd.body.stmts().iter().any(|s| match s {
crate::ast::Stmt::Expr(e) | crate::ast::Stmt::Binding(_, _, e) => expr_calls_map_set(e),
})
}
fn collect_called_fns(
expr: &crate::ast::Spanned<crate::ast::Expr>,
out: &mut BTreeSet<String>,
) {
use crate::ast::Expr;
if let Expr::FnCall(callee, args) = &expr.node {
if let Some(n) = super::shared::expr_dotted_name(callee) {
out.insert(n);
}
args.iter().for_each(|a| collect_called_fns(a, out));
}
match &expr.node {
Expr::FnCall(callee, _) => collect_called_fns(callee, out),
Expr::Attr(base, _) => collect_called_fns(base, out),
Expr::BinOp(_, l, r) => {
collect_called_fns(l, out);
collect_called_fns(r, out);
}
Expr::Neg(i) | Expr::ErrorProp(i) => collect_called_fns(i, out),
Expr::Match { subject, arms } => {
collect_called_fns(subject, out);
arms.iter().for_each(|a| collect_called_fns(&a.body, out));
}
Expr::Constructor(_, Some(i)) => collect_called_fns(i, out),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().for_each(|i| collect_called_fns(i, out));
}
_ => {}
}
}
if body_calls_map_set(fd) {
return true;
}
let mut callees = BTreeSet::new();
for s in fd.body.stmts() {
match s {
crate::ast::Stmt::Expr(e) | crate::ast::Stmt::Binding(_, _, e) => {
collect_called_fns(e, &mut callees)
}
}
}
callees.iter().any(|name| {
ctx.fn_def_by_name(name, ctx.active_module_scope().as_deref())
.is_some_and(body_calls_map_set)
})
}
fn emit_map_fold_homomorphism(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
target_idx: usize,
plan: &MapFoldHomomorphism,
) -> AutoProof {
let simp_list = law_simp_defs(ctx, vb, law)
.into_iter()
.collect::<Vec<_>>()
.join(", ");
let target_lean = &intro_names[target_idx];
let key = &plan.key_lean;
let fold_lean = aver_name_to_lean(&vb.fn_name);
let map_lemmas = match plan.query {
MapFoldQuery::Get => "AverMap.get_set_self, AverMap.get_set_ne, beq_iff_eq",
MapFoldQuery::Has => "AverMap.has_set, beq_iff_eq",
};
let close = match plan.query {
MapFoldQuery::Get => " <;> omega",
MapFoldQuery::Has => "",
};
let nil_simp = format!("{simp_list}, AverMap.get, AverMap.has");
let rung_close = match plan.query {
MapFoldQuery::Get => format!("simp_all [{map_lemmas}, hkey, hget]{close}"),
MapFoldQuery::Has => format!("simp_all [{map_lemmas}, hkey, hget]; try rfl; done"),
};
let cons_arm = format!(
"| cons head tail ih => simp only [{simp_list}, List.contains_cons] at * <;> by_cases hkey : head = {key} <;> cases hget : AverMap.get ({fold_lean} tail) head <;> first | ({rung_close}) | (simp_all [{map_lemmas}, hkey, hget]; done) | sorry"
);
let proof_lines = vec![
format!(" intro {}", intro_names.join(" ")),
format!(" induction {target_lean} with"),
format!(" | nil => first | (simp [{nil_simp}]) | sorry"),
format!(" {cons_arm}"),
];
AutoProof {
support_lines: Vec::new(),
proof_lines,
replaces_theorem: false,
}
}
fn emit_list_induction(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
target_idx: usize,
discovered: &[String],
) -> Option<AutoProof> {
if discovered.is_empty()
&& let Some(plan) = recognize_map_fold_homomorphism(vb, law, ctx, intro_names, target_idx)
{
return Some(emit_map_fold_homomorphism(
vb,
law,
ctx,
intro_names,
target_idx,
&plan,
));
}
let mut simp_defs: BTreeSet<String> = law_simp_defs(ctx, vb, law);
let law_uid = format!(
"{}_{}",
aver_name_to_lean(&vb.fn_name),
aver_name_to_lean(&law.name)
);
let rev_ops = crate::codegen::proof_recognize::collect_rev_ops_in_law(law, ctx);
let (rev_support, rev_simp) = lean_rev_support(&rev_ops, &law_uid);
simp_defs.extend(rev_simp);
let discovered_simp = discovered_simp_entries(ctx, discovered);
let siblings = earlier_law_lemmas(vb, law, ctx);
let fast_simp = fastpath_simp_entries(ctx, discovered, &siblings);
simp_defs.extend(discovered_simp.iter().cloned());
let simp_list = simp_defs.into_iter().collect::<Vec<_>>().join(", ");
let target_lean = &intro_names[target_idx];
use crate::codegen::recursion::detect::{
param_decremented_in_recursion, param_threaded_in_recursion,
single_list_structural_param_index,
};
let gen_given: Option<(String, bool)> = ctx
.fn_def_by_name(&vb.fn_name, ctx.active_module_scope().as_deref())
.and_then(|fd| {
let lidx = single_list_structural_param_index(fd)?;
let given_intro = |fn_param: &str| -> Option<String> {
law.givens
.iter()
.position(|g| g.name == fn_param)
.map(|gi| intro_names[gi].clone())
};
if let Some((_, (pname, _))) = fd.params.iter().enumerate().find(|(i, (_, ty))| {
*i != lidx
&& (ty.trim() == "Nat"
|| crate::codegen::proof_recognize::peano_type_named(ctx, ty.trim())
.is_some())
&& param_decremented_in_recursion(fd, *i)
}) {
return given_intro(pname).map(|n| (n, true));
}
if let Some((_, (pname, _))) = fd
.params
.iter()
.enumerate()
.find(|(i, _)| *i != lidx && param_threaded_in_recursion(fd, *i))
{
return given_intro(pname).map(|n| (n, false));
}
None
});
let split_set = if simp_list.is_empty() {
"List.cons_append".to_string()
} else {
format!("{simp_list}, List.cons_append")
};
let mk_arms = |arm_simp: &str,
arm_split: &str,
bridges: Option<&str>,
with_sorry: bool|
-> (String, String) {
let nil_bridge = bridges
.map(|b| format!(" | (simp [{arm_simp}]; simp only [{b}] <;> omega)"))
.unwrap_or_default();
let cons_bridge = bridges
.map(|b| format!(" | (simp_all [{arm_simp}]; simp only [{b}] <;> omega)"))
.unwrap_or_default();
let split_bridge = bridges
.map(|b| format!(" <;> (try simp only [{b}])"))
.unwrap_or_default();
let tail = if with_sorry { " | sorry" } else { "" };
(
format!(
"| nil => first | (simp [{arm_simp}]; done) | (simp [{arm_simp}]; omega){nil_bridge} | (simp only [{arm_split}]; split <;> simp_all [{arm_simp}]{split_bridge} <;> omega){tail}"
),
format!(
"| cons head tail ih => first | (simp_all [{arm_simp}]; done) | (simp_all [{arm_simp}]; omega){cons_bridge} | (simp only [{arm_split}]; split <;> simp_all [{arm_simp}]{split_bridge} <;> omega){tail}"
),
)
};
let mut proof_lines = vec![format!(" intro {}", intro_names.join(" "))];
let mut support_lines = Vec::new();
if let Some((gv, needs_cases)) = gen_given.as_ref().filter(|_| fast_simp.is_empty()) {
let ladder = |s: &str| -> String {
format!(
"first | ({s} [{d}]; done) | ({s} [{d}]; omega) | (simp only [{sp}]; split <;> simp_all [{d}] <;> omega) | sorry",
d = simp_list,
sp = split_set
)
};
let wrap = |arm: &str| -> String {
if *needs_cases {
format!("cases {gv} <;> ({arm})")
} else {
arm.to_string()
}
};
proof_lines.push(format!(
" induction {} generalizing {} with",
target_lean, gv
));
proof_lines.push(format!(" | nil => {}", wrap(&ladder("simp"))));
proof_lines.push(format!(
" | cons head tail ih => {}",
wrap(&ladder("simp_all"))
));
} else if fast_simp.is_empty() && gen_given.is_none() {
let (nil_arm, cons_arm) = mk_arms(&simp_list, &split_set, None, true);
proof_lines.push(format!(" induction {} with", target_lean));
proof_lines.push(format!(" {nil_arm}"));
proof_lines.push(format!(" {cons_arm}"));
} else {
let lemma_fns = feedback_source_fns(ctx, discovered, &siblings);
let (arith_support, arith_bridges, bridged_fns) =
lean_nat_lift_support(law, ctx, &law_uid, &lemma_fns);
let mut fast_lemmas: Vec<String> = fast_simp.clone();
fast_lemmas.extend(arith_bridges.iter().cloned());
let fast_unfolds: BTreeSet<String> = law_simp_defs(ctx, vb, law)
.into_iter()
.chain(fast_simp.iter().cloned())
.chain(arith_bridges.iter().cloned())
.filter(|n| !bridged_fns.contains(n))
.collect();
let bridge_set = if arith_bridges.is_empty() {
None
} else {
Some(arith_bridges.join(", "))
};
let arm_forward_siblings: Vec<String> = fast_simp
.iter()
.filter(|e| !e.starts_with("← ") && !discovered_simp.contains(*e))
.cloned()
.collect();
proof_lines.push(" first".to_string());
proof_lines.push(format!(
" | (simp only [{}] <;> omega)",
fast_lemmas.join(", ")
));
let fast_lemmas_set: BTreeSet<String> = fast_lemmas.iter().cloned().collect();
if fast_unfolds != fast_lemmas_set {
proof_lines.push(format!(
" | (simp only [{}] <;> omega)",
fast_unfolds.into_iter().collect::<Vec<_>>().join(", ")
));
}
if let Some((gv, needs_cases)) = gen_given.as_ref() {
let gen_simp = {
let mut v: Vec<String> = simp_list.split(", ").map(String::from).collect();
v.extend(arm_forward_siblings.iter().cloned());
v.retain(|s| !s.is_empty());
v.join(", ")
};
let gen_split = if gen_simp.is_empty() {
"List.cons_append".to_string()
} else {
format!("{gen_simp}, List.cons_append")
};
let ladder = |s: &str| -> String {
let bridge_arm = bridge_set
.as_deref()
.map(|b| format!(" | ({s} [{gen_simp}]; simp only [{b}] <;> omega)"))
.unwrap_or_default();
let split_bridge = bridge_set
.as_deref()
.map(|b| format!(" <;> (try simp only [{b}])"))
.unwrap_or_default();
format!(
"first | ({s} [{gen_simp}]; done) | ({s} [{gen_simp}]; omega){bridge_arm} | (simp only [{gen_split}]; split <;> simp_all [{gen_simp}]{split_bridge} <;> omega) | sorry"
)
};
let wrap = |arm: &str| -> String {
if *needs_cases {
format!("cases {gv} <;> ({arm})")
} else {
arm.to_string()
}
};
proof_lines.push(format!(
" | (induction {} generalizing {} with",
target_lean, gv
));
proof_lines.push(format!(" | nil => {}", wrap(&ladder("simp"))));
proof_lines.push(format!(
" | cons head tail ih => {})",
wrap(&ladder("simp_all"))
));
} else if arm_forward_siblings.is_empty() {
let (nil_arm, cons_arm) = mk_arms(&simp_list, &split_set, bridge_set.as_deref(), true);
proof_lines.push(format!(" | (induction {} with", target_lean));
proof_lines.push(format!(" {nil_arm}"));
proof_lines.push(format!(" {cons_arm})"));
} else {
let (nil_a, cons_a) = mk_arms(&simp_list, &split_set, bridge_set.as_deref(), false);
proof_lines.push(format!(" | (induction {} with", target_lean));
proof_lines.push(format!(" {nil_a}"));
proof_lines.push(format!(" {cons_a})"));
let simp_b = {
let mut v: Vec<String> = simp_list.split(", ").map(String::from).collect();
v.extend(arm_forward_siblings.iter().cloned());
v.retain(|s| !s.is_empty());
v.join(", ")
};
let split_b = if simp_b.is_empty() {
"List.cons_append".to_string()
} else {
format!("{simp_b}, List.cons_append")
};
let (nil_b, cons_b) = mk_arms(&simp_b, &split_b, bridge_set.as_deref(), true);
proof_lines.push(format!(" | (induction {} with", target_lean));
proof_lines.push(format!(" {nil_b}"));
proof_lines.push(format!(" {cons_b})"));
}
support_lines.extend(discovered_support_lines(ctx, vb, law, discovered));
support_lines.extend(arith_support);
}
support_lines.extend(rev_support);
Some(AutoProof {
support_lines,
proof_lines,
replaces_theorem: false,
})
}
fn both_args_peeling_comm_theorem(fn_lean: &str, law_uid: &str) -> (String, String) {
let name = format!("{law_uid}_{fn_lean}_comm");
let ladder = format!(
"first | (cases b <;> simp_all [{fn_lean}]; done) | (cases b <;> simp_all [{fn_lean}]; omega) | sorry"
);
let text = format!(
"theorem {name} : ∀ (a b : Nat), {fn_lean} a b = {fn_lean} b a := by\n intro a b\n induction a generalizing b with\n | zero => {ladder}\n | succ k ih => {ladder}"
);
(text, name)
}
fn consumed_comm_lemmas(
ctx: &CodegenContext,
vb: &VerifyBlock,
law: &VerifyLaw,
law_uid: &str,
) -> (Vec<String>, Vec<String>) {
let mut support = Vec::new();
let mut names = Vec::new();
for src_name in super::shared::law_simp_source_names(ctx, vb, law) {
if src_name == vb.fn_name {
continue;
}
let Some(fd) = ctx.fn_def_by_name(&src_name, ctx.active_module_scope().as_deref()) else {
continue;
};
if crate::codegen::proof_recognize::both_args_peeling_is_commutative(fd, ctx).is_none() {
continue;
}
let fn_lean = aver_name_to_lean(&src_name);
let (text, name) = both_args_peeling_comm_theorem(&fn_lean, law_uid);
support.push(text);
names.push(name);
}
(support, names)
}
fn emit_both_args_peeling_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
) -> Option<AutoProof> {
if law.givens.len() != 2 && law.givens.len() != 3 {
return None;
}
let fd = ctx.fn_def_by_name(&vb.fn_name, ctx.active_module_scope().as_deref())?;
if !crate::codegen::recursion::detect::recurses_decrementing_both_args(fd) {
return None;
}
let param_type = fd.params.first()?.1.trim().to_string();
crate::codegen::proof_recognize::peano_type_named(ctx, ¶m_type)?;
if law.givens.iter().any(|g| g.type_name.trim() != param_type) {
return None;
}
if intro_names.len() != law.givens.len() {
return None;
}
let given_names: Vec<String> = law.givens.iter().map(|g| g.name.clone()).collect();
crate::codegen::proof_recognize::recognize_binary_law_shape(law, &vb.fn_name, &given_names)?;
let simp_list = law_simp_defs(ctx, vb, law)
.into_iter()
.collect::<Vec<_>>()
.join(", ");
let induct_on = &intro_names[0];
let rest = &intro_names[1..];
let generalizing = rest.join(" ");
let cases_prefix = rest
.iter()
.map(|g| format!("cases {g} <;> "))
.collect::<String>();
let ladder = format!(
"first | ({cases_prefix}simp_all [{simp_list}]; done) | ({cases_prefix}simp_all [{simp_list}]; omega) | sorry"
);
let proof_lines = vec![
format!(" intro {}", intro_names.join(" ")),
format!(" induction {induct_on} generalizing {generalizing} with"),
format!(" | zero => {ladder}"),
format!(" | succ k ih => {ladder}"),
];
Some(AutoProof {
support_lines: Vec::new(),
proof_lines,
replaces_theorem: false,
})
}
#[allow(clippy::too_many_arguments)]
fn emit_simple_induction(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
intro_names: &[String],
target_idx: usize,
type_name: &str,
variants: &[TypeVariant],
discovered: &[String],
) -> Option<AutoProof> {
let mut simp_defs: BTreeSet<String> = law_simp_defs(ctx, vb, law);
let discovered_simp = discovered_simp_entries(ctx, discovered);
let siblings = earlier_law_lemmas(vb, law, ctx);
let fast_simp = fastpath_simp_entries(ctx, discovered, &siblings);
simp_defs.extend(discovered_simp.iter().cloned());
let simp_list = simp_defs.into_iter().collect::<Vec<_>>().join(", ");
let target_lean = &intro_names[target_idx];
let premise_names = premise_intro_names(law, intro_names);
let law_uid = format!(
"{}_{}",
aver_name_to_lean(&vb.fn_name),
aver_name_to_lean(&law.name)
);
let lemma_fns = feedback_source_fns(ctx, discovered, &siblings);
let (arith_support, arith_bridges, bridged_fns) =
lean_nat_lift_support(law, ctx, &law_uid, &lemma_fns);
let mut intro_parts = intro_names.to_vec();
intro_parts.extend(premise_names.iter().cloned());
let peano = crate::codegen::proof_recognize::peano_type_named(ctx, type_name);
let arm_bridge = if !arith_bridges.is_empty() {
Some(arith_bridges.join(", "))
} else {
None
};
let (comm_support, comm_lemma_names) = consumed_comm_lemmas(ctx, vb, law, &law_uid);
let comm_arm: Option<(String, String)> = if comm_lemma_names.is_empty() {
None
} else {
let commd_fns: BTreeSet<String> = comm_lemma_names
.iter()
.filter_map(|n| n.strip_suffix("_comm"))
.filter_map(|s| s.strip_prefix(&format!("{law_uid}_")))
.map(str::to_string)
.collect();
let mut comm_set: Vec<String> = law_simp_defs(ctx, vb, law)
.into_iter()
.filter(|d| !commd_fns.contains(d))
.collect();
comm_set.extend(comm_lemma_names.iter().cloned());
let set = comm_set.join(", ");
Some((
format!(" | (simp [{set}]; done)"),
format!(" | (simp_all [{set}]; done)"),
))
};
let mut arm_lines: Vec<String> = Vec::new();
for variant in variants {
let lean_variant = match &peano {
Some(p) if variant.name == p.base_ctor => "zero".to_string(),
Some(p) if variant.name == p.succ_ctor => "succ".to_string(),
_ => to_lower_first(&variant.name),
};
let field_binders: Vec<String> = (0..variant.fields.len())
.map(|index| format!("f{}", index))
.collect();
match classify_variant(variant, type_name) {
VariantKind::Leaf => {
let binders = if field_binders.is_empty() {
String::new()
} else {
format!(" {}", field_binders.join(" "))
};
let bridge = arm_bridge
.as_deref()
.map(|b| format!(" | (simp [{d}]; simp only [{b}] <;> omega)", d = simp_list))
.unwrap_or_default();
let comm = comm_arm
.as_ref()
.map(|(leaf, _)| leaf.as_str())
.unwrap_or_default();
arm_lines.push(format!(
"| {v}{b} => first | (simp [{d}]; done) | (simp [{d}]; omega){bridge}{comm} | sorry",
v = lean_variant,
b = binders,
d = simp_list
));
}
VariantKind::DirectRec => {
let ih_names: Vec<String> = variant
.fields
.iter()
.enumerate()
.filter(|(_, field)| field.trim() == type_name)
.map(|(index, _)| format!("ih{}", index))
.collect();
let bridge = arm_bridge
.as_deref()
.map(|b| {
format!(
" | (simp_all [{d}]; simp only [{b}] <;> omega)",
d = simp_list
)
})
.unwrap_or_default();
let comm = comm_arm
.as_ref()
.map(|(_, rec)| rec.as_str())
.unwrap_or_default();
arm_lines.push(format!(
"| {v} {b} {ih} => first | (simp_all [{d}]; done) | (simp_all [{d}]; omega){bridge}{comm} | sorry",
v = lean_variant,
b = field_binders.join(" "),
ih = ih_names.join(" "),
d = simp_list
));
}
VariantKind::IndirectRec => return None,
}
}
let mut proof_lines = vec![format!(" intro {}", intro_parts.join(" "))];
if arith_bridges.is_empty() && fast_simp.is_empty() {
proof_lines.push(format!(" induction {} with", target_lean));
proof_lines.extend(arm_lines.into_iter().map(|a| format!(" {a}")));
} else {
let mut fast_lemmas: Vec<String> = fast_simp.clone();
fast_lemmas.extend(arith_bridges.iter().cloned());
proof_lines.push(" first".to_string());
proof_lines.push(format!(
" | (simp only [{}] <;> omega)",
fast_lemmas.join(", ")
));
if !fast_simp.is_empty() {
let fast_unfolds: BTreeSet<String> = law_simp_defs(ctx, vb, law)
.into_iter()
.chain(fast_simp.iter().cloned())
.chain(arith_bridges.iter().cloned())
.filter(|n| !bridged_fns.contains(n))
.collect();
let fast_lemmas_set: BTreeSet<String> = fast_lemmas.iter().cloned().collect();
if fast_unfolds != fast_lemmas_set {
proof_lines.push(format!(
" | (simp only [{}] <;> omega)",
fast_unfolds.into_iter().collect::<Vec<_>>().join(", ")
));
}
}
proof_lines.push(format!(" | (induction {} with", target_lean));
let last = arm_lines.len().saturating_sub(1);
for (idx, arm) in arm_lines.into_iter().enumerate() {
if idx == last {
proof_lines.push(format!(" {arm})"));
} else {
proof_lines.push(format!(" {arm}"));
}
}
}
let mut support_lines = discovered_support_lines(ctx, vb, law, discovered);
support_lines.extend(arith_support);
support_lines.extend(comm_support);
Some(AutoProof {
support_lines,
proof_lines,
replaces_theorem: false,
})
}