← Back to AILP Home

unknown and ok(...)

unknown is Nitpick's "this value was never determined" sentinel. Unlike NIL / NULL / ERR, unknown is not tied to a specific type shape — instead, it taints whatever binding it lands in, and forwarding a tainted binding without unwrapping it via ok(val) is a hard error.

Decision D-17 (Phase 3): when a binding's static origin can produce unknown, forwarding that binding to another call without wrapping in ok(...) is a compile-time error (ARIA-045). The runtime-only failsafe arm is deferred (no Nitpick program currently needs it).

Decision D-17a (sub-decision): "a function whose return can be unknown" is opt-in via a declared return-flow marker (working spelling func:f = T?unknown(...)). The marker itself is deferred — today only explicit unknown literals taint the symbol. This keeps stdlib precision tractable.

The taint flag

Internally, every Symbol carries a mayBeUnknown : bool flag (include/frontend/sema/symbol_table.h:60). It is set when:

Once set, it propagates: any later reassignment from another tainted source taints the LHS as well. The walker TypeChecker::exprCarriesUnknownTaint(ASTNode*) answers "does this expression carry the taint?" for use at enforcement sites.

The rule — ok(...) to launder the taint

func:consume = int32($$i int32:x) { pass x; };

func:main = int32() {
    int32:t = unknown;          // t.mayBeUnknown = true
    consume(t);                 // error: ARIA-045
    consume(ok(t));             // ok — taint laundered
    pass 0i32;
};
error: ARIA-045: forwarding `unknown`-tainted binding `t` requires
       `ok(t)` (or a value-source check)
   --> example.npk:5:13
    |
  5 |     consume(t);
    |             ^

ok(val) is the explicit "I have checked that val is not unknown" cast. Today it is a no-op at runtime; the compile-time side strips the taint flag from the result expression.

pick exhaustiveness picks this up

pick on a tainted selector must cover an unknown arm (or use the wildcard (*)):

func:main = int32() {
    int32:t = unknown;          // tainted
    pick t {
        (0i32)     { println(`zero`); },
        (*)        { println(`other`); },    // covers unknown via wildcard
    };
    pass 0i32;
};

If both unknown and (*) are missing, the diagnostic is:

error: Non-exhaustive pick expression
       Missing cases: unknown
       (unknown-tainted selector requires `unknown` arm or wildcard *)

See pick-exhaustiveness.md for the full rule.

What is not yet wired (Phase 4 / deferred)

Fixtures