mod decimal;
mod floor_window;
mod induction;
mod sampled;
mod shared;
mod spec;
mod suffix_roundtrip;
use super::VerifyEmitMode;
use super::expr::aver_name_to_lean;
use super::recursive_pure_fn_names;
use crate::ast::{VerifyBlock, VerifyLaw};
use crate::codegen::CodegenContext;
use crate::verify_law::{collect_missing_helper_law_hints, missing_helper_law_message};
use sampled::emit_guarded_domain_law;
pub(crate) use induction::admitted_dep_law_theorems;
pub(in crate::codegen::lean) use induction::recognize_conditional_comparison_bridge;
pub(in crate::codegen::lean) use induction::recognize_conditional_inductive_generic;
pub use induction::residual_probe_body;
pub struct AutoProof {
pub support_lines: Vec<String>,
pub body: super::tactic_ir::Tactic,
pub replaces_theorem: bool,
}
fn law_strategy_for(
ctx: &CodegenContext,
fn_name: &str,
law_name: &str,
) -> Option<crate::ir::ProofStrategy> {
let fn_id = ctx.law_target_fn_id(fn_name)?;
ctx.proof_ir
.law_theorems
.iter()
.find(|t| t.fn_id == fn_id && t.law_name == law_name)
.map(|t| t.strategy.clone())
}
pub fn emit_verify_law_forall_auto_proof(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
verify_mode: VerifyEmitMode,
theorem_base: &str,
quant_params: &str,
theorem_prop: &str,
) -> Option<AutoProof> {
let inner = emit_verify_law_forall_auto_proof_inner(
vb,
law,
ctx,
verify_mode,
theorem_base,
quant_params,
theorem_prop,
)?;
Some(maybe_wrap_with_grind_rung(vb, law, ctx, inner))
}
fn grind_is_shape_amenable(ctx: &CodegenContext, vb: &VerifyBlock, law: &VerifyLaw) -> bool {
let cone = shared::law_simp_source_names(ctx, vb, law);
if cone.is_empty() {
return false;
}
let recursive = recursive_pure_fn_names(ctx);
!cone.iter().any(|name| recursive.contains(name))
}
fn maybe_wrap_with_grind_rung(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
proof: AutoProof,
) -> AutoProof {
if proof.replaces_theorem {
return proof;
}
if law.when.is_some() {
return proof;
}
let lines = proof.body.render();
if lines.iter().any(|l| l.contains("grind")) {
return proof;
}
if !grind_is_shape_amenable(ctx, vb, law) {
return proof;
}
let ends_in_sorry = lines
.iter()
.rev()
.find(|l| !l.trim().is_empty())
.is_some_and(|l| l.trim_end().ends_with("sorry") || l.trim_end().ends_with("sorry)"));
if !ends_in_sorry {
return proof;
}
let Some(first_line) = lines.first() else {
return proof;
};
if !first_line.trim_start().starts_with("intro ") {
return proof;
}
let cone: Vec<String> = shared::law_simp_defs(ctx, vb, law).into_iter().collect();
if cone.is_empty() {
return proof;
}
use crate::codegen::lean::tactic_ir::Tactic;
let support_lines = proof.support_lines;
let body = match proof.body {
Tactic::Seq(mut steps) if !steps.is_empty() => {
let intro = steps.remove(0);
Tactic::Seq(vec![
Tactic::Leaf(intro.render().join("\n").trim().to_string()),
Tactic::First(vec![
Tactic::Leaf(format!("grind [{}]; done", cone.join(", "))),
Tactic::Seq(steps),
]),
])
}
other => other,
};
AutoProof {
support_lines,
body,
replaces_theorem: false,
}
}
fn emit_verify_law_forall_auto_proof_inner(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
verify_mode: VerifyEmitMode,
theorem_base: &str,
quant_params: &str,
theorem_prop: &str,
) -> Option<AutoProof> {
if verify_mode != VerifyEmitMode::NativeDecide {
return None;
}
let intro_names: Vec<String> = law
.givens
.iter()
.map(|g| aver_name_to_lean(&g.name))
.collect();
let proof_intro_names = extend_intro_names_with_premises(law, &intro_names);
if law.when.is_some()
&& let Some(proof) =
induction::emit_conditional_comparison_bridge_law(vb, law, ctx, &intro_names)
{
return Some(proof);
}
if law.when.is_some()
&& let Some(proof) =
induction::emit_conditional_inductive_generic_law(vb, law, ctx, &intro_names)
{
return Some(proof);
}
let pinned = law_strategy_for(ctx, &vb.fn_name, &law.name);
let discovered: Vec<String> = match &pinned {
Some(crate::ir::ProofStrategy::SimpOverLemmas(names)) => names.clone(),
_ => Vec::new(),
};
if matches!(
pinned,
Some(crate::ir::ProofStrategy::Induction { .. })
| Some(crate::ir::ProofStrategy::SimpOverLemmas(_))
) && let Some(proof) = induction::emit_structural_induction_law(
vb,
law,
ctx,
&intro_names,
theorem_base,
quant_params,
theorem_prop,
&discovered,
) {
return Some(proof);
}
if let Some(proof) =
floor_window::emit_floor_window_law(vb, law, ctx, theorem_base, quant_params)
{
return Some(proof);
}
if let Some(crate::ir::ProofStrategy::TailRecFixedBaseFold {
spec_fn,
loop_fn,
combine_fn,
combine_op,
..
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) = spec::emit_tailrec_fixed_base_fold_law(
law,
ctx,
theorem_base,
&spec_fn,
&loop_fn,
&combine_fn,
combine_op,
)
{
return Some(proof);
}
if let Some(strategy) = law_strategy_for(ctx, &vb.fn_name, &law.name) {
use crate::ast::BinOp;
use crate::ir::ProofStrategy;
let fn_lean = aver_name_to_lean(&vb.fn_name);
let proof_lines = match strategy {
ProofStrategy::Reflexive => Some(vec!["rfl".to_string()]),
ProofStrategy::Commutative { op } => match op {
BinOp::Add => Some(vec![format!("simp [{}, Int.add_comm]", fn_lean)]),
BinOp::Mul => Some(vec![format!("simp [{}, Int.mul_comm]", fn_lean)]),
_ => None,
},
ProofStrategy::Associative { op } => match op {
BinOp::Add => Some(vec![format!("simp [{}, Int.add_assoc]", fn_lean)]),
BinOp::Mul => Some(vec![format!("simp [{}, Int.mul_assoc]", fn_lean)]),
_ => None,
},
ProofStrategy::IdentityElement { .. } => {
Some(vec![format!("simp [{}]", fn_lean)])
}
ProofStrategy::UnaryEqualsBinary { ref inner_fn } => {
Some(vec![format!(
"simp [{}, {}]",
fn_lean,
aver_name_to_lean(inner_fn)
)])
}
ProofStrategy::AntiCommutative { neg_on_rhs, .. } => {
let a = aver_name_to_lean(&law.givens[0].name);
let b = aver_name_to_lean(&law.givens[1].name);
let step = if neg_on_rhs {
format!("simpa [{}] using (Int.neg_sub {} {}).symm", fn_lean, b, a)
} else {
format!("simpa [{}] using (Int.neg_sub {} {})", fn_lean, b, a)
};
Some(vec![step])
}
ProofStrategy::LinearArithmetic { .. } => None,
_ => None,
};
if let Some(lines) = proof_lines {
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
lines,
)),
replaces_theorem: false,
});
}
}
if let Some(crate::ir::ProofStrategy::SpecEquivalence { ref extra_unfolds }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let lean_names: Vec<String> = extra_unfolds.iter().map(|n| aver_name_to_lean(n)).collect();
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!("simpa [{}]", lean_names.join(", "))],
)),
replaces_theorem: false,
});
}
if let Some(crate::ir::ProofStrategy::EnumConstantFold { ref unfold_fns }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let lean_names: Vec<String> = unfold_fns.iter().map(|n| aver_name_to_lean(n)).collect();
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!(
"simp only [{}] <;> (first | (split <;> rfl) | rfl | decide)",
lean_names.join(", ")
)],
)),
replaces_theorem: false,
});
}
if let Some(crate::ir::ProofStrategy::FiniteDomainCases { ref givens }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let cascade = givens
.iter()
.map(|g| format!("cases {}", aver_name_to_lean(g)))
.collect::<Vec<_>>()
.join(" <;> ");
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!("{cascade} <;> (first | rfl | decide | sorry)")],
)),
replaces_theorem: false,
});
}
if let Some(crate::ir::ProofStrategy::IntDecimalRoundtrip {
ref parse_fn,
ref neg_fn,
ref pos_fn,
ref sign_fn,
ref scanner_fn,
ref predicate_fn,
ref finish_fn,
ref finish_int_fn,
ref serializer_fn,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) = decimal::emit_int_decimal_roundtrip_law(
law,
ctx,
theorem_base,
parse_fn,
neg_fn,
pos_fn,
sign_fn,
scanner_fn,
predicate_fn,
finish_fn,
finish_int_fn,
serializer_fn,
)
{
return Some(proof);
}
if let Some(crate::ir::ProofStrategy::StringEscapeRoundtrip(ref pin)) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) =
suffix_roundtrip::emit_string_escape_roundtrip_law(law, ctx, theorem_base, pin)
{
return Some(proof);
}
if let Some(crate::ir::ProofStrategy::LinearIntSpecEquivalence {
ref unfolded_impl,
ref unfolded_spec,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
{
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![
format!(
"change {} = {}",
super::expr::emit_expr(unfolded_impl, ctx),
super::expr::emit_expr(unfolded_spec, ctx)
),
"omega".to_string(),
],
)),
replaces_theorem: false,
});
}
if let Some(crate::ir::ProofStrategy::SpecEquivalenceSimpNormalized { ref extra_unfolds }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let lean_names: Vec<String> = extra_unfolds.iter().map(|n| aver_name_to_lean(n)).collect();
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!("simp [{}]", lean_names.join(", "))],
)),
replaces_theorem: false,
});
}
if let Some(crate::ir::ProofStrategy::EffectfulSpecEquivalence {
ref impl_fn,
ref spec_fn,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
{
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!(
"simp [{}, {}]",
aver_name_to_lean(impl_fn),
aver_name_to_lean(spec_fn)
)],
)),
replaces_theorem: false,
});
}
if matches!(
law_strategy_for(ctx, &vb.fn_name, &law.name),
Some(crate::ir::ProofStrategy::LinearRecurrence2SpecEquivalence { .. })
) && let Some(proof) = spec::emit_second_order_linear_recurrence_spec_equivalence_law(
vb,
law,
ctx,
&proof_intro_names,
) {
return Some(proof);
}
if let Some(crate::ir::ProofStrategy::ResultPipelineChain {
chain_qm_fn,
chain_manual_fn,
step_fns,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) = spec::emit_result_pipeline_chain_law(
vb,
law,
ctx,
&chain_qm_fn,
&chain_manual_fn,
&step_fns,
)
{
return Some(proof);
}
if let Some(crate::ir::ProofStrategy::WrapperOverRecursion {
wrapper_fn,
inner_fn,
other_fn,
combine_op,
driver,
combine_fn,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) = spec::emit_wrapper_over_recursion_law(
vb,
law,
ctx,
&wrapper_fn,
&inner_fn,
&other_fn,
combine_op,
&driver,
combine_fn.as_deref(),
)
{
return Some(proof);
}
spec::emit_spec_function_equivalence_law(vb, law, ctx, &proof_intro_names)
.or_else(|| {
if let Some(crate::ir::ProofStrategy::LibraryAxiom {
ref axiom,
ref args,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
&& matches!(axiom.as_str(), "Map.has_set_self" | "Map.get_set_self")
&& args.len() == 3
{
let lemma = match axiom.as_str() {
"Map.has_set_self" => "AverMap.has_set_self",
"Map.get_set_self" => "AverMap.get_set_self",
_ => unreachable!(),
};
let atom_arg = |e: &crate::ast::Spanned<crate::ir::hir::ResolvedExpr>| {
let rendered = super::expr::emit_expr(e, ctx);
if rendered.contains(' ') && !rendered.starts_with('(') {
format!("({rendered})")
} else {
rendered
}
};
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!(
"simpa using {} {} {} {}",
lemma,
atom_arg(&args[0]),
atom_arg(&args[1]),
atom_arg(&args[2]),
)],
)),
replaces_theorem: false,
});
}
None
})
.or_else(|| {
if let Some(crate::ir::ProofStrategy::MapUpdatePostcondition {
ref outer_fn,
kind,
ref map_arg,
ref key_arg,
ref extra_unfolds,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let outer_lean = aver_name_to_lean(outer_fn);
let extras_lean: Vec<String> =
extra_unfolds.iter().map(|n| aver_name_to_lean(n)).collect();
let atom_render = |e: &crate::ast::Spanned<crate::ir::hir::ResolvedExpr>| {
let rendered = super::expr::emit_expr(e, ctx);
if rendered.contains(' ') && !rendered.starts_with('(') {
format!("({rendered})")
} else {
rendered
}
};
let (axiom_lemma, prefix_extras): (&str, Vec<String>) = match kind {
crate::ir::MapUpdatePostconditionKind::HasAfter => {
("AverMap.has_set_self", Vec::new())
}
crate::ir::MapUpdatePostconditionKind::GetAfter => {
("AverMap.get_set_self", extras_lean.clone())
}
};
let simp_first: String = {
let mut items = vec![outer_lean.clone()];
items.extend(prefix_extras);
format!("simp [{}]", items.join(", "))
};
let simp_second: String = {
let mut items = vec![axiom_lemma.to_string()];
if matches!(kind, crate::ir::MapUpdatePostconditionKind::GetAfter) {
items.push(outer_lean.clone());
items.extend(extras_lean.iter().cloned());
}
format!(
"cases h : AverMap.get {} {} <;> simp [{}]",
atom_render(map_arg),
atom_render(key_arg),
items.join(", ")
)
};
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![simp_first, simp_second],
)),
replaces_theorem: false,
});
}
None
})
.or_else(|| {
if let Some(crate::ir::ProofStrategy::MapKeyTrackedIncrement {
ref outer_fn,
ref map_arg,
ref key_arg,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
{
let outer_lean = aver_name_to_lean(outer_fn);
let atom_render = |e: &crate::ast::Spanned<crate::ir::hir::ResolvedExpr>| {
let rendered = super::expr::emit_expr(e, ctx);
if rendered.contains(' ') && !rendered.starts_with('(') {
format!("({rendered})")
} else {
rendered
}
};
let lines = vec![
format!("simp [{}]", outer_lean),
format!(
"cases h : AverMap.get {} {} <;> simp [AverMap.get_set_self, h]",
atom_render(map_arg),
atom_render(key_arg),
),
];
return Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
lines,
)),
replaces_theorem: false,
});
}
None
})
.or_else(|| {
emit_int_abs_identity_law(law, &proof_intro_names)
})
.or_else(|| {
if let Some(crate::ir::ProofStrategy::LinearArithmetic {
ref unfold_fns,
wrapper_return,
ref smart_guard,
lifted,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
{
if wrapper_return && !lifted && law.when.is_some() {
return None;
}
let chosen_intro: &[String] = if lifted {
&intro_names
} else {
&proof_intro_names
};
let uses_max_min = linear_arith_uses_max_min(unfold_fns, ctx);
return Some(AutoProof {
support_lines: Vec::new(),
body: emit_simp_omega_from_ir(
unfold_fns,
wrapper_return,
smart_guard.as_ref(),
lifted,
chosen_intro,
&intro_names,
law.when.is_some(),
uses_max_min,
ctx,
),
replaces_theorem: false,
});
}
None
})
.or_else(|| {
emit_guarded_domain_law(law).map(|proof_lines| AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(proof_lines),
replaces_theorem: false,
})
})
.or_else(|| {
emit_map_empty_fact_law(vb, law, ctx, &proof_intro_names)
})
.or_else(|| {
emit_map_len_set_positive_law(law, &proof_intro_names)
})
.or_else(|| {
emit_ring_identity_law(vb, law, ctx, &proof_intro_names)
})
.or_else(|| {
emit_simp_over_prelude_lemmas_law(vb, law, ctx, &proof_intro_names)
})
.or_else(|| {
emit_string_length_additive_law(law, &proof_intro_names)
})
.or_else(|| {
emit_string_append_monoid_law(law, &proof_intro_names)
})
.or_else(|| {
if law.when.is_some() {
return None;
}
let defs: Vec<String> = shared::law_simp_defs(ctx, vb, law).into_iter().collect();
if defs.is_empty() {
return None;
}
Some(AutoProof {
support_lines: Vec::new(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(intro_then(
&proof_intro_names,
vec![format!(
"first | (simp [{}] <;> done) | sorry",
defs.join(", ")
)],
)),
replaces_theorem: false,
})
})
}
const RING_NORMALIZATION_LEMMAS: [&str; 11] = [
"Int.mul_add",
"Int.add_mul",
"Int.mul_comm",
"Int.mul_left_comm",
"Int.mul_assoc",
"Int.add_comm",
"Int.add_left_comm",
"Int.add_assoc",
"Int.sub_eq_add_neg",
"Int.zero_sub",
"Int.neg_mul",
];
fn emit_ring_identity_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
proof_intro_names: &[String],
) -> Option<AutoProof> {
let Some(crate::ir::ProofStrategy::RingIdentity { unfold_fns }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
else {
return None;
};
let grind_cone: Vec<String> = unfold_fns.iter().map(|f| aver_name_to_lean(f)).collect();
let simp_set: Vec<String> = grind_cone
.iter()
.cloned()
.chain(RING_NORMALIZATION_LEMMAS.iter().map(|s| s.to_string()))
.collect();
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec![
format!("grind [{}]; done", grind_cone.join(", ")),
format!("simp [{}]; done", simp_set.join(", ")),
],
),
replaces_theorem: false,
})
}
fn emit_simp_over_prelude_lemmas_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
proof_intro_names: &[String],
) -> Option<AutoProof> {
let Some(crate::ir::ProofStrategy::SimpOverPreludeLemmas {
unfold_fns,
fuel_fns,
builtins,
}) = law_strategy_for(ctx, &vb.fn_name, &law.name)
else {
return None;
};
let mut simp_set: Vec<String> = Vec::new();
let push_unique = |set: &mut Vec<String>, name: String| {
if !set.contains(&name) {
set.push(name);
}
};
for f in &unfold_fns {
push_unique(&mut simp_set, aver_name_to_lean(f));
}
for f in &fuel_fns {
push_unique(&mut simp_set, aver_name_to_lean(f));
for name in super::toplevel::law_fuel_simp_names(f, ctx) {
push_unique(&mut simp_set, name);
}
}
for lemma in super::prelude_spec_lemmas_for_builtins(&builtins) {
push_unique(&mut simp_set, lemma);
}
push_unique(&mut simp_set, "Int.add_sub_cancel".to_string());
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec![format!("simp [{}]; done", simp_set.join(", "))],
),
replaces_theorem: false,
})
}
fn emit_string_length_additive_law(
law: &VerifyLaw,
proof_intro_names: &[String],
) -> Option<AutoProof> {
if law.when.is_some() {
return None;
}
if !(law_expr_has_string_len_over_concat(&law.lhs)
|| law_expr_has_string_len_over_concat(&law.rhs))
{
return None;
}
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec!["simp only [String.add_eq_append, String.length_append] <;> omega".to_string()],
),
replaces_theorem: false,
})
}
fn law_expr_has_string_len_over_concat(e: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::Expr;
let here = matches!(&e.node, Expr::FnCall(callee, args)
if args.len() == 1
&& crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref()
== Some("String.len")
&& expr_contains_add(&args[0]));
here || law_expr_children_any(e, law_expr_has_string_len_over_concat)
}
fn expr_contains_add(e: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::{BinOp, Expr};
matches!(&e.node, Expr::BinOp(BinOp::Add, _, _)) || law_expr_children_any(e, expr_contains_add)
}
fn law_expr_children_any(
e: &crate::ast::Spanned<crate::ast::Expr>,
f: fn(&crate::ast::Spanned<crate::ast::Expr>) -> bool,
) -> bool {
use crate::ast::Expr;
match &e.node {
Expr::Attr(o, _) => f(o),
Expr::FnCall(c, args) => f(c) || args.iter().any(f),
Expr::BinOp(_, l, r) => f(l) || f(r),
Expr::Neg(i) | Expr::ErrorProp(i) => f(i),
Expr::Match { subject, arms } => f(subject) || arms.iter().any(|a| f(&a.body)),
Expr::Constructor(_, Some(inner)) => f(inner),
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => xs.iter().any(f),
Expr::MapLiteral(kvs) => kvs.iter().any(|(k, v)| f(k) || f(v)),
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| f(v)),
Expr::RecordUpdate { base, updates, .. } => f(base) || updates.iter().any(|(_, v)| f(v)),
Expr::TailCall(d) => d.args.iter().any(f),
_ => false,
}
}
fn emit_string_append_monoid_law(
law: &VerifyLaw,
proof_intro_names: &[String],
) -> Option<AutoProof> {
if law.when.is_some() {
return None;
}
let fires = law_expr_has_empty_string_concat(&law.lhs)
|| law_expr_has_empty_string_concat(&law.rhs)
|| is_concat_reassociation(&law.lhs, &law.rhs);
if !fires {
return None;
}
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec![
"simp only [String.add_eq_append, String.append_empty, \
String.empty_append, String.append_assoc]"
.to_string(),
],
),
replaces_theorem: false,
})
}
fn law_expr_has_empty_string_concat(e: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::{BinOp, Expr, Literal};
let is_empty_str = |s: &crate::ast::Spanned<Expr>| matches!(&s.node, Expr::Literal(Literal::Str(t)) if t.is_empty());
let here =
matches!(&e.node, Expr::BinOp(BinOp::Add, l, r) if is_empty_str(l) || is_empty_str(r));
here || law_expr_children_any(e, law_expr_has_empty_string_concat)
}
fn is_concat_reassociation(
lhs: &crate::ast::Spanned<crate::ast::Expr>,
rhs: &crate::ast::Spanned<crate::ast::Expr>,
) -> bool {
use crate::ast::{BinOp, Expr};
let left_nested = matches!(&lhs.node, Expr::BinOp(BinOp::Add, l, _) if matches!(l.node, Expr::BinOp(BinOp::Add, _, _)));
let right_nested = matches!(&rhs.node, Expr::BinOp(BinOp::Add, _, r) if matches!(r.node, Expr::BinOp(BinOp::Add, _, _)));
left_nested && right_nested
}
fn emit_int_abs_identity_law(law: &VerifyLaw, proof_intro_names: &[String]) -> Option<AutoProof> {
if law.when.is_some() || !(law_expr_calls_int_abs(&law.lhs) || law_expr_calls_int_abs(&law.rhs))
{
return None;
}
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec![
"simp only [Int.natAbs_natCast, Int.natAbs_mul, Int.natCast_mul] <;> omega"
.to_string(),
"simp [Int.natAbs_natCast, Int.natAbs_mul, Int.natCast_mul]".to_string(),
],
),
replaces_theorem: false,
})
}
fn law_expr_calls_int_abs(e: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::Expr;
let here = matches!(&e.node, Expr::FnCall(callee, _)
if crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref() == Some("Int.abs"));
here || law_expr_children_any(e, law_expr_calls_int_abs)
}
fn emit_map_empty_fact_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
proof_intro_names: &[String],
) -> Option<AutoProof> {
if law.when.is_some() || !subject_fn_accesses_empty_map(vb, ctx) {
return None;
}
let mut simp_set: Vec<String> = shared::law_simp_defs(ctx, vb, law).into_iter().collect();
simp_set.extend(
[
"AverMap.get",
"AverMap.has",
"AverMap.len",
"List.any_nil",
"List.length_nil",
"Int.ofNat_zero",
]
.iter()
.map(|s| s.to_string()),
);
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec![format!("simp only [{}] ; done", simp_set.join(", "))],
),
replaces_theorem: false,
})
}
fn subject_fn_accesses_empty_map(vb: &VerifyBlock, ctx: &CodegenContext) -> bool {
let Some(fd) = ctx.fn_def_by_name(&vb.fn_name, None) else {
return false;
};
fd.body.stmts().iter().any(|stmt| {
let e = match stmt {
crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => e,
};
expr_accesses_empty_map(e, ctx)
})
}
fn expr_accesses_empty_map(
e: &crate::ast::Spanned<crate::ast::Expr>,
ctx: &CodegenContext,
) -> bool {
use crate::ast::Expr;
if let Expr::FnCall(callee, args) = &e.node
&& !args.is_empty()
&& matches!(
crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref(),
Some("Map.get") | Some("Map.has") | Some("Map.len")
)
&& expr_is_empty_map(&args[0], ctx)
{
return true;
}
match &e.node {
Expr::Attr(o, _) => expr_accesses_empty_map(o, ctx),
Expr::FnCall(c, args) => {
expr_accesses_empty_map(c, ctx) || args.iter().any(|a| expr_accesses_empty_map(a, ctx))
}
Expr::BinOp(_, l, r) => expr_accesses_empty_map(l, ctx) || expr_accesses_empty_map(r, ctx),
Expr::Neg(i) | Expr::ErrorProp(i) => expr_accesses_empty_map(i, ctx),
Expr::Match { subject, arms } => {
expr_accesses_empty_map(subject, ctx)
|| arms.iter().any(|a| expr_accesses_empty_map(&a.body, ctx))
}
Expr::Constructor(_, Some(inner)) => expr_accesses_empty_map(inner, ctx),
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
xs.iter().any(|x| expr_accesses_empty_map(x, ctx))
}
_ => false,
}
}
fn expr_is_empty_map(e: &crate::ast::Spanned<crate::ast::Expr>, ctx: &CodegenContext) -> bool {
use crate::ast::{Expr, Stmt};
match &e.node {
Expr::MapLiteral(entries) => entries.is_empty(),
Expr::FnCall(callee, args) if args.is_empty() => {
crate::codegen::common::expr_to_dotted_name(&callee.node)
.and_then(|n| ctx.fn_def_by_name(&n, None))
.is_some_and(|fd| {
let stmts = fd.body.stmts();
stmts.len() == 1
&& matches!(&stmts[0],
Stmt::Expr(b) if matches!(&b.node, Expr::MapLiteral(es) if es.is_empty()))
})
}
_ => false,
}
}
fn emit_map_len_set_positive_law(
law: &VerifyLaw,
proof_intro_names: &[String],
) -> Option<AutoProof> {
if law.when.is_some() || !law_is_map_len_set_ge_one(law) {
return None;
}
Some(AutoProof {
support_lines: Vec::new(),
body: intro_then_first(
proof_intro_names,
vec!["exact AverMap.len_set_ge_one _ _ _".to_string()],
),
replaces_theorem: false,
})
}
fn law_is_map_len_set_ge_one(law: &VerifyLaw) -> bool {
use crate::ast::{BinOp, Expr, Literal};
let Expr::BinOp(BinOp::Gte, left, right) = &law.lhs.node else {
return false;
};
let is_one = matches!(&right.node, Expr::Literal(Literal::Int(1)));
let len_over_set = matches!(&left.node, Expr::FnCall(callee, args)
if crate::codegen::common::expr_to_dotted_name(&callee.node).as_deref() == Some("Map.len")
&& args.len() == 1
&& matches!(&args[0].node, Expr::FnCall(set_callee, _)
if crate::codegen::common::expr_to_dotted_name(&set_callee.node).as_deref()
== Some("Map.set")));
is_one && len_over_set
}
const COMPARISON_NORMALIZERS: &[&str] = &[
"Bool.beq_comm",
"Bool.or_eq_true",
"Bool.and_eq_true",
"decide_eq_decide",
"decide_eq_true_eq",
"← decide_not",
"ge_iff_le",
"gt_iff_lt",
];
#[allow(clippy::too_many_arguments)]
fn emit_simp_omega_from_ir(
unfold_fns: &[String],
wrapper_return: bool,
smart_guard: Option<&crate::ir::SmartGuard>,
lifted: bool,
intro_names: &[String],
given_names: &[String],
has_when: bool,
uses_max_min: bool,
ctx: &CodegenContext,
) -> super::tactic_ir::Tactic {
use super::tactic_ir::Tactic;
let lean_names: Vec<String> = unfold_fns.iter().map(|n| aver_name_to_lean(n)).collect();
if lifted && wrapper_return {
Tactic::raw(intro_then(
intro_names,
vec![
format!("unfold {}", lean_names.join(" ")),
"simp [Int.add_comm, Int.mul_comm]".to_string(),
],
))
} else if wrapper_return {
let by_cases_clauses: Vec<String> = given_names
.iter()
.map(|n| {
let predicate = match smart_guard {
Some(g) => {
let substituted = crate::codegen::common::substitute_ident_in_resolved_expr(
&g.predicate,
&g.param,
n,
);
super::expr::emit_expr(&substituted, ctx)
}
None => format!("{n} ≥ 0"),
};
format!("by_cases h_{n} : {predicate}")
})
.collect();
let by_cases_chain = by_cases_clauses.join(" <;> ");
let simp_hyps: Vec<String> = given_names
.iter()
.map(|n| format!("h_{n}"))
.chain(["Int.add_comm".to_string(), "Int.mul_comm".to_string()])
.collect();
let simp_args = simp_hyps.join(", ");
let cmp = format!(
"simp only [{}] <;> omega",
COMPARISON_NORMALIZERS.join(", ")
);
intro_prefix_then_first(
intro_names,
vec![format!("unfold {}", lean_names.join(" "))],
vec![format!("{by_cases_chain} <;> simp [{simp_args}]"), cmp],
)
} else {
if uses_max_min {
let lemmas: Vec<String> = lean_names
.iter()
.cloned()
.chain(["Int.max_def".to_string(), "Int.min_def".to_string()])
.collect();
let l = lemmas.join(", ");
intro_then_first(
intro_names,
vec![
format!("simp only [{l}] <;> omega"),
format!("simp only [{l}] <;> (try split) <;> simp_all <;> omega"),
],
)
} else if has_when {
Tactic::raw(intro_then(
intro_names,
vec![format!("simp_all [{}] <;> omega", lean_names.join(", "))],
))
} else {
Tactic::raw(intro_then(
intro_names,
vec![format!("simp only [{}] <;> omega", lean_names.join(", "))],
))
}
}
}
fn linear_arith_uses_max_min(unfold_fns: &[String], ctx: &CodegenContext) -> bool {
unfold_fns.iter().any(|name| {
let fd = ctx.fn_def_by_name(name, None).or_else(|| {
ctx.modules
.iter()
.find_map(|m| ctx.fn_def_by_name(name, Some(&m.prefix)))
});
fd.is_some_and(|fd| fn_body_calls_max_min(&fd.body))
})
}
fn fn_body_calls_max_min(body: &crate::ast::FnBody) -> bool {
body.stmts().iter().any(|stmt| match stmt {
crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => expr_calls_max_min(e),
})
}
fn expr_calls_max_min(expr: &crate::ast::Spanned<crate::ast::Expr>) -> bool {
use crate::ast::Expr;
match &expr.node {
Expr::FnCall(f, args) => {
let is_max_min = crate::codegen::common::expr_to_dotted_name(&f.node)
.is_some_and(|n| n == "Int.max" || n == "Int.min");
is_max_min || args.iter().any(expr_calls_max_min)
}
Expr::BinOp(_, l, r) => expr_calls_max_min(l) || expr_calls_max_min(r),
Expr::Neg(inner) | Expr::Attr(inner, _) | Expr::ErrorProp(inner) => {
expr_calls_max_min(inner)
}
Expr::Match { subject, arms } => {
expr_calls_max_min(subject) || arms.iter().any(|arm| expr_calls_max_min(&arm.body))
}
Expr::Constructor(_, Some(inner)) => expr_calls_max_min(inner),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(expr_calls_max_min)
}
Expr::MapLiteral(entries) => entries
.iter()
.any(|(k, v)| expr_calls_max_min(k) || expr_calls_max_min(v)),
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_calls_max_min(e)),
Expr::RecordUpdate { base, updates, .. } => {
expr_calls_max_min(base) || updates.iter().any(|(_, e)| expr_calls_max_min(e))
}
Expr::TailCall(boxed) => boxed.args.iter().any(expr_calls_max_min),
_ => false,
}
}
pub fn emit_verify_law_support_theorems(
vb: &VerifyBlock,
_law: &VerifyLaw,
ctx: &CodegenContext,
_theorem_base: &str,
) -> Vec<String> {
collect_missing_helper_law_hints(&ctx.items, ctx)
.into_iter()
.find(|hint| hint.line == vb.line && hint.fn_name == vb.fn_name)
.map(|hint| {
vec![
format!("-- hint: {}", missing_helper_law_message(&hint)),
"-- hint: the main theorem can stay generic, but it still needs those helper laws as intermediate theorems".to_string(),
]
})
.unwrap_or_default()
}
pub(super) fn intro_then(intro_names: &[String], steps: Vec<String>) -> Vec<String> {
let mut lines = Vec::new();
if !intro_names.is_empty() {
lines.push(format!("intro {}", intro_names.join(" ")));
}
lines.extend(steps);
indent_lines(lines, 2)
}
fn intro_then_first(intro_names: &[String], branches: Vec<String>) -> super::tactic_ir::Tactic {
intro_prefix_then_first(intro_names, Vec::new(), branches)
}
fn intro_prefix_then_first(
intro_names: &[String],
prefix: Vec<String>,
branches: Vec<String>,
) -> super::tactic_ir::Tactic {
use super::tactic_ir::Tactic;
let mut steps = Vec::new();
if !intro_names.is_empty() {
steps.push(Tactic::Leaf(format!("intro {}", intro_names.join(" "))));
}
steps.extend(prefix.into_iter().map(Tactic::Leaf));
let mut alts: Vec<Tactic> = branches.into_iter().map(Tactic::Leaf).collect();
alts.push(Tactic::Sorry);
steps.push(Tactic::First(alts));
Tactic::Seq(steps)
}
pub(super) fn support_theorem(header: &str, body: super::tactic_ir::Tactic) -> String {
let mut out = String::from(header);
for line in body.render_body() {
out.push('\n');
out.push_str(&line);
}
out
}
fn extend_intro_names_with_premises(law: &VerifyLaw, intro_names: &[String]) -> Vec<String> {
let mut names = intro_names.to_vec();
if law.when.is_some() {
names.extend(intro_names.iter().map(|name| format!("h_{name}")));
names.push("h_when".to_string());
}
names
}
pub(super) fn indent_lines(lines: Vec<String>, spaces: usize) -> Vec<String> {
let pad = " ".repeat(spaces);
lines
.into_iter()
.map(|line| format!("{pad}{line}"))
.collect()
}