pub struct SharedCell {
pub state: AtomicU8,
pub value: UnsafeCell<u64>,
/* private fields */
}Expand description
Interior-mutable cell backing a CaptureKind::Shared capture.
A Shared capture slot stores *const SharedCell — a raw pointer
obtained via Arc::into_raw on an Arc<SharedCell>. Each live slot
holds exactly one strong-count share; closure Drop reclaims it with
Arc::from_raw(ptr).drop().
§⚠ JIT-coupled ABI: payload offset is part of the contract
The 8-byte payload sits at offset 8 (SHARED_CELL_VALUE_OFFSET). The
JIT in crates/shape-jit/src/mir_compiler/places.rs and the inline
lock/unlock lowering in shape-jit::ffi::object::closure both read
offset 8 directly via Cranelift codegen with this constant baked in.
Changing the layout requires updating the JIT in lockstep — the
const _: () = { ... } static assertion below catches a drifting
definition at compile time, but a mismatch in the JIT’s hardcoded
constants would still need a manual audit. Per-FieldKind read/write
helpers in closure_raw.rs::read_shared_* / write_shared_*
reinterpret the 8-byte payload through narrower FieldKind widths
for sub-8-byte scalar inner types but never change the physical
offset.
§ABI and layout (Track A.1E)
Pre-A.1E this was a parking_lot::Mutex<ValueWord> type alias. The
JIT Cranelift inline lock/unlock lowering in A.1E reads the lock
state byte and the value payload at hard-coded byte offsets
(state @ 0, value @ 8), so the cell is redefined as an explicit
#[repr(C)] struct with a hand-rolled spinlock. This gives the JIT
full ABI control without depending on parking_lot’s (non-repr-C)
internal layout. The interpreter continues to use the .lock()
API, which returns a guard that supports *guard = ... and
let bits = *guard; — so interpreter code paths stay unchanged.
§Layout invariants (load-bearing for JIT)
- Offset 0:
AtomicU8state.0= unlocked,1= locked. All other bit patterns are reserved — the JIT CAS is0 → 1for lock and1 → 0for unlock. - Offsets 1..=7: padding. Must be zero on construction but not read.
- Offset 8:
ValueWordpayload (u64 bit pattern). - Trailing fields after offset 16: kind tracking (added by ADR-006
§2.7.8 / Q10). NOT read by the JIT — JIT only touches state @ 0
and value @ 8 via the
SHARED_CELL_*_OFFSETconstants below.
§ADR-006 §2.7.8 / Q10 — parallel-kind invariant extended to cells
Cell-storage structs that hold raw heap-pointer bits grow a parallel
NativeKind companion alongside their raw payload (per ADR-006
§2.7.8 / Q10). For SharedCell the payload is single-slot
(UnsafeCell<u64>), so the companion is a single kind: NativeKind
field set at construction (SharedCell::new(value, kind)) and read
at drop (Drop for SharedCell). The drop dispatch mirrors
KindedSlot::drop in kinded_slot.rs:274 — same retire-the-Arc
matrix, same forbidden alternatives (no vw_drop, no is_heap probe,
no Bool-default fallback). Construction sites must source the kind
at the same call where the bits are sourced — see ADR-006 §2.7.8 for
the binding rules.
§Contention
The JIT’s inline fast path is a single CAS from 0→1 for lock and
1→0 for unlock. On failure it calls the jit_shared_lock_contended
/ jit_shared_unlock_contended FFI helpers. The interpreter’s
.lock() method runs the same acquire-loop. Closure-capture
contention is rare so a simple spin_loop-based wait is sufficient
— no parking behaviour is preserved from the old parking_lot-based
implementation.
Memory ordering: lock acquire is Acquire, lock release is Release,
matching the standard Mutex contract.
Fields§
§state: AtomicU8Lock state byte at offset 0. 0 = unlocked, 1 = locked.
value: UnsafeCell<u64>Value payload. Read/written only while the lock is held.
Implementations§
Sourcepub fn new(value: u64, kind: NativeKind) -> Self
pub fn new(value: u64, kind: NativeKind) -> Self
Construct a new unlocked cell holding value with the matching
NativeKind companion (ADR-006 §2.7.8 / Q10).
kind MUST classify value’s bits at construction. When kind
selects a heap-bearing arm (e.g. NativeKind::String,
NativeKind::Ptr(_)), value MUST be the result of
Arc::into_raw::<T> for the matching T and the caller transfers
exactly one strong-count share into the cell. Drop retires that
share when the last Arc<SharedCell> share is released. For
inline-scalar kinds the bits are the raw scalar value and Drop
is a no-op for the value field.
Mid-life kind changes are forbidden: every write that changes the
kind must replace the whole cell (drop + reconstruct), never
reassign value alone — the lockstep invariant matches the
stack-side §2.7.7 rule.
Sourcepub fn kind(&self) -> NativeKind
pub fn kind(&self) -> NativeKind
Read the cell’s NativeKind companion.
Set once at construction; never changes during the cell’s lifetime
(ADR-006 §2.7.8 / Q10 lockstep invariant). Callers that need to
drop the cell’s value through KindedSlot / drop_with_kind
dispatch read this and pass it alongside the value bits.
Sourcepub fn lock(&self) -> SharedCellGuard<'_>
pub fn lock(&self) -> SharedCellGuard<'_>
Acquire the lock, blocking (spinning) until the state byte
transitions from 0 to 1. Returns a RAII guard that unlocks
on Drop.
Memory ordering: Acquire on the successful CAS, so all writes
protected by the lock on the previous owner are visible here.
Sourcepub fn lock_contended(&self)
pub fn lock_contended(&self)
Spin-wait on the state byte until it becomes 0 and we
successfully flip it to 1. Uses spin_loop hints to ease the
CPU during the busy-wait. Closure-capture contention is rare in
practice so the simplicity of a spinlock is acceptable.
pub so the JIT’s jit_shared_lock_contended FFI helper can
call it directly on a &SharedCell reborrowed from the raw
pointer bits stored in a capture slot. The lock transitions from
0 → 1 with Acquire ordering and does NOT return a guard —
the JIT-emitted body is responsible for the matching unlock.
Sourcepub unsafe fn unlock(&self)
pub unsafe fn unlock(&self)
Release the lock. Only the current lock holder may call this.
§Safety
The caller must currently hold the lock (state == 1). Callers
other than SharedCellGuard::drop must guarantee this manually;
the normal path is to let the guard go out of scope.
pub so the JIT’s jit_shared_unlock_contended FFI helper can
call it on a &SharedCell reborrowed from a capture slot.
Trait Implementations§
The cell is wrapped in Arc<SharedCell>; this Drop fires only when
the last Arc share retires. At that point the value slot’s bits
must release whatever resource the kind companion classifies them
as. This mirrors KindedSlot::drop in kinded_slot.rs:274 exactly —
same Arc-decrement matrix, same forbidden alternatives:
- No
vw_drop(bits)(forbidden #8 per CLAUDE.md / ADR-006 §2.7.7): the dispatch is onself.kind, not on tag bits. - No
is_heap()/ “drop only if heap-shaped” probe (forbidden #7): the kind already encodes the discriminator; inline-scalar arms fall through to a no-op without probing. - No Bool-default fallback (§2.7.7 #9): the kind is always
concrete — set at construction, never
Unknown/Pending/Dynamic(thoseNativeKindvariants are deleted).