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 ExprKind::Lambda { body, .. } => {
298 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out)
299 }
300 _ => {}
301 }
302}
303
304fn propagate_rust_import_errors(
305 func: &Expr,
306 rust_imports: &[ResolvedRustImport],
307 out: &mut ErrorSet,
308) {
309 let ExprKind::Ident(id) = &func.kind else {
310 return;
311 };
312 for imp in rust_imports {
313 if imp.local_name == id.name && rust_import_returns_result(&imp.crate_name, &imp.item) {
314 out.insert("Thrown");
315 return;
316 }
317 }
318}
319
320fn propagate_call_errors(
321 module: &str,
322 func: &Expr,
323 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
324 callee_sigs: &BTreeMap<String, ErrorSet>,
325 out: &mut ErrorSet,
326) {
327 let Some(callee_key) = resolve_callee_key(module, func, fn_defs) else {
328 return;
329 };
330 if let Some(errors) = callee_sigs.get(&callee_key) {
331 out.extend(errors);
332 }
333}
334
335fn resolve_callee_key(
336 module: &str,
337 func: &Expr,
338 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
339) -> Option<String> {
340 match &func.kind {
341 ExprKind::Ident(id) => {
342 let local = format!("{module}::{}", id.name);
343 if fn_defs.contains_key(&local) {
344 return Some(local);
345 }
346 for (key, (m, def)) in fn_defs {
347 if def.name.name == id.name {
348 return Some(key.clone());
349 }
350 if m != module && def.is_pub && def.name.name == id.name {
351 return Some(key.clone());
352 }
353 }
354 None
355 }
356 ExprKind::Field { base, field } => {
357 if let ExprKind::Ident(id) = &base.kind {
358 let local = format!("{module}::{}::{}", id.name, field.name);
359 if fn_defs.contains_key(&local) {
360 return Some(local);
361 }
362 let suffix = format!("::{}::{}", id.name, field.name);
363 for key in fn_defs.keys() {
364 if key.ends_with(&suffix) {
365 return Some(key.clone());
366 }
367 }
368 }
369 let suffix = format!("::{}", field.name);
370 let hits: Vec<&String> = fn_defs
371 .keys()
372 .filter(|k| k.ends_with(&suffix) && k.matches("::").count() >= 2)
373 .collect();
374 if hits.len() == 1 {
375 return Some(hits[0].clone());
376 }
377 None
378 }
379 _ => None,
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use std::path::PathBuf;
387
388 fn fixture(name: &str) -> PathBuf {
389 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("tests/fixtures/{name}"))
390 }
391
392 fn examples(name: &str) -> PathBuf {
393 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../examples/{name}"))
394 }
395
396 #[test]
397 fn infer_fallible_chain() {
398 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
399 let read = result.get("main", "read_config").expect("read_config");
400 assert!(read.fallible);
401 assert!(read.errors.contains("IoError"));
402 assert!(read.errors.contains("ParseError"));
403 }
404
405 #[test]
406 fn catch_makes_main_infallible() {
407 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
408 let main = result.get("main", "main").expect("main");
409 assert!(!main.fallible);
410 }
411
412 #[test]
413 fn synthesize_crisp_error_enum() {
414 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
415 let names: Vec<_> = result
416 .crisp_error
417 .variants
418 .iter()
419 .map(|v| v.name.as_str())
420 .collect();
421 assert!(names.contains(&"IoError"));
422 assert!(names.contains(&"ParseError"));
423 }
424
425 #[test]
426 fn never_annotation_rejected() {
427 let err = ErrorPass::analyze_crate(&fixture("never_bad")).expect_err("never");
428 assert!(matches!(err, ErrorPassError::NeverViolated { .. }));
429 }
430
431 #[test]
432 fn declared_set_rejected() {
433 let err = ErrorPass::analyze_crate(&fixture("declared_bad")).expect_err("declared");
434 assert!(matches!(err, ErrorPassError::DeclaredMismatch { .. }));
435 }
436
437 #[test]
438 fn hello_has_no_errors() {
439 let result = ErrorPass::analyze_crate(&examples("hello")).expect("hello");
440 assert!(result.signatures.values().all(|s| !s.fallible));
441 }
442
443 #[test]
444 fn rust_import_marks_main_fallible() {
445 let result = ErrorPass::analyze_crate(&examples("rust_import")).expect("rust_import");
446 let main = result.get("main", "main").expect("main");
447 assert!(main.fallible, "Result APIs should mark main fallible");
448 assert!(main.errors.contains("Thrown"));
449 assert!(
450 result
451 .crisp_error
452 .variants
453 .iter()
454 .any(|v| v.name == "Thrown")
455 );
456 }
457}