@henols/c64-re-tools 2.0.0-rc.8 → 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 +98 -115
- 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 +43 -8
- 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/src/trace.ts +55 -23
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
package/src/protocol/tools.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type Fields, invalid, isInteger, isObject, isOneOf, onlyFields, Protoco
|
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Native-tool operations. Short-lived requests on "tool" connections; they
|
|
8
|
-
// never touch an emulator or knowledge.
|
|
8
|
+
// never touch an emulator or the knowledge.
|
|
9
9
|
|
|
10
10
|
/** A transferred source tree: relative POSIX paths, one attachment per file in this order. */
|
|
11
11
|
export type SourceTree = Array<{ path: string; size: number }>;
|
|
@@ -80,7 +80,7 @@ export interface ToolStatus {
|
|
|
80
80
|
runs: boolean;
|
|
81
81
|
/** What is wrong and what to do, when it is missing or does not run. */
|
|
82
82
|
problem?: string;
|
|
83
|
-
/** Known flaws of this version, each with the version that fixes it
|
|
83
|
+
/** Known flaws of this version, each with the version that fixes it. The tool still runs. */
|
|
84
84
|
flaws?: string[];
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -109,20 +109,6 @@ export function isRelativePath(value: unknown): value is string {
|
|
|
109
109
|
return typeof value === "string" && value.length <= 1024 && RELATIVE_PATH.test(value) && !value.includes("//") && !value.endsWith("/");
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** Validates a transferred source tree against its attachments. Throws WireFailure(invalid-input). */
|
|
113
|
-
export function validateSourceTree(value: unknown, attachments: readonly Uint8Array[]): SourceTree {
|
|
114
|
-
if (!Array.isArray(value) || value.length > MAX_TREE_FILES) invalid(`files must list at most ${MAX_TREE_FILES} files`);
|
|
115
|
-
if (value.length !== attachments.length) invalid("each listed file needs exactly one attachment");
|
|
116
|
-
const seen = new Set<string>();
|
|
117
|
-
return value.map((entry, index) => {
|
|
118
|
-
if (!isObject(entry) || !isRelativePath(entry.path)) invalid("each file needs a relative path inside the source root");
|
|
119
|
-
if (seen.has(entry.path)) invalid(`the file ${entry.path} is listed twice`);
|
|
120
|
-
seen.add(entry.path);
|
|
121
|
-
if (entry.size !== attachments[index]!.length) invalid(`the size of ${entry.path} does not match its bytes`);
|
|
122
|
-
return { path: entry.path, size: entry.size as number };
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
112
|
/** Refuses anything but exactly one attachment with at least one byte, named by what it must hold. */
|
|
127
113
|
function oneFile(attachments: readonly Uint8Array[], what: string): void {
|
|
128
114
|
if (attachments.length !== 1 || attachments[0]!.length === 0) invalid(`${what} must be the one attachment, and it must not be empty`);
|
package/src/protocol/vice.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const MAX_WATCH_SIZE = 256;
|
|
|
94
94
|
export interface Breakpoint {
|
|
95
95
|
id: number;
|
|
96
96
|
address: number;
|
|
97
|
-
/** A range from address: the CPU stops at the first instruction it executes in it
|
|
97
|
+
/** A range from address: the CPU stops at the first instruction it executes in it. One address when not given. */
|
|
98
98
|
size?: number;
|
|
99
99
|
space: Space;
|
|
100
100
|
enabled: boolean;
|
|
@@ -168,7 +168,7 @@ export interface CiaState {
|
|
|
168
168
|
ddrB: number;
|
|
169
169
|
timerA: number;
|
|
170
170
|
timerB: number;
|
|
171
|
-
/** The values that a timer loads at an underflow: write-only on the chip, from VICE's dump
|
|
171
|
+
/** The values that a timer loads at an underflow: write-only on the chip, from VICE's dump. */
|
|
172
172
|
latchA: number;
|
|
173
173
|
latchB: number;
|
|
174
174
|
controlA: number;
|
|
@@ -217,6 +217,50 @@ export interface HistoryEntry {
|
|
|
217
217
|
rasterCycle?: number;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Traces: checkpoints that record each hit and let the program run on.
|
|
221
|
+
|
|
222
|
+
export const TRACE_KINDS = ["exec", "read", "write"] as const;
|
|
223
|
+
export type TraceKind = (typeof TRACE_KINDS)[number];
|
|
224
|
+
export const MAX_TRACE_POINTS = 8;
|
|
225
|
+
export const MAX_TRACE_FRAMES = 3000;
|
|
226
|
+
export const MAX_TRACE_HITS = 20_000;
|
|
227
|
+
export const TRACE_ENDS = ["frames", "hits", "breakpoint", "watchpoint", "jam"] as const;
|
|
228
|
+
|
|
229
|
+
export interface TracePoint {
|
|
230
|
+
kind: TraceKind;
|
|
231
|
+
start: number;
|
|
232
|
+
end: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** One hit of a trace: the instruction that executed or made the access, the registers before it, and where the raster was. */
|
|
236
|
+
export interface TraceHit {
|
|
237
|
+
/** The index of the point in the request. */
|
|
238
|
+
point: number;
|
|
239
|
+
/** The address of the instruction. */
|
|
240
|
+
pc: number;
|
|
241
|
+
bytes: string;
|
|
242
|
+
text: string;
|
|
243
|
+
a: number;
|
|
244
|
+
x: number;
|
|
245
|
+
y: number;
|
|
246
|
+
sp: number;
|
|
247
|
+
/** The frame of the trace, from 0, and the raster position when the instruction started. */
|
|
248
|
+
frame: number;
|
|
249
|
+
rasterLine: number;
|
|
250
|
+
rasterCycle: number;
|
|
251
|
+
/** read and write: the address that the instruction used, when its mode tells it, and the byte there after the access. */
|
|
252
|
+
address?: number;
|
|
253
|
+
value?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface TraceResult {
|
|
257
|
+
hits: TraceHit[];
|
|
258
|
+
frames: number;
|
|
259
|
+
/** frames: the frame limit; hits: the hit limit; breakpoint, watchpoint or jam: the program stopped there. */
|
|
260
|
+
end: (typeof TRACE_ENDS)[number];
|
|
261
|
+
pc: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
220
264
|
export interface BacktraceFrame {
|
|
221
265
|
/** The routine's entry address. */
|
|
222
266
|
address: number;
|
|
@@ -238,7 +282,7 @@ export interface ProfileEntry {
|
|
|
238
282
|
percent: number;
|
|
239
283
|
}
|
|
240
284
|
|
|
241
|
-
/** Addresses with the same access flags: the coverage byte of src/c64.ts
|
|
285
|
+
/** Addresses with the same access flags: the coverage byte of src/c64.ts, RAM, ROM and I/O apart. */
|
|
242
286
|
export interface MemmapRange {
|
|
243
287
|
start: number;
|
|
244
288
|
end: number;
|
|
@@ -337,8 +381,16 @@ export interface MachineStatus {
|
|
|
337
381
|
pc?: number;
|
|
338
382
|
/** True while a cartridge from an autostart is attached (D64). */
|
|
339
383
|
cartridge: boolean;
|
|
384
|
+
/** Present only while stopped by a user breakpoint or watchpoint, or by a JAM. */
|
|
385
|
+
stopReason?: PointStopReason;
|
|
386
|
+
/** The id of the breakpoint or watchpoint that stopped the machine. */
|
|
387
|
+
pointId?: number;
|
|
340
388
|
}
|
|
341
389
|
|
|
390
|
+
/** What stopped the machine outside a request that stops it. */
|
|
391
|
+
export const POINT_STOP_REASONS = ["breakpoint", "watchpoint", "jam"] as const;
|
|
392
|
+
export type PointStopReason = (typeof POINT_STOP_REASONS)[number];
|
|
393
|
+
|
|
342
394
|
export interface MemoryReadParams {
|
|
343
395
|
address: number;
|
|
344
396
|
size: number;
|
|
@@ -396,7 +448,7 @@ export interface ExecutionParams {
|
|
|
396
448
|
/**
|
|
397
449
|
* advance-frames only: the raster position that ends a frame, instead of the
|
|
398
450
|
* position where this call starts. Runs that stop between frames then keep
|
|
399
|
-
* the frames of one timeline
|
|
451
|
+
* the frames of one timeline.
|
|
400
452
|
*/
|
|
401
453
|
anchor?: { line: number; cycle: number };
|
|
402
454
|
}
|
|
@@ -406,6 +458,10 @@ export interface ExecutionResult {
|
|
|
406
458
|
pc?: number;
|
|
407
459
|
executed?: number;
|
|
408
460
|
advancedFrames?: number;
|
|
461
|
+
/** advance-frames only: frames when all frames ran, else what stopped the run early. */
|
|
462
|
+
stopReason?: "frames" | PointStopReason;
|
|
463
|
+
/** The id of the breakpoint or watchpoint that stopped the run. */
|
|
464
|
+
pointId?: number;
|
|
409
465
|
}
|
|
410
466
|
|
|
411
467
|
/** c64_run_until targets. */
|
|
@@ -425,12 +481,14 @@ export interface RunUntilResult {
|
|
|
425
481
|
/** The PC of the CPU in `space`: the CPU that stopped. */
|
|
426
482
|
pc: number;
|
|
427
483
|
space: Space;
|
|
484
|
+
/** The id of the breakpoint or watchpoint that stopped the run. */
|
|
485
|
+
pointId?: number;
|
|
428
486
|
}
|
|
429
487
|
|
|
430
488
|
export interface ViceOperations {
|
|
431
489
|
status: { params: Record<string, never>; result: MachineStatus };
|
|
432
490
|
memoryRead: { params: MemoryReadParams; result: MemoryReadResult };
|
|
433
|
-
/** The I/O registers at $d000-$dfff as the chips hold them, whatever the CPU banks in, without side effects
|
|
491
|
+
/** The I/O registers at $d000-$dfff as the chips hold them, whatever the CPU banks in, without side effects. */
|
|
434
492
|
ioRead: { params: { address: number; size: number }; result: MemoryReadResult };
|
|
435
493
|
registersGet: { params: { space: Space }; result: Registers };
|
|
436
494
|
memoryWrite: { params: MemoryWriteParams; result: { address: number; bytesWritten: number } };
|
|
@@ -451,8 +509,8 @@ export interface ViceOperations {
|
|
|
451
509
|
programLoad: { params: { address?: number }; result: { state: RunState; loadAddress: number; size: number } };
|
|
452
510
|
/** Attachment: the program or image bytes; `type` is the file's extension. `index` is VICE's file number: 0 loads "*", 1 is the first directory entry. */
|
|
453
511
|
/**
|
|
454
|
-
* `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets
|
|
455
|
-
* `joysticksCentred`: the control ports that held a direction or fire and were centred first
|
|
512
|
+
* `emulatorFlaw`: for a tape image, a known flaw of the host's VICE version that a tape load meets.
|
|
513
|
+
* `joysticksCentred`: the control ports that held a direction or fire and were centred first.
|
|
456
514
|
*/
|
|
457
515
|
autostart: { params: { type: string; index: number; run: boolean }; result: { state: RunState; emulatorFlaw?: string; joysticksCentred?: Array<1 | 2> } };
|
|
458
516
|
/** Attachment: the disk image bytes; `type` is the file's extension. */
|
|
@@ -460,16 +518,18 @@ export interface ViceOperations {
|
|
|
460
518
|
/** pc is the reset vector when run is false: the CPU stops there. */
|
|
461
519
|
reset: { params: { mode: ResetMode; run: boolean }; result: { state: RunState; pc?: number } };
|
|
462
520
|
/** `data`: PETSCII bytes as lowercase hex. */
|
|
463
|
-
/** `emulatorFlaw`: a known flaw of the host's VICE version that this input meets
|
|
521
|
+
/** `emulatorFlaw`: a known flaw of the host's VICE version that this input meets. */
|
|
464
522
|
keyboard: { params: { data: string }; result: { queuedBytes: number; emulatorFlaw?: string } };
|
|
465
523
|
joystick: { params: JoystickState; result: JoystickState & { emulatorFlaw?: string } };
|
|
466
524
|
breakpoint: { params: BreakpointParams; result: Breakpoint | PointRemoved | { breakpoints: Breakpoint[] } };
|
|
467
525
|
watchpoint: { params: WatchpointParams; result: Watchpoint | PointRemoved | { watchpoints: Watchpoint[] } };
|
|
468
526
|
cpuHistory: { params: { limit: number; space: Space }; result: { entries: HistoryEntry[] } };
|
|
527
|
+
trace: { params: { points: TracePoint[]; frames: number; maxHits: number }; result: TraceResult };
|
|
469
528
|
backtrace: { params: { depth: number; space: Space }; result: { frames: BacktraceFrame[] } };
|
|
470
529
|
/** `cycles` is a decimal string. */
|
|
471
530
|
timing: { params: { action: (typeof TIMING_ACTIONS)[number] }; result: { started: boolean } | { cycles: string } };
|
|
472
|
-
|
|
531
|
+
/** clear starts the profiler again from zero. */
|
|
532
|
+
profile: { params: { action: "read"; limit: number } | { action: "clear" }; result: { entries: ProfileEntry[] } | { cleared: boolean } };
|
|
473
533
|
memmap: {
|
|
474
534
|
params: { action: "read"; start: number; end: number; maxRanges: number } | { action: "clear" };
|
|
475
535
|
/** `next`: more ranges follow from this address; read again from there. */
|
|
@@ -493,7 +553,7 @@ export interface ViceOperations {
|
|
|
493
553
|
result: { saved: boolean; name: string } | { restored: boolean; state: RunState } | { snapshots: string[] } | { discarded: boolean };
|
|
494
554
|
};
|
|
495
555
|
/**
|
|
496
|
-
* Snapshot files
|
|
556
|
+
* Snapshot files: export sends the bytes of a named snapshot back as
|
|
497
557
|
* one attachment; restore takes the bytes of a .vsf as one attachment.
|
|
498
558
|
*/
|
|
499
559
|
snapshotFile: {
|
|
@@ -532,6 +592,7 @@ export const VICE_OPERATIONS = [
|
|
|
532
592
|
"snapshotFile",
|
|
533
593
|
"ioRead",
|
|
534
594
|
"cpuHistory",
|
|
595
|
+
"trace",
|
|
535
596
|
"backtrace",
|
|
536
597
|
"timing",
|
|
537
598
|
"profile",
|
|
@@ -607,6 +668,20 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
607
668
|
if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
|
|
608
669
|
return { limit: params.limit, space: params.space } as ViceOperations[O]["params"];
|
|
609
670
|
}
|
|
671
|
+
case "trace": {
|
|
672
|
+
onlyFields(params, ["points", "frames", "maxHits"]);
|
|
673
|
+
if (!Array.isArray(params.points) || params.points.length === 0 || params.points.length > MAX_TRACE_POINTS) invalid(`points must be a list of 1 to ${MAX_TRACE_POINTS} points`);
|
|
674
|
+
const points = (params.points as unknown[]).map((point) => {
|
|
675
|
+
if (!isObject(point)) return invalid("a point is an object with kind, start and end");
|
|
676
|
+
onlyFields(point, ["kind", "start", "end"]);
|
|
677
|
+
if (!isOneOf(TRACE_KINDS, point.kind)) invalid("a point kind must be exec, read or write");
|
|
678
|
+
if (!isInteger(point.start, 0, 0xffff) || !isInteger(point.end, point.start as number, 0xffff)) invalid("a point needs start and end from $0000 to $ffff, the end not before the start");
|
|
679
|
+
return { kind: point.kind, start: point.start, end: point.end } as TracePoint;
|
|
680
|
+
});
|
|
681
|
+
if (!isInteger(params.frames, 1, MAX_TRACE_FRAMES)) invalid(`frames must be an integer from 1 to ${MAX_TRACE_FRAMES}`);
|
|
682
|
+
if (!isInteger(params.maxHits, 1, MAX_TRACE_HITS)) invalid(`maxHits must be an integer from 1 to ${MAX_TRACE_HITS}`);
|
|
683
|
+
return { points, frames: params.frames, maxHits: params.maxHits } as ViceOperations[O]["params"];
|
|
684
|
+
}
|
|
610
685
|
case "backtrace": {
|
|
611
686
|
onlyFields(params, ["depth", "space"]);
|
|
612
687
|
if (!isInteger(params.depth, 1, MAX_BACKTRACE)) invalid(`depth must be an integer from 1 to ${MAX_BACKTRACE}`);
|
|
@@ -646,9 +721,11 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
646
721
|
return result as ViceOperations[O]["params"];
|
|
647
722
|
}
|
|
648
723
|
case "profile": {
|
|
649
|
-
onlyFields(params, ["limit"]);
|
|
724
|
+
onlyFields(params, params.action === "clear" ? ["action"] : ["action", "limit"]);
|
|
725
|
+
if (params.action === "clear") return { action: "clear" } as ViceOperations[O]["params"];
|
|
726
|
+
if (params.action !== "read") invalid("action must be read or clear");
|
|
650
727
|
if (!isInteger(params.limit, 1, MAX_PROFILE)) invalid(`limit must be an integer from 1 to ${MAX_PROFILE}`);
|
|
651
|
-
return { limit: params.limit } as ViceOperations[O]["params"];
|
|
728
|
+
return { action: "read", limit: params.limit } as ViceOperations[O]["params"];
|
|
652
729
|
}
|
|
653
730
|
case "memmap": {
|
|
654
731
|
onlyFields(params, params.action === "clear" ? ["action"] : ["action", "start", "end", "maxRanges"]);
|
|
@@ -1011,7 +1088,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1011
1088
|
typeof value.warp === "boolean" &&
|
|
1012
1089
|
typeof value.window === "boolean" &&
|
|
1013
1090
|
(value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
|
|
1014
|
-
typeof value.cartridge === "boolean"
|
|
1091
|
+
typeof value.cartridge === "boolean" &&
|
|
1092
|
+
(value.stopReason === undefined || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
|
|
1093
|
+
(value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
|
|
1015
1094
|
if (!ok) throw new ProtocolError("status result is malformed");
|
|
1016
1095
|
break;
|
|
1017
1096
|
}
|
|
@@ -1080,7 +1159,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1080
1159
|
isOneOf(STOP_REASONS, value.stopReason) &&
|
|
1081
1160
|
isOneOf(RUN_STATES, value.state) &&
|
|
1082
1161
|
isInteger(value.pc, 0, 0xffff) &&
|
|
1083
|
-
isOneOf(SPACES, value.space)
|
|
1162
|
+
isOneOf(SPACES, value.space) &&
|
|
1163
|
+
(value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
|
|
1084
1164
|
if (!ok) throw new ProtocolError("runUntil result is malformed");
|
|
1085
1165
|
break;
|
|
1086
1166
|
}
|
|
@@ -1089,7 +1169,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1089
1169
|
isOneOf(RUN_STATES, value.state) &&
|
|
1090
1170
|
(value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
|
|
1091
1171
|
(value.executed === undefined || isInteger(value.executed, 0, MAX_EXECUTION_COUNT)) &&
|
|
1092
|
-
(value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT))
|
|
1172
|
+
(value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT)) &&
|
|
1173
|
+
(value.stopReason === undefined || value.stopReason === "frames" || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
|
|
1174
|
+
(value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
|
|
1093
1175
|
if (!ok) throw new ProtocolError("execution result is malformed");
|
|
1094
1176
|
break;
|
|
1095
1177
|
}
|
|
@@ -1182,6 +1264,29 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1182
1264
|
if (!ok) throw new ProtocolError("cpuHistory result is malformed");
|
|
1183
1265
|
break;
|
|
1184
1266
|
}
|
|
1267
|
+
case "trace": {
|
|
1268
|
+
const ok =
|
|
1269
|
+
Array.isArray(value.hits) &&
|
|
1270
|
+
isInteger(value.frames, 0, MAX_TRACE_FRAMES) &&
|
|
1271
|
+
isOneOf(TRACE_ENDS, value.end) &&
|
|
1272
|
+
isInteger(value.pc, 0, 0xffff) &&
|
|
1273
|
+
value.hits.every(
|
|
1274
|
+
(hit) =>
|
|
1275
|
+
isObject(hit) &&
|
|
1276
|
+
isInteger(hit.point, 0, MAX_TRACE_POINTS - 1) &&
|
|
1277
|
+
isInteger(hit.pc, 0, 0xffff) &&
|
|
1278
|
+
isHexData(hit.bytes) &&
|
|
1279
|
+
typeof hit.text === "string" &&
|
|
1280
|
+
(["a", "x", "y", "sp"] as const).every((name) => isInteger(hit[name], 0, 0xff)) &&
|
|
1281
|
+
isInteger(hit.frame, 0, MAX_TRACE_FRAMES) &&
|
|
1282
|
+
isInteger(hit.rasterLine, 0, 511) &&
|
|
1283
|
+
isInteger(hit.rasterCycle, 0, 127) &&
|
|
1284
|
+
(hit.address === undefined || isInteger(hit.address, 0, 0xffff)) &&
|
|
1285
|
+
(hit.value === undefined || isInteger(hit.value, 0, 0xff)),
|
|
1286
|
+
);
|
|
1287
|
+
if (!ok) throw new ProtocolError("trace result is malformed");
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1185
1290
|
case "backtrace": {
|
|
1186
1291
|
const ok =
|
|
1187
1292
|
Array.isArray(value.frames) &&
|
|
@@ -1198,6 +1303,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1198
1303
|
break;
|
|
1199
1304
|
}
|
|
1200
1305
|
case "profile": {
|
|
1306
|
+
if ("cleared" in value) {
|
|
1307
|
+
if (value.cleared !== true) throw new ProtocolError("profile result is malformed");
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1201
1310
|
const ok =
|
|
1202
1311
|
Array.isArray(value.entries) &&
|
|
1203
1312
|
value.entries.every(
|
package/src/script.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The one error envelope for the skill scripts and the CLI: a script prints one JSON line,
|
|
2
2
|
// a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
|
|
3
|
-
// usage text as an invalid-input error with exit status 2.
|
|
3
|
+
// usage text as an invalid-input error with exit status 2. Every script takes --project <folder>.
|
|
4
4
|
|
|
5
|
+
import { ProjectError, useProject } from "./project.ts";
|
|
5
6
|
import { shownMessage, WireFailure } from "./protocol/messages.ts";
|
|
6
7
|
import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
|
|
7
8
|
|
|
@@ -17,27 +18,65 @@ function isParseArgsError(error: unknown): error is Error {
|
|
|
17
18
|
return error instanceof Error && typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** The line that every usage text ends with. */
|
|
22
|
+
export const PROJECT_USAGE = "Every script takes --project <folder>: the project folder, relative to the working directory. Without it, the script uses the nearest folder at or above the working directory that holds .c64-re-tools/.";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Takes --project <folder> and --project=<folder> out of a command line, up to "--".
|
|
26
|
+
* Returns the folder and the other arguments. Throws UsageError for a --project without a folder or a second --project.
|
|
27
|
+
*/
|
|
28
|
+
export function takeProject(argv: readonly string[]): { project?: string; argv: string[] } {
|
|
29
|
+
const rest: string[] = [];
|
|
30
|
+
let project: string | undefined;
|
|
31
|
+
for (let index = 0; index < argv.length; index++) {
|
|
32
|
+
const argument = argv[index]!;
|
|
33
|
+
if (argument === "--") {
|
|
34
|
+
rest.push(...argv.slice(index));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
let value: string | undefined;
|
|
38
|
+
if (argument === "--project") {
|
|
39
|
+
value = argv[index + 1];
|
|
40
|
+
if (value === undefined || value.startsWith("-")) throw new UsageError("--project needs a folder");
|
|
41
|
+
index++;
|
|
42
|
+
} else if (argument.startsWith("--project=")) {
|
|
43
|
+
value = argument.slice("--project=".length);
|
|
44
|
+
if (value === "") throw new UsageError("--project needs a folder");
|
|
45
|
+
} else {
|
|
46
|
+
rest.push(argument);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (project !== undefined) throw new UsageError("give --project only once");
|
|
50
|
+
project = value;
|
|
51
|
+
}
|
|
52
|
+
return project === undefined ? { argv: rest } : { project, argv: rest };
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
function printLine(value: unknown): void {
|
|
21
56
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
/**
|
|
25
|
-
* Runs a script body
|
|
26
|
-
*
|
|
60
|
+
* Runs a script body with the command line of the process, without --project: that sets the project first.
|
|
61
|
+
* Prints its result as one JSON line when it returns a value (undefined prints nothing).
|
|
62
|
+
* A refusal (WireFailure and ProjectError always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
|
|
27
63
|
* A UsageError or a node:util parseArgs error (code ERR_PARSE_ARGS_*) prints {"error":{"code":"invalid-input",
|
|
28
64
|
* "message": message === "" ? usage : message + "\n\n" + usage}} with exit status 2. Anything else is rethrown.
|
|
29
65
|
* The body may set process.exitCode itself for an extra exit status; a value it returns is printed as usual.
|
|
30
66
|
*/
|
|
31
|
-
export async function runScript(body: () => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
|
|
67
|
+
export async function runScript(body: (argv: string[]) => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
|
|
68
|
+
const usage = `${options.usage}\n\n${PROJECT_USAGE}`;
|
|
32
69
|
// The trace of the script, when C64RT_TRACE is set; its start event carries the script and its arguments.
|
|
33
70
|
startTrace("script", { warn: (line) => process.stderr.write(`${line}\n`) });
|
|
34
71
|
const started = performance.now();
|
|
35
72
|
const end = (fields: TraceFields) => trace().event("script.end", { ms: elapsedMs(started), status: process.exitCode ?? 0, ...fields });
|
|
36
73
|
let result: unknown;
|
|
37
74
|
try {
|
|
38
|
-
|
|
75
|
+
const { project, argv } = takeProject(process.argv.slice(2));
|
|
76
|
+
if (project !== undefined) useProject(project);
|
|
77
|
+
result = await body(argv);
|
|
39
78
|
} catch (error) {
|
|
40
|
-
if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
79
|
+
if ([WireFailure, ProjectError, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
41
80
|
const { code } = error as Error & { code: string };
|
|
42
81
|
const message = shownMessage(error as Error & { code: string });
|
|
43
82
|
printLine({ error: { code, message } });
|
|
@@ -47,7 +86,7 @@ export async function runScript(body: () => unknown, options: { usage: string; r
|
|
|
47
86
|
}
|
|
48
87
|
if (error instanceof UsageError || isParseArgsError(error)) {
|
|
49
88
|
const { message } = error;
|
|
50
|
-
printLine({ error: { code: "invalid-input", message: message === "" ?
|
|
89
|
+
printLine({ error: { code: "invalid-input", message: message === "" ? usage : `${message}\n\n${usage}` } });
|
|
51
90
|
process.exitCode = 2;
|
|
52
91
|
end({ error: { code: "invalid-input", message } });
|
|
53
92
|
return;
|
package/src/trace.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
|
|
2
|
-
// in the environment, each process appends its events as
|
|
3
|
-
// of its own in that directory
|
|
4
|
-
//
|
|
2
|
+
// in the environment (the host: --log-dir), each process appends its events as
|
|
3
|
+
// JSON lines to a file of its own in that directory. The host can also echo
|
|
4
|
+
// each event to the console (--log-console). Off, every call does nothing. The
|
|
5
|
+
// trace is for people: nothing of it reaches the agent, and no result depends on it.
|
|
5
6
|
|
|
6
7
|
import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -66,14 +67,16 @@ export class Trace {
|
|
|
66
67
|
readonly kind: TraceKind;
|
|
67
68
|
/** The directory of the trace, or undefined when the trace is off. */
|
|
68
69
|
readonly dir: string | undefined;
|
|
69
|
-
/** The file this trace appends to, or undefined when the trace
|
|
70
|
+
/** The file this trace appends to, or undefined when the trace writes no file. */
|
|
70
71
|
readonly file: string | undefined;
|
|
72
|
+
readonly #echo: ((line: string) => void) | undefined;
|
|
71
73
|
#broken = false;
|
|
72
74
|
|
|
73
|
-
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
|
|
75
|
+
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined, echo?: (line: string) => void) {
|
|
74
76
|
this.kind = kind;
|
|
75
77
|
this.dir = dir;
|
|
76
78
|
this.file = file;
|
|
79
|
+
this.#echo = echo;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/** A trace that records nothing. */
|
|
@@ -81,31 +84,46 @@ export class Trace {
|
|
|
81
84
|
return new Trace(kind, undefined, undefined);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** A trace that writes no file and gives each event to `echo` as one line. */
|
|
88
|
+
static echoing(kind: TraceKind, echo: (line: string) => void): Trace {
|
|
89
|
+
return new Trace(kind, undefined, undefined, echo);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
/**
|
|
85
93
|
* A trace into `dir`, created when it is missing. The file is
|
|
86
94
|
* `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
|
|
87
95
|
* Throws when the directory cannot be used.
|
|
88
96
|
*/
|
|
89
|
-
static open(kind: TraceKind, dir: string, now = new Date()): Trace {
|
|
97
|
+
static open(kind: TraceKind, dir: string, now = new Date(), echo?: (line: string) => void): Trace {
|
|
90
98
|
mkdirSync(dir, { recursive: true });
|
|
91
|
-
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
|
|
99
|
+
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`), echo);
|
|
92
100
|
trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
|
|
93
101
|
return trace;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
get on(): boolean {
|
|
105
|
+
return this.#writes || this.#echo !== undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get #writes(): boolean {
|
|
97
109
|
return this.file !== undefined && !this.#broken;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Records one event: appends it to the file, and gives it to the echo as
|
|
114
|
+
* "<event> <fields as JSON>". The echo skips `log` events, which the program
|
|
115
|
+
* prints itself. A write that fails stops the file; nothing throws.
|
|
116
|
+
*/
|
|
101
117
|
event(name: string, fields: TraceFields = {}): void {
|
|
102
118
|
if (!this.on) return;
|
|
103
|
-
const
|
|
119
|
+
const summary: Record<string, unknown> = {};
|
|
104
120
|
for (const [key, value] of Object.entries(fields)) {
|
|
105
|
-
if (!RESERVED.has(key) && value !== undefined)
|
|
121
|
+
if (!RESERVED.has(key) && value !== undefined) summary[key] = summarize(value);
|
|
106
122
|
}
|
|
123
|
+
if (this.#echo !== undefined && name !== "log") this.#echo(`${name} ${JSON.stringify(summary)}`);
|
|
124
|
+
if (!this.#writes) return;
|
|
107
125
|
try {
|
|
108
|
-
appendFileSync(this.file!, `${JSON.stringify(
|
|
126
|
+
appendFileSync(this.file!, `${JSON.stringify({ t: isoCet(), kind: this.kind, event: name, ...summary })}\n`);
|
|
109
127
|
} catch {
|
|
110
128
|
this.#broken = true;
|
|
111
129
|
}
|
|
@@ -121,7 +139,7 @@ export class Trace {
|
|
|
121
139
|
* trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
|
|
122
140
|
*/
|
|
123
141
|
keep(source: string, name: string): void {
|
|
124
|
-
if (!this
|
|
142
|
+
if (!this.#writes) return;
|
|
125
143
|
const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
|
|
126
144
|
try {
|
|
127
145
|
copyFileSync(source, target);
|
|
@@ -135,23 +153,37 @@ export class Trace {
|
|
|
135
153
|
// Off until a program calls startTrace; the kind is a placeholder until then.
|
|
136
154
|
let current = Trace.off("script");
|
|
137
155
|
|
|
156
|
+
/** Where the trace of a process goes: a directory for its file, and an echo for each event. */
|
|
157
|
+
export interface TraceTarget {
|
|
158
|
+
/** The directory, and how the user gave it (`--log-dir <path>`, for example) for a warning. */
|
|
159
|
+
dir?: { path: string; given: string };
|
|
160
|
+
echo?: (line: string) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The target that C64RT_TRACE in `env` gives: its directory, or nothing. */
|
|
164
|
+
export function traceTargetFromEnv(env: NodeJS.ProcessEnv = process.env): TraceTarget {
|
|
165
|
+
const path = env[TRACE_VARIABLE];
|
|
166
|
+
return path === undefined || path === "" ? {} : { dir: { path, given: `${TRACE_VARIABLE}=${path}` } };
|
|
167
|
+
}
|
|
168
|
+
|
|
138
169
|
/**
|
|
139
|
-
* Switches the trace of this process on
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* start of a program.
|
|
170
|
+
* Switches the trace of this process on for `target` (by default what
|
|
171
|
+
* C64RT_TRACE in `env` gives), and off when the target is empty. A directory
|
|
172
|
+
* that cannot be used writes no file and reports that through `warn`; an echo
|
|
173
|
+
* stays on. Called once, at the start of a program.
|
|
143
174
|
*/
|
|
144
|
-
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
|
|
145
|
-
const dir =
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; target?: TraceTarget; warn?: (line: string) => void } = {}): Trace {
|
|
176
|
+
const { dir, echo } = options.target ?? traceTargetFromEnv(options.env);
|
|
177
|
+
const noFile = () => (echo === undefined ? Trace.off(kind) : Trace.echoing(kind, echo));
|
|
178
|
+
if (dir === undefined) {
|
|
179
|
+
current = noFile();
|
|
148
180
|
return current;
|
|
149
181
|
}
|
|
150
182
|
try {
|
|
151
|
-
current = Trace.open(kind, dir);
|
|
183
|
+
current = Trace.open(kind, dir.path, new Date(), echo);
|
|
152
184
|
} catch (error) {
|
|
153
|
-
current =
|
|
154
|
-
options.warn?.(`${
|
|
185
|
+
current = noFile();
|
|
186
|
+
options.warn?.(`${dir.given} cannot be used (${error instanceof Error ? error.message : String(error)}); no trace file is written.`);
|
|
155
187
|
}
|
|
156
188
|
return current;
|
|
157
189
|
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// The byte check of a build (D47): the assembled program against the image
|
|
2
|
-
// of the unpacker (image.json with image.bin), a reference PRG, or 64 KiB of
|
|
3
|
-
// memory, segment by segment.
|
|
4
|
-
|
|
5
|
-
import { posix } from "node:path";
|
|
6
|
-
|
|
7
|
-
import { formatC64Address, prgRange } from "#src/c64.ts";
|
|
8
|
-
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
9
|
-
import { UsageError } from "#src/script.ts";
|
|
10
|
-
|
|
11
|
-
export interface SegmentCheck {
|
|
12
|
-
start: string;
|
|
13
|
-
end: string;
|
|
14
|
-
result: "identical" | "different" | "not covered";
|
|
15
|
-
/** Bytes of the segment that the build does not hold. */
|
|
16
|
-
uncovered?: number;
|
|
17
|
-
differences?: number;
|
|
18
|
-
firstDifference?: { address: string; built: number; reference: number };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const FULL = 0x10000;
|
|
22
|
-
|
|
23
|
-
/** The reference as memory and the ranges to check in it. */
|
|
24
|
-
function reference(path: string, built: { start: number; end: number }): { memory: (address: number) => number | undefined; segments: Array<{ start: number; end: number }> } {
|
|
25
|
-
const { bytes } = readProjectFile(path);
|
|
26
|
-
if (path.endsWith(".json")) {
|
|
27
|
-
let manifest: { image?: unknown; segments?: unknown };
|
|
28
|
-
try {
|
|
29
|
-
manifest = JSON.parse(bytes.toString("utf8")) as typeof manifest;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
throw new UsageError(`--compare ${path} is not JSON: ${(error as Error).message}`);
|
|
32
|
-
}
|
|
33
|
-
const segments = manifest.segments;
|
|
34
|
-
if (typeof manifest.image !== "string" || !Array.isArray(segments) || segments.length === 0) throw new UsageError(`--compare ${path} is not an image.json: it needs image and segments`);
|
|
35
|
-
const image = readProjectFile(posix.join(posix.dirname(path), manifest.image)).bytes;
|
|
36
|
-
if (image.length !== FULL) throw new UsageError(`the image of ${path} has ${image.length} bytes, not 65536`);
|
|
37
|
-
return {
|
|
38
|
-
memory: (address) => image[address],
|
|
39
|
-
segments: segments.map((segment: { start?: unknown; end?: unknown }) => {
|
|
40
|
-
const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : NaN);
|
|
41
|
-
const range = { start: parse(segment.start), end: parse(segment.end) };
|
|
42
|
-
if (!(range.start <= range.end)) throw new UsageError(`--compare ${path}: a segment needs start and end as $ addresses`);
|
|
43
|
-
return range;
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (bytes.length === FULL && !path.endsWith(".prg")) return { memory: (address) => bytes[address], segments: [built] };
|
|
48
|
-
let range: ReturnType<typeof prgRange>;
|
|
49
|
-
try {
|
|
50
|
-
range = prgRange(bytes);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
throw new UsageError(`--compare ${path}: ${(error as Error).message}`);
|
|
53
|
-
}
|
|
54
|
-
return { memory: (address) => (address >= range.start && address <= range.end ? range.body[address - range.start] : undefined), segments: [{ start: range.start, end: range.end }] };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Compares the assembled PRG with the reference, segment by segment. */
|
|
58
|
-
export function compareBuild(prg: Uint8Array, path: string): { against: string; identical: boolean; segments: SegmentCheck[] } {
|
|
59
|
-
const built = prgRange(prg);
|
|
60
|
-
const { memory, segments } = reference(path, built);
|
|
61
|
-
const checks = segments.map((segment): SegmentCheck => {
|
|
62
|
-
const from = Math.max(segment.start, built.start);
|
|
63
|
-
const to = Math.min(segment.end, built.end);
|
|
64
|
-
const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
|
|
65
|
-
if (from > to) return { ...shown, result: "not covered", uncovered: segment.end - segment.start + 1 };
|
|
66
|
-
let differences = 0;
|
|
67
|
-
let first: SegmentCheck["firstDifference"];
|
|
68
|
-
for (let address = from; address <= to; address++) {
|
|
69
|
-
const builtByte = built.body[address - built.start]!;
|
|
70
|
-
const referenceByte = memory(address);
|
|
71
|
-
if (referenceByte === undefined || builtByte === referenceByte) continue;
|
|
72
|
-
differences++;
|
|
73
|
-
first ??= { address: formatC64Address(address), built: builtByte, reference: referenceByte };
|
|
74
|
-
}
|
|
75
|
-
const uncovered = segment.end - segment.start + 1 - (to - from + 1);
|
|
76
|
-
return {
|
|
77
|
-
...shown,
|
|
78
|
-
result: differences > 0 ? "different" : uncovered > 0 ? "not covered" : "identical",
|
|
79
|
-
...(uncovered > 0 ? { uncovered } : {}),
|
|
80
|
-
...(differences > 0 ? { differences, firstDifference: first! } : {}),
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
return { against: path, identical: checks.every((check) => check.result === "identical"), segments: checks };
|
|
84
|
-
}
|