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. AZM still produces one output program with no hidden linker.
This chapter splits a tiny program across files with .include, pulls strlen_u8 from a shared library and states the contracts (register contracts, naming, register roles) that keep the file boundary readable. AZM 0.3.2 also supports .import for module-style source files with explicit @ exports; Book 0 covers that reference workflow. 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:
book2/examples/
07_include_demo.asm
lib/
strings.asm
Assemble from examples/ so the relative path matches:
cd azm-book/book2/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 separate private symbol table — non-local declarations join the including source unit, while
_namelabels remain local to their nearest non-local owner. - Not a substitute for register contracts; contracts stay in
.routinedirectives on callable entries. - Recursive include/import chains are rejected with a source diagnostic. 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)
.routine in HL out A clobbers F,B,HL
strlen_u8:
ld b, 0
_loop:
ld a, (hl)
or a
jr z, _done
inc hl
inc b
jr _loop
_done:
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 2). - Every exported routine gets register contracts — same as Book 1 Chapter 12 and Book 2 Chapters 1–3.
- Routine entries have
.routinedirectives. Prefix a label with@only when the library exports it for.import.
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 and routines need @ only when they are exported from an imported source unit.
Files and contracts
Without import, the contract is documentation plus naming discipline:
| Mechanism | What it guarantees |
|---|---|
.routine in / .routine out / .routine clobbers |
Register roles at call and ret |
.routine immediately before a label |
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 use ordinary non-local names when several routines in the source unit call them. Branch labels inside a routine use owner-local names such as _loop and _done; each non-local owner gets a separate local namespace. Add @ only to symbols exported from an imported source unit.
Symbol collisions
Because included text shares one source-unit namespace, two files must not both define buffer or count as non-local labels. Fixes:
- Prefix workspace labels:
demo_buffer,demo_str_len. - Prefix library routines:
str_strlen_u8if two included libraries both definestrlen_u8; rename once, update register contracts and allcallsites. - Use owner-local branch labels such as
_loopand_found; another routine may reuse those spellings under its own owner.
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 2 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 register contracts |
cd azm-book/book2/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..import "path"loads module-style source with exported@labels. This chapter keeps using.includebecause the companion example is a text-composition example.- Library files hold subroutines (and optional
.equheaders), notmain, not stray.org. .routinedirectives mark callable boundaries so--rc warncan check callers across file boundaries;@is needed only for imported exports.- Prefixed non-local names and owner-local
_looplabels avoid duplicate 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?