Directives reference¶
Quick reference for every assembler directive a816 understands. Each section lists the syntax, what it emits (or doesn't), and a minimal example.
Layout¶
*= — code position¶
Sets the logical address the next emitted byte targets. Drives where the bytes land in the ROM image.
@= — reloc address¶
Sets the runtime address symbols resolve against, independent of
where the bytes are physically placed. Useful when code is copied to
RAM at runtime: emit at the ROM position with *=, but compute jumps
and label addresses for the RAM target with @=.
*= 0x00C000 ; bytes go into ROM at C000
@= 0x7E2000 ; but symbols resolve as if running from WRAM 7E:2000
ram_routine:
lda.w some_var
rts
Write-overlap detection¶
When two *= regions (or one *= block plus a .alloc placement,
etc.) produce byte spans that share addresses, the assembler emits a
diagnostic so a routine that silently grew past its expected end is
caught early. Default mode is error: the build fails on the
first overlap, naming both source locations. Override with
--overlap-mode warn (logged, build continues) or
--overlap-mode off (silent) on the CLI, or via
Program(overlap_mode=...) from the Python API.
WARNING write at $008004..$00800b overlaps previous write at
$008000..$008009 ($008004..$008009 would be silently overwritten)
.map — memory map¶
Selects the cartridge address mapping. Affects how *= translates
into a physical ROM offset.
Symbols¶
name = expr — constant¶
Defines a constant. Evaluated lazily; can reference externs (resolved at link time).
name := expr — assign¶
Same shape as = but the resolver treats the binding as mutable
during a build (rebinds allowed). Prefer = unless you need this.
.label NAME = ADDR¶
Names a constant address as a label without moving the position counter and without emitting any bytes. Use it for original-ROM stubs, WRAM scratch slots, hardware register aliases — anything you want crash traces, the disassembler, and the LSP to symbolicate by name.
"""Bank-2 hardware Mult8 entry. Input $26 * $28 → $2A. RTL."""
.label mult8_far = 0x02855C
"""WRAM byte at $7E:1BAE — field-menu HDMA channel-5 enable shadow."""
.label field_menu_hdma_enable = 0x1BAE
Differences vs name = expr:
| Property | .label |
= (constant) |
|---|---|---|
| Position counter | untouched | untouched |
| Emits bytes | no | no |
.adbg LABEL record |
yes | no |
lookup_label(addr) resolves |
yes | no |
Cross-module via .extern |
yes | yes |
| Documentable (fluff) | yes (docstring above) | no |
The RHS must evaluate to an int at the current resolution pass —
external references are not allowed (use .extern for that).
.extern name¶
Declares a symbol defined in another module. Required for cross-module
references. Sub-symbols (name.sub) need their own .extern. See
Modules.
.struct Name { ... }¶
Layout-only declaration; emits no bytes. Field names export as
Name.field byte offsets plus Name.__size. Primitive field types:
byte, word, long (24-bit), dword (32-bit). A field type can
also be the name of another previously declared struct, in which
case the nested layout flattens into dotted offsets
(Outer.pos.x, Outer.pos.y).
.struct OAM {
word x
byte y
byte tile
byte attr
}
.struct Inner {
word x
word y
}
.struct Outer {
byte tag
Inner pos
byte flags
}
; Bit fields — `uN` (any positive N) declares an N-bit field that
; packs into the surrounding byte run. Mixing with byte/word/long
; flushes the current byte before the primitive lands.
.struct INIDISP {
u4 brightness
u3 unused
u1 force_blank
}
; → INIDISP.force_blank = 0 (byte offset)
; INIDISP.force_blank.mask = 0x80 (pre-shifted)
; INIDISP.force_blank.shift = 7 (LSB position)
; INIDISP.__size = 1
; → Outer.tag = 0, Outer.pos = 1, Outer.pos.x = 1, Outer.pos.y = 3,
; Outer.flags = 5, Outer.__size = 6
Typed access: as casts and := binds¶
A (expr as T) cast tags an address with a struct type so a postfix
.field resolves through the struct's layout. The two forms share one
mechanism:
; Inline cast, single use.
lda.w (0x2100 as PPU).OAMADDR ; → lda.w $2102
; Typed bind, reusable across many accesses.
p := (0x7e0000 as OAM)
lda.l p.x ; → lda.l $7e0000
lda.l p.y ; → lda.l $7e0002
; Bare form (no parens) also works for `:=`.
q := 0x010000 as Pt
p := (...) eager-expands one constant per (possibly nested) field
of T, so p.field is just a flat symbol after the bind. Nested
struct fields chain cleanly: (o as Outer).pos.y and
o.pos.y both resolve to base + Outer.pos + Inner.y.
Auto-sized opcodes on typed accesses¶
When a typed instance is referenced directly as an operand
(lda p.field), the assembler picks the addressing mode (lda /
lda.w / lda.l) from the binding's base bank — no operand-string
guessing involved. The mapping is:
| Base value | Addressing mode |
|---|---|
< 0x100 |
direct page (lda) |
< 0x10000 |
absolute (lda.w) |
| otherwise | long (lda.l) |
An explicit .b / .w / .l on the opcode always wins. Compound
operands (p.field + 1, raw addresses, casts) keep using the
existing operand-string heuristic.
If the field's declared width disagrees with the current REP/SEP
register width (e.g. lda p.word_field while .a8 is in effect),
the assembler emits a warning suggesting the rep / sep flip
the user probably wants.
Lint hooks:
S001— cast targets a struct type the file never declared.S003—(p as T).fieldwhenpis already bound asT.S004— same(expr as T)repeated more than once; promote to:=.
.a8 / .a16 / .i8 / .i16 — register width¶
Tell the assembler whether the accumulator (A) and index (X/Y)
registers are currently 8-bit or 16-bit. Width drives immediate-mode
opcode sizing: under .a16, lda #0x42 emits A9 42 00 (3 bytes);
under .a8, the same line emits A9 42 (2 bytes).
Inference from rep / sep¶
rep #N and sep #N mutate the CPU's M / X flags at runtime;
the assembler mirrors that at assembly time so source doesn't have
to repeat itself:
rep #0x30 ; clears M+X -> A and X are 16-bit
lda #0x42 ; A9 42 00 (widened because M=16, not because of value)
sep #0x20 ; sets M -> A back to 8-bit
lda #0x42 ; A9 42
Bit 0x20 controls A, bit 0x10 controls X/Y. rep clears
(16-bit), sep sets (8-bit). The inference only fires for constant
immediate operands; symbolic constants resolved at assembly time
count, but forward references and non-immediate forms are left
alone (and explicit .a* / .i* always wins).
Code¶
.scope name { ... } and { ... }¶
Named scopes export labels as name.label. Anonymous { ... } blocks
keep labels strictly local; nothing inside leaks to the parent.
Inside any scope, names starting with _ are LOCAL (private to the
module); other names are GLOBAL.
.macro name(args) { ... }¶
Parameterised expansion. Arguments are textual at expansion time; docstring as first body statement attaches to the macro.
.macro store_byte_at(addr, val) {
"""Stash a byte at `addr`."""
lda.b #val
sta.l addr
}
store_byte_at(0x2100, 0x80)
.if expr { ... } else { ... }¶
Static conditional. The expression is evaluated at assembly time; the unselected branch isn't emitted.
.for var := lo, hi { ... }¶
Compile-time loop. Body is expanded once per integer in
[lo, hi]. var is a binding visible inside the body.
Data¶
.db / .dw / .dl / .dd¶
Emit raw bytes / words / 24-bit longs / 32-bit dwords.
.text "..." and .table "path"¶
Encodes a string using the active character map. Set the map per
scope with .table. Strings expand ${VAR} references against
defined symbols.
.ascii "..."¶
Emits the literal bytes of a string with no character-map translation.
.incbin "data.bin"¶
Includes a binary file verbatim. Defines the named label and
<label>__size with the byte count.
assets_intro_map:
.incbin "assets/intro.map"
; symbols emitted: assets_intro_map, assets_intro_map__size
.include "file.s"¶
Lexically inlines the file at this position. Symbols defined inside
join the current scope. Use .import for module-style separation.
.include_ips "patch.ips"¶
Replays the records of an existing IPS patch into the current build.
Modules¶
See the dedicated Modules page for .import / .extern
semantics, the build workflow, and prelude usage.
Freespace pools¶
See the dedicated Freespace pools page for the full reference. Quick form:
.pool NAME { ... }¶
Declares a named freespace pool with one or more ranges, optional
fill byte, and allocation strategy (pack | order).
.alloc NAME in POOL { body }¶
Reserves space for body in the named pool; the allocator picks the
address and binds NAME there.
.alloc [NAME] at ADDR [size N] { body }¶
Pinned placement: body lands at the literal ADDR. NAME is
optional (3-byte hijacks shouldn't tax with names); the assembler
auto-generates a stable identifier for anonymous allocs.
size N upper-bounds the body. Overflow past ADDR + N - 1 is a
hard error pointing at the byte that no longer fits. Omit size
for an unbounded body (stops at the bank boundary, matching the
legacy *= shape).
.alloc vector_table at 0x00FFE0 size 0x20 {
.dw 0, 0
.dw brk_handler, brk_handler, brk_handler, nmi_handler
.dw 0, brk_handler
.dw 0, 0
.dw brk_handler, 0, brk_handler, 0
.dw reset, brk_handler
}
.alloc at 0x07FFFF size 0x01 {
.db 0 ; pad ROM to 256KB
}
Overlap with any other pinned region (legacy *= included) trips
the overlap auditor with both locations named.
.reserve NAME SIZE [at ADDR] in POOL¶
Byte-less reservation into a (typically bss) pool; lays out a RAM/VRAM
variable flat, no wrapper .alloc block. NAME binds at the reserved
address; nothing is emitted into the image.
.reserve NAME SIZE in POOL: the allocator picks the address..reserve NAME SIZE at ADDR in POOL: pins the slot atADDR. The allocator validates the span lies within a pool range and overlaps no other allocation (pinned or floating), then carves it out. Use for fixed memory maps (VRAM, MMIO mirrors) where the address is the contract but you still want overlap checking across the whole layout..reserve NAME as TYPE in POOL: reservessizeof(TYPE)and publishesNAME.<field>at each struct offset.
.pool vram { bss range 0x0000 0x7fff strategy order }
.reserve bg_char 0x2000 at 0x1000 in vram ; pinned VRAM word
.reserve bg1_map 0x0800 at 0x6800 in vram
.reserve scratch 0x0040 in vram ; allocator picks a free hole
Pinned spans that fall outside the pool or collide with another allocation fail the build, naming the offending reservation.
.relocate SYMBOL OLD_START OLD_END into POOL { body }¶
Moves SYMBOL from [OLD_START, OLD_END] into the pool — old range
is reclaimed before the new body is placed.
.reclaim POOL START END¶
Adds [START, END] to the named pool. Escape hatch for slack with
no original label.
Comments and docstrings¶
; line comment
/* block comment
spans lines */
"""one-line docstring"""
"""
multi-line docstring
attached to the next public target
"""
my_label:
Docstrings attach to modules, scopes, macros, and labels. See Fluff (lint + format) for the placement rules.