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}