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

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>. SeedLift is composed at the chain head when .run is called; every stored lift in pre_lift sees SeedNode<N> as its input.

Type evolution

After three sugars on a TreeishPipeline<Shared, u64, u64, u64>:

%3aTreeishPipeline<Shared, u64, u64, u64>bStage2Pipeline<  TreeishPipeline<..>,  ComposedLift<Identity, WrapInit>>a->b.wrap_init(…)cStage2Pipeline<  TreeishPipeline<..>,  ComposedLift<    ComposedLift<Identity, WrapInit>,    ZipMap  >>b->c.zipmap(…)dStage2Pipeline<  TreeishPipeline<..>,  ComposedLift<    ComposedLift<      ComposedLift<Identity, WrapInit>,      ZipMap    >,    FilterEdges  >>c->d.filter_edges(…)

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.