Pass Structure
How FantASM’s three passes fit together, for someone reading the source.
How Assembly Works is the same structure written for someone writing Z80: which names may be used before they are defined, where a MODULE body lands, why a warning names a line long after it was read. This page is the half that only matters inside the crate — what each pass may still change, and what a diagnostic raised in each one can say about where it came from.
It is the one page here written for a reader of the crate rather than a user of the assembler. It is not a walkthrough of the code: nothing in it names a function that is not load-bearing for the answer.
Three Passes
gather → decide → emitGather reads the source once. It records what every name is, works out how long each line is, and runs every directive in the order written. A line it cannot finish — an operand naming a symbol further down — is recorded with its bytes unresolved rather than waited on.
Decide runs with the whole file read. It places MODULE and !library bodies after the program, sweeps what --gc-modules or --gc-libraries asked for, checks KEEP and GLOBAL, and catches a block the source never closed.
Emit walks the placed lines and writes the bytes. This is where a name is resolved, so an operand that names nothing is reported here.
What each pass may still change is the whole of why this matters:
| Settled by | |
|---|---|
| How long a line is | gather |
| Whether a line exists at all | gather |
| What address a line lands at | decide |
| What a name is worth | emit |
A directive that decides a size or whether code exists therefore cannot wait: IF and DS both need their operand at gather, and a name defined further down is E1003 rather than a value.
A Diagnostic’s Position
A diagnostic carries the file, line, column and source text the reader was standing on. SessionState holds those while the source is read, and they are correct for anything gather raises.
⚠ After gather, the reader is nowhere. The include stack is empty, so the file name falls back to <none> and the line to 0. A diagnostic raised in decide or emit that takes its position from the reader points at nothing, and quotes whatever line was read last.
Two records exist for that, and they are the same idea at two scales.
Origin is what a placed line carries: its file, line, column, source text, and the scope its names meant something in. It is recorded during gather, and only for a line that could be reported later — one with a deferred operand, one whose size came from $, or one a placement warning could name. Recording it for every line would be keeping a second copy of the source.
If a line fails during the emit walk then:
if let Some(o) = &p.origin {
e.location = Location::new(&o.file, o.line_no, o.column);
e.line_code = o.line_code.clone();
}Site is the same for a !test body, which is replayed after the read for the same reason. enter_site stands the session back where a body line was written, so an assertion failing inside it reports its own line rather than the endtest below it.
A diagnostic raised in decide or emit that carries neither is a fault: it will report <none>:0. W1068 and W1069 did until they were given the placed line’s Origin.
Diagnostic Order
Errors and warnings are printed by different rules, and the difference is visible in a build.
Errors are sorted, by file name and then by line:
order.sort_by(|a, b| a.file_name().cmp(b.file_name()).then(a.line_no().cmp(&b.line_no())));They have to be. Gather files its errors as it reads and emit files its own afterwards, so a fault settled on the emit walk would otherwise arrive after every fault the reader found, whichever line it is on. A MODULE body widens the gap, being placed after the program.
Warnings are printed in the order they were filed, which is the order the passes raised them. A warning raised after the emit walk therefore prints above an error from the walk itself.
⚠ Read the file:line under each rather than the order down the screen. The two lists are ordered by different rules and are printed one after the other.
Line -1
A finding about the build rather than about a line carries -1 — an unknown device named on the command line, a deprecated spelling, a project-file fault.
-1 means no position, and the renderer reads it that way: the file:line:column line is suppressed entirely rather than printed as <none>:-1:1. Sorting reads it too, so a build-level error sorts before every finding that has a line.
⚠ 0 is not -1. Line 0 prints, so a diagnostic that lost its position reports <none>:0:1 and looks like a finding about the first line of a file nobody can open.
PassCoordinator::prepare keeps the -1 findings across a second assemble() on one Assembler and clears the rest, a build-level warning not belonging to the source that is about to be read.