Formatter
What Is a Formatter?
Imagine you share a bedroom with a sibling. You both put your clothes away, but in totally different ways -- one person folds everything neatly and the other just shoves things into drawers. A formatter is like a robot that goes through the whole room and organizes everything in the exact same way, every time.
In programming, a formatter takes your code and rewrites it so it follows consistent style rules. The code does the exact same thing as before -- the formatter only changes how it looks, not how it works.
Why Format Code?
When many people work on the same project, everyone has slightly different habits:
- Some people put spaces around
+, others don't - Some people indent with 2 spaces, others with 4
- Some people like blank lines between functions, others don't
These differences make code harder to read. A formatter eliminates all arguments about style by applying one set of rules automatically.
Using the Formatter
Format a file in place (rewrites the file):
Check if a file is already formatted (useful in CI):
The --check flag exits with code 1 if the file isn't formatted, without modifying it. This is handy for automated checks that ensure all code is formatted before it's merged.
What the Formatter Does
Consistent Indentation
All blocks use 4-space indentation:
Operator Spacing
Binary operators always get spaces around them:
Brace Placement
Opening braces go on the same line. Closing braces get their own line:
Blank Lines Between Definitions
Functions, classes, structs, and enums are separated by blank lines:
Comment Preservation
The formatter keeps your comments in place:
How does it know where "in place" is, after it has rebuilt every line? Each
line the formatter writes remembers which line of your file it came from --
like sticky notes saying "this came from line 7". A comment at the end of a
line rejoins the line made from that same source line. A comment on its own
line goes just before the next statement you wrote after it, indented to match
the { } blocks it was written inside (even right at the end of a block, just
before the closing }).
How It Works
The formatter works in three steps:
-
Extract comments -- Since the lexer discards
#comments, the formatter does a separate pass over the raw source text to find and remember every comment, along with which line it was on. -
Parse and walk the AST -- The source is lexed and parsed normally into an Abstract Syntax Tree. The formatter walks every node in the tree and produces clean, consistently-styled output.
-
Interleave comments -- The extracted comments are inserted back into the formatted output at their original positions.
This approach is called AST-based formatting. It's the same technique used by famous formatters like Go's gofmt, Rust's rustfmt, and Python's black. Because the formatter rebuilds the code from the AST, the output is always canonical -- formatting the same code twice always produces the same result.
Parenthesization
One tricky part is knowing when to add parentheses. The AST doesn't store parentheses -- it stores the expression tree structure. So (1 + 2) * 3 becomes a multiply node with an add node as its left child.
The formatter uses a precedence table (the same one the parser uses!) to decide: if a child expression has lower precedence than its parent, wrap it in parentheses. So the add child (+, precedence 9) inside a multiply parent (*, precedence 10) gets parenthesized because 9 < 10.
String Escaping
Another challenge: the AST stores decoded string values. A string literal "hello\nworld" becomes a StringLiteral node whose value contains an actual newline character. The formatter must re-escape these characters when outputting the string, converting the real newline back to \n.
Idempotency
A good formatter is idempotent -- formatting already-formatted code produces the exact same output. You can run pebble fmt as many times as you like and the result won't change after the first time. This is an important property because it means the formatter is predictable and won't create noisy diffs.
Never Breaking Your Code
Tidying must never change what a program means, and it must never produce code that won't run. Think of a tidy-up robot in your bedroom: it can sort your things into neat piles, but it must never throw anything away or glue a drawer shut.
We check this two ways:
- Round-trip tests. For every example (and hundreds of random programs invented by the Hypothesis library), we check that the formatted code still parses, means exactly the same thing as the original, and stays the same when formatted again.
- A safety net in
pebble fmt. Becausepebble fmtoverwrites your file, it parses the tidied code before saving. If that ever failed, it refuses to write and leaves your original file untouched.
These checks once caught a real bug: the formatter wrote struct fields one per line, but the parser didn't allow a line break after the comma, so struct Point { x, y } became code Pebble couldn't read.