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

Funnel: Parallel Fused Hylomorphism

The funnel executor parallelizes a fused hylomorphism — an unfold (tree traversal) composed with a fold (bottom-up accumulation) where the intermediate tree is never materialized. Children are discovered one at a time through a push-based callback, processed concurrently across worker threads, and their results flow back to the parent through defunctionalized continuations.

What a fused hylomorphism is

A hylomorphism composes an unfold (anamorphism) with a fold (catamorphism). The unfold generates a tree structure from a seed; the fold consumes it bottom-up. When fused, the two interleave: each node is produced, its children recursively processed, and their results accumulated — without materializing the tree.

In hylic terms: a Treeish<N> (the coalgebra) exposes visit(&node, |child| ...) and a Fold<N, H, R> (the algebra, factored as init/accumulate/finalize) provides the per-node bracket. The executor calls visit to discover children, recursively processes each, accumulates their R results into the parent’s H heap, and finalizes. The intermediate tree is never materialized as a data structure.

The funnel parallelizes this: children beyond the first are pushed to a work-stealing queue. Worker threads steal and process subtrees concurrently. Results flow back through continuations to the parent’s accumulator. The challenge is coordinating the fold — detecting when all children are done, accumulating their results, and cascading upward — without locks, without allocation on the critical path.

Design values

Four properties define the funnel’s design:

%3iterIterator-basedtraversal(push callback)parFullyparallel(work-stealing)iter->parfusedFusedunfold+fold(no tree built)par->fusedzeroZero allochot path(arenas + inline)fused->zero

  1. Iterator-based traversal. The graph exposes visit(&node, |child| ...). Children arrive one at a time. There is no children(&node) -> Vec<N>.

  2. Fully parallel. Each child beyond the first is pushed to a work-stealing queue. Workers steal and process subtrees concurrently. The first child is walked inline — zero queue overhead on the DFS spine.

  3. Fused unfold+fold. Results accumulate into the parent as they arrive (streaming) or in bulk by the last thread (finalize). The tree is never materialized.

  4. Zero allocation on the hot path. Tasks are enum variants stored inline in deque slots. Multi-child accumulators are arena-allocated. Single-child nodes carry their heap inside the continuation. No Box<dyn FnOnce>, no Arc per task.

Where funnel sits

ExecutorParallelismUnfold/fold fusionTask reprAllocation
Fusednonefully fusedstack frameszero
FunnelCPS + work-stealingfully fusedFunnelTask enumarenas

Fused is the sequential baseline — zero overhead, callback-based recursion on a single thread. Funnel preserves the fused property while adding parallelism through CPS (continuation-passing style) and work-stealing queues. The fold/graph are unchanged between the two — only the executor differs.

Both use the same Exec<D, S> type-level pattern. Funnel’s policy system is an instance of the generic Spec → Store → Handle pattern for zero-cost executor configuration.

Module map

The funnel’s code is organized into four clusters:

%3cluster_cpscps/cluster_dispatchdispatch/cluster_policypolicy/cluster_queuequeue/cluster_accaccumulate/cluster_wakewake/cluster_infrainfra/walkwalk.rswalk_cpsfire_contcontcont.rsFunnelTaskCont, ChainNodewalk->contchainchain.rsFoldChainticket systemwalk->chainworkerworker.rsWorkerCtxworker_loopwalk->workercont->chainarenaarena.rsArena<T>(wraps slab)cont->arenacont_arenacont_arena.rsContArena<T>(wraps slab)cont->cont_arenarunmod.rsrun_foldrun->walkrun->workerpoolpool.rsPool, JobPoolStatedispatch()run->poolworker->walkqmodmod.rsWorkStealingTaskOpsworker->qmodviewview.rsFoldViewpmodmod.rsFunnelPolicyPolicy<Q,A,W>pmod->qmodamodmod.rsAccumulateStrategypmod->amodwmodmod.rsWakeStrategypmod->wmodpwper_worker.rsqmod->pwshshared.rsqmod->shdequedeque.rsWorkerDeque<T>pw->dequeslabsegmented_slab.rsSegmentedSlab<T>slab->arenaslab->cont_arenaeceventcount.rsEventCountpool->ecmodrsmod.rsSpec<P>Session<P>modrs->runmodrs->pmodmodrs->pool

