# pass-lang v0.2.0 — The Pass Manager
**The core, and the hard part of the roadmap.** v0.2.0 turns the scaffold into a
working pass manager: a generic pipeline that orders passes, runs them over a unit
of compilation, iterates to a fixpoint, and reports what happened. The surface is
deliberately small — one trait, one manager, a report, an outcome, and an error —
and it is generic over the unit, so the manager binds to no concrete IR and wires
no first-party dependency.
## What is pass-lang?
The pass manager: it orders optimization and transform passes and runs them over a
unit of compilation, and it is the plugin seam capability crates register their
passes into. It is generic over the unit a pass rewrites — an intermediate
representation, a single function, a module, an abstract syntax tree, or a struct
bundling an IR with the diagnostics and analysis a pass needs. A consumer brings
the unit, implements one trait per transform, registers the transforms in order,
and runs the pipeline; the manager schedules and runs, and never inspects the unit
itself. It is one of the SEMA-tier crates of the `-lang` language-construction
family.
## What's new in 0.2.0
### `Pass<T>` — the plugin seam
A pass is a transform over a unit of type `T`: it names itself and rewrites the
unit in place, reporting an [`Outcome`](#outcome--the-honest-change-signal) or
failing with a `PassError`. The trait is two methods, object-safe, and `'static`,
so any crate can contribute a pass without the manager knowing its concrete type.
```rust
use pass_lang::{Outcome, Pass, PassError};
struct DropZeros;
impl Pass<Vec<i64>> for DropZeros {
fn name(&self) -> &'static str { "drop-zeros" }
fn run(&mut self, unit: &mut Vec<i64>) -> Result<Outcome, PassError> {
let before = unit.len();
unit.retain(|&x| x != 0);
Ok(Outcome::from_changed(unit.len() != before))
}
}
```
### `PassManager<T>` — the ordered pipeline
The manager holds passes in registration order — `add` is the plugin seam — and
runs them two ways. `run` makes a single sweep; `run_to_fixpoint` repeats the sweep
until a full pass changes nothing, or an iteration bound is reached. The bound
guarantees termination even when a pass oscillates, so a pipeline can be driven to
completion without the risk of an unbounded loop.
```rust
use pass_lang::PassManager;
# use pass_lang::{Outcome, Pass, PassError};
# struct DropZeros;
# impl Pass<Vec<i64>> for DropZeros {
# fn name(&self) -> &'static str { "drop-zeros" }
# fn run(&mut self, unit: &mut Vec<i64>) -> Result<Outcome, PassError> {
# let before = unit.len();
# unit.retain(|&x| x != 0);
# Ok(Outcome::from_changed(unit.len() != before))
# }
# }
let mut pm = PassManager::new();
pm.add(DropZeros);
let mut unit = vec![0, 1, 0, 2];
let report = pm.run_to_fixpoint(&mut unit, 16).unwrap();
assert_eq!(unit, vec![1, 2]);
assert!(report.converged());
```
### `Outcome` — the honest change signal
Every run reports `Changed` or `Unchanged`. The manager takes the value at its
word: a full sweep with no `Changed` is a fixpoint, and that is what stops the
loop. Reporting `Unchanged` after a mutation would break termination, so the
contract is explicit — keep it honest.
### `Report` — observability
A run returns a `Report`: every pass execution in order (`runs`), how many reported
a change (`changes`), how many sweeps ran (`iterations`), and whether the pipeline
settled (`converged`). It is the hook for logging and inspecting a pipeline, and
behind the `serde` feature it serializes.
### `PassError` — contained failure
A pass that cannot proceed returns a `PassError` carrying a human-readable reason,
never a panic. The manager stamps in the name of the failing pass and stops the
pipeline, so a caller always knows which pass halted the run and why. The reason is
a `Cow<'static, str>`, so a static message costs no allocation.
### Examples, benchmarks, and property tests
Two runnable examples ship: `expr_optimizer` (constant folding and algebraic
simplification driven to a fixpoint, including the overflow error path) and
`text_normalizer` (the same machinery over a `String`). Criterion benchmarks cover
`run` and `run_to_fixpoint`. Property tests hold the manager to the invariants in
`dev/DIRECTIVES.md`: determinism, fixpoint termination, fixpoint stability, and a
report that accounts for every pass that ran.
### Dependency wiring — none, by design
No first-party dependency is wired, and none is planned. A pass manager is a
generic scheduling engine; coupling it to one concrete IR would bind it to a single
layer and pull in a dependency the scheduler never reads. The manager is generic
over the unit instead — the same self-contained precedent the sibling SEMA crates
set. The reasoning is recorded in `dev/ROADMAP.md` under the anti-deferral rule.
## Breaking changes
**None.** v0.1.0 exposed no public API; everything in 0.2.0 is new surface.
## Verification
Run on Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable 1.95 and the 1.85 MSRV;
identical commands run in the configured CI matrix:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo build --examples
cargo build --no-default-features
cargo +1.85 build --all-features
cargo deny check
cargo audit
```
All green. Counts at this tag:
- Default features: 27 unit + 4 integration + 3 property + 38 doctests. The
doctests include every `rust` example in `README.md` and `docs/API.md`, wired
into `cargo test` so the published examples cannot drift from the API.
- `--all-features`: adds 2 `serde` serialization tests.
Benchmark baselines (Linux, WSL2 Ubuntu, release profile): a single three-pass
sweep over a one-thousand-element unit runs in ~1.0 µs; a fixpoint that decrements
256 elements to zero across 33 sweeps runs in ~2.0 µs. The manager's own per-pass
overhead is a single indirect call — the figure is dominated by the work the passes
do, which is the intent.
## What's next
- **1.0.0 — API freeze.** Freeze the 0.2.0 surface as the 1.0 contract, mark
`docs/API.md` stable with a SemVer promise, and confirm the full test and
benchmark suite on all three platforms. Analysis passes with cached, invalidatable
results and inter-pass dependency declaration are additive later-minor work, not a
1.0 requirement.
## Installation
```toml
[dependencies]
pass-lang = "0.2"
```
MSRV: Rust 1.85.
## Documentation
- [README](https://github.com/jamesgober/pass-lang/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/pass-lang/blob/main/docs/API.md)
- [CHANGELOG](https://github.com/jamesgober/pass-lang/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/pass-lang/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/pass-lang/blob/main/CHANGELOG.md#020---2026-06-30).