The model
Rulesdescribe a language's layout as data:NodeRules (group, indent, spacing before and after a node, list delimiters and separators with a trailing-separator policy, blank-line caps) andTokenRules (spacing before and after a token kind, everywhere or inside one node kind).Rules::compileresolves the kind names against a language once and returns aStyle.formatformats a tree with a style at a width.format_keepingalso takes the regions of lexical errors to leave as written;format_docreturns thepretty_lang::Docinstead of a string.
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
Formatting is idempotent: fmt(fmt(x)) == fmt(x). |
Property tests re-parse and re-format the output of random valid and invalid sources in two languages forged with lang-forge, under several rule sets and random widths. |
The significant tokens are unchanged, except a trailing separator a Trailing::Always / Never policy adds or removes. |
The same tests compare every non-trivia token, kind and text, before and after. |
| Every comment appears exactly once, unchanged and in order; a line comment always ends its line. | The same tests compare the lexed comments before and after, and check the character after every line comment. |
| Error nodes and anything unparsable are written as they are. | Parse errors are ERROR nodes written verbatim; lexical errors are kept as written with format_keeping. Token soups and random strings are in the property tests. |
| No panics on any tree, error nodes included; a tree that does not match its source is an error value. | Arbitrary hand-built trees and hostile token spans in the property tests. |
| Deterministic. | Every property test formats twice and compares. |
| With no rules, only whitespace changes, exactly as specified. | Compared with a reference implementation written over the lexer's flat token list. |
| Deep and large input is safe. | The walk is iterative and linear; a tree nested 200,000 levels deep formats in a test, and indentation is capped (Rules::max_indent) so output cannot explode. |
The property tests run 400 cases each by default; before this release they were soaked at 400,000 cases each.
Installation
[]
= "0.2"
Without the standard library:
[]
= { = "0.2", = false }
Quick start
A JSON-like language forged with lang-forge, and its whole formatter:
use ;
use Language;
let json = from_lsf?;
let style = new
.indent
.verbatim
.token
.node
.node
.compile?;
let parse = json.parse;
assert_eq!;
# Ok::
The line comment must end its line, so the object cannot be laid out flat and breaks, one member per line; the array still fits and stays flat. Without the comment the whole object fits on one line at width 80; at width 12 the array breaks too.
Comments stay where they belong
Comments are trailing (on the line of the token before them), leading (on their own line before the next token), or dangling (on their own line at the end of a block, indented with it):
use ;
use Language;
let json = from_lsf?;
let style = new
.indent
.token
.node
.compile?;
let src = "{\n// leading\n\"a\":1, /* trailing */\n\n\n\"b\":2\n // dangling\n}";
let parse = json.parse;
assert_eq!;
# Ok::
Expression languages
Token rules give operators their spacing, a node rule makes a prefix operator hug its operand, and a hanging indent wraps long expressions. Nothing ever fuses two tokens: - -3 keeps its space.
use ;
use Language;
let calc = from_lsf?;
let mut rules = new
.token
.token
.token
.token
.token
.node
.node
.node;
for op in
let style = rules.compile?;
let parse = calc.parse;
assert_eq!;
# Ok::
Examples
| Example | What it shows |
|---|---|
format_json |
A forged JSON-with-comments language formatted at two widths from six rules, then a broken file formatted with its error nodes and lexical errors kept. cargo run --example format_json |
Performance
The formatter is two linear passes over the tree, both iterative: one flattens the tree into an event list (checking it against the source and settling list delimiters), one builds a single pretty_lang::Doc, merging runs of tokens and spaces into one text node, and pretty-lang renders it in one linear pass. Rule lookups are a binary search over rules sorted by kind, once per node and once per token.
Measured with the benchmarks in benches/, Windows x86_64, Rust stable, release profile. Parsing is done once, outside the measurement; the numbers are the formatter alone:
| Benchmark | Input | Time | Throughput |
|---|---|---|---|
format_json/rules |
7.9k nodes (37 KB) | ~2.5 ms | ~3.1M nodes/s (~14 MB/s) |
format_json/rules |
79k nodes (381 KB) | ~51 ms | ~1.6M nodes/s (~7 MB/s) |
format_json/rules |
792k nodes (4.0 MB) | ~645 ms | ~1.2M nodes/s (~6 MB/s) |
format_json/doc_only |
792k nodes, building the Doc without rendering it |
~510 ms | ~1.6M nodes/s |
format_json/no_rules |
792k nodes, empty style (whitespace kept) | ~300 ms | ~2.6M nodes/s |
format_deep/nested_100k |
a tree nested 100,000 levels deep | ~155 ms | ~0.64M nodes/s |
These are first numbers, not tuned ones, and they were taken on a machine running other builds, so expect ±20%. Throughput falls as inputs grow. A phase split at 792k nodes (one run, same input) showed roughly 15% flattening, 45% building the Doc, 15% rendering it, and 25% dropping it: most of the cost is allocating and freeing pretty-lang's reference-counted document nodes (one per text run, break, group, and join). The Doc in pretty-lang 1.x has no arena, so that is the next thing to change for speed.
Limits
- Whether two tokens may touch is decided by a conservative heuristic (
can_touch) unless the style supplies an exact test. - Tokens whose extent depends on the whitespace after them (an unterminated string ends at the line break) need their regions passed to
format_keeping; without them, removing that line break changes the token. - A parser's recovery that leaves no trace in the tree is invisible to the formatter; trailing-separator edits skip lists with error nodes or unclosed delimiters, but
Trailing::Preserveis the policy that never edits. - In languages whose line breaks are tokens, rules that break lines add tokens.
- Indentation is spaces; widths count
chars, not display columns. - Range formatting (for LSP
rangeFormatting) and a trailing comma "only when broken" are planned (see the ROADMAP).
Contributing
See dev/DIRECTIVES.md for 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.