Skip to content

feat(plurality): MultiPool, an object pool for values of any type - #660

Open
Sander Saares (sandersaares) wants to merge 45 commits into
mainfrom
feat/plurality-blindpool-design
Open

feat(plurality): MultiPool, an object pool for values of any type#660
Sander Saares (sandersaares) wants to merge 45 commits into
mainfrom
feat/plurality-blindpool-design

Conversation

@sandersaares

@sandersaares Sander Saares (sandersaares) commented Aug 12, 2026

Copy link
Copy Markdown
Member

[Copilot speaking]

Motivation

plurality::Pool<T> serves one element type. Callers holding a heterogeneous mix of values — a scheduler's task payloads, a state machine's per-state data, anything reached through a trait object — either need one pool per type, with the bookkeeping and the wasted slack that implies, or they give up pooling.

This adds MultiPool: the same pool, with the element type moved from the pool to the allocation call.

let pool = MultiPool::new();
let a = pool.alloc_box(42_u64);
let b = pool.alloc_box(String::from("hello"));
let c: Box<dyn Display> = Box::unsize(pool.alloc_box(1.5_f64), coerce!(dyn Display));

What changed

MultiPool<A> and MultiPoolBuilder. A pool that accepts values of any type. It hands out the same four handle flavors as Pool<T> (Box, Alloc, Arc, Rc) with the same guarantees, the same handle size, and the same reclamation cost — a handle from a multi pool is indistinguishable from a handle from a typed pool, including for unsize coercion.

Internally it is a router. Each allocation derives a routing key from Layout::new::<T>() and dispatches to a crate-private LayoutPool serving that key, creating one on first sight:

                 MultiPool
                     │
        routing key from T  ──►  directory (linear scan, first-seen order)
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
   LayoutPool   LayoutPool   LayoutPool
   (8,align 8) (24,align 8) (1,align 4)
        │            │            │
     chunks       chunks       chunks

Routing is by slot geometry, not by type: the key is the value's size paired with its alignment widened to hold the trailing refcount and index words, which is precisely what the slot's stride and offsets are a function of. Every type that lays out an identical slot shares a layout pool, so u64 and i64 land in the same one, and so do [u8; 8] and [u16; 4]. Sizes never merge. This is what makes the multi pool's memory behaviour predictable rather than proportional to the number of types in play.

The directory is a linear scan over first-seen order, not a sorted structure. Programs present few distinct geometries, and a scan over a contiguous array beats a search that has to maintain order — the benchmarks vary directory size specifically to keep that assumption honest.

One pool body, two geometry providers. Pool<T>'s allocation, free-list, and chunk logic is now parameterised over a SlotGeometry provider instead of hard-coding SlotCell<T>. TypedGeometry<T> answers with const expressions, so the typed pool folds to exactly the code it emitted before; RuntimeGeometry answers with loaded fields, which is what lets one body serve a layout known only at run time. The typed pool's measured instruction counts are unchanged.

A relaxed Send bound on Pool<T, A>. The pool required T: Send to be Send itself, which was overstated. A pool object owns no values: every safely reachable value is owned through a handle, there is no iteration or drain, and teardown deallocates chunks without reading or dropping element storage. A thread receiving a pool can only draw free slots, which hold nothing live. Pool<T, A> is therefore Send whenever A is, for any T, and thread mobility for values is carried entirely by the handles, which impose their own bounds. Pool remains !Sync, so allocation is still confined to one thread at a time.

This is what lets MultiPool — which cannot name its element types, let alone bound them — be Send, and lets one multi pool hold values of types with different thread affinities at once. tests/send_bound_probe.rs holds the reasoning to account: a compile-time assertion rejects reintroducing the bound, and the scenarios move pools of a thread-bound type across threads while exercising values, slot reuse, teardown, and a concurrent free-versus-allocate hand-off.

