nova-std 0.3.0

The Nova language's standard library sources (internal to the nova toolchain)
Documentation
//! Tasks: starting async work, waiting for it, and running it from code
//! that is not `async`.
//!
//! `spawn` hands a future to the executor as a new task and returns a
//! `JoinHandle`, whose `join` waits for the task's value. `block_on` runs
//! tasks from code that is not `async`, and `yield_now` lets the other tasks
//! take a turn. Every module sees these names without an import, and a
//! module's own definition of one of them shadows the one here.
//!
//! Tasks take turns on one thread. A task runs until it awaits something that
//! is not ready, and then the next ready task runs, in the order they became
//! ready. No task runs except while `block_on` is running, and `block_on`
//! returns only once every task has finished. An `async fn main` runs under
//! `block_on`. A task that panics ends the whole program.
//!
//! Two tasks started with `spawn` and joined, run from code that is not
//! `async` by `block_on`:
//!
//! ```
//! async fn square(n: Int) -> Int {
//!     yield_now().await // let the other tasks take a turn
//!     n * n
//! }
//!
//! async fn sum_of_squares() -> Int {
//!     let a = spawn(square(3))
//!     let b = spawn(square(4))
//!     a.join().await + b.join().await
//! }
//!
//! fn main() {
//!     // `block_on` runs async work from code that is not async.
//!     assert_eq(block_on(sum_of_squares()), 25)
//! }
//! ```

// Nova standard library — tasks.
//
// Compiled as an implicit module and glob-imported into every user module, so
// these names need no `import`. A user definition of the same name shadows the
// one here (see docs/adr/0004-stdlib-compile-model.md).
//
// This module, together with `std/time`'s `sleep`, is the whole of the
// surface between Nova source and the single-threaded cooperative executor in
// `crates/nova-runtime/src/task.rs`. `sleep` lives in `std/time` because it
// takes a `Duration`, but it wraps `task_sleep_future_nanos`, whose runtime
// function and park set both live here -- a maintainer auditing every seam
// into the park set needs both modules, not just this one. Every function
// here is a thin wrapper over a std-only builtin, and each wrapper exists
// because the builtin it calls cannot be generic on its own: a builtin's
// signature is a fixed list of types, so `Future<T>` in one of them means
// "the first type parameter of whatever generic function is calling me" (see
// `builtin_signature` in `nova-typeck`). That is why every generic function
// below declares exactly one type parameter, and why it is `T`.
//
// The executor is single-threaded and has no wakers of its own: a suspended
// task is re-queued for its next turn unless it parks — waiting on a
// deadline (`std/time`'s `sleep`) or another task's completion (`join`) —
// in which case it resumes only once that deadline passes or that task
// finishes, the executor's only two wake sources. `spawn` starts nothing on
// its own, and every task on a thread is advanced only while a `block_on`
// call is running. See that module's own doc comment, and ADR 0009.

// A handle on a spawned task, holding nothing but the future it is driving.
//
// The future alone is both this handle's key into the executor and where a
// finished task's value lives. `task_is_done`/`task_release` resolve it to a
// task by its state address (word 1 of the future) rather than by an index,
// and the output slot of that same state object is what the task's poll
// function wrote when it completed. Reading the value there rather than
// through the executor is what makes `join` both idempotent and correct for
// every output type: idempotent because nothing is consumed, and correct
// because the value arrives in its own machine class rather than as the `Int`
// the executor hands outputs back as (see `Builtin::TaskOutput`).
//
// This is also what keeps a handle from naming a task other than its own. An
// `Int` id was constructible out of thin air — record fields are public (ADR
// 0007) — and nothing stopped it from equaling some other task's id,
// including one this handle never spawned. A future's state address cannot be
// forged the same way: the only way to obtain one is to call an `async fn`,
// so `task_is_done`/`task_release` have no id space to guess at, only the one
// state this handle's `fut` actually points to.
//
// An address is an identity only while the object at it lives — stated in full
// in `crates/nova-runtime/src/gc.rs`'s module doc comment, which owns that
// property. The executor holds to it: it stops recognising a state address when
// the collector frees the state object, so a fresh future landing on the address
// of a collected one is still a future no task was spawned for, and is
// rejected rather than answered for whatever task used to be there.
//
// Holding the future also keeps the state object reachable for as long as the
// handle itself is: the record is a scanned heap object, so the collector
// traces through it. That is what makes the value still readable after the
// executor has released its own claim on it, which for a spawned task
// happens when it completes.
//
// No bound on `T` here: a bound on a record's type parameter is a resolution
// scope and is not enforced at construction (ADR 0007), so any bound belongs on
// the `impl` instead. There is none to state.
/// A handle on a task started by `spawn`, whose `join` waits for the task's
/// value.
///
/// Copying a `JoinHandle` copies the handle: every copy refers to the same
/// task. There is no way to cancel a task, and dropping its handle leaves it
/// to run on.
pub record JoinHandle<T> { fut: Future<T> }

