Async

Lumina's concurrency runs on stackful coroutines. Every task gets its own small stack and is scheduled cooperatively, with cancellation built in. A multi-threaded work-stealing runtime spreads tasks across cores in progress, so I/O-bound and CPU-bound work both scale. You get the ergonomics of Go's goroutines with the explicitness of async/await, and none of Rust's Pin, Future trait bounds or executor choice.

The short version

async function fetchScore(id: Integer): Integer {
    await delay_ms(10)
    return id * 10
}

async function total(): Integer {
    a = await fetchScore(1)
    b = await fetchScore(2)
    return a + b
}

function main(console: Console): Integer {
    t1 = spawn(() => fetchScore(3))
    t2 = spawn(() => total())
    run()
    r1 = match t1.join() { Ok(v) => v, Err(_) => -1 }
    r2 = match t2.join() { Ok(v) => v, Err(_) => -1 }
    console.println(`results {r1} {r2}`)
    return 0
}
results 30 30

"Colorless" async

In JavaScript, Python and Rust, async "colors" a function: once one function is async, every caller has to be async too. Lumina removes that split. Any function may suspend, and await is optional at the point where a value is used in progress.

Language_Async.md holds the phased plan.

Channels

Channels are typed, bounded queues between tasks, as in Go:

async function producer(out: Channel<Integer>, n: Integer): Integer {
    for i in 1..=n {
        out.send(i * i)
    }
    return 0
}

async function main(console: Console): Integer {
    squares = Channel<Integer>(16)
    spawn(() => producer(squares, 5))
    run()

    mut sum = 0
    for _ in 1..=5 {
        sum += squares.receive()
    }
    console.println(`sum of squares: {sum}`)
    return 0
}
sum of squares: 55

Channel<T>(capacity) creates the channel, send blocks the sending task when the channel is full, and receive blocks the receiving task when it is empty. main itself can be async.

A producer calls close() when it is done, and a consumer can then drain the channel with for v in ch (or loop on hasNext()). select { } waits on several receives at once, with clock.timeout(duration) as a timeout arm; a timeout of 0 plays the role of Go's default. If every task is blocked, the runtime reports a deadlock instead of hanging.

Channels follow Go's semantics: draining with for v in ch works for any element type, a receive tells "closed" apart from a value, a send on a closed channel fails loudly, a capacity-0 channel is a rendezvous, and select and directional Sender<T>/Receiver<T> types round them out in progress.

The full Go-to-Lumina table is in Comparison.

Cancellation

async function slow(): Integer {
    await delay_ms(1000)
    return 99
}

function main(console: Console): Integer {
    task = spawn(() => slow())
    task.cancel()
    run()
    outcome = match task.join() {
        Ok(v) => `finished with {v}`,
        Err(_) => "cancelled"
    }
    console.println(outcome)     // cancelled
    return 0
}

cancel() is a request: the task stops at its next suspension point. You still have to handle the outcome, because join() returns a result.

Async streams

function main(console: Console): Integer {
    mut sum = 0
    for await x in Stream.range(1, 11) {
        sum += x
    }
    console.println(`sum {sum}`)     // sum 55
    return 0
}

for await consumes any async stream. The HTTP server example uses the same loop to accept connections (for await client in listener.incoming()); see Zero-Copy & Servers.

Under the hood

Coming from...

You knowIn Lumina
Go go f()spawn(() => f())
Go chan intChannel<Integer>(n)
JS Promise.allspawn several tasks, then join each
Rust tokio::spawn + .awaitspawn + await, with no runtime crate and no Send bounds to write
Python asyncio.run(main())just run the program

Next: Memory