A teardown soundness fix. Freeing a pool through its last surviving handle deallocated through a pointer derived from a shared borrow, which is undefined behaviour. Miri caught it as a borrow-stack violation once the erased free path exercised it. Fixed for Pool<T> as well as the new code.

Allocator reentrancy made safe. An allocator that allocates from the pool it serves corrupted the pool: chunk growth derived a new chunk's base slot index from a count it published only after calling the allocator, so a nested allocation claimed the same slot indices, and directory reservation held &mut over a vector across a global-allocator call. Both predate this work. Growth now allocates first and reads the count after, re-checks the chunk cap so reentry cannot overshoot it, and reserves directory room without a live borrow, deferring the free of the displaced buffer until the reservation has been consumed. A pool that can no longer grow hands out any slot a nested allocation left free rather than reporting exhaustion. Reentry is supported from every direction — allocate, deallocate, Clone::clone on a multi pool's allocator, pooled values' destructors, and construction closures — so the pools place no obligation on the allocator at all. That covers both a reentrant pool allocator A and a reentrant #[global_allocator]; the two reach different paths, since directory growth calls the global allocator and never A, and they carry different evidence: the A paths are checked under Miri with Tree Borrows, while the global-allocator path is checked under the ordinary test runner, which Miri's allocator model cannot host. docs/implementation/reentrancy.md records the ordering the code relies on and which door each argument covers.

Documentation restructured. docs/design.md and docs/implementation.md were single documents covering a crate that has outgrown that shape; they are now hubs linking to area documents under docs/design/ and docs/implementation/. The implementation guide now describes the whole implementation, not only the parts this work touches.

Cost

Measured with Callgrind, instructions per operation:

Operation Typed Multi
alloc + drop, 32-byte value 58 95
Box<dyn Trait> round trip, 32-byte value 97 131

Roughly 6 instructions of the difference are the directory scan, per entry examined. The remainder is a fixed cost, and about half of it is the scan itself; the rest is reaching the pool body, threading the value through the routing helper, and one instruction for addressing a slot from a loaded stride rather than a folded constant. docs/implementation/performance.md attributes it line by line. In wall clock the gap is narrower than the instruction counts suggest — about 1.17× for an allocate-and-free, and a sixteen-layout directory does not separate from a one-layout one, because the scan overlaps with the pool's own pointer chasing. For comparison, std::boxed::Box costs 175 on the trait-object benchmark and infinity_pool's equivalent BlindPool costs 294. Wall-clock figures and the rest of the comparison field are in docs/PERF.md.

Verification

MultiPool is covered to 100% lines and functions, and the four modules involved survive no mutants (422 tested). Miri runs the full suite, including the cross-layout property test that asserts no slot address is ever served for two different layouts.

Adds a heterogeneous object pool that accepts values of any type, dropping
the element type parameter from the pool and moving it to the allocation.

DESIGN.md gains a chapter covering the user-visible contract: the router
over per-layout pools, exact-layout routing, byte-target chunk sizing, the
two growth caps, the relaxed Send bound, the two-tier introspection surface,
and a feature comparison against infinity_pool's blind pool family.

IMPLEMENTATION.md is new and covers the internals: extracting slot geometry
behind a provider trait so the typed pool keeps its compile-time constants,
the crate-private LayoutPool, the router's reentrancy protocol, fallible
metadata allocation, the cost model, benchmarks, verification and staging.

The design rests on plurality's reclamation half already being layout-driven
rather than type-driven: a handle recovers its slot, chunk and pool by
arithmetic over the value's own size and alignment. The router is therefore
consulted only on allocation and never on free, which keeps blind handles one
pointer wide and destructor-reentrancy safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
…esign docs

Distinguish exact layout from slot geometry where the blind pool's growth caps
are described: routing is by exact layout, and geometry is a strictly coarser
partition that never divides anything.

Frame the pool as externalising its lock rather than lacking one. A caller-owned
Mutex around allocation, with drops running unlocked, is the ordinary
multithreaded deployment, and the comparison with infinity_pool now contrasts
lock ownership rather than implying single-threaded use.

