@henols/c64-re-tools 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +7 -3
  3. package/skills/c64-assembler/scripts/assembler.ts +29 -11
  4. package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
  5. package/skills/c64-basic/SKILL.md +1 -0
  6. package/skills/c64-disk/SKILL.md +3 -2
  7. package/skills/c64-disk/scripts/disk.ts +26 -12
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -11
  9. package/skills/c64-docs/SKILL.md +2 -1
  10. package/skills/c64-docs/scripts/docs.ts +1 -1
  11. package/skills/c64-docs/scripts/lib/links.ts +5 -3
  12. package/skills/c64-docs/scripts/lib/reference.ts +2 -2
  13. package/skills/c64-emulator/SKILL.md +9 -2
  14. package/skills/c64-knowledge/SKILL.md +9 -5
  15. package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
  17. package/skills/c64-memory-map/SKILL.md +2 -1
  18. package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
  19. package/skills/c64-provenance/SKILL.md +6 -4
  20. package/skills/c64-provenance/scripts/lib/library.ts +18 -6
  21. package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
  22. package/skills/c64-provenance/scripts/provenance.ts +14 -10
  23. package/skills/c64-source/SKILL.md +3 -0
  24. package/skills/c64-source/scripts/lib/source.ts +9 -31
  25. package/skills/c64-source/scripts/source.ts +14 -10
  26. package/skills/c64-static-analysis/SKILL.md +2 -0
  27. package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
  28. package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
  29. package/skills/c64-testing/SKILL.md +14 -3
  30. package/skills/c64-testing/references/recording-format.md +2 -0
  31. package/skills/c64-testing/references/scenario-format.md +6 -0
  32. package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
  33. package/skills/c64-testing/scripts/lib/diff.ts +3 -3
  34. package/skills/c64-testing/scripts/lib/recording.ts +50 -15
  35. package/skills/c64-testing/scripts/lib/replay.ts +12 -1
  36. package/skills/c64-testing/scripts/lib/runs.ts +84 -4
  37. package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
  38. package/skills/c64-testing/scripts/testing.ts +20 -7
  39. package/skills/c64-unpacker/SKILL.md +17 -10
  40. package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
  41. package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
  42. package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
  43. package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
  44. package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
  45. package/src/host/vice/adapter.ts +4 -1
  46. package/src/host/vice/session.ts +148 -13
  47. package/src/host-client/image.ts +40 -8
  48. package/src/host-client/steps.ts +145 -9
  49. package/src/host-client/tools.ts +1 -1
  50. package/src/host-client/transfer.ts +33 -18
  51. package/src/knowledge/expression.ts +1 -1
  52. package/src/knowledge/history.ts +74 -4
  53. package/src/knowledge/import.ts +79 -7
  54. package/src/knowledge/names.ts +2 -1
  55. package/src/knowledge/program.ts +32 -18
  56. package/src/knowledge/read.ts +46 -0
  57. package/src/knowledge/schema.ts +4 -2
  58. package/src/knowledge/text.ts +2 -0
  59. package/src/knowledge/write.ts +58 -16
  60. package/src/mcp/inputs.ts +6 -3
  61. package/src/mcp/output.ts +5 -0
  62. package/src/mcp/server.ts +20 -4
  63. package/src/mcp/tools/debug.ts +36 -9
  64. package/src/mcp/tools/execution.ts +18 -5
  65. package/src/mcp/tools/machine.ts +3 -1
  66. package/src/mcp/tools/media.ts +14 -2
  67. package/src/mcp/tools/memory.ts +11 -6
  68. package/src/mcp/tools/video.ts +5 -3
  69. package/src/media.ts +42 -0
  70. package/src/opcodes.ts +51 -18
  71. package/src/project.ts +111 -3
  72. package/src/protocol/vice.ts +8 -2
  73. package/src/script.ts +55 -3
@@ -9,17 +9,17 @@
9
9
  import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
10
10
  import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
11
11
  import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
12
- import { resetToReady } from "#src/host-client/entry-state.ts";
13
12
  import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
14
13
  import { WireFailure } from "#src/host-client/tools.ts";
15
- import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
14
+ import { readPrg } from "#src/host-client/image.ts";
15
+ import { writeReported, type Written } from "#src/host-client/transfer.ts";
16
16
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
17
17
  import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
18
- import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
18
+ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
19
19
  import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
20
20
  import { loads, readLoadSteps } from "./lib/loads.ts";
21
21
  import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
