Source src/silo:std.sync
1##! Synchronisation primitives — `Mutex`, `RwLock`, `Atomic`, `Lazy`, `Barrier`, `Condvar`, `OnceLock`, and their guards. 2##! 3##! The memory-ordering enum is named `MemoryOrdering` rather than the 4##! spec's `Ordering` to avoid colliding with `core.Ordering`. 5##! `Atomic` has a full set of real bodies; `Mutex`, `RwLock`, 6##! `OnceLock`, `Barrier`, and `Condvar` cover the core operations. 7##! `Lazy`, `Condvar .wait`, `RwLock .try-read` / `.try-write`, and 8##! `OnceLock .get-or-init` remain stubbed pending quotation-dispatch 9##! from intrinsics and `:dispose` runtime wiring. 10##! 11##! The guard types should be `local` so the locality check at 12##! `.spawn` prevents capture, but the checker today rejects `local` 13##! types in return positions. In the meantime guards are plain 14##! by-value records; their `:dispose` impls (lock release on drop) 15##! land with the dispose runtime. `.lock` / `.read` / `.write` 16##! currently thread the lock handle back alongside the guard — the 17##! spec-accurate signature that consumes the lock returns with RAII 18##! guards. Only scalar values (`Int` / `Float` / `Decimal` / `Bool` / 19##! `Char` / `Unit`) may be stored inside `Mutex` / `RwLock` / 20##! `OnceLock`; the intrinsics return an error for cell-typed values. 21##! The `+Executor` effect row is elided from every signature. 22##! 23##! Not re-exported by the prelude — use 24##! `:use :open silo:std.sync ...`. 25 26:use 27 :open core AnyUInt Option Result 28 :open effects.panic panic 29:end 30 31# si[impl sync.atomic] 32## Memory-ordering tag for atomic operations. Models the Rust/C++20 33## acquire-release family. Every atomic op MUST take an explicit 34## `MemoryOrdering` argument; defaults MUST NOT be inferred. 35## Renamed from spec's `Ordering` to avoid colliding with 36## `core.Ordering` (`Less` / `Equal` / `Greater`). 37:union(pub) MemoryOrdering 38 | Relaxed 39 | Acquire 40 | Release 41 | AcqRel 42 | SeqCst 43:end 44 45# si[impl sync.mutex] 46## Exclusive lock guarding a value of type `val`. The spec's `.lock` 47## returns a `MutexGuard` that dereferences to a unique borrow of `val` 48## and releases on drop. 49:record(pub) Mutex val 50 .value val 51:end 52 53# si[impl sync.mutex.guard] 54## Linear guard that MUST release the underlying lock on drop. The spec 55## declares this `local` so the locality check at `.spawn` prevents 56## capture; that attribute is omitted here because the current checker 57## rejects `local` types in return positions (see divergence #1 above). 58## The spec-mandated `:dispose` impl lives with the real lock/unlock 59## implementation. 60:record(pub) MutexGuard val 61 .value val 62:end 63 64# si[impl sync.mutex] 65## Construct a `Mutex` wrapping an initial value. Scalar `val`s only in 66## the WP-F1-real MVP — cells would need RC interaction on store, which 67## is not modelled yet (see the module header for the full list). 68@host 69:fn(pub) mutex-new ( val -> (Mutex val) ) :end 70 71:impl (Mutex val) 72 # si[impl sync.mutex] 73 ## Acquire the underlying Rust mutex transiently, clone the scalar out, 74 ## release. Diverges from the spec signature by threading the `Mutex` 75 ## handle back through — Silo does not yet enforce `local`/`:dispose` 76 ## on `MutexGuard`, so each `.lock` call returns both the handle and 77 ## the guard record for chaining. 78 @host 79 .mutex-lock ( (Mutex val) -> (Mutex val) (MutexGuard val) ) ; 80 81 # si[impl sync.mutex] 82 ## Non-blocking variant of `.mutex-lock`. Returns `None` if the 83 ## underlying Rust mutex is currently held by another holder. 84 @host 85 .mutex-try-lock ( (Mutex val) -> (Mutex val) (Option (MutexGuard val)) ) ; 86:end 87 88# si[impl sync.rwlock] 89## Reader-writer lock guarding a value. Multiple concurrent `ReadGuard`s 90## are permitted; `WriteGuard` is exclusive. 91:record(pub) RwLock val 92 .value val 93:end 94 95# si[impl sync.rwlock] 96## Shared read guard. Spec declares this `local`; omitted per 97## divergence #1. Release happens via `:dispose`. 98:record(pub) ReadGuard val 99 .value val 100:end 101 102# si[impl sync.rwlock] 103## Exclusive write guard. Spec declares this `local`; omitted per 104## divergence #1. Release happens via `:dispose`. 105:record(pub) WriteGuard val 106 .value val 107:end 108 109# si[impl sync.rwlock] 110## Construct a `RwLock` wrapping an initial value. Scalar-only per the 111## module-header simplification. 112@host 113:fn(pub) rwlock-new ( val -> (RwLock val) ) :end 114 115:impl (RwLock val) 116 # si[impl sync.rwlock] 117 ## Acquire shared-read access transiently, clone the scalar, release. 118 ## Diverges from the spec signature by threading the `RwLock` handle 119 ## back through alongside the guard record (see Mutex note above). 120 @host 121 .rwlock-read ( (RwLock val) -> (RwLock val) (ReadGuard val) ) ; 122 123 # si[impl sync.rwlock] 124 ## Acquire exclusive write access transiently, clone the scalar, 125 ## release. Diverges from the spec signature identically to 126 ## `.rwlock-read`. 127 @host 128 .rwlock-write ( (RwLock val) -> (RwLock val) (WriteGuard val) ) ; 129 130 # si[impl sync.rwlock] 131 ## Non-blocking read. Returns `None` if a writer is active. Kept as a 132 ## stub — needs the real Rust try-variant and is not covered by the 133 ## WP-F1 real MVP. 134 .try-read ( (RwLock val) -> (Option (ReadGuard val)) ) 135 drop "RwLock .try-read not implemented yet — awaits full RwLock surface" panic :ret ; 136 137 # si[impl sync.rwlock] 138 ## Non-blocking write. Returns `None` if any reader or writer is 139 ## active. Kept as a stub for the same reason as `.try-read`. 140 .try-write ( (RwLock val) -> (Option (WriteGuard val)) ) 141 drop "RwLock .try-write not implemented yet — awaits full RwLock surface" panic :ret ; 142:end 143 144# si[impl sync.atomic] 145## Atomic cell over a word-sized primitive type. All ops take an 146## explicit `MemoryOrdering`. The WP-F1 followup wires the real host 147## intrinsics on top of Rust's `std::sync::atomic::AtomicI64` — see 148## `src/frontend/intrinsics/sync.rs`. Two caller-visible simplifications 149## documented there: 150## 1. Every `Atomic t` is backed by an `AtomicI64` at runtime, 151## regardless of `t`. Fine for any Silo integer scalar. 152## 2. Every op uses `SeqCst` ordering regardless of the 153## `MemoryOrdering` argument the caller supplies. The argument is 154## still required so the checker-visible signature matches the 155## spec; it is consumed-and-ignored by the host. 156:record(pub) Atomic t 157 .value t 158:end 159 160# si[impl sync.atomic] 161## Construct an `Atomic` wrapping an initial value. The value is stored 162## inside a shared `AtomicI64` by the host; cloning the returned handle 163## shares the same underlying atomic. 164@host 165:fn(pub) atomic-new ( t -> (Atomic t) ) :end 166 167:impl (Atomic t) 168 # si[impl sync.atomic] 169 ## Atomic load. Returns the atomic threaded through plus the current value. 170 @host 171 .atomic-load ( (Atomic t) MemoryOrdering -> (Atomic t) t ) ; 172 173 # si[impl sync.atomic] 174 ## Atomic store. Returns the atomic threaded through. 175 @host 176 .atomic-store ( (Atomic t) t MemoryOrdering -> (Atomic t) ) ; 177 178 # si[impl sync.atomic] 179 ## Atomic swap. Returns the atomic threaded through plus the previous value. 180 @host 181 .atomic-swap ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 182 183 # si[impl sync.atomic] 184 ## Atomic compare-and-swap. Takes expected, new, success ordering, failure 185 ## ordering; returns `Ok prev` on success, `Err current` on mismatch. 186 @host 187 .atomic-cas ( (Atomic t) t t MemoryOrdering MemoryOrdering -> (Atomic t) (Result t t) ) ; 188 189 # si[impl sync.atomic] 190 ## Atomic fetch-add. Returns the atomic plus the previous value. 191 @host 192 .atomic-fetch-add ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 193 194 # si[impl sync.atomic] 195 ## Atomic fetch-sub. Returns the atomic plus the previous value. 196 @host 197 .atomic-fetch-sub ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 198 199 # si[impl sync.atomic] 200 ## Atomic fetch-and. Returns the atomic plus the previous value. 201 @host 202 .atomic-fetch-and ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 203 204 # si[impl sync.atomic] 205 ## Atomic fetch-or. Returns the atomic plus the previous value. 206 @host 207 .atomic-fetch-or ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 208 209 # si[impl sync.atomic] 210 ## Atomic fetch-xor. Returns the atomic plus the previous value. 211 @host 212 .atomic-fetch-xor ( (Atomic t) t MemoryOrdering -> (Atomic t) t ) ; 213:end 214 215# si[impl sync.lazy] 216## One-shot initialisation cell. The spec's constructor accepts a nullary 217## quotation computing the value; `.force` runs it on first call and 218## caches the result. 219## 220## WP-F1 followup deferral: `Lazy` stays stubbed because invoking the 221## stored initializer quotation from inside a host intrinsic needs 222## machine-instruction support that hasn't landed yet. The same 223## limitation applies to `OnceLock .get-or-init` (see below). Host 224## intrinsics only receive `(stack, arena, symbols)` — running a 225## quotation requires the full VM execute loop to re-enter. The 226## +Executor effect WP will wire compound instructions (mirroring 227## `RangeFold` in `src/machine.rs`) that let host code drive a 228## quotation; `Lazy` and `OnceLock .get-or-init` land with it. 229:record(pub) Lazy val 230 .value val 231:end 232 233# si[impl sync.lazy] 234## Construct a `Lazy` from a nullary quotation. Runs the quotation at 235## most once across concurrent callers. 236:fn(pub) lazy-new ( [ -> val ] -> (Lazy val) ) 237 drop "lazy-new not implemented yet — intrinsic cannot re-enter VM to run a quotation; awaits +Executor compound instructions" panic 238:end 239 240:impl (Lazy val) 241 # si[impl sync.lazy] 242 ## Force the lazy — runs the initializer on first call, returns the 243 ## cached value on every subsequent call. Spec signature carries 244 ## `+Executor`; omitted for the partial stub. 245 .force ( (Lazy val) -> (Lazy val) val ) 246 drop "Lazy .force not implemented yet — intrinsic cannot re-enter VM to run a quotation; awaits +Executor compound instructions" panic :ret ; 247:end 248 249# si[impl sync.condvar.type] 250## Condition variable paired with a `Mutex`. Opaque handle — the field 251## `.id` holds a host-managed identifier. Spec declares `unique`; the 252## attribute is dropped here alongside the guard-type locality caveat 253## (divergence #1) to keep the WP-F1 partial compile-clean. 254:record(pub) Condvar 255 .id AnyUInt 256:end 257 258# si[impl sync.condvar.type] 259## Construct a fresh `Condvar` backed by a shared 260## `Arc<std::sync::Condvar>`. Cloning the returned handle shares the 261## same underlying condvar — compatible with the "shared across spawn 262## boundaries" locality rule. 263@host 264:fn(pub) condvar-new ( -> Condvar ) :end 265 266:impl Condvar 267 # si[impl sync.condvar.wait] 268 ## Atomically release the paired guard, block until woken, then 269 ## re-acquire the guard before returning. Spec signature carries 270 ## `+Executor`; omitted for the partial stub. Stays stubbed in the 271 ## WP-F1 followup because Rust's `Condvar::wait` needs a live 272 ## `MutexGuard`, and the guard record currently wraps a by-value 273 ## copy of the locked scalar rather than a real RAII borrow (see 274 ## sync module header divergence #3). Lands with the :dispose 275 ## runtime wiring. 276 .wait ( Condvar (MutexGuard a) -> Condvar (MutexGuard a) ) 277 drop drop "Condvar .wait not implemented yet — awaits :dispose runtime wiring so MutexGuard can carry a live lock" panic :ret ; 278 279 # si[impl sync.condvar.notify-one] 280 ## Wake at most one task currently blocked in `.wait`. With zero 281 ## waiters this is a no-op. Renamed from spec-exact `.notify-one` 282 ## to `.condvar-notify-one` to avoid colliding with a future trait 283 ## method of the same name (matches `.mutex-lock` / `.barrier-wait` 284 ## pattern — see sync.si divergence #4). 285 @host 286 .condvar-notify-one ( Condvar -> Condvar ) ; 287 288 # si[impl sync.condvar.notify-all] 289 ## Wake every task currently blocked in `.wait` on this `Condvar`. 290 ## Renamed from `.notify-all` for the same reason as 291 ## `.condvar-notify-one`. 292 @host 293 .condvar-notify-all ( Condvar -> Condvar ) ; 294:end 295 296# si[impl sync.oncelock.type] 297## One-shot cell distinct from `Lazy`. Constructable empty and 298## initialised by the first `.get-or-init` call. Once set, the value 299## MUST NOT change. Spec declares `unique`; dropped per divergence #1. 300:record(pub) OnceLock a 301 .value (Option a) 302:end 303 304# si[impl sync.oncelock.type] 305## Construct an empty `OnceLock`. Scalar-only per the module-header 306## simplification. 307@host 308:fn(pub) oncelock-new ( -> (OnceLock a) ) :end 309 310:impl (OnceLock a) 311 # si[impl sync.oncelock.get-or-init] 312 ## Atomically initialise on first call or return the cached value. 313 ## Still stubbed — invoking the initializer quotation needs special 314 ## machine-instruction support that lands with the `+Executor` effect. 315 ## Callers that already have an evaluated value can use `.oncelock-set` 316 ## below as a manual-seed building block in the meantime. 317 .get-or-init ( (OnceLock a) [ -> a ] -> (OnceLock a) a ) 318 :todo "awaits +Executor and quotation-dispatch support; use .oncelock-set for pre-evaluated values" ; 319 320 # si[impl sync.oncelock.get] 321 ## Return `Some` if the cell has been initialised, `None` otherwise. 322 ## Never blocks and never triggers initialisation. 323 @host 324 .oncelock-get ( (OnceLock a) -> (OnceLock a) (Option a) ) ; 325:end 326 327# si[impl sync.oncelock.get-or-init] 328## Set the cell's value if still empty. Returns `true` if this call 329## performed the initialisation, `false` if the cell was already set. 330## This is the host-level building block; the spec's `.get-or-init` 331## (with an initializer quotation) needs the `+Executor` effect and 332## quotation-dispatch support that hasn't landed yet. 333## 334## Kept as a free fn for now: making this a method on `(OnceLock a)` 335## breaks `.oncelock-get` dispatch during stdlib bootstrap in a way 336## not yet fully understood. WP-M1 revisit when the bare-impl 337## dispatch quirks are fully debugged. 338@host 339:fn(pub) oncelock-set ( (OnceLock a) a -> (OnceLock a) Bool ) :end 340 341# si[impl sync.barrier] 342## Rendezvous point for `n` participants. `.wait` blocks until all `n` 343## participants have arrived. 344:record(pub) Barrier 345 .count AnyUInt 346:end 347 348# si[impl sync.barrier] 349## Construct a `Barrier` expecting the given participant count. In the 350## WP-F1-real MVP `0` is coerced to `1` (a zero-count barrier would never 351## release); for multi-thread rendezvous pass the real participant count. 352@host 353:fn(pub) barrier-new ( AnyUInt -> Barrier ) :end 354 355# si[impl sync.no-poison] 356# si[impl sync.modes+1] 357## The no-poison policy and the locality-on-guards rule are file-level 358## invariants, enforced by the signatures above: every lock method 359## returns its guard (or `(Option guard)`) directly instead of 360## `(Result guard PoisonError)`, and the guard types are declared with 361## the default global locality (the spec's `local` attribute lands when 362## the checker allows `local` return types — see divergence #1). 363:impl Barrier 364 # si[impl sync.barrier] 365 ## Block the calling OS thread until `n` participants have called 366 ## `.wait`. In a single-task Silo program with a barrier constructed 367 ## by `barrier-new` (which coerces 0 to 1), the barrier is always 368 ## already full so this call returns immediately. 369 @host 370 .barrier-wait ( Barrier -> Barrier ) ; 371:end