Skip to main content

aver/checker/
mod.rs

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