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:
Three associated types capture the lifecycle:
- Spec — construction-time configuration. Carried in the
executor’s
Spec<P>. Small, Copy, Default. - Store — per-fold resources created from the Spec. Owned by the fold’s stack frame. Send+Sync (shared across workers).
- 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:
| PerWorker | Shared | |
|---|---|---|
| Spec | PerWorkerSpec { deque_capacity } (Copy) | SharedSpec (ZST, Copy) |
| Store | Vec<WorkerDeque> + AtomicU64 bitmask | StealQueue |
| Handle | refs to own deque + all deques + bitmask | ref 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:
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):
- Define a trait:
pub trait StealOrder: 'static { type Spec: Copy + Default + Send + Sync; ... } - Add implementations:
struct Fifo;,struct Lifo; - Add to
FunnelPolicy:type Steal: StealOrder; - Update
Policy<Q, A, W, St>and named presets - Thread through
Spec<P>andrun_fold
The call chain monomorphizes automatically. No runtime cost for the new axis.