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 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 struct AutoProof {
pub support_lines: Vec<String>,
pub proof_lines: Vec<String>,
pub replaces_theorem: bool,
}
fn law_strategy_for(
ctx: &CodegenContext,
fn_name: &str,
law_name: &str,
) -> Option<crate::ir::ProofStrategy> {
let fn_id = ctx
.symbol_table
.fn_id_of(&crate::ir::FnKey::entry(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> {
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);
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(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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: 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::MatchDispatcherFold { fold_fn, spec_fn }) =
law_strategy_for(ctx, &vb.fn_name, &law.name)
&& let Some(proof) = spec::emit_match_dispatcher_fold_law(vb, law, ctx, &fold_fn, &spec_fn)
{
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,
}) = 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,
)
{
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(),
proof_lines: 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(),
proof_lines: 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(),
proof_lines: intro_then(&proof_intro_names, lines),
replaces_theorem: false,
});
}
None
})
.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(),
proof_lines: 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(),
proof_lines,
replaces_theorem: false,
})
})
.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)
})
}
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 simp_set: Vec<String> = unfold_fns
.iter()
.map(|f| aver_name_to_lean(f))
.chain(RING_NORMALIZATION_LEMMAS.iter().map(|s| s.to_string()))
.collect();
Some(AutoProof {
support_lines: Vec::new(),
proof_lines: intro_then(
proof_intro_names,
vec![format!(
"first | (simp [{}]; done) | sorry",
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(),
proof_lines: intro_then(
proof_intro_names,
vec![format!(
"first | (simp [{}]; done) | sorry",
simp_set.join(", ")
)],
),
replaces_theorem: false,
})
}
#[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,
) -> Vec<String> {
let lean_names: Vec<String> = unfold_fns.iter().map(|n| aver_name_to_lean(n)).collect();
if lifted && wrapper_return {
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(", ");
intro_then(
intro_names,
vec![
format!("unfold {}", lean_names.join(" ")),
format!("{by_cases_chain} <;> simp [{simp_args}]"),
],
)
} else {
let tac = if uses_max_min {
let lemmas: Vec<String> = lean_names
.iter()
.cloned()
.chain(["Int.max_def".to_string(), "Int.min_def".to_string()])
.collect();
format!(
"simp only [{}] <;> (try split) <;> simp_all <;> omega",
lemmas.join(", ")
)
} else if has_when {
format!("simp_all [{}] <;> omega", lean_names.join(", "))
} else {
format!("simp only [{}] <;> omega", lean_names.join(", "))
};
intro_then(intro_names, vec![tac])
}
}
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 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()
}