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 result

  • a <- await <| fn(args) — move-assign the result

  • let 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 completion

  • async_run_all(tasks) — drive all tasks cooperatively, round-robin one step per task per frame

  • async_timeout(it, max_frames) — drive a task for at most max_frames steps; returns true if it completed in time

  • async_race(a, b) — drive two tasks cooperatively; returns 0 if a finishes first, 1 if 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 macrosasync_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)