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        _ => false,
29    }
30}
31
32fn match_definitely_exits(arms: &[MatchArm]) -> bool {
33    if arms.is_empty() {
34        return false;
35    }
36    let has_unguarded_wildcard = arms.iter().any(|arm| {
37        arm.guard.is_none() && matches!(&arm.pattern.node, Node::Identifier(name) if name == "_")
38    });
39    has_unguarded_wildcard && arms.iter().all(|arm| block_definitely_exits(&arm.body))
40}
41
42/// Check whether a block definitely exits (contains a terminating statement).
43pub fn block_definitely_exits(stmts: &[SNode]) -> bool {
44    stmts.iter().any(stmt_definitely_exits)
45}