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