5.1.49. Async / Await
This tutorial covers daslib/async_boost — an async/await framework
built on top of daslang generators. Every [async] function is
transformed at compile time into a state-machine generator. No threads,
channels, or job queues are involved — everything is single-threaded
cooperative multitasking.
Prerequisites: Tutorial 15 (Iterators and Generators), Tutorial 40 (Coroutines).
options gen2
options no_unused_function_arguments = false
require daslib/async_boost
require daslib/coroutines
5.1.49.1. Void async — the simplest form
An [async] function with : void return type becomes a
generator<bool> state machine, just like a [coroutine].
Call await_next_frame() to suspend until the next step:
[async]
def greet(name : string) : void {
print(" hello, ")
await_next_frame()
print("{name}!\n")
}
def demo_void_async() {
print("=== void async ===\n")
var it <- greet("world")
var step = 1
for (running in it) {
print(" -- step {step} --\n")
step ++
}
print(" -- done --\n")
}
Each iteration of the for loop advances the generator by one
step. The function body resumes after the last await_next_frame()
and runs until the next one (or until it returns).
5.1.49.2. Typed async — yielding values
An [async] function with a non-void return type yields values
wrapped in variant<res:T; wait:bool>. Each await_next_frame()
yields variant(wait=true); each yield value yields
variant(res=value):
[async]
def compute(x : int) : int {
await_next_frame() // simulate one frame of work
yield x * 2
}
def demo_typed_async() {
for (v in compute(21)) {
if (v is wait) {
print(" (waiting...)\n")
} elif (v is res) {
print(" result = {v as res}\n") // 42
}
}
}
The consumer checks v is wait vs v is res to distinguish
suspension frames from actual results.
5.1.49.3. Await — waiting for an async result
Inside an [async] function you can await another async call.
await suspends the parent until the child completes and extracts
the result:
[async]
def add_one(x : int) : int {
await_next_frame()
yield x + 1
}
[async]
def chained_math() : void {
var a = 0
a = await <| add_one(0) // copy-assign: a = 1
a <- await <| add_one(a) // move-assign: a = 2
let b <- await <| add_one(a) // let-bind: b = 3
print(" a={a}, b={b}\n")
}
Three forms of await:
a = await <| fn(args)— copy-assign the resulta <- await <| fn(args)— move-assign the resultlet b <- await <| fn(args)— bind to a new variable
5.1.49.4. Struct return with move semantics
Async functions can yield structs. Use yield <- to move the
result out (useful for non-copyable data):
struct Measurement {
sensor_id : int
value : float
tag : string
}
[async]
def read_sensor(id : int) : Measurement {
await_next_frame()
var m : Measurement
m.sensor_id = id
m.value = 3.14
m.tag = "temperature"
yield <- m
}
[async]
def process_sensors() : void {
let m <- await <| read_sensor(1)
print(" sensor {m.sensor_id}: {m.value} ({m.tag})\n")
}
5.1.49.5. Iterating async generators
A typed async function that yields multiple values acts as an
asynchronous generator. Consumers iterate and check v is res
to extract each value:
[async]
def fibonacci_async(n : int) : int {
var a = 0
var b = 1
for (i in range(n)) {
yield a
let next_val = a + b
a = b
b = next_val
await_next_frame()
}
}
def demo_async_iteration() {
print(" fibonacci: ")
for (v in fibonacci_async(8)) {
if (v is res) {
print("{v as res} ")
}
}
print("\n")
}
Output: 0 1 1 2 3 5 8 13
5.1.49.6. Running tasks
async_boost provides four task runners:
async_run(it)— drive a single task to completionasync_run_all(tasks)— drive all tasks cooperatively, round-robin one step per task per frameasync_timeout(it, max_frames)— drive a task for at most max_frames steps; returnstrueif it completed in timeasync_race(a, b)— drive two tasks cooperatively; returns0if a finishes first,1if b finishes first
[async]
def worker(name : string; frames : int) : void {
for (i in range(frames)) {
print(" {name}: frame {i + 1}/{frames}\n")
await_next_frame()
}
}
async_run steps through a single task:
var single <- worker("solo", 3)
async_run(single)
async_run_all interleaves multiple tasks:
var tasks : array<iterator<bool>>
tasks |> emplace <| worker("alpha", 2)
tasks |> emplace <| worker("beta", 3)
async_run_all(tasks)
async_timeout enforces a deadline:
var fast <- worker("fast", 2)
let completed = async_timeout(fast, 10) // true
var slow <- worker("slow", 100)
let timed_out = async_timeout(slow, 3) // false (timeout)
async_race runs two tasks and returns which finishes first:
var racer_a <- worker("A", 2)
var racer_b <- worker("B", 5)
let winner = async_race(racer_a, racer_b) // 0 (A wins)
5.1.49.7. Mixing async with coroutines
An [async] function can await a [coroutine]. This lets
you compose low-level coroutine logic with high-level async
orchestration:
[coroutine]
def tick_counter(n : int) {
for (i in range(n)) {
print(" tick {i + 1}\n")
co_continue()
}
}
[async]
def orchestrator() : void {
print(" orchestrator: start\n")
await_next_frame()
print(" orchestrator: awaiting coroutine\n")
await <| tick_counter(3)
print(" orchestrator: coroutine done\n")
await_next_frame()
print(" orchestrator: finish\n")
}
5.1.49.8. Full source
The complete tutorial source is in
tutorials/language/49_async.das.
Run it with:
daslang.exe tutorials/language/49_async.das
See also
Full source: tutorials/language/49_async.das
Async/await coroutine macros — async_boost module reference.
Coroutines and additional generator support — coroutines module reference (underlying generator framework).
Coroutines — Tutorial 40: Coroutines (prerequisite).
Previous tutorial: Compile-Time Field Iteration with apply
Next tutorial: Structure-of-Arrays (SOA)