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