Formatting
primate ships a normative formatter: there’s one canonical form for
any source. primate fmt rewrites a file to that form; the LSP can
format on save.
Run it
primate fmt path/to/file.prim # format one file in place
primate fmt # format everything under `input`
primate fmt --check # exit non-zero if any file would change
In supported editors, format document (or format-on-save, if
configured) runs primate fmt on the buffer.
Rules at a glance
- 4 spaces. No tabs anywhere.
- One declaration per line. No semicolons.
- Single space around
=and after:. - Sugared types preferred.
T[]overarray<T>,T?overoptional<T>. - Trailing comma in multi-line collections (arrays, maps, tuple values, enum bodies).
- No trailing comma in single-line collections.
- Magic trailing comma in value literals keeps multi-line layout — see Values.
Alignment within groups
Consecutive declarations with no blank line between them form a group.
Within a group, the formatter aligns the type, name, and = columns:
duration TIMEOUT = 30s
u32 MAX_RETRIES = 5
u64 MAX_UPLOAD = 100MiB
A /// doc block is part of the declaration that follows it and does
not break the group. A blank line breaks the group. A standalone //
comment on its own line breaks the group.
Enum bodies follow the same rule — variants align, and = aligns when
any variant has an explicit value:
enum LogLevel: u8 {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
Long-line wrapping
When a logical line would exceed column 100, the formatter wraps at the shallowest delimiter that lets the line fit:
// Before: 134 columns.
type ServiceConfig = map<string, tuple<duration, u64, optional<url>, regex, string>>
// After.
type ServiceConfig = map<
string,
tuple<duration, u64, optional<url>, regex, string>,
>
When a line is wrapped:
- One item per line.
- Trailing comma on the last item.
- Inner contents indented +4 from the line that opened the delimiter.
The wrapper recurses if the inner line is also over budget.
The 100-column budget is fixed.
use block normalization
The block of use statements at the top of a file is normalized:
- Single-item brace groups collapse:
use a::b::{X}→use a::b::X. - Same-path
uselines merge:use a::b::X+use a::b::Y→use a::b::{X, Y}. - Top-level
uselines sort by path. - Items inside a brace group sort lexicographically.
A leading comment on a use line pins that line — sort/merge
happens within contiguous comment-free runs.
See use statements for examples.
What the formatter doesn’t do
- It doesn’t reorder declarations (only
useblocks are sorted). - It doesn’t fix naming-convention violations — the parser flags those
as
naming-conventiondiagnostics; you fix them by hand. - It doesn’t rewrite literals (
100MiBstays100MiB; not normalized to1024 * 100). - It doesn’t desugar
T?→optional<T>(the sugar is preferred).
Config
primate fmt has no command-line knobs in v1. Output is fully determined
by the formatter rules above. This is intentional: one canonical form,
no .editorconfig-style negotiation.