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#[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)>, pub law_expr: String,
39}
40
41pub struct VerifyResult {
42 pub fn_name: String,
43 pub block_label: String, pub passed: usize,
45 pub failed: usize,
46 pub skipped: usize,
47 pub case_results: Vec<VerifyCaseResult>,
48 pub failures: Vec<(String, String, String)>, }
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(crate::visibility::member_key(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| crate::visibility::member_key(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
220use verify::collect_target_call_args;
222use verify::verify_case_calls_target;
223
224pub 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};