@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.
- package/README.md +8 -4
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +162 -0
- package/src/native/discover.ts +8 -0
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +25 -4
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- 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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
166
|
+
export const showRange = (range: AddressRange) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
|