use std::collections::{HashMap, HashSet};
use super::expr::aver_name_to_lean;
use super::fn_def::{emit_fn_body_for, lower_pure_question_bang_for_emit};
use super::is_pure_fn;
use super::render::{emit_doc_comment, emit_fn_params, ret_type_or_unit};
use crate::ast::*;
use crate::codegen::CodegenContext;
#[derive(Clone)]
enum LexListArg {
Subterm {
strict: bool,
},
Filtered { helper: String, lemma_args: String },
}
struct LexListEdge {
callee: String,
arg: LexListArg,
}
#[derive(Clone)]
struct LengthLemma {
helper_lean: String,
params: String,
param_names: Vec<String>,
}
impl LengthLemma {
fn lemma_name(&self) -> String {
format!("{}_len_le", self.helper_lean)
}
}
struct LexListMemberPlan<'a> {
fd: &'a FnDef,
list_param_lean: String,
offset: usize,
rank: usize,
edges: Vec<LexListEdge>,
}
fn lex_list_measure_param(fd: &FnDef, ctx: &CodegenContext) -> Option<(usize, String)> {
let resolved_fd = crate::codegen::common::fn_id_for_decl(ctx, fd)
.and_then(|id| ctx.resolved_program.fn_by_id(id));
let resolved_owned = match resolved_fd {
Some(_) => None,
None => Some(ctx.resolve_fn_def(fd, None)),
};
let rfd: &crate::ir::hir::ResolvedFnDef =
resolved_fd.unwrap_or_else(|| resolved_owned.as_ref().unwrap().as_ref());
let mut found: Option<(usize, String)> = None;
for (idx, (name, ty)) in rfd.params.iter().enumerate() {
match ty {
crate::types::Type::List(_) => {
if found.is_some() {
return None;
}
found = Some((idx, aver_name_to_lean(name)));
}
crate::types::Type::Vector(_) | crate::types::Type::Str => return None,
_ => {}
}
}
found
}
fn lex_list_filter_helper<'a>(helper: &'a FnDef, ctx: &CodegenContext) -> Option<&'a FnDef> {
let resolved_fd = crate::codegen::common::fn_id_for_decl(ctx, helper)
.and_then(|id| ctx.resolved_program.fn_by_id(id));
let resolved_owned = match resolved_fd {
Some(_) => None,
None => Some(ctx.resolve_fn_def(helper, None)),
};
let rfd: &crate::ir::hir::ResolvedFnDef =
resolved_fd.unwrap_or_else(|| resolved_owned.as_ref().unwrap().as_ref());
let first_param = rfd.params.first()?;
if !matches!(first_param.1, crate::types::Type::List(_)) {
return None;
}
if !matches!(rfd.return_type, crate::types::Type::List(_)) {
return None;
}
let list_param_name = first_param.0.clone();
let tail = helper.body.tail_expr()?;
if helper.body.stmts().len() != 1 {
return None;
}
let Expr::Match { subject, arms } = &tail.node else {
return None;
};
let subject_name = crate::codegen::recursion::detect::local_name_of(subject)?;
if subject_name != list_param_name {
return None;
}
let mut saw_nil_empty = false;
let mut saw_cons_filter = false;
for arm in arms {
match &arm.pattern {
Pattern::EmptyList if matches!(&arm.body.node, Expr::List(items) if items.is_empty()) =>
{
saw_nil_empty = true;
}
Pattern::Cons(head, tail_bind)
if lex_filter_cons_arm_ok(&arm.body, helper, head, tail_bind) =>
{
saw_cons_filter = true;
}
_ => return None,
}
}
(saw_nil_empty && saw_cons_filter).then_some(helper)
}
fn lex_filter_cons_arm_ok(
body: &Spanned<Expr>,
helper: &FnDef,
head: &str,
tail_bind: &str,
) -> bool {
match &body.node {
Expr::Match { arms, .. } => arms
.iter()
.all(|arm| lex_filter_branch_ok(&arm.body, helper, head, tail_bind)),
_ => lex_filter_branch_ok(body, helper, head, tail_bind),
}
}
fn lex_filter_branch_ok(body: &Spanned<Expr>, helper: &FnDef, head: &str, tail_bind: &str) -> bool {
let self_set: HashSet<String> = std::iter::once(helper.name.clone()).collect();
let recur_on_tail = |target: &str, args: &[Spanned<Expr>]| -> bool {
let is_self =
crate::codegen::recursion::detect::canonical_callee_name(target, &self_set).is_some();
is_self
&& args
.first()
.and_then(crate::codegen::recursion::detect::local_name_of)
.filter(|n| *n == tail_bind)
.is_some()
};
match &body.node {
Expr::FnCall(callee, args) => {
let name = crate::codegen::recursion::detect::expr_to_dotted_name(callee);
match name.as_deref() {
Some("List.prepend") if args.len() == 2 => {
crate::codegen::recursion::detect::local_name_of(&args[0])
.filter(|n| *n == head)
.is_some()
&& lex_filter_branch_ok(&args[1], helper, head, tail_bind)
}
Some(n) => recur_on_tail(n, args),
None => false,
}
}
Expr::TailCall(boxed) => recur_on_tail(&boxed.target, &boxed.args),
_ => false,
}
}
fn classify_lex_list_arg(
arg: &Spanned<Expr>,
caller_list_param: &str,
tail_binders: &HashSet<String>,
ctx: &CodegenContext,
) -> Option<LexListArg> {
use crate::codegen::recursion::detect::{expr_to_dotted_name, local_name_of};
if let Some(name) = local_name_of(arg) {
if name == caller_list_param {
return Some(LexListArg::Subterm { strict: false });
}
if tail_binders.contains(name) {
return Some(LexListArg::Subterm { strict: true });
}
return None;
}
if let Expr::FnCall(callee, args) = &arg.node {
let dotted = expr_to_dotted_name(callee)?;
let bare = dotted.rsplit('.').next().unwrap_or(&dotted).to_string();
let first = args.first()?;
let s = local_name_of(first)?;
let s_ok = s == caller_list_param || tail_binders.contains(s);
if !s_ok {
return None;
}
let helper_fd = find_user_fn_by_name(ctx, &bare)?;
lex_list_filter_helper(helper_fd, ctx)?;
let lemma_args = args
.iter()
.map(|a| super::expr::emit_expr_legacy(a, ctx, None))
.collect::<Vec<_>>()
.join(" ");
return Some(LexListArg::Filtered {
helper: bare,
lemma_args,
});
}
None
}
fn find_user_fn_by_name<'a>(ctx: &'a CodegenContext, name: &str) -> Option<&'a FnDef> {
ctx.fn_defs
.iter()
.chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
.find(|fd| fd.name == name)
}
fn recognize_lex_list_wf_scc<'a>(
fns: &'a [&'a FnDef],
ctx: &CodegenContext,
) -> Option<(Vec<LexListMemberPlan<'a>>, Vec<LengthLemma>)> {
let names: HashSet<String> = fns.iter().map(|fd| fd.name.clone()).collect();
if fns.len() < 2 {
return None;
}
struct Raw<'a> {
fd: &'a FnDef,
edges: Vec<LexListEdge>,
}
let mut raws: Vec<Raw<'a>> = Vec::new();
for fd in fns {
if !is_pure_fn(fd) {
return None;
}
let (_idx, list_param) = lex_list_measure_param(fd, ctx)?;
let tail_binders =
crate::codegen::recursion::detect::collect_list_tail_binders(fd, &list_param);
let mut edges: Vec<LexListEdge> = Vec::new();
for (callee_raw, args) in
crate::codegen::recursion::detect::collect_calls_from_body(fd.body.as_ref())
{
let Some(callee) =
crate::codegen::recursion::detect::canonical_callee_name(&callee_raw, &names)
else {
continue;
};
let callee_fd = fns.iter().find(|f| f.name == callee)?;
let (callee_list_idx, _) = lex_list_measure_param(callee_fd, ctx)?;
let arg = args.get(callee_list_idx)?;
let classified = classify_lex_list_arg(arg, &list_param, &tail_binders, ctx)?;
edges.push(LexListEdge {
callee,
arg: classified,
});
}
if edges.is_empty() {
return None;
}
raws.push(Raw { fd, edges });
}
let mut offset: HashMap<String, usize> = HashMap::new();
offset.insert(raws[0].fd.name.clone(), 0);
let mut changed = true;
let mut iterations = 0;
while changed {
changed = false;
iterations += 1;
if iterations > raws.len() * raws.len() + 4 {
return None;
}
for raw in &raws {
let Some(&off_f) = offset.get(&raw.fd.name) else {
continue;
};
for edge in &raw.edges {
if let LexListArg::Subterm { strict } = &edge.arg {
let want = if *strict { off_f + 1 } else { off_f };
match offset.get(&edge.callee) {
Some(&existing) if existing == want => {}
Some(_) => return None, None => {
offset.insert(edge.callee.clone(), want);
changed = true;
}
}
}
}
}
for raw in &raws {
if !offset.contains_key(&raw.fd.name) {
offset.insert(raw.fd.name.clone(), 0);
changed = true;
}
}
}
for raw in &raws {
let off_f = offset[&raw.fd.name];
for edge in &raw.edges {
if matches!(edge.arg, LexListArg::Filtered { .. }) {
let off_c = offset[&edge.callee];
if off_c >= off_f {
return None;
}
}
}
}
let max_off = offset.values().copied().max().unwrap_or(0);
let mut plans: Vec<LexListMemberPlan<'a>> = Vec::new();
for raw in raws {
let off = offset[&raw.fd.name];
let rank = max_off - off;
let (_idx, list_param_lean) = lex_list_measure_param(raw.fd, ctx)?;
plans.push(LexListMemberPlan {
fd: raw.fd,
list_param_lean,
offset: off,
rank,
edges: raw.edges,
});
}
let rank_of: HashMap<String, usize> =
plans.iter().map(|p| (p.fd.name.clone(), p.rank)).collect();
for plan in &plans {
for edge in &plan.edges {
if let LexListArg::Subterm { strict } = &edge.arg {
if *strict {
if rank_of[&edge.callee] >= plan.rank {
return None;
}
} else {
return None;
}
}
}
}
let mut lemmas: Vec<LengthLemma> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for plan in &plans {
for edge in &plan.edges {
if let LexListArg::Filtered { helper, .. } = &edge.arg {
if !seen.insert(helper.clone()) {
continue;
}
let helper_fd = find_user_fn_by_name(ctx, helper)?;
let helper_lean = aver_name_to_lean(helper);
let params = emit_fn_params(&helper_fd.params);
let param_names: Vec<String> = helper_fd
.params
.iter()
.map(|(n, _)| aver_name_to_lean(n))
.collect();
lemmas.push(LengthLemma {
helper_lean,
params,
param_names,
});
}
}
}
Some((plans, lemmas))
}
fn emit_length_lemma(lemma: &LengthLemma) -> String {
let first = &lemma.param_names[0];
let rest_args = lemma.param_names[1..].join(" ");
let call = if rest_args.is_empty() {
format!("{} {}", lemma.helper_lean, first)
} else {
format!("{} {} {}", lemma.helper_lean, first, rest_args)
};
let forall_binders = lemma.params.clone();
let intro_names = lemma.param_names.join(" ");
let mut lines = Vec::new();
lines.push(format!(
"theorem {} : ∀ {}, ({}).length ≤ {}.length := by",
lemma.lemma_name(),
forall_binders,
call,
first
));
lines.push(format!(" intro {}", intro_names));
lines.push(format!(" induction {} with", first));
lines.push(format!(" | nil => simp [{}]", lemma.helper_lean));
lines.push(format!(
" | cons x rest ih => simp only [{}]; split",
lemma.helper_lean
));
lines.push(" · simp only [List.length_cons]; omega".to_string());
lines.push(" · simp only [List.length_cons]; omega".to_string());
lines.join("\n")
}
pub(super) fn emit_native_mutual_lex_list_wf_group(
fns: &[&FnDef],
ctx: &CodegenContext,
) -> Option<String> {
let (plans, lemmas) = recognize_lex_list_wf_scc(fns, ctx)?;
let mut lines: Vec<String> = Vec::new();
for lemma in &lemmas {
lines.push(emit_length_lemma(lemma));
lines.push(String::new());
}
lines.push("mutual".to_string());
for plan in &plans {
let fd = plan.fd;
let fn_name = aver_name_to_lean(&fd.name);
let params = emit_fn_params(&fd.params);
let ret_type = ret_type_or_unit(fd);
let lowered = lower_pure_question_bang_for_emit(fd);
let body_fn = lowered.as_ref().unwrap_or(fd);
let body_ast = lowered
.as_ref()
.map(|l| l.body.as_ref())
.unwrap_or(fd.body.as_ref());
let body = emit_fn_body_for(body_fn, body_ast, ctx);
lines.extend(
emit_doc_comment(&fd.desc)
.into_iter()
.map(|line| format!(" {line}")),
);
lines.push(format!(" def {} {} : {} :=", fn_name, params, ret_type));
for body_line in body.lines() {
lines.push(format!(" {body_line}"));
}
let measure_first = if plan.offset == 0 {
format!("{}.length", plan.list_param_lean)
} else {
format!("{}.length + {}", plan.list_param_lean, plan.offset)
};
lines.push(format!(
" termination_by ({}, {})",
measure_first, plan.rank
));
lines.push(" decreasing_by".to_string());
for edge in &plan.edges {
match &edge.arg {
LexListArg::Subterm { .. } => {
lines.push(" · simp_wf; exact Prod.Lex.right _ (by omega)".to_string());
}
LexListArg::Filtered { helper, lemma_args } => {
let lemma_name = format!("{}_len_le", aver_name_to_lean(helper));
lines.push(format!(
" · simp_wf; have := {} {}; omega",
lemma_name, lemma_args
));
}
}
}
lines.push(String::new());
}
lines.push("end".to_string());
Some(lines.join("\n"))
}