Skip to content

Formatter

Sema includes a built-in code formatter that enforces consistent style across your codebase. It preserves all comments, handles shebang lines, and produces idempotent output.

Usage

sema fmt [OPTIONS] [FILES...]

With no arguments, sema fmt formats all .sema files in the current directory recursively.

Options

FlagDescription
--checkCheck formatting without writing changes (exit 1 if unformatted)
--diffPrint diff of formatting changes
--width <N>Max line width (default: 80)
--indent <N>Indentation width for body forms (default: 2)
--alignAlign consecutive similar forms (defines, cond clauses, let bindings)

Examples

bash
# Format all .sema files in current directory
sema fmt

# Format specific files
sema fmt src/main.sema lib/utils.sema

# Format with glob patterns
sema fmt "src/**/*.sema"

# Check formatting in CI (exits 1 if changes needed)
sema fmt --check

# Preview changes without writing
sema fmt --diff

# Use wider lines and 4-space indent
sema fmt --width 100 --indent 4

# Enable decorative alignment
sema fmt --align

Project Configuration

Create a sema.toml file in your project root to set persistent formatting options. The formatter walks up from the current directory to find the nearest sema.toml.

toml
[fmt]
width = 80
indent = 2
align = false

Options

KeyTypeDefaultDescription
widthinteger80Maximum line width
indentinteger2Number of spaces for body indentation
alignbooleanfalseEnable decorative column alignment

Precedence

Settings are merged in this order (later wins):

  1. Defaultswidth=80, indent=2, align=false
  2. sema.toml — project-level configuration
  3. CLI flags--width, --indent, --align override everything
bash
# sema.toml sets width=100, but CLI overrides to 120
sema fmt --width 120

Formatting Rules

Line Breaking

The formatter uses a "try flat, then multi-line" strategy. If a form fits within the line width, it stays on one line. Otherwise, it breaks across multiple lines with appropriate indentation.

scheme
;; Fits on one line
(+ 1 2 3)

;; Too long — breaks with body indentation
(define (calculate-fibonacci-sequence n)
  (if (< n 2)
    n
    (+ (calculate-fibonacci-sequence (- n 1))
      (calculate-fibonacci-sequence (- n 2)))))

Form-Aware Indentation

The formatter recognizes Sema's special forms and applies context-appropriate indentation:

Body forms (define, defn, fn, lambda, do, when, unless, etc.) place the head and key arguments on the first line, then indent the body:

scheme
(defn factorial (n)
  (if (< n 2)
    n
    (* n (factorial (- n 1)))))

Binding forms (let, let*, letrec, when-let, if-let) keep bindings aligned:

scheme
(let ((x 1)
      (y 2)
      (z 3))
  (+ x y z))

Clause forms (cond, case, match) indent each clause:

scheme
(cond
  ((= x 1) "one")
  ((= x 2) "two")
  (else "other"))

Threading macros (->, ->>, as->, some->) indent each step:

scheme
(-> data
  (filter even?)
  (map square)
  (reduce +))

Conditionals (if) place condition, then-branch, and else-branch on separate lines when they don't fit:

scheme
(if (> x 0)
  "positive"
  "non-positive")

Comment Preservation

All comments are preserved in their original positions — inline, trailing, and standalone:

scheme
;; Module header comment
(define x 42) ; inline comment

;; Between forms
(define y 10)

Decorative Alignment

When --align is enabled (or align = true in sema.toml), the formatter column-aligns consecutive similar forms for visual clarity. This is opt-in because it can cause noisier git diffs.

Aligned defines:

scheme
(define x         1)
(define longer-y  2)
(define z         3)

Aligned cond clauses:

scheme
(cond
  ((= x 1)    "one")
  ((= x 100)  "hundred")
  (else       "other"))

Alignment groups are broken by blank lines, so you can control which forms get aligned together.