Skip to content

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 .macro definitions and macro invocations longer than the configured max_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 by DOC007, not rewritten.
  • Comment text.
  • Data-directive layout — .dw / .db lines stay how the author wrote them. Use ; noqa: E501 to 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

$ a816 fix --select UP001 --unsafe-fixes src/
$ a816 format src/

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:

.db 0x16, 0x20, 0x17, 0x20, 0x17, 0x20, ... ; noqa: E501
MyLabel:                                    ; noqa: N801

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.

include-paths = ["src/include"]

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.