State the Send bounds accurately: a pool object never owns a value, so pool
mobility is independent of value mobility. The typed pool's T: Send bound is a
conservative choice rather than a soundness requirement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
A pool object owns no values. Every safely reachable value is owned through a
handle, the pool exposes no iteration or drain, and teardown deallocates chunks
without ever reading or dropping element storage. A thread that receives a pool
therefore has no route to a value another thread placed in it; it can only draw
free slots, which hold nothing live. Thread mobility for values is carried by
the handles, each of which already imposes its own bound.

Pool<T, A> is consequently Send whenever A is, for any T. Pool remains !Sync,
so allocation is still confined to one thread at a time.

Add tests/send_bound_probe.rs, which holds this reasoning to account. A
compile-time assertion rejects reintroduction of the bound, values record the
thread that built them and assert in Drop that they were not destroyed
elsewhere, and the scenarios move pools of a thread-bound type across thread
boundaries while values, slot reuse, teardown, and a concurrent free-versus-
allocate hand-off are exercised.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
The document is written in target-state voice throughout; a preamble that
describes what the document is adds nothing and misleads about scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
…ents

Restructure the design documentation as a high-level hub plus five area
documents covering handles, concurrency, memory, allocation, and the blind
pool.

Behavioural statements corrected and added in the process:

- The invariant list records that the pool object neither yields nor drops
  pooled values, which is what licenses a pool being Send on the strength of
  its allocator alone, and constrains future API accordingly.
- The reentrancy invariant covers the pool's allocator as well as the global
  allocator, and scopes itself to allocator callbacks so that the unrestricted
  reentrancy of value destructors and construction closures stands unqualified.
- The blind pool documents its per-layout clamping of chunk size and chunk cap,
  and that the effective figures are observable through per-layout queries.
- The blind pool documents that its memory is monotonic per layout.
- The layout cap is documented as optional and unbounded by default, and the
  memory bound is stated conditionally on both caps being set.
- Blind-to-typed pool conversion is attributed to distinct instantiations with
  distinct teardown hooks and sizing policies rather than to differing slot
  geometry, which both forms derive identically.
- The infinity_pool comparison names the version it describes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
…ocuments

The implementation guide covers the whole crate: the module map, the core
data structures, the slot lifecycle, chunk management, pointer recovery, the
two reference counts, teardown, handles, the blind pool, performance and
verification. It is organised as a hub plus one document per area.

The blind-pool delivery sequence moves to TODO.md, where a plan belongs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
`!Sync` is the sole barrier keeping allocation on one thread at a time now
that no `T: Send` bound stands behind it, so assert it.

The concurrent probe shared a `&Pool` across threads through the `AssertSend`
shim, which asserts `Sync` rather than `Send` and so exercised neither the
relaxation nor a property the crate offers. Move the pool by value instead,
which is what the relaxation licenses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
The pool and the handle must agree exactly on where a slot's refcount, index
and chunk header sit, but the handle cannot ask the pool because finding the
pool is the point of the walk. Both now evaluate the same formulas over the
same size and alignment, so agreement is structural rather than maintained by
hand in two places.

A typed provider answers with compile-time constants and a runtime provider
answers with stored fields, which is what will let one pool body serve a
layout known only at run time. The typed provider carries a const cross-check
against the compiler's own layout of the slot struct, forced from its
accessors so no instantiation can route around it.

Ref: docs/implementation/geometry.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
The pool body manages chunk memory without ever reading or dropping value
storage, so it needs the element type only for its layout numbers. It now takes
those from a geometry provider and carries no element type of its own, which is
what lets the same body serve a layout known only at run time.

The provider also owns the slot and header pointer arithmetic. The typed
provider expresses it as indexing over the compiler's own slot array while the
runtime provider multiplies out the stride; the const cross-check proves the
two land on the same addresses, and the typed pool keeps the addressing modes
it had. Instruction counts are unchanged across the benchmark suite.

