use crate::ast::*;
use crate::codegen::CodegenContext;
use crate::codegen::common::parse_type_annotation;
use crate::types::Type;
use super::expr::{aver_name_to_dafny, emit_expr_legacy};
fn bounded_oracle_predicate_for(method: &str) -> Option<&'static str> {
match method {
"Random.int" => Some("IsRandomIntInBounds"),
"Random.float" => Some("IsRandomFloatInUnit"),
"Time.unixMs" => Some("IsTimeUnixMsNonneg"),
_ => None,
}
}
pub fn emit_type(type_str: &str) -> String {
type_to_dafny(&parse_type_annotation(type_str))
}
pub fn emit_type_from(ty: &Type) -> String {
type_to_dafny(ty)
}
fn resolved_view_for_emit<'a>(
fd: &'a FnDef,
ctx: &'a CodegenContext,
) -> std::borrow::Cow<'a, crate::ir::hir::ResolvedFnDef> {
let canonical = crate::codegen::common::fn_id_for_decl(ctx, fd)
.and_then(|id| ctx.resolved_program.fn_by_id(id))
.filter(|rfd| rfd.params.len() == fd.params.len());
if let Some(rfd) = canonical {
return std::borrow::Cow::Borrowed(rfd);
}
let module_name = ctx.items.iter().find_map(|i| match i {
TopLevel::Module(m) => Some(m.name.clone()),
_ => None,
});
let mut rctx = crate::ir::hir::ResolveCtx::new(&ctx.symbol_table);
rctx.current_module = module_name;
if let Some(lifted) = crate::ir::hir::resolve_fn_def_external(&rctx, fd) {
return std::borrow::Cow::Owned(lifted);
}
ctx.resolve_fn_def(fd, None)
}
pub fn type_ref_to_dafny(ty: &Type) -> String {
type_to_dafny(ty)
}
fn type_to_dafny(ty: &Type) -> String {
match ty {
Type::Int => "int".to_string(),
Type::Float => "real".to_string(),
Type::Str => "string".to_string(),
Type::Bool => "bool".to_string(),
Type::Unit => "()".to_string(),
Type::List(inner) => format!("seq<{}>", type_to_dafny(inner)),
Type::Vector(inner) => format!("seq<{}>", type_to_dafny(inner)),
Type::Map(k, v) if crate::codegen::common::is_set_type(ty) => {
format!("set<{}>", type_to_dafny(k))
}
Type::Map(k, v) => format!("map<{}, {}>", type_to_dafny(k), type_to_dafny(v)),
Type::Result(ok, err) => format!("Result<{}, {}>", type_to_dafny(ok), type_to_dafny(err)),
Type::Option(inner) => format!("Option<{}>", type_to_dafny(inner)),
Type::Tuple(items) => {
let parts: Vec<String> = items.iter().map(type_to_dafny).collect();
format!("({})", parts.join(", "))
}
Type::Fn(params, ret, _) => {
let parts: Vec<String> = params.iter().map(type_to_dafny).collect();
let ret_ty = type_to_dafny(ret);
if parts.len() == 1 {
format!("{} -> {}", parts[0], ret_ty)
} else {
format!("({}) -> {}", parts.join(", "), ret_ty)
}
}
Type::Named { name, .. } => {
if crate::codegen::builtin_records::find(name).is_some() {
name.replace('.', "_")
} else if let Some(dot) = name.rfind('.') {
let module_part = &name[..dot];
let local = &name[dot + 1..];
format!("Aver_{}.{}", module_part.replace('.', "_"), local)
} else {
name.to_string()
}
}
Type::Var(_) | Type::Invalid => "/* unknown type */".to_string(),
}
}
fn literal_int_value(expr: &Spanned<Expr>) -> Option<String> {
match &expr.node {
Expr::Literal(Literal::Int(n)) => Some(n.to_string()),
Expr::Neg(inner) => {
let inner_str = literal_int_value(inner)?;
Some(format!("-{inner_str}"))
}
_ => None,
}
}
pub fn emit_type_def(td: &TypeDef, ctx: &CodegenContext) -> Option<String> {
emit_type_def_in_scope(td, ctx, None)
}
pub fn emit_type_def_in_scope(
td: &TypeDef,
ctx: &CodegenContext,
scope: Option<&str>,
) -> Option<String> {
match td {
TypeDef::Sum { name, variants, .. } => {
let variant_strs: Vec<String> = variants
.iter()
.map(|v| {
if v.fields.is_empty() {
v.name.clone()
} else {
let prefix = crate::codegen::common::to_lower_first(&v.name);
let fields: Vec<String> = v
.fields
.iter()
.enumerate()
.map(|(i, f)| format!("{}_{}: {}", prefix, i, emit_type(f)))
.collect();
format!("{}({})", v.name, fields.join(", "))
}
})
.collect();
Some(format!(
"datatype {} = {}\n",
name,
variant_strs.join(" | ")
))
}
TypeDef::Product { name, fields, .. } => {
if let Some(decl) = crate::codegen::common::find_refined_type_scoped(ctx, name, scope)
&& decl.carrier_type == "Int"
{
let predicate = super::expr::emit_expr(&decl.invariant.expr, ctx);
let bind = aver_name_to_dafny(&decl.predicate_param);
let witness = decl.witness.clone().unwrap_or_else(|| "0".to_string());
return Some(format!(
"type {name} = {bind}: int | {predicate} witness {witness}\n"
));
}
let field_strs: Vec<String> = fields
.iter()
.map(|(fname, ftype)| {
format!("{}: {}", aver_name_to_dafny(fname), emit_type(ftype))
})
.collect();
Some(format!(
"datatype {} = {}({})\n",
name,
name,
field_strs.join(", ")
))
}
}
}
pub fn emit_fn_def_axiom(fd: &FnDef, ctx: &CodegenContext) -> String {
let name = aver_name_to_dafny(&fd.name);
let rfd_holder = resolved_view_for_emit(fd, ctx);
let rfd: &crate::ir::hir::ResolvedFnDef = rfd_holder.as_ref();
let params: Vec<String> = rfd
.params
.iter()
.map(|(pname, ptype)| format!("{}: {}", aver_name_to_dafny(pname), emit_type_from(ptype)))
.collect();
let ret_type = emit_type_from(&rfd.return_type);
let mut lines = Vec::new();
if let Some(desc) = &fd.desc {
lines.push(format!("// {}", desc));
}
lines.push(
"// Axiom: recursion pattern outside Dafny proof subset (emitted opaque)".to_string(),
);
lines.push(format!(
"function {{:axiom}} {}({}): {}\n",
name,
params.join(", "),
ret_type,
));
lines.join("\n")
}
pub fn emit_fn_def(fd: &FnDef, ctx: &CodegenContext) -> String {
let name = aver_name_to_dafny(&fd.name);
let rfd_holder = resolved_view_for_emit(fd, ctx);
let rfd: &crate::ir::hir::ResolvedFnDef = rfd_holder.as_ref();
let params: Vec<String> = rfd
.params
.iter()
.map(|(pname, ptype)| format!("{}: {}", aver_name_to_dafny(pname), emit_type_from(ptype)))
.collect();
let ret_type = emit_type_from(&rfd.return_type);
let lowered = lower_pure_question_bang_for_emit(fd);
let body_ast = lowered
.as_ref()
.map(|lowered_fd| lowered_fd.body.as_ref())
.unwrap_or(fd.body.as_ref());
let body = emit_fn_body(body_ast, ctx);
let needs_decreases = body_has_recursive_call(body_ast, &fd.name);
let mut lines = Vec::new();
if let Some(desc) = &fd.desc {
lines.push(format!("// {}", desc));
}
lines.push(format!(
"function {}({}): {}",
name,
params.join(", "),
ret_type
));
if needs_decreases && let Some(info) = infer_decreases(fd) {
for req in &info.requires {
lines.push(format!(" requires {}", req));
}
lines.push(format!(" decreases {}", info.expr));
}
lines.push("{".to_string());
lines.push(format!(" {}", body));
lines.push("}\n".to_string());
lines.join("\n")
}
fn lower_pure_question_bang_for_emit(fd: &FnDef) -> Option<FnDef> {
crate::types::checker::effect_lifting::lower_pure_question_bang_fn(fd)
.ok()
.flatten()
}
pub(super) fn emit_fn_body(body: &FnBody, ctx: &CodegenContext) -> String {
match body {
FnBody::Block(stmts) => emit_block_as_expr(stmts, ctx),
}
}
fn emit_block_as_expr(stmts: &[Stmt], ctx: &CodegenContext) -> String {
if stmts.is_empty() {
return "()".to_string();
}
if stmts.len() == 1
&& let Stmt::Expr(expr) = &stmts[0]
{
return emit_expr_legacy(expr, ctx, None);
}
let mut parts = Vec::new();
let mut final_expr = None;
for (i, stmt) in stmts.iter().enumerate() {
match stmt {
Stmt::Binding(name, type_ann, expr) => {
let mut val = emit_expr_legacy(expr, ctx, None);
if let Some(ann) = type_ann
&& crate::codegen::common::is_set_annotation(ann)
&& val == "map[]"
{
val = "{}".to_string();
}
parts.push((aver_name_to_dafny(name), val));
}
Stmt::Expr(expr) => {
if i == stmts.len() - 1 {
final_expr = Some(emit_expr_legacy(expr, ctx, None));
}
}
}
}
if let Some(final_e) = final_expr {
if parts.is_empty() {
final_e
} else {
let mut result = final_e;
for (name, val) in parts.into_iter().rev() {
result = format!("var {} := {}; {}", name, val, result);
}
result
}
} else {
"()".to_string()
}
}
struct SelfCallChanges {
preserved: std::collections::HashSet<String>,
incremented: std::collections::HashSet<String>,
saw_call: bool,
}
impl SelfCallChanges {
fn preserved_to(&self, name: &str) -> bool {
self.saw_call && self.preserved.contains(name)
}
fn incremented(&self, name: &str) -> bool {
self.saw_call && self.incremented.contains(name)
}
}
fn analyse_self_call_args(fd: &FnDef) -> SelfCallChanges {
let mut state = SelfCallChanges {
preserved: fd.params.iter().map(|(n, _)| n.clone()).collect(),
incremented: fd.params.iter().map(|(n, _)| n.clone()).collect(),
saw_call: false,
};
let formals: Vec<(String, String)> = fd.params.clone();
walk_self_call_args(fd.body.as_ref(), &fd.name, &formals, &mut state);
state
}
fn walk_self_call_args(
body: &FnBody,
fn_name: &str,
formals: &[(String, String)],
state: &mut SelfCallChanges,
) {
let FnBody::Block(stmts) = body;
for stmt in stmts {
match stmt {
Stmt::Binding(_, _, expr) | Stmt::Expr(expr) => {
walk_self_call_args_expr(expr, fn_name, formals, state);
}
}
}
}
fn walk_self_call_args_expr(
expr: &Spanned<Expr>,
fn_name: &str,
formals: &[(String, String)],
state: &mut SelfCallChanges,
) {
match &expr.node {
Expr::FnCall(callee, args) => {
let is_self = matches!(&callee.node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == fn_name);
if is_self && args.len() == formals.len() {
record_self_call(args, formals, state);
}
walk_self_call_args_expr(callee, fn_name, formals, state);
for a in args {
walk_self_call_args_expr(a, fn_name, formals, state);
}
}
Expr::TailCall(call) if call.target == fn_name && call.args.len() == formals.len() => {
record_self_call(&call.args, formals, state);
for a in &call.args {
walk_self_call_args_expr(a, fn_name, formals, state);
}
}
Expr::TailCall(call) => {
for a in &call.args {
walk_self_call_args_expr(a, fn_name, formals, state);
}
}
Expr::BinOp(_, l, r) => {
walk_self_call_args_expr(l, fn_name, formals, state);
walk_self_call_args_expr(r, fn_name, formals, state);
}
Expr::Attr(b, _) | Expr::Neg(b) | Expr::ErrorProp(b) => {
walk_self_call_args_expr(b, fn_name, formals, state);
}
Expr::Match { subject, arms } => {
walk_self_call_args_expr(subject, fn_name, formals, state);
for arm in arms {
walk_self_call_args_expr(&arm.body, fn_name, formals, state);
}
}
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
for it in items {
walk_self_call_args_expr(it, fn_name, formals, state);
}
}
Expr::Constructor(_, Some(inner)) => {
walk_self_call_args_expr(inner, fn_name, formals, state);
}
_ => {}
}
}
fn record_self_call(
args: &[Spanned<Expr>],
formals: &[(String, String)],
state: &mut SelfCallChanges,
) {
state.saw_call = true;
for (i, (pname, _)) in formals.iter().enumerate() {
let arg = &args[i].node;
let preserved_here = matches!(
arg,
Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == pname
);
if !preserved_here {
state.preserved.remove(pname);
}
let incremented_here = match arg {
Expr::BinOp(BinOp::Add, l, r) => {
is_param_plus_positive_lit(&l.node, &r.node, pname)
|| is_param_plus_positive_lit(&r.node, &l.node, pname)
}
_ => false,
};
if !incremented_here {
state.incremented.remove(pname);
}
}
}
fn is_param_plus_positive_lit(maybe_param: &Expr, maybe_lit: &Expr, pname: &str) -> bool {
let same = matches!(maybe_param, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == pname);
let positive = matches!(maybe_lit, Expr::Literal(Literal::Int(k)) if *k > 0);
same && positive
}
fn body_has_recursive_call(body: &FnBody, fn_name: &str) -> bool {
match body {
FnBody::Block(stmts) => stmts.iter().any(|s| match s {
Stmt::Binding(_, _, expr) => expr_has_call(expr, fn_name),
Stmt::Expr(expr) => expr_has_call(expr, fn_name),
}),
}
}
fn expr_has_call(expr: &Spanned<Expr>, fn_name: &str) -> bool {
match &expr.node {
Expr::FnCall(fn_expr, args) => {
if let Expr::Ident(name) = &fn_expr.node
&& name == fn_name
{
return true;
}
expr_has_call(fn_expr, fn_name) || args.iter().any(|a| expr_has_call(a, fn_name))
}
Expr::TailCall(inner) => {
let TailCallData {
target: name, args, ..
} = inner.as_ref();
name == fn_name || args.iter().any(|a| expr_has_call(a, fn_name))
}
Expr::BinOp(_, l, r) => expr_has_call(l, fn_name) || expr_has_call(r, fn_name),
Expr::Match { subject, arms, .. } => {
expr_has_call(subject, fn_name)
|| arms.iter().any(|arm| expr_has_call(&arm.body, fn_name))
}
Expr::List(elems) => elems.iter().any(|e| expr_has_call(e, fn_name)),
Expr::Tuple(elems) => elems.iter().any(|e| expr_has_call(e, fn_name)),
Expr::MapLiteral(entries) => entries
.iter()
.any(|(k, v)| expr_has_call(k, fn_name) || expr_has_call(v, fn_name)),
Expr::Constructor(_, arg) => arg.as_ref().is_some_and(|a| expr_has_call(a, fn_name)),
Expr::Attr(obj, _) => expr_has_call(obj, fn_name),
Expr::ErrorProp(inner) => expr_has_call(inner, fn_name),
Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
StrPart::Parsed(e) => expr_has_call(e, fn_name),
_ => false,
}),
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, e)| expr_has_call(e, fn_name)),
Expr::RecordUpdate { base, updates, .. } => {
expr_has_call(base, fn_name) || updates.iter().any(|(_, e)| expr_has_call(e, fn_name))
}
_ => false,
}
}
struct DecreasesInfo {
expr: String,
requires: Vec<String>,
}
fn infer_decreases(fd: &FnDef) -> Option<DecreasesInfo> {
let changes = analyse_self_call_args(fd);
let collection_param_any = fd
.params
.iter()
.find(|(_, t)| t.starts_with("List<") || t == "String");
let incrementing_int = fd
.params
.iter()
.find(|(name, t)| t == "Int" && changes.incremented(name));
if let (Some((list_name, _)), Some((int_name, _))) = (collection_param_any, incrementing_int) {
let dlist = aver_name_to_dafny(list_name);
let dint = aver_name_to_dafny(int_name);
return Some(DecreasesInfo {
expr: format!("|{}| - {}", dlist, dint),
requires: vec![],
});
}
for (pname, ptype) in &fd.params {
if ptype.starts_with("List<") && !changes.preserved_to(pname) {
return Some(DecreasesInfo {
expr: format!("|{}|", aver_name_to_dafny(pname)),
requires: vec![],
});
}
}
for (pname, ptype) in &fd.params {
if ptype == "String" && !changes.preserved_to(pname) {
return Some(DecreasesInfo {
expr: format!("|{}|", aver_name_to_dafny(pname)),
requires: vec![],
});
}
}
for (pname, ptype) in &fd.params {
if ptype == "Int" {
let dname = aver_name_to_dafny(pname);
if fn_handles_negative_first(fd, pname) {
return Some(DecreasesInfo {
expr: format!("if {} >= 0 then {} else 0", dname, dname),
requires: vec![],
});
}
return Some(DecreasesInfo {
expr: dname.clone(),
requires: vec![format!("{} >= 0", dname)],
});
}
}
None
}
fn fn_handles_negative_first(fd: &FnDef, pname: &str) -> bool {
let Some(first) = fd.body.stmts().first() else {
return false;
};
let expr = match first {
Stmt::Expr(e) => e,
Stmt::Binding(_, _, _) => return false,
};
let Expr::Match { subject, .. } = &expr.node else {
return false;
};
let Expr::BinOp(op, lhs, rhs) = &subject.node else {
return false;
};
let lhs_name = match &lhs.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => n,
_ => return false,
};
if lhs_name != pname {
return false;
}
let Expr::Literal(crate::ast::Literal::Int(rhs_val)) = &rhs.node else {
return false;
};
use crate::ast::BinOp;
matches!(
(op, *rhs_val),
(BinOp::Lt, 0) | (BinOp::Lte, 0) | (BinOp::Lt, 1)
)
}
fn collect_called_fns(expr: &Spanned<Expr>, out: &mut std::collections::BTreeSet<String>) {
match &expr.node {
Expr::FnCall(f, args) => {
if let Some(name) = crate::codegen::common::expr_to_dotted_name(&f.node) {
if !name.contains('.') {
out.insert(name);
}
}
collect_called_fns(f, out);
for a in args {
collect_called_fns(a, out);
}
}
Expr::BinOp(_, l, r) => {
collect_called_fns(l, out);
collect_called_fns(r, out);
}
Expr::Match { subject, arms, .. } => {
collect_called_fns(subject, out);
for arm in arms {
collect_called_fns(&arm.body, out);
}
}
Expr::ErrorProp(inner) => collect_called_fns(inner, out),
Expr::Constructor(_, Some(arg)) => collect_called_fns(arg, out),
Expr::RecordCreate { fields, .. } => {
for (_, e) in fields {
collect_called_fns(e, out);
}
}
Expr::List(elems) => {
for e in elems {
collect_called_fns(e, out);
}
}
Expr::TailCall(tc) => {
let TailCallData { target, args, .. } = tc.as_ref();
if !target.contains('.') {
out.insert(target.clone());
}
for a in args {
collect_called_fns(a, out);
}
}
Expr::Tuple(elems) | Expr::IndependentProduct(elems, _) => {
for e in elems {
collect_called_fns(e, out);
}
}
Expr::Attr(obj, _) => collect_called_fns(obj, out),
Expr::Neg(inner) => collect_called_fns(inner, out),
_ => {}
}
}
fn law_top_level_fn(expr: &Spanned<Expr>) -> Option<String> {
match &expr.node {
Expr::FnCall(fn_expr, _) => crate::codegen::common::expr_to_dotted_name(&fn_expr.node),
_ => None,
}
}
fn is_directly_recursive(fn_name: &str, ctx: &CodegenContext) -> bool {
ctx.fn_defs
.iter()
.any(|fd| fd.name == fn_name && body_has_recursive_call(&fd.body, &fd.name))
}
fn count_recursive_calls(expr: &Spanned<Expr>, fn_name: &str) -> usize {
match &expr.node {
Expr::FnCall(fn_expr, args) => {
let self_call = if let Expr::Ident(name) = &fn_expr.node {
if name == fn_name { 1 } else { 0 }
} else {
0
};
self_call
+ count_recursive_calls(fn_expr, fn_name)
+ args
.iter()
.map(|a| count_recursive_calls(a, fn_name))
.sum::<usize>()
}
Expr::TailCall(inner) => {
let TailCallData {
target: name, args, ..
} = inner.as_ref();
let self_call = if name == fn_name { 1 } else { 0 };
self_call
+ args
.iter()
.map(|a| count_recursive_calls(a, fn_name))
.sum::<usize>()
}
Expr::BinOp(_, l, r) => {
count_recursive_calls(l, fn_name) + count_recursive_calls(r, fn_name)
}
Expr::Match { subject, arms, .. } => {
let subj = count_recursive_calls(subject, fn_name);
let arm_max = arms
.iter()
.map(|arm| count_recursive_calls(&arm.body, fn_name))
.max()
.unwrap_or(0);
subj + arm_max
}
_ => 0,
}
}
fn count_recursive_calls_in_body(body: &FnBody, fn_name: &str) -> usize {
match body {
FnBody::Block(stmts) => stmts
.iter()
.map(|s| match s {
Stmt::Binding(_, _, expr) => count_recursive_calls(expr, fn_name),
Stmt::Expr(expr) => count_recursive_calls(expr, fn_name),
})
.sum(),
}
}
fn collect_called_fns_in_body(body: &FnBody, out: &mut std::collections::BTreeSet<String>) {
match body {
FnBody::Block(stmts) => {
for stmt in stmts {
match stmt {
Stmt::Binding(_, _, expr) => collect_called_fns(expr, out),
Stmt::Expr(expr) => collect_called_fns(expr, out),
}
}
}
}
}
const MAX_LAW_SAMPLES: usize = 5;
const SAMPLE_CLOSABLE_LITERAL_LIMIT: i64 = 999_999_999;
fn sample_within_closable_range(bindings: &[(String, Spanned<Expr>)]) -> bool {
bindings.iter().all(|(_, v)| match literal_int_value(v) {
Some(s) => s
.parse::<i64>()
.map(|n| n.abs() <= SAMPLE_CLOSABLE_LITERAL_LIMIT)
.unwrap_or(false),
None => true,
})
}
pub fn emit_law_samples(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
suffix: &str,
opaque_fns: &std::collections::HashSet<crate::ir::FnId>,
fuel_emitted: &std::collections::HashSet<crate::ir::FnId>,
native_emitted: &std::collections::HashSet<crate::ir::FnId>,
) -> Option<String> {
if vb.cases.is_empty() {
return None;
}
if crate::codegen::common::law_lhs_has_trace_projection(&law.lhs) {
return None;
}
let fn_name = aver_name_to_dafny(&vb.fn_name);
let law_name = aver_name_to_dafny(&law.name);
let first_rewrite = vb.cases.first().map(|(lhs, rhs)| {
let case_bindings = vb.case_givens.first().map(|v| v.as_slice()).unwrap_or(&[]);
let mode = OracleInjectionMode::SampleCaseBinding(case_bindings);
(
rewrite_effectful_calls_in_law(
lhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
mode.clone(),
),
rewrite_effectful_calls_in_law(
rhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
mode,
),
)
});
let any_opaque = first_rewrite
.as_ref()
.map(|(l, r)| {
law_refs_opaque_fn(l, ctx, opaque_fns) || law_refs_opaque_fn(r, ctx, opaque_fns)
})
.unwrap_or(false);
let any_native_mutual = first_rewrite
.as_ref()
.map(|(l, r)| {
law_refs_opaque_fn(l, ctx, native_emitted) || law_refs_opaque_fn(r, ctx, native_emitted)
})
.unwrap_or(false);
let needs_bounded_form = any_opaque || any_native_mutual;
let bounded_universal_targets = !law.givens.is_empty()
&& law.givens.iter().all(|g| {
g.type_name == "Int"
&& matches!(
&g.domain,
VerifyGivenDomain::Explicit(vs)
if vs.iter().all(|v| literal_int_value(v).is_some())
)
});
let cap = if needs_bounded_form && bounded_universal_targets {
vb.cases.len()
} else {
MAX_LAW_SAMPLES
};
let samples: Vec<_> = vb.cases.iter().take(cap).collect();
let truncated = vb.cases.len() > cap;
let rewritten: Vec<(Spanned<Expr>, Spanned<Expr>)> = samples
.iter()
.enumerate()
.map(|(idx, (lhs, rhs))| {
let case_bindings = vb.case_givens.get(idx).map(|v| v.as_slice()).unwrap_or(&[]);
let mode = OracleInjectionMode::SampleCaseBinding(case_bindings);
let lhs_rw = rewrite_effectful_calls_in_law(
lhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
mode.clone(),
);
let rhs_rw = rewrite_effectful_calls_in_law(
rhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
mode,
);
(lhs_rw, rhs_rw)
})
.collect();
let mut lines = Vec::new();
if truncated {
lines.push(format!(
"// Sample assertions for {}.{} ({} of {} from given domain)",
fn_name,
law_name,
samples.len(),
vb.cases.len()
));
} else {
lines.push(format!(
"// Sample assertions for {}.{} (from given domain)",
fn_name, law_name
));
}
if needs_bounded_form {
let known: std::collections::HashSet<String> = ctx
.items
.iter()
.filter_map(|i| {
if let TopLevel::FnDef(fd) = i {
Some(fd.name.clone())
} else {
None
}
})
.chain(
ctx.modules
.iter()
.flat_map(|m| m.fn_defs.iter().map(|fd| fd.name.clone())),
)
.collect();
for (idx, (lhs_rw, rhs_rw)) in rewritten.iter().enumerate() {
let l = emit_expr_legacy(lhs_rw, ctx, None);
let r = emit_expr_legacy(rhs_rw, ctx, None);
let mut callees = std::collections::BTreeSet::new();
collect_called_fns(lhs_rw, &mut callees);
collect_called_fns(rhs_rw, &mut callees);
let mut changed = true;
while changed {
changed = false;
let snapshot: Vec<String> = callees.iter().cloned().collect();
for f in &snapshot {
if let Some(fd) = ctx
.items
.iter()
.filter_map(|i| {
if let TopLevel::FnDef(fd) = i {
Some(fd)
} else {
None
}
})
.chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
.find(|fd| &fd.name == f)
{
let before = callees.len();
collect_called_fns_in_body(&fd.body, &mut callees);
if callees.len() != before {
changed = true;
}
}
}
}
let mut fuel_targets: Vec<String> = Vec::new();
for f in &callees {
if !known.contains(f) {
continue;
}
fuel_targets.push(aver_name_to_dafny(f));
if crate::codegen::common::fn_id_for_dotted_name(ctx, f)
.is_some_and(|id| fuel_emitted.contains(&id))
{
fuel_targets.push(crate::codegen::recursion::fuel_helper_name(f));
}
}
fuel_targets.sort();
fuel_targets.dedup();
let fuel_attrs = fuel_targets
.iter()
.map(|f| format!("{{:fuel {}, 100}}", f))
.collect::<Vec<_>>()
.join(" ");
let bindings = vb.case_givens.get(idx).map(|v| v.as_slice()).unwrap_or(&[]);
let all_native = callees.iter().all(|f| {
!crate::codegen::common::fn_id_for_dotted_name(ctx, f)
.is_some_and(|id| fuel_emitted.contains(&id))
});
let body = if all_native || sample_within_closable_range(bindings) {
"{ }".to_string()
} else {
format!("{{\n assume {{:axiom}} {} == {};\n}}", l, r)
};
lines.push(format!(
"lemma {} {}_{}{}__sample_{}()\n ensures {} == {}\n{}",
fuel_attrs,
fn_name,
law_name,
suffix,
idx + 1,
l,
r,
body
));
}
} else {
lines.push(format!(
"method test_{}_{}{}_samples() {{",
fn_name, law_name, suffix
));
for (lhs_rw, rhs_rw) in &rewritten {
let l = emit_expr_legacy(lhs_rw, ctx, None);
let r = emit_expr_legacy(rhs_rw, ctx, None);
lines.push(format!(" assert {{:split_here}} {} == {};", l, r));
}
lines.push("}\n".to_string());
}
Some(lines.join("\n"))
}
use crate::codegen::common::{OracleInjectionMode, rewrite_effectful_calls_in_law};
pub fn transitive_opaque_closure(
ctx: &CodegenContext,
opaque: &std::collections::HashSet<crate::ir::FnId>,
) -> std::collections::HashSet<crate::ir::FnId> {
let mut result = opaque.clone();
let all_fns: Vec<&FnDef> = ctx
.items
.iter()
.filter_map(|it| {
if let TopLevel::FnDef(fd) = it {
Some(fd)
} else {
None
}
})
.chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
.collect();
let mut changed = true;
while changed {
changed = false;
for fd in &all_fns {
let Some(fd_id) = crate::codegen::common::fn_id_for_decl(ctx, fd) else {
continue;
};
if result.contains(&fd_id) {
continue;
}
let mut callees = std::collections::BTreeSet::new();
collect_called_fns_in_body(&fd.body, &mut callees);
let hits = callees.iter().any(|name| {
crate::codegen::common::fn_id_for_dotted_name(ctx, name)
.is_some_and(|id| result.contains(&id))
});
if hits {
result.insert(fd_id);
changed = true;
}
}
}
result
}
fn law_refs_opaque_fn(
expr: &Spanned<Expr>,
ctx: &CodegenContext,
opaque: &std::collections::HashSet<crate::ir::FnId>,
) -> bool {
match &expr.node {
Expr::FnCall(callee, args) => {
let hits_callee = crate::codegen::common::expr_to_dotted_name(&callee.node)
.and_then(|n| crate::codegen::common::fn_id_for_dotted_name(ctx, &n))
.is_some_and(|id| opaque.contains(&id));
hits_callee
|| law_refs_opaque_fn(callee, ctx, opaque)
|| args.iter().any(|a| law_refs_opaque_fn(a, ctx, opaque))
}
Expr::BinOp(_, l, r) => {
law_refs_opaque_fn(l, ctx, opaque) || law_refs_opaque_fn(r, ctx, opaque)
}
Expr::Match { subject, arms } => {
law_refs_opaque_fn(subject, ctx, opaque)
|| arms
.iter()
.any(|a| law_refs_opaque_fn(&a.body, ctx, opaque))
}
Expr::Attr(inner, _) | Expr::ErrorProp(inner) => law_refs_opaque_fn(inner, ctx, opaque),
Expr::Constructor(_, Some(inner)) => law_refs_opaque_fn(inner, ctx, opaque),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(|i| law_refs_opaque_fn(i, ctx, opaque))
}
Expr::RecordCreate { fields, .. } => fields
.iter()
.any(|(_, v)| law_refs_opaque_fn(v, ctx, opaque)),
Expr::RecordUpdate { base, updates, .. } => {
law_refs_opaque_fn(base, ctx, opaque)
|| updates
.iter()
.any(|(_, v)| law_refs_opaque_fn(v, ctx, opaque))
}
Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
StrPart::Parsed(inner) => law_refs_opaque_fn(inner, ctx, opaque),
_ => false,
}),
_ => false,
}
}
fn emit_linear_recurrence2_support_stack(
impl_fn: &str,
spec_fn: &str,
helper_fn: &str,
impl_dafny: &str,
law_name_dafny: &str,
) -> String {
let impl_d = aver_name_to_dafny(impl_fn);
let spec_d = aver_name_to_dafny(spec_fn);
let helper_d = aver_name_to_dafny(helper_fn);
let spec_nat = format!("{spec_d}__nat");
let worker_nat = format!("{helper_d}__natWorker");
let theorem_base = format!("{impl_dafny}_{law_name_dafny}");
let shift_thm = format!("{theorem_base}__worker_nat_shift");
let helper_nat_thm = format!("{theorem_base}__helper_nat");
let helper_seed_thm = format!("{theorem_base}__helper_seed");
let spec_bridge_thm = format!("{theorem_base}__spec_nat_bridge");
let mut lines = Vec::new();
lines.push(format!(
"// Law: {impl_fn}.{spec_fn} — recurrence support stack"
));
lines.push(format!(
"function {spec_nat}(n: nat): int {{ if n == 0 then 0 else if n == 1 then 1 else {spec_nat}(n - 1) + {spec_nat}(n - 2) }}"
));
lines.push(format!(
"function {worker_nat}(k: nat, a: int, b: int): int {{ if k == 0 then a else {worker_nat}(k - 1, b, a + b) }}"
));
lines.push(format!(
"lemma {shift_thm}(k: nat, i: nat)\n ensures {worker_nat}(k, {spec_nat}(i), {spec_nat}(i + 1)) == {spec_nat}(i + k)\n{{\n if k == 0 {{\n }} else {{\n {shift_thm}(k - 1, i + 1);\n }}\n}}"
));
lines.push(format!(
"lemma {{:fuel {helper_d}, 100}} {helper_nat_thm}(k: nat, a: int, b: int)\n ensures {helper_d}(k as int, a, b) == {worker_nat}(k, a, b)\n{{\n if k == 0 {{\n }} else {{\n {helper_nat_thm}(k - 1, b, a + b);\n }}\n}}"
));
lines.push(format!(
"lemma {helper_seed_thm}(k: nat)\n ensures {helper_d}(k as int, 0, 1) == {spec_nat}(k)\n{{\n {helper_nat_thm}(k, 0, 1);\n {shift_thm}(k, 0);\n}}"
));
lines.push(format!(
"lemma {{:fuel {spec_d}, 100}} {spec_bridge_thm}(k: nat)\n ensures {spec_d}(k as int) == {spec_nat}(k)\n{{\n if k == 0 {{\n }} else if k == 1 {{\n }} else {{\n {spec_bridge_thm}(k - 1);\n {spec_bridge_thm}(k - 2);\n }}\n}}"
));
lines.push(format!(
"lemma {{:fuel {impl_d}, 100}} {theorem_base}(n: int)\n ensures {impl_d}(n) == {spec_d}(n)\n{{\n if n < 0 {{\n }} else {{\n var k := n as nat;\n {helper_seed_thm}(k);\n {spec_bridge_thm}(k);\n }}\n}}\n"
));
lines.join("\n")
}
pub fn emit_verify_law(
vb: &VerifyBlock,
law: &VerifyLaw,
ctx: &CodegenContext,
opaque_fns: &std::collections::HashSet<crate::ir::FnId>,
native_emitted: &std::collections::HashSet<crate::ir::FnId>,
suffix: &str,
) -> String {
let fn_name = aver_name_to_dafny(&vb.fn_name);
let law_name = aver_name_to_dafny(&law.name);
if crate::codegen::common::law_lhs_has_trace_projection(&law.lhs) {
return format!(
"// Law {}.{}{}: trace-projection LHS is runtime-only (see docs/oracle.md)",
fn_name, law_name, suffix,
);
}
let vb_fn_id = ctx
.symbol_table
.fn_id_of(&crate::ir::FnKey::entry(&vb.fn_name));
let ir_strategy_closes_const_rhs = vb_fn_id
.and_then(|fn_id| {
ctx.proof_ir
.law_theorems
.iter()
.find(|t| t.fn_id == fn_id && t.law_name == law.name)
})
.is_some_and(|t| {
!matches!(
t.strategy,
crate::ir::ProofStrategy::Induction { .. }
| crate::ir::ProofStrategy::BackendDispatch
| crate::ir::ProofStrategy::Sorry
)
});
let singleton_const_rhs = !ir_strategy_closes_const_rhs
&& crate::codegen::common::all_givens_are_singletons(law)
&& crate::codegen::common::law_rhs_is_independent_of_givens(law);
let unclassified = crate::codegen::common::unclassified_fn_names(ctx);
let calls_fuel_bounded = crate::codegen::common::law_calls_unclassified_fn(law, &unclassified);
if singleton_const_rhs || calls_fuel_bounded {
let reason = if singleton_const_rhs {
"singleton-domain givens with constant RHS"
} else {
"calls a fuel-bounded fn outside the proof subset"
};
return format!(
"// Law {}.{}{}: {}, sample-only (universal lemma omitted)",
fn_name, law_name, suffix, reason,
);
}
if let Some(crate::ir::ProofStrategy::LinearRecurrence2SpecEquivalence {
impl_fn,
spec_fn,
helper_fn,
}) = vb_fn_id
.and_then(|fn_id| {
ctx.proof_ir
.law_theorems
.iter()
.find(|t| t.fn_id == fn_id && t.law_name == law.name)
})
.map(|t| t.strategy.clone())
{
return emit_linear_recurrence2_support_stack(
&impl_fn, &spec_fn, &helper_fn, &fn_name, &law_name,
);
}
let mut lifted_vars: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for g in &law.givens {
if let Some(refined) = crate::codegen::common::refinement_lift_for_given(
&g.name,
&g.type_name,
&law.lhs,
&law.rhs,
ctx,
) {
lifted_vars.insert(g.name.clone(), refined.to_string());
}
}
let params: Vec<String> = law
.givens
.iter()
.map(|g| {
if let Some(refined) = lifted_vars.get(&g.name) {
let display = match refined.rsplit_once('.') {
Some((prefix, bare)) => {
format!("{}.{}", super::dafny_module_name(prefix), bare)
}
None => refined.clone(),
};
return format!("{}: {}", aver_name_to_dafny(&g.name), display);
}
let type_text = match crate::types::checker::effect_classification::oracle_signature(
&g.type_name,
) {
Some(oracle_ty) => type_ref_to_dafny(&oracle_ty),
None => emit_type(&g.type_name),
};
format!("{}: {}", aver_name_to_dafny(&g.name), type_text)
})
.collect();
let law_lhs = rewrite_effectful_calls_in_law(
&law.lhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
OracleInjectionMode::LemmaBinding,
);
let law_rhs = rewrite_effectful_calls_in_law(
&law.rhs,
law,
|n| ctx.fn_def_by_name(n, ctx.active_module_scope().as_deref()),
OracleInjectionMode::LemmaBinding,
);
let (law_lhs, law_rhs) = if lifted_vars.is_empty() {
(law_lhs, law_rhs)
} else {
(
crate::codegen::common::strip_refinement_wrappers(&law_lhs, &lifted_vars, ctx),
crate::codegen::common::strip_refinement_wrappers(&law_rhs, &lifted_vars, ctx),
)
};
let lhs = emit_expr_legacy(&law_lhs, ctx, None);
let rhs = emit_expr_legacy(&law_rhs, ctx, None);
let mut lines = Vec::new();
let mut law_fns = std::collections::BTreeSet::new();
collect_called_fns(&law.lhs, &mut law_fns);
collect_called_fns(&law.rhs, &mut law_fns);
let mut transitive_fns = std::collections::BTreeSet::new();
for f in &law_fns {
if let Some(fd) = ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref()) {
collect_called_fns_in_body(&fd.body, &mut transitive_fns);
}
}
law_fns.extend(transitive_fns);
let fuel_attrs: String = law_fns
.iter()
.filter(|f| {
ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref())
.is_some()
})
.map(|f| format!("{{:fuel {}, 5}}", aver_name_to_dafny(f)))
.collect::<Vec<_>>()
.join(" ");
lines.push(format!("// Law: {}.{}", fn_name, law_name));
if fuel_attrs.is_empty() {
lines.push(format!(
"lemma {}_{}({})",
fn_name,
law_name,
params.join(", ")
));
} else {
lines.push(format!(
"lemma {} {}_{}({})",
fuel_attrs,
fn_name,
law_name,
params.join(", ")
));
}
for given in &law.givens {
if let Some(pred) = bounded_oracle_predicate_for(&given.type_name) {
let oracle_name = aver_name_to_dafny(&given.name);
lines.push(format!(" requires {}({})", pred, oracle_name));
}
}
if let Some(when_expr) = &law.when {
let when_redundant = crate::codegen::common::when_is_redundant_with_refinement_lifts(
when_expr,
&lifted_vars,
ctx,
);
if !when_redundant {
let when_str = emit_expr_legacy(when_expr, ctx, None);
lines.push(format!(" requires {}", when_str));
}
}
let is_opaque = law_refs_opaque_fn(&law.lhs, ctx, opaque_fns)
|| law_refs_opaque_fn(&law.rhs, ctx, opaque_fns);
let is_native_mutual = law_refs_opaque_fn(&law.lhs, ctx, native_emitted)
|| law_refs_opaque_fn(&law.rhs, ctx, native_emitted);
let needs_bounded_form = is_opaque || is_native_mutual;
let all_explicit_int = !law.givens.is_empty()
&& law.givens.iter().all(|g| {
(g.type_name == "Int" || lifted_vars.contains_key(&g.name))
&& matches!(
&g.domain,
VerifyGivenDomain::Explicit(vs)
if vs.iter().all(|v| literal_int_value(v).is_some())
)
});
if needs_bounded_form && all_explicit_int {
for given in &law.givens {
let values = match &given.domain {
VerifyGivenDomain::Explicit(vs) => vs,
_ => unreachable!(),
};
let n = aver_name_to_dafny(&given.name);
let disj = values
.iter()
.map(|v| format!("{} == {}", n, literal_int_value(v).unwrap()))
.collect::<Vec<_>>()
.join(" || ");
lines.push(format!(" requires {}", disj));
}
}
lines.push(format!(" ensures {} == {}", lhs, rhs));
lines.push("{".to_string());
if needs_bounded_form {
if all_explicit_int {
for (idx, _) in vb.cases.iter().enumerate() {
let Some(bindings) = vb.case_givens.get(idx) else {
continue;
};
let guard = bindings
.iter()
.map(|(n, v)| {
let val =
literal_int_value(v).unwrap_or_else(|| emit_expr_legacy(v, ctx, None));
format!("{} == {}", aver_name_to_dafny(n), val)
})
.collect::<Vec<_>>()
.join(" && ");
let sample_name = format!("{}_{}{}__sample_{}", fn_name, law_name, suffix, idx + 1);
lines.push(format!(" if {} {{ {}(); }}", guard, sample_name));
}
lines.push("}\n".to_string());
return lines.join("\n");
}
lines.push(format!(" assume {{:axiom}} {} == {};", lhs, rhs));
lines.push("}\n".to_string());
return lines.join("\n");
}
if law.givens.len() == 1 && law.givens[0].type_name == "Int" {
let param = aver_name_to_dafny(&law.givens[0].name);
let lemma_name = format!("{}_{}", fn_name, law_name);
let lhs_fn = law_top_level_fn(&law.lhs);
let rhs_fn = law_top_level_fn(&law.rhs);
let lhs_recursive = lhs_fn
.as_ref()
.is_some_and(|f| is_directly_recursive(f, ctx));
let rhs_recursive = rhs_fn
.as_ref()
.is_some_and(|f| is_directly_recursive(f, ctx));
if lhs_recursive || rhs_recursive {
let has_double = [&lhs_fn, &rhs_fn].iter().any(|opt| {
opt.as_ref().is_some_and(|f| {
ctx.fn_def_by_name(f, ctx.active_module_scope().as_deref())
.is_some_and(|fd| count_recursive_calls_in_body(&fd.body, &fd.name) >= 2)
})
});
lines.push(format!(" if {} < 0 {{", param));
lines.push(format!(" }} else if {} == 0 {{", param));
lines.push(format!(" }} else if {} == 1 {{", param));
lines.push(" } else {".to_string());
lines.push(format!(" {}({} - 1);", lemma_name, param));
if has_double {
lines.push(format!(" {}({} - 2);", lemma_name, param));
}
lines.push(" }".to_string());
}
} else if let Some(list_given_idx) = law
.givens
.iter()
.position(|g| g.type_name.starts_with("List<") || g.type_name == "String")
{
let mut called: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
collect_called_fns(&law.lhs, &mut called);
collect_called_fns(&law.rhs, &mut called);
for f in called.clone() {
if let Some(fd) = ctx.fn_def_by_name(&f, ctx.active_module_scope().as_deref()) {
collect_called_fns_in_body(&fd.body, &mut called);
}
}
let any_recursive = called.iter().any(|f| is_directly_recursive(f, ctx));
if any_recursive {
let list_param = aver_name_to_dafny(&law.givens[list_given_idx].name);
let lemma_name = format!("{}_{}", fn_name, law_name);
let other_args: Vec<String> = law
.givens
.iter()
.enumerate()
.map(|(i, g)| {
if i == list_given_idx {
format!("{}[1..]", list_param)
} else {
aver_name_to_dafny(&g.name)
}
})
.collect();
lines.push(format!(" if |{}| == 0 {{", list_param));
lines.push(" } else {".to_string());
lines.push(format!(" {}({});", lemma_name, other_args.join(", ")));
lines.push(" }".to_string());
}
}
lines.push("}\n".to_string());
lines.join("\n")
}