Skip to main content

aver/checker/
mod.rs

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