> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sensecode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Formatter

> sense fmt: a canonical pretty-printer built on the existing AST — minimal parens, preserved blank lines, comments never dropped.

## Usage

```bash theme={null}
sense fmt myfile.sns          # print formatted source to stdout
sense fmt myfile.sns --write  # rewrite the file in place
sense fmt src/ --check         # exit 1 if anything under src/ isn't canonical (CI-friendly)
```

`sense fmt` parses to the AST and re-emits it — it's a printer over
exactly the grammar in the [Grammar Reference](/reference/grammar), not a
separate tool with its own rules. Directory targets are searched
recursively for every `.sns` file, no naming restriction (unlike
[`sense test`](/language/testing), which only matches `test_*.sns`/
`*_test.sns` in a directory).

## The two guarantees

<CardGroup cols={2}>
  <Card title="Never changes behavior" icon="shield-check">
    Verified against every real example in `examples/`, not just synthetic
    snippets: run a file, format it, run the formatted version, compare
    output. Identical, every time.
  </Card>

  <Card title="Idempotent" icon="rotate">
    `fmt(fmt(x)) == fmt(x)`. Formatting an already-formatted file is a
    no-op — there's exactly one canonical form, not a moving target.
  </Card>
</CardGroup>

## Minimal parens, not "no parens"

The AST already records the exact grouping the parser produced — `sense
fmt` doesn't need to re-derive meaning, only re-print it correctly. The
printer walks operator precedence (the same levels as the grammar: `or` \<
`and` \< equality \< comparison \< `+ -` \< `* / %` \< unary \< call/index/member)
and adds a paren only where the child's own precedence is lower than what
the parent position requires:

<CodeGroup>
  ```sns Before theme={null}
  x = 1 + 2 * 3
  y = (1 + 2) * 3
  ```

  ```sns After (unchanged) theme={null}
  x = 1 + 2 * 3
  y = (1 + 2) * 3
  ```
</CodeGroup>

The first line's parens were never there to begin with (multiplication
already binds tighter); the second line's parens are semantic — removing
them would change the value — so they're kept.

## Blank lines: preserved, not imposed

Wherever the source had at least one blank line immediately before a
statement or comment, the output gets exactly one — multiple consecutive
blanks collapse to one, and none stays none. There's no rule that forces a
blank line around every function or agent a user didn't already separate;
adding vertical space nobody asked for would cut against Sense's own
minimum-lines goal (see [Non-Goals](/philosophy/non-goals)).

## Comments are re-attached, not dropped

Comments aren't part of the AST — the lexer discards them before the
parser ever sees a token stream (see
[Indentation-Sensitive Lexing](/architecture/indentation-lexing)) — so
`sense fmt` captures them separately and re-attaches them by position:

<CodeGroup>
  ```sns Standalone comment theme={null}
  # explains the next line
  x = 5
  ```

  ```sns Trailing comment theme={null}
  x = 5  # stays on this line
  ```
</CodeGroup>

A *standalone* comment (its own source line) prints immediately before the
next statement; a *trailing* comment (followed real code on the same
line) stays attached to that line. What isn't preserved: manual
column-alignment of several trailing comments, or extra internal spacing
after `#` — text is never lost, but re-printed with normalized spacing
(`# text`, `statement  # text`).

## What gets canonicalized, not preserved verbatim

A few constructs have exactly one printed form regardless of how they were
written, which is what idempotence requires — the *input* doesn't need to
round-trip, only the *output* does:

<Frame>
  | Written as                      | Always prints as                           |
  | ------------------------------- | ------------------------------------------ |
  | `policy:` with exactly one rule | inline: `policy: allow x.y`                |
  | `requires approval, x.y`        | capability first: `requires x.y, approval` |
  | `if x: return y` (inline block) | expanded: `if x:` / `    return y`         |
</Frame>

## What this doesn't do (yet)

<AccordionGroup>
  <Accordion title="Line-width wrapping">
    A very long line stays one very long line — no 80/100/120-column
    reflow.
  </Accordion>

  <Accordion title="Alignment">
    Consecutive `=` signs, trailing comments across several lines, etc.
    are never column-aligned.
  </Accordion>

  <Accordion title="Configuration">
    There is exactly one style. No settings file, no options beyond
    `--write`/`--check`.
  </Accordion>

  <Accordion title="Ignore lists">
    Directory discovery has no `.gitignore`-style exclusion mechanism —
    every `.sns` file under the given directory is in scope.
  </Accordion>
</AccordionGroup>

## Continue

<CardGroup cols={2}>
  <Card title="CLI reference" icon="terminal" href="/reference/cli">
    `sense run`/`sense repl`/`sense test` — the rest of the CLI.
  </Card>

  <Card title="Grammar reference" icon="code" href="/reference/grammar">
    The exact grammar `sense fmt` prints.
  </Card>
</CardGroup>
