Skip to main content

SYNC

Constant SYNC 

Source
pub const SYNC: &str = "//! A mutex and a bounded channel, for tasks that share data.\n//!\n//! A `Mutex` guards a value so that one task at a time works on it: `lock`\n//! returns a `MutexGuard`, which reads and replaces the value until it is\n//! released. `channel` makes a `Channel`, a queue holding at most a fixed\n//! number of values, which tasks fill through a `Sender` and empty through a\n//! `Receiver`, in the order the values were sent. Every module sees these\n//! names without an import, and a module\'s own definition of one of them\n//! shadows the one here.\n//!\n//! Tasks take turns on one thread, so a task is interrupted only where it\n//! awaits; a mutex is for a value that must stay consistent across an\n//! `.await`. Copying a `Mutex`, `MutexGuard`, `Channel`, `Sender` or\n//! `Receiver`, or passing it to a function, shares it rather than copying it.\n//! The methods that lock, set, release, send, receive or close take\n//! `mut self`, and need a mutable receiver, such as one held in a `mut`\n//! binding.\n//!\n//! A task waiting for a lock, for room in a channel or for a value yields to\n//! the other tasks and tries again, over and over: it keeps the processor\n//! busy while it waits, and a wait that can never end, such as for a lock\n//! that is never released, goes on forever rather than being reported as a\n//! deadlock.\n//!\n//! A producer task sends values through a channel and closes it; the\n//! receiver takes them until `recv` gives `None`:\n//!\n//! ```\n//! async fn produce(mut tx: Sender<Int>) {\n//!     for i in 1..4 { tx.send(i * 10).await }\n//!     tx.close() // once the channel is empty, `recv` gives `None`\n//! }\n//!\n//! async fn main() {\n//!     let ch: Channel<Int> = channel(2)\n//!     spawn(produce(ch.sender()))\n//!     let mut rx = ch.receiver()\n//!     let mut total = 0\n//!     while true {\n//!         match rx.recv().await {\n//!             Some(v) => total = total + v\n//!             None => break\n//!         }\n//!     }\n//!     assert_eq(total, 60)\n//! }\n//! ```\n\n// Nova standard library -- synchronisation.\n//\n// An *async* mutex, which is the primitive a cooperative executor needs. A\n// thread mutex would be pointless here: `crates/nova-runtime/src/task.rs`\'s\n// first line is \"A single-threaded cooperative executor\". But a task can be\n// suspended in the middle of a critical section at any `.await`, and while it\n// is suspended another task runs -- so anything whose invariant spans a\n// suspension point needs exactly this.\n//\n// Contention is handled by yielding and retrying rather than by parking. That\n// keeps the whole module in Nova with no runtime change, and the cost is\n// stated rather than hidden: a waiter stays *runnable*, so `report_deadlock`\n// cannot see it and a never-released lock spins instead of being diagnosed.\n// See the design doc\'s \u{a7}5 for the condition that would justify revisiting.\n\n/// A value of type `T` that one task at a time can work on, through a\n/// `MutexGuard`.\n///\n/// `lock` and `try_lock` return a guard, and the guard\'s `release` unlocks\n/// the mutex. Nothing releases a guard for you: one never released leaves the\n/// mutex locked for the rest of the program. A mutex is not re-entrant: a\n/// task that locks one it already holds waits forever. It makes no fairness\n/// guarantee either: when several tasks wait, which one gets the lock next is\n/// unspecified.\npub record Mutex<T> { locked: Bool, value: T }\n\n// The right to touch a `Mutex`\'s value, and the obligation to release it.\n//\n// NOT RAII, and not by choice: `Drop` is described in three spec files\n// (`12-TYPESYSTEM.md`, `13-RUNTIME.md`, `14-CODEGEN.md`) and implemented in\n// none of them -- no handling in nova-typeck, nova-resolver or nova-mir, and\n// no `trait Drop` in std/core. Releasing this guard is pure Nova\n// (`self.owner.locked = false`), so RAII here would need that *language*\n// feature; nothing in the runtime substitutes for it.\n//\n// ADR 0012 is the precedent for what to do instead, not the reason there is\n// no RAII: it chose explicit, idempotent `close` for `File` over a\n// collector-based backstop, and accepted a uniform documented leak. So\n// release is explicit and idempotent here too, and a guard that is never\n// released leaves the mutex locked for the life of the process -- the same\n// trade, accepted for the same reason.\n//\n// (An earlier version of this comment claimed ADR 0012 shows the collector\n// \"offers no per-object hook\". When written, ADR 0012 showed the opposite:\n// the hook existed and the ADR named it, and its argument was that the hook\n// reported *the dying object\'s own address*, which tells an `fd`-keyed table\n// nothing. Since 2026-10-01 the collector no longer has a per-object hook at\n// all (ADR 0020). Either way the argument is about `File`, not about a\n// pure-Nova guard, and does not transfer here.)\n//\n// `released` is what makes that idempotence true rather than merely claimed,\n// and `File`\'s precedent does not supply it. `File`\'s second `close` is safe\n// because an `fd` is a key into a runtime table and absence from that table\n// *is* closedness (ADR 0012). A guard has no table and no key: `owner` is the\n// live mutex itself, because records are reference types\n// (`docs/adr/0005-mutable-receivers-and-one-shot-hash.md`), and this compiler\n// has no move or linear checking, so a guard stays usable after `release`\n// (`crates/nova-cli/tests/run_tests.rs:3916-3919` records the grep behind\n// that). An unconditional `self.owner.locked = false` would therefore let a\n// stale guard clear `locked` while a *different* task is inside its critical\n// section. The flag supplies the missing state directly, so `release` and\n// `set` are both no-ops on a released guard -- see both below, and\n// `tests/runtime/sync_mutex_stale_guard_cannot_steal.nova` and\n// `sync_mutex_stale_guard_cannot_write.nova`, one fixture per use.\n//\n// Inert against mistakes, not against tampering, and the reason is one fact:\n// **Nova enforces no field privacy.** Every hole below follows from it.\n// `released` is an ordinary readable, writable field, so `g.released = false`\n// on a guard that was genuinely obtained and genuinely released resurrects it,\n// and its next `release` frees a lock a different task now holds -- measured.\n// That needs nothing but the stale guard itself, which is a *strictly weaker*\n// precondition than building `MutexGuard { owner: m, released: false }` from\n// scratch (also legal, also measured, and it needs a `Mutex<T>` to name). The\n// same fact is why `File { fd: 9999 }` is constructible\n// (`docs/adr/0012-file-descriptor-lifecycle.md`), with one difference worth\n// stating: a forged `fd` safely misses a table lookup, whereas a forged or\n// resurrected guard writes straight to the live mutex. Documented, not\n// enforced. `get` is not guarded by the flag at all, deliberately, and gives\n// its own reason below.\n/// The right to work on a locked `Mutex`\'s value, and the duty to release\n/// it.\n///\n/// `get` reads the value and `set` replaces it. Call `release` when done: the\n/// mutex stays locked until then. After `release`, `set` and a second\n/// `release` do nothing, though `get` still reads the value, unguarded.\npub record MutexGuard<T> { owner: Mutex<T>, released: Bool }\n\nimpl<T> Mutex<T> {\n    /// Returns an unlocked mutex holding `value`.\n    pub fn new(value: T) -> Mutex<T> { Mutex { locked: false, value: value } }\n\n    // Take the flag if it is free. Private: the two public entry points differ\n    // only in what they do when it is not, and returning a `Bool` is what lets\n    // `lock`\'s `while` condition read the result directly -- `!self.take()`\n    // rather than an `Option` the loop would have to inspect and discard.\n    //\n    // (An earlier version of this comment gave the reason as `Option` having\n    // \"no `unwrap`\". That is false: `Option::unwrap` is at\n    // `std/core/lib.nova:89` and std/core calls it six times, on five lines\n    // -- 490, 520, 538, 601 and 637, the last of which calls it twice.)\n    fn take(mut self) -> Bool {\n        if self.locked { return false }\n        self.locked = true\n        true\n    }\n\n    // Take the lock, or report that it is held. The caller that gets `Some`\n    // receives the guard it needs; the caller that gets `None` has nothing it\n    // could mistake for one.\n    /// Locks the mutex and returns a guard if it is free, or returns `None`\n    /// if it is locked, even by the calling task.\n    ///\n    /// It is not `async`, and never waits.\n    pub fn try_lock(mut self) -> Option<MutexGuard<T>> {\n        if self.take() { Some(MutexGuard { owner: self, released: false }) } else { None }\n    }\n\n    // Take the lock, yielding to every other runnable task until it is free.\n    //\n    // `while`, not `loop`: Nova has no `loop` keyword -- `loop {` parses as a\n    // record literal with `loop` as the type name. `yield_now` re-queues this\n    // task behind everything currently runnable, so the holder always gets a\n    // turn and can always release; which *waiter* wins afterwards is\n    // unspecified, and this mutex makes no fairness guarantee.\n    /// Locks the mutex, waiting while it is locked, and returns a guard.\n    ///\n    /// An `async fn`. If the mutex is free, it locks it without suspending;\n    /// otherwise the calling task yields to the other tasks and tries again\n    /// until the mutex is released. The wait keeps the processor busy, and\n    /// goes on forever, with no deadlock reported, if the mutex is never\n    /// released, as when the calling task already holds it.\n    ///\n    /// Two tasks adding to one total, each holding the lock across a\n    /// suspension:\n    ///\n    /// ```\n    /// async fn add(mut total: Mutex<Int>, n: Int) {\n    ///     let mut guard = total.lock().await\n    ///     let before = guard.get()\n    ///     yield_now().await // other tasks run here, but none can lock `total`\n    ///     guard.set(before + n)\n    ///     guard.release() // nothing releases a guard for you\n    /// }\n    ///\n    /// async fn main() {\n    ///     let mut total = Mutex::new(0)\n    ///     let a = spawn(add(total, 1))\n    ///     let b = spawn(add(total, 2))\n    ///     a.join().await\n    ///     b.join().await\n    ///     assert_eq(total.try_lock().unwrap().get(), 3)\n    /// }\n    /// ```\n    pub async fn lock(mut self) -> MutexGuard<T> {\n        while !self.take() { yield_now().await }\n        MutexGuard { owner: self, released: false }\n    }\n}\n\nimpl<T> MutexGuard<T> {\n    // The guarded value. Records and arrays are reference types, so mutating\n    // the returned value\'s fields or elements mutates what the mutex holds --\n    // which is the point. But a `T` with no assignable interior is *copied*\n    // out, and mutating that copy cannot reach the mutex: `Int`, `Bool`,\n    // `Float` and `Char` have no fields to assign, and `std/strings` has no\n    // in-place mutation at all (zero `mut self` methods in its nineteen\n    // `pub fn`s). Use `set` for those.\n    //\n    // Callable after `release`, and unlike `release` and `set` this\n    // deliberately does *not* return early on `released`. The guard now\n    // carries the state to detect the case, so the earlier reason -- that the\n    // language cannot prevent it -- is no longer the reason: the library could\n    // prevent it and chooses not to. There is nothing to return instead. `get`\n    // yields a `T`, `T` is generic, and no default exists to invent for it, so\n    // an early return has no value to hand back. The two alternatives are a\n    // signature change to `Option<T>`, which every caller would then unwrap,\n    // and a panic -- and a Nova panic aborts the process rather than unwinding,\n    // because nothing unwinds across a poll boundary. A stale read is also the\n    // harmless one of the three: it cannot corrupt the mutex, where a stale\n    // `set` or `release` writes to it. Documented, not enforced.\n    /// Returns the guarded value.\n    ///\n    /// A record or an array is shared rather than copied, so a change to its\n    /// fields or elements changes the value the mutex holds. A value with\n    /// nothing to change in place, such as an `Int`, `Float`, `Bool`, `Char`\n    /// or `String`, comes out as a copy: replace it with `set`. `get` still\n    /// answers after `release`, though the value is then unguarded.\n    pub fn get(self) -> T { self.owner.value }\n\n    // Replace the guarded value -- the other half of the pair `Vec<T>` already\n    // ships, `get` at `std/collections/lib.nova:117` with `set` at `:125`, and\n    // for the same reason: without it a `Mutex<Int>` is a read-only view.\n    //\n    // A no-op on a released guard, exactly as `release` is. A stale `set` is\n    // the worse of the two hazards `released` closes: a stale `release` frees\n    // a flag another task owns, but a stale `set` writes into the protected\n    // value while that task is inside its critical section. Pinned by\n    // `tests/runtime/sync_mutex_stale_guard_cannot_write.nova`; deleting the\n    // line below fails that fixture and no other.\n    /// Replaces the guarded value with `v`.\n    ///\n    /// Does nothing if the guard has been released.\n    pub fn set(mut self, v: T) {\n        if self.released { return }\n        self.owner.value = v\n    }\n\n    // Release the lock. Idempotent -- a second call is a no-op, and `released`\n    // makes that true rather than only stated: without it this body is an\n    // unconditional write that would free a lock the caller no longer owns.\n    /// Unlocks the mutex, so another task can lock it.\n    ///\n    /// Releasing the same guard again does nothing.\n    pub fn release(mut self) {\n        if self.released { return }\n        self.released = true\n        self.owner.locked = false\n    }\n}\n\n// ------------------------------------------------------------------ channel\n\n// A fixed-capacity FIFO over a flat array. Private: it exists only to give\n// `Channel` its buffer, and `std/collections` has no `Queue` to borrow --\n// `Vec::pop` takes from the end, which is LIFO.\n//\n// `head` is the index of the next value out; `len` is how many are live.\n// Capacity lives on `Channel` rather than here, because `data` starts empty\n// and so cannot report it until the first send allocates.\n//\n// INVARIANT: `head >= 0`, `len >= 0`, `cap >= 1`. Every `%` below therefore\n// has non-negative operands, which matters because Nova\'s `%` is a\n// TRUNCATING remainder -- `-7 % 3` is `-1`, not `2`. Any operation added\n// later that seeks backwards must re-establish this, or the wrap arithmetic\n// lands outside the array. `std/core/lib.nova:285` and\n// `std/collections/lib.nova:203` carry the same warning for hash bucketing.\nrecord Ring<T> { data: [T], head: Int, len: Int }\n\n/// A first-in, first-out queue of values of type `T` that holds at most a\n/// fixed number of them, from `channel`.\n///\n/// Values go in through a `Sender` and come out through a `Receiver`, in the\n/// order they were sent. `sender` and `receiver` make as many of each as are\n/// needed, all sharing the one channel. Once a sender closes it, no more\n/// values go in, but those already in it can still be received.\npub record Channel<T> { ring: Ring<T>, cap: Int, closed: Bool }\n\n// Two views onto one channel. Records are reference types, so both observe\n// every mutation; the split carries intent -- hand `tx` to producers and `rx`\n// to consumers -- and not enforcement. Nova enforces no field privacy, and\n// that is unenforceable rather than merely unenforced: a field\'s `vis`\n// survives the AST (`crates/nova-ast/src/item.rs:71`) and is dropped at\n// AST->HIR lowering (`crates/nova-hir/src/lib.rs:918-923`), so\n// `check_field_set` (`crates/nova-typeck/src/check.rs:6214-6272`) has no\n// visibility left to check. Three routes, of very unequal reach:\n//\n// - Forging `Sender { ch: c }` grants NOTHING. The literal needs a\n//   `Channel<T>` to name, and `sender()`/`receiver()` below are `pub` and\n//   take a plain `self`, so whoever can name the channel can already obtain\n//   both handles legitimately.\n// - `ch.closed = false` REOPENS a closed channel, from any legitimately-held\n//   handle, and destroys the one property that gives a consumer loop a\n//   termination condition: that `recv`\'s `None` means closed AND drained.\n//   `close` only ever sets the flag true, so no API path reaches this -- the\n//   field is simply ordinary. This is the strongest of the three.\n// - Writing `ch.ring.head` or `ch.ring.len` breaks the INVARIANT above that\n//   the truncating `%` depends on, and reaches it without ever naming the\n//   non-`pub` `Ring<T>`.\n//\n// Documented, not prevented, as the standing `MutexGuard` is -- and ranked\n// the same way, in `nova-spec/20-STDLIB.md` section 13, under the paragraph\n// beginning \"Neither flag reaches tampering\", where naming forgery alone\n// was corrected on 2026-08-21 for understating the reach. An earlier\n// version of this comment made exactly that mistake here; corrected\n// 2026-08-22. Cited by its opening words, and its LINE NUMBER IS\n// DELIBERATELY NOT RECORDED, because it moves: every number this citation\n// has ever carried has been wrong, including the one it was written with.\n// Grep the quoted phrase.\n/// The sending side of a `Channel`, from `Channel::sender`.\n///\n/// Every `Sender` of a channel sends into the same queue, and `close` on any\n/// of them closes the channel for all.\npub record Sender<T> { ch: Channel<T> }\n\n/// The receiving side of a `Channel`, from `Channel::receiver`.\n///\n/// Every `Receiver` of a channel takes from the same queue, so each value is\n/// received once, by whichever receiver takes it first.\npub record Receiver<T> { ch: Channel<T> }\n\n// A bounded channel holding at most `buffer` values.\n//\n// `buffer < 1` clamps to 1. A zero-capacity rendezvous channel needs a\n// handoff protocol that yield-and-retry cannot express without a second wait\n// state, and clamping follows `Int::pad`\'s precedent of an early return over\n// a panic.\n//\n// The call site must annotate: `let ch: Channel<Int> = channel(2)`. The type\n// parameter appears only in the return type, and Nova has no turbofish --\n// `channel<Int>(2)` is a parse error, read as two comparisons.\n/// Returns a new channel, open and empty, that holds at most `buffer` values.\n///\n/// A `buffer` below 1 counts as 1. Annotate the binding with the value type,\n/// as in `let ch: Channel<Int> = channel(8)`: nothing in the call names it.\n/// Room for all `buffer` values is allocated by the first send.\npub fn channel<T>(buffer: Int) -> Channel<T> {\n    let cap = if buffer < 1 { 1 } else { buffer }\n    Channel { ring: Ring { data: [], head: 0, len: 0 }, cap: cap, closed: false }\n}\n\nimpl<T> Channel<T> {\n    /// Returns a `Sender` for the channel.\n    pub fn sender(self) -> Sender<T> { Sender { ch: self } }\n\n    /// Returns a `Receiver` for the channel.\n    pub fn receiver(self) -> Receiver<T> { Receiver { ch: self } }\n\n    // Enqueue, or report refusal. Private: `Sender::try_send` is the public\n    // door. Returns false when closed and when full, which is why the async\n    // `send` must re-check `closed` itself to tell the two apart.\n    fn push(mut self, v: T) -> Bool {\n        if self.closed { return false }\n        if self.ring.len == self.cap { return false }\n        if self.ring.data.len() == 0 {\n            // First send. `[fill; n]` needs a value of `T` and a fresh\n            // channel has none, so allocation waits for one -- the same trick\n            // `Vec::push` uses to grow. Every slot holds `v` until\n            // overwritten, so the channel briefly retains up to `cap`\n            // references to the first value sent.\n            self.ring.data = [v; self.cap]\n            self.ring.len = 1\n            return true\n        }\n        self.ring.data[(self.ring.head + self.ring.len) % self.cap] = v\n        self.ring.len = self.ring.len + 1\n        true\n    }\n\n    // Dequeue, or None when empty. Says nothing about `closed` -- that\n    // distinction belongs to `Receiver::recv`.\n    fn pop(mut self) -> Option<T> {\n        if self.ring.len == 0 { return None }\n        let v = self.ring.data[self.ring.head]\n        self.ring.head = (self.ring.head + 1) % self.cap\n        self.ring.len = self.ring.len - 1\n        Some(v)\n    }\n}\n\nimpl<T> Sender<T> {\n    // Send if there is room. False when the channel is full OR closed; a\n    // caller that needs to distinguish them should use `send`, which returns\n    // false only for closed.\n    /// Sends `v` and returns `true` if the channel has room, or returns\n    /// `false` without sending it if the channel is full or closed.\n    ///\n    /// It is not `async`, and never waits. To tell a full channel from a\n    /// closed one, use `send`.\n    pub fn try_send(mut self, v: T) -> Bool { self.ch.push(v) }\n\n    // Refuse all further sends. Buffered values stay readable, so a producer\n    // may close the instant it stops producing and the consumer still drains.\n    // Idempotent: a second call sets an already-set flag.\n    /// Closes the channel: later sends, through any sender, return `false`.\n    ///\n    /// Values already in the channel stay there for the receivers, whose\n    /// `recv` returns `None` once they are all taken. Closing a closed channel\n    /// does nothing. Nothing closes a channel for you: close it when done\n    /// sending, or a receiver waiting in `recv` waits forever.\n    pub fn close(mut self) { self.ch.closed = true }\n\n    // Send, waiting while the channel is full. Returns false only when the\n    // channel is closed -- unlike `try_send`, which also returns false when\n    // merely full.\n    //\n    // Waiting is `yield_now`, not parking, which keeps this module in Nova\n    // with no executor change (ADR 0016 made the same call for `Mutex`). The\n    // cost is stated rather than hidden: a waiter stays *runnable*, so\n    // `report_deadlock` cannot see it, and a producer blocked on a channel\n    // nobody drains spins instead of being diagnosed.\n    /// Sends `v`, waiting while the channel is full, and returns `true` once\n    /// it is in, or `false`, without sending it, if the channel is closed.\n    ///\n    /// An `async fn`. If the channel has room, it sends without suspending;\n    /// otherwise the calling task yields to the other tasks and tries again\n    /// until there is room or the channel is closed. The wait keeps the\n    /// processor busy, and goes on forever, with no deadlock reported, if no\n    /// task ever takes a value out or closes the channel.\n    pub async fn send(mut self, v: T) -> Bool {\n        while !self.ch.push(v) {\n            if self.ch.closed { return false }\n            yield_now().await\n        }\n        true\n    }\n}\n\nimpl<T> Receiver<T> {\n    // Take a value if one is buffered. None means empty, and says nothing\n    // about whether more will arrive -- `recv` is what distinguishes those.\n    /// Takes the oldest value in the channel, or returns `None` if it is\n    /// empty, whether or not it is closed.\n    ///\n    /// It is not `async`, and never waits.\n    pub fn try_recv(mut self) -> Option<T> { self.ch.pop() }\n\n    // Take the next value, waiting while the channel is empty and open.\n    //\n    // None means closed AND drained, never merely empty. That is the whole\n    // reason `close` is explicit: `Drop` is specified in three spec files and\n    // implemented in none, so nothing closes a channel on the producer\'s\n    // behalf, and without this distinction a consumer loop could not\n    // terminate.\n    //\n    // Buffered values are drained before the close is reported, because `pop`\n    // is tried before `closed` is read on every iteration.\n    /// Takes the oldest value in the channel, waiting while it is empty and\n    /// open, or returns `None` once it is closed and empty.\n    ///\n    /// An `async fn`. If a value is there, it takes it without suspending;\n    /// otherwise the calling task yields to the other tasks and tries again.\n    /// The values still in a closed channel are received before `None`. The\n    /// wait keeps the processor busy, and goes on forever, with no deadlock\n    /// reported, if no task ever sends a value or closes the channel.\n    pub async fn recv(mut self) -> Option<T> {\n        let mut out = self.ch.pop()\n        while out.is_none() {\n            if self.ch.closed { return None }\n            yield_now().await\n            out = self.ch.pop()\n        }\n        out\n    }\n}\n";
Expand description

std/sync/lib.nova.