# driver-lang v0.2.0 — The Core
**The first release with domain logic.** v0.1.0 was the scaffold; v0.2.0 is the driver itself — a [`Session`](../API.md#session) that carries a compile's configuration and diagnostics, a [`Stage`](../API.md#stage) trait for a single phase, and a [`Pipeline`](../API.md#pipeline) that composes stages with their types checked at compile time. Every public item has rustdoc with a runnable example, the invariants are property-tested, and there is a Criterion suite and a worked example. The surface is designed here; `1.0.0` freezes it.
## What is driver-lang?
The driver that ties a compiler's phases into one run. A compiler is a sequence of phases — lex, parse, resolve names, check types, lower, emit — each taking the previous phase's artifact and producing the next. driver-lang is the machinery that holds those phases together: it carries the shared configuration and diagnostics, threads each artifact into the next phase, and stops cleanly when a phase fails. It owns no phases and wires no first-party dependency; a language plugs its own lexer, parser, and backend in as stages. `no_std` (needs only `alloc`), no `unsafe` (`#![forbid(unsafe_code)]`).
## What's new in 0.2.0
### `Session<C>` — configuration and diagnostics for one run
Every stage receives `&mut Session<C>`. It holds the compilation configuration `C` (whatever a language needs) and the diagnostics the phases emit, and it keeps an incremental error count so the driver can check `has_errors` between phases in O(1).
```rust
use driver_lang::Session;
let mut session = Session::new("x86_64-unknown-linux-gnu");
session.warn("unused import `std::mem`");
session.error("cannot find type `Foo`");
assert_eq!(session.config(), &"x86_64-unknown-linux-gnu");
assert_eq!(session.error_count(), 1); // the warning does not count
assert!(session.has_errors());
```
`abort_if_errors` is the checkpoint primitive — the "aborting due to N previous errors" step at the end of a phase. Because a stage can emit several errors before it is checked, one checkpoint reports them all at once instead of stopping at the first:
```rust
# use driver_lang::Session;
let mut session = Session::new(());
session.error("first");
session.error("second");
let err = session.abort_if_errors().unwrap_err();
assert_eq!(err.message(), "aborting due to 2 previous errors");
```
### `Stage<C>` — one compilation phase
A stage has an `Input` type, an `Output` type, a stable `name`, and a `run` that consumes the input, uses the session, and produces the output. Unlike a *pass* (which rewrites one type in place), a stage **changes the type** as the program moves down the pipeline — a lexer is `Stage<Input = Source, Output = Tokens>`, a parser is `Stage<Input = Tokens, Output = Ast>`. That type-threading is exactly what a driver exists to manage, and it is why this is a separate crate from [`pass-lang`](https://crates.io/crates/pass-lang).
### `Pipeline<C, S>` — stages composed, checked at compile time
`then` appends a stage whose input matches the current output. The match is a type bound — a pipeline that feeds tokens into a stage expecting an AST does not compile — and the whole pipeline is one monomorphized type, so composing stages adds no runtime indirection.
```rust
use driver_lang::{DriverError, Pipeline, Session, Stage};
struct Lex;
impl Stage<()> for Lex {
type Input = &'static str;
type Output = Vec<i64>;
fn name(&self) -> &'static str { "lex" }
fn run(&mut self, i: &'static str, _s: &mut Session<()>) -> Result<Vec<i64>, DriverError> {
i.split_whitespace()
.map(|w| w.parse().map_err(|_| DriverError::new("not an integer")))
.collect()
}
}
struct Sum;
impl Stage<()> for Sum {
type Input = Vec<i64>;
type Output = i64;
fn name(&self) -> &'static str { "sum" }
fn run(&mut self, i: Vec<i64>, _s: &mut Session<()>) -> Result<i64, DriverError> {
Ok(i.iter().sum())
}
}
let mut driver = Pipeline::new(Lex).then(Sum);
let mut session = Session::new(());
assert_eq!(driver.run("1 2 3 4", &mut session).unwrap(), 10);
```
On the first stage that returns a [`DriverError`](../API.md#drivererror), the run stops and the error names the failing stage; diagnostics emitted before the failure remain in the session.
### `Diagnostic`, `Severity`, `DriverError`
A [`Diagnostic`](../API.md#diagnostic) is a [`Severity`](../API.md#severity) (`Error` / `Warning` / `Note`) plus a `Cow<'static, str>` message, so a constant message never allocates. Only `Error` counts toward the abort checkpoint. A [`DriverError`](../API.md#drivererror) is the control-flow failure that ends a run, carrying the message and the stage the pipeline stamps in — distinct from a diagnostic, which is a record a stage may keep going after.
### `serde` feature
Behind the opt-in `serde` feature, `Severity` and `Diagnostic` derive `serde::Serialize`, so a run's diagnostics can be serialized for tooling. Enabling it does not change any existing behavior.
### Worked example
[`examples/calculator.rs`](../../examples/calculator.rs) is a three-stage driver — lex, cap-check against a configured maximum, sum — that renders its diagnostics at the end. `cargo run --example calculator` shows a clean run, a run that aborts at the cap check reporting two errors at once, and a run that fails at lex.
## Breaking changes
**None.** v0.1.0 exposed no API; this is the first release with a public surface.
## Performance
A pipeline is a single monomorphized type — no boxing, no `dyn` dispatch — so driving it costs no more than calling the stages by hand. The session keeps its error tally incrementally. 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 8 diagnostics into a session | ~36 ns |
| Emit 128 diagnostics into a session | ~261 ns |
| Emit 1024 diagnostics into a session | ~991 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.
## Verification
Run on Windows x86_64, with testing on Linux (WSL2 Ubuntu), Rust stable and the 1.85 MSRV; the same commands run in the CI matrix across Linux, macOS, and Windows:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --no-default-features --lib -- -D warnings
cargo test
cargo test --all-features
cargo test --no-default-features --lib
cargo build --no-default-features --lib
cargo run --example calculator
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check
```
All green. Counts at this tag:
- Default features: 37 unit + 5 integration + 3 property + 30 doctests.
- `--no-default-features` (lib): 37 unit (the crate is `no_std` + `alloc`).
## What's next
- **1.0.0 — API freeze.** Mark `docs/API.md` stable, record the SemVer promise, and lock the surface for the `1.x` series. No functional change is planned; the design is small and settled.
## Installation
```toml
[dependencies]
driver-lang = "0.2"
# no_std:
driver-lang = { version = "0.2", default-features = false }
# with serde derives on Severity and Diagnostic:
driver-lang = { version = "0.2", features = ["serde"] }
```
MSRV: Rust 1.85 (2024 edition).
## Documentation
- [README](https://github.com/jamesgober/driver-lang/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/driver-lang/blob/main/docs/API.md)
- [CHANGELOG](https://github.com/jamesgober/driver-lang/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/driver-lang/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/driver-lang/blob/main/CHANGELOG.md#020---2026-07-01).