1use crate::error::ErrorPassError;
2use crate::result::{CrispErrorEnum, CrispErrorVariant, ErrorResult, ErrorSet, ErrorSig};
3use crate::set::{absorbs_all, catch_handled_set, declared_set_from_fn, thrown_error_name};
4use crisp_ast::expr::{Block, Expr, ExprKind, Stmt};
5use crisp_ast::item::{FunctionDef, Item};
6use crisp_resolve::ResolvedRustImport;
7use crisp_resolve::module::load_module_graph;
8use crisp_typeck::{TypeChecker, rust_import_returns_result};
9use std::collections::BTreeMap;
10use std::path::Path;
11
12pub struct ErrorPass;
13
14impl ErrorPass {
15 pub fn analyze_crate(crate_root: &Path) -> Result<ErrorResult, ErrorPassError> {
16 let typed = TypeChecker::check_crate(crate_root)?;
17 let graph = load_module_graph(crate_root)?;
18 let rust_imports = &typed.rust_imports;
19
20 let mut fn_defs: BTreeMap<String, (String, FunctionDef)> = BTreeMap::new();
21 for node in graph.modules.values() {
22 for item in &node.ast.items {
23 match item {
24 Item::Function(f) => {
25 let key = format!("{}::{}", node.module_path, f.name.name);
26 fn_defs.insert(key, (node.module_path.clone(), f.clone()));
27 }
28 Item::Impl(ib) => {
29 let ty_name = match &ib.ty.kind {
30 crisp_ast::ty::TypeKind::Named(id) => id.name.clone(),
31 _ => continue,
32 };
33 for f in &ib.items {
34 let key = format!("{}::{ty_name}::{}", node.module_path, f.name.name);
35 fn_defs.insert(key, (node.module_path.clone(), f.clone()));
36 }
37 }
38 _ => {}
39 }
40 }
41 }
42
43 let mut sigs: BTreeMap<String, ErrorSet> = BTreeMap::new();
44 for key in fn_defs.keys() {
45 sigs.insert(key.clone(), ErrorSet::new());
46 }
47
48 let max_iters = fn_defs.len().max(1) * 4 + 8;
49 for _ in 0..max_iters {
50 let mut changed = false;
51 for (key, (module, def)) in &fn_defs {
52 let local = collect_local_errors(module, def, &fn_defs, &sigs, rust_imports);
53 let prev = sigs.get(key).cloned().unwrap_or_default();
54 if prev != local {
55 changed = true;
56 sigs.insert(key.clone(), local);
57 }
58 }
59 if !changed {
60 break;
61 }
62 }
63
64 let mut signatures = BTreeMap::new();
65 let mut global = ErrorSet::new();
66
67 for (key, (module, def)) in &fn_defs {
68 let errors = sigs.get(key).cloned().unwrap_or_default();
69 let (declared, asserts_never) = declared_set_from_fn(def);
70
71 if asserts_never && !errors.is_empty() {
72 return Err(ErrorPassError::NeverViolated {
73 name: def.name.name.clone(),
74 produced: format_error_set(&errors),
75 span: def.span,
76 });
77 }
78
79 if let Some(ref decl) = declared
80 && !decl.is_empty()
81 {
82 for e in errors.iter() {
83 if !decl.contains(e) {
84 return Err(ErrorPassError::DeclaredMismatch {
85 name: def.name.name.clone(),
86 declared: format_error_set(decl),
87 produced: e.clone(),
88 span: def.error_type.as_ref().map(|t| t.span).unwrap_or(def.span),
89 });
90 }
91 }
92 }
93
94 let fallible = !errors.is_empty();
95 global.extend(&errors);
96 signatures.insert(
97 key.clone(),
98 ErrorSig {
99 module: module.clone(),
100 name: def.name.name.clone(),
101 fallible,
102 errors: errors.clone(),
103 declared,
104 asserts_never,
105 span: def.span,
106 },
107 );
108 }
109
110 Ok(ErrorResult {
111 signatures,
112 crisp_error: synthesize_enum(&global),
113 })
114 }
115}
116
117fn format_error_set(set: &ErrorSet) -> String {
118 set.iter().cloned().collect::<Vec<_>>().join(" | ")
119}
120
121fn synthesize_enum(global: &ErrorSet) -> CrispErrorEnum {
122 let mut variants: Vec<CrispErrorVariant> = global
123 .iter()
124 .map(|name| CrispErrorVariant {
125 name: name.clone(),
126 payload_type: if name == "Thrown" {
127 "String".into()
128 } else {
129 name.clone()
130 },
131 })
132 .collect();
133 variants.sort_by(|a, b| a.name.cmp(&b.name));
134 CrispErrorEnum { variants }
135}
136
137fn collect_local_errors(
138 module: &str,
139 def: &FunctionDef,
140 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
141 callee_sigs: &BTreeMap<String, ErrorSet>,
142 rust_imports: &[ResolvedRustImport],
143) -> ErrorSet {
144 let mut out = ErrorSet::new();
145 walk_expr(
146 module,
147 &def.body,
148 fn_defs,
149 callee_sigs,
150 rust_imports,
151 &mut out,
152 );
153 out
154}
155
156fn walk_block(
157 module: &str,
158 block: &Block,
159 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
160 callee_sigs: &BTreeMap<String, ErrorSet>,
161 rust_imports: &[ResolvedRustImport],
162 out: &mut ErrorSet,
163) {
164 for stmt in &block.stmts {
165 walk_stmt(module, stmt, fn_defs, callee_sigs, rust_imports, out);
166 }
167 if let Some(tail) = &block.tail {
168 walk_expr(module, tail, fn_defs, callee_sigs, rust_imports, out);
169 }
170}
171
172fn walk_stmt(
173 module: &str,
174 stmt: &Stmt,
175 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
176 callee_sigs: &BTreeMap<String, ErrorSet>,
177 rust_imports: &[ResolvedRustImport],
178 out: &mut ErrorSet,
179) {
180 match stmt {
181 Stmt::Expr(e) => walk_expr(module, e, fn_defs, callee_sigs, rust_imports, out),
182 Stmt::Bind { value, .. } | Stmt::Assign { value, .. } => {
183 walk_expr(module, value, fn_defs, callee_sigs, rust_imports, out);
184 }
185 }
186}
187
188fn walk_expr(
189 module: &str,
190 expr: &Expr,
191 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
192 callee_sigs: &BTreeMap<String, ErrorSet>,
193 rust_imports: &[ResolvedRustImport],
194 out: &mut ErrorSet,
195) {
196 match &expr.kind {
197 ExprKind::Block(b) => walk_block(module, b, fn_defs, callee_sigs, rust_imports, out),
198 ExprKind::If {
199 cond,
200 then_branch,
201 else_branch,
202 } => {
203 walk_expr(module, cond, fn_defs, callee_sigs, rust_imports, out);
204 walk_expr(module, then_branch, fn_defs, callee_sigs, rust_imports, out);
205 if let Some(e) = else_branch {
206 walk_expr(module, e, fn_defs, callee_sigs, rust_imports, out);
207 }
208 }
209 ExprKind::Throw(inner) => {
210 if let Some(name) = thrown_error_name(inner) {
211 out.insert(name);
212 }
213 }
214 ExprKind::Try(inner) => {
215 walk_expr(module, inner, fn_defs, callee_sigs, rust_imports, out);
216 propagate_call_errors(module, inner, fn_defs, callee_sigs, out);
217 propagate_rust_import_errors(inner, rust_imports, out);
218 }
219 ExprKind::Catch { body, arms } => {
220 let mut inner = ErrorSet::new();
221 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, &mut inner);
222 let handled = catch_handled_set(arms);
223 if absorbs_all(&handled) {
224 } else {
226 let remaining = ErrorSet::subtract(&inner, &handled);
227 out.extend(&remaining);
228 }
229 for arm in arms {
230 walk_expr(module, &arm.body, fn_defs, callee_sigs, rust_imports, out);
231 }
232 }
233 ExprKind::Call { func, args } => {
234 walk_expr(module, func, fn_defs, callee_sigs, rust_imports, out);
235 for arg in args {
236 walk_expr(module, arg, fn_defs, callee_sigs, rust_imports, out);
237 }
238 propagate_call_errors(module, func, fn_defs, callee_sigs, out);
239 propagate_rust_import_errors(func, rust_imports, out);
240 }
241 ExprKind::MethodCall { receiver, args, .. } => {
242 walk_expr(module, receiver, fn_defs, callee_sigs, rust_imports, out);
243 for arg in args {
244 walk_expr(module, arg, fn_defs, callee_sigs, rust_imports, out);
245 }
246 }
247 ExprKind::Bind { value, .. } => {
248 walk_expr(module, value, fn_defs, callee_sigs, rust_imports, out)
249 }
250 ExprKind::Assign { value, .. } => {
251 walk_expr(module, value, fn_defs, callee_sigs, rust_imports, out)
252 }
253 ExprKind::Return(Some(v)) => walk_expr(module, v, fn_defs, callee_sigs, rust_imports, out),
254 ExprKind::Binary { left, right, .. } => {
255 walk_expr(module, left, fn_defs, callee_sigs, rust_imports, out);
256 walk_expr(module, right, fn_defs, callee_sigs, rust_imports, out);
257 }
258 ExprKind::Unary { expr: inner, .. } => {
259 walk_expr(module, inner, fn_defs, callee_sigs, rust_imports, out)
260 }
261 ExprKind::Field { base, .. } => {
262 walk_expr(module, base, fn_defs, callee_sigs, rust_imports, out)
263 }
264 ExprKind::Pipe { left, right } => {
265 walk_expr(module, left, fn_defs, callee_sigs, rust_imports, out);
266 walk_expr(module, right, fn_defs, callee_sigs, rust_imports, out);
267 }
268 ExprKind::StructLit { fields, .. } => {
269 for f in fields {
270 walk_expr(module, &f.value, fn_defs, callee_sigs, rust_imports, out);
271 }
272 }
273 ExprKind::Str(parts) => {
274 for part in &parts.0 {
275 if let crisp_ast::expr::StringPart::Expr(e) = part {
276 walk_expr(module, e, fn_defs, callee_sigs, rust_imports, out);
277 }
278 }
279 }
280 ExprKind::While { cond, body } => {
281 walk_expr(module, cond, fn_defs, callee_sigs, rust_imports, out);
282 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out);
283 }
284 ExprKind::For { iter, body, .. } => {
285 walk_expr(module, iter, fn_defs, callee_sigs, rust_imports, out);
286 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out);
287 }
288 ExprKind::Loop(body)
289 | ExprKind::Async(body)
290 | ExprKind::Await(body)
291 | ExprKind::Spawn(body)
292 | ExprKind::Unsafe(body) => {
293 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out)
294 }
295 ExprKind::Break(Some(v)) => walk_expr(module, v, fn_defs, callee_sigs, rust_imports, out),
296 ExprKind::Break(None) | ExprKind::Continue => {}
297 _ => {}
298 }
299}
300
301fn propagate_rust_import_errors(
302 func: &Expr,
303 rust_imports: &[ResolvedRustImport],
304 out: &mut ErrorSet,
305) {
306 let ExprKind::Ident(id) = &func.kind else {
307 return;
308 };
309 for imp in rust_imports {
310 if imp.local_name == id.name && rust_import_returns_result(&imp.crate_name, &imp.item) {
311 out.insert("Thrown");
312 return;
313 }
314 }
315}
316
317fn propagate_call_errors(
318 module: &str,
319 func: &Expr,
320 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
321 callee_sigs: &BTreeMap<String, ErrorSet>,
322 out: &mut ErrorSet,
323) {
324 let Some(callee_key) = resolve_callee_key(module, func, fn_defs) else {
325 return;
326 };
327 if let Some(errors) = callee_sigs.get(&callee_key) {
328 out.extend(errors);
329 }
330}
331
332fn resolve_callee_key(
333 module: &str,
334 func: &Expr,
335 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
336) -> Option<String> {
337 match &func.kind {
338 ExprKind::Ident(id) => {
339 let local = format!("{module}::{}", id.name);
340 if fn_defs.contains_key(&local) {
341 return Some(local);
342 }
343 for (key, (m, def)) in fn_defs {
344 if def.name.name == id.name {
345 return Some(key.clone());
346 }
347 if m != module && def.is_pub && def.name.name == id.name {
348 return Some(key.clone());
349 }
350 }
351 None
352 }
353 ExprKind::Field { base, field } => {
354 if let ExprKind::Ident(id) = &base.kind {
355 let local = format!("{module}::{}::{}", id.name, field.name);
356 if fn_defs.contains_key(&local) {
357 return Some(local);
358 }
359 let suffix = format!("::{}::{}", id.name, field.name);
360 for key in fn_defs.keys() {
361 if key.ends_with(&suffix) {
362 return Some(key.clone());
363 }
364 }
365 }
366 let suffix = format!("::{}", field.name);
367 let hits: Vec<&String> = fn_defs
368 .keys()
369 .filter(|k| k.ends_with(&suffix) && k.matches("::").count() >= 2)
370 .collect();
371 if hits.len() == 1 {
372 return Some(hits[0].clone());
373 }
374 None
375 }
376 _ => None,
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use std::path::PathBuf;
384
385 fn fixture(name: &str) -> PathBuf {
386 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("tests/fixtures/{name}"))
387 }
388
389 fn examples(name: &str) -> PathBuf {
390 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../examples/{name}"))
391 }
392
393 #[test]
394 fn infer_fallible_chain() {
395 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
396 let read = result.get("main", "read_config").expect("read_config");
397 assert!(read.fallible);
398 assert!(read.errors.contains("IoError"));
399 assert!(read.errors.contains("ParseError"));
400 }
401
402 #[test]
403 fn catch_makes_main_infallible() {
404 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
405 let main = result.get("main", "main").expect("main");
406 assert!(!main.fallible);
407 }
408
409 #[test]
410 fn synthesize_crisp_error_enum() {
411 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
412 let names: Vec<_> = result
413 .crisp_error
414 .variants
415 .iter()
416 .map(|v| v.name.as_str())
417 .collect();
418 assert!(names.contains(&"IoError"));
419 assert!(names.contains(&"ParseError"));
420 }
421
422 #[test]
423 fn never_annotation_rejected() {
424 let err = ErrorPass::analyze_crate(&fixture("never_bad")).expect_err("never");
425 assert!(matches!(err, ErrorPassError::NeverViolated { .. }));
426 }
427
428 #[test]
429 fn declared_set_rejected() {
430 let err = ErrorPass::analyze_crate(&fixture("declared_bad")).expect_err("declared");
431 assert!(matches!(err, ErrorPassError::DeclaredMismatch { .. }));
432 }
433
434 #[test]
435 fn hello_has_no_errors() {
436 let result = ErrorPass::analyze_crate(&examples("hello")).expect("hello");
437 assert!(result.signatures.values().all(|s| !s.fallible));
438 }
439
440 #[test]
441 fn rust_import_marks_main_fallible() {
442 let result = ErrorPass::analyze_crate(&examples("rust_import")).expect("rust_import");
443 let main = result.get("main", "main").expect("main");
444 assert!(main.fallible, "Result APIs should mark main fallible");
445 assert!(main.errors.contains("Thrown"));
446 assert!(
447 result
448 .crisp_error
449 .variants
450 .iter()
451 .any(|v| v.name == "Thrown")
452 );
453 }
454}