Skip to main content

harn_parser/typechecker/
exits.rs

1//! Free helpers for "does this statement / block definitely exit?" analysis.
2//!
3//! Used both by `harn-lint` (publicly) and the type checker's flow narrowing
4//! logic (via the same-named methods on `TypeChecker`, which delegate here).
5
6use crate::ast::*;
7
8/// Check whether a single statement definitely exits (return/throw/break/continue
9/// or an if/else / match where every reachable branch exits).
10pub fn stmt_definitely_exits(stmt: &SNode) -> bool {
11    match &stmt.node {
12        Node::ReturnStmt { .. } | Node::ThrowStmt { .. } | Node::BreakStmt | Node::ContinueStmt => {
13            true
14        }
15        Node::IfElse {
16            then_body,
17            else_body: Some(else_body),
18            ..
19        } => block_definitely_exits(then_body) && block_definitely_exits(else_body),
20        // A `match` definitely exits when every arm exits AND at least
21        // one arm is an unguarded wildcard (`_ -> { ... }`) — that
22        // guarantees the match is exhaustive at the source level
23        // without re-deriving the value's type here. The lint and flow
24        // narrowing layers both rely on this to flag code after a
25        // returning `match` as unreachable, and to let the type
26        // checker treat the tail as `never`.
27        Node::MatchExpr { arms, .. } => match_definitely_exits(arms),
28        Node::Block(body)
29        | Node::TryExpr { body }
30        | Node::CostRoute { body, .. }
31        | Node::MutexBlock { body }
32        | Node::DeadlineBlock { body, .. }
33        | Node::Retry { body, .. } => block_definitely_exits(body),
34        Node::TryCatch {
35            body,
36            catch_body,
37            finally_body,
38            ..
39        } => {
40            finally_body
41                .as_ref()
42                .is_some_and(|body| block_definitely_exits(body))
43                || (block_definitely_exits(body) && block_definitely_exits(catch_body))
44        }
45        _ => false,
46    }
47}
48
49fn match_definitely_exits(arms: &[MatchArm]) -> bool {
50    if arms.is_empty() {
51        return false;
52    }
53    let has_unguarded_wildcard = arms.iter().any(|arm| {
54        arm.guard.is_none() && matches!(&arm.pattern.node, Node::Identifier(name) if name == "_")
55    });
56    has_unguarded_wildcard && arms.iter().all(|arm| block_definitely_exits(&arm.body))
57}
58
59/// Check whether a block definitely exits (contains a terminating statement).
60pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
61    stmts.iter().any(stmt_definitely_exits)
62}