Skip to main content

SharedCell

Struct SharedCell 

Source
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: AtomicU8 state. 0 = unlocked, 1 = locked. All other bit patterns are reserved — the JIT CAS is 0 → 1 for lock and 1 → 0 for unlock.
  • Offsets 1..=7: padding. Must be zero on construction but not read.
  • Offset 8: ValueWord payload (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_*_OFFSET constants 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: AtomicU8

Lock state byte at offset 0. 0 = unlocked, 1 = locked.

§value: UnsafeCell<u64>

Value payload. Read/written only while the lock is held.

Implementations§

Source§

impl SharedCell

Source

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.

Source

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.

Source

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.

Source

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 01 with Acquire ordering and does NOT return a guard — the JIT-emitted body is responsible for the matching unlock.

Source

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§

Source§

impl Debug for SharedCell

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for SharedCell

Retire the inner value share when the cell itself is dropped (ADR-006 §2.7.8 / Q10 — “set at construction, read at drop”).

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 on self.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 (those NativeKind variants are deleted).
Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl Send for SharedCell

Source§

impl Sync for SharedCell

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.