@henols/c64-re-tools 2.1.2 → 2.1.3

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 (44) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-disk/scripts/lib/containers.ts +2 -1
  3. package/skills/c64-emulator/SKILL.md +2 -0
  4. package/skills/c64-knowledge/SKILL.md +6 -2
  5. package/skills/c64-knowledge/scripts/knowledge.ts +6 -12
  6. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -1
  7. package/skills/c64-memory-map/SKILL.md +2 -0
  8. package/skills/c64-memory-map/scripts/memory-map.ts +22 -3
  9. package/skills/c64-provenance/SKILL.md +4 -3
  10. package/skills/c64-provenance/scripts/lib/library.ts +28 -5
  11. package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
  12. package/skills/c64-provenance/scripts/provenance.ts +13 -4
  13. package/skills/c64-source/SKILL.md +1 -1
  14. package/skills/c64-source/scripts/lib/source.ts +18 -4
  15. package/skills/c64-source/scripts/source.ts +9 -13
  16. package/skills/c64-static-analysis/SKILL.md +4 -3
  17. package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
  18. package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
  19. package/skills/c64-static-analysis/scripts/static-analysis.ts +17 -14
  20. package/skills/c64-testing/SKILL.md +6 -3
  21. package/skills/c64-testing/references/scenario-format.md +5 -3
  22. package/skills/c64-testing/scripts/lib/diff-files.ts +27 -8
  23. package/skills/c64-testing/scripts/lib/runs.ts +23 -7
  24. package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
  25. package/skills/c64-testing/scripts/testing.ts +5 -2
  26. package/skills/c64-unpacker/SKILL.md +2 -2
  27. package/skills/c64-unpacker/scripts/lib/loads.ts +4 -1
  28. package/skills/c64-unpacker/scripts/lib/machine.ts +31 -0
  29. package/skills/c64-unpacker/scripts/unpacker.ts +4 -1
  30. package/src/host/vice/session.ts +98 -11
  31. package/src/host-client/image.ts +22 -8
  32. package/src/host-client/steps.ts +2 -2
  33. package/src/host-client/tools.ts +4 -3
  34. package/src/host-client/transfer.ts +8 -3
  35. package/src/knowledge/write.ts +64 -28
  36. package/src/mcp/output.ts +1 -1
  37. package/src/mcp/server.ts +7 -0
  38. package/src/mcp/tools/debug.ts +2 -1
  39. package/src/mcp/tools/media.ts +5 -1
  40. package/src/media.ts +15 -0
  41. package/src/opcodes.ts +31 -1
  42. package/src/platform.ts +14 -1
  43. package/src/project.ts +8 -1
  44. package/src/script.ts +63 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
5
5
  "license": "MIT",
6
6
  "author": "Henrik Olsson",
