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

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 (117) 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 +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +18 -4
  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 +1 -1
  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/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/skills/c64-assembler/scripts/compare.ts +0 -84
  117. package/skills/c64-source/scripts/source.ts +0 -595
@@ -1,136 +1,136 @@
1
- // The c64-source script (D48): generates ACME source from an image, current
2
- // knowledge and Ghidra's instructions, which it runs itself, and checks with
3
- // ACME that the source assembles to the image byte by byte. With --loader it
1
+ // The c64-source script: generates ACME source from the program and the
2
+ // knowledge and Ghidra's instructions, which it runs itself, and checks
3
+ // with ACME that the source assembles to the program 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 is built on
6
+ // alone and checked. A macro use 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
- import { join, posix } from "node:path";
11
+ import { join } from "node:path";
10
12
  import { parseArgs } from "node:util";
11
13
 
12
- import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
14
+ import { COVERAGE, coverageSeedKinds, formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
13
15
  import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
14
- import { readCoverageFiles } from "#src/host-client/coverage.ts";
15
16
  import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
16
- import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
17
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
17
18
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
19
+ import { conflicts } from "#src/knowledge/conflicts.ts";
18
20
  import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
19
- import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
21
+ import { identifiers } from "#src/knowledge/expression.ts";
22
+ import { analyzedBytes, programCoverage, requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
23
+ import { ANALYZER_NAME_ORDER, currentRevision, listAnalyzerNames, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols, type SymbolRow } from "#src/knowledge/read.ts";
20
24
  import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
21
25
  import { assemble } from "#src/native/acme.ts";
22
26
  import { analyze } from "#src/native/ghidra/analyze.ts";
23
27
  import { localToolContext } from "#src/native/local.ts";
24
28
  import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
25
- import { platformNames } from "#src/platform.ts";
29
+ import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
26
30
  import { resolveProjectPath } from "#src/project.ts";
27
31
  import { runScript, UsageError } from "#src/script.ts";
28
- import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
29
- import { OPCODES } from "./opcodes.ts";
30
- import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
32
+ import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, readEntryState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
33
+ import { OPCODES } from "#src/opcodes.ts";
34
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./lib/source.ts";
31
35
 
32
- const USAGE = `generate.ts <image> --out <dir> [options]
36
+ const USAGE = `generate.ts --out <dir> [options]
37
+
38
+ The program, its entry state and the coverage come from the knowledge:
39
+ put them in with knowledge.ts image and coverage of the c64-knowledge skill.
40
+ Code that no imported run executed is marked and listed; <dir>/not_executed.json
41
+ has all of its ranges.
33
42
 
34
- <image> an image.json of the c64-unpacker skill, or a PRG, relative to
35
- the project directory
36
43
  --out <dir> the directory for the source files, relative to the project
37
44
  --entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
38
- --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
39
- that no run executed is marked and listed; <dir>/not_executed.json
40
- has all of its ranges
41
- --space <space> the knowledge space of the image: c64 (the default), drive8, or
42
- bank0 to bank255; --coverage and --loader are for c64 only
45
+ --space <space> the knowledge space of the program: c64 (the default), drive8,
46
+ or bank0 to bank255; the coverage and --loader are for c64 only
43
47
  --loader also write loader.asm and loader.prg: the loader copies the
44
- segments into place, sets the entry state from entry_state.json
45
- next to image.json, and jumps to the entry; it is checked in
46
- the emulator
48
+ segments into place, sets the entry state of the knowledge,
49
+ and jumps to the entry; it is checked in the emulator
50
+ --analyzer-names name an address that has no name of the user or the LLM, and
51
+ no platform name, with its analyzer name
47
52
 
48
53
  The result is one JSON object. Addresses are $ and four hex digits.`;
49
54
 
50
55
  const MAX_LISTED = 20;
56
+ /** Rounds of assembly that move operand expressions to numbers and macro uses to instructions. */
57
+ const ROUNDS = 6;
51
58
  const PROBE_BASE = 0x1000;
52
59
  const PROBE_SLOT = 4;
53
60
  /** The copies of a full 64 KiB take about 140 frames. */
54
61
  const LOADER_FRAMES = 1500;
55
62
 
56
- interface Image {
57
- path: string;
58
- segments: Range[];
59
- byteAt: (address: number) => number;
60
- /** The bytes that Ghidra analyzes, and how. */
61
- analyzed: { bytes: Buffer; kind: "prg" | "flat64k" };
62
- entry?: number;
63
- /** entry_state.json next to image.json, when it exists. */
64
- statePath?: string;
65
- }
66
-
67
- function readImage(path: string): Image {
68
- const { bytes } = readProjectFile(path);
69
- if (!path.endsWith(".json")) {
70
- let range: ReturnType<typeof prgRange>;
71
- try {
72
- range = prgRange(bytes);
73
- } catch (error) {
74
- throw new UsageError(`${path}: ${(error as Error).message}`);
75
- }
76
- return { path, segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]!, analyzed: { bytes, kind: "prg" } };
77
- }
78
- const fail = (what: string): never => {
79
- throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
80
- };
81
- let manifest: Record<string, unknown>;
82
- try {
83
- manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
84
- } catch (error) {
85
- return fail((error as Error).message);
86
- }
87
- const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
88
- if (typeof manifest.image !== "string") fail("it has no image");
89
- if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
90
- const segments = (manifest.segments as Array<Record<string, unknown>>)
91
- .map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
92
- .sort((a, b) => a.start - b.start);
93
- segments.forEach((segment, index) => {
94
- if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
95
- if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
96
- });
97
- const directory = posix.dirname(path);
98
- const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
99
- if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
100
- const image: Image = { path, segments, byteAt: (at) => memory[at]!, analyzed: { bytes: memory, kind: "flat64k" }, statePath: posix.join(directory, "entry_state.json") };
101
- if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
102
- return image;
103
- }
104
-
105
- /** The knowledge of one space that the source uses, read once. */
106
- function readKnowledge(space: Space) {
63
+ /** The knowledge of one space that the source uses, read once. With `analyzerNames`, an address without a name takes its first analyzer name. */
64
+ function readKnowledge(space: Space, analyzerNames = false) {
107
65
  const db = openForRead();
108
66
  try {
67
+ // With conflicts from a git merge, the source would depend on which of two rows comes first.
68
+ const found = conflicts(db).length;
69
+ 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.`);
70
+ const symbols = listSymbols(db, undefined, undefined, undefined, space);
71
+ const second = listAnalyzerNames(db, undefined, undefined, undefined, space);
109
72
  return {
110
73
  revision: currentRevision(db),
111
- symbols: listSymbols(db, undefined, undefined, undefined, space),
74
+ program: requireProgram(db, space),
75
+ // The coverage is of the C64: drive code and banks have none.
76
+ coverage: space === DEFAULT_SPACE ? programCoverage(db)?.map : undefined,
77
+ symbols: analyzerNames ? withAnalyzerNames(symbols, second) : symbols,
78
+ // Analyzer routines are entry points for the decode, also without --analyzer-names.
79
+ routines: [...symbols, ...second].filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address),
112
80
  regions: listRegions(db, undefined, undefined, undefined, space),
113
81
  comments: listComments(db, undefined, undefined, space),
114
82
  modules: listModules(db, undefined, undefined, space),
115
83
  references: listReferences(db, undefined, undefined, undefined, space),
84
+ operands: listOperands(db, undefined, undefined, space),
85
+ patches: listPatches(db, undefined, undefined, undefined, space),
86
+ macros: listMacros(db, undefined, space),
87
+ macroUses: listMacroUses(db, undefined, undefined, undefined, space),
116
88
  };