22
- import { entryState, IO_AREAS, json, machineState, readRam, vectorsOf } from "./lib/machine.ts";
22
+ import { entryStateFile, machineState, readRam, readyState, vectorsOf } from "./lib/machine.ts";
23
23
 
24
24
  /** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
25
25
  const UNPACKED_BYTES = 256;
@@ -38,10 +38,12 @@ async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise
38
38
  return undefined;
39
39
  }
40
40
 
41
- async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[] }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
42
- const { bytes } = readProjectFile(program);
43
- if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
44
- if (options.entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
41
+ /** inspect, trace, stages and capture take a PRG: a disk, tape or cartridge file goes to loads. */
42
+ const NOT_PRG_REMEDY = "Use loads, which runs a disk, tape or cartridge image, or get the PRG out of it with disk.ts read of the c64-disk skill";
43
+ const readProgram = (path: string) => readPrg(path, NOT_PRG_REMEDY).bytes;
44
+
45
+ async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; max: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
46
+ const bytes = startable(program, options.entry);
45
47
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
46
48
  try {
47
49
  // Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
@@ -54,7 +56,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
54
56
  const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
55
57
  const advanced = step.advancedFrames ?? 0;
56
58
  ran += advanced;
57
- if (advanced === 0) break;
59
+ // A JAM or a point stops the run before its frames: the evidence would be of a run that did not happen.
60
+ if ((step.stopReason !== undefined && step.stopReason !== "frames") || advanced === 0) {
61
+ const stopReason = step.stopReason ?? "no-frames";
62
+ const at = step.pc ?? (await client.registersGet("c64")).pc;
63
+ return { output: { traced: false, frames: ran, stopReason, pc: formatC64Address(at), reason: stoppedEarly(stopReason, at, ran, options.frames) }, failed: true };
64
+ }
58
65
  }
59
66
  const { pc } = await client.registersGet("c64");
