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
| Flag | Description |
|---|---|
--check | Check formatting without writing changes (exit 1 if unformatted) |
--diff | Print diff of formatting changes |
--width <N> | Max line width (default: 80) |
--indent <N> | Indentation width for body forms (default: 2) |
--align | Align consecutive similar forms (defines, cond clauses, let bindings) |
Examples
# 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 --alignProject 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.
[fmt]
width = 80
indent = 2
align = falseOptions
| Key | Type | Default | Description |
|---|---|---|---|
width | integer | 80 | Maximum line width |
indent | integer | 2 | Number of spaces for body indentation |
align | boolean | false | Enable decorative column alignment |
Precedence
Settings are merged in this order (later wins):
- Defaults —
width=80,indent=2,align=false sema.toml— project-level configuration- CLI flags —
--width,--indent,--alignoverride everything
# sema.toml sets width=100, but CLI overrides to 120
sema fmt --width 120Formatting 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.
;; 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:
(defn factorial (n)
(if (< n 2)
n
(* n (factorial (- n 1)))))Binding forms (let, let*, letrec, when-let, if-let) keep bindings aligned:
(let ((x 1)
(y 2)
(z 3))
(+ x y z))Clause forms (cond, case, match) indent each clause:
(cond
((= x 1) "one")
((= x 2) "two")
(else "other"))Threading macros (->, ->>, as->, some->) indent each step:
(-> data
(filter even?)
(map square)
(reduce +))Conditionals (if) place condition, then-branch, and else-branch on separate lines when they don't fit:
(if (> x 0)
"positive"
"non-positive")Comment Preservation
All comments are preserved in their original positions — inline, trailing, and standalone:
;; 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:
(define x 1)
(define longer-y 2)
(define z 3)Aligned cond clauses:
(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.