pick exhaustiveness for special values
Nitpick's pick expression is exhaustively checked: the compiler
proves at compile time that every possible inhabitant of the
selector is covered by some arm or by the wildcard (*). The base
rule (integer-range exhaustiveness) was introduced at v0.21.3 in
work item A-013; Decision D-23 (Phase 3, landed in v0.31.2.10)
extended the same machinery to all four special values.
| Selector type | Special arm required | Diagnostic hint |
|---|---|---|
Optional<T> (T?) |
(NIL) or wildcard (*) |
(Optional<T> requires NIL handling or wildcard *) |
Pointer<T> (T->) |
(NULL) or wildcard (*) |
(Pointer<T> requires NULL handling or wildcard *) |
tbb<N> |
(ERR) or wildcard (*) |
(legacy INTEGER_RANGE.requiresERR() path) |
unknown-tainted binding |
(unknown) or wildcard (*) |
(unknown-tainted selector requires `unknown` arm or wildcard *) |
The diagnostic is the long-standing "Non-exhaustive pick expression" error; the hint is appended after the "Missing cases: …" list.
How it works
The exhaustiveness checker models each selector type as a
TypeDomain. For Phase 3, two new factories were added:
TypeDomain::forOptional()—kind = INFINITE,hasNIL = true.TypeDomain::forPointer()—kind = INFINITE,hasNULL = true.
The dispatch in getDomain() routes TypeKind::OPTIONAL →
forOptional() and TypeKind::POINTER → forPointer() before
the ENUM check.
For each arm, analyzePattern recognises the special literal
before falling through to the generic string-variant dispatch
(NIL and NULL both parse as monostate-valued literals tagged
with explicit_type = "NIL" / "NULL"). It then calls
coverage.addNIL() / addNULL_() / addUnknown() parallel to the
existing addERR() for tbb.
The taint side is threaded in at the call site:
TypeChecker::checkPickStmt computes
bool selectorTainted = exprCarriesUnknownTaint(stmt->selector.get());
and passes it as the third arg to
ExhaustivenessAnalyzer::analyze(), which folds it into the
TypeDomain via setRequiresUnknown(true).
Examples
Optional selector
func:main = int32() {
int32?:opt = 42i32;
// OK — explicit NIL arm
pick opt {
(NIL) { println(`absent`); },
(42i32) { println(`forty-two`); },
(*) { println(`other present`); },
};
// Rejected — missing NIL
pick opt {
(42i32) { println(`forty-two`); },
(*) { println(`other`); }, // covers everything except NIL
};
pass 0i32;
};
error: Non-exhaustive pick expression
Missing cases: NIL
(Optional<T> requires NIL handling or wildcard *)
Pointer selector
func:main = int32() {
int32->:p = NULL;
pick p {
(NULL) { println(`null`); },
(*) { println(`non-null`); },
};
pass 0i32;
};
A wildcard (*) alone covers NULL too — both diagnostics fire
only when the wildcard is absent.
tbb selector
func:main = int32() {
tbb32:t = 5t32;
pick t {
(ERR) { println(`error`); },
(0t32) { println(`zero`); },
(*) { println(`other`); },
};
pass 0i32;
};
Unknown-tainted selector
func:main = int32() {
int32:u = unknown; // taints u.mayBeUnknown
pick u {
(unknown) { println(`unknown`); },
(0i32) { println(`zero`); },
(*) { println(`other`); },
};
pass 0i32;
};
Without the (unknown) arm and without the wildcard, the
diagnostic is:
error: Non-exhaustive pick expression
Missing cases: unknown
(unknown-tainted selector requires `unknown` arm or wildcard *)
Wildcard is always the escape hatch
(*) covers every otherwise-uncovered inhabitant, including all
four special values. Use it freely when an arm-by-arm enumeration
is impractical; the checker will not penalise it.
Fixtures
bug418—pickon tbb missingERR(regression — already worked viaINTEGER_RANGE.requiresERR()).bug419—pickonint32?missingNILrejected.bug420—pickonint32->missingNULLrejected.bug421—pickonunknown-tainted selector missing(unknown)arm rejected.bug422— positive case:int32?with explicitNILarm + wildcard compiles and exits 0.
All five live under tests/bugs/run_bug_tests_03130.sh (5/5 PASS).