pretty-lang 1.0.0

AST/CST to source pretty-printer and formatter core.
Documentation
  • Coverage
  • 100%
    2 out of 2 items documented2 out of 2 items with examples
  • Size
  • Source code size: 189.04 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 474.15 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • jamesgober/pretty-lang
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • jamesgober

Performance First

Rendering is a single linear pass over the document with width-bounded look-ahead — no backtracking, no intermediate materialization, no per-node allocation on the render path. Latest local Criterion means (cargo bench --bench bench, Linux x86_64 / WSL2, Rust stable, release build):

Workload Layout Time
JSON tree (~85 nodes, ~340 B out) flat (fits) ~1.8 µs (~550 MiB/s)
JSON tree (~85 nodes) broken (reflowed) ~4.0 µs
Call, 128 arguments flat ~1.9 µs
Call, 128 arguments broken ~2.7 µs
Call, 8 arguments flat ~190 ns

Numbers vary by CPU and environment; run the suite on your target to establish a baseline. Building a Doc is one Rc allocation per combinator; rendering reuses a single work stack and writes straight into the output buffer.

Features

  • Eight combinators, any languagetext, line, softline, hardline, append, nest, group, plus concat / join. Build a Doc from any AST and render.
  • Fit-aware reflowgroup lays its contents flat when they fit the remaining width and breaks them all-or-nothing when they do not.
  • Linear time, no recursion — Wadler/Lindig layout with bounded look-ahead; deep documents render and drop without overflowing the stack.
  • Stream or collect — render to a String, into any core::fmt::Write, or into a std::io::Write.
  • no_std — needs only alloc; the std feature adds the I/O renderer.
  • Fully safe — no unsafe, #![forbid(unsafe_code)].
  • Property-tested — flat-layout, panic-safety, associativity, and idempotence invariants checked across randomized documents with proptest.

Installation

[dependencies]
pretty-lang = "1"

# no_std (drops the io::Write renderer):
pretty-lang = { version = "1", default-features = false }

MSRV is 1.85+ (Rust 2024 edition).

Quick Start

use pretty_lang::Doc;

// Build `f(a, b, c)` as a document that can break one-argument-per-line.
let call = Doc::text("f(")
    .append(
        Doc::softline()
            .append(Doc::join(
                Doc::text(",").append(Doc::line()),
                ["a", "b", "c"].map(Doc::text),
            ))
            .nest(4),
    )
    .append(Doc::softline())
    .append(Doc::text(")"))
    .group();

// Wide: it all fits on one line.
assert_eq!(call.render(80), "f(a, b, c)");

// Narrow: the group breaks and the arguments stack, indented.
assert_eq!(call.render(6), "f(\n    a,\n    b,\n    c\n)");

A JSON pretty-printer, end to end

A single walk over a value tree produces a document that renders as dense one-liners where they fit and indented blocks where they do not — with no width logic in the walk. See examples/json.rs for the full version.

use pretty_lang::Doc;

// Wrap a body between brackets so it collapses to `open body close` when it
// fits and becomes a multi-line block otherwise.
fn bracket(open: &'static str, body: Doc, close: &'static str) -> Doc {
    Doc::text(open)
        .append(Doc::softline().append(body).nest(2))
        .append(Doc::softline())
        .append(Doc::text(close))
        .group()
}

let array = bracket(
    "[",
    Doc::join(
        Doc::text(",").append(Doc::line()),
        ["1", "2", "3"].map(Doc::text),
    ),
    "]",
);

assert_eq!(array.render(80), "[1, 2, 3]");
assert_eq!(array.render(4), "[\n  1,\n  2,\n  3\n]");

How the layout engine works

A Doc is a small tree of nodes: literal text, flexible breaks (line, softline, hardline), concatenation, nest (indentation), and group (a layout choice point). Rendering walks the tree once with an explicit work stack, carrying a modeflat or broken — and the current column.

At each group the engine asks a single question: do the group's contents, laid out flat, fit in the width left on this line? The look-ahead that answers it scans only as far as the first newline or the first column past the target width, which is what keeps the whole algorithm linear. If the contents fit, every flexible break inside becomes its flat form — a space or nothing. If they do not — or the group contains a hardline — every flexible break inside becomes a newline plus the current indentation. The decision is all-or-nothing for the breaks a group directly owns; nested groups are decided independently, so an inner list can stay flat inside an outer one that broke.

Because both the render pass and the look-ahead use heap work stacks rather than the call stack, a document nested tens of thousands of levels deep renders without a stack overflow — and is torn down the same way when dropped.

API Overview

For the complete reference with examples, see docs/API.md.

Feature Flags

Feature Default Description
std Adds Doc::render_writer for io::Write sinks. The crate core is no_std + alloc.

Testing

cargo test                 # unit + doctests
cargo test --all-features  # adds the std io::Write tests
cargo test --test proptests # property-based invariants
cargo bench --bench bench  # Criterion layout benchmarks

The property suite in tests/proptests.rs checks the core invariants — the flat layout matches an independently-built oracle, rendering never panics at any width, append is associative, and group is idempotent — across randomized documents.

Cross-Platform Support

The layout engine 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.