use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use syn::{Attribute, File, ImplItem, ImplItemFn, ItemFn};
fn impl_item_fn_to_item_fn(impl_fn: &ImplItemFn) -> ItemFn {
ItemFn {
attrs: impl_fn.attrs.clone(),
vis: impl_fn.vis.clone(),
sig: impl_fn.sig.clone(),
block: Box::new(impl_fn.block.clone()),
}
}
#[derive(Debug, Clone)]
pub struct Contract {
pub contract_type: ContractType,
pub condition: String,
pub expr: Option<syn::Expr>, pub is_spec_derived: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContractType {
Requires,
Ensures,
Axiom,
}
fn extract_contracts(func: &ItemFn) -> Vec<Contract> {
let mut contracts = Vec::new();
for attr in &func.attrs {
let path = attr.path();
if path.is_ident("cfg_attr") {
if let Ok(meta) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) {
if let Some(syn::Meta::List(inner_list)) = meta.iter().nth(1) {
let last_ident = inner_list.path.segments.last().map(|s| s.ident.to_string());
let contract_kind = match last_ident.as_deref() {
Some("ensures") => Some(ContractType::Ensures),
Some("requires") => Some(ContractType::Requires),
Some("axiom") => Some(ContractType::Axiom),
_ => None,
};
if let Some(kind) = contract_kind {
let arg_str = inner_list.tokens.to_string();
let expr = syn::parse_str::<syn::Expr>(&arg_str).ok();
contracts.push(Contract {
contract_type: kind,
condition: arg_str,
expr,
is_spec_derived: false,
});
}
}
}
continue;
}
let is_requires = path.is_ident("requires")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "requires");
let is_ensures = path.is_ident("ensures")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "ensures");
let is_axiom = path.is_ident("axiom")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "axiom");
if is_requires || is_ensures || is_axiom {
let kind = if is_requires {
ContractType::Requires
} else if is_ensures {
ContractType::Ensures
} else {
ContractType::Axiom
};
if let Ok(expr) = attr.parse_args::<syn::Expr>() {
let condition_str = quote::quote!(#expr).to_string();
contracts.push(Contract {
contract_type: kind,
condition: condition_str,
expr: Some(expr),
is_spec_derived: false,
});
} else {
let condition_str = quote::quote!(#attr).to_string();
contracts.push(Contract {
contract_type: kind,
condition: condition_str,
expr: None,
is_spec_derived: false,
});
}
}
}
contracts
}
#[derive(Debug, Clone)]
pub struct FunctionToVerify {
pub file_path: PathBuf,
pub function_name: String,
pub contracts: Vec<Contract>,
pub section: Option<String>,
pub spec_name_override: Option<String>,
pub formula_anchor: Option<String>,
pub constant_anchor: Option<String>,
pub function_sig: Option<syn::ItemFn>, }
#[derive(Debug, Clone)]
pub struct CalleePostcond {
pub function_name: String,
pub condition: String,
}
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let path_str = path.to_string_lossy();
if path_str.contains("/target/")
|| path_str.contains("/.git/")
|| path_str.contains("/.cargo/")
{
continue;
}
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
out.push(path);
}
}
}
pub fn discover_functions(workspace_root: &Path) -> Result<Vec<FunctionToVerify>, String> {
let mut functions = Vec::new();
let mut errors = Vec::new();
let mut rs_files = Vec::new();
collect_rs_files(workspace_root, &mut rs_files);
for path in &rs_files {
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
match parse_file_for_functions(path) {
Ok(mut file_functions) => {
functions.append(&mut file_functions);
}
Err(e) => {
errors.push(format!("{}: {}", path.display(), e));
}
}
}
}
if !functions.is_empty() || errors.is_empty() {
Ok(functions)
} else {
Err(format!(
"Failed to discover functions:\n{}",
errors.join("\n")
))
}
}
fn parse_file_for_functions(file_path: &std::path::Path) -> Result<Vec<FunctionToVerify>, String> {
let content = std::fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?;
let ast: File = syn::parse_file(&content)
.map_err(|e| format!("Failed to parse {}: {}", file_path.display(), e))?;
let mut functions = Vec::new();
for item in ast.items {
match item {
syn::Item::Fn(func) if has_spec_locked(&func.attrs) => {
functions.push(make_function_to_verify(
file_path,
&func.sig.ident.to_string(),
&func.attrs,
&func,
));
}
syn::Item::Impl(impl_item) => {
for assoc_item in &impl_item.items {
if let ImplItem::Fn(impl_fn) = assoc_item {
if has_spec_locked(&impl_fn.attrs) {
if std::env::var("SPEC_LOCK_DEBUG_ATTR_PARSE").is_ok() {
eprintln!(
"IMPL_FN: {} has {} attrs",
impl_fn.sig.ident,
impl_fn.attrs.len()
);
}
let contracts = extract_contracts_from_attrs(&impl_fn.attrs);
let section = extract_section(&impl_fn.attrs);
let item_fn = impl_item_fn_to_item_fn(impl_fn);
functions.push(FunctionToVerify {
file_path: file_path.to_path_buf(),
function_name: impl_fn.sig.ident.to_string(),
contracts,
section,
spec_name_override: extract_spec_name_override(&impl_fn.attrs),
formula_anchor: extract_formula_anchor(&impl_fn.attrs),
constant_anchor: extract_constant_anchor(&impl_fn.attrs),
function_sig: Some(item_fn),
});
}
}
}
}
_ => {}
}
}
Ok(functions)
}
fn make_function_to_verify(
file_path: &std::path::Path,
function_name: &str,
attrs: &[Attribute],
func: &ItemFn,
) -> FunctionToVerify {
let contracts = extract_contracts(func);
let section = extract_section(attrs);
FunctionToVerify {
file_path: file_path.to_path_buf(),
function_name: function_name.to_string(),
contracts,
section,
spec_name_override: extract_spec_name_override(attrs),
formula_anchor: extract_formula_anchor(attrs),
constant_anchor: extract_constant_anchor(attrs),
function_sig: Some(func.clone()),
}
}
fn extract_contracts_from_attrs(attrs: &[Attribute]) -> Vec<Contract> {
let mut contracts = Vec::new();
for attr in attrs {
let path = attr.path();
if std::env::var("SPEC_LOCK_DEBUG_ATTR_PARSE").is_ok() {
let segs: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
let is_ens_check = path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "ensures";
eprintln!(
"ATTR_PARSE: path_segs={:?} is_ident_ensures={} 2seg_check={}",
segs,
path.is_ident("ensures"),
is_ens_check
);
}
if path.is_ident("cfg_attr") {
if let Ok(meta) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
) {
if let Some(syn::Meta::List(inner_list)) = meta.iter().nth(1) {
let last_ident = inner_list.path.segments.last().map(|s| s.ident.to_string());
let contract_kind = match last_ident.as_deref() {
Some("ensures") => Some(ContractType::Ensures),
Some("requires") => Some(ContractType::Requires),
Some("axiom") => Some(ContractType::Axiom),
_ => None,
};
if let Some(kind) = contract_kind {
let arg_str = inner_list.tokens.to_string();
let expr = syn::parse_str::<syn::Expr>(&arg_str).ok();
contracts.push(Contract {
contract_type: kind,
condition: arg_str,
expr,
is_spec_derived: false,
});
}
}
}
continue;
}
let is_requires = path.is_ident("requires")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "requires");
let is_ensures = path.is_ident("ensures")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "ensures");
let is_axiom = path.is_ident("axiom")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "axiom");
if is_requires || is_ensures || is_axiom {
let kind = if is_requires {
ContractType::Requires
} else if is_ensures {
ContractType::Ensures
} else {
ContractType::Axiom
};
if let Ok(expr) = attr.parse_args::<syn::Expr>() {
contracts.push(Contract {
contract_type: kind,
condition: quote::quote!(#expr).to_string(),
expr: Some(expr),
is_spec_derived: false,
});
} else {
contracts.push(Contract {
contract_type: kind,
condition: quote::quote!(#attr).to_string(),
expr: None,
is_spec_derived: false,
});
}
}
}
contracts
}
fn has_spec_locked(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
let path = attr.path();
if path.is_ident("spec_locked")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "spec_locked")
{
return true;
}
if path.is_ident("cfg_attr") {
let tokens = quote::quote!(#attr).to_string();
return tokens.contains("spec_locked");
}
false
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SecondarySpecAnchor {
Formula(String),
Constant(String),
}
fn classify_secondary_anchor(id: &str) -> Option<SecondarySpecAnchor> {
if id.starts_with("F_")
&& id.len() > 2
&& id[2..]
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Some(SecondarySpecAnchor::Formula(id.to_string()));
}
if id.starts_with("C_")
&& id.len() > 2
&& id[2..]
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Some(SecondarySpecAnchor::Constant(id.to_string()));
}
None
}
pub fn extract_secondary_spec_anchor(attrs: &[Attribute]) -> Option<SecondarySpecAnchor> {
static RE_NAMED: OnceLock<regex::Regex> = OnceLock::new();
static RE_COMBO: OnceLock<regex::Regex> = OnceLock::new();
static RE_DUAL: OnceLock<regex::Regex> = OnceLock::new();
fn re_named() -> &'static regex::Regex {
RE_NAMED.get_or_init(|| {
regex::Regex::new(r#"function\s*=\s*"((?:F_|C_)[A-Za-z0-9_]+)""#)
.expect("secondary anchor named-regex")
})
}
fn re_combo() -> &'static regex::Regex {
RE_COMBO.get_or_init(|| {
regex::Regex::new(
r#"spec_locked\s*\(\s*"((?:\d+)(?:\.\d+)*)::((?:F_|C_)[A-Za-z0-9_]+)"\s*\)"#,
)
.expect("secondary anchor combo regex")
})
}
fn re_dual() -> &'static regex::Regex {
RE_DUAL.get_or_init(|| {
regex::Regex::new(r#"spec_locked\s*\(\s*"[^"]*"\s*,\s*"((?:F_|C_)[A-Za-z0-9_]+)""#)
.expect("secondary anchor dual regex")
})
}
for attr in attrs {
let path = attr.path();
let tokens = quote::quote!(#attr).to_string();
let is_direct = path.is_ident("spec_locked")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "spec_locked");
let is_cfg_nested = path.is_ident("cfg_attr") && tokens.contains("spec_locked");
if !(is_direct || is_cfg_nested) {
continue;
}
if let Some(c) = re_named().captures(&tokens) {
if let Some(id) = c.get(1).and_then(|m| classify_secondary_anchor(m.as_str())) {
return Some(id);
}
}
if let Some(c) = re_combo().captures(&tokens) {
if let Some(id) = c.get(2).and_then(|m| classify_secondary_anchor(m.as_str())) {
return Some(id);
}
}
if let Some(c) = re_dual().captures(&tokens) {
if let Some(id) = c.get(1).and_then(|m| classify_secondary_anchor(m.as_str())) {
return Some(id);
}
}
}
None
}
pub fn extract_formula_anchor(attrs: &[Attribute]) -> Option<String> {
match extract_secondary_spec_anchor(attrs) {
Some(SecondarySpecAnchor::Formula(id)) => Some(id),
_ => None,
}
}
pub fn extract_constant_anchor(attrs: &[Attribute]) -> Option<String> {
match extract_secondary_spec_anchor(attrs) {
Some(SecondarySpecAnchor::Constant(id)) => Some(id),
_ => None,
}
}
fn extract_spec_name_override(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
let path = attr.path();
let tokens = quote::quote!(#attr).to_string();
let is_spec_locked = path.is_ident("spec_locked")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "spec_locked")
|| (path.is_ident("cfg_attr") && tokens.contains("spec_locked"));
if is_spec_locked {
if let Some(spec_pos) = tokens.find("spec_locked") {
let after_spec = &tokens[spec_pos..];
if let Some(start1) = after_spec.find('"') {
if let Some(end1) = after_spec[start1 + 1..].find('"') {
let after_first = &after_spec[start1 + 1 + end1 + 1..];
if let Some(start2) = after_first.find('"') {
if let Some(end2) = after_first[start2 + 1..].find('"') {
let name = &after_first[start2 + 1..start2 + 1 + end2];
if !name.starts_with("F_")
&& !name.starts_with("C_")
&& !name.is_empty()
&& name.chars().all(|c| c.is_alphanumeric() || c == '_')
{
return Some(name.to_string());
}
}
}
}
}
}
}
}
None
}
fn extract_section(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
let path = attr.path();
let tokens = quote::quote!(#attr).to_string();
let is_spec_locked = path.is_ident("spec_locked")
|| (path.segments.len() == 2
&& path.segments[0].ident == "blvm_spec_lock"
&& path.segments[1].ident == "spec_locked")
|| (path.is_ident("cfg_attr") && tokens.contains("spec_locked"));
if is_spec_locked {
if let Some(spec_pos) = tokens.find("spec_locked") {
let after_spec = &tokens[spec_pos..];
if let Some(start) = after_spec.find('"') {
if let Some(end) = after_spec[start + 1..].find('"') {
let section_raw = &after_spec[start + 1..start + 1 + end];
if section_raw.chars().any(|c| c.is_ascii_digit()) {
let section_normalized = section_raw.split_once("::").map_or_else(
|| section_raw.to_string(),
|(lhs, rhs)| {
if rhs.starts_with("F_") || rhs.starts_with("C_") {
lhs.to_string()
} else {
section_raw.to_string()
}
},
);
return Some(section_normalized);
}
}
}
}
}
}
None
}
fn extract_return_type_from_sig(func: &syn::ItemFn) -> Option<syn::Type> {
let output = match &func.sig.output {
syn::ReturnType::Default => return None,
syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
};
if let syn::Type::Path(tp) = &output {
if let Some(seg) = tp.path.segments.last() {
if seg.ident == "Result" || seg.ident == "Option" {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return Some(inner.clone());
}
}
}
}
}
Some(output)
}
#[allow(unused_variables)]
pub fn verify_function(
function: &FunctionToVerify,
timeout_secs: u64,
callee_postconds: &[CalleePostcond],
) -> VerificationResult {
if std::env::var("SPEC_LOCK_DEBUG_CONTRACTS").is_ok() {
eprintln!(
"CONTRACTS_DEBUG[{}]: {} contracts",
function.function_name,
function.contracts.len()
);
for c in &function.contracts {
eprintln!(
" {:?} sd={} cond={:?}",
c.contract_type, c.is_spec_derived, c.condition
);
}
}
if function.contracts.is_empty() {
if let Some(ref sig) = function.function_sig {
use crate::translator::z3_translator::auto_type_contracts;
let return_ty = extract_return_type_from_sig(sig);
if let Some(ret) = return_ty {
let type_contracts = auto_type_contracts(&ret);
if !type_contracts.is_empty() {
let synthetic: Vec<Contract> = type_contracts
.iter()
.filter_map(|s| {
let expr: syn::Expr = syn::parse_str(s).ok()?;
Some(Contract {
contract_type: ContractType::Ensures,
condition: s.clone(),
expr: Some(expr),
is_spec_derived: true,
})
})
.collect();
if !synthetic.is_empty() {
let mut synthetic_fn = function.clone();
synthetic_fn.contracts = synthetic;
return verify_function(&synthetic_fn, timeout_secs, callee_postconds);
}
}
}
}
return VerificationResult::NoContracts {
section: function.section.clone().unwrap_or_else(|| "?".to_string()),
};
}
let mut verified_count = 0;
let mut failed_contracts: Vec<(String, String, bool)> = Vec::new();
let mut requires_z3_count = 0;
let translation_error = RefCell::new(None::<String>);
let mut any_body_translated = false;
let requires_contracts: Vec<_> = function
.contracts
.iter()
.filter(|c| c.contract_type == ContractType::Requires)
.collect();
let ensures_contracts: Vec<_> = function
.contracts
.iter()
.filter(|c| c.contract_type == ContractType::Ensures)
.collect();
let axiom_contracts: Vec<_> = function
.contracts
.iter()
.filter(|c| c.contract_type == ContractType::Axiom)
.collect();
for contract in &axiom_contracts {
if contract.expr.is_some() {
verified_count += 1;
} else {
failed_contracts.push((
"Axiom".to_string(),
"Axiom condition could not be parsed".to_string(),
contract.is_spec_derived,
));
}
}
for contract in &requires_contracts {
if contract.condition.trim().is_empty() {
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Empty contract condition".to_string(),
contract.is_spec_derived,
));
continue;
}
if contract.expr.is_some() {
verified_count += 1;
} else {
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Cannot verify: contract condition could not be parsed as expression".to_string(),
contract.is_spec_derived,
));
}
}
if let Some(result) =
demote_if_all_spec_derived(&failed_contracts, verified_count, function.contracts.len())
{
return result;
}
if !failed_contracts.is_empty() {
let (contract_type, reason, _) = &failed_contracts[0];
return failed_verification(contract_type, reason, failed_contracts.len());
}
for contract in &ensures_contracts {
if contract.condition.trim().is_empty() {
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Empty contract condition".to_string(),
contract.is_spec_derived,
));
continue;
}
if crate::parser::condition::is_result_equality(&contract.condition) {
#[cfg(feature = "z3")]
{
if let Some(ref func) = function.function_sig {
requires_z3_count += 1;
match verify_determinism(func, &requires_contracts, timeout_secs) {
Ok(()) => verified_count += 1,
Err(e) => {
if e.contains("Could not translate body") {
if contract.is_spec_derived {
verified_count += 1;
} else {
requires_z3_count += 1;
let mut slot = translation_error.borrow_mut();
if slot.is_none() {
*slot = Some(e);
}
}
} else {
failed_contracts.push((
format!("{:?}", contract.contract_type),
format!("Determinism: {e}"),
contract.is_spec_derived,
));
}
}
}
} else {
requires_z3_count += 1;
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Determinism requires function signature".to_string(),
contract.is_spec_derived,
));
}
}
#[cfg(not(feature = "z3"))]
{
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Z3 required for determinism verification. Build blvm-spec-lock with --features z3.".to_string(),
contract.is_spec_derived,
));
}
continue;
}
if let Some(ref expr) = contract.expr {
match check_contract_statically(
expr,
contract.contract_type,
function.function_sig.as_ref(),
) {
StaticCheck::Passed => {
verified_count += 1;
}
StaticCheck::Failed(reason) => {
failed_contracts.push((
format!("{:?}", contract.contract_type),
reason,
contract.is_spec_derived,
));
}
StaticCheck::RequiresZ3 => {
requires_z3_count += 1;
#[cfg(feature = "z3")]
{
match verify_with_z3(
contract,
function.function_sig.as_ref(),
&requires_contracts,
&axiom_contracts,
timeout_secs,
callee_postconds,
) {
Err(e) => {
failed_contracts.push((
format!("{:?}", contract.contract_type),
format!("Z3: {e}"),
contract.is_spec_derived,
));
}
Ok(body_translated) => {
verified_count += 1;
if body_translated {
any_body_translated = true;
}
}
}
}
#[cfg(not(feature = "z3"))]
{
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Z3 required but not built. Build blvm-spec-lock with --features z3."
.to_string(),
contract.is_spec_derived,
));
}
}
}
} else {
requires_z3_count += 1;
failed_contracts.push((
format!("{:?}", contract.contract_type),
"Cannot verify: contract condition could not be parsed".to_string(),
contract.is_spec_derived,
));
}
}
if let Some(result) =
demote_if_all_spec_derived(&failed_contracts, verified_count, function.contracts.len())
{
return result;
}
if !failed_contracts.is_empty() {
let (contract_type, reason, _) = &failed_contracts[0];
return failed_verification(contract_type, reason, failed_contracts.len());
}
if verified_count == function.contracts.len() {
VerificationResult::Passed {
body_translated: any_body_translated,
}
} else if requires_z3_count > 0 {
let trans_err = translation_error.into_inner();
let reason_msg = format!(
"Z3 verification required but unavailable or incomplete ({} of {} verified). {}",
verified_count,
function.contracts.len(),
trans_err
.as_deref()
.unwrap_or("Build with --features z3 for full verification.")
);
#[cfg(not(feature = "z3"))]
let partial_reason = PartialReason::MissingZ3Build;
#[cfg(feature = "z3")]
let partial_reason = if trans_err.is_some() {
PartialReason::UnsupportedTranslation
} else {
PartialReason::IncompleteCoverage
};
VerificationResult::Partial {
verified: verified_count,
total: function.contracts.len(),
reason: Some(reason_msg),
partial_reason: Some(partial_reason),
}
} else {
VerificationResult::Passed {
body_translated: any_body_translated,
}
}
}
enum StaticCheck {
Passed,
Failed(String),
RequiresZ3,
}
fn check_contract_statically(
expr: &syn::Expr,
_contract_type: ContractType,
func_sig: Option<&syn::ItemFn>,
) -> StaticCheck {
if let syn::Expr::Lit(lit) = expr {
if let syn::Lit::Bool(b) = &lit.lit {
if b.value {
return StaticCheck::Passed;
}
}
}
if is_bool_exhaustion_tautology(expr) {
return StaticCheck::Passed;
}
if let Some(func) = func_sig {
if is_nonneg_tautology_for_return_type(expr, func) {
return StaticCheck::Passed;
}
}
match expr {
syn::Expr::Binary(bin) if matches!(bin.op, syn::BinOp::Ge(_)) => {
if is_zero_literal(&bin.right) || is_zero_literal(&bin.left) {
return StaticCheck::RequiresZ3;
}
}
syn::Expr::Binary(bin) if matches!(bin.op, syn::BinOp::Eq(_)) => {
if is_literal(&bin.left) || is_literal(&bin.right) {
return StaticCheck::RequiresZ3;
}
}
syn::Expr::Binary(bin)
if matches!(
bin.op,
syn::BinOp::Lt(_) | syn::BinOp::Le(_) | syn::BinOp::Gt(_) | syn::BinOp::Ge(_)
) =>
{
return StaticCheck::RequiresZ3;
}
syn::Expr::Binary(bin) if matches!(bin.op, syn::BinOp::And(_) | syn::BinOp::Or(_)) => {
return StaticCheck::RequiresZ3;
}
_ => {
return StaticCheck::RequiresZ3;
}
}
StaticCheck::RequiresZ3
}
fn is_bool_exhaustion_tautology(expr: &syn::Expr) -> bool {
let inner = match expr {
syn::Expr::Paren(p) => p.expr.as_ref(),
other => other,
};
let syn::Expr::Binary(or_bin) = inner else {
return false;
};
if !matches!(or_bin.op, syn::BinOp::Or(_)) {
return false;
}
let (syn::Expr::Binary(left_eq), syn::Expr::Binary(right_eq)) =
(or_bin.left.as_ref(), or_bin.right.as_ref())
else {
return false;
};
if !matches!(left_eq.op, syn::BinOp::Eq(_)) || !matches!(right_eq.op, syn::BinOp::Eq(_)) {
return false;
}
let left_true = is_bool_true(&left_eq.right) || is_bool_true(&left_eq.left);
let left_false = is_bool_false(&left_eq.right) || is_bool_false(&left_eq.left);
let right_true = is_bool_true(&right_eq.right) || is_bool_true(&right_eq.left);
let right_false = is_bool_false(&right_eq.right) || is_bool_false(&right_eq.left);
(left_true && right_false) || (left_false && right_true)
}
fn is_nonneg_tautology_for_return_type(expr: &syn::Expr, func: &syn::ItemFn) -> bool {
use crate::translator::z3_translator::auto_type_contracts;
let syn::Expr::Binary(bin) = expr else {
return false;
};
if !matches!(bin.op, syn::BinOp::Ge(_)) {
return false;
}
let (lhs, _rhs) = if is_zero_literal(&bin.right) {
(bin.left.as_ref(), bin.right.as_ref())
} else if is_zero_literal(&bin.left) {
(bin.right.as_ref(), bin.left.as_ref())
} else {
return false;
};
let var_name = match lhs {
syn::Expr::Path(p) => p
.path
.get_ident()
.map(|id| id.to_string())
.unwrap_or_default(),
_ => return false,
};
if var_name != "result" && !var_name.starts_with("result_") {
return false;
}
let return_ty = extract_return_type_from_sig(func);
let Some(ret) = return_ty else { return false };
let type_contracts = auto_type_contracts(&ret);
let needle = format!("{var_name} >= 0");
type_contracts.iter().any(|c| c.as_str() == needle.as_str())
}
fn is_bool_true(expr: &syn::Expr) -> bool {
if let syn::Expr::Lit(lit) = expr {
if let syn::Lit::Bool(b) = &lit.lit {
return b.value;
}
}
false
}
fn is_bool_false(expr: &syn::Expr) -> bool {
if let syn::Expr::Lit(lit) = expr {
if let syn::Lit::Bool(b) = &lit.lit {
return !b.value;
}
}
false
}
fn is_zero_literal(expr: &syn::Expr) -> bool {
if let syn::Expr::Lit(lit) = expr {
if let syn::Lit::Int(int_lit) = &lit.lit {
return int_lit.base10_digits() == "0";
}
}
false
}
fn is_literal(expr: &syn::Expr) -> bool {
matches!(expr, syn::Expr::Lit(_))
}
#[cfg(feature = "z3")]
fn verify_determinism(
func: &syn::ItemFn,
requires_contracts: &[&Contract],
timeout_secs: u64,
) -> Result<(), String> {
use crate::parser::contracts::{
Contract as LibraryContract, ContractType as LibraryContractType,
};
use crate::translator::z3_verifier::{VerificationResult, Z3Verifier};
let timeout_ms = if timeout_secs > 0 {
timeout_secs * 1000
} else {
10000
};
let mut verifier = Z3Verifier::new(timeout_ms);
let requires_library: Vec<_> = requires_contracts
.iter()
.filter_map(|c| {
c.expr.as_ref().map(|expr| LibraryContract {
contract_type: LibraryContractType::Requires,
condition: expr.clone(),
comment: None,
})
})
.collect();
match verifier.verify_determinism(func, &requires_library) {
VerificationResult::Verified { .. } => Ok(()),
VerificationResult::Failed { counterexample } => {
let msg = if let Some(ce) = counterexample {
format!("Non-deterministic. Counterexample: {:?}", ce.assignments)
} else {
"Non-deterministic (counterexample not extracted)".to_string()
};
Err(msg)
}
VerificationResult::Unknown { reason } => Err(format!("Z3 unknown: {reason}")),
VerificationResult::Error { error } => Err(format!("Z3 error: {error}")),
}
}
#[cfg(feature = "z3")]
fn verify_with_z3(
contract: &Contract,
func_sig: Option<&syn::ItemFn>,
requires_contracts: &[&Contract],
axiom_contracts: &[&Contract],
timeout_secs: u64,
callee_postconds: &[CalleePostcond],
) -> Result<bool, String> {
use crate::parser::contracts::{
Contract as LibraryContract, ContractType as LibraryContractType,
};
use crate::translator::z3_verifier::{VerificationResult, Z3Verifier};
let expr = contract
.expr
.as_ref()
.ok_or_else(|| "Cannot verify: missing parsed expression".to_string())?;
let library_contract = LibraryContract {
contract_type: match contract.contract_type {
ContractType::Requires => LibraryContractType::Requires,
ContractType::Ensures => LibraryContractType::Ensures,
ContractType::Axiom => LibraryContractType::Requires,
},
condition: expr.clone(),
comment: None,
};
let timeout_ms = if timeout_secs > 0 {
timeout_secs * 1000
} else {
5000
};
let mut verifier = Z3Verifier::new(timeout_ms);
let requires_library: Vec<_> = requires_contracts
.iter()
.chain(axiom_contracts.iter())
.filter_map(|c| {
c.expr.as_ref().map(|expr| LibraryContract {
contract_type: LibraryContractType::Requires,
condition: expr.clone(),
comment: None,
})
})
.collect();
let callee_refs: Vec<(&str, &str)> = callee_postconds
.iter()
.map(|cp| (cp.function_name.as_str(), cp.condition.as_str()))
.collect();
match verifier.verify_contract_with_context(
&library_contract,
func_sig,
&requires_library,
&callee_refs,
) {
VerificationResult::Verified { body_translated } => Ok(body_translated),
VerificationResult::Failed { counterexample } => {
let msg = if let Some(ce) = counterexample {
format!("Contract violated. Counterexample: {:?}", ce.assignments)
} else {
"Contract violated (no counterexample available)".to_string()
};
Err(msg)
}
VerificationResult::Unknown { reason } => Err(format!("Z3 verification unknown: {reason}")),
VerificationResult::Error { error } => Err(format!("Z3 verification error: {error}")),
}
}
#[cfg(not(feature = "z3"))]
fn verify_with_z3(
_contract: &Contract,
_func_sig: Option<&syn::ItemFn>,
_requires: &[&Contract],
_axioms: &[&Contract],
_timeout_secs: u64,
_callee_postconds: &[CalleePostcond],
) -> Result<bool, String> {
Err("Z3 feature not enabled. Build with --features z3 to enable Z3 verification.".to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureKind {
Counterexample,
ParseError,
SolverUnknown,
SolverError,
Tooling,
Other,
}
impl FailureKind {
pub fn as_str(self) -> &'static str {
match self {
FailureKind::Counterexample => "counterexample",
FailureKind::ParseError => "parse_error",
FailureKind::SolverUnknown => "solver_unknown",
FailureKind::SolverError => "solver_error",
FailureKind::Tooling => "tooling",
FailureKind::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartialReason {
Z3Unknown,
Z3Timeout,
UnsupportedTranslation,
MissingZ3Build,
IncompleteCoverage,
SpecDerivedCounterexample,
Other,
}
impl PartialReason {
pub fn as_str(self) -> &'static str {
match self {
PartialReason::Z3Unknown => "z3_unknown",
PartialReason::Z3Timeout => "z3_timeout",
PartialReason::UnsupportedTranslation => "unsupported_translation",
PartialReason::MissingZ3Build => "missing_z3_build",
PartialReason::IncompleteCoverage => "incomplete_coverage",
PartialReason::SpecDerivedCounterexample => "spec_derived_counterexample",
PartialReason::Other => "other",
}
}
}
fn solver_unknown_partial_reason(reason: &str) -> PartialReason {
let r = reason.to_lowercase();
if r.contains("timeout") || r.contains("timed out") || r.contains("time out") {
PartialReason::Z3Timeout
} else {
PartialReason::Z3Unknown
}
}
fn demote_if_all_spec_derived(
failed_contracts: &[(String, String, bool)],
verified_count: usize,
total: usize,
) -> Option<VerificationResult> {
if failed_contracts.is_empty() {
return None;
}
let is_translator_gap = |reason: &str| -> bool {
reason.contains("could not be parsed")
|| reason.contains("Could not translate function body")
|| reason.contains("counterexample model has no named variable assignments")
|| reason.contains("Translation error")
};
let all_gaps = failed_contracts
.iter()
.all(|(_, r, _)| is_translator_gap(r));
if std::env::var("SPEC_LOCK_DEBUG_DEMOTE").is_ok() {
for (ct, reason, sd) in failed_contracts {
eprintln!(
"DEMOTE_DEBUG: ct={ct} sd={sd} gap={} reason_snippet={}",
is_translator_gap(reason),
&reason[..reason.len().min(80)]
);
}
eprintln!("DEMOTE_DEBUG: all_gaps={all_gaps}");
}
if !all_gaps {
let all_spec_derived = failed_contracts.iter().all(|(_, _, sd)| *sd);
if !all_spec_derived {
return None;
}
return None;
}
Some(VerificationResult::Partial {
verified: verified_count,
total,
reason: Some(failed_contracts[0].1.clone()),
partial_reason: Some(PartialReason::UnsupportedTranslation),
})
}
pub(crate) fn failed_verification(
contract_type: &str,
primary_reason: &str,
failed_contracts_len: usize,
) -> VerificationResult {
let reason = format!("{primary_reason} ({failed_contracts_len} total failures)");
let kind = failure_kind(contract_type, &reason);
let partial_reason = if kind == FailureKind::SolverUnknown {
Some(solver_unknown_partial_reason(&reason))
} else {
None
};
VerificationResult::Failed {
contract: contract_type.to_string(),
reason,
kind,
partial_reason,
}
}
fn failure_kind(contract_type: &str, reason: &str) -> FailureKind {
let r = reason.to_lowercase();
let determinism_z3_unknown = r.contains("determinism")
&& (r.contains("z3 unknown") || r.contains("z3 verification unknown"));
if r.contains("counterexample")
|| r.contains("contract violated")
|| r.contains("non-deterministic")
|| ((r.contains("determinism") && !r.contains("could not translate"))
&& !determinism_z3_unknown)
{
return FailureKind::Counterexample;
}
if r.contains("could not be parsed")
|| r.contains("could not parse")
|| r.contains("cannot verify: contract condition could not be parsed")
|| reason.contains("Cannot verify: contract condition could not be parsed as expression")
{
return FailureKind::ParseError;
}
if r.contains("z3 unknown") || r.contains("z3 verification unknown") {
return FailureKind::SolverUnknown;
}
if r.contains("z3 error") || r.contains("z3 verification error") {
return FailureKind::SolverError;
}
if r.contains("z3 required but not built")
|| r.contains("build blvm-spec-lock with --features z3")
|| r.contains("z3 feature not enabled")
{
return FailureKind::Tooling;
}
if contract_type == "Z3" && r.contains("unknown") {
return FailureKind::SolverUnknown;
}
FailureKind::Other
}
#[derive(Debug, Clone)]
pub enum VerificationResult {
Passed {
body_translated: bool,
},
Failed {
contract: String,
reason: String,
kind: FailureKind,
partial_reason: Option<PartialReason>,
},
Partial {
verified: usize,
total: usize,
reason: Option<String>,
partial_reason: Option<PartialReason>,
},
NoContracts {
section: String,
},
NotImplemented,
}
#[cfg(test)]
mod spec_locked_anchor_extract_tests {
use super::{extract_constant_anchor, extract_formula_anchor, extract_section};
use syn::{ItemFn, parse_quote};
#[test]
fn dual_literal_extracts_formula_and_section() {
let f: ItemFn = parse_quote! {
#[spec_locked("9.91", "F_X")]
pub fn dummy() {}
};
assert_eq!(extract_formula_anchor(&f.attrs).as_deref(), Some("F_X"));
assert_eq!(extract_constant_anchor(&f.attrs), None);
assert_eq!(extract_section(&f.attrs).as_deref(), Some("9.91"));
}
#[test]
fn dual_literal_extracts_constant_and_section() {
let f: ItemFn = parse_quote! {
#[spec_locked("4.91", "C_SMK")]
pub fn dummy() {}
};
assert_eq!(extract_formula_anchor(&f.attrs), None);
assert_eq!(extract_constant_anchor(&f.attrs).as_deref(), Some("C_SMK"));
assert_eq!(extract_section(&f.attrs).as_deref(), Some("4.91"));
}
#[test]
fn combined_literal_section_and_formula() {
let f: ItemFn = parse_quote! {
#[spec_locked("44.44::F_C")]
pub fn dummy() {}
};
assert_eq!(extract_formula_anchor(&f.attrs).as_deref(), Some("F_C"));
assert_eq!(extract_constant_anchor(&f.attrs), None);
assert_eq!(extract_section(&f.attrs).as_deref(), Some("44.44"));
}
#[test]
fn combined_literal_section_and_constant() {
let f: ItemFn = parse_quote! {
#[spec_locked("44.44::C_K")]
pub fn dummy() {}
};
assert_eq!(extract_formula_anchor(&f.attrs), None);
assert_eq!(extract_constant_anchor(&f.attrs).as_deref(), Some("C_K"));
assert_eq!(extract_section(&f.attrs).as_deref(), Some("44.44"));
}
#[test]
fn named_function_param_extracts_formula() {
let f: ItemFn = parse_quote! {
#[spec_locked(section = "8.82", function = "F_Z")]
pub fn z() {}
};
assert_eq!(extract_formula_anchor(&f.attrs).as_deref(), Some("F_Z"));
assert_eq!(extract_constant_anchor(&f.attrs), None);
assert_eq!(extract_section(&f.attrs).as_deref(), Some("8.82"));
}
#[test]
fn named_function_param_extracts_constant() {
let f: ItemFn = parse_quote! {
#[spec_locked(section = "4.82", function = "C_Z")]
pub fn z() {}
};
assert_eq!(extract_formula_anchor(&f.attrs), None);
assert_eq!(extract_constant_anchor(&f.attrs).as_deref(), Some("C_Z"));
assert_eq!(extract_section(&f.attrs).as_deref(), Some("4.82"));
}
}
#[cfg(test)]
mod failure_kind_tests {
use super::{
FailureKind, PartialReason, VerificationResult, demote_if_all_spec_derived,
failed_verification, failure_kind,
};
#[test]
fn determinism_z3_unknown_is_solver_unknown_not_counterexample() {
let msg = "Determinism: Z3 unknown: Try --timeout 30. (1 total failures)";
assert_eq!(failure_kind("Ensures", msg), FailureKind::SolverUnknown);
match failed_verification("Ensures", "Determinism: Z3 unknown: Try --timeout 30.", 1) {
VerificationResult::Failed {
kind,
partial_reason,
..
} => {
assert_eq!(kind, FailureKind::SolverUnknown);
assert_eq!(partial_reason, Some(PartialReason::Z3Timeout));
}
_ => panic!("expected Failed"),
}
}
#[test]
fn determinism_non_deterministic_stays_counterexample() {
let msg = "Determinism: Non-deterministic. Counterexample: [] (1 total failures)";
assert_eq!(failure_kind("Ensures", msg), FailureKind::Counterexample);
}
#[test]
fn spec_derived_counterexample_stays_failed() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Contract violated. Counterexample: {}".to_string(),
true,
)];
assert!(
demote_if_all_spec_derived(&failed, 0, 1).is_none(),
"spec-derived Z3 counterexample must not be demoted to Partial"
);
}
#[test]
fn spec_derived_translation_gap_demotes_to_partial() {
let failed = vec![(
"Ensures".to_string(),
"Contract could not be parsed: unsupported LaTeX expression".to_string(),
true,
)];
let result =
demote_if_all_spec_derived(&failed, 0, 1).expect("translation gap should demote");
match result {
VerificationResult::Partial { partial_reason, .. } => {
assert_eq!(partial_reason, Some(PartialReason::UnsupportedTranslation));
}
_ => panic!("expected Partial"),
}
}
#[test]
fn manual_counterexample_stays_failed() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Contract violated.".to_string(),
false,
)];
assert!(demote_if_all_spec_derived(&failed, 0, 1).is_none());
}
#[test]
fn spec_derived_body_translation_failure_demotes_to_partial() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Z3 verification unknown: Could not translate function body to Z3 constraints; \
SAT result without body constraints is not meaningful"
.to_string(),
true,
)];
let result = demote_if_all_spec_derived(&failed, 0, 1)
.expect("body-translation gap should demote to Partial");
match result {
VerificationResult::Partial { partial_reason, .. } => {
assert_eq!(partial_reason, Some(PartialReason::UnsupportedTranslation));
}
_ => panic!("expected Partial, got {result:?}"),
}
}
#[test]
fn spec_derived_empty_assignments_demotes_to_partial() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Z3 verification unknown: Z3 found SAT but counterexample model has no named \
variable assignments (incomplete translator); result is not a concrete witness \
against the implementation"
.to_string(),
true,
)];
let result = demote_if_all_spec_derived(&failed, 0, 1)
.expect("empty-assignments gap should demote to Partial");
match result {
VerificationResult::Partial { partial_reason, .. } => {
assert_eq!(partial_reason, Some(PartialReason::UnsupportedTranslation));
}
_ => panic!("expected Partial, got {result:?}"),
}
}
#[test]
fn spec_derived_translation_type_error_demotes_to_partial() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Z3 verification error: Translation error: Type error: Expected Bool".to_string(),
true,
)];
let result = demote_if_all_spec_derived(&failed, 0, 1)
.expect("translation type error should demote to Partial");
match result {
VerificationResult::Partial { partial_reason, .. } => {
assert_eq!(partial_reason, Some(PartialReason::UnsupportedTranslation));
}
_ => panic!("expected Partial, got {result:?}"),
}
}
#[test]
fn spec_derived_counterexample_with_concrete_assignments_stays_failed() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Contract violated. Counterexample: {\"x\": \"5\", \"y\": \"-1\"} (1 total failures)"
.to_string(),
true,
)];
assert!(
demote_if_all_spec_derived(&failed, 0, 1).is_none(),
"spec-derived failure with concrete counterexample must not be demoted"
);
}
#[test]
fn manual_no_named_assignments_demotes_to_partial() {
let failed = vec![(
"Ensures".to_string(),
"Z3: Z3 verification unknown: Z3 found SAT but counterexample model has no named \
variable assignments (incomplete translator); result is not a concrete witness \
against the implementation (3 total failures)"
.to_string(),
false, )];
let result = demote_if_all_spec_derived(&failed, 0, 1)
.expect("no-named-assignments gap must demote to Partial even for manual contracts");
match result {
VerificationResult::Partial { partial_reason, .. } => {
assert_eq!(partial_reason, Some(PartialReason::UnsupportedTranslation));
}
_ => panic!("expected Partial, got {result:?}"),
}
}
}