Skip to main content

aver/checker/
mod.rs

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