Skip to main content

aver/checker/
mod.rs

1mod coverage;
2mod cse;
3mod independence;
4mod intent;
5#[cfg(feature = "runtime")]
6mod law;
7mod module_effects;
8mod naming;
9mod perf;
10mod traversal;
11mod verify;
12mod verify_effects;
13
14use crate::ast::{
15    Expr, Literal, Pattern, SourceSpan, Spanned, TopLevel, TypeDef, VerifyBlock, VerifyKind,
16};
17
18// -- Structured verify results ------------------------------------------------
19
20#[derive(Debug, Clone)]
21pub enum VerifyCaseOutcome {
22    Pass,
23    Skipped,
24    /// Hostile-profile case for a `case_expr` whose un-effected base
25    /// case already failed. Aver doesn't run the VM for these — the
26    /// counter-example is the base failure itself; the per-profile
27    /// follow-ups would only re-confirm the same case under harder
28    /// worlds. Distinct from `Skipped` (which is `when`-driven and
29    /// drives the vacuous-under-hostile warning).
30    SkippedAfterBaseFail,
31    Mismatch {
32        expected: String,
33        actual: String,
34    },
35    RuntimeError {
36        error: String,
37    },
38    UnexpectedErr {
39        err_repr: String,
40    },
41}
42
43#[derive(Debug, Clone)]
44pub struct VerifyCaseResult {
45    pub outcome: VerifyCaseOutcome,
46    pub span: Option<SourceSpan>,
47    pub case_expr: String,
48    pub case_index: usize,
49    pub case_total: usize,
50    pub law_context: Option<VerifyLawContext>,
51    /// `true` for cases injected by `aver verify --hostile` boundary
52    /// expansion (a binding the user did not declare). Drives differential
53    /// reporting: a hostile-only failure means the claim is not universal,
54    /// so it isn't a law — either encode the missing precondition with
55    /// `when`, or downgrade from `law` form to `verify` (cases form,
56    /// example/scenario semantics) with the values you actually meant.
57    pub from_hostile: bool,
58    /// Display label for the effect-side hostile profile, e.g.
59    /// `"Time.now/frozen + Random.int/min"`. `None` when the case wasn't
60    /// effect-hostile-expanded (declared, value-hostile-only, or fns
61    /// without applicable classified effects). Reporting prepends this to
62    /// the diagnostic so the user sees which adversarial world broke the
63    /// law: "Time.now/frozen + Random.int/min: assumed deadline > now".
64    pub hostile_profile: Option<String>,
65    /// VM-computed ground-truth value of the case's EXPECTED (right) side,
66    /// recorded on `Pass` by the VM verify runner. Proof export consumes it
67    /// to literalize the expected side of bounded Lean checks
68    /// (model-vs-ground-truth instead of model-vs-model, which is vacuously
69    /// true when fuel exhaustion collapses both sides to `default`). `None`
70    /// for non-`Pass` outcomes and for runners that don't compute values
71    /// (wasm-gc differential verify).
72    pub expected_value: Option<crate::value::Value>,
73}
74
75#[derive(Debug, Clone)]
76pub struct VerifyLawContext {
77    pub givens: Vec<(String, String)>, // (name, value_repr)
78    pub law_expr: String,
79}
80
81pub struct VerifyResult {
82    pub fn_name: String,
83    /// True for `verify ... law ...` blocks. Carried as a field so
84    /// consumers never derive law-ness from the rendered label string.
85    pub is_law: bool,
86    pub block_label: String, // "add" or "sort law isSorted"
87    pub passed: usize,
88    pub failed: usize,
89    pub skipped: usize,
90    pub case_results: Vec<VerifyCaseResult>,
91    // Legacy field — kept temporarily for existing consumers
92    pub failures: Vec<(String, String, String)>, // (expr_src, expected, actual)
93}
94
95pub struct ModuleCheckFindings {
96    pub errors: Vec<CheckFinding>,
97    pub warnings: Vec<CheckFinding>,
98}
99
100pub(crate) type FnSigSummary = (Vec<crate::types::Type>, crate::types::Type, Vec<String>);
101pub(crate) type FnSigMap = std::collections::HashMap<String, FnSigSummary>;
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct FindingSpan {
105    pub line: usize,
106    pub col: usize,
107    pub len: usize,
108    pub label: String,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct CheckFinding {
113    pub line: usize,
114    pub module: Option<String>,
115    pub file: Option<String>,
116    pub fn_name: Option<String>,
117    pub message: String,
118    pub extra_spans: Vec<FindingSpan>,
119}
120
121fn module_name_for_items(items: &[TopLevel]) -> Option<String> {
122    items.iter().find_map(|item| {
123        if let TopLevel::Module(m) = item {
124            Some(m.name.clone())
125        } else {
126            None
127        }
128    })
129}
130
131fn dotted_name(expr: &Spanned<Expr>) -> Option<String> {
132    match &expr.node {
133        Expr::Ident(name) => Some(name.clone()),
134        Expr::Attr(base, field) => {
135            let mut prefix = dotted_name(base)?;
136            prefix.push('.');
137            prefix.push_str(field);
138            Some(prefix)
139        }
140        _ => None,
141    }
142}
143
144fn normalize_constructor_tag(path: &str) -> Option<String> {
145    let mut parts = path.split('.').collect::<Vec<_>>();
146    if parts.len() < 2 {
147        return None;
148    }
149    let variant = parts.pop()?;
150    let type_name = parts.pop()?;
151    Some(crate::visibility::member_key(type_name, variant))
152}
153
154fn constructor_tag_from_pattern(pattern: &Pattern) -> Option<String> {
155    match pattern {
156        Pattern::Constructor(path, _) => normalize_constructor_tag(path),
157        _ => None,
158    }
159}
160
161fn constructor_tag_from_expr(expr: &Spanned<Expr>) -> Option<String> {
162    match &expr.node {
163        Expr::Attr(_, _) => normalize_constructor_tag(&dotted_name(expr)?),
164        Expr::FnCall(callee, _) => normalize_constructor_tag(&dotted_name(callee)?),
165        Expr::Constructor(name, _) => normalize_constructor_tag(name),
166        _ => None,
167    }
168}
169
170fn expr_is_result_err_case(expr: &Spanned<Expr>) -> bool {
171    match &expr.node {
172        Expr::FnCall(callee, _) => dotted_name(callee)
173            .and_then(|path| normalize_constructor_tag(&path))
174            .is_some_and(|tag| tag == "Result.Err"),
175        Expr::Constructor(name, _) => {
176            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Err")
177        }
178        _ => false,
179    }
180}
181
182fn expr_is_result_ok_case(expr: &Spanned<Expr>) -> bool {
183    match &expr.node {
184        Expr::FnCall(callee, _) => dotted_name(callee)
185            .and_then(|path| normalize_constructor_tag(&path))
186            .is_some_and(|tag| tag == "Result.Ok"),
187        Expr::Constructor(name, _) => {
188            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Ok")
189        }
190        _ => false,
191    }
192}
193
194fn expr_is_option_none_case(expr: &Spanned<Expr>) -> bool {
195    match &expr.node {
196        Expr::Attr(_, _) => dotted_name(expr)
197            .and_then(|path| normalize_constructor_tag(&path))
198            .is_some_and(|tag| tag == "Option.None"),
199        Expr::Constructor(name, None) => {
200            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.None")
201        }
202        _ => false,
203    }
204}
205
206fn expr_is_option_some_case(expr: &Spanned<Expr>) -> bool {
207    match &expr.node {
208        Expr::FnCall(callee, _) => dotted_name(callee)
209            .and_then(|path| normalize_constructor_tag(&path))
210            .is_some_and(|tag| tag == "Option.Some"),
211        Expr::Constructor(name, _) => {
212            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.Some")
213        }
214        _ => false,
215    }
216}
217
218fn expr_is_bool_case(expr: &Spanned<Expr>, expected: bool) -> bool {
219    matches!(&expr.node, Expr::Literal(Literal::Bool(value)) if *value == expected)
220}
221
222fn expr_is_empty_list_case(expr: &Spanned<Expr>) -> bool {
223    matches!(&expr.node, Expr::List(items) if items.is_empty())
224}
225
226fn expr_is_non_empty_list_case(expr: &Spanned<Expr>) -> bool {
227    matches!(&expr.node, Expr::List(items) if !items.is_empty())
228}
229
230fn expr_is_empty_string_case(expr: &Spanned<Expr>) -> bool {
231    matches!(&expr.node, Expr::Literal(Literal::Str(value)) if value.is_empty())
232}
233
234fn expr_is_int_literal_case(expr: &Spanned<Expr>, expected: i64) -> bool {
235    matches!(&expr.node, Expr::Literal(Literal::Int(value)) if *value == expected)
236}
237
238fn verify_cases_block_is_well_formed(block: &VerifyBlock) -> bool {
239    matches!(block.kind, VerifyKind::Cases)
240        && !block.cases.is_empty()
241        && block.cases.iter().all(|(left, right)| {
242            verify_case_calls_target(left, &block.fn_name)
243                && !verify_case_calls_target(right, &block.fn_name)
244        })
245}
246
247fn local_sum_type_constructors(items: &[TopLevel], type_name: &str) -> Option<Vec<String>> {
248    items.iter().find_map(|item| match item {
249        TopLevel::TypeDef(TypeDef::Sum { name, variants, .. }) if name == type_name => Some(
250            variants
251                .iter()
252                .map(|variant| crate::visibility::member_key(name, &variant.name))
253                .collect(),
254        ),
255        _ => None,
256    })
257}
258
259fn callee_is_target(callee: &Spanned<Expr>, fn_name: &str) -> bool {
260    matches!(&callee.node, Expr::Ident(name) if name == fn_name)
261}
262
263// Re-export from verify submodule
264use verify::collect_target_call_args;
265use verify::verify_case_calls_target;
266
267// Public re-exports so external callers don't break
268pub use coverage::{collect_verify_coverage_warnings, collect_verify_coverage_warnings_in};
269pub use cse::{collect_cse_warnings, collect_cse_warnings_in};
270pub use independence::{collect_independence_warnings, collect_independence_warnings_in};
271pub use intent::{
272    check_module_intent, check_module_intent_with_sigs, check_module_intent_with_sigs_in,
273    index_decisions,
274};
275#[cfg(feature = "runtime")]
276pub use law::{collect_verify_law_dependency_warnings, collect_verify_law_dependency_warnings_in};
277pub use module_effects::{collect_module_effects_warnings, collect_module_effects_warnings_in};
278pub use naming::{collect_naming_warnings, collect_naming_warnings_in};
279pub use perf::{collect_perf_warnings, collect_perf_warnings_in};
280pub use traversal::collect_traversal_warnings_in;
281pub use verify::{expr_to_str, merge_verify_blocks};
282pub use verify_effects::{
283    collect_plain_cases_effectful_warnings, collect_plain_cases_effectful_warnings_in,
284};