60
67
  const ranges = await readMemoryMap(async (start) => {
@@ -80,7 +87,7 @@ async function trace(program: string, options: { frames: number; entry?: number;
80
87
  traced: true,
81
88
  frames: ran,
82
89
  pc: formatC64Address(pc),
83
- writtenThenExecuted: written.map((range) => ({
90
+ writtenThenExecuted: listed(written, options.max, (range) => ({
84
91
  start: formatC64Address(range.start),
85
92
  end: formatC64Address(range.end),
86
93
  ...(underRom(range.start) ? { underRom: true } : {}),
@@ -107,6 +114,12 @@ async function trace(program: string, options: { frames: number; entry?: number;
107
114
 
108
115
  const NO_BASIC = "the program is no BASIC program at $0801, so RUN cannot start it. Give --entry <address>.";
109
116
 
117
+ /** Why a run stopped before its frames: what stopped the CPU, where, and after how many frames. */
118
+ function stoppedEarly(stopReason: string, pc: number, ran: number, frames: number): string {
119
+ const what = stopReason === "jam" ? "a JAM instruction" : stopReason === "no-frames" ? "the emulator, which ran no frame" : `a ${stopReason}`;
120
+ return `The CPU stopped at ${formatC64Address(pc)} by ${what} after ${ran} of ${frames} frames. Check --entry and the steps: the program must run for the frames.`;
121
+ }
122
+
110
123
  /** The first PC in one of the ranges within SYS_FRAMES frames, found with exec breakpoints over them. */
111
124
  async function firstPcIn(client: ViceSessionClient, ranges: Array<{ start: number; end: number }>): Promise<number | undefined> {
112
125
  const points = [];
@@ -151,6 +164,13 @@ async function firstPcAfterBasic(client: ViceSessionClient, from: number, to: nu
151
164
  return from > to ? undefined : firstPcIn(client, [{ start: from, end: to }]);
152
165
  }
153
166
 
167
+ /**
168
+ * What a stage wrote: every written block, also a small one. --min-stage
169
+ * decides only where a stage starts; the wrote list and the capture segments
170
+ * keep each byte that a stage wrote.
171
+ */
172
+ const stageWrote = (map: Uint8Array) => writtenBlocks(map, 1);
173
+
154
174
  /** Frames between two looks at the memory map while stages are followed. */
155
175
  const STAGE_CHUNK_FRAMES = 25;
156
176
  const MAX_STAGES = 16;
@@ -175,6 +195,8 @@ interface Followed {
175
195
  reached: boolean;
176
196
  /** Why the program did not start, when it did not. */
177
197
  failed?: string;
198
+ /** What stopped the CPU before the frames ran: a JAM or a point of the program. */
199
+ halted?: { stopReason: string; pc: number };
178
200
  }
179
201
 
180
202
  type RunUntilStep = Extract<ActionStep, { runUntil: unknown }>;
@@ -197,13 +219,11 @@ function trackedRunUntil(step: ActionStep): step is RunUntilStep & { runUntil: {
197
219
  async function follow(
198
220
  client: ViceSessionClient,
199
221
  program: string,
222
+ bytes: Uint8Array,
200
223
  options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
201
224
  ): Promise<Followed> {
202
- const { bytes } = readProjectFile(program);
203
- if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
204
225
  const load = bytes[0]! | (bytes[1]! << 8);
205
226
  const basic = basicStart(bytes);
206
- if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
207
227
  const notStarted = await startLoaded(client, program, bytes, options.entry);
208
228
  if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
209
229
  // The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts.
@@ -216,6 +236,7 @@ async function follow(
216
236
  let firstFrame = 0;
217
237
  let total = 0;
218
238
  let ended = false;
239
+ let halted: { stopReason: string; pc: number } | undefined;
219
240
  /** Where the run stops: the target of a runUntil step while it runs, then until. */
220
241
  let stopAt: number | undefined;
221
242
  const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
@@ -223,7 +244,7 @@ async function follow(
223
244
  let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
224
245
  const startStage = async (next: number, last: Uint8Array) => {
225
246
  mergeMaps(stage.map, last);
226
- found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage), next });
247
+ found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map), next });
227
248
  entry = next;
228
249
  firstFrame = total;
229
250
  stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] };
@@ -235,7 +256,7 @@ async function follow(
235
256
  * blocks, and stops at the first opcode that the stage wrote and the CPU
236
257
  * then executes. Own code stops the run once; a write watch arms it again.
237
258
  */
238
- const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
259
+ const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean; halted?: { stopReason: string; pc: number } }> => {
239
260
  const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
240
261
  const arm = async (wanted: { exec: AddressRange[]; watch: AddressRange[] }) => {
241
262
  const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
@@ -295,6 +316,7 @@ async function follow(
295
316
  left -= advanced;
296
317
  const pc = run.pc ?? (await client.registersGet("c64")).pc;
297
318
  map = await readCoverage(client);
319
+ if (run.stopReason === "jam") return { used, map, atTarget: false, halted: { stopReason: "jam", pc } };
298
320
  // A frame can end in written code that the stage called before: that stop starts no stage.
299
321
  if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
300
322
  if (written(pc)) {
@@ -341,6 +363,14 @@ async function follow(
341
363
  const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64", ...framed() });
342
364
  const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
343
365
  const map = await readCoverage(client);
366
+ // A JAM, or a point that is not the target, ends the run before its frames.
367
+ if (first.stopReason !== undefined && first.stopReason !== "frames" && !atTarget(firstPc)) {
368
+ total += first.advancedFrames ?? 0;
369
+ mergeMaps(stage.map, map);
370
+ halted = { stopReason: first.stopReason, pc: firstPc };
371
+ ended = true;
372
+ continue;
373
+ }
344
374
  const candidates = stageCandidates(stage.map, map, stage.own, options.minStage, stage.called);
345
375
  if (candidates.length === 0) {
346
376
  const advanced = first.advancedFrames ?? 0;
@@ -369,6 +399,10 @@ async function follow(
369
399
  if (again.next !== undefined) await startStage(again.next, again.map);
370
400
  else mergeMaps(stage.map, again.map);
371
401
  if (again.atTarget) stopped = true;
402
+ if (again.halted !== undefined) {
403
+ halted = again.halted;
404
+ ended = true;
405
+ }
372
406
  }
373
407
  return stopped;
374
408
  };
@@ -389,7 +423,8 @@ async function follow(
389
423
  else if (trackedRunUntil(step)) {
390
424
  if (!(await trackTo(knowledgeAt()(step.runUntil.target.address), step.runUntil.timeoutFrames))) {
391
425
  const { pc } = await client.registersGet("c64");
392
- return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
426
+ const why = halted === undefined ? "" : ` ${stoppedEarly(halted.stopReason, halted.pc, total, step.runUntil.timeoutFrames)}`;
427
+ return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.${why}` };
393
428
  }
394
429
  } else await runActionStep(client, step, knowledgeAt());
395
430
  }
@@ -399,8 +434,8 @@ async function follow(
399
434
  else reached = await trackTo(options.until, left);
400
435
  await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
401
436
  mergeMaps(stage.map, await readCoverage(client));
402
- found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
403
- return { stages: found, frames: total, reached };
437
+ found.push({ entry, firstFrame, frames: total - firstFrame, wrote: stageWrote(stage.map) });
438
+ return { stages: found, frames: total, reached, ...(halted === undefined ? {} : { halted }) };
404
439
  }
405
440
 
406
441
  const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
@@ -413,11 +448,23 @@ const showStages = (stages: Stage[]) =>
413
448
  ...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
414
449
  }));
415
450
 
451
+ /** The bytes of a PRG that the emulator starts: RUN needs a BASIC program at $0801, else --entry gives the start. Checked before the emulator opens. */
452
+ function startable(program: string, entry: number | undefined): Uint8Array {
453
+ const bytes = readProgram(program);
454
+ if (entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
455
+ return bytes;
456
+ }
457
+
416
458
  async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
459
+ const bytes = startable(program, options.entry);
417
460
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
418
461
  try {
419
- const followed = await follow(client, program, options);
462
+ const followed = await follow(client, program, bytes, options);
420
463
  if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
464
+ if (followed.halted !== undefined) {
465
+ const { stopReason, pc } = followed.halted;
466
+ return { output: { followed: false, frames: followed.frames, stopReason, pc: formatC64Address(pc), reason: stoppedEarly(stopReason, pc, followed.frames, options.frames), stages: showStages(followed.stages) }, failed: true };
467
+ }
421
468
  return {
422
469
  output: {
423
470
  followed: true,
@@ -437,14 +484,20 @@ async function capture(
437
484
  program: string,
438
485
  options: { until: number; out: string; range?: AddressRange; exclude: AddressRange[]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
439
486
  ): Promise<{ output: Record<string, unknown>; failed: boolean }> {
487
+ const bytes = startable(program, options.entry);
440
488
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
441
489
  try {
442
- const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
490
+ const followed = await follow(client, program, bytes, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until, ...(options.entry === undefined ? {} : { entry: options.entry }) });
443
491
  if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
444
492
  const registers = await client.registersGet("c64");
445
493
  if (!followed.reached) {
446
494
  return {
447
- output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.`, stages: showStages(followed.stages) },
495
+ output: {
496
+ captured: false,
497
+ reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.${followed.halted === undefined ? "" : ` ${stoppedEarly(followed.halted.stopReason, followed.halted.pc, followed.frames, options.timeoutFrames)}`}`,
498
+ ...(followed.halted === undefined ? {} : { stopReason: followed.halted.stopReason }),
499
+ stages: showStages(followed.stages),
500
+ },
448
501
  failed: true,
449
502
  };
450
503
  }
@@ -453,10 +506,6 @@ async function capture(
453
506
  const state = await machineState(client);
454
507
  // The segments: what the stages wrote, without loader leftovers, with no overlap.
455
508
  const found = options.range !== undefined ? [options.range] : captureSegments(followed.stages, registers.pc);
456
- const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
457
- if (inside !== undefined) {
458
- return { output: { captured: false, reason: `The entry ${formatC64Address(registers.pc)} is in --exclude ${formatC64Address(inside.start)}-${formatC64Address(inside.end)}. A segment must hold the entry.` }, failed: true };
459
- }
460
509
  const segments = withoutRanges(found, options.exclude);
461
510
  const file = (path: string) => `${options.out}/${path}`;
462
511
  const written: Written[] = [];
@@ -486,14 +535,8 @@ async function capture(
486
535
  ),
487
536
  );
488
537
  // A clean machine at the READY prompt shows what the loader and the stages changed.
489
- await resetToReady(client);
490
- const clean = await machineState(client);
491
- const differs: Array<{ address: string; entry: number; reset: number }> = [];
492
- const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
493
- compare(0x0000, state.port, clean.port);
494
- compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
495
- for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
496
- write("entry_state.json", json(entryState(registers, state, vectors, differs)));
538
+ const entry = entryStateFile(registers, state, vectors, await readyState(client));
539
+ write("entry_state.json", entry.bytes);
497
540
  return {
498
541
  output: {
499
542
  captured: true,
@@ -503,7 +546,7 @@ async function capture(
503
546
  segments: segments.map(showRange),
504
547
  ...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
505
548
  stages: followed.stages.length,
506
- differsFromReset: differs.length,
549
+ differsFromReset: entry.differsFromReset,
507
550
  },
508
551
  failed: false,
509
552
  };
@@ -526,7 +569,7 @@ const entryOf = (call: Call) => {
526
569
  };
527
570
  const stepsOf = (call: Call) => {
528
571
  const path = call.string("steps");
529
- return path === undefined ? [] : readActionSteps(path, "--steps");
572
+ return path === undefined ? [] : readActionSteps(path, "--steps", knowledgeAt());
530
573
  };
531
574
 
532
575
  const COMMANDS: CommandSpec[] = [
@@ -534,14 +577,14 @@ const COMMANDS: CommandSpec[] = [
534
577
  name: "inspect",
535
578
  summary: "Packing evidence: the BASIC start and the entropy of the bytes.",
536
579
  subjects: [PROGRAM],
537
- run: (call) => inspect(readProjectFile(call.subject(0)).bytes),
580
+ run: (call) => inspect(readProgram(call.subject(0))),
538
581
  },
539
582
  {
540
583
  name: "trace",
541
584
  summary: "Runs the program and reports the memory that it wrote and then executed.",
542
585
  subjects: [PROGRAM],
543
- options: { frames: FRAMES, entry: ENTRY, steps: STEPS },
544
- run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), ...entryOf(call) })),
586
+ options: { frames: FRAMES, entry: ENTRY, steps: STEPS, "max-listed": OPTIONS["max-listed"] },
587
+ run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), max: call.count("max-listed")!, ...entryOf(call) })),
545
588
  },
546
589
  {
547
590
  name: "stages",
@@ -552,7 +595,8 @@ const COMMANDS: CommandSpec[] = [
552
595
  run: async (call) => {
553
596
  const steps = stepsOf(call);
554
597
  // A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
555
- if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture");
598
+ const untracked = steps.findIndex((step) => "runUntil" in step && !trackedRunUntil(step));
599
+ if (untracked >= 0) throw new UsageError(`--steps ${call.string("steps")!}: step ${untracked + 1}: stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture`);
556
600
  return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
557
601
  },
558
602
  },
@@ -562,7 +606,7 @@ const COMMANDS: CommandSpec[] = [
562
606
  subjects: [PROGRAM],
563
607
  options: {
564
608
  until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
565
- out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
609
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
566
610
  range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
567
611
  exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
568
612
  "timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
@@ -573,7 +617,14 @@ const COMMANDS: CommandSpec[] = [
573
617
  notes: "entry_state.json has the registers, $00/$01, pages 0-3, the VIC, SID and CIA registers, the vectors, and what differs from a clean reset.",
574
618
  run: async (call) => {
575
619
  const range = call.range("range");
576
- return outcome(await capture(call.subject(0), { until: call.address("until")!, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
620
+ const until = call.address("until")!;
621
+ // A segment must hold the entry: refused before the emulator runs.
622
+ const show = (item: AddressRange) => `${formatC64Address(item.start)}-${formatC64Address(item.end)}`;
623
+ const holds = (item: AddressRange) => until >= item.start && until <= item.end;
624
+ const excluded = call.ranges("exclude").find(holds);
625
+ if (excluded !== undefined) throw new UsageError(`--exclude ${show(excluded)} holds the entry --until ${formatC64Address(until)}. A segment must hold the entry: give an --exclude range without it`);
626
+ if (range !== undefined && !holds(range)) throw new UsageError(`--range ${show(range)} does not hold the entry --until ${formatC64Address(until)}. Give a --range that holds the entry`);
627
+ return outcome(await capture(call.subject(0), { until, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
577
628
  },
578
629
  },
579
630
  {
@@ -582,7 +633,7 @@ const COMMANDS: CommandSpec[] = [
582
633
  subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
583
634
  options: {
584
635
  steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
585
- out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
636
+ out: { ...OPTIONS.out, output: "folder", value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
586
637
  drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
587
638
  "min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
588
639
  },
@@ -59,7 +59,10 @@ export function conditionExpression(condition: Condition, space: Space): string
59
59
  }
60
60
  case "memory": {
61
61
  if (condition.space !== "c64") {
62
- throw new WireFailure("unsupported-in-space", "A memory condition can test only space c64 memory.");
62
+ throw new WireFailure(
63
+ "unsupported-in-space",
64
+ "A memory target or a memory condition tests only space c64 memory. For the drive, use a watchpoint in space drive8, or a c64_run_until address target in space drive8.",
65
+ );
63
66
  }
64
67
  return `(@${condition.view}:${formatC64Address(condition.address)} ${operator(condition.operator)} ${formatC64Byte(condition.value)})`;
65
68
  }