Ref: docs/implementation/geometry.md, docs/implementation/pool-body.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Share the slot machinery between pool flavours. `alloc_slot`, `grow` and
the directory walk move onto `PoolInner<A, G>`, where they address slots
through the geometry provider rather than through a `SlotCell<T>` type, and
the occupancy helpers move onto `PoolCore`, which is the only state they
touch. The typed pool keeps its own thin wrappers over both.

`LayoutPool` is that body driven by `RuntimeGeometry`: a pool whose value
layout is fixed at construction rather than by a type parameter. Its allocation
entry point is unchecked, because its only caller selects it by matching
layouts. Sizing is clamped rather than asserted -- one blind-pool-wide
configuration meets many layouts -- and its metadata allocation is fallible, so
a first allocation for an unseen layout can report failure instead of aborting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
`BlindPool` moves the element type from the pool to the allocation, so one
pool object backs a heterogeneous working set. It routes each request by
`Layout::of::<T>()` to a `LayoutPool` serving that exact layout, creating one
on first sight, and layers the typed pool's full handle surface over that
router.

Chunks are sized by a byte target rather than a slot count, so layouts spanning
several orders of magnitude commit comparable memory per growth step. Growth is
bounded per layout and, optionally, in the number of layouts. Introspection
splits into aggregate queries over the whole pool and per-layout queries named
for a type; the latter report the effective sizing after clamping and never
create a layout pool.

The router's cold path releases control to user code at several points — a
construction closure, a rejected value's destructor, `A::clone`, the global
allocator — so it builds the new layout pool before touching the directory,
reserves after constructing, re-scans and re-checks the cap, and publishes the
pool before its key. No directory borrow is ever live across user code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Add the blind-pool benchmark rows to both harnesses: allocate-and-free against
a pool holding one layout and against one holding sixteen, which separates the
per-entry directory scan from the rest of the routing cost, plus a trait-object
row that lines up with the owning fat-pointer comparison.

Outline layout-pool creation behind a cold, non-generic call so that the
allocation path pays only for the scan that finds an existing pool.

Assert with the allocation tracker that a warmed blind pool reuses slots across
a mix of layouts without touching the system allocator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Cover the blind pool: heterogeneous mixes, types sharing one layout, zero-sized
and over-aligned values, coercion, handles outliving the pool, both capacity
limits, the sizing clamps, allocator failure on the chunk and metadata paths,
panic safety, and reentrancy at every point the cold path releases control —
including from the allocator's own `Clone`.

A chunk header's pool pointer frees the pool when the last handle outlives the
pool object, so it cannot be derived from a `&self` borrow: such a pointer
permits only reads and interior-mutable writes, and Miri rejects the
deallocation. Each pool now records its own address at construction, from the
pointer its allocation was created with.

Generalise the allocator-failure message: it also reports the metadata
allocation a first-seen layout needs, so naming chunks was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Cover the closure, uninitialized and pinned allocation entry points, which
until now had no test reaching them, and assert that they all land in the
one layout pool their layout selects.

Route `free_slot_erased` through `SlotGeometry::header_of` rather than
recomputing the header offset from the stride, removing the duplicated
formula its own comment admitted to.

Mark the two hand-written `Clone` impls that exist only because deriving
would demand `A: Clone` as uninstrumented, since both forward to `Copy`.

Move the blind-pool statistics assertions to `tests/stats.rs`, where the
`stats` feature's tests live.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Kills the mutants that survived the blind pool's first pass and corrects the
implementation notes that promised more than the code delivered.

Three mutants survived because nothing exercised the paths they broke: the
directory reservation's failure arm, a chunk cap of zero, and the layout pool's
destructor. Each now has a test. The directory reservation is provoked by
letting the layout pool's own allocation through and refusing the next one,
which is the reservation, so the deny gate grows an allowance budget. The
destructor is proven by an allocator that counts live bytes and must reach zero.

