← Back to AILP Home

Surface

The user-facing async surface in v0.31.x is small: one declaration form, one expression form, one type at the await site, and one compiler-inserted runtime call. This page is the inventory.

async func: declaration

async func:get_value = int32() {
    pass 42i32;
};

The leading async keyword sets the isAsync flag on FuncDeclStmt (parser: parser.cpp:2821-2890). It threads through FunctionType (type.cpp:342, 378, 407, 1108-1120), through GenericResolver, the type checker, the safety checker, and finally codegen, which lowers the body to an LLVM coroutine state machine. The return type after lowering is always wrapped in Result<T>: the success Result is constructed on pass, the error Result on fail, and the final Result<T> is stored to the coroutine promise via llvm.coro.promise.

Everything else about the declaration is the same as a regular func::

await <expr> expression

async func:run_tests = int32() {
    // Unwrap the success case with raw():
    int32:result = raw await get_value();

    // Or take the full Result and inspect:
    Result<int32>:err_result = await get_error();
    if (err_result.is_error == false) { pass 2i32; }

    pass 0i32;
};

await <expr> is parsed as an expression (parser.cpp:1122-1135) and lowers to an llvm.coro.suspend followed by an llvm.coro.promise read of the awaited task's result. The type of the whole await <expr> is Result<T> where T is the body return type of the awaited async func:.

To extract the success value when you know the call cannot fail, prefix with raw: raw await get_value() strips the Result<T> and yields a bare T. (raw is the same unwrap-or-trap shorthand used elsewhere in Nitpick for cross-module return values; see guide/functions/.)

await only inside async func:

await outside an async func: body is a hard error since v0.31.0.1:

ARIA-040: 'await' may only appear inside an 'async func:' body

Before v0.31.0.1 this was a soft-fail at runtime (stderr + exit 0). Now it stops the compile. Regression: bug334.

What await does not accept

await resolves only other compiled async func: calls. In Phase 1 it does not accept:

Result<T> at the await site

Awaiting an async function produces a Result<T>:

async func:get_value = int32() { pass 42i32; };
async func:get_error = int32() { fail 99i32; };

async func:caller = int32() {
    Result<int32>:ok  = await get_value(); // ok.is_error  == false
    Result<int32>:bad = await get_error(); // bad.is_error == true
    pass 0i32;
};

Result<T> exposes:

The standard Result helpers (raw, ?, !!, defaults, pick) all apply at the await site. They are the substitute for try/catch (which was pulled in v0.31.0.2 — see errors.md).

The compiler-injected executor drain

If any function in your module is declared async, the compiler injects a call to npk_executor_run(NULL) immediately before the user-level exit N in main (or before the implicit return at the end of main). This is the v0.31.0.1 D-2 decision:

[user main body]
    drop run_tests();
    exit 0;

// What actually lowers:
    drop run_tests();
    npk_executor_run(NULL);     // <-- injected by codegen
    exit 0;

npk_executor_run(NULL) is a no-op when there is no global executor (no async tasks were ever spawned), so modules that could be async but aren't pay nothing. Regression: bug336 (sync-only main has zero references to npk_executor_run in the emitted IR). For modules that do spawn, bug335 verifies the call lands and that the program actually drains its tasks before exit.

The contract:

Spawning and forgetting: drop <call>

A common pattern in main (which is not itself async):

func:main = int32() {
    drop run_tests();   // start the async task, abandon the future
    exit 0;             // executor drains it before exit (injected drain)
};

drop <call> discards the call's return value, including the implicit task handle returned by an async call. The task is still scheduled on the executor and is still drained by the injected npk_executor_run. This is the only way today to launch async work from a synchronous main.

What is not on the surface

These exist in the runtime but have no Phase 1 user-facing spelling: