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.