117
89
  } finally {
118
90
  db?.close();
119
91
  }
120
92
  }
121
93
 
122
- /** Ghidra's instruction starts and lengths inside the image, seeded from the entry and from knowledge of any origin. */
123
- async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>, entries: number[]): Promise<Map<number, number>> {
94
+ /**
95
+ * The names of the user and the LLM, and for each address without one the
96
+ * first analyzer name in the order heuristic, Ghidra, DXA. An analyzer
97
+ * name that another address has already is left out. The analyzer names keep
98
+ * their origin, so a platform name still wins over them.
99
+ */
100
+ function withAnalyzerNames(symbols: SymbolRow[], second: SymbolRow[]): SymbolRow[] {
101
+ const named = new Set(symbols.map((symbol) => symbol.address));
102
+ const taken = new Set(symbols.map((symbol) => symbol.name));
103
+ const added: SymbolRow[] = [];
104
+ for (const origin of ANALYZER_NAME_ORDER) {
105
+ for (const row of second.filter((candidate) => candidate.origin === origin)) {
106
+ if (named.has(row.address) || taken.has(row.name)) continue;
107
+ named.add(row.address);
108
+ taken.add(row.name);
109
+ added.push(row);
110
+ }
111
+ }
112
+ return [...symbols, ...added].sort((a, b) => a.address - b.address);
113
+ }
114
+
115
+ /**
116
+ * Ghidra's instruction starts and lengths inside the image, seeded from the
117
+ * entry, from knowledge of any origin, and from the coverage: a run's flow
118
+ * starts, also after a JSR, so code that an analyzer does not reach by itself
119
+ * is in the source.
120
+ */
121
+ async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnowledge>, entries: number[], coverage: Uint8Array | undefined): Promise<Map<number, number>> {
124
122
  const inImage = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
125
123
  const seeds = new Set<number>([...(image.entry === undefined ? [] : [image.entry]), ...entries]);
126
- for (const symbol of knowledge.symbols) if (symbol.kind === "routine") seeds.add(symbol.address);
124
+ for (const address of knowledge.routines) seeds.add(address);
125
+ const flows = coverage === undefined ? { routines: [], codeStarts: [] } : coverageSeedKinds(coverage, (address) => (image.segments.some((segment) => address >= segment.start && address <= segment.end) ? image.byteAt(address) : undefined));
126
+ for (const address of flows.routines) seeds.add(address);
127
127
  for (const region of knowledge.regions) {
128
128
  if (CODE_REGION_TYPES.includes(region.type)) seeds.add(region.start);
129
129
  if (region.type === "jumps") for (let at = region.start; at + 2 <= region.end; at += 3) seeds.add(at);
130
130
  }
131
131
  const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
132
132
  if (entryPoints.length === 0) {
133
- // An entry in no segment is a capture fault: the image is data only, and the result says entryOutside (R3-01).
133
+ // An entry in no segment is a capture fault: the image is data only, and the result says entryOutside.
134
134
  if (image.entry !== undefined && !inImage(image.entry)) return new Map();
135
135
  throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
136
136
  }
@@ -138,8 +138,15 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
138
138
  .filter((region) => !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
139
139
  .flatMap((region) => image.segments.filter((segment) => region.start <= segment.end && segment.start <= region.end).map((segment) => ({ start: Math.max(region.start, segment.start), end: Math.min(region.end, segment.end) })));
140
140
  const { result } = await analyze(
141
- { imageKind: image.analyzed.kind, entryPoints: entryPoints.slice(0, GHIDRA_LIMITS.entryPoints), dataRanges: dataRanges.slice(0, GHIDRA_LIMITS.dataRanges), labels: [], decompile: [] },
142
- image.analyzed.bytes,
141
+ {
142
+ imageKind: analyzedBytes(image).kind,
143
+ entryPoints: entryPoints.slice(0, GHIDRA_LIMITS.entryPoints),
144
+ codeStarts: flows.codeStarts.filter((address) => !seeds.has(address)).slice(0, GHIDRA_LIMITS.codeStarts),
145
+ dataRanges: dataRanges.slice(0, GHIDRA_LIMITS.dataRanges),
146
+ labels: [],
147
+ decompile: [],
148
+ },
149
+ analyzedBytes(image).bytes,
143
150
  localToolContext(),
144
151
  );
145
152
  const instructions = new Map<number, number>();
@@ -152,10 +159,14 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
152
159
  }
153
160
 
154
161
  /** Assembles files given as text with ACME; the program or the diagnostics. */
155
- async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
162
+ async function assembleFiles(
163
+ files: Array<{ name: string; text: string }>,
164
+ entry: string,
165
+ defines: Record<string, number> = {},
166
+ ): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
156
167
  const contents = files.map((file) => Buffer.from(file.text, "utf8"));
157
168
  const { result, attachments } = await assemble(
158
- { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
169
+ { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines },
159
170
  contents,
160
171
  localToolContext(),
161
172
  );
@@ -205,6 +216,121 @@ async function probe(laid: Layout): Promise<Set<string>> {
205
216
  throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
206
217
  }
207
218
 
219
+ /** 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. */
220
+ function usesInErrors(diagnostics: Diagnostic[], lineOf: ReadonlyMap<string, number>, uses: ReadonlyArray<{ start: number; macro: string }>, macroLines: Rendered["macroLines"], markLines: number): Map<number, string> {
221
+ const blamed = new Map<number, string>();
222
+ for (const diagnostic of diagnostics) {
223
+ if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
224
+ const start = lineOf.get(`${diagnostic.file}:${diagnostic.line}`);
225
+ if (start !== undefined) blamed.set(start, `ACME refuses it: ${diagnostic.message}`);
226
+ if (diagnostic.file !== "macros.asm") continue;
227
+ for (const [macro, lines] of macroLines) {
228
+ if (diagnostic.line < lines.first + markLines || diagnostic.line > lines.last + markLines) continue;
229
+ for (const use of uses) if (use.macro === macro) blamed.set(use.start, `ACME refuses the macro ${macro}: ${diagnostic.message}`);
230
+ }
231
+ }
232
+ return blamed;
233
+ }
234
+
235
+ /** The first address where two byte runs differ, as a reason. */
236
+ function firstDifference(start: number, built: (address: number) => number, original: (address: number) => number, end: number): string | undefined {
237
+ for (let address = start; address <= end; address++) {
238
+ 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")}.`;
239
+ }
240
+ return undefined;
241
+ }
242
+
243
+ /**
244
+ * Assembles each macro use alone at its own address, with the constants and
245
+ * the macros, and gives the uses that ACME refuses or assembles to other bytes
246
+ * than the image, with the reason. A use of another length moves no
247
+ * other use, so each one is checked on its own, as the opcode probe does.
248
+ */
249
+ async function probeUses(laid: Layout, rendered: Rendered, image: ProgramImage): Promise<Map<number, string>> {
250
+ const blamed = new Map<number, string>();
251
+ const support = rendered.files.filter((file) => file.name === "constants.asm" || file.name === "macros.asm");
252
+ for (let round = 0; round < 3; round++) {
253
+ const uses = [...laid.macroUses.values()].filter((use) => !blamed.has(use.start));
254
+ if (uses.length === 0) return blamed;
255
+ // The labels of the image that the args name, with their values: the probe has no other source.
256
+ const labels = new Map<string, number>();
257
+ for (const [address, name] of laid.names) if (laid.inImage(address)) labels.set(name, address);
258
+ const used = [...new Set(uses.flatMap((use) => use.args.flatMap((arg) => identifiers(arg))))].filter((name) => labels.has(name)).sort();
259
+ 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)}`];
260
+ const lineOf = new Map<string, number>();
261
+ uses.forEach((use, index) => {
262
+ lines.push(`!pseudopc ${formatC64Address(use.start)} {`, ` +${use.macro}${use.args.length === 0 ? "" : ` ${use.args.join(", ")}`}`);
263
+ lineOf.set(`probe.asm:${lines.length}`, use.start);
264
+ lines.push(`probe_end_${index}`, "}");
265
+ });
266
+ const built = await assembleFiles([...support, { name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
267
+ if (built.program === undefined) {
268
+ const refused = usesInErrors(built.diagnostics, lineOf, uses, rendered.macroLines, 0);
269
+ if (refused.size === 0) throw new WireFailure("operation-failed", `ACME refused the macro probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
270
+ for (const [start, reason] of refused) blamed.set(start, reason);
271
+ continue;
272
+ }
273
+ const ends = new Map((built.symbols ?? []).map((symbol) => [symbol.name, symbol.kind === "address" ? symbol.address : symbol.value]));
274
+ let offset = 2 + PROBE_BASE - built.start!;
275
+ uses.forEach((use, index) => {
276
+ const length = ends.get(`probe_end_${index}`)! - use.start;
277
+ const wanted = use.end - use.start + 1;
278
+ const program = built.program!;
279
+ const at = offset;
280
+ offset += length;
281
+ if (length !== wanted) {
282
+ blamed.set(use.start, `ACME assembles it to ${length} bytes, and the range has ${wanted}.`);
283
+ return;
284
+ }
285
+ const reason = firstDifference(use.start, (address) => program[at + address - use.start]!, image.byteAt, use.end);
286
+ if (reason !== undefined) blamed.set(use.start, reason);
287
+ });
288
+ return blamed;
289
+ }
290
+ throw new WireFailure("operation-failed", "ACME refused the macro probe three times.");
291
+ }
292
+
293
+ /**
294
+ * The operand expressions and macro uses that made the assembly
295
+ * fail or that ACME assembled to other bytes than the image, with the
296
+ * reason. `markLines` is the number of lines added before the rendered text of
297
+ * each file. A use of another length moves every byte after it: then only the
298
+ * first use that differs is blamed, and no operand.
299
+ */
300
+ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: ProgramImage, markLines: number): { numbers: Map<number, string>; uses: Map<number, string> } {
301
+ const blamed = new Map<number, string>();
302
+ if (built.program === undefined) {
303
+ const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
304
+ for (const diagnostic of built.diagnostics) {
305
+ if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
306
+ const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
307
+ if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
308
+ }
309
+ const useLines = new Map(rendered.uses.map((use) => [`${use.file}:${use.line + markLines}`, use.start]));
310
+ return { numbers: blamed, uses: usesInErrors(built.diagnostics, useLines, rendered.uses, rendered.macroLines, markLines) };
311
+ }
312
+ const end = built.start! + built.program.length - 3;
313
+ const at = (address: number) => (address >= built.start! && address <= end ? built.program![2 + address - built.start!]! : -1);
314
+ const uses = new Map<number, string>();
315
+ const imageEnd = Math.max(...image.segments.map((segment) => segment.end));
316
+ for (const use of rendered.uses) {
317
+ const reason = firstDifference(use.start, at, image.byteAt, use.end);
318
+ if (reason === undefined) continue;
319
+ uses.set(use.start, reason);
320
+ if (end !== imageEnd) break;
321
+ }
322
+ if (uses.size > 0) return { numbers: blamed, uses };
323
+ 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));
324
+ const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
325
+ for (const item of rendered.expressions) {
326
+ if (item.address + item.length - 1 > end || item.address < built.start!) continue;
327
+ const assembled = word((address) => built.program![2 + address - built.start!]!, item);
328
+ const original = word(image.byteAt, item);
329
+ if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
330
+ }
331
+ return { numbers: blamed, uses };
332
+ }
333
+
208
334
  interface SegmentCheck {
209
335
  start: string;
210
336
  end: string;
@@ -214,7 +340,7 @@ interface SegmentCheck {
214
340
  }
215
341
 
216
342
  /** Compares the assembled program with the image, segment by segment. */
217
- function check(program: Buffer, start: number, image: Image): { identical: boolean; segments: SegmentCheck[] } {
343
+ function check(program: Buffer, start: number, image: ProgramImage): { identical: boolean; segments: SegmentCheck[] } {
218
344
  const end = start + program.length - 3;
219
345
  const segments = image.segments.map((segment): SegmentCheck => {
220
346
  const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
@@ -232,6 +358,57 @@ function check(program: Buffer, start: number, image: Image): { identical: boole
232
358
  return { identical: segments.every((segment) => segment.result === "identical"), segments };
233
359
  }
234
360
 
361
+ /** What a build with one switch on gives. */
362
+ interface SwitchCheck {
363
+ name: string;
364
+ define: string;
365
+ patches: Array<{ start: string; end: string; bytes: number; insideNames?: string[] }>;
366
+ result: "fits" | "overflow" | "changes outside" | "does not assemble";
367
+ changedBytes?: number;
368
+ outside?: { count: number; first: { address: string; built: number; default: number } };
369
+ overflow?: Array<{ start: string; message: string }>;
370
+ diagnostics?: Diagnostic[];
371
+ }
372
+
373
+ /**
374
+ * Builds the source with one switch on and compares it with the default
375
+ * build: only the ranges of its patches may change, and the program keeps its
376
+ * start and length. A patch longer than its range stops ACME with !error.
377
+ */
378
+ async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ name: string; text: string }>, base: { program: Buffer; start: number }, names: Layout["names"]): Promise<SwitchCheck> {
379
+ const shown: SwitchCheck = {
380
+ name,
381
+ define: `--define ${name}=1`,
382
+ patches: patches.map((patch) => {
383
+ const inside = patch.inner.map((address) => names.get(address)!);
384
+ return { start: formatC64Address(patch.start), end: formatC64Address(patch.end), bytes: patch.end - patch.start + 1, ...(inside.length === 0 ? {} : { insideNames: inside }) };
385
+ }),
386
+ result: "fits",
387
+ };
388
+ const built = await assembleFiles(files, "main.asm", { [name]: 1 });
389
+ if (built.program === undefined) {
390
+ const overflow = built.diagnostics.flatMap((diagnostic) => {
391
+ const found = /The patch \S+ at (\$[0-9a-f]{4}) has .*/.exec(diagnostic.message);
392
+ return found === null ? [] : [{ start: found[1]!, message: found[0] }];
393
+ });
394
+ if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
395
+ return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_LISTED) };
396
+ }
397
+ const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
398
+ let changed = 0;
399
+ let outside: SwitchCheck["outside"];
400
+ const end = Math.max(base.start + base.program.length, built.start! + built.program.length) - 2;
401
+ for (let address = Math.min(base.start, built.start!); address < end; address++) {
402
+ const at = (program: Buffer, start: number) => (address >= start && address < start + program.length - 2 ? program[2 + address - start]! : -1);
403
+ const now = at(built.program, built.start!);
404
+ const before = at(base.program, base.start);
405
+ if (now === before) continue;
406
+ if (inside(address)) changed++;
407
+ else outside = { count: (outside?.count ?? 0) + 1, first: outside?.first ?? { address: formatC64Address(address), built: now, default: before } };
408
+ }
409
+ return outside === undefined ? { ...shown, changedBytes: changed } : { ...shown, result: "changes outside", changedBytes: changed, outside };
410
+ }
411
+
235
412
  /** Reads RAM in parts. */
236
413
  async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
237
414
  const parts: Buffer[] = [];
@@ -246,18 +423,37 @@ async function readRam(client: ViceSessionClient, start: number, size: number):
246
423
  * Runs the loader in the emulator to its jump to the entry and compares the
247
424
  * machine with the entry state there. At the jump, every byte and register is
248
425
  * set and no interrupt has run since the PLP: the 6502 takes one only after
249
- * the JMP, as the original program can take one right at its entry (F-32).
426
+ * the JMP, as the original program can take one right at its entry.
250
427
  */
251
- async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: Image, state: EntryState, jump: number): Promise<{ verified: boolean; differences: string[] }> {
428
+ /** What the check of the loader gives. `stop`: why the loader did not get to its copier or its jump. */
429
+ interface LoaderRun {
430
+ verified: boolean;
431
+ differences: string[];
432
+ stop?: { pc: string; port: string; cause?: string; history: string[] };
433
+ }
434
+
435
+ /** The history entries that a stop of the loader gives, so the agent sees the last instructions. */
436
+ const STOP_HISTORY = 16;
437
+
438
+ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: ProgramImage, state: EntryState, jump: number, copierSource: number, expected: (address: number) => number): Promise<LoaderRun> {
252
439
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
253
440
  try {
254
441
  await client.reset({ mode: "hard", run: false });
255
442
  if (!(await autostartLoaded(client, path, program))) return { verified: false, differences: ["The loader was not in memory after autostart."] };
256
443
  await client.keyboard(textToPetscii("RUN\n"));
444
+ // A stop before the copier or the jump gives $01, the last instructions and the first byte that is not as planned.
445
+ const stopped = async (pc: number, copied: boolean, what: string): Promise<LoaderRun> => {
446
+ const ram = await readRam(client, 0x0000, 0x10000);
447
+ const port = Buffer.from((await client.memoryRead({ address: 0x0001, size: 1, space: "c64", view: "cpu" })).data, "hex")[0]!;
448
+ const history = (await client.cpuHistory({ limit: STOP_HISTORY, space: "c64" })).entries.map((entry) => `${formatC64Address(entry.address)} ${entry.text}`);
449
+ const cause = stopCause(ram, plan, copierSource, (address) => program[2 + address - 0x0801]!, copied, expected);
450
+ const port01 = `$${port.toString(16).padStart(2, "0")}`;
451
+ return { verified: false, differences: [`The loader did not get to ${what}; it stopped at ${formatC64Address(pc)}.`], stop: { pc: formatC64Address(pc), port: port01, ...(cause === undefined ? {} : { cause }), history } };
452
+ };
257
453
  const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
258
- if (!copier.reached) return { verified: false, differences: [`The loader did not get to its copier at ${formatC64Address(plan.copier)}; it stopped at ${formatC64Address(copier.pc)}.`] };
454
+ if (!copier.reached) return await stopped(copier.pc, false, `its copier at ${formatC64Address(plan.copier)}`);
259
455
  const atJump = await client.runUntil({ target: { kind: "address", address: jump, space: "c64" }, timeoutFrames: LOADER_FRAMES });
260
- if (!atJump.reached) return { verified: false, differences: [`The loader did not get to its jump to the entry at ${formatC64Address(jump)}; it stopped at ${formatC64Address(atJump.pc)}.`] };
456
+ if (!atJump.reached) return await stopped(atJump.pc, true, `its jump to the entry at ${formatC64Address(jump)}`);
261
457
  const memory = await readRam(client, 0x0000, 0x10000);
262
458
  if (memory[jump] !== 0x4c || (memory[jump + 1]! | (memory[jump + 2]! << 8)) !== state.pc) {
263
459
  return { verified: false, differences: [`The instruction at ${formatC64Address(jump)} is not JMP ${formatC64Address(state.pc)}.`] };
@@ -280,7 +476,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
280
476
  const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
281
477
  differ("flags", flags, state.flags & 0xcf);
282
478
  if (state.vic !== undefined) {
283
- // The display registers and the raster line (D60): the loader waited for the line, so the jump comes in it or the next one.
479
+ // The display registers and the raster line: the loader waited for the line, so the jump comes in it or the next one.
284
480
  const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
285
481
  for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
286
482
  differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
@@ -289,7 +485,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
289
485
  if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
290
486
  }
291
487
  if (plan.cia !== undefined) {
292
- // The CIA timers (D63): at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
488
+ // The CIA timers: at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
293
489
  const chips = (await client.cia("both")).chips;
294
490
  const word = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
295
491
  const differWord = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${word(got)}, not ${word(wanted)}`);
@@ -309,12 +505,37 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
309
505
  }
310
506
  }
311
507
 
508
+ /**
509
+ * The ROMs the program uses: from the coverage when the knowledge has
510
+ * it, else from $01 of the entry state, else both.
511
+ */
512
+ function romsInUse(image: ProgramImage, coverage: Uint8Array | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
513
+ if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
514
+ const port = (image.state?.cpuPort as Record<string, unknown> | undefined)?.$01;
515
+ if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
516
+ return { basic: true, kernal: true, from: "none" };
517
+ }
518
+
312
519
  /** The first words of every file that the script writes. */
313
520
  const GENERATED_MARK = "; Generated by c64-source";
314
- /** The file with every range of instructions that no run executed (D49). */
521
+ /** The file with every range of instructions that no run executed. */
315
522
  const NOT_EXECUTED_FILE = "not_executed.json";
523
+ /** The line of each name and address in the source, for links from the documentation. */
524
+ const SOURCE_MAP_FILE = "source-map.json";
316
525
 
317
526
  /** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
527
+ /**
528
+ * source-map.json: the line of each name and of each address that the
529
+ * files write, after the mark line. One entry per line of the file, so a diff
530
+ * of two maps stays short.
531
+ */
532
+ function sourceMapText(rendered: Rendered, space: string, revision: string | null, files: ReadonlyArray<{ name: string }>): string {
533
+ const labels = rendered.labels.map((label) => JSON.stringify({ address: formatC64Address(label.address), name: label.name, file: label.file, line: label.line + 1 }));
534
+ const items = rendered.items.map((item) => JSON.stringify({ start: formatC64Address(item.start), end: formatC64Address(item.end), file: item.file, line: item.line + 1 }));
535
+ const head = JSON.stringify({ space, revision, files: files.map((file) => file.name) }).slice(0, -1);
536
+ return `${head},\n"labels": [\n${labels.join(",\n")}\n],\n"items": [\n${items.join(",\n")}\n]}\n`;
537
+ }
538
+
318
539
  function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
319
540
  const directory = resolveProjectPath(out);
320
541
  if (!existsSync(directory)) return;
@@ -322,7 +543,7 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
322
543
  if (!entry.isFile()) continue;
323
544
  const name = entry.name;
324
545
  const path = join(directory, name);
325
- if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
546
+ if (name === "loader.prg" || name === NOT_EXECUTED_FILE || name === SOURCE_MAP_FILE) rmSync(path, { force: true });
326
547
  else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
327
548
  }
328
549
  }
@@ -332,15 +553,22 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
332
553
  args: argv,
333
554
  strict: true,
334
555
  allowPositionals: true,
335
- options: { out: { type: "string" }, entry: { type: "string", multiple: true }, coverage: { type: "string", multiple: true }, space: { type: "string" }, loader: { type: "boolean" }, help: { type: "boolean" } },
556
+ options: {
557
+ out: { type: "string" },
558
+ entry: { type: "string", multiple: true },
559
+ space: { type: "string" },
560
+ loader: { type: "boolean" },
561
+ "analyzer-names": { type: "boolean" },
562
+ help: { type: "boolean" },
563
+ },
336
564
  });
337
565
  if (values.help) throw new UsageError("");
338
- if (positionals.length !== 1) throw new UsageError("give one image");
566
+ if (positionals.length !== 0) throw new UsageError("generate takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
339
567
  if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
340
568
  try {
341
569
  resolveProjectPath(values.out);
342
570
  } catch {
343
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
571
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
344
572
  }
345
573
  const entries = (values.entry ?? []).map((value) => {
346
574
  try {
@@ -351,16 +579,17 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
351
579
  });
352
580
  const space = values.space ?? DEFAULT_SPACE;
353
581
  if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
354
- if (space !== DEFAULT_SPACE && (values.coverage !== undefined || values.loader)) throw new UsageError("--coverage and --loader work only in space c64");
355
- const image = readImage(positionals[0]!);
356
- const coverage = readCoverageFiles(values.coverage ?? [], (message) => {
357
- throw new UsageError(message);
358
- });
359
- if (values.loader && (image.statePath === undefined || image.entry === undefined)) throw new UsageError("--loader needs an image.json with an entry and an entry_state.json next to it, from c64-unpacker capture");
360
- // Read the entry state before the long steps, so a missing file stops the run at once.
361
- const state = values.loader ? readEntryState(readProjectFile(image.statePath!).bytes.toString("utf8"), image.statePath!) : undefined;
362
- const knowledge = readKnowledge(space);
363
- const instructions = await decode(image, knowledge, entries);
582
+ if (space !== DEFAULT_SPACE && values.loader) throw new UsageError("--loader works only in space c64");
583
+ const knowledge = readKnowledge(space, values["analyzer-names"] === true);
584
+ const image = knowledge.program;
585
+ const coverage = knowledge.coverage;
586
+ if (values.loader && (image.state === undefined || image.entry === undefined)) {
587
+ throw new UsageError("--loader needs the entry and the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json>");
588
+ }
589
+ // Read the entry state before the long steps, so a bad state stops the run at once.
590
+ const state = values.loader ? readEntryState(JSON.stringify(image.state), "the entry state of the knowledge") : undefined;
591
+ const romUse = romsInUse(image, coverage);
592
+ const instructions = await decode(image, knowledge, entries, coverage);
364
593
  const laid = layout({
365
594
  segments: image.segments,
366
595
  byteAt: image.byteAt,
@@ -371,50 +600,84 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
371
600
  comments: knowledge.comments,
372
601
  modules: knowledge.modules,
373
602
  referenceTargets: knowledge.references.map((reference) => reference.to),
603
+ operands: knowledge.operands,
604
+ patches: knowledge.patches,
605
+ macros: knowledge.macros,
606
+ macroUses: knowledge.macroUses,
374
607
  ...(coverage === undefined ? {} : { coverage }),
375
- // The platform names are C64 names: drive code and banks get none.
376
- ...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
608
+ // The platform names are C64 names: drive code and banks get none. A ROM that the program does not use gives none.
609
+ ...(space === DEFAULT_SPACE ? { platform: platformNamesFor(romUse), describe: describeAddress, decode: decodeRegister } : {}),
377
610
  });
378
611
  const asBytes = await probe(laid);
379
- 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."];
380
- const rendered = render(laid, asBytes, header);
381
- const files = [...rendered.files];
612
+ const header = [`Generated by c64-source from knowledge revision ${knowledge.revision}${space === DEFAULT_SPACE ? "" : ` in space ${space}`}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
382
613
  let loader: LoaderPlan | undefined;
383
- // An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
614
+ // An image without its entry has no program: the source is still written and checked, but the run fails.
384
615
  const missingEntry = entryOutsideImage(image.entry, image.segments);
385
616
  // A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
386
617
  let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
387
618
  if (state !== undefined && missingEntry === undefined) {
388
619
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
389
620
  try {
390
- loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
391
- files.push({ name: "loader.asm", text: loader.text });
621
+ const entryName = laid.names.get(image.entry!);
622
+ 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 knowledge revision ${knowledge.revision}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
392
623
  } catch (error) {
393
624
  if (!(error instanceof WireFailure)) throw error;
394
625
  loaderRefused = error.message;
395
626
  }
396
627
  }
397
- // 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).
398
- for (const [index, file] of files.entries()) if (!file.text.startsWith(GENERATED_MARK)) files[index] = { ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` };
628
+ // Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again.
629
+ 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}` }));
630
+ const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
631
+ // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and a macro use as its
632
+ // instructions; then the source is assembled again.
633
+ const numbers = new Map<number, string>();
634
+ const asInstructions = new Map<number, string>();
635
+ let rendered = render(laid, asBytes, header, numbers, asInstructions);
636
+ if (laid.macroUses.size > 0) for (const [start, reason] of await probeUses(laid, rendered, image)) asInstructions.set(start, reason);
637
+ if (asInstructions.size > 0) rendered = render(laid, asBytes, header, numbers, asInstructions);
638
+ let files = marked([...rendered.files, ...loaderFiles]);
639
+ let built = await assembleFiles(files, "main.asm");
640
+ for (let round = 0; round < ROUNDS && (rendered.expressions.length > 0 || rendered.uses.length > 0); round++) {
641
+ const blamed = blame(rendered, built, image, 1);
642
+ if (blamed.numbers.size === 0 && blamed.uses.size === 0) break;
643
+ for (const [address, reason] of blamed.numbers) numbers.set(address, reason);
644
+ for (const [start, reason] of blamed.uses) asInstructions.set(start, reason);
645
+ rendered = render(laid, asBytes, header, numbers, asInstructions);
646
+ files = marked([...rendered.files, ...loaderFiles]);
647
+ built = await assembleFiles(files, "main.asm");
648
+ }
399
649
  removeOldFiles(values.out, new Set(files.map((file) => file.name)));
400
650
  for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
651
+ writeProjectFile(`${values.out}/${SOURCE_MAP_FILE}`, Buffer.from(sourceMapText(rendered, space, knowledge.revision, files), "utf8"), { what: "--out" });
401
652
  const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
402
653
  const inData = entriesInData(entries, knowledge.regions);
403
654
  if (rendered.notExecuted !== undefined) {
404
- const all = { image: image.path, revision: knowledge.revision, instructions: rendered.notExecuted.instructions, ranges: rendered.notExecuted.ranges.map(showRange) };
655
+ // Each range with the file of the source that holds it and the routine before it, as a report groups them.
656
+ const routines = knowledge.symbols.filter((symbol) => symbol.kind === "routine").sort((a, b) => a.address - b.address);
657
+ const placed = rendered.notExecuted.ranges.map((range) => {
658
+ const file = laid.pieces.find((piece) => range.start >= piece.start && range.start <= piece.end)?.file;
659
+ const routine = routines.filter((symbol) => symbol.address <= range.start).at(-1)?.name;
660
+ return { ...showRange(range), ...(file === undefined ? {} : { file }), ...(routine === undefined ? {} : { routine }) };
661
+ });
662
+ const files: Record<string, number> = {};
663
+ for (const range of placed) if (range.file !== undefined) files[range.file] = (files[range.file] ?? 0) + 1;
664
+ const all = { revision: knowledge.revision, instructions: rendered.notExecuted.instructions, files, ranges: placed };
405
665
  writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
406
666
  }
407
667
 
408
668
  const output: Record<string, unknown> = {
409
669
  out: values.out,
410
670
  files: files.map((file) => file.name),
671
+ sourceMap: `${values.out}/${SOURCE_MAP_FILE}`,
411
672
  revision: knowledge.revision,
412
673
  instructions: rendered.instructions,
413
674
  dataBytes: rendered.dataBytes,
414
675
  ...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
676
+ // The ROM names that the source leaves out, and the evidence.
677
+ ...(space !== DEFAULT_SPACE || (romUse.basic && romUse.kernal) ? {} : { romNamesLeftOut: { roms: [...(romUse.basic ? [] : ["BASIC"]), ...(romUse.kernal ? [] : ["KERNAL"])], ...(romUse.basic || romUse.kernal ? {} : { systemArea: true }), from: romUse.from } }),
415
678
  ...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
416
679
  labels: { named: laid.namedLabels, generated: laid.generatedLabels },
417
- // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it (F-27).
680
+ // An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it.
418
681
  ...(inData.length === 0
419
682
  ? {}
420
683
  : { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
@@ -432,18 +695,50 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
432
695
  },
433
696
  }),
434
697
  ...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
698
+ ...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
699
+ ...(rendered.operandsAsNumbers.length === 0
700
+ ? {}
701
+ : {
702
+ operandsAsNumbers: {
703
+ count: rendered.operandsAsNumbers.length,
704
+ listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
705
+ },
706
+ }),
707
+ ...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
708
+ // A macro use that the source writes as its instructions does not fail the run: the source stays identical.
709
+ ...(rendered.macrosAsInstructions.length === 0
710
+ ? {}
711
+ : {
712
+ macrosAsInstructions: {
713
+ count: rendered.macrosAsInstructions.length,
714
+ listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
715
+ },
716
+ }),
717
+ // A patch that the source cannot write fails the run: the switch would miss a part.
718
+ ...(laid.patchesNotWritten.length === 0
719
+ ? {}
720
+ : {
721
+ patchesNotWritten: {
722
+ count: laid.patchesNotWritten.length,
723
+ listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
724
+ },
725
+ }),
435
726
  };
