@henols/c64-re-tools 2.0.0-rc.9 → 2.0.1

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 +97 -116
  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 +14 -5
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +25 -5
  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 +8 -2
  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/package.ts +13 -3
  111. package/src/platform.ts +70 -7
  112. package/src/project.ts +180 -3
  113. package/src/protocol/messages.ts +1 -1
  114. package/src/protocol/tools.ts +2 -16
  115. package/src/protocol/vice.ts +124 -15
  116. package/src/script.ts +46 -7
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -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.db.
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 (D56). The tool still runs. */
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`);
@@ -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 (D43). One address when not given. */
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 (D63). */
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 (D40), RAM, ROM and I/O apart. */
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 (D62).
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 (D43). */
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 (D56).
455
- * `joysticksCentred`: the control ports that held a direction or fire and were centred first (R3-05).
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 (D56). */
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
- profile: { params: { limit: number }; result: { entries: ProfileEntry[] } };
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 (D41): export sends the bytes of a named snapshot back as
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. Prints its result as one JSON line when it returns a value (undefined prints nothing).
26
- * A refusal (WireFailure always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
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
- result = await body();
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 === "" ? options.usage : `${message}\n\n${options.usage}` } });
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;
@@ -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
- }