use std::collections::HashMap;
#[derive(Clone, Debug)]
pub(super) struct CalleeSpec {
pub(super) params: Vec<String>,
pub(super) ensures: Vec<String>,
}
pub(super) fn sibling_free_fn_specs(items: &[syn::Item]) -> HashMap<String, CalleeSpec> {
let mut out = HashMap::new();
for item in items {
let syn::Item::Fn(func) = item else {
continue;
};
let Some((name, spec)) = fn_spec(&func.sig, &func.attrs) else {
continue;
};
out.entry(name).or_insert(spec);
}
out
}
fn fn_spec(sig: &syn::Signature, attrs: &[syn::Attribute]) -> Option<(String, CalleeSpec)> {
let ensures = clause_bodies(attrs, &["ensures"]);
if ensures.is_empty() {
return None;
}
let mut params = Vec::new();
for input in &sig.inputs {
match input {
syn::FnArg::Receiver(_) => return None,
syn::FnArg::Typed(pat) => params.push(pat_name(&pat.pat)?),
}
}
Some((sig.ident.to_string(), CalleeSpec { params, ensures }))
}
fn pat_name(pat: &syn::Pat) -> Option<String> {
match pat {
syn::Pat::Ident(id) if id.by_ref.is_none() && id.subpat.is_none() => {
Some(id.ident.to_string())
}
syn::Pat::Type(inner) => pat_name(&inner.pat),
syn::Pat::Reference(inner) => pat_name(&inner.pat),
_ => None,
}
}
pub(super) fn clause_bodies(attrs: &[syn::Attribute], keys: &[&str]) -> Vec<String> {
let mut out = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc") {
if let syn::Meta::NameValue(nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &nv.value
&& let Some(body) = doc_clause(&lit_str.value(), keys)
{
out.push(body);
}
continue;
}
if !keys.iter().any(|key| attr.path().is_ident(key)) {
continue;
}
if let syn::Meta::List(list) = &attr.meta {
let body = list.tokens.to_string();
let body = body.trim();
if !body.is_empty() {
out.push(body.to_string());
}
}
}
out
}
fn doc_clause(line: &str, keys: &[&str]) -> Option<String> {
let rest = line.trim().strip_prefix('@')?;
let (keyword, body) = match rest.find(char::is_whitespace) {
Some(index) => (&rest[..index], rest[index + 1..].trim()),
None => (rest, ""),
};
if keys.contains(&keyword) && !body.is_empty() {
Some(body.to_string())
} else {
None
}
}
pub(super) fn loop_attrs(expr: &syn::Expr) -> &[syn::Attribute] {
match expr {
syn::Expr::While(w) => w.attrs.as_slice(),
syn::Expr::ForLoop(f) => f.attrs.as_slice(),
syn::Expr::Loop(l) => l.attrs.as_slice(),
_ => &[],
}
}
pub(super) fn simple_call(expr: &syn::Expr) -> Option<&syn::ExprCall> {
match expr {
syn::Expr::Call(call) => Some(call),
syn::Expr::Paren(inner) => simple_call(&inner.expr),
syn::Expr::Group(inner) => simple_call(&inner.expr),
_ => None,
}
}
pub(super) fn call_fn_name(func: &syn::Expr) -> Option<String> {
match func {
syn::Expr::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
Some(path.path.segments[0].ident.to_string())
}
syn::Expr::Paren(inner) => call_fn_name(&inner.expr),
syn::Expr::Group(inner) => call_fn_name(&inner.expr),
_ => None,
}
}
pub(super) fn expr_has_mut_ref(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Reference(reference) => {
reference.mutability.is_some() || expr_has_mut_ref(&reference.expr)
}
syn::Expr::Paren(inner) => expr_has_mut_ref(&inner.expr),
syn::Expr::Group(inner) => expr_has_mut_ref(&inner.expr),
syn::Expr::Cast(cast) => expr_has_mut_ref(&cast.expr),
syn::Expr::Unary(unary) => expr_has_mut_ref(&unary.expr),
syn::Expr::Field(field) => expr_has_mut_ref(&field.base),
syn::Expr::Binary(binary) => {
expr_has_mut_ref(&binary.left) || expr_has_mut_ref(&binary.right)
}
syn::Expr::Call(call) => {
expr_has_mut_ref(&call.func) || call.args.iter().any(expr_has_mut_ref)
}
syn::Expr::MethodCall(call) => {
expr_has_mut_ref(&call.receiver) || call.args.iter().any(expr_has_mut_ref)
}
syn::Expr::Tuple(tuple) => tuple.elems.iter().any(expr_has_mut_ref),
syn::Expr::Array(array) => array.elems.iter().any(expr_has_mut_ref),
syn::Expr::Repeat(repeat) => {
expr_has_mut_ref(&repeat.expr) || expr_has_mut_ref(&repeat.len)
}
syn::Expr::Index(index) => expr_has_mut_ref(&index.expr) || expr_has_mut_ref(&index.index),
syn::Expr::Range(range) => {
range
.start
.as_ref()
.is_some_and(|start| expr_has_mut_ref(start))
|| range.end.as_ref().is_some_and(|end| expr_has_mut_ref(end))
}
syn::Expr::Block(block) => block.block.stmts.iter().any(stmt_has_mut_ref),
syn::Expr::If(if_expr) => {
expr_has_mut_ref(&if_expr.cond)
|| if_expr.then_branch.stmts.iter().any(stmt_has_mut_ref)
|| if_expr
.else_branch
.as_ref()
.is_some_and(|(_, else_expr)| expr_has_mut_ref(else_expr))
}
syn::Expr::Struct(struct_expr) => {
struct_expr
.fields
.iter()
.any(|field| expr_has_mut_ref(&field.expr))
|| struct_expr
.rest
.as_ref()
.is_some_and(|rest| expr_has_mut_ref(rest))
}
syn::Expr::Path(_) | syn::Expr::Lit(_) => false,
_ => true,
}
}
fn stmt_has_mut_ref(stmt: &syn::Stmt) -> bool {
match stmt {
syn::Stmt::Expr(expr, _) => expr_has_mut_ref(expr),
syn::Stmt::Local(local) => local.init.as_ref().is_some_and(|init| {
expr_has_mut_ref(&init.expr)
|| init
.diverge
.as_ref()
.is_some_and(|(_, else_expr)| expr_has_mut_ref(else_expr))
}),
_ => true,
}
}
pub(super) fn ident_expr(name: &str) -> Option<syn::Expr> {
syn::parse_str(name).ok()
}
pub(super) fn expr_mentions_ident(expr: &syn::Expr, name: &str) -> bool {
match expr {
syn::Expr::Path(path) => path.path.is_ident(name),
syn::Expr::Binary(binary) => {
expr_mentions_ident(&binary.left, name) || expr_mentions_ident(&binary.right, name)
}
syn::Expr::Unary(unary) => expr_mentions_ident(&unary.expr, name),
syn::Expr::Paren(inner) => expr_mentions_ident(&inner.expr, name),
syn::Expr::Group(inner) => expr_mentions_ident(&inner.expr, name),
syn::Expr::Reference(inner) => expr_mentions_ident(&inner.expr, name),
syn::Expr::Cast(cast) => expr_mentions_ident(&cast.expr, name),
syn::Expr::Field(field) => expr_mentions_ident(&field.base, name),
syn::Expr::Index(index) => {
expr_mentions_ident(&index.expr, name) || expr_mentions_ident(&index.index, name)
}
syn::Expr::Call(call) => {
expr_mentions_ident(&call.func, name)
|| call.args.iter().any(|arg| expr_mentions_ident(arg, name))
}
syn::Expr::Block(block) => block
.block
.stmts
.iter()
.any(|stmt| stmt_mentions_ident(stmt, name)),
syn::Expr::If(if_expr) => {
expr_mentions_ident(&if_expr.cond, name)
|| if_expr
.then_branch
.stmts
.iter()
.any(|stmt| stmt_mentions_ident(stmt, name))
|| if_expr
.else_branch
.as_ref()
.is_some_and(|(_, else_expr)| expr_mentions_ident(else_expr, name))
}
_ => false,
}
}
fn stmt_mentions_ident(stmt: &syn::Stmt, name: &str) -> bool {
match stmt {
syn::Stmt::Expr(expr, _) => expr_mentions_ident(expr, name),
syn::Stmt::Local(local) => local
.init
.as_ref()
.is_some_and(|init| expr_mentions_ident(&init.expr, name)),
_ => false,
}
}
pub(super) fn lower_assumes(assumes: &[String], names: &[&str]) -> Option<String> {
let mut preds = Vec::new();
for assume in assumes {
let expr: syn::Expr = syn::parse_str(assume).ok()?;
preds.push(expr_to_ir_pred(&expr, names)?);
}
let mut preds = preds.into_iter();
let mut combined = preds.next()?;
for pred in preds {
combined = format!("and ({combined}) ({pred})");
}
Some(combined)
}
fn expr_to_ir_pred(expr: &syn::Expr, names: &[&str]) -> Option<String> {
match peel_expr(expr) {
syn::Expr::Binary(binary) => {
if let Some(op) = cmp_op_name(&binary.op) {
let lhs = expr_to_pred_arg(&binary.left, names)?;
let rhs = expr_to_pred_arg(&binary.right, names)?;
return Some(format!("cmp {op} {lhs} {rhs}"));
}
if matches!(binary.op, syn::BinOp::And(_)) {
let lhs = expr_to_ir_pred(&binary.left, names)?;
let rhs = expr_to_ir_pred(&binary.right, names)?;
return Some(format!("and ({lhs}) ({rhs})"));
}
if matches!(binary.op, syn::BinOp::Or(_)) {
let lhs = expr_to_ir_pred(&binary.left, names)?;
let rhs = expr_to_ir_pred(&binary.right, names)?;
return Some(format!("or ({lhs}) ({rhs})"));
}
None
}
syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Not(_)) => {
let inner = expr_to_ir_pred(&unary.expr, names)?;
Some(format!("not {inner}"))
}
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(value),
..
}) => Some(if value.value { "true" } else { "false" }.to_string()),
_ => None,
}
}
fn expr_to_pred_arg(expr: &syn::Expr, names: &[&str]) -> Option<String> {
match peel_expr(expr) {
syn::Expr::Path(path) if path.path.segments.len() == 1 => {
let name = path.path.segments[0].ident.to_string();
let index = names.iter().position(|candidate| *candidate == name)?;
Some(format!("${index}"))
}
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(number),
..
}) => {
let text = number.base10_digits();
text.parse::<i64>().ok()?;
Some(format!("(const {text})"))
}
syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(number),
..
}) = peel_expr(&unary.expr)
{
let value = number.base10_parse::<i64>().ok()?.checked_neg()?;
return Some(format!("(const {value})"));
}
let inner = expr_to_pred_arg(&unary.expr, names)?;
Some(format!("(arith sub (const 0) {inner})"))
}
syn::Expr::Binary(binary) => {
let op = arith_op_name(&binary.op)?;
let lhs = expr_to_pred_arg(&binary.left, names)?;
let rhs = expr_to_pred_arg(&binary.right, names)?;
Some(format!("(arith {op} {lhs} {rhs})"))
}
_ => None,
}
}
fn peel_expr(expr: &syn::Expr) -> &syn::Expr {
match expr {
syn::Expr::Paren(inner) => peel_expr(&inner.expr),
syn::Expr::Group(inner) => peel_expr(&inner.expr),
syn::Expr::Reference(inner) => peel_expr(&inner.expr),
other => other,
}
}
fn cmp_op_name(op: &syn::BinOp) -> Option<&'static str> {
Some(match op {
syn::BinOp::Eq(_) => "eq",
syn::BinOp::Ne(_) => "ne",
syn::BinOp::Lt(_) => "lt",
syn::BinOp::Le(_) => "le",
syn::BinOp::Gt(_) => "gt",
syn::BinOp::Ge(_) => "ge",
_ => return None,
})
}
fn arith_op_name(op: &syn::BinOp) -> Option<&'static str> {
Some(match op {
syn::BinOp::Add(_) => "add",
syn::BinOp::Sub(_) => "sub",
syn::BinOp::Mul(_) => "mul",
syn::BinOp::Div(_) => "div",
syn::BinOp::Rem(_) => "mod",
_ => return None,
})
}