Cookbook: adopting index_base / index_region
index_base() and
index_region() fix a real defect — a table
indexed by a key no longer masquerades on the memory map as a single
owned byte with phantom reads and writes (see
Indexed-base addresses and regions for the API, and the reference-kinds memo
in docs/design/reference-kinds-memo.md for the why). But they are
annotation-only: they never change emitted bytes, so verify
passing tells you nothing about whether a conversion was correct.
Correctness comes from one habit:
Read the label and its usage in context before converting it. The tooling finds candidates; only the code tells you what a label actually is.
This how-to is the workflow and judgement for retrofitting these calls
onto a real, already-annotated driver — distilled from migrating a
full multi-version 6502 disassembly (Acorn NFS/ANFS, 11 ROMs) to
dasmos 2.0: roughly 480 index_base conversions, plus a careful
decision not to use index_region anywhere in that codebase.
1. Sequence the work
Do the bare version bump on its own first — regenerate, and confirm
verify still passes byte-identically and only cross-reference
prose changed. dasmos 2.0 is annotation / JSON-shape only; if bytes
move, stop and investigate before editing anything. Commit that clean
upgrade separately, then start converting on a known-good baseline.
If you have local tooling that parses the output JSON, note the schema
is now versioned: references entries are {addr, kind, move_id?}
objects, and each memory_map row’s access is an ordered
["r", "w", "b"] array — a base is a row carrying "b" (schema v4;
earlier dasmos used a separate index_bases array).
2. Find candidates — as leads, not as an oracle
A pure index base shows in the .asm cross-reference as used as
index base N times with no referenced. A mixed address
(referenced … also used as index base …) is touched both ways —
leave it a plain label(). In the JSON, pure
bases you have already declared show as memory_map rows whose
access is ["b"]; references[].kind gives you the split
programmatically.
Two blind spots to compensate for:
Low-page (zero-page / page 0–2) bases are under-reported in the JSON. The
.asmcross-reference does classify them (used as index base), but the JSONreferences[].kindcurrently does not carry the kind for those targets — so anything built on the JSON (including orchestration CLIs) will miss them. Trust the.asmthere.Relocated-code entries masquerade as pure data bases. A block copied to RAM by
add_move()has its code items at the source address; the destination entry point therefore has no localcodeitem, and its copy loop writes it assta dest,Y— which looks exactly like an index base. Always also excludeentry()targets and move destinations. Converting one moves a live entry point off the map — the cardinal error this feature must not cause.
Orchestration tools can list the pure set for you (for example
fantasm’s labels list --index-base-only, which also flags
type=='code' items). Treat their output as a worklist to read,
never as a batch to convert.
3. Decide per label, by reading
You’re looking at… |
Do |
|---|---|
a |
Convert to |
any |
Leave — code label (the caveat). |
a |
Leave — it isn’t a |
an auto-generated ( |
Leave — no |
The name usually gives the class away — table / template / offsets /
strings / lookup is data; entry / handler / start / a jmp-or-branch
target is code — but when name and usage disagree, trust the usage. A
…_entry that is only ever read as lda …,X to scan a header is a
data base; a …_table that is jumped into is not. Read the inline
comments you already wrote: they usually say which.
A throwaway script that rewrites label( → index_base( over a
curated address set (dropping access=, handling multi-line
calls) saves tedium — but the set must come from your reading, never
straight from a tool. Afterwards, re-run verify and your linter, and
assert that no entry() target became an
index_base in any driver. verify passing proves you did not change
bytes; it does not prove the classification is right.
4. index_region: use it only where it earns its place
index_region() renders in-window neighbours
as anchor±k,X, so you do not hand-write a label per slot. Its payoff
is eliminating tedious per-slot labels for mechanically-related
notional slots around a real anchor — the free-space-map-scratch case
in the API docs.
Do not reach for it just because addresses are close. Most “clusters” are the opposite of its use case, and a region makes them worse:
lo/hi address-table pairs (
*_lo/*_hi): the names carry the meaning;anchor+k,Xburies it. Keep twoindex_bases.distinct adjacent tables that merely abut: different data → different names.
N-byte-spaced parallel arrays (one column per field, shared index): each is its own documented array, not a slot.
single base-adjustment tricks (a base chosen so
base+Ylands in another region, often already carrying an explanatory comment): one meaningful base, not a swept range — a region flattens the arithmetic and discards the comment.
Rule of thumb: if you would otherwise be forced to hand-name many
interchangeable slots, a region helps. If each address has a distinct
role and a comment explaining it, naming beats arithmetic — leave
them as index_base. Decide by reading the usages, not by address
spacing. (Region windows must be disjoint; an explicit
label() inside a window still wins.)
5. Wrap up
Re-run lint / verify across every version (clean and
byte-identical). Spot-check the JSON: converted addresses should sit in
memory_map with access containing "b" (a ["b"]-only row
for a pure base). Commit the conversions
separately from the bare upgrade, and record the data-vs-code / banner /
auto-label rules and the “don’t force index_region” guidance in your
project’s own notes so the next pass stays consistent.