Skip to content

zig fmt (Zig)

The formatter whose line-breaking decision is made by the author, one bit at a time, using a trailing comma. zig fmt has no column limit and no fits test; whether a construct renders on one line or one-item-per-line is determined by whether the source has a trailing comma before the closing delimiter — and by whether a comment is present. It is the most extreme version of the author's-breaks-preserved paradigm, and the cleanest demonstration that a formatter can be fully deterministic and fully opinionated while making almost no layout decisions of its own.

LanguageZig
LicenseMIT
Repositoryziglang/zig @ 1bcd8d9f (tag 0.16.0, 2026-06-02)
Layout enginelib/std/zig/Ast/Render.zig (3,507 lines) — a single file
Supportinglib/std/zig/Ast.zig (4,013) · driver src/fmt.zig (377)
Categoryfull AST + token index · source-hint breaking · zero options
Layout paradigmsource-hint (trailing comma decides)

Overview

What it solves

Zig's position is that the formatter should be predictable and the author should retain control over vertical structure, without the formatter needing a width model, a cost model, or a search. The mechanism is a single, learnable rule: a trailing comma means "explode this".

zig
const x = Foo{ .a = 1, .b = 2 };     // stays on one line

const y = Foo{
    .a = 1,
    .b = 2,                          // trailing comma → one field per line, forever
};

The rule is implemented, not inferred. isOneLineFnProto is the decision for a function signature and it reads exactly as described:

zig
fn isOneLineFnProto(tree: Ast, fn_proto: Ast.full.FnProto,
                    lparen: Ast.TokenIndex, rparen: Ast.TokenIndex) bool {
    const trailing_comma = tree.tokenTag(rparen - 1) == .comma;
    if (trailing_comma or hasComment(tree, lparen, rparen))
        return false;

    return !hasDocComment(tree, after_last_param, rparen);
}

lib/std/zig/Ast/Render.zig

Three conditions force multi-line, and none of them is width: a trailing comma, an ordinary comment inside the parens, or a doc comment. There is no measurement anywhere in that function because there is no measurement anywhere in the formatter.

Design philosophy

Indentation is a compile-time constant:

zig
const indent_delta = 4;
const asm_indent_delta = 2;

Render.zig

There is no configuration type, no options struct, no config file. The philosophy is the same as gofmt's — end the debate — reached by a different route: gofmt preserves the author's newlines wholesale, zig fmt asks the author for one bit per construct and derives everything else.


How it works

AutoIndentingStream

Rendering is a direct AST walk (renderExpression, renderFnProto, …) writing into an AutoIndentingStream that tracks indentation with pushIndent/popIndent and can insertNewline. There is no intermediate document: the tree is walked once and bytes come out. That makes zig fmt the simplest architecture in wave 1 — one file, one pass, no IR.

Each render call takes a Space parameter (.none, .space, .newline, .comma, …) telling it what to emit after the token, which is how spacing policy is expressed without a whitespace alphabet or a Doc.

The one place width appears

grep for column/width in Render.zig finds nineteen hits, and all of them are about aligning the columns of a multi-line array literal — computing col_widths across a "section" of rows so a grid-shaped literal lines up:

zig
// Determine the width of each column
var col_widths = try gpa.alloc(usize, row_size);

Render.zig

That is alignment, not line breaking. There is no page-width constant in the formatter.

Fixups — the formatter as an edit channel

renderTree takes a Fixups struct that lets a caller inject semantic edits during rendering: omit_nodes, gut_functions, unused_var_decls (insert _ = foo;), replace_nodes_with_string, replace_nodes_with_node, append_string_after_node.

This is unusual and worth noting: zig fmt is not only a formatter but the rendering back end for compiler-driven source edits, which is how zig fmt-adjacent tooling applies fixes. For a D formatter the analogous question is whether the printer should be reusable by a future refactoring or dfix-style tool — see layout preservation, where that is the original motivation for the whole family.


1. Input model & fidelity

A full AST with token indices. Ast.zig keeps a token array alongside the tree, and render functions address tokens by index (tree.tokenTag(rparen - 1)) — so the renderer can ask questions about the source token stream, which is exactly how the trailing-comma rule is implementable. This is a middle position between dfmt's pure token stream and a bare AST, and it is the minimum a source-hint formatter needs.

Comments are reachable via hasComment(tree, from, to) — a token-range predicate rather than an attachment.

Behaviour on unparseable input: refuses; the AST must parse.

2. Layout IR & break decision

Paradigm: source-hint. No IR, no fits, no cost, no search. The decision procedure per construct is a boolean derived from the token stream.

Width policy: none. A long single-line call stays long. Combined with the trailing-comma rule, the author's remedy is to add a comma — which is a genuinely different social contract from every other formatter here: the tool does not wrap for you; it makes your decision permanent.

3. Alignment, indentation & vertical rhythm

indent_delta = 4 (2 inside asm), spaces. Array-literal column alignment is the one real alignment feature, computed per section (above). Blank-line handling is part of the render walk.

4. Comments, trivia & preservation

Comments force multi-line rendering wherever they appear inside a construct — a deliberate simplification that sidesteps most of the attachment problem by refusing to make any layout decision in a comment's vicinity. No comment reflow. Doc comments (///) are likewise a multi-line trigger.

5. Configurability, opinionation & config discovery

Zero options. indent_delta is a const. No config file, no discovery, nothing to migrate.

6. Integration surface & output contract

Whole document. zig fmt with --check; no range formatting, no on-type, no cursor. The Fixups mechanism is an input channel, not an edit-output contract.


Strengths

  • Radically simple — one 3,507-line file, one pass, no IR, no search, no measurement.
  • Perfectly predictable. A reader can state the rule in one sentence and always be right.
  • Zero options, with no config machinery to maintain.
  • The author keeps control of vertical structure with a one-character signal.
  • Minimal diffs: layout is a function of the author's own commas, so reformatting is a no-op for anyone already using the tool.
  • Fast — no backtracking, no re-measurement.

Weaknesses

  • Cannot make anything fit. Same objection as gofmt, and for the same reason.
  • The rule is load-bearing punctuation. Deleting a trailing comma is a layout change with no semantic signal; a careless edit silently collapses a list.
  • Comments force expansion, which can be surprising — adding a // inside a call rewrites the call's layout.
  • Refuses invalid input.
  • No range formatting, cursor, or edit output.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Trailing comma decides one-line vs explodedOne learnable rule; the author keeps control; the formatter needs no width modelLayout depends on punctuation with no other meaning; deleting a comma is a layout edit
A comment forces multi-lineAvoids every hard case where a comment interacts with a break decisionAdding a comment reformats the construct around it
No column limit at allRemoves the entire line-breaking problemLong lines stay long
indent_delta as a constZero options, zero config machineryNothing adapts to an existing codebase's conventions
Direct AST walk, no intermediate documentSimplest possible architecture; one fileNo reusable layout engine; every construct's policy is hand-written
Token indices alongside the treeLets the renderer read source hints (commas, comments) the AST does not carryThe renderer is coupled to token positions, so tree edits must keep them consistent
Fixups as a render-time edit channelOne renderer serves formatting and compiler-driven source fixesRendering and rewriting are entangled in one API

What a D formatter should take

Take seriously: the trailing-comma signal. It is the highest-value-per-line idea in wave 1 — black's magic trailing comma is the same mechanism — and it costs almost nothing to implement on a token spine. A D formatter that otherwise wraps by width can still honour "the author wrote a trailing comma, so keep this exploded".

Do not take: the absence of a width limit, or "a comment forces multi-line". The first is discussed under gofmt; the second is affordable in Zig's grammar and would be too blunt for D, where a trailing // comment on a parameter is idiomatic.


Sources

  • ziglang/zig @ 1bcd8d9fe60f72849254b7f74d9ea0f48eae6aaa (tag 0.16.0): lib/std/zig/Ast/Render.zig (3,507), lib/std/zig/Ast.zig (4,013), src/fmt.zig (377)

Related deep-dives in this tree:Concepts · gofmt · dfmt · Layout preservation · Comparison