Chapter 4 - Ops and Register Care
Ops and register care are the two AZM-specific subsystems that sit above plain Z80 instruction assembly. Ops expand source into visible inline assembly. Register care analyses the resulting routines and calls.
These features belong together in the codebase tour because they meet at the same boundary: parsed source items. Ops produce source items. Register care reads source items.
Ops as Visible Expansion
Ops are named inline instruction idioms. They let source define a small
operation once and expand it visibly at each use site. The implementation lives
in src/expansion/op-expansion.ts.
An op is closer to a typed inline template than to a text macro. The op parser understands operands, chooses an overload and parses the expanded body back through the normal AZM parser. The result is visible assembly with the same diagnostic and register-care behaviour as handwritten source.
For example:
op clear(reg8 r)
xor r
end
clear a
The expansion stage matches a as a register operand, substitutes it into the
template and emits the source item for xor a. Address planning and emission
then treat that instruction exactly like a line written directly in the source.
Op Collection and Invocation
collectOps() scans logical lines before normal parsing. It finds top-level
op blocks, parses their parameter lists, records the body template and marks
the source lines that belong to the definition body.
An op declaration has:
- a name
- a parameter list
- matcher information for overload selection
- a body template
- source location metadata for diagnostics
The registry is complete before invocation parsing starts. parseOpInvocation()
checks whether a source line could be an op call. If the name matches a
collected op, expandOpInvocation() selects an overload and instantiates the
body.
The parser handles op invocations before parseLogicalLine(). An op head can
look like an instruction head at the source level. The expansion stage resolves
it before ordinary line parsing.
Overloads and Templates
Ops support overloads. selectOpOverload() compares invocation operands against
each candidate signature. It prefers the most specific matching overload and
emits diagnostics for arity errors, unsupported operands, ambiguous matches and
invalid expansions.
The matcher vocabulary recognises fixed tokens, registers, register pairs, immediates, conditions, ports and indexed operands. It stays close to the Z80 operand model, so op dispatch and instruction parsing describe operands in the same terms.
An op body template is parsed into template items. During expansion, operands from the call site are substituted into the template. The result is formatted as ordinary source text and parsed through the same line parser used for top-level source.
Local label rewriting is part of expansion. A local label in an op expansion becomes unique at the use site so each expansion receives its own generated label. Once the rewritten labels become source items, address planning defines and resolves them through the ordinary symbol path.
Op Diagnostics and Register Care
Op diagnostics point at the call site while explaining the definition that matched or failed. Invalid expanded instructions are reported as op expansion failures with the underlying Z80 parser diagnostic included.
Ops expand before register care builds routines. Register care sees the expanded instructions. An op is visible inline assembly, so its register effects belong to the caller.
Register-Care Analysis
Register care analyses how routines use Z80 registers. It reads routine boundaries, instruction effects and AZMDoc contract comments, then reports conflicts where a caller still needs a register value that a callee may change.
The implementation lives in src/register-care/. The public analysis entry
point is analyzeRegisterCare() in src/register-care/analyze.ts.
Register care is a data-flow analysis over assembled source structure. It works with routines, calls, instruction effects and contracts. It analyses the source structure to find values live across a call and callee summaries that can change those values.
A Register-Care Conflict
This source shape captures the problem:
@Caller:
ld b,8
Loop:
call Worker
djnz Loop
ret
;! clobbers B
@Worker:
ld b,0
ret
Caller uses B as the djnz counter. Worker declares that it clobbers
B. Liveness sees that B is still needed after the call because djnz Loop
reads it. The register-care conflict is at the call site: Caller passes
through a routine boundary that may change a live unit.
The programmer can preserve B, choose a different counter register, change
Worker so it leaves B unchanged or update the calling sequence. Register
care identifies the conflict and the source location where the caller crosses
the boundary.
Routine Model and Contracts
src/register-care/programModel.ts builds the program model from parsed source
items. It finds routine boundaries, direct calls, labels and instructions.
Routine entry labels use @ in source and become callable public routine names
after the marker is removed.
src/register-care/smartComments.ts reads AZMDoc comments from the comment maps
captured during loading. It builds routine contracts from ;! lines and from
external .asmi interfaces.
Contracts can describe:
- inputs
- outputs
- clobbered registers
- preserved registers
- expected outputs at call sites
Source comments and external interfaces describe the same kind of fact: a
routine contract. Source comments attach to routines in the current program.
.asmi entries attach to routines whose source is assembled elsewhere.
Effects, Summaries and Liveness
Register care depends on src/z80/effects.ts. Effects describe which registers
and flags an instruction reads, writes or preserves. instruction-shape.ts and
carriers.ts translate between Z80 instruction shapes and register-care units
such as A, HL, carry and register pairs.
src/register-care/summary.ts infers a summary for a single routine.
routine-summaries.ts and summaries.ts combine routine summaries, external
contracts and profile summaries into lookup tables. A summary records the
observable contract of a routine: the units it reads, writes, preserves,
clobbers and returns as outputs.
src/register-care/liveness.ts performs the caller-side analysis. It works
backwards through each routine. At a call, it compares the live-after set with
the callee summary. A live unit that the callee clobbers becomes a conflict. A
unit produced by the callee and read by the caller becomes an output candidate.
Reports, Interfaces and Tooling
report.ts renders human-readable .regcare.txt reports and .asmi interface
metadata. annotate.ts, annotations.ts, fix.ts and sourceText.ts support
source updates for generated AZMDoc comments and conservative fixes.
The CLI can request these behaviours through:
--reg-report--reg-interface--contracts--fix--accept-out
src/register-care/tooling.ts exposes editor-friendly diagnostics and code
actions through analyzeRegisterCareForTools(). Tooling diagnostics carry file,
line, column, message, fixability and optional text edits. An editor can show
the same register-care information that the CLI reports while using normal
editor actions for accepted fixes.
Changing Ops or Register Care
Op changes belong in op-expansion.ts, with tests under
test/unit/expansion/ and integration tests for source-level behaviour.
Register-care changes usually begin in one of these files:
- Routine boundaries and calls:
programModel.ts - AZMDoc parsing:
smartComments.ts - Instruction effects:
z80/effects.ts,instruction-shape.ts - Summary inference:
summary.ts - Caller liveness:
liveness.ts - Output text:
report.ts - Source edits:
annotate.ts,fix.ts,annotations.ts - Tooling surface:
tooling.ts
Run unit tests under test/unit/register-care/, integration tests under
test/integration/register-care/ and CLI tests in
test/cli/register_care_cli.test.ts.