@henols/c64-re-tools 2.0.0-rc.11 → 2.0.0-rc.13

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 (54) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +11 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +4 -4
  5. package/skills/c64-basic/SKILL.md +8 -1
  6. package/skills/c64-basic/scripts/basic.ts +3 -3
  7. package/skills/c64-disk/SKILL.md +10 -3
  8. package/skills/c64-disk/scripts/disk.ts +4 -4
  9. package/skills/c64-emulator/SKILL.md +4 -0
  10. package/skills/c64-knowledge/SKILL.md +62 -12
  11. package/skills/c64-knowledge/scripts/knowledge.ts +352 -102
  12. package/skills/c64-memory-map/SKILL.md +7 -0
  13. package/skills/c64-memory-map/scripts/memmap.ts +1 -1
  14. package/skills/c64-provenance/SKILL.md +56 -3
  15. package/skills/c64-provenance/scripts/library.ts +584 -0
  16. package/skills/c64-provenance/scripts/provenance.ts +201 -9
  17. package/skills/c64-reverse-engineering/SKILL.md +16 -2
  18. package/skills/c64-source/SKILL.md +38 -7
  19. package/skills/c64-source/scripts/generate.ts +213 -29
  20. package/skills/c64-source/scripts/loader.ts +3 -1
  21. package/skills/c64-source/scripts/opcodes.ts +19 -0
  22. package/skills/c64-source/scripts/source.ts +387 -35
  23. package/skills/c64-static-analysis/SKILL.md +11 -2
  24. package/skills/c64-static-analysis/scripts/analyze.ts +95 -8
  25. package/skills/c64-static-analysis/scripts/macros.ts +233 -0
  26. package/skills/c64-testing/SKILL.md +8 -1
  27. package/skills/c64-testing/references/scenario-format.md +1 -1
  28. package/skills/c64-testing/scripts/checklist.ts +2 -2
  29. package/skills/c64-testing/scripts/run.ts +4 -4
  30. package/skills/c64-testing/scripts/scenario.ts +1 -1
  31. package/skills/c64-testing/scripts/test.ts +4 -4
  32. package/skills/c64-unpacker/SKILL.md +11 -4
  33. package/skills/c64-unpacker/scripts/unpack.ts +4 -4
  34. package/src/cli/main.ts +5 -3
  35. package/src/host-client/image.ts +1 -1
  36. package/src/host-client/transfer.ts +34 -10
  37. package/src/host-client/vice-session.ts +9 -9
  38. package/src/knowledge/conflicts.ts +157 -0
  39. package/src/knowledge/database.ts +150 -38
  40. package/src/knowledge/history.ts +54 -20
  41. package/src/knowledge/import.ts +3 -2
  42. package/src/knowledge/read.ts +169 -20
  43. package/src/knowledge/schema.ts +115 -72
  44. package/src/knowledge/text.ts +391 -0
  45. package/src/knowledge/write.ts +508 -51
  46. package/src/mcp/output.ts +14 -7
  47. package/src/mcp/tools/debug.ts +8 -6
  48. package/src/mcp/tools/media.ts +17 -13
  49. package/src/mcp/tools/memory.ts +9 -5
  50. package/src/mcp/tools/video.ts +6 -5
  51. package/src/platform.ts +28 -5
  52. package/src/project.ts +166 -3
  53. package/src/protocol/tools.ts +1 -1
  54. package/src/script.ts +46 -7
@@ -2,8 +2,10 @@
2
2
  // knowledge and Ghidra's instructions, which it runs itself, and checks with
3
3
  // ACME that the source assembles to the image byte by byte. With --loader it
4
4
  // also writes a loader that recreates the entry state, simulates it, and
5
- // checks it in the emulator. Exit status: 0 when every check passes, 1 when
6
- // one fails, 2 for a bad call.
5
+ // checks it in the emulator. Each switch of the patches (D72) is built on
6
+ // alone and checked. A macro use (D73) that does not give the bytes of the
7
+ // image is written as its instructions. Exit status: 0 when every check
8
+ // passes, 1 when one fails, 2 for a bad call.
7
9
 
8
10
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
9
11
  import { join, posix } from "node:path";
@@ -16,24 +18,26 @@ import { ImageError, readImage as readSharedImage, type Image } from "#src/host-
16
18
  import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
17
19
  import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
18
20
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
21
+ import { conflicts } from "#src/knowledge/conflicts.ts";
19
22
  import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