impl<T> JoinHandle<T> {
    // Parks until the task finishes, rather than re-checking each turn. One
    // await is enough: the only thing that wakes this task is the target's
    // completion, so there is no state in which it resumes early. The
    // `task_is_done` spin this replaced cost one poll per turn and could not
    // be told apart from a deadlock. Joining a task that is parked with
    // nothing left in the program that could ever wake it — for example,
    // two tasks joining each other in a cycle — now reports a deadlock
    // instead of hanging. Joining a task parked on a deadline (one that is
    // sleeping) is not, by itself, this. Joining a task that never finishes
    // because it keeps re-queueing itself (a `yield_now` loop, forever) is
    // a livelock rather than a deadlock and still hangs: the ready queue is
    // never empty, so the check that would report one is never reached
    // (ADR 0009 §1's 2026-08-10 amendment).
    //
    // `task_release` before the read. The executor already released a
    // spawned task's claim on its state object when the task completed, so
    // for every task `join` reaches — it awaits completion first — this
    // call finds the claim gone and does nothing. It stays because it is
    // idempotent and costs nothing, and because it keeps `join` correct if
    // the executor's release point ever moves. The read below depends only
    // on the claim this handle has through `self.fut`. Calling `join` twice
    // on one handle reads the same value twice — deliberately, because Nova
    // has no move checking, so `self` by value cannot stop a second call.
    /// Waits for the task to finish, and returns its value.
    ///
    /// An `async fn`: it suspends the calling task until the task finishes,
    /// and other tasks run meanwhile. If the task has already finished, it
    /// returns at once, without suspending. Any number of tasks can join one
    /// task, and joining it again returns the same value again. If every task
    /// left is waiting in a `join`, as when two tasks join each other, the
    /// program prints a deadlock report naming each one and ends. A task that
    /// never finishes because it keeps yielding is not detected: joining it
    /// waits forever.
    pub async fn join(self) -> T {
        task_join_future(self.fut).await
        task_release(self.fut)
        task_output(self.fut)
    }
}

// Hand `fut` to this thread's executor and return a handle on it.
//
// Nothing runs yet. The task is queued, and the queue is drained by `block_on`
// (or by a `yield_now` inside a task the current `block_on` is already
// draining) — so a `spawn` with no `block_on` anywhere above it never runs at
// all.
//
// `task_spawn`'s returned id is discarded: the handle's identity is `fut`
// itself now, so nothing downstream ever needs the id back.
/// Starts `fut` as a new task, and returns a `JoinHandle` for it.
///
/// It is not `async`, and the new task does not run yet: it joins the back
/// of the queue of ready tasks, and runs only while `block_on` is running, so
/// a task spawned outside `block_on`, with no `block_on` called after, never
/// runs at all. Each future is meant to run once: do not also await it or
/// pass it to `block_on`. Panics if `fut` is already a task that has not
/// finished.
pub fn spawn<T>(fut: Future<T>) -> JoinHandle<T> {
    task_spawn(fut)
    JoinHandle { fut: fut }
}

// Suspend the current task, letting every other queued task take a turn.
//
// The one function in this module whose body must suspend, and the reason
// `task_yield_future` is a builtin: an `async fn` body suspends only at an
// `.await`, and nothing else in the language is a future that is not already
// ready. Each call awaits a fresh one, so two tasks yielding at the same time
// do not share a resume point.
/// Suspends the calling task for one turn, letting the other ready tasks run
/// first.
///
/// An `async fn` that always suspends: the task goes to the back of the queue
/// of ready tasks, and resumes once each task ahead of it has had a turn.
/// With no other task ready, it resumes at once.
pub async fn yield_now() { task_yield_future().await }

// Run this thread's executor until every queued task has finished, and produce
// `fut`'s value. The way async code is entered from sync code.
//
// This drains the whole queue, not just `fut`: with no wakers and no driver
// thread, a task left pending when this returned could never be advanced again.
// So `block_on` implicitly joins everything spawned on this thread, which is
// where it parts company with tokio's.
//
// If every remaining task is parked and none of them can ever wake each
// other — most simply, two tasks joining each other — this reports a
// deadlock naming each one and what it is waiting for, then aborts, rather
// than hanging. A task that never parks (a `yield_now` loop, forever) is not
// this: it keeps the queue non-empty, so this neither returns nor aborts.
//
// **Not callable from inside an `async fn`.** A nested call would run a second
// executor loop from inside the first one's frame; the runtime detects that and
// ends the process with a diagnostic rather than corrupting the shared queue.
/// Runs `fut` to completion from code that is not `async`, and returns its
/// value.
///
/// It runs every other task too, including those spawned before the call,
/// and returns only once all of them have finished, even those asleep or
/// waiting for a connection. If every task left is waiting in a `join`, as
/// when two tasks join each other, it prints a deadlock report naming each
/// one and ends the program. A task that keeps yielding without finishing
/// keeps it from returning. An `async fn main` runs under `block_on`, so it
/// needs no call of its own.
///
/// Panics if called while `block_on` is already running, as from an
/// `async fn` or a function one calls: there, `.await` the future instead.
/// Panics too if `fut` is already a task that has not finished.
///
/// `block_on` runs every task, not only the one it is given:
///
/// ```
/// async fn visit(mut log: Vec<String>, name: String) { log.push(name) }
///
/// fn main() {
///     let mut log: Vec<String> = Vec::new()
///     spawn(visit(log, "task"))
///     log.push("main") // the task has not run: no `block_on` is running yet
///
///     // `block_on` runs every task, not only the one it is given.
///     block_on(yield_now())
///     assert_eq(log.len(), 2)
///     assert_eq(log.get(0).unwrap(), "main")
///     assert_eq(log.get(1).unwrap(), "task")
/// }
/// ```
pub fn block_on<T>(fut: Future<T>) -> T {
    task_drive(fut)
    task_output(fut)
}