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
bug383…bug386—NIL/NULLrejected at statement-level=(D-15 gap close,run_bug_tests_03122.sh).bug419—pickonint32?missing theNILarm rejected.bug420—pickonint32->missing theNULLarm rejected.bug422—pickonint32?with explicitNILarm + wildcard passes and exits cleanly.