Cells reference
The two sharing tools: std::shared::Shared (one shared mutable cell) and
std::arena::Arena (stable identities for graphs). When to reach for
which: the memory model.
Shared<T>
A heap cell two places can hold at once: the escape hatch from value-semantics copying.
impl Shared<type T> {
fun new(value: T): Shared<T>
fun read(self): T // a COPY of the contents
fun clone(self): Shared<T> // another handle to the SAME cell
fun write(self): &mut T borrows self // a writable view of the contents
}
import std::shared::Shared;
fun main() {
let log: Shared<List<str>> = Shared::new([]);
let record = |entry: str| {
log.write().push(entry);
};
record("first");
record("second");
print(log.read().len());
}
read()copies: mutating the result is lost (shared.read().push(x), the classic trap). Mutate throughwrite().write()returns a view. Use it within the same statement (cell.write() = v,cell.write().push(item)); it obeys the usual view rules (no storing, no holding acrossawait).- Copying the
Sharedvalue itself copies the handle: both handles see one cell. That’s the point.
Arena<T> + Handle<T>
A generational arena: insert values, get back small copyable
Handle<T> keys. Handles are plain values, storable in struct fields and
lists (which views are not), so nodes can reference each other:
struct Handle<T> { … } // slot index + generation; copy freely
impl Arena<type T> {
fun new(): Arena<T>
fun branded(): Arena<T> // a fresh brand — a handle from one arena cannot index another
fun insert(&mut self, value: T): Handle<T>
fun get(&self, handle: Handle<T>): Option<&T> borrows self // a view; None once removed
fun set(&mut self, handle: Handle<T>, value: T): bool
fun remove(&mut self, handle: Handle<T>): Option<T>
fun contains(self, handle: Handle<T>): bool
fun len(self): i32
fun is_empty(self): bool
}
import std::arena::{ Arena, Handle };
import std::option::Option::{ self, Some, None };
struct Node {
label: str,
edges: List<Handle<Node>>,
}
fun main() {
mut nodes: Arena<Node> = Arena::new();
let a = nodes.insert(Node { label = "a", edges = [] });
let b = nodes.insert(Node { label = "b", edges = [a] });
// Close the cycle: a → b. `get` hands back a view, so copy it (`*node`),
// edit the copy, and write it back with `set`.
match nodes.get(a) {
Some(let node) => {
mut updated = *node;
updated.edges.push(b);
nodes.set(a, updated);
},
None => {},
}
print(nodes.len());
}
- Generational means deletion-safe: removing a value and reusing its
slot bumps a generation counter, so a stale handle
getsNoneinstead of aliasing the new occupant. getreturns a view (Option<&T>), second-class like any other: read through it, but it may not outlive an arena mutation or be stored. To change a value, copy it out (*view), edit, andsetit back, or design nodes so edges/fields update independently.- Traversal is re-
getper step, so the arena stays mutable while you walk.
Handles cross the wire
A handle is two integers, so Handle<T> is Wire: it can sit in an rpc
payload, and a server-side arena becomes the naming layer for clients:
the stable entity reference they quote back (“update node X”). The T is
phantom; only { index, generation } travels, so a handle names entities
whose type is not itself Wire.
import std::arena::{ Arena, Handle };
import std::json::{ encode_json, decode_json };
import std::result::Result::{ self, Ok, Err };
import std::option::Option::{ self, Some, None };
[derive(Wire)]
struct Rename { node: Handle<str>, title: str }
fun main() {
mut titles: Arena<str> = Arena::new();
let node = titles.insert("old");
// The client received `node` earlier and now quotes it back.
let request: Result<Rename, str> = decode_json(encode_json(Rename { node = node, title = "new" }));
match request {
Ok(let rename) => {
titles.set(rename.node, rename.title);
print(titles.get(rename.node).unwrap_or("gone"));
},
Err(let reason) => print(reason),
}
}
The generational rule becomes the distributed staleness story for free: a
client acting on an entity another client deleted gets the same clean None
(and set returns false) as local code holding a stale handle. No phantom
write, one rule from a local list to an rpc boundary.
Scope the arena to the session. A handle is a name, and (index, generation)
is guessable, so an arena shared across tenants hands every client names that
mean something to the others. A per-session arena (created when the
session is established, dropped with it) makes a handle from one session
name nothing in another, by construction. Authorize the session; then look
the handle up in that session’s arena.
When one arena is shared and its handles must not be interchangeable,
Arena::branded() adds the belt to that suspenders: its generation counters
start at a random value instead of 0, so a handle issued by one branded arena
resolves to None in every other one rather than naming that arena’s slot of
the same index.
import std::arena::{ Arena, Handle };
import std::option::Option::{ self, Some, None };
fun main() {
mut mine: Arena<i32> = Arena::branded();
mut theirs: Arena<i32> = Arena::branded();
let handle = mine.insert(7);
theirs.insert(9);
print(theirs.get(handle).unwrap_or(-1)); // -1 — a foreign name
print(mine.get(handle).unwrap_or(-1)); // 7
}
Everything else is unchanged: branding only moves where the counters start, so
removal, staleness and slot reuse behave exactly as above, and a brand mismatch
is the same clean None (and false from set) as a stale handle, never a
panic.
A brand is a confusion guard, not an authorization check. It travels inside the handles it issues, so a client holding one valid handle can derive it. It stops one tenant’s names from meaning something to another and stops blind guessing; it does not make a handle unforgeable. Authorize the session first, then look the handle up in that session’s arena.