← Back to AILP Home

NIL and NULL

Nitpick splits "absence" into two distinct keywords, each tied to a distinct type shape:

Keyword Pairs with Mental model
NIL Optional<T>, NIL slot "no value is present"
NULL Pointer<T> (spelt T->) "no address is held"

Decision D-15 (Phase 3): mixing them is a hard error with a "did you mean the other one?" hint. This is enforced at both the variable-declaration site and at statement-level = (the latter closed in v0.31.2.2 — that gap is the headline win of Phase 3 slice 2).

NIL — value-side absence

NIL is the (sole) inhabitant of the NIL type and the "empty" tag of any Optional<T>. It binds to value-shaped slots:

func:main = int32() {
    int32?:maybe_x = NIL;
    if (maybe_x == NIL) {
        println(`no value`);
    };
    pass 0i32;
};

Assigning NIL to a non-optional binding is rejected at both the declaration site and at later =:

func:main = int32() {
    int32:x = NIL;   // error: NIL can only be assigned to optional types
    x       = NIL;   // also rejected (closed in v0.31.2.2 — D-15 gap)
    pass 0i32;
};

The diagnostic carries a friendly hint:

error: NIL can only be assigned to optional types
       (did you mean NULL for a pointer?)

NIL-shaped Result<T>

A function declared func:f = NIL(...) { ... }; returns Result<NIL> — see result-and-fail.md. On failure, the .value field reads as NIL (D-20). Result<NIL> is the canonical shape for "this might fail, but on success there is no useful payload" (the void-shaped function form).

NULL — pointer-side absence

NULL is the (sole) "no address" inhabitant of Pointer<T> (spelt T-> in Nitpick). It binds to pointer-shaped slots only:

func:main = int32() {
    int32->:p = NULL;
    if (p == NULL) {
        println(`null pointer`);
    };
    pass 0i32;
};

The symmetric rejection:

func:main = int32() {
    int32:x = NULL;       // error: NULL can only be assigned to pointer types
                          //        (did you mean NIL for an optional?)
    int32?:y = NULL;      // error (same)
    pass 0i32;
};

Why the split?

Optional<T> is a tagged union at the value level: it carries a hidden discriminant bit. Pointer<T> is a raw machine word at the pointer level: its discriminant is "the value is the all-zeros bit pattern". Conflating the two would let a non-optional int32 silently become "absent" with no runtime tag — a footgun the language refuses to allow.

For C interop, you almost always want Pointer<T> + NULL. For Nitpick data, you almost always want Optional<T> + NIL.

Fixtures