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