Logos. Format
(Logos v0.2.0)
Copy Markdown
The Logos code formatter (mix logos.format). Style rules: 2-space
indent; one space between siblings; no space before a closing paren;
defn/fn/let/ns/cond/try bodies indent 2 spaces from the
opening form (not aligned to the first argument). This is deliberately
the simplest consistent rule, not a full cljfmt-parity style guide.
Why this is built on Logos.Reader.tokenize/1, not Logos.Reader.read/1
Logos.Reader.read/1/read_all/1 go through Logos.Reader.Actions,
which reifies -- comments and original whitespace/layout are thrown
away entirely (per that module's own moduledoc, it produces plain
Logos.Form.t() data, nothing about source position or trivia
survives). Reusing that pipeline for a formatter would silently delete
every comment in the source. Logos.Reader.tokenize/1 (a thin
delegate to mix ichor.gen's checked-in generated module -- see
Logos.Reader's own moduledoc) instead returns the complete,
position-preserving [%Grammar.VM.Token{name, text, line, column}]
stream for the whole input -- including :TRIVIA tokens (the
grammar's @skip token, (SPACE | COMMENT)*), which is where every
comment's exact text lives. This module walks that raw token stream
directly and never touches Logos.Reader.Actions.
The formatting algorithm
A single left-to-right walk over the non-trivia tokens, tracking one
integer depth (incremented on any opening delimiter -- ( [ {
#{ #( -- decremented on any closing one -- ) ] } -- tracking
paren-nesting depth, not per-open-bracket-type matching, since
token-level balance is all indentation needs). Between each pair of
adjacent real tokens, the (at most one, since the
grammar's TRIVIA := (SPACE | COMMENT)* already merges any run of
whitespace/comments into a single token) intervening :TRIVIA token's
raw text is classified into:
- a trailing comment -- a comment on the same source line as the
previous token (no
\nbefore the;in the trivia) -- printed immediately after that token, one space before the;, verbatim after that. - zero or more standalone comments -- each on its own output line,
indented to the current depth (
2 * depthspaces), text verbatim after the leading whitespace is stripped. - whether a blank line existed (collapsed to at most one blank output line -- see "Known gap" below).
- whether any newline existed at all (with no comment) -- this
module does not invent its own line-wrapping/fill algorithm (out of
scope for "the simplest consistent rule, not a full
cljfmt-parity style guide"); it instead respects the source's own line-break choices: wherever the original had a newline between two siblings, the output keeps a line break there (reindented to2 * depth); wherever it didn't, siblings get exactly one space. A closing delimiter is the one deliberate exception -- per "no space before a closing paren", closing delimiters always attach directly to whatever precedes them with no line break, even if the source had one -- unless a comment intervenes (a comment always ends the line it's on, so a)immediately following a trailing/standalone comment is forced onto the next line, dedented to2 * (depth - 1)). - a reader-macro prefix (
',`,~,~@,^,#',#_) always attaches with no space to whatever follows it.
Known gaps (honestly documented)
- Blank-line count is not preserved exactly -- any run of 2+ blank source lines between two forms collapses to exactly one blank output line. This is an accepted simplification, not a bug -- comments themselves are never dropped, only the number of blank lines around them is normalized.
- This is not a
cljfmt-parity formatter: there is no per-form special-casing ofdefn/let/cond/etc. bodies beyond the single uniform2 * depthindent rule described above ("not aligned to the first argument") -- a long single-lineletbinding vector is never automatically reflowed into one-pair-per-line, for example; this module only reindents line breaks the input already had.
Summary
Functions
Formats source, returning {:ok, formatted} or {:error, reason} (a tokenizer failure -- malformed input).
Whether source is already exactly the output of format/1 -- mirrors mix format --check-formatted.