@henols/c64-re-tools 2.1.1 → 2.1.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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +3 -4
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +2 -0
- package/skills/c64-emulator/SKILL.md +2 -1
- package/skills/c64-knowledge/SKILL.md +6 -4
- package/skills/c64-knowledge/scripts/knowledge.ts +35 -13
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +1 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +1 -1
- package/skills/c64-provenance/SKILL.md +1 -0
- package/skills/c64-provenance/scripts/provenance.ts +9 -6
- package/skills/c64-source/SKILL.md +1 -0
- package/skills/c64-source/scripts/lib/source.ts +7 -29
- package/skills/c64-source/scripts/source.ts +4 -4
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/static-analysis.ts +14 -5
- package/skills/c64-testing/SKILL.md +3 -2
- package/skills/c64-testing/scripts/lib/diff-files.ts +14 -2
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +27 -7
- package/skills/c64-testing/scripts/testing.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +8 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +18 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +49 -3
- package/src/host-client/image.ts +10 -10
- package/src/host-client/steps.ts +39 -3
- package/src/host-client/transfer.ts +14 -26
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +22 -14
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools/debug.ts +29 -3
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +110 -2
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +55 -3
|
@@ -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.
|
|
@@ -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,
|
|
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. */
|
|
@@ -319,7 +319,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
319
319
|
const outside = new Set<number>();
|
|
320
320
|
// A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
|
|
321
321
|
const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
|
|
322
|
-
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) &&
|
|
322
|
+
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && labelProblem(name) === undefined).map(({ address, name }) => [address, name]));
|
|
323
323
|
// An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
|
|
324
324
|
const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
|
|
325
325
|
/** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
|
|
@@ -450,7 +450,8 @@ export function layout(input: SourceInput): Layout {
|
|
|
450
450
|
for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
|
|
451
451
|
const known = symbols.get(address);
|
|
452
452
|
if (known !== undefined) {
|
|
453
|
-
|
|
453
|
+
const problem = labelProblem(known);
|
|
454
|
+
if (problem !== undefined) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)}: ${problem} Rename the symbol with c64-knowledge.`);
|
|
454
455
|
names.set(address, known);
|
|
455
456
|
if (inImage(address)) namedLabels++;
|
|
456
457
|
continue;
|
|
@@ -574,27 +575,6 @@ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: Instruc
|
|
|
574
575
|
return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
|
|
575
576
|
}
|
|
576
577
|
|
|
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
578
|
/** The plain number of an operand: what the probe assembles, and what a comment shows. */
|
|
599
579
|
export function literalOperand(opcode: number, value: number): string {
|
|
600
580
|
const { mode } = OPCODES[opcode]!;
|
|
@@ -1046,7 +1026,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1046
1026
|
const form = formOf(layout, item);
|
|
1047
1027
|
const { mode } = OPCODES[item.opcode]!;
|
|
1048
1028
|
const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
|
|
1049
|
-
const literal =
|
|
1029
|
+
const literal = instructionWith(item.opcode, literalOperand(item.opcode, value));
|
|
1050
1030
|
let written = layout.operands.get(item.address);
|
|
1051
1031
|
// An expression that is not written stays visible as a side comment.
|
|
1052
1032
|
const asNumber = (expression: string, reason: string) => {
|
|
@@ -1062,13 +1042,11 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1062
1042
|
out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
|
|
1063
1043
|
result.asBytes.push({ address: item.address, text: literal });
|
|
1064
1044
|
} else if (written !== undefined) {
|
|
1065
|
-
|
|
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(" ")));
|
|
1045
|
+
out.push(withSide(`${INDENT}${instructionWith(item.opcode, written, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
1068
1046
|
result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
|
|
1069
1047
|
} else {
|
|
1070
1048
|
const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
|
|
1071
|
-
out.push(withSide(`${INDENT}${
|
|
1049
|
+
out.push(withSide(`${INDENT}${instructionWith(item.opcode, expression, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
1072
1050
|
}
|
|
1073
1051
|
mapLine(item.address, item.address + item.length - 1);
|
|
1074
1052
|
result.instructions++;
|
|
@@ -31,8 +31,8 @@ import { resolveProjectPath } from "#src/project.ts";
|
|
|
31
31
|
import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
|
|
32
32
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
33
33
|
import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, loaderState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
|
|
34
|
-
import { OPCODES } from "#src/opcodes.ts";
|
|
35
|
-
import { entriesInData, entryOutsideImage,
|
|
34
|
+
import { instructionWith, OPCODES } from "#src/opcodes.ts";
|
|
35
|
+
import { entriesInData, entryOutsideImage, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
|
|
36
36
|
|
|
37
37
|
/** At most this many diagnostics or differences of one check. */
|
|
38
38
|
const MAX_DIAGNOSTICS = 20;
|
|
@@ -182,7 +182,7 @@ async function probe(laid: Layout): Promise<Set<string>> {
|
|
|
182
182
|
? formatC64Address((slot + 2 + (item.bytes[1]! < 0x80 ? item.bytes[1]! : item.bytes[1]! - 0x100)) & 0xffff)
|
|
183
183
|
: literalOperand(item.form.opcode, item.bytes.length === 3 ? item.bytes[1]! | (item.bytes[2]! << 8) : item.bytes[1]!);
|
|
184
184
|
lines.push(`* = ${formatC64Address(slot)}`);
|
|
185
|
-
lines.push(` ${
|
|
185
|
+
lines.push(` ${instructionWith(item.form.opcode, operand, item.form.suffix)}`);
|
|
186
186
|
lineOf.set(lines.length, item.form.key);
|
|
187
187
|
});
|
|
188
188
|
const built = await assembleFiles([{ name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
|
|
@@ -734,7 +734,7 @@ await runScript({
|
|
|
734
734
|
name: "generate",
|
|
735
735
|
summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
|
|
736
736
|
options: {
|
|
737
|
-
out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
|
|
737
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
|
|
738
738
|
entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
|
|
739
739
|
space: { ...OPTIONS.space, help: "the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255. The coverage and --loader are for c64 only" },
|
|
740
740
|
loader: { kind: "flag", help: "also write loader.asm and loader.prg: the loader copies the segments into place, sets the entry state of the knowledge, and jumps to the entry. It is checked in the emulator" },
|
|
@@ -61,6 +61,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
61
61
|
- 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.
|
|
62
62
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
63
63
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
64
|
+
- 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.
|
|
64
65
|
- 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.
|
|
65
66
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
66
67
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -87,6 +88,7 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
|
|
|
87
88
|
- Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
|
|
88
89
|
- If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
|
|
89
90
|
- `conflicts` lists the places where the analyzer and the current knowledge disagree.
|
|
91
|
+
- DXA or Ghidra can find data where a run executed an instruction start (`executed`), or where a `user` or `llm` symbol of kind `label` or `routine` is (`knowledge`). Then the script does not write that data region, and the earlier region of the analyzer stays. `conflicts` lists each such place. Executed code and your names have priority over the guess of an analyzer.
|
|
90
92
|
- `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
|
|
91
93
|
- `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
|
|
92
94
|
- `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// structural findings into .c64-re-tools/knowledge/.
|
|
5
5
|
|
|
6
6
|
import { COVERAGE, formatC64Address, prgRange } from "#src/c64.ts";
|
|
7
|
-
import { Refusal } from "#src/errors.ts";
|
|
7
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
8
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
9
9
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
10
10
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
@@ -121,6 +121,15 @@ function withRemedy(reason: string): string {
|
|
|
121
121
|
|
|
122
122
|
/** One conflict: what the knowledge has, and what the analyzer found, under the analyzer's name. */
|
|
123
123
|
function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<string, unknown> {
|
|
124
|
+
if (conflict.category === "code-evidence") {
|
|
125
|
+
return {
|
|
126
|
+
range: { start: formatC64Address(conflict.start), end: formatC64Address(conflict.end) },
|
|
127
|
+
[analyzer]: conflict.finding,
|
|
128
|
+
...(conflict.executed === undefined ? {} : { executed: formatC64Address(conflict.executed) }),
|
|
129
|
+
...(conflict.symbol === undefined ? {} : { knowledge: { ...conflict.symbol, address: formatC64Address(conflict.symbol.address) } }),
|
|
130
|
+
problem: conflict.executed !== undefined ? "a run executed code here: the data finding is not imported" : "the knowledge has a code name here: the data finding is not imported",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
124
133
|
if (conflict.category === "symbol") {
|
|
125
134
|
return {
|
|
126
135
|
at: formatC64Address(conflict.address),
|
|
@@ -270,7 +279,7 @@ async function analyze(call: Call): Promise<unknown> {
|
|
|
270
279
|
written = writeReported(listingPath, listing, { what: "--out-listing" });
|
|
271
280
|
} catch (error) {
|
|
272
281
|
const recorded = imported.revision === null ? "The knowledge did not change." : `The knowledge has the findings as revision ${imported.revision}.`;
|
|
273
|
-
throw new WireFailure("operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message}. ${recorded}`);
|
|
282
|
+
throw new WireFailure(isRefusal(error) ? error.code : "operation-failed", `The script did not write the listing to ${listingPath}: ${(error as Error).message.replace(/\.$/, "")}. ${recorded}`);
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
const found = idiomsResult();
|
|
@@ -334,9 +343,9 @@ await runScript({
|
|
|
334
343
|
decompile: { kind: "address", value: "<address>", multiple: true, help: "ghidra: a routine to decompile, at most 32" },
|
|
335
344
|
space: OPTIONS.space,
|
|
336
345
|
"max-listed": { ...OPTIONS["max-listed"], help: "list at most n labels, functions or candidates (default 50, 0 for none). The counts are always given, and the knowledge has all labels and functions" },
|
|
337
|
-
"out-listing": { kind: "path", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
|
|
338
|
-
"out-instructions": { kind: "path", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
|
|
339
|
-
"out-macro-batch": { kind: "path", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
|
|
346
|
+
"out-listing": { kind: "path", output: "file", value: "<file>", help: "dxa: write the disassembly listing to this project file" },
|
|
347
|
+
"out-instructions": { kind: "path", output: "file", value: "<file>", help: "ghidra: write every instruction (address, bytes, mnemonic, mode, operand, target, executed) to this project file as JSON" },
|
|
348
|
+
"out-macro-batch": { kind: "path", output: "file", value: "<file>", help: "write the macro candidates as an apply batch of c64-knowledge (the macros and their uses) to this project file" },
|
|
340
349
|
},
|
|
341
350
|
notes: "The program and the coverage come from the knowledge: put them in with knowledge.ts image and coverage of the c64-knowledge skill. The executed code of the coverage is seeded. Routine symbols in the knowledge are entry points too. Ghidra needs at least one entry point. DXA finds the SYS of a BASIC start by itself.",
|
|
342
351
|
run: analyze,
|
|
@@ -120,7 +120,7 @@ node <skill>/scripts/testing.ts diff .c64-re-tools/local/runs/rooms/results.json
|
|
|
120
120
|
|
|
121
121
|
- Two snapshot files: the script restores each one in its own emulator. It gives the changed ranges of the RAM and of the colour RAM, and the changed VIC-II, CIA, SID and CPU registers.
|
|
122
122
|
- Two memory files of a run: the changed ranges. For a file of less than 64 KiB, `--address` gives the address of its first byte.
|
|
123
|
-
- Two observations of runs, also of one run: the changed memory items and registers.
|
|
123
|
+
- Two observations of runs, also of one run: the changed memory items and registers. `compared` gives the count of items that both observations have. With `compared: 0`, `reason` tells what each observation has. Then the diff compared nothing, and the observations are not equal.
|
|
124
124
|
|
|
125
125
|
`registers` gives each changed register and chip value. `cpu.pc` is a `$` address. Each CPU flag is `cpu.flags.<flag>`, for example `cpu.flags.c`, with `true` or `false`.
|
|
126
126
|
|
|
@@ -135,6 +135,7 @@ The script refuses a snapshot that does not restore in its emulator. An example
|
|
|
135
135
|
- 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.
|
|
136
136
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
137
137
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
138
|
+
- 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.
|
|
138
139
|
- 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.
|
|
139
140
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
140
141
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -154,7 +155,7 @@ The script refuses a snapshot that does not restore in its emulator. An example
|
|
|
154
155
|
1. Write the scenario file. Start the steps with a frame count, for example `{ "frames": 150 }`. Each side starts at the reset vector, and the C64 needs these frames to start.
|
|
155
156
|
2. Use symbol names, for example `player_x`, not fixed addresses. Each side finds the name in its own symbols. Use a `$` address only when both programs use the same address.
|
|
156
157
|
3. Make each checkpoint a condition, for example a `runUntil` step with an `address` target at a routine. Do not wait for a time.
|
|
157
|
-
4.
|
|
158
|
+
4. Write the symbol file of the rebuild with `--out-symbols build/game.json` of c64-assembler `build`. Give `--all-symbols`, so that the file also has the constants that the source does not use.
|
|
158
159
|
5. Run `testing.ts test`.
|
|
159
160
|
6. Read the result:
|
|
160
161
|
- `PASS`: all observations are equal, or the differences are in the allowed tolerance.
|
|
@@ -118,6 +118,18 @@ const show = (range: ChangedRange) => ({ start: formatC64Address(range.start), e
|
|
|
118
118
|
/** A changed value as a person reads it: the pc as "$xxxx". */
|
|
119
119
|
const showValue = (item: { name: string; a: number | boolean; b: number | boolean }) => (item.name === "cpu.pc" && typeof item.a === "number" && typeof item.b === "number" ? { name: item.name, a: formatC64Address(item.a), b: formatC64Address(item.b) } : item);
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* How many items two observations have in common, by name: the items that the
|
|
123
|
+
* diff compared. With none, the reason names what each side has, so that a
|
|
124
|
+
* count of 0 does not read as "equal".
|
|
125
|
+
*/
|
|
126
|
+
export function comparedItems(what: string, a: readonly string[], b: readonly string[], first: string, second: string): { compared: number; reason?: string } {
|
|
127
|
+
const compared = a.filter((name) => b.includes(name)).length;
|
|
128
|
+
if (compared > 0) return { compared };
|
|
129
|
+
const has = (names: readonly string[]) => (names.length === 0 ? "none" : names.join(", "));
|
|
130
|
+
return { compared, reason: `No ${what} was compared: the observations have none in common. ${first} has ${has(a)}; ${second} has ${has(b)}.` };
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
/** The differences of two states of one kind: snapshots, memory files or observations of runs. */
|
|
122
134
|
export async function diffFiles(first: string, second: string, options: { gap: number; max: number; ntsc: boolean; at?: number; out?: string }): Promise<Record<string, unknown>> {
|
|
123
135
|
const kind = kindOf(first);
|
|
@@ -159,8 +171,8 @@ export async function diffFiles(first: string, second: string, options: { gap: n
|
|
|
159
171
|
b: second,
|
|
160
172
|
...(diff.memory === undefined ? {} : { memory: part(diff.memory) }),
|
|
161
173
|
...(diff.colorRam === undefined ? {} : { colorRam: part(diff.colorRam) }),
|
|
162
|
-
...(diff.values === undefined ? {} : { registers: listed(diff.values, max, showValue, rest) }),
|
|
163
|
-
...(diff.observed === undefined ? {} : { observed: listed(diff.observed, max, undefined, rest) }),
|
|
174
|
+
...(diff.values === undefined ? {} : { registers: { ...(kind === "observation" ? comparedItems("register", Object.keys(a.values ?? {}), Object.keys(b.values ?? {}), first, second) : {}), ...listed(diff.values, max, showValue, rest) } }),
|
|
175
|
+
...(diff.observed === undefined ? {} : { observed: { ...comparedItems("memory item", Object.keys(a.observed ?? {}), Object.keys(b.observed ?? {}), first, second), ...listed(diff.observed, max, undefined, rest) } }),
|
|
164
176
|
...(written === undefined ? {} : written),
|
|
165
177
|
};
|
|
166
178
|
}
|
|
@@ -145,11 +145,11 @@ export function joystickMask(direction: Extract<ActionStep, { joystick: unknown
|
|
|
145
145
|
return direction.split("-").reduce((mask, part) => mask | (bits[part] ?? 0), fire ? 0x10 : 0);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/** A state item of the command line: a name or a $ address, with :size for more than one byte. */
|
|
149
|
-
export function parseStateItem(text: string): StateItem {
|
|
148
|
+
/** A state item of the command line: a name or a $ address, with :size for more than one byte. `option` names the option of the item in a refusal. */
|
|
149
|
+
export function parseStateItem(text: string, option = "--state"): StateItem {
|
|
150
150
|
const match = /^([^:]+)(?::(\d+))?$/.exec(text);
|
|
151
151
|
const size = Number(match?.[2] ?? 1);
|
|
152
|
-
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new Refusal("invalid-input",
|
|
152
|
+
if (match === null || !isInteger(size, 1, MAX_STATE_BYTES)) throw new Refusal("invalid-input", `${option} ${text}: give a name or a $ address, with :size from 1 to ${MAX_STATE_BYTES}`);
|
|
153
153
|
return { address: match[1]!, size };
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -37,7 +37,7 @@ export async function replay(values: ReplayOptions): Promise<ReplayResult> {
|
|
|
37
37
|
const folder = values.folder.replace(/\/+$/, "").replace(/\/recording\.json$/, "");
|
|
38
38
|
const recording = readRecording(folder);
|
|
39
39
|
const given: Comparison | undefined =
|
|
40
|
-
values.checkpoint === undefined ? undefined : { checkpoint: values.checkpoint, state: values.state.map(parseStateItem), ...(values.pin.length === 0 ? {} : { pin: values.pin.map(parseStateItem) }) };
|
|
40
|
+
values.checkpoint === undefined ? undefined : { checkpoint: values.checkpoint, state: values.state.map((item) => parseStateItem(item, "--state")), ...(values.pin.length === 0 ? {} : { pin: values.pin.map((item) => parseStateItem(item, "--pin")) }) };
|
|
41
41
|
const compare = given ?? recording.compare;
|
|
42
42
|
if (values.rebuild !== undefined && compare === undefined) throw new UsageError("the recording has no checkpoint and state: give --checkpoint and --state");
|
|
43
43
|
const result = (outcome: ReplayResult["result"], side: ReplayResult["side"], fields: Partial<ReplayResult> = {}): ReplayResult => ({
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { formatC64Address, formatSprite, formatVicii, parseC64Address } from "#src/c64.ts";
|
|
7
7
|
import { runProgram } from "#src/host-client/autostart.ts";
|
|
8
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
8
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
9
|
-
import { ACTION_STEP_KINDS, checkActionStep, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkStepNames, isInteger, isObject, refuseOldStep, renamedField, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
11
|
import { readPrg } from "#src/host-client/image.ts";
|
|
11
12
|
import { MAX_MEMORY_READ, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, WireFailure, type TraceHit, type TraceKind } from "#src/host-client/tools.ts";
|
|
12
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
@@ -161,11 +162,26 @@ function readRun(path: string): Run {
|
|
|
161
162
|
/** The remedy for a start program that is no PRG. */
|
|
162
163
|
const PROGRAM_REMEDY = "A run starts a PRG. Get the PRG out of the disk, tape or cartridge with disk.ts read (or disk.ts crt) of the c64-disk skill";
|
|
163
164
|
|
|
164
|
-
/** Checks each file that the run reads before the emulator starts: the start program or snapshot, and each snapshot step. */
|
|
165
|
-
function checkRunFiles(run: Run): void {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
/** Checks each file that the run reads before the emulator starts: the start program or snapshot, and each snapshot step. A refusal names the run file and the step. */
|
|
166
|
+
function checkRunFiles(path: string, run: Run): void {
|
|
167
|
+
const named = (where: string, check: () => unknown) => {
|
|
168
|
+
try {
|
|
169
|
+
check();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (isRefusal(error)) throw new Refusal(error.code, `run ${path}: ${where}: ${error.message}`);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if ("program" in run.start) {
|
|
176
|
+
const { program } = run.start;
|
|
177
|
+
named("start.program", () => readPrg(program, PROGRAM_REMEDY));
|
|
178
|
+
} else {
|
|
179
|
+
const { snapshot } = run.start;
|
|
180
|
+
named("start.snapshot", () => readProjectFile(snapshot));
|
|
181
|
+
}
|
|
182
|
+
run.steps.forEach((step, index) => {
|
|
183
|
+
if ("snapshot" in step) named(`step ${index + 1}: snapshot`, () => readProjectFile(step.snapshot));
|
|
184
|
+
});
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
/** A trace hit with $ addresses, as a person reads it. */
|
|
@@ -176,7 +192,7 @@ function showHit(hit: TraceHit) {
|
|
|
176
192
|
/** Runs the run file and writes its files into `out`. failed: a step did not run. */
|
|
177
193
|
export async function runFile(path: string, out: string): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
178
194
|
const run = readRun(path);
|
|
179
|
-
checkRunFiles(run);
|
|
195
|
+
checkRunFiles(path, run);
|
|
180
196
|
const symbols = namesOf(run.symbols ?? "none");
|
|
181
197
|
const at = (location: string): number => {
|
|
182
198
|
if (location.startsWith("$")) return parseC64Address(location);
|
|
@@ -184,6 +200,10 @@ export async function runFile(path: string, out: string): Promise<{ output: Reco
|
|
|
184
200
|
if (address === undefined) throw new UsageError(`the symbols have no ${location}`);
|
|
185
201
|
return address;
|
|
186
202
|
};
|
|
203
|
+
// The names of the action steps resolve before the emulator starts.
|
|
204
|
+
run.steps.forEach((step, index) => {
|
|
205
|
+
if ((ACTION_STEP_KINDS as readonly string[]).some((kind) => kind in step) && !("observe" in step) && !("trace" in step)) checkStepNames([step as ActionStep], at, `run ${path}`, index + 1);
|
|
206
|
+
});
|
|
187
207
|
const files: string[] = [];
|
|
188
208
|
const written: Written[] = [];
|
|
189
209
|
const write = (name: string, bytes: Uint8Array) => {
|
|
@@ -123,14 +123,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
123
123
|
name: "test",
|
|
124
124
|
summary: "Runs a scenario on the original and then on the rebuild in one emulator. result is PASS, FAIL with the differences, or INCONCLUSIVE with the reason.",
|
|
125
125
|
subjects: [{ name: "<scenario.json>", help: "a scenario file, relative to the project folder. See references/scenario-format.md" }],
|
|
126
|
-
options: { out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
|
|
126
|
+
options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write both screens, the difference image and memory.json with each differing byte into this folder" } },
|
|
127
127
|
run: test,
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
name: "run",
|
|
131
131
|
summary: "Runs the steps of a run file in its own emulator and writes what they ask for into --out: screens, coverage maps, snapshot files, traces, and results.json with the observations. result is FAIL when a step did not run.",
|
|
132
132
|
subjects: [{ name: "<run.json>", help: "a run file, relative to the project folder. See references/scenario-format.md, section Runs" }],
|
|
133
|
-
options: { out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
133
|
+
options: { out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the files of the steps and results.json" } },
|
|
134
134
|
run: async (call) => {
|
|
135
135
|
const { output, failed } = await runFile(call.subject(0), call.string("out")!);
|
|
136
136
|
return { result: failed ? "FAIL" : "PASS", ...output };
|
|
@@ -141,7 +141,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
141
141
|
summary: "Plays the steps of a record file on the original in the replay core (c64run), and writes the session as a recording folder: recording.json, reads.txt and coverage.bin. result is FAIL when the session did not complete.",
|
|
142
142
|
subjects: [{ name: "<record.json>", help: "a record file, relative to the project folder. See references/recording-format.md" }],
|
|
143
143
|
options: {
|
|
144
|
-
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
144
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the recording folder, for example tests/recordings/<name>" },
|
|
145
145
|
play: { kind: "flag", help: "a person plays after the steps: the script prints the address of a page on stderr. The page shows the start screen frozen until Start (Enter). End (F12) saves the recording, Cancel (F11) ends without one" },
|
|
146
146
|
joystick: { kind: "count", value: "<1|2>", min: 1, max: 2, help: "with --play: the joystick port that the program reads, at the start (default 2). F9 on the page changes it" },
|
|
147
147
|
"http-port": { kind: "count", value: "<n>", min: 0, max: 65535, help: `with --play: the port of the page (default ${DEFAULT_HTTP_PORT})` },
|
|
@@ -158,7 +158,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
158
158
|
pin: { kind: "text", value: "<item>", multiple: true, help: `random state in RAM, in the form of --state: at each checkpoint the rebuild gets the original's values there` },
|
|
159
159
|
rebuild: { kind: "path", value: "<prg>", help: "the rebuild to compare with the original" },
|
|
160
160
|
symbols: { ...OPTIONS.symbols, help: "the names of the rebuild: the c64-assembler output (default: the symbols of the recording)" },
|
|
161
|
-
out: { ...OPTIONS.out, value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
161
|
+
out: { ...OPTIONS.out, output: "folder", value: "<dir>", help: "with a FAIL, write the RAM of both sides at the first different checkpoint into this folder" },
|
|
162
162
|
},
|
|
163
163
|
notes: "A recording without names takes the names of the knowledge for the original. A $ address is an address of the original: on the rebuild it moves with the name before it.",
|
|
164
164
|
run: (call) => {
|
|
@@ -178,7 +178,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
178
178
|
"max-listed": OPTIONS["max-listed"],
|
|
179
179
|
ntsc: { kind: "flag", help: "the snapshots are of an NTSC machine (default PAL)" },
|
|
180
180
|
address: { kind: "address", value: "<address>", help: "the address of the first byte of a memory file that is shorter than 64 KiB" },
|
|
181
|
-
out: { ...OPTIONS.out, value: "<file>", help: "also write all ranges to this file" },
|
|
181
|
+
out: { ...OPTIONS.out, output: "file", value: "<file>", help: "also write all ranges to this file" },
|
|
182
182
|
},
|
|
183
183
|
run: (call) => {
|
|
184
184
|
const [address, out] = [call.address("address"), call.string("out")];
|
|
@@ -26,7 +26,7 @@ node <skill>/scripts/unpacker.ts capture original/game.prg --until '$c000' --out
|
|
|
26
26
|
node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/local/parts.json --out .c64-re-tools/local/parts --drive
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
29
|
+
`inspect` examines the file locally. `trace`, `stages`, `capture` and `loads` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `stages` runs the program and finds each stage: a decruncher, an intro, a copier, and then the program. For each stage it gives the entry, the frames, the written memory (`wrote`) and the entry of the next stage (`next`). `wrote` lists each block that the stage wrote, also a small block. `capture` stops the program when the CPU gets to the `--until` address. In `stages` and `capture`, a `frames` step and a `runUntil` step to an address without a condition count as run time. `stages` refuses a different `runUntil` step.
|
|
30
30
|
|
|
31
31
|
| Option | Meaning |
|
|
32
32
|
|---|---|
|
|
@@ -41,11 +41,11 @@ node <skill>/scripts/unpacker.ts loads original/game.d64 --steps .c64-re-tools/l
|
|
|
41
41
|
| `--max-listed 50` | `trace` only: the most ranges that `writtenThenExecuted` lists (default 50). |
|
|
42
42
|
| `--drive` | `loads` only: each capture also writes the 2 KiB RAM of the 1541 drive as `drive8.prg`. |
|
|
43
43
|
| `--min-block 64` | `loads` only: the smallest block of changed memory that becomes a segment (default 64). The result counts smaller changes as `smallChanges`. |
|
|
44
|
-
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). |
|
|
44
|
+
| `--min-stage 64` | `stages` and `capture`: the smallest block of written memory that starts a new stage when the CPU executes it (default 64). It does not change `wrote` or the segments. |
|
|
45
45
|
|
|
46
46
|
The capture reads RAM, also the RAM under the ROMs and the I/O area. The script refuses an option that the command does not use. It refuses a wrong `--range` or `--exclude` before the emulator starts.
|
|
47
47
|
|
|
48
|
-
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. For example, wait 50 frames and type a space:
|
|
48
|
+
A steps file is a JSON list of steps. Each step has the input of the MCP tool of the same action. An address in a step can be a name. The script refuses an unknown name before the emulator starts. The message gives the file and the step. For example, wait 50 frames and type a space:
|
|
49
49
|
|
|
50
50
|
```json
|
|
51
51
|
[{ "frames": 50 }, { "type": { "mode": "text", "text": " " } }]
|
|
@@ -69,6 +69,7 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
|
|
|
69
69
|
- 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.
|
|
70
70
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
71
71
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
72
|
+
- 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.
|
|
72
73
|
- 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.
|
|
73
74
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
74
75
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -107,7 +108,7 @@ The scripts refuse a step in the form of version 2.0, for example `{ "runUntil":
|
|
|
107
108
|
- A program can be relocated and not packed. Then a short copier moves the code and jumps to it. The moved code is the last stage. Make sure that the `entry` of the last stage is not code that the program writes later. An example is a routine that the program makes at run time. Disassemble the entry to make sure.
|
|
108
109
|
4. Run `capture` with that address and a directory in `--out`. Give the same `--steps` as before. The directory gets:
|
|
109
110
|
- `image.bin`: all 64 KiB of RAM at the entry.
|
|
110
|
-
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
111
|
+
- `image.json`: the entry, the segments, the vectors and the stages. A segment is a block that a stage wrote, also a small block. A block is not a segment when it holds the entry of a later stage before the program, and no stage wrote over that entry. A block that a stage before the program wrote in such a block is also not a segment, for example a variable of a loader. A block that is fully in pages 0 to 3 is not a segment, because `entry_state.json` holds these pages. The block with the capture entry is always a segment. Segments do not overlap. `writtenBy` tells the last stage that wrote into the segment.
|
|
111
112
|
- One PRG for each segment.
|
|
112
113
|
- `entry_state.json`: the registers, `$00` and `$01`, pages 0 to 3, the VIC, SID and CIA registers, and the vectors. `cia` gives the CIA timer latches and interrupt masks. `differsFromReset` lists the bytes that are not as a clean C64 has them at READY. A rebuild must make these bytes the same, because the program can read them.
|
|
113
114
|
5. Make sure that the result is the application: the code at the entry must not be the unpacker again. A segment can be data that a loader left, for example intro graphics. Keep it until other evidence shows that the program does not use it. Then capture again with `--exclude` for that range. Thus the source and the loader do not include the range.
|
|
@@ -150,7 +151,9 @@ The packing evidence, the stages, or the unpacked program with its image, segmen
|
|
|
150
151
|
|
|
151
152
|
`trace`, `stages`, `capture` and `loads` give `result: "PASS"` or `result: "FAIL"`. A FAIL has `reason`. `capture` gives the written files in `files`, each as `{out, bytes, sha256}`.
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
`capture` gives `differsFromReset`: the count of bytes that are not as a clean C64 has them at READY.
|
|
155
|
+
|
|
156
|
+
For `loads`: `captures`, with the name, the PC, the segments, `differsFromReset`, the files and the drive file of each capture. Each `entry_state.json` of `loads` has `differsFromReset` as the one of `capture` has. `keyWait: true` tells that the CPU was in the KERNAL wait for a key.
|
|
154
157
|
|
|
155
158
|
## Failure and conflicts
|
|
156
159
|
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
8
8
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
9
9
|
import { readCoverage } from "#src/host-client/coverage.ts";
|
|
10
|
-
import { ACTION_STEP_KINDS, checkActionStep, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
10
|
+
import { ACTION_STEP_KINDS, checkActionStep, checkStepNames, isObject, runActionStep, VICE_STEP_KINDS, type ActionStep, type ActionStepKind } from "#src/host-client/steps.ts";
|
|
11
11
|
import { knowledgeAt } from "#src/knowledge/names.ts";
|
|
12
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
13
|
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
15
15
|
import { UsageError } from "#src/script.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { entryStateFile, json, machineState, readRam, readyState, vectorsOf } from "./machine.ts";
|
|
17
|
+
import { FIRST_SEGMENT_ADDRESS } from "./stages.ts";
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
export type LoadStep = ActionStep | { capture: string };
|
|
@@ -22,8 +23,6 @@ export type LoadStep = ActionStep | { capture: string };
|
|
|
22
23
|
const NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
23
24
|
/** The drive RAM of a 1541. */
|
|
24
25
|
const DRIVE_RAM = 0x0800;
|
|
25
|
-
/** Pages 0-3 belong to the entry state, not to the segments. */
|
|
26
|
-
const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
27
26
|
/** The KERNAL LOAD entry in the jump table. */
|
|
28
27
|
const KERNAL_LOAD = 0xffd5;
|
|
29
28
|
/** The KERNAL loop that waits for a key at READY. or at an INPUT statement. */
|
|
@@ -57,7 +56,10 @@ export function readLoadSteps(path: string): LoadStep[] {
|
|
|
57
56
|
if (kinds.length > 1) return fail(`${at} has ${kinds.length} step kinds: ${kinds.join(", ")}. Give each one its own step`);
|
|
58
57
|
if (kinds.length !== 1) return fail(`${at} must have exactly one of ${[...VICE_STEP_KINDS, "capture"].join(", ")}`);
|
|
59
58
|
// The step with its defaults filled in, as the shared schema of the action steps gives it.
|
|
60
|
-
|
|
59
|
+
const checked = checkActionStep(kinds[0] as ActionStepKind, step, at, fail);
|
|
60
|
+
// Its names resolve now: an unknown name stops the call before the emulator starts.
|
|
61
|
+
checkStepNames([checked], knowledgeAt(), `--steps ${path}`, index + 1);
|
|
62
|
+
(value as unknown[])[index] = checked;
|
|
61
63
|
});
|
|
62
64
|
if (names.size === 0) fail("the steps have no capture step");
|
|
63
65
|
return value as LoadStep[];
|
|
@@ -97,6 +99,8 @@ const hex4 = (value: number) => value.toString(16).padStart(4, "0");
|
|
|
97
99
|
export async function loads(program: string, options: { out: string; steps: LoadStep[]; drive: boolean; minBlock: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
98
100
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
99
101
|
try {
|
|
102
|
+
// A clean machine at the READY prompt shows what the program changed at each capture. The run goes on after a capture, so it is read first.
|
|
103
|
+
const clean = await readyState(client);
|
|
100
104
|
await client.reset({ mode: "hard", run: false });
|
|
101
105
|
let previous: Buffer;
|
|
102
106
|
/** A PRG that autostart put into memory without the CPU: its bytes count as written in the first capture. */
|
|
@@ -184,7 +188,8 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
184
188
|
...(drive === undefined ? {} : { drive }),
|
|
185
189
|
}),
|
|
186
190
|
);
|
|
187
|
-
|
|
191
|
+
const entry = entryStateFile(registers, state, vectors, clean);
|
|
192
|
+
write("entry_state.json", entry.bytes);
|
|
188
193
|
const keyWait = registers.pc >= KERNAL_KEY_WAIT.start && registers.pc <= KERNAL_KEY_WAIT.end;
|
|
189
194
|
captures.push({
|
|
190
195
|
capture: step.capture,
|
|
@@ -193,6 +198,7 @@ export async function loads(program: string, options: { out: string; steps: Load
|
|
|
193
198
|
pc: formatC64Address(registers.pc),
|
|
194
199
|
...(keyWait ? { keyWait: true } : {}),
|
|
195
200
|
segments: segments.map(showRange),
|
|
201
|
+
differsFromReset: entry.differsFromReset,
|
|
196
202
|
...(small === 0 ? {} : { smallChanges: small }),
|
|
197
203
|
...(drive === undefined ? {} : { drive: drive.file }),
|
|
198
204
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// capture and loads commands of the unpacker share it.
|
|
4
4
|
|
|
5
5
|
import { formatC64Address } from "#src/c64.ts";
|
|
6
|
+
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
6
7
|
import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
7
8
|
|
|
8
9
|
type Registers = Awaited<ReturnType<ViceSessionClient["registersGet"]>>;
|
|
@@ -61,8 +62,34 @@ export function vectorsOf(state: MachineState, memory: Buffer) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
/** The
|
|
65
|
-
export function
|
|
65
|
+
/** The state of a clean machine at the READY prompt after a hard reset: what differsFromReset compares with. It resets the machine. */
|
|
66
|
+
export async function readyState(client: ViceSessionClient): Promise<MachineState> {
|
|
67
|
+
await resetToReady(client);
|
|
68
|
+
return machineState(client);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The bytes that differ from a clean machine: the CPU port, pages 0-3 from $0002, and the I/O areas. */
|
|
72
|
+
export function differsFrom(state: MachineState, clean: MachineState): Array<{ address: string; entry: number; reset: number }> {
|
|
73
|
+
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
74
|
+
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
75
|
+
compare(0x0000, state.port, clean.port);
|
|
76
|
+
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
77
|
+
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
78
|
+
return differs;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* entry_state.json, the one writer of capture and loads: the state at the
|
|
83
|
+
* entry, and what differs from `clean` (readyState). Gives the file and the
|
|
84
|
+
* count of differing bytes.
|
|
85
|
+
*/
|
|
86
|
+
export function entryStateFile(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, clean: MachineState): { bytes: Buffer; differsFromReset: number } {
|
|
87
|
+
const differs = differsFrom(state, clean);
|
|
88
|
+
return { bytes: json(entryState(registers, state, vectors, differs)), differsFromReset: differs.length };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The content of entry_state.json. */
|
|
92
|
+
function entryState(registers: Registers, state: MachineState, vectors: ReturnType<typeof vectorsOf>, differsFromReset: Array<{ address: string; entry: number; reset: number }>) {
|
|
66
93
|
return {
|
|
67
94
|
registers: { pc: formatC64Address(registers.pc), a: registers.a, x: registers.x, y: registers.y, sp: registers.sp, flags: registers.flags },
|
|
68
95
|
cpuPort: { $00: state.port[0], $01: state.port[1] },
|
|
@@ -71,7 +98,7 @@ export function entryState(registers: Registers, state: MachineState, vectors: R
|
|
|
71
98
|
// The CIA latches and interrupt masks: write-only on the chip, so the I/O bytes do not hold them.
|
|
72
99
|
cia: state.cia,
|
|
73
100
|
vectors,
|
|
74
|
-
|
|
101
|
+
differsFromReset,
|
|
75
102
|
};
|
|
76
103
|
}
|
|
77
104
|
|
|
@@ -93,25 +93,33 @@ export function executedIn(map: Uint8Array, block: AddressRange): AddressRange[]
|
|
|
93
93
|
return ranges;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Pages 0-3 ($0000-$03ff) are in entry_state.json: a written block that is all in them is no segment. */
|
|
97
|
+
export const FIRST_SEGMENT_ADDRESS = 0x0400;
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
100
|
* The segments of a capture: the blocks that the stages wrote, without loader
|
|
98
|
-
* leftovers
|
|
101
|
+
* leftovers and without blocks that are all in pages 0-3 (the entry state
|
|
102
|
+
* holds them), joined into ranges that do not overlap or touch. A block
|
|
99
103
|
* is a leftover when it holds the entry of an intermediate stage that started
|
|
100
104
|
* after the block's writer, and no write from that stage on replaced the code
|
|
101
105
|
* at that entry. Code that a stage ran before a later stage wrote over
|
|
102
106
|
* it never drops a block, and the block that holds the capture entry is always
|
|
103
|
-
* a segment.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
107
|
+
* a segment. A block that an intermediate stage wrote inside a leftover, for
|
|
108
|
+
* example a variable of the loader, is a leftover too. The filter comes before
|
|
109
|
+
* the join: a join first would drop the program with a leftover. `writtenBy` is
|
|
110
|
+
* the last stage that wrote into the range.
|
|
106
111
|
*/
|
|
107
112
|
export function captureSegments(stages: ReadonlyArray<{ entry: number; wrote: readonly AddressRange[] }>, entry: number): Array<AddressRange & { writtenBy: number }> {
|
|
108
113
|
const last = stages.length - 1;
|
|
109
114
|
const replaced = (from: number, address: number) => stages.slice(from).some((stage) => inRanges(stage.wrote, address));
|
|
110
115
|
const leftover = (block: AddressRange, writer: number) =>
|
|
111
116
|
!contains(block, entry) && stages.some((stage, index) => index > writer && index < last && contains(block, stage.entry) && !replaced(index, stage.entry));
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const written = stages.flatMap((stage, index) => stage.wrote.map((range) => ({ start: range.start, end: range.end, writtenBy: index })));
|
|
118
|
+
const leftovers = written.filter((block) => leftover(block, block.writtenBy));
|
|
119
|
+
const inLeftover = (block: AddressRange & { writtenBy: number }) =>
|
|
120
|
+
block.writtenBy < last && !contains(block, entry) && leftovers.some((left) => block.start >= left.start && block.end <= left.end);
|
|
121
|
+
const blocks = written
|
|
122
|
+
.filter((block) => !leftover(block, block.writtenBy) && !inLeftover(block) && (block.end >= FIRST_SEGMENT_ADDRESS || contains(block, entry)))
|
|
115
123
|
.sort((a, b) => a.start - b.start);
|
|
116
124
|
const segments: Array<AddressRange & { writtenBy: number }> = [];
|
|
117
125
|
for (const block of blocks) {
|