use std::ops::Range;
use assura_parser::ast::{ClauseKind, Expr, SpExpr};
use crate::TypeError;
fn extract_fn_effects(f: &assura_parser::ast::FnDef) -> Option<Vec<String>> {
for clause in &f.clauses {
if clause.kind == ClauseKind::Effects {
let mut names = Vec::new();
extract_effect_names(&clause.body, &mut names);
return Some(names);
}
}
None
}
fn extract_effect_names(expr: &SpExpr, names: &mut Vec<String>) {
match &expr.node {
Expr::Ident(s) => names.push(s.clone()),
Expr::Raw(tokens) => {
for tok in tokens {
let trimmed = tok.trim().to_string();
if !trimmed.is_empty() && trimmed != "," {
names.push(trimmed);
}
}
}
Expr::Block(items) => {
for item in items {
extract_effect_names(item, names);
}
}
_ => {}
}
}
pub(crate) fn check_lemma_fn_effects(
f: &assura_parser::ast::FnDef,
span: &Range<usize>,
errors: &mut Vec<TypeError>,
) {
if let Some(effects) = extract_fn_effects(f) {
let has_non_pure = effects.iter().any(|e| e != "pure");
if has_non_pure {
let effect_list = effects
.iter()
.filter(|e| *e != "pure")
.cloned()
.collect::<Vec<_>>()
.join(", ");
errors.push(TypeError {
code: "A55001".into(),
message: format!(
"lemma function `{}` has non-pure effects: {effect_list}; \
lemma functions must be pure (no side effects)",
f.name,
),
span: span.clone(),
secondary: None,
suggestion: None,
});
}
}
}
pub(crate) fn check_ghost_fn_effects(
f: &assura_parser::ast::FnDef,
span: &Range<usize>,
errors: &mut Vec<TypeError>,
) {
if let Some(effects) = extract_fn_effects(f) {
let has_non_pure = effects.iter().any(|e| e != "pure");
if has_non_pure {
let effect_list = effects
.iter()
.filter(|e| *e != "pure")
.cloned()
.collect::<Vec<_>>()
.join(", ");
errors.push(TypeError {
code: "A54001".into(),
message: format!(
"ghost function `{}` has non-pure effects: {effect_list}; \
ghost functions must be pure (no side effects)",
f.name,
),
span: span.clone(),
secondary: None,
suggestion: None,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assura_parser::ast::{Clause, FnDef, Spanned};
fn make_fn(name: &str, is_ghost: bool, is_lemma: bool, clauses: Vec<Clause>) -> FnDef {
FnDef {
name: name.into(),
is_ghost,
is_lemma,
params: vec![],
return_ty: None,
clauses,
}
}
fn effects_clause_idents(names: &[&str]) -> Clause {
let items: Vec<SpExpr> = names
.iter()
.map(|n| Spanned::no_span(Expr::Ident((*n).into())))
.collect();
Clause {
kind: ClauseKind::Effects,
body: Spanned::no_span(Expr::Block(items)),
effect_variables: vec![],
}
}
fn effects_clause_raw(tokens: &[&str]) -> Clause {
Clause {
kind: ClauseKind::Effects,
body: Spanned::no_span(Expr::Raw(tokens.iter().map(|t| (*t).into()).collect())),
effect_variables: vec![],
}
}
#[test]
fn ghost_fn_no_effects_clause_ok() {
let f = make_fn("my_ghost", true, false, vec![]);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert!(
errors.is_empty(),
"ghost fn with no effects clause should be OK"
);
}
#[test]
fn ghost_fn_pure_effects_ok() {
let f = make_fn(
"my_ghost",
true,
false,
vec![effects_clause_idents(&["pure"])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert!(errors.is_empty(), "ghost fn with pure effects should be OK");
}
#[test]
fn ghost_fn_io_effects_a54001() {
let f = make_fn(
"my_ghost",
true,
false,
vec![effects_clause_idents(&["io"])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, "A54001");
assert!(errors[0].message.contains("my_ghost"));
assert!(errors[0].message.contains("io"));
}
#[test]
fn ghost_fn_multiple_non_pure_effects() {
let f = make_fn(
"spec_fn",
true,
false,
vec![effects_clause_idents(&["io", "database"])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert_eq!(errors.len(), 1);
assert!(errors[0].message.contains("io"));
assert!(errors[0].message.contains("database"));
}
#[test]
fn ghost_fn_mixed_pure_and_non_pure() {
let f = make_fn(
"spec_fn",
true,
false,
vec![effects_clause_idents(&["pure", "net"])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert_eq!(errors.len(), 1);
assert!(errors[0].message.contains("net"));
let effect_part = errors[0]
.message
.split("non-pure effects: ")
.nth(1)
.unwrap();
let effect_list = effect_part.split(';').next().unwrap();
assert!(
!effect_list.contains("pure"),
"effect list should not include 'pure': {effect_list}"
);
}
#[test]
fn ghost_fn_raw_token_effects() {
let f = make_fn(
"ghost_raw",
true,
false,
vec![effects_clause_raw(&["fs", ",", "logging"])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..10), &mut errors);
assert_eq!(errors.len(), 1);
assert!(errors[0].message.contains("fs"));
assert!(errors[0].message.contains("logging"));
}
#[test]
fn lemma_fn_no_effects_clause_ok() {
let f = make_fn("my_lemma", false, true, vec![]);
let mut errors = Vec::new();
check_lemma_fn_effects(&f, &(0..10), &mut errors);
assert!(
errors.is_empty(),
"lemma fn with no effects clause should be OK"
);
}
#[test]
fn lemma_fn_pure_effects_ok() {
let f = make_fn(
"my_lemma",
false,
true,
vec![effects_clause_idents(&["pure"])],
);
let mut errors = Vec::new();
check_lemma_fn_effects(&f, &(0..10), &mut errors);
assert!(errors.is_empty(), "lemma fn with pure effects should be OK");
}
#[test]
fn lemma_fn_database_effect_a55001() {
let f = make_fn(
"my_lemma",
false,
true,
vec![effects_clause_idents(&["database"])],
);
let mut errors = Vec::new();
check_lemma_fn_effects(&f, &(0..10), &mut errors);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, "A55001");
assert!(errors[0].message.contains("my_lemma"));
assert!(errors[0].message.contains("database"));
}
#[test]
fn lemma_fn_span_propagated() {
let f = make_fn(
"my_lemma",
false,
true,
vec![effects_clause_idents(&["io"])],
);
let mut errors = Vec::new();
check_lemma_fn_effects(&f, &(42..99), &mut errors);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].span, 42..99);
}
#[test]
fn raw_tokens_whitespace_and_commas_filtered() {
let f = make_fn(
"g",
true,
false,
vec![effects_clause_raw(&[" ", ",", "io", " ", ",", ""])],
);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..1), &mut errors);
assert_eq!(errors.len(), 1);
assert!(errors[0].message.contains("io"));
}
#[test]
fn non_effects_clause_ignored() {
let requires = Clause {
kind: ClauseKind::Requires,
body: Spanned::no_span(Expr::Ident("io".into())),
effect_variables: vec![],
};
let f = make_fn("g", true, false, vec![requires]);
let mut errors = Vec::new();
check_ghost_fn_effects(&f, &(0..1), &mut errors);
assert!(errors.is_empty(), "non-effects clauses should be ignored");
}
}