Cellular Mutability
Regional mutability separates construction from use: build a graph while it is flexible, then freeze it into immutable data. Reussir’s second mutation model is cellular mutability. A cell keeps one mutable value behind a stable reference, and that value may change throughout the cell’s lifetime. Every alias to the cell observes the same slot.
At the language level, a cell is always a heap-allocated, reference-counted object. It is analogous to a reference cell in Koka, OCaml’s refref, or Haskell’s IORefIORef and STRefSTRef. It is not an inline mutable field. A record may store a cell handle, but projecting that member produces a reference to the same separately managed cell object.
Cell types and operations are built into the corecore package. Import their single operation namespace:
import core::intrinsic::cell;fn answer() -> i64 { let counter = cell::alloc(0); let alias = counter; cell::set(alias, 41); cell::get(counter) + 1}
import core::intrinsic::cell;fn answer() -> i64 { let counter = cell::alloc(0); let alias = counter; cell::set(alias, 41); cell::get(counter) + 1}
Without a type argument, cell::alloc(value)cell::alloc(value) creates a Cell<T>Cell<T>, where TT is inferred from valuevalue and its context. countercounter and aliasalias are two handles to one heap object, so the write through aliasalias is visible through countercounter. The operation borrows its cell argument: using a handle in getget or setset does not consume the cell itself.
To select another cell kind, give allocalloc the complete cell type, not merely the element type:
let guarded = cell::alloc<RefCell<i64>>(0);let atomic = cell::alloc<Atomic<i64>>(0);let locked = cell::alloc<Mutex<i64>>(0);
let guarded = cell::alloc<RefCell<i64>>(0);let atomic = cell::alloc<Atomic<i64>>(0);let locked = cell::alloc<Mutex<i64>>(0);
All six cell kinds use the same intrinsic namespace. The operand’s type decides which implementation and synchronization discipline is used:
cell::alloc(value)cell::alloc(value)movesvaluevalueinto a new cell. An explicit cell-type argument selects its kind.cell::get(cell)cell::get(cell)borrows the handle and returns an owned clone of the stored value. For an RC-managed element, this means retaining the element; for a scalar, it is an ordinary value load.cell::set(cell, replacement)cell::set(cell, replacement)borrows the handle, consumesreplacementreplacement, drops the old element, and installs the replacement.cell::rmw(cell, update)cell::rmw(cell, update)moves the element through anupdate: T -> Tupdate: T -> Tclosure and stores the returned replacement. It returnsunitunitand is available onRefCellRefCell,MutexMutex,FlatLockFlatLock, andRwLockRwLock.cell::in_use(cell)cell::in_use(cell)reports whether aRefCellRefCellelement is currently moved into an activermwrmwcallback. It is available only onRefCellRefCell.cell::rdlock(cell, reader)cell::rdlock(cell, reader)runsreader: T -> Rreader: T -> Rover a clone of anRwLockRwLockelement while holding a shared read lock and returnsRR. It is available only onRwLockRwLock.
Immutable inductive construction cannot make an older object point back to a newer one. A cell removes that restriction, so it can also recreate the cyclic RC problem that regional freezing solves.
Here a node owns a handle to linklink, then linklink is changed to own the node:
import core::intrinsic::cell;struct CycleNode { value: i64, next: Cell<Nullable<CycleNode>>}fn read_cycle_then_break_it() -> i64 { let link = cell::alloc(Nullable::Null); let node = CycleNode { value: 42, next: link }; // node -> link -> node cell::set(link, Nullable::NonNull { node }); let result = match cell::get(link) { Nullable::NonNull(found) => found.value, Nullable::Null => 0 }; // Remove link -> node while an outside handle can still reach the cycle. cell::set(link, Nullable::Null); result}
import core::intrinsic::cell;struct CycleNode { value: i64, next: Cell<Nullable<CycleNode>>}fn read_cycle_then_break_it() -> i64 { let link = cell::alloc(Nullable::Null); let node = CycleNode { value: 42, next: link }; // node -> link -> node cell::set(link, Nullable::NonNull { node }); let result = match cell::get(link) { Nullable::NonNull(found) => found.value, Nullable::Null => 0 }; // Remove link -> node while an outside handle can still reach the cycle. cell::set(link, Nullable::Null); result}
If the final setset is omitted, dropping the local variables cannot reclaim the cycle. nodenode keeps linklink alive, and linklink keeps nodenode alive, so neither reference count reaches zero. Reussir cells are not traced by a cycle collector.
Sometimes that is deliberate: a cyclic service graph intended to remain alive until process exit may not need teardown. Otherwise, the program must break enough cell-backed edges while it still has an outside handle. Once every outside handle is gone, an unreachable RC cycle can no longer be reached to be broken manually. Prefer regional construction when a cyclic graph naturally has a build-then-read lifetime.
Calling getget, calculating a replacement, and then calling setset works, but getget first creates another owned reference to a managed element. rmwrmw avoids that extra ownership operation. It temporarily moves the exact value out of the cell, gives ownership of it to the updater, and moves the updater’s result back into the slot:
import core::intrinsic::cell;struct Counter { value: i64}fn increment(counter: RefCell<Counter>) { cell::rmw(counter, |old| { Counter { value: old.value + 1 } });}
import core::intrinsic::cell;struct Counter { value: i64}fn increment(counter: RefCell<Counter>) { cell::rmw(counter, |old| { Counter { value: old.value + 1 } });}
The ownership sequence is precise:
rmwrmwborrows theRefCellRefCellhandle; it does not consume the cell.- The stored
CounterCounterreference is moved intooldoldwithout a retain or a clone. TheRefCellRefCellmarks its slot as in use while it is empty. - The updater consumes
oldoldand returns an owned replacement. - That replacement is moved into the slot, and the in-use mark is cleared.
When the cell’s slot held the only live copy of the CounterCounter reference, oldold is unique. Reussir’s reuse analysis can then reuse its allocation for Counter { ... }Counter { ... }, turning the functional reconstruction into an in-place update. If another reference to the old CounterCounter is still live—for example, a snapshot previously returned by getget—the old value is not unique. The compiler preserves that snapshot and constructs separate storage for the new value instead. In this context, “only copy” means the only live owned reference, not a byte-for-byte copy of the record.
MutexMutex, FlatLockFlatLock, and RwLockRwLock use the same move-out/move-back ownership contract for rmwrmw, but perform it inside their respective exclusive critical sections. The updater closure itself is consumed by the call.
A plain CellCell has no exclusive guard protecting the temporarily empty slot. Trying to use the guarded operation is rejected:
import core::intrinsic::cell;fn update(cell_value: Cell<i64>) { cell::rmw(cell_value, |value| value + 1);}
import core::intrinsic::cell;fn update(cell_value: Cell<i64>) { cell::rmw(cell_value, |value| value + 1);}
Error: `core::intrinsic::cell::rmw` is not supported on `Cell<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`) ╭─[ cellular-bad-plain-rmw.rr:4:5 ] │ 4 │ cell::rmw(cell_value, |value| value + 1); │ ────────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rmw` is not supported on `Cell<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`)───╯
Error: `core::intrinsic::cell::rmw` is not supported on `Cell<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`) ╭─[ cellular-bad-plain-rmw.rr:4:5 ] │ 4 │ cell::rmw(cell_value, |value| value + 1); │ ────────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rmw` is not supported on `Cell<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`)───╯
Use getget and setset when a plain cell is sufficient, or choose RefCell<T>RefCell<T> when a managed value needs the ownership-preserving update path.
During RefCell::rmwRefCell::rmw, a second getget, setset, or rmwrmw through an alias of the same cell would observe an empty slot. Reussir detects that reentrant access at runtime and panics. cell::in_usecell::in_use exposes the guard state when code needs to query it; outside an rmwrmw callback it is false. This is a dynamic single-threaded exclusivity check, not synchronization between threads.
Four cell kinds carry synchronization as part of their own type and cell box: AtomicAtomic, MutexMutex, FlatLockFlatLock, and RwLockRwLock. They do not need—and cannot be made safe merely by adding—an ArcArc around a plain cell. The Synchronization chapter develops Reussir’s broader thread-safety rules; here the important part is how each cell protects its slot.
Atomic<T>Atomic<T> provides atomic getget and setset for integer and floating-point primitives. A read uses acquire ordering and a write uses release ordering. Integer widths must be powers of two and at least eight bits. boolbool, charchar, records, and other aggregate types are not valid atomic-cell elements:
struct Data { value: i64 }fn invalid(cell_value: Atomic<Data>) {}
struct Data { value: i64 }fn invalid(cell_value: Atomic<Data>) {}
Error: `Atomic<Data>` is ill-formed: the element is not an integer or floating-point primitive ╭─[ cellular-bad-atomic-element.rr:3:1 ] │ 3 │ ╭─▶ fn invalid(cell_value: Atomic<Data>) { 4 │ ├─▶ } │ │ │ ╰─────── `Atomic<Data>` is ill-formed: the element is not an integer or floating-point primitive───╯
Error: `Atomic<Data>` is ill-formed: the element is not an integer or floating-point primitive ╭─[ cellular-bad-atomic-element.rr:3:1 ] │ 3 │ ╭─▶ fn invalid(cell_value: Atomic<Data>) { 4 │ ├─▶ } │ │ │ ╰─────── `Atomic<Data>` is ill-formed: the element is not an integer or floating-point primitive───╯
The current source-level cell API does not expose the closure form of rmwrmw on an AtomicAtomic cell. A closure is not a retryable primitive atomic operation, so the compiler rejects it:
import core::intrinsic::cell;fn update(cell_value: Atomic<i64>) { cell::rmw(cell_value, |value| value + 1);}
import core::intrinsic::cell;fn update(cell_value: Atomic<i64>) { cell::rmw(cell_value, |value| value + 1);}
Error: `core::intrinsic::cell::rmw` is not supported on `Atomic<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`) ╭─[ cellular-bad-atomic-rmw.rr:4:5 ] │ 4 │ cell::rmw(cell_value, |value| value + 1); │ ────────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rmw` is not supported on `Atomic<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`)───╯
Error: `core::intrinsic::cell::rmw` is not supported on `Atomic<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`) ╭─[ cellular-bad-atomic-rmw.rr:4:5 ] │ 4 │ cell::rmw(cell_value, |value| value + 1); │ ────────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rmw` is not supported on `Atomic<i64>`; it requires an exclusive or lock-guarded cell (`RefCell`, `Mutex`, `FlatLock`, or `RwLock`)───╯
For now, use getget and setset for an Atomic<T>Atomic<T> from Reussir source, or select a lock-backed cell when an update must be one indivisible transaction.
Mutex<T>Mutex<T> serializes getget, setset, and rmwrmw with one exclusive lock. The stored element must itself satisfy Reussir’s SyncSync rules and have a shape that can occupy a lock slot: a suitable primitive or an RC pointer. In particular, a bare shared record uses a non-atomic reference count and cannot leave the critical section as a cloned value:
struct Data { value: i64 }fn invalid(cell_value: Mutex<Data>) {}
struct Data { value: i64 }fn invalid(cell_value: Mutex<Data>) {}
Error: `Mutex<Data>` is ill-formed: it is a plain `[shared]` rc box whose refcount is not atomic; the element of a lock-guarded cell must be `Sync` — the lock protects the stored slot, while clones of the element leave the critical section ╭─[ cellular-bad-mutex-element.rr:3:1 ] │ 3 │ ╭─▶ fn invalid(cell_value: Mutex<Data>) { 4 │ ├─▶ } │ │ │ ╰─────── `Mutex<Data>` is ill-formed: it is a plain `[shared]` rc box whose refcount is not atomic; the element of a lock-guarded cell must be `Sync` — the lock protects the stored slot, while clones of the element leave the critical section───╯
Error: `Mutex<Data>` is ill-formed: it is a plain `[shared]` rc box whose refcount is not atomic; the element of a lock-guarded cell must be `Sync` — the lock protects the stored slot, while clones of the element leave the critical section ╭─[ cellular-bad-mutex-element.rr:3:1 ] │ 3 │ ╭─▶ fn invalid(cell_value: Mutex<Data>) { 4 │ ├─▶ } │ │ │ ╰─────── `Mutex<Data>` is ill-formed: it is a plain `[shared]` rc box whose refcount is not atomic; the element of a lock-guarded cell must be `Sync` — the lock protects the stored slot, while clones of the element leave the critical section───╯
Color the record with ArcArc when it must cross synchronized access:
import core::intrinsic::cell;struct Data { value: i64 }fn replace(data: Arc<Data>) -> Mutex<Arc<Data>> { cell::alloc<Mutex<Arc<Data>>>(data)}
import core::intrinsic::cell;struct Data { value: i64 }fn replace(data: Arc<Data>) -> Mutex<Arc<Data>> { cell::alloc<Mutex<Arc<Data>>>(data)}
getget clones the protected element while holding the lock, but the returned clone lives after the critical section. That is why protecting the slot alone is insufficient: the element’s own ownership operations must also be safe.
FlatLock<T>FlatLock<T> has the same source operations and element restrictions as Mutex<T>Mutex<T>, but uses a flat-combining lock. Under contention, callers publish small operation requests. A thread that becomes the combiner acquires the lock and executes a batch of pending requests on behalf of their callers, amortizing lock hand-offs and movement of the contended cache line.
import core::intrinsic::cell;fn increment(counter: FlatLock<i64>) { cell::rmw(counter, |value| value + 1);}
import core::intrinsic::cell;fn increment(counter: FlatLock<i64>) { cell::rmw(counter, |value| value + 1);}
The updater runs exactly once, but it may run on the combining thread rather than the caller’s thread. Conceptually, the closure is an operation submitted to the lock; it should not depend on the identity of the calling thread. Whether flat combining is preferable to a mutex depends on contention and the size of each protected operation.
RwLock<T>RwLock<T> permits concurrent readers and exclusive writers. getget takes a read lock long enough to clone the current element; setset and rmwrmw take the write lock. Use rdlockrdlock when a complete read callback should remain inside one shared read critical section:
import core::intrinsic::cell;struct Data { value: i64 }fn read_value(cell_value: RwLock<Arc<Data>>) -> i64 { cell::rdlock(cell_value, |data| data.value)}
import core::intrinsic::cell;struct Data { value: i64 }fn read_value(cell_value: RwLock<Arc<Data>>) -> i64 { cell::rdlock(cell_value, |data| data.value)}
rdlockrdlock is specific to RwLockRwLock. Applying it to another lock kind gives a direct diagnostic:
import core::intrinsic::cell;fn read(cell_value: Mutex<i64>) -> i64 { cell::rdlock(cell_value, |value| value)}
import core::intrinsic::cell;fn read(cell_value: Mutex<i64>) -> i64 { cell::rdlock(cell_value, |value| value)}
Error: `core::intrinsic::cell::rdlock` is not supported on `Mutex<i64>`; it requires an `RwLock` cell ╭─[ cellular-bad-rdlock.rr:4:5 ] │ 4 │ cell::rdlock(cell_value, |value| value) │ ───────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rdlock` is not supported on `Mutex<i64>`; it requires an `RwLock` cell───╯
Error: `core::intrinsic::cell::rdlock` is not supported on `Mutex<i64>`; it requires an `RwLock` cell ╭─[ cellular-bad-rdlock.rr:4:5 ] │ 4 │ cell::rdlock(cell_value, |value| value) │ ───────────────────┬─────────────────── │ ╰───────────────────── `core::intrinsic::cell::rdlock` is not supported on `Mutex<i64>`; it requires an `RwLock` cell───╯
| Type | Thread discipline | Operations | Element and intended use |
|---|---|---|---|
Cell<T>Cell<T> |
Unsynchronized | allocalloc, getget, setset |
No additional cell-kind bound; general-purpose heap reference. |
RefCell<T>RefCell<T> |
Unsynchronized, dynamically exclusive during rmwrmw |
allocalloc, getget, setset, rmwrmw, in_usein_use |
No additional cell-kind bound; ownership-preserving update without an extra clone. |
Atomic<T>Atomic<T> |
Atomic slot | allocalloc, getget, setset |
Integer of power-of-two width at least 8 bits, or floating-point primitive; small synchronized scalar state. |
Mutex<T>Mutex<T> |
Exclusive lock | allocalloc, getget, setset, rmwrmw |
SyncSync primitive or RC-pointer-shaped TT; serial critical sections. |
FlatLock<T>FlatLock<T> |
Flat-combining exclusive lock | allocalloc, getget, setset, rmwrmw |
SyncSync primitive or RC-pointer-shaped TT; batched operations under contention. |
RwLock<T>RwLock<T> |
Shared reads, exclusive writes | allocalloc, getget, setset, rmwrmw, rdlockrdlock |
SyncSync primitive or RC-pointer-shaped TT; read-heavy access with read transactions. |