20
- import { currentRevision, listComments, listModules, listOperands, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
23
+ import { identifiers } from "#src/knowledge/expression.ts";
24
+ import { currentRevision, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
21
25
  import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
22
26
  import { assemble } from "#src/native/acme.ts";
23
27
  import { analyze } from "#src/native/ghidra/analyze.ts";
24
28
  import { localToolContext } from "#src/native/local.ts";
25
29
  import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
26
- import { platformNames } from "#src/platform.ts";
30
+ import { describeAddress, platformNames } from "#src/platform.ts";
27
31
  import { resolveProjectPath } from "#src/project.ts";
28
32
  import { runScript, UsageError } from "#src/script.ts";
29
33
  import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
30
34
  import { OPCODES } from "./opcodes.ts";
31
- import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range, type Rendered } from "./source.ts";
35
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./source.ts";
32
36
 
33
37
  const USAGE = `generate.ts <image> --out <dir> [options]
34
38
 
35
39
  <image> an image.json of the c64-unpacker skill, or a PRG, relative to
36
- the project directory
40
+ the project folder
37
41
  --out <dir> the directory for the source files, relative to the project
38
42
  --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
39
43
  --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
@@ -49,8 +53,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
49
53
  The result is one JSON object. Addresses are $ and four hex digits.`;
50
54
 
51
55
  const MAX_LISTED = 20;
52
- /** Rounds of assembly that move operand expressions to numbers (D65). */
53
- const OPERAND_ROUNDS = 3;
56
+ /** Rounds of assembly that move operand expressions to numbers (D65) and macro uses to instructions (D73). */
57
+ const ROUNDS = 6;
54
58
  const PROBE_BASE = 0x1000;
55
59
  const PROBE_SLOT = 4;
56
60
  /** The copies of a full 64 KiB take about 140 frames. */
@@ -69,6 +73,9 @@ function readImage(path: string): Image {
69
73
  function readKnowledge(space: Space) {
70
74
  const db = openForRead();
71
75
  try {
76
+ // With conflicts from a git merge, the source would depend on which of two rows comes first (D69).
77
+ const found = conflicts(db).length;
78
+ if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was generated. Run c64-knowledge's knowledge.ts merge, then generate again.`);
72
79
  return {
73
80
  revision: currentRevision(db),
74
81
  symbols: listSymbols(db, undefined, undefined, undefined, space),
@@ -77,6 +84,9 @@ function readKnowledge(space: Space) {
77
84
  modules: listModules(db, undefined, undefined, space),
78
85
  references: listReferences(db, undefined, undefined, undefined, space),
79
86
  operands: listOperands(db, undefined, undefined, space),
87
+ patches: listPatches(db, undefined, undefined, undefined, space),
88
+ macros: listMacros(db, undefined, space),
89
+ macroUses: listMacroUses(db, undefined, undefined, undefined, space),
80
90
  };
81
91
  } finally {
82
92
  db?.close();
@@ -116,10 +126,14 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
116
126
  }
117
127
 
118
128
  /** Assembles files given as text with ACME; the program or the diagnostics. */
119
- async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
129
+ async function assembleFiles(
130
+ files: Array<{ name: string; text: string }>,
131
+ entry: string,
132
+ defines: Record<string, number> = {},
133
+ ): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
120
134
  const contents = files.map((file) => Buffer.from(file.text, "utf8"));
121
135
  const { result, attachments } = await assemble(
122
- { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
136
+ { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines },
123
137
  contents,
124
138
  localToolContext(),
125
139
  );
@@ -169,12 +183,88 @@ async function probe(laid: Layout): Promise<Set<string>> {
169
183
  throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
170
184
  }
171
185
 
186
+ /** The macro uses that an ACME error names: its line, or a line of the macro body in macros.asm, which every use of that macro shares. */
187
+ function usesInErrors(diagnostics: Diagnostic[], lineOf: ReadonlyMap<string, number>, uses: ReadonlyArray<{ start: number; macro: string }>, macroLines: Rendered["macroLines"], markLines: number): Map<number, string> {
188
+ const blamed = new Map<number, string>();
189
+ for (const diagnostic of diagnostics) {
190
+ if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
191
+ const start = lineOf.get(`${diagnostic.file}:${diagnostic.line}`);
192
+ if (start !== undefined) blamed.set(start, `ACME refuses it: ${diagnostic.message}`);
193
+ if (diagnostic.file !== "macros.asm") continue;
194
+ for (const [macro, lines] of macroLines) {
195
+ if (diagnostic.line < lines.first + markLines || diagnostic.line > lines.last + markLines) continue;
196
+ for (const use of uses) if (use.macro === macro) blamed.set(use.start, `ACME refuses the macro ${macro}: ${diagnostic.message}`);
197
+ }
198
+ }
199
+ return blamed;
200
+ }
201
+
202
+ /** The first address where two byte runs differ, as a reason. */
203
+ function firstDifference(start: number, built: (address: number) => number, original: (address: number) => number, end: number): string | undefined {
204
+ for (let address = start; address <= end; address++) {
205
+ if (built(address) !== original(address)) return `ACME assembles it to other bytes than the image: at ${formatC64Address(address)} it gives $${built(address).toString(16).padStart(2, "0")}, and the image has $${original(address).toString(16).padStart(2, "0")}.`;
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ /**
211
+ * Assembles each macro use alone at its own address, with the constants and
212
+ * the macros, and gives the uses that ACME refuses or assembles to other bytes
213
+ * than the image, with the reason (D73). A use of another length moves no
214
+ * other use, so each one is checked on its own, as the opcode probe does.
215
+ */
216
+ async function probeUses(laid: Layout, rendered: Rendered, image: Image): Promise<Map<number, string>> {
217
+ const blamed = new Map<number, string>();
218
+ const support = rendered.files.filter((file) => file.name === "constants.asm" || file.name === "macros.asm");
219
+ for (let round = 0; round < 3; round++) {
220
+ const uses = [...laid.macroUses.values()].filter((use) => !blamed.has(use.start));
221
+ if (uses.length === 0) return blamed;
222
+ // The labels of the image that the args name, with their values: the probe has no other source.
223
+ const labels = new Map<string, number>();
224
+ for (const [address, name] of laid.names) if (laid.inImage(address)) labels.set(name, address);
225
+ const used = [...new Set(uses.flatMap((use) => use.args.flatMap((arg) => identifiers(arg))))].filter((name) => labels.has(name)).sort();
226
+ const lines = ["!cpu 6510", '!source "constants.asm"', '!source "macros.asm"', ...used.map((name) => `${name} = ${labels.get(name)! < 0x100 ? `$${labels.get(name)!.toString(16).padStart(2, "0")}` : formatC64Address(labels.get(name)!)}`), `* = ${formatC64Address(PROBE_BASE)}`];
227
+ const lineOf = new Map<string, number>();
228
+ uses.forEach((use, index) => {
229
+ lines.push(`!pseudopc ${formatC64Address(use.start)} {`, ` +${use.macro}${use.args.length === 0 ? "" : ` ${use.args.join(", ")}`}`);
230
+ lineOf.set(`probe.asm:${lines.length}`, use.start);
231
+ lines.push(`probe_end_${index}`, "}");
232
+ });
233
+ const built = await assembleFiles([...support, { name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
234
+ if (built.program === undefined) {
235
+ const refused = usesInErrors(built.diagnostics, lineOf, uses, rendered.macroLines, 0);
236
+ if (refused.size === 0) throw new WireFailure("operation-failed", `ACME refused the macro probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
237
+ for (const [start, reason] of refused) blamed.set(start, reason);
238
+ continue;
239
+ }
240
+ const ends = new Map((built.symbols ?? []).map((symbol) => [symbol.name, symbol.kind === "address" ? symbol.address : symbol.value]));
241
+ let offset = 2 + PROBE_BASE - built.start!;
242
+ uses.forEach((use, index) => {
243
+ const length = ends.get(`probe_end_${index}`)! - use.start;
244
+ const wanted = use.end - use.start + 1;
245
+ const program = built.program!;
246
+ const at = offset;
247
+ offset += length;
248
+ if (length !== wanted) {
249
+ blamed.set(use.start, `ACME assembles it to ${length} bytes, and the range has ${wanted}.`);
250
+ return;
251
+ }
252
+ const reason = firstDifference(use.start, (address) => program[at + address - use.start]!, image.byteAt, use.end);
253
+ if (reason !== undefined) blamed.set(use.start, reason);
254
+ });
255
+ return blamed;
256
+ }
257
+ throw new WireFailure("operation-failed", "ACME refused the macro probe three times.");
258
+ }
259
+
172
260
  /**
173
- * The operand expressions that made the assembly fail or that ACME assembled
174
- * to other bytes than the image, with the reason (D65). `markLines` is the
175
- * number of lines added before the rendered text of each file.
261
+ * The operand expressions (D65) and macro uses (D73) that made the assembly
262
+ * fail or that ACME assembled to other bytes than the image, with the
263
+ * reason. `markLines` is the number of lines added before the rendered text of
264
+ * each file. A use of another length moves every byte after it: then only the
265
+ * first use that differs is blamed, and no operand.
176
266
  */
177
- function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): Map<number, string> {
267
+ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): { numbers: Map<number, string>; uses: Map<number, string> } {
178
268
  const blamed = new Map<number, string>();
179
269
  if (built.program === undefined) {
180
270
  const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
@@ -183,9 +273,20 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
183
273
  const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
184
274
  if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
185
275
  }
186
- return blamed;
276
+ const useLines = new Map(rendered.uses.map((use) => [`${use.file}:${use.line + markLines}`, use.start]));
277
+ return { numbers: blamed, uses: usesInErrors(built.diagnostics, useLines, rendered.uses, rendered.macroLines, markLines) };
187
278
  }
188
279
  const end = built.start! + built.program.length - 3;
280
+ const at = (address: number) => (address >= built.start! && address <= end ? built.program![2 + address - built.start!]! : -1);
281
+ const uses = new Map<number, string>();
282
+ const imageEnd = Math.max(...image.segments.map((segment) => segment.end));
283
+ for (const use of rendered.uses) {
284
+ const reason = firstDifference(use.start, at, image.byteAt, use.end);
285
+ if (reason === undefined) continue;
286
+ uses.set(use.start, reason);
287
+ if (end !== imageEnd) break;
288
+ }
289
+ if (uses.size > 0) return { numbers: blamed, uses };
189
290
  const word = (at: (address: number) => number, item: Rendered["expressions"][number]) => (item.length === 3 ? at(item.address + 1) | (at(item.address + 2) << 8) : at(item.address + 1));
190
291
  const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
191
292
  for (const item of rendered.expressions) {
@@ -194,7 +295,7 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
194
295
  const original = word(image.byteAt, item);
195
296
  if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
196
297
  }
197
- return blamed;
298
+ return { numbers: blamed, uses };
198
299
  }
199
300
 
200
301
  interface SegmentCheck {
@@ -224,6 +325,57 @@ function check(program: Buffer, start: number, image: Image): { identical: boole
224
325
  return { identical: segments.every((segment) => segment.result === "identical"), segments };
225
326
  }
226
327
 
328
+ /** What a build with one switch on gives (D72). */
329
+ interface SwitchCheck {
330
+ name: string;
331
+ define: string;
332
+ patches: Array<{ start: string; end: string; bytes: number; insideNames?: string[] }>;
333
+ result: "fits" | "overflow" | "changes outside" | "does not assemble";
334
+ changedBytes?: number;
335
+ outside?: { count: number; first: { address: string; built: number; default: number } };
336
+ overflow?: Array<{ start: string; message: string }>;
337
+ diagnostics?: Diagnostic[];
338
+ }
339
+
340
+ /**
341
+ * Builds the source with one switch on and compares it with the default
342
+ * build: only the ranges of its patches may change, and the program keeps its
343
+ * start and length. A patch longer than its range stops ACME with !error.
344
+ */
345
+ async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ name: string; text: string }>, base: { program: Buffer; start: number }, names: Layout["names"]): Promise<SwitchCheck> {
346
+ const shown: SwitchCheck = {
347
+ name,
348
+ define: `--define ${name}=1`,
349
+ patches: patches.map((patch) => {
350
+ const inside = patch.inner.map((address) => names.get(address)!);
351
+ return { start: formatC64Address(patch.start), end: formatC64Address(patch.end), bytes: patch.end - patch.start + 1, ...(inside.length === 0 ? {} : { insideNames: inside }) };
352
+ }),
353
+ result: "fits",
354
+ };
355
+ const built = await assembleFiles(files, "main.asm", { [name]: 1 });
356
+ if (built.program === undefined) {
357
+ const overflow = built.diagnostics.flatMap((diagnostic) => {
358
+ const found = /The patch \S+ at (\$[0-9a-f]{4}) has .*/.exec(diagnostic.message);
359
+ return found === null ? [] : [{ start: found[1]!, message: found[0] }];
360
+ });
361
+ if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
362
+ return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_LISTED) };
363
+ }
364
+ const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
365
+ let changed = 0;
366
+ let outside: SwitchCheck["outside"];
367
+ const end = Math.max(base.start + base.program.length, built.start! + built.program.length) - 2;
368
+ for (let address = Math.min(base.start, built.start!); address < end; address++) {
369
+ const at = (program: Buffer, start: number) => (address >= start && address < start + program.length - 2 ? program[2 + address - start]! : -1);
370
+ const now = at(built.program, built.start!);
371
+ const before = at(base.program, base.start);
372
+ if (now === before) continue;
373
+ if (inside(address)) changed++;
374
+ else outside = { count: (outside?.count ?? 0) + 1, first: outside?.first ?? { address: formatC64Address(address), built: now, default: before } };
375
+ }
376
+ return outside === undefined ? { ...shown, changedBytes: changed } : { ...shown, result: "changes outside", changedBytes: changed, outside };
377
+ }
378
+
227
379
  /** Reads RAM in parts. */
228
380
  async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
229
381
  const parts: Buffer[] = [];
@@ -332,7 +484,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
332
484
  try {
333
485
  resolveProjectPath(values.out);
334
486
  } catch {
335
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
487
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
336
488
  }
337
489
  const entries = (values.entry ?? []).map((value) => {
338
490
  try {
@@ -364,9 +516,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
364
516
  modules: knowledge.modules,
365
517
  referenceTargets: knowledge.references.map((reference) => reference.to),
366
518
  operands: knowledge.operands,
519
+ patches: knowledge.patches,
520
+ macros: knowledge.macros,
521
+ macroUses: knowledge.macroUses,
367
522
  ...(coverage === undefined ? {} : { coverage }),
368
523
  // The platform names are C64 names: drive code and banks get none.
369
- ...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
524
+ ...(space === DEFAULT_SPACE ? { platform: platformNames(), describe: describeAddress } : {}),
370
525
  });
371
526
  const asBytes = await probe(laid);
372
527
  const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
@@ -379,7 +534,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
379
534
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
380
535
  try {
381
536
  const entryName = laid.names.get(image.entry!);
382
- loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
537
+ loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), macros: laid.macros.length > 0, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
383
538
  } catch (error) {
384
539
  if (!(error instanceof WireFailure)) throw error;
385
540
  loaderRefused = error.message;
@@ -388,16 +543,21 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
388
543
  // Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
389
544
  const marked = (list: Array<{ name: string; text: string }>) => list.map((file) => ({ ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` }));
390
545
  const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
391
- // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and the source is assembled again (D65).
546
+ // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and a macro use as its
547
+ // instructions; then the source is assembled again (D65, D73).
392
548
  const numbers = new Map<number, string>();
393
- let rendered = render(laid, asBytes, header, numbers);
549
+ const asInstructions = new Map<number, string>();
550
+ let rendered = render(laid, asBytes, header, numbers, asInstructions);
551
+ if (laid.macroUses.size > 0) for (const [start, reason] of await probeUses(laid, rendered, image)) asInstructions.set(start, reason);
552
+ if (asInstructions.size > 0) rendered = render(laid, asBytes, header, numbers, asInstructions);
394
553
  let files = marked([...rendered.files, ...loaderFiles]);
395
554
  let built = await assembleFiles(files, "main.asm");
396
- for (let round = 0; round < OPERAND_ROUNDS && rendered.expressions.length > 0; round++) {
555
+ for (let round = 0; round < ROUNDS && (rendered.expressions.length > 0 || rendered.uses.length > 0); round++) {
397
556
  const blamed = blame(rendered, built, image, 1);
398
- if (blamed.size === 0) break;
399
- for (const [address, reason] of blamed) numbers.set(address, reason);
400
- rendered = render(laid, asBytes, header, numbers);
557
+ if (blamed.numbers.size === 0 && blamed.uses.size === 0) break;
558
+ for (const [address, reason] of blamed.numbers) numbers.set(address, reason);
559
+ for (const [start, reason] of blamed.uses) asInstructions.set(start, reason);
560
+ rendered = render(laid, asBytes, header, numbers, asInstructions);
401
561
  files = marked([...rendered.files, ...loaderFiles]);
402
562
  built = await assembleFiles(files, "main.asm");
403
563
  }
@@ -446,6 +606,25 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
446
606
  listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
447
607
  },
448
608
  }),
609
+ ...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
610
+ // A macro use that the source writes as its instructions does not fail the run: the source stays identical (D73).
611
+ ...(rendered.macrosAsInstructions.length === 0
612
+ ? {}
613
+ : {
614
+ macrosAsInstructions: {
615
+ count: rendered.macrosAsInstructions.length,
616
+ listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
617
+ },
618
+ }),
619
+ // A patch that the source cannot write fails the run: the switch would miss a part (D72).
620
+ ...(laid.patchesNotWritten.length === 0
621
+ ? {}
622
+ : {
623
+ patchesNotWritten: {
624
+ count: laid.patchesNotWritten.length,
625
+ listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
626
+ },
627
+ }),
449
628
  };
450
629
  // The loader includes the source, so it is built only from a source that is identical to the image.
451
630
  const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
@@ -455,8 +634,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
455
634
  const checked = check(built.program, built.start!, image);
456
635
  output.check = checked;
457
636
  if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
637
+ // Each switch on alone, against the default build (D72); combinations are not built.
638
+ const switches: SwitchCheck[] = [];
639
+ for (const [name, patches] of switchesOf(laid)) switches.push(await checkSwitch(name, patches, files, { program: built.program, start: built.start! }, laid.names));
640
+ if (switches.length > 0) output.switches = switches;
641
+ const sourceFailed = laid.patchesNotWritten.length > 0 || switches.some((item) => item.result !== "fits");
458
642
  if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
459
- if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
643
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
460
644
 
461
645
  const loaderBuilt = await assembleFiles(files, "loader.asm");
462
646
  if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
@@ -490,12 +674,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
490
674
  // What the loader cannot set as it was at the entry (D63).
491
675
  ...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
492
676
  };
493
- return { output, failed: !ran.verified };
677
+ return { output, failed: !ran.verified || sourceFailed };
494
678
  }
495
679
 
496
680
  await runScript(
497
- async () => {
498
- const { output, failed } = await run(process.argv.slice(2));
681
+ async (argv) => {
682
+ const { output, failed } = await run(argv);
499
683
  if (failed) process.exitCode = 1;
500
684
  return output;
501
685
  },
@@ -357,6 +357,8 @@ export interface LoaderInput {
357
357
  /** The label of the entry in the source: the loader jumps to it, so the jump follows the code when it moves (D66). */
358
358
  entryName?: string;
359
359
  header: string[];
360
+ /** The source has macros.asm (D73): the loader includes it after the constants. */
361
+ macros?: boolean;
360
362
  }
361
363
 
362
364
  /** The label of a block (D66): its first byte and the byte after it in the loader file, and its first address where it runs. */
@@ -419,7 +421,7 @@ export function planLoader(input: LoaderInput): LoaderPlan {
419
421
  const copyBlocks = order.map((index) => blockOf[index]!);
420
422
 
421
423
  const stackByte = 0x0100 + state.sp;
422
- const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', "", `* = ${hex4(LOAD)}`];
424
+ const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(input.macros ? ['!source "macros.asm"'] : []), "", `* = ${hex4(LOAD)}`];
423
425
  lines.push(` !byte $0b,$08,$0a,$00,$9e,${[...String(STUB)].map((digit) => hex2(digit.charCodeAt(0))).join(",")},$00,$00,$00 ; 10 SYS ${STUB}`);
424
426
  lines.push(
425
427
  "!zone loader_stub {",
@@ -59,3 +59,22 @@ export function takesAddress(mode: Mode): boolean {
59
59
  export function isZeroPageMode(mode: Mode): boolean {
60
60
  return mode === "zp" || mode === "zpx" || mode === "zpy" || mode === "izx" || mode === "izy";
61
61
  }
62
+
63
+ /** How an instruction uses the address of its operand, in the order a summary lists them. */
64
+ export const USE_KINDS = ["read", "written", "modified", "called", "jumped to", "jumped through", "branched to", "pointer", "address", "table", "macro argument"] as const;
65
+ export type UseKind = (typeof USE_KINDS)[number];
66
+
67
+ const WRITES = new Set(["sta", "stx", "sty", "sax", "sha", "shx", "shy", "tas"]);
68
+ const MODIFIES = new Set(["inc", "dec", "asl", "lsr", "rol", "ror", "slo", "rla", "sre", "rra", "dcp", "isc"]);
69
+
70
+ /** The use of the operand address of an instruction that takes an address. Through (zp),y and (zp,x) the address is a pointer. */
71
+ export function useKind(opcode: number): UseKind {
72
+ const { mnemonic, mode } = OPCODES[opcode]!;
73
+ if (mode === "izx" || mode === "izy") return "pointer";
74
+ if (mode === "rel") return "branched to";
75
+ if (mnemonic === "jsr") return "called";
76
+ if (mnemonic === "jmp") return mode === "ind" ? "jumped through" : "jumped to";
77
+ if (WRITES.has(mnemonic)) return "written";
78
+ if (MODIFIES.has(mnemonic)) return "modified";
79
+ return "read";
80
+ }