Stage 2 — Stage2Pipeline
#![allow(unused)]
fn main() {
/// Stage-2 typestate pipeline. Wraps a Stage-1 base with a lift chain.
/// The chain's input N is `<Base::Wrap as Wrap>::Of<UN>` — see the
/// `Stage2Base` and `Wrap` traits in this module.
#[must_use]
pub struct Stage2Pipeline<Base, L = IdentityLift> {
pub(crate) base: Base,
pub(crate) pre_lift: L,
}
}
base is a Stage-1 pipeline. pre_lift is one lift value, but
typically a ComposedLift<L1, L2> tree built up through
.then_lift calls and Stage-2 sugars. Each sugar appends one
node to the tree.
The chain’s input N is determined by the Base via the
Wrap projection on
Stage2Base:
Base = TreeishPipeline<D, N, H, R>—Wrap::Of<N> = N.Base = SeedPipeline<D, N, Seed, H, R>—Wrap::Of<N> = SeedNode<N>.SeedLiftis composed at the chain head when.runis called; every stored lift inpre_liftseesSeedNode<N>as its input.
Type evolution
After three sugars on a TreeishPipeline<Shared, u64, u64, u64>:
Each sugar wraps the previous chain in one more ComposedLift
layer. The base is unchanged. The whole chain monomorphises and
inlines together; there is no per-lift dispatch at runtime.
Entering Stage 2
#![allow(unused)]
fn main() {
let lp = tree_pipeline.lift(); // Stage2Pipeline<TreeishPipeline<..>, IdentityLift>
let lsp = seed_pipeline.lift(); // Stage2Pipeline<SeedPipeline<..>, IdentityLift>
}
TreeishPipeline also auto-lifts: tree_pipeline.wrap_init(w)
calls .lift() internally. SeedPipeline does not — .lift()
must be written explicitly.
Compositional primitives
then_lift — append
#![allow(unused)]
fn main() {
/// Post-compose `outer` onto the chain. Pure struct construction;
/// no bounds. The composition's *meaningfulness* is enforced where
/// the chain is consumed (`.run_*`, `TreeishSource`).
pub fn then_lift<L2>(
self,
outer: L2,
) -> Stage2Pipeline<Base, ComposedLift<L, L2>> {
Stage2Pipeline {
base: self.base,
pre_lift: ComposedLift::compose(self.pre_lift, outer),
}
}
}
L2’s inputs must match the chain tip’s outputs. The new tip
becomes (L2::N2, L2::MapH, L2::MapR). Available on every
Stage2Pipeline<Base, L>.
then_lift is unconstrained at the struct-method level (pure
construction). Validity is enforced where the chain is consumed
— the .run* methods and the TreeishSource impl.
before_lift — prepend (treeish-rooted only)
#![allow(unused)]
fn main() {
/// Pre-compose a type-preserving lift `first` before the chain.
/// `first`'s output (N, H, R) must equal the base's input.
/// For non-type-preserving pre-adaptation, use the variance-aware
/// sugars (`map_node_bi`, `map_r_bi`, `n_lift`, `phases_lift`).
///
/// Available only for treeish-rooted pipelines: seed-rooted
/// chains have `SeedLift` composed at `.run` time as the natural
/// chain head, leaving no meaningful "before" position.
pub fn before_lift<L0>(self, first: L0)
-> Stage2Pipeline<TreeishPipeline<D, N, H, R>, ComposedLift<L0, L>>
where L0: Lift<D, N, H, R>,
D: Domain<L0::N2>,
{
Stage2Pipeline { base: self.base, pre_lift: ComposedLift::compose(first, self.pre_lift) }
}
}
Pre-compose L0 at the head of the chain. L0 must be
type-preserving — its outputs must equal the Base’s inputs —
which restricts L0 to lifts that don’t change (N, H, R)
(filter_edges_lift, wrap_visit_lift, memoize_by_lift are
the practical choices).
Available only on Stage2Pipeline<TreeishPipeline<…>, L>.
Sugars
Stage-2 sugars all delegate to then_lift after building a
ShapeLift through Wrap dispatch. The user’s closures type at
&UN regardless of Base; the seed-rooted case adapts via a
SeedNode::Node(_)-peeling adapter inside the Wrap impl. Full
catalogue: Sugars. Type-level mechanism:
Wrap dispatch.
#![allow(unused)]
fn main() {
#[test]
fn lifted_sugar_chain() {
use hylic_pipeline::prelude::*;
let tp: TreeishPipeline<Shared, u64, u64, u64> = TreeishPipeline::new(
treeish(|n: &u64| if *n > 0 { vec![*n - 1] } else { vec![] }),
&fold(|n: &u64| *n, |h: &mut u64, c: &u64| *h += c, |h: &u64| *h),
);
let r: String = tp
.wrap_init(|n: &u64, orig: &dyn Fn(&u64) -> u64| orig(n) + 1)
.zipmap(|r: &u64| *r > 5)
.filter_edges(|n: &u64| *n != 0)
.map_r_bi(
|r: &(u64, bool)| format!("{}:{}", r.0, r.1),
|s: &String| {
let (a, b) = s.split_once(':').unwrap();
(a.parse().unwrap(), b == "true")
},
)
.run_from_node(&FUSED, &3u64);
// filter_edges drops the 0-step: tree visits 3→2→1, three nodes.
// wrap_init adds +1 each → values 4, 3, 2; sum = 9. zipmap > 5 → true.
assert_eq!(r, "9:true");
}
}
Running
Stage2Pipeline inherits run from its Stage-1 base.
Treeish-rooted: .run_from_node(&exec, &root) — see
TreeishPipeline. Seed-rooted:
.run(&exec, root_seeds, entry_heap) and
.run_from_slice(&exec, &[seed], entry_heap) — see
SeedPipeline. The call shape is unchanged
across stages.