A chunk cap of zero previously floored to one, which silently contradicted the
typed pool, where zero means the pool can never allocate. It now means the same
thing in both.

The chunk-size clamp's lower bound is promoted from a debug assertion to a real
one: a zero-slot chunk divides by zero and underflows the slot mask, which is
not a debug-only concern.

The geometry formulas are asserted against `Layout::extend` and `pad_to_align`,
which is `core`'s own `repr(C)` field placement, over the same spread of types.
Comparing the two providers against each other proved little, since they share
the formulas; comparing the formulas against a second implementation of the
placement rules proves the formulas.

A property test drives three layouts through one blind pool and asserts that
values drop exactly once, that the pool empties, and that no slot address is
ever served for two different layouts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds heterogeneous object pooling through BlindPool, sharing core allocation machinery with typed pools while preserving handle behavior and reclamation guarantees.

Changes:

  • Adds layout-routed blind pools, builders, and runtime slot geometry.
  • Fixes pool teardown pointer provenance and relaxes Pool<T>’s Send bound.
  • Expands tests, benchmarks, performance reports, and architecture documentation.

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/blind_pool.rs Implements layout routing and allocation APIs.
src/blind_builder.rs Adds blind-pool configuration.
src/layout_pool.rs Adds runtime-layout pool storage.
src/geometry.rs Centralizes slot geometry calculations.
src/pool.rs Generalizes pool internals and fixes teardown provenance.
src/builder.rs Constructs typed geometry and records pool address.
src/chunk.rs Delegates layout arithmetic to geometry providers.
src/error.rs Generalizes allocator-failure wording.
src/lib.rs Exports the new public API.
tests/blind_pool.rs Covers blind-pool behavior and failure paths.
tests/bolero_blind_pool.rs Adds heterogeneous property testing.
tests/send_bound_probe.rs Exercises relaxed Send semantics.
tests/stats.rs Tests aggregate blind-pool statistics.
tests/smart_ptr.rs Pins the pool’s !Sync contract.
tests/pool.rs Updates allocator error expectations.
tests/alloc_tracking.rs Checks allocation-free steady-state reuse.
benches/criterion/main.rs Registers Criterion blind-pool benchmarks.
benches/criterion/ops.rs Defines Criterion workloads.
benches/gungraun/linux.rs Registers instruction-count benchmarks.
benches/gungraun/ops.rs Defines Gungraun workloads.
scripts/perf_report.rs Adds blind-pool report rows.
docs/PERF.md Records updated performance results.
docs/TODO.md Documents potential follow-up work.
docs/DESIGN.md Restructures the architecture overview.
docs/IMPLEMENTATION.md Adds the implementation hub.
docs/design/allocation.md Documents allocation and failure behavior.
docs/design/blind-pool.md Describes the blind-pool design.
docs/design/concurrency.md Describes concurrency guarantees.
docs/design/handles.md Documents handle semantics.
docs/design/memory.md Documents memory organization.
docs/implementation/blind-pool.md Explains routing and reentrancy.
docs/implementation/geometry.md Explains geometry derivation.
docs/implementation/handles.md Describes handle implementation.
docs/implementation/performance.md Documents the cost model.
docs/implementation/pool-body.md Documents shared pool internals.
docs/implementation/verification.md Describes verification strategy.
README.md Regenerates crate-level documentation.
CHANGELOG.md Records the feature and soundness fix.
Suppressed comments (1)

crates/plurality/docs/implementation/handles.md:94

  • This explanation is based on the nonexistent _not_send_sync field above. In the implementation, the PhantomData<&Pool<T, A>> marker both carries the lifetime/type parameters and denies Send because Pool is !Sync.