436
727
  // The loader includes the source, so it is built only from a source that is identical to the image.
437
728
  const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
438
- const built = await assembleFiles(files, "main.asm");
439
729
  if (built.program === undefined) {
440
730
  return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
441
731
  }
442
732
  const checked = check(built.program, built.start!, image);
443
733
  output.check = checked;
444
734
  if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
735
+ // Each switch on alone, against the default build; combinations are not built.
736
+ const switches: SwitchCheck[] = [];
737
+ for (const [name, patches] of switchesOf(laid)) switches.push(await checkSwitch(name, patches, files, { program: built.program, start: built.start! }, laid.names));
738
+ if (switches.length > 0) output.switches = switches;
739
+ const sourceFailed = laid.patchesNotWritten.length > 0 || switches.some((item) => item.result !== "fits");
445
740
  if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
446
- if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
741
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
447
742
 
448
743
  const loaderBuilt = await assembleFiles(files, "loader.asm");
449
744
  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 };
@@ -467,22 +762,25 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
467
762
  const loaderPath = `${values.out}/loader.prg`;
468
763
  writeProjectFile(loaderPath, program, { what: "--out" });
469
764
  (output.files as string[]).push("loader.prg");
470
- const ran = await runLoader(loaderPath, program, loader, image, state, jump);
765
+ const copierSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === COPIER_SOURCE);
766
+ if (copierSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${COPIER_SOURCE} label. This is a defect of the c64-source script.`);
767
+ const copierSource = copierSymbol.kind === "address" ? copierSymbol.address : copierSymbol.value;
768
+ const ran = await runLoader(loaderPath, program, loader, image, state, jump, copierSource, expected);
471
769
  output.loader = {
472
770
  program: loaderPath,
473
771
  copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
474
772
  copier: formatC64Address(loader.copier),
475
773
  stackByte: formatC64Address(loader.stackByte),
476
774
  ...ran,
477
- // What the loader cannot set as it was at the entry (D63).
478
- ...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
775
+ // What the loader cannot set as it was at the entry.
776
+ ...(loaderLimits(loader).length === 0 ? {} : { limits: loaderLimits(loader) }),
479
777
  };
480
- return { output, failed: !ran.verified };
778
+ return { output, failed: !ran.verified || sourceFailed };
481
779
  }
482
780
 
483
781
  await runScript(
484
- async () => {
485
- const { output, failed } = await run(process.argv.slice(2));
782
+ async (argv) => {
783
+ const { output, failed } = await run(argv);
486
784
  if (failed) process.exitCode = 1;
487
785
  return output;
488
786
  },