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