`_pool` carries the borrow lifetime and mentions both type parameters, which a
struct must do for every parameter it declares. `_not_send_sync` denies `Send`
and `Sync` explicitly, using the same marker type `Rc` uses. The denial has to
be explicit: a bare `PhantomData<&'pool ()>` supplies the lifetime but is
`Send` and `Sync`, which would silently make the bound owner thread-mobile

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/plurality/src/multi_pool.rs
Comment thread crates/plurality/src/geometry.rs
Comment thread crates/plurality/docs/implementation/verification.md Outdated
Comment thread crates/plurality/docs/IMPLEMENTATION.md Outdated
Comment thread crates/plurality/src/pool.rs Outdated
Comment thread crates/plurality/docs/implementation/geometry.md Outdated
Comment thread crates/plurality/docs/TODO.md Outdated
Comment thread crates/plurality/docs/implementation/pool-body.md Outdated
Comment thread crates/plurality/docs/implementation/handles.md Outdated
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (cd27014) to head (c7a1834).

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #660    +/-   ##
========================================
  Coverage   100.0%   100.0%            
========================================
  Files         503      508     +5     
  Lines       57407    58023   +616     
========================================
+ Hits        57407    58023   +616     
Flag Coverage Δ
linux 89.6% <100.0%> (-10.4%) ⬇️
linux-arm 89.2% <100.0%> (-10.8%) ⬇️
scheduled ?
windows 89.8% <100.0%> (-10.2%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sandersaares Sander Saares (sandersaares) changed the title plurality: BlindPool, an object pool for values of any type feat(plurality): BlindPool, an object pool for values of any type Aug 12, 2026
Applies rustfmt and drops the spellings the workspace dictionary rejects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Comment thread crates/plurality/src/blind_builder.rs Outdated
Comment thread crates/plurality/src/blind_pool.rs Outdated
Comment thread crates/plurality/src/blind_pool.rs Outdated
Comment thread crates/plurality/src/geometry.rs Outdated
Nine claims in the guides described a design the code does not have. Each is
corrected to what the implementation does.

The typed geometry's compile-time check is forced from the constructor, which is
the only way to obtain the type, not from each accessor. The layering diagram
had `PoolInner`'s generic parameters reversed. The bound owner carries one
phantom that both ties it to the pool borrow and denies `Send` and `Sync`
through the pool's own thread affinity, not a separate marker field. The typed
builder and the layout pool obtain their metadata block by different routes,
because one must abort and the other must report. `BlindPool`'s caps bound
memory in chunks rather than in bytes, since the chunk byte target is a target
and not a ceiling.

The verification guide claimed the source tree carries no unit tests, which the
geometry module contradicts; the exception is now stated along with why it
exists. That module also gains the workspace's coverage exclusion, so its
test-only lines stop counting against the production coverage gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
The builder's chunk sizing and the blind pool's `Send` justification described
themselves by pointing at the typed pool, which forces the reader to look
elsewhere to learn what the blind pool does. Both now make their own argument.

`Debug` names types with `type_name`, matching the workspace convention and
surviving renames.

The geometry module's two lint suppressions move from the file to the four
methods that need them, so a later addition cannot inherit a justification that
does not apply to it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Replace the file-level `multiple_unsafe_ops_per_block` suppressions with
per-block expectations. A file-level suppression silently covers code
written later that its justification was never meant to excuse; a per-block
attribute names the invariant or the indivisible step that puts those
particular operations in one block, and the compiler rejects it where it
does not apply.

Describe the blind pool without reference to the typed pool, so its
contract stands on its own rather than sending the reader elsewhere to
assemble it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Audit every checkable claim the design and implementation guides make about
the source, rather than only the ones a reviewer happened to name. The
corrections span reference-count ownership, thread mobility, mutable access
under pinning, teardown's role, chunk-size clamp rationale, metadata failure
behavior, and the public surface listing.

Two claims described verification that does not exist: loom models for the
blind pool, and an allocation check over every pooled benchmark body. The
first is withdrawn — the router is `!Sync` and its layout pools run the same
protocol the typed models already explore. The second is made true instead,
by covering the blind pool's fat-pointer body alongside the typed one.

Spell in US English throughout, matching the sources.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
The crate had no runnable examples. Five programs now cover the
scenarios the API documentation can only gesture at:

- pool_basic: the four handle flavors, address stability, slot reuse.
- pool_across_threads: a Mutex-shared pool, slots reclaimed from
  worker threads, and a non-Send value in a Send pool.
- blind_pool_basic: values of unrelated types in one pool, layout
  routing, per-layout capacity.
- blind_pool_dyn_dispatch: a pipeline of differently sized trait
  objects backed by a single pool.
- blind_pool_tuning: chunk sizing, capacity bounds, graceful
  exhaustion.

The crate documentation indexes them, so the generated README carries
the list too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Published 20 findings. 2 findings follow up on existing discussion threads.

See diagnostics
Diagnostic Value
Cache Hit

Comment thread crates/plurality/benches/plurality_ops_common/mod.rs
Comment thread crates/plurality/src/lib.rs Outdated
Comment thread crates/plurality/docs/design/blind-pool.md Outdated
Comment thread crates/plurality/docs/implementation/blind-pool.md Outdated
Comment thread crates/plurality/docs/design/blind-pool.md Outdated
Comment thread crates/plurality/tests/stats.rs Outdated
Comment thread crates/plurality/src/blind_pool.rs Outdated
Comment thread crates/plurality/docs/implementation/verification.md Outdated
Comment thread crates/plurality/src/blind_builder.rs Outdated
Comment thread crates/plurality/docs/implementation/pool-body.md Outdated
`clippy::useless_borrows_in_formatting` fires on the newer toolchain CI
runs. The format machinery takes its arguments by reference already, so
the explicit borrow adds nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Growth and directory reservation both hold state that a reentrant
allocation could observe mid-update: growth derives a chunk's base slot
index from a count it publishes only afterwards, and directory
reservation holds `&mut` over a vector while calling the global
allocator. Both windows are now latched. A nested fallible allocation is
refused as an allocator failure; a nested introspection read panics,
because introspection has no error channel.

Also addresses the review findings on the blind pool work: borrow-free
introspection through `LayoutPoolRef`, source-neutral allocation failure
wording, a corrected slot-count bound, and documentation and benchmark
consistency fixes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
`clamp_chunk_size` asserted that a one-slot chunk always has a
representable layout. That holds on a 64-bit target, where the compiler
caps an object at far below the `Layout` size ceiling, but not on a
narrow-pointer target, where the largest object comes within the slot
metadata of the ceiling. The assertion would then fire from inside
`try_alloc_*`, which promises to report rather than panic.

The floor is now reported. `LayoutPool::new` returns an allocator
failure for a layout no chunk can hold, and the per-layout sizing
queries report zero for it, which is also the cap query's answer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 13, 2026 15:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/plurality/docs/implementation/verification.md:31

  • This test-organization claim is already false: src/layout_pool.rs:281-318 also contains a unit-test module for sizing layouts that cannot be reached through the public API. Please document both exceptions so the verification guide matches the suite.
Tests are external integration targets, with one exception. That is a
deliberate constraint: it keeps the tests honest about the public surface.

The exception is `src/geometry.rs`, which carries a unit test module. The
geometry formulas take a size and an alignment, not a type, and the property

The verification guide claimed a single exception to the external-target
rule. The sizing floor tests in `layout_pool.rs` are a second one, so both
are now named along with the reason each cannot be driven from outside the
crate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 13, 2026 18:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (3)

crates/plurality/src/lib.rs:33

  • This describes routing as exact alignment matching, but routing_key widens alignments below the slot-metadata alignment, so types such as [u8; 8] and [u16; 4] intentionally share a layout pool. Describe the routing key as slot geometry to keep the public crate documentation consistent with the API's actual capacity-sharing behavior.
    crates/plurality/src/multi_pool.rs:79
  • The routing key is not the value's exact alignment: alignments narrower than the slot metadata are widened and may share a pool. Calling this “exact size and alignment” contradicts routing_key and the layouts() documentation; use slot geometry here instead.
    crates/plurality/src/geometry.rs:55
  • This reference points to a nonexistent “Routing” heading in docs/design/multi-pool.md, so readers cannot find the cited rationale. The relevant section is “Exact sizes, no size classes.”

The crate and type documentation described routing as exact size and
alignment matching, which contradicts the routing key: alignments below
the slot metadata's width are widened, so distinct layouts may share a
pool. The `routing_key` rationale also cited a heading that does not
exist.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 13, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Suppressed comments (10)

crates/plurality/tests/pool.rs:692

  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.
    crates/plurality/tests/pool.rs:713
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.
    crates/plurality/src/geometry.rs:533
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.
    crates/plurality/src/layout_pool.rs:315
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.
    crates/plurality/src/multi_pool.rs:85
  • This public description says every handle supports unsizing, but Alloc is sized-only and has no coercion API. Limit the coercion claim to the three detachable handle types so the documented surface matches the implementation.
    crates/plurality/tests/send_bound_probe.rs:189
  • The name says teardown occurs on the far thread, but this test explicitly verifies that the retained handle causes teardown on the originating thread. Rename it to match the scenario and distinguish it from the following far-thread teardown test.
    crates/plurality/tests/pool.rs:656
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.

This issue also appears in the following locations of the same file:

  • line 692
  • line 713
    crates/plurality/tests/multi_pool.rs:1758
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.
    crates/plurality/src/geometry.rs:505
  • Test code in this repository uses unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.

This issue also appears on line 533 of the same file.
crates/plurality/src/layout_pool.rs:294

  • Test helpers in #[cfg(test)] code should use unwrap() rather than custom expect() messages because the backtrace already identifies the failed setup.

This issue also appears on line 315 of the same file.

The multi pool's type documentation attributed unsizing coercion to every
handle, but the bound owner is sized-only. A send-bound probe was also
named for far-thread teardown while it asserts the opposite: teardown
deferred back to the originating thread.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 13, 2026 19:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Test code takes `unwrap` rather than `expect`, since the backtrace already
identifies the failed setup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 14, 2026 05:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Force the routing helper inline: measured out of line, the call it
saves costs more than the routing it wraps. Re-derive the routed
intercept from the current build, separating the benchmark harness's
own overhead from the 25 instructions routing actually costs a caller,
and correct the claim that a loaded stride is free.

Assert the directory reservation in `PoolCore::grow`, matching the
assertion the multi pool already makes at the structurally identical
push. Drop the `Pool::occupy_local` forwarder, which used no `self`,
in favour of the free function it wrapped. Describe the reservation's
growth policy accurately in the reentrancy guide. Record the packed
directory key as future work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
…ool-design

# Conflicts:
#	crates/plurality/README.md
#	crates/plurality/docs/DESIGN.md
#	crates/plurality/docs/PERF.md
#	crates/plurality/docs/TODO.md
#	crates/plurality/scripts/perf_report.rs
Copilot AI review requested due to automatic review settings August 14, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

The performance report prices capabilities, but type erasure appeared in it
only as one row inside the fat-pointer comparison, where it is confounded
with unsizing and a virtual call. Add a section that measures it directly,
against the typed pool doing the same work, at one layout and at sixteen.

Wall clock does not resolve the directory scan: sixteen layouts lands within
a percent of one, because the scan is a predictable walk over a contiguous
vector that the processor overlaps with the pool's own pointer chasing, and
what remains is below the swing that heap and code placement produce. The
report therefore prices type erasure at the step from the typed pool rather
than at the difference between layout counts, and the implementation guide
and the packed-key backlog entry record why the scan is judged in
instructions instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6085a7e-d13f-4f5d-a980-86c36499eb7e
Copilot AI review requested due to automatic review settings August 14, 2026 07:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants