@henols/c64-re-tools 2.0.1 → 2.1.0-rc.2

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 (104) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +24 -9
  4. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  5. package/skills/c64-basic/SKILL.md +17 -9
  6. package/skills/c64-basic/scripts/basic.ts +26 -36
  7. package/skills/c64-disk/SKILL.md +17 -7
  8. package/skills/c64-disk/scripts/disk.ts +130 -104
  9. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  10. package/skills/c64-docs/SKILL.md +47 -28
  11. package/skills/c64-docs/references/style.md +6 -6
  12. package/skills/c64-docs/scripts/docs.ts +323 -0
  13. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  14. package/skills/c64-emulator/SKILL.md +11 -7
  15. package/skills/c64-knowledge/SKILL.md +34 -21
  16. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  17. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  18. package/skills/c64-memory-map/SKILL.md +18 -10
  19. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  20. package/skills/c64-provenance/SKILL.md +82 -26
  21. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  22. package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
  23. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  24. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  25. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  26. package/skills/c64-provenance/scripts/provenance.ts +222 -252
  27. package/skills/c64-reverse-engineering/SKILL.md +18 -15
  28. package/skills/c64-source/SKILL.md +28 -16
  29. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  30. package/skills/c64-source/scripts/lib/source.ts +33 -42
  31. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  32. package/skills/c64-static-analysis/SKILL.md +24 -13
  33. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  34. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  35. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  36. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  37. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  38. package/skills/c64-testing/SKILL.md +94 -19
  39. package/skills/c64-testing/references/recording-format.md +125 -0
  40. package/skills/c64-testing/references/scenario-format.md +24 -19
  41. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  42. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  43. package/skills/c64-testing/scripts/lib/play.ts +334 -0
  44. package/skills/c64-testing/scripts/lib/recording.ts +662 -0
  45. package/skills/c64-testing/scripts/lib/replay.ts +105 -0
  46. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  47. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  48. package/skills/c64-testing/scripts/testing.ts +189 -0
  49. package/skills/c64-unpacker/SKILL.md +42 -25
  50. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  51. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  52. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  53. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  54. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  55. package/src/c64.ts +35 -20
  56. package/src/cli/main.ts +2 -2
  57. package/src/errors.ts +38 -0
  58. package/src/graphics.ts +3 -1
  59. package/src/host/server.ts +3 -2
  60. package/src/host/vice/session.ts +4 -4
  61. package/src/host-client/entry-state.ts +106 -0
  62. package/src/host-client/image.ts +21 -10
  63. package/src/host-client/steps.ts +285 -108
  64. package/src/host-client/transfer.ts +21 -15
  65. package/src/host-client/vice-session.ts +1 -1
  66. package/src/knowledge/database.ts +3 -23
  67. package/src/knowledge/expression.ts +8 -11
  68. package/src/knowledge/history.ts +2 -2
  69. package/src/knowledge/import.ts +13 -14
  70. package/src/knowledge/names.ts +50 -0
  71. package/src/knowledge/program.ts +11 -10
  72. package/src/knowledge/read.ts +16 -21
  73. package/src/knowledge/schema.ts +9 -3
  74. package/src/knowledge/text.ts +5 -5
  75. package/src/knowledge/write.ts +93 -137
  76. package/src/mcp/inputs.ts +105 -0
  77. package/src/mcp/output.ts +2 -13
  78. package/src/mcp/server.ts +12 -60
  79. package/src/mcp/tools/debug.ts +17 -12
  80. package/src/mcp/tools/execution.ts +5 -32
  81. package/src/mcp/tools/input.ts +7 -32
  82. package/src/mcp/tools/media.ts +6 -5
  83. package/src/mcp/tools/memory.ts +11 -38
  84. package/src/mcp/tools/video.ts +6 -5
  85. package/src/native/c64run.ts +162 -0
  86. package/src/native/discover.ts +8 -0
  87. package/src/native/dxa.ts +7 -6
  88. package/src/native/ghidra/analyze.ts +2 -1
  89. package/src/native/ghidra/index.ts +2 -1
  90. package/src/native/processes.ts +4 -3
  91. package/src/native/status.ts +25 -4
  92. package/src/native/types.ts +3 -7
  93. package/src/opcodes.ts +13 -13
  94. package/src/platform.ts +9 -9
  95. package/src/project.ts +16 -26
  96. package/src/protocol/messages.ts +3 -21
  97. package/src/protocol/vice.ts +7 -7
  98. package/src/script.ts +491 -49
  99. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  100. package/skills/c64-docs/scripts/generate.ts +0 -109
  101. package/skills/c64-docs/scripts/link.ts +0 -239
  102. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  103. package/skills/c64-testing/scripts/checklist.ts +0 -51
  104. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -0,0 +1,442 @@
1
+ // Leftovers in one program for the c64-provenance script: text in PETSCII,
2
+ // screen codes, ASCII or text with bit 7 set (Apple II), assembler source
3
+ // lines, BASIC lines away from the BASIC start, fills, untouched
4
+ // bytes outside code regions that no other finding explains, exact copies of
5
+ // a byte range, and code that no call or jump reaches and no run executed. Each finding gets the stages and the accesses of
6
+ // its bytes from the separation. Evidence only: nothing here decides what a
7
+ // leftover is or who made it.
8
+
9
+ import { type AddressRange, basicLineText } from "#src/c64.ts";
10
+ import { MNEMONICS } from "#src/opcodes.ts";
11
+ import type { Access, Separation } from "./separate.ts";
12
+
13
+ export interface LeftoversInput {
14
+ segments: AddressRange[];
15
+ /** 64 KiB with the program bytes at their addresses. */
16
+ memory: Uint8Array;
17
+ separation: Separation;
18
+ /** The first address of the BASIC start, which RUN lists: it is no leftover. */
19
+ basicStart?: number;
20
+ /** The fewest characters of a text (default 8). */
21
+ minText?: number;
22
+ }
23
+
24
+ /** What the separation tells about the bytes of a finding. */
25
+ export interface Placement {
26
+ /** The stages that wrote bytes of the finding, absent when no stage did. */
27
+ stages?: number[];
28
+ /** The accesses of its bytes in the coverage, absent without coverage. */
29
+ access?: Access[];
30
+ }
31
+
32
+ /** high-bit is text with bit 7 set: ASCII as the Apple II keeps it, or C64 shifted letters with $a0 spaces. Its spaces can also be $20. */
33
+ export type TextEncoding = "petscii" | "screen" | "ascii" | "high-bit";
34
+
35
+ export interface TextFinding extends AddressRange, Placement {
36
+ encoding: TextEncoding;
37
+ /** Screen codes with bit 7 set: the characters show reversed. */
38
+ reversed?: boolean;
39
+ /** The text; a line end is "\n". */
40
+ text: string;
41
+ /** Lines of assembler source: most lines are an instruction, a data directive or a comment. */
42
+ source?: true;
43
+ /** Words that are 6502 mnemonics before an operand or a line end. */
44
+ mnemonics?: number;
45
+ }
46
+
47
+ export interface BasicFinding extends AddressRange, Placement {
48
+ /** The address of the first line where the links put it: where the lines were when BASIC linked them. */
49
+ linkedAt: number;
50
+ lines: string[];
51
+ }
52
+
53
+ export interface FillFinding extends AddressRange, Placement {
54
+ pattern: number[];
55
+ }
56
+
57
+ /** Untouched bytes that are no text, BASIC, fill or code region. */
58
+ export type ResidueFinding = AddressRange & Placement;
59
+
60
+ export interface CopyFinding extends AddressRange, Placement {
61
+ /** The first address of the same bytes earlier in the program. */
62
+ copyOf: number;
63
+ }
64
+
65
+ /** A code region that no call or jump reaches from the entry, and that no run executed. */
66
+ export interface DeadCodeFinding extends AddressRange, Placement {
67
+ type: string;
68
+ /** false when the coverage shows that it did not run; absent without coverage. */
69
+ executed?: false;
70
+ }
71
+
72
+ export interface Leftovers {
73
+ text: TextFinding[];
74
+ basic: BasicFinding[];
75
+ fill: FillFinding[];
76
+ residue: ResidueFinding[];
77
+ copies: CopyFinding[];
78
+ deadCode: DeadCodeFinding[];
79
+ }
80
+
81
+ const MIN_TEXT = 8;
82
+ /** A fill of one byte has at least this many bytes; a pattern of two to four bytes twice this many. */
83
+ const MIN_FILL = 16;
84
+ const MIN_RESIDUE = 16;
85
+ const MIN_COPY = 32;
86
+ /** A copy has at least this many different byte values in its first MIN_COPY bytes: no table of a few values. */
87
+ const MIN_COPY_VALUES = 8;
88
+ const MAX_BASIC_LINE = 63999;
89
+
90
+ /** A character of a text, and its letter class: "a" is one case, "b" the other. */
91
+ interface Character {
92
+ /** The character when the class "b" is absent from the run. */
93
+ plain: string;
94
+ /** The character when the run has class "b": the second character set. */
95
+ mixed: string;
96
+ letter?: "a" | "b";
97
+ /** A line end. */
98
+ end?: true;
99
+ }
100
+
101
+ const LINE_END: Character = { plain: "\n", mixed: "\n", end: true };
102
+
103
+ /**
104
+ * PETSCII: $41-$5a is class a, the shifted letters $c1-$da class b. The
105
+ * copies $61-$7a are no letters here: text seldom uses them, and a stray one
106
+ * in front of a text would put the text in the second character set.
107
+ */
108
+ function petsciiCharacter(byte: number): Character | undefined {
109
+ if (byte >= 0x41 && byte <= 0x5a) {
110
+ const upper = String.fromCharCode(byte);
111
+ return { plain: upper, mixed: upper.toLowerCase(), letter: "a" };
112
+ }
113
+ if (byte >= 0xc1 && byte <= 0xda) {
114
+ const upper = String.fromCharCode(byte - 0x80);
115
+ return { plain: upper, mixed: upper, letter: "b" };
116
+ }
117
+ const sign = byte >= 0x20 && byte <= 0x40 ? String.fromCharCode(byte) : { 0x5b: "[", 0x5c: "£", 0x5d: "]", 0x5e: "↑", 0x5f: "←" }[byte];
118
+ return sign === undefined ? undefined : { plain: sign, mixed: sign };
119
+ }
120
+
121
+ /** Screen codes: $01-$1a is class a, $41-$5a (the upper case of the second set) class b. $00 ends a text: it is a common end mark. */
122
+ function screenCharacter(byte: number): Character | undefined {
123
+ if (byte >= 0x01 && byte <= 0x1a) {
124
+ const upper = String.fromCharCode(byte + 0x40);
125
+ return { plain: upper, mixed: upper.toLowerCase(), letter: "a" };
126
+ }
127
+ if (byte >= 0x41 && byte <= 0x5a) {
128
+ const upper = String.fromCharCode(byte);
129
+ return { plain: upper, mixed: upper, letter: "b" };
130
+ }
131
+ const sign = byte >= 0x20 && byte <= 0x3f ? String.fromCharCode(byte) : { 0x1b: "[", 0x1c: "£", 0x1d: "]", 0x1e: "↑", 0x1f: "←" }[byte];
132
+ return sign === undefined ? undefined : { plain: sign, mixed: sign };
133
+ }
134
+
135
+ /** ASCII: small letters are class a, capitals class b, and each shows as it is. $0a and $0d end a line. */
136
+ function asciiCharacter(byte: number): Character | undefined {
137
+ if (byte === 0x0a || byte === 0x0d) return LINE_END;
138
+ if (byte < 0x20 || byte > 0x7e) return undefined;
139
+ const character = String.fromCharCode(byte);
140
+ return { plain: character, mixed: character, ...(byte >= 0x61 && byte <= 0x7a ? { letter: "a" as const } : byte >= 0x41 && byte <= 0x5a ? { letter: "b" as const } : {}) };
141
+ }
142
+
143
+ /** ASCII with bit 7 set: $8d ends a line, and $20 is a space too, as in the comments of Apple II assembler source. */
144
+ function highAsciiCharacter(byte: number): Character | undefined {
145
+ if (byte === 0x8d) return LINE_END;
146
+ if (byte === 0x20) return { plain: " ", mixed: " " };
147
+ return byte >= 0xa0 && byte <= 0xfe ? asciiCharacter(byte - 0x80) : undefined;
148
+ }
149
+
150
+ /** PETSCII with $0d as a line end. */
151
+ function petsciiOrLineEnd(byte: number): Character | undefined {
152
+ return byte === 0x0d ? LINE_END : petsciiCharacter(byte);
153
+ }
154
+
155
+ const COMMON_SIGNS = /[ 0-9.,!?:;'"()\-/&+*=#$%]/;
156
+ const VOWELS = /[AEIOUY]/i;
157
+
158
+ /**
159
+ * Whether a run of characters reads as text: enough letters, at most one odd
160
+ * sign, a word of three letters, each word in one case or a capital and then
161
+ * small letters, and vowels in the words. Bytes of code or data seldom pass
162
+ * all of these. Characters that count up or repeat in a short pattern are a
163
+ * character map of a screen, not text.
164
+ */
165
+ function readsAsText(characters: Character[], min: number): boolean {
166
+ if (characters.length < min) return false;
167
+ const letters = characters.filter((character) => character.letter !== undefined);
168
+ if (letters.length < 4) return false;
169
+ if (characters.filter((character) => character.letter === undefined && !COMMON_SIGNS.test(character.plain)).length > 1) return false;
170
+ if (new Set(characters.map((character) => character.plain)).size < 4) return false;
171
+ if (letters.filter((character) => VOWELS.test(character.plain)).length < letters.length * 0.2) return false;
172
+ const codes = characters.map((character) => character.plain.charCodeAt(0));
173
+ const pairs = codes.length - 1;
174
+ if (codes.slice(1).filter((code, index) => code === codes[index]! + 1).length >= pairs / 2) return false;
175
+ for (let period = 2; period <= 4; period++) if (codes.slice(period).filter((code, index) => code === codes[index]).length >= (codes.length - period) / 2) return false;
176
+ let longest = 0;
177
+ let word: Character[] = [];
178
+ const wordFits = () => {
179
+ const rest = word.slice(1);
180
+ const sameCase = rest.every((character) => character.letter === word[0]!.letter) || (word[0]!.letter === "b" && rest.every((character) => character.letter === "a"));
181
+ return sameCase && (word.length < 4 || word.some((character) => VOWELS.test(character.plain)));
182
+ };
183
+ for (const character of [...characters, { plain: " ", mixed: " " } as Character]) {
184
+ if (character.letter !== undefined) {
185
+ word.push(character);
186
+ continue;
187
+ }
188
+ if (word.length > 0 && !wordFits()) return false;
189
+ longest = Math.max(longest, word.length);
190
+ word = [];
191
+ }
192
+ return longest >= 3;
193
+ }
194
+
195
+ /** The undocumented opcodes: words such as LAX or ISC in a text are seldom source. */
196
+ const UNDOCUMENTED = new Set(["jam", "slo", "rla", "sre", "rra", "sax", "lax", "dcp", "isc", "anc", "asr", "arr", "ane", "sha", "tas", "shy", "shx", "lxa", "las", "sbx"]);
197
+
198
+ /** Counts the words that are a documented 6502 mnemonic followed by an operand sign or the end of the text. */
199
+ function mnemonicCount(text: string): number {
200
+ return [...text.toLowerCase().matchAll(/(?:^|[^a-z])([a-z]{3})(?= ?[#$(<>%]| *$)/g)].filter((match) => MNEMONICS.has(match[1]!) && !UNDOCUMENTED.has(match[1]!)).length;
201
+ }
202
+
203
+ const DIRECTIVES = ["dfb", "db", "dw", "ddb", "dc", "dcb", "ds", "dfs", "byte", "word", "hex", "asc", "dci", "str", "txt", "text", "org", "equ", "epz", "end"];
204
+
205
+ /** A line of assembler source: a comment, or an optional label and then an instruction or a data directive. */
206
+ function sourceLine(line: string): boolean {
207
+ if (/^[*;]/.test(line)) return true;
208
+ const words = line.toLowerCase().split(/\s+/);
209
+ const first = words[0]!.replace(/^[.!]/, "");
210
+ const second = (words[1] ?? "").replace(/^[.!]/, "");
211
+ const known = (word: string) => (MNEMONICS.has(word) && !UNDOCUMENTED.has(word)) || DIRECTIVES.includes(word);
212
+ return known(first) || (/^[a-z_][\w.]*:?$/.test(first) && known(second));
213
+ }
214
+
215
+ /** Whether the bytes of a run show the encoding and no other one: the other scans report the rest. */
216
+ function ownEncoding(encoding: TextEncoding, bytes: number[], characters: Character[]): boolean {
217
+ if (encoding === "screen") return characters.some((character) => character.letter === "a");
218
+ // ASCII text is in capitals, or mostly in small letters: a few small letters are stray bytes at the edges of another text.
219
+ const letters = characters.filter((character) => character.letter !== undefined).length;
220
+ const small = characters.filter((character) => character.letter === "a").length;
221
+ if (small > 0 && small * 2 < letters) return false;
222
+ // Capitals and signs alone are PETSCII too.
223
+ if (encoding === "ascii") return bytes.some((byte) => byte >= 0x61 && byte <= 0x7a);
224
+ // Shifted letters alone are PETSCII too: a space, a digit or a sign with bit 7 shows ASCII.
225
+ if (encoding === "high-bit") return bytes.some((byte) => byte >= 0xa0 && byte <= 0xc0);
226
+ return true;
227
+ }
228
+
229
+ /** The runs of one encoding in a segment that read as text. `reversed` reads the bytes with bit 7 set, without that bit. */
230
+ function textRuns(memory: Uint8Array, segment: AddressRange, encoding: TextEncoding, reversed: boolean, min: number): TextFinding[] {
231
+ const decode = { petscii: petsciiOrLineEnd, screen: screenCharacter, ascii: asciiCharacter, "high-bit": highAsciiCharacter }[encoding];
232
+ const found: TextFinding[] = [];
233
+ let run: Array<{ address: number; character: Character }> = [];
234
+ const solid = (character: Character) => character.letter !== undefined || /[0-9]/.test(character.plain);
235
+ /** Reports a piece of the run as text, from its first to its last solid character, when it reads as text. */
236
+ const report = (piece: typeof run, asSource: boolean) => {
237
+ let first = 0;
238
+ let last = piece.length - 1;
239
+ const edge = asSource ? (character: Character) => character.end !== true && character.plain.trim() !== "" : solid;
240
+ while (first <= last && !edge(piece[first]!.character)) first++;
241
+ while (last >= first && !edge(piece[last]!.character)) last--;
242
+ const kept = piece.slice(first, last + 1);
243
+ const characters = kept.map((item) => item.character);
244
+ if (!(asSource ? characters.length >= min : readsAsText(characters, min))) return;
245
+ if (!ownEncoding(encoding, kept.map((item) => memory[item.address]!), characters)) return;
246
+ const mixed = encoding !== "ascii" && encoding !== "high-bit" && characters.some((character) => character.letter === "b");
247
+ const text = characters.map((character) => (mixed ? character.mixed : character.plain)).join("");
248
+ const mnemonics = mnemonicCount(text);
249
+ found.push({ start: kept[0]!.address, end: kept.at(-1)!.address, encoding, ...(reversed ? { reversed } : {}), text, ...(asSource ? { source: true as const } : {}), ...(mnemonics > 0 ? { mnemonics } : {}) });
250
+ };
251
+ const close = () => {
252
+ const lines = run.map((item) => item.character.plain).join("").split("\n").map((line) => line.trim()).filter((line) => line !== "");
253
+ const source = lines.filter(sourceLine).length;
254
+ if (lines.length >= 2 && source >= 2 && source * 2 >= lines.length) report(run, true);
255
+ else {
256
+ // Not source: each line is a text of its own.
257
+ let piece: typeof run = [];
258
+ for (const item of [...run, { address: -1, character: LINE_END }]) {
259
+ if (item.character.end !== true) piece.push(item);
260
+ else {
261
+ report(piece, false);
262
+ piece = [];
263
+ }
264
+ }
265
+ }
266
+ run = [];
267
+ };
268
+ for (let address = segment.start; address <= segment.end; address++) {
269
+ const byte = memory[address]!;
270
+ const character = reversed ? (byte & 0x80 ? decode(byte & 0x7f) : undefined) : byte & 0x80 && encoding === "screen" ? undefined : decode(byte);
271
+ if (character === undefined) close();
272
+ else run.push({ address, character });
273
+ }
274
+ close();
275
+ return found;
276
+ }
277
+
278
+ export function findText(memory: Uint8Array, segments: AddressRange[], min = MIN_TEXT): TextFinding[] {
279
+ const scan = (encoding: TextEncoding, reversed = false) => segments.flatMap((segment) => textRuns(memory, segment, encoding, reversed, min));
280
+ // A run that is mostly a run of an earlier list is the same text, read in another encoding, with stray bytes at its edges.
281
+ const overlap = (a: AddressRange, b: AddressRange) => Math.max(0, Math.min(a.end, b.end) - Math.max(a.start, b.start) + 1);
282
+ const kept: TextFinding[] = [];
283
+ for (const list of [[...scan("high-bit"), ...scan("ascii")], scan("petscii"), [...scan("screen"), ...scan("screen", true)]]) {
284
+ kept.push(...list.filter((run) => !kept.some((other) => overlap(run, other) * 2 > run.end - run.start + 1)));
285
+ }
286
+ return kept.sort((a, b) => a.start - b.start);
287
+ }
288
+
289
+ /** Whether tokenized BASIC bytes hold only keywords, signs, digits and letters outside quotes. */
290
+ function tokensFit(tokens: Uint8Array): boolean {
291
+ let literal = false;
292
+ for (const byte of tokens) {
293
+ if (byte === 0x22) literal = !literal;
294
+ else if (!literal && !(byte >= 0x20 && byte <= 0x5d) && !(byte >= 0x80 && byte <= 0xcb) && byte !== 0xff) return false;
295
+ }
296
+ return true;
297
+ }
298
+
299
+ /**
300
+ * BASIC lines with links that agree with each other: the distance between two
301
+ * links is the length of a line. Two lines or more, each with a keyword, make
302
+ * a finding. The links give the address where BASIC put the lines, which can
303
+ * be another address than where they are now.
304
+ */
305
+ export function findBasic(memory: Uint8Array, segments: AddressRange[], basicStart?: number): BasicFinding[] {
306
+ const found: BasicFinding[] = [];
307
+ for (const segment of segments) {
308
+ const word = (at: number) => memory[at]! | (memory[at + 1]! << 8);
309
+ /** The line at `at`: its number, its tokens and the address after its end zero, or undefined. */
310
+ const lineAt = (at: number) => {
311
+ if (at + 4 > segment.end) return undefined;
312
+ const number = word(at + 2);
313
+ if (number > MAX_BASIC_LINE) return undefined;
314
+ let end = at + 4;
315
+ while (end <= segment.end && end - at <= 255 && memory[end] !== 0) end++;
316
+ if (end > segment.end || memory[end] !== 0 || end === at + 4) return undefined;
317
+ const tokens = memory.slice(at + 4, end);
318
+ return tokensFit(tokens) ? { number, tokens, next: end + 1 } : undefined;
319
+ };
320
+ for (let at = segment.start; at + 4 <= segment.end; at++) {
321
+ const first = lineAt(at);
322
+ if (first === undefined || (memory[at + 1] ?? 0) === 0) continue;
323
+ const linkedAt = word(at) - (first.next - at);
324
+ if (linkedAt < 0x0400) continue;
325
+ const lines = [first];
326
+ let link = word(at);
327
+ let next = first.next;
328
+ for (;;) {
329
+ if (next + 1 > segment.end || memory[next + 1] === 0) break;
330
+ const line = lineAt(next);
331
+ if (line === undefined || line.number <= lines.at(-1)!.number || word(next) - link !== line.next - next) break;
332
+ lines.push(line);
333
+ link = word(next);
334
+ next = line.next;
335
+ }
336
+ const endMark = next + 1 <= segment.end && memory[next] === 0 && memory[next + 1] === 0;
337
+ if (lines.length < 2 || !lines.every((line) => line.tokens.some((byte) => byte >= 0x80 && byte <= 0xcb))) continue;
338
+ if (at === basicStart && linkedAt === basicStart) {
339
+ at = next - 1;
340
+ continue;
341
+ }
342
+ found.push({ start: at, end: endMark ? next + 1 : next - 1, linkedAt, lines: lines.map(basicLineText) });
343
+ at = endMark ? next + 1 : next - 1;
344
+ }
345
+ }
346
+ return found;
347
+ }
348
+
349
+ /** Runs of one byte of at least MIN_FILL bytes, and runs of a pattern of two to four bytes of at least twice that. */
350
+ export function findFills(memory: Uint8Array, segments: AddressRange[]): FillFinding[] {
351
+ const found: FillFinding[] = [];
352
+ for (const segment of segments) {
353
+ for (let at = segment.start; at <= segment.end; ) {
354
+ let best = { period: 0, length: 0 };
355
+ for (let period = 1; period <= 4; period++) {
356
+ let end = at + period;
357
+ while (end <= segment.end && memory[end] === memory[end - period]) end++;
358
+ const length = end - at;
359
+ const pattern = memory.subarray(at, at + period);
360
+ // A pattern that is one byte repeated is the fill of period 1.
361
+ if (period > 1 && pattern.every((byte) => byte === pattern[0])) continue;
362
+ if (length >= (period === 1 ? MIN_FILL : 2 * MIN_FILL) && length > best.length) best = { period, length };
363
+ }
364
+ if (best.length === 0) {
365
+ at++;
366
+ continue;
367
+ }
368
+ found.push({ start: at, end: at + best.length - 1, pattern: [...memory.subarray(at, at + best.period)] });
369
+ at += best.length;
370
+ }
371
+ }
372
+ return found;
373
+ }
374
+
375
+ /** Exact copies of at least MIN_COPY bytes of an earlier range, with enough different byte values to be no fill. */
376
+ export function findCopies(memory: Uint8Array, segments: AddressRange[]): CopyFinding[] {
377
+ const found: CopyFinding[] = [];
378
+ const seen = new Map<string, number>();
379
+ const inside = (address: number) => segments.some((segment) => address >= segment.start && address <= segment.end);
380
+ for (const segment of segments) {
381
+ for (let at = segment.start; at + MIN_COPY - 1 <= segment.end; at++) {
382
+ const window = memory.subarray(at, at + MIN_COPY);
383
+ if (new Set(window).size < MIN_COPY_VALUES) continue;
384
+ const key = Buffer.from(window).toString("latin1");
385
+ const earlier = seen.get(key);
386
+ if (earlier === undefined) {
387
+ seen.set(key, at);
388
+ continue;
389
+ }
390
+ let length = MIN_COPY;
391
+ while (at + length <= segment.end && inside(earlier + length) && memory[at + length] === memory[earlier + length]) length++;
392
+ found.push({ start: at, end: at + length - 1, copyOf: earlier });
393
+ // The windows of the copy stay unseen: a later copy points to the first range, not to this one.
394
+ at += length - 1;
395
+ }
396
+ }
397
+ return found;
398
+ }
399
+
400
+ /** Splits `range` at the parts that `taken` covers, and keeps the pieces of at least `min` bytes. */
401
+ function without(range: AddressRange, taken: AddressRange[], min: number): AddressRange[] {
402
+ const pieces: AddressRange[] = [];
403
+ let start = range.start;
404
+ for (const part of taken.filter((item) => item.start <= range.end && item.end >= range.start).sort((a, b) => a.start - b.start)) {
405
+ if (part.start > start) pieces.push({ start, end: part.start - 1 });
406
+ start = Math.max(start, part.end + 1);
407
+ }
408
+ if (start <= range.end) pieces.push({ start, end: range.end });
409
+ return pieces.filter((piece) => piece.end - piece.start + 1 >= min);
410
+ }
411
+
412
+ export function findLeftovers(input: LeftoversInput): Leftovers {
413
+ const { memory, segments, separation } = input;
414
+ const ranges = separation.ranges;
415
+ const placement = (range: AddressRange): Placement => {
416
+ const parts = ranges.filter((part) => part.start <= range.end && part.end >= range.start);
417
+ const stages = [...new Set(parts.flatMap((part) => (part.stage === undefined ? [] : [part.stage])))].sort((a, b) => a - b);
418
+ const access = [...new Set(parts.flatMap((part) => (part.access === undefined ? [] : [part.access])))];
419
+ return { ...(stages.length === 0 ? {} : { stages }), ...(access.length === 0 ? {} : { access }) };
420
+ };
421
+ const placed = <T extends AddressRange>(items: T[]) => items.map((item) => ({ ...item, ...placement(item) }));
422
+
423
+ const text = findText(memory, segments, input.minText);
424
+ const basic = findBasic(memory, segments, input.basicStart);
425
+ const fill = findFills(memory, segments);
426
+ // Untouched bytes that no other finding explains.
427
+ const untouched: AddressRange[] = [];
428
+ for (const part of ranges) {
429
+ if (part.access !== "untouched") continue;
430
+ const last = untouched.at(-1);
431
+ if (last !== undefined && last.end + 1 === part.start) last.end = part.end;
432
+ else untouched.push({ start: part.start, end: part.end });
433
+ }
434
+ // Untouched code is code that the runs did not get to, or dead code, which has its own list.
435
+ const explained = [...text, ...basic, ...fill, ...separation.codeRegions];
436
+ const residue = untouched.flatMap((range) => without(range, explained, MIN_RESIDUE));
437
+ // The references miss IRQ vectors and jumps through tables: code that ran is not dead.
438
+ const deadCode: DeadCodeFinding[] = separation.codeRegions
439
+ .filter((region) => region.reachable === false && region.executed !== true)
440
+ .map((region) => ({ start: region.start, end: region.end, type: region.type, ...(region.executed === false ? { executed: false as const } : {}) }));
441
+ return { text: placed(text), basic: placed(basic), fill: placed(fill), residue: placed(residue), copies: placed(findCopies(memory, segments)), deadCode: placed(deadCode) };
442
+ }
@@ -7,10 +7,11 @@
7
7
  import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join, resolve } from "node:path";
9
9
 
10
+ import { Refusal } from "#src/errors.ts";
10
11
  import { formatC64Address, instructionLength } from "#src/c64.ts";
11
- import { evaluate, ExpressionError, identifiers, operandText } from "#src/knowledge/expression.ts";
12
+ import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
12
13
  import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
13
- import { COMMENT_PLACEMENTS, OPERAND_SELECTS, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type OperandSelect, type ReferenceKind, type RegionType, type SymbolKind } from "#src/knowledge/schema.ts";
14
+ 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";
14
15
  import { PROJECT_DIRECTORY } from "#src/project.ts";
15
16
 
16
17
  /** A routine shorter than this is not saved and not searched: its bytes occur by chance. */
@@ -22,17 +23,6 @@ const ROUTINE_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
22
23
  /** Bytes on each line of a routine file, so that git shows a small diff. */
23
24
  const BYTES_PER_LINE = 16;
24
25
 
25
- /** A refusal of the library rules, with a stable code. */
26
- export class LibraryError extends Error {
27
- override name = "LibraryError";
28
- readonly code: "invalid-input" | "not-found" | "conflict";
29
-
30
- constructor(code: "invalid-input" | "not-found" | "conflict", message: string) {
31
- super(message);
32
- this.code = code;
33
- }
34
- }
35
-
36
26
  /**
37
27
  * Bytes that move with the routine: a word, or the low or high byte, of the
38
28
  * address at `value` (an offset from the routine start; -1 for the RTS entry
@@ -110,9 +100,9 @@ export interface LeftOut {
110
100
  export function routineFrom(source: RoutineSource): { routine: Routine; leftOut: LeftOut[] } {
111
101
  const { start, end } = source;
112
102
  const where = (address: number) => formatC64Address(address);
113
- if (!ROUTINE_NAME.test(source.name)) throw new LibraryError("invalid-input", `A routine name is a letter or underscore, then letters, digits or underscores (at most 64), not ${source.name}.`);
103
+ if (!ROUTINE_NAME.test(source.name)) throw new Refusal("invalid-input", `A routine name is a letter or underscore, then letters, digits or underscores (at most 64), not ${source.name}.`);
114
104
  const length = end - start + 1;
115
- if (length < MIN_ROUTINE) throw new LibraryError("invalid-input", `${where(start)}-${where(end)} has ${length} bytes. A routine has at least ${MIN_ROUTINE}, because shorter byte sequences occur by chance.`);
105
+ if (length < MIN_ROUTINE) throw new Refusal("invalid-input", `${where(start)}-${where(end)} has ${length} bytes. A routine has at least ${MIN_ROUTINE}, because shorter byte sequences occur by chance.`);
116
106
  const inside = (address: number) => address >= start && address <= end;
117
107
  const bytes = Uint8Array.from({ length }, (_, index) => source.byteAt(start + index));
118
108
  const leftOut: LeftOut[] = [];
@@ -125,11 +115,11 @@ export function routineFrom(source: RoutineSource): { routine: Routine; leftOut:
125
115
  for (const region of codeRegions) {
126
116
  for (let at = Math.max(region.start, next); at <= Math.min(region.end, end); at = next) {
127
117
  next = at + instructionLength(bytes[at - start]!);
128
- if (next - 1 > end) throw new LibraryError("invalid-input", `The instruction at ${where(at)} runs past the end ${where(end)} of the routine. Give an end after it.`);
118
+ if (next - 1 > end) throw new Refusal("invalid-input", `The instruction at ${where(at)} runs past the end ${where(end)} of the routine. Give an end after it.`);
129
119
  instructions.push(at - start);
130
120
  }
131
121
  }
132
- if (instructions.length === 0) throw new LibraryError("invalid-input", `The knowledge has no code region in ${where(start)}-${where(end)}. Run c64-static-analysis first, or classify the code.`);
122
+ if (instructions.length === 0) throw new Refusal("invalid-input", `The knowledge has no code region in ${where(start)}-${where(end)}. Run c64-static-analysis first, or classify the code.`);
133
123
 
134
124
  // The operands: an address inside the routine moves with it; one outside may differ.
135
125
  const relocations = new Map<number, Relocation>();
@@ -246,7 +236,7 @@ export function routineText(routine: Routine): string {
246
236
  /** Reads the text of a routine file. Throws LibraryError for a file that is no routine. */
247
237
  export function parseRoutine(text: string, file: string): Routine {
248
238
  const fail = (what: string): never => {
249
- throw new LibraryError("invalid-input", `${file} is not a routine file of the library: ${what}.`);
239
+ throw new Refusal("invalid-input", `${file} is not a routine file of the library: ${what}.`);
250
240
  };
251
241
  let value: Record<string, unknown>;
252
242
  try {
@@ -305,15 +295,15 @@ export function parseRoutine(text: string, file: string): Routine {
305
295
  * `create`, a missing folder is made.
306
296
  */
307
297
  export function libraryFolder(folder: string, options: { create: boolean; cwd?: string }): string {
308
- if (folder.length === 0) throw new LibraryError("invalid-input", "The library folder must not be empty.");
298
+ if (folder.length === 0) throw new Refusal("invalid-input", "The library folder must not be empty.");
309
299
  const path = resolve(options.cwd ?? process.cwd(), folder);
310
300
  const stat = statSync(path, { throwIfNoEntry: false });
311
301
  if (stat === undefined) {
312
- if (!options.create) throw new LibraryError("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
302
+ if (!options.create) throw new Refusal("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
313
303
  mkdirSync(path, { recursive: true });
314
- } else if (!stat.isDirectory()) throw new LibraryError("invalid-input", `${folder} is not a folder.`);
304
+ } else if (!stat.isDirectory()) throw new Refusal("invalid-input", `${folder} is not a folder.`);
315
305
  if (statSync(join(path, PROJECT_DIRECTORY), { throwIfNoEntry: false }) !== undefined) {
316
- throw new LibraryError("invalid-input", `${folder} is a project folder. A library is a plain folder beside the projects, for example library/ at the workspace root.`);
306
+ throw new Refusal("invalid-input", `${folder} is a project folder. A library is a plain folder beside the projects, for example library/ at the workspace root.`);
317
307
  }
318
308
  return path;
319
309
  }
@@ -331,7 +321,7 @@ export function readLibrary(folder: string): Routine[] {
331
321
  export function saveRoutine(folder: string, routine: Routine, replace: boolean): string {
332
322
  const file = join(folder, `${routine.name}.json`);
333
323
  if (!replace && statSync(file, { throwIfNoEntry: false }) !== undefined) {
334
- throw new LibraryError("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
324
+ throw new Refusal("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
335
325
  }
336
326
  const temporary = `${file}.${process.pid}.tmp`;
337
327
  writeFileSync(temporary, routineText(routine));
@@ -449,7 +439,7 @@ export interface Suggestions {
449
439
  leftOut: Skipped[];
450
440
  }
451
441
 
452
- const semantic = (row: { origin: string }) => row.origin === "user" || row.origin === "llm";
442
+ const semantic = (row: { origin: string }) => isSemantic(row.origin);
453
443
 
454
444
  /**
455
445
  * The names, regions, comments, references and operand expressions of each
@@ -573,7 +563,7 @@ function operandProblem(address: number, text: string, image: Searched, resolveN
573
563
  try {
574
564
  value = evaluate(text, resolveName);
575
565
  } catch (error) {
576
- if (error instanceof ExpressionError) return error.message;
566
+ if (error instanceof Refusal) return error.message;
577
567
  throw error;
578
568
  }
579
569
  const range = size === 2 ? 0x100 : 0x10000;
@@ -4,14 +4,15 @@
4
4
  // Watchpoints and range breakpoints stop the run at an access; the coverage
5
5
  // map then tells which range and which access it was.
6
6
 
7
- import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
7
+ import { type AddressRange, COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
8
8
  import { runProgram } from "#src/host-client/autostart.ts";
9
9
  import { readCoverage } from "#src/host-client/coverage.ts";
10
+ import { startAtEntry, type EntryState } from "#src/host-client/entry-state.ts";
10
11
  import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
- import { MAX_MEMORY_READ, MAX_MEMORY_WRITE } from "#src/host-client/tools.ts";
12
+ import { knowledgeAt } from "#src/knowledge/names.ts";
13
+ import { MAX_MEMORY_READ } from "#src/host-client/tools.ts";
12
14
  import { readProjectFile } from "#src/host-client/transfer.ts";
13
15
  import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
- import type { Range } from "./compare.ts";
15
16
 
16
17
  export type FirstAccess = "read" | "write" | "execute" | "none";
17
18
 
@@ -21,26 +22,12 @@ export interface RangeAccess {
21
22
  changed: boolean;
22
23
  }
23
24
 
24
- /** How a release starts in the emulator. */
25
+ /** How a release starts in the emulator: autostart of a PRG, or the captured program at its entry. */
25
26
  export type Start =
26
27
  | { kind: "program"; path: string }
27
- | { kind: "image"; segments: Range[]; byteAt: (address: number) => number; state: ImageState };
28
-
29
- export interface ImageState {
30
- pc: number;
31
- a: number;
32
- x: number;
33
- y: number;
34
- sp: number;
35
- flags: { n: boolean; v: boolean; d: boolean; i: boolean; z: boolean; c: boolean };
36
- port: [number, number];
37
- /** $0000-$03ff at the entry. */
38
- pages: Uint8Array;
39
- }
28
+ | { kind: "image"; segments: AddressRange[]; byteAt: (address: number) => number; state: EntryState };
40
29
 
41
30
  const MAX_WATCH = 256;
42
- /** Frames until the KERNAL is at the READY prompt, before an image is written. */
43
- const READY_FRAMES = 150;
44
31
 
45
32
  const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
46
33
  const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
@@ -62,20 +49,7 @@ async function start(client: ViceSessionClient, how: Start): Promise<string | un
62
49
  if (failed === "not-started") return "The program did not start after RUN; it needs a BASIC start line.";
63
50
  return undefined;
64
51
  }
65
- await client.reset({ mode: "hard", run: true });
66
- await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
67
- for (const segment of how.segments) {
68
- for (let at = segment.start; at <= segment.end; at += MAX_MEMORY_WRITE) {
69
- const last = Math.min(segment.end, at + MAX_MEMORY_WRITE - 1);
70
- const bytes = Buffer.from(Array.from({ length: last - at + 1 }, (_, offset) => how.byteAt(at + offset)));
71
- await client.memoryWrite({ address: at, data: bytes.toString("hex"), space: "c64", view: "ram" });
72
- }
73
- }
74
- const { state } = how;
75
- // Pages 0 to 3 from $0002, then the CPU port: the port bits switch what the CPU sees.
76
- await client.memoryWrite({ address: 0x0002, data: Buffer.from(state.pages.subarray(2)).toString("hex"), space: "c64", view: "ram" });
77
- await client.memoryWrite({ address: 0x0000, data: Buffer.from(state.port).toString("hex"), space: "c64", view: "cpu" });
78
- await client.registersSet("c64", { pc: state.pc, a: state.a, x: state.x, y: state.y, sp: state.sp, flags: state.flags });
52
+ await startAtEntry(client, how, how.state);
79
53
  return undefined;
80
54
  }
81
55
 
@@ -86,7 +60,7 @@ async function start(client: ViceSessionClient, how: Start): Promise<string | un
86
60
  export async function watchRelease(
87
61
  client: ViceSessionClient,
88
62
  how: Start,
89
- ranges: Range[],
63
+ ranges: AddressRange[],
90
64
  release: (address: number) => number,
91
65
  run: { frames: number; steps: ActionStep[] },
92
66
  ): Promise<{ started: true; accesses: RangeAccess[] } | { started: false; reason: string }> {
@@ -151,6 +125,7 @@ export async function watchRelease(
151
125
  if (left > 0 && !(await settle())) left--;
152
126
  }
153
127
  };
128
+ const at = knowledgeAt();
154
129
  for (const step of run.steps) {
155
130
  if ("frames" in step) {
156
131
  await runFrames(step.frames);
@@ -159,13 +134,13 @@ export async function watchRelease(
159
134
  if ("runUntil" in step) {
160
135
  // A stop at a point is not the target: settle and run to the target again.
161
136
  while (guard-- > 0) {
162
- const reached = await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
137
+ const reached = await runActionStep(client, step, at);
163
138
  if (reached === undefined || reached.stopReason !== "watchpoint" && reached.stopReason !== "breakpoint") break;
164
139
  await settle();
165
140
  }
166
141
  continue;
167
142
  }
168
- await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
143
+ await runActionStep(client, step, at);
169
144
  }
170
145
  await runFrames(run.frames);
171
146
  if (points.size > 0) await settle();
@@ -188,4 +163,4 @@ export async function watchRelease(
188
163
  return { started: true, accesses };
189
164
  }
190
165
 
191
- export const showRange = (range: Range) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
166
+ export const showRange = (range: AddressRange) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;