@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.
Files changed (118) hide show
  1. package/README.md +98 -115
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +43 -8
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/src/trace.ts +55 -23
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -3,14 +3,17 @@
3
3
  // session starts headless and can move the machine into a VICE with a window
4
4
  // and back; breakpoints, watchpoints and the rest go along.
5
5
 
6
- import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
- import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
6
+ import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
+ import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
8
+ import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type TraceHit, type TraceKind, type TraceResult, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
8
9
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
10
  import { tmpdir } from "node:os";
10
11
  import { join } from "node:path";
11
12
  import { setTimeout as sleep } from "node:timers/promises";
12
13
 
13
- import { formatC64Address } from "../../c64.ts";
14
+ import { formatC64Address, instructionLength } from "../../c64.ts";
15
+ import { encodePng, type IndexedFrame } from "../../graphics.ts";
16
+ import { instructionText, operandAddress } from "../../opcodes.ts";
14
17
  import type { ProcessSupervisor } from "../../native/processes.ts";
15
18
  import { trace } from "../../trace.ts";
16
19
  import type { ViceSessionFactory, ViceSessionHandle } from "../server.ts";
@@ -26,7 +29,7 @@ import {
26
29
  } from "./adapter.ts";
27
30
  import { decodeProgramCounter, MonitorConnectionError, MonitorError, MonitorTimeoutError, ResponseType, type MonitorResponse } from "./binary-monitor.ts";
28
31
  import { decodeCia, decodeSid, decodeSprite, decodeVicii } from "./chips.ts";
29
- import { compareFrames, differenceImage, encodePng, type IndexedFrame } from "./screen.ts";
32
+ import { compareFrames, differenceImage } from "./screen.ts";
30
33
  import { launchVice, startErrorLines, type ViceMode, type ViceProcess } from "./process.ts";
31
34
  import { TextMonitorError, TextMonitorTimeoutError } from "./text-monitor.ts";
32
35
 
@@ -121,10 +124,14 @@ export interface SessionOptions {
121
124
  launch?: ViceLauncher;
122
125
  /** Makes the host remove a path if it dies; returns the release. */
123
126
  ownPath?: (path: string) => () => void;
124
- /** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
127
+ /** The known flaws of this VICE version; keyboard and joystick results name them. */
125
128
  flaws?: readonly string[];
129
+ /** Lists the entries of a disk image, so an autostart index is checked first. Without it, VICE gets the index unchecked. */
130
+ directory?: DiskDirectory;
126
131
  }
127
132
 
133
+ export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
134
+
128
135
  /**
129
136
  * Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
130
137
  * in the datasette and a cartridge in the port, until the same place gets
@@ -151,6 +158,7 @@ export class ViceSession implements ViceSessionHandle {
151
158
  readonly #untilReturnLimitMs: number;
152
159
  readonly #stepLimitMs: number;
153
160
  readonly #launch: ViceLauncher | undefined;
161
+ readonly #directory: DiskDirectory | undefined;
154
162
  /** Session files that outlive one VICE: snapshots, staged media, handovers. */
155
163
  readonly #workDir: string;
156
164
  readonly #releaseWorkDir: () => void;
