Chapter 7 — Composition
Every chapter so far kept the whole program in one .asm file. That is fine while you are learning a single algorithm. Real projects outgrow one screen: string helpers, table drivers and board-specific I/O stubs each deserve their own file — but AZM still produces one flat listing with no import, no modules and no hidden linker.
This chapter splits a tiny program across files with .include, pulls strlen_u8 from a shared library and states the contracts (AZMDoc, naming, register roles) that replace a module system. The companion build is examples/07_include_demo.asm with examples/lib/strings.asm.
The problem: one file stops scaling
Chapter 3’s strlen_u8 is twenty lines. Add copy, compare, ring buffer helpers and GCD from Chapter 1 — the listing scrolls, labels crowd together and you cannot reuse the string walk on the next project without copy-paste.
You need two things at once:
- Physical split — edit strings in one file, main flow in another.
- Logical contract — callers still know which registers to set before
call.
AZM answers the physical split with .include plus documented globals. The CPU never sees files; the assembler merges text before it emits bytes.
.include: paste another file here
The directive:
.include "lib/strings.asm"
tells the assembler to read lib/strings.asm and treat its contents as if you had typed them at that exact line. There is no separate link step, no export table and no namespace prefix on call strlen_u8.
Paths resolve relative to the file that contains the .include. In the companion tree, 07_include_demo.asm lives in examples/ and includes lib/strings.asm beside it:
book3/examples/
07_include_demo.asm
lib/
strings.asm
Assemble from examples/ so the relative path matches:
cd azm-book/book3/examples
azm 07_include_demo.asm
If you assemble from another working directory, either cd to examples/ first or pass a path the assembler can resolve — the rule is always “path relative to the including source file,” not relative to your shell cwd unless they coincide.
One assembly unit
After expansion, the project is a single program: one address space, one set of global labels, one .org sequence you are responsible for coordinating.
Typical layout:
| File | Holds |
|---|---|
main.asm (or 07_include_demo.asm) |
main, halt, RAM labels, .org for data |
lib/strings.asm |
Subroutines only — no second main, no conflicting .org unless you intend overlay |
constants.asm (optional) |
.equ shared by several includes |
Put .include where the library code should land — often after main and before data, or at the bottom of the code section. Forward references work: call strlen_u8 in main is legal even when the .include line appears later in the source.
Include scope
- Not a library with a private symbol table — every label in the included file is global unless you discipline names yourself.
- Not a substitute for AZMDoc — contracts stay in
;!comments on@routines. - Not circular-safe — if
a.asmincludesb.asmandb.asmincludesa.asm, the assembler loops until you stop it. Keep a directed acyclic graph: application includes libraries; libraries do not include the application.
Shared library pattern: lib/strings.asm
Treat a library file as implementation you paste in, plus a header comment that states the calling convention. The companion library holds Chapter 3’s length walk:
; strlen_u8: count bytes before null (terminator not counted)
;! in HL
;! out A
;! clobbers AF, B, HL
@strlen_u8:
ld b, 0
StrLenLoop:
ld a, (hl)
or a
jr z, StrLenDone
inc hl
inc b
jr StrLenLoop
StrLenDone:
ld a, b
ret
Rules that keep libraries boring and reliable:
- No
mainand nohaltin the library — only subroutines and maybe private helpers (ring_advance_indexstyle). - No
.orgin the library unless you are deliberately placing code at a fixed address (unusual in Book 3). - Every exported routine gets AZMDoc — same as Book 1 Chapter 12 and Book 3 Chapters 1–3.
- Entry labels use
@name:on routines the register-care analyzer should treat as callable bodies.
The application file stays short:
.org $0000
main:
ld hl, message
call strlen_u8
ld (str_len), a
halt
.include "lib/strings.asm"
.org $8000
message:
.db "HELLO", 0
.org $8008
str_len:
.ds byte
Reload HL before each call if a routine clobbers HL — the library documents that in clobbers.
Growing the library
Add strcpy_u8, strcmp_u8 and str_find_char from Chapter 3 into the same lib/strings.asm. The main file only grows by more call sites and result stores. When two programs need the same walk, they both .include the same library path instead of duplicating twenty lines.
Optional constants header — if several files need CHAR_L or RING_CAP, a tiny lib/strings.equ (or constants.asm) that only contains .equ lines can be included from both the app and the library. Constants do not need @ labels; routines do.
Files + contracts (no modules)
Without import, the contract is documentation plus naming discipline:
| Mechanism | What it guarantees |
|---|---|
;! in / ;! out / ;! clobbers |
Register roles at call and ret |
@routine: |
Analyzer entry point for --rc warn |
| Prefix on globals | str_ on string routines, ring_ on buffer helpers — reduces label collisions |
.equ in one included header |
Single source for buffer size and field offsets |
Comment block at top of lib/*.asm |
Human-readable summary: “String convention: HL pointer, A length” |
Callers obey the contract the same way they obey Chapter 3’s table: set HL, call, read A, assume everything in clobbers is garbage unless you saved it.
Private helpers stay local by convention: avoid @ on helpers that are not meant to be called from outside the library file. For branch labels inside a routine body, use prefixed names (str_loop, str_done) so they stay unique across the translation unit — all labels are global to the assembler. If a helper must be shared between two routines in the same library, give it a prefixed name (str_advance) and document it as internal in the file header.
Symbol collisions
Because all included text shares one namespace, two files must not both define buffer, count or done at global scope. Fixes:
- Prefix workspace labels:
demo_buffer,demo_str_len. - Prefix library routines:
str_strlen_u8if you ever link two libraries that both exportedstrlen_u8— rename once, update AZMDoc and allcallsites. - Keep branch labels unique by prefixing them with the routine name (
StrLenLoop,FindScan).
When the assembler reports “duplicate label,” search all .include branches — the second definition wins silently in some tools; in AZM treat it as an error to fix immediately.
External code: .asmi interfaces (brief)
Chapter 3’s string routines live in your ROM image. Monitor ROM, BIOS and emulator stubs live at fixed addresses in someone else’s code. You still need register contracts for --rc warn, but there is no AZM source to paste with .include.
Book 1 Chapter 12 introduced .asmi files: contract records only, no instructions:
extern MON_PRINT_CHAR
in A
clobbers A
end
extern MON_GET_KEY
out A
out zero
clobbers carry
end
Assemble with the interface loaded:
azm --interface monitor.asmi --rc warn main.asm
Your program calls MON_PRINT_CHAR like any other label; the analyzer checks that you do not keep A live across the call if clobbers A says otherwise. Update the .asmi when the platform manual changes — the call sites stay the same.
Contrast:
| Feature | .include "lib.asm" |
.asmi + extern |
|---|---|---|
| Delivers | Source pasted into your program | Contracts only |
| Code in output | Yes — your bytes | No — you supply address binding separately |
| Typical use | Your reusable subroutines | ROM / monitor / third-party binary |
Book 3 examples stay self-contained in RAM; .asmi matters when you wire the same libraries into hardware later.
Memory layout after halt
Companion program after a successful run:
$8000 ┌──┬──┬──┬──┬──┬──┐
│48│45│4C│4C│4F│00│ message ("HELLO" + null)
$8008 ├──┐
│05│ str_len
└──┘
Same result as Chapter 3’s single-file demo — proof that the include did not change the algorithm, only where the listing lives on disk.
Examples
| File | Role |
|---|---|
examples/07_include_demo.asm |
main + .include + data/results |
examples/lib/strings.asm |
Shared strlen_u8 with AZMDoc |
cd azm-book/book3/examples
azm 07_include_demo.asm
azm --rc warn 07_include_demo.asm
Step into strlen_u8 once: confirm the library file’s labels appear in the listing at the include point, and that str_len is 5 at $8008.
Summary
.include "path"pastes another.asmfile into the current unit; paths are relative to the including file.- There is no module system — one address space, global labels, files + AZMDoc contracts instead of
import. - Library files hold subroutines (and optional
.equheaders), notmain, not stray.org. @routine:and;!tags stay mandatory so--rc warncan check callers across file boundaries.- Prefix names and dotted loop labels avoid duplicate global symbols when includes multiply.
.asmidocuments external ROM/monitor routines for the analyzer; it does not paste implementation.
Exercises
- Move
messageandstr_lenintodemo_data.asm. Include it from07_include_demo.asmafter the library include. Assemble and confirmstr_lenis still 5. - Add
strcpy_u8andstrcmp_u8from Chapter 3 tolib/strings.asm. Extend the demo to copy into an 8-byte buffer, set acopy_okbyte like Chapter 3 and verify in the emulator. - Create
lib/strings.equwithCHAR_L .equ 'L'and include it from both the library and main. Remove duplicate.equlines from main. - Deliberately define two global labels named
donein different included files. Record the assembler error, then fix one label with a file-specific prefix. - Write a one-routine
lib/math.asmwithgcd_u16from Chapter 1. Include it from a new08_gcd_client.asmthat only calls GCD and stores the result — no string code in that binary. - Sketch a
monitor.asmiwith twoexternroutines you might call on a machine with a character output routine in A and a key reader returning A. Listin,outandclobbersfor each without writing Z80 bodies. - Draw the include graph for a project with
main.asm→lib/strings.asm,lib/ring.asmandconstants.asmincluded by both libraries. Which edges would create a cycle ifring.asmincludedmain.asm?