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.

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:
    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.

Where a Forward Reference 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.

A Deferred Body’s Address

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.

-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
    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.

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-modules in full.
  • Diagnostics — every code the stages above can raise.
  • Writing Source — what may appear in an expression.

Documentation