Module sync/refcell
RefCell(T): shared mutation checked at run time, on one thread.
The one spelling of the dynamic exclusivity check
(plans/VALUES_BY_DEFAULT.md decisions 41 and 46). An Rc(T)'s payload is
read-only through the handle, as in Rust; a payload that several handles
mutate is an Rc(RefCell(T)). Every access is scoped to a closure, as
Mutex(T)'s is: with lends the value &T, with_mut lends it
&mut T, and a conflicting access while one is running panics, as Rust's
RefCell::borrow_mut does. Mutex blocks; RefCell panics.
{ RefCell } :: import("std/sync/refcell");
log := rc(RefCell(i32).new(i32(0)));
other := log; // a second handle to the same cell
other.replace_with(n => (n + i32(1))); // returns the old value, 0
seen := log.with(n => n); // 1
Beside the two closure doors, the cell has Rust's value-moving methods,
which lend nothing to the caller: replace(v) and take() swap a value
in and return the old one, replace_with(f) computes the new value from
the current one, and into_inner() consumes the cell.
Stability
unstable — Generation A of decision 46. The expression-scoped projections
get() / get_mut() (cell.get_mut().field = x) wait for the compiler
support that releases a mark at the end of the enclosing expression and
rejects binding the projection (plans/VALUES_BY_DEFAULT.md §6 "Decision
46"). The value lives in a cell of its own (one allocation) until a &T
receiver is passed by address (decision 43).
Types
A value that is mutated through shared handles on one thread, with the
exclusivity of each access checked at run time — Rust's RefCell<T>,
without a guard: access is granted only inside a closure.
with(body)runsbodywithv : &T; several may run at once (nested), and none while awith_mutbody runs.with_mut(body)runsbodywithv : &mut T, alone: awithorwith_muton the same cell while it runs, or awith_mutwhile awithruns, panics.replace(v),replace_with(f)andtake()store a new value and return the old one, lending nothing to the caller; each panics while awithorwith_mutbody runs, andreplace_withholds the cell aswith_mutdoes whilefruns.into_inner()consumes the cell.
The mark is released when the body returns or unwinds. A RefCell is
Send when T is and never Sync, so Arc(RefCell(T)) is rejected:
across threads, share an Arc(Mutex(T)) or Arc(RwLock(T)).
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), RefCell(T), ...)
new : (fn(value : T) -> Self)A new cell holding value, with no access running.
Returns: Self
with : (fn(generic(R : Type), self : &Self, body : &Impl(Fn(v : &T) -> R)) -> R)Run body with the value lent read-only, and return its result —
Rust's borrow() with the guard's scope. Panics if a with_mut body
of this cell is running.
Returns: R
with_mut : (fn(generic(R : Type), self : &Self, body : &Impl(Fn(v : &mut T) -> R)) -> R)Run body with the value lent exclusively, and return its result —
Rust's borrow_mut() with the guard's scope. Panics if any with or
with_mut body of this cell is running.
Returns: R
replace : (fn(self : &Self, value : T) -> T)Store value and return the value it replaces — Rust's replace.
Panics if any with or with_mut body of this cell is running: the old
value moves out, and a running body is reading or writing it.
Returns: T
replace_with : (fn(self : &Self, f : &Impl(Fn(v : &T) -> T)) -> T)Store f(v), computed from the current value v, and return the value
it replaces — Rust's replace_with. f reads the value as v : &T,
as a with body does, and returns its successor: the result replaces
the value wholesale, so f is not lent &mut T (an edit in place is
with_mut). The cell is held as with_mut holds it while f runs, so
any access to it from f panics; the mark is released when f returns
or unwinds, and an unwinding f leaves the value unchanged. Panics if
any with or with_mut body of this cell is running.
Unlike Rust's replace_with, whose f is an FnOnce(&mut T) -> T,
f is an Fn lent &T, as a with body is: it cannot move a
captured move-only value into its result (E0913). Until V2b gives a
String or a collection its own buffer, an f that returns its
argument unchanged (v => v) returns a value sharing storage with the
old one, so a write through either reaches the other; return
v.clone() for an independent value.
{ RefCell } :: import("std/sync/refcell");
hits := RefCell(i32).new(i32(4));
old := hits.replace_with(n => (n + i32(1))); // old is 4, the cell holds 5
Returns: T
into_inner : (fn(self : Self) -> T)The value, out of the consumed cell — Rust's into_inner. No mark is
checked: a running with or with_mut body holds the cell borrowed,
so the cell cannot be given up by value while one runs, and a shared
Rc(RefCell(T)) reaches this only through Rc.try_unwrap, which needs
the only handle.
The value is bound out of the cell's Box: a copy, which the box's
drop then releases. A move-only T is therefore rejected (E0901), since
nothing moves a payload out of a Box bitwise yet
(issues/2026-10-23-a-move-only-payload-cannot-move-out-of-a-box.md).
Returns: T
_swap : (fn(slot : *_RefCellSlot(T), value : T, mark : isize) -> T)Move the slot's value out and value in, leaving mark as the slot's
mark; the caller has checked the old mark.
Returns: T
impl(generic(T : Type), where(T <: Default), RefCell(T), ...)
take : (fn(self : &Self) -> T)Store T's default and return the value it replaces — Rust's take.
Panics as replace does.
Returns: T
impl(generic(T : Type), where(T <: Clone), RefCell(T), Clone(...))
clone : (fn(self : &Self) -> Self)Create an independent clone of self.
Returns: Self