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        // `while true { ... }` with no `break` binding to it never proceeds
29        // past the loop: control either stays inside forever or leaves the
30        // function via `return`/`throw` from the body. Treat it as diverging
31        // so a function whose tail is such a loop needs no unreachable
32        // trailing `return`, and code after it is flagged unreachable. Any
33        // `break` at this loop's level restores fall-through (breaks inside
34        // nested loops bind the inner loop and do not count).
35        Node::WhileLoop { condition, body } => {
36            matches!(condition.node, Node::BoolLiteral(true)) && !body_breaks_enclosing_loop(body)
37        }
38        Node::Block(body)
39        | Node::TryExpr { body }
40        | Node::CostRoute { body, .. }
41        | Node::MutexBlock { body, .. }
42        | Node::DeadlineBlock { body, .. }
43        | Node::Retry { body, .. } => block_definitely_exits(body),
44        Node::TryCatch {
45            body,
46            catch_body,
47            finally_body,
48            ..
49        } => {
50            finally_body
51                .as_ref()
52                .is_some_and(|body| block_definitely_exits(body))
53                || (block_definitely_exits(body) && block_definitely_exits(catch_body))
54        }
55        _ => false,
56    }
57}
58
59fn match_definitely_exits(arms: &[MatchArm]) -> bool {
60    if arms.is_empty() {
61        return false;
62    }
63    let has_unguarded_wildcard = arms.iter().any(|arm| {
64        arm.guard.is_none() && matches!(&arm.pattern.node, Node::Identifier(name) if name == "_")
65    });
66    has_unguarded_wildcard && arms.iter().all(|arm| block_definitely_exits(&arm.body))
67}
68
69/// Check whether a block definitely exits (contains a terminating statement).
70pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
71    stmts.iter().any(stmt_definitely_exits)
72}
73
74/// Whether `body` contains a `break` that binds to the enclosing loop — i.e.
75/// a `break` not nested inside a further `while`/`for` loop. Every other
76/// construct (if/match/try/blocks/closures embedded in expressions, ...) is
77/// scanned through via the shared [`crate::visit`] child collector, so the
78/// scan is deliberately conservative: an unexpected `break` can only make the
79/// caller treat the loop as breakable (demanding an explicit trailing
80/// `return`), never the unsound reverse.
81fn body_breaks_enclosing_loop(body: &[SNode]) -> bool {
82    body.iter().any(stmt_breaks_enclosing_loop)
83}
84
85fn stmt_breaks_enclosing_loop(stmt: &SNode) -> bool {
86    match &stmt.node {
87        Node::BreakStmt => true,
88        // Nested loops capture `break`s in their own bodies.
89        Node::WhileLoop { .. } | Node::ForIn { .. } => false,
90        _ => crate::visit::immediate_children(stmt)
91            .into_iter()
92            .any(stmt_breaks_enclosing_loop),
93    }
94}