How Assembly Works

Read this when something puzzles you — a name that resolves in one place and not another, a MODULE body that lands somewhere you did not put it, a warning about a line you thought was fine.

FantASM reads your source once, then thinks, then writes. Three stages:

gather  →  decide  →  emit

Gather

The source is read a line at a time: tokenised, macros expanded, included files pulled in where they appear.

As it goes it records what things are — every label and its address, every constant, every macro body, every MODULE boundary, every !test block — and works out how long each line is, because that is what the next address depends on.

It also notes every name a line refers to, without yet caring whether that name exists.

Directives run here. org, if, bank, incbin, !rhai — all of them do their work during gather, in the order written.

Decide

Now the whole file has been read, so questions that need the end of the source can be answered.

  • MODULE bodies are placed. A module is library code, so it is moved to sit after the program rather than wherever it appeared in the file.
  • Unreachable module code is discarded, if you asked for that with --gc-modules, using the reference graph gather built.
  • KEEP and GLOBAL are checked. Both usually name something defined further down, so neither can be resolved until now. One naming nothing is an error rather than a pin that silently protects nothing.
  • Unclosed blocks are caught — a MACRO, MODULE, STRUCT, ENUM, !test or !rhai still open when the source ran out.

Emit

The placed lines are walked and the bytes written. This is where names are resolved: every operand is looked up, and one that names nothing is reported here.

Afterwards — not during — the warnings that depend on where a line ended up are raised, W1069 and W1073 among them.

Seeing it in your source

Nearly every surprising behaviour is one of these three stages showing through.

Referring forward in an operand

    org $8000
    ld  hl,later        ; 21 04 80 — resolved in emit
    ret
later:
    nop

Gather does not need to know what later is; it only needs to know that ld hl,nn is three bytes long. Emit looks it up, by which time the whole file has been read.

Referring forward where it cannot work

    org $8000
    if  LATER           ; E1003: Undefined label or constant: `LATER`
    nop
    endif
LATER equ 1

if decides whether the following lines exist, and it runs during gather. There is no later moment to revisit — the lines it would have skipped have already been read or not read.

The same applies to anything that changes a line’s length rather than its contents:

    org $8000
    ds  SIZE            ; E1003 — gather has to know how many bytes this is
SIZE equ 4

The rule: a name may be used before it is defined when it is a value, and must be defined first when it decides a size or whether code exists. Define constants at the top and this never comes up.

Placing a MODULE body

MODULE Lib
Helper:
    ret
ENDMODULE

    org $8000
main:
    call Lib.Helper     ; CD 04 80 — Helper is at $8004
    ret

The module is written first and assembled last: main gets $8000 and Lib.Helper lands at $8004, after the program. That is decide moving it, and it is what makes a library file safe to include at the top.

$ inside a module body is settled before the move. A size computed from $ keeps the value it had where the line was read, not where it was placed — W1073 reports it and names both addresses. Alignment padding is the usual casualty: it pads to a boundary the line no longer sits on, and everything else about the build looks correct.

Assembling above your ORG

    nop                 ; W1069 — assembled at $0000, in ROM
    org $8000
    ret

Gather assembles lines as it meets them, and there is nothing to assemble into until an org says where. Usually this is a code-bearing include sitting above the org. Give it its own org, or put it in a MODULE — which is placed after the program and so cannot land in ROM.

The warning arrives after the emit walk rather than at the line, because until everything has been placed the assembler does not know a module body written above the ORG was moved to safety.

Reading the first error

Diagnostics are collected through all three stages, and the build reports the first:

    org $8000
    ld  hl,nowhere      ; E1003 — this one is reported
    ld  de,alsonothing  ; also undefined; not shown

Fix and rebuild. A run that reports one undefined name may have several.

What next

  • Modules — placement, nesting, and --gc-modules in full.
  • Diagnostics — every code the stages above can raise.
  • Writing Source — what may appear in an expression.

Documentation