@@ -33,7 +33,8 @@ export interface Crt {
33
33
 
34
34
  export function parseCrt(bytes: Uint8Array): Crt {
35
35
  const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
36
- if (buffer.length < 0x40 || !hasSignature("crt", buffer)) throw new Refusal("media-error", "the file is not a CRT: it does not start with C64 CARTRIDGE");
36
+ if (!hasSignature("crt", buffer)) throw new Refusal("media-error", "the file is not a CRT: it does not start with C64 CARTRIDGE");
37
+ if (buffer.length < 0x40) throw new Refusal("media-error", `the CRT header is shorter than 64 bytes: the file has only ${buffer.length} bytes`);
37
38
  const headerLength = buffer.readUInt32BE(0x10);
38
39
  const type = buffer.readUInt16BE(0x16);
39
40
  const exrom = buffer[0x18]!;
@@ -135,6 +135,8 @@ An error is `{"error": {"code": "...", "message": "..."}}`. The message tells wh
135
135
  - `machine-unavailable` and the message says that the VICE window closed: the session starts the emulator again. Wait a few seconds. Then do the operation again.
136
136
  - `machine-state-lost` with a different message: the connection to the host runtime was lost, and the machine state is gone. Do not continue with results from before. The next call starts a new machine when the host runtime runs. Load the program again, and set the breakpoints and the watchpoints again. If the next call gives `machine-unavailable` with a message that the host runtime does not run, obey the rule for that message.
137
137
  - `machine-unavailable` and the message names a pause or a dialog in the emulator window: this occurs only while the window is open. Do not send more operations. Tell the user to resume the emulator (Pause, Alt+P) or to close the dialog in the VICE window. Then do the operation again. The machine state does not change.
138
+ - `machine-unavailable` and the message says that the emulator does not take commands now, with no window open: wait a few seconds. Then do the operation again one time. If you get the same error again, stop the task. Tell the user to restart the c64-re-tools MCP server, and wait for the user.
139
+ - `media-error`: the file is not of the type that its extension names. Give a file of that type.
138
140
  - `machine-running`: stop the CPU first, then try again.
139
141
  - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script. Do not try to repair the installation.
140
142
  - `c64_run_until` with `stopReason` `timeout`: the target did not occur in the frame limit. This is evidence too. Report it.
@@ -129,12 +129,16 @@ Write:
129
129
 
130
130
  Each write is one revision. A classification inside a region splits that region, so regions never overlap.
131
131
 
132
+ A write can give the same fact that an analyzer gave. Then the fact gets the origin of the write, and `previous` in the result shows the row of the analyzer. The next analysis does not remove the fact. This is true for a symbol, a region, a comment and a reference. A `--by user` write over the same fact of `llm` also changes the origin to `user`. A `--by llm` write over the same fact of `user` changes nothing.
133
+
134
+ A comment and a module are at the start of an instruction. In a code region, the script refuses an address that is an operand byte of an instruction. The error gives the start of that instruction. In data, each address is correct.
135
+
132
136
  ## The program and the coverage
133
137
 
134
138
  The knowledge holds the program, so a project needs no image or coverage files. The other skills read them from the knowledge and take no path to them.
135
139
 
136
140
  1. Capture a packed program with the c64-unpacker skill into `.c64-re-tools/local/`. Then put it in with `image .c64-re-tools/local/capture/image.json`. The script reads `image.bin`, `entry_state.json` and the stages next to it.
137
- 2. For a program without a packer, drive code or a cartridge bank, put the PRG in with `image <file.prg>`. Give `--space drive8` or `--space bank0` for drive code or a bank.
141
+ 2. For a program without a packer, drive code or a cartridge bank, put the PRG in with `image <file.prg>`. Give `--space drive8` or `--space bank0` for drive code or a bank. A PRG has the extension `.prg` or no extension. The script refuses a file with another extension.
138
142
  3. Write each coverage map of a run into `.c64-re-tools/local/`. Then add it with `coverage <map.bin> ...`.
139
143
  4. Read what the knowledge holds with `program`: the segments, the entry, and the coverage maps that it has.
140
144
 
@@ -235,7 +239,7 @@ A patch is ACME code that replaces a range of the program when the build defines
235
239
  - The code must fit in the range. NOP ($EA) fills the bytes after the code. `--nop` gives a range of NOP only.
236
240
  - `--code` gives the code as text. `--code-file` reads the code from a project file. The code can use the labels and the macros of the source, and local labels with a dot, for example `.skip`.
237
241
  - The braces in the code must be pairs, because the source puts the code inside `{ }`.
238
- - A patch cannot overlap another patch or a macro use.
242
+ - A patch cannot overlap another patch or a macro use. A patch with the same start and the same switch replaces the patch. A patch of another switch at the same start gives the error `conflict`.
239
243
  - Each byte of the range must be in the program of the knowledge.
240
244
  - A `patch` write without `--comment` keeps the comment that the patch has.
241
245
 
@@ -328,6 +328,9 @@ function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
328
328
  if ("switch" in row) return patch(row);
329
329
  if ("body" in row) return macro(row);
330
330
  if ("macro" in row) return macroUse(row);
331
+ if ("placement" in row) return comment(row);
332
+ if ("from" in row) return reference(row);
333
+ if ("file" in row) return module(row);
331
334
  return symbol(row);
332
335
  }
333
336
 
@@ -659,10 +662,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
659
662
  (call) => {
660
663
  const context = contextOf(call);
661
664
  const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), placement: oneOf(COMMENT_PLACEMENTS, call.subject(1), "<placement>"), text: call.rest(2).join(" ") };
662
- return (db) => {
663
- const result = setComment(db, context, request);
664
- return { revision: result.revision, comment: comment(result.current) };
665
- };
665
+ return (db) => shown(setComment(db, context, request), "comment", comment);
666
666
  },
667
667
  ),
668
668
  writeCommand({ name: "uncomment", summary: "Removes a comment.", subjects: [ADDRESS, { name: "<placement>", help: `one of ${COMMENT_PLACEMENTS.join(", ")}` }] }, (call) => {
@@ -681,10 +681,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
681
681
  const context = contextOf(call);
682
682
  const request = { space: call.space, from: call.parseAddress(call.subject(0), "<from>"), to: call.parseAddress(call.subject(1), "<to>"), kind: oneOf(REFERENCE_KINDS, call.subject(2), "<kind>") };
683
683
  return name === "reference"
684
- ? (db) => {
685
- const result = addReference(db, context, request);
686
- return { revision: result.revision, reference: reference(result.current) };
687
- }
684
+ ? (db) => shown(addReference(db, context, request), "reference", reference)
688
685
  : (db) => ({ revision: removeReference(db, context, request).revision });
689
686
  },
690
687
  ),
@@ -695,10 +692,7 @@ const WRITE_COMMANDS: CommandSpec[] = [
695
692
  const context = contextOf(call);
696
693
  const title = call.string("title");
697
694
  const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>"), file: call.subject(1), ...(title === undefined ? {} : { title }) };
698
- return (db) => {
699
- const result = setModule(db, context, request);
700
- return { revision: result.revision, module: module(result.current) };
701
- };
695
+ return (db) => shown(setModule(db, context, request), "module", module);
702
696
  },
703
697
  ),
