melinoe
Branded, multi-token phantom capabilities for compile-time data-access and thread-synchronization proofs.
melinoe is a #![no_std] foundation crate that encodes who may touch what,
and from where entirely in the type system. It is a generalised evolution of
the GhostCell pattern: where GhostCell pairs one brand with one token,
Melinoe offers a family of zero-sized tokens that share a single permit
interface yet differ in cardinality and thread-safety posture.
The crate ships no allocator, arena, or GlobalAlloc — only the
compile-time machinery. It is built to slot into the Mnemosyne memory
ecosystem alongside its ZST AllocPolicy, heap branding, and Branded* types.
Collections
This repository now ships BrandedVec<'brand, T> and
BrandedVecDeque<'brand, T> as built-in collection adapters in
melinoe::collections (re-exported at the crate root). They own
Vec<MelinoeCell<'brand, T>> / VecDeque<MelinoeCell<'brand, T>> and delegate
branding, token permits, zero-copy slice views, and conditional Cow boundaries
to the Melinoe permit system rather than carrying an independent ghost-token
implementation. Their default std feature also routes partitioned concurrent
reads and mutation through Melinoe's PartitionPlan / WriterShard driver.
For generated workloads, BrandedVec::from_fn constructs indexed values
inside an existing brand, while collections::with_generated creates a fresh
brand and passes the generated vector plus its exclusive token to a callback.
Consumers that retain Melinoe cells in a domain-specific branded container can
use BrandedVec::into_boxed_cells to transfer owned cell storage while keeping
the brand attached:
use with_generated;
let checksum = with_generated;
assert_eq!;
The callback result may escape, but the branded vector and token cannot. Under
std, the callback can compose this generated region with the existing
partitioned read/write helpers without introducing another scheduler or
runtime borrow flag.
In Greek myth, Melinoë leads a restless train of phantoms through the night. Here she leads a train of phantom types — each a wisp of pure evidence, weightless at runtime, that polices access to memory.
Why
RefCell pays for safety with a runtime borrow flag and a panic path.
Mutex/RwLock pay with atomics and syscalls. Both verify at run time what a
sufficiently expressive type system can verify at compile time. GhostCell
showed that a single branded token can hoist the T xor &mut T xor &T rule out
of one cell and over an entire region, at zero cost. Melinoe extends that idea
along the axis allocators actually care about — thread topology — by giving
each region a token whose Send/Sync posture is its cross-thread contract.
The model
A brand_scope mints a unique ExclusiveToken<'brand> over a fresh,
invariant lifetime. MelinoeCell<'brand, T> values created under that
brand reveal their contents only to a matching permit:
| Borrow of the owning token | Permit | Grants |
|---|---|---|
&token |
ReadPermit |
&T (shared) |
&mut token |
WritePermit |
&mut T (exclusive) |
Because the borrow checker already forbids holding &mut token and &token
simultaneously, it transitively forbids a write and any read across every cell
of the brand — no runtime state required.
Token families
All tokens are ZSTs parameterised by 'brand; they differ only in how many may
exist and where they may travel.
| Token | Cardinality | Send / Sync |
Role |
|---|---|---|---|
ExclusiveToken |
exactly one per brand | both | sole owner; read + write |
SharedReadToken |
many (Copy) |
both | fan a brand's read capability out to many readers/threads |
ThreadLocalToken |
one per brand | neither | owner pinned to one thread; soundness by confinement |
SyncRegionToken |
one per brand | both | owner that may migrate between threads (single writer) |
Multi-token composition
Distinct brands are non-unifiable, so they compose into several independent
exclusion domains. melinoe ships one primitive per axis and composes them — no
arity-specific brand_scopeN/CellN variants:
-
Multi-XOR — nest
brand_scope. Each nested scope is a fresh, distinct brand, so a&mutinto one region and a&mutinto another are held simultaneously, proven disjoint at compile time (no runtime checks). Composition yields any arity for free:use ; brand_scope; -
Disjoint concurrent writes —
region::WriterShardsplits one brand into disjoint sub-regions for parallel writers;SyncRegionTokenmoves a whole brand's write capability across threads. -
Ambient state —
reentrant::GuardedCell/reentrant::ReentrancyCellbrand thread-lifetime exclusive state (e.g. an allocator's per-thread slot): one boundary check yields a borrow-checked&mut T(or a fresh-brand token), re-entry is refused rather than aliased, panic-safe by construction. This is the sound bridge for state that outlives any singlebrand_scopeclosure. -
Conditional atomics —
atomic::BrandedAtomicis the write-side analogue ofCow: the capability you present selects the cost. AWritePermit(proven single-writer phase) gives plain, non-atomic access; aReadPermit(shared phase) gives atomicload/store/fetch_add/compare_exchange. You pay for synchronization only while sharing — ~32× cheaper in the exclusive phase (0.19 ns vs 6.1 ns/op). The brand makes plain and atomic access temporally exclusive, so they can never race; verified data-race-free under Miri. The_withmethods use sealed ZST ordering policies directly, so fixed ordering contracts do not route through the runtime-OrderingAPI. -
Conditional
Cow—CellCowExtplaces borrow-or-retain decisions at the ownership boundary.BorrowedandRetainedare ZST policies for static retain decisions;RetainDecisioncovers data-dependent retention by routing to the same sealed policy bodies. -
Thread-local value caches —
thread_cached!declares a per-threadCopyvalue cache with nightly#[thread_local]storage when the compiler supports it and a stablestd::thread_local!fallback otherwise. It is the shared Atlas primitive for repeated node/CPU/shard lookups without duplicating cfg-paired TLS boilerplate in each consumer.
Quick start
use ;
brand_scope;
Zero-copy guard projection
A borrow guard can be narrowed to a component of its payload — the branded
analogue of Ref::map / RefMut::map — carrying the permit evidence through
without copying. map_split derives two disjoint &mut projections from a
single write permit, the multi-field-writer pattern a RefCell can only express
at runtime:
use ;
brand_scope;
This reaches into a large branded payload (e.g. a slab block header) at zero
copy; the benchmarks contrast it with cloning the block out to
read one field. Verified sound under Miri (Stacked and Tree Borrows),
including the disjoint-&mut split.
The unsound interleavings simply do not compile:
use ;
brand_scope;
Thread safety without atomics
MelinoeCell<'brand, T> is Send when T: Send and Sync when
T: Send + Sync — the bound proven sound for GhostCell by RustBelt. Combined
with the token cardinality guarantees, this yields two statically-checked
parallelism shapes:
- Exclusive handoff — move a
SyncRegionTokento another thread to relocate the sole write capability. No competing writer can exist because no competing token exists.sync::scope_exclusivedemonstrates this overstd::thread::scope. - Shared fan-out — share
&SyncRegionToken(or copies of aSharedReadToken) across threads for concurrent reads; the absence of a live&muttoken statically excludes writers.
use scope_exclusive;
use MelinoeCell;
// The whole branded computation runs on a worker thread; the write
// capability is transferred across the boundary and proven unique.
let sum = scope_exclusive;
assert_eq!;
A ThreadLocalToken is !Send, so the compiler rejects any attempt to move a
thread-confined capability — or a cell it governs — onto another thread.
Benchmarks
Token access is a bare reference: zero synchronization instructions, fully
transparent to the optimizer. Measured against the runtime primitives that
discharge the same T xor &mut T xor &T exclusion at run time (Criterion
harness in benches/access.rs; equivalence of the measured
work pinned by tests/differential.rs):
| Single-threaded RMW | Per op | vs Melinoe |
|---|---|---|
Melinoe (borrow_mut) |
~0.20 ns | 1.0× |
AtomicU64 (Relaxed fetch_add) |
~6.1 ns | ~30× |
Mutex |
~11.9 ns | ~59× |
RwLock (write) |
~12.0 ns | ~60× |
Against the single-threaded analogues RefCell and Cell, Melinoe is at
parity (~0.19 ns/op) — same cost as Cell, but with real &mut T/&T
references and no RefCell runtime borrow flag or borrow() panic path.
For concurrent reads, Melinoe's SharedReadToken scales near-linearly
with cores, reaching ~10× RwLock and ~15× Mutex at 16 threads — where
RwLock stops scaling entirely, its reader-count atomic bouncing between cores.
For concurrent writes, disjoint WriterShard partitions
scale near-linearly using plain stores, matching lock-free atomics while a
Mutex<Vec> — which cannot express disjoint &mut — serializes and loses to the
single-threaded baseline. The partition driver reserves only the shard handles it
can actually spawn and uses an overflow-safe ceiling division, so adversarially
large parts values do not amplify allocation beyond the non-empty shard count.
For conditional atomics (BrandedAtomic), the exclusive
phase is ~32× cheaper than a real atomic RMW (plain stores), the shared phase
is at parity with a raw AtomicU64 (zero-cost wrapper), and a mixed
build-then-publish workload is ~1.93× faster end to end. Relaxed,
AcqRel, and SeqCst are ZST ordering policies for monomorphized call sites;
the _with methods lower directly through associated constants, while the
runtime Ordering methods remain available for data-dependent ordering.
as_atomic exposes the underlying atomic under a read permit for zero-copy
interop with APIs that already operate on Atomic*.
Full methodology, all tables (five Criterion harnesses), and the honest caveats
are in BENCHMARKS.md. Ratios are the signal; absolute figures
are hardware-dependent. The benchmark suite includes direct-vs-ZST Cow policy
rows and BrandedAtomic::as_atomic raw-interop rows. Reproduce with
cargo bench.
Concurrent writes via disjoint shards
Two threads writing the same cell is a data race no phantom type can excuse.
Sound concurrent writes mean concurrent access to disjoint partitions — the
per-thread allocator-slab pattern. A WriterShard is a
move-only, Send write capability over a disjoint &mut [MelinoeCell<'brand, T>]
sub-slice; disjointness comes from the standard library's split_at_mut. Read is
gated behind &shard and write behind &mut shard, so write strictly subsumes
read — the dependency realized structurally, at zero runtime cost.
The region module is split by responsibility: region::shard owns the
WriterShard capability, and region::chunks owns exact-size chunk iteration.
use partition_for_each;
use ;
brand_scope;
Verified data-race-free under Miri (Stacked Borrows + data-race detection). Empty regions produce no worker shards; requesting more partitions than cells produces one non-empty shard per cell.
When the caller needs a typed scheduling policy instead of a raw part count,
PartitionPlan supports fixed part count, reported hardware parallelism, and
fixed chunk size:
use ;
use ;
brand_scope;
Integrators that provide their own scheduler can install a process-global
blocking executor with register_parallel_executor; clear_parallel_executor
restores the default std::thread::scope driver for later calls.
Integration with Mnemosyne
Melinoe is intentionally orthogonal to allocation:
- Brand storage in place.
MelinoeCell::from_mut(andBrandedAtomic::from_mut) reborrow an existing&mut T/&mut AtomicU64as a branded view at zero cost (both are#[repr(transparent)]), so Mnemosyne'sBrandedHeapstorage or an existing counter can be governed by a Melinoe token without copying or wrapping. - Bulk slab access, zero-copy.
CellSliceExtviews a whole[MelinoeCell<'brand, T>]slab as a native&[T]/&mut [T]once a permit is presented —slab.borrow_slice_mut(&mut token).fill(0)for vectorised initialisation,slab.borrow_slice(&token)for SIMD scans — instead ofBrandedCell-at-a-time access. Benchmarks confirm this matches the already-zero-cost per-cell path while adding slice ergonomics. - Borrow-or-retain at the boundary.
CellCowExt::borrow_cow_withreturns a zero-copyCow::Borrowedunder theBorrowedZST policy or clones exactly once underRetained;borrow_cow/retain_coware the direct branch-free forms for common static cases. All Cow entry points share the sealed policy bodies, so retention stays explicit without infecting the core access path with ownership branching. - Upgrade the token model. Mnemosyne's
AllocatorTokenis a single!Sendtoken with runtimeassert_ne!distinctness checks inborrow_mut_2/3. Melinoe replaces those with the compile-time-disjointWriterShardand addsSend/Synctoken families for cross-thread slabs. - Compose with
branded_scope. A Mnemosyne brand and a Melinoe brand are both invariant lifetimes; nest the scopes to require both an allocation witness and an access permit at a call site. - Replace lock-based interior mutability on validated hot paths with
[
MelinoeCell] + aSyncRegionToken, moving the synchronization proof from runtime atomics to compile-time evidence. - Encode
BrandedCellcapabilities as distinct tokens (e.g. a read-only view handed to observers viaSharedReadTokenwhile a single subsystem retains theExclusiveToken).
Every token is a ZST; carrying one through an API costs zero bytes and zero instructions after monomorphization.
Cargo features
| Feature | Default | Effect |
|---|---|---|
std |
✅ | Superset of alloc; enables std::thread::scope-based helpers such as sync::scope_exclusive. |
alloc |
Links the alloc crate for heap-payload examples/tests. |
|
nightly |
Enables doc_cfg for precise feature-gated docs (requires a nightly toolchain); reserved for future generic_const_exprs capability sets. |
The crate is #![no_std] by default and brings no global allocator of its own.
Safety
Every unsafe block carries a // SAFETY: comment discharging its obligation,
and they fall into a few categories:
- Token minting (
*_scopefunctions) — thefor<'brand>higher-ranked bound makes each brand fresh and invariant, so a minted owning token is provably unique. The ambient gates (ReentrancyCell/GuardedCell) mint under a runtime re-entrancy flag instead, on!Syncthread-confined state. - Cell access (
borrow/borrow_mut,CellSliceExt,WriterShard) — a liveReadPermit/WritePermit(or an owning&mut [MelinoeCell]) is the exclusion proof, so the produced&T/&mut T/&[T]/&mut [T]cannot alias. Zero-copy slice andfrom_mutviews rest on the#[repr(transparent)]layout chainMelinoeCell → UnsafeCell<T> → T. - Conditional atomics (
BrandedAtomic) — aWritePermitproves exclusivity for plain access; the atomic↔value pointer cast is layout-valid (an atomic has the same size/bit-validity as its value). ZST ordering policies andCowretain policies are pinned by compile-time size/alignment assertions. Static ordering methods call the sealed atomic mediation surface directly with associated constants. Raw atomic interop throughas_atomicis tied to a live read permit. Send/Syncimpls — theGhostCellbound, with reasoning recorded inline.
The capability traits are sealed: downstream crates cannot forge a permit.
Soundness is pinned two ways: compile_fail doctests (brand mixing, read/write
overlap, sending a thread-local token) and Miri (Stacked Borrows +
data-race detection) over every test target, including the cross-thread
disjoint-write and conditional-atomic transitions.
The Miri job runs cargo +nightly miri test --all-features with
PROPTEST_CASES=8. That bound is a wall-clock constraint, not a reduction in
coverage: tests/partition.rs carries three proptests at proptest's default 256
cases, each spawning real threads over up to 255 elements, and under Miri's
slowdown that one suite exceeds a 20-minute budget and is killed mid-run. At 8
cases the full 26-test suite completes in ~67s. Every test still runs, and any
counterexample the shrinker finds is still minimized and reported.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.