Result<T> and fail
Every body-bearing Nitpick function implicitly returns Result<T>,
where T is the declared return type. This is the universal
"this might fail" wrapper that pairs with pass val (success) and
fail(code) (failure).
| Construct | Lowers to |
|---|---|
func:f = T(...) { ... }; |
Result<T> f(...) |
pass val |
Result<T>{ value: val, is_error: false, error_code: 0 } |
fail(code) |
Result<T>{ value: <NIL>, is_error: true, error_code: code } |
func:f = NIL(...) { ... }; |
Result<NIL> f(...) (the void-shaped form, D-21) |
The shape
A Result<T> is a three-field struct:
{ T value, ptr error, i8 is_error }
The value field holds the success payload; error is a pointer
to an error descriptor (or null); is_error is the discriminant.
pass and fail
func:safe_div = int32(int32:a, int32:b) {
if (b == 0i32) {
fail(1i32);
};
pass a / b;
};
func:main = int32() {
Result<int32>:r = safe_div(10i32, 0i32);
if (r.is_error) {
println(`failed with code {r.error_code}`);
} else {
println(`= {r.value}`);
};
pass 0i32;
};
The D-20 rule — .value == NIL when is_error
Decision D-20 (Phase 3, landed in v0.31.2.8): when a Result<T>
was created via fail(code), the .value field reads as NIL. This
is observable for the two type shapes where NIL is a valid
inhabitant:
Result<NIL>— the wrapped type isNIL; the zero-fill is the only inhabitant; reading.valueafterfailalways yieldsNIL. Allowed without a flow-sensitive check.Result<Optional<T>>— the wrapped type carries a discriminant bit; the zero-fill sets that bit to "None"; reading.valueafterfailyieldsNone. Allowed without a flow-sensitive check.
For all other Result<T>, reading .value without first checking
.is_error is a static error:
func:bad = int32() { fail(7i32); };
func:main = int32() {
Result<int32>:r = bad();
int32:x = r.value; // error: ARIA-046 — read of `.value` on
// a potentially-failed Result<int32>
pass 0i32;
};
The sema gate exempts the two NIL-safe shapes; everything else
must if (r.is_error == false) { ... use r.value ... };.
func:f = NIL(...) { ... }; — the void-shaped form (D-21)
A function whose explicit return type is NIL still wraps to
Result<NIL>, which makes pass NIL and fail(code) both
well-typed inside the body:
func:noop = NIL() {
println(`hello`);
pass NIL;
};
func:bad = NIL() {
fail(7i32);
};
func:main = int32() {
Result<NIL>:r1 = noop();
Result<NIL>:r2 = bad();
println(`r1.is_error = {r1.is_error}, r2.is_error = {r2.is_error}`);
pass 0i32;
};
A NIL-shaped Result is silently-discard-rejected just like any
other Result; use raw noop(); if you genuinely want to throw the
result away.
Known gotcha — DEF-CHAIN-RESULT
Chained access of a Result<T> field directly off the call
expression segfaults at runtime for every Result-returning
function:
// CRASHES at runtime — chained field access off the call:
bool:e = bad().is_error;
// Always-correct workaround — bind first:
Result<NIL>:r = bad();
bool:e = r.is_error;
Discovered in v0.31.2.7 during D-21 verification. Pulled into
Phase 4 as a dedicated slice (DEF-CHAIN-RESULT); not a NIL- or
D-21-specific issue (the call-site member-access codegen is the
common root cause).
Fixtures
bug406…bug409—Result<NIL>shape,pass NIL/failround-trip (run_bug_tests_03127.sh).bug410…bug413—Result<NIL>.value/Result<int32?>.valueread afterfailallowed (D-20),Result<int32>.valueread rejected (ARIA-046) (run_bug_tests_03128.sh).