704
698
  writeCommand({ name: "unmodule", summary: "Removes the module that starts at an address.", subjects: [START] }, (call) => {
@@ -68,7 +68,14 @@ export function listing(source: ListingSource, start: number, end: number): List
68
68
  const bytes = Array.from({ length }, (_, offset) => source.byteAt(address + offset)!.toString(16).padStart(2, "0")).join(" ");
69
69
  const label = source.nameOf(address);
70
70
  const second = source.secondNames(address).filter((name) => name !== label);
71
- const { line: lineComments, side } = source.comments(address);
71
+ const { line: lineComments, side: own } = source.comments(address);
72
+ // A comment inside the line (knowledge of an older version) shows with its address, as in the source of c64-source.
73
+ const inner: string[] = [];
74
+ for (let offset = 1; offset < length; offset++) {
75
+ const found = source.comments(address + offset);
76
+ for (const text of [...found.line, ...(found.side === undefined ? [] : [found.side])]) inner.push(`${formatC64Address(address + offset)}: ${text}`);
77
+ }
78
+ const side = own === undefined && inner.length === 0 ? undefined : [...(own === undefined ? [] : [own]), ...inner].join(" ");
72
79
  const references = source.references(address);
73
80
  const access = accessOf(source.coverage, address, length);
74
81
  return {
@@ -36,6 +36,8 @@ node <skill>/scripts/memory-map.ts export --out src/hw.asm
36
36
 
37
37
  Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
38
38
 
39
+ `at` and `decode` also take a platform name, for example `SCROLY` or `CHROUT`, in upper or lower case. A platform name comes before a name of the knowledge. `list` and `export` show the platform names.
40
+
39
41
  ### Calling the script
40
42
 
41
43
  - Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
@@ -3,6 +3,7 @@
3
3
  // writes the platform names to a project file as an ACME include.
4
4
 
5
5
  import { formatC64Address, formatC64Byte } from "#src/c64.ts";
6
+ import { isRefusal, Refusal } from "#src/errors.ts";
6
7
  import { writeReported } from "#src/host-client/transfer.ts";
7
8
  import { knowledgeNames } from "#src/knowledge/names.ts";
8
9
  import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
@@ -10,6 +11,24 @@ import { listed, OPTIONS, runScript, UsageError } from "#src/script.ts";
10
11
 
11
12
  const show = (entry: Entry) => ({ start: formatC64Address(entry.start), ...(entry.end !== entry.start ? { end: formatC64Address(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
12
13
 
14
+ /** The platform names by name in upper case: memory-map knows them also outside a project. */
15
+ const PLATFORM = new Map(platformNames().map((entry) => [entry.name.toUpperCase(), entry.address]));
16
+
17
+ /**
18
+ * The address of a name: a platform name (a register, a KERNAL or BASIC entry,
19
+ * a system variable) in any letter case first, else a name of the knowledge.
20
+ */
21
+ function names(name: string, space: string): number {
22
+ const platform = PLATFORM.get(name.toUpperCase());
23
+ if (platform !== undefined) return platform;
24
+ try {
25
+ return knowledgeNames(name, space);
26
+ } catch (error) {
27
+ if (!isRefusal(error) || error.code !== "not-found") throw error;
28
+ throw new Refusal("not-found", `${name} is not a C64 platform name and not a name in the knowledge. Give a $ address, a platform name that memory-map.ts list or export shows (for example SCROLY or CHROUT), or a name that knowledge.ts symbols lists.`);
29
+ }
30
+ }
31
+
13
32
  const LISTS: Record<string, () => Entry[]> = {
14
33
  vic: () => IO[0]!.registers,
15
34
  sid: () => IO[1]!.registers,
@@ -22,12 +41,12 @@ const LISTS: Record<string, () => Entry[]> = {
22
41
  await runScript({
23
42
  name: "memory-map",
24
43
  summary: "The platform meaning of C64 addresses and of values in platform registers.",
25
- names: knowledgeNames,
44
+ names,
26
45
  commands: [
27
46
  {
28
47
  name: "at",
29
48
  summary: "The platform meaning of each address: its name, area and meaning.",
30
- subjects: [{ name: "<address>", help: "an address or a name of the knowledge", list: true }],
49
+ subjects: [{ name: "<address>", help: "an address, a platform name such as SCROLY, or a name of the knowledge", list: true }],
31
50
  run: (call) => ({
32
51
  addresses: call.subjects.map((text) => {
33
52
  const found = lookup(call.parseAddress(text, "<address>"));
@@ -47,7 +66,7 @@ await runScript({
47
66
  name: "decode",
48
67
  summary: "The meaning of a value in a platform register.",
49
68
  subjects: [
50
- { name: "<register>", help: "the register: $0001, $d011, $d016, $d018, $d010, $d015-$d01f, $dc0d, $dd00 or $dd0d" },
69
+ { name: "<register>", help: "the register, as an address or a platform name: $0001, $d011 (SCROLY), $d016, $d018, $d010, $d015-$d01f, $dc0d, $dd00 or $dd0d" },
51
70
  { name: "<value>", help: "the value of the register: $ and hex, decimal, or % and binary, 0 to 255" },
52
71
  ],
53
72
  options: { bank: { kind: "number", value: "<value>", help: "the value of $dd00, for the VIC-II bank of a $d018 value", max: 0xff } },
@@ -97,7 +97,8 @@ Use `separate` when you have only one release. It reads the program, its stages
97
97
  - `ranges`: the program in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. The last stage is the program. An earlier stage is a depacker, an intro or a loader. Only a capture of c64-unpacker `capture` has stages. For a program from `loads` or a PRG, `noStages` tells this.
98
98
  - `access`: from the coverage of the knowledge: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
99
99
  - `codeRegions.total`: the number of code regions in the knowledge.
100
- - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. A region also reaches the code after it when its last instruction is not RTS, RTI, JMP, BRK or JAM: the CPU continues there. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
100
+ - `roots`: the addresses where the reachability starts. They are the entry, and the NMI, RESET and IRQ vectors at `$FFFA`-`$FFFF` when the image holds them. An interrupt handler starts from its vector, because no call or jump goes to it. `from` tells the source of each root.
101
+ - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from a root. A region also reaches the code after it when its last instruction is not RTS, RTI, JMP, BRK or JAM: the CPU continues there. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
101
102
  - `outside`: RAM outside the program that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
102
103
 
103
104
  Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed` (default 50). `--out` writes all of them and the result shows the file as `{out, bytes, sha256}`. Run more coverage runs for parts that are `untouched`, and add them with `coverage` of the c64-knowledge skill, before you decide.
@@ -113,7 +114,7 @@ Use `leftovers` to find what the program holds in addition to its working code.
113
114
  | `fill` | One byte that repeats, at least 16 times, or a pattern of two to four bytes that repeats, at least 32 bytes in total. `pattern` is the repeated bytes. |
114
115
  | `residue` | Bytes that no run touched and that are no text, BASIC, fill or code region. `bytes` shows the first 16 bytes. |
115
116
  | `copies` | Bytes that are the same as earlier bytes of the program, at least 32 bytes. `copyOf` is the address of the earlier bytes. |
116
- | `deadCode` | Code regions of the knowledge that the entry does not reach, as in `codeRegions.unreachable` of `separate`, and that no run executed. |
117
+ | `deadCode` | Code regions of the knowledge that no root reaches, as in `codeRegions.unreachable` of `separate`, and that no run executed. |
117
118
 
118
119
  Text from another computer can show the tools of the developer:
119
120
 
@@ -194,7 +195,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
194
195
 
195
196
  The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. `releases` gives the segments and the `shift` of each release. With `--frames`, each difference also has `runtime`, and `run` tells if each release started, or why not.
196
197
 
197
- For `leftovers`: the lists of findings, with `entry` and the number of `stages`. `noEntry`, `noStages` and `noCoverage` tell which knowledge is missing.
198
+ For `leftovers`: the lists of findings, with `entry`, `roots` and the number of `stages`. `noStages` and `noCoverage` tell which knowledge is missing. Without a root, `roots` tells why `deadCode` is empty.
198
199
 
199
200
  The runtime evidence and the leftovers are evidence only. The script does not decide which bytes are original.
200
201
 
@@ -5,8 +5,8 @@
5
5
  // suggests its names there as an apply batch. Nothing here writes knowledge.
6
6
 
7
7
  import { createHash } from "node:crypto";
8
- import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
9
- import { join, relative, resolve, sep } from "node:path";
8
+ import { mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
9
+ import { dirname, join, relative, resolve, sep } from "node:path";
10
10
 
11
11
  import { Refusal } from "#src/errors.ts";
12
12
  import { writeRefusal, type Written } from "#src/host-client/transfer.ts";
@@ -14,7 +14,7 @@ import { formatC64Address, instructionLength } from "#src/c64.ts";
14
14
  import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
15
15
  import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
16
16
  import { COMMENT_PLACEMENTS, type CommentPlacement, isSemantic, OPERAND_SELECTS, type OperandSelect, REFERENCE_KINDS, type ReferenceKind, REGION_TYPES, type RegionType, SYMBOL_KINDS, type SymbolKind } from "#src/knowledge/schema.ts";
17
- import { PROJECT_DIRECTORY } from "#src/project.ts";
17
+ import { checkOutputPath, isInside, PROJECT_DIRECTORY } from "#src/project.ts";
18
18
 
19
19
  /** A routine shorter than this is not saved and not searched: its bytes occur by chance. */
20
20
  export const MIN_ROUTINE = 16;
@@ -291,14 +291,37 @@ export function parseRoutine(text: string, file: string): Routine {
291
291
  };
292
292
  }
293
293
 
294
+ /**
295
+ * Refuses a library folder to write that is inside the project `root`, also
296
+ * through a symbolic link, when the output check of every script refuses it
297
+ * (checkOutputPath): for example a folder in the knowledge. A folder outside
298
+ * the project gets no check.
299
+ */
300
+ function checkLibraryOutput(path: string, root: string): void {
301
+ let existing = path;
302
+ while (statSync(existing, { throwIfNoEntry: false }) === undefined && dirname(existing) !== existing) existing = dirname(existing);
303
+ const real = join(realpathSync(existing), relative(existing, path));
304
+ const realRoot = realpathSync(root);
305
+ for (const [base, target] of [[root, path], [realRoot, real]] as const) {
306
+ if (isInside(base, target)) {
307
+ // A project path shows with "/" on every system, as the messages give it.
308
+ checkOutputPath(relative(base, target).split(sep).join("/") || ".", "folder", { root: base, what: "--library" });
309
+ return;
310
+ }
311
+ }
312
+ }
313
+
294
314
  /**
295
315
  * The library folder that --library names, relative to the working directory
296
316
  * like --project (D71). A library is a plain folder, never a project. With
297
- * `create`, a missing folder is made.
317
+ * `create`, a missing folder is made; a folder inside the project must pass
318
+ * the output check of every script, which refuses the knowledge.
298
319
  */
299
320
  export function libraryFolder(folder: string, options: { create: boolean; cwd?: string }): string {
300
321
  if (folder.length === 0) throw new Refusal("invalid-input", "The library folder must not be empty.");
301
- const path = resolve(options.cwd ?? process.cwd(), folder);
322
+ const root = options.cwd ?? process.cwd();
323
+ const path = resolve(root, folder);
324
+ if (options.create) checkLibraryOutput(path, root);
302
325
  const stat = statSync(path, { throwIfNoEntry: false });
303
326
  if (stat === undefined) {
304
327
  if (!options.create) throw new Refusal("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
@@ -1,7 +1,8 @@
1
1
  // Evidence from one release: for each range of the image, the unpack
2
2
  // stage that wrote it and the access class from coverage; for each code
3
3
  // region of knowledge, whether call and jump references or the run on from
4
- // its last instruction reach it from the entry; and what the program reads
4
+ // its last instruction reach it from a root (the entry, and the NMI, RESET
5
+ // and IRQ vectors at $fffa-$ffff when the image holds them); and what the program reads
5
6
  // or executes outside the image. Evidence only: nothing here decides what is
6
7
  // original.
7
8
 
@@ -30,13 +31,21 @@ export interface EvidenceRange extends AddressRange {
30
31
 
31
32
  export interface CodeRegionEvidence extends AddressRange {
32
33
  type: string;
33
- /** Absent when the image has no entry. */
34
+ /** Absent when the image has no root: no entry and no hardware vector. */
34
35
  reachable?: boolean;
35
36
  /** Absent without coverage. */
36
37
  executed?: boolean;
37
38
  }
38
39
 
40
+ /** An address where the reachability starts, and why: the entry, or a hardware vector in the image. */
41
+ export interface Root {
42
+ address: number;
43
+ from: "entry" | "NMI vector $fffa" | "RESET vector $fffc" | "IRQ vector $fffe";
44
+ }
45
+
39
46
  export interface Separation {
47
+ /** The roots of the reachability, in the order entry, NMI, RESET, IRQ. */
48
+ roots: Root[];
40
49
  ranges: EvidenceRange[];
41
50
  codeRegions: CodeRegionEvidence[];
42
51
  /**
@@ -50,6 +59,24 @@ export interface Separation {
50
59
 
51
60
  const SYSTEM_PAGES_END = 0x03ff;
52
61
 
62
+ /** The hardware vectors of the 6510: the CPU reads them from $fffa-$ffff, the RAM when the KERNAL is banked out. */
63
+ const HARDWARE_VECTORS = [
64
+ { at: 0xfffa, from: "NMI vector $fffa" },
65
+ { at: 0xfffc, from: "RESET vector $fffc" },
66
+ { at: 0xfffe, from: "IRQ vector $fffe" },
67
+ ] as const;
68
+
69
+ /**
70
+ * The roots of the reachability: the entry, and each hardware vector whose
71
+ * two bytes are in the image. An interrupt handler runs by its vector, and
72
+ * no call or jump reference reaches it.
73
+ */
74
+ export function reachabilityRoots(segments: readonly AddressRange[], memory: Uint8Array, entry?: number): Root[] {
75
+ const inImage = (address: number) => segments.some((segment) => address >= segment.start && address <= segment.end);
76
+ const vectors = HARDWARE_VECTORS.filter((vector) => inImage(vector.at) && inImage(vector.at + 1)).map((vector): Root => ({ address: memory[vector.at]! | (memory[vector.at + 1]! << 8), from: vector.from }));
77
+ return [...(entry === undefined ? [] : [{ address: entry, from: "entry" as const }]), ...vectors];
78
+ }
79
+
53
80
  /** BRK, JMP, RTI, RTS, JMP (indirect) and the JAM opcodes: the CPU does not run on to the next instruction after them. */
54
81
  const NO_FALL_THROUGH = new Set([0x00, 0x4c, 0x40, 0x60, 0x6c, 0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
55
82
 
@@ -114,12 +141,13 @@ export function separate(input: SeparationInput): Separation {
114
141
  }
115
142
  }
116
143
 
117
- // Reachability: from the region that holds the entry, through call and jump references and the run on
144
+ // Reachability: from the regions that hold a root, through call and jump references and the run on
118
145
  // from the last instruction of a region into the next address, to a fixed point.
119
146
  const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
120
147
  const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
148
+ const roots = reachabilityRoots(input.segments, input.memory, input.entry);
121
149
  const reached = new Set<number>();
122
- if (input.entry !== undefined && holding(input.entry) >= 0) reached.add(holding(input.entry));
150
+ for (const root of roots) if (holding(root.address) >= 0) reached.add(holding(root.address));
123
151
  // The CPU runs the bytes after the region as instructions: through bytes of no code region (a dual-use
124
152
  // instruction, for example) into the next code region, unless one of them does not run on.
125
153
  const runOn = (from: number): number | undefined => {
@@ -154,7 +182,7 @@ export function separate(input: SeparationInput): Separation {
154
182
  ran = false;
155
183
  for (let address = region.start; address <= region.end && !ran; address++) ran = executed[address] === 1;
156
184
  }
157
- return { start: region.start, end: region.end, type: region.type, ...(input.entry === undefined ? {} : { reachable: reached.has(index) }), ...(ran === undefined ? {} : { executed: ran }) };
185
+ return { start: region.start, end: region.end, type: region.type, ...(roots.length === 0 ? {} : { reachable: reached.has(index) }), ...(ran === undefined ? {} : { executed: ran }) };
158
186
  });
159
187
 
160
188
  const read: number[] = [];
@@ -174,5 +202,5 @@ export function separate(input: SeparationInput): Separation {
174
202
  for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
175
203
  return true;
176
204
  };
177
- return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
205
+ return { roots, ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
178
206
  }
@@ -47,7 +47,7 @@ import { compareReleases, shiftSuggestion, suggestShifts, type Release } from ".
47
47
  import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
48
48
  import { watchRelease, type Start } from "./lib/runtime.ts";
49
49
  import { findLeftovers } from "./lib/leftovers.ts";
50
- import { separate } from "./lib/separate.ts";
50
+ import { separate, type Root } from "./lib/separate.ts";
51
51
 
52
52
  const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
53
53
 
@@ -239,6 +239,11 @@ function programEvidence() {
239
239
  return { program, stages, coverage, found };
240
240
  }
241
241
 
242
+ /** The roots of the reachability as a person reads them. */
243
+ const showRoots = (roots: readonly Root[]) => roots.map((root) => ({ address: formatC64Address(root.address), from: root.from }));
244
+
245
+ const NO_ROOTS = "none: the program in the knowledge has no entry, and the image does not hold the hardware vectors at $fffa-$ffff, so no code region is reachable or unreachable.";
246
+
242
247
  const NO_STAGES = "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none.";
243
248
 
244
249
  /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
@@ -251,7 +256,8 @@ function separateCommand(call: Call): unknown {
251
256
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
252
257
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
253
258
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
254
- const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify({ ranges, codeRegions, outside }, null, 1)}\n`, { what: "--out" });
259
+ const roots = showRoots(found.roots);
260
+ const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify({ roots, ranges, codeRegions, outside }, null, 1)}\n`, { what: "--out" });
255
261
  const rest = out === undefined ? undefined : out;
256
262
  const list = <T>(items: T[]) => listed(items, max, undefined, rest);
257
263
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
@@ -266,10 +272,12 @@ function separateCommand(call: Call): unknown {
266
272
  }),
267
273
  ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
268
274
  ranges: list(ranges),
275
+ // The roots of the reachability: the entry and the hardware vectors in the image.
276
+ roots: roots.length === 0 ? NO_ROOTS : roots,
269
277
  // total counts every code region; unreachable is a list like the others.
270
278
  codeRegions: {
271
279
  total: codeRegions.length,
272
- ...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
280
+ ...(roots.length === 0 ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
273
281
  },
274
282
  ...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
275
283
  ...(written === undefined ? {} : written),
@@ -310,7 +318,8 @@ function leftoversCommand(call: Call): unknown {
310
318
  const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify(all, null, 1)}\n`, { what: "--out" });
311
319
  const list = <T>(items: T[]) => listed(items, max, undefined, out ?? "a larger --max-listed, or --out <file>");
312
320
  return {
313
- ...(program.entry === undefined ? { noEntry: "The program in the knowledge has no entry, so deadCode is empty." } : { entry: formatC64Address(program.entry) }),
321
+ ...(program.entry === undefined ? {} : { entry: formatC64Address(program.entry) }),
322
+ roots: found.roots.length === 0 ? NO_ROOTS : showRoots(found.roots),
314
323
  ...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
315
324
  ...(coverage === undefined ? { noCoverage: "The knowledge has no coverage, so residue is empty and no finding has access. Add coverage with coverage of the c64-knowledge skill." } : {}),
316
325
  text: list(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
@@ -80,7 +80,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
80
80
  - `constants.asm` holds the names of addresses outside the program, for example `EXTCOL = $d020`. The file puts the names in groups by memory area, for example zero page, VIC-II registers or CIA 1 registers. Each group is in address order. Drive code and banks have one group only. The side comment of a name tells what the address is. This text is the knowledge comment of the address. Without a comment, the text is the platform meaning. When the knowledge name is not the platform name, the platform name comes first. For an I/O mirror without a meaning, the text gives the register that the mirror repeats. Then `Used:` counts how the source uses the name. The kinds are `read`, `written`, `modified`, `called`, `jumped to`, `jumped through`, `branched to`, `pointer`, `address`, `table` and `macro argument`. `address` is an immediate operand expression. `table` is an entry in a table of addresses.
81
81
  - The top of `constants.asm` sets each switch to 0, for example `!ifndef CRACK { CRACK = 0 }`. A build that defines the switch as 1 uses the patches of that switch.
82
82
  - A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. A platform name wins over a name from an analyzer (`dxa`, `ghidra` or `heuristic`), for example `EXTCOL` over `ld020`. A `user` or `llm` name wins over a platform name. An address in the program without a name gets `l_xxxx`.
83
- - The source uses the names of a ROM only when the program uses that ROM. When the knowledge has coverage, a ROM is in use when a run executed code of it. A read of ROM bytes as data does not count. Without coverage, `$01` of the entry state tells which ROMs are in. When the program uses neither ROM, the zero page and pages 2 and 3 get no system names. `romNamesLeftOut` gives the ROMs that the source did not name, and the evidence.
83
+ - The source uses the names of a ROM only when the program uses that ROM. When the knowledge has coverage, a ROM is in use when a run executed code of it. A read of ROM bytes as data does not count. Without coverage, `$01` of the entry state tells which ROMs are in. When the program uses neither ROM, the zero page and pages 2 and 3 get no system names. `romNamesLeftOut` gives the ROMs that the source did not name, and the evidence. An operand expression or a macro use with such a name gets that reason.
84
84
  - `sta $d011 ; $d011: yScroll 3, rows 25, displayEnabled` is a store of an immediate to a register that the c64-memory-map skill decodes. The side comment gives the decoded value. A side comment in the knowledge replaces it.
85
85
  - Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
86
86
  - Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
@@ -34,6 +34,8 @@ export interface SourceInput {
34
34
  coverage?: Uint8Array;
35
35
  /** Platform names: the name of an address that knowledge does not name, the last address of its entry, and what it is. */
36
36
  platform?: ReadonlyArray<{ address: number; end: number; name: string; meaning?: string }>;
37
+ /** The platform names that `platform` leaves out, because the program does not use their ROM: the reason of an expression that names one tells it. */
38
+ platformLeftOut?: ReadonlySet<string>;
37
39
  /** The platform area of an address, and the register that an I/O mirror repeats: constants.asm groups and describes its names with them. */
38
40
  describe?: (address: number) => { area: Area; mirrorOf?: number };
39
41
  /** What a value in a platform register means (c64-memory-map): an immediate stored to such a register gets it as a side comment. Throws for a register without a decoder. */
@@ -355,6 +357,17 @@ export function layout(input: SourceInput): Layout {
355
357
  for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
356
358
  for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
357
359
  const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
360
+ /** Why names of an expression are not names of the source: not known, or platform names of a ROM that the program does not use. */
361
+ const unknownNames = (missing: readonly string[], remedy: string) => {
362
+ const leftOut = missing.filter((name) => input.platformLeftOut?.has(name) === true);
363
+ const unknown = missing.filter((name) => !leftOut.includes(name));
364
+ const are = (names: readonly string[]) => `${names.join(", ")} ${names.length === 1 ? "is" : "are"}`;
365
+ return [
366
+ ...(unknown.length === 0 ? [] : [`${are(unknown)} not a symbol of the knowledge or a platform name.`]),
367
+ ...(leftOut.length === 0 ? [] : [`${are(leftOut)} a platform name of a ROM that the program does not use, so the source leaves ${leftOut.length === 1 ? "it" : "them"} out (romNamesLeftOut).`]),
368
+ remedy,
369
+ ].join(" ");
370
+ };
358
371
  // A macro use is a run of whole instructions of one file, of a macro with one param for each arg, whose args name known names.
359
372
  const macrosByName = new Map((input.macros ?? []).map((macro) => [macro.name, macro]));
360
373
  const macrosAsInstructions: MacroAsInstructions[] = [];
@@ -384,7 +397,7 @@ export function layout(input: SourceInput): Layout {
384
397
  }
385
398
  const missing = [...new Set(use.args.flatMap((arg) => identifiers(arg)))].filter((name) => !byName.has(name));
386
399
  if (missing.length > 0) {
387
- fallBack(`${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not a symbol of the knowledge or a platform name. Name the address with knowledge.ts rename, or give other args with knowledge.ts use.`);
400
+ fallBack(unknownNames(missing, "Name the address with knowledge.ts rename, or give other args with knowledge.ts use."));
388
401
  continue;
389
402
  }
390
403
  candidates.push({ use, items: run as InstructionItem[] });
@@ -403,7 +416,7 @@ export function layout(input: SourceInput): Layout {
403
416
  }
404
417
  const missing = identifiers(expression).filter((name) => !byName.has(name));
405
418
  if (missing.length > 0) {
406
- operandsAsNumbers.push({ address: operand.address, expression, reason: `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not a symbol of the knowledge or a platform name. Name the address with knowledge.ts rename, or write the operand again with knowledge.ts operand.` });
419
+ operandsAsNumbers.push({ address: operand.address, expression, reason: unknownNames(missing, "Name the address with knowledge.ts rename, or write the operand again with knowledge.ts operand.") });
407
420
  continue;
408
421
  }
409
422
  // An immediate operand builds the address of a name; any other operand uses the address as the instruction does.
@@ -1020,8 +1033,9 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1020
1033
  out.push(`${label} = * + ${offset}`);
1021
1034
  result.labels.push({ address: inside, name: label, file: piece.file, line: out.length });
1022
1035
  }
1023
- for (const text of lineComments.get(inside) ?? []) sides.push(text);
1024
- if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
1036
+ // A comment at an operand byte (knowledge of an older version) shows with its address, as in the listing of c64-knowledge.
1037
+ for (const text of lineComments.get(inside) ?? []) sides.push(`${formatC64Address(inside)}: ${text}`);
1038
+ if (sideComments.has(inside)) sides.push(`${formatC64Address(inside)}: ${sideComments.get(inside)!}`);
1025
1039
  }
1026
1040
  const form = formOf(layout, item);
1027
1041
  const { mode } = OPCODES[item.opcode]!;
@@ -11,7 +11,7 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
 
13
13
  import { Refusal } from "#src/errors.ts";
14
- import { type AddressRange, COVERAGE, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
14
+ import { type AddressRange, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
15
15
  import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
16
16
  import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
17
17
  import { writeReported, type Written } from "#src/host-client/transfer.ts";
@@ -26,7 +26,7 @@ import { assemble } from "#src/native/acme.ts";
26
26
  import { analyze } from "#src/native/ghidra/analyze.ts";
27
27
  import { localToolContext } from "#src/native/local.ts";
28
28
  import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
29
- import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
29
+ import { decode as decodeRegister, describeAddress, platformNames, platformNamesFor, romUseOfProgram } from "#src/platform.ts";
30
30
  import { resolveProjectPath } from "#src/project.ts";
31
31
  import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
32
32
  import { knowledgeNames } from "#src/knowledge/names.ts";
@@ -491,15 +491,10 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
491
491
  }
492
492
  }
493
493
 
494
- /**
495
- * The ROMs the program uses: from the coverage when the knowledge has
496
- * it, else from $01 of the entry state, else both.
497
- */
498
- function romsInUse(image: ProgramImage, coverage: Uint8Array | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
499
- if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
500
- const port = (image.state?.cpuPort as Record<string, unknown> | undefined)?.$01;
501
- if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
502
- return { basic: true, kernal: true, from: "none" };
494
+ /** The platform names that `used` leaves out: the names of the ROMs that the program does not use. */
495
+ function leftOutNames(used: ReadonlyArray<{ name: string }>): Set<string> {
496
+ const kept = new Set(used.map((entry) => entry.name));
497
+ return new Set(platformNames().flatMap((entry) => (kept.has(entry.name) ? [] : [entry.name])));
503
498
  }
504
499
 
505
500
  /** The first words of every file that the script writes. */
@@ -549,7 +544,8 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
549
544
  }
550
545
  // Read the entry state before the long steps, so a bad state stops the run at once.
551
546
  const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
552
- const romUse = romsInUse(image, coverage);
547
+ const romUse = romUseOfProgram(coverage, image.state);
548
+ const platform = platformNamesFor(romUse);
553
549
  const { instructions, dataOnly } = await decode(image, knowledge, entries, coverage);
554
550
  const laid = layout({
555
551
  segments: image.segments,
@@ -567,7 +563,7 @@ async function generate(call: Call): Promise<{ output: Record<string, unknown>;
567
563
  macroUses: knowledge.macroUses,
568
564
  ...(coverage === undefined ? {} : { coverage }),
569
565
  // The platform names are C64 names: drive code and banks get none. A ROM that the program does not use gives none.
570
- ...(space === DEFAULT_SPACE ? { platform: platformNamesFor(romUse), describe: describeAddress, decode: decodeRegister } : {}),
566
+ ...(space === DEFAULT_SPACE ? { platform, platformLeftOut: leftOutNames(platform), describe: describeAddress, decode: decodeRegister } : {}),
571
567
  });
572
568
  const asBytes = await probe(laid);
573
569
  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."];
@@ -40,9 +40,10 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
40
40
 
41
41
  - Each routine is an entry point.
42
42
  - Each region that is not code stays data.
43
+ - Each symbol of the kind `data` or `variable` stays data. The region that is not code and that holds the symbol stays data, also a region of an analyzer. Without such a region, the byte of the symbol stays data. A symbol in a code region or at an executed address is not data. When the region holds an entry point or an executed address, only the byte of the symbol stays data.
43
44
  - Each name is a label. The listing, Ghidra and the decompiler show that name.
44
45
 
45
- Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
46
+ Facts that an analyzer found are not seeds alone. A new analysis can then correct or remove them.
46
47
 
47
48
  More seeds come from evidence:
48
49
 
@@ -90,9 +91,9 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
90
91
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
91
92
  - DXA or Ghidra can find data where a run executed an instruction start (`executed`), or where a `user` or `llm` symbol of kind `label` or `routine` is (`knowledge`). Then the script does not write that data region, and the earlier region of the analyzer stays. `conflicts` lists each such place. Executed code and your names have priority over the guess of an analyzer.
92
93
  - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
93
- - `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
94
+ - `candidates.pointers` gives code that makes an address from immediates. The script reads only the instructions of the code regions, from the start of each region. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair. A `(zp),y` instruction of the code uses the zero-page pair. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
94
95
  - `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
95
- - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
96
+ - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. A platform name of a ROM that the program does not use is not an arg, because the c64-source skill does not use it. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--out-macro-batch`, `batch` gives the file, the count of writes (`writes`) and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
96
97
  - When the knowledge has coverage, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
97
98
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
98
99