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