driver-lang 1.0.0

Compiler driver and session orchestration.
Documentation

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 chainPipeline::then only 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 severitieserror, warning, and note, recorded in order; only errors count toward the abort checkpoint.
  • Clean stops — a stage returns a DriverError to halt the run, and the pipeline stamps in the failing stage's name. Session::abort_if_errors turns accumulated errors into that stop.
  • Zero-cost composition — fully monomorphized; no dynamic dispatch on the run path.
  • no_std — needs only alloc; the default std feature only toggles the no_std attribute.
  • Fully safe — no unsafe, #![forbid(unsafe_code)].
  • Property-tested — diagnostic accounting and pipeline value-threading checked across randomized inputs with proptest.

Installation

[dependencies]
driver-lang = "1"

# no_std:
driver-lang = { version = "1", default-features = false }

# with serde derives on Severity and Diagnostic:
driver-lang = { version = "1", features = ["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 driver_lang::{DriverError, Pipeline, Session, Stage};

// Phase 1: text -> integers.
struct Lex;
impl Stage<()> for Lex {
    type Input = &'static str;
    type Output = Vec<i64>;
    fn name(&self) -> &'static str { "lex" }
    fn run(&mut self, input: &'static str, _session: &mut Session<()>)
        -> Result<Vec<i64>, DriverError>
    {
        input.split_whitespace()
            .map(|w| w.parse::<i64>().map_err(|_| DriverError::new("not an integer")))
            .collect()
    }
}

// Phase 2: integers -> their sum.
struct Sum;
impl Stage<()> for Sum {
    type Input = Vec<i64>;
    type Output = i64;
    fn name(&self) -> &'static str { "sum" }
    fn run(&mut self, input: Vec<i64>, _session: &mut Session<()>)
        -> Result<i64, DriverError>
    { Ok(input.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);

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 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, input: &'static str, session: &mut Session<()>)
        -> Result<Vec<i64>, DriverError>
    {
        let mut out = Vec::new();
        for token in input.split_whitespace() {
            match token.parse::<i64>() {
                Ok(n) => out.push(n),
                Err(_) => { session.error("not an integer"); }
            }
        }
        session.abort_if_errors()?; // stop here if any token failed
        Ok(out)
    }
}

let mut driver = Pipeline::new(Lex);
let mut session = Session::new(());

let err = driver.run("1 x 2 y", &mut session).unwrap_err();
assert_eq!(err.stage(), "lex");
assert_eq!(err.message(), "aborting due to 2 previous errors");
assert_eq!(session.error_count(), 2); // 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.

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

cargo test                    # unit + doctests
cargo test --all-features     # adds the serde-gated paths
cargo test --test proptests   # property-based invariants
cargo test --test driver      # end-to-end integration tests
cargo bench --bench bench     # Criterion pipeline + session benchmarks
cargo run --example calculator

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.