pipeline-dsl 0.7.0

Static #[pipeline]/#[stage] DSL over the pipeline-core value layer.
Documentation

pipeline-dsl

The static front-end of the pipeline family: write ordinary functions, annotate them, and let the #[pipeline] macro derive a dependency DAG and a deterministically ordered compute().

Functions + names ⇒ DAG ⇒ deterministic compute. Edges are inferred from name-based parameter binding and mutability (&T reads, &mut T writes); the macro enforces single-writer, detects missing producers, topologically sorts, and emits crisp compile-time errors.

Setup

A single dependency — pipeline-dsl re-exports both the macros and the value layer:

[dependencies]
pipeline-dsl = "0.7"

(Optionally also depend on pipeline-core to refer to the value types under the shared pipeline:: name; they're the same re-exported types.)

Quick start

use pipeline_dsl::{pipeline, stage};

#[derive(Default)]
struct Db { count: i32 }
#[derive(Default)]
struct Cache { total: i32 }

#[pipeline(name = "App", context = "db, cache")]
mod app {
    use super::*;

    #[stage]
    pub fn tick(db: &mut Db) { db.count += 1; }

    #[stage]
    pub fn sum(cache: &mut Cache, db: &Db) {
        cache.total += db.count; // reads db (after tick), writes cache
    }
}

fn main() -> Result<(), pipeline::Error> {
    let mut app = App::new();
    let (mut db, mut cache) = (Db::default(), Cache::default());
    app.compute(&mut db, &mut cache)?; // stages run in dependency order
    Ok(())
}

Runtime value types (Vector, Value, Buckets, Reset) are re-exported by this crate, so you can name them as pipeline_dsl::Vector etc., and the code generated by #[pipeline] resolves its runtime support through pipeline-dsl — no second dependency required.

Attribute summary

  • #[pipeline(name="…", args="…", context="…", external="…", error="…", controlflow_break="…", constructor="…")] — declares the pipeline container; args/context/external describe inputs.
  • #[stage] — marks a function as a stage. Parameter attributes: #[rename = "field"], #[skip_reset], #[unused], #[state].

Parameter kinds

A stage parameter is one base kind (where its storage lives and who fills it):

Kind Owner Reset/cycle Stage access Shared?
args pipeline none &T many readers
context caller none &T / &mut T many
external (external="…") pipeline dirty cleared caller-fed many readers
internal Value<T>/Vector<T> pipeline dirty cleared one writer, N readers yes
#[state] pipeline none one stage, &mut T no (private)

…optionally tweaked by a modifier: #[rename] (bind to a different field name), #[skip_reset] (a slot that persists instead of resetting), #[unused] (keep a parameter that's deliberately disconnected from the graph).

#[state] is the only off-graph kind: per-stage-private, persistent, plain T the pipeline owns and exactly one stage mutates — for an accumulator, cache, or scratch that must survive across cycles. It is never reset, never read by another stage, and does not appear in dot() / html_diagram(). It requires &mut T; sharing one state across two stages is an error (use a Value<T> slot instead).

In the dynamic pipeline-graph front-end there is no #[state]: stages are closures, so a stage simply captures its own persistent mutable state (with a Cell/RefCell for interior mutability). #[state] is the static front-end's equivalent, since its stages are free fns that can't capture.

Constructors

When every field is Default (the usual case), the macro generates Pipeline::new(args…); if there are no args it also derives Default. Each Default-initialized field is bounded by Default, so a field whose type isn't Default produces a clear T: Default is not satisfied error rather than one buried in generated code. In that case, pass constructor = "manual" and write your own constructor:

#[pipeline(name = "P", constructor = "manual")]
mod p { /**/ }

impl P {
    pub fn with_caps(n: usize) -> Self { /* build all fields yourself */ }
}

See the repository for the full guide (binding rules, multiple contexts, generics, diagrams, diagnostics).

Safety

This front-end is fully checked at compile time and uses no unsafe — mis-wiring (a missing producer, two writers, a cycle) is a compile error. The dynamic pipeline-graph front-end trades those compile-time guarantees for runtime flexibility, validating wiring at build() and using a small encapsulated unsafe core. If a fixed graph fits your problem, this crate gives you the stronger guarantees.

Related crates

Part of the pipeline family — a shared value layer with two front-ends:

Crate What it is
pipeline-core the value layer (Value/Vector/Buckets + Reset), imported as pipeline
pipeline-dsl static front-end: derive the graph at compile time with #[pipeline]/#[stage]
pipeline-graph dynamic front-end: wire the graph at runtime (Graph, Input/Output)

Need the graph wired at runtime (stages/implementations chosen dynamically)? See pipeline-graph.

License

MIT OR Apache-2.0.