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:
- the binding's initialiser is the literal
unknown, or - the initialiser is a call to a function whose declared return-flow
contains
unknown(deferred / D-17a).
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)
is unknown/== unknownboolean form (D-18). Today the parser acceptsx == unknownbut the type checker folds it to a structural equality on the literal's underlying zero — that is not the documented semantic. Usepickwith anunknownarm for now.- Declared return-flow
T?unknown(D-17a opt-in). Until the marker exists, only explicitunknownliterals taint a binding. - Runtime failsafe arm for D-17. The existing
failsafe(tbb32:err)plumbing can be reused; deferred until a real program asks for it.
Fixtures
bug394…bug399—Symbol::mayBeUnknowntaint propagation;ok(...)laundering;ARIA-045on unwrapped forward (run_bug_tests_03124.sh).bug421—pickon tainted selector missing theunknownarm rejected.