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:

  1. Physical split — edit strings in one file, main flow in another.
  2. 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 _name labels remain local to their nearest non-local owner.
  • Not a substitute for register contracts; contracts stay in .routine directives 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:

  1. No main and no halt in the library — only subroutines and maybe private helpers (ring_advance_index style).
  2. No .org in the library unless you are deliberately placing code at a fixed address (unusual in Book 2).
  3. Every exported routine gets register contracts — same as Book 1 Chapter 12 and Book 2 Chapters 1–3.
  4. Routine entries have .routine directives. 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_u8 if two included libraries both define strlen_u8; rename once, update register contracts and all call sites.
  • Use owner-local branch labels such as _loop and _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 .asm file into the current unit; paths are relative to the including file.
  • .import "path" loads module-style source with exported @ labels. This chapter keeps using .include because the companion example is a text-composition example.
  • Library files hold subroutines (and optional .equ headers), not main, not stray .org.
  • .routine directives mark callable boundaries so --rc warn can check callers across file boundaries; @ is needed only for imported exports.
  • Prefixed non-local names and owner-local _loop labels avoid duplicate symbols when includes multiply.
  • .asmi documents external ROM/monitor routines for the analyzer; it does not paste implementation.

Exercises

  1. Move message and str_len into demo_data.asm. Include it from 07_include_demo.asm after the library include. Assemble and confirm str_len is still 5.
  2. Add strcpy_u8 and strcmp_u8 from Chapter 3 to lib/strings.asm. Extend the demo to copy into an 8-byte buffer, set a copy_ok byte like Chapter 3 and verify in the emulator.
  3. Create lib/strings.equ with CHAR_L .equ 'L' and include it from both the library and main. Remove duplicate .equ lines from main.
  4. Deliberately define two global labels named done in different included files. Record the assembler error, then fix one label with a file-specific prefix.
  5. Write a one-routine lib/math.asm with gcd_u16 from Chapter 1. Include it from a new 08_gcd_client.asm that only calls GCD and stores the result — no string code in that binary.
  6. Sketch a monitor.asmi with two extern routines you might call on a machine with a character output routine in A and a key reader returning A. List in, out and clobbers for each without writing Z80 bodies.
  7. Draw the include graph for a project with main.asmlib/strings.asm, lib/ring.asm and constants.asm included by both libraries. Which edges would create a cycle if ring.asm included main.asm?