use std::collections::{HashMap, HashSet};
use crate::ast::{BinOp, Expr, FnDef, Literal, Pattern, Spanned, VerifyBlock, VerifyLaw};
use crate::codegen::CodegenContext;
use crate::codegen::common::is_pure_fn;
use super::super::expr::aver_name_to_lean;
use super::super::types::type_annotation_to_lean;
use super::AutoProof;
#[derive(Clone, Debug, PartialEq)]
enum PosClass {
Same,
Weak(String),
AdvConst { skipws: Option<String> },
Semantic {
skipws: Option<String>,
binder: String,
},
}
fn pos_render(class: &PosClass, s: &str, pos: &str) -> String {
let g = |g: &str| aver_name_to_lean(g);
match class {
PosClass::Same => pos.to_string(),
PosClass::Weak(h) => format!("({} {s} {pos})", g(h)),
PosClass::AdvConst { skipws: None } => format!("({pos} + 1)"),
PosClass::AdvConst { skipws: Some(h) } => format!("({} {s} ({pos} + 1))", g(h)),
PosClass::Semantic {
skipws: None,
binder,
} => binder.clone(),
PosClass::Semantic {
skipws: Some(h),
binder,
} => format!("({} {s} {binder})", g(h)),
}
}
fn classify_pos(
expr: &Spanned<Expr>,
_s: &str,
pos: &str,
result_binders: &HashSet<String>,
weak_helpers: &HashSet<String>,
) -> Option<PosClass> {
if let Some(name) = ident_name(expr) {
if name == pos {
return Some(PosClass::Same);
}
if result_binders.contains(name) {
return Some(PosClass::Semantic {
skipws: None,
binder: name.to_string(),
});
}
return None;
}
match &expr.node {
Expr::BinOp(BinOp::Add, l, r) if is_ident(l, pos) && is_int_lit(r, 1) => {
Some(PosClass::AdvConst { skipws: None })
}
Expr::FnCall(callee, args) if args.len() == 2 => {
let g = super::shared::expr_dotted_name(callee)?;
if !weak_helpers.contains(&g) {
return None;
}
if let Some(name) = ident_name(&args[1]) {
if name == pos {
return Some(PosClass::Weak(g));
}
if result_binders.contains(name) {
return Some(PosClass::Semantic {
skipws: Some(g),
binder: name.to_string(),
});
}
return None;
}
match &args[1].node {
Expr::BinOp(BinOp::Add, l, r) if is_ident(l, pos) && is_int_lit(r, 1) => {
Some(PosClass::AdvConst { skipws: Some(g) })
}
_ => None,
}
}
_ => None,
}
}
fn ctor_app(expr: &Spanned<Expr>) -> Option<(String, Vec<&Spanned<Expr>>)> {
match &expr.node {
Expr::Constructor(name, payload) => {
let args = match payload.as_ref().map(|b| &b.node) {
Some(Expr::Tuple(items)) => items.iter().collect(),
Some(_) => vec![payload.as_ref().unwrap().as_ref()],
None => vec![],
};
Some((name.clone(), args))
}
Expr::FnCall(callee, args) => {
if let Expr::Attr(base, variant) = &callee.node
&& ident_name(base).is_some()
&& variant.chars().next().is_some_and(|c| c.is_uppercase())
{
let name = super::shared::expr_dotted_name(callee)?;
Some((name, args.iter().collect()))
} else {
None
}
}
_ => None,
}
}
fn ident_name(e: &Spanned<Expr>) -> Option<&str> {
match &e.node {
Expr::Ident(n) => Some(n.as_str()),
Expr::Resolved { name, .. } => Some(name.as_str()),
_ => None,
}
}
fn is_ident(e: &Spanned<Expr>, name: &str) -> bool {
ident_name(e) == Some(name)
}
fn is_int_lit(e: &Spanned<Expr>, v: i64) -> bool {
matches!(&e.node, Expr::Literal(Literal::Int(n)) if *n == v)
}
fn ctor_to_lean(name: &str) -> String {
fn lower_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
Some(f) => f.to_lowercase().collect::<String>() + c.as_str(),
None => String::new(),
}
}
match name.rsplit_once('.') {
Some((ty, variant)) => format!("{ty}.{}", lower_first(variant)),
None => lower_first(name),
}
}
#[derive(Clone, Debug)]
enum Leaf {
Err,
OkTerminal,
Sibling { name: String, pos: PosClass },
External { name: String, pos: PosClass },
}
#[derive(Clone, Debug)]
enum Member {
CharAt {
some: CharSome,
},
DispatchParam { leaves: Vec<Leaf> },
ResultMatch {
scrut: String,
scrut_is_sibling: bool,
edge: Leaf,
err_first: bool,
},
AdtMatch {
edge: Leaf,
ctor_first: bool,
},
}
#[derive(Clone, Debug)]
enum CharSome {
Direct(Leaf),
Nested(Vec<Leaf>),
}
#[derive(Clone, Debug)]
struct Citation {
hyp: String,
src_fn: String,
kind: CiteKind,
}
#[derive(Clone, Debug)]
enum CiteKind {
Parser {
binders: String,
arg_names: Vec<String>,
call: String,
v_name: String,
p_name: String,
pos_name: String,
ok_ctor: String,
extras_count: usize,
theorem: String,
},
Weak { g_lean: String, theorem: String },
}
struct MemberInfo<'a> {
fd: &'a FnDef,
lean: String,
s_name: String,
pos_name: String,
extras: Vec<(String, String)>,
shape: Member,
edges: Vec<(String, PosClass)>,
dispatch_param: Option<String>,
rank: usize,
}
struct CliqueShape<'a> {
members: Vec<MemberInfo<'a>>,
index_of: HashMap<String, usize>,
rep: String,
r: usize,
val_type: String,
ok_ctor: String,
variants: Vec<(String, usize)>,
citations: Vec<Citation>,
hyp_of: HashMap<String, String>,
}
impl CliqueShape<'_> {
fn parser_cite(&self, fn_name: &str) -> Option<(&str, usize)> {
self.citations.iter().find_map(|c| match &c.kind {
CiteKind::Parser { extras_count, .. } if c.src_fn == fn_name => {
Some((c.hyp.as_str(), *extras_count))
}
_ => None,
})
}
}
fn member_body_match(fd: &FnDef) -> Option<(&Spanned<Expr>, &Vec<crate::ast::MatchArm>)> {
let tail = fd.body.tail_expr()?;
if let Expr::Match { subject, arms } = &tail.node {
Some((subject, arms))
} else {
None
}
}
fn as_call(expr: &Spanned<Expr>) -> Option<(String, &Spanned<Expr>)> {
match &expr.node {
Expr::FnCall(callee, args) if args.len() >= 2 => {
Some((super::shared::expr_dotted_name(callee)?, &args[1]))
}
Expr::TailCall(tc) if tc.args.len() >= 2 => Some((tc.target.clone(), &tc.args[1])),
_ => None,
}
}
fn classify_leaf(
body: &Spanned<Expr>,
s: &str,
pos: &str,
members: &HashSet<String>,
result_binders: &HashSet<String>,
ok_ctor: &str,
weak_helpers: &HashSet<String>,
) -> Option<Leaf> {
if let Some((name, args)) = ctor_app(body) {
if name == ok_ctor {
let ok = args.len() == 2
&& (is_ident(args[1], pos)
|| matches!(&args[1].node, Expr::BinOp(BinOp::Add, l, r) if is_ident(l, pos) && is_int_lit(r, 1)));
return if ok { Some(Leaf::OkTerminal) } else { None };
}
return Some(Leaf::Err);
}
let (name, posexpr) = as_call(body)?;
let class = classify_pos(posexpr, s, pos, result_binders, weak_helpers)?;
if members.contains(&name) {
Some(Leaf::Sibling { name, pos: class })
} else {
Some(Leaf::External { name, pos: class })
}
}
type MemberAnalysis = (Member, Vec<(String, PosClass)>, Option<String>);
fn analyse_member(
fd: &FnDef,
members: &HashSet<String>,
ok_ctor: &str,
weak_helpers: &HashSet<String>,
) -> Option<MemberAnalysis> {
let s = fd.params.first()?.0.clone();
let pos = fd.params.get(1)?.0.clone();
let (subject, arms) = member_body_match(fd)?;
let empty = HashSet::new();
let mut edges: Vec<(String, PosClass)> = Vec::new();
let record = |leaf: &Leaf, edges: &mut Vec<(String, PosClass)>| {
if let Leaf::Sibling { name, pos } = leaf {
edges.push((name.clone(), pos.clone()));
}
};
if let Some((callee, cargs)) = call_parts(subject)
&& callee == "String.charAt"
&& cargs.len() == 2
&& is_ident(&cargs[0], &s)
&& is_ident(&cargs[1], &pos)
{
if arms.len() != 2 {
return None;
}
let some_arm = &arms[1].body;
let some = match &some_arm.node {
Expr::Match {
subject: inner_subj,
arms: inner_arms,
} if ident_name(inner_subj).is_some() => {
let mut leaves = Vec::new();
for a in inner_arms {
let leaf =
classify_leaf(&a.body, &s, &pos, members, &empty, ok_ctor, weak_helpers)?;
record(&leaf, &mut edges);
leaves.push(leaf);
}
CharSome::Nested(leaves)
}
_ => {
let leaf =
classify_leaf(some_arm, &s, &pos, members, &empty, ok_ctor, weak_helpers)?;
record(&leaf, &mut edges);
CharSome::Direct(leaf)
}
};
return Some((Member::CharAt { some }, edges, None));
}
if let Some(cname) = ident_name(subject)
&& fd.params.iter().any(|(n, _)| n == cname)
&& arms
.iter()
.all(|a| matches!(a.pattern, Pattern::Literal(_) | Pattern::Wildcard))
{
let cname = cname.to_string();
let mut leaves = Vec::new();
for a in arms {
let leaf = classify_leaf(&a.body, &s, &pos, members, &empty, ok_ctor, weak_helpers)?;
record(&leaf, &mut edges);
leaves.push(leaf);
}
return Some((Member::DispatchParam { leaves }, edges, Some(cname.clone())));
}
if let Some((scrut, sargs)) = call_parts(subject)
&& sargs.len() >= 2
&& is_ident(&sargs[0], &s)
&& is_ident(&sargs[1], &pos)
&& arms.len() == 2
{
let (ok_idx, err_idx) = ok_err_arm_indices(arms, ok_ctor)?;
let ok_arm = &arms[ok_idx];
let binders = ctor_binders(&ok_arm.pattern)?;
if binders.len() != 2 {
return None;
}
let mut rb = HashSet::new();
rb.insert(binders[1].clone());
let edge = classify_leaf(&ok_arm.body, &s, &pos, members, &rb, ok_ctor, weak_helpers)?;
record(&edge, &mut edges);
let scrut_is_sibling = members.contains(&scrut);
if scrut_is_sibling {
edges.push((scrut.clone(), PosClass::Same));
}
return Some((
Member::ResultMatch {
scrut,
scrut_is_sibling,
edge,
err_first: err_idx < ok_idx,
},
edges,
None,
));
}
if let Some(varname) = ident_name(subject)
&& fd.params.iter().any(|(n, _)| n == varname)
&& arms.len() == 2
{
let (ctor_idx, wild_idx) = ctor_wild_arm_indices(arms)?;
let ctor_arm = &arms[ctor_idx];
let edge = classify_leaf(
&ctor_arm.body,
&s,
&pos,
members,
&empty,
ok_ctor,
weak_helpers,
)?;
record(&edge, &mut edges);
return Some((
Member::AdtMatch {
edge,
ctor_first: ctor_idx < wild_idx,
},
edges,
None,
));
}
None
}
fn call_parts(expr: &Spanned<Expr>) -> Option<(String, &[Spanned<Expr>])> {
match &expr.node {
Expr::FnCall(callee, args) => {
Some((super::shared::expr_dotted_name(callee)?, args.as_slice()))
}
Expr::TailCall(tc) => Some((tc.target.clone(), tc.args.as_slice())),
_ => None,
}
}
fn ctor_binders(pat: &Pattern) -> Option<Vec<String>> {
match pat {
Pattern::Constructor(_, binders) => Some(binders.clone()),
_ => None,
}
}
fn ok_err_arm_indices(arms: &[crate::ast::MatchArm], ok_ctor: &str) -> Option<(usize, usize)> {
let mut ok = None;
let mut err = None;
for (i, a) in arms.iter().enumerate() {
if let Pattern::Constructor(name, _) = &a.pattern {
if name == ok_ctor {
ok = Some(i);
} else {
err = Some(i);
}
}
}
Some((ok?, err?))
}
fn ctor_wild_arm_indices(arms: &[crate::ast::MatchArm]) -> Option<(usize, usize)> {
let mut ctor = None;
let mut wild = None;
for (i, a) in arms.iter().enumerate() {
match &a.pattern {
Pattern::Constructor(_, _) => ctor = Some(i),
Pattern::Wildcard => wild = Some(i),
_ => {}
}
}
Some((ctor?, wild?))
}
fn assign_ranks(members: &[MemberInfo]) -> Option<Vec<usize>> {
let n = members.len();
let idx: HashMap<&str, usize> = members
.iter()
.enumerate()
.map(|(i, m)| (m.fd.name.as_str(), i))
.collect();
let mut edges: Vec<(usize, usize)> = Vec::new();
for (i, m) in members.iter().enumerate() {
for (callee, class) in &m.edges {
let must_drop = matches!(
class,
PosClass::Same | PosClass::Weak(_) | PosClass::Semantic { .. }
);
if must_drop && let Some(&j) = idx.get(callee.as_str()) {
edges.push((i, j));
}
}
}
rank_dag(n, &edges)
}
fn rank_dag(n: usize, edges: &[(usize, usize)]) -> Option<Vec<usize>> {
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for &(u, v) in edges {
adj[u].push(v);
}
let mut rank = vec![usize::MAX; n];
let mut on_stack = vec![false; n];
fn dfs(
u: usize,
adj: &[Vec<usize>],
rank: &mut [usize],
on_stack: &mut [bool],
) -> Option<usize> {
if on_stack[u] {
return None; }
if rank[u] != usize::MAX {
return Some(rank[u]);
}
on_stack[u] = true;
let mut best = 0usize;
for &v in &adj[u] {
let rv = dfs(v, adj, rank, on_stack)?;
best = best.max(rv + 1);
}
on_stack[u] = false;
rank[u] = best;
Some(best)
}
for u in 0..n {
dfs(u, &adj, &mut rank, &mut on_stack)?;
}
Some(rank)
}
struct LawShape {
target: String,
ok_ctor: String,
val_name: String,
val_type: String,
p_name: String,
pos_name: String,
}
fn read_law_shape(law: &VerifyLaw) -> Option<LawShape> {
let when = law.when.as_ref()?;
let Expr::BinOp(BinOp::Eq, call, ctor) = &when.node else {
return None;
};
let (target, cargs) = call_parts(call)?;
if cargs.len() < 2 {
return None;
}
let pos_name = match &cargs[1].node {
Expr::Ident(n) => n.clone(),
_ => return None,
};
let (ok_ctor, cargs2) = ctor_app(ctor)?;
if cargs2.len() != 2 {
return None;
}
let (val_name, p_name) = match (&cargs2[0].node, &cargs2[1].node) {
(Expr::Ident(vn), Expr::Ident(pn)) => (vn.clone(), pn.clone()),
_ => return None,
};
let Expr::BinOp(BinOp::Gte, cl, cr) = &law.lhs.node else {
return None;
};
if !is_ident(cl, &p_name) || !is_ident(cr, &pos_name) {
return None;
}
if !matches!(&law.rhs.node, Expr::Literal(Literal::Bool(true))) {
return None;
}
Some(LawShape {
target,
ok_ctor,
val_name,
val_type: String::new(),
p_name,
pos_name,
})
}
fn owning_scc<'a>(target: &str, ctx: &'a CodegenContext) -> Option<Vec<&'a FnDef>> {
let pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure_fn(fd)).collect();
if !pure.iter().any(|fd| fd.name == target) {
return None;
}
let comps = crate::call_graph::ordered_fn_components(&pure, &ctx.module_prefixes);
let comp = comps
.into_iter()
.find(|c| c.iter().any(|fd| fd.name == target))?;
if comp.len() < 2 {
return None;
}
Some(comp)
}
struct WeakRaw {
theorem: String,
line: usize,
universal: bool,
}
struct ParserRaw<'a> {
theorem: String,
binders: String,
arg_names: Vec<String>,
call: String,
v_name: String,
p_name: String,
pos_name: String,
ok_ctor: String,
extras_count: usize,
line: usize,
vb: &'a VerifyBlock,
law: &'a VerifyLaw,
}
fn weak_law_raw(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
) -> Option<(String, WeakRaw)> {
if law.when.is_some() {
return None;
}
let Expr::BinOp(BinOp::Gte, call, r) = &law.lhs.node else {
return None;
};
let (g, cargs) = call_parts(call)?;
if cargs.len() != 2 {
return None;
}
let q = ident_name(&cargs[1])?;
if ident_name(r) != Some(q) {
return None;
}
if !matches!(&law.rhs.node, Expr::Literal(Literal::Bool(true))) {
return None;
}
if law.givens.len() != 2 {
return None;
}
let theorem = crate::codegen::lean::toplevel::law_theorem_base(vb, law, ctx);
let universal = ctx
.fn_defs
.iter()
.find(|fd| fd.name == g)
.is_some_and(|fd| {
crate::codegen::lean::toplevel::fuel::detect_simple_string_pos_skip_literal(fd)
.is_some()
});
Some((
g,
WeakRaw {
theorem,
line: vb.line,
universal,
},
))
}
fn parser_law_raw<'a>(
vb: &'a VerifyBlock,
law: &'a VerifyLaw,
ctx: &CodegenContext,
) -> Option<(String, ParserRaw<'a>)> {
let ls = read_law_shape(law)?;
let when = law.when.as_ref()?;
let Expr::BinOp(BinOp::Eq, call, _) = &when.node else {
return None;
};
let call_args: Vec<String> = match &call.node {
Expr::FnCall(_, args) => args
.iter()
.filter_map(|a| ident_name(a).map(str::to_string))
.collect(),
Expr::TailCall(tc) => tc
.args
.iter()
.filter_map(|a| ident_name(a).map(str::to_string))
.collect(),
_ => return None,
};
if call_args.len() < 2 {
return None;
}
let call = format!(
"{} {}",
aver_name_to_lean(&ls.target),
call_args
.iter()
.map(|n| aver_name_to_lean(n))
.collect::<Vec<_>>()
.join(" ")
);
let mut binders = String::new();
let mut arg_names = Vec::new();
for g in &law.givens {
let name = aver_name_to_lean(&g.name);
let ty = type_annotation_to_lean(&g.type_name);
binders.push_str(&format!("({name} : {ty}) "));
arg_names.push(name);
}
let theorem = crate::codegen::lean::toplevel::law_theorem_base(vb, law, ctx);
Some((
ls.target.clone(),
ParserRaw {
theorem,
binders: binders.trim_end().to_string(),
arg_names,
call,
v_name: aver_name_to_lean(&ls.val_name),
p_name: aver_name_to_lean(&ls.p_name),
pos_name: aver_name_to_lean(&ls.pos_name),
ok_ctor: ctor_to_lean(&ls.ok_ctor),
extras_count: call_args.len() - 2,
line: vb.line,
vb,
law,
},
))
}
fn collect_pool_laws<'a>(
ctx: &'a CodegenContext,
) -> (HashMap<String, WeakRaw>, HashMap<String, ParserRaw<'a>>) {
use crate::ast::{TopLevel, VerifyKind};
let mut weak = HashMap::new();
let mut parser = HashMap::new();
for item in &ctx.items {
let TopLevel::Verify(vb) = item else { continue };
let VerifyKind::Law(law) = &vb.kind else {
continue;
};
if let Some((g, raw)) = weak_law_raw(vb, law, ctx) {
weak.entry(g).or_insert(raw);
}
if let Some((f, raw)) = parser_law_raw(vb, law, ctx) {
parser.entry(f).or_insert(raw);
}
}
(weak, parser)
}
const PROBE_DEPTH_TOP: usize = 8;
fn build_shape<'a>(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &'a CodegenContext,
depth: usize,
) -> Option<(CliqueShape<'a>, LawShape)> {
if depth == 0 {
return None;
}
let mut ls = read_law_shape(law)?;
ls.val_type = law
.givens
.iter()
.find(|g| g.name == ls.val_name)
.map(|g| type_annotation_to_lean(&g.type_name))?;
let comp = owning_scc(&ls.target, ctx)?;
let member_names: HashSet<String> = comp.iter().map(|fd| fd.name.clone()).collect();
let (weak_pool, parser_pool) = collect_pool_laws(ctx);
let weak_helpers: HashSet<String> = weak_pool.keys().cloned().collect();
let mut members: Vec<MemberInfo> = Vec::new();
for fd in &comp {
let (s_name, s_ty) = fd.params.first()?.clone();
let (pos_name, pos_ty) = fd.params.get(1)?.clone();
if type_annotation_to_lean(&s_ty) != "String" || type_annotation_to_lean(&pos_ty) != "Int" {
return None;
}
let extras: Vec<(String, String)> = fd.params[2..]
.iter()
.map(|(n, t)| (n.clone(), type_annotation_to_lean(t)))
.collect();
let (shape, edges, dispatch_param) =
analyse_member(fd, &member_names, &ls.ok_ctor, &weak_helpers)?;
members.push(MemberInfo {
fd,
lean: aver_name_to_lean(&fd.name),
s_name,
pos_name,
extras,
shape,
edges,
dispatch_param,
rank: 0,
});
}
let ranks = assign_ranks(&members)?;
for (m, r) in members.iter_mut().zip(ranks) {
m.rank = r;
}
let index_of: HashMap<String, usize> = members
.iter()
.enumerate()
.map(|(i, m)| (m.fd.name.clone(), i))
.collect();
let mut parser_ext: Vec<String> = Vec::new();
let mut weak_ext: Vec<String> = Vec::new();
for m in &members {
for kind in edges_and_externals(m) {
match kind {
EdgeKind::External(f) => {
if !parser_ext.contains(&f) {
parser_ext.push(f);
}
}
EdgeKind::UsesSkipWs(g) => {
if !weak_ext.contains(&g) {
weak_ext.push(g);
}
}
EdgeKind::Sibling => {}
}
}
}
parser_ext.sort();
weak_ext.sort();
let mut citations: Vec<Citation> = Vec::new();
let mut hyp_of: HashMap<String, String> = HashMap::new();
let mut missing: Vec<String> = Vec::new();
for f in &parser_ext {
match parser_pool.get(f) {
Some(raw)
if raw.line < vb.line && build_shape(raw.vb, raw.law, ctx, depth - 1).is_some() =>
{
let hyp = format!("hcite{}", citations.len());
hyp_of.insert(f.clone(), hyp.clone());
citations.push(Citation {
hyp,
src_fn: f.clone(),
kind: CiteKind::Parser {
binders: raw.binders.clone(),
arg_names: raw.arg_names.clone(),
call: raw.call.clone(),
v_name: raw.v_name.clone(),
p_name: raw.p_name.clone(),
pos_name: raw.pos_name.clone(),
ok_ctor: raw.ok_ctor.clone(),
extras_count: raw.extras_count,
theorem: raw.theorem.clone(),
},
});
}
_ => missing.push(f.clone()),
}
}
for g in &weak_ext {
match weak_pool.get(g) {
Some(raw) if raw.line < vb.line && raw.universal => {
let hyp = format!("hcite{}", citations.len());
hyp_of.insert(g.clone(), hyp.clone());
citations.push(Citation {
hyp,
src_fn: g.clone(),
kind: CiteKind::Weak {
g_lean: aver_name_to_lean(g),
theorem: raw.theorem.clone(),
},
});
}
_ => missing.push(g.clone()),
}
}
if !missing.is_empty() {
if depth == PROBE_DEPTH_TOP {
eprintln!(
"aver: clique cursor-monotonicity for `{}` declines — no universal \
advance law in the pool (declared before line {}) for: {}. The law \
keeps its bounded-domain proof.",
ls.target,
vb.line,
missing.join(", ")
);
}
return None;
}
let target_idx = *index_of.get(&vb.fn_name)?;
if !members[target_idx].extras.is_empty() {
return None;
}
let result_type = ls.ok_ctor.rsplit_once('.').map(|(t, _)| t.to_string())?;
let variants = lookup_variants(ctx, &result_type)?;
let rep = members[0].lean.clone();
let r = members.len();
let shape = CliqueShape {
members,
index_of,
rep,
r,
val_type: ls.val_type.clone(),
ok_ctor: ctor_to_lean(&ls.ok_ctor),
variants,
citations,
hyp_of,
};
Some((shape, ls))
}
fn lookup_variants(ctx: &CodegenContext, type_name: &str) -> Option<Vec<(String, usize)>> {
let find = |defs: &[crate::ast::TypeDef]| -> Option<Vec<(String, usize)>> {
defs.iter().find_map(|d| match d {
crate::ast::TypeDef::Sum { name, variants, .. } if name == type_name => Some(
variants
.iter()
.map(|v| {
let mut c = v.name.chars();
let lean = match c.next() {
Some(f) => f.to_lowercase().collect::<String>() + c.as_str(),
None => v.name.clone(),
};
(lean, v.fields.len())
})
.collect(),
),
_ => None,
})
};
find(&ctx.type_defs).or_else(|| ctx.modules.iter().find_map(|m| find(&m.type_defs)))
}
enum EdgeKind {
External(String),
UsesSkipWs(String),
Sibling,
}
fn edges_and_externals(m: &MemberInfo) -> Vec<EdgeKind> {
fn visit(leaf: &Leaf, out: &mut Vec<EdgeKind>) {
match leaf {
Leaf::External { name, pos } => {
out.push(EdgeKind::External(name.clone()));
if let Some(g) = skipws_fn(pos) {
out.push(EdgeKind::UsesSkipWs(g.to_string()));
}
}
Leaf::Sibling { pos, .. } => {
if let Some(g) = skipws_fn(pos) {
out.push(EdgeKind::UsesSkipWs(g.to_string()));
}
out.push(EdgeKind::Sibling);
}
_ => {}
}
}
let mut out = Vec::new();
match &m.shape {
Member::CharAt { some } => match some {
CharSome::Direct(l) => visit(l, &mut out),
CharSome::Nested(ls) => ls.iter().for_each(|l| visit(l, &mut out)),
},
Member::DispatchParam { leaves } => leaves.iter().for_each(|l| visit(l, &mut out)),
Member::ResultMatch {
scrut,
scrut_is_sibling,
edge,
..
} => {
if !scrut_is_sibling {
out.push(EdgeKind::External(scrut.clone()));
}
visit(edge, &mut out);
}
Member::AdtMatch { edge, .. } => visit(edge, &mut out),
}
out
}
fn skipws_fn(pos: &PosClass) -> Option<&str> {
match pos {
PosClass::Weak(h) => Some(h.as_str()),
PosClass::AdvConst { skipws: Some(h) } => Some(h.as_str()),
PosClass::Semantic {
skipws: Some(h), ..
} => Some(h.as_str()),
_ => None,
}
}
pub(in crate::codegen::lean) fn recognize_clique_position_monotonicity(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
) -> bool {
build_shape(vb, law, ctx, PROBE_DEPTH_TOP).is_some()
}
fn proj(i: usize, r: usize) -> String {
if i + 1 == r {
".2".repeat(r - 1)
} else {
format!("{}.1", ".2".repeat(i))
}
}
fn underscores(n: usize) -> String {
std::iter::repeat_n("_", n).collect::<Vec<_>>().join(" ")
}
fn rebind_semantic(leaf: &Leaf, new_binder: &str) -> Leaf {
match leaf {
Leaf::Sibling {
name,
pos: PosClass::Semantic { skipws, .. },
} => Leaf::Sibling {
name: name.clone(),
pos: PosClass::Semantic {
skipws: skipws.clone(),
binder: new_binder.to_string(),
},
},
other => other.clone(),
}
}
fn skipws_inner(class: &PosClass, pos: &str) -> Option<String> {
match class {
PosClass::Weak(_) => Some(pos.to_string()),
PosClass::AdvConst { skipws: Some(_) } => Some(format!("({pos} + 1)")),
PosClass::Semantic {
skipws: Some(_),
binder,
} => Some(binder.clone()),
_ => None,
}
}
fn emit_skipws_bound(
class: &PosClass,
s: &str,
pos: &str,
shape: &CliqueShape,
out: &mut Vec<String>,
indent: &str,
) {
if let (Some(g), Some(inner)) = (skipws_fn(class), skipws_inner(class, pos))
&& let Some(hyp) = shape.hyp_of.get(g)
{
out.push(format!("{indent}have hws := {hyp} {s} {inner}"));
}
}
fn emit_leaf(
leaf: &Leaf,
s: &str,
pos: &str,
shape: &CliqueShape,
t: &str,
out: &mut Vec<String>,
indent: &str,
) {
match leaf {
Leaf::Err => out.push(format!("{indent}exact absurd heq (by simp)")),
Leaf::OkTerminal => {
out.push(format!("{indent}injection heq with _ hp"));
out.push(format!("{indent}omega"));
}
Leaf::Sibling { name, pos: class } => {
let idx = shape.index_of[name];
let callee = &shape.members[idx];
let extras = underscores(callee.extras.len());
let extras = if extras.is_empty() {
String::new()
} else {
format!(" {extras}")
};
let hc = if callee.dispatch_param.is_some() {
" hc"
} else {
""
};
emit_skipws_bound(class, s, pos, shape, out, indent);
let posr = pos_render(class, s, pos);
out.push(format!(
"{indent}have hcall := ih{idx} {s} {posr}{extras} v p{hc} (by unfold {t} at *; omega) heq",
));
out.push(format!("{indent}omega"));
}
Leaf::External { name, pos: class } => {
let (hyp, extras_count) = shape
.parser_cite(name)
.expect("external leaf without a resolved parser citation");
let extras = underscores(extras_count);
let extras = if extras.is_empty() {
String::new()
} else {
format!(" {extras}")
};
emit_skipws_bound(class, s, pos, shape, out, indent);
let posr = pos_render(class, s, pos);
out.push(format!(
"{indent}have hcall := {hyp} {s} {posr}{extras} v p heq"
));
out.push(format!("{indent}omega"));
}
}
}
fn leaves_need_bounds(leaves: &[Leaf]) -> bool {
leaves.iter().any(|l| {
matches!(
l,
Leaf::Sibling {
pos: PosClass::AdvConst { .. },
..
}
)
})
}
fn emit_member_block(
m: &MemberInfo,
i: usize,
shape: &CliqueShape,
t: &str,
out: &mut Vec<String>,
) {
let s = &m.s_name;
let pos = &m.pos_name;
let extra_names: Vec<String> = m.extras.iter().map(|(n, _)| n.clone()).collect();
let extra_str = if extra_names.is_empty() {
String::new()
} else {
format!(" {}", extra_names.join(" "))
};
out.push(format!(" · -- ({i}) {} (rank {})", m.fd.name, m.rank));
let prem = if m.dispatch_param.is_some() {
"hc ht heq"
} else {
"ht heq"
};
out.push(format!(" intro {s} {pos}{extra_str} v p {prem}"));
if let Some(cparam) = &m.dispatch_param {
out.push(format!(
" obtain ⟨h0, hlt⟩ := String.charAt_some_bounds {s} {pos} {cparam} hc"
));
}
out.push(format!(" simp only [{}__fuel] at heq", m.lean));
match &m.shape {
Member::CharAt { some } => {
out.push(" split at heq".to_string());
out.push(" · simp at heq".to_string());
match some {
CharSome::Direct(leaf) => {
out.push(" · next c hc =>".to_string());
emit_leaf(leaf, s, pos, shape, t, out, " ");
}
CharSome::Nested(leaves) => {
out.push(" · next c hc =>".to_string());
if leaves_need_bounds(leaves) {
out.push(format!(
" obtain ⟨h0, hlt⟩ := String.charAt_some_bounds {s} {pos} c hc"
));
}
out.push(" split at heq".to_string());
for leaf in leaves {
out.push(" ·".to_string());
emit_leaf(leaf, s, pos, shape, t, out, " ");
}
}
}
}
Member::DispatchParam { leaves } => {
out.push(" split at heq".to_string());
for leaf in leaves {
out.push(" ·".to_string());
emit_leaf(leaf, s, pos, shape, t, out, " ");
}
}
Member::ResultMatch {
scrut,
scrut_is_sibling,
edge,
err_first,
} => {
out.push(" split at heq".to_string());
let hmono = if *scrut_is_sibling {
let scrut_idx = shape.index_of[scrut];
let scrut_extras = underscores(shape.members[scrut_idx].extras.len());
let scrut_extras = if scrut_extras.is_empty() {
String::new()
} else {
format!(" {scrut_extras}")
};
format!(
" have hmono := ih{scrut_idx} {s} {pos}{scrut_extras} scrutVal scrutPos (by unfold {t} at *; omega) hv"
)
} else {
let (hyp, extras_count) = shape
.parser_cite(scrut)
.expect("external scrutinee without a resolved parser citation");
let scrut_extras = underscores(extras_count);
let scrut_extras = if scrut_extras.is_empty() {
String::new()
} else {
format!(" {scrut_extras}")
};
format!(
" have hmono := {hyp} {s} {pos}{scrut_extras} scrutVal scrutPos hv"
)
};
let edge = rebind_semantic(edge, "scrutPos");
let emit_err = |out: &mut Vec<String>| {
out.push(" · simp at heq".to_string());
};
let emit_ok = |out: &mut Vec<String>| {
out.push(" · next scrutVal scrutPos hv =>".to_string());
out.push(hmono.clone());
emit_leaf(&edge, s, pos, shape, t, out, " ");
};
if *err_first {
emit_err(out);
emit_ok(out);
} else {
emit_ok(out);
emit_err(out);
}
}
Member::AdtMatch {
edge, ctor_first, ..
} => {
out.push(" split at heq".to_string());
let emit_ctor = |out: &mut Vec<String>| {
out.push(" ·".to_string());
emit_leaf(edge, s, pos, shape, t, out, " ");
};
let emit_wild = |out: &mut Vec<String>| {
out.push(" · simp at heq".to_string());
};
if *ctor_first {
emit_ctor(out);
emit_wild(out);
} else {
emit_wild(out);
emit_ctor(out);
}
}
}
}
fn conjunct_statement(m: &MemberInfo, shape: &CliqueShape, t: &str) -> String {
let mut binders = String::from("(s : String) (pos : Int)");
let mut args = String::from("s pos");
for (n, ty) in &m.extras {
binders.push_str(&format!(" ({n} : {ty})"));
args.push_str(&format!(" {n}"));
}
binders.push_str(&format!(" (v : {}) (p : Int)", shape.val_type));
let premise = match &m.dispatch_param {
Some(cparam) => format!("String.charAtAv s pos = some {cparam} -> "),
None => String::new(),
};
let call = format!("{}__fuel fuel {args}", m.lean);
format!(
"(∀ {binders}, {premise}{t} s pos {rank} ≤ fuel -> {call} = {ok} v p -> pos ≤ p)",
rank = m.rank,
ok = shape.ok_ctor,
)
}
fn cite_hyp_type(c: &Citation) -> String {
match &c.kind {
CiteKind::Parser {
binders,
call,
v_name,
p_name,
pos_name,
ok_ctor,
..
} => format!("∀ {binders}, {call} = {ok_ctor} {v_name} {p_name} → {pos_name} ≤ {p_name}"),
CiteKind::Weak { g_lean, .. } => {
format!("∀ (s : String) (q : Int), q ≤ {g_lean} s q")
}
}
}
fn cite_hyp_binder(c: &Citation) -> String {
format!("({} : {})", c.hyp, cite_hyp_type(c))
}
fn emit_cite_have(c: &Citation, indent: &str, out: &mut Vec<String>) {
let ty = cite_hyp_type(c);
out.push(format!("{indent}have {} : {ty} := by", c.hyp));
match &c.kind {
CiteKind::Parser {
arg_names,
call,
v_name,
p_name,
ok_ctor,
theorem,
..
} => {
let args = arg_names.join(" ");
out.push(format!("{indent} intro {args} heqc"));
out.push(format!(
"{indent} have hbc : ({call} == {ok_ctor} {v_name} {p_name}) = true := by"
));
out.push(format!(
"{indent} rw [heqc]; show ({v_name} == {v_name} && {p_name} == {p_name}) = true; simp"
));
out.push(format!("{indent} have hgec := {theorem} {args} hbc"));
out.push(format!("{indent} simpa using hgec"));
}
CiteKind::Weak { theorem, .. } => {
out.push(format!("{indent} intro s q"));
out.push(format!("{indent} simpa using {theorem} s q"));
}
}
}
pub(super) fn emit_clique_position_monotonicity_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
theorem_base: &str,
quant_params: &str,
) -> Option<AutoProof> {
let (shape, ls) = build_shape(vb, law, ctx, PROBE_DEPTH_TOP)?;
let r = shape.r;
let t = format!("{}_advanceThr", shape.rep);
let thm = format!("{}_advance_mono", shape.rep);
let mut lines: Vec<String> = Vec::new();
lines.push(format!(
"def {t} (s : String) (pos : Int) (rank : Nat) : Nat :=\n (s.toList.length - pos.toNat) * {r} + rank + 1"
));
lines.push(String::new());
let conjuncts: Vec<String> = shape
.members
.iter()
.map(|m| conjunct_statement(m, &shape, &t))
.collect();
let hyp_binders = shape
.citations
.iter()
.map(cite_hyp_binder)
.collect::<Vec<_>>()
.join(" ");
if hyp_binders.is_empty() {
lines.push(format!("theorem {thm} :"));
} else {
lines.push(format!("theorem {thm} {hyp_binders} :"));
}
lines.push(" ∀ (fuel : Nat),".to_string());
lines.push(format!(" {} := by", conjuncts.join("\n ∧ ")));
lines.push(" intro fuel".to_string());
lines.push(" first".to_string());
lines.push(" | (induction fuel with".to_string());
let holes = std::iter::repeat_n("?_", r).collect::<Vec<_>>().join(", ");
lines.push(format!(
" | zero => refine ⟨{holes}⟩ <;> (intros; unfold {t} at *; omega)"
));
lines.push(" | succ t ih =>".to_string());
let ih_names = (0..r)
.map(|i| format!("ih{i}"))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(" obtain ⟨{ih_names}⟩ := ih"));
lines.push(format!(" refine ⟨{holes}⟩"));
for (i, m) in shape.members.iter().enumerate() {
let mut block = Vec::new();
emit_member_block(m, i, &shape, &t, &mut block);
for l in block {
lines.push(format!(" {l}"));
}
}
lines.push(" )".to_string());
lines.push(" | sorry".to_string());
lines.push(String::new());
let target_idx = *shape.index_of.get(&vb.fn_name)?;
let target = &shape.members[target_idx];
let target_lean = aver_name_to_lean(&vb.fn_name);
let render = |e: &Spanned<Expr>| super::super::expr::emit_expr_legacy(e, ctx, None);
let when_render = law.when.as_ref().map(&render)?;
let lhs_render = render(&law.lhs);
let rhs_render = render(&law.rhs);
let proj_acc = proj(target_idx, r);
if !target.extras.is_empty() {
return None;
}
let s_name = ls_s_name(law);
let pos_name = &ls.pos_name;
let v_name = &ls.val_name;
let p_name = &ls.p_name;
let ok_variant = shape
.ok_ctor
.rsplit_once('.')
.map(|(_, v)| v)
.unwrap_or(&shape.ok_ctor);
lines.push(format!(
"theorem {theorem_base} : ∀ {quant_params}, {when_render} = true -> {lhs_render} = {rhs_render} := by"
));
lines.push(format!(
" intro {} hbeq",
law.givens
.iter()
.map(|g| aver_name_to_lean(&g.name))
.collect::<Vec<_>>()
.join(" ")
));
let hyp_args = if shape.citations.is_empty() {
String::new()
} else {
format!(
"{} ",
shape
.citations
.iter()
.map(|c| c.hyp.clone())
.collect::<Vec<_>>()
.join(" ")
)
};
lines.push(" first".to_string());
if shape.citations.is_empty() {
lines.push(format!(
" | (cases hF : {target_lean} {s_name} {pos_name} with"
));
} else {
lines.push(" | (".to_string());
for c in &shape.citations {
emit_cite_have(c, " ", &mut lines);
}
lines.push(format!(
" cases hF : {target_lean} {s_name} {pos_name} with"
));
}
for (variant, arity) in &shape.variants {
if variant == ok_variant {
lines.push(format!(" | {variant} okVal okPos =>"));
lines.push(" rw [hF] at hbeq".to_string());
lines.push(format!(
" have hh : (okVal == {v_name} && okPos == {p_name}) = true := hbeq"
));
lines.push(" simp only [Bool.and_eq_true] at hh".to_string());
lines.push(format!(
" have hpp : okPos = {p_name} := eq_of_beq hh.2"
));
lines.push(" rw [hpp] at hF".to_string());
lines.push(format!(
" have key := ({thm} {hyp_args}(averStringPosFuel {s_name} {pos_name} {r})){proj_acc} {s_name} {pos_name} okVal {p_name} (by unfold {t} averStringPosFuel; omega) hF"
));
lines.push(" simp [key]".to_string());
} else {
let binders = (0..*arity)
.map(|k| format!("e{k}"))
.collect::<Vec<_>>()
.join(" ");
let binders = if binders.is_empty() {
String::new()
} else {
format!(" {binders}")
};
lines.push(format!(
" | {variant}{binders} => rw [hF] at hbeq; nomatch hbeq"
));
}
}
lines.push(" )".to_string());
lines.push(" | sorry".to_string());
Some(AutoProof {
support_lines: lines,
body: crate::codegen::lean::tactic_ir::Tactic::raw(Vec::new()),
replaces_theorem: true,
})
}
fn ls_s_name(law: &VerifyLaw) -> String {
if let Some(when) = &law.when
&& let Expr::BinOp(BinOp::Eq, call, _) = &when.node
&& let Expr::FnCall(_, args) = &call.node
&& let Some(first) = args.first()
&& let Expr::Ident(n) = &first.node
{
return n.clone();
}
"s".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinOp, Expr, Literal, Spanned};
use std::collections::HashSet;
fn ident(n: &str) -> Spanned<Expr> {
Spanned::bare(Expr::Ident(n.to_string()))
}
fn call(name: &str, args: Vec<Spanned<Expr>>) -> Spanned<Expr> {
Spanned::bare(Expr::FnCall(Box::new(ident(name)), args))
}
fn add1(inner: Spanned<Expr>) -> Spanned<Expr> {
Spanned::bare(Expr::BinOp(
BinOp::Add,
Box::new(inner),
Box::new(Spanned::bare(Expr::Literal(Literal::Int(1)))),
))
}
fn weak_set(names: &[&str]) -> HashSet<String> {
names.iter().map(|n| n.to_string()).collect()
}
#[test]
fn classify_pos_same_and_advance() {
let no_binders = HashSet::new();
let no_weak = HashSet::new();
assert_eq!(
classify_pos(&ident("pos"), "s", "pos", &no_binders, &no_weak),
Some(PosClass::Same)
);
assert_eq!(
classify_pos(&add1(ident("pos")), "s", "pos", &no_binders, &no_weak),
Some(PosClass::AdvConst { skipws: None })
);
}
#[test]
fn classify_pos_weak_helper_is_pool_law_keyed_not_name_keyed() {
let no_binders = HashSet::new();
let weak = weak_set(&["trimWs"]);
assert_eq!(
classify_pos(
&call("trimWs", vec![ident("s"), ident("pos")]),
"s",
"pos",
&no_binders,
&weak
),
Some(PosClass::Weak("trimWs".to_string()))
);
assert_eq!(
classify_pos(
&call("trimWs", vec![ident("s"), add1(ident("pos"))]),
"s",
"pos",
&no_binders,
&weak
),
Some(PosClass::AdvConst {
skipws: Some("trimWs".to_string())
})
);
let empty = HashSet::new();
assert_eq!(
classify_pos(
&call("trimWs", vec![ident("s"), ident("pos")]),
"s",
"pos",
&no_binders,
&empty
),
None
);
assert_eq!(
classify_pos(
&call("skipWs", vec![ident("s"), ident("pos")]),
"s",
"pos",
&no_binders,
&empty
),
None
);
}
#[test]
fn classify_pos_semantic_from_result_binder() {
let mut binders = HashSet::new();
binders.insert("p2".to_string());
let weak = weak_set(&["skipWs"]);
assert_eq!(
classify_pos(&ident("p2"), "s", "pos", &binders, &weak),
Some(PosClass::Semantic {
skipws: None,
binder: "p2".to_string()
})
);
assert_eq!(
classify_pos(
&call("skipWs", vec![ident("s"), ident("p2")]),
"s",
"pos",
&binders,
&weak
),
Some(PosClass::Semantic {
skipws: Some("skipWs".to_string()),
binder: "p2".to_string()
})
);
assert_eq!(
classify_pos(&ident("acc"), "s", "pos", &binders, &weak),
None
);
}
#[test]
fn rank_dag_reproduces_json_clique_table() {
let edges = [
(1, 0), (2, 3), (3, 1), (3, 4), (5, 6), (11, 1), (11, 10), (8, 9), (7, 8), ];
let ranks = rank_dag(12, &edges).expect("acyclic");
let expected = [0, 1, 3, 2, 0, 1, 0, 2, 1, 0, 0, 2];
assert_eq!(ranks, expected, "ranks must match the design table");
}
#[test]
fn rank_dag_rejects_cycle_fail_closed() {
let edges = [(0, 1), (1, 2), (2, 0)];
assert_eq!(rank_dag(3, &edges), None);
}
#[test]
fn rank_dag_witness_three_clique() {
let edges = [(1, 0), (1, 2)];
let ranks = rank_dag(3, &edges).expect("acyclic");
assert_eq!(ranks, [0, 1, 0]);
}
}