Skip to main content

aver/checker/
mod.rs

1mod coverage;
2mod cse;
3mod independence;
4mod intent;
5mod law;
6mod perf;
7mod verify;
8
9use crate::ast::{
10    Expr, Literal, Pattern, SourceSpan, Spanned, TopLevel, TypeDef, VerifyBlock, VerifyKind,
11};
12
13// -- Structured verify results ------------------------------------------------
14
15#[derive(Debug, Clone)]
16pub enum VerifyCaseOutcome {
17    Pass,
18    Skipped,
19    Mismatch { expected: String, actual: String },
20    RuntimeError { error: String },
21    UnexpectedErr { err_repr: String },
22}
23
24#[derive(Debug, Clone)]
25pub struct VerifyCaseResult {
26    pub outcome: VerifyCaseOutcome,
27    pub span: Option<SourceSpan>,
28    pub case_expr: String,
29    pub case_index: usize,
30    pub case_total: usize,
31    pub law_context: Option<VerifyLawContext>,
32}
33
34#[derive(Debug, Clone)]
35pub struct VerifyLawContext {
36    pub givens: Vec<(String, String)>, // (name, value_repr)
37    pub law_expr: String,
38}
39
40pub struct VerifyResult {
41    pub fn_name: String,
42    pub block_label: String, // "add" or "sort spec isSorted"
43    pub passed: usize,
44    pub failed: usize,
45    pub skipped: usize,
46    pub case_results: Vec<VerifyCaseResult>,
47    // Legacy field — kept temporarily for existing consumers
48    pub failures: Vec<(String, String, String)>, // (expr_src, expected, actual)
49}
50
51pub struct ModuleCheckFindings {
52    pub errors: Vec<CheckFinding>,
53    pub warnings: Vec<CheckFinding>,
54}
55
56pub(crate) type FnSigSummary = (Vec<crate::types::Type>, crate::types::Type, Vec<String>);
57pub(crate) type FnSigMap = std::collections::HashMap<String, FnSigSummary>;
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct FindingSpan {
61    pub line: usize,
62    pub col: usize,
63    pub len: usize,
64    pub label: String,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CheckFinding {
69    pub line: usize,
70    pub module: Option<String>,
71    pub file: Option<String>,
72    pub fn_name: Option<String>,
73    pub message: String,
74    pub extra_spans: Vec<FindingSpan>,
75}
76
77fn module_name_for_items(items: &[TopLevel]) -> Option<String> {
78    items.iter().find_map(|item| {
79        if let TopLevel::Module(m) = item {
80            Some(m.name.clone())
81        } else {
82            None
83        }
84    })
85}
86
87fn dotted_name(expr: &Spanned<Expr>) -> Option<String> {
88    match &expr.node {
89        Expr::Ident(name) => Some(name.clone()),
90        Expr::Attr(base, field) => {
91            let mut prefix = dotted_name(base)?;
92            prefix.push('.');
93            prefix.push_str(field);
94            Some(prefix)
95        }
96        _ => None,
97    }
98}
99
100fn normalize_constructor_tag(path: &str) -> Option<String> {
101    let mut parts = path.split('.').collect::<Vec<_>>();
102    if parts.len() < 2 {
103        return None;
104    }
105    let variant = parts.pop()?;
106    let type_name = parts.pop()?;
107    Some(format!("{}.{}", type_name, variant))
108}
109
110fn constructor_tag_from_pattern(pattern: &Pattern) -> Option<String> {
111    match pattern {
112        Pattern::Constructor(path, _) => normalize_constructor_tag(path),
113        _ => None,
114    }
115}
116
117fn constructor_tag_from_expr(expr: &Spanned<Expr>) -> Option<String> {
118    match &expr.node {
119        Expr::Attr(_, _) => normalize_constructor_tag(&dotted_name(expr)?),
120        Expr::FnCall(callee, _) => normalize_constructor_tag(&dotted_name(callee)?),
121        Expr::Constructor(name, _) => normalize_constructor_tag(name),
122        _ => None,
123    }
124}
125
126fn expr_is_result_err_case(expr: &Spanned<Expr>) -> bool {
127    match &expr.node {
128        Expr::FnCall(callee, _) => dotted_name(callee)
129            .and_then(|path| normalize_constructor_tag(&path))
130            .is_some_and(|tag| tag == "Result.Err"),
131        Expr::Constructor(name, _) => {
132            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Err")
133        }
134        _ => false,
135    }
136}
137
138fn expr_is_result_ok_case(expr: &Spanned<Expr>) -> bool {
139    match &expr.node {
140        Expr::FnCall(callee, _) => dotted_name(callee)
141            .and_then(|path| normalize_constructor_tag(&path))
142            .is_some_and(|tag| tag == "Result.Ok"),
143        Expr::Constructor(name, _) => {
144            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Ok")
145        }
146        _ => false,
147    }
148}
149
150fn expr_is_option_none_case(expr: &Spanned<Expr>) -> bool {
151    match &expr.node {
152        Expr::Attr(_, _) => dotted_name(expr)
153            .and_then(|path| normalize_constructor_tag(&path))
154            .is_some_and(|tag| tag == "Option.None"),
155        Expr::Constructor(name, None) => {
156            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.None")
157        }
158        _ => false,
159    }
160}
161
162fn expr_is_option_some_case(expr: &Spanned<Expr>) -> bool {
163    match &expr.node {
164        Expr::FnCall(callee, _) => dotted_name(callee)
165            .and_then(|path| normalize_constructor_tag(&path))
166            .is_some_and(|tag| tag == "Option.Some"),
167        Expr::Constructor(name, _) => {
168            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.Some")
169        }
170        _ => false,
171    }
172}
173
174fn expr_is_bool_case(expr: &Spanned<Expr>, expected: bool) -> bool {
175    matches!(&expr.node, Expr::Literal(Literal::Bool(value)) if *value == expected)
176}
177
178fn expr_is_empty_list_case(expr: &Spanned<Expr>) -> bool {
179    matches!(&expr.node, Expr::List(items) if items.is_empty())
180}
181
182fn expr_is_non_empty_list_case(expr: &Spanned<Expr>) -> bool {
183    matches!(&expr.node, Expr::List(items) if !items.is_empty())
184}
185
186fn expr_is_empty_string_case(expr: &Spanned<Expr>) -> bool {
187    matches!(&expr.node, Expr::Literal(Literal::Str(value)) if value.is_empty())
188}
189
190fn expr_is_int_literal_case(expr: &Spanned<Expr>, expected: i64) -> bool {
191    matches!(&expr.node, Expr::Literal(Literal::Int(value)) if *value == expected)
192}
193
194fn verify_cases_block_is_well_formed(block: &VerifyBlock) -> bool {
195    matches!(block.kind, VerifyKind::Cases)
196        && !block.cases.is_empty()
197        && block.cases.iter().all(|(left, right)| {
198            verify_case_calls_target(left, &block.fn_name)
199                && !verify_case_calls_target(right, &block.fn_name)
200        })
201}
202
203fn local_sum_type_constructors(items: &[TopLevel], type_name: &str) -> Option<Vec<String>> {
204    items.iter().find_map(|item| match item {
205        TopLevel::TypeDef(TypeDef::Sum { name, variants, .. }) if name == type_name => Some(
206            variants
207                .iter()
208                .map(|variant| format!("{name}.{}", variant.name))
209                .collect(),
210        ),
211        _ => None,
212    })
213}
214
215fn callee_is_target(callee: &Spanned<Expr>, fn_name: &str) -> bool {
216    matches!(&callee.node, Expr::Ident(name) if name == fn_name)
217}
218
219// Re-export from verify submodule
220use verify::collect_target_call_args;
221use verify::verify_case_calls_target;
222
223// Public re-exports so external callers don't break
224pub use coverage::{collect_verify_coverage_warnings, collect_verify_coverage_warnings_in};
225pub use cse::{collect_cse_warnings, collect_cse_warnings_in};
226pub use independence::{collect_independence_warnings, collect_independence_warnings_in};
227pub use intent::{
228    check_module_intent, check_module_intent_with_sigs, check_module_intent_with_sigs_in,
229    index_decisions,
230};
231pub use law::{collect_verify_law_dependency_warnings, collect_verify_law_dependency_warnings_in};
232pub use perf::{collect_perf_warnings, collect_perf_warnings_in};
233pub use verify::{expr_to_str, merge_verify_blocks, run_verify};