Modules
A module is a namespace. Everything defined inside one is stored under the module’s name, so a library can use short internal names without colliding with the program that includes it — and with --gc-modules, a program pays only for the parts of a library it actually calls.
ORG $8000
MODULE Gfx
DrawIt: ; stored as Gfx.DrawIt
ret
ENDMODULE
main:
call Gfx.DrawIt
retLabels, constants and ENUM members are all namespaced, so two modules may each define their own WIDTH.
Modules nest, and the name joins with a dot at each level — Outer.Inner.Deep.
⛔ A module name cannot be a reserved word. MODULE A and MODULE B are rejected, because a and b are register names. That is E1062 and it names the word; see Diagnostics.
Reaching in and out
A module is a closed scope. There are exactly three forms, and no implicit search of enclosing scopes:
| Written | Looked up |
|---|---|
One | in the current module only |
Gfx.One | exactly as written, from anywhere |
@One | at global scope, ignoring every enclosing module |
A bare name is module-only. A global helper: is not reachable as call helper from inside a module. FantASM says so rather than leaving you to work it out:
Error [E1003]: Undefined label or constant: `helper`
Hint: `helper` is defined outside this module — reach it with `@helper`A dotted name is used as written, which is how one module calls another and how a module may name itself.
Forward references work in all three forms — the target may be defined later in the file.
Reaching out of a nested module
@ means global, not “one scope out”. A nested module does not see its parent’s names unqualified, and @ will not find them either:
MODULE Outer
One:
ret
MODULE Inner
Two:
call One ; E1003 — Outer is not in scope here
call @One ; E1003 — @ looks at global scope, not at Outer
call Outer.One ; correct
ret
ENDMODULE
ENDMODULE@ is a reference form only. @name: is not a definition, and @ is not valid inside a name.
Local labels
A local label (leading .) attaches to the nearest non-local label above it, and inside a module that label is already qualified — so .loop after One: in MODULE Gfx is stored as Gfx.One.loop. Two modules can use the same local names freely, which is the ordinary way to write loops.
Exporting with GLOBAL
GLOBAL controls export, not visibility. Every label in a module is reachable from outside by qualified name whether or not it is global. GLOBAL decides what --export-labels and the .sld file contain, and the name written is the qualified one:
MODULE Gfx
GLOBAL DrawIt
DrawIt: ; exported as "Gfx.DrawIt = 0x8005"
ret
Hidden: ; still callable as Gfx.Hidden, just not exported
ret
ENDMODULEAn export is also a use. -W does not report an exported label as unused and --gc-modules does not discard one, because in both cases the caller is outside this assembly and there is no reference here to find.
Discarding what you never call
--gc-modules, or gc_modules = true in fantasm.toml, leaves out module code the program never reaches. The rule is the same one that governs scope:
Code outside a module is the program. Always emitted, and where reachability starts. Code inside a module is library. Emitted only if the program reaches it.
So nothing written outside a module can ever be discarded, however unreferenced — turning this on cannot shrink a program that uses no modules.
⛔ It is off by default. A routine wrongly discarded produces no diagnostic, only a crash when the program runs.
A discarded label is not also reported by -W as unused — it was dropped precisely because nothing referenced it.
Pinning with KEEP
Reachability is worked out from references in your source. Code reached by an address the assembler never sees — computed arithmetically, built at run time, or reached by self-modifying code — has no reference to follow and would be discarded.
MODULE Gfx
KEEP Handler ; reached only via a computed address
Handler:
ret
ENDMODULEKEEP naming a label that does not exist is an error (E1067), because a pin that quietly protects nothing is worse than no pin. It resolves like any other reference: written inside a module it names that module’s label; from outside it needs the qualified name.
Building a jump table
A jump table needs no KEEP. dw names its targets, so they are ordinary references and are followed like any other:
MODULE Gfx
table:
dw HandlerA ; both named here, so both are reachable
dw HandlerB
HandlerA:
ret
HandlerB:
ret
ENDMODULE⛔ But only if the table itself is reached. Reachability is a walk from the program inwards: if nothing refers to table, the table is discarded, and then its dw entries are not references from anywhere and HandlerA and HandlerB go with it. A table that only a computed address ever reads needs KEEP table — pinning the table is enough, since the entries then follow.
Avoiding cross-bank calls
A module body placed in a different bank from the code that calls it is untested with --gc-modules and is not diagnosed. If you page banks yourself and call across them, leave elimination off.
Meeting the errors
E1062 | The module name is a reserved word |
E1063 | Invalid or missing module name |
E1064 | ENDMODULE without MODULE |
E1065 | A MODULE was never closed |
E1067 | KEEP names something that is not defined |
MODULE and ENDMODULE must pair up, including when nested.