@@ -177,6 +185,8 @@ export class ViceSession implements ViceSessionHandle {
177
185
  #driveStop = false;
178
186
  /** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
179
187
  #haltCount = 0;
188
+ /** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
189
+ #lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
180
190
  #queue: Promise<unknown> = Promise.resolve();
181
191
  #closed = false;
182
192
  #lost = false;
@@ -218,6 +228,7 @@ export class ViceSession implements ViceSessionHandle {
218
228
  this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
219
229
  this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
220
230
  this.#launch = options.launch;
231
+ this.#directory = options.directory;
221
232
  this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
222
233
  this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
223
234
  this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
@@ -374,14 +385,16 @@ export class ViceSession implements ViceSessionHandle {
374
385
  return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
375
386
  case "autostart": {
376
387
  const { type, index, run } = params as ViceOperations["autostart"]["params"];
377
- const file = this.#stage(onlyAttachment(attachments), type);
388
+ const bytes = onlyAttachment(attachments);
389
+ if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
390
+ const file = this.#stage(bytes, type);
378
391
  // VICE keeps a cartridge through every reset and autostart, as a real C64 does.
379
392
  if (type !== "crt" && this.#cartridge) {
380
393
  await this.#observe(() => this.#machine.detachCartridge());
381
394
  this.#cartridge = false;
382
395
  this.#replaceStaged("cartridge", undefined);
383
396
  }
384
- // Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types (R3-05).
397
+ // Port 1 shares the keyboard lines: a held direction types keys into the RUN that the autostart types.
385
398
  const centred: Array<1 | 2> = [];
386
399
  for (const [port, joystick] of this.#joysticks) {
387
400
  if (joystick.direction === "center" && !joystick.fire) continue;
@@ -445,6 +458,8 @@ export class ViceSession implements ViceSessionHandle {
445
458
  return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
446
459
  case "cpuHistory":
447
460
  return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
461
+ case "trace":
462
+ return this.#trace(params as ViceOperations["trace"]["params"]);
448
463
  case "backtrace": {
449
464
  const { depth, space } = params as ViceOperations["backtrace"]["params"];
450
465
  return this.#observe(async () => {
@@ -462,8 +477,12 @@ export class ViceSession implements ViceSessionHandle {
462
477
  return { cycles: (now - this.#timingStart).toString() };
463
478
  }
464
479
  case "profile": {
465
- const { limit } = params as ViceOperations["profile"]["params"];
466
- return { entries: await this.#observe(() => this.#machine.profile(limit)) };
480
+ const request = params as ViceOperations["profile"]["params"];
481
+ if (request.action === "clear") {
482
+ await this.#observe(() => this.#machine.startProfiler());
483
+ return { cleared: true };
484
+ }
485
+ return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
467
486
  }
468
487
  case "memmap": {
469
488
  const request = params as ViceOperations["memmap"]["params"];
@@ -508,7 +527,12 @@ export class ViceSession implements ViceSessionHandle {
508
527
  const warp = await this.#observe(() => this.#machine.warp());
509
528
  // Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
510
529
  const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
511
- if (status.state === "stopped") status.pc = await this.#pc();
530
+ if (status.state === "stopped") {
531
+ status.pc = await this.#pc();
532
+ // VICE 3.9 sends no JAM event: the opcode at the pc tells.
533
+ const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
534
+ if (stop !== undefined) Object.assign(status, stop);
535
+ }
512
536
  return status;
513
537
  }
514
538
 
@@ -539,7 +563,13 @@ export class ViceSession implements ViceSessionHandle {
539
563
  throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
540
564
  }
541
565
  const outcome = await this.#runFrames(params.count, undefined, params.anchor);
542
- return { state: "stopped", pc: outcome.pc, advancedFrames: outcome.frames };
566
+ return {
567
+ state: "stopped",
568
+ pc: outcome.pc,
569
+ advancedFrames: outcome.frames,
570
+ stopReason: outcome.end === "target" ? "frames" : outcome.end,
571
+ ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
572
+ };
543
573
  }
544
574
  await this.#stop();
545
575
  if (params.action === "until-return") {
@@ -600,7 +630,7 @@ export class ViceSession implements ViceSessionHandle {
600
630
  * checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
601
631
  * The machine is stopped afterwards.
602
632
  */
603
- async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
633
+ async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
604
634
  await this.#stop();
605
635
  const lines = RASTER[this.#videoStandard].lines;
606
636
  // A frame ends where the raster passes the anchor; without one, where this run starts.
@@ -642,13 +672,127 @@ export class ViceSession implements ViceSessionHandle {
642
672
  }
643
673
  // A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
644
674
  const driveStop = this.#driveStop;
645
- return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64" };
675
+ const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
676
+ return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
646
677
  }
647
678
  } finally {
648
679
  await this.#machine.deleteCheckpoint(clock);
649
680
  }
