@henols/c64-re-tools 2.0.0-rc.9 → 2.0.0
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 +97 -116
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +18 -4
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
// Reads the emulator's memory access map as coverage bytes
|
|
1
|
+
// Reads the emulator's memory access map as coverage bytes: one byte
|
|
2
2
|
// per address, RAM, ROM and I/O apart. The MCP tool and the skill scripts share it.
|
|
3
3
|
|
|
4
4
|
import { COVERAGE, type CodeRange } from "../c64.ts";
|
|
5
5
|
import { MAX_MEMMAP_RANGES, type MemmapRange } from "../protocol/vice.ts";
|
|
6
|
-
import { readProjectFile } from "./transfer.ts";
|
|
7
6
|
import type { ViceSessionClient } from "./vice-session.ts";
|
|
8
7
|
|
|
9
|
-
/** A full coverage file: offset n is address n
|
|
8
|
+
/** A full coverage file: offset n is address n. */
|
|
10
9
|
export const FULL_COVERAGE = 0x10000;
|
|
11
10
|
|
|
12
|
-
/** Reads full coverage files from the project, one map per file. `fail` refuses a file of another size. */
|
|
13
|
-
export function readCoverageMaps(paths: string[], fail: (message: string) => never): Uint8Array[] {
|
|
14
|
-
return paths.map((path) => {
|
|
15
|
-
const { bytes } = readProjectFile(path);
|
|
16
|
-
if (bytes.length !== FULL_COVERAGE) {
|
|
17
|
-
fail(`--coverage ${path} has ${bytes.length} bytes; a full coverage file has 65536, one for each address. Write it with c64_memmap action read and out, without start and end.`);
|
|
18
|
-
}
|
|
19
|
-
return bytes;
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Joins coverage maps: an address has every flag that any run gave it. */
|
|
24
|
-
export function joinCoverage(maps: readonly Uint8Array[]): Uint8Array {
|
|
25
|
-
const joined = new Uint8Array(FULL_COVERAGE);
|
|
26
|
-
for (const map of maps) for (let address = 0; address < FULL_COVERAGE; address++) joined[address]! |= map[address]!;
|
|
27
|
-
return joined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Reads full coverage files from the project and joins them: an address has
|
|
32
|
-
* every flag that any run gave it. `fail` refuses a file of another size.
|
|
33
|
-
*/
|
|
34
|
-
export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
|
|
35
|
-
return paths.length === 0 ? undefined : joinCoverage(readCoverageMaps(paths, fail));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
11
|
/** The coverage bytes of start..end, read from the session in parts. */
|
|
39
12
|
export async function readCoverage(session: Pick<ViceSessionClient, "memmap">, start = 0x0000, end = 0xffff): Promise<Uint8Array> {
|
|
40
13
|
const map = new Uint8Array(end - start + 1);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// An image.json of the c64-unpacker skill (a 64 KiB memory and its segments)
|
|
2
|
+
// or a PRG, read from the project to put it into the knowledge.
|
|
3
|
+
|
|
4
|
+
import { posix } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { formatC64Address, prgRange } from "../c64.ts";
|
|
7
|
+
import { readProjectFile } from "./transfer.ts";
|
|
8
|
+
|
|
9
|
+
/** A file that is no image: the scripts report it as a bad call. */
|
|
10
|
+
export class ImageError extends Error {
|
|
11
|
+
override name = "ImageError";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Image {
|
|
15
|
+
segments: Array<{ start: number; end: number }>;
|
|
16
|
+
/** The byte at an address inside a segment. */
|
|
17
|
+
byteAt: (address: number) => number;
|
|
18
|
+
entry?: number;
|
|
19
|
+
/** entry_state.json next to image.json. */
|
|
20
|
+
statePath?: string;
|
|
21
|
+
/** The stages of the capture in image.json, as the unpacker wrote them. */
|
|
22
|
+
stages?: Array<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Reads an image.json or a PRG, relative to the project folder. */
|
|
26
|
+
export function readImage(path: string): Image {
|
|
27
|
+
const { bytes } = readProjectFile(path);
|
|
28
|
+
if (!path.endsWith(".json")) {
|
|
29
|
+
let range: ReturnType<typeof prgRange>;
|
|
30
|
+
try {
|
|
31
|
+
range = prgRange(bytes);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new ImageError(`${path}: ${(error as Error).message}`);
|
|
34
|
+
}
|
|
35
|
+
return { segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]! };
|
|
36
|
+
}
|
|
37
|
+
const fail = (what: string): never => {
|
|
38
|
+
throw new ImageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
39
|
+
};
|
|
40
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
let parsed: unknown;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return fail((error as Error).message);
|
|
46
|
+
}
|
|
47
|
+
const manifest = isObject(parsed) ? parsed : fail("it is not a JSON object");
|
|
48
|
+
const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
|
|
49
|
+
if (typeof manifest.image !== "string") fail("it has no image");
|
|
50
|
+
if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
|
|
51
|
+
const segments = (manifest.segments as unknown[])
|
|
52
|
+
.map((segment, index) => (isObject(segment) ? segment : fail(`segment ${index + 1} is not an object`)))
|
|
53
|
+
.map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
|
|
54
|
+
.sort((a, b) => a.start - b.start);
|
|
55
|
+
segments.forEach((segment, index) => {
|
|
56
|
+
if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
|
|
57
|
+
if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
|
|
58
|
+
});
|
|
59
|
+
const directory = posix.dirname(path);
|
|
60
|
+
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
61
|
+
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
62
|
+
const image: Image = { segments, byteAt: (at) => memory[at]!, statePath: posix.join(directory, "entry_state.json") };
|
|
63
|
+
if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
|
|
64
|
+
if (Array.isArray(manifest.stages)) image.stages = manifest.stages.map((stage, index) => (isObject(stage) ? stage : fail(`stage ${index + 1} is not an object`)));
|
|
65
|
+
return image;
|
|
66
|
+
}
|
package/src/host-client/steps.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The action steps of the step language
|
|
1
|
+
// The action steps of the step language: write, registers, joystick,
|
|
2
2
|
// type, frames and runUntil. c64-testing scenarios and the c64-unpacker
|
|
3
3
|
// steps file share them; each caller adds its own steps around them.
|
|
4
4
|
|
|
@@ -90,12 +90,12 @@ export function checkActionStep(kind: string, step: Record<string, unknown>, at:
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
case "frames":
|
|
93
|
-
if (!isInteger(step.frames, 1,
|
|
93
|
+
if (!isInteger(step.frames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs a frame count from 1 to ${MAX_TIMEOUT_FRAMES}`);
|
|
94
94
|
return;
|
|
95
95
|
case "runUntil": {
|
|
96
96
|
const target = step.runUntil;
|
|
97
97
|
const keys = isObject(target) ? Object.keys(target).sort().join(",") : "";
|
|
98
|
-
// at, raster, at with raster (the address only on that line
|
|
98
|
+
// at, raster, at with raster (the address only on that line), or memory.
|
|
99
99
|
if (!["at", "raster", "at,raster", "memory"].includes(keys)) return fail(`${at}: runUntil needs at, raster, at and raster, or memory`);
|
|
100
100
|
const until = target as Record<string, unknown>;
|
|
101
101
|
if ("raster" in until && !isInteger(until.raster, 0, MAX_RASTER_LINE)) fail(`${at}: runUntil.raster must be a raster line from 0 to ${MAX_RASTER_LINE}`);
|
|
@@ -154,7 +154,12 @@ export async function runActionStep(client: ViceSessionClient, step: ActionStep,
|
|
|
154
154
|
await client.keyboard(textToPetscii(step.type));
|
|
155
155
|
} else if ("frames" in step) {
|
|
156
156
|
await stop(client);
|
|
157
|
-
|
|
157
|
+
// One advance-frames runs at most MAX_EXECUTION_COUNT frames. Each part ends at the raster position
|
|
158
|
+
// where it started, so the parts give the same frames as one run; a breakpoint ends them all.
|
|
159
|
+
for (let left = step.frames; left > 0; left -= MAX_EXECUTION_COUNT) {
|
|
160
|
+
const part = await client.execution({ action: "advance-frames", count: Math.min(left, MAX_EXECUTION_COUNT), space: "c64" });
|
|
161
|
+
if (part.stopReason !== "frames") break;
|
|
162
|
+
}
|
|
158
163
|
} else {
|
|
159
164
|
const until = step.runUntil;
|
|
160
165
|
const target: RunTarget =
|
package/src/host-client/tools.ts
CHANGED
|
@@ -87,3 +87,4 @@ export async function hostTools(options: ToolCallOptions = {}): Promise<{ host:
|
|
|
87
87
|
export { WireFailure } from "../protocol/messages.ts";
|
|
88
88
|
export { DISK_ACTIONS } from "../protocol/tools.ts";
|
|
89
89
|
export { JOYSTICK_DIRECTIONS, MAX_BASELINES, MAX_EXECUTION_COUNT, MAX_KEYBOARD_BYTES, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, maskCoversFrame } from "../protocol/vice.ts";
|
|
90
|
+
export { MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, type TraceHit, type TraceKind } from "../protocol/vice.ts";
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
// Reads project files on the client side for transfer to the Host Runtime.
|
|
2
2
|
// The host never sees a client path: only bytes and the file's type.
|
|
3
3
|
|
|
4
|
-
import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, extname, join, relative, sep } from "node:path";
|
|
6
6
|
|
|
7
|
-
import { isInside, projectRoot, resolveProjectFile, resolveProjectPath } from "../project.ts";
|
|
7
|
+
import { isInside, ProjectError, projectRoot, resolveProject, resolveProjectFile, resolveProjectPath } from "../project.ts";
|
|
8
8
|
import { MAX_ATTACHMENT_BYTES } from "../protocol/framing.ts";
|
|
9
9
|
import { WireFailure } from "../protocol/messages.ts";
|
|
10
10
|
import { MAX_TREE_FILES, type SourceTree } from "../protocol/tools.ts";
|
|
11
11
|
|
|
12
|
+
/** Runs a project lookup and gives a refusal of the project rules as a WireFailure with the same code and message. */
|
|
13
|
+
function asWireFailure<T>(lookup: () => T): T {
|
|
14
|
+
try {
|
|
15
|
+
return lookup();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof ProjectError) throw new WireFailure(error.code, error.message);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The project folder of a call that names it with `project` (relative to the
|
|
24
|
+
* working directory) or not. Throws WireFailure when the rules
|
|
25
|
+
* refuse it, for example at a folder with several projects below it.
|
|
26
|
+
*/
|
|
27
|
+
export function projectFolder(project: string | undefined): string {
|
|
28
|
+
return asWireFailure(() => resolveProject(project, process.cwd(), { remedy: "Give project: <folder>" }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The project folder of this process (--project or the project lookup), or a WireFailure. */
|
|
32
|
+
function currentProject(): string {
|
|
33
|
+
return asWireFailure(projectRoot);
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
export interface ProjectFile {
|
|
13
37
|
bytes: Buffer;
|
|
14
38
|
/** Lowercase extension without the dot; empty when the file has none. */
|
|
@@ -21,16 +45,16 @@ export interface ProjectFile {
|
|
|
21
45
|
* the file is too large to transfer.
|
|
22
46
|
*/
|
|
23
47
|
export function readProjectFile(path: string, options: { root?: string } = {}): ProjectFile {
|
|
24
|
-
const root = options.root ??
|
|
48
|
+
const root = options.root ?? currentProject();
|
|
25
49
|
let real: string;
|
|
26
50
|
try {
|
|
27
51
|
real = resolveProjectFile(path, root);
|
|
28
52
|
} catch (error) {
|
|
29
53
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
30
|
-
throw new WireFailure("not-found", `There is no file at ${path} in the project
|
|
54
|
+
throw new WireFailure("not-found", `There is no file at ${path} in the project folder.`);
|
|
31
55
|
}
|
|
32
56
|
if (error instanceof TypeError) {
|
|
33
|
-
throw new WireFailure("invalid-input", `${path}: give a path relative to the project
|
|
57
|
+
throw new WireFailure("invalid-input", `${path}: give a path relative to the project folder that stays inside it.`);
|
|
34
58
|
}
|
|
35
59
|
throw error;
|
|
36
60
|
}
|
|
@@ -48,14 +72,18 @@ export function readProjectFile(path: string, options: { root?: string } = {}):
|
|
|
48
72
|
* project, also one that leaves it through a symbolic link.
|
|
49
73
|
*/
|
|
50
74
|
export function writeProjectFile(path: string, bytes: Uint8Array, options: { root?: string; what?: string } = {}): void {
|
|
51
|
-
const root = options.root ??
|
|
52
|
-
const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project
|
|
75
|
+
const root = options.root ?? currentProject();
|
|
76
|
+
const refuse = () => new WireFailure("invalid-input", `${options.what ?? path}: give a path relative to the project folder that stays inside it, not ${path}.`);
|
|
53
77
|
let file: string;
|
|
54
78
|
try {
|
|
55
79
|
file = resolveProjectPath(path, root);
|
|
56
80
|
} catch {
|
|
57
81
|
throw refuse();
|
|
58
82
|
}
|
|
83
|
+
// The nearest folder that exists must be inside the project before a folder is made below it.
|
|
84
|
+
let existing = dirname(file);
|
|
85
|
+
while (!existsSync(existing)) existing = dirname(existing);
|
|
86
|
+
if (!isInside(realpathSync(root), realpathSync(existing))) throw refuse();
|
|
59
87
|
mkdirSync(dirname(file), { recursive: true });
|
|
60
88
|
if (!isInside(realpathSync(root), realpathSync(dirname(file)))) throw refuse();
|
|
61
89
|
let link = false;
|
|
@@ -72,18 +100,18 @@ export function writeProjectFile(path: string, bytes: Uint8Array, options: { roo
|
|
|
72
100
|
const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", ".c64-re-tools"]);
|
|
73
101
|
|
|
74
102
|
/**
|
|
75
|
-
* Reads a project
|
|
103
|
+
* Reads a project folder as a source tree for staging: ordinary
|
|
76
104
|
* files with relative POSIX paths. A symbolic link is followed only when its
|
|
77
105
|
* target stays inside the source root; otherwise the tree is refused.
|
|
78
106
|
*/
|
|
79
107
|
export function readProjectTree(path: string, options: { root?: string } = {}): { files: SourceTree; contents: Buffer[] } {
|
|
80
|
-
const projectRootPath = options.root ??
|
|
108
|
+
const projectRootPath = options.root ?? currentProject();
|
|
81
109
|
let sourceRoot: string;
|
|
82
110
|
try {
|
|
83
111
|
sourceRoot = resolveProjectFile(path, projectRootPath);
|
|
84
112
|
} catch (error) {
|
|
85
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new WireFailure("not-found", `There is no directory ${path} in the project
|
|
86
|
-
if (error instanceof TypeError) throw new WireFailure("invalid-input", `${path}: give a directory relative to the project
|
|
113
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new WireFailure("not-found", `There is no directory ${path} in the project folder.`);
|
|
114
|
+
if (error instanceof TypeError) throw new WireFailure("invalid-input", `${path}: give a directory relative to the project folder that stays inside it.`);
|
|
87
115
|
throw error;
|
|
88
116
|
}
|
|
89
117
|
if (!statSync(sourceRoot).isDirectory()) throw new WireFailure("invalid-input", `${path} is not a directory.`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// one VICE. Exposes C64-domain operations only.
|
|
3
3
|
|
|
4
4
|
import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
|
|
5
|
-
import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
|
|
5
|
+
import { MAX_MEMORY_READ, validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type ProfileEntry, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
|
|
6
6
|
import { HostConnection } from "./connect.ts";
|
|
7
7
|
import { readProjectFile } from "./transfer.ts";
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP s
|
|
|
13
13
|
const LOST_BEFORE_ANSWER = "The connection to the c64-re-tools host runtime was lost before the emulator answered. No machine state was lost. Do the call again.";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* The loss of a session that never answered
|
|
16
|
+
* The loss of a session that never answered: it had no machine state,
|
|
17
17
|
* so the caller can do the call again on a new session.
|
|
18
18
|
*/
|
|
19
19
|
export class LostBeforeAnswer extends WireFailure {
|
|
@@ -127,21 +127,21 @@ export class ViceSessionClient {
|
|
|
127
127
|
return this.#request("runUntil", params);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/** Loads a project PRG into memory without reset or start; finishes stopped. */
|
|
131
|
-
async programLoad(params: { path: string; address?: number }): Promise<{ state: RunState; loadAddress: number; size: number }> {
|
|
132
|
-
const file = readProjectFile(params.path);
|
|
130
|
+
/** Loads a project PRG into memory without reset or start; finishes stopped. `root` is the project folder; it stays on this side. */
|
|
131
|
+
async programLoad(params: { path: string; address?: number; root?: string }): Promise<{ state: RunState; loadAddress: number; size: number }> {
|
|
132
|
+
const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
|
|
133
133
|
return this.#request("programLoad", params.address === undefined ? {} : { address: params.address }, [file.bytes]);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** Autostarts a project program or image, as VICE's autostart does. */
|
|
137
|
-
async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState; emulatorFlaw?: string }> {
|
|
138
|
-
const file = readProjectFile(params.path);
|
|
136
|
+
/** Autostarts a project program or image, as VICE's autostart does. `root` is the project folder; it stays on this side. */
|
|
137
|
+
async autostart(params: { path: string; index: number; run: boolean; root?: string }): Promise<{ state: RunState; emulatorFlaw?: string }> {
|
|
138
|
+
const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
|
|
139
139
|
return this.#request("autostart", { type: file.type, index: params.index, run: params.run }, [file.bytes]);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/** Attaches a project disk image to drive 8. */
|
|
143
|
-
async diskAttach(params: { path: string }): Promise<{ attached: boolean }> {
|
|
144
|
-
const file = readProjectFile(params.path);
|
|
142
|
+
/** Attaches a project disk image to drive 8. `root` is the project folder; it stays on this side. */
|
|
143
|
+
async diskAttach(params: { path: string; root?: string }): Promise<{ attached: boolean }> {
|
|
144
|
+
const file = readProjectFile(params.path, params.root === undefined ? {} : { root: params.root });
|
|
145
145
|
return this.#request("diskAttach", { type: file.type }, [file.bytes]);
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -178,6 +178,11 @@ export class ViceSessionClient {
|
|
|
178
178
|
return this.#request("cpuHistory", params);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** Records each hit of the points while the program runs on. */
|
|
182
|
+
trace(params: ViceOperations["trace"]["params"]): Promise<ViceOperations["trace"]["result"]> {
|
|
183
|
+
return this.#request("trace", params);
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
backtrace(params: ViceOperations["backtrace"]["params"]): Promise<ViceOperations["backtrace"]["result"]> {
|
|
182
187
|
return this.#request("backtrace", params);
|
|
183
188
|
}
|
|
@@ -186,8 +191,10 @@ export class ViceSessionClient {
|
|
|
186
191
|
return this.#request("timing", { action });
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
profile(limit: number): Promise<
|
|
190
|
-
|
|
194
|
+
profile(params: { action: "read"; limit: number }): Promise<{ entries: ProfileEntry[] }>;
|
|
195
|
+
profile(params: { action: "clear" }): Promise<{ cleared: boolean }>;
|
|
196
|
+
profile(params: ViceOperations["profile"]["params"]): Promise<ViceOperations["profile"]["result"]> {
|
|
197
|
+
return this.#request("profile", params);
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
/** Action read gives the ranges; action clear gives cleared. */
|
|
@@ -231,7 +238,7 @@ export class ViceSessionClient {
|
|
|
231
238
|
return this.#request("snapshot", params);
|
|
232
239
|
}
|
|
233
240
|
|
|
234
|
-
/** The bytes of a named snapshot, as a .vsf file holds them
|
|
241
|
+
/** The bytes of a named snapshot, as a .vsf file holds them. */
|
|
235
242
|
async snapshotExport(name: string): Promise<Buffer> {
|
|
236
243
|
const { attachments } = await this.#requestWithAttachments("snapshotFile", { action: "export", name });
|
|
237
244
|
const [bytes] = attachments;
|
|
@@ -266,12 +273,12 @@ export class ViceSessionClient {
|
|
|
266
273
|
return this.#lost !== undefined;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
/** True when the emulator answered a request: then the session had machine state to lose
|
|
276
|
+
/** True when the emulator answered a request: then the session had machine state to lose. */
|
|
270
277
|
get answered(): boolean {
|
|
271
278
|
return this.#answered;
|
|
272
279
|
}
|
|
273
280
|
|
|
274
|
-
/** The loss of a lost session, when no request got it; undefined while the session lives
|
|
281
|
+
/** The loss of a lost session, when no request got it; undefined while the session lives. */
|
|
275
282
|
get unreportedLoss(): WireFailure | undefined {
|
|
276
283
|
return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
|
|
277
284
|
}
|
|
@@ -314,3 +321,13 @@ export class ViceSessionClient {
|
|
|
314
321
|
void this.#connection.close();
|
|
315
322
|
}
|
|
316
323
|
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
/** Reads `size` bytes from `address` of the C64 in reads of at most 4 KiB, in one view: up to all 64 KiB. */
|
|
327
|
+
export async function readMemoryInParts(client: Pick<ViceSessionClient, "memoryRead">, address: number, size: number, view: "cpu" | "ram"): Promise<Buffer> {
|
|
328
|
+
const parts: Buffer[] = [];
|
|
329
|
+
for (let at = address; at < address + size; at += MAX_MEMORY_READ) {
|
|
330
|
+
parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, address + size - at), space: "c64", view })).data, "hex"));
|
|
331
|
+
}
|
|
332
|
+
return Buffer.concat(parts);
|
|
333
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Conflicts: current rows that break a current-row rule. One computer
|
|
2
|
+
// never writes them, but git joins the knowledge of two computers line by
|
|
3
|
+
// line, so after a merge two names can be current at one address, or two
|
|
4
|
+
// regions can overlap. Reads show them; writes are refused until a merge
|
|
5
|
+
// revision decides them.
|
|
6
|
+
|
|
7
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
8
|
+
|
|
9
|
+
import type { HistoryEntry } from "./history.ts";
|
|
10
|
+
import {
|
|
11
|
+
COMMENT_COLUMNS,
|
|
12
|
+
MACRO_COLUMNS,
|
|
13
|
+
MACRO_USE_COLUMNS,
|
|
14
|
+
MODULE_COLUMNS,
|
|
15
|
+
OPERAND_COLUMNS,
|
|
16
|
+
PATCH_COLUMNS,
|
|
17
|
+
REFERENCE_COLUMNS,
|
|
18
|
+
REGION_COLUMNS,
|
|
19
|
+
SUPPRESSION_COLUMNS,
|
|
20
|
+
SYMBOL_COLUMNS,
|
|
21
|
+
toComment,
|
|
22
|
+
toMacro,
|
|
23
|
+
toMacroUse,
|
|
24
|
+
toModule,
|
|
25
|
+
toOperand,
|
|
26
|
+
toPatch,
|
|
27
|
+
toReference,
|
|
28
|
+
toRegion,
|
|
29
|
+
toSuppression,
|
|
30
|
+
toSymbol,
|
|
31
|
+
} from "./read.ts";
|
|
32
|
+
import type { Space } from "./schema.ts";
|
|
33
|
+
import { compareText, sqlName } from "./text.ts";
|
|
34
|
+
|
|
35
|
+
/** The current-row rules, each with what breaks it. */
|
|
36
|
+
export const CONFLICT_RULES = {
|
|
37
|
+
"symbol-address": "two symbols at one address",
|
|
38
|
+
"analyzer-name-address": "two names of one analyzer at one address",
|
|
39
|
+
"analyzer-name-name": "one analyzer name at two addresses",
|
|
40
|
+
"symbol-name": "one name at two addresses",
|
|
41
|
+
region: "regions that overlap",
|
|
42
|
+
comment: "two comments at one place",
|
|
43
|
+
reference: "the same reference two times",
|
|
44
|
+
operand: "two operand expressions at one address",
|
|
45
|
+
"module-start": "two modules at one address",
|
|
46
|
+
"module-file": "one module file at two addresses",
|
|
47
|
+
"suppression-symbol": "the same symbol suppression two times",
|
|
48
|
+
"suppression-reference": "the same reference suppression two times",
|
|
49
|
+
"suppression-region": "region suppressions that overlap",
|
|
50
|
+
patch: "patches that overlap",
|
|
51
|
+
macro: "two macros with one name",
|
|
52
|
+
"macro-use": "macro uses that overlap",
|
|
53
|
+
} as const;
|
|
54
|
+
export type ConflictRule = keyof typeof CONFLICT_RULES;
|
|
55
|
+
|
|
56
|
+
/** One row of a conflict: the row with its revisions and its id. */
|
|
57
|
+
export type ConflictRow = HistoryEntry & { id: string };
|
|
58
|
+
|
|
59
|
+
/** Current rows that break one rule together, oldest first. */
|
|
60
|
+
export interface Conflict {
|
|
61
|
+
rule: ConflictRule;
|
|
62
|
+
space: Space;
|
|
63
|
+
rows: ConflictRow[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Raw = Record<string, unknown>;
|
|
67
|
+
|
|
68
|
+
interface TableRule {
|
|
69
|
+
rule: ConflictRule;
|
|
70
|
+
table: string;
|
|
71
|
+
entity: HistoryEntry["entity"];
|
|
72
|
+
columns: string;
|
|
73
|
+
map: (raw: Raw) => HistoryEntry["row"];
|
|
74
|
+
/** The columns that must differ between current rows, or "overlap" for address ranges. */
|
|
75
|
+
key: readonly string[] | "overlap";
|
|
76
|
+
/** Rows of the table that the rule is about. */
|
|
77
|
+
where?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const RULES: readonly TableRule[] = [
|
|
81
|
+
{ rule: "symbol-address", table: "symbols", entity: "symbol", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["address"] },
|
|
82
|
+
{ rule: "symbol-name", table: "symbols", entity: "symbol", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["name"] },
|
|
83
|
+
{ rule: "analyzer-name-address", table: "analyzer_names", entity: "analyzer-name", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["address", "origin"] },
|
|
84
|
+
{ rule: "analyzer-name-name", table: "analyzer_names", entity: "analyzer-name", columns: SYMBOL_COLUMNS, map: toSymbol, key: ["name", "origin"] },
|
|
85
|
+
{ rule: "region", table: "regions", entity: "region", columns: REGION_COLUMNS, map: toRegion, key: "overlap" },
|
|
86
|
+
{ rule: "comment", table: "comments", entity: "comment", columns: COMMENT_COLUMNS, map: toComment, key: ["address", "placement"] },
|
|
87
|
+
{ rule: "reference", table: "references", entity: "reference", columns: REFERENCE_COLUMNS, map: toReference, key: ["from_address", "to_address", "kind"] },
|
|
88
|
+
{ rule: "operand", table: "operands", entity: "operand", columns: OPERAND_COLUMNS, map: toOperand, key: ["address"] },
|
|
89
|
+
{ rule: "module-start", table: "modules", entity: "module", columns: MODULE_COLUMNS, map: toModule, key: ["start_address"] },
|
|
90
|
+
{ rule: "module-file", table: "modules", entity: "module", columns: MODULE_COLUMNS, map: toModule, key: ["file"] },
|
|
91
|
+
{ rule: "suppression-symbol", table: "suppressions", entity: "suppression", columns: SUPPRESSION_COLUMNS, map: toSuppression, key: ["start_address"], where: "entity = 'symbol'" },
|
|
92
|
+
{
|
|
93
|
+
rule: "suppression-reference",
|
|
94
|
+
table: "suppressions",
|
|
95
|
+
entity: "suppression",
|
|
96
|
+
columns: SUPPRESSION_COLUMNS,
|
|
97
|
+
map: toSuppression,
|
|
98
|
+
key: ["start_address", "to_address", "kind"],
|
|
99
|
+
where: "entity = 'reference'",
|
|
100
|
+
},
|
|
101
|
+
{ rule: "suppression-region", table: "suppressions", entity: "suppression", columns: SUPPRESSION_COLUMNS, map: toSuppression, key: "overlap", where: "entity = 'region'" },
|
|
102
|
+
{ rule: "patch", table: "patches", entity: "patch", columns: PATCH_COLUMNS, map: toPatch, key: "overlap" },
|
|
103
|
+
{ rule: "macro", table: "macros", entity: "macro", columns: MACRO_COLUMNS, map: toMacro, key: ["name"] },
|
|
104
|
+
{ rule: "macro-use", table: "macro_uses", entity: "macro-use", columns: MACRO_USE_COLUMNS, map: toMacroUse, key: "overlap" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function conflictRow(rule: TableRule, raw: Raw): ConflictRow {
|
|
108
|
+
return { entity: rule.entity, row: rule.map(raw), from: raw.valid_from_revision as string, id: raw.id as string } as ConflictRow;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const byAge = (a: ConflictRow, b: ConflictRow) => compareText(a.from, b.from) || compareText(a.id, b.id);
|
|
112
|
+
|
|
113
|
+
/** The current rows of one rule that share a key, in groups. */
|
|
114
|
+
function sameKey(db: DatabaseSync, rule: TableRule, key: readonly string[]): Conflict[] {
|
|
115
|
+
const where = `valid_to_revision IS NULL${rule.where === undefined ? "" : ` AND ${rule.where}`}`;
|
|
116
|
+
const columns = ["space", ...key].join(", ");
|
|
117
|
+
const rows = db
|
|
118
|
+
.prepare(`SELECT ${rule.columns}, id FROM ${sqlName(rule.table)} WHERE ${where} AND (${columns}) IN (SELECT ${columns} FROM ${sqlName(rule.table)} WHERE ${where} GROUP BY ${columns} HAVING count(*) > 1)`)
|
|
119
|
+
.all() as Raw[];
|
|
120
|
+
const groups = new Map<string, Conflict>();
|
|
121
|
+
for (const raw of rows) {
|
|
122
|
+
const id = JSON.stringify(["space", ...key].map((column) => raw[column]));
|
|
123
|
+
const group = groups.get(id) ?? { rule: rule.rule, space: raw.space as Space, rows: [] };
|
|
124
|
+
group.rows.push(conflictRow(rule, raw));
|
|
125
|
+
groups.set(id, group);
|
|
126
|
+
}
|
|
127
|
+
return [...groups.values()];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The current rows of one rule whose address ranges overlap, in groups of rows that overlap in a chain. */
|
|
131
|
+
function overlapping(db: DatabaseSync, rule: TableRule): Conflict[] {
|
|
132
|
+
const rows = db
|
|
133
|
+
.prepare(`SELECT ${rule.columns}, id FROM ${sqlName(rule.table)} WHERE valid_to_revision IS NULL${rule.where === undefined ? "" : ` AND ${rule.where}`} ORDER BY space, start_address, end_address`)
|
|
134
|
+
.all() as Raw[];
|
|
135
|
+
const conflicts: Conflict[] = [];
|
|
136
|
+
let group: Conflict | undefined;
|
|
137
|
+
let end = -1;
|
|
138
|
+
for (const raw of rows) {
|
|
139
|
+
if (group !== undefined && raw.space === group.space && (raw.start_address as number) <= end) {
|
|
140
|
+
group.rows.push(conflictRow(rule, raw));
|
|
141
|
+
end = Math.max(end, raw.end_address as number);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (group !== undefined && group.rows.length > 1) conflicts.push(group);
|
|
145
|
+
group = { rule: rule.rule, space: raw.space as Space, rows: [conflictRow(rule, raw)] };
|
|
146
|
+
end = raw.end_address as number;
|
|
147
|
+
}
|
|
148
|
+
if (group !== undefined && group.rows.length > 1) conflicts.push(group);
|
|
149
|
+
return conflicts;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every conflict of the knowledge, rule by rule; the rows of each, oldest first. */
|
|
153
|
+
export function conflicts(db: DatabaseSync | undefined): Conflict[] {
|
|
154
|
+
if (db === undefined) return [];
|
|
155
|
+
return RULES.flatMap((rule) => (rule.key === "overlap" ? overlapping(db, rule) : sameKey(db, rule, rule.key))).map((conflict) => ({ ...conflict, rows: conflict.rows.sort(byAge) }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The table of a conflict rule. */
|
|
159
|
+
export function conflictTable(rule: ConflictRule): string {
|
|
160
|
+
return RULES.find((item) => item.rule === rule)!.table;
|
|
161
|
+
}
|