a816¶
Another 65c816 assembler.
Targets Super Famicom / SNES ROM hacking and patching. Ships a CLI assembler,
an object-file linker, an LSP server, and xdds (a SNES-aware hex dump /
disassembler).
Usage¶
Command line¶
The a816 CLI is subcommand-driven (ruff / cargo style):
$ a816 build <files> -o <output> # assemble + link
$ a816 check <paths> # lint with fluff (DOC*, E501, N801, N802, S00*, UP001)
$ a816 format <paths> # format .s / .i sources with fluff
$ a816 fix <paths> # apply fluff autofixes (--diff / --check / --select / --unsafe-fixes)
$ a816 explain <CODE> # rule rationale + good/bad example pair
Bare invocation (a816 file.s -o out.ips) still routes to build
for backwards compatibility — existing scripts keep working.
a816 build flags¶
-o, --output OUTPUT Output file (default a.out)
-f FORMAT Output format (ips, sfc, obj)
-m MAPPING Address mapping (low, low_rom_2, high_rom)
--copier-header Add 0x200 address delta for ips writer.
--dump-symbols Dump the symbol table.
-c, --compile-only Compile to object files without linking.
-D KEY=VALUE [KEY=VALUE ...]
Define symbols (numeric values use int(., 0)).
--no-auto-imports Disable automatic import resolution.
-I, --module-path PATH Add a module search path (repeatable).
--obj-dir DIR Directory for compiled object files (default
build/obj).
--include-path PATH Add directory to include search path for `.include`.
Separate compilation¶
Compile each module to an object file, then link:
$ a816 build --compile-only file1.s file2.s # produces file1.o, file2.o
$ a816 build file1.o file2.o -o output.ips # link to IPS
$ a816 build file1.o file2.o -f sfc -o output.sfc
$ a816 build file1.s file2.o -o output.ips # mix sources and objects
Lint and format¶
See Fluff (lint + format) for the full rule set, ; noqa
suppression syntax, and editor integration.
$ a816 check src/ # report lint hits ([*]=fixable safe, [!]=fixable unsafe)
$ a816 format src/ # rewrite sources in place
$ a816 format --check src/ # exit non-zero if reformatting needed
$ a816 format --diff src/ # print unified diffs without writing
$ a816 fix src/ # apply safe fixes in place
$ a816 fix --diff src/ # preview as unified diff
$ a816 fix --select UP001 --unsafe-fixes src/ # opt in to one rule's unsafe fix
$ a816 explain DOC003 # rationale + good/bad example pair
Private symbols (_-prefixed labels / macros / scopes) can carry
docstrings without firing DOC002 — naming alone marks them internal.
The legacy a816-fluff binary still works but prints a deprecation
notice on stderr — prefer a816 check / a816 format going forward.
From Python¶
from a816.program import Program
def build_patch(input, output):
program = Program()
program.assemble_as_patch(input, output)
program.resolver.dump_symbol_map()
Syntax¶
See the Directives reference for the full set of
assembler directives — *=, @=, .scope, .macro, .struct,
.if, .for, .text / .table, .incbin, and friends.
Mnemonics¶
adc, and, asl, bcc, bcs, beq, bit, bmi, bne, bpl, bra, brk, brl, bvc, bvs, clc, cld, cli, clv, cmp, cop, cpx, cpy, db, dec, dex, dey, eor, inc, inx, iny, jml, jmp, jsl, jsr, lda, ldx, ldy, lsr, mvn, mvp, nop, ora, pea, pei, per, pha, phb, phd, phk, php, phx, phy, pla, plb, pld, plp, plx, ply, rep, rol, ror, rti, rtl, rts, sbc, sec, sed, sei, sep, sta, stp, stx, sty, stz, tax, tay, tcd, tcs, tdc, trb, tsb, tsc, tsx, txa, txs, txy, tya, tyx, wai, xba, xce
Macros¶
.macro test(var_1, var_2) {
lda.w var_1 << 16 + var_2
}
test(0x10, 0x10)
; expands to: lda.w 0x10 << 16 + 0x10
; emits: lda.w 0x1010
Code pointer relocation¶
Scopes¶
some_address = 0x54
{
lda.b some_address
beq no_action
; label only visible inside this scope
no_action:
}
Named scopes¶
*=0x009000
named_scope {
addr = 0x1234
youhou_text:
.text 'youhou'
.db 0
yaha_text:
.text 'yaha'
.db 0
}
*=0x019A52
load_system_menu_text_pointer(named_scope.youhou_text)
*=0x019A80
load_system_menu_text_pointer(named_scope.yaha_text)
Structs¶
.struct Name { ... } declares a layout. Each field is one of byte,
word, long (24-bit), or dword (32-bit). Field names export as
Name.field constants holding the byte offset from the start of the
struct, plus Name.__size for the total length.
emits OAM.x = 0, OAM.y = 2, OAM.tile = 3, OAM.attr = 4,
OAM.__size = 5.
Use the offsets against any base address — a hardware register, a WRAM pointer, an array stride:
.struct PPU {
byte INIDISP
byte OBSEL
word OAMADDR
}
*=0x008000
lda.w 0x2100 + PPU.OAMADDR ; assembles as LDA $2102
player = 0x7E0010
lda.b player + OAM.x
sta.b player + OAM.tile
Structs are layout-only; they don't reserve storage and don't emit
bytes. Pair with *= or a memory-map directive to place an instance.
Modules¶
.import "module" brings symbols from another translation unit; .extern
declares cross-module references. See Modules for the full
workflow, visibility rules, and constants over externs.
Freespace pools¶
Declare reusable chunks of free ROM, relocate functions into them,
and let the assembler place everything deterministically. The
.pool / .alloc / .relocate / .reclaim directives replace the
manual *= ADDR + end-label + overflow guard pattern with a
declarative pool the assembler manages. In object compilation the
linker unions same-named pools across translation units and runs the
allocator over the merged view, so multiple modules can share a pool
name. See Freespace pools for syntax,
cross-TU usage, error model, and migration from the manual pattern.
Project configuration (a816.toml)¶
Drop an a816.toml at the project root. a816 build (and the bare
a816 <file> form) finds it by walking up from the first input file;
the LSP and fluff read the same file.
entrypoint = "src/main.s"
include-paths = ["src/include"]
module-paths = ["src/modules"]
board = "SHVC-1A3M-30" # a real cartridge board (and/or [map.N])
rom_size = 0x400000
[experimental]
track_register_size = true
| Key | Read by | Meaning |
|---|---|---|
entrypoint |
LSP | root file the server indexes from |
include-paths |
build, LSP, fluff | directories searched by .include |
module-paths |
build, LSP, fluff | directories searched by .import |
board |
build | cartridge board from ares' boards.bml ("SHVC-1A3M-30") |
[map.N] |
build | bus region N, in bsnes/boards.bml form |
rom_size |
build | ROM image size in bytes; required with read-only [map.N] regions |
[experimental] |
build | opt-in feature flags (--experimental NAME) |
--include-path / -I replace the file's include-paths /
module-paths; --experimental flags add to [experimental].
Bus map: board and [map.N]¶
The cartridge layout belongs to the project, not to each module. The
regions declared here are put on the bus of every translation unit
before its own .map lines run, so a module with no local .map still
places code in the project's banks. In object mode they are written
into each .o like a source .map; the linker keeps one copy.
boardnames a real cartridge board from ares'boards.bml(vendored, ares revision pinned ina816.boards.ARES_REVISION, ISC):"SHVC-1A3M-30"(LoROM + SRAM),"SHVC-1J3M-20"(HiROM + SRAM),"SHVC-LJ3M-01"(ExHiROM),"SHVC-1L5B-20"(SA-1), and every other board in the file. It expands to that board's ROM and RAMmaplines (coprocessor MMIO and cartridge slots are left out) plus the console's WRAM (7e-7f:0000-ffff), which no board lists. Boards with ROM needrom_size.[map.N]tables add to a board.[map.N]declares regionN(the identifier a source.mapuses, soNmust be an integer:[map.3],[map.0x3]) the way bsnes and ares'boards.bmlwrite it, so a board'smaplines copy over as they are.addressis required;mask,base(both default 0) andwritableare optional:
rom_size = 0x400000 # 4 MB image
[map.1] # SHVC-1A3M: LoROM ROM
address = "00-7d,80-ff:8000-ffff"
mask = 0x8000
[map.2] # ... and its SRAM, in the same banks
address = "70-7d,f0-ff:0000-7fff"
mask = 0x8000
writable = true
address is BANKS:WINDOW in hex; several bank ranges separate
with commas and share the window. A region owns only its window, so
ROM and SRAM can share banks. The file offset of a read-only address
is computed as in bsnes: the mask bits are removed from the full
24-bit address, base is added, and the result folds into
rom_size (so mirrors land on the same bytes). rom_size is
required as soon as one region is read-only. Writable regions have
no file offset.
- A source
.mapwith the same identifier and the same shape as a toml region is accepted and does nothing; a different shape fails withE0308on the source line. - Declaring any region replaces the
-mdefault bus, exactly as a source.mapdoes, soboardand[map.N]together must cover every region the project uses.-m(defaultlow) only picks the bus of a project that declares none. mapper = "lorom"/"hirom"existed during the 1.1.0 alphas and is gone: writeboard = "SHVC-1A0N-30"/"SHVC-1J0N-20"androm_sizeinstead (E0504names the board).- Changing the bus map rebuilds every cached object.
LSP¶
a816-lsp-server ships with the package: diagnostics, goto-definition
(including .import targets), and hover info. See LSP for
editor setup.
Built-in symbols¶
BUILD_DATE— set automatically to the current date.
.text strings expand ${VAR} references against defined symbols.
xdds¶
SNES-aware hex dump and disassembler.
$ xdds --help
$ xdds rom.sfc --low-rom -s 0x008000 -l 256
$ xdds rom.sfc --low-rom -d --m16 --x16 -n 32 # disassemble 32 instrs
$ xdds rom.sfc --ips patch.ips -s '$01:FF40' # apply IPS, dump from SNES addr
xobj¶
Inspector for the .o object-file format the assembler / linker
exchange. Useful when debugging a link failure or auditing what a
module exports.