Performance First
A pipeline is a single monomorphized type: composing stages with then builds a nested Then whose run inlines into straight-line calls, so driving a pipeline of any length costs no more than calling the stages by hand — no boxing, no dyn dispatch, no per-phase indirection. The session keeps its error tally incrementally, so error_count and has_errors are O(1) checkpoints a driver can run between every phase. Diagnostic messages are Cow<'static, str>, so a constant message never allocates and a computed one is owned only when it is built.
Latest local Criterion means (cargo bench --bench bench, Linux x86_64 / WSL2, Rust stable, release build):
| Workload | Time |
|---|---|
Double → Increment → Sum pipeline, 16 elements |
~15 ns |
Double → Increment → Sum pipeline, 256 elements |
~100 ns |
Double → Increment → Sum pipeline, 4096 elements |
~1.16 µs |
| Emit 128 diagnostics into a session | ~260 ns |
Numbers vary by CPU and environment; run the suite on your target to establish a baseline. The pipeline benchmark clones its input each iteration, so it includes the allocation the two mapping stages consume.
Features
- Type-checked phase chain —
Pipeline::thenonly accepts a stage whose input is the current output. A pipeline that feeds tokens into a stage expecting an AST does not compile. - One shared session — every stage reads and writes the same
Session: the language's configuration, and the diagnostics accumulated so far. - Diagnostics with severities —
error,warning, andnote, recorded in order; only errors count toward the abort checkpoint. - Clean stops — a stage returns a
DriverErrorto halt the run, and the pipeline stamps in the failing stage's name.Session::abort_if_errorsturns accumulated errors into that stop. - Zero-cost composition — fully monomorphized; no dynamic dispatch on the run path.
no_std— needs onlyalloc; the defaultstdfeature only toggles theno_stdattribute.- Fully safe — no
unsafe,#![forbid(unsafe_code)]. - Property-tested — diagnostic accounting and pipeline value-threading checked across randomized inputs with
proptest.
Installation
[]
= "1"
# no_std:
= { = "1", = false }
# with serde derives on Severity and Diagnostic:
= { = "1", = ["serde"] }
MSRV is 1.85+ (Rust 2024 edition).
Quick Start
Build a driver from stages and run an input through it. Each stage is a phase; the pipeline threads the artifact from one to the next.
use ;
// Phase 1: text -> integers.
;
// Phase 2: integers -> their sum.
;
let mut driver = new.then;
let mut session = new;
assert_eq!;
Diagnostics and a clean stop
A stage records problems as diagnostics and decides when they are fatal. abort_if_errors is the checkpoint: it reports every error emitted so far at once, instead of stopping at the first.
use ;
;
let mut driver = new;
let mut session = new;
let err = driver.run.unwrap_err;
assert_eq!;
assert_eq!;
assert_eq!; // both bad tokens were reported
See examples/calculator.rs for a three-stage driver — lex, cap-check, sum — that renders its diagnostics at the end.
How the pipeline fits together
A Stage<C> has an Input type, an Output type, a name, and a run that consumes the input, uses the shared Session<C>, and produces the output. Pipeline::then appends a stage with the bound N: Stage<C, Input = S::Output> — the compile-time check that the phases connect. The result is a longer pipeline whose type encodes the whole chain, and whose run threads an input through every stage left to right.
The distinction from a pass manager is the type change. A pass rewrites one type in place (an IR stays an IR); a stage transforms one artifact into a different one (source becomes tokens becomes an AST). Managing that type-threading — while carrying the session and stopping cleanly on failure — is exactly what a driver exists to do, and it is why this is a separate crate from pass-lang.
When a stage returns a DriverError, the run stops there; later stages do not execute, and the error names the stage that failed. Diagnostics emitted before the failure remain in the session, so a driver can render them alongside the error.
API Overview
For the complete reference with examples, see docs/API.md.
Session<C>— the ambient state of one run: configuration and diagnostics.- Config:
new,config,config_mut,into_config. - Diagnostics:
emit,error,warn,note,diagnostics,take_diagnostics. - Checkpoints:
error_count,has_errors,abort_if_errors.
- Config:
Stage<C>— one phase:Input,Output,name,run.Pipeline<C, S>— stages composed:new,then,run,name.Diagnostic/Severity— one message and its seriousness.DriverError— the failure that halts a run.
Feature Flags
| Feature | Default | Description |
|---|---|---|
std |
✅ | Links the standard library. With it off the crate is no_std + alloc; every type works in both modes. |
serde |
⬜ | Derives serde::Serialize for Severity and Diagnostic, so a run's diagnostics can be serialized for tooling. |
Testing
The property suite in tests/proptests.rs checks the core invariants — the session's error count matches the errors emitted, take_diagnostics drains and resets, and a pipeline computes the same result as doing its stages by hand — across randomized inputs.
Cross-Platform Support
The driver is pure computation with no platform-specific code, so it behaves identically everywhere Rust runs. CI covers Linux, macOS, and Windows on both stable and the 1.85 MSRV.
Contributing
See REPS.md for the engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.