Module sync/mutex

sync/mutex
Stability: unstable — the closure-scoped shape (`with_lock` / `try_with_lock`, no guard type) is settled and concurrently tested (`tests/sync/mutex.test.yo`, 12/12), but ONE row of `plans/STD_API_STABILIZATION.md` §4 "Concurrency" is still open and it is on this surface: `_raw_lock` / `_raw_unlock` / `_raw_handle_ptr` are reachable from outside the module and are meant to be removed. They cannot go yet because `Cond.wait` takes the raw handle they hand out — `Mutex(T)` has no guard to pass instead — so freezing this module waits on a condvar API that couples to a `Mutex(T)` without exposing its primitive. There is also no poisoning: a thread that unwinds out of `body` releases the lock and leaves the data as it was, where Rust would mark the `Mutex` poisoned. That is a deliberate choice (Yo has no poisoned state anywhere), not an omission, and it is not what is holding the freeze. — stable modules only change additively; this one may still change.

Mutual exclusion lock primitives.

Phase D (THREAD_SAFETY): Mutex parameterized over the protected data. Access is granted via with_lock(closure) — the closure receives a inout(v): T that is second-class (cannot escape the closure scope). The private __MutexUnlocker handles unlock on both normal return and unwind via its Dispose impl.

Stability

unstable — the closure-scoped shape (with_lock / try_with_lock, no guard type) is settled and concurrently tested (tests/sync/mutex.test.yo, 12/12), but ONE row of plans/STD_API_STABILIZATION.md §4 "Concurrency" is still open and it is on this surface: _raw_lock / _raw_unlock / _raw_handle_ptr are reachable from outside the module and are meant to be removed. They cannot go yet because Cond.wait takes the raw handle they hand out — Mutex(T) has no guard to pass instead — so freezing this module waits on a condvar API that couples to a Mutex(T) without exposing its primitive. There is also no poisoning: a thread that unwinds out of body releases the lock and leaves the data as it was, where Rust would mark the Mutex poisoned. That is a deliberate choice (Yo has no poisoned state anywhere), not an omission, and it is not what is holding the freeze.

Types

__YO_THREAD_SYNC_TYPE

Trait Implementations

Mutex type-function
fn(T : Type) -> Type

A mutual-exclusion lock that OWNS the data it protects — Rust's Mutex<T>, minus the guard.

Access is granted only inside a closure: with_lock hands body an inout(v) : T that is second-class and cannot escape the closure, so there is no way to hold a reference to the protected value after the lock is released. That is the deliberate divergence from Rust, which returns a MutexGuard and relies on its lifetime; Yo has no lifetimes to lean on, so the scope is the closure body.

T must be Send (it crosses threads) and Acyclic (the handle is atomically reference-counted, and atomic RC is only sound for data that cannot form a cycle). The Acyclic impl below is what lets a Mutex(T) nest inside another sync container or an std/imm structure.

A Mutex(T) value is a SHARED HANDLE, which is why every method takes self : Self rather than inout(self): copying the handle (into a thread closure, say) bumps the atomic reference count and both copies name the same lock. The OS primitive is destroyed by the Dispose impl when the last handle goes away, so a mutex cannot outlive its own destruction — but it also is not destroyed while any thread still holds a handle.

There is no poisoning: if body unwinds, the lock is released and the data keeps whatever value it had at that moment.

Type Parameters

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Acyclic())
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), ...)
new : (fn(value : T) -> Self)

Wrap value in a new, unlocked mutex — Rust's Mutex::new.

value moves INTO the lock and is thereafter reachable only through with_lock / try_with_lock. Creating the OS primitive is the only allocation-time cost; there is no lazy initialization on first lock.

Returns: Self

with_lock : ( fn( generic(R : Type), self : Self, body : Impl(Fn(inout(v) : T) -> R) ) -> R )

Take the lock, run body on the protected value, release the lock, and return body's result — Rust's lock().unwrap() plus the guard's scope, in one call.

This blocks until the lock is available, parking the thread rather than spinning. body receives an inout(v) : T, so it may mutate the protected value in place; the mutation is visible to the next holder.

Release is structural, not by falling off the end: a private __MutexUnlocker is created inside the critical section and its Dispose impl unlocks, so an early return out of body or an unwind through this frame still releases the lock. Nothing marks the mutex poisoned in that case (see the type doc).

Re-entering the same mutex from the same thread is not portable. The POSIX primitive is a default pthread_mutex_t, so a second with_lock on a mutex this thread already holds deadlocks; on Windows the primitive is a CRITICAL_SECTION, which is recursive and lets the call through. Do not rely on either — nest different mutexes, not the same one.

Returns: R

try_with_lock : ( fn( generic(R : Type), self : Self, body : Impl(Fn(inout(v) : T) -> R) ) -> Option(R) )

Run body under the lock IF it can be taken without waiting, and return .Some(result); .None when another thread holds it.

The closure-scoped counterpart of Rust's try_lock. Rust hands back a Result<MutexGuard, TryLockError>; Mutex(T) deliberately has no guard type — access is only ever granted inside a closure — so the "did I get it" answer is the Option instead. body not running IS the failure case, which is why the result is optional rather than the lock state being reported separately: there is no way to observe "not acquired" and still have a value.

Release is by the same __MutexUnlocker with_lock uses, so an early return or an unwind out of body still unlocks.

Do not spin on this. A while(!took, ...) loop around try_with_lock is a busy-wait that starves the holder on a single core; with_lock parks instead. try_with_lock is for the case where there is other useful work to do when the lock is busy.

One platform note: a Windows CRITICAL_SECTION is RECURSIVE, so a thread that ALREADY holds this mutex gets .Some here, where a POSIX mutex gives .None. Do not use the answer to detect self-reentrancy.

Returns: Option(R)

is_unlocked : (fn(self : Self) -> bool)

True when the mutex is NOT held right now.

Advisory only, and racy by construction: it takes the lock to find out and releases it again, so the answer describes a moment that has already passed. Never branch on this to decide whether a later with_lock will block — that is a TOCTOU bug. It exists for assertions and diagnostics, which is why it is spelled as a question about the past rather than as can_lock.

Returns: bool

_raw_lock : (fn(self : Self) -> unit)

Returns: unit

_raw_unlock : (fn(self : Self) -> unit)

Returns: unit

_raw_handle_ptr : (fn(inout(self) : Self) -> *__YO_THREAD_SYNC_TYPE)

Returns: *__YO_THREAD_SYNC_TYPE

impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Dispose(...))
dispose : (fn(self : Self) -> unit)

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Returns: unit