650
681
  }
651
682
 
683
+ /**
684
+ * A trace: runs the machine for up to `frames` frames, counted as
685
+ * #runFrames counts them, with a stopping checkpoint for each point. At each
686
+ * hit the session records the instruction, its registers and the raster
687
+ * position, and runs on. It ends at the frame limit, at `maxHits` hits, or
688
+ * where a user point or a JAM stops the program. VICE also reports the hits
689
+ * of a checkpoint that does not stop, but without the PC, the registers or
690
+ * the raster position, so each point stops.
691
+ */
692
+ async #trace(params: ViceOperations["trace"]["params"]): Promise<TraceResult> {
693
+ await this.#stop();
694
+ const lines = RASTER[this.#videoStandard].lines;
695
+ const start = await this.#machine.rasterPosition();
696
+ const arming = rasterLinesExpression((start.line + Math.floor(lines / 2)) % lines, lines);
697
+ const ending = rasterWindowExpression(start.line, start.cycle, lines);
698
+ const operation: Record<TraceKind, number> = { exec: 0x04, read: 0x01, write: 0x02 };
699
+ const points: number[] = [];
700
+ const hits: TraceHit[] = [];
701
+ let frames = 0;
702
+ const clock = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
703
+ try {
704
+ for (const point of params.points) points.push(await this.#machine.addCheckpoint({ start: point.start, end: point.end, operation: operation[point.kind], space: "c64" }));
705
+ await this.#machine.setCondition(clock, arming);
706
+ let armingPhase = true;
707
+ for (;;) {
708
+ const next = this.#nextStop(HALF_FRAME_LIMIT_MS);
709
+ await this.#resume();
710
+ const stop = await next;
711
+ if (stop === undefined) {
712
+ await this.#stop();
713
+ throw new WireFailure("operation-failed", "The emulator did not reach the next frame in time.");
714
+ }
715
+ const traced = points.flatMap((checkpoint, index) => (stop.hits.includes(checkpoint) ? [index] : []));
716
+ for (const index of traced) {
717
+ hits.push(await this.#traceHit(index, params.points[index]!.kind, frames));
718
+ if (hits.length >= params.maxHits) return { hits, frames, end: "hits", pc: stop.pc };
719
+ }
720
+ if (stop.hits.includes(clock)) {
721
+ if (armingPhase) await this.#machine.setCondition(clock, ending);
722
+ else {
723
+ frames++;
724
+ if (frames >= params.frames) return { hits, frames, end: "frames", pc: stop.pc };
725
+ await this.#machine.setCondition(clock, arming);
726
+ }
727
+ armingPhase = !armingPhase;
728
+ continue;
729
+ }
730
+ if (traced.length > 0) continue;
731
+ const point = this.#userPoint(stop.hits);
732
+ if (point !== undefined) return { hits, frames, end: point.stopReason, pc: stop.pc };
733
+ if (await this.#jammedAt(stop.pc)) return { hits, frames, end: "jam", pc: stop.pc };
734
+ throw new WireFailure("operation-failed", "The emulator stopped the machine for a reason outside this trace.");
735
+ }
736
+ } finally {
737
+ await this.#machine.deleteCheckpoint(clock);
738
+ for (const checkpoint of points) await this.#machine.deleteCheckpoint(checkpoint);
739
+ }
740
+ }
741
+
742
+ /**
743
+ * One hit of a trace point. An exec point stops before its instruction: the
744
+ * registers and the raster position are the ones of now. A read or write
745
+ * point stops after the instruction that made the access: the last entry of
746
+ * the CPU history is that instruction, with its registers and its clock.
747
+ */
748
+ async #traceHit(point: number, kind: TraceKind, frame: number): Promise<TraceHit> {
749
+ const position = await this.#machine.rasterPosition();
750
+ const byteAt = async (address: number) => Number.parseInt(await this.#machine.readMemory({ address, size: 1, space: "c64", view: "cpu" }), 16);
751
+ if (kind === "exec") {
752
+ const registers = await this.#machine.readRegisters("c64");
753
+ const bytes = [...Buffer.from(await this.#machine.readMemory({ address: registers.pc, size: 3, space: "c64", view: "cpu" }), "hex")];
754
+ const length = instructionLength(bytes[0]!);
755
+ return {
756
+ point,
757
+ pc: registers.pc,
758
+ bytes: Buffer.from(bytes.slice(0, length)).toString("hex"),
759
+ text: instructionText(bytes, registers.pc),
760
+ a: registers.a,
761
+ x: registers.x,
762
+ y: registers.y,
763
+ sp: registers.sp,
764
+ frame,
765
+ rasterLine: position.line,
766
+ rasterCycle: position.cycle,
767
+ };
768
+ }
769
+ const [entry] = await this.#machine.history(1, "c64");
770
+ if (entry === undefined) throw new WireFailure("operation-failed", "The emulator gave no CPU history at a trace hit.");
771
+ const now = await this.#machine.clock();
772
+ const { lines, cycles } = RASTER[this.#videoStandard];
773
+ const span = BigInt(lines * cycles);
774
+ const at = Number((((BigInt(position.line * cycles + position.cycle) - (now - entry.clock)) % span) + span) % span);
775
+ const bytes = [...Buffer.from(entry.bytes, "hex")];
776
+ // A zero-page pointer is read now: the instruction did not change it.
777
+ const pointer = new Map<number, number>();
778
+ for (const address of [bytes[1] ?? 0, ((bytes[1] ?? 0) + 1) & 0xff, ((bytes[1] ?? 0) + entry.x) & 0xff, ((bytes[1] ?? 0) + entry.x + 1) & 0xff]) pointer.set(address, await byteAt(address));
779
+ const address = operandAddress(bytes, entry.x, entry.y, (zp) => pointer.get(zp) ?? 0);
780
+ return {
781
+ point,
782
+ pc: entry.address,
783
+ bytes: entry.bytes,
784
+ text: instructionText(bytes, entry.address),
785
+ a: entry.a,
786
+ x: entry.x,
787
+ y: entry.y,
788
+ sp: entry.sp,
789
+ frame,
790
+ rasterLine: Math.floor(at / cycles),
791
+ rasterCycle: at % cycles,
792
+ ...(address === undefined ? {} : { address, value: await byteAt(address) }),
793
+ };
794
+ }
795
+
652
796
  /**
653
797
  * Completes a stop that a drive 8 checkpoint caused: the computer runs to
654
798
  * its next instruction boundary, where a computer checkpoint stops it, so
@@ -659,6 +803,8 @@ export class ViceSession implements ViceSessionHandle {
659
803
  async #completeDriveStop(): Promise<number | undefined> {
660
804
  if (!this.#driveStop) return undefined;
661
805
  this.#driveStop = false;
806
+ // The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
807
+ const reason = this.#lastStop;
662
808
  const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
663
809
  try {
664
810
  for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
@@ -669,6 +815,7 @@ export class ViceSession implements ViceSessionHandle {
669
815
  // The drive can hit its checkpoint again before the computer gets to the next instruction.
670
816
  if (stop.hits.includes(boundary)) {
671
817
  this.#driveStop = false;
818
+ this.#lastStop = reason;
672
819
  trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
673
820
  return stop.pc;
674
821
  }
@@ -683,14 +830,20 @@ export class ViceSession implements ViceSessionHandle {
683
830
  /** Names what caused a stop during a frame-clocked run. */
684
831
  #stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
685
832
  if (target !== undefined && stop.hits.includes(target)) return "target";
686
- const isPoint = (registry: Map<number, { checkpoint: number }>) =>
687
- [...registry.values()].some((point) => stop.hits.includes(point.checkpoint));
688
- if (isPoint(this.#breakpoints)) return "breakpoint";
689
- if (isPoint(this.#watchpoints)) return "watchpoint";
833
+ const point = this.#userPoint(stop.hits);
834
+ if (point !== undefined) return point.stopReason;
690
835
  if (stop.hits.includes(clock)) return "clock";
691
836
  return "unknown";
692
837
  }
693
838
 
839
+ /** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
840
+ #userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
841
+ for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
842
+ for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
843
+ }
844
+ return undefined;
845
+ }
846
+
694
847
  async #jammedAt(pc: number): Promise<boolean> {
695
848
  const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
696
849
  return JAM_OPCODES.has(Number.parseInt(opcode, 16));
@@ -734,9 +887,9 @@ export class ViceSession implements ViceSessionHandle {
734
887
  if (expression !== undefined) await this.#machine.setCondition(checkpoint, expression);
735
888
  const outcome = await this.#runFrames(params.timeoutFrames, { checkpoint, deferred });
736
889
  const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
737
- // A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
890
+ // A stop event carries the computer's PC; a stop in the drive reports the drive's.
738
891
  const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
739
- return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
892
+ return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
740
893
  } finally {
741
894
  await this.#machine.deleteCheckpoint(checkpoint);
742
895
  }
@@ -801,7 +954,7 @@ export class ViceSession implements ViceSessionHandle {
801
954
  if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
802
955
  if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
803
956
  if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
804
- // A breakpoint over a range stops at the first instruction executed in it: exec, not a watch (D43).
957
+ // A breakpoint over a range stops at the first instruction executed in it: exec, not a watch.
805
958
  const added = await this.#addPoint({
806
959
  ...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
807
960
  space: params.space,
@@ -1002,7 +1155,7 @@ export class ViceSession implements ViceSessionHandle {
1002
1155
  return { restored: true, state: "stopped" };
1003
1156
  }
1004
1157
 
1005
- /** A snapshot as bytes (D41): export a named one into `reply`, or restore from the attached .vsf. */
1158
+ /** A snapshot as bytes: export a named one into `reply`, or restore from the attached .vsf. */
1006
1159
  async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
1007
1160
  if (params.action === "export") {
1008
1161
  const file = this.#snapshots.get(params.name);
@@ -1012,7 +1165,7 @@ export class ViceSession implements ViceSessionHandle {
1012
1165
  return { exported: true, bytes: bytes.length };
1013
1166
  }
1014
1167
  const bytes = onlyAttachment(attachments);
1015
- // Checked before the machine stops: VICE gives no reason for a file that is no snapshot (F-07).
1168
+ // Checked before the machine stops: VICE gives no reason for a file that is no snapshot.
1016
1169
  if (!bytes.subarray(0, VSF_MAGIC.length).equals(VSF_MAGIC)) throw new WireFailure("invalid-input", "The file is not a VICE snapshot file (.vsf). Give a file that c64_snapshot save wrote.");
1017
1170
  const file = this.#stage(bytes, "vsf");
1018
1171
  try {
@@ -1185,6 +1338,7 @@ export class ViceSession implements ViceSessionHandle {
1185
1338
  */
1186
1339
  async #resumingCommand(send: () => Promise<void>): Promise<void> {
1187
1340
  this.#state = "running";
1341
+ this.#lastStop = undefined;
1188
1342
  try {
1189
1343
  await send();
1190
1344
  } catch (error) {
@@ -1360,10 +1514,12 @@ export class ViceSession implements ViceSessionHandle {
1360
1514
  const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
1361
1515
  if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
1362
1516
  if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
1517
+ this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
1363
1518
  this.#checkpointHits = [];
1364
1519
  this.#state = "stopped";
1365
1520
  this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
1366
1521
  } else if (event.type === ResponseType.resumed) {
1522
+ this.#lastStop = undefined;
1367
1523
  this.#state = "running";
1368
1524
  }
1369
1525
  }
@@ -1400,6 +1556,17 @@ async function setPoint(
1400
1556
  return checkpoint;
1401
1557
  }
1402
1558
 
1559
+ /**
1560
+ * Refuses an autostart index that selects no program: VICE numbers the
1561
+ * entries from 1, and the caller from 0, as the c64-disk directory list does.
1562
+ * A DEL entry holds no program, and an index past the last entry selects nothing.
1563
+ */
1564
+ function checkDirectoryEntry(entries: DiskFile[], index: number): void {
1565
+ const entry = entries[index - 1];
1566
+ if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
1567
+ if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
1568
+ }
1569
+
1403
1570
  /** The production factory: launch a headless VICE per session through the host's supervisor. */
1404
1571
  export function viceSessionFactory(options: {
1405
1572
  supervisor: ProcessSupervisor;
@@ -1407,6 +1574,7 @@ export function viceSessionFactory(options: {
1407
1574
  log?: (line: string) => void;
1408
1575
  /** The known flaws of the VICE version that the host checked at start. */
1409
1576
  flaws?: readonly string[];
1577
+ directory?: DiskDirectory;
1410
1578
  }): ViceSessionFactory {
1411
1579
  return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
1412
1580
  const launch: ViceLauncher = (mode, launchLog) =>
@@ -1421,6 +1589,11 @@ export function viceSessionFactory(options: {
1421
1589
  ...(options.env === undefined ? {} : { env: options.env }),
1422
1590
  });
1423
1591
  const vice = await launch("headless", () => {});
1424
- return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path), ...(options.flaws === undefined ? {} : { flaws: options.flaws }) });
1592
+ return ViceSession.start(vice, videoStandard, log, {
1593
+ launch,
1594
+ ownPath: (path) => options.supervisor.ownPath(path),
1595
+ ...(options.flaws === undefined ? {} : { flaws: options.flaws }),
1596
+ ...(options.directory === undefined ? {} : { directory: options.directory }),
1597
+ });
1425
1598
  };
1426
1599
  }
@@ -42,7 +42,7 @@ export async function runProgram(client: ViceSessionClient, path: string, prg: U
42
42
  /**
43
43
  * Types RUN at the READY prompt and stops where BASIC starts the first line,
44
44
  * before its SYS. Every tool that starts a loaded program this way reaches
45
- * its entry at the same raster line and timer values (D62). False when BASIC
45
+ * its entry at the same raster line and timer values. False when BASIC
46
46
  * runs no line within RUN_LIMIT_FRAMES.
47
47
  */
48
48
  export async function typeRun(client: ViceSessionClient): Promise<boolean> {
@@ -37,7 +37,7 @@ export function hostEndpoints(env: NodeJS.ProcessEnv = process.env): Endpoint[]
37
37
  return ["127.0.0.1", "host.docker.internal", "host.containers.internal"].map((host) => ({ host, port: DEFAULT_HOST_PORT }));
38
38
  }
39
39
 
40
- /** A host that does not run or cannot be reached is machine-unavailable for every role (D37). */
40
+ /** A host that does not run or cannot be reached is machine-unavailable for every role. */
41
41
  function unreachable(): WireFailure {
42
42
  return new WireFailure(
43
43
  "machine-unavailable",
@@ -1,40 +1,13 @@
1
- // Reads the emulator's memory access map as coverage bytes (D40): one byte
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 (D40). */
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
+ }
@@ -1,4 +1,4 @@
1
- // The action steps of the step language (D43): write, registers, joystick,
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, MAX_EXECUTION_COUNT)) fail(`${at} needs a frame count from 1 to ${MAX_EXECUTION_COUNT}`);
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, D46), or memory.
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
- await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
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 =
@@ -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";