Chapter 2 — Source Syntax and Symbols

Every line in an AZM source file either emits bytes, names a location or controls what comes next. Before any of the larger features — layouts, register contracts, op declarations — there are rules about how lines are formed, what numbers look like and what distinguishes a label from a directive. This chapter covers those rules.


Line structure

Each ordinary source line contains one of:

  • A label, optionally followed by an instruction or directive
  • A standalone instruction or directive
  • A comment alone
  • A blank line

A label on the same line as an instruction:

BUFFER_SIZE   .equ 256
Start:        ld   a,0

A label on its own line, with the instruction on the next:

Start:
              ld   a,0

Both forms are valid. Labels on their own line are common for routines; labels on the same line are common for constants.

Chained instruction lines

Short instruction runs can share one physical line when the instructions are separated by a spaced backslash:

main: ld a,b \ inc a \ ret

AZM assembles that line exactly as if you had written the instructions on separate lines:

main:
        ld      a,b
        inc     a
        ret

Use the chain form for very small runs where the source stays easier to scan on one line. The backslash must be readable as a separator: put whitespace on both sides. A backslash inside a quoted string is still part of the string, not an instruction separator.

Only instructions and op invocations belong in a chain. Directives and declarations still use their own lines. A label may appear before the first instruction in the chain, but not before a later segment:

Start:  xor a \ ld b,a \ ret     ; valid

        ld a,1 \ .db 2           ; error: directive in a chain
        ld a,1 \ Next: inc a     ; error: later label in a chain

Whitespace and separators

One or more spaces or tabs separate tokens. Commas separate operand lists in .db, .dw and similar data directives:

.db $48,$65,$6C,$6C,$6F   ; five bytes

The canonical form is NAME .equ expr. The name always comes first.

Comments

A semicolon starts a comment that runs to the end of the line:

; This whole line is a comment.
        ld a,0   ; inline comment

Register contracts use the .routine directive immediately before a routine label:

.routine in A,HL out carry clobbers BC
ReadKey:
        ret

The directive emits no bytes. Chapter 6 covers routine boundaries and contract analysis.


Labels

Symbols are what let you write djnz READ_LOOP instead of djnz $0105. Every time you write a label in source, AZM records the current assembly address under that name. Every time you reference that name in an operand or expression, AZM substitutes the address. By the time the binary is written, all the names are gone — only bytes remain.

A label names the assembly address at the point where it appears:

Buffer:
        .db 0

AZM records that BUFFER equals the current assembly address. Any instruction or data that references BUFFER gets that address substituted in.

Code labels work the same way:

ReadLoop:
        ld      a,(hl)
        inc     hl
        djnz    ReadLoop

READ_LOOP is the address of the ld instruction. djnz READ_LOOP becomes a relative branch to that address.

Non-local labels

A plain label declares a non-local symbol. Calls, jumps, expressions and data declarations in the same assembled source unit can refer to it directly. Two non-local labels in that source unit cannot share a name.

; error: two definitions of Count
Count:  .db 0
Count:  .db 0

Label syntax

A plain label is an identifier followed by a colon, on a line by itself or before an instruction or directive:

MyLabel:
MyLabel: ld a,0

Both forms are valid. Non-local identifiers contain letters, digits and underscores and must start with a letter.

Do not use $ as a namespace separator in source labels. $ has two source-level meanings in AZM: the current assembly address when written by itself, and hexadecimal notation when followed by hex digits, such as $4000. Imported files provide privacy through .import and @ exports, not through $-qualified labels.

Exported labels

An exported label begins with @ followed by a plain identifier:

@ShiftRow:

The symbol name is ShiftRow, so call sites write call ShiftRow. The @ marks the declaration as visible outside an imported source unit. It has no register-contract meaning; .routine declares a routine boundary.

Export and routine declarations are independent:

.routine in HL out A
@ReadByte:
        ld      a,(hl)
        ret

Owner-local labels

A label beginning with one underscore belongs to the nearest preceding non-local label. The same local spelling can be reused under another owner:

.routine in HL
ShiftRow:
        ld      b,8
_loop:
        rl      (hl)
        inc     hl
        djnz    _loop
        ret

.routine in HL,DE
CopyRow:
        ld      b,8
_loop:
        ld      a,(de)
        ld      (hl),a
        inc     de
        inc     hl
        djnz    _loop
        ret

ShiftRow._loop and CopyRow._loop have distinct identities in AZM output and Debug80 maps. Source code uses the short _loop spelling. A local label cannot be exported, so @_loop: is an error. Equates, enum members, type names and op names cannot begin with _. Names beginning with __ are reserved for assembler-generated symbols.

Forward references

Labels may be used before they are defined:

        ld      hl,DataTable
        ld      b,TABLE_LEN

DataTable:
        .db 1,2,3,4
TABLE_LEN .equ $ - DataTable

AZM uses a two-pass strategy: the first pass assigns addresses to all labels; the second pass substitutes those addresses into instruction encodings. Any reference still unresolved after both passes is an error — typically a typo in a label name.

Multiple labels at one address

Two or more labels can name the same address:

EntryA:
EntryB:
        ld      a,(hl)
        ret

Both EntryA and EntryB call into the same instruction. When a .routine directive precedes consecutive non-local labels, AZM treats them as aliases for the same routine body.


Naming conventions

User symbols are case-sensitive. START, start and Start are three distinct symbols.

The preferred AZM style:

  • Constants (SCREEN_WIDTH, MAX_SPRITES, LCD_DATA): uppercase with underscores.
  • Routine and data labels (DrawSprite:, InitTimer:, SpriteTable:): PascalCase.
  • Owner-local labels (_loop:, _skipInit:, _done:): a leading underscore followed by short camelCase.
  • Exported labels (@ReadKey:, @DrawSprite:): PascalCase after the @.

The assembler enforces no naming policy; different projects may use their own conventions. These give a concrete starting point and match the style used throughout this manual.

Keep non-local labels distinct within their source unit. Owner-local labels can reuse familiar names such as _loop and _done because the owner supplies their identity. Use @ only for declarations that form an imported module’s public interface.


Declaration syntax

Declarations put the declared name on the left, without a colon:

COUNT       .equ 8
Colour      .enum Red, Green, Blue

Sprite      .type
x           .field byte
y           .field byte
            .endtype

SpriteArray .typealias Sprite[2]

A colon marks an address label only — it names the current assembly address, not a constant or type:

COUNT   .equ 8      ; assemble-time constant

Count:              ; address label
        .db 8

COUNT .equ 8 and COUNT: are different things. The first binds a name to the value 8. The second records the address of the .db 8 byte that follows. AZM reports an error for COUNT: .equ 8.


Directives

AZM’s canonical directive names start with a dot:

.org    .equ    .db    .dw    .ds    .end

The full directive list is in Appendix A.

Directives are lowercase and case-sensitive. .db is the canonical form; .DB and .Db are parse errors. Compatibility forms for other assembler source are covered in Chapter 7.


Opcode and register case

AZM is case-insensitive for Z80 instruction mnemonics and register names. LD, ld and Ld all parse as the same instruction; A, a, HL and hl all name the same register. Pick one case for mnemonics and stay with it throughout the project.

The --case-style flag enforces consistency if you want the assembler to flag mixed casing.


Numeric literals

AZM accepts all numeric literal forms common in Z80 assembly:

Form Example Base
$ prefix $FF, $0100 hex
0x prefix 0xFF, 0x2A hex
Trailing H 0FFH, 02AH hex
% prefix %10101010 binary
0b prefix 0b10101010 binary
Trailing B 11110000B binary
Plain decimal 42, 255 decimal
Quoted character 'A', "Z" ASCII value

Trailing-H rule: the token must start with a decimal digit. 0FFH is hex 255. FFH starts with a letter, so the parser reads it as a symbol name. Write $FF or 0FFH.

All numeric forms can appear freely in any expression and can be mixed within one expression:

WIDTH   .equ $20           ; hex prefix
HEIGHT  .equ 32            ; decimal
FLAGS   .equ %00001111     ; binary prefix
DOT     .equ 'A' + 1       ; ASCII + offset
SIZE    .equ WIDTH * HEIGHT ; 1024

See Appendix B for the full numeric literal table.