← Back to AILP Home

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:

The dispatch in getDomain() routes TypeKind::OPTIONALforOptional() and TypeKind::POINTERforPointer() 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

All five live under tests/bugs/run_bug_tests_03130.sh (5/5 PASS).