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
    ret

Labels, 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:

WrittenLooked up
Onein the current module only
Gfx.Oneexactly as written, from anywhere
@Oneat 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
ENDMODULE

An 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
ENDMODULE

KEEP 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

E1062The module name is a reserved word
E1063Invalid or missing module name
E1064ENDMODULE without MODULE
E1065A MODULE was never closed
E1067KEEP names something that is not defined

MODULE and ENDMODULE must pair up, including when nested.

Documentation