silo:std.sync

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