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::
- The signature uses the normal
func:name = ReturnType(params)shape. The bare return type (int32above) is what the body produces; the outerResult<int32>is invisible at the declaration site. - The body terminator is
};(function declarations end with the explicit semicolon — same as sync functions). failsafeis not invoked for afailinside an async function body. The error rides theResult<T>to whoever awaits the call.
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:
- A bare value (
await 42i32) — type error. - A
Future<T>as a named type —Future<T>is not nameable in user code this cycle (deferred). - A runtime
NitpickFutureHandle(the C-API future returned bynpk_*_asyncfile I/O). Bridging is deferred; seefile_io.mdfor the spin-poll alternative.
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:
.is_error: bool— true if the awaited task ended infail..value: T— the success payload (defined whenis_error == false)..error: tbb32— the error payload (defined whenis_error == true).
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:
- You don't call
npk_executor_runyourself. The compiler inserts it. Calling it manually is not wrong, but it is redundant. exit Nstill skips Drop (same as sync; seeguide/drop/DROP-DEC-008). The executor drain happens beforeexit, so any task that completes beforeexitwill run its Drops. Tasks still suspended atexitare abandoned (D-10 (b) deferred — seedrop.md).
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:
Future<T>as a nameable type. You cannot sayFuture<int32>:f = ....Result<T>is what surfaces fromawait; theFutureis internal.- A multi-threaded executor.
work_stealing.cppis compiled and present but is not the default. The single- threaded executor is what your async calls run on. - Async network.
async_net.cpp(705 lines) is compiled in but has no extern bindings exposed at the user surface in Phase 1. Phase 2. try/catch. Pulled in v0.31.0.2 (D-8). Seeerrors.md.- A timer / sleep / yield primitive at the user surface. The runtime has the plumbing; the spelling is deferred.