@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
@@ -17,11 +17,12 @@ import { operandText } from "#src/knowledge/expression.ts";
17
17
  import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
18
18
  import { joinBatches, type Batch, type JoinChoice, type JoinClash } from "#src/knowledge/join.ts";
19
19
  import { knowledgeNames } from "#src/knowledge/names.ts";
20
- import { addCoverage, coverageRuns, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
20
+ import { addCoverage, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
21
21
  import {
22
22
  analyzerNamesAt,
23
23
  at,
24
24
  commentsAt,
25
+ coverageRuns,
25
26
  currentRevision,
26
27
  listAnalyzerNames,
27
28
  listComments,
@@ -83,7 +84,7 @@ import {
83
84
  } from "#src/knowledge/write.ts";
84
85
  import { platformNames } from "#src/platform.ts";
85
86
  import { isInside, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRelative, projectRoot, resolveProjectPath } from "#src/project.ts";
86
- import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec, type SubjectSpec } from "#src/script.ts";
87
+ import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec, type SubjectSpec } from "#src/script.ts";
87
88
  import { listing, listingText, type ListingSource } from "./lib/listing.ts";
88
89
 
89
90
  function oneOf<T extends string>(values: readonly T[], value: string | undefined, what: string): T {
@@ -150,9 +151,18 @@ function suppression(row: SuppressionRow) {
150
151
  return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
151
152
  }
152
153
 
154
+ /** A row of the program in a revision: memory bytes, the entry, or a coverage range. */
155
+ function programRow(entry: Extract<HistoryEntry, { entity: "memory" | "entry" | "coverage" }>) {
156
+ if (entry.entity === "memory") return { ...spaceOf(entry.row), start: formatC64Address(entry.row.address), end: formatC64Address(entry.row.address + entry.row.bytes - 1), bytes: entry.row.bytes, origin: entry.row.origin, revision: entry.row.revision };
157
+ if (entry.entity === "entry") return { ...spaceOf(entry.row), address: formatC64Address(entry.row.address), state: entry.row.state, stages: entry.row.stages, origin: entry.row.origin, revision: entry.row.revision };
158
+ return { ...spaceOf(entry.row), kind: entry.row.kind, start: formatC64Address(entry.row.start), end: formatC64Address(entry.row.end), origin: entry.row.origin, revision: entry.row.revision };
159
+ }
160
+
153
161
  function historyEntry(entry: HistoryEntry) {
154
162
  const shown =
155
- entry.entity === "symbol" || entry.entity === "analyzer-name"
163
+ entry.entity === "memory" || entry.entity === "entry" || entry.entity === "coverage"
164
+ ? programRow(entry)
165
+ : entry.entity === "symbol" || entry.entity === "analyzer-name"
156
166
  ? symbol(entry.row)
157
167
  : entry.entity === "region"
158
168
  ? region(entry.row)
@@ -226,13 +236,17 @@ function listingSource(db: DatabaseSync | undefined, space: string): ListingSour
226
236
  };
227
237
  }
228
238
 
229
- /** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
239
+ /** Every value of --origin, for the message of a wrong one. */
240
+ const ORIGIN_VALUES = `one of ${ORIGINS.join(", ")}, semantic or analyzer, or a comma list of them. semantic is ${SEMANTIC_ORIGINS.join(" and ")}. analyzer is ${ANALYZERS.slice(0, -1).join(", ")} and ${ANALYZERS.at(-1)}`;
241
+
242
+ /** --origin for reads: one origin, semantic (user and llm), analyzer (dxa, ghidra and heuristic), or a comma list. */
230
243
  function originFilter(value: string): Origin[] {
231
244
  const origins = new Set<Origin>();
232
245
  for (const part of value.split(",")) {
233
246
  if (part === "semantic") for (const origin of SEMANTIC_ORIGINS) origins.add(origin);
234
247
  else if (part === "analyzer") for (const origin of ANALYZERS) origins.add(origin);
235
- else origins.add(oneOf(ORIGINS, part, "--origin"));
248
+ else if ((ORIGINS as readonly string[]).includes(part)) origins.add(part as Origin);
249
+ else throw new UsageError(`--origin cannot be ${JSON.stringify(part)}. Give ${ORIGIN_VALUES}`);
236
250
  }
237
251
  return [...origins];
238
252
  }
@@ -371,6 +385,20 @@ const WRITE: Record<string, OptionSpec> = {
371
385
  "expect-revision": OPTIONS["expect-revision"],
372
386
  };
373
387
 
388
+ /** A list command: --max-listed limits each list of its result. */
389
+ const LIST = { "max-listed": OPTIONS["max-listed"] };
390
+ /** Where to get the rest of a list that a --range limits. */
391
+ const RANGE_REST = "a larger --max-listed, a smaller --range, or knowledge.ts export --out <file> for all current knowledge";
392
+ const MORE = "a larger --max-listed";
393
+
394
+ /** A list in a result: its count and its first --max-listed items, with rest when it is cut. */
395
+ function list<T, U>(call: Call, items: readonly T[], show: (item: T) => U, rest: string) {
396
+ return listed(items, call.count("max-listed")!, show, rest);
397
+ }
398
+
399
+ /** All rows: the lists count every row, and --max-listed limits only what the result shows. */
400
+ const ALL = Number.MAX_SAFE_INTEGER;
401
+
374
402
  /** The rows from --range, or every address. */
375
403
  const rangeOf = (call: Call) => call.range("range") ?? { start: 0x0000, end: 0xffff };
376
404
  const originsOf = (call: Call) => {
@@ -401,7 +429,8 @@ function writeCommand(spec: Omit<CommandSpec, "run">, prepare: (call: Call) => W
401
429
  return {
402
430
  ...spec,
403
431
  notes: [spec.notes, "A write is one revision. It makes .c64-re-tools/knowledge/ when it is not there."].filter((note) => note !== undefined).join("\n"),
404
- options: { ...spec.options, ...WRITE },
432
+ // A command can give a WRITE option its own help or default, for example --space of revert.
433
+ options: Object.fromEntries(Object.entries({ ...spec.options, ...WRITE }).map(([name, option]) => [name, spec.options?.[name] ?? option])),
405
434
  run: (call) => withWrite(prepare(call)),
406
435
  };
407
436
  }
@@ -459,50 +488,50 @@ const READ_COMMANDS: CommandSpec[] = [
459
488
  name: "search",
460
489
  summary: "Symbol names, analyzer names and comment texts that hold the text.",
461
490
  subjects: [{ name: "<text>", help: "the text to find" }],
462
- options: { origin: OPTIONS.origin, limit: { kind: "count", value: "<n>", min: 1, max: 10_000, default: 50, help: "at most n rows of each kind (default 50)" } },
491
+ options: { origin: OPTIONS.origin, ...LIST },
463
492
  },
464
493
  (db, call) => {
465
- const found = search(db, call.subject(0), call.count("limit")!, originsOf(call), call.space);
466
- return { symbols: found.symbols.map(symbol), analyzerNames: found.analyzerNames.map(symbol), comments: found.comments.map(comment) };
494
+ const found = search(db, call.subject(0), ALL, originsOf(call), call.space);
495
+ const rest = "a larger --max-listed, or a longer <text>";
496
+ return { symbols: list(call, found.symbols, symbol, rest), analyzerNames: list(call, found.analyzerNames, symbol, rest), comments: list(call, found.comments, comment, rest) };
467
497
  },
468
498
  ),
469
499
  readCommand(
470
- { name: "symbols", summary: "The symbols, or with --analyzer the second names that DXA, Ghidra and the idioms gave.", options: { ...FILTER, kind: SYMBOL_KIND, analyzer: { kind: "flag", help: "the analyzer names in place of the symbols" } } },
500
+ { name: "symbols", summary: "The symbols, or with --analyzer the second names that DXA, Ghidra and the idioms gave.", options: { ...FILTER, kind: SYMBOL_KIND, analyzer: { kind: "flag", help: "the analyzer names in place of the symbols" }, ...LIST } },
471
501
  (db, call) => {
472
502
  const kind = call.string("kind") as (typeof SYMBOL_KINDS)[number] | undefined;
473
- if (call.flag("analyzer")) return { analyzerNames: listAnalyzerNames(db, rangeOf(call), kind, originsOf(call), call.space).map(symbol) };
474
- return { symbols: listSymbols(db, rangeOf(call), kind, originsOf(call), call.space).map(symbol) };
503
+ if (call.flag("analyzer")) return { analyzerNames: list(call, listAnalyzerNames(db, rangeOf(call), kind, originsOf(call), call.space), symbol, RANGE_REST) };
504
+ return { symbols: list(call, listSymbols(db, rangeOf(call), kind, originsOf(call), call.space), symbol, RANGE_REST) };
475
505
  },
476
506
  ),
477
- readCommand({ name: "regions", summary: "The regions: code, data, tables and the other types.", options: { ...FILTER, type: { kind: "choice", value: "<type>", choices: REGION_TYPES, help: `one of ${REGION_TYPES.join(", ")}` } } }, (db, call) => ({
478
- regions: listRegions(db, rangeOf(call), call.string("type") as (typeof REGION_TYPES)[number] | undefined, originsOf(call), call.space).map(region),
507
+ readCommand({ name: "regions", summary: "The regions: code, data, tables and the other types.", options: { ...FILTER, type: { kind: "choice", value: "<type>", choices: REGION_TYPES, help: `one of ${REGION_TYPES.join(", ")}` }, ...LIST } }, (db, call) => ({
508
+ regions: list(call, listRegions(db, rangeOf(call), call.string("type") as (typeof REGION_TYPES)[number] | undefined, originsOf(call), call.space), region, RANGE_REST),
479
509
  })),
480
- readCommand({ name: "comments", summary: "The comments.", options: FILTER }, (db, call) => ({ comments: listComments(db, rangeOf(call), originsOf(call), call.space).map(comment) })),
481
- readCommand({ name: "references", summary: "The references: calls, jumps, reads, writes and pointers.", options: { ...FILTER, kind: REFERENCE_KIND } }, (db, call) => ({
482
- references: listReferences(db, rangeOf(call), call.string("kind") as (typeof REFERENCE_KINDS)[number] | undefined, originsOf(call), call.space).map(reference),
510
+ readCommand({ name: "comments", summary: "The comments.", options: { ...FILTER, ...LIST } }, (db, call) => ({ comments: list(call, listComments(db, rangeOf(call), originsOf(call), call.space), comment, RANGE_REST) })),
511
+ readCommand({ name: "references", summary: "The references: calls, jumps, reads, writes and pointers.", options: { ...FILTER, kind: REFERENCE_KIND, ...LIST } }, (db, call) => ({
512
+ references: list(call, listReferences(db, rangeOf(call), call.string("kind") as (typeof REFERENCE_KINDS)[number] | undefined, originsOf(call), call.space), reference, RANGE_REST),
483
513
  })),
484
- readCommand({ name: "suppressions", summary: "The places that removals keep free of analyzer facts, with the reason of each.", options: { range: OPTIONS.range } }, (db, call) => {
514
+ readCommand({ name: "suppressions", summary: "The places that removals keep free of analyzer facts, with the reason of each.", options: { range: OPTIONS.range, ...LIST } }, (db, call) => {
485
515
  const rows = listSuppressions(db, rangeOf(call), call.space);
486
- const reasons = reasonsOf(db, rows);
487
- return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
516
+ const reasons = reasonsOf(db, rows.slice(0, call.count("max-listed")!));
517
+ return { suppressions: list(call, rows, (row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) }), RANGE_REST) };
488
518
  }),
489
- readCommand({ name: "modules", summary: "Where the files of the generated source start.", options: FILTER }, (db, call) => ({ modules: listModules(db, rangeOf(call), originsOf(call), call.space).map(module) })),
490
- readCommand({ name: "operands", summary: "The operand expressions of instructions.", options: FILTER }, (db, call) => ({ operands: listOperands(db, rangeOf(call), originsOf(call), call.space).map(operand) })),
491
- readCommand({ name: "patches", summary: "The code that a switch puts in.", options: { ...FILTER, switch: { kind: "text", value: "<switch>", help: "only the patches of this switch" } } }, (db, call) => ({
492
- patches: listPatches(db, rangeOf(call), call.string("switch"), originsOf(call), call.space).map(patch),
519
+ readCommand({ name: "modules", summary: "Where the files of the generated source start.", options: { ...FILTER, ...LIST } }, (db, call) => ({ modules: list(call, listModules(db, rangeOf(call), originsOf(call), call.space), module, RANGE_REST) })),
520
+ readCommand({ name: "operands", summary: "The operand expressions of instructions.", options: { ...FILTER, ...LIST } }, (db, call) => ({ operands: list(call, listOperands(db, rangeOf(call), originsOf(call), call.space), operand, RANGE_REST) })),
521
+ readCommand({ name: "patches", summary: "The code that a switch puts in.", options: { ...FILTER, switch: { kind: "text", value: "<switch>", help: "only the patches of this switch" }, ...LIST } }, (db, call) => ({
522
+ patches: list(call, listPatches(db, rangeOf(call), call.string("switch"), originsOf(call), call.space), patch, RANGE_REST),
493
523
  })),
494
- readCommand({ name: "switches", summary: "Each switch with the ranges of its patches." }, (db, call) => {
524
+ readCommand({ name: "switches", summary: "Each switch with the ranges of its patches.", options: LIST }, (db, call) => {
495
525
  const switches = new Map<string, PatchRow[]>();
496
526
  for (const row of listPatches(db, undefined, undefined, undefined, call.space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
497
- return {
498
- switches: [...switches].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, rows]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) })),
499
- };
527
+ const show = ([name, rows]: [string, PatchRow[]]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) });
528
+ return { switches: list(call, [...switches].sort(([a], [b]) => (a < b ? -1 : 1)), show, MORE) };
500
529
  }),
501
- readCommand({ name: "macros", summary: "The macros of the generated source.", options: { origin: OPTIONS.origin } }, (db, call) => ({ macros: listMacros(db, originsOf(call), call.space).map(macro) })),
502
- readCommand({ name: "uses", summary: "The instructions written as a macro.", options: { ...FILTER, macro: { kind: "text", value: "<name>", help: "only the uses of this macro" } } }, (db, call) => ({
503
- uses: listMacroUses(db, rangeOf(call), call.string("macro"), originsOf(call), call.space).map(macroUse),
530
+ readCommand({ name: "macros", summary: "The macros of the generated source.", options: { origin: OPTIONS.origin, ...LIST } }, (db, call) => ({ macros: list(call, listMacros(db, originsOf(call), call.space), macro, MORE) })),
531
+ readCommand({ name: "uses", summary: "The instructions written as a macro.", options: { ...FILTER, macro: { kind: "text", value: "<name>", help: "only the uses of this macro" }, ...LIST } }, (db, call) => ({
532
+ uses: list(call, listMacroUses(db, rangeOf(call), call.string("macro"), originsOf(call), call.space), macroUse, RANGE_REST),
504
533
  })),
505
- readCommand({ name: "spaces", summary: "The address spaces that hold knowledge." }, (db) => ({ spaces: listSpaces(db) })),
534
+ readCommand({ name: "spaces", summary: "The address spaces that hold knowledge.", options: LIST }, (db, call) => ({ spaces: list(call, listSpaces(db), (space) => space, MORE) })),
506
535
  readCommand({ name: "program", summary: "The program in the knowledge: its segments and entry, and the coverage maps that coverage imported, with the ranges." }, (db, call) => {
507
536
  const image = programImage(db, call.space);
508
537
  const ranges = db === undefined ? [] : (db.prepare("SELECT kind, count(*) AS count FROM current_coverage WHERE space = ? GROUP BY kind ORDER BY kind").all(call.space) as Array<{ kind: string; count: number }>);
@@ -526,7 +555,7 @@ const READ_COMMANDS: CommandSpec[] = [
526
555
  name: "history",
527
556
  summary: "The rows at an address, of a name or of a macro, through all revisions, each with the reason and the evidence of its revision.",
528
557
  subjects: [{ ...ADDRESS, optional: true }],
529
- options: { name: { kind: "text", value: "<name>", help: "the history of a name in place of an address" }, macro: { kind: "text", value: "<name>", help: "the history of a macro in place of an address" } },
558
+ options: { name: { kind: "text", value: "<name>", help: "the history of a name in place of an address" }, macro: { kind: "text", value: "<name>", help: "the history of a macro in place of an address" }, ...LIST },
530
559
  notes: "Give exactly one of <address>, --name and --macro.",
531
560
  },
532
561
  (db, call) => {
@@ -534,25 +563,22 @@ const READ_COMMANDS: CommandSpec[] = [
534
563
  const macroName = call.string("macro");
535
564
  if ([call.subjects.length > 0, name !== undefined, macroName !== undefined].filter(Boolean).length !== 1) throw new UsageError("history needs exactly one of <address>, --name and --macro");
536
565
  const entries = macroName !== undefined ? historyOfMacro(db, macroName, call.space) : name !== undefined ? historyNamed(db, name, call.space) : historyAt(db, call.parseAddress(call.subject(0), "<address>"), call.space);
537
- return { history: entries.map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })) };
566
+ return { history: list(call, entries, (entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) }), MORE) };
538
567
  },
539
568
  ),
540
569
  readCommand(
541
- { name: "revisions", summary: "The revisions, newest first.", options: { limit: { kind: "count", value: "<n>", min: 1, max: 100_000, help: "at most n revisions" }, before: { kind: "text", value: "<revision>", help: "only the revisions before this one" } } },
570
+ { name: "revisions", summary: "The revisions, newest first.", options: { before: { kind: "text", value: "<revision>", help: "only the revisions before this one" }, ...LIST } },
542
571
  (db, call) => {
543
- const options: { limit?: number; before?: string } = {};
544
- const limit = call.count("limit");
545
- if (limit !== undefined) options.limit = limit;
546
572
  const before = call.string("before");
547
- if (before !== undefined) options.before = before;
548
- return { revisions: revisions(db, options) };
573
+ const rows = revisions(db, before === undefined ? { limit: ALL } : { limit: ALL, before });
574
+ return { revisions: list(call, rows, (row) => row, "a larger --max-listed, or --before <revision> with the oldest revision in listed") };
549
575
  },
550
576
  ),
551
- readCommand({ name: "revision", summary: "The rows that one revision added and closed.", subjects: [REVISION] }, (db, call) => {
577
+ readCommand({ name: "revision", summary: "The rows that one revision added and closed: knowledge rows, and memory, entry and coverage rows of the program.", subjects: [REVISION], options: LIST }, (db, call) => {
552
578
  const changes = revision(db, call.subject(0));
553
- return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
579
+ return { revision: changes.revision, added: list(call, changes.added, historyEntry, MORE), closed: list(call, changes.closed, historyEntry, MORE) };
554
580
  }),
555
- readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
581
+ readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
556
582
  const range = rangeOf(call);
557
583
  const origins = originsOf(call);
558
584
  const space = call.space;
@@ -579,14 +605,14 @@ const READ_COMMANDS: CommandSpec[] = [
579
605
  name: "listing",
580
606
  summary: "The program as ACME lines with names, operand expressions, comments, the access of each line in the coverage (x executed, r read, w written) and the references in and out.",
581
607
  subjects: [RANGE],
582
- options: { out: { ...OPTIONS.out, value: "<file>", help: "write the listing as text to this file in place of the lines in the result" } },
608
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the listing as text to this file in place of the lines in the result" }, ...LIST },
583
609
  },
584
610
  (db, call) => {
585
611
  const { start, end } = call.parseRange(call.subject(0), "<range>");
586
612
  const lines = listing(listingSource(db, call.space), start, end);
587
613
  const out = call.string("out");
588
614
  if (out !== undefined) return { lines: lines.length, ...writeReported(out, listingText(lines), { what: "--out" }) };
589
- return { lines: lines.map((row) => ({ ...row, address: formatC64Address(row.address) })) };
615
+ return { lines: list(call, lines, (row) => ({ ...row, address: formatC64Address(row.address) }), "a larger --max-listed, a smaller <range>, or --out <file> for all lines") };
590
616
  },
591
617
  ),
592
618
  ];
@@ -821,11 +847,23 @@ const WRITE_COMMANDS: CommandSpec[] = [
821
847
  };
822
848
  },
823
849
  ),
824
- writeCommand({ name: "revert", summary: "Undoes one revision with a new revision.", subjects: [REVISION] }, (call) => {
825
- const context = contextOf(call);
826
- const request = { revision: call.subject(0) };
827
- return (db) => ({ revision: revert(db, context, request).revision });
828
- }),
850
+ writeCommand(
851
+ {
852
+ name: "revert",
853
+ summary: "Makes the knowledge the same as at one revision: all spaces, or with --space only that space. The revert is a new revision: the history stays complete.",
854
+ subjects: [REVISION],
855
+ options: { space: { kind: "space", value: "<space>", help: "revert only this space: c64, drive8, or bank0 to bank255. The other spaces do not change. Without --space, every space reverts" } },
856
+ },
857
+ (call) => {
858
+ const context = contextOf(call);
859
+ const space = call.string("space");
860
+ const request = { revision: call.subject(0), ...(space === undefined ? {} : { space }) };
861
+ return (db) => {
862
+ const result = revert(db, context, request);
863
+ return { revision: result.revision, restores: result.current.restoredRevision, ...(space === undefined ? { spaces: "all" } : { space }) };
864
+ };
865
+ },
866
+ ),
829
867
  writeCommand({ name: "merge", summary: "Decides the conflicts that a git merge of two computers' work left: of the rows of one conflict, the newest stays. One revision for all." }, (call) => {
830
868
  const context = contextOf(call);
831
869
  const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
@@ -869,7 +907,7 @@ const JOIN: CommandSpec = {
869
907
  name: "join",
870
908
  summary: "Joins the batches of agents that worked in parallel into one batch for apply. It writes no knowledge.",
871
909
  subjects: [{ name: "<batch>", help: 'a batch: a list of writes, or {"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}', list: true }],
872
- options: { ...SPACE, out: { ...OPTIONS.out, value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
910
+ options: { ...SPACE, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
873
911
  notes: "At an address in the owner range of a batch, its name, comment, operand and module win, and its regions come last. One name or one module file at two addresses writes nothing.",
874
912
  run: (call) => {
875
913
  if (call.subjects.length < 2) throw new UsageError("join needs two or more batch files");
@@ -924,7 +962,7 @@ function batchOperation(call: Call, item: unknown, at: string): BatchOperation {
924
962
  function batchFields(fields: Record<string, unknown>, at: string, address: (text: string, what: string) => number): BatchOperation {
925
963
  const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
926
964
  const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
927
- if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
965
+ if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${[...BATCH_FIELDS[op], "space"].join(", ")}, not ${unknown.join(", ")}`);
928
966
  const text = (key: string) => {
929
967
  if (typeof fields[key] !== "string") throw new Refusal("invalid-input", `${at}: ${key} must be a string`);
930
968
  return fields[key] as string;
@@ -990,7 +1028,7 @@ function batchFields(fields: Record<string, unknown>, at: string, address: (text
990
1028
 
991
1029
  await runScript({
992
1030
  name: "knowledge",
993
- summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks.",
1031
+ summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks. revert without --space reverts all spaces.",
994
1032
  names: knowledgeNames,
995
1033
  commands: [...READ_COMMANDS, ...WRITE_COMMANDS, JOIN],
996
1034
  });
@@ -4,7 +4,7 @@
4
4
  // reads files: knowledge.ts gives the lookups.
5
5
 
6
6
  import { COVERAGE, formatC64Address, formatC64Byte } from "#src/c64.ts";
7
- import { MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
7
+ import { instructionWith, MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
8
8
 
9
9
  export interface ListingSource {
10
10
  /** The image byte at an address, or undefined outside the image. */
@@ -52,36 +52,13 @@ function accessOf(coverage: Uint8Array | undefined, start: number, length: numbe
52
52
  return text === "" ? "-" : text;
53
53
  }
54
54
 
55
- /** The operand of an instruction in ACME syntax, with the name of its target where one exists. */
55
+ /** The operand value of an instruction as an expression: the name of its target where one exists, else the number. Empty for no operand. */
56
56
  function operandOf(mode: Mode, address: number, low: number, high: number, nameOf: ListingSource["nameOf"]): string {
57
- const word = low | (high << 8);
58
57
  const named = (target: number, digits: 2 | 4) => nameOf(target) ?? (digits === 2 ? formatC64Byte(target) : formatC64Address(target));
59
- switch (mode) {
60
- case "imp":
61
- return "";
62
- case "imm":
63
- return `#${formatC64Byte(low)}`;
64
- case "zp":
65
- return named(low, 2);
66
- case "zpx":
67
- return `${named(low, 2)},x`;
68
- case "zpy":
69
- return `${named(low, 2)},y`;
70
- case "abs":
71
- return named(word, 4);
72
- case "abx":
73
- return `${named(word, 4)},x`;
74
- case "aby":
75
- return `${named(word, 4)},y`;
76
- case "ind":
77
- return `(${named(word, 4)})`;
78
- case "izx":
79
- return `(${named(low, 2)},x)`;
80
- case "izy":
81
- return `(${named(low, 2)}),y`;
82
- case "rel":
83
- return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
84
- }
58
+ if (mode === "imp") return "";
59
+ if (mode === "imm") return formatC64Byte(low);
60
+ if (mode === "rel") return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
61
+ return MODE_LENGTH[mode] === 3 ? named(low | (high << 8), 4) : named(low, 2);
85
62
  }
86
63
 
87
64
  /** The lines of start..end: instructions in code regions, decoded from the region start, and bytes elsewhere. */
@@ -123,8 +100,9 @@ export function listing(source: ListingSource, start: number, end: number): List
123
100
  const length = MODE_LENGTH[opcode.mode];
124
101
  const bytes = Array.from({ length: length - 1 }, (_, offset) => source.byteAt(address + 1 + offset));
125
102
  if (address + length - 1 <= region.end && bytes.every((value) => value !== undefined)) {
103
+ // The operand expression of the knowledge in the syntax of the addressing mode, as c64-source writes it.
126
104
  const operand = source.operand(address) ?? operandOf(opcode.mode, address, bytes[0] ?? 0, bytes[1] ?? 0, source.nameOf);
127
- if (address >= start) lines.push(line(address, length, operand === "" ? opcode.mnemonic : `${opcode.mnemonic} ${operand}`));
105
+ if (address >= start) lines.push(line(address, length, instructionWith(byte, operand)));
128
106
  address += length;
129
107
  continue;
130
108
  }
@@ -31,7 +31,7 @@ node <skill>/scripts/memory-map.ts export --out src/hw.asm
31
31
  |---|---|
32
32
  | `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
33
33
  | `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F`, `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`. |
34
- | `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
34
+ | `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | The entries of that group as a list: `count` and `listed`. `--max-listed` limits `listed`. The default is 50. |
35
35
  | `export --out <file>` | Writes all platform names to a project file as an ACME include: one line for each name, with its meaning as a comment. A zero-page name is `NAME = $xx`. Other names are `NAME = $xxxx`. |
36
36
 
37
37
  Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
@@ -43,6 +43,7 @@ Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal numb
43
43
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
44
44
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
45
45
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
46
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
46
47
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
47
48
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
48
49
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -6,7 +6,7 @@ import { formatC64Address, formatC64Byte } from "#src/c64.ts";
6
6
  import { writeReported } from "#src/host-client/transfer.ts";
7
7
  import { knowledgeNames } from "#src/knowledge/names.ts";
8
8
  import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
9
- import { OPTIONS, runScript, UsageError } from "#src/script.ts";
9
+ import { listed, OPTIONS, runScript, UsageError } from "#src/script.ts";
10
10
 
11
11
  const show = (entry: Entry) => ({ start: formatC64Address(entry.start), ...(entry.end !== entry.start ? { end: formatC64Address(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
12
12
 
@@ -61,16 +61,17 @@ await runScript({
61
61
  name: "list",
62
62
  summary: "The registers of a chip, the KERNAL jump table, or the system areas.",
63
63
  subjects: [{ name: "<list>", help: `one of ${Object.keys(LISTS).join(", ")}` }],
64
+ options: { "max-listed": OPTIONS["max-listed"] },
64
65
  run: (call) => {
65
66
  const list = LISTS[call.subject(0)];
66
67
  if (list === undefined) throw new UsageError(`<list> must be one of ${Object.keys(LISTS).join(", ")}, not ${call.subject(0)}`);
67
- return { entries: list().map(show) };
68
+ return { entries: listed(list(), call.count("max-listed")!, show, "a larger --max-listed, or memory-map.ts export --out <file> for every platform name") };
68
69
  },
69
70
  },
70
71
  {
71
72
  name: "export",
72
73
  summary: "Writes the platform names as an ACME include: NAME = $xxxx.",
73
- options: { out: { ...OPTIONS.out, value: "<file>", help: "the include file, relative to the project folder", required: true } },
74
+ options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the include file, relative to the project folder", required: true } },
74
75
  run: (call) => {
75
76
  const names = platformNames();
76
77
  const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${item.address < 0x100 ? formatC64Byte(item.address) : formatC64Address(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
@@ -38,7 +38,7 @@ node <skill>/scripts/provenance.ts leftovers --out .c64-re-tools/local/leftovers
38
38
 
39
39
  | Option | Meaning |
40
40
  |---|---|
41
- | `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. |
41
+ | `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. Give one shift for each release. |
42
42
  | `--range '$0900-$09ff'` | Compare only the addresses in this range. |
43
43
  | `--frames 600` | Run each release for this many frames, and give each listed difference its runtime evidence. |
44
44
  | `--steps .c64-re-tools/local/start-game.json` | With `--frames`: a JSON list of action steps to run before the frames, for example a key to start the game. The steps are the steps of a c64-unpacker steps file. |
@@ -56,6 +56,7 @@ With `--frames`, a PRG starts with autostart and RUN. An image starts from its b
56
56
  - An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
57
57
  - A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
58
58
  - Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
59
+ - The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
59
60
  - The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
60
61
  - An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
61
62
  - The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
@@ -96,7 +97,7 @@ Use `separate` when you have only one release. It reads the program, its stages
96
97
  - `ranges`: the program in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. The last stage is the program. An earlier stage is a depacker, an intro or a loader. Only a capture of c64-unpacker `capture` has stages. For a program from `loads` or a PRG, `noStages` tells this.
97
98
  - `access`: from the coverage of the knowledge: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
98
99
  - `codeRegions.total`: the number of code regions in the knowledge.
99
- - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
100
+ - `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. A region also reaches the code after it when its last instruction is not RTS, RTI, JMP, BRK or JAM: the CPU continues there. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
100
101
  - `outside`: RAM outside the program that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
101
102
 
102
103
  Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed` (default 50). `--out` writes all of them and the result shows the file as `{out, bytes, sha256}`. Run more coverage runs for parts that are `untouched`, and add them with `coverage` of the c64-knowledge skill, before you decide.
@@ -112,7 +113,7 @@ Use `leftovers` to find what the program holds in addition to its working code.
112
113
  | `fill` | One byte that repeats, at least 16 times, or a pattern of two to four bytes that repeats, at least 32 bytes in total. `pattern` is the repeated bytes. |
113
114
  | `residue` | Bytes that no run touched and that are no text, BASIC, fill or code region. `bytes` shows the first 16 bytes. |
114
115
  | `copies` | Bytes that are the same as earlier bytes of the program, at least 32 bytes. `copyOf` is the address of the earlier bytes. |
115
- | `deadCode` | Code regions of the knowledge that no call or jump reaches from the entry, and that no run executed. |
116
+ | `deadCode` | Code regions of the knowledge that the entry does not reach, as in `codeRegions.unreachable` of `separate`, and that no run executed. |
116
117
 
117
118
  Text from another computer can show the tools of the developer:
118
119
 
@@ -162,7 +163,8 @@ To save a routine:
162
163
  - `--name` is the name of the routine and of its file. When you do not give it, the script uses the user or LLM name at the start.
163
164
  - `--title` is the title of the routine. When you do not give it, the script uses the module title.
164
165
  - A routine has at least 16 bytes. Its last instruction must end in the range.
165
- 3. Read `leftOut`. It lists the rows that the file does not keep, and why. For example, the file does not keep an operand expression that uses a name outside the routine.
166
+ 3. The result shows the routine file as `{out, bytes, sha256}`. `out` is relative to the project folder.
167
+ 4. Read `leftOut`. It lists the rows that the file does not keep, and why. For example, the file does not keep an operand expression that uses a name outside the routine.
166
168
 
167
169
  To find the routines in a program:
168
170
 
@@ -4,10 +4,12 @@
4
4
  // its start, so match finds it in another program at another address and
5
5
  // suggests its names there as an apply batch. Nothing here writes knowledge.
6
6
 
7
+ import { createHash } from "node:crypto";
7
8
  import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
8
- import { join, resolve } from "node:path";
9
+ import { join, relative, resolve, sep } from "node:path";
9
10
 
10
11
  import { Refusal } from "#src/errors.ts";
12
+ import { writeRefusal, type Written } from "#src/host-client/transfer.ts";
11
13
  import { formatC64Address, instructionLength } from "#src/c64.ts";
12
14
  import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
13
15
  import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
@@ -317,16 +319,26 @@ export function readLibrary(folder: string): Routine[] {
317
319
  .map((name) => parseRoutine(readFileSync(join(folder, name), "utf8"), name));
318
320
  }
319
321
 
320
- /** Writes a routine file into the library: a temporary file, then a rename. Refuses a name that exists unless `replace`. */
321
- export function saveRoutine(folder: string, routine: Routine, replace: boolean): string {
322
+ /**
323
+ * Writes a routine file into the library: a temporary file, then a rename.
324
+ * Refuses a name that exists unless `replace`. Reports the file as a written
325
+ * file: its path relative to the project folder `root`, its size and its SHA-256.
326
+ */
327
+ export function saveRoutine(folder: string, routine: Routine, replace: boolean, root = process.cwd()): Written {
322
328
  const file = join(folder, `${routine.name}.json`);
329
+ const out = relative(root, file).split(sep).join("/");
323
330
  if (!replace && statSync(file, { throwIfNoEntry: false }) !== undefined) {
324
331
  throw new Refusal("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
325
332
  }
333
+ const bytes = Buffer.from(routineText(routine), "utf8");
326
334
  const temporary = `${file}.${process.pid}.tmp`;
327
- writeFileSync(temporary, routineText(routine));
328
- renameSync(temporary, file);
329
- return file;
335
+ try {
336
+ writeFileSync(temporary, bytes);
337
+ renameSync(temporary, file);
338
+ } catch (error) {
339
+ throw writeRefusal(error, out, "--library") ?? error;
340
+ }
341
+ return { out, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
330
342
  }
331
343
 
332
344
  /** An image to search: its segments and its bytes inside them. */
@@ -1,10 +1,11 @@
1
1
  // Evidence from one release: for each range of the image, the unpack
2
2
  // stage that wrote it and the access class from coverage; for each code
3
- // region of knowledge, whether call and jump references reach it from the
4
- // entry; and what the program reads or executes outside the image. Evidence
5
- // only: nothing here decides what is original.
3
+ // region of knowledge, whether call and jump references or the run on from
4
+ // its last instruction reach it from the entry; and what the program reads
5
+ // or executes outside the image. Evidence only: nothing here decides what is
6
+ // original.
6
7
 
7
- import { type AddressRange, COVERAGE, executedRamCode } from "#src/c64.ts";
8
+ import { type AddressRange, COVERAGE, executedRamCode, instructionLength } from "#src/c64.ts";
8
9
 
9
10
  export type Access = "executed" | "read" | "written" | "read-written" | "untouched";
10
11
 
@@ -49,6 +50,27 @@ export interface Separation {
49
50
 
50
51
  const SYSTEM_PAGES_END = 0x03ff;
51
52
 
53
+ /** BRK, JMP, RTI, RTS, JMP (indirect) and the JAM opcodes: the CPU does not run on to the next instruction after them. */
54
+ const NO_FALL_THROUGH = new Set([0x00, 0x4c, 0x40, 0x60, 0x6c, 0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
55
+
56
+ /**
57
+ * The address after a code region where the CPU runs on, or undefined when
58
+ * its last instruction does not run on (RTS, RTI, JMP, BRK, JAM). The
59
+ * instructions follow one another from the start of the region.
60
+ */
61
+ export function fallThrough(region: AddressRange, memory: Uint8Array): number | undefined {
62
+ let at = region.start;
63
+ let last = at;
64
+ while (at <= region.end) {
65
+ last = at;
66
+ at += instructionLength(memory[at]!);
67
+ }
68
+ return NO_FALL_THROUGH.has(memory[last]!) || at > 0xffff ? undefined : at;
69
+ }
70
+
71
+ /** The most instructions outside the code regions that the run on from a region goes through, for example an instruction in a dual-use region. */
72
+ const MAX_RUN_ON = 16;
73
+
52
74
  /** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
53
75
  function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): AddressRange[] {
54
76
  const ranges: AddressRange[] = [];
@@ -92,12 +114,29 @@ export function separate(input: SeparationInput): Separation {
92
114
  }
93
115
  }
94
116
 
95
- // Reachability: from the region that holds the entry, through call and jump references, to a fixed point.
117
+ // Reachability: from the region that holds the entry, through call and jump references and the run on
118
+ // from the last instruction of a region into the next address, to a fixed point.
96
119
  const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
97
120
  const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
98
121
  const reached = new Set<number>();
99
122
  if (input.entry !== undefined && holding(input.entry) >= 0) reached.add(holding(input.entry));
100
- const flows = input.references.filter((reference) => reference.kind === "call" || reference.kind === "jump");
123
+ // The CPU runs the bytes after the region as instructions: through bytes of no code region (a dual-use
124
+ // instruction, for example) into the next code region, unless one of them does not run on.
125
+ const runOn = (from: number): number | undefined => {
126
+ let at = from;
127
+ for (let count = 0; count <= MAX_RUN_ON && at <= 0xffff && inImage(at); count++) {
128
+ if (holding(at) >= 0) return at;
129
+ if (NO_FALL_THROUGH.has(input.memory[at]!)) return undefined;
130
+ at += instructionLength(input.memory[at]!);
131
+ }
132
+ return undefined;
133
+ };
134
+ const runsOn = regions.flatMap((region) => {
135
+ const next = fallThrough(region, input.memory);
136
+ const to = next === undefined ? undefined : runOn(next);
137
+ return to === undefined ? [] : [{ from: region.end, to }];
138
+ });
139
+ const flows = [...input.references.filter((reference) => reference.kind === "call" || reference.kind === "jump"), ...runsOn];
101
140
  for (let grown = true; grown; ) {
102
141
  grown = false;
103
142
  for (const flow of flows) {