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.
Consequences in Your Source
Nearly every surprising behaviour is one of these three stages showing through.
Forward References 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.
Where a Forward Reference 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.
A Deferred Body’s Address
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.
-vv says where each body landed, which is otherwise invisible — the address in the source is not the one it runs at:
Detail: Lib placed at 0x8004..0x8005, 1 byte⚠ $ 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.
Code Above Your ORG
nop ; 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.
⚠ A bin build refuses this outright as E1085, the byte at $0000 and the byte at $8000 being two regions a flat file cannot hold. Other formats carry each region’s own address, and report W1068 and W1069 instead — code emitted in ROM, and code assembled before any ORG set an origin.
Those two name the cause where E1085 names only the symptom. Where a refused build shows the error alone, --format hex will surface them.
The warnings arrive 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.
The Order Diagnostics Arrive In
Diagnostics are collected through all three stages and reported together, so one run names everything it found:
org $8000
ld hl,nowhere ; E1003
ld de,alsonothing ; E1003, and also reported⚠ They are not in source order. A stage reports what it found before the next one runs, so a warning raised after the emit walk — W1068, W1069, W1073 — is printed above an E1003 from the walk itself. Read the file:line under each rather than the order down the screen.
Further Reading
- Modules — placement, nesting, and
--gc-modulesin full. - Diagnostics — every code the stages above can raise.
- Writing Source — what may appear in an expression.