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
async functionmarks a function that may suspend.awaitsuspends the current task until the value is ready. Other tasks run in the meantime.spawn(() => ...)starts a concurrent task and returns a handle. Concurrency is always explicit. Nothing runs concurrently unless you spawn it.run()drives the scheduler until the spawned work is done.join()returns a result.Errmeans the task failed or was cancelled. A cancelled task never yields a made-up zero value.
"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.
- Because tasks are stackful, the runtime does not need the coloring. An async function called without
awaitstill produces its value:async function total(): Integer { a = fetchScore(1) // no await b = fetchScore(2) return a + b // 30 } - Explicit
awaitis the escape hatch that stays. It states clearly where a task can suspend, and you can always write it. - Lints help with the transition: W801 flags an
awaitthat is not needed (under elision), W802 flags anawaitinside a loop body, and W803 flags anasyncfunction that has no suspension point.
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
- Scheduler.
lumina runuses the cooperative coroutine scheduler by default, and worker threads run spawned tasks in parallel in progress. Tasks yield at suspension points rather than being preempted in progress.@Testfunctions run in a synchronous mode, so you can test async business logic without starting a scheduler. - I/O. Network and file waits go through epoll on Linux, and an io_uring backend exists for networking. kqueue (macOS) and IOCP (Windows) backends cover the other platforms designed.
- Stacks. Coroutine stacks are small by default and guarded by a guard page. Because deep recursion can overflow a small stack, the compiler warns about recursive functions (W601) and suggests
@Runtime(stackSize: N).
Coming from...
| You know | In Lumina |
|---|---|
Go go f() | spawn(() => f()) |
Go chan int | Channel<Integer>(n) |
JS Promise.all | spawn several tasks, then join each |
Rust tokio::spawn + .await | spawn + await, with no runtime crate and no Send bounds to write |
Python asyncio.run(main()) | just run the program |
Next: Memory