Skip to main content

Usage

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

The two guarantees

Never changes behavior

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.

Idempotent

fmt(fmt(x)) == fmt(x). Formatting an already-formatted file is a no-op — there’s exactly one canonical form, not a moving target.

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:
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).

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) — so sense fmt captures them separately and re-attaches them by position:
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:

What this doesn’t do (yet)

A very long line stays one very long line — no 80/100/120-column reflow.
Consecutive = signs, trailing comments across several lines, etc. are never column-aligned.
There is exactly one style. No settings file, no options beyond --write/--check.
Directory discovery has no .gitignore-style exclusion mechanism — every .sns file under the given directory is in scope.

Continue

CLI reference

sense run/sense repl/sense test — the rest of the CLI.

Grammar reference

The exact grammar sense fmt prints.