use super::*;
struct CountedRepeat {
fn_name: String,
params: Vec<(String, String)>,
count_idx: usize,
elem_lean: String,
}
fn cr_ident_name(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<String> {
match &e.node {
crate::ast::Expr::Ident(n) => Some(n.clone()),
crate::ast::Expr::Resolved { name, .. } => Some(name.clone()),
_ => None,
}
}
fn cr_is_int_lit(e: &crate::ast::Spanned<crate::ast::Expr>, n: i64) -> bool {
matches!(&e.node, crate::ast::Expr::Literal(crate::ast::Literal::Int(k)) if *k == n)
}
fn cr_fn_call(
e: &crate::ast::Spanned<crate::ast::Expr>,
) -> Option<(String, &[crate::ast::Spanned<crate::ast::Expr>])> {
if let crate::ast::Expr::FnCall(callee, args) = &e.node {
let name = crate::codegen::common::expr_to_dotted_name(&callee.node)?;
Some((name, args.as_slice()))
} else {
None
}
}
fn counted_repeat_elem_lean(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<String> {
use crate::ast::{Expr, Literal};
match &e.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
Expr::Literal(Literal::Int(k)) => Some(k.to_string()),
_ => None,
}
}
fn detect_counted_repeat(fd: &crate::ast::FnDef) -> Option<CountedRepeat> {
use crate::ast::{BinOp, Expr, Literal, Pattern, Stmt};
if fd.params.is_empty() {
return None;
}
let Some(Stmt::Expr(term)) = fd.body.stmts().last() else {
return None;
};
let Expr::Match { subject, arms } = &term.node else {
return None;
};
let Expr::BinOp(BinOp::Lte, l, r) = &subject.node else {
return None;
};
if !cr_is_int_lit(r, 0) {
return None;
}
let count_name = cr_ident_name(l)?;
let count_idx = fd
.params
.iter()
.position(|(n, t)| *n == count_name && t == "Int")?;
if arms.len() != 2 {
return None;
}
let mut true_body = None;
let mut false_body = None;
for arm in arms {
match &arm.pattern {
Pattern::Literal(Literal::Bool(true)) => true_body = Some(&arm.body),
Pattern::Literal(Literal::Bool(false)) => false_body = Some(&arm.body),
_ => return None,
}
}
if !matches!(&true_body?.node, Expr::List(items) if items.is_empty()) {
return None;
}
let (concat, cargs) = cr_fn_call(false_body?)?;
if concat != "List.concat" || cargs.len() != 2 {
return None;
}
let (rec, rargs) = cr_fn_call(&cargs[0])?;
if rec != fd.name || rargs.len() != fd.params.len() {
return None;
}
for (j, ra) in rargs.iter().enumerate() {
if j == count_idx {
let Expr::BinOp(BinOp::Sub, sl, sr) = &ra.node else {
return None;
};
if cr_ident_name(sl).as_deref() != Some(count_name.as_str()) || !cr_is_int_lit(sr, 1) {
return None;
}
} else if cr_ident_name(ra).as_deref() != Some(fd.params[j].0.as_str()) {
return None;
}
}
let Expr::List(tail) = &cargs[1].node else {
return None;
};
if tail.len() != 1 {
return None;
}
let elem_lean = counted_repeat_elem_lean(&tail[0])?;
Some(CountedRepeat {
fn_name: fd.name.clone(),
params: fd.params.clone(),
count_idx,
elem_lean,
})
}
fn counted_repeat_lemmas(shape: &CountedRepeat, base: &str) -> Vec<(String, String)> {
let f_l = crate::codegen::lean::aver_name_to_lean(&shape.fn_name);
let fuel_l = crate::codegen::lean::aver_name_to_lean(
&crate::codegen::recursion::fuel_helper_name(&shape.fn_name),
);
let count_name = &shape.params[shape.count_idx].0;
let binders: Vec<String> = shape
.params
.iter()
.map(|(p, t)| {
format!(
"({p} : {})",
crate::codegen::lean::type_to_lean(&crate::codegen::common::parse_type_annotation(
t
))
)
})
.collect();
let lhs_args: Vec<String> = shape
.params
.iter()
.enumerate()
.map(|(i, (p, _))| {
if i == shape.count_idx {
format!("({count_name} + 1)")
} else {
p.clone()
}
})
.collect();
let rhs_args: Vec<String> = shape.params.iter().map(|(p, _)| p.clone()).collect();
let elem = &shape.elem_lean;
let natabs = format!("{base}_natAbs_succ");
let succ = format!("{base}_repeat_succ");
let natabs_thm = format!(
"theorem {natabs} (n : Int) (hn : 0 <= n) : Int.natAbs (n + 1) = Int.natAbs n + 1 := by\n \
apply Int.ofNat_inj.mp\n \
change (Int.natAbs (n + 1) : Int) = (Int.natAbs n : Int) + 1\n \
rw [Int.natAbs_of_nonneg (by omega), Int.natAbs_of_nonneg hn]\n"
);
let succ_thm = format!(
"theorem {succ} {binders} (hn : 0 <= {count_name}) : \
{f_l} {lhs_args} = {f_l} {rhs_args} ++ [{elem}] := by\n \
unfold {f_l}\n \
rw [{natabs} {count_name} hn]\n \
have hpos : ¬ {count_name} + 1 <= 0 := by omega\n \
simp [{fuel_l}, hpos]\n",
binders = binders.join(" "),
lhs_args = lhs_args.join(" "),
rhs_args = rhs_args.join(" "),
);
vec![(natabs, natabs_thm), (succ, succ_thm)]
}
enum FieldInvariant {
Nonneg,
Bounded { lo: i64, hi: i64 },
}
fn cr_attr_of(e: &crate::ast::Spanned<crate::ast::Expr>, acc: &str) -> Option<String> {
if let crate::ast::Expr::Attr(obj, field) = &e.node
&& cr_ident_name(obj).as_deref() == Some(acc)
{
return Some(field.clone());
}
None
}
fn collect_acc_records<'a>(
e: &'a crate::ast::Spanned<crate::ast::Expr>,
acc_type: &str,
out: &mut Vec<&'a [(String, crate::ast::Spanned<crate::ast::Expr>)]>,
) {
use crate::ast::Expr;
match &e.node {
Expr::RecordCreate { type_name, fields } if type_name == acc_type => {
out.push(fields.as_slice());
}
Expr::Match { subject, arms } => {
collect_acc_records(subject, acc_type, out);
for arm in arms {
collect_acc_records(&arm.body, acc_type, out);
}
}
Expr::BinOp(_, l, r) => {
collect_acc_records(l, acc_type, out);
collect_acc_records(r, acc_type, out);
}
Expr::FnCall(_, args) => {
for a in args {
collect_acc_records(a, acc_type, out);
}
}
Expr::Attr(obj, _) => collect_acc_records(obj, acc_type, out),
Expr::List(items) => {
for i in items {
collect_acc_records(i, acc_type, out);
}
}
_ => {}
}
}
fn nonneg_preserving(v: &crate::ast::Spanned<crate::ast::Expr>, acc: &str, field: &str) -> bool {
use crate::ast::{BinOp, Expr, Literal};
if let Expr::Literal(Literal::Int(k)) = &v.node {
return *k >= 0;
}
if cr_attr_of(v, acc).as_deref() == Some(field) {
return true;
}
if let Expr::BinOp(BinOp::Add | BinOp::Mul, l, r) = &v.node {
let is_field = |e: &crate::ast::Spanned<Expr>| cr_attr_of(e, acc).as_deref() == Some(field);
let nonneg_lit = |e: &crate::ast::Spanned<Expr>| matches!(&e.node, Expr::Literal(Literal::Int(k)) if *k >= 0);
return (is_field(l) && nonneg_lit(r)) || (nonneg_lit(l) && is_field(r));
}
false
}
fn detect_field_invariants(
fd: &crate::ast::FnDef,
inputs: &ProofLowerInputs,
) -> Vec<(String, FieldInvariant)> {
use crate::ast::{Stmt, TypeDef};
if fd.params.is_empty() || fd.params[0].1 != fd.return_type {
return Vec::new();
}
let acc_param = fd.params[0].0.as_str();
let acc_type = &fd.params[0].1;
let Some(TypeDef::Product {
fields: acc_fields, ..
}) = inputs.find_type_def(acc_type)
else {
return Vec::new();
};
let int_fields: Vec<&str> = acc_fields
.iter()
.filter(|(_, ty)| ty == "Int")
.map(|(n, _)| n.as_str())
.collect();
let Some(Stmt::Expr(term)) = fd.body.stmts().last() else {
return Vec::new();
};
let mut records = Vec::new();
collect_acc_records(term, acc_type, &mut records);
if records.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for f in int_fields {
let values: Vec<&crate::ast::Spanned<crate::ast::Expr>> = records
.iter()
.filter_map(|rec| rec.iter().find(|(name, _)| name == f).map(|(_, v)| v))
.collect();
if values.len() != records.len() {
continue;
}
if values.iter().all(|v| nonneg_preserving(v, acc_param, f)) {
out.push((f.to_string(), FieldInvariant::Nonneg));
continue;
}
let deltas: Option<Vec<i64>> = values
.iter()
.map(|v| relative_delta(v, acc_param, f))
.collect();
if let Some(deltas) = deltas
&& !deltas.is_empty()
{
out.push((
f.to_string(),
FieldInvariant::Bounded {
lo: deltas.iter().copied().min().unwrap(),
hi: deltas.iter().copied().max().unwrap(),
},
));
}
}
out
}
fn lemma_binders_args(params: &[(String, String)]) -> (String, String) {
let mut binders = Vec::new();
let mut args = Vec::new();
for (pname, ptype) in params {
let ty_l = crate::codegen::lean::type_to_lean(
&crate::codegen::common::parse_type_annotation(ptype),
);
binders.push(format!("({pname} : {ty_l})"));
args.push(pname.clone());
}
(binders.join(" "), args.join(" "))
}
fn nonneg_lemma(
fn_name: &str,
params: &[(String, String)],
field: &str,
base: &str,
) -> (String, String) {
let step_l = crate::codegen::lean::aver_name_to_lean(fn_name);
let (binders, args) = lemma_binders_args(params);
let acc = ¶ms[0].0;
let name = format!("{base}_{field}_nonneg");
let text = format!(
"theorem {name} {binders} (hcount : 0 <= {acc}.{field}) : \
0 <= ({step_l} {args}).{field} := by\n \
unfold {step_l}\n \
split <;> (try split) <;> simp_all <;> omega\n",
);
(name, text)
}
fn relative_delta(
v: &crate::ast::Spanned<crate::ast::Expr>,
acc: &str,
field: &str,
) -> Option<i64> {
use crate::ast::{BinOp, Expr, Literal};
if cr_attr_of(v, acc).as_deref() == Some(field) {
return Some(0);
}
let Expr::BinOp(op, l, r) = &v.node else {
return None;
};
let is_field = |e: &crate::ast::Spanned<Expr>| cr_attr_of(e, acc).as_deref() == Some(field);
let int_lit = |e: &crate::ast::Spanned<Expr>| match &e.node {
Expr::Literal(Literal::Int(k)) => Some(*k),
_ => None,
};
match op {
BinOp::Add if is_field(l) => int_lit(r),
BinOp::Add if is_field(r) => int_lit(l),
BinOp::Sub if is_field(l) => int_lit(r).map(|k| -k),
_ => None,
}
}
fn render_acc_offset(acc: &str, field: &str, k: i64) -> String {
match k.cmp(&0) {
std::cmp::Ordering::Equal => format!("{acc}.{field}"),
std::cmp::Ordering::Greater => format!("{acc}.{field} + {k}"),
std::cmp::Ordering::Less => format!("{acc}.{field} - {}", -k),
}
}
fn bounded_lemma(
fn_name: &str,
params: &[(String, String)],
field: &str,
lo: i64,
hi: i64,
base: &str,
) -> (String, String) {
let step_l = crate::codegen::lean::aver_name_to_lean(fn_name);
let (binders, args) = lemma_binders_args(params);
let acc = ¶ms[0].0;
let lo_s = render_acc_offset(acc, field, lo);
let hi_s = render_acc_offset(acc, field, hi);
let name = format!("{base}_{field}_bounds");
let text = format!(
"theorem {name} {binders} : \
{lo_s} <= ({step_l} {args}).{field} ∧ ({step_l} {args}).{field} <= {hi_s} := by\n \
unfold {step_l}\n \
split <;> (try split) <;> simp_all <;> omega\n",
);
(name, text)
}
pub fn structural_lemma_groups(inputs: &ProofLowerInputs) -> Vec<Vec<(String, String)>> {
let mut next_base = 0usize;
let mut next = |kind: &str| {
let b = format!("aver_{kind}_{next_base}");
next_base += 1;
b
};
let mut relational: Vec<Vec<(String, String)>> = Vec::new();
let mut covered_counted: HashSet<String> = HashSet::new();
let mut covered_nonneg: HashSet<(String, String)> = HashSet::new();
for enc in detect_encoders(inputs) {
let base = next("relational");
if let Some((group, cov)) = relational_lemma_group(&enc, inputs, &base) {
covered_counted.insert(cov.counted_fn);
covered_nonneg.insert((cov.step_fn, cov.field));
relational.push(group);
}
}
for shape in detect_monoidal_specs(inputs) {
let base = next("monoidal");
relational.push(monoidal_spec_group(&shape, &base));
}
let mut groups: Vec<Vec<(String, String)>> = Vec::new();
for fd in inputs.pure_fns() {
if let Some(shape) = detect_counted_repeat(fd) {
if !covered_counted.contains(&fd.name) {
let base = next("structural");
groups.push(counted_repeat_lemmas(&shape, &base));
}
continue;
}
for (field, inv) in detect_field_invariants(fd, inputs) {
if matches!(inv, FieldInvariant::Nonneg)
&& covered_nonneg.contains(&(fd.name.clone(), field.clone()))
{
continue;
}
let base = next("structural");
let lemma = match inv {
FieldInvariant::Nonneg => nonneg_lemma(&fd.name, &fd.params, &field, &base),
FieldInvariant::Bounded { lo, hi } => {
bounded_lemma(&fd.name, &fd.params, &field, lo, hi, &base)
}
};
groups.push(vec![lemma]);
}
}
groups.extend(relational);
groups
}
fn as_call(
e: &crate::ast::Spanned<crate::ast::Expr>,
) -> Option<(String, &[crate::ast::Spanned<crate::ast::Expr>])> {
match &e.node {
crate::ast::Expr::FnCall(..) => cr_fn_call(e),
crate::ast::Expr::TailCall(tc) => Some((tc.target.clone(), tc.args.as_slice())),
_ => None,
}
}
struct ShapeFoldRoles {
loop_fn: String,
step_fn: Option<String>,
step_op: Option<crate::ast::BinOp>,
finish_fn: Option<String>,
}
fn shape_fold_roles(inputs: &ProofLowerInputs, wrapper: &str) -> Option<ShapeFoldRoles> {
use crate::analysis::shape::ModulePattern;
inputs.program_shape?.patterns.iter().find_map(|p| match p {
ModulePattern::AccumulatorFold {
wrapper_fn,
loop_fn,
step_fn,
step_op,
finish_fn,
..
} if wrapper_fn == wrapper => Some(ShapeFoldRoles {
loop_fn: loop_fn.clone(),
step_fn: step_fn.clone(),
step_op: *step_op,
finish_fn: finish_fn.clone(),
}),
_ => None,
})
}
fn last_body_expr(fd: &crate::ast::FnDef) -> Option<&crate::ast::Spanned<crate::ast::Expr>> {
match fd.body.stmts().last() {
Some(crate::ast::Stmt::Expr(e)) => Some(e),
_ => None,
}
}
#[derive(Debug, Clone)]
pub(super) struct EncoderShape {
pub(super) wrapper: String,
pub(super) inverse: String,
pub(super) loop_fn: String,
pub(super) finish: String,
pub(super) step: String,
pub(super) expand: String,
#[allow(dead_code)]
pub(super) var: String,
init_acc: String,
}
fn detect_encoder(
law: &crate::ast::VerifyLaw,
subject_fn: &str,
inputs: &ProofLowerInputs,
) -> Option<EncoderShape> {
use crate::ast::{Expr, Pattern};
let var = cr_ident_name(&law.rhs)?;
if !law.givens.iter().any(|g| g.name == var) {
return None;
}
let (inverse, inv_args) = as_call(&law.lhs)?;
if inv_args.len() != 1 {
return None;
}
let (wrapper, wrap_args) = as_call(&inv_args[0])?;
if wrapper != subject_fn
|| wrap_args.len() != 1
|| cr_ident_name(&wrap_args[0]).as_deref() != Some(var.as_str())
{
return None;
}
let roles = shape_fold_roles(inputs, &wrapper)?;
let loop_fn = roles.loop_fn;
let (Some(step), Some(finish)) = (roles.step_fn, roles.finish_fn) else {
return None;
};
let wf = inputs.find_fn_def_by_call_name(&wrapper)?;
let (wbody_loop, loop_args) = as_call(last_body_expr(wf)?)?;
if wbody_loop != loop_fn || loop_args.len() != 2 {
return None;
}
let init_acc = render_init_literal(&loop_args[1])?;
let invf = inputs.find_fn_def_by_call_name(&inverse)?;
if invf.params.len() != 1 {
return None;
}
let inv_list_p = invf.params[0].0.as_str();
let Expr::Match {
subject: isubj,
arms: iarms,
} = &last_body_expr(invf)?.node
else {
return None;
};
if cr_ident_name(isubj).as_deref() != Some(inv_list_p) || iarms.len() != 2 {
return None;
}
let mut expand: Option<String> = None;
for arm in iarms {
match &arm.pattern {
Pattern::EmptyList => {
if !matches!(&arm.body.node, Expr::List(items) if items.is_empty()) {
return None;
}
}
Pattern::Cons(h, t) => {
let (concat, cargs) = as_call(&arm.body)?;
if concat != "List.concat" || cargs.len() != 2 {
return None;
}
let (e, eargs) = as_call(&cargs[0])?;
if eargs.len() != 1 || cr_ident_name(&eargs[0]).as_deref() != Some(h.as_str()) {
return None;
}
let (inv2, iargs) = as_call(&cargs[1])?;
if inv2 != inverse
|| iargs.len() != 1
|| cr_ident_name(&iargs[0]).as_deref() != Some(t.as_str())
{
return None;
}
expand = Some(e);
}
_ => return None,
}
}
Some(EncoderShape {
wrapper,
inverse,
loop_fn,
finish,
step,
expand: expand?,
var,
init_acc,
})
}
pub(super) fn detect_encoders(inputs: &ProofLowerInputs) -> Vec<EncoderShape> {
use crate::ast::{TopLevel, VerifyKind};
let mut out = Vec::new();
for item in inputs.entry_items {
if let TopLevel::Verify(vb) = item
&& let VerifyKind::Law(law) = &vb.kind
&& let Some(enc) = detect_encoder(law, &vb.fn_name, inputs)
{
out.push(enc);
}
}
out
}
fn render_init_literal(e: &crate::ast::Spanned<crate::ast::Expr>) -> Option<String> {
use crate::ast::{Expr, Literal};
match &e.node {
Expr::List(items) => {
let parts: Option<Vec<String>> = items.iter().map(render_init_literal).collect();
Some(format!("[{}]", parts?.join(", ")))
}
Expr::Literal(Literal::Int(k)) => Some(k.to_string()),
Expr::Literal(Literal::Bool(b)) => Some(if *b { "true" } else { "false" }.to_string()),
Expr::Literal(Literal::Str(s)) => Some(format!(
"\"{}\"",
s.replace('\\', "\\\\").replace('"', "\\\"")
)),
Expr::Literal(Literal::Unit) => Some("()".to_string()),
Expr::RecordCreate { fields, .. } => {
let parts: Option<Vec<String>> = fields
.iter()
.map(|(name, v)| Some(format!("{name} := {}", render_init_literal(v)?)))
.collect();
Some(format!("{{ {} }}", parts?.join(", ")))
}
_ => None,
}
}
fn collect_callees(e: &crate::ast::Spanned<crate::ast::Expr>, out: &mut BTreeSet<String>) {
use crate::ast::Expr;
match &e.node {
Expr::FnCall(callee, args) => {
if let Some(name) = crate::codegen::common::expr_to_dotted_name(&callee.node) {
out.insert(name);
}
for a in args {
collect_callees(a, out);
}
}
Expr::TailCall(tc) => {
out.insert(tc.target.clone());
for a in &tc.args {
collect_callees(a, out);
}
}
Expr::Match { subject, arms } => {
collect_callees(subject, out);
for arm in arms {
collect_callees(&arm.body, out);
}
}
Expr::BinOp(_, l, r) => {
collect_callees(l, out);
collect_callees(r, out);
}
Expr::Neg(x) | Expr::Attr(x, _) | Expr::ErrorProp(x) => collect_callees(x, out),
Expr::List(items) | Expr::Tuple(items) => {
for i in items {
collect_callees(i, out);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, v) in fields {
collect_callees(v, out);
}
}
_ => {}
}
}
fn counted_one_lemma(shape: &CountedRepeat, base: &str) -> (String, String) {
let f_l = crate::codegen::lean::aver_name_to_lean(&shape.fn_name);
let fuel_l = crate::codegen::lean::aver_name_to_lean(
&crate::codegen::recursion::fuel_helper_name(&shape.fn_name),
);
let binders: Vec<String> = shape
.params
.iter()
.enumerate()
.filter(|(i, _)| *i != shape.count_idx)
.map(|(_, (p, t))| {
format!(
"({p} : {})",
crate::codegen::lean::type_to_lean(&crate::codegen::common::parse_type_annotation(
t
))
)
})
.collect();
let args: Vec<String> = shape
.params
.iter()
.enumerate()
.map(|(i, (p, _))| {
if i == shape.count_idx {
"1".to_string()
} else {
p.clone()
}
})
.collect();
let name = format!("{base}_counted_one");
let binders_s = binders.join(" ");
let text = format!(
"theorem {name} {binders_s} : {f_l} {args} = [{elem}] := by\n simp [{f_l}, {fuel_l}]\n",
args = args.join(" "),
elem = shape.elem_lean,
);
(name, text)
}
fn inv_append_lemma(inverse: &str, elem_ty_lean: &str, base: &str) -> (String, String) {
let inv_l = crate::codegen::lean::aver_name_to_lean(inverse);
let name = format!("{base}_inv_append");
let text = format!(
"theorem {name} (a b : List {elem_ty_lean}) : \
{inv_l} (a ++ b) = {inv_l} a ++ {inv_l} b := by\n \
induction a with\n \
| nil => simp [{inv_l}]\n \
| cons x xs ih => simp [{inv_l}, ih, List.append_assoc]\n",
);
(name, text)
}
fn flush_fold_step_lemma(
enc: &EncoderShape,
acc_ty_lean: &str,
x_ty_lean: &str,
field: &str,
base: &str,
) -> (String, String) {
let inv_l = crate::codegen::lean::aver_name_to_lean(&enc.inverse);
let step_l = crate::codegen::lean::aver_name_to_lean(&enc.step);
let finish_l = crate::codegen::lean::aver_name_to_lean(&enc.finish);
let expand_l = crate::codegen::lean::aver_name_to_lean(&enc.expand);
let name = format!("{base}_flush_fold_step");
let text = format!(
"theorem {name} (acc : {acc_ty_lean}) (x : {x_ty_lean}) (h : 0 <= acc.{field}) :\n \
{inv_l} ({finish_l} ({step_l} acc x)) = {inv_l} ({finish_l} acc) ++ [x] := by\n \
unfold {step_l} {finish_l}\n \
split <;> (try split) <;> (try split) <;>\n \
simp_all [{inv_l}, {expand_l}, {base}_inv_append, {base}_counted_one, {base}_repeat_succ, beq_iff_eq] <;>\n \
omega\n",
);
(name, text)
}
fn loop_gen_lemma(
enc: &EncoderShape,
acc_ty_lean: &str,
x_ty_lean: &str,
field: &str,
base: &str,
) -> (String, String) {
let inv_l = crate::codegen::lean::aver_name_to_lean(&enc.inverse);
let loop_l = crate::codegen::lean::aver_name_to_lean(&enc.loop_fn);
let finish_l = crate::codegen::lean::aver_name_to_lean(&enc.finish);
let step_l = crate::codegen::lean::aver_name_to_lean(&enc.step);
let name = format!("{base}_loop_gen");
let text = format!(
"theorem {name} : ∀ (list : List {x_ty_lean}) (acc : {acc_ty_lean}), 0 <= acc.{field} →\n \
{inv_l} ({loop_l} list acc) = {inv_l} ({finish_l} acc) ++ list := by\n \
intro list\n \
induction list with\n \
| nil => intro acc _; simp [{loop_l}]\n \
| cons c rest ih =>\n \
intro acc h\n \
simp only [{loop_l}]\n \
rw [ih ({step_l} acc c) ({base}_{field}_nonneg acc c h), {base}_flush_fold_step acc c h]\n \
simp\n",
);
(name, text)
}
fn roundtrip_lemma(enc: &EncoderShape, x_ty_lean: &str, base: &str) -> (String, String) {
let inv_l = crate::codegen::lean::aver_name_to_lean(&enc.inverse);
let wrapper_l = crate::codegen::lean::aver_name_to_lean(&enc.wrapper);
let finish_l = crate::codegen::lean::aver_name_to_lean(&enc.finish);
let name = format!("{base}_roundtrip");
let text = format!(
"theorem {name} (xs : List {x_ty_lean}) : {inv_l} ({wrapper_l} xs) = xs := by\n \
unfold {wrapper_l}\n \
rw [{base}_loop_gen xs {init_acc} (by decide)]\n \
simp [{finish_l}, {inv_l}]\n",
init_acc = enc.init_acc,
);
(name, text)
}
struct RelationalCoverage {
counted_fn: String,
step_fn: String,
field: String,
}
fn relational_lemma_group(
enc: &EncoderShape,
inputs: &ProofLowerInputs,
base: &str,
) -> Option<(Vec<(String, String)>, RelationalCoverage)> {
let step_fd = inputs.find_fn_def_by_call_name(&enc.step)?;
if step_fd.params.len() != 2 {
return None;
}
let acc_ty_lean = crate::codegen::lean::type_to_lean(
&crate::codegen::common::parse_type_annotation(&step_fd.params[0].1),
);
let x_ty_lean = crate::codegen::lean::type_to_lean(
&crate::codegen::common::parse_type_annotation(&step_fd.params[1].1),
);
let field = detect_field_invariants(step_fd, inputs)
.into_iter()
.find_map(|(f, inv)| matches!(inv, FieldInvariant::Nonneg).then_some(f))?;
let inverse_fd = inputs.find_fn_def_by_call_name(&enc.inverse)?;
let Type::List(elem) =
crate::codegen::common::parse_type_annotation(&inverse_fd.params.first()?.1)
else {
return None;
};
let elem_ty_lean = crate::codegen::lean::type_to_lean(&elem);
let expand_fd = inputs.find_fn_def_by_call_name(&enc.expand)?;
let mut callees = BTreeSet::new();
if let Some(body) = last_body_expr(expand_fd) {
collect_callees(body, &mut callees);
}
let cr = inputs
.pure_fns()
.iter()
.filter(|fd| callees.contains(&fd.name))
.find_map(|fd| detect_counted_repeat(fd))?;
let mut group = Vec::new();
group.push(inv_append_lemma(&enc.inverse, &elem_ty_lean, base));
group.push(counted_one_lemma(&cr, base));
group.extend(counted_repeat_lemmas(&cr, base));
group.push(nonneg_lemma(&enc.step, &step_fd.params, &field, base));
group.push(flush_fold_step_lemma(
enc,
&acc_ty_lean,
&x_ty_lean,
&field,
base,
));
group.push(loop_gen_lemma(enc, &acc_ty_lean, &x_ty_lean, &field, base));
group.push(roundtrip_lemma(enc, &x_ty_lean, base));
let coverage = RelationalCoverage {
counted_fn: cr.fn_name.clone(),
step_fn: enc.step.clone(),
field,
};
Some((group, coverage))
}
struct MonoidalShape {
wrapper: String,
loop_fn: String,
direct: String,
x_ty_lean: String,
acc_ty_lean: String,
neutral: String,
}
fn detect_monoidal_spec(
law: &crate::ast::VerifyLaw,
subject_fn: &str,
inputs: &ProofLowerInputs,
) -> Option<MonoidalShape> {
use crate::ast::Expr;
let (lf, largs) = as_call(&law.lhs)?;
let (rf, rargs) = as_call(&law.rhs)?;
if largs.len() != 1 || rargs.len() != 1 {
return None;
}
let var = cr_ident_name(&largs[0])?;
if cr_ident_name(&rargs[0]).as_deref() != Some(var.as_str())
|| !law.givens.iter().any(|g| g.name == var)
{
return None;
}
let (wrapper, direct) = if lf == subject_fn {
(lf, rf)
} else if rf == subject_fn {
(rf, lf)
} else {
return None;
};
let roles = shape_fold_roles(inputs, &wrapper)?;
let loop_fn = roles.loop_fn;
if roles.finish_fn.is_some() || roles.step_op != Some(crate::ast::BinOp::Add) {
return None;
}
let wf = inputs.find_fn_def_by_call_name(&wrapper)?;
let (wbody_loop, loop_args) = as_call(last_body_expr(wf)?)?;
if wbody_loop != loop_fn
|| loop_args.len() != 2
|| cr_ident_name(&loop_args[0]).as_deref() != Some(var.as_str())
{
return None;
}
let neutral = render_init_literal(&loop_args[1])?;
let lpf = inputs.find_fn_def_by_call_name(&loop_fn)?;
if lpf.params.len() != 2 {
return None;
}
let Type::List(elem) = crate::codegen::common::parse_type_annotation(&lpf.params[0].1) else {
return None;
};
let x_ty_lean = crate::codegen::lean::type_to_lean(&elem);
let acc_ty_lean = crate::codegen::lean::type_to_lean(
&crate::codegen::common::parse_type_annotation(&lpf.params[1].1),
);
let df = inputs.find_fn_def_by_call_name(&direct)?;
if !matches!(
last_body_expr(df).map(|e| &e.node),
Some(Expr::Match { .. })
) {
return None;
}
Some(MonoidalShape {
wrapper,
loop_fn,
direct,
x_ty_lean,
acc_ty_lean,
neutral,
})
}
fn detect_monoidal_specs(inputs: &ProofLowerInputs) -> Vec<MonoidalShape> {
use crate::ast::{TopLevel, VerifyKind};
let mut out = Vec::new();
for item in inputs.entry_items {
if let TopLevel::Verify(vb) = item
&& let VerifyKind::Law(law) = &vb.kind
&& let Some(s) = detect_monoidal_spec(law, &vb.fn_name, inputs)
{
out.push(s);
}
}
out
}
fn monoidal_spec_group(shape: &MonoidalShape, base: &str) -> Vec<(String, String)> {
let loop_l = crate::codegen::lean::aver_name_to_lean(&shape.loop_fn);
let direct_l = crate::codegen::lean::aver_name_to_lean(&shape.direct);
let wrapper_l = crate::codegen::lean::aver_name_to_lean(&shape.wrapper);
let MonoidalShape {
x_ty_lean,
acc_ty_lean,
neutral,
..
} = shape;
let loop_gen = format!("{base}_loop_gen");
let loop_gen_text = format!(
"theorem {loop_gen} : ∀ (list : List {x_ty_lean}) (acc : {acc_ty_lean}), \
{loop_l} list acc = acc + {direct_l} list := by\n \
intro list\n \
induction list with\n \
| nil => intro acc; simp [{loop_l}, {direct_l}]\n \
| cons h t ih =>\n \
intro acc\n \
simp only [{loop_l}, {direct_l}]\n \
rw [ih (acc + h)]\n \
omega\n",
);
let spec = format!("{base}_spec_equiv");
let spec_text = format!(
"theorem {spec} (xs : List {x_ty_lean}) : {wrapper_l} xs = {direct_l} xs := by\n \
unfold {wrapper_l}\n \
rw [{loop_gen} xs {neutral}]\n \
simp\n",
);
vec![(loop_gen, loop_gen_text), (spec, spec_text)]
}