brine fmt rewrites source files in the one canonical Brine layout: 4-space indentation, one space around binary operators and after commas, at most one blank line in a row, and lines of at most 100 characters. It only moves whitespace: it never adds, removes or reorders a token, so a formatted program means exactly what it meant before. Run it before every commit; brine fmt --check fails a CI job when a file isn’t formatted.
brine fmt # every .br file in the projectbrine fmt src/main.br lib/ # these files, and every .br file in these foldersbrine fmt --check # change nothing; list unformatted files, exit 1 if any Formatted messy.br1 file checked, 1 reformattedWithout arguments, brine fmt formats every .br file under the project root (the folder with brine.toml), or under the current directory outside a project. It skips target/ and every folder whose name starts with ..
There are no options to configure. Every Brine file in every project looks the same, which is one less thing for people and agents to decide.
Before and after
Section titled “Before and after”fn area(w:float,h :float)->float : return w*h # width times height
struct Point : x:int y:intfn main(): let xs=[1,2,3,] var total=0 for i in 0..len(xs): total+=xs[i] if total>5 and not(total==7): print( "big",total ) let p=Point(x=1,y=2) print(area(2.0,3.0),p)brine fmt messy.brfn area(w: float, h: float) -> float: return w * h # width times height
struct Point: x: int y: int
fn main(): let xs = [1, 2, 3,] var total = 0 for i in 0..len(xs): total += xs[i] if total > 5 and not (total == 7): print("big", total) let p = Point(x=1, y=2) print(area(2.0, 3.0), p)Both versions compile and print the same thing. Note the trailing comma in [1, 2, 3,]: it’s a token, so brine fmt keeps it.
What it changes
Section titled “What it changes”| Rule | Before | After |
|---|---|---|
| Indentation is 4 spaces per block, whatever the input used | return w*h |
return w * h |
| Tabs in indentation become 4 spaces | a tab | 4 spaces |
| One space around binary and assignment operators | total+=xs[i] |
total += xs[i] |
One space after , and after : in annotations; none before |
fn area(w:float,h :float) |
fn area(w: float, h: float) |
-> has a space on each side; no space before a block’s : |
->float : |
-> float: |
| No space inside brackets, or between a name and its call or index | print( "big",total ) |
print("big", total) |
No spaces around = in keyword arguments |
Point(x = 1) |
Point(x=1) |
No spaces around : and .. in slices and ranges |
xs[0 : 2], 0 .. n |
xs[0:2], 0..n |
| Keywords are separated from what follows | not(total==7) |
not (total == 7) |
| At most one blank line in a row | three blank lines | one |
No blank line right after a line ending in : |
||
One blank line before each top-level fn, struct, enum and test, and after a top-level block ends |
struct Point: right before fn main(): |
a blank line between them |
| Two spaces before a trailing comment | w*h # width |
w * h # width |
No trailing whitespace; the file ends with exactly one newline; \r\n becomes \n |
||
| An expression spread over several lines inside brackets is joined into one line | sum( / x * x / for x in xs / ) |
sum(x * x for x in xs) |
| A line longer than 100 characters is split at its first bracket, one element per line | see below |
A long call:
fn describe(name: str, age: int, city: str, country: str, email: str, phone: str) -> str: return f"{name} {age} {city} {country} {email} {phone}"
fn main(): print( describe("Ada Lovelace", 36, "London", "United Kingdom", "ada@example.com", "+44 20 7946 0000") )The line was split at the ( of print; each element of that bracket goes on its own line, indented one level, and the closing bracket returns to the original indentation. Only the first bracket is split, so a very long single element (here the describe(...) call) can still be over 100 characters.
What it keeps
Section titled “What it keeps”-
Comments, exactly as written (only their indentation and the spacing before a trailing comment change). A comment on its own line takes the indentation of the code below it, unless it is indented deeper (then it stays with the end of the block above).
-
Literal spellings:
0xFF,1_000_000,1e-9, string escapes, f-string contents and format specs. -
Every token: trailing commas, redundant parentheses and the order of everything stay.
-
Lines that have a comment in the middle: a bracketed list spread over lines with a comment after an element keeps its layout; only its indentation changes. This is the way to keep a hand-made layout:
let ys = [1, # one2,]
This file came out of brine fmt with its comment, hex literal, underscores and f-string spec untouched:
# Tools for points.const LIMIT = 0xFF
fn main(): let big = 1_000_000 # keep the underscores let xs = [1, 2, 3] let ys = [x * 2 for x in xs if x > 1] print(xs[0:2], -big, ys, LIMIT) print(f"{big:>12}")Safety
Section titled “Safety”brine fmt works on tokens, not on the syntax tree, so comments and spellings survive. After formatting, it lexes the result again and compares the tokens with the input’s. If they differ in any way (which would be a bug in the formatter), it leaves the file as it was, apart from replacing tabs in indentation.
A file with a syntax error isn’t changed. The error is reported and the command exits with 1:
error[E0110]: expected `)`, found end of line --> broken.br:3:1 |3 | | ^ expected `)` = run `brine explain E0110` for more about this errorerror: could not compile due to 1 error1 file checked, 0 reformattedOnly syntax is checked: brine fmt doesn’t resolve imports or check types, so it formats files that brine check would still reject.
brine fmt –check
Section titled “brine fmt –check”--check writes nothing. It prints each file that would change and exits with 1 if there is one:
brine fmt --checkwould reformat messy.br1 file checked, 1 need formattingWith --json, each file is a line of its own, and the summary isn’t printed:
{"unformatted":"messy.br"}| Result | Exit code |
|---|---|
| everything formatted | 0 |
a file would change (--check) |
1 |
| a file has a syntax error | 1 |
| a file can’t be read or written | 2 |
Add a step that runs brine fmt --check after installing Brine. It needs no C compiler, so it can run before anything is built:
jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install brine run: | curl -fsSL https://raw.githubusercontent.com/brinelang/brine/main/install.sh | sh echo "$HOME/.brine/bin" >> "$GITHUB_PATH" - run: brine fmt --check - run: brine check - run: brine testAs a Git pre-commit hook:
#!/bin/shexec brine fmt --check