← Back to AILP Home

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:

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