Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Policy Traits: Zero-Cost Configuration

Funnel’s three behavioral axes (queue, accumulation, wake) are each a trait with an associated Spec type. The FunnelPolicy bundle combines them into one type parameter. This pattern — Spec → Store/State → Handle, resolved at compile time — is the general recipe for adding zero-overhead configuration axes to any executor.

This page describes the pattern generically. For the concrete implementations (Chase-Lev deques, streaming sweep, etc.), see the Funnel section.

Specs as data

Every Spec in hylic is Copy — a small value type that fully describes configuration. This follows from the defunctionalization principle: Specs are data, not behavior. Combining Specs via axis transformations produces new Specs. Attaching a resource to a Spec produces a Session. Running a Spec creates the resource internally.

The policy sub-specs (PerWorkerSpec, OnFinalizeSpec, EveryKSpec, etc.) are all Copy + Default + Send + Sync. Most are ZSTs. The funnel Spec<P> composes them and is itself Copy (~40 bytes of usizes and ZSTs).

The Spec → Store → Handle pattern

Each axis follows the same three-phase lifecycle:

%3trait_Trait(e.g. WorkStealing)associated types:Spec, Store, HandlespecSpecconstruction config(Copy + Default)trait_->spectype SpecstoreStore<N, H, R>per-fold resources(Send + Sync)trait_->storetype StorehandleHandle<'a, N, H, R>per-worker view(borrows Store)trait_->handletype Handlespec->storecreate_store()store->handlehandle()

Three associated types capture the lifecycle:

  1. Spec — construction-time configuration. Carried in the executor’s Spec<P>. Small, Copy, Default.
  2. Store — per-fold resources created from the Spec. Owned by the fold’s stack frame. Send+Sync (shared across workers).
  3. Handle — per-worker view that borrows from the Store. Has the actual push/pop/steal methods.

All three use GATs to carry the task’s generic parameters without boxing.

Concrete example: WorkStealing

#![allow(unused)]
fn main() {
/// A work-stealing strategy. Associates typed Store and Handle via GATs.
pub trait WorkStealing: 'static {
    type Spec: Copy + Default + Send + Sync;
    type Store<N: Send + 'static, H: 'static, R: Send + 'static>: Send + Sync;
    type Handle<'a, N: Send + 'static, H: 'static, R: Send + 'static>: TaskOps<N, H, R>
    where Self: 'a;

    fn create_store<N: Send + 'static, H: 'static, R: Send + 'static>(
        spec: &Self::Spec, n_workers: usize,
    ) -> Self::Store<N, H, R>;

    fn reset_store<N: Send + 'static, H: 'static, R: Send + 'static>(
        store: &mut Self::Store<N, H, R>,
    );

    fn handle<'a, N: Send + 'static, H: 'static, R: Send + 'static>(
        store: &'a Self::Store<N, H, R>, worker_idx: usize,
    ) -> Self::Handle<'a, N, H, R>;
}
}

Two implementations:

PerWorkerShared
SpecPerWorkerSpec { deque_capacity } (Copy)SharedSpec (ZST, Copy)
StoreVec<WorkerDeque> + AtomicU64 bitmaskStealQueue
Handlerefs to own deque + all deques + bitmaskref to queue

Bundling: FunnelPolicy

Three independent axes combined into one type parameter:

#![allow(unused)]
fn main() {
/// Bundles queue topology, accumulation strategy, and wake policy.
/// One type parameter on the executor replaces three.
pub trait FunnelPolicy: 'static {
    type Queue: WorkStealing;
    type Accumulate: AccumulateStrategy;
    type Wake: WakeStrategy;
}
}
#![allow(unused)]
fn main() {
/// Generic policy: any combination of axes. Named presets are type aliases over this.
pub struct Policy<
    Q: WorkStealing = queue::PerWorker,
    A: AccumulateStrategy = accumulate::OnFinalize,
    W: WakeStrategy = wake::EveryPush,
>(PhantomData<(Q, A, W)>);

impl<Q: WorkStealing, A: AccumulateStrategy, W: WakeStrategy> FunnelPolicy for Policy<Q, A, W> {
    type Queue = Q;
    type Accumulate = A;
    type Wake = W;
}
}

Policy<Q, A, W> is the generic implementor. Named presets are type aliases. The funnel Spec<P> carries each axis’s sub-spec:

#![allow(unused)]
fn main() {
pub struct Spec<P: FunnelPolicy = policy::Default> {
    /// Pool size for `.run()` and `.session()`. Not consulted when
    /// attaching to an explicit pool via `.attach()`.
    pub default_pool_size: usize,
    pub queue: <P::Queue as WorkStealing>::Spec,
    pub accumulate: <P::Accumulate as AccumulateStrategy>::Spec,
    pub wake: <P::Wake as WakeStrategy>::Spec,
}
}

Named presets as transformations

Every named preset is a transformation of Spec::default(n). Default values live in ONE place — the default() constructor. Presets compose axis builders on top:

// WideLight = default + Shared queue + OnArrival accumulation
fn for_wide_light(n: usize) -> Spec<WideLight> {
    Spec::default(n)
        .with_queue::<Shared>(SharedSpec)
        .with_accumulate::<OnArrival>(OnArrivalSpec)
}

The axis builders (with_queue, with_accumulate, with_wake) are typestate transformations — they change the Policy type parameter, producing a new Spec type.

How monomorphization flows

The type parameter propagates from Spec to every call site:

%3userSpec<WideLight>= Spec<Policy<Shared,  OnArrival, EveryPush>>run.run()routes throughwith_sessionuser->runstoreP::Queue::create_store()= SharedStorerun->storedeliverP::Accumulate::deliver()= OnArrival::deliver(direct call)run->deliverhandleP::Queue::handle()= SharedHandlestore->handlepushhandle.push(task)= SharedHandle::push(direct call)handle->pushnotifyP::Wake::should_notify()= EveryPush::should_notify(returns true)push->notify

From Spec<WideLight> to the innermost push/deliver/notify — every call is resolved at compile time. No vtable, no trait object, no indirect call.

The const generic optimization

Wake strategies like EveryK<K> use a const generic for the notification interval. The modulus count % K compiles to a bitmask when K is a power of 2 — the compiler sees the constant and optimizes.

Applying the pattern to new axes

To add a fourth axis (e.g., steal ordering):

  1. Define a trait: pub trait StealOrder: 'static { type Spec: Copy + Default + Send + Sync; ... }
  2. Add implementations: struct Fifo;, struct Lifo;
  3. Add to FunnelPolicy: type Steal: StealOrder;
  4. Update Policy<Q, A, W, St> and named presets
  5. Thread through Spec<P> and run_fold

The call chain monomorphizes automatically. No runtime cost for the new axis.