← Back to AILP Home

Runtime taint and T?unknown

This chapter documents the runtime layer of the unknown value: how is unknown lowers, how the callee-return-summary path infers taint without an annotation, and how the declared T?unknown return-flow marker opts a function into the same contract explicitly.

All three sub-features landed in the v0.31.9.x sub-cycle:

Slice Lands
v0.31.9.1 Real is unknown taint fold (PHASE3-DEC-002/003/004).
v0.31.9.2 Callee return-taint summary; TBB_UNKNOWN_AT_USE reservation.
v0.31.9.3 Reject assignment to a fixed struct field (ARIA-056) — see
../types/fixed_field.md.
v0.31.9.4 Declared return-flow marker T?unknown (D-17a / PHASE3-DEC-011).
v0.31.9.5 K-semantics runtime layer locked (PHASE3-DEC-010).

The is unknown test

Given a binding of any ordinary type, x is unknown evaluates to a bool that is true when the binding's value originated from the unknown literal (directly or transitively through propagation), and false otherwise.

func:main = int32() {
    int32:x = unknown;
    pick (x is unknown) {
        (true)  { exit 0i32; },
        (false) { exit 1i32; }
    }
};

The compiler folds x is unknown to a constant true / false at each use site by consulting the per-binding Symbol::mayBeUnknown flag. When the flag cannot be statically resolved (e.g. the binding came from an arithmetic mix of tainted and clean sources), the test conservatively folds to true — callers always learn "yes, this may be unknown".

The callee return-taint summary

A function body that contains pass unknown (directly, or through a folded constant) automatically gets returnMayBeUnknown = true set on its declaration. Callers then treat the bound result as tainted without any annotation:

func:maker = int32() {
    pass unknown;
};

func:main = int32() {
    int32:x = raw maker();   // x is now flagged tainted
    pick (x is unknown) {
        (true)  { exit 0i32; },   // taken
        (false) { exit 1i32; }
    }
};

The summary is body-discovered. A caller never needs to write any qualifier on the binding — Symbol::mayBeUnknown is set at initialization time from the callee's flag.

The declared T?unknown marker

When you want to opt a function into the same contract without having pass unknown in its body (for instance, the body is a stub or an extern shim that may surface unknown values from C), write the return type as T?unknown:

func:maker = int32?unknown() {
    pass 42i32;
};

func:main = int32() {
    int32?:x = raw maker();
    pick (x is unknown) {
        (true)  { exit 0i32; },   // conservatively taken
        (false) { exit 1i32; }
    }
};

The trailing unknown is opt-in. A bare T? retains the original optional-type semantics from earlier slices and does not taint caller bindings. Both paths — body-inferred and declared — write into the same FuncDeclStmt::returnMayBeUnknown flag, and both are consumed by the same caller-side hook (initializerTaintsBinding's CALL arm), so the caller-visible contract is identical regardless of which path opted the callee in.

Stripping the taint with ok(...)

A tainted binding stays tainted until you wrap a read with ok(...):

func:main = int32() {
    int32?:x = raw maker();
    int32:y = ok(x);          // y is clean
    pick (y is unknown) {
        (true)  { exit 1i32; },
        (false) { exit 0i32; }   // taken
    }
};

ok(...) is the only way to convert from a taint-carrying binding to a clean one. The compiler folds ok(x) is unknown to false at the declaration site of y.

Arithmetic propagation

unknown is a propagating bottom for arithmetic: unknown + 5, unknown * 0, unknown - unknown all stay unknown. The K semantics models this directly (see nitpick.k lines around 2112 for the Unknown propagation rules, and tests 158–161 in k-semantics/tests/core/ for the runtime regressions).

Cross-references