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 }
223 ExprKind::Catch { body, arms } => {
224 let mut inner = ErrorSet::new();
225 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, &mut inner);
226 let handled = catch_handled_set(arms);
227 if absorbs_all(&handled) {
228 } else {
230 let remaining = ErrorSet::subtract(&inner, &handled);
231 out.extend(&remaining);
232 }
233 for arm in arms {
234 walk_expr(module, &arm.body, fn_defs, callee_sigs, rust_imports, out);
235 }
236 }
237 ExprKind::Call { func, args } => {
238 walk_expr(module, func, fn_defs, callee_sigs, rust_imports, out);
239 for arg in args {
240 walk_expr(module, arg, fn_defs, callee_sigs, rust_imports, out);
241 }
242 propagate_call_errors(module, func, fn_defs, callee_sigs, out);
243 propagate_rust_import_errors(func, rust_imports, out);
244 }
245 ExprKind::MethodCall { receiver, args, .. } => {
246 walk_expr(module, receiver, fn_defs, callee_sigs, rust_imports, out);
247 for arg in args {
248 walk_expr(module, arg, fn_defs, callee_sigs, rust_imports, out);
249 }
250 }
251 ExprKind::Bind { value, .. } => {
252 walk_expr(module, value, fn_defs, callee_sigs, rust_imports, out)
253 }
254 ExprKind::Assign { value, .. } => {
255 walk_expr(module, value, fn_defs, callee_sigs, rust_imports, out)
256 }
257 ExprKind::Return(Some(v)) => walk_expr(module, v, fn_defs, callee_sigs, rust_imports, out),
258 ExprKind::Binary { left, right, .. } => {
259 walk_expr(module, left, fn_defs, callee_sigs, rust_imports, out);
260 walk_expr(module, right, fn_defs, callee_sigs, rust_imports, out);
261 }
262 ExprKind::Unary { expr: inner, .. } => {
263 walk_expr(module, inner, fn_defs, callee_sigs, rust_imports, out)
264 }
265 ExprKind::Cast { expr: inner, .. } => {
266 walk_expr(module, inner, fn_defs, callee_sigs, rust_imports, out)
267 }
268 ExprKind::Field { base, .. } => {
269 walk_expr(module, base, fn_defs, callee_sigs, rust_imports, out)
270 }
271 ExprKind::Pipe { left, right } => {
272 walk_expr(module, left, fn_defs, callee_sigs, rust_imports, out);
273 walk_expr(module, right, fn_defs, callee_sigs, rust_imports, out);
274 }
275 ExprKind::StructLit { fields, .. } => {
276 for f in fields {
277 walk_expr(module, &f.value, fn_defs, callee_sigs, rust_imports, out);
278 }
279 }
280 ExprKind::Str(parts) => {
281 for part in &parts.0 {
282 if let crisp_ast::expr::StringPart::Expr(e) = part {
283 walk_expr(module, e, fn_defs, callee_sigs, rust_imports, out);
284 }
285 }
286 }
287 ExprKind::While { cond, body } => {
288 walk_expr(module, cond, fn_defs, callee_sigs, rust_imports, out);
289 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out);
290 }
291 ExprKind::For { iter, body, .. } => {
292 walk_expr(module, iter, fn_defs, callee_sigs, rust_imports, out);
293 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out);
294 }
295 ExprKind::Loop(body)
296 | ExprKind::Async(body)
297 | ExprKind::Await(body)
298 | ExprKind::Spawn(body)
299 | ExprKind::Unsafe(body) => {
300 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out)
301 }
302 ExprKind::Break(Some(v)) => walk_expr(module, v, fn_defs, callee_sigs, rust_imports, out),
303 ExprKind::Break(None) | ExprKind::Continue => {}
304 ExprKind::Lambda { body, .. } => {
305 walk_expr(module, body, fn_defs, callee_sigs, rust_imports, out)
306 }
307 _ => {}
308 }
309}
310
311fn propagate_rust_import_errors(func: &Expr, rust_imports: &HashSet<String>, out: &mut ErrorSet) {
312 let ExprKind::Ident(id) = &func.kind else {
313 return;
314 };
315 for name in rust_imports {
316 if name == &id.name {
317 out.insert("Thrown");
318 return;
319 }
320 }
321}
322
323fn propagate_call_errors(
324 module: &str,
325 func: &Expr,
326 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
327 callee_sigs: &BTreeMap<String, ErrorSet>,
328 out: &mut ErrorSet,
329) {
330 let Some(callee_key) = resolve_callee_key(module, func, fn_defs) else {
331 return;
332 };
333 if let Some(errors) = callee_sigs.get(&callee_key) {
334 out.extend(errors);
335 }
336}
337
338fn resolve_callee_key(
339 module: &str,
340 func: &Expr,
341 fn_defs: &BTreeMap<String, (String, FunctionDef)>,
342) -> Option<String> {
343 match &func.kind {
344 ExprKind::Ident(id) => {
345 let local = format!("{module}::{}", id.name);
346 if fn_defs.contains_key(&local) {
347 return Some(local);
348 }
349 for (key, (m, def)) in fn_defs {
350 if def.name.name == id.name {
351 return Some(key.clone());
352 }
353 if m != module && def.is_pub && def.name.name == id.name {
354 return Some(key.clone());
355 }
356 }
357 None
358 }
359 ExprKind::Field { base, field } => {
360 if let ExprKind::Ident(id) = &base.kind {
361 let local = format!("{module}::{}::{}", id.name, field.name);
362 if fn_defs.contains_key(&local) {
363 return Some(local);
364 }
365 let suffix = format!("::{}::{}", id.name, field.name);
366 for key in fn_defs.keys() {
367 if key.ends_with(&suffix) {
368 return Some(key.clone());
369 }
370 }
371 }
372 let suffix = format!("::{}", field.name);
373 let hits: Vec<&String> = fn_defs
374 .keys()
375 .filter(|k| k.ends_with(&suffix) && k.matches("::").count() >= 2)
376 .collect();
377 if hits.len() == 1 {
378 return Some(hits[0].clone());
379 }
380 None
381 }
382 _ => None,
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use std::path::PathBuf;
390
391 fn fixture(name: &str) -> PathBuf {
392 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("tests/fixtures/{name}"))
393 }
394
395 fn examples(name: &str) -> PathBuf {
396 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../examples/{name}"))
397 }
398
399 #[test]
400 fn infer_fallible_chain() {
401 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
402 let read = result.get("main", "read_config").expect("read_config");
403 assert!(read.fallible);
404 assert!(read.errors.contains("IoError"));
405 assert!(read.errors.contains("ParseError"));
406 }
407
408 #[test]
409 fn catch_makes_main_infallible() {
410 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
411 let main = result.get("main", "main").expect("main");
412 assert!(!main.fallible);
413 }
414
415 #[test]
416 fn synthesize_crisp_error_enum() {
417 let result = ErrorPass::analyze_crate(&fixture("fallible")).expect("fallible");
418 let names: Vec<_> = result
419 .crisp_error
420 .variants
421 .iter()
422 .map(|v| v.name.as_str())
423 .collect();
424 assert!(names.contains(&"IoError"));
425 assert!(names.contains(&"ParseError"));
426 }
427
428 #[test]
429 fn never_annotation_rejected() {
430 let err = ErrorPass::analyze_crate(&fixture("never_bad")).expect_err("never");
431 assert!(matches!(err, ErrorPassError::NeverViolated { .. }));
432 }
433
434 #[test]
435 fn declared_set_rejected() {
436 let err = ErrorPass::analyze_crate(&fixture("declared_bad")).expect_err("declared");
437 assert!(matches!(err, ErrorPassError::DeclaredMismatch { .. }));
438 }
439
440 #[test]
441 fn hello_has_no_errors() {
442 let result = ErrorPass::analyze_crate(&examples("hello")).expect("hello");
443 assert!(result.signatures.values().all(|s| !s.fallible));
444 }
445
446 #[test]
447 fn rust_import_marks_main_fallible() {
448 let result = ErrorPass::analyze_crate(&examples("rust_import")).expect("rust_import");
449 let main = result.get("main", "main").expect("main");
450 assert!(main.fallible, "Result APIs should mark main fallible");
451 assert!(main.errors.contains("Thrown"));
452 assert!(
453 result
454 .crisp_error
455 .variants
456 .iter()
457 .any(|v| v.name == "Thrown")
458 );
459 }
460}