This is the full documentation for gilramir/gren-format-lib, the library
behind gren-format. The README covers only how to call the
library; everything else lives here — a formatted example, the formatting
philosophy, the comment rules, known limitations, performance, and the
comparison with elm-format — followed by links to every companion document
under Deep dive.
Turning your source file into its formatted version happens through a pipeline of steps, each step handing its result to the next. Any step can fail: a parse error means the source itself is invalid Gren, while a failure in Step 1 or Step 2 means the formatter has caught an internal bug in its own logic — not that anything is wrong with your code. Either way, nothing is silently mangled; the failure is reported instead. This is the flow:

For the full step-by-step tour of that pipeline — what a Logical Printing Tree is, how a render plan is built from it, and a real example at each stage — see How the formatter works.
Here is one function, formatted, showing several rules at once.
{- Only members and coupon holders get the discount, and it always
applies to the pre-tax subtotal.
-}
summarize : Order -> Array Float -> Order
summarize order prices =
let
subtotal =
prices
|> Array.keepIf (\price -> price > 0) -- refunds are recorded as negatives
|> Array.foldl (+) 0
eligible =
order.isMember && subtotal > 100
|| order.hasCoupon && order.status /= Cancelled
discount =
if eligible then
subtotal * {- ten percent -} 0.1
else
0
in
when order.status is
Cancelled ->
{ order | total = 0 }
_ ->
{ order
| total = subtotal - discount
, hasCoupon = False
}
A few things worth noticing:
subtotal is a pipeline: each |> step lands on its own line, indented +4
from prices (see Pipelines).eligible is a binop chain the author wrote across two rows. || is the
loosest operator here, so it’s the only one that breaks; && and /= bind
tighter and stay glued to their operands (see
Binary operators).discount’s if branches always drop to their own line, whether or not
they’d fit inline (see
If expressions).when branches return a record update, { order | ... } — the
Cancelled branch’s field was written on one line and stays inline, while
the other branch’s two fields were written across rows and stay that way.
Neither is about length; it’s however the author wrote it (see
Record updates).{- ... -} above the signature keeps its own lines and its inner indentation-- note stays trailing on the pipeline step it was written on, rather than being
pushed to a line of its own{- ten percent -} sits inside
the expression, between the * and its right operand, so the line it’s on has
to stay flatEvery one of these decisions follows from how the code was written, not from any line-width target — see Formatting Philosophy below, and the full Formatter Rules for the complete reference.
This section covers the core ideas as to how gren-format formats the code; for a rule
reference with a before/after example for every construct (module
declarations, records, pipelines, comments, and everything else), see
Formatter Rules.
The Gren formatter has one central idea: your line breaks are your layout decisions. Write something on one line and it stays on one line. Put a line break between items and the formatter keeps them on separate lines, normalizing to one item per line.
There is no page width. The formatter never wraps a long line. A function call with five arguments all on one row stays on one line no matter how wide it is. A type signature written as one long line stays that way. If you want something to break, put a line break in it.
The four core rules:
One row → one line. If you wrote a construct on a single row, the formatter keeps it on one line. Width is irrelevant.
Multiple rows → one item per line. If you put a line break between any two items of a container — an array, a record, a call’s arguments, a pipeline’s steps — the formatter keeps every item on its own line. There is no “some items here, some there” shape: a line break anywhere in the container means every item gets its own line.
An operator chain is the one construct that breaks differently, because its items are not peers. A chain you wrote across rows breaks at its loosest operators and keeps the tighter ones glued to their operands, so the shape shows you the grouping (see Binary operators):
-- you write: -- gren-format writes:
chain = chain =
aa && bb aa && bb
|| cc && dd || cc && dd
-- and if you break it at the tighter operator instead,
-- the whole chain comes back flat:
chain = chain =
aa aa && bb || cc && dd
&& bb || cc && dd
The formatter never changes what your code means. It never rewrites an
expression — every paren you wrote is kept, redundant or not — and never
edits the text inside a comment or string. It reorders exactly two things,
neither of which is code: the names in an exposing ( … ) list, and a run
of import statements (see Sorting).
Formatting is stable. Running the formatter on already-formatted code
produces the same code back. Format once or ten times — same result, and
nothing in the test corpus shifts. There is one corner case wher we cannot
produce proper formatting: an upstream parser bug
(compiler-common#35)
reads 10 - ⏎ ····3 as the call 10 (-3), so a -- written after that
- comes back as ---. The formatter’s own AST check catches that and
refuses to write the file rather than corrupt it. Those cases are registered
by name so our automated testing allows that pass, until the bug is fixed. See
Known Limitations.
A few things are always fixed, regardless of how you wrote them:
when branch body always starts on its own line.if branch body always starts on its own line.else/else if from the branch above it.let bindings.module line, exposing always stays glued to the module name —
never on its own line — though the exposed list itself can still spread
across multiple rows below it (see
Module declaration).Everything else follows your layout choices.
gren-format never changes the text of a comment — it only decides where the
comment sits relative to the code around it. Seven rules decide every comment in
a file:
The first two settle which piece of code a comment is attached to; the last
five settle how the attached comment is laid out. Much of the rest follows
from one mechanical fact: a -- runs to the end of its line and a multi-line
{- … -} spans lines, so neither can share a line with the code around it,
while a one-line {- -} can — see
The two kinds of comment.
Each rule, with a “you write / gren-format writes” example for every case, is in How gren-format places your comments.
gren-format has a handful of known gaps:
when pattern shape the Haskell-based compiler can reject=,
:, |, in, a bracket’s closing paren, an effect module’s where block)
that the parser doesn’t record a position forFor a full write-up, with examples, see: Known Limitations.
Furthermore, here is a list of the GitHub issues we are tracking in upstream
packages that affect the output of gren-format.
Real Gren files are small enough that formatting speed is a non-issue, but
the formatter is also checked against synthetic files pushed far past
anything realistic — thousands of top-level declarations, thousands of
stacked comments, deeply nested expressions — to catch algorithmic hot spots.
That stress suite is tests/pathological-other.py (size/shape probes) and
tests/pathological-nesting.py (depth probes), both described in
Testing gates.
A few representative numbers:
| Shape | Size | Time |
|---|---|---|
| Top-level function declarations | 15,131 | ~4s |
| Top-level function declarations | 40,000 | ~20s |
| Stacked top-level comments (no code — a stress case, not realistic) | 4,005 | ~0.5s |
| Stacked top-level comments (no code — a stress case, not realistic) | 32,000 | ~21s |
How it grows. Declarations are close to linear: each doubling of the file costs about 1.9–2.4× the time. The comments-only shape is the steepest thing measured here, at roughly 2.9–3.4× per doubling — still nowhere near a hang at sizes an order of magnitude past real source, but the one curve worth watching if comment handling changes.
The pattern behind those numbers is worth knowing if you work on the formatter,
because the same mistake is easy to make twice. Every performance problem this
codebase has had was one of two shapes: rescanning settled work — rebuilding
or re-walking the entire array of already-processed declarations or comments
once per new one, which is O(n²) in the file — or rendering the same subtree
twice, which is exponential in nesting depth. The fixes are equally uniform:
accumulate with Array.Builder (amortized O(1) per append) rather than
Array.pushLast/++ in a loop, and render each subtree once, up front, letting
every path consume the same result.
gren-format is a spiritual descendent of elm-format, and agree on
formatted syntax in most places. Both
formatters share the same “your line breaks are your layout decisions”
philosophy — neither reflows code to fit a page width — so they agree almost
everywhere. Where they don’t, it’s a catalogued choice: 32 divergences,
covering things like blank-line placement around comments, redundant parens
(the most common difference on real code), and how a multi-line operator chain
breaks. One of them isn’t a choice at all — Gren’s parser throws away the
position of =, ,, |, -> and the keywords, so a comment written beside
one of those has to snap to a canonical side
(#22).
The full catalogue, with a real before/after example for every entry, is in Comparison with elm-format.
Everything above is the short version. The full documents are in this same
docs/ directory, in two groups: the first is for reading about what
gren-format does to your code, the second for changing how it does it.
Using the formatter
exposing ( … ) list, and a run of import statements.gren-format deliberately diverges from elm-format, and why.Working on the formatter
fuzzrun.py, and
across several hosts.Working on some other formatter
llm/ is a third group, written to be read by a model rather
than a person: approaches already tried and backed out, the generator’s grammar
log, and a triage the divergence catalogue rests on. Nothing there is needed to
use or to change the formatter.