@henols/c64-re-tools 2.1.1 → 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 (67) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +6 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +19 -11
  4. package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
  5. package/skills/c64-basic/SKILL.md +1 -0
  6. package/skills/c64-disk/SKILL.md +1 -0
  7. package/skills/c64-disk/scripts/disk.ts +2 -2
  8. package/skills/c64-disk/scripts/lib/containers.ts +4 -4
  9. package/skills/c64-docs/SKILL.md +2 -1
  10. package/skills/c64-docs/scripts/docs.ts +1 -1
  11. package/skills/c64-docs/scripts/lib/links.ts +2 -0
  12. package/skills/c64-emulator/SKILL.md +4 -1
  13. package/skills/c64-knowledge/SKILL.md +12 -6
  14. package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
  15. package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
  16. package/skills/c64-memory-map/SKILL.md +3 -0
  17. package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
  18. package/skills/c64-provenance/SKILL.md +5 -3
  19. package/skills/c64-provenance/scripts/lib/library.ts +28 -5
  20. package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
  21. package/skills/c64-provenance/scripts/provenance.ts +22 -10
  22. package/skills/c64-source/SKILL.md +2 -1
  23. package/skills/c64-source/scripts/lib/source.ts +25 -33
  24. package/skills/c64-source/scripts/source.ts +13 -17
  25. package/skills/c64-static-analysis/SKILL.md +6 -3
  26. package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
  27. package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
  29. package/skills/c64-testing/SKILL.md +9 -5
  30. package/skills/c64-testing/references/scenario-format.md +5 -3
  31. package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
  32. package/skills/c64-testing/scripts/lib/recording.ts +3 -3
  33. package/skills/c64-testing/scripts/lib/replay.ts +1 -1
  34. package/skills/c64-testing/scripts/lib/runs.ts +45 -9
  35. package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
  36. package/skills/c64-testing/scripts/testing.ts +9 -6
  37. package/skills/c64-unpacker/SKILL.md +10 -7
  38. package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
  39. package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
  40. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  41. package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
  42. package/src/host/vice/adapter.ts +4 -1
  43. package/src/host/vice/session.ts +144 -11
  44. package/src/host-client/image.ts +27 -13
  45. package/src/host-client/steps.ts +41 -5
  46. package/src/host-client/tools.ts +4 -3
  47. package/src/host-client/transfer.ts +22 -29
  48. package/src/knowledge/history.ts +55 -2
  49. package/src/knowledge/import.ts +79 -7
  50. package/src/knowledge/program.ts +31 -2
  51. package/src/knowledge/read.ts +7 -4
  52. package/src/knowledge/schema.ts +3 -2
  53. package/src/knowledge/text.ts +1 -0
  54. package/src/knowledge/write.ts +86 -42
  55. package/src/mcp/inputs.ts +6 -3
  56. package/src/mcp/output.ts +1 -1
  57. package/src/mcp/server.ts +9 -1
  58. package/src/mcp/tools/debug.ts +31 -4
  59. package/src/mcp/tools/machine.ts +3 -1
  60. package/src/mcp/tools/media.ts +5 -1
  61. package/src/mcp/tools/memory.ts +4 -3
  62. package/src/media.ts +57 -0
  63. package/src/opcodes.ts +81 -18
  64. package/src/platform.ts +14 -1
  65. package/src/project.ts +118 -3
  66. package/src/protocol/vice.ts +4 -1
  67. package/src/script.ts +118 -5
@@ -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.
@@ -43,6 +45,7 @@ Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal numb
43
45
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
44
46
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
45
47
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
48
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
46
49
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
47
50
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
48
51
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -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 } },
@@ -71,7 +90,7 @@ await runScript({
71
90
  {
72
91
  name: "export",
73
92
  summary: "Writes the platform names as an ACME include: NAME = $xxxx.",
74
- options: { out: { ...OPTIONS.out, value: "<file>", help: "the include file, relative to the project folder", required: true } },
93
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the include file, relative to the project folder", required: true } },
75
94
  run: (call) => {
76
95
  const names = platformNames();
77
96
  const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${item.address < 0x100 ? formatC64Byte(item.address) : formatC64Address(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
@@ -56,6 +56,7 @@ With `--frames`, a PRG starts with autostart and RUN. An image starts from its b
56
56
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
57
57
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
58
58
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
59
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
59
60
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
60
61
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
61
62
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -96,7 +97,8 @@ Use `separate` when you have only one release. It reads the program, its stages
96
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.
97
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.
98
99
  - `codeRegions.total`: the number of code regions in the knowledge.
99
- - `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.
100
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.
101
103
 
102
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.
@@ -112,7 +114,7 @@ Use `leftovers` to find what the program holds in addition to its working code.
112
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. |
113
115
  | `residue` | Bytes that no run touched and that are no text, BASIC, fill or code region. `bytes` shows the first 16 bytes. |
114
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. |
115
- | `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. |
116
118
 
117
119
  Text from another computer can show the tools of the developer:
118
120
 
@@ -193,7 +195,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
193
195
 
194
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.
195
197
 
196
- 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.
197
199
 
198
200
  The runtime evidence and the leftovers are evidence only. The script does not decide which bytes are original.
199
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
  }
@@ -41,13 +41,13 @@ import {
41
41
  import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
42
42
  import { platformNames } from "#src/platform.ts";
43
43
  import { projectRoot } from "#src/project.ts";
44
- import { knowledgeNames } from "#src/knowledge/names.ts";
44
+ import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
45
45
  import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
46
46
  import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
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
 
@@ -107,14 +107,17 @@ async function compare(call: Call): Promise<unknown> {
107
107
  const shifts = new Map<number, number>();
108
108
  for (const text of call.strings("shift")) {
109
109
  const match = /^(\d+):([+-]?\d+)$/.exec(text);
110
- if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
110
+ if (match === null) throw new UsageError(`--shift must be <release number>:<bytes>, for example 2:-256, not ${text}`);
111
+ if (Number(match[1]) < 1 || Number(match[1]) > positionals.length) {
112
+ throw new UsageError(`--shift ${text}: there is no release ${match[1]}. This compare has ${positionals.length} releases: give a release number from 1 to ${positionals.length}`);
113
+ }
111
114
  if (shifts.has(Number(match[1]))) throw new UsageError(`--shift gives release ${match[1]} more than one shift (${shifts.get(Number(match[1]))} and ${match[2]}). Give one --shift for each release`);
112
115
  shifts.set(Number(match[1]), Number(match[2]));
113
116
  }
114
117
  const frames = call.count("frames");
115
118
  const stepsFile = call.string("steps");
116
119
  if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
117
- const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
120
+ const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps", knowledgeAt());
118
121
  const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
119
122
  const releases = loaded.map((item) => item.release);
120
123
  const range = call.range("range");
@@ -236,6 +239,11 @@ function programEvidence() {
236
239
  return { program, stages, coverage, found };
237
240
  }
238
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
+
239
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.";
240
248
 
241
249
  /** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
@@ -248,7 +256,8 @@ function separateCommand(call: Call): unknown {
248
256
  const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
249
257
  const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
250
258
  const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
251
- 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" });
252
261
  const rest = out === undefined ? undefined : out;
253
262
  const list = <T>(items: T[]) => listed(items, max, undefined, rest);
254
263
  const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
@@ -263,10 +272,12 @@ function separateCommand(call: Call): unknown {
263
272
  }),
264
273
  ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
265
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,
266
277
  // total counts every code region; unreachable is a list like the others.
267
278
  codeRegions: {
268
279
  total: codeRegions.length,
269
- ...(loaded.entry === undefined ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
280
+ ...(roots.length === 0 ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
270
281
  },
271
282
  ...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
272
283
  ...(written === undefined ? {} : written),
@@ -307,7 +318,8 @@ function leftoversCommand(call: Call): unknown {
307
318
  const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify(all, null, 1)}\n`, { what: "--out" });
308
319
  const list = <T>(items: T[]) => listed(items, max, undefined, out ?? "a larger --max-listed, or --out <file>");
309
320
  return {
310
- ...(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),
311
323
  ...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
312
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." } : {}),
313
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))),
@@ -462,14 +474,14 @@ const COMMANDS: CommandSpec[] = [
462
474
  {
463
475
  name: "separate",
464
476
  summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references, or the run on from the code before it, reach each code region from the entry, and what the program reads or executes outside its segments.",
465
- options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
477
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
466
478
  run: separateCommand,
467
479
  },
468
480
  {
469
481
  name: "leftovers",
470
482
  summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
471
483
  options: {
472
- out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
484
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all findings to this file" },
473
485
  "min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
474
486
  "max-listed": OPTIONS["max-listed"],
475
487
  },
@@ -492,7 +504,7 @@ const COMMANDS: CommandSpec[] = [
492
504
  summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
493
505
  options: {
494
506
  library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
495
- out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
507
+ out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
496
508
  "max-listed": OPTIONS["max-listed"],
497
509
  },
498
510
  run: matchCommand,
@@ -47,6 +47,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
47
47
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
48
48
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
49
49
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
50
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
50
51
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
51
52
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
52
53
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -79,7 +80,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
79
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.
80
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.
81
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`.
82
- - 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.
83
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.
84
85
  - Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
85
86
  - Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
@@ -13,7 +13,7 @@ import { WireFailure } from "#src/host-client/tools.ts";
13
13
  import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
14
14
  import { type CommentPlacement, isAnalyzer, isGeneratedFile, type Origin, type RegionType } from "#src/knowledge/schema.ts";
15
15
  import type { Area } from "#src/platform.ts";
16
- import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "#src/opcodes.ts";
16
+ import { hasZeroPageForm, instructionWith, labelProblem, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type UseKind } from "#src/opcodes.ts";
17
17
 
18
18
  export interface SourceInput {
19
19
  /** In address order, without overlap. */
@@ -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. */
@@ -319,7 +321,7 @@ export function layout(input: SourceInput): Layout {
319
321
  const outside = new Set<number>();
320
322
  // A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
321
323
  const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
322
- const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
324
+ const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && labelProblem(name) === undefined).map(({ address, name }) => [address, name]));
323
325
  // An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
324
326
  const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
325
327
  /** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
@@ -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.
@@ -450,7 +463,8 @@ export function layout(input: SourceInput): Layout {
450
463
  for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
451
464
  const known = symbols.get(address);
452
465
  if (known !== undefined) {
453
- if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
466
+ const problem = labelProblem(known);
467
+ if (problem !== undefined) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)}: ${problem} Rename the symbol with c64-knowledge.`);
454
468
  names.set(address, known);
455
469
  if (inImage(address)) namedLabels++;
456
470
  continue;
@@ -574,27 +588,6 @@ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: Instruc
574
588
  return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
575
589
  }
576
590
 
577
- /** The instruction text with an operand expression; the expression is a name or a $ number. */
578
- export function instructionText(opcode: number, suffix: string, expression: string): string {
579
- const { mnemonic, mode } = OPCODES[opcode]!;
580
- const operand: Record<Mode, string> = {
581
- imp: "",
582
- imm: `#${expression}`,
583
- zp: expression,
584
- zpx: `${expression},x`,
585
- zpy: `${expression},y`,
586
- abs: expression,
587
- abx: `${expression},x`,
588
- aby: `${expression},y`,
589
- ind: `(${expression})`,
590
- izx: `(${expression},x)`,
591
- izy: `(${expression}),y`,
592
- rel: expression,
593
- };
594
- const text = operand[mode];
595
- return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
596
- }
597
-
598
591
  /** The plain number of an operand: what the probe assembles, and what a comment shows. */
599
592
  export function literalOperand(opcode: number, value: number): string {
600
593
  const { mode } = OPCODES[opcode]!;
@@ -1040,13 +1033,14 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1040
1033
  out.push(`${label} = * + ${offset}`);
1041
1034
  result.labels.push({ address: inside, name: label, file: piece.file, line: out.length });
1042
1035
  }
1043
- for (const text of lineComments.get(inside) ?? []) sides.push(text);
1044
- 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)!}`);
1045
1039
  }
1046
1040
  const form = formOf(layout, item);
1047
1041
  const { mode } = OPCODES[item.opcode]!;
1048
1042
  const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
1049
- const literal = instructionText(item.opcode, "", literalOperand(item.opcode, value));
1043
+ const literal = instructionWith(item.opcode, literalOperand(item.opcode, value));
1050
1044
  let written = layout.operands.get(item.address);
1051
1045
  // An expression that is not written stays visible as a side comment.
1052
1046
  const asNumber = (expression: string, reason: string) => {
@@ -1062,13 +1056,11 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
1062
1056
  out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
1063
1057
  result.asBytes.push({ address: item.address, text: literal });
1064
1058
  } else if (written !== undefined) {
1065
- // ACME reads an operand in brackets as indirect, so a non-immediate expression does not start with one.
1066
- const expression = mode !== "imm" && written.startsWith("(") ? `0+${written}` : written;
1067
- out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1059
+ out.push(withSide(`${INDENT}${instructionWith(item.opcode, written, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
1068
1060
  result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
1069
1061
  } else {
1070
1062
  const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
1071
- out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
1063
+ out.push(withSide(`${INDENT}${instructionWith(item.opcode, expression, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
1072
1064
  }
1073
1065
  mapLine(item.address, item.address + item.length - 1);
1074
1066
  result.instructions++;