10. Canonical form

The compiler rejects non-canonical code with non-canonical errors. synod fmt rewrites any parseable file into canonical form, so in practice nobody formats by hand. Code across all repositories looks alike.

Area Canonical rule
Encoding UTF-8, final newline, no trailing spaces, no byte order mark
Line endings LF; CR LF is non-canonical
Header the first non-blank line is # package <name>: the folder name, or main in the folder's main file; synod fmt never adds or changes it
Imports directly after the header, one per line, sorted by the byte order of the full path text, no blank lines between them; a use after a declaration is moved up
Header and body with imports: one blank line after the last import; without imports: the first declaration follows the header directly
Indentation 2 spaces per level
Blank lines exactly one between top-level declarations (none between imports); none inside blocks, none between a block and its continuation line
Comments # with one space, then the text verbatim; a bare # stays bare; a trailing comment has one space before #; a comment line takes the indentation of the line it precedes, and a comment after the last line of a block takes that block's indentation; a comment between a block and its continuation stays and precedes the continuation line; top-level comments detached by a blank line stay detached
Parentheses around every multi-item argument; never around a single item. The formatter removes parentheses but never inserts them
Holes always typed, element types included: [n: node], [ns: list of Node]
Receivers define [n: Node] Learn [k: node] at [v: number], when [n: Node] (net.Received digest from sender): one space after the receiver
Members n.Known, Vote.term: never parenthesized; a space around . is a syntax error; a package's own qualifier inside it (n.raft.Role in raft) is non-canonical, and synod fmt drops it when the package loads (the own name alone as a package value stays)
Numbers 0.5, -1, 2; no leading zeros, no trailing zeros. .5, +1 and x-1 are never written: they are syntax errors, not formatting
Durations the largest unit giving a whole number: 1500ms, 2s; otherwise milliseconds with the fraction (20.431ms)
Text double quotes; escapes only \", \\, \n; no control characters (syntax)
Commas {a, b}, message T with a 1, b 2, each k, v in m: one space after each comma, none before, none inside braces
Records message T with a 1, b 2: settings in declaration order of the fields; out of order is non-canonical, and synod fmt reorders it when the package loads
Colors upper-case hex: #F4A340
Continuation lines else, otherwise: own line at the statement's indentation, directly after its block
Negation not (a is b) becomes (a is not b) and not (a is not b) becomes (a is b); likewise less than/at least, more than/at most, is in/is not in; the rewrite applies only when the parenthesized sentence is a single comparison, so not (a and b) is unchanged
Names exported names start upper-case, nothing else does; no underscores; locals, each names, hole names (receivers included), new when bindings, draw names, attachment words and import aliases start lower-case (non-canonical; synod fmt renames them when the package loads and the new name is free)

The negation row is how Synod keeps one spelling per meaning while still offering is not, at least and friends: the formatter rewrites a negated comparison into the operator that means the same.