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 \n before 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 * depth spaces), 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 to 2 * 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 to 2 * (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 of defn/let/cond/etc. bodies beyond the single uniform 2 * depth indent rule described above ("not aligned to the first argument") -- a long single-line let binding 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.

Functions

format(source)

@spec format(String.t()) :: {:ok, String.t()} | {:error, term()}

Formats source, returning {:ok, formatted} or {:error, reason} (a tokenizer failure -- malformed input).

formatted?(source)

@spec formatted?(String.t()) :: boolean()

Whether source is already exactly the output of format/1 -- mirrors mix format --check-formatted.