use super::AutoProof;
use super::aver_name_to_lean;
use super::shared::{self, expr_dotted_name, find_fn_def_by_call_name};
use crate::ast::{BinOp, Expr, FnDef, Literal, Spanned, Stmt, VerifyBlock, VerifyLaw};
use crate::codegen::CodegenContext;
fn is_field(e: &Spanned<Expr>, param: &str, field: &str) -> bool {
matches!(&e.node, Expr::Attr(base, f) if f == field && shared::ident_name(base) == Some(param))
}
fn as_mul(e: &Spanned<Expr>) -> Option<(&Spanned<Expr>, &Spanned<Expr>)> {
match &e.node {
Expr::BinOp(BinOp::Mul, l, r) => Some((l, r)),
_ => None,
}
}
fn is_sign_product(e: &Spanned<Expr>, p: &str) -> bool {
matches!(as_mul(e), Some((l, r)) if is_field(l, p, "top") && is_field(r, p, "bottom"))
}
fn is_bottom_product(e: &Spanned<Expr>, p: &str, q: &str) -> bool {
matches!(as_mul(e), Some((l, r)) if is_field(l, p, "bottom") && is_field(r, q, "bottom"))
}
fn fn_terminal_expr(fd: &FnDef) -> Option<&Spanned<Expr>> {
match fd.body.stmts() {
[Stmt::Expr(e)] => Some(e),
_ => None,
}
}
fn resolve_pure_fn<'a>(ctx: &'a CodegenContext, name: &str, arity: usize) -> Option<&'a FnDef> {
let fd = find_fn_def_by_call_name(ctx, name)?;
(fd.effects.is_empty() && fd.params.len() == arity).then_some(fd)
}
fn validate_order_primitives(
ctx: &CodegenContext,
lessthan_src: &str,
isnonneg_src: &str,
minus_src: &str,
) -> Option<()> {
let lt = resolve_pure_fn(ctx, lessthan_src, 2)?;
if lt.return_type.trim() != "Bool" {
return None;
}
let (a, b) = (lt.params[0].0.as_str(), lt.params[1].0.as_str());
let Expr::BinOp(BinOp::Lt, ll, lr) = &fn_terminal_expr(lt)?.node else {
return None;
};
let (Some((la, lb)), Some((ra, rb))) = (as_mul(ll), as_mul(lr)) else {
return None;
};
if !(is_sign_product(la, a)
&& is_bottom_product(lb, b, b)
&& is_sign_product(ra, b)
&& is_bottom_product(rb, a, a))
{
return None;
}
let nn = resolve_pure_fn(ctx, isnonneg_src, 1)?;
if nn.return_type.trim() != "Bool" {
return None;
}
let na = nn.params[0].0.as_str();
let Expr::BinOp(BinOp::Gte, gl, gr) = &fn_terminal_expr(nn)?.node else {
return None;
};
if !(is_sign_product(gl, na) && matches!(&gr.node, Expr::Literal(Literal::Int(0)))) {
return None;
}
let mi = resolve_pure_fn(ctx, minus_src, 2)?;
let (ma, mb) = (mi.params[0].0.as_str(), mi.params[1].0.as_str());
let Expr::RecordCreate { fields, .. } = &fn_terminal_expr(mi)?.node else {
return None;
};
let field = |name: &str| fields.iter().find(|(f, _)| f == name).map(|(_, v)| v);
let (Some(top), Some(bottom)) = (field("top"), field("bottom")) else {
return None;
};
let Expr::BinOp(BinOp::Sub, ts, td) = &top.node else {
return None;
};
let (Some((tsl, tsr)), Some((tdl, tdr))) = (as_mul(ts), as_mul(td)) else {
return None;
};
if !(is_field(tsl, ma, "top")
&& is_field(tsr, mb, "bottom")
&& is_field(tdl, mb, "top")
&& is_field(tdr, ma, "bottom")
&& is_bottom_product(bottom, ma, mb))
{
return None;
}
Some(())
}
fn substitute(
e: &Spanned<Expr>,
map: &std::collections::HashMap<String, Spanned<Expr>>,
) -> Spanned<Expr> {
let node = match &e.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => {
if let Some(rep) = map.get(n) {
return rep.clone();
}
e.node.clone()
}
Expr::BinOp(op, a, b) => Expr::BinOp(
*op,
Box::new(substitute(a, map)),
Box::new(substitute(b, map)),
),
Expr::Neg(a) => Expr::Neg(Box::new(substitute(a, map))),
Expr::Attr(b, f) => Expr::Attr(Box::new(substitute(b, map)), f.clone()),
Expr::FnCall(c, args) => Expr::FnCall(
Box::new(substitute(c, map)),
args.iter().map(|x| substitute(x, map)).collect(),
),
other => other.clone(),
};
Spanned::bare(node)
}
fn mentions_var(e: &Spanned<Expr>, vars: &[String]) -> bool {
match &e.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => vars.iter().any(|v| v == n),
Expr::BinOp(_, a, b) => mentions_var(a, vars) || mentions_var(b, vars),
Expr::Neg(a) | Expr::Attr(a, _) | Expr::ErrorProp(a) => mentions_var(a, vars),
Expr::FnCall(c, args) => {
mentions_var(c, vars) || args.iter().any(|x| mentions_var(x, vars))
}
Expr::Match { subject, arms } => {
mentions_var(subject, vars) || arms.iter().any(|a| mentions_var(&a.body, vars))
}
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| mentions_var(v, vars)),
Expr::RecordUpdate { base, updates, .. } => {
mentions_var(base, vars) || updates.iter().any(|(_, v)| mentions_var(v, vars))
}
Expr::List(xs) | Expr::Tuple(xs) | Expr::IndependentProduct(xs, _) => {
xs.iter().any(|x| mentions_var(x, vars))
}
Expr::Constructor(_, Some(inner)) => mentions_var(inner, vars),
_ => false,
}
}
struct Edge {
left: String,
right: String,
right_closed: bool,
strict: bool,
conj_idx: usize,
}
enum Link {
PremiseStrict { to: String, conj_idx: usize },
PremiseNonstrict { to: String, conj_idx: usize },
Ground { to: String },
}
impl Link {
fn to(&self) -> &str {
match self {
Link::PremiseStrict { to, .. }
| Link::PremiseNonstrict { to, .. }
| Link::Ground { to, .. } => to,
}
}
}
pub(super) struct FracOrderTransitivity {
lo: String,
links: Vec<Link>,
subject: String,
lessthan: String,
isnonneg: String,
minus: String,
n_conj: usize,
used_conj: Vec<usize>,
}
#[allow(clippy::too_many_arguments)]
fn find_chain(
cur: &str,
cur_closed: bool,
hi: &str,
hi_closed: bool,
edges: &[Edge],
used: &mut Vec<bool>,
depth: usize,
path: &mut Vec<Link>,
) -> bool {
if cur == hi {
return !path.is_empty();
}
if depth >= 3 {
return false;
}
for (ei, e) in edges.iter().enumerate() {
if used[ei] || e.left != cur {
continue;
}
used[ei] = true;
path.push(if e.strict {
Link::PremiseStrict {
to: e.right.clone(),
conj_idx: e.conj_idx,
}
} else {
Link::PremiseNonstrict {
to: e.right.clone(),
conj_idx: e.conj_idx,
}
});
if find_chain(
&e.right,
e.right_closed,
hi,
hi_closed,
edges,
used,
depth + 1,
path,
) {
return true;
}
path.pop();
used[ei] = false;
}
if cur_closed && hi_closed && cur != hi {
path.push(Link::Ground { to: hi.to_string() });
return true;
}
false
}
pub(super) fn recognize_frac_order_transitivity_shape(
_vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
) -> Option<FracOrderTransitivity> {
law.when.as_ref()?;
if !matches!(law.rhs.node, Expr::Literal(crate::ast::Literal::Bool(true))) {
return None;
}
let Expr::FnCall(callee, call_args) = &law.lhs.node else {
return None;
};
let subject_src = expr_dotted_name(callee)?;
let subj_fd = find_fn_def_by_call_name(ctx, &subject_src)?;
if subj_fd.return_type.trim() != "Bool"
|| subj_fd.params.len() != call_args.len()
|| !subj_fd.effects.is_empty()
{
return None;
}
let [Stmt::Expr(body)] = subj_fd.body.stmts() else {
return None;
};
let lt = shared::call_named(body, "lessThan", 2)?;
let Expr::FnCall(lt_callee, _) = &body.node else {
return None;
};
let lessthan_src = expr_dotted_name(lt_callee)?;
let (isnonneg_src, minus_src) = match lessthan_src.rsplit_once('.') {
Some((prefix, _)) => (format!("{prefix}.isNonNeg"), format!("{prefix}.minus")),
None => ("isNonNeg".to_string(), "minus".to_string()),
};
validate_order_primitives(ctx, &lessthan_src, &isnonneg_src, &minus_src)?;
let lessthan = aver_name_to_lean(&lessthan_src);
let isnonneg = aver_name_to_lean(&isnonneg_src);
let minus = aver_name_to_lean(&minus_src);
let mut map: std::collections::HashMap<String, Spanned<Expr>> =
std::collections::HashMap::new();
for ((pname, _), arg) in subj_fd.params.iter().zip(call_args.iter()) {
map.insert(pname.clone(), arg.clone());
}
let lo_e = substitute(<[0], &map);
let hi_e = substitute(<[1], &map);
let render = |e: &Spanned<Expr>| super::super::expr::emit_expr_legacy(e, ctx, None);
let lo = render(&lo_e);
let hi = render(&hi_e);
if lo == hi {
return None;
}
let given_names: Vec<String> = law.givens.iter().map(|g| g.name.clone()).collect();
let hi_closed = !mentions_var(&hi_e, &given_names);
let lo_closed = !mentions_var(&lo_e, &given_names);
let when = law.when.as_ref()?;
let conj = shared::collect_when_clauses(when);
let n_conj = conj.len();
let mut edges: Vec<Edge> = Vec::new();
for (i, c) in conj.iter().enumerate() {
if let Some(a) = shared::call_qualified(c, &lessthan_src, 2) {
edges.push(Edge {
left: render(&a[0]),
right: render(&a[1]),
right_closed: !mentions_var(&a[1], &given_names),
strict: true,
conj_idx: i,
});
} else if let Some(nn) = shared::call_qualified(c, &isnonneg_src, 1)
&& let Some(m) = shared::call_qualified(&nn[0], &minus_src, 2)
{
edges.push(Edge {
left: render(&m[1]),
right: render(&m[0]),
right_closed: !mentions_var(&m[0], &given_names),
strict: false,
conj_idx: i,
});
}
}
if edges.is_empty() {
return None;
}
let mut used = vec![false; edges.len()];
let mut links: Vec<Link> = Vec::new();
if !find_chain(
&lo, lo_closed, &hi, hi_closed, &edges, &mut used, 0, &mut links,
) {
return None;
}
if !matches!(links.first(), Some(Link::PremiseStrict { .. })) {
return None;
}
if links.iter().skip(1).any(|l| {
let t = l.to();
match l {
Link::Ground { .. } => false,
_ => edges
.iter()
.find(|e| e.right == t)
.map(|e| !e.right_closed)
.unwrap_or(true),
}
}) {
return None;
}
let mut used_conj: Vec<usize> = links
.iter()
.filter_map(|l| match l {
Link::PremiseStrict { conj_idx, .. } | Link::PremiseNonstrict { conj_idx, .. } => {
Some(*conj_idx)
}
Link::Ground { .. } => None,
})
.collect();
used_conj.sort_unstable();
used_conj.dedup();
Some(FracOrderTransitivity {
lo,
links,
subject: aver_name_to_lean(&subject_src),
lessthan,
isnonneg,
minus,
n_conj,
used_conj,
})
}
pub(in crate::codegen::lean) fn recognize_frac_order_transitivity(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
) -> bool {
recognize_frac_order_transitivity_shape(vb, law, ctx).is_some()
}
pub(super) fn emit_frac_order_transitivity_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
theorem_base: &str,
quant_params: &str,
) -> Option<AutoProof> {
let c = recognize_frac_order_transitivity_shape(vb, law, ctx)?;
let render = |e: &Spanned<Expr>| super::super::expr::emit_expr_legacy(e, ctx, None);
let when = render(law.when.as_ref()?);
let lhs = render(&law.lhs);
let intros: Vec<String> = law
.givens
.iter()
.map(|g| aver_name_to_lean(&g.name))
.collect();
let text = render_chain(
theorem_base,
quant_params,
&intros.join(" "),
&when,
&lhs,
&c,
);
Some(AutoProof {
support_lines: text.lines().map(|l| l.to_string()).collect(),
body: crate::codegen::lean::tactic_ir::Tactic::raw(Vec::new()),
replaces_theorem: true,
})
}
fn render_chain(
base: &str,
quant_params: &str,
intros: &str,
when: &str,
lhs: &str,
c: &FracOrderTransitivity,
) -> String {
let FracOrderTransitivity {
lo,
links,
subject,
lessthan,
isnonneg,
minus,
n_conj,
used_conj,
} = c;
let p = format!("{base}__");
let kit = format!(
r#"theorem {p}sq_nonneg (x : Int) : 0 ≤ x * x := by
cases Int.le_total 0 x with
| inl h => exact Int.mul_nonneg h h
| inr h => exact Int.mul_nonneg_of_nonpos_of_nonpos h h
theorem {p}sq_pos {{x : Int}} (hx : x ≠ 0) : 0 < x * x := by
cases Int.lt_or_gt_of_ne hx with
| inl h => exact Int.mul_pos_of_neg_of_neg h h
| inr h => exact Int.mul_pos h h
theorem {p}frac_lt_imp_le (a b : Fraction) (h : {lessthan} a b = true) :
{isnonneg} ({minus} b a) = true := by
simp only [{lessthan}, {isnonneg}, {minus}, decide_eq_true_eq, ge_iff_le] at h ⊢
have hid : (b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)
= (b.top*b.bottom)*(a.bottom*a.bottom) - (a.top*a.bottom)*(b.bottom*b.bottom) := by grind
omega
theorem {p}frac_le_trans (a b cc : Fraction) (hb : b.bottom ≠ 0)
(hab : {isnonneg} ({minus} b a) = true) (hbc : {isnonneg} ({minus} cc b) = true) :
{isnonneg} ({minus} cc a) = true := by
simp only [{isnonneg}, {minus}, decide_eq_true_eq, ge_iff_le] at hab hbc ⊢
have hP : 0 ≤ ((b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)) * (cc.bottom*cc.bottom) :=
Int.mul_nonneg hab ({p}sq_nonneg cc.bottom)
have hQ : 0 ≤ ((cc.top*b.bottom - b.top*cc.bottom) * (cc.bottom*b.bottom)) * (a.bottom*a.bottom) :=
Int.mul_nonneg hbc ({p}sq_nonneg a.bottom)
have hid : ((b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)) * (cc.bottom*cc.bottom)
+ ((cc.top*b.bottom - b.top*cc.bottom) * (cc.bottom*b.bottom)) * (a.bottom*a.bottom)
= (b.bottom*b.bottom) * ((cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom)) := by
grind
have hsum : 0 ≤ (b.bottom*b.bottom) * ((cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom)) := by
rw [← hid]; omega
exact Int.nonneg_of_mul_nonneg_right hsum ({p}sq_pos hb)
theorem {p}lessThan_right_bottom_ne (x y : Fraction) (h : {lessthan} x y = true) : y.bottom ≠ 0 := by
intro h0
simp only [{lessthan}, decide_eq_true_eq] at h
rw [h0] at h
simp only [Int.mul_zero, Int.zero_mul] at h
omega
theorem {p}frac_lt_le_trans (a b cc : Fraction) (hc : cc.bottom ≠ 0)
(hab : {lessthan} a b = true) (hbc : {isnonneg} ({minus} cc b) = true) :
{lessthan} a cc = true := by
have hb : b.bottom ≠ 0 := {p}lessThan_right_bottom_ne a b hab
simp only [{lessthan}, {isnonneg}, {minus}, decide_eq_true_eq, ge_iff_le] at hab hbc ⊢
have hab' : 0 < (b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom) := by
have hid_ab : (b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)
= (b.top*b.bottom)*(a.bottom*a.bottom) - (a.top*a.bottom)*(b.bottom*b.bottom) := by grind
omega
have hP : 0 < ((b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)) * (cc.bottom*cc.bottom) :=
Int.mul_pos hab' ({p}sq_pos hc)
have hQ : 0 ≤ ((cc.top*b.bottom - b.top*cc.bottom) * (cc.bottom*b.bottom)) * (a.bottom*a.bottom) :=
Int.mul_nonneg hbc ({p}sq_nonneg a.bottom)
have hid : ((b.top*a.bottom - a.top*b.bottom) * (b.bottom*a.bottom)) * (cc.bottom*cc.bottom)
+ ((cc.top*b.bottom - b.top*cc.bottom) * (cc.bottom*b.bottom)) * (a.bottom*a.bottom)
= (b.bottom*b.bottom) * ((cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom)) := by
grind
have hsum : 0 < (b.bottom*b.bottom) * ((cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom)) := by
rw [← hid]; omega
have hid_goal : (cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom)
= (cc.top*cc.bottom)*(a.bottom*a.bottom) - (a.top*a.bottom)*(cc.bottom*cc.bottom) := by grind
have hT3nn : 0 ≤ (cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom) :=
Int.nonneg_of_mul_nonneg_right (Int.le_of_lt hsum) ({p}sq_pos hb)
have hT3ne : (cc.top*a.bottom - a.top*cc.bottom) * (cc.bottom*a.bottom) ≠ 0 := by
intro h0; rw [h0, Int.mul_zero] at hsum; omega
omega"#
);
let mut names: Vec<String> = (0..*n_conj)
.map(|i| {
if used_conj.contains(&i) {
format!("hc{i}")
} else {
"_".to_string()
}
})
.collect();
let mut obtain = names.remove(0);
for nm in names {
obtain = format!("⟨{obtain}, {nm}⟩");
}
let mut steps: Vec<String> = Vec::new();
let node = |i: usize| -> String {
if i == 0 {
lo.clone()
} else {
links[i - 1].to().to_string()
}
};
let k = links.len();
let Link::PremiseStrict {
conj_idx: first_idx,
..
} = &links[0]
else {
unreachable!("recognizer guarantees a strict first link")
};
steps.push(format!("have h0 := hc{first_idx}"));
if k == 1 {
steps.push("exact h0".to_string());
} else {
for (j, link) in links.iter().enumerate().skip(1) {
let from = node(j);
let to = node(j + 1);
match link {
Link::PremiseStrict { conj_idx, .. } => steps.push(format!(
"have e{j} := {p}frac_lt_imp_le ({from}) ({to}) hc{conj_idx}"
)),
Link::PremiseNonstrict { conj_idx, .. } => {
steps.push(format!("have e{j} := hc{conj_idx}"))
}
Link::Ground { .. } => {
steps.push(format!(
"have g{j} : {lessthan} ({from}) ({to}) = true := by decide"
));
steps.push(format!(
"have e{j} := {p}frac_lt_imp_le ({from}) ({to}) g{j}"
));
}
}
}
let n1 = node(1);
steps.push("have acc := e1".to_string());
for j in 2..k {
let mid = node(j);
let nxt = node(j + 1);
steps.push(format!(
"have acc := {p}frac_le_trans ({n1}) ({mid}) ({nxt}) (by decide) acc e{j}"
));
}
let rr = node(k);
steps.push(format!(
"exact {p}frac_lt_le_trans ({lo}) ({n1}) ({rr}) (by decide) h0 acc"
));
}
let mut block: Vec<String> = Vec::new();
if *n_conj > 1 {
block.push(" simp only [Bool.and_eq_true] at h_when".to_string());
block.push(format!(" obtain {obtain} := h_when"));
} else {
block.push(format!(" have {obtain} := h_when"));
}
block.push(format!(" simp only [_root_.{subject}]"));
for s in &steps {
block.push(format!(" {s}"));
}
let last = block.len() - 1;
block[last] = format!("{})", block[last]);
let assembly = format!(
"set_option maxHeartbeats 4000000 in\n\
theorem {base} : ∀ {quant_params}, {when} = true -> {lhs} = true := by\n \
intro {intros} h_when\n \
first\n \
| ({body}\n \
| sorry",
body = {
let mut it = block.iter();
let head = it.next().unwrap().trim_start().to_string();
let rest: Vec<String> = it.cloned().collect();
if rest.is_empty() {
head
} else {
format!("{head}\n{}", rest.join("\n"))
}
}
);
format!("{kit}\n{assembly}")
}