@henols/c64-re-tools 2.0.0-rc.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +97 -116
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +14 -5
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +25 -5
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +8 -2
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/package.ts +13 -3
  111. package/src/platform.ts +70 -7
  112. package/src/project.ts +180 -3
  113. package/src/protocol/messages.ts +1 -1
  114. package/src/protocol/tools.ts +2 -16
  115. package/src/protocol/vice.ts +124 -15
  116. package/src/script.ts +46 -7
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -0,0 +1,1121 @@
1
+ // ACME source from an image, current knowledge and Ghidra's instruction
2
+ // boundaries. The same input gives the same text. Names come from
3
+ // knowledge; a target that knowledge does not name gets l_xxxx. Data is
4
+ // written by its region type. The source assembles to the same bytes: the
5
+ // generator forces operand sizes, and writes an opcode form that ACME
6
+ // encodes differently as bytes (see the probe in generate.ts). An operand
7
+ // expression of the knowledge is written in place of its number. A
8
+ // patch is written as !if SWITCH { code } else { original }, and a
9
+ // macro use as one +name args line.
10
+
11
+ import { COVERAGE, formatC64Address, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
12
+ import { WireFailure } from "#src/host-client/tools.ts";
13
+ import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
14
+ import { ANALYZERS } from "#src/knowledge/import.ts";
15
+ import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
16
+ import type { Area } from "#src/platform.ts";
17
+ import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "#src/opcodes.ts";
18
+
19
+ export interface Range {
20
+ start: number;
21
+ end: number;
22
+ }
23
+
24
+ export interface SourceInput {
25
+ /** In address order, without overlap. */
26
+ segments: Range[];
27
+ /** The image byte at an address inside a segment. */
28
+ byteAt: (address: number) => number;
29
+ entry?: number;
30
+ /** Where Ghidra decoded an instruction, with its length. */
31
+ instructions: ReadonlyMap<number, number>;
32
+ /** Without an origin, a symbol counts as a user or LLM name. */
33
+ symbols: ReadonlyArray<{ address: number; name: string; origin?: Origin }>;
34
+ regions: ReadonlyArray<{ start: number; end: number; type: RegionType; element?: number }>;
35
+ comments: ReadonlyArray<{ address: number; placement: CommentPlacement; text: string }>;
36
+ modules: ReadonlyArray<{ start: number; file: string; title?: string }>;
37
+ /** Targets of knowledge references: each one inside the image gets a label. */
38
+ referenceTargets: readonly number[];
39
+ /** Joined coverage files: an instruction that no run executed is marked. */
40
+ coverage?: Uint8Array;
41
+ /** Platform names: the name of an address that knowledge does not name, the last address of its entry, and what it is. */
42
+ platform?: ReadonlyArray<{ address: number; end: number; name: string; meaning?: string }>;
43
+ /** The platform area of an address, and the register that an I/O mirror repeats: constants.asm groups and describes its names with them. */
44
+ describe?: (address: number) => { area: Area; mirrorOf?: number };
45
+ /** What a value in a platform register means (c64-memory-map): an immediate stored to such a register gets it as a side comment. Throws for a register without a decoder. */
46
+ decode?: (register: number, value: number) => Record<string, unknown>;
47
+ /** Operand expressions of the knowledge, by instruction address. */
48
+ operands?: ReadonlyArray<OperandExpression & { address: number }>;
49
+ /** Patches of the knowledge: code that replaces a range when the build defines the switch. */
50
+ patches?: ReadonlyArray<{ start: number; end: number; switch: string; code: string; comment?: string }>;
51
+ /** Macros of the knowledge, written to macros.asm. */
52
+ macros?: ReadonlyArray<{ name: string; params: readonly string[]; body: string; comment?: string }>;
53
+ /** Macro uses of the knowledge: the instructions of a range, written as one use of a macro. */
54
+ macroUses?: ReadonlyArray<{ start: number; end: number; macro: string; args: readonly string[] }>;
55
+ }
56
+
57
+ /** A macro use that the source writes, with its args in the names of this source. */
58
+ export interface MacroUseBlock {
59
+ start: number;
60
+ end: number;
61
+ macro: string;
62
+ args: string[];
63
+ }
64
+
65
+ /** A macro use that the source writes as its instructions, and why. */
66
+ export interface MacroAsInstructions {
67
+ start: number;
68
+ end: number;
69
+ macro: string;
70
+ reason: string;
71
+ }
72
+
73
+ /** A patch that the source writes, with the addresses inside it that have a name. */
74
+ export interface PatchBlock {
75
+ start: number;
76
+ end: number;
77
+ switch: string;
78
+ code: string;
79
+ comment?: string;
80
+ /** Named addresses after the start of the range: the source defines them before the patch, so both builds have them. */
81
+ inner: number[];
82
+ }
83
+
84
+ /** A patch of the knowledge that the source does not write, and why. */
85
+ export interface PatchNotWritten {
86
+ start: number;
87
+ end: number;
88
+ switch: string;
89
+ reason: string;
90
+ }
91
+
92
+ /** An operand expression that the source writes as a number, with its text and why. */
93
+ export interface OperandAsNumber {
94
+ address: number;
95
+ expression: string;
96
+ reason: string;
97
+ }
98
+
99
+ const CODE_TYPES: readonly RegionType[] = ["code", "jumps", "dual-use"];
100
+ const BYTES_PER_LINE = 8;
101
+ const WORDS_PER_LINE = 4;
102
+ const CHARS_PER_LINE = 32;
103
+ const MIN_FILL = 16;
104
+ const SIDE_COLUMN = 40;
105
+ const INDENT = " ";
106
+
107
+ type Region = SourceInput["regions"][number];
108
+
109
+ /** LDA/LDX/LDY immediate, and the stores of the same register: absolute and zero page. */
110
+ const LOADS: Record<number, "a" | "x" | "y"> = { 0xa9: "a", 0xa2: "x", 0xa0: "y" };
111
+ const STORES: Record<number, "a" | "x" | "y"> = { 0x8d: "a", 0x85: "a", 0x8e: "x", 0x86: "x", 0x8c: "y", 0x84: "y" };
112
+
113
+ /** A decoded register value in a few words: the true flags, the numbers and the texts, without the note. */
114
+ function decodedText(decoded: Record<string, unknown>): string {
115
+ const parts: string[] = [];
116
+ for (const [key, value] of Object.entries(decoded)) {
117
+ if (key === "note" || value === false || value === undefined) continue;
118
+ if (value === true) parts.push(key);
119
+ else if (Array.isArray(value)) parts.push(`${key} ${value.join(",")}`);
120
+ else if (typeof value === "object" && value !== null) continue;
121
+ else parts.push(`${key} ${String(value)}`);
122
+ }
123
+ return parts.join(", ");
124
+ }
125
+
126
+ /**
127
+ * Side comments for an immediate that the next instruction stores to a platform
128
+ * register with a decoder, for example "$d011: yScroll 3, rows 25". A knowledge
129
+ * side comment at the store wins.
130
+ */
131
+ function decodedStores(pieces: readonly Piece[], input: SourceInput): Array<{ address: number; placement: CommentPlacement; text: string }> {
132
+ if (input.decode === undefined) return [];
133
+ const own = new Set(input.comments.filter((comment) => comment.placement === "side").map((comment) => comment.address));
134
+ const found: Array<{ address: number; placement: CommentPlacement; text: string }> = [];
135
+ for (const piece of pieces) {
136
+ const items = piece.items.filter((item): item is InstructionItem => item.kind === "instruction");
137
+ for (let index = 1; index < items.length; index++) {
138
+ const load = items[index - 1]!;
139
+ const store = items[index]!;
140
+ const register = STORES[store.opcode];
141
+ if (register === undefined || LOADS[load.opcode] !== register || load.address + load.length !== store.address || own.has(store.address)) continue;
142
+ const target = store.length === 3 ? input.byteAt(store.address + 1) | (input.byteAt(store.address + 2) << 8) : input.byteAt(store.address + 1);
143
+ // A register inside the image is RAM of the program, not the register.
144
+ if (input.segments.some((segment) => target >= segment.start && target <= segment.end)) continue;
145
+ try {
146
+ const text = decodedText(input.decode(target, input.byteAt(load.address + 1)));
147
+ if (text !== "") found.push({ address: store.address, placement: "side", text: `${hex4(target)}: ${text}` });
148
+ } catch {
149
+ // No decoder for this register.
150
+ }
151
+ }
152
+ }
153
+ return found;
154
+ }
155
+
156
+ interface InstructionItem {
157
+ kind: "instruction";
158
+ address: number;
159
+ length: number;
160
+ opcode: number;
161
+ }
162
+
163
+ interface DataItem {
164
+ kind: "data";
165
+ start: number;
166
+ end: number;
167
+ /** The region type, or unclassified. In a code region, bytes that Ghidra did not decode. */
168
+ type: RegionType | "unclassified" | "undecoded";
169
+ region?: Region;
170
+ }
171
+
172
+ type Item = InstructionItem | DataItem;
173
+
174
+ export interface Piece {
175
+ start: number;
176
+ end: number;
177
+ file: string;
178
+ title: string;
179
+ /** True when the piece starts a segment: the source sets the address there. */
180
+ segmentStart: boolean;
181
+ items: Item[];
182
+ }
183
+
184
+ export interface Layout {
185
+ pieces: Piece[];
186
+ /** Names of addresses: inside the image a label, outside it a constant. */
187
+ names: Map<number, string>;
188
+ /** Addresses outside the image that the source names, as constants. */
189
+ constants: number[];
190
+ /** How the source uses each constant: instructions by kind, and pointer table entries. */
191
+ uses: Map<number, Map<UseKind, number>>;
192
+ platform: NonNullable<SourceInput["platform"]>;
193
+ describe?: SourceInput["describe"];
194
+ inImage: (address: number) => boolean;
195
+ byteAt: (address: number) => number;
196
+ comments: SourceInput["comments"];
197
+ coverage?: Uint8Array;
198
+ generatedLabels: number;
199
+ namedLabels: number;
200
+ unusedModules: Array<{ start: number; file: string }>;
201
+ /** The operand of an instruction as an expression with the names of this source. */
202
+ operands: Map<number, string>;
203
+ /** Operand expressions that cannot be written. */
204
+ operandsAsNumbers: OperandAsNumber[];
205
+ /** The patches that the source writes, by start. */
206
+ patches: Map<number, PatchBlock>;
207
+ patchesNotWritten: PatchNotWritten[];
208
+ /** The macros, by name. */
209
+ macros: NonNullable<SourceInput["macros"]>;
210
+ /** The macro uses that the source can write, by start. */
211
+ macroUses: Map<number, MacroUseBlock>;
212
+ macrosAsInstructions: MacroAsInstructions[];
213
+ }
214
+
215
+ const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
216
+ const hex4 = (value: number) => formatC64Address(value);
217
+ /** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
218
+ const constantValue = (address: number) => (address < 0x100 ? hex2(address) : hex4(address));
219
+ const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
220
+
221
+ function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
222
+ const { mode } = OPCODES[opcode]!;
223
+ const low = MODE_LENGTH[mode] >= 2 ? byteAt(address + 1) : 0;
224
+ if (mode === "imp") return undefined;
225
+ if (mode === "rel") return (address + 2 + signed(low)) & 0xffff;
226
+ if (MODE_LENGTH[mode] === 3) return low | (byteAt(address + 2) << 8);
227
+ return low;
228
+ }
229
+
230
+ /**
231
+ * Each entry point inside a data region of the knowledge, with that region.
232
+ * A data region wins over an entry: Ghidra does not decode there,
233
+ * and the source has data there. A code region in the knowledge changes that.
234
+ */
235
+ export function entriesInData<R extends { start: number; end: number; type: RegionType }>(entries: readonly number[], regions: readonly R[]): Array<{ entry: number; region: R }> {
236
+ return entries.flatMap((entry) => {
237
+ const region = regions.find((row) => entry >= row.start && entry <= row.end && !CODE_TYPES.includes(row.type));
238
+ return region === undefined ? [] : [{ entry, region }];
239
+ });
240
+ }
241
+
242
+ /** Why an image without its entry fails: the image does not hold the program. Undefined when the entry is in a segment, or the image has no entry. */
243
+ export function entryOutsideImage(entry: number | undefined, segments: readonly Range[]): string | undefined {
244
+ if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
245
+ return `The entry ${hex4(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
246
+ }
247
+
248
+ /** Splits the segments into pieces at module starts and lays out instructions and data. */
249
+ export function layout(input: SourceInput): Layout {
250
+ const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
251
+ const regionAt = (address: number) => input.regions.find((region) => address >= region.start && address <= region.end);
252
+ const isData = (address: number) => {
253
+ const region = regionAt(address);
254
+ return region !== undefined && !CODE_TYPES.includes(region.type);
255
+ };
256
+
257
+ for (const module of input.modules) {
258
+ if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
259
+ }
260
+ const modules = [...input.modules].sort((a, b) => a.start - b.start);
261
+ const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
262
+
263
+ // A patch starts and ends at an item boundary, so a data run stops there.
264
+ const patches = [...(input.patches ?? [])].sort((a, b) => a.start - b.start);
265
+ const macroUses = [...(input.macroUses ?? [])].sort((a, b) => a.start - b.start);
266
+ const cuts = new Set([...patches, ...macroUses].flatMap((range) => [range.start, range.end + 1]));
267
+
268
+ const pieces: Piece[] = [];
269
+ for (const segment of input.segments) {
270
+ const starts = modules.filter((module) => module.start > segment.start && module.start <= segment.end).map((module) => module.start);
271
+ const bounds = [segment.start, ...starts, segment.end + 1];
272
+ for (let index = 0; index + 1 < bounds.length; index++) {
273
+ const start = bounds[index]!;
274
+ const end = bounds[index + 1]! - 1;
275
+ const module = modules.find((row) => row.start === start);
276
+ const file = module?.file ?? `segment_${start.toString(16).padStart(4, "0")}.asm`;
277
+ const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${hex4(segment.start)}-${hex4(segment.end)}`;
278
+ pieces.push({ start, end, file, title, segmentStart: index === 0, items: [] });
279
+ }
280
+ }
281
+ const files = new Map<string, number>();
282
+ for (const piece of pieces) {
283
+ const other = files.get(piece.file);
284
+ if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${hex4(other)} and at ${hex4(piece.start)}. Give the module another file.`);
285
+ files.set(piece.file, piece.start);
286
+ }
287
+
288
+ for (const piece of pieces) {
289
+ let address = piece.start;
290
+ while (address <= piece.end) {
291
+ const region = regionAt(address);
292
+ const codeRegion = region !== undefined && CODE_TYPES.includes(region.type);
293
+ const length = input.instructions.get(address);
294
+ const opcode = input.byteAt(address);
295
+ const fits =
296
+ length !== undefined &&
297
+ (codeRegion || region === undefined) &&
298
+ MODE_LENGTH[OPCODES[opcode]!.mode] === length &&
299
+ address + length - 1 <= piece.end &&
300
+ Array.from({ length }, (_, offset) => address + offset).every((at) => !isData(at));
301
+ if (fits) {
302
+ piece.items.push({ kind: "instruction", address, length: length!, opcode });
303
+ address += length!;
304
+ continue;
305
+ }
306
+ // A data run: one region, or unclassified bytes up to the next decoded instruction.
307
+ const type: DataItem["type"] = codeRegion ? "undecoded" : (region?.type ?? "unclassified");
308
+ let end = address;
309
+ const same = (at: number) => {
310
+ if (at > piece.end || cuts.has(at)) return false;
311
+ const next = regionAt(at);
312
+ if (region !== undefined && !codeRegion) return next === region;
313
+ if (codeRegion) return next === region && !input.instructions.has(at);
314
+ return next === undefined && !input.instructions.has(at);
315
+ };
316
+ while (same(end + 1)) end++;
317
+ const last = piece.items.at(-1);
318
+ if (last?.kind === "data" && last.type === type && last.region === region && last.end + 1 === address && !cuts.has(address)) last.end = end;
319
+ else piece.items.push({ kind: "data", start: address, end, type, ...(region === undefined ? {} : { region }) });
320
+ address = end + 1;
321
+ }
322
+ }
323
+
324
+ // Labels: every address inside the image that an operand, a table, a symbol, a reference or the entry names.
325
+ const wanted = new Set<number>();
326
+ const want = (address: number) => inImage(address) && wanted.add(address);
327
+ const outside = new Set<number>();
328
+ // A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
329
+ const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
330
+ const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
331
+ // An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
332
+ const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
333
+ const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
334
+ /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
335
+ const insideEntry = new Map<number, number>();
336
+ const uses = new Map<number, Map<UseKind, number>>();
337
+ const count = (constant: number, kind: UseKind) => {
338
+ outside.add(constant);
339
+ const kinds = uses.get(constant) ?? new Map<UseKind, number>();
340
+ kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
341
+ uses.set(constant, kinds);
342
+ };
343
+ /** The start of the named data region that holds an address without a name of its own: the operand is written name+offset. */
344
+ const tableStart = (address: number) => {
345
+ if (symbols.has(address)) return undefined;
346
+ const region = input.regions.find((row) => address > row.start && address <= row.end && !CODE_TYPES.includes(row.type) && row.type !== "dual-use");
347
+ return region !== undefined && symbols.has(region.start) ? region.start : undefined;
348
+ };
349
+ const use = (address: number, kind: UseKind) => {
350
+ if (inImage(address)) {
351
+ const table = tableStart(address);
352
+ wanted.add(table ?? address);
353
+ if (table !== undefined) insideEntry.set(address, table);
354
+ } else if (symbols.has(address) || platform.has(address)) count(address, kind);
355
+ else {
356
+ const entry = input.platform?.find((row) => address > row.address && address <= row.end && !inImage(row.address) && (symbols.has(row.address) || platform.has(row.address)));
357
+ if (entry === undefined) return;
358
+ count(entry.address, kind);
359
+ insideEntry.set(address, entry.address);
360
+ }
361
+ };
362
+ // The names of an operand expression are found by name, a knowledge symbol before a platform name, and written with the name the source gives that address.
363
+ const byName = new Map<string, number>();
364
+ for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
365
+ for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
366
+ const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
367
+ // A macro use is a run of whole instructions of one file, of a macro with one param for each arg, whose args name known names.
368
+ const macrosByName = new Map((input.macros ?? []).map((macro) => [macro.name, macro]));
369
+ const macrosAsInstructions: MacroAsInstructions[] = [];
370
+ const candidates: Array<{ use: (typeof macroUses)[number]; items: InstructionItem[] }> = [];
371
+ for (const use of macroUses) {
372
+ const fallBack = (reason: string) => macrosAsInstructions.push({ start: use.start, end: use.end, macro: use.macro, reason });
373
+ const macro = macrosByName.get(use.macro);
374
+ if (macro === undefined) {
375
+ fallBack(`There is no macro ${use.macro}.`);
376
+ continue;
377
+ }
378
+ if (macro.params.length !== use.args.length) {
379
+ fallBack(`The macro has ${macro.params.length} params, and the use gives ${use.args.length} args.`);
380
+ continue;
381
+ }
382
+ if (patches.some((patch) => patch.start <= use.end && use.start <= patch.end)) {
383
+ fallBack("The use lies in a patch.");
384
+ continue;
385
+ }
386
+ const piece = pieces.find((row) => use.start >= row.start && use.start <= row.end);
387
+ const run = piece?.items.filter((item) => (item.kind === "instruction" ? item.address : item.start) >= use.start && (item.kind === "instruction" ? item.address : item.start) <= use.end) ?? [];
388
+ const last = run.at(-1);
389
+ const whole = last?.kind === "instruction" && run.every((item) => item.kind === "instruction") && (run[0] as InstructionItem).address === use.start && last.address + last.length - 1 === use.end;
390
+ if (!whole) {
391
+ fallBack("The range is not a run of whole instructions of one file.");
392
+ continue;
393
+ }
394
+ const missing = [...new Set(use.args.flatMap((arg) => identifiers(arg)))].filter((name) => !byName.has(name));
395
+ if (missing.length > 0) {
396
+ fallBack(`No symbol and no platform name is ${missing.join(", ")}.`);
397
+ continue;
398
+ }
399
+ candidates.push({ use, items: run as InstructionItem[] });
400
+ }
401
+ /** The instructions of a written use name nothing: the macro writes them. */
402
+ const inUse = new Set(candidates.flatMap((candidate) => candidate.items.map((item) => item.address)));
403
+ const expressions: Array<{ address: number; text: string; kind: UseKind }> = [];
404
+ const operandsAsNumbers: OperandAsNumber[] = [];
405
+ for (const operand of [...(input.operands ?? [])].sort((a, b) => a.address - b.address)) {
406
+ const expression = operandText(operand);
407
+ const item = instructionAt.get(operand.address);
408
+ const mode = item === undefined ? undefined : OPCODES[item.opcode]!.mode;
409
+ if (mode === undefined || mode === "imp" || mode === "rel") {
410
+ operandsAsNumbers.push({ address: operand.address, expression, reason: "The source has no instruction with an operand value at this address." });
411
+ continue;
412
+ }
413
+ const missing = identifiers(expression).filter((name) => !byName.has(name));
414
+ if (missing.length > 0) {
415
+ operandsAsNumbers.push({ address: operand.address, expression, reason: `No symbol and no platform name is ${missing.join(", ")}.` });
416
+ continue;
417
+ }
418
+ // An immediate operand builds the address of a name; any other operand uses the address as the instruction does.
419
+ expressions.push({ address: operand.address, text: expression, kind: mode === "imm" ? "address" : useKind(item!.opcode) });
420
+ }
421
+ for (const { text: expression, kind } of expressions) for (const name of identifiers(expression)) use(byName.get(name)!, kind);
422
+ // An instruction with an expression names what the expression names, not its plain target.
423
+ const written = new Set(expressions.map((expression) => expression.address));
424
+ for (const piece of pieces) {
425
+ for (const item of piece.items) {
426
+ if (item.kind === "instruction") {
427
+ if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address) && !inUse.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
428
+ continue;
429
+ }
430
+ for (const pointer of pointersOf(item, input.byteAt)) use(pointer, "table");
431
+ }
432
+ }
433
+ for (const candidate of candidates) for (const arg of candidate.use.args) for (const name of identifiers(arg)) use(byName.get(name)!, "macro argument");
434
+ for (const symbol of input.symbols) want(symbol.address);
435
+ // A reference target inside a named data region without its own name is that region's name plus the offset: it makes no label.
436
+ for (const target of input.referenceTargets) want(tableStart(target) ?? target);
437
+ if (input.entry !== undefined) want(input.entry);
438
+ // A use with a label inside it is written as its instructions, which then name their targets: that can put a label inside another use.
439
+ const accepted = new Set(candidates);
440
+ for (let changed = true; changed; ) {
441
+ changed = false;
442
+ for (const candidate of [...accepted]) {
443
+ const inside = [...wanted].filter((address) => address > candidate.use.start && address <= candidate.use.end).sort((a, b) => a - b)[0];
444
+ if (inside === undefined) continue;
445
+ accepted.delete(candidate);
446
+ changed = true;
447
+ macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${hex4(inside)}, inside the use.` });
448
+ for (const item of candidate.items) {
449
+ inUse.delete(item.address);
450
+ if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
451
+ }
452
+ }
453
+ }
454
+
455
+ const names = new Map<number, string>();
456
+ const taken = new Set([...knowledgeNames, ...platform.values()]);
457
+ let generatedLabels = 0;
458
+ let namedLabels = 0;
459
+ for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
460
+ const known = symbols.get(address);
461
+ if (known !== undefined) {
462
+ if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${hex4(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
463
+ names.set(address, known);
464
+ if (inImage(address)) namedLabels++;
465
+ continue;
466
+ }
467
+ const named = platform.get(address);
468
+ if (named !== undefined) {
469
+ names.set(address, named);
470
+ if (inImage(address)) namedLabels++;
471
+ continue;
472
+ }
473
+ let name = `l_${address.toString(16).padStart(4, "0")}`;
474
+ while (taken.has(name)) name = `${name}_`;
475
+ taken.add(name);
476
+ names.set(address, name);
477
+ generatedLabels++;
478
+ }
479
+ // A patch is written when its range is whole items of one file and its switch is no name of the source.
480
+ const blocks = new Map<number, PatchBlock>();
481
+ const patchesNotWritten: PatchNotWritten[] = [];
482
+ const sourceNames = new Set([...names.values(), ...taken]);
483
+ for (const patch of patches) {
484
+ const refuse = (reason: string) => patchesNotWritten.push({ start: patch.start, end: patch.end, switch: patch.switch, reason });
485
+ const piece = pieces.find((row) => patch.start >= row.start && patch.start <= row.end);
486
+ if (piece === undefined) {
487
+ refuse("The range is not in the image.");
488
+ continue;
489
+ }
490
+ if (patch.end > piece.end) {
491
+ refuse(`The range runs past the end of the file ${piece.file} at ${hex4(piece.end)}: a patch is in one file.`);
492
+ continue;
493
+ }
494
+ const starts = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address : item.start)));
495
+ const ends = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address + item.length - 1 : item.end)));
496
+ if (!starts.has(patch.start) || !ends.has(patch.end)) {
497
+ const cut = piece.items.find((item) => item.kind === "instruction" && ((patch.start > item.address && patch.start < item.address + item.length) || (patch.end >= item.address && patch.end < item.address + item.length - 1)));
498
+ refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${hex4((cut as InstructionItem).address)}.`);
499
+ continue;
500
+ }
501
+ if (sourceNames.has(patch.switch)) {
502
+ refuse(`The switch name ${patch.switch} is also a name of the source.`);
503
+ continue;
504
+ }
505
+ const inner = [...names.keys()].filter((address) => address > patch.start && address <= patch.end).sort((a, b) => a - b);
506
+ blocks.set(patch.start, { start: patch.start, end: patch.end, switch: patch.switch, code: patch.code, ...(patch.comment === undefined ? {} : { comment: patch.comment }), inner });
507
+ }
508
+ // The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
509
+ // An address inside a table keeps its own label when the source needs one there anyway, for example as the entry.
510
+ for (const [address, start] of insideEntry) if (!wanted.has(address)) names.set(address, `${names.get(start)!}+${address - start}`);
511
+ /** An expression with the names of this source: a name with an offset in brackets, so an operator after it keeps its meaning. */
512
+ const sourceText = (text: string) => replaceIdentifiers(text, (name) => {
513
+ const label = names.get(byName.get(name)!)!;
514
+ return label.includes("+") ? `(${label})` : label;
515
+ });
516
+ const usesWritten = new Map<number, MacroUseBlock>();
517
+ for (const { use: row } of [...accepted].sort((a, b) => a.use.start - b.use.start)) usesWritten.set(row.start, { start: row.start, end: row.end, macro: row.macro, args: row.args.map(sourceText) });
518
+ const operands = new Map<number, string>();
519
+ for (const { address, text } of expressions) {
520
+ const unnamed: string[] = [];
521
+ const written = replaceIdentifiers(text, (name) => {
522
+ const label = names.get(byName.get(name)!);
523
+ if (label === undefined) unnamed.push(name);
524
+ return label === undefined ? name : label.includes("+") ? `(${label})` : label;
525
+ });
526
+ if (unnamed.length === 0) operands.set(address, written);
527
+ else operandsAsNumbers.push({ address, expression: text, reason: `The source has no name for ${unnamed.join(", ")}.` });
528
+ }
529
+ return {
530
+ pieces,
531
+ names,
532
+ constants: [...outside].sort((a, b) => a - b),
533
+ uses,
534
+ platform: input.platform ?? [],
535
+ ...(input.describe === undefined ? {} : { describe: input.describe }),
536
+ inImage,
537
+ byteAt: input.byteAt,
538
+ comments: [...input.comments, ...decodedStores(pieces, input)],
539
+ ...(input.coverage === undefined ? {} : { coverage: input.coverage }),
540
+ generatedLabels,
541
+ namedLabels,
542
+ unusedModules,
543
+ operands,
544
+ operandsAsNumbers: operandsAsNumbers.sort((a, b) => a.address - b.address),
545
+ patches: blocks,
546
+ patchesNotWritten,
547
+ macros: [...(input.macros ?? [])].sort((a, b) => (a.name < b.name ? -1 : 1)),
548
+ macroUses: usesWritten,
549
+ macrosAsInstructions: macrosAsInstructions.sort((a, b) => a.start - b.start),
550
+ };
551
+ }
552
+
553
+ /** The pointer region types: a table of addresses, or of addresses minus 1 for RTS. */
554
+ const POINTER_TYPES: ReadonlyArray<DataItem["type"]> = POINTER_TABLE_TYPES;
555
+ const isPointerTable = (region: Region): region is Region & { type: PointerTableType } => (POINTER_TABLE_TYPES as readonly string[]).includes(region.type);
556
+
557
+ /** The pairs of a pointer table: the address of each low and high byte, and the target it names. */
558
+ function pairsOf(region: Region, byteAt: (address: number) => number): Array<{ low: number; high: number; target: number }> {
559
+ return isPointerTable(region) ? pointerTableEntries(region, byteAt) : [];
560
+ }
561
+
562
+ function pointersOf(item: DataItem, byteAt: (address: number) => number): number[] {
563
+ if (item.region === undefined || !POINTER_TYPES.includes(item.type)) return [];
564
+ return pairsOf(item.region, byteAt)
565
+ .filter((pair) => (pair.low >= item.start && pair.low <= item.end) || (pair.high >= item.start && pair.high <= item.end))
566
+ .map((pair) => pair.target);
567
+ }
568
+
569
+ /** How an instruction is written: the text with a given operand expression, and the key of its opcode form for the probe. */
570
+ export interface InstructionForm {
571
+ key: string;
572
+ opcode: number;
573
+ suffix: string;
574
+ }
575
+
576
+ /** The operand size suffix: +2 forces an absolute operand below $0100, +1 a zero-page operand that is a label of the image. */
577
+ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: InstructionItem): InstructionForm {
578
+ const { mnemonic, mode } = OPCODES[item.opcode]!;
579
+ const value = takesAddress(mode) ? operandValue(item.address, item.opcode, layout.byteAt)! : 0;
580
+ let suffix = "";
581
+ if ((mode === "abs" || mode === "abx" || mode === "aby") && value < 0x100 && hasZeroPageForm(mnemonic, mode)) suffix = "+2";
582
+ if ((mode === "zp" || mode === "zpx" || mode === "zpy") && layout.inImage(value)) suffix = "+1";
583
+ return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
584
+ }
585
+
586
+ /** The instruction text with an operand expression; the expression is a name or a $ number. */
587
+ export function instructionText(opcode: number, suffix: string, expression: string): string {
588
+ const { mnemonic, mode } = OPCODES[opcode]!;
589
+ const operand: Record<Mode, string> = {
590
+ imp: "",
591
+ imm: `#${expression}`,
592
+ zp: expression,
593
+ zpx: `${expression},x`,
594
+ zpy: `${expression},y`,
595
+ abs: expression,
596
+ abx: `${expression},x`,
597
+ aby: `${expression},y`,
598
+ ind: `(${expression})`,
599
+ izx: `(${expression},x)`,
600
+ izy: `(${expression}),y`,
601
+ rel: expression,
602
+ };
603
+ const text = operand[mode];
604
+ return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
605
+ }
606
+
607
+ /** The plain number of an operand: what the probe assembles, and what a comment shows. */
608
+ export function literalOperand(opcode: number, value: number): string {
609
+ const { mode } = OPCODES[opcode]!;
610
+ return MODE_LENGTH[mode] === 3 || mode === "rel" ? hex4(value) : hex2(value);
611
+ }
612
+
613
+ /** One instruction of each form, for the probe: the form, and the instruction bytes. */
614
+ export function probeForms(layout: Layout): Array<{ form: InstructionForm; bytes: number[] }> {
615
+ const forms = new Map<string, { form: InstructionForm; bytes: number[] }>();
616
+ for (const piece of layout.pieces) {
617
+ for (const item of piece.items) {
618
+ if (item.kind !== "instruction") continue;
619
+ const form = formOf(layout, item);
620
+ if (!forms.has(form.key)) forms.set(form.key, { form, bytes: Array.from({ length: item.length }, (_, offset) => layout.byteAt(item.address + offset)) });
621
+ }
622
+ }
623
+ return [...forms.values()].sort((a, b) => (a.form.key < b.form.key ? -1 : 1));
624
+ }
625
+
626
+ // --- Data cells -------------------------------------------------------------
627
+
628
+ type Directive = "!byte" | "!word" | "!fill" | "!pet" | "!scr";
629
+
630
+ interface Cell {
631
+ address: number;
632
+ length: number;
633
+ directive: Directive;
634
+ /** The value text: an expression, a fill, or the characters of a string. */
635
+ text: string;
636
+ }
637
+
638
+ /** PETSCII bytes that ACME's !pet writes from one source character. */
639
+ function petsciiChar(byte: number): string | undefined {
640
+ if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte + 0x20);
641
+ if (byte >= 0xc1 && byte <= 0xda) return String.fromCharCode(byte - 0x80);
642
+ if (byte >= 0x20 && byte <= 0x40 && byte !== 0x22) return String.fromCharCode(byte);
643
+ if (byte === 0x5b || byte === 0x5d) return String.fromCharCode(byte);
644
+ return undefined;
645
+ }
646
+
647
+ /** Screen codes that ACME's !scr writes from one source character. */
648
+ function screenChar(byte: number): string | undefined {
649
+ if (byte === 0x00) return "@";
650
+ if (byte >= 0x01 && byte <= 0x1a) return String.fromCharCode(byte + 0x60);
651
+ if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte);
652
+ if (byte >= 0x20 && byte <= 0x3f && byte !== 0x22) return String.fromCharCode(byte);
653
+ return undefined;
654
+ }
655
+
656
+ const FILL_TYPES: ReadonlyArray<DataItem["type"]> = ["bytes", "workspace", "unused", "unclassified", "bitmap"];
657
+
658
+ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
659
+ const { byteAt } = layout;
660
+ const cells: Cell[] = [];
661
+ const name = (address: number) => layout.names.get(address);
662
+ const expression = (target: number) => name(target) ?? hex4(target);
663
+ const rtsExpression = (target: number) => (name(target) === undefined ? hex4((target - 1) & 0xffff) : `${name(target)}-1`);
664
+ const byteCell = (address: number, text = hex2(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
665
+
666
+ if (item.region !== undefined && POINTER_TYPES.includes(item.type)) {
667
+ const rts = item.type === "rts-addresses" || item.type === "rts-addresses-split";
668
+ const split = item.type === "addresses-split" || item.type === "rts-addresses-split";
669
+ const pairs = pairsOf(item.region, byteAt);
670
+ const partOf = new Map<number, { pair: (typeof pairs)[number]; high: boolean }>();
671
+ for (const pair of pairs) {
672
+ partOf.set(pair.low, { pair, high: false });
673
+ partOf.set(pair.high, { pair, high: true });
674
+ }
675
+ for (let address = item.start; address <= item.end; address++) {
676
+ const part = partOf.get(address);
677
+ if (part === undefined) {
678
+ cells.push(byteCell(address));
679
+ continue;
680
+ }
681
+ const text = rts ? rtsExpression(part.pair.target) : expression(part.pair.target);
682
+ const whole = !split && !part.high && address + 1 <= item.end && !breaks.has(address + 1);
683
+ if (whole) {
684
+ cells.push({ address, length: 2, directive: "!word", text });
685
+ address++;
686
+ continue;
687
+ }
688
+ cells.push(byteCell(address, `${part.high ? ">" : "<"}${/^[$A-Za-z_.0-9]+$/.test(text) ? text : `(${text})`}`));
689
+ }
690
+ return cells;
691
+ }
692
+
693
+ if (item.type === "words") {
694
+ const odd = (item.region === undefined ? 0 : (item.region.end - item.region.start + 1) % 2) === 1;
695
+ for (let address = item.start; address <= item.end; address++) {
696
+ const offset = address - (item.region?.start ?? item.start);
697
+ const last = odd && address === item.region!.end;
698
+ if (offset % 2 === 0 && !last && address + 1 <= item.end && !breaks.has(address + 1)) {
699
+ cells.push({ address, length: 2, directive: "!word", text: hex4(byteAt(address) | (byteAt(address + 1) << 8)) });
700
+ address++;
701
+ } else cells.push(byteCell(address));
702
+ }
703
+ return cells;
704
+ }
705
+
706
+ if (item.type === "petscii" || item.type === "screen") {
707
+ const toChar = item.type === "petscii" ? petsciiChar : screenChar;
708
+ const directive = item.type === "petscii" ? "!pet" : "!scr";
709
+ for (let address = item.start; address <= item.end; address++) {
710
+ const char = toChar(byteAt(address));
711
+ cells.push(char === undefined ? byteCell(address) : { address, length: 1, directive, text: char });
712
+ }
713
+ return cells;
714
+ }
715
+
716
+ const binary = item.type === "sprite";
717
+ const fill = FILL_TYPES.includes(item.type) && item.region?.element === undefined;
718
+ for (let address = item.start; address <= item.end; address++) {
719
+ const value = byteAt(address);
720
+ if (fill) {
721
+ let end = address;
722
+ while (end + 1 <= item.end && byteAt(end + 1) === value && !breaks.has(end + 1)) end++;
723
+ if (end - address + 1 >= MIN_FILL) {
724
+ cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${hex2(value)}` });
725
+ address = end;
726
+ continue;
727
+ }
728
+ }
729
+ cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : hex2(value)));
730
+ }
731
+ return cells;
732
+ }
733
+
734
+ /** The cells of one data item as lines: one directive per line, broken at labels, comments and elements. */
735
+ function dataLines(item: DataItem, layout: Layout, breaks: Set<number>): Array<{ address: number; text: string }> {
736
+ const cells = cellsOf(item, layout, breaks);
737
+ const element = item.region?.element;
738
+ const perLine = (directive: Directive) =>
739
+ directive === "!word" ? WORDS_PER_LINE : directive === "!pet" || directive === "!scr" ? CHARS_PER_LINE : item.type === "sprite" ? 3 : item.type === "charset" ? 8 : element !== undefined && element <= 16 ? element : BYTES_PER_LINE;
740
+ const lines: Array<{ address: number; text: string }> = [];
741
+ let group: Cell[] = [];
742
+ const flush = () => {
743
+ if (group.length === 0) return;
744
+ const [first] = group;
745
+ const directive = first!.directive;
746
+ const text =
747
+ directive === "!pet" || directive === "!scr"
748
+ ? `${directive} "${group.map((cell) => cell.text).join("")}"`
749
+ : `${directive} ${group.map((cell) => cell.text).join(directive === "!fill" ? "" : ",")}`;
750
+ lines.push({ address: first!.address, text });
751
+ group = [];
752
+ };
753
+ for (const cell of cells) {
754
+ const first = group[0];
755
+ const elementStart = element !== undefined && item.region !== undefined && (cell.address - item.region.start) % element === 0;
756
+ if (first !== undefined && (first.directive !== cell.directive || cell.directive === "!fill" || group.length >= perLine(first.directive) || breaks.has(cell.address) || elementStart)) flush();
757
+ group.push(cell);
758
+ }
759
+ flush();
760
+ return lines;
761
+ }
762
+
763
+ // --- Constants --------------------------------------------------------------
764
+
765
+ /** A longer name moves only its own comment, not the columns of the file. */
766
+ const MAX_NAME_COLUMN = 24;
767
+
768
+ /** What an address outside the image is: the comment of the knowledge, else the platform meaning, else the register that a mirror repeats. */
769
+ function describeConstant(layout: Layout, address: number): string | undefined {
770
+ const comments = layout.comments.filter((comment) => comment.address === address);
771
+ // Line comments come before the side comment, as they do in the code.
772
+ const own = [...comments.filter((comment) => comment.placement === "line"), ...comments.filter((comment) => comment.placement === "side")];
773
+ if (own.length > 0) return own.map((comment) => comment.text.replace(/\s*\r?\n\s*/g, " ").trim()).join(" ");
774
+ const entry = layout.platform.find((row) => address >= row.address && address <= row.end);
775
+ if (entry?.meaning !== undefined) {
776
+ const platformName = address === entry.address ? entry.name : `${entry.name}+${address - entry.address}`;
777
+ return platformName === layout.names.get(address) ? entry.meaning : `${platformName}: ${entry.meaning}`;
778
+ }
779
+ const mirrorOf = layout.describe?.(address).mirrorOf;
780
+ if (mirrorOf === undefined) return undefined;
781
+ const register = layout.platform.find((row) => row.address === mirrorOf);
782
+ return `Mirror of ${hex4(mirrorOf)}${register === undefined ? "" : ` (${register.name})`}.`;
783
+ }
784
+
785
+ /** How the source uses a constant, for example "Used: read 4, written 1." */
786
+ function usageText(kinds: ReadonlyMap<UseKind, number> | undefined): string | undefined {
787
+ const parts = USE_KINDS.filter((kind) => kinds?.has(kind)).map((kind) => `${kind} ${kinds!.get(kind)!}`);
788
+ return parts.length === 0 ? undefined : `Used: ${parts.join(", ")}.`;
789
+ }
790
+
791
+ /** The switches of the patches, each 0 unless the build defines it, with the ranges of its patches. */
792
+ export function switchesOf(layout: Pick<Layout, "patches">): Map<string, PatchBlock[]> {
793
+ const switches = new Map<string, PatchBlock[]>();
794
+ for (const patch of [...layout.patches.values()].sort((a, b) => a.start - b.start)) switches.set(patch.switch, [...(switches.get(patch.switch) ?? []), patch]);
795
+ return new Map([...switches].sort(([a], [b]) => (a < b ? -1 : 1)));
796
+ }
797
+
798
+ function switchLines(layout: Layout): string[] {
799
+ const switches = switchesOf(layout);
800
+ if (switches.size === 0) return [];
801
+ const width = Math.max(...[...switches.keys()].map((name) => name.length));
802
+ const lines = ["; Switches: each is 0 unless the build defines it. Assemble with -DNAME=1", "; (c64-assembler --define NAME=1) to put in the patches of NAME."];
803
+ for (const [name, patches] of switches) {
804
+ const ranges = patches.map((patch) => `${hex4(patch.start)}-${hex4(patch.end)}`).join(", ");
805
+ lines.push(`!ifndef ${name} { ${name} = 0 }${" ".repeat(width - name.length)} ; ${patches.length} patch${patches.length === 1 ? "" : "es"}: ${ranges}`);
806
+ }
807
+ return [...lines, ""];
808
+ }
809
+
810
+ /** constants.asm: the names outside the image by platform area, each with what it is and how the source uses it. */
811
+ function constantsText(layout: Layout): { text: string; labels: SourceLabel[] } {
812
+ const { describe } = layout;
813
+ const labels: SourceLabel[] = [];
814
+ const rows = layout.constants.map((address) => {
815
+ const comment = [describeConstant(layout, address), usageText(layout.uses.get(address))].filter((text) => text !== undefined).join(" ");
816
+ return { address, name: layout.names.get(address)!, value: constantValue(address), comment };
817
+ });
818
+ const nameColumn = Math.min(MAX_NAME_COLUMN, Math.max(0, ...rows.map((row) => row.name.length)));
819
+ const valueColumn = Math.max(0, ...rows.map((row) => row.value.length));
820
+ const out = [...switchLines(layout)];
821
+ out.push(
822
+ describe === undefined ? "; Names of addresses outside the image." : "; Names of addresses outside the image, by memory area.",
823
+ '; "Used" counts the instructions and table entries of this source that use the name.',
824
+ );
825
+ // Without platform areas the names form one group.
826
+ if (describe === undefined && rows.length > 0) out.push("");
827
+ let area: Area | undefined;
828
+ for (const row of rows) {
829
+ const next = describe?.(row.address).area;
830
+ if (next !== undefined && next.start !== area?.start) {
831
+ area = next;
832
+ out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${hex4(area.start)}-${hex4(area.end)}`);
833
+ if (area.meaning !== undefined) out.push(`; ${area.meaning}`);
834
+ }
835
+ const line = `${row.name.padEnd(nameColumn)} = ${row.value.padEnd(valueColumn)}`;
836
+ out.push(row.comment === "" ? line.trimEnd() : `${line} ; ${row.comment}`);
837
+ labels.push({ address: row.address, name: row.name, file: "constants.asm", line: out.length });
838
+ }
839
+ return { text: `${out.join("\n")}\n`, labels };
840
+ }
841
+
842
+ // --- Rendering --------------------------------------------------------------
843
+
844
+ /** The line of a name: its label line, or its line in constants.asm (1 is the first line of the text). */
845
+ export interface SourceLabel {
846
+ address: number;
847
+ name: string;
848
+ file: string;
849
+ line: number;
850
+ }
851
+
852
+ /** The first line of an instruction, a macro use or a data line, and the addresses that it writes. */
853
+ export interface SourceItem {
854
+ start: number;
855
+ end: number;
856
+ file: string;
857
+ line: number;
858
+ }
859
+
860
+ export interface Rendered {
861
+ files: Array<{ name: string; text: string }>;
862
+ instructions: number;
863
+ dataBytes: number;
864
+ undecodedBytes: number;
865
+ /** Instructions written as bytes, because ACME writes their form with other bytes. */
866
+ asBytes: Array<{ address: number; text: string }>;
867
+ /** With coverage: runs of instructions that no run executed. */
868
+ notExecuted?: { instructions: number; ranges: Range[] };
869
+ /** Each instruction written with an operand expression: its file and line (1 is the first line of the text). */
870
+ expressions: Array<{ address: number; length: number; file: string; line: number }>;
871
+ /** Operand expressions written as numbers, in address order. */
872
+ operandsAsNumbers: OperandAsNumber[];
873
+ /** The patches written, in address order. */
874
+ patches: number;
875
+ /** Each macro use written: its file and line (1 is the first line of the text). */
876
+ uses: Array<{ start: number; end: number; macro: string; file: string; line: number }>;
877
+ /** The macro uses written as instructions, in address order, with the reason. */
878
+ macrosAsInstructions: MacroAsInstructions[];
879
+ /** The lines of each macro in macros.asm, from its !macro line to its closing brace. */
880
+ macroLines: Map<string, { first: number; last: number }>;
881
+ /** The source map: the line of each name, and the line of each address that the source writes. */
882
+ labels: SourceLabel[];
883
+ items: SourceItem[];
884
+ }
885
+
886
+ /** macros.asm: each macro with its comment, as ACME reads it; a param is .name in the body. */
887
+ function macrosText(layout: Layout): { text: string; lines: Map<string, { first: number; last: number }> } {
888
+ const out = ["; Macros of the knowledge. The source writes a use as +name args."];
889
+ const lines = new Map<string, { first: number; last: number }>();
890
+ for (const macro of layout.macros) {
891
+ out.push("");
892
+ if (macro.comment !== undefined) out.push(...macro.comment.split(/\r?\n/).map((line) => `; ${line}`.trimEnd()));
893
+ const first = out.length + 1;
894
+ out.push(`!macro ${macro.name}${macro.params.map((param, index) => `${index === 0 ? " " : ", "}.${param}`).join("")} {`, ...macro.body.split(/\r?\n/), "}");
895
+ lines.set(macro.name, { first, last: out.length });
896
+ }
897
+ return { text: `${out.join("\n")}\n`, lines };
898
+ }
899
+
900
+ /** The lines that open a patch: its zone, the switch, the code, and the guard that stops a patch longer than its range. */
901
+ function patchOpening(patch: PatchBlock): string[] {
902
+ const bytes = patch.end - patch.start + 1;
903
+ const range = `${hex4(patch.start)}-${hex4(patch.end)}`;
904
+ const comment = patch.comment === undefined ? [] : patch.comment.split(/\r?\n/).map((line) => `${INDENT}; ${line}`.trimEnd());
905
+ return [
906
+ `!zone patch_${patch.switch}_${patch.start.toString(16).padStart(4, "0")} {`,
907
+ ".patch_start",
908
+ `!if ${patch.switch} {`,
909
+ `${INDENT}; ${patch.switch} on: ${bytes} byte${bytes === 1 ? "" : "s"} at ${range}, NOP after the code`,
910
+ ...comment,
911
+ ...(patch.code === "" ? [] : patch.code.split(/\r?\n/)),
912
+ `${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${hex4(patch.start)} has ", * - .patch_start, " bytes; the range has ${bytes}." } else { !fill ${bytes} - (* - .patch_start), $ea }`,
913
+ "} else {",
914
+ ];
915
+ }
916
+
917
+ const withSide = (text: string, side: string | undefined) => (side === undefined ? text : `${text.padEnd(SIDE_COLUMN - 1)} ; ${side}`);
918
+
919
+ /**
920
+ * The source text of each piece, the main file and the constants. `asBytes`
921
+ * holds the opcode form keys that the probe refused; `numbers` the operand
922
+ * expressions that ACME did not assemble to the image, with the reason.
923
+ */
924
+ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: string[], numbers: ReadonlyMap<number, string> = new Map(), asInstructions: ReadonlyMap<number, string> = new Map()): Rendered {
925
+ const lineComments = new Map<number, string[]>();
926
+ const sideComments = new Map<number, string>();
927
+ for (const comment of layout.comments) {
928
+ if (!layout.inImage(comment.address)) continue;
929
+ if (comment.placement === "line") lineComments.set(comment.address, comment.text.split(/\r?\n/));
930
+ else sideComments.set(comment.address, comment.text.replace(/\s*\r?\n\s*/g, " "));
931
+ }
932
+ // A name with an offset (table+5) names an operand only: it is no label, so no line starts there.
933
+ const labelAt = (address: number) => {
934
+ const name = layout.names.get(address);
935
+ return name === undefined || name.includes("+") ? undefined : name;
936
+ };
937
+ const breaks = new Set<number>([...[...layout.names.keys()].filter((address) => labelAt(address) !== undefined), ...lineComments.keys(), ...sideComments.keys()]);
938
+ const result: Rendered = {
939
+ files: [],
940
+ instructions: 0,
941
+ dataBytes: 0,
942
+ undecodedBytes: 0,
943
+ asBytes: [],
944
+ expressions: [],
945
+ operandsAsNumbers: [...layout.operandsAsNumbers],
946
+ patches: 0,
947
+ uses: [],
948
+ macrosAsInstructions: [...layout.macrosAsInstructions],
949
+ macroLines: new Map(),
950
+ labels: [],
951
+ items: [],
952
+ };
953
+ for (const [start, reason] of asInstructions) {
954
+ const use = layout.macroUses.get(start);
955
+ if (use !== undefined) result.macrosAsInstructions.push({ start, end: use.end, macro: use.macro, reason });
956
+ }
957
+ result.macrosAsInstructions.sort((a, b) => a.start - b.start);
958
+ const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
959
+ const { coverage } = layout;
960
+ const notExecuted = { instructions: 0, ranges: [] as Range[] };
961
+
962
+ for (const piece of layout.pieces) {
963
+ const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
964
+ /** True while the instructions before have not executed: they form one range. */
965
+ let running = false;
966
+ /** The line of the mark of the open range of instructions that did not execute. */
967
+ let markLine = 0;
968
+ /** The open patch: its labels are defined before it, so the original code writes none. */
969
+ let patch: PatchBlock | undefined;
970
+ /** The last address of the macro use just written: its instructions are in the use line. */
971
+ let useEnd = -1;
972
+ /** Marks the start of a range of instructions that no run executed, or makes the open range longer. */
973
+ const mark = (address: number, last: number, count: number) => {
974
+ const executed = coverage === undefined || (coverage[address]! & COVERAGE.ramExecute) !== 0;
975
+ if (!executed) {
976
+ if (running) notExecuted.ranges.at(-1)!.end = last;
977
+ else {
978
+ notExecuted.ranges.push({ start: address, end: last });
979
+ markLine = out.length;
980
+ out.push("");
981
+ }
982
+ // The mark gives the range, so a reader finds it in the full list.
983
+ const range = notExecuted.ranges.at(-1)!;
984
+ out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
985
+ notExecuted.instructions += count;
986
+ }
987
+ running = !executed;
988
+ };
989
+ const head = (address: number) => {
990
+ if (patch?.start === address) return;
991
+ for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
992
+ const label = labelAt(address);
993
+ if (label !== undefined && patch === undefined) {
994
+ out.push(label);
995
+ result.labels.push({ address, name: label, file: piece.file, line: out.length });
996
+ }
997
+ };
998
+ const mapLine = (start: number, end: number) => result.items.push({ start, end, file: piece.file, line: out.length });
999
+ for (const item of piece.items) {
1000
+ const itemStart = item.kind === "instruction" ? item.address : item.start;
1001
+ const opened = layout.patches.get(itemStart);
1002
+ if (opened !== undefined) {
1003
+ head(itemStart);
1004
+ for (const address of opened.inner) {
1005
+ out.push(`${layout.names.get(address)!} = * + ${address - opened.start}`);
1006
+ result.labels.push({ address, name: layout.names.get(address)!, file: piece.file, line: out.length });
1007
+ }
1008
+ out.push(...patchOpening(opened));
1009
+ patch = opened;
1010
+ result.patches++;
1011
+ }
1012
+ const close = () => {
1013
+ const itemEnd = item.kind === "instruction" ? item.address + item.length - 1 : item.end;
1014
+ if (patch === undefined || itemEnd !== patch.end) return;
1015
+ out.push("}", "}");
1016
+ patch = undefined;
1017
+ };
1018
+ if (item.kind === "instruction" && item.address <= useEnd) {
1019
+ result.instructions++;
1020
+ continue;
1021
+ }
1022
+ const macroUse = item.kind === "instruction" && !asInstructions.has(item.address) ? layout.macroUses.get(item.address) : undefined;
1023
+ if (macroUse !== undefined) {
1024
+ head(macroUse.start);
1025
+ const count = piece.items.filter((other) => other.kind === "instruction" && other.address >= macroUse.start && other.address <= macroUse.end).length;
1026
+ mark(macroUse.start, macroUse.end, count);
1027
+ // The comments inside the use go to its side comment, as for the bytes of an instruction.
1028
+ const sides: string[] = [];
1029
+ for (let address = macroUse.start; address <= macroUse.end; address++) {
1030
+ if (address !== macroUse.start) for (const text of lineComments.get(address) ?? []) sides.push(text);
1031
+ if (sideComments.has(address)) sides.push(sideComments.get(address)!);
1032
+ }
1033
+ out.push(withSide(`${INDENT}+${macroUse.macro}${macroUse.args.length === 0 ? "" : ` ${macroUse.args.join(", ")}`}`, sides.length === 0 ? undefined : sides.join(" ")));
1034
+ result.uses.push({ start: macroUse.start, end: macroUse.end, macro: macroUse.macro, file: piece.file, line: out.length });
1035
+ mapLine(macroUse.start, macroUse.end);
1036
+ result.instructions++;
1037
+ useEnd = macroUse.end;
1038
+ continue;
1039
+ }
1040
+ if (item.kind === "instruction") {
1041
+ head(item.address);
1042
+ mark(item.address, item.address + item.length - 1, 1);
1043
+ const sides: string[] = [];
1044
+ if (sideComments.has(item.address)) sides.push(sideComments.get(item.address)!);
1045
+ for (let offset = 1; offset < item.length; offset++) {
1046
+ const inside = item.address + offset;
1047
+ const label = labelAt(inside);
1048
+ if (label !== undefined && patch === undefined) {
1049
+ out.push(`${label} = * + ${offset}`);
1050
+ result.labels.push({ address: inside, name: label, file: piece.file, line: out.length });
1051
+ }
1052
+ for (const text of lineComments.get(inside) ?? []) sides.push(text);
1053
+ if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
1054
+ }
1055
+ const form = formOf(layout, item);
1056
+ const { mode } = OPCODES[item.opcode]!;
1057
+ const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
1058
+ const literal = instructionText(item.opcode, "", literalOperand(item.opcode, value));
1059
+ let written = layout.operands.get(item.address);
1060
+ // An expression that is not written stays visible as a side comment.
1061
+ const asNumber = (expression: string, reason: string) => {
1062
+ sides.unshift(`= ${expression}`);
1063
+ result.operandsAsNumbers.push({ address: item.address, expression, reason });
1064
+ written = undefined;
1065
+ };
1066
+ if (written !== undefined && asBytes.has(form.key)) asNumber(written, "ACME writes this instruction with other bytes, so the source has it as bytes.");
1067
+ if (written !== undefined && numbers.has(item.address)) asNumber(written, numbers.get(item.address)!);
1068
+ if (unwritten.has(item.address)) sides.unshift(`= ${unwritten.get(item.address)!}`);
1069
+ if (asBytes.has(form.key)) {
1070
+ const bytes = Array.from({ length: item.length }, (_, offset) => hex2(layout.byteAt(item.address + offset)));
1071
+ out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
1072
+ result.asBytes.push({ address: item.address, text: literal });
1073
+ } else if (written !== undefined) {
1074
+ // ACME reads an operand in brackets as indirect, so a non-immediate expression does not start with one.
1075
+ const expression = mode !== "imm" && written.startsWith("(") ? `0+${written}` : written;
1076
+ out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1077
+ result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
1078
+ } else {
1079
+ const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
1080
+ out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1081
+ }
1082
+ mapLine(item.address, item.address + item.length - 1);
1083
+ result.instructions++;
1084
+ close();
1085
+ continue;
1086
+ }
1087
+ running = false;
1088
+ const lines = dataLines(item, layout, breaks);
1089
+ lines.forEach((line, index) => {
1090
+ head(line.address);
1091
+ const note = item.type === "undecoded" && index === 0 ? "not decoded" : undefined;
1092
+ const side = [note, sideComments.get(line.address)].filter((text) => text !== undefined).join(" ");
1093
+ out.push(withSide(`${INDENT}${line.text}`, side === "" ? undefined : side));
1094
+ mapLine(line.address, (lines[index + 1]?.address ?? item.end + 1) - 1);
1095
+ });
1096
+ const bytes = item.end - item.start + 1;
1097
+ if (item.type === "undecoded") result.undecodedBytes += bytes;
1098
+ else result.dataBytes += bytes;
1099
+ close();
1100
+ }
1101
+ result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
1102
+ }
1103
+
1104
+ const constants = constantsText(layout);
1105
+ result.files.push({ name: "constants.asm", text: constants.text });
1106
+ result.labels.push(...constants.labels);
1107
+ if (layout.macros.length > 0) {
1108
+ const macros = macrosText(layout);
1109
+ result.files.push({ name: "macros.asm", text: macros.text });
1110
+ result.macroLines = macros.lines;
1111
+ }
1112
+ const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(layout.macros.length > 0 ? ['!source "macros.asm"'] : [])];
1113
+ for (const piece of layout.pieces) {
1114
+ if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
1115
+ main.push(`!source "${piece.file}"`);
1116
+ }
1117
+ result.files.push({ name: "main.asm", text: `${main.join("\n")}\n` });
1118
+ if (coverage !== undefined) result.notExecuted = notExecuted;
1119
+ result.operandsAsNumbers.sort((a, b) => a.address - b.address);
1120
+ return result;
1121
+ }