Modules

A module is a namespace bounded by MODULE and ENDMODULE. Everything defined inside one is stored under the module’s name, and its body is placed after the program.

    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 may be a reserved word. MODULE A is legal though a is a register name, a module name being read only as a prefix. It was refused until 2.0, as E1062, which is withdrawn.

Modules and Libraries

MODULE is the legacy form. A new library wants !library, which is the same namespace declared per file — no ENDMODULE to forget, a guard of its own, includes scoped by position, and an INTERFACE saying what is public.

MODULE is kept for two things:

  • Source that already uses it. Nothing about it has changed.
  • Placing bytes. A module body goes after the program, which is how a payload a loader reads out of the file gets to the end. Used that way it is not a namespace at all, and --gc-libraries leaves it alone.

The scope rules below are shared with libraries and are written here once.

Scope and References

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.Onein the current module first, then exactly as written
@Gfx.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 the current module’s first. Col.Green inside MODULE Gfx is Gfx.Col.Green where that exists, and otherwise is used as written — which is how one module calls another:

MODULE Gfx
ENUM Col
    Red
    Green
ENDE
Inner:
    ld      a, Col.Green    ; Gfx.Col.Green
    call    Snd.Play        ; nothing of that name here, so the other module
    ret
ENDMODULE

This is what makes a STRUCT instance or an ENUM usable inside the module that declares it, without repeating the module name on every line.

Where both exist, the module’s own wins, as it does for a bare name. @ reaches past it: inside MODULE Alpha holding an instance pos, pos.x is Alpha.pos.x and @pos.x is member x of a module called pos.

Forward references work in all three forms — the target may be defined later in the file.

Nested Modules

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

Types and Macros

A STRUCT type and a MACRO belong to the module declaring them, so two may each define a Pt or an Init without one having to rename around the other. The rule is the same for a library, and is written up there — including the fall-back to the program’s, which a label does not have.

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.

GLOBAL and Export

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

Garbage Collection

--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 -u as unused — it was dropped precisely because nothing referenced it.

It takes a placement module too. A module used to put bytes at the end of the file is structurally unreferenced, so this sweeps it — which is what the KEEP in such a module is usually working around. --gc-libraries leaves it alone, and is the flag to move to.

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.

Naming a member reaches the whole instance. A STRUCT instance is one object, so ld (Gfx.view.colour),a keeps all of Gfx.view — a field is not separately discardable, because dropping it would move the fields after it. -u reads it the same way.

That was broken until 2.0, and silently: an instance reached only through a member name was swept, its bytes left the image, and the instruction went on addressing where they had been. !test could not see it — a test runs against flat RAM, so the write landed and assert_mem passed. If you are on an older build, reach the instance by name once.

Jump Tables

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.

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.

Error Codes

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. E1064 and E1065 are MODULE’s alone — a library closes at the end of its own file, so there is nothing to leave open.

A library’s own errors are on the Libraries page.

Documentation · Libraries