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.
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.
| 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) 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. |
UP001 |
Legacy *= ADDR placement should be .alloc at ADDR { ... }. |
Rules marked fixable in a816 check output carry [*] (safe) or
[!] (unsafe). Today: S003, DOC003, DOC004, DOC006, DOC007
ship a safe fix; 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.
$ 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.
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.
This means a project that already configures the LSP via a816.toml
gets the same module / include resolution from fluff with no extra
flags. The entrypoint and module-paths fields are read but only
consumed by the LSP today.
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.