use std::collections::BTreeSet;
use super::super::expr::aver_name_to_lean;
use crate::ast::{
BinOp, Expr, FnBody, FnDef, Literal, Spanned, Stmt, TopLevel, VerifyBlock, VerifyKind,
VerifyLaw,
};
use crate::ast_rewrite::rewrite_idents_scoped;
use crate::codegen::CodegenContext;
pub(super) fn render(e: &Spanned<Expr>, ctx: &CodegenContext) -> String {
super::super::expr::emit_expr_legacy(e, ctx, None)
}
pub(super) fn same_atom(a: &Spanned<Expr>, b: &Spanned<Expr>, ctx: &CodegenContext) -> bool {
render(a, ctx) == render(b, ctx)
}
pub(super) fn floor_call<'a>(
e: &'a Spanned<Expr>,
floor_src: &str,
) -> Option<(&'a Spanned<Expr>, &'a Spanned<Expr>)> {
let Expr::FnCall(callee, args) = &e.node else {
return None;
};
if expr_dotted_name(callee).as_deref() != Some(floor_src) || args.len() != 2 {
return None;
}
Some((&args[0], &args[1]))
}
pub(super) fn is_euclidean_floor_fn(floor_src: &str, ctx: &CodegenContext) -> bool {
let Some(fd) = find_fn_def_by_call_name(ctx, floor_src) else {
return false;
};
if !fd.effects.is_empty() || fd.params.len() != 2 {
return false;
}
let [Stmt::Expr(body)] = fd.body.stmts() else {
return false;
};
let Expr::FnCall(callee, args) = &body.node else {
return false;
};
if expr_dotted_name(callee).as_deref() != Some("Result.withDefault") || args.len() != 2 {
return false;
}
if !matches!(&args[1].node, Expr::Literal(Literal::Int(0))) {
return false;
}
let Expr::FnCall(div_callee, div_args) = &args[0].node else {
return false;
};
expr_dotted_name(div_callee).as_deref() == Some("Int.div") && div_args.len() == 2
}
pub(super) fn flatten_and(e: &Spanned<Expr>, out: &mut Vec<Spanned<Expr>>) {
match &e.node {
Expr::FnCall(callee, args)
if expr_dotted_name(callee).as_deref() == Some("Bool.and") && args.len() == 2 =>
{
flatten_and(&args[0], out);
flatten_and(&args[1], out);
}
_ => out.push(crate::codegen::common::canonicalize_comparison(e)),
}
}
pub(super) fn collect_when_clauses(e: &Spanned<Expr>) -> Vec<Spanned<Expr>> {
let mut out = Vec::new();
flatten_and(e, &mut out);
out
}
pub(super) fn call_name_args(e: &Spanned<Expr>) -> Option<(String, &[Spanned<Expr>])> {
match &e.node {
Expr::FnCall(callee, args) => Some((expr_dotted_name(callee)?, args.as_slice())),
Expr::TailCall(tc) => Some((tc.target.clone(), tc.args.as_slice())),
_ => None,
}
}
pub(super) fn short_call_name_args(e: &Spanned<Expr>) -> Option<(String, &[Spanned<Expr>])> {
let (dotted, args) = call_name_args(e)?;
let short = dotted.rsplit('.').next().unwrap_or(&dotted).to_string();
Some((short, args))
}
pub(super) fn call_named<'a>(
expr: &'a Spanned<Expr>,
name: &str,
n: usize,
) -> Option<&'a [Spanned<Expr>]> {
let (short, args) = short_call_name_args(expr)?;
(short == name && args.len() == n).then_some(args)
}
pub(super) fn call_named_with_dotted<'a>(
expr: &'a Spanned<Expr>,
name: &str,
n: usize,
) -> Option<(String, &'a [Spanned<Expr>])> {
let (dotted, args) = call_name_args(expr)?;
let short = dotted.rsplit('.').next().unwrap_or(&dotted);
(short == name && args.len() == n).then_some((dotted, args))
}
pub(super) fn call_qualified<'a>(
expr: &'a Spanned<Expr>,
qual: &str,
n: usize,
) -> Option<&'a [Spanned<Expr>]> {
let (dotted, args) = call_name_args(expr)?;
(dotted == qual && args.len() == n).then_some(args)
}
pub(super) fn ident_name(expr: &Spanned<Expr>) -> Option<&str> {
match &expr.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.as_str()),
_ => None,
}
}
pub(super) fn is_ident(expr: &Spanned<Expr>, name: &str) -> bool {
ident_name(expr) == Some(name)
}
pub(super) fn child_exprs(e: &Spanned<Expr>) -> Vec<&Spanned<Expr>> {
match &e.node {
Expr::FnCall(callee, args) => {
let mut v = vec![callee.as_ref()];
v.extend(args.iter());
v
}
Expr::BinOp(_, l, r) => vec![l.as_ref(), r.as_ref()],
Expr::Neg(inner) | Expr::ErrorProp(inner) => vec![inner.as_ref()],
Expr::Attr(base, _) => vec![base.as_ref()],
Expr::Constructor(_, Some(inner)) => vec![inner.as_ref()],
Expr::Match { subject, arms } => {
let mut v = vec![subject.as_ref()];
v.extend(arms.iter().map(|a| a.body.as_ref()));
v
}
Expr::List(items) | Expr::Tuple(items) => items.iter().collect(),
Expr::TailCall(tc) => tc.args.iter().collect(),
_ => Vec::new(),
}
}
pub(super) fn collect_fncall_names(e: &Expr, out: &mut Vec<String>) {
match e {
Expr::FnCall(callee, args) => {
if let Some(n) = expr_dotted_name(callee) {
out.push(n);
}
for a in args {
collect_fncall_names(&a.node, out);
}
}
Expr::TailCall(tc) => {
out.push(tc.target.clone());
for a in &tc.args {
collect_fncall_names(&a.node, out);
}
}
Expr::BinOp(_, a, b) => {
collect_fncall_names(&a.node, out);
collect_fncall_names(&b.node, out);
}
Expr::Neg(a) => collect_fncall_names(&a.node, out),
Expr::Attr(b, _) => collect_fncall_names(&b.node, out),
Expr::RecordCreate { fields, .. } => {
for (_, v) in fields {
collect_fncall_names(&v.node, out);
}
}
Expr::Match { subject, arms } => {
collect_fncall_names(&subject.node, out);
for arm in arms {
collect_fncall_names(&arm.body.node, out);
}
}
_ => {}
}
}
fn short_call_tree_children(e: &Spanned<Expr>) -> Vec<&Spanned<Expr>> {
match &e.node {
Expr::FnCall(_, args) => args.iter().collect(),
Expr::TailCall(tc) => tc.args.iter().collect(),
Expr::Attr(b, _) | Expr::Neg(b) => vec![b.as_ref()],
Expr::BinOp(_, l, r) => vec![l.as_ref(), r.as_ref()],
Expr::Constructor(_, Some(inner)) => vec![inner.as_ref()],
_ => Vec::new(),
}
}
pub(super) fn collect_short_callees(expr: &Spanned<Expr>, out: &mut BTreeSet<String>) {
if let Some((short, _)) = short_call_name_args(expr) {
out.insert(short);
}
for child in short_call_tree_children(expr) {
collect_short_callees(child, out);
}
}
pub(super) fn find_map_short_call_tree<T, F>(expr: &Spanned<Expr>, f: &mut F) -> Option<T>
where
F: FnMut(&str, &[Spanned<Expr>]) -> Option<T>,
{
if let Some((short, args)) = short_call_name_args(expr)
&& let Some(found) = f(&short, args)
{
return Some(found);
}
short_call_tree_children(expr)
.into_iter()
.find_map(|child| find_map_short_call_tree(child, f))
}
pub(super) fn call_on_binder(
expr: &Spanned<Expr>,
field: &str,
ctx: &CodegenContext,
) -> Option<String> {
let mut found: Option<String> = None;
fn walk(e: &Spanned<Expr>, field: &str, ctx: &CodegenContext, found: &mut Option<String>) {
if let Some((name, args)) = call_name_args(e)
&& args.len() == 1
&& is_ident(&args[0], field)
&& find_fn_def(ctx, &name).is_some()
{
*found = Some(name);
}
for child in child_exprs(e) {
walk(child, field, ctx, found);
}
}
walk(expr, field, ctx, &mut found);
found
}
pub(super) fn calls_fn_on_ident(expr: &Spanned<Expr>, fn_src: &str, ident: &str) -> bool {
if let Some((name, args)) = call_name_args(expr)
&& args.len() == 1
&& is_ident(&args[0], ident)
&& name == fn_src
{
return true;
}
child_exprs(expr)
.into_iter()
.any(|c| calls_fn_on_ident(c, fn_src, ident))
}
pub(super) fn direct_user_calls(src: &str, ctx: &CodegenContext) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let Some(fd) = find_fn_def(ctx, src) else {
return out;
};
fn walk(e: &Spanned<Expr>, ctx: &CodegenContext, out: &mut BTreeSet<String>) {
if let Some((name, _)) = call_name_args(e)
&& let Some(fd) = find_fn_def(ctx, &name)
{
out.insert(fd.name.clone());
}
for c in child_exprs(e) {
walk(c, ctx, out);
}
}
for stmt in fd.body.stmts() {
match stmt {
Stmt::Expr(e) | Stmt::Binding(_, _, e) => walk(e, ctx, &mut out),
}
}
out
}
pub(super) fn clause_gives_pos(
clause: &Spanned<Expr>,
x_render: &str,
ctx: &CodegenContext,
) -> bool {
let Expr::BinOp(op, l, r) = &clause.node else {
return false;
};
let int_lit = |e: &Spanned<Expr>| match &e.node {
Expr::Literal(Literal::Int(n)) => Some(*n),
_ => None,
};
match op {
BinOp::Lt => int_lit(l).is_some_and(|c| c >= 0) && render(r, ctx) == x_render,
BinOp::Lte => int_lit(l).is_some_and(|c| c >= 1) && render(r, ctx) == x_render,
_ => false,
}
}
pub(super) fn clause_gives_nonneg(
clause: &Spanned<Expr>,
x_render: &str,
ctx: &CodegenContext,
) -> bool {
if clause_gives_pos(clause, x_render, ctx) {
return true;
}
let Expr::BinOp(op, l, r) = &clause.node else {
return false;
};
let int_lit = |e: &Spanned<Expr>| match &e.node {
Expr::Literal(Literal::Int(n)) => Some(*n),
_ => None,
};
match op {
BinOp::Lte => int_lit(l).is_some_and(|c| c >= 0) && render(r, ctx) == x_render,
_ => false,
}
}
pub(super) enum PositivityFact {
Decide,
MulPos(Box<PositivityFact>, Box<PositivityFact>),
CitedLaw { citation: String },
WhenGuard,
}
impl PositivityFact {
pub(super) fn needs_int_ascription(&self) -> bool {
match self {
PositivityFact::Decide => true,
PositivityFact::MulPos(l, r) => l.needs_int_ascription() && r.needs_int_ascription(),
PositivityFact::CitedLaw { .. } | PositivityFact::WhenGuard => false,
}
}
pub(super) fn lean_term(&self) -> String {
match self {
PositivityFact::Decide => "by decide".to_string(),
PositivityFact::WhenGuard => "by omega".to_string(),
PositivityFact::CitedLaw { citation } => {
format!(
"by have hpos := {citation}; \
simp only [ge_iff_le, eq_iff_iff, iff_true] at hpos; omega"
)
}
PositivityFact::MulPos(l, r) => {
format!("Int.mul_pos ({}) ({})", l.lean_term(), r.lean_term())
}
}
}
}
pub(super) fn divisor_positivity(
atom: &Spanned<Expr>,
clauses: &[Spanned<Expr>],
ctx: &CodegenContext,
before_line: usize,
) -> Option<PositivityFact> {
let atom_render = render(atom, ctx);
if clauses
.iter()
.any(|cl| clause_gives_pos(cl, &atom_render, ctx))
{
return Some(PositivityFact::WhenGuard);
}
if let Expr::Literal(Literal::Int(n)) = &atom.node {
return (*n > 0).then_some(PositivityFact::Decide);
}
if let Expr::BinOp(BinOp::Mul, x, y) = &atom.node {
let fx = divisor_positivity(x, clauses, ctx, before_line)?;
let fy = divisor_positivity(y, clauses, ctx, before_line)?;
return Some(PositivityFact::MulPos(Box::new(fx), Box::new(fy)));
}
cited_positivity_law(atom, ctx, before_line)
.map(|citation| PositivityFact::CitedLaw { citation })
}
fn cited_positivity_law(
atom: &Spanned<Expr>,
ctx: &CodegenContext,
before_line: usize,
) -> Option<String> {
let (f_src, args) = call_name_args(atom)?;
if args.len() != 1 {
return None;
}
let f_short = f_src.rsplit('.').next().unwrap_or(&f_src);
for vb in citable_pool_blocks(ctx, before_line) {
let VerifyKind::Law(law) = &vb.kind else {
continue;
};
if vb.fn_name.rsplit('.').next().unwrap_or(&vb.fn_name) != f_short {
continue;
}
if !law_claims_positive_over(law, f_short) {
continue;
}
if !super::recursive_mono::recognize_recursive_positive(vb, law, ctx) {
continue;
}
let base = format!(
"{}_law_{}",
aver_name_to_lean(&vb.fn_name),
aver_name_to_lean(&law.name)
);
return Some(format!("{base} ({})", render(&args[0], ctx)));
}
None
}
fn citable_pool_blocks(ctx: &CodegenContext, before_line: usize) -> Vec<&VerifyBlock> {
let mut out: Vec<&VerifyBlock> = Vec::new();
for module in &ctx.modules {
out.extend(module.verify_laws.iter());
}
for item in &ctx.items {
if let TopLevel::Verify(b) = item
&& b.line < before_line
{
out.push(b);
}
}
out
}
fn law_claims_positive_over(law: &VerifyLaw, f_short: &str) -> bool {
if law.when.is_some() {
return false;
}
let [binder] = law.givens.as_slice() else {
return false;
};
if !matches!(law.rhs.node, Expr::Literal(Literal::Bool(true))) {
return false;
}
let int_lit = |e: &Spanned<Expr>| match &e.node {
Expr::Literal(Literal::Int(n)) => Some(*n),
_ => None,
};
let is_f_of_binder = |e: &Spanned<Expr>| -> bool {
matches!(call_name_args(e), Some((n, a))
if n.rsplit('.').next().unwrap_or(&n) == f_short
&& a.len() == 1
&& ident_name(&a[0]) == Some(binder.name.as_str()))
};
match &law.lhs.node {
Expr::BinOp(BinOp::Gte, l, r) => is_f_of_binder(l) && int_lit(r).is_some_and(|b| b >= 1),
Expr::BinOp(BinOp::Lte, l, r) => int_lit(l).is_some_and(|b| b >= 1) && is_f_of_binder(r),
_ => false,
}
}
pub(super) fn clause_is_lt(
clause: &Spanned<Expr>,
x_render: &str,
y_render: &str,
ctx: &CodegenContext,
) -> bool {
let Expr::BinOp(BinOp::Lt, l, r) = &clause.node else {
return false;
};
render(l, ctx) == x_render && render(r, ctx) == y_render
}
pub(super) fn body_terminal_expr(body: &FnBody) -> Option<&Spanned<Expr>> {
match body.stmts() {
[Stmt::Expr(expr)] => Some(expr),
_ => None,
}
}
pub(super) fn substitute_expr(
expr: &Spanned<Expr>,
bindings: &std::collections::HashMap<&str, &Spanned<Expr>>,
) -> Spanned<Expr> {
rewrite_idents_scoped(expr, |name| bindings.get(name).map(|v| (*v).clone()))
}
pub(super) fn law_simp_defs(
ctx: &CodegenContext,
vb: &VerifyBlock,
law: &VerifyLaw,
) -> BTreeSet<String> {
law_simp_source_names(ctx, vb, law)
.into_iter()
.map(|name| {
let rendered = aver_name_to_lean(&name);
if is_dep_module_fn(ctx, &name) {
rendered
} else {
format!("_root_.{rendered}")
}
})
.collect()
}
fn is_dep_module_fn(ctx: &CodegenContext, source_name: &str) -> bool {
ctx.modules
.iter()
.any(|m| m.fn_defs.iter().any(|fd| fd.name == source_name))
}
pub(super) fn law_simp_source_names(
ctx: &CodegenContext,
vb: &VerifyBlock,
law: &VerifyLaw,
) -> BTreeSet<String> {
let mut names = BTreeSet::new();
names.insert(vb.fn_name.clone());
collect_user_fn_simp_names(&law.lhs, ctx, &vb.fn_name, &mut names);
collect_user_fn_simp_names(&law.rhs, ctx, &vb.fn_name, &mut names);
if let Some(when_expr) = &law.when {
collect_user_fn_simp_names(when_expr, ctx, &vb.fn_name, &mut names);
}
expand_pure_fn_simp_names(ctx, &vb.fn_name, &mut names);
names
}
fn expand_pure_fn_simp_names(ctx: &CodegenContext, skip_fn: &str, out: &mut BTreeSet<String>) {
loop {
let before = out.len();
let current = out.iter().cloned().collect::<Vec<_>>();
for name in current {
let Some(fd) = find_fn_def(ctx, &name) else {
continue;
};
if !fd.effects.is_empty() || fd.name == "main" {
continue;
}
for stmt in fd.body.stmts() {
match stmt {
Stmt::Expr(expr) | Stmt::Binding(_, _, expr) => {
collect_user_fn_simp_names(expr, ctx, skip_fn, out);
}
}
}
}
if out.len() == before {
return;
}
}
}
fn collect_user_fn_simp_names(
expr: &Spanned<Expr>,
ctx: &CodegenContext,
skip_fn: &str,
out: &mut BTreeSet<String>,
) {
match &expr.node {
Expr::FnCall(callee, args) => {
if let Some(name) = expr_dotted_name(callee)
&& let Some(fd) = find_fn_def_by_call_name(ctx, &name)
&& fd.effects.is_empty()
&& fd.name != "main"
&& fd.name != skip_fn
{
out.insert(fd.name.clone());
}
collect_user_fn_simp_names(callee, ctx, skip_fn, out);
for arg in args {
collect_user_fn_simp_names(arg, ctx, skip_fn, out);
}
}
Expr::Attr(base, _) => collect_user_fn_simp_names(base, ctx, skip_fn, out),
Expr::BinOp(_, l, r) => {
collect_user_fn_simp_names(l, ctx, skip_fn, out);
collect_user_fn_simp_names(r, ctx, skip_fn, out);
}
Expr::Neg(inner) => collect_user_fn_simp_names(inner, ctx, skip_fn, out),
Expr::Match { subject, arms, .. } => {
collect_user_fn_simp_names(subject, ctx, skip_fn, out);
for arm in arms {
collect_user_fn_simp_names(&arm.body, ctx, skip_fn, out);
}
}
Expr::Constructor(_, inner) => {
if let Some(inner) = inner {
collect_user_fn_simp_names(inner, ctx, skip_fn, out);
}
}
Expr::ErrorProp(inner) => collect_user_fn_simp_names(inner, ctx, skip_fn, out),
Expr::InterpolatedStr(parts) => {
for part in parts {
if let crate::ast::StrPart::Parsed(inner) = part {
collect_user_fn_simp_names(inner, ctx, skip_fn, out);
}
}
}
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
for item in items {
collect_user_fn_simp_names(item, ctx, skip_fn, out);
}
}
Expr::MapLiteral(entries) => {
for (k, v) in entries {
collect_user_fn_simp_names(k, ctx, skip_fn, out);
collect_user_fn_simp_names(v, ctx, skip_fn, out);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, v) in fields {
collect_user_fn_simp_names(v, ctx, skip_fn, out);
}
}
Expr::RecordUpdate { base, updates, .. } => {
collect_user_fn_simp_names(base, ctx, skip_fn, out);
for (_, v) in updates {
collect_user_fn_simp_names(v, ctx, skip_fn, out);
}
}
Expr::TailCall(call) => {
if let Some(fd) = find_fn_def_by_call_name(ctx, &call.target)
&& fd.effects.is_empty()
&& fd.name != "main"
&& fd.name != skip_fn
{
out.insert(fd.name.clone());
}
for arg in &call.args {
collect_user_fn_simp_names(arg, ctx, skip_fn, out);
}
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
}
}
pub(super) fn find_fn_def<'a>(ctx: &'a CodegenContext, fn_name: &str) -> Option<&'a FnDef> {
ctx.modules
.iter()
.flat_map(|m| m.fn_defs.iter())
.chain(ctx.fn_defs.iter())
.find(|fd| fd.name == fn_name)
}
pub(super) fn find_fn_def_by_call_name<'a>(
ctx: &'a CodegenContext,
call_name: &str,
) -> Option<&'a FnDef> {
find_fn_def(ctx, call_name).or_else(|| {
let short = call_name.rsplit('.').next()?;
find_fn_def(ctx, short)
})
}
pub(super) fn expr_dotted_name(expr: &Spanned<Expr>) -> Option<String> {
match &expr.node {
Expr::Ident(name) | Expr::Resolved { name, .. } => Some(name.clone()),
Expr::Attr(base, field) => expr_dotted_name(base).map(|p| format!("{p}.{field}")),
_ => None,
}
}
pub(super) fn callee_matches_name(expr: &Spanned<Expr>, target: &str) -> bool {
let Some(name) = expr_dotted_name(expr) else {
return false;
};
name == target
}