Code Formatting in Turmeric

Turmeric ships a built-in formatter that produces canonical, consistent style for .tur source files. It is a pure pretty-printer: it reads source text, walks the Form tree produced by the existing reader, and re-emits formatted output. It never touches the elaborator or code generator, so it works on any syntactically valid file regardless of type errors.

CLI usage

# Format a file and print to stdout
tur format myfile.tur

# Format stdin and print to stdout
tur format < myfile.tur

# Overwrite a file in-place
tur format myfile.tur > myfile.tur.tmp && mv myfile.tur.tmp myfile.tur

# Check whether a file is already formatted (exits 1 if not)
tur format --check myfile.tur

# Show a unified diff of what formatting would change
tur format --diff myfile.tur

tur format reads with the reader the file's extension selects (.tur.sweet is sweet-exp, .scm is Scheme), and a #lang line takes over when the extension says plain Turmeric. Input on stdin carries no extension, so name its dialect with --lang -- the same vocabulary as tur fmt --lang, which takes a reader name (sweet, neoteric, r7rs, ...) or any base tur dialects lists (saffron/sweet, turmeric/neoteric, ...):

tur format --lang sweet < buffer.tur.sweet

Prefer tur fmt, which has the same --lang and also formats in place.

The --check flag is useful in CI:

# Fail the build if any .tur file is not canonically formatted
find stdlib -name '*.tur' | xargs -n1 tur format --check

Web REPL

The Try Turmeric web interface has a Format button in the editor toolbar (between Clear and Share). It calls turi_wasm_format() from the WASM module and replaces the editor contents with the formatted output.

Keyboard shortcut: Alt+Shift+F (or Option+Shift+F on macOS).

Formatting rules

Indentation

Two-space indentation by default. A form is emitted on one line if it fits within the configured line width (default 80 columns); otherwise it is broken into a block layout.

Line breaking

The formatter measures each form's flat width -- the column count it would occupy if rendered on a single line with no breaks -- and compares it against the remaining columns on the current line (line_width - col). The decision is per-form, not per-file: a form that fits stays inline; one that does not is broken into a block layout.

Three things force a break regardless of width:

  1. Interior newlines. A form whose flat rendering contains a literal newline (a multi-line string, a multi-line inline-C block) is unmeasurable, so every enclosing inline check declines and the form is broken.
  2. Interior comments. The flat printer has no way to re-emit comments (the AST carries no comment nodes), so a form whose source span contains a ; comment is reported as unmeasurable. This prevents the formatter from silently deleting a comment by collapsing the form around it -- and it propagates upward, so a comment inside a vector nested in a call that would itself fit cannot be flattened away one level up.
  3. Multi-pair let / let* / loop. A binding vector with two or more pairs is always broken one pair per line, per the house style. This is enforced at measurement time (the form is reported as unmeasurable), so it also propagates: a multi-pair let nested inside an if test forces the if to break too.

Always-break vs. try-inline-first

Special forms (if, when, do, case, cond, handle, defn, fn, let with multiple binding pairs, etc.) have fixed block layouts and always break -- they never attempt an inline rendering, even if the whole form would fit on the line. The layout table below shows the fixed shape each one uses.

The exceptions are let / loop with a single binding pair (or an empty binding vector), which try inline first and fall back to the pair-per-line layout only on overflow, and defeffect, which is usually short enough to fit and uses the generic call layout when it does not.

Everything else -- regular function calls, vectors, maps, sets -- tries inline first and breaks only when col + flat_width > line_width.

Special forms

These forms have well-known argument roles and use custom layouts:

Form Layout
defn (defn name params :ret\n body...)
defmacro same as defn
fn (fn params\n body...)
let (let [bindings...]\n body...) -- each binding pair on its own line
if (if test\n then\n else)
when / unless (when test\n body...)
do (do\n form...)
case (case expr\n pat result...) -- each arm on its own line
loop (loop [bindings]\n body...)
handle (handle expr\n arm...)
defclass (defclass Name [params]\n method...)
definstance (definstance Name Class Type\n impl...)
defeffect (defeffect Name [] :ret)

Regular function calls

Vectors, maps, sets

Comments

Type annotations

:keyword type annotations stay on the same line as the expression they annotate and are never line-wrapped independently.

Quote / quasiquote sugar

Sugar forms are preserved: 'x, `x, ,x, ,@x stay as-is rather than being expanded to their (quote ...) equivalents.

Inline C blocks

```c ... ``` blocks are emitted verbatim; the formatter does not reformat embedded C.

Dialects whose layout is their syntax

A sweet-exp buffer (turmeric/sweet, saffron/sweet, r7rs/sweet) is checked and kept as written. Its indentation is part of its meaning, so the formatter parses it -- a syntax error still fails the run -- and returns the text with only the ends normalized: no leading blank lines and exactly one trailing newline. A Scheme (r7rs) buffer is re-indented and never reprinted. Every other reader goes through the pretty-printer described above.

Example

Before formatting:

(defn add [x : int y : int] : int (+ x y))
(defn factorial [n : int] : int (if (<= n 1) 1 (* n (factorial (- n 1)))))

After tur format (a sweet-exp file is kept as written -- see Dialects whose layout is their syntax):

(defn add [x : int y : int] : int
  (+ x y))

(defn factorial [n : int] : int
  (if (<= n 1)
    1
    (* n (factorial (- n 1)))))

See also