Domain integration
The domain system lets executors accept folds without knowing their
concrete storage. The Domain trait maps a marker type to a concrete
Fold type via a GAT. The graph type is a separate concern — the
Executor trait accepts any G: TreeOps<N>, with per-executor
bounds checked at the call site.
The Domain trait
The Domain trait provides a single associated type — the fold:
#![allow(unused)]
fn main() {
pub trait Domain<N: 'static>: 'static {
type Fold<H: 'static, R: 'static>: FoldOps<N, H, R>;
type Graph<E: 'static> where E: 'static;
type Grow<Seed: 'static, NOut: 'static>;
/// Construct a fold from three closures. Uniform Send+Sync
/// bound; each domain sheds Send+Sync at storage time if it
/// doesn't need it.
fn make_fold<H: 'static, R: 'static>(
init: impl Fn(&N) -> H + Send + Sync + 'static,
acc: impl Fn(&mut H, &R) + Send + Sync + 'static,
fin: impl Fn(&H) -> R + Send + Sync + 'static,
) -> Self::Fold<H, R>;
/// Construct a grow closure from a Fn. Uniform Send+Sync bound.
fn make_grow<Seed: 'static, NOut: 'static>(
f: impl Fn(&Seed) -> NOut + Send + Sync + 'static,
) -> Self::Grow<Seed, NOut>;
/// Invoke a stored grow closure.
fn invoke_grow<Seed: 'static, NOut: 'static>(
g: &Self::Grow<Seed, NOut>,
s: &Seed,
) -> NOut;
/// Construct a graph (Edgy) closure. Uniform Send+Sync bound.
fn make_graph<E: 'static>(
visit: impl Fn(&N, &mut dyn FnMut(&E)) + Send + Sync + 'static,
) -> Self::Graph<E>;
}
}
Each domain marker (Shared, Local, Owned) implements this
trait with a different closure boxing strategy:
| Domain | Fold<H, R> storage | Send+Sync |
|---|---|---|
| Shared | Arc<dyn Fn + Send + Sync> | yes |
| Local | Rc<dyn Fn> | no |
| Owned | Box<dyn Fn> | no |
Graph types are domain-independent. Treeish<N> and Edgy<N, E>
in hylic::graph are always Arc-based (they need Clone for graph
composition). Any type implementing TreeOps<N> can serve as a
graph, including user-defined structs with no boxing at all.
The Executor trait
The executor trait has four type parameters: N (node), R
(result), D (domain), and G (graph):
#![allow(unused)]
fn main() {
/// Run a fold on a tree. Both Specs and Sessions implement this.
///
/// The fold is domain-specific (`D::Fold<H, R>`). The graph type G
/// is a trait-level parameter — each executor impl declares its own
/// bounds on G (e.g. Fused accepts any TreeOps, Funnel requires
/// Send+Sync). The compiler checks G at the call site.
pub trait Executor<N: 'static, R: 'static, D: Domain<N>, G: TreeOps<N> + 'static> {
/// Run the given `fold` over the `graph` starting at `root` and
/// return the fold's final result for the root.
fn run<H: 'static>(&self, fold: &D::Fold<H, R>, graph: &G, root: &N) -> R;
}
}
The domain D determines the fold type (D::Fold<H, R>). The graph
type G is constrained per executor implementation. This separation
means the fold’s boxing strategy and the graph’s storage are
independent choices.
The type resolution at a call site proceeds as follows:
The compiler checks that G satisfies the executor’s requirements.
For Fused, any TreeOps<N> suffices. For Funnel, G must also be
Send + Sync (the graph reference is shared across a scoped pool).
If the graph type does not satisfy the executor’s bounds, the call
site produces a compile error.
Why D is on the executor, not the fold
Fold<N, H, R> carries no domain parameter — the domain lives on
the executor: Exec<D, S>. This resolves a type inference problem:
GATs are not injective (D::Fold<H, R> does not uniquely identify
D), so the compiler cannot infer D from a fold argument alone.
With D fixed by the executor constant or exec() call, everything
resolves statically.
Domain compatibility
| Shared | Local | Owned | |
|---|---|---|---|
| Fused | yes | yes | yes |
| Funnel | yes | — | — |
Fused supports all domains because it borrows both fold and graph
on a single thread. Funnel requires N: Clone + Send and R: Send
on the fold’s types, which the Shared domain satisfies. The graph
must additionally be Send + Sync.
The FoldOps trait
Executors do not call fold methods through the concrete domain type.
They operate through the FoldOps<N, H, R> trait, which all domain
Fold types implement:
The executor’s recursion engine takes &impl FoldOps<N, H, R> —
fully monomorphized for the concrete fold type, with no runtime
dispatch beyond the closure’s own vtable.