Three behavioral axes

The funnel is parameterized along three independent axes, all resolved at compile time through the FunnelPolicy trait:

#![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;
}
}

Each axis is a trait with its own Spec, Store/State, and implementations. The Policy<Q, A, W> struct bundles any combination:

#![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;
}
}

Named presets are type aliases:

#![allow(unused)]
fn main() {
// ── Named presets (type aliases) ─────────────────
//
// Each is a concrete instantiation of Policy<Q, A, W>.
// `Default` aliases the robust all-rounder; other names describe
// the workload shape they're tuned for.

/// PerWorker + OnFinalize + EveryPush. The robust all-rounder.
pub type Robust = Policy;

/// The default policy. Alias for Robust.
pub type Default = Robust;

/// Same axes as Default. Distinguished by Spec configuration (larger arenas).
pub type GraphHeavy = Robust;

/// Shared + OnArrival + EveryPush. Wide trees (bf=20+).
pub type WideLight = Policy<queue::Shared, accumulate::OnArrival>;

/// PerWorker + OnFinalize + OncePerBatch. Overhead-sensitive (noop-like).
pub type LowOverhead = Policy<queue::PerWorker, accumulate::OnFinalize, wake::OncePerBatch>;

/// PerWorker + OnArrival + EveryPush. Streaming sweep with per-worker deques.
pub type PerWorkerArrival = Policy<queue::PerWorker, accumulate::OnArrival>;

/// Shared + OnFinalize + EveryPush.
pub type SharedDefault = Policy<queue::Shared>;

/// PerWorker + OnFinalize + EveryK<4>. Balanced wakeups for heavy workloads.
pub type HighThroughput = Policy<queue::PerWorker, accumulate::OnFinalize, wake::EveryK<4>>;

/// Shared + OnArrival + OncePerBatch.
pub type StreamingWide = Policy<queue::Shared, accumulate::OnArrival, wake::OncePerBatch>;

/// PerWorker + OnFinalize + EveryK<2>. For deep narrow trees (bf=2).
pub type DeepNarrow = Policy<queue::PerWorker, accumulate::OnFinalize, wake::EveryK<2>>;
````<div class="mdbook-graphviz-output"><!-- Generated by graphviz version 2.43.0 (0) --><!-- Title: %3 Pages: 1 --><svg width="671pt" height="188pt" viewBox="0.00 0.00 671.00 188.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184)"><title>%3</title><polygon fill="white" stroke="transparent" points="-4,4 -4,-184 667,-184 667,4 -4,4"/><!-- policy --><g id="node1" class="node"><title>policy</title><path fill="#e2d9f3" stroke="black" d="M352,-180C352,-180 255,-180 255,-180 249,-180 243,-174 243,-168 243,-168 243,-156 243,-156 243,-150 249,-144 255,-144 255,-144 352,-144 352,-144 358,-144 364,-150 364,-156 364,-156 364,-168 364,-168 364,-174 358,-180 352,-180"/><text text-anchor="middle" x="303.5" y="-165" font-family="sans-serif" font-size="10.00">FunnelPolicy</text><text text-anchor="middle" x="303.5" y="-154" font-family="sans-serif" font-size="10.00">(one type parameter)</text></g><!-- q --><g id="node2" class="node"><title>q</title><path fill="#cce5ff" stroke="black" d="M192,-108C192,-108 135,-108 135,-108 129,-108 123,-102 123,-96 123,-96 123,-84 123,-84 123,-78 129,-72 135,-72 135,-72 192,-72 192,-72 198,-72 204,-78 204,-84 204,-84 204,-96 204,-96 204,-102 198,-108 192,-108"/><text text-anchor="middle" x="163.5" y="-93" font-family="sans-serif" font-size="10.00">Queue</text><text text-anchor="middle" x="163.5" y="-82" font-family="sans-serif" font-size="10.00">WorkStealing</text></g><!-- policy&#45;&gt;q --><g id="edge1" class="edge"><title>policy&#45;&gt;q</title><path fill="none" stroke="black" d="M269.25,-143.88C250.44,-134.47 226.92,-122.71 206.83,-112.67"/><polygon fill="black" stroke="black" points="208.39,-109.53 197.88,-108.19 205.26,-115.79 208.39,-109.53"/></g><!-- a --><g id="node3" class="node"><title>a</title><path fill="#fff3cd" stroke="black" d="M349.5,-108C349.5,-108 257.5,-108 257.5,-108 251.5,-108 245.5,-102 245.5,-96 245.5,-96 245.5,-84 245.5,-84 245.5,-78 251.5,-72 257.5,-72 257.5,-72 349.5,-72 349.5,-72 355.5,-72 361.5,-78 361.5,-84 361.5,-84 361.5,-96 361.5,-96 361.5,-102 355.5,-108 349.5,-108"/><text text-anchor="middle" x="303.5" y="-93" font-family="sans-serif" font-size="10.00">Accumulate</text><text text-anchor="middle" x="303.5" y="-82" font-family="sans-serif" font-size="10.00">AccumulateStrategy</text></g><!-- policy&#45;&gt;a --><g id="edge2" class="edge"><title>policy&#45;&gt;a</title><path fill="none" stroke="black" d="M303.5,-143.7C303.5,-135.98 303.5,-126.71 303.5,-118.11"/><polygon fill="black" stroke="black" points="307,-118.1 303.5,-108.1 300,-118.1 307,-118.1"/></g><!-- w --><g id="node4" class="node"><title>w</title><path fill="#d4edda" stroke="black" d="M520.5,-108C520.5,-108 460.5,-108 460.5,-108 454.5,-108 448.5,-102 448.5,-96 448.5,-96 448.5,-84 448.5,-84 448.5,-78 454.5,-72 460.5,-72 460.5,-72 520.5,-72 520.5,-72 526.5,-72 532.5,-78 532.5,-84 532.5,-84 532.5,-96 532.5,-96 532.5,-102 526.5,-108 520.5,-108"/><text text-anchor="middle" x="490.5" y="-93" font-family="sans-serif" font-size="10.00">Wake</text><text text-anchor="middle" x="490.5" y="-82" font-family="sans-serif" font-size="10.00">WakeStrategy</text></g><!-- policy&#45;&gt;w --><g id="edge3" class="edge"><title>policy&#45;&gt;w</title><path fill="none" stroke="black" d="M349.25,-143.88C376.33,-133.74 410.72,-120.87 438.78,-110.36"/><polygon fill="black" stroke="black" points="440.29,-113.53 448.42,-106.75 437.83,-106.98 440.29,-113.53"/></g><!-- pw --><g id="node5" class="node"><title>pw</title><path fill="#cce5ff" stroke="black" d="M99,-36C99,-36 12,-36 12,-36 6,-36 0,-30 0,-24 0,-24 0,-12 0,-12 0,-6 6,0 12,0 12,0 99,0 99,0 105,0 111,-6 111,-12 111,-12 111,-24 111,-24 111,-30 105,-36 99,-36"/><text text-anchor="middle" x="55.5" y="-20.8" font-family="sans-serif" font-size="9.00">PerWorker</text><text text-anchor="middle" x="55.5" y="-10.8" font-family="sans-serif" font-size="9.00">Chase&#45;Lev + bitmask</text></g><!-- q&#45;&gt;pw --><g id="edge4" class="edge"><title>q&#45;&gt;pw</title><path fill="none" stroke="black" d="M137.08,-71.88C123.09,-62.81 105.72,-51.55 90.61,-41.76"/><polygon fill="black" stroke="black" points="92.32,-38.69 82.02,-36.19 88.51,-44.57 92.32,-38.69"/></g><!-- shared --><g id="node6" class="node"><title>shared</title><path fill="#cce5ff" stroke="black" d="M185.5,-36C185.5,-36 141.5,-36 141.5,-36 135.5,-36 129.5,-30 129.5,-24 129.5,-24 129.5,-12 129.5,-12 129.5,-6 135.5,0 141.5,0 141.5,0 185.5,0 185.5,0 191.5,0 197.5,-6 197.5,-12 197.5,-12 197.5,-24 197.5,-24 197.5,-30 191.5,-36 185.5,-36"/><text text-anchor="middle" x="163.5" y="-20.8" font-family="sans-serif" font-size="9.00">Shared</text><text text-anchor="middle" x="163.5" y="-10.8" font-family="sans-serif" font-size="9.00">StealQueue</text></g><!-- q&#45;&gt;shared --><g id="edge5" class="edge"><title>q&#45;&gt;shared</title><path fill="none" stroke="black" d="M163.5,-71.7C163.5,-63.98 163.5,-54.71 163.5,-46.11"/><polygon fill="black" stroke="black" points="167,-46.1 163.5,-36.1 160,-46.1 167,-46.1"/></g><!-- arrive --><g id="node7" class="node"><title>arrive</title><path fill="#fff3cd" stroke="black" d="M297.5,-36C297.5,-36 227.5,-36 227.5,-36 221.5,-36 215.5,-30 215.5,-24 215.5,-24 215.5,-12 215.5,-12 215.5,-6 221.5,0 227.5,0 227.5,0 297.5,0 297.5,0 303.5,0 309.5,-6 309.5,-12 309.5,-12 309.5,-24 309.5,-24 309.5,-30 303.5,-36 297.5,-36"/><text text-anchor="middle" x="262.5" y="-20.8" font-family="sans-serif" font-size="9.00">OnArrival</text><text text-anchor="middle" x="262.5" y="-10.8" font-family="sans-serif" font-size="9.00">streaming sweep</text></g><!-- a&#45;&gt;arrive --><g id="edge6" class="edge"><title>a&#45;&gt;arrive</title><path fill="none" stroke="black" d="M293.37,-71.7C288.65,-63.64 282.94,-53.89 277.72,-44.98"/><polygon fill="black" stroke="black" points="280.59,-42.96 272.52,-36.1 274.55,-46.5 280.59,-42.96"/></g><!-- finalize --><g id="node8" class="node"><title>finalize</title><path fill="#fff3cd" stroke="black" d="M383.5,-36C383.5,-36 339.5,-36 339.5,-36 333.5,-36 327.5,-30 327.5,-24 327.5,-24 327.5,-12 327.5,-12 327.5,-6 333.5,0 339.5,0 339.5,0 383.5,0 383.5,0 389.5,0 395.5,-6 395.5,-12 395.5,-12 395.5,-24 395.5,-24 395.5,-30 389.5,-36 383.5,-36"/><text text-anchor="middle" x="361.5" y="-20.8" font-family="sans-serif" font-size="9.00">OnFinalize</text><text text-anchor="middle" x="361.5" y="-10.8" font-family="sans-serif" font-size="9.00">bulk sweep</text></g><!-- a&#45;&gt;finalize --><g id="edge7" class="edge"><title>a&#45;&gt;finalize</title><path fill="none" stroke="black" d="M317.84,-71.7C324.79,-63.3 333.27,-53.07 340.9,-43.86"/><polygon fill="black" stroke="black" points="343.64,-46.04 347.33,-36.1 338.25,-41.57 343.64,-46.04"/></g><!-- every --><g id="node9" class="node"><title>every</title><path fill="#d4edda" stroke="black" d="M465.5,-36C465.5,-36 425.5,-36 425.5,-36 419.5,-36 413.5,-30 413.5,-24 413.5,-24 413.5,-12 413.5,-12 413.5,-6 419.5,0 425.5,0 425.5,0 465.5,0 465.5,0 471.5,0 477.5,-6 477.5,-12 477.5,-12 477.5,-24 477.5,-24 477.5,-30 471.5,-36 465.5,-36"/><text text-anchor="middle" x="445.5" y="-15.8" font-family="sans-serif" font-size="9.00">EveryPush</text></g><!-- w&#45;&gt;every --><g id="edge8" class="edge"><title>w&#45;&gt;every</title><path fill="none" stroke="black" d="M479.38,-71.7C474.14,-63.56 467.8,-53.69 462.02,-44.7"/><polygon fill="black" stroke="black" points="464.85,-42.62 456.5,-36.1 458.96,-46.41 464.85,-42.62"/></g><!-- once --><g id="node10" class="node"><title>once</title><path fill="#d4edda" stroke="black" d="M563.5,-36C563.5,-36 507.5,-36 507.5,-36 501.5,-36 495.5,-30 495.5,-24 495.5,-24 495.5,-12 495.5,-12 495.5,-6 501.5,0 507.5,0 507.5,0 563.5,0 563.5,0 569.5,0 575.5,-6 575.5,-12 575.5,-12 575.5,-24 575.5,-24 575.5,-30 569.5,-36 563.5,-36"/><text text-anchor="middle" x="535.5" y="-15.8" font-family="sans-serif" font-size="9.00">OncePerBatch</text></g><!-- w&#45;&gt;once --><g id="edge9" class="edge"><title>w&#45;&gt;once</title><path fill="none" stroke="black" d="M501.62,-71.7C506.86,-63.56 513.2,-53.69 518.98,-44.7"/><polygon fill="black" stroke="black" points="522.04,-46.41 524.5,-36.1 516.15,-42.62 522.04,-46.41"/></g><!-- everyk --><g id="node11" class="node"><title>everyk</title><path fill="#d4edda" stroke="black" d="M651,-36C651,-36 606,-36 606,-36 600,-36 594,-30 594,-24 594,-24 594,-12 594,-12 594,-6 600,0 606,0 606,0 651,0 651,0 657,0 663,-6 663,-12 663,-12 663,-24 663,-24 663,-30 657,-36 651,-36"/><text text-anchor="middle" x="628.5" y="-15.8" font-family="sans-serif" font-size="9.00">EveryK&lt;K&gt;</text></g><!-- w&#45;&gt;everyk --><g id="edge10" class="edge"><title>w&#45;&gt;everyk</title><path fill="none" stroke="black" d="M524.26,-71.88C542.72,-62.51 565.78,-50.81 585.52,-40.8"/><polygon fill="black" stroke="black" points="587.28,-43.83 594.61,-36.19 584.11,-37.59 587.28,-43.83"/></g></g></svg></div>



See [Policies](policies.md) for the full decision guide and
benchmark-informed recommendations.

# Reading order

|Page|What you learn|
|----|--------------|
|[CPS walk](cps_walk.md)|The downward pass: how nodes are processed and tasks created|
|[Continuations](continuations.md)|`FunnelTask`, `Cont`, `ChainNode`, `RootCell` — the CPS data types|
|[Cascade](cascade.md)|`fire_cont`: the trampolined upward pass|
|[Ticket system](ticket_system.md)|Packed `AtomicU64` for exactly-one-finalizer detection|
|[Pool and dispatch](pool_dispatch.md)|Thread pool, `Job` struct, the `dispatch()` CPS lifecycle|
|[Queue strategies](queue_strategies.md)|PerWorker (Chase-Lev + bitmask) vs Shared (StealQueue)|
|[Accumulation](accumulation.md)|OnArrival (streaming sweep) vs OnFinalize (bulk)|
|[Policies](policies.md)|`FunnelPolicy` GAT, three axes, named presets, decision guide|
|[Infrastructure](infrastructure.md)|Arena, ContArena, WorkerDeque, EventCount|
|[Testing](testing.md)|Correctness, stress, interleaving proof|
}