Libraries
A library is a file. !library says the rest of it is one, so there is no ENDMODULE to write or to forget:
INCLUDE "macros.inc"
!library Ctc
Start: ; stored as Ctc.Start
retThis is the form to reach for. MODULE still exists and still works, but it is the legacy way to namespace and a new library wants !library.
A library names things exactly as a module does, and its body is placed after the program in the same way — so the scope rules, @, and local labels are the module page’s and are not repeated here. What follows is what a library adds.
⚠ A library cannot be declared inside anything — not a MODULE, MACRO, STRUCT, ENUM or another library. That is E1095. Two files declaring one name is E1096, which is not the same as one file arriving twice.
A library name may be a reserved word. !library Device is legal, a library name being read only as a prefix.
Include Guard
Included twice, a library is emitted once. The IFNDEF/ENDIF wrapper every hand-written library carries is what this replaces.
That is why two files claiming one name is an error rather than a second include: one prefix cannot mean two things, and nothing downstream could tell which was meant.
Include Scope
Above the declaration its names are global; below, they are the library’s.
Shared above, private below. Shared definitions go above the line and stay global, which is where every existing library already puts macros.inc. A .inc that belongs to one library goes below.
⚠ A shared constant is reached from inside a library with @NAME. A constant does not fall back to global scope from inside a module and a library is no different, so WIDTH defined above the line is @WIDTH within it. Macros need no such thing — see below.
⚠ Guard a shared .inc on a plain constant, not on @__NAME__. A plain guard is itself scoped, so every library including that file below the line gets its own copy. A global guard means the first include wins and every later library silently gets nothing.
Types and Macros
A STRUCT type and a MACRO belong to the module or library declaring them, so two may each define a Pt or an Init without one having to rename around the other:
!library Gfx
STRUCT Pt ; Gfx.Pt
x byte
y byte
ENDS
here Pt 1, 2 ; the library's own, by the short nameThe type name may be qualified anywhere an instance is written — pos Gfx.Pt 1, 2 — which is how the program, or another library, instantiates one.
⚠ These fall back to the program’s where a label does not. A bare name is the current module’s if it has one, and otherwise the program’s. That is deliberate and it is what keeps shared above, private below working: macros.inc sits above every !library line in the estate, and every library below invokes what it defines. A constant in the same file needs @NAME; a macro beside it does not.
@NAME reaches the program’s past one of the same name, as it does for a label:
MACRO Twice ; the program's
ld a, 2
ENDM
!library Gfx
MACRO Twice ; Gfx.Twice, shadowing it
ld a, 1
ENDM
Draw:
Twice ; the library's — 3E 01
@Twice ; the program's — 3E 02
retSIZEOF follows the type, so SIZEOF(Pt) inside a library answers for the library’s and SIZEOF(Gfx.Pt) answers from anywhere.
@NAME works inside SIZEOF too, so a library that shadows a type can both build one of the program’s and ask how big one is:
org $8000
STRUCT Pt ; the program's
x byte
y byte
ENDS
!library Alpha
STRUCT Pt ; this library's own, shadowing it
only byte
ENDS
MINE = SIZEOF(Pt) ; 1 — this library's
THEIRS = SIZEOF(@Pt) ; 2 — the program's
here @Pt 1, 2 ; and one of the program's, built here⚠ @ does not fall back. It is written when the nearest scope has a name of its own and the program’s is wanted instead, so where global scope has no such type or label it is E1037 rather than the library’s own. That is also why there is no other spelling: a program’s global types carry no prefix, so SIZEOF(Global.Pt) cannot be written.
ENUM was already namespaced and needs none of this.
The Interface
An INTERFACE lists what the library makes public. It is unnamed and written inside the library it describes, so there is nothing to bind it to and nothing to keep in step:
!library Ctc
INTERFACE
Start
Stop
CHANNELS
ENDINTERFACEThe library must define every name listed, or E1100 against the !library line. A promise may name a label, a constant, a macro, a STRUCT type, an ENUM or a STRUCT instance — naming a container names its parts, since an instance is no use without its members.
A name arriving from an INCLUDE below the declaration counts, and that is the point. lib/fs.asm lists Open, Close, Read and Write and writes none of them; they arrive with INCLUDE "fs/esx.asm". Swap the backend for one that provides less and the check says so — which is the guarantee a list of names in a comment cannot give:
Routines arrive with their backend rather than being declared here: a list of names in a comment is a promise that rots the first time one of them changes.
Enforcement
A name the list omits is refused from outside the library, and @ goes round it:
call Ctc.Start ; listed — fine
call Ctc.scratch ; E1101, and the message says what Ctc does offer
call @Ctc.scratch ; assembles, silentlyIt is a design contract, not security. A Z80 program can call any address, so nothing here claims to prevent a reach inside — it makes a deliberate one legible. @Ctc.scratch is greppable and obviously a decision; arriving at a private name by accident is what this catches. Enforced against carelessness, not against intent, which is the only enforcement available.
⚠ No warning on the @ form. It is already the author saying they know, and the source shows it. A warning would fire on every legitimate use to catch a misuse that is visible anyway.
Naming a container publishes its parts — a published instance publishes its members, a published ENUM its values. A published label’s local labels stay private: Ctc.One.loop is a jump target and no caller has business naming one.
The library’s own code is unaffected, and so is !test, which reaches whatever it needs wherever it is written — a test body emits nothing, so the artifact is byte-identical either way.
⚠ A library naming no interface is unchanged, so this is opt-in and nothing existing breaks. MODULE has none at all.
One per library — a second is E1098 — and it goes at the top level of the file that declares the library, below the !library line. Above that line there is no library yet, and in a file included below it the contract would travel with the thing it constrains. Anywhere else, including nested in a MODULE inside the library, is E1097.
Garbage Collection
--gc-libraries, or gc_libraries = true in fantasm.toml, sweeps !library code the program never reaches and nothing else. --gc-modules takes any namespace, and the two are separate for a reason:
MODULE does two jobs — it namespaces, and it places its body after the program — and a module used for the second is structurally unreferenced, so a sweep aimed at unused library routines takes it. The usual workaround is a KEEP that says nothing about reachability:
MODULE Payload ; a loader reads this out of the file
KEEP body ; unnecessary under --gc-libraries
body:
incbin "payload.bin"
ENDMODULERun both while moving, and drop --gc-modules once every remaining MODULE is either a !library or genuinely a placement device. A MODULE written inside a library is the library’s, and goes with it.
⚠ An INTERFACE name is not a root. The list says what may be called, not what is — if listing a name protected it, a program using one routine from a twelve-routine library would carry all twelve. KEEP, GLOBAL and references from outside any library remain the roots, and pinning works as it does for a module.
⚠ It is off by default. A routine wrongly discarded produces no diagnostic, only a crash when the program runs.
Error Codes
E1063 | Invalid or missing library name |
E1095 | A library declared inside something |
E1096 | Two files declaring one library name |
E1097 | INTERFACE outside a library, or nested in something inside one |
E1098 | A second INTERFACE in one library |
E1099 | ENDINTERFACE without INTERFACE |
E1100 | The interface promises a name the library does not define |
E1101 | A name outside the interface, reached from outside the library |
← Documentation · Modules