Async / Await
Overview
Djinn supports async functions and await expressions for writing asynchronous code. Under the hood, async functions compile to LLVM coroutines that are
fully optimized by the backend — in many cases resulting in zero overhead after inlining and coroutine elision.
async i32 compute(i32 x) {
return x * 2 + 1;
}
async i32 main() {
i32 result = await compute(10);
printf("result: %d\n", result); // result: 21
return 0;
}Async Functions
Any function can be marked async by adding the keyword before the return type. An async function returns a coroutine handle (ptr) instead of its declared return type.
async i32 fetch_value() {
return 42;
}The caller uses await to obtain the actual return value.
Syntax
async <return_type> <name>(<params>) { <body> }Rules
Rule
Description
Await Expressions
The await keyword suspends the caller until the async function completes, then extracts the return value from the coroutine promise.
i32 result = await some_async_function(args);Where you can use await
- Inside
asyncfunctions (chained coroutines) - Inside regular
main()or any function (the compiler generates an await loop)
Multiple awaits
You can await multiple async calls sequentially:
async i32 add(i32 a, i32 b) {
return a + b;
}
async i32 square(i32 x) {
return x * x;
}
async i32 main() {
i32 a = await square(3); // 9
i32 b = await square(4); // 16
i32 sum = await add(a, b); // 25
return sum;
}Chaining
Async functions can call and await other async functions, creating chains of coroutines:
async i32 step1(i32 x) {
return x + 10;
}
async i32 step2(i32 x) {
i32 val = await step1(x);
return val * 2;
}
async i32 pipeline(i32 x) {
i32 val = await step2(x);
return val + 5;
}
// pipeline(5) -> step2(5) -> step1(5) = 15 -> 30 -> 35Async Main
When main is declared as async, the compiler automatically generates:
__djinn_async_main()— the actual coroutine with your codemain()— a synchronous wrapper that calls and awaits the async entry point
// You write:
async i32 main() {
i32 val = await compute(10);
return val;
}
// Compiler generates (conceptual):
// ptr __djinn_async_main() { ... coroutine body ... }
// i32 main() { hdl = __djinn_async_main(); await hdl; return result; }Performance
Djinn's async implementation uses LLVM's native coroutine intrinsics (coro.id, coro.begin, coro.suspend, etc.). At optimization level -O2, the LLVM backend applies:
Pass
Effect
For simple "compute and return" async functions where the result is immediately awaited, the optimizer can eliminate all coroutine overhead — no malloc, no frame, no indirect calls.
Implementation Details
Each async function generates the following LLVM IR structure:
entry:
%promise = alloca <return_type>
%id = call token @llvm.coro.id(...)
%alloc = call i1 @llvm.coro.alloc(token %id)
br i1 %alloc, label %coro.alloc, label %coro.begin
coro.alloc:
%size = call i64 @llvm.coro.size.i64()
%mem = call ptr @malloc(i64 %size)
br label %coro.begin
coro.begin:
%hdl = call ptr @llvm.coro.begin(token %id, ptr %mem)
; ... function body ...
store <result>, ptr %promise
br label %coro.final
coro.final:
call i8 @llvm.coro.suspend(token none, i1 true)
; switch → cleanup or trap
coro.cleanup:
%free.mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
call void @free(ptr %free.mem)
coro.suspend:
call void @llvm.coro.end(ptr %hdl, ...)
ret ptr %hdlThe await expression generates:
await.loop:
%done = call i1 @llvm.coro.done(ptr %handle)
br i1 %done, label %await.ready, label %await.resume
await.resume:
call void @llvm.coro.resume(ptr %handle)
br label %await.loop
await.ready:
%promise = call ptr @llvm.coro.promise(ptr %handle, ...)
%result = load <type>, ptr %promise
call void @llvm.coro.destroy(ptr %handle)Functions are marked with the presplitcoroutine attribute so the LLVM coroutine passes can process them.
task<T>
The task<T> struct in std::sys represents a handle to an async coroutine. It wraps a raw coroutine handle and provides methods to check completion, resume execution, and retrieve results.
import std::sys;
struct task<T> {
void* handle;
}Constructor
task<i32> t = task<i32>(some_handle);Methods
Method
Description
is_completed() → boolReturns true if the coroutine has finished execution.resume()Resumes the suspended coroutine.get_result() → TRetrieves the return value from the completed coroutine promise.destroy()Destroys the coroutine frame and frees its memory.Coroutine Primitives
The low-level coro struct in std::builtin provides direct access to LLVM coroutine intrinsics:
import std::builtin;
struct coro {
static void* handle(); // get current coroutine handle
static bool done(void* h); // check if coroutine is done
static void resume(void* h);
static void destroy(void* h);
static void suspend(); // suspend current coroutine
}These primitives are used internally by Socket, Console, and other async APIs to implement cooperative scheduling with the Djinn runtime's event loop.