Fluff: lint, format, fix¶
a816 check, a816 format, and a816 fix are the lint, format, and
autofix passes over .s / .i sources. All three share the parser
the assembler uses, so the rules see real AST, not regex hits.
Given no paths, they take the project the nearest a816.toml (from
the working directory up) describes: the sources under the
entrypoint's directory and every module-paths directory, leaving
out build/, obj/ and dot directories. In a git work tree, files
git ignores are left out too, and the toml's exclude patterns
(exclude = ["scratch", "old_*.s"], relative to the toml) leave out
the files and directories they match. Outside a project, name the
files.
Format¶
$ a816 format src/ # rewrite in place
$ a816 format --check src/ # exit non-zero if any file would change
$ a816 format --diff src/ # print unified diffs without writing
$ a816 format - # read stdin, write formatted text to stdout
The formatter is a fixed-point pass: format(format(x)) == format(x).
What it touches:
- Indents instructions, aligns operands, lowercases mnemonics.
- Wraps
.macrodefinitions and macro invocations longer than the configuredmax_line_length(default 120) with hanging-indent parameter lists. - Strips per-line trailing whitespace and dropped wrapper-blank lines inside docstrings.
What it deliberately does not touch:
- Docstring content (text between
"""..."""). Author's prose stays verbatim: alignment with the target is enforced byDOC007, not rewritten. - Comment text.
- Data-directive layout:
.dw/.dblines stay how the author wrote them. Use; noqa: E501to silence line-length on long data rows.
Lint rules¶
Codes follow ruff conventions: DOC* for docstring coverage and
placement, E*** for physical layout, N*** for naming, S*** for
struct casts, OP*** for opcodes, ST*** for program structure and
UP*** for upgrades from legacy syntax.
| Code | What it flags |
|---|---|
DOC001 |
Module is missing a leading docstring. |
DOC002 |
Public macro / scope / label / .label directive is missing a docstring. |
DOC003 |
Docstring sits directly above a public target. For macros / scopes, move it inside the body (first statement after {); for code labels (name:), move it below the label (first statement after the colon). .label directives are body-less, so the docstring above is the canonical attach point and DOC003 does not fire. |
DOC004 |
Orphan docstring used as a comment. Convert to ; or attach to a target. |
DOC005 |
A leading comment block (≥2 ; lines, or a block comment with embedded newlines) sits where a docstring belongs. |
DOC006 |
Public target carries both a leading comment block and a docstring; pick one. |
DOC007 |
Docstring content is under- / over-indented relative to its opening """ (mirrors pydocstyle's D207 / D208). |
E501 |
Source line longer than 120 characters. |
N801 |
Label name is not snake_case. |
N802 |
Constant name is not snake_case or SCREAMING_SNAKE_CASE. |
S001 |
(expr as T).field / p := (expr as T) / .istruct T { ... } references a struct type that isn't declared in the current translation unit. |
S003 |
Redundant cast: (p as T).field when p is already typed-bound to T. |
S004 |
The same (expr as T) cast appears more than once in the file; promote it to a := typed bind. |
OP001 |
Explicit .w / .l size suffix the operand would get anyway. |
ST001 |
Placement directive (*=, .alloc ... at, .relocate) nested inside an .alloc body: it re-anchors the code the alloc was placing. |
ST002 |
.import outside the file prelude (after a placement or inside a block); the build rejects it with E0311. |
UP001 |
Legacy *= ADDR placement should be .alloc at ADDR { ... }. |
W0002 |
.if on a name the project defines nowhere: it reads as false, so a deleted or misspelt flag drops code silently. Declare a -D-only name under [defines] in a816.toml. |
W0001 |
Reference to a name .incbin derived from its path (assets_vwf_bin, assets_vwf_bin__size); the build warns on it too. A warning: a816 check prints it but still exits 0. |
Rules marked fixable in a816 check output carry [*] (safe) or
[!] (unsafe). Today: S003, OP001, DOC003, DOC004, DOC006,
DOC007 ship a safe fix, and W0001 does when the blob is alone in a
named alloc (vwf_font, sizeof(vwf_font)); DOC005 and UP001 ship
an unsafe fix.
Autofix: a816 fix¶
$ a816 fix src/ # apply safe fixes in place
$ a816 fix --diff src/ # preview as unified diff
$ a816 fix --check src/ # exit non-zero if any file would change
$ a816 fix --select DOC003,DOC004 src/ # only those rules
$ a816 fix --unsafe-fixes src/ # also apply UNSAFE fixes
Safe fixes are guaranteed behaviour-preserving: they don't change
emitted bytes, drop comments, or restructure semantics. Unsafe fixes
might change runtime behaviour, repackage author whitespace, or
surface latent bugs as new build errors. --unsafe-fixes opts in;
without it, unsafe hits stay flagged but untouched.
Edits inside one file apply in reverse-offset order so an earlier
edit's replacement length can't invalidate a later edit's offsets.
Overlapping edits are dropped silently; re-run a816 fix and the
next pass produces non-overlapping edits.
Migration recipe: *= → .alloc at¶
UP001 wraps each *= ADDR and the following body run (up to the next
placement directive) in .alloc at ADDR { ... }. The bank-edge
semantic shift is real: where legacy *= silently let body bytes
cross a bank boundary, the new form raises with the offending byte
named. Surfaces real bugs; you may want to walk through the build
errors after the fix.
Editor integration: code actions¶
a816-lsp-server exposes every fluff fix as a textDocument/codeAction
quickfix. The editor's lightbulb (Ctrl-., Code Action menu) lists
each applicable fix; safe ones are marked isPreferred=true so most
editors default to them, unsafe ones carry an (unsafe) suffix in
the title.
Names with a single leading underscore (_loop, _private_macro) are
treated as private and skipped by DOC002 / DOC003 / naming rules.
a816 explain <CODE>¶
Print a rule's rationale plus a minimal bad / good example pair.
Given a build diagnostic's code (E0317, W0001), it prints that
code's entry from the error catalog.
$ a816 explain DOC003
DOC003 docstring above macro/scope should live inside the body
For block-bodied targets (`.macro`, `.scope`) the canonical attach point is the
first statement inside the body. ...
Bad:
"""Module."""
"""Setup loop counter."""
.macro setup_counter() {
ldx.w #0
}
Good:
"""Module."""
.macro setup_counter() {
"""Setup loop counter."""
ldx.w #0
}
The examples are checked: tests round-trip every rule's bad / good
through lint_text, so a rule that drifts from its docs fails CI.
Private symbols¶
Names with a single leading underscore (_loop, _helper, _internal)
are LOCAL to their module. Fluff doesn't require them to carry a
docstring (DOC002), and DOC005 / DOC006 don't apply to them: those
are public-API hygiene rules. They can still carry a docstring; if
they do, DOC003 / DOC004 / DOC007 enforce the same placement and
formatting rules as on public targets.
Suppressing rules: ; noqa¶
A trailing ; noqa comment silences every rule on that line. Pass codes
to suppress selectively, ruff-style:
Code lists are case-insensitive: ; noqa: e501,n801 works the same.
Selecting rules: [lint]¶
A rule the project breaks on purpose goes in a816.toml rather than on
every line:
[lint]
ignore = ["N802"] # French CamelCase constants
# select = ["DOC", "N"] # only these; empty or absent runs every rule
Codes match by prefix, as in ruff: N is every naming rule. check,
fix and the LSP all honour the table. a816 check --select CODES and
--ignore CODES (comma-separated, or repeat the flag) replace the
table's list of the same name for one run. The other list still
applies: with ignore = ["N802"] in the table, --select N802 shows
nothing; add --ignore "" to clear the table's ignore. A code no rule
starts with is an error (E0516). Parse errors always show.
a816.toml discovery¶
Both a816 check and a816 format walk upwards from each input file
looking for a816.toml. When found, the config's include-paths are
forwarded to the parser so .include "..." directives resolve the
same way they do under a816 build.
a816.toml is the project file: a816 build, fluff and the LSP all
read it, so the three resolve .include and .import the same way.
module-paths lets struct-type rules follow .import chains;
entrypoint is only used by the LSP. See
Project configuration for
every key.
Editor integration¶
The a816-lsp-server runs lint_text on every analyze and surfaces
results as DiagnosticSeverity.Warning with source = "a816 fluff" and
the rule code attached. No additional setup beyond the LSP: see
LSP.