use std::collections::HashSet;
use crate::ast::{
Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, StrPart, TailCallData,
TopLevel, TypeDef, TypeVariant, VerifyBlock, VerifyGivenDomain, VerifyKind,
};
use crate::codegen::CodegenContext;
use crate::types::Type;
#[derive(Debug, Clone)]
pub struct RefinementInfo<'a> {
pub carrier_type: &'a str,
pub carrier_field: &'a str,
pub param_name: &'a str,
pub constructor_fn: &'a str,
pub predicate: &'a Spanned<Expr>,
}
pub fn refinement_info_for<'a>(
type_name: &str,
inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
) -> Option<RefinementInfo<'a>> {
refinement_info_for_walk(type_name, inputs, None)
}
pub fn refinement_info_for_in_scope<'a>(
type_name: &str,
inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
scope: Option<&str>,
) -> Option<RefinementInfo<'a>> {
refinement_info_for_walk(type_name, inputs, Some(scope))
}
fn refinement_info_for_walk<'a>(
type_name: &str,
inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
scope_filter: Option<Option<&str>>,
) -> Option<RefinementInfo<'a>> {
if let Some(shape) = inputs.program_shape
&& let Some(info) = refinement_info_from_shape(shape, inputs, type_name, scope_filter)
{
return Some(info);
}
refinement_info_for_walk_legacy(type_name, inputs, scope_filter)
}
fn refinement_info_from_shape<'a>(
shape: &crate::analysis::shape::ProgramShape,
inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
type_name: &str,
scope_filter: Option<Option<&str>>,
) -> Option<RefinementInfo<'a>> {
use crate::analysis::shape::ModulePattern;
let pattern_match = shape.patterns.iter().find_map(|p| {
let ModulePattern::RefinementSmartConstructor {
scope,
type_name: ptn,
constructor_fn,
..
} = p
else {
return None;
};
if ptn != type_name {
return None;
}
let scope_ok = match (scope_filter, scope.as_deref()) {
(None, _) => true,
(Some(None), None) => true,
(Some(None), Some(_)) => false,
(Some(Some(want)), Some(have)) => want == have,
(Some(Some(_)), None) => false,
};
if !scope_ok {
return None;
}
Some((scope.clone(), constructor_fn.clone()))
})?;
let (scope, constructor_fn) = pattern_match;
let (carrier_field, carrier_type) = match scope.as_deref() {
None => inputs.entry_items.iter().find_map(|i| match i {
TopLevel::TypeDef(TypeDef::Product { name, fields, .. })
if name == type_name && fields.len() == 1 =>
{
let (fname, ftype) = &fields[0];
Some((fname.as_str(), ftype.as_str()))
}
_ => None,
}),
Some(prefix) => inputs
.dep_modules
.iter()
.find(|m| m.prefix == prefix)
.and_then(|m| {
m.type_defs.iter().find_map(|td| match td {
TypeDef::Product { name, fields, .. }
if name == type_name && fields.len() == 1 =>
{
let (fname, ftype) = &fields[0];
Some((fname.as_str(), ftype.as_str()))
}
_ => None,
})
}),
}?;
let constructor_fd: &'a FnDef = match scope.as_deref() {
None => inputs.entry_items.iter().find_map(|i| match i {
TopLevel::FnDef(fd) if fd.name == constructor_fn => Some(fd),
_ => None,
}),
Some(prefix) => inputs
.dep_modules
.iter()
.find(|m| m.prefix == prefix)
.and_then(|m| m.fn_defs.iter().find(|fd| fd.name == constructor_fn)),
}?;
let (param_name, _) = constructor_fd.params.first()?;
let stmts = constructor_fd.body.stmts();
let Stmt::Expr(body_expr) = stmts.first()? else {
return None;
};
let Expr::Match { subject, .. } = &body_expr.node else {
return None;
};
Some(RefinementInfo {
carrier_type,
carrier_field,
param_name,
constructor_fn: constructor_fd.name.as_str(),
predicate: subject,
})
}
fn refinement_info_for_walk_legacy<'a>(
type_name: &str,
inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
scope_filter: Option<Option<&str>>,
) -> Option<RefinementInfo<'a>> {
let entry_only = matches!(scope_filter, Some(None));
let module_only_prefix = match scope_filter {
Some(Some(p)) => Some(p),
_ => None,
};
let allow_entry = scope_filter.is_none() || entry_only;
let entry_typedefs: Vec<&'a TypeDef> = if allow_entry {
inputs
.entry_items
.iter()
.filter_map(|item| match item {
TopLevel::TypeDef(td) => Some(td),
_ => None,
})
.collect()
} else {
Vec::new()
};
let module_typedefs: Vec<&'a TypeDef> = inputs
.dep_modules
.iter()
.filter(|m| match (scope_filter, module_only_prefix) {
(None, _) => true,
(Some(None), _) => false,
(Some(Some(_)), Some(p)) => m.prefix == p,
_ => false,
})
.flat_map(|m| m.type_defs.iter())
.collect();
let (carrier_field, carrier_type) = entry_typedefs
.into_iter()
.chain(module_typedefs)
.find_map(|td| match td {
TypeDef::Product { name, fields, .. } if name == type_name && fields.len() == 1 => {
let (fname, ftype) = &fields[0];
Some((fname.as_str(), ftype.as_str()))
}
_ => None,
})?;
let entry_fns: Vec<&'a FnDef> = if allow_entry {
inputs
.entry_items
.iter()
.filter_map(|item| match item {
TopLevel::FnDef(fd) => Some(fd),
_ => None,
})
.collect()
} else {
Vec::new()
};
let module_fns: Vec<&'a FnDef> = inputs
.dep_modules
.iter()
.filter(|m| match (scope_filter, module_only_prefix) {
(None, _) => true,
(Some(None), _) => false,
(Some(Some(_)), Some(p)) => m.prefix == p,
_ => false,
})
.flat_map(|m| m.fn_defs.iter())
.collect();
for fd in entry_fns.into_iter().chain(module_fns) {
if !fd.return_type.starts_with("Result<") {
continue;
}
if !fd.return_type[7..].starts_with(type_name) {
continue;
}
if fd.params.len() != 1 {
continue;
}
let (param_name, _) = &fd.params[0];
let stmts = fd.body.stmts();
if stmts.len() != 1 {
continue;
}
let Stmt::Expr(body_expr) = &stmts[0] else {
continue;
};
let Expr::Match { subject, arms } = &body_expr.node else {
continue;
};
if !is_bool_ok_err_match(arms, type_name, carrier_field, param_name) {
continue;
}
return Some(RefinementInfo {
carrier_type,
carrier_field,
param_name,
constructor_fn: fd.name.as_str(),
predicate: subject,
});
}
None
}
pub fn is_refinement_bool_ok_err_match(
arms: &[MatchArm],
type_name: &str,
carrier_field: &str,
param_name: &str,
) -> bool {
is_bool_ok_err_match(arms, type_name, carrier_field, param_name)
}
fn is_bool_ok_err_match(
arms: &[MatchArm],
type_name: &str,
carrier_field: &str,
param_name: &str,
) -> bool {
if arms.len() != 2 {
return false;
}
let mut true_ok = false;
let mut false_err = false;
for arm in arms {
match &arm.pattern {
Pattern::Literal(Literal::Bool(true)) => {
if is_ok_constructor_with_identity(&arm.body, type_name, carrier_field, param_name)
{
true_ok = true;
}
}
Pattern::Literal(Literal::Bool(false)) => {
if is_err_constructor(&arm.body) {
false_err = true;
}
}
_ => return false,
}
}
true_ok && false_err
}
fn is_ok_constructor_with_identity(
expr: &Spanned<Expr>,
type_name: &str,
carrier_field: &str,
param_name: &str,
) -> bool {
let (ctor_name, ctor_arg_node) = match &expr.node {
Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
Expr::FnCall(callee, args) if args.len() == 1 => {
let Some(name) = expr_to_dotted_name(&callee.node) else {
return false;
};
(name, &args[0].node)
}
_ => return false,
};
if ctor_name != "Result.Ok" {
return false;
}
let (t, fields) = match ctor_arg_node {
Expr::RecordCreate {
type_name: t,
fields,
} => (t.as_str(), fields),
_ => return false,
};
if t != type_name || fields.len() != 1 {
return false;
}
let (fname, fvalue) = &fields[0];
if fname != carrier_field {
return false;
}
match &fvalue.node {
Expr::Ident(name) | Expr::Resolved { name, .. } => name == param_name,
_ => false,
}
}
pub fn refinement_lift_for_given(
given_name: &str,
given_type: &str,
lhs: &Spanned<Expr>,
rhs: &Spanned<Expr>,
ctx: &CodegenContext,
) -> Option<String> {
if given_type == "Float" {
return None;
}
let mut result: Option<String> = None;
search_refinement_wrapper(lhs, given_name, given_type, ctx, &mut result);
search_refinement_wrapper(rhs, given_name, given_type, ctx, &mut result);
result
}
fn search_refinement_wrapper(
expr: &Spanned<Expr>,
given_name: &str,
given_type: &str,
ctx: &CodegenContext,
result: &mut Option<String>,
) {
if result.is_some() {
return;
}
match &expr.node {
Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
let (_, fvalue) = &fields[0];
let matches_var = matches!(
&fvalue.node,
Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == given_name
);
if matches_var
&& let Some((canonical_key, decl)) = find_refined_type_with_key(ctx, type_name)
&& decl.carrier_type == given_type
{
*result = Some(canonical_key);
return;
}
for (_, v) in fields {
search_refinement_wrapper(v, given_name, given_type, ctx, result);
}
}
Expr::FnCall(callee, args) => {
search_refinement_wrapper(callee, given_name, given_type, ctx, result);
for a in args {
search_refinement_wrapper(a, given_name, given_type, ctx, result);
}
}
Expr::BinOp(_, l, r) => {
search_refinement_wrapper(l, given_name, given_type, ctx, result);
search_refinement_wrapper(r, given_name, given_type, ctx, result);
}
Expr::Attr(o, _) => search_refinement_wrapper(o, given_name, given_type, ctx, result),
Expr::Neg(i) | Expr::ErrorProp(i) => {
search_refinement_wrapper(i, given_name, given_type, ctx, result);
}
Expr::Match { subject, arms } => {
search_refinement_wrapper(subject, given_name, given_type, ctx, result);
for arm in arms {
search_refinement_wrapper(&arm.body, given_name, given_type, ctx, result);
}
}
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
for it in items {
search_refinement_wrapper(it, given_name, given_type, ctx, result);
}
}
Expr::Constructor(_, Some(arg)) => {
search_refinement_wrapper(arg, given_name, given_type, ctx, result);
}
_ => {}
}
}
pub fn project_lifted_idents_to_val(
expr: &Spanned<Expr>,
lifted_vars: &std::collections::HashMap<String, String>,
) -> Spanned<Expr> {
if lifted_vars.is_empty() {
return expr.clone();
}
let new_node = match &expr.node {
Expr::BinOp(op, l, r) if is_comparator_binop(*op) => {
let l_proj = project_lifted_ident_leaf(l, lifted_vars);
let r_proj = project_lifted_ident_leaf(r, lifted_vars);
Expr::BinOp(*op, Box::new(l_proj), Box::new(r_proj))
}
Expr::FnCall(callee, args) => {
let name = expr_to_dotted_name(&callee.node);
if matches!(name.as_deref(), Some("Bool.and") | Some("Bool.or")) {
Expr::FnCall(
callee.clone(),
args.iter()
.map(|a| project_lifted_idents_to_val(a, lifted_vars))
.collect(),
)
} else {
return expr.clone();
}
}
_ => return expr.clone(),
};
Spanned::new(new_node, expr.line)
}
fn is_comparator_binop(op: crate::ast::BinOp) -> bool {
use crate::ast::BinOp::*;
matches!(op, Lt | Gt | Lte | Gte | Eq | Neq)
}
fn project_lifted_ident_leaf(
expr: &Spanned<Expr>,
lifted_vars: &std::collections::HashMap<String, String>,
) -> Spanned<Expr> {
let target_name = match &expr.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => n,
_ => return expr.clone(),
};
if lifted_vars.contains_key(target_name) {
Spanned::new(
Expr::Attr(
Box::new(Spanned::new(Expr::Ident(target_name.clone()), expr.line)),
"val".to_string(),
),
expr.line,
)
} else {
expr.clone()
}
}
pub fn strip_refinement_wrappers(
expr: &Spanned<Expr>,
lifted_vars: &std::collections::HashMap<String, String>,
ctx: &CodegenContext,
) -> Spanned<Expr> {
let new_node = match &expr.node {
Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
let (_, fvalue) = &fields[0];
let var_name = match &fvalue.node {
Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
_ => None,
};
let canonical_for_ast = find_refined_type_with_key(ctx, type_name).map(|(k, _)| k);
if let Some(name) = var_name
&& let Some(refined) = lifted_vars.get(&name)
&& canonical_for_ast.as_deref() == Some(refined.as_str())
{
return Spanned::new(Expr::Ident(name), expr.line);
}
let new_fields: Vec<(String, Spanned<Expr>)> = fields
.iter()
.map(|(n, v)| (n.clone(), strip_refinement_wrappers(v, lifted_vars, ctx)))
.collect();
Expr::RecordCreate {
type_name: type_name.clone(),
fields: new_fields,
}
}
Expr::FnCall(callee, args) => Expr::FnCall(
Box::new(strip_refinement_wrappers(callee, lifted_vars, ctx)),
args.iter()
.map(|a| strip_refinement_wrappers(a, lifted_vars, ctx))
.collect(),
),
Expr::BinOp(op, l, r) => Expr::BinOp(
*op,
Box::new(strip_refinement_wrappers(l, lifted_vars, ctx)),
Box::new(strip_refinement_wrappers(r, lifted_vars, ctx)),
),
Expr::Attr(o, f) => Expr::Attr(
Box::new(strip_refinement_wrappers(o, lifted_vars, ctx)),
f.clone(),
),
Expr::Neg(i) => Expr::Neg(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx))),
Expr::ErrorProp(i) => {
Expr::ErrorProp(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx)))
}
_ => expr.node.clone(),
};
Spanned::new(new_node, expr.line)
}
pub fn swap_comparison_operands_op(op: &crate::ast::BinOp) -> Option<crate::ast::BinOp> {
use crate::ast::BinOp::*;
match op {
Lt => Some(Gt),
Gt => Some(Lt),
Lte => Some(Gte),
Gte => Some(Lte),
Eq => Some(Eq),
Neq => Some(Neq),
_ => None,
}
}
pub fn predicate_syntactic_eq(a: &Spanned<Expr>, b: &Spanned<Expr>) -> bool {
match (&a.node, &b.node) {
(Expr::BinOp(op_a, la, ra), Expr::BinOp(op_b, lb, rb)) => {
if op_a == op_b && predicate_syntactic_eq(la, lb) && predicate_syntactic_eq(ra, rb) {
return true;
}
if let Some(swapped) = swap_comparison_operands_op(op_a)
&& &swapped == op_b
&& predicate_syntactic_eq(la, rb)
&& predicate_syntactic_eq(ra, lb)
{
return true;
}
false
}
_ => a.node == b.node,
}
}
pub fn flatten_bool_and_conjuncts(expr: &Spanned<Expr>) -> Vec<Spanned<Expr>> {
if let Expr::FnCall(callee, args) = &expr.node
&& args.len() == 2
&& let Some(name) = expr_to_dotted_name(&callee.node)
&& name == "Bool.and"
{
let mut out = flatten_bool_and_conjuncts(&args[0]);
out.extend(flatten_bool_and_conjuncts(&args[1]));
return out;
}
vec![expr.clone()]
}
pub fn flatten_bool_and_conjuncts_resolved(
expr: &Spanned<crate::ir::hir::ResolvedExpr>,
) -> Vec<Spanned<crate::ir::hir::ResolvedExpr>> {
use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
if let ResolvedExpr::Call(ResolvedCallee::Builtin(name), args) = &expr.node
&& name == "Bool.and"
&& args.len() == 2
{
let mut out = flatten_bool_and_conjuncts_resolved(&args[0]);
out.extend(flatten_bool_and_conjuncts_resolved(&args[1]));
return out;
}
vec![expr.clone()]
}
pub fn predicate_syntactic_eq_resolved(
a: &Spanned<crate::ir::hir::ResolvedExpr>,
b: &Spanned<crate::ir::hir::ResolvedExpr>,
) -> bool {
use crate::ir::hir::ResolvedExpr;
match (&a.node, &b.node) {
(ResolvedExpr::BinOp(op_a, la, ra), ResolvedExpr::BinOp(op_b, lb, rb)) => {
if op_a == op_b
&& predicate_syntactic_eq_resolved(la, lb)
&& predicate_syntactic_eq_resolved(ra, rb)
{
return true;
}
if let Some(swapped) = swap_comparison_operands_op(op_a)
&& &swapped == op_b
&& predicate_syntactic_eq_resolved(la, rb)
&& predicate_syntactic_eq_resolved(ra, lb)
{
return true;
}
false
}
_ => a.node == b.node,
}
}
pub fn substitute_ident_in_resolved_expr(
expr: &Spanned<crate::ir::hir::ResolvedExpr>,
from: &str,
to: &str,
) -> Spanned<crate::ir::hir::ResolvedExpr> {
use crate::ir::hir::{ResolvedExpr, ResolvedMatchArm, ResolvedStrPart};
let line = expr.line;
let rec = |e: &Spanned<ResolvedExpr>| substitute_ident_in_resolved_expr(e, from, to);
let new_node = match &expr.node {
ResolvedExpr::Ident(name) | ResolvedExpr::Resolved { name, .. } if name == from => {
ResolvedExpr::Ident(to.to_string())
}
ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {
return expr.clone();
}
ResolvedExpr::Attr(obj, field) => ResolvedExpr::Attr(Box::new(rec(obj)), field.clone()),
ResolvedExpr::Call(callee, args) => {
ResolvedExpr::Call(callee.clone(), args.iter().map(&rec).collect())
}
ResolvedExpr::BinOp(op, left, right) => {
ResolvedExpr::BinOp(*op, Box::new(rec(left)), Box::new(rec(right)))
}
ResolvedExpr::Neg(inner) => ResolvedExpr::Neg(Box::new(rec(inner))),
ResolvedExpr::Match { subject, arms } => ResolvedExpr::Match {
subject: Box::new(rec(subject)),
arms: arms
.iter()
.map(|arm| ResolvedMatchArm {
pattern: arm.pattern.clone(),
body: Box::new(rec(&arm.body)),
binding_slots: std::sync::OnceLock::new(),
})
.collect(),
},
ResolvedExpr::Ctor(ctor, args) => {
ResolvedExpr::Ctor(ctor.clone(), args.iter().map(&rec).collect())
}
ResolvedExpr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(rec(inner))),
ResolvedExpr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
parts
.iter()
.map(|p| match p {
ResolvedStrPart::Literal(_) => p.clone(),
ResolvedStrPart::Parsed(inner) => ResolvedStrPart::Parsed(Box::new(rec(inner))),
})
.collect(),
),
ResolvedExpr::List(items) => ResolvedExpr::List(items.iter().map(&rec).collect()),
ResolvedExpr::Tuple(items) => ResolvedExpr::Tuple(items.iter().map(&rec).collect()),
ResolvedExpr::IndependentProduct(items, flag) => {
ResolvedExpr::IndependentProduct(items.iter().map(&rec).collect(), *flag)
}
ResolvedExpr::MapLiteral(entries) => {
ResolvedExpr::MapLiteral(entries.iter().map(|(k, v)| (rec(k), rec(v))).collect())
}
ResolvedExpr::RecordCreate {
type_id,
type_name,
fields,
} => ResolvedExpr::RecordCreate {
type_id: *type_id,
type_name: type_name.clone(),
fields: fields.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
},
ResolvedExpr::RecordUpdate {
type_id,
type_name,
base,
updates,
} => ResolvedExpr::RecordUpdate {
type_id: *type_id,
type_name: type_name.clone(),
base: Box::new(rec(base)),
updates: updates.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
},
ResolvedExpr::TailCall { target, args } => ResolvedExpr::TailCall {
target: *target,
args: args.iter().map(&rec).collect(),
},
};
Spanned::new(new_node, line)
}
pub fn substitute_ident_in_expr(expr: &Spanned<Expr>, from: &str, to: &str) -> Spanned<Expr> {
use crate::ast::{MatchArm, StrPart, TailCallData};
let line = expr.line;
let new_node = match &expr.node {
Expr::Ident(name) | Expr::Resolved { name, .. } if name == from => {
Expr::Ident(to.to_string())
}
Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => return expr.clone(),
Expr::Attr(obj, field) => Expr::Attr(
Box::new(substitute_ident_in_expr(obj, from, to)),
field.clone(),
),
Expr::FnCall(callee, args) => Expr::FnCall(
Box::new(substitute_ident_in_expr(callee, from, to)),
args.iter()
.map(|a| substitute_ident_in_expr(a, from, to))
.collect(),
),
Expr::BinOp(op, left, right) => Expr::BinOp(
*op,
Box::new(substitute_ident_in_expr(left, from, to)),
Box::new(substitute_ident_in_expr(right, from, to)),
),
Expr::Neg(inner) => Expr::Neg(Box::new(substitute_ident_in_expr(inner, from, to))),
Expr::Match { subject, arms } => Expr::Match {
subject: Box::new(substitute_ident_in_expr(subject, from, to)),
arms: arms
.iter()
.map(|arm| MatchArm {
pattern: arm.pattern.clone(),
body: Box::new(substitute_ident_in_expr(&arm.body, from, to)),
binding_slots: std::sync::OnceLock::new(),
})
.collect(),
},
Expr::Constructor(name, arg) => Expr::Constructor(
name.clone(),
arg.as_ref()
.map(|inner| Box::new(substitute_ident_in_expr(inner, from, to))),
),
Expr::ErrorProp(inner) => {
Expr::ErrorProp(Box::new(substitute_ident_in_expr(inner, from, to)))
}
Expr::InterpolatedStr(parts) => Expr::InterpolatedStr(
parts
.iter()
.map(|part| match part {
StrPart::Literal(_) => part.clone(),
StrPart::Parsed(inner) => {
StrPart::Parsed(Box::new(substitute_ident_in_expr(inner, from, to)))
}
})
.collect(),
),
Expr::List(items) => Expr::List(
items
.iter()
.map(|item| substitute_ident_in_expr(item, from, to))
.collect(),
),
Expr::Tuple(items) => Expr::Tuple(
items
.iter()
.map(|item| substitute_ident_in_expr(item, from, to))
.collect(),
),
Expr::IndependentProduct(items, flag) => Expr::IndependentProduct(
items
.iter()
.map(|item| substitute_ident_in_expr(item, from, to))
.collect(),
*flag,
),
Expr::MapLiteral(entries) => Expr::MapLiteral(
entries
.iter()
.map(|(k, v)| {
(
substitute_ident_in_expr(k, from, to),
substitute_ident_in_expr(v, from, to),
)
})
.collect(),
),
Expr::RecordCreate { type_name, fields } => Expr::RecordCreate {
type_name: type_name.clone(),
fields: fields
.iter()
.map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
.collect(),
},
Expr::RecordUpdate {
type_name,
base,
updates,
} => Expr::RecordUpdate {
type_name: type_name.clone(),
base: Box::new(substitute_ident_in_expr(base, from, to)),
updates: updates
.iter()
.map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
.collect(),
},
Expr::TailCall(boxed) => Expr::TailCall(Box::new(TailCallData::new(
boxed.target.clone(),
boxed
.args
.iter()
.map(|a| substitute_ident_in_expr(a, from, to))
.collect(),
))),
};
Spanned::new(new_node, line)
}
pub fn when_is_redundant_with_refinement_lifts(
when_expr: &Spanned<Expr>,
lifted_vars: &std::collections::HashMap<String, String>,
ctx: &CodegenContext,
) -> bool {
if lifted_vars.is_empty() {
return false;
}
let resolved_when = ctx.resolve_expr(when_expr, ctx.active_module_scope().as_deref());
let when_conjuncts = flatten_bool_and_conjuncts_resolved(&resolved_when);
let mut lifted_predicates: Vec<Spanned<crate::ir::hir::ResolvedExpr>> = Vec::new();
for (given_name, refined_type) in lifted_vars {
let Some(decl) = find_refined_type(ctx, refined_type) else {
return false;
};
let substituted = substitute_ident_in_resolved_expr(
&decl.invariant.expr,
decl.predicate_param.as_str(),
given_name,
);
lifted_predicates.extend(flatten_bool_and_conjuncts_resolved(&substituted));
}
if when_conjuncts.len() != lifted_predicates.len() {
return false;
}
let mut matched = vec![false; lifted_predicates.len()];
for wc in &when_conjuncts {
let Some(idx) = (0..lifted_predicates.len())
.find(|&i| !matched[i] && predicate_syntactic_eq_resolved(wc, &lifted_predicates[i]))
else {
return false;
};
matched[idx] = true;
}
true
}
fn is_err_constructor(expr: &Spanned<Expr>) -> bool {
match &expr.node {
Expr::Constructor(name, Some(_)) => name == "Result.Err",
Expr::FnCall(callee, args) if args.len() == 1 => {
matches!(
expr_to_dotted_name(&callee.node),
Some(name) if name == "Result.Err"
)
}
_ => false,
}
}
pub fn is_pure_fn(fd: &FnDef) -> bool {
fd.effects.is_empty() && fd.name != "main"
}
pub fn find_refined_type<'a>(
ctx: &'a CodegenContext,
name: &str,
) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
find_refined_type_with_key_scoped(ctx, name, None).map(|(_, d)| d)
}
pub fn backend_named_type_key(ctx: &CodegenContext, ty: &crate::types::Type) -> Option<String> {
let crate::types::Type::Named { id, name } = ty else {
return None;
};
if let Some(type_id) = id {
return Some(ctx.symbol_table.type_entry(*type_id).key.canonical());
}
Some(name.clone())
}
pub fn backend_type_def_key(ctx: &CodegenContext, td: &crate::ast::TypeDef) -> String {
let key = type_key_for_decl(ctx, td);
if ctx.symbol_table.type_id_of(&key).is_some() {
key.canonical()
} else {
type_def_name(td).to_string()
}
}
pub fn find_refined_type_by_id(
ctx: &CodegenContext,
type_id: crate::ir::TypeId,
) -> Option<&crate::ir::proof_ir::RefinedTypeDecl> {
ctx.proof_ir.refined_types.get(&type_id)
}
pub fn find_refined_type_for_named<'a>(
ctx: &'a CodegenContext,
named: &crate::types::Type,
) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
let crate::types::Type::Named { id, name } = named else {
return None;
};
match id {
Some(type_id) => find_refined_type_by_id(ctx, *type_id),
None => find_refined_type(ctx, name),
}
}
pub fn find_fn_contract_scoped<'a>(
ctx: &'a CodegenContext,
name: &str,
scope: Option<&str>,
) -> Option<&'a crate::ir::proof_ir::FnContract> {
let symbols = &ctx.symbol_table;
let bare = name.rsplit('.').next().unwrap_or(name);
let name_is_already_qualified = name.contains('.');
let try_key = |key: crate::ir::FnKey| -> Option<&'a crate::ir::proof_ir::FnContract> {
let id = symbols.fn_id_of(&key)?;
ctx.proof_ir.fn_contracts.get(&id)
};
if let Some(prefix) = scope
&& !name_is_already_qualified
&& let Some(c) = try_key(crate::ir::FnKey::in_module(prefix.to_string(), bare))
{
return Some(c);
}
let direct_key =
if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
crate::ir::FnKey::in_module(prefix.to_string(), bare_part)
} else {
crate::ir::FnKey::entry(name)
};
if let Some(c) = try_key(direct_key) {
return Some(c);
}
for m in &ctx.modules {
for fd in &m.fn_defs {
if fd.name == bare
&& let Some(c) = try_key(crate::ir::FnKey::in_module(m.prefix.clone(), bare))
{
return Some(c);
}
}
}
None
}
pub fn fn_contract_exists_scoped(ctx: &CodegenContext, name: &str, scope: Option<&str>) -> bool {
find_fn_contract_scoped(ctx, name, scope).is_some()
}
pub fn fn_key_for_decl(ctx: &CodegenContext, fd: &FnDef) -> crate::ir::FnKey {
match fn_owning_scope_for(ctx, fd) {
Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), fd.name.clone()),
None => crate::ir::FnKey::entry(fd.name.clone()),
}
}
pub fn type_key_for_decl(ctx: &CodegenContext, td: &TypeDef) -> crate::ir::TypeKey {
for m in &ctx.modules {
for t in &m.type_defs {
if std::ptr::eq(t, td) {
return crate::ir::TypeKey::in_module(m.prefix.clone(), type_def_name(td));
}
}
}
crate::ir::TypeKey::entry(type_def_name(td))
}
pub fn type_key_for_name(
ctx: &CodegenContext,
name: &str,
scope: Option<&str>,
) -> crate::ir::TypeKey {
let bare = name.rsplit('.').next().unwrap_or(name);
let name_is_qualified = name.contains('.');
if let Some(prefix) = scope
&& !name_is_qualified
{
for m in &ctx.modules {
if m.prefix == prefix && m.type_defs.iter().any(|td| type_def_name(td) == bare) {
return crate::ir::TypeKey::in_module(prefix.to_string(), bare);
}
}
}
if !name_is_qualified && ctx.type_defs.iter().any(|td| type_def_name(td) == bare) {
return crate::ir::TypeKey::entry(bare);
}
if name_is_qualified
&& let Some((prefix, bare_part)) = name.rsplit_once('.')
&& ctx.modules.iter().any(|m| m.prefix == prefix)
{
return crate::ir::TypeKey::in_module(prefix.to_string(), bare_part);
}
for m in &ctx.modules {
if m.type_defs.iter().any(|td| type_def_name(td) == bare) {
return crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
}
}
crate::ir::TypeKey::entry(name)
}
pub fn fn_owning_scope_for<'a>(ctx: &'a CodegenContext, fd: &FnDef) -> Option<&'a str> {
for m in &ctx.modules {
for f in &m.fn_defs {
if std::ptr::eq(f, fd) {
return Some(m.prefix.as_str());
}
}
}
None
}
pub fn find_fn_contract_for_fn<'a>(
ctx: &'a CodegenContext,
fd: &FnDef,
) -> Option<&'a crate::ir::proof_ir::FnContract> {
let symbols = &ctx.symbol_table;
let fn_key = fn_key_for_decl(ctx, fd);
let fn_id = symbols.fn_id_of(&fn_key)?;
ctx.proof_ir.fn_contracts.get(&fn_id)
}
pub fn fn_contract_exists_for_fn(ctx: &CodegenContext, fd: &FnDef) -> bool {
find_fn_contract_for_fn(ctx, fd).is_some()
}
pub fn fn_id_for_decl(ctx: &CodegenContext, fd: &FnDef) -> Option<crate::ir::FnId> {
let fn_key = fn_key_for_decl(ctx, fd);
ctx.symbol_table.fn_id_of(&fn_key)
}
pub fn fn_id_for_dotted_name(ctx: &CodegenContext, dotted: &str) -> Option<crate::ir::FnId> {
let key = if let Some((prefix, bare)) = dotted.rsplit_once('.') {
crate::ir::FnKey::in_module(prefix.to_string(), bare)
} else {
crate::ir::FnKey::entry(dotted)
};
ctx.symbol_table.fn_id_of(&key)
}
pub fn find_refined_type_with_key<'a>(
ctx: &'a CodegenContext,
name: &str,
) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
find_refined_type_with_key_scoped(ctx, name, None)
}
pub fn find_refined_type_scoped<'a>(
ctx: &'a CodegenContext,
name: &str,
scope: Option<&str>,
) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
find_refined_type_with_key_scoped(ctx, name, scope).map(|(_, d)| d)
}
pub fn find_refined_type_with_key_scoped<'a>(
ctx: &'a CodegenContext,
name: &str,
scope: Option<&str>,
) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
let symbols = &ctx.symbol_table;
let bare = name.rsplit('.').next().unwrap_or(name);
let name_is_already_qualified = name.contains('.');
let try_key =
|key: crate::ir::TypeKey| -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
let id = symbols.type_id_of(&key)?;
let decl = ctx.proof_ir.refined_types.get(&id)?;
Some((key.canonical(), decl))
};
if let Some(prefix) = scope
&& !name_is_already_qualified
&& let Some(hit) = try_key(crate::ir::TypeKey::in_module(prefix.to_string(), bare))
{
return Some(hit);
}
let direct_key =
if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
} else {
crate::ir::TypeKey::entry(name)
};
if let Some(hit) = try_key(direct_key) {
return Some(hit);
}
for m in &ctx.modules {
for td in &m.type_defs {
if type_def_name(td) == bare
&& let Some(hit) = try_key(crate::ir::TypeKey::in_module(m.prefix.clone(), bare))
{
return Some(hit);
}
}
}
None
}
pub fn resolve_refined_type_in<'a>(
refined_types: &'a std::collections::HashMap<
crate::ir::TypeId,
crate::ir::proof_ir::RefinedTypeDecl,
>,
symbols: &crate::ir::SymbolTable,
modules: &[crate::codegen::ModuleInfo],
name: &str,
) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
resolve_refined_type_in_with_key(refined_types, symbols, modules, name).map(|(_, d)| d)
}
pub fn resolve_refined_type_in_with_key<'a>(
refined_types: &'a std::collections::HashMap<
crate::ir::TypeId,
crate::ir::proof_ir::RefinedTypeDecl,
>,
symbols: &crate::ir::SymbolTable,
modules: &[crate::codegen::ModuleInfo],
name: &str,
) -> Option<(crate::ir::TypeId, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
let bare = name.rsplit('.').next().unwrap_or(name);
let name_is_already_qualified = name.contains('.');
let direct_key =
if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
} else {
crate::ir::TypeKey::entry(name)
};
if let Some(id) = symbols.type_id_of(&direct_key)
&& let Some(decl) = refined_types.get(&id)
{
return Some((id, decl));
}
for m in modules {
for td in &m.type_defs {
if type_def_name(td) == bare {
let canonical = crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
if let Some(id) = symbols.type_id_of(&canonical)
&& let Some(decl) = refined_types.get(&id)
{
return Some((id, decl));
}
}
}
}
None
}
pub fn all_givens_are_singletons(law: &crate::ast::VerifyLaw) -> bool {
!law.givens.is_empty()
&& law.givens.iter().all(|g| match &g.domain {
VerifyGivenDomain::Explicit(values) => values.len() == 1,
VerifyGivenDomain::IntRange { start, end } => start == end,
})
}
pub fn unclassified_fn_names(ctx: &CodegenContext) -> HashSet<String> {
ctx.proof_ir
.unclassified_fns
.iter()
.filter_map(|uf| {
let s = &uf.message;
let start = s.find('\'')?;
let rest = &s[start + 1..];
let end = rest.find('\'')?;
Some(rest[..end].to_string())
})
.collect()
}
pub fn law_calls_unclassified_fn(
law: &crate::ast::VerifyLaw,
unclassified: &HashSet<String>,
) -> bool {
if unclassified.is_empty() {
return false;
}
expr_calls_named(&law.lhs, unclassified) || expr_calls_named(&law.rhs, unclassified)
}
fn expr_calls_named(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
match &expr.node {
Expr::FnCall(callee, args) => {
let direct = expr_to_dotted_name(&callee.node)
.map(|n| {
let bare = n.rsplit('.').next().unwrap_or(n.as_str());
names.contains(n.as_str()) || names.contains(bare)
})
.unwrap_or(false);
direct
|| expr_calls_named(callee, names)
|| args.iter().any(|a| expr_calls_named(a, names))
}
Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
expr_calls_named(inner, names)
}
Expr::BinOp(_, l, r) => expr_calls_named(l, names) || expr_calls_named(r, names),
Expr::Match { subject, arms } => {
expr_calls_named(subject, names)
|| arms.iter().any(|a| expr_calls_named(&a.body, names))
}
Expr::Constructor(_, Some(arg)) => expr_calls_named(arg, names),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(|i| expr_calls_named(i, names))
}
Expr::MapLiteral(entries) => entries
.iter()
.any(|(k, v)| expr_calls_named(k, names) || expr_calls_named(v, names)),
Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| expr_calls_named(v, names)),
Expr::RecordUpdate { base, updates, .. } => {
expr_calls_named(base, names) || updates.iter().any(|(_, v)| expr_calls_named(v, names))
}
Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
crate::ast::StrPart::Parsed(inner) => expr_calls_named(inner, names),
crate::ast::StrPart::Literal(_) => false,
}),
Expr::TailCall(boxed) => {
let crate::ast::TailCallData { target, args, .. } = boxed.as_ref();
names.contains(target) || args.iter().any(|a| expr_calls_named(a, names))
}
_ => false,
}
}
pub fn accumulator_fold_fn_names(ctx: &CodegenContext) -> HashSet<String> {
use crate::codegen::recursion::detect::{
param_decremented_in_recursion, param_threaded_in_recursion,
single_list_structural_param_index,
};
let threads_over_recursive_adt = |fd: &FnDef| -> bool {
single_list_structural_param_index(fd).is_none()
&& (0..fd.params.len()).any(|i| param_threaded_in_recursion(fd, i))
&& fd.params.iter().enumerate().any(|(i, (_, ty))| {
ctx_type_is_recursive_sum(ctx, ty.trim()) && param_decremented_in_recursion(fd, i)
})
};
ctx.fn_defs
.iter()
.chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
.filter(|fd| threads_over_recursive_adt(fd))
.map(|fd| fd.name.clone())
.collect()
}
fn ctx_type_is_recursive_sum(ctx: &CodegenContext, type_name: &str) -> bool {
use crate::ast::TypeDef;
ctx.type_defs
.iter()
.chain(ctx.modules.iter().flat_map(|m| m.type_defs.iter()))
.any(|td| match td {
TypeDef::Sum { name, variants, .. } if name == type_name => variants.iter().any(|v| {
v.fields.iter().any(|f| {
let f = f.trim();
f == type_name
|| f.contains(&format!("<{type_name}"))
|| f.contains(&format!("{type_name}>"))
|| f.contains(&format!(", {type_name}"))
|| f.contains(&format!("{type_name},"))
})
}),
_ => false,
})
}
fn accfold_combine_fn(fd: &FnDef) -> Option<String> {
use crate::ast::Expr;
use crate::codegen::recursion::detect::{
call_matches, collect_calls_from_body, param_threaded_in_recursion,
};
let acc_idx = (0..fd.params.len()).find(|&i| param_threaded_in_recursion(fd, i))?;
let calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
.into_iter()
.filter(|(name, _)| call_matches(name, &fd.name))
.map(|(_, args)| args)
.collect();
let acc_arg = calls.first()?.get(acc_idx).copied()?;
let Expr::FnCall(callee, _) = &acc_arg.node else {
return None;
};
expr_to_dotted_name(&callee.node)
}
fn call_args_are_idents(e: &Spanned<Expr>, fn_name: &str, arg_names: &[&str]) -> bool {
let Expr::FnCall(callee, args) = &e.node else {
return false;
};
expr_to_dotted_name(&callee.node).as_deref() == Some(fn_name)
&& args.len() == arg_names.len()
&& args.iter().zip(arg_names).all(
|(a, n)| matches!(&a.node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == n),
)
}
fn law_is_commutativity(vb: &VerifyBlock, combine: &str) -> bool {
let crate::ast::VerifyKind::Law(law) = &vb.kind else {
return false;
};
if law.givens.len() != 2 || law.when.is_some() {
return false;
}
let a = law.givens[0].name.as_str();
let b = law.givens[1].name.as_str();
(call_args_are_idents(&law.lhs, combine, &[a, b])
&& call_args_are_idents(&law.rhs, combine, &[b, a]))
|| (call_args_are_idents(&law.lhs, combine, &[b, a])
&& call_args_are_idents(&law.rhs, combine, &[a, b]))
}
fn law_is_associativity(vb: &VerifyBlock, combine: &str) -> bool {
let crate::ast::VerifyKind::Law(law) = &vb.kind else {
return false;
};
if law.givens.len() != 3 || law.when.is_some() {
return false;
}
let a = law.givens[0].name.as_str();
let b = law.givens[1].name.as_str();
let c = law.givens[2].name.as_str();
let nested = |e: &Spanned<Expr>| -> bool {
let Expr::FnCall(callee, args) = &e.node else {
return false;
};
expr_to_dotted_name(&callee.node).as_deref() == Some(combine)
&& args.len() == 2
&& call_args_are_idents(&args[0], combine, &[a, b])
&& matches!(&args[1].node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == c)
};
let flat = |e: &Spanned<Expr>| -> bool {
let Expr::FnCall(callee, args) = &e.node else {
return false;
};
expr_to_dotted_name(&callee.node).as_deref() == Some(combine)
&& args.len() == 2
&& matches!(&args[0].node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == a)
&& call_args_are_idents(&args[1], combine, &[b, c])
};
(nested(&law.lhs) && flat(&law.rhs)) || (nested(&law.rhs) && flat(&law.lhs))
}
pub fn nat_accfold_self_closeable(
ctx: &CodegenContext,
verified_fn: &str,
accgen_law_name: &str,
) -> bool {
if !accumulator_fold_fn_names(ctx).contains(verified_fn) {
return false;
}
let Some(fd) = ctx.fn_def_by_name(verified_fn, ctx.active_module_scope().as_deref()) else {
return false;
};
let Some(combine) = accfold_combine_fn(fd) else {
return false;
};
if !ctx
.fn_def_by_name(&combine, ctx.active_module_scope().as_deref())
.is_some_and(combine_is_additive_monoid)
{
return false;
}
let citable: Vec<&VerifyBlock> = ctx
.items
.iter()
.filter_map(|i| match i {
TopLevel::Verify(vb) => Some(vb),
_ => None,
})
.take_while(|vb| {
!matches!(&vb.kind, VerifyKind::Law(l)
if vb.fn_name == verified_fn && l.name == accgen_law_name)
})
.collect();
let has_comm = citable.iter().any(|vb| law_is_commutativity(vb, &combine));
let has_assoc = citable.iter().any(|vb| law_is_associativity(vb, &combine));
has_comm && has_assoc
}
fn combine_is_additive_monoid(fd: &FnDef) -> bool {
use crate::ast::{Expr, Pattern, Stmt};
if fd.params.len() != 2 {
return false;
}
let second = fd.params[1].0.as_str();
let [Stmt::Expr(body)] = fd.body.stmts() else {
return false;
};
let Expr::Match { arms, .. } = &body.node else {
return false;
};
arms.iter().any(|arm| {
matches!(&arm.pattern, Pattern::Constructor(_, binders) if binders.is_empty())
&& matches!(&arm.body.node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == second)
})
}
pub fn law_calls_foreign_accumulator_fold(
ctx: &CodegenContext,
law: &crate::ast::VerifyLaw,
verified_fn: &str,
) -> bool {
let foreign: HashSet<String> = accumulator_fold_fn_names(ctx)
.into_iter()
.filter(|n| n != verified_fn)
.collect();
law_calls_unclassified_fn(law, &foreign)
}
pub fn dafny_should_bound_accumulator_fold(
ctx: &CodegenContext,
law: &crate::ast::VerifyLaw,
verified_fn: &str,
) -> bool {
law_calls_foreign_accumulator_fold(ctx, law, verified_fn)
|| (accumulator_fold_fn_names(ctx).contains(verified_fn)
&& !nat_accfold_self_closeable(ctx, verified_fn, &law.name))
}
pub fn law_rhs_is_independent_of_givens(law: &crate::ast::VerifyLaw) -> bool {
let given_names: HashSet<&str> = law.givens.iter().map(|g| g.name.as_str()).collect();
if given_names.is_empty() {
return true;
}
!expr_references_any_ident(&law.rhs, &given_names)
}
fn expr_references_any_ident(expr: &Spanned<Expr>, names: &HashSet<&str>) -> bool {
match &expr.node {
Expr::Ident(name) | Expr::Resolved { name, .. } => names.contains(name.as_str()),
Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
expr_references_any_ident(inner, names)
}
Expr::FnCall(callee, args) => {
expr_references_any_ident(callee, names)
|| args.iter().any(|a| expr_references_any_ident(a, names))
}
Expr::BinOp(_, l, r) => {
expr_references_any_ident(l, names) || expr_references_any_ident(r, names)
}
Expr::Match { subject, arms } => {
expr_references_any_ident(subject, names)
|| arms
.iter()
.any(|a| expr_references_any_ident(&a.body, names))
}
Expr::Constructor(_, Some(arg)) => expr_references_any_ident(arg, names),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(|i| expr_references_any_ident(i, names))
}
Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
expr_references_any_ident(k, names) || expr_references_any_ident(v, names)
}),
Expr::RecordCreate { fields, .. } => fields
.iter()
.any(|(_, v)| expr_references_any_ident(v, names)),
Expr::RecordUpdate { base, updates, .. } => {
expr_references_any_ident(base, names)
|| updates
.iter()
.any(|(_, v)| expr_references_any_ident(v, names))
}
Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
crate::ast::StrPart::Parsed(inner) => expr_references_any_ident(inner, names),
crate::ast::StrPart::Literal(_) => false,
}),
Expr::TailCall(boxed) => {
let crate::ast::TailCallData { args, .. } = boxed.as_ref();
args.iter().any(|a| expr_references_any_ident(a, names))
}
_ => false,
}
}
pub fn law_lhs_has_trace_projection(expr: &Spanned<Expr>) -> bool {
expr_has_trace_field(expr) && expr_has_trace_api_call(expr)
}
fn expr_has_trace_field(expr: &Spanned<Expr>) -> bool {
match &expr.node {
Expr::Attr(inner, field) => field == "trace" || expr_has_trace_field(inner),
Expr::FnCall(callee, args) => {
expr_has_trace_field(callee) || args.iter().any(expr_has_trace_field)
}
Expr::BinOp(_, l, r) => expr_has_trace_field(l) || expr_has_trace_field(r),
Expr::Match { subject, arms } => {
expr_has_trace_field(subject) || arms.iter().any(|a| expr_has_trace_field(&a.body))
}
Expr::ErrorProp(inner) => expr_has_trace_field(inner),
Expr::Constructor(_, Some(arg)) => expr_has_trace_field(arg),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(expr_has_trace_field)
}
_ => false,
}
}
const TRACE_API_METHODS: &[&str] = &["event", "group", "branch", "length", "contains", "count"];
fn expr_has_trace_api_call(expr: &Spanned<Expr>) -> bool {
match &expr.node {
Expr::FnCall(callee, args) => {
let direct = matches!(
&callee.node,
Expr::Attr(_, method) if TRACE_API_METHODS.contains(&method.as_str())
);
direct || expr_has_trace_api_call(callee) || args.iter().any(expr_has_trace_api_call)
}
Expr::Attr(inner, _) | Expr::ErrorProp(inner) => expr_has_trace_api_call(inner),
Expr::BinOp(_, l, r) => expr_has_trace_api_call(l) || expr_has_trace_api_call(r),
Expr::Match { subject, arms } => {
expr_has_trace_api_call(subject)
|| arms.iter().any(|a| expr_has_trace_api_call(&a.body))
}
Expr::Constructor(_, Some(arg)) => expr_has_trace_api_call(arg),
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
items.iter().any(expr_has_trace_api_call)
}
_ => false,
}
}
pub fn is_recursive_type_def(td: &TypeDef) -> bool {
match td {
TypeDef::Sum { name, variants, .. } => is_recursive_sum(name, variants),
TypeDef::Product { name, fields, .. } => is_recursive_product(name, fields),
}
}
pub fn type_def_name(td: &TypeDef) -> &str {
match td {
TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
}
}
pub fn is_recursive_sum(name: &str, variants: &[TypeVariant]) -> bool {
variants
.iter()
.any(|v| v.fields.iter().any(|f| type_ref_contains(f, name)))
}
pub fn is_recursive_product(name: &str, fields: &[(String, String)]) -> bool {
fields.iter().any(|(_, ty)| type_ref_contains(ty, name))
}
fn type_ref_contains(annotation: &str, type_name: &str) -> bool {
annotation == type_name
|| annotation.contains(&format!("<{}", type_name))
|| annotation.contains(&format!("{}>", type_name))
|| annotation.contains(&format!(", {}", type_name))
|| annotation.contains(&format!("{},", type_name))
}
pub(crate) fn is_user_type(name: &str, ctx: &CodegenContext) -> bool {
let check_td = |td: &TypeDef| match td {
TypeDef::Sum { name: n, .. } => n == name,
TypeDef::Product { name: n, .. } => n == name,
};
ctx.type_defs.iter().any(check_td)
|| ctx.modules.iter().any(|m| m.type_defs.iter().any(check_td))
}
pub(crate) fn resolve_module_call<'a>(
dotted_name: &'a str,
ctx: &'a CodegenContext,
) -> Option<(&'a str, &'a str)> {
let mut best: Option<&str> = None;
for prefix in &ctx.module_prefixes {
let dotted_prefix = format!("{}.", prefix);
if dotted_name.starts_with(&dotted_prefix) && best.is_none_or(|b| prefix.len() > b.len()) {
best = Some(prefix.as_str());
}
}
best.map(|prefix| (prefix, &dotted_name[prefix.len() + 1..]))
}
pub(crate) fn module_prefix_to_rust_segments(prefix: &str) -> Vec<String> {
prefix.split('.').map(module_segment_to_rust).collect()
}
pub(crate) fn module_prefix_to_filename(prefix: &str) -> String {
prefix.replace('.', "/")
}
pub(crate) struct DeclaredEffects {
pub bare_namespaces: HashSet<String>,
pub methods: HashSet<String>,
}
impl DeclaredEffects {
pub fn includes(&self, c_method: &str) -> bool {
if self.methods.contains(c_method) {
return true;
}
if let Some((ns, _)) = c_method.split_once('.') {
return self.bare_namespaces.contains(ns);
}
false
}
}
pub(crate) fn collect_declared_effects(ctx: &CodegenContext) -> DeclaredEffects {
let mut bare_namespaces: HashSet<String> = HashSet::new();
let mut methods: HashSet<String> = HashSet::new();
let mut record = |effect: &str| {
if effect.contains('.') {
methods.insert(effect.to_string());
} else {
bare_namespaces.insert(effect.to_string());
}
};
for item in &ctx.items {
if let TopLevel::FnDef(fd) = item {
for eff in &fd.effects {
record(&eff.node);
}
}
}
for module in &ctx.modules {
for fd in &module.fn_defs {
for eff in &fd.effects {
record(&eff.node);
}
}
}
DeclaredEffects {
bare_namespaces,
methods,
}
}
pub fn entry_basename(ctx: &CodegenContext) -> String {
ctx.items
.iter()
.find_map(|item| match item {
TopLevel::Module(m) => Some(m.name.clone()),
_ => None,
})
.unwrap_or_else(|| {
let mut chars = ctx.project_name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
})
}
pub fn verify_block_counter_key(vb: &crate::ast::VerifyBlock) -> String {
match &vb.kind {
crate::ast::VerifyKind::Cases => format!("fn:{}", vb.fn_name),
crate::ast::VerifyKind::Law(law) => format!("law:{}::{}", vb.fn_name, law.name),
}
}
pub(crate) fn module_prefix_to_rust_path(prefix: &str) -> String {
format!(
"crate::aver_generated::{}",
module_prefix_to_rust_segments(prefix).join("::")
)
}
fn module_segment_to_rust(segment: &str) -> String {
let chars = segment.chars().collect::<Vec<_>>();
let mut out = String::new();
for (idx, ch) in chars.iter().enumerate() {
if ch.is_ascii_alphanumeric() {
if ch.is_ascii_uppercase() {
let prev_is_lower_or_digit = idx > 0
&& (chars[idx - 1].is_ascii_lowercase() || chars[idx - 1].is_ascii_digit());
let next_is_lower = chars
.get(idx + 1)
.is_some_and(|next| next.is_ascii_lowercase());
if idx > 0 && (prev_is_lower_or_digit || next_is_lower) && !out.ends_with('_') {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch.to_ascii_lowercase());
}
} else if !out.ends_with('_') {
out.push('_');
}
}
let trimmed = out.trim_matches('_');
let mut normalized = if trimmed.is_empty() {
"module".to_string()
} else {
trimmed.to_string()
};
if matches!(
normalized.as_str(),
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
) {
normalized.push_str("_mod");
}
normalized
}
pub(crate) fn split_type_params(s: &str, delim: char) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
for ch in s.chars() {
match ch {
'<' | '(' => {
depth += 1;
current.push(ch);
}
'>' | ')' => {
depth = depth.saturating_sub(1);
current.push(ch);
}
_ if ch == delim && depth == 0 => {
parts.push(current.trim().to_string());
current.clear();
}
_ => current.push(ch),
}
}
let rest = current.trim().to_string();
if !rest.is_empty() {
parts.push(rest);
}
parts
}
pub(crate) fn escape_string_literal_ext(s: &str, unicode_escapes: bool) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\0"),
c if c.is_control() => {
if unicode_escapes {
out.push_str(&format!("\\U{{{:06x}}}", c as u32));
} else {
out.push_str(&format!("\\x{:02x}", c as u32));
}
}
c => out.push(c),
}
}
out
}
pub(crate) fn escape_string_literal(s: &str) -> String {
escape_string_literal_ext(s, false)
}
pub(crate) fn escape_string_literal_unicode(s: &str) -> String {
escape_string_literal_ext(s, true)
}
pub(crate) fn parse_type_annotation(ann: &str) -> Type {
crate::types::parse_type_str(ann)
}
pub(crate) fn is_set_type(ty: &Type) -> bool {
matches!(ty, Type::Map(_, v) if matches!(v.as_ref(), Type::Unit))
}
pub(crate) fn is_set_annotation(ann: &str) -> bool {
is_set_type(&parse_type_annotation(ann))
}
pub(crate) fn is_unit_expr_resolved(expr: &crate::ir::hir::ResolvedExpr) -> bool {
matches!(
expr,
crate::ir::hir::ResolvedExpr::Literal(crate::ast::Literal::Unit)
)
}
pub(crate) fn escape_reserved_word(name: &str, reserved: &[&str], suffix: &str) -> String {
if reserved.contains(&name) {
format!("{}{}", name, suffix)
} else {
name.to_string()
}
}
pub(crate) fn escape_reserved_word_prefix(name: &str, reserved: &[&str], prefix: &str) -> String {
if reserved.contains(&name) {
format!("{}{}", prefix, name)
} else {
name.to_string()
}
}
pub(crate) fn to_lower_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_lowercase().to_string() + chars.as_str(),
}
}
pub(crate) fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
crate::ir::expr_to_dotted_name(expr)
}
#[derive(Debug, Clone)]
pub(crate) enum OracleInjectionMode<'a> {
LemmaBinding,
LemmaBindingProjected,
#[allow(dead_code)]
SampleValue,
SampleCaseBinding(&'a [(String, crate::ast::Spanned<Expr>)]),
}
pub(crate) fn rewrite_effectful_calls_in_law<'fd, F>(
expr: &crate::ast::Spanned<Expr>,
law: &crate::ast::VerifyLaw,
find_fn_def: F,
mode: OracleInjectionMode,
) -> crate::ast::Spanned<Expr>
where
F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
{
use crate::ast::{Spanned, VerifyGivenDomain};
let injection_by_effect: std::collections::HashMap<String, Spanned<Expr>> = law
.givens
.iter()
.filter_map(|g| {
let arg_expr = match &mode {
OracleInjectionMode::LemmaBinding => {
Spanned::new(Expr::Ident(g.name.clone()), expr.line)
}
OracleInjectionMode::LemmaBindingProjected => {
Spanned::new(Expr::Ident(g.name.clone()), expr.line)
}
OracleInjectionMode::SampleValue => match &g.domain {
VerifyGivenDomain::Explicit(vals) => vals.first().cloned()?,
_ => return None,
},
OracleInjectionMode::SampleCaseBinding(case_bindings) => case_bindings
.iter()
.find(|(name, _)| name == &g.name)
.map(|(_, v)| v.clone())?,
};
Some((g.type_name.clone(), arg_expr))
})
.collect();
let rewritten = rewrite_effectful_call(expr, &injection_by_effect, find_fn_def);
if matches!(mode, OracleInjectionMode::LemmaBindingProjected) {
let oracle_names: std::collections::HashSet<String> = law
.givens
.iter()
.filter(|g| crate::types::checker::oracle_subtypes::has_bounded_subtype(&g.type_name))
.map(|g| g.name.clone())
.collect();
if !oracle_names.is_empty() {
return project_oracle_direct_calls(&rewritten, &oracle_names);
}
}
rewritten
}
fn project_oracle_direct_calls(
expr: &crate::ast::Spanned<Expr>,
oracle_names: &std::collections::HashSet<String>,
) -> crate::ast::Spanned<Expr> {
use crate::ast::Spanned;
let line = expr.line;
let project_ident = |name: &str, line: usize| -> Spanned<Expr> {
Spanned::new(
Expr::Attr(
Box::new(Spanned::new(Expr::Ident(name.to_string()), line)),
"val".to_string(),
),
line,
)
};
let new_node = match &expr.node {
Expr::Ident(name) if oracle_names.contains(name) => {
return project_ident(name, line);
}
Expr::FnCall(callee, args) => {
let new_args: Vec<Spanned<Expr>> = args
.iter()
.map(|a| project_oracle_direct_calls(a, oracle_names))
.collect();
let new_callee = if let Expr::Ident(name) = &callee.node
&& oracle_names.contains(name)
{
project_ident(name, callee.line)
} else {
project_oracle_direct_calls(callee, oracle_names)
};
Expr::FnCall(Box::new(new_callee), new_args)
}
Expr::Constructor(name, Some(arg)) => Expr::Constructor(
name.clone(),
Some(Box::new(project_oracle_direct_calls(arg, oracle_names))),
),
Expr::Attr(obj, field) => Expr::Attr(
Box::new(project_oracle_direct_calls(obj, oracle_names)),
field.clone(),
),
Expr::BinOp(op, l, r) => Expr::BinOp(
*op,
Box::new(project_oracle_direct_calls(l, oracle_names)),
Box::new(project_oracle_direct_calls(r, oracle_names)),
),
other => other.clone(),
};
Spanned::new(new_node, line)
}
fn rewrite_effectful_call<'fd, F>(
expr: &crate::ast::Spanned<Expr>,
injection_by_effect: &std::collections::HashMap<String, crate::ast::Spanned<Expr>>,
find_fn_def: F,
) -> crate::ast::Spanned<Expr>
where
F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
{
use crate::ast::Spanned;
use crate::types::checker::effect_classification::{EffectDimension, classify};
match &expr.node {
Expr::FnCall(callee, args) => {
let rewritten_args: Vec<Spanned<Expr>> = args
.iter()
.map(|a| rewrite_effectful_call(a, injection_by_effect, find_fn_def))
.collect();
let rewritten_callee = Box::new(rewrite_effectful_call(
callee,
injection_by_effect,
find_fn_def,
));
let callee_name = match &callee.node {
Expr::Ident(name) => Some(name.clone()),
Expr::Resolved { name, .. } => Some(name.clone()),
_ => None,
};
if let Some(name) = callee_name
&& let Some(fd) = find_fn_def(&name)
&& !fd.effects.is_empty()
&& fd
.effects
.iter()
.all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
{
let mut injected: Vec<Spanned<Expr>> = Vec::new();
let needs_path = fd.effects.iter().any(|e| {
matches!(
classify(&e.node).map(|c| c.dimension),
Some(EffectDimension::Generative | EffectDimension::GenerativeOutput)
)
});
if needs_path {
injected.push(Spanned::new(
Expr::Attr(
Box::new(Spanned::new(
Expr::Ident("BranchPath".to_string()),
expr.line,
)),
"Root".to_string(),
),
expr.line,
));
}
let mut seen = std::collections::HashSet::new();
for e in &fd.effects {
if !seen.insert(e.node.clone()) {
continue;
}
let Some(c) = classify(&e.node) else { continue };
if matches!(c.dimension, EffectDimension::Output) {
continue;
}
if let Some(inj) = injection_by_effect.get(&e.node) {
injected.push(inj.clone());
}
}
injected.extend(rewritten_args);
return Spanned::new(Expr::FnCall(rewritten_callee, injected), expr.line);
}
Spanned::new(Expr::FnCall(rewritten_callee, rewritten_args), expr.line)
}
Expr::BinOp(op, l, r) => Spanned::new(
Expr::BinOp(
*op,
Box::new(rewrite_effectful_call(l, injection_by_effect, find_fn_def)),
Box::new(rewrite_effectful_call(r, injection_by_effect, find_fn_def)),
),
expr.line,
),
Expr::Tuple(items) => Spanned::new(
Expr::Tuple(
items
.iter()
.map(|i| rewrite_effectful_call(i, injection_by_effect, find_fn_def))
.collect(),
),
expr.line,
),
_ => expr.clone(),
}
}
pub(crate) fn verify_reachable_fn_names(items: &[TopLevel]) -> HashSet<String> {
let mut reachable: HashSet<String> = HashSet::new();
for item in items {
if let TopLevel::Verify(vb) = item {
collect_verify_block_refs(vb, &mut reachable);
}
}
loop {
let mut changed = false;
for item in items {
if let TopLevel::FnDef(fd) = item
&& reachable.contains(&fd.name)
{
let mut called = HashSet::new();
collect_called_idents_in_body(&fd.body, &mut called);
for name in called {
if reachable.insert(name) {
changed = true;
}
}
}
}
if !changed {
break;
}
}
reachable
}
fn collect_verify_block_refs(vb: &VerifyBlock, out: &mut HashSet<String>) {
out.insert(vb.fn_name.clone());
for (lhs, rhs) in &vb.cases {
collect_called_idents(lhs, out);
collect_called_idents(rhs, out);
}
if let VerifyKind::Law(law) = &vb.kind {
collect_called_idents(&law.lhs, out);
collect_called_idents(&law.rhs, out);
if let Some(when) = &law.when {
collect_called_idents(when, out);
}
for given in &law.givens {
if let VerifyGivenDomain::Explicit(values) = &given.domain {
for v in values {
collect_called_idents(v, out);
}
}
}
}
for given in &vb.cases_givens {
if let VerifyGivenDomain::Explicit(values) = &given.domain {
for v in values {
collect_called_idents(v, out);
}
}
}
}
fn collect_called_idents_in_body(body: &FnBody, out: &mut HashSet<String>) {
for stmt in body.stmts() {
match stmt {
Stmt::Binding(_, _, e) | Stmt::Expr(e) => collect_called_idents(e, out),
}
}
}
fn collect_called_idents(expr: &Spanned<Expr>, out: &mut HashSet<String>) {
match &expr.node {
Expr::FnCall(callee, args) => {
if let Expr::Ident(name) | Expr::Resolved { name, .. } = &callee.node {
out.insert(name.clone());
} else {
collect_called_idents(callee, out);
}
for a in args {
collect_called_idents(a, out);
}
}
Expr::TailCall(boxed) => {
let TailCallData { target, args, .. } = boxed.as_ref();
out.insert(target.clone());
for a in args {
collect_called_idents(a, out);
}
}
Expr::Ident(name) | Expr::Resolved { name, .. } => {
out.insert(name.clone());
}
Expr::BinOp(_, l, r) => {
collect_called_idents(l, out);
collect_called_idents(r, out);
}
Expr::Neg(inner) => collect_called_idents(inner, out),
Expr::Match { subject, arms, .. } => {
collect_called_idents(subject, out);
for arm in arms {
collect_called_idents(&arm.body, out);
}
}
Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
collect_called_idents(inner, out);
}
Expr::Constructor(_, Some(inner)) => {
collect_called_idents(inner, out);
}
Expr::InterpolatedStr(parts) => {
for part in parts {
if let StrPart::Parsed(inner) = part {
collect_called_idents(inner, out);
}
}
}
Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
for i in items {
collect_called_idents(i, out);
}
}
Expr::MapLiteral(entries) => {
for (k, v) in entries {
collect_called_idents(k, out);
collect_called_idents(v, out);
}
}
Expr::RecordCreate { fields, .. } => {
for (_, v) in fields {
collect_called_idents(v, out);
}
}
Expr::RecordUpdate { base, updates, .. } => {
collect_called_idents(base, out);
for (_, v) in updates {
collect_called_idents(v, out);
}
}
Expr::Literal(_) | Expr::Constructor(_, None) => {}
}
}
pub(crate) struct PerScopeSections {
pub by_scope: std::collections::HashMap<String, Vec<String>>,
}
impl PerScopeSections {
pub(crate) fn take(&mut self, scope: &str) -> Vec<String> {
self.by_scope.remove(scope).unwrap_or_default()
}
}
pub(crate) fn route_pure_components_per_scope<F, G>(
ctx: &CodegenContext,
is_pure: F,
mut emit: G,
) -> PerScopeSections
where
F: Fn(&FnDef) -> bool,
G: FnMut(&[&FnDef], &str) -> Vec<String>,
{
let mut by_scope: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut process =
|fns: Vec<&FnDef>,
scope: String,
by_scope: &mut std::collections::HashMap<String, Vec<String>>| {
let comps = crate::call_graph::ordered_fn_components(&fns, &ctx.module_prefixes);
let bucket = by_scope.entry(scope.clone()).or_default();
for comp in comps {
bucket.extend(emit(&comp, scope.as_str()));
}
};
for module in &ctx.modules {
let pure: Vec<&FnDef> = module.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
process(pure, module.prefix.clone(), &mut by_scope);
}
let entry_pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
process(entry_pure, String::new(), &mut by_scope);
PerScopeSections { by_scope }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Literal, VerifyGiven, VerifyGivenDomain, VerifyLaw};
fn sb(node: Expr) -> Spanned<Expr> {
Spanned::new(node, 1)
}
fn bsb(node: Expr) -> Box<Spanned<Expr>> {
Box::new(sb(node))
}
fn law_with(lhs: Spanned<Expr>, rhs: Spanned<Expr>) -> VerifyLaw {
VerifyLaw {
name: "test".to_string(),
givens: vec![VerifyGiven {
name: "xs".to_string(),
type_name: "List<String>".to_string(),
domain: VerifyGivenDomain::Explicit(vec![sb(Expr::List(vec![]))]),
}],
when: None,
lhs,
rhs,
sample_guards: Vec::new(),
}
}
#[test]
fn law_calls_unclassified_fn_detects_dotted_callee() {
let lhs = sb(Expr::FnCall(
bsb(Expr::Attr(
bsb(Expr::Ident("Dep".to_string())),
"toSorted".to_string(),
)),
vec![sb(Expr::Ident("xs".to_string()))],
));
let rhs = sb(Expr::Ident("xs".to_string()));
let law = law_with(lhs, rhs);
let mut canonical = HashSet::new();
canonical.insert("Dep.toSorted".to_string());
assert!(law_calls_unclassified_fn(&law, &canonical));
let mut bare_only = HashSet::new();
bare_only.insert("toSorted".to_string());
assert!(
law_calls_unclassified_fn(&law, &bare_only),
"bare unclassified name must catch a dotted callsite via suffix match"
);
let mut unrelated = HashSet::new();
unrelated.insert("somethingElse".to_string());
assert!(!law_calls_unclassified_fn(&law, &unrelated));
}
#[test]
fn law_lhs_has_trace_projection_skips_user_record_field() {
let user_field_lhs = sb(Expr::Attr(
bsb(Expr::Ident("log".to_string())),
"trace".to_string(),
));
assert!(
!law_lhs_has_trace_projection(&user_field_lhs),
"bare user-record `.trace` field must not trigger the gate"
);
let runtime_trace_lhs = sb(Expr::FnCall(
bsb(Expr::Attr(
bsb(Expr::Attr(
bsb(Expr::FnCall(bsb(Expr::Ident("fn".to_string())), vec![])),
"trace".to_string(),
)),
"event".to_string(),
)),
vec![sb(Expr::Literal(Literal::Int(0)))],
));
assert!(
law_lhs_has_trace_projection(&runtime_trace_lhs),
"Oracle `.trace.event(0)` projection must trigger the gate"
);
}
#[test]
fn resolve_refined_type_disambiguates_cross_module_same_bare_name() {
use crate::ast::{TypeDef, TypeVariant};
use crate::codegen::ModuleInfo;
use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
use std::collections::HashMap;
let _ = TypeVariant {
name: String::new(),
fields: Vec::new(),
};
let make_module = |prefix: &str| ModuleInfo {
prefix: prefix.to_string(),
depends: Vec::new(),
type_defs: vec![TypeDef::Product {
name: "Natural".to_string(),
fields: vec![("value".to_string(), "Int".to_string())],
line: 1,
}],
fn_defs: Vec::new(),
verify_laws: Vec::new(),
analysis: None,
};
let modules = vec![make_module("A"), make_module("B")];
let make_decl = |predicate_param: &str, witness: i64| RefinedTypeDecl {
name: "Natural".to_string(),
carrier_type: "Int".to_string(),
carrier_field: "value".to_string(),
predicate_param: predicate_param.to_string(),
invariant: Predicate {
free_vars: vec![(
predicate_param.to_string(),
QuantifierType::Plain("Int".to_string()),
)],
expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
Literal::Bool(true),
)),
},
witness: Some(witness.to_string()),
interval: None,
op_classes: Vec::new(),
};
let symbols = crate::ir::SymbolTable::build(&[], &modules);
let a_id = symbols
.type_id_of(&crate::ir::TypeKey::in_module("A", "Natural"))
.expect("A.Natural TypeId");
let b_id = symbols
.type_id_of(&crate::ir::TypeKey::in_module("B", "Natural"))
.expect("B.Natural TypeId");
let mut refined_types: HashMap<crate::ir::TypeId, RefinedTypeDecl> = HashMap::new();
refined_types.insert(a_id, make_decl("a", 0));
refined_types.insert(b_id, make_decl("b", 10));
let a = resolve_refined_type_in(&refined_types, &symbols, &modules, "A.Natural")
.expect("A.Natural canonical lookup");
assert_eq!(a.predicate_param, "a");
assert_eq!(a.witness.as_deref(), Some("0"));
let b = resolve_refined_type_in(&refined_types, &symbols, &modules, "B.Natural")
.expect("B.Natural canonical lookup");
assert_eq!(b.predicate_param, "b");
assert_eq!(b.witness.as_deref(), Some("10"));
let bare = resolve_refined_type_in(&refined_types, &symbols, &modules, "Natural")
.expect("bare Natural resolves via module walk");
assert!(
bare.predicate_param == "a" || bare.predicate_param == "b",
"bare Natural must resolve to one of the canonical decls"
);
assert!(resolve_refined_type_in(&refined_types, &symbols, &modules, "Unrelated").is_none());
}
#[test]
fn find_refined_type_scoped_prefers_current_module_over_entry_collision() {
use crate::ast::{TopLevel, TypeDef};
use crate::codegen::{CodegenContext, ModuleInfo};
use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
use std::collections::{HashMap, HashSet};
let entry_natural = TypeDef::Product {
name: "Natural".to_string(),
fields: vec![("value".to_string(), "Int".to_string())],
line: 1,
};
let module = ModuleInfo {
prefix: "Mod".to_string(),
depends: Vec::new(),
type_defs: vec![TypeDef::Product {
name: "Natural".to_string(),
fields: vec![("value".to_string(), "Int".to_string())],
line: 1,
}],
fn_defs: Vec::new(),
verify_laws: Vec::new(),
analysis: None,
};
let make_decl = |param: &str, witness: &str| RefinedTypeDecl {
name: "Natural".to_string(),
carrier_type: "Int".to_string(),
carrier_field: "value".to_string(),
predicate_param: param.to_string(),
invariant: Predicate {
free_vars: vec![(param.to_string(), QuantifierType::Plain("Int".to_string()))],
expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
Literal::Bool(true),
)),
},
witness: Some(witness.to_string()),
interval: None,
op_classes: Vec::new(),
};
let items = vec![TopLevel::TypeDef(entry_natural)];
let modules = vec![module];
let symbol_table = crate::ir::SymbolTable::build(&items, &modules);
let entry_id = symbol_table
.type_id_of(&crate::ir::TypeKey::entry("Natural"))
.expect("entry Natural id");
let mod_id = symbol_table
.type_id_of(&crate::ir::TypeKey::in_module("Mod", "Natural"))
.expect("Mod.Natural id");
let mut ctx = CodegenContext {
items,
type_defs: Vec::new(),
fn_defs: Vec::new(),
project_name: "scope-test".to_string(),
modules,
module_prefixes: HashSet::new(),
#[cfg(feature = "runtime")]
policy: None,
emit_replay_runtime: false,
runtime_policy_from_env: false,
guest_entry: None,
emit_self_host_support: false,
extra_fn_defs: Vec::new(),
mutual_tco_members: HashSet::new(),
recursive_fns: HashSet::new(),
buffer_build_sinks: HashMap::new(),
buffer_fusion_sites: Vec::new(),
synthesized_buffered_fns: Vec::new(),
proof_ir: crate::ir::ProofIR::default(),
symbol_table,
resolved_fn_defs: Vec::new(),
resolved_module_fn_defs: Vec::new(),
current_module_scope: std::cell::RefCell::new(None),
resolved_program: crate::codegen::program_view::ResolvedProgramView::default(),
program_shape: None,
mir_program: None,
bare_i64: Default::default(),
discovered_lemmas: Vec::new(),
sample_expected: std::collections::HashMap::new(),
};
ctx.proof_ir
.refined_types
.insert(entry_id, make_decl("entry_n", "0"));
ctx.proof_ir
.refined_types
.insert(mod_id, make_decl("mod_n", "10"));
let from_module = find_refined_type_scoped(&ctx, "Natural", Some("Mod"))
.expect("Mod-scoped Natural lookup");
assert_eq!(
from_module.predicate_param, "mod_n",
"scope=Some(\"Mod\") + bare `Natural` must resolve to Mod.Natural, \
not entry's bare-keyed slot"
);
let from_entry =
find_refined_type_scoped(&ctx, "Natural", None).expect("entry Natural lookup");
assert_eq!(from_entry.predicate_param, "entry_n");
}
}