Easy to understand

Code that waits needs no marking

A call that waits is an ordinary call

In JavaScript, Python or C#, a function that waits for a file or the network is marked async, every call to it says await, and when a function starts to wait, its callers have to change as well. In Nikaia you do not need await, because the compiler finds where a function pauses. A call that waits pauses its function, the program goes on with other work, and the call is written like any other. A function that starts to wait leaves its callers as they are. Where a piece of code must not wait, the function says sync; the compiler holds it to that, and it may call only other sync functions.

Running two things at once today

Lines run in the order they are written. To read two files at the same time, the program below puts both reads in an overlap block, which runs its lines together and waits for all of them. It compiles with today’s compiler. If one read fails, its error is thrown out of the block, and the compiler asks for it to be handled there, as for any call that can fail: here by throws on main.

The design being built

The specification replaces overlap and spawn with one word, async, written before an expression (#983, not built yet). async e starts e at once, and the program waits for its value where the value is first needed:

let user = async db::load_user(id)
let rights = async db::load_rights(id)
let prefs = cache::load_prefs(id)      // runs while both loads are in flight
render(user, rights, prefs)            // waits for both first

In the same way f(async g(), async h()) runs g and h beside each other and calls f when both are done, and a catch written after an async expression is its fallback. A task that is to be joined or cancelled later is started with async(handle), and one that may outlive the function that started it with async(detached), which takes the place of spawn; a task is cancelled only through such a handle. Work over every element of a list is xs.par_iter(), whose lambdas may pause. The option names handle, deferred and detached are placeholders until the names are settled.

What the compiler checks about tasks

Two tasks that would change the same value at once, or both write to standard output, are refused, naming what they share (Conflicting actions); where the compiler cannot tell what a call touches, it does not refuse. A task’s error is thrown where the program waits for the task. A task handle that is never joined, cancelled or detached is refused, and the tasks a block starts are waited for at its end. Each task is to get a memory budget of its own, so that a task that uses it up ends with task::OverBudget and the rest of the program runs on; that is specified and not built yet, and memory a Rust crate or a C library allocates for itself is not counted (A memory budget for every task).

Reading two files at once, as it compiles today
use std::fs

fn size_of(path: String) -> int throws {
    let text = fs::read_to_string(path, fs::Root::Anywhere)
    return text.len()
}

fn main() throws {
    let (a, b) = overlap {
        size_of("a.txt")
        size_of("b.txt")
    }
    println(f"{a + b} characters in both files")
}

More under Easy to understand: Readable errorsFormats are part of the language