Skip to content

Doc IR and printer

Language rules do not produce text. They produce a Doc: a small tree describing text, possible line breaks, and indentation. The printer (rainbow_fmt.core.printer) decides which line breaks to take so that the output fits the configured width. This split is what lets one printer serve every language (see architecture.md, section 4).

from rainbow_fmt.core.doc import group, indent, join, line, softline
from rainbow_fmt.core.printer import PrintOptions, print_doc

def bracketed(*items):
    return group("[", indent(softline, join([",", line], items)), softline, "]")

print_doc(bracketed("1", "2"), PrintOptions())
# [1, 2]
print_doc(bracketed("1", "2"), PrintOptions(max_width=4, indent_size=2))
# [
#   1,
#   2
# ]

Docs are values

A Doc is immutable data: nodes are frozen dataclasses that compare and hash by value (group("a") == group("a")). Nothing depends on object identity, and the printer never calls back into rule code (ADR 0002).

Wherever a Doc is expected, builders also accept:

Argument Means
"text" text("text")
[a, b] or (a, b) concat(a, b)

Anything else raises TypeError.

Building blocks

Builder Flat (fits) Broken
text(s) s s (must not contain line breaks)
concat(*parts) parts in order parts in order
line a space a line break
softline nothing a line break
hardline always a line break; breaks every enclosing group
break_parent prints nothing; breaks every enclosing group
group(*parts, should_break=False, id=None) contents flat if they fit contents broken
conditional_group(*states) the first state that fits, flat the last state, broken (see below)
indent(*parts) — contents one level deeper (indent_size spaces or a tab)
align(n, *parts) — contents n spaces deeper, even when indenting with tabs
join(sep, items) items separated by sep
fill(items) alternating content and separators; each separator breaks only if the next content does not fit
if_break(broken, flat="", group_id=None) flat broken; follows the enclosing group, or the group with id == group_id
line_suffix(*parts) contents moved to the end of the current line (trailing comments)
table(rows) each row on its own line; every cell except the last in a row padded to its column's widest cell
verbatim(s) s exactly: not re-indented, not trimmed

A group is flat when its contents, plus everything after it up to the next line break, fit in the remaining width. Otherwise it is broken, and its line/softlines become line breaks. Nested groups are decided independently, outermost first.

Forced breaks

A group cannot be flat if it contains a hardline, a break_parent, a multi-line verbatim, or a table with more than one row. Each node records this in a forced attribute, computed from its direct children when it is built, so no separate pass over the tree is needed.

conditional_group: alternative layouts

function = concat("function () {", indent(hardline, "run()"), hardline, "}")
conditional_group(
    concat("f(a, ", function, ")"),                             # hug the argument
    group("f(", indent(softline, "a,", line, function), softline, ")", should_break=True),
)
# f(a, function () {
#     run()
# })

Prettier's conditionalGroup: each state is tried in turn, flat (groups with forced breaks inside it are still broken), and the first one whose text up to its first line break, followed by the rest of the line, fits is printed; if none fits, the last is printed broken. Forced breaks inside a conditional group do not break the groups around it (its forced is False); that is what lets a call hug a function argument whose body spans lines. After a hardline printed inside a state, the groups that follow are measured again rather than printed flat.

table: column alignment

table([["x", " = 1"], ["long_name", " = 2"]])
# x         = 1
# long_name = 2

Cells are printed flat and must not contain forced breaks. Rows follow the current indentation. Widths are display widths (see below).

verbatim: exact source text

Used for preserve and rainbow: off regions, and for multi-line string literals. Lines after the first are emitted as-is, without the current indentation, and trailing whitespace inside is kept. Line breaks inside are written as \n and printed with the configured newline (PrintOptions.newline).

Printing

print_doc(doc, PrintOptions(
    max_width=80,          # >= 1
    indent_style="space",  # "space" | "tab"
    indent_size=4,         # columns per level with spaces
    tab_width=4,           # columns a tab counts as
    newline="\n",          # "\n" | "\r\n"
))
  • Width is measured in terminal columns: East Asian wide and fullwidth characters count 2, combining marks and format characters (such as zero-width space) count 0, and a tab counts tab_width (rainbow_fmt.core.printer.display_width).
  • Trailing whitespace is removed from every line, except inside verbatim text.
  • Tabs and alignment: with indent_style="tab", indent adds a tab and align adds spaces ("smart tabs").
  • The printer is iterative, so deeply nested or very long documents do not hit Python's recursion limit.

The algorithm is the Wadler/Leijen "prettier printer" as implemented by Prettier (doc-printer.js), with table and verbatim added.