Skip to content
volt
GitHub

Async

An async fn can pause itself with suspend and be continued later. Its state lives in a frame: a plain value whose size is known at compile time, built in place in the local that val f = async g(args) declares. There is no heap allocation, no scheduler and no runtime: you drive frames yourself, and an event loop can be a library on top.

val f = async g(args) start g; it runs until its first suspend (or the end)
suspend inside an async fn: pause here
resume f run f until its next suspend
await f run f to the end and give its result
await g(args) call and wait in one step

A plain call of an async fn (without async) just runs it to the end. Resuming a frame that has finished, or awaiting one twice, stops the program (panic: resumed a frame that already finished), in release builds too.

use std::io;
async fn phases() -> i32 {
var result = 10;
suspend;
result += 5;
suspend;
return result * 2;
}
fn main() -> void {
val f = async phases(); // runs to the first suspend
resume f; // to the second
std::println("{}", await f); // to the end
std::println("{}", phases()); // a plain call: to the end, right away
}
// expect: 30
// expect: 30

Parameters, locals and loop state all live in the frame across suspends, so a generator is just a loop with a suspend in it:

use std::io;
async fn count(from: i32, to: i32, out: i32&) -> void {
for (i) in from..to {
*out = i;
suspend;
}
}
fn main() -> void {
var cur = 0;
val g = async count(3, 6, &cur);
std::print("{} ", cur);
resume g;
std::print("{} ", cur);
resume g;
std::println("{}", cur);
await g;
}
// expect: 3 4 5

A frame owns its locals. A frame deleted before it finishes runs its defers and deletes what it holds, like a scope that ends early.

use std::io;
async fn task(label: str) -> void {
defer std::println("{} cleaned up", label);
std::println("{} started", label);
suspend;
std::println("{} finished", label);
}
fn main() -> void {
{
val t = async task("a");
} // never resumed: its defer runs here
std::println("end");
}
// expect: a started
// expect: a cleaned up
// expect: end

main, extern and export fns can’t be async, and a frame can’t be copied or moved (it may point into itself). suspend can’t go in a defer, or in a match, loop or block used as a value. An async fn can’t start itself with async (its frame would hold itself), but it can call or await itself.

A Rust crate’s async fn comes in as a Volt async fn: see Volt calls Rust.