Skip to main content

crisp_errors/
analyze.rs

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                // all errors from body absorbed
230            } 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}