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 → emitGather
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.
MODULEbodies 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. KEEPandGLOBALare 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,!testor!rhaistill 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:
nopGather 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 1if 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 4The 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
retThe 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
retGather 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 shownFix and rebuild. A run that reports one undefined name may have several.
What next
- Modules — placement, nesting, and
--gc-modulesin full. - Diagnostics — every code the stages above can raise.
- Writing Source — what may appear in an expression.