@henols/c64-re-tools 2.0.0-rc.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +98 -115
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +43 -8
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/src/trace.ts +55 -23
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -11,30 +11,30 @@ Find the static structure of a C64 program. Record the structure in the project
11
11
 
12
12
  ## Inputs
13
13
 
14
- - The program: a PRG file, relative to the project directory. Or 64 KiB of memory from `$0000`, for example a memory dump.
14
+ - The program in the knowledge. Put it in with `image` of the c64-knowledge skill: a PRG, or a capture of the c64-unpacker skill.
15
+ - Optional: the coverage in the knowledge. Add it with `coverage` of the c64-knowledge skill.
15
16
  - Entry points: addresses where code starts, for example the SYS address from the c64-basic skill. Routine symbols in the knowledge are entry points too. Ghidra needs at least one. DXA finds the SYS of a BASIC start by itself.
16
17
  - Optional: routines to decompile (Ghidra), or a file for the listing (DXA).
17
18
 
18
19
  ## Tools / execution path
19
20
 
20
- The script reads the current knowledge and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in `.c64-re-tools/knowledge.db`.
21
+ The script reads the program, the coverage and the current knowledge, and runs DXA or Ghidra on this machine. The host runtime is not necessary. The script records the result in the project knowledge, `.c64-re-tools/knowledge/`.
21
22
 
22
23
  ```
23
- node <skill>/scripts/analyze.ts extracted/game.prg --analyzer dxa --listing analysis/game.lst
24
- node <skill>/scripts/analyze.ts extracted/game.prg --entry '$080d' --decompile '$080d'
24
+ node <skill>/scripts/analyze.ts --analyzer dxa --listing .c64-re-tools/local/game.lst
25
+ node <skill>/scripts/analyze.ts --entry '$080d' --decompile '$080d'
25
26
  ```
26
27
 
27
28
  | Option | Meaning |
28
29
  |---|---|
29
30
  | `--analyzer dxa` | Use DXA: a fast first pass. Without this option, the script uses Ghidra. |
30
31
  | `--entry '$080d'` | An address where code starts. Use it again for more addresses. |
31
- | `--listing analysis/game.lst` | DXA only: write the disassembly listing to this project file. |
32
+ | `--listing .c64-re-tools/local/game.lst` | DXA only: write the disassembly listing to this project file. |
32
33
  | `--decompile '$0818'` | Ghidra only: decompile the routine at this address. Use it again for more routines, at most 32. |
33
- | `--flat64k` | The file is 64 KiB of memory from `$0000`, not a PRG. |
34
34
  | `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
35
- | `--instructions analysis/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. With `--coverage`, `executed` tells if it ran. |
36
- | `--space drive8` | The knowledge space of the image: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
37
- | `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` (action `read` with `out`). Use it again for more runs. The executed code becomes entry points. |
35
+ | `--instructions .c64-re-tools/local/instructions.json` | Ghidra only: write every instruction to this project file as JSON. Each has `address`, `bytes`, `mnemonic`, `mode` and `operand` in 6502 syntax. A branch, jump or call also has its `target`. When the knowledge has coverage, `executed` tells if it ran. |
36
+ | `--space drive8` | The knowledge space of the program: `drive8` for code from the 1541 drive, or `bank0` to `bank255` for a cartridge bank. The default is `c64`. The seeds come from that space, and the findings go into it. |
37
+ | `--macro-batch .c64-re-tools/local/macros.json` | Write the macro candidates to this project file as a batch for `apply` of the c64-knowledge skill. |
38
38
 
39
39
  The script prints one JSON object. Addresses are `$` and four hex digits.
40
40
 
@@ -49,9 +49,21 @@ Facts that an analyzer found are not seeds. A new analysis can then correct or r
49
49
  More seeds come from evidence:
50
50
 
51
51
  - The start of each `code` region and each entry of a `jumps` table from the user or the LLM is an entry point.
52
- - With `--coverage`, each executed instruction that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP or BRK. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches.
52
+ - When the knowledge has coverage, each executed instruction of the program that starts a flow is an entry point. A flow starts after a gap, or after RTS, RTI, JMP, BRK or JSR. Then the analyzers find code that only a vector, an RTS table or an interrupt reaches. They also find the code after a JSR to a routine that an analyzer thinks does not return.
53
+ - For Ghidra, a flow start is a routine only when no executed branch or JMP from another flow gets to it. Also, no JSR can be immediately before it. Another flow start is code in a routine, and Ghidra does not make a function of it.
54
+ - When the knowledge has coverage, Ghidra gives `coverageGaps`: the executed instruction starts in the program that are not in its instructions. Normally the count is 0. Each start in the list is missing from `--instructions` and from the source of c64-source. Examine the code at the start. Overlapped instructions and self-modified code can cause a gap. A start in a `dual-use` region is a known overlap, for example a BIT skip, and is not in the list.
55
+ - When the knowledge has coverage, some RAM under a ROM is data for the analysis. At these addresses, the runs used the ROM and did not read or execute the RAM. Then a call into the KERNAL does not decode the RAM bytes under it as code. A knowledge routine or an `--entry` in such a range keeps its code. Ghidra does not follow BRK, because the vector at `$fffe` is in ROM unless the program removes the KERNAL.
53
56
 
54
- The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used.
57
+ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` come first, then code regions and tables, then coverage. Ghidra also takes at most 8192 flow starts that are code in a routine. `coverageSeeds` gives how many entry points the coverage gave and how many the analysis used. `romOnlyRanges` and `romOnlyUsed` give the same counts for the data ranges under ROM.
58
+
59
+ ### Project folder
60
+
61
+ - Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
62
+ - If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
63
+ - Write each file that you make for your work in `.c64-re-tools/local/` of the project. Git ignores that folder. Examples are results, batches, step files, runs, snapshots and memory dumps.
64
+ - Move a file out of `.c64-re-tools/local/` only when it is the evidence for a finding. Move it before you give it to `--evidence` of c64-knowledge. `--evidence` refuses a file in `.c64-re-tools/local/`.
65
+ - Put a run that is evidence in `runs/<name>/`, with its run file, its results and the snapshot files that it uses. Change the paths in the run file to the new paths. Then git keeps them, and another person can do the run again.
66
+ - A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
55
67
 
56
68
  ## Workflow
57
69
 
@@ -60,13 +72,16 @@ The analyzers take at most 1024 entry points. Knowledge routines and `--entry` c
60
72
  3. Find the important routines. Give them names with the c64-knowledge skill.
61
73
  4. Run a deeper pass with Ghidra. Give the entry points with `--entry`. Ask for decompiled code of the routines that you must understand.
62
74
  5. Read the result:
63
- - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
75
+ - `changes` counts the analyzer names (`names`), regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`. `symbols.retired` counts the old symbols of the analyzer that moved to its analyzer names. `suppressed` counts the findings that the analysis did not write, because a removal blocks that place.
64
76
  - DXA: `regions` lists the code and data regions. `labels` gives the number of names that DXA found (`count`) and lists the first names (`listed`).
65
77
  - Ghidra: `functions` gives the number of routines that Ghidra found (`count`) and lists the first routines (`listed`). `decompilations` gives C-like code for the requested routines.
66
78
  - If a list is not complete, `rest` gives the c64-knowledge command that lists all of them. For `candidates`, `rest` tells you to give a larger `--max-listed`.
67
79
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
68
- - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
69
- - With `--coverage`, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data.
80
+ - `idioms` tells what the script found by pattern: RTS dispatch tables (`rtsTables`), JMP tables (`jumpTables`) and BIT skips (`bitSkips`). It also finds the `CBM80` header of a cartridge bank at `$8000` (`cartridgeHeaders`). The two start vectors of the header become entry points. The script writes them as knowledge with the origin `heuristic`, and seeds their targets into the same analysis. `candidates` gives DOP padding (`dop`) and opcodes that the program wrote and executed (`selfModified`). For `selfModified`, the write and the execution must be in one coverage file. A coverage file does not record the order. Thus a candidate can also be code that ran first, and that the program then wrote over. To prevent this, save one coverage file for each part of the program, for example the intro and the game. A loader that copies relocated code is also such a part. Clear the memory map between the parts. Each list has a `count`. The script does not write the candidates. Look at them and decide.
81
+ - `candidates.pointers` gives code that makes an address from immediates. A `vector` or a `zero-page pointer` candidate stores a low byte (`low`) and a high byte (`high`) to a vector or to a zero-page pair that `(zp),y` uses. Its `target` is in the program or has a platform name. A `sprite pointer` candidate stores an immediate (`at`) to a sprite pointer of the screen at $0400 in VIC bank 0. Its `target` (the value × 64) is in a `sprite` region. For each candidate that you confirm, write operand expressions with `operand` of the c64-knowledge skill, for example `operand '<low>' --lo <target name>` and `operand '<high>' --hi <target name>`. Then a rebuild can move the target.
82
+ - `candidates.pitfalls` gives code that a C64 runs in a different way than it reads. `raster-bit-8`: the instruction at `at` reads $D011, and the instruction at `store` writes the value back. A read of $D011 gives bit 8 of the raster line in bit 7. Thus the write can move the raster interrupt to line 256 or later, and on PAL past line 311 the interrupt never comes. Examine each candidate. Where the code must not keep bit 7, add a comment and test the program.
83
+ - `candidates.macros` gives runs of instructions that a macro writes as one line, for example `+inc16 ptr`. The script knows these 6502 idioms: `inc16`, `dec16`, `add16_8`, `add16`, `sub16`, `set16`, `mov16` and `wait_line`. It reads the code regions after the analysis. An arg has the name of a `user` or `llm` symbol or a platform name. Each candidate has `macro`, `start`, `end`, `args` and `text`. `known` counts the uses that the knowledge has. `leftOut` counts the candidates with a label inside, or with a patch or a use over them. The c64-source skill writes such a candidate as instructions. With `--macro-batch`, `batch` gives the file, the count of writes and the `apply` command. The batch holds the macros that the knowledge does not have, and the uses. When the knowledge has a macro with the same name and another body, `otherMacros` tells that the batch does not have its uses. The script does not write the candidates. Apply the batch, then generate the source again.
84
+ - When the knowledge has coverage, a code finding replaces the DXA or Ghidra data where a run executed an instruction start. `coverageConflicts` lists the analyzer data regions where the coverage still shows executed code. Classify them as code with the c64-knowledge skill, or as `dual-use` when the bytes are also data. For many conflicts, write all classifications in one `apply` batch of the c64-knowledge skill.
70
85
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
71
86
 
72
87
  The script records only structure: generated names, code and data regions, and references from Ghidra. The listing and the decompiled code are only for the current task. The script does not record them in the knowledge.
@@ -75,14 +90,16 @@ The script records only structure: generated names, code and data regions, and r
75
90
 
76
91
  Automatic write:
77
92
  - code and data regions
78
- - names that DXA generated (labels) and routine entry points with names that Ghidra generated
93
+ - analyzer names: the labels that DXA generated and the functions that Ghidra found, for example `l17e2` and `FUN_17e2`
79
94
  - references from Ghidra: call, jump, read and write.
80
95
 
81
- The script never replaces a name or a region from the user or the LLM. A name that an analyzer only shows back stays with its owner. DXA and Ghidra do not replace the facts of each other, with one exception. With `--coverage`, a code finding replaces a DXA or Ghidra data region or data name where a run executed an instruction start. The revision keeps the coverage files as evidence. Thus run DXA first, then Ghidra with all coverage files.
96
+ An analyzer name is a second name of the address. It is not a symbol. Each analyzer keeps its own name at an address. The generated source does not use an analyzer name, unless you give `--analyzer-names` to the c64-source skill. To use a name in the source, give it to the address with `rename` of the c64-knowledge skill. A Ghidra function that only branches and jumps get to is a `label`, not a `routine`.
97
+
98
+ The script never replaces a name or a region from the user or the LLM. DXA and Ghidra do not replace the facts of each other, with one exception. When the knowledge has coverage, a code finding replaces a DXA or Ghidra data region where a run executed an instruction start. Thus add all coverage maps to the knowledge first. Then run DXA, then Ghidra.
82
99
 
83
100
  A `heuristic` region replaces the part of a DXA or Ghidra region where it lies, because an idiom is specific evidence about those bytes. DXA and Ghidra never replace a `heuristic` region. If they find code there, the result shows a conflict. A heuristic table can be too long or too short, because the script cannot know its length. Correct it with the c64-knowledge skill. Your region replaces it, and a removal holds.
84
101
 
85
- A new analysis replaces the earlier facts of the same analyzer in the analyzed range. The history keeps the earlier facts. DXA labels stay until a person or the LLM removes them, because DXA names only the addresses that it sees used.
102
+ A new analysis replaces the earlier facts of the same analyzer in the analyzed range, also its analyzer names. The history keeps the earlier facts. Knowledge from an older version can have analyzer names as symbols. A new analysis moves the names of that analyzer to its analyzer names.
86
103
 
87
104
  A fact that the user or the LLM removes does not come back. The next analysis does not write a fact at that place, and counts it as `suppressed`. To remove a wrong fact, use the c64-knowledge skill.
88
105
 
@@ -100,7 +117,7 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
100
117
 
101
118
  - A conflict: the analyzer finds code where the knowledge has data, or data where the knowledge has code. The knowledge stays unchanged. Find the correct answer with the c64-emulator skill, then correct the knowledge with the c64-knowledge skill.
102
119
  - A conflict with the problem "another address has this name": the knowledge has the generated name at a different address. The knowledge stays unchanged.
103
- - A false reference through a vector: the code jumps through a vector, for example `JMP ($0408)`, and the program writes the vector at run time. The analyzer follows the bytes that the image has there. Do these steps:
120
+ - A false reference through a vector: the code jumps through a vector, for example `JMP ($0408)`, and the program writes the vector at run time. The analyzer follows the bytes that the program has there. Do these steps:
104
121
  1. Find the real targets with the c64-emulator skill, or in a coverage file.
105
122
  2. If the vector bytes are also code at another time, classify them as `dual-use`.
106
123
  3. Remove the false reference with `unreference`. It does not come back.
@@ -1,49 +1,54 @@
1
- // The c64-static-analysis script: analyzes a project program with DXA (fast
2
- // first pass) or Ghidra (deeper), which it runs itself, seeded from
3
- // current knowledge, and imports the structural findings into
4
- // .c64-re-tools/knowledge.db.
1
+ // The c64-static-analysis script: analyzes the program in the knowledge
2
+ // with DXA (fast first pass) or Ghidra (deeper), which it runs itself, seeded
3
+ // from current knowledge and the coverage of the knowledge, and imports the
4
+ // structural findings into .c64-re-tools/knowledge/.
5
5
 
6
- import { createHash } from "node:crypto";
7
6
  import { mkdirSync, writeFileSync } from "node:fs";
8
7
  import { dirname } from "node:path";
9
8
  import { parseArgs } from "node:util";
10
9
 
11
10
  import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
12
- import { joinCoverage } from "#src/host-client/coverage.ts";
13
11
  import { WireFailure } from "#src/host-client/tools.ts";
14
- import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
12
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
15
13
  import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
16
14
  import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
17
15
  import { localToolContext } from "#src/native/local.ts";
18
16
  import { GHIDRA_LIMITS } from "#src/native/types.ts";
19
17
  import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
18
+ import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/program.ts";
20
19
  import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
21
- import { currentRevision } from "#src/knowledge/read.ts";
20
+ import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
22
21
  import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
22
+ import { platformNames } from "#src/platform.ts";
23
23
  import { resolveProjectPath } from "#src/project.ts";
24
24
  import { runScript, UsageError } from "#src/script.ts";
25
- import { codeEntries, coverageConflicts, coverageEntries, imageReader, readCoverageMaps } from "./coverage.ts";
26
- import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.ts";
27
- import { findIdioms } from "./idioms.ts";
28
- import { instructionList } from "./instructions.ts";
25
+ import { codeEntries, coverageConflicts, coverageEntries, coverageSeedKinds, executedStarts, imageReader, romOnlyRanges, type ImageByte } from "./lib/coverage.ts";
26
+ import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./lib/findings.ts";
27
+ import { findIdioms } from "./lib/idioms.ts";
28
+ import { instructionList } from "./lib/instructions.ts";
29
+ import { CATALOGUE, findMacroUses, type MacroCandidate } from "./lib/macros.ts";
30
+ import { findPitfalls, type Pitfall } from "./lib/pitfalls.ts";
31
+ import { findPointers, type PointerCandidate } from "./lib/pointers.ts";
29
32
 
30
- const USAGE = `analyze.ts <image> [options]
33
+ const USAGE = `analyze.ts [options]
34
+
35
+ The program and the coverage come from the knowledge: put them in with
36
+ knowledge.ts image and coverage of the c64-knowledge skill. The executed code of
37
+ the coverage is seeded, and analyzer data over it is reported.
31
38
 
32
- <image> a PRG file, relative to the project directory
33
39
  --analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
34
- --flat64k the image is 64 KiB of memory from $0000, not a PRG
35
40
  --entry <address> an entry point, for example '$080d' (repeatable)
36
41
  --decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
37
42
  --listing <file> dxa: write the disassembly listing to this project file
38
43
  --instructions <file> ghidra: write every instruction (address, bytes, mnemonic,
39
44
  mode, operand, target, executed) to this project file as JSON
40
- --coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): its
41
- executed code is seeded, and analyzer data over it is reported
42
45
  --max-listed <n> list at most n labels, functions or candidates (default 50,
43
46
  0 for none); the counts are always given, and knowledge has
44
47
  all labels and functions
45
- --space <space> the knowledge space of the image: c64 (the default), drive8
48
+ --space <space> the knowledge space of the program: c64 (the default), drive8
46
49
  for 1541 drive code, or bank0 to bank255 for a cartridge bank
50
+ --macro-batch <file> write the macro candidates as an apply batch of c64-knowledge
51
+ (the macros and their uses) to this project file
47
52
 
48
53
  Routine symbols in knowledge are entry points too. Ghidra needs at least one
49
54
  entry point; dxa finds the SYS of a BASIC start by itself. The result is one
@@ -56,6 +61,92 @@ function listed<T, U>(items: T[], max: number, show: (item: T) => U, all: string
56
61
  return { count: items.length, listed: items.slice(0, max).map(show), ...(items.length > max ? { rest: all } : {}) };
57
62
  }
58
63
 
64
+ /** A pitfall with $ addresses and what goes wrong. */
65
+ function showPitfall(pitfall: Pitfall) {
66
+ return {
67
+ kind: pitfall.kind,
68
+ at: formatC64Address(pitfall.at),
69
+ store: formatC64Address(pitfall.store),
70
+ problem: "A read of $D011 gives raster bit 8 in bit 7; the write sets bit 8 of the raster compare. Clear bit 7 (AND #$7F) unless the code wants a compare at line 256 or later.",
71
+ };
72
+ }
73
+
74
+ /** A pointer candidate with $ addresses, and the operand writes that would name it. */
75
+ function showPointer(candidate: PointerCandidate) {
76
+ const hex = formatC64Address;
77
+ if (candidate.kind === "sprite pointer") {
78
+ return { kind: candidate.kind, at: hex(candidate.at), to: hex(candidate.to), value: `$${candidate.value.toString(16).padStart(2, "0")}`, target: hex(candidate.target), assumes: "VIC bank 0 and the screen at $0400" };
79
+ }
80
+ return { kind: candidate.kind, to: hex(candidate.to), target: hex(candidate.target), low: hex(candidate.low), high: hex(candidate.high) };
81
+ }
82
+
83
+ /** A macro candidate with $ addresses and its source line. */
84
+ function showMacro(candidate: MacroCandidate) {
85
+ return { macro: candidate.macro, start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), args: candidate.args, text: `+${candidate.macro} ${candidate.args.join(", ")}` };
86
+ }
87
+
88
+ /**
89
+ * The macro candidates in the current code regions of every origin,
90
+ * with the names of the knowledge in their args. A candidate is left out when
91
+ * a name or a reference target is inside it, or a patch or a use overlaps it:
92
+ * c64-source would write its instructions. The same use in the knowledge is
93
+ * known. With `batchPath`, the candidates go to an apply batch with the
94
+ * macros they need; a macro of the catalogue that the knowledge has with
95
+ * another body leaves its candidates out of the batch.
96
+ */
97
+ function macroCandidates(space: string, byteAt: ImageByte, batchPath: string | undefined) {
98
+ const db = openForRead();
99
+ try {
100
+ const platform = space === DEFAULT_SPACE ? new Map(platformNames().map((entry) => [entry.address, entry.name])) : new Map<number, string>();
101
+ const semantic = new Map(listSymbols(db, undefined, undefined, ["user", "llm"], space).map((symbol) => [symbol.address, symbol.name]));
102
+ const code = listRegions(db, undefined, "code", undefined, space);
103
+ const found = findMacroUses(byteAt, code, (address) => semantic.get(address) ?? platform.get(address));
104
+ const named = new Set([...listSymbols(db, undefined, undefined, undefined, space).map((symbol) => symbol.address), ...listReferences(db, undefined, undefined, undefined, space).map((reference) => reference.to)]);
105
+ const patches = listPatches(db, undefined, undefined, undefined, space);
106
+ const uses = listMacroUses(db, undefined, undefined, undefined, space);
107
+ const overlaps = (a: { start: number; end: number }, b: { start: number; end: number }) => a.start <= b.end && b.start <= a.end;
108
+ const candidates: MacroCandidate[] = [];
109
+ let known = 0;
110
+ let leftOut = 0;
111
+ for (const candidate of found) {
112
+ const same = uses.find((use) => use.start === candidate.start && use.end === candidate.end && use.macro === candidate.macro && use.args.join(",") === candidate.args.join(","));
113
+ if (same !== undefined) known++;
114
+ else if ([...named].some((address) => address > candidate.start && address <= candidate.end) || patches.some((patch) => overlaps(patch, candidate)) || uses.some((use) => overlaps(use, candidate))) leftOut++;
115
+ else candidates.push(candidate);
116
+ }
117
+ if (batchPath === undefined) return { candidates, known, leftOut };
118
+ // A macro of the catalogue goes into the batch unless the knowledge has it; with another body, its uses stay out.
119
+ const macros = new Map(listMacros(db, undefined, space).map((macro) => [macro.name, macro]));
120
+ const batch: Array<Record<string, unknown>> = [];
121
+ const batched: MacroCandidate[] = [];
122
+ const other = new Set<string>();
123
+ for (const idiom of CATALOGUE.filter((item) => candidates.some((candidate) => candidate.macro === item.name))) {
124
+ const existing = macros.get(idiom.name);
125
+ if (existing === undefined) batch.push({ op: "macro", name: idiom.name, params: idiom.params, body: idiom.body, comment: idiom.comment, ...(space === DEFAULT_SPACE ? {} : { space }) });
126
+ else if (existing.params.join(",") !== idiom.params.join(",") || existing.body !== idiom.body) other.add(idiom.name);
127
+ }
128
+ for (const candidate of candidates) {
129
+ if (other.has(candidate.macro)) continue;
130
+ batched.push(candidate);
131
+ batch.push({ op: "use", start: formatC64Address(candidate.start), end: formatC64Address(candidate.end), macro: candidate.macro, args: candidate.args, ...(space === DEFAULT_SPACE ? {} : { space }) });
132
+ }
133
+ if (batch.length > 0) writeProjectFile(batchPath, Buffer.from(`${JSON.stringify(batch, null, 1)}\n`), { what: "--macro-batch" });
134
+ return {
135
+ candidates,
136
+ known,
137
+ leftOut,
138
+ batch: {
139
+ out: batchPath,
140
+ writes: batch.length,
141
+ ...(other.size === 0 ? {} : { otherMacros: [...other].map((name) => `The knowledge has another macro ${name}: its candidates are not in the batch.`) }),
142
+ ...(batch.length === 0 ? {} : { apply: `knowledge.ts apply ${batchPath} --reason "macro uses of 6502 idioms"` }),
143
+ },
144
+ };
145
+ } finally {
146
+ db?.close();
147
+ }
148
+ }
149
+
59
150
  /** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
60
151
  function withRemedy(reason: string): string {
61
152
  return reason.startsWith("the Ghidra decompiler did not start")
@@ -93,10 +184,10 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
93
184
 
94
185
  /**
95
186
  * Imports one analyzer's findings as one revision, refused when knowledge
96
- * changed since the seeds were read. A refused import creates no knowledge.db.
187
+ * changed since the seeds were read. A refused import creates no knowledge.
97
188
  * The revision records the image hash and the analyzer version.
98
189
  */
99
- function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
190
+ function record(findings: NormalizedFindings, revision: string, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
100
191
  let imported: ImportResult;
101
192
  try {
102
193
  imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
@@ -108,7 +199,7 @@ function record(findings: NormalizedFindings, revision: number, description: str
108
199
  }
109
200
  return {
110
201
  revision: imported.revision,
111
- changes: { symbols: imported.symbols, regions: imported.regions, references: imported.references },
202
+ changes: { names: imported.names, ...(imported.symbols.retired === 0 ? {} : { symbols: { retired: imported.symbols.retired } }), regions: imported.regions, references: imported.references },
112
203
  conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
113
204
  };
114
205
  }
@@ -120,20 +211,18 @@ async function run(argv: string[]): Promise<unknown> {
120
211
  allowPositionals: true,
121
212
  options: {
122
213
  analyzer: { type: "string" },
123
- flat64k: { type: "boolean" },
124
214
  entry: { type: "string", multiple: true },
125
215
  decompile: { type: "string", multiple: true },
126
216
  listing: { type: "string" },
127
217
  "max-listed": { type: "string" },
128
- coverage: { type: "string", multiple: true },
129
218
  instructions: { type: "string" },
130
219
  space: { type: "string" },
220
+ "macro-batch": { type: "string" },
131
221
  help: { type: "boolean" },
132
222
  },
133
223
  });
134
224
  if (values.help) throw new UsageError("");
135
- if (positionals.length !== 1) throw new UsageError("give one image");
136
- const image = positionals[0]!;
225
+ if (positionals.length !== 0) throw new UsageError("analyze takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
137
226
  const entries = addresses(values.entry, "--entry");
138
227
  const decompile = addresses(values.decompile, "--decompile");
139
228
  if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
@@ -150,45 +239,68 @@ async function run(argv: string[]): Promise<unknown> {
150
239
  try {
151
240
  listingPath = resolveProjectPath(values.listing);
152
241
  } catch {
153
- throw new UsageError(`--listing must be a path relative to the project directory that stays inside it, not ${values.listing}`);
242
+ throw new UsageError(`--listing must be a path relative to the project folder that stays inside it, not ${values.listing}`);
243
+ }
244
+ }
245
+
246
+ if (values["macro-batch"] !== undefined) {
247
+ try {
248
+ resolveProjectPath(values["macro-batch"]);
249
+ } catch {
250
+ throw new UsageError(`--macro-batch must be a path relative to the project folder that stays inside it, not ${values["macro-batch"]}`);
154
251
  }
155
252
  }
156
253
 
157
254
  const space = values.space ?? DEFAULT_SPACE;
158
255
  if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
159
- // A coverage map is of the C64 CPU: VICE keeps none for the drive (D53).
160
- if (space !== DEFAULT_SPACE && values.coverage !== undefined) throw new UsageError("--coverage works only in space c64");
161
- const imageKind = values.flat64k ? "flat64k" : "prg";
162
- const bytes = readProjectFile(image).bytes;
163
- const runs = readCoverageMaps(values.coverage ?? []);
164
- const coverage = runs.length === 0 ? undefined : joinCoverage(runs);
256
+ const reader = openForRead();
257
+ const program = requireProgram(reader, space);
258
+ // A coverage map is of the C64 CPU: VICE keeps none for the drive.
259
+ const covered = space === DEFAULT_SPACE ? programCoverage(reader) : undefined;
260
+ const coverage = covered?.map;
261
+ const { bytes, kind: imageKind } = analyzedBytes(program);
262
+ // The analyzer sees all bytes it gets; the coverage seeds only the program, as c64-source does.
165
263
  const byteAt = imageReader(bytes, imageKind);
166
- // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer (D58), and the files are the evidence.
264
+ const inProgram: ImageByte = (address) => (program.segments.some((segment) => address >= segment.start && address <= segment.end) ? program.byteAt(address) : undefined);
265
+ // With coverage, a code finding over an executed instruction start replaces the data of the other analyzer.
167
266
  const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
168
- const coverageEvidence = coverage === undefined ? {} : { evidence: (values.coverage ?? []).map((path) => ({ kind: "coverage" as const, ref: path, sha256: createHash("sha256").update(readProjectFile(path).bytes).digest("hex") })) };
169
-
170
- let loaded: { start: number; end: number };
171
- try {
172
- loaded = imageKind === "flat64k" ? { start: 0x0000, end: bytes.length - 1 } : prgRange(bytes);
173
- } catch (error) {
174
- throw new UsageError(`${image}: ${(error as Error).message}`);
175
- }
176
- const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, runs);
267
+ const loaded = imageKind === "flat64k" ? { start: 0x0000, end: 0xffff } : prgRange(bytes);
268
+ const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, covered);
177
269
 
178
- const reader = openForRead();
179
270
  const revision = currentRevision(reader);
180
271
  const seeds = seedsFromKnowledge(reader, entries, space);
181
- // Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound (D44).
182
- const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, byteAt);
183
- const ordered = [...new Set([...seeds.entryPoints, ...codeEntries(reader, byteAt, space), ...idioms.entryPoints, ...fromCoverage])];
272
+ // Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound.
273
+ const fromCoverage = coverage === undefined ? [] : coverageEntries(coverage, inProgram);
274
+ const known = [...seeds.entryPoints, ...codeEntries(reader, byteAt, space)];
275
+ // Ghidra takes the coverage as code starts without a function; DXA has one list.
276
+ const kinds = analyzer === "dxa" || coverage === undefined ? { routines: fromCoverage, codeStarts: [] } : coverageSeedKinds(coverage, inProgram);
277
+ const ordered = [...new Set([...known, ...idioms.entryPoints, ...kinds.routines])];
184
278
  seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
185
- // Idiom tables are data for this run too; semantic data ranges come first.
186
- seeds.dataRanges = [...seeds.dataRanges, ...idioms.dataRanges.filter((range) => !seeds.dataRanges.some((seed) => range.start <= seed.end && range.end >= seed.start))].slice(0, GHIDRA_LIMITS.dataRanges);
187
- const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry)).length;
279
+ const codeStarts = kinds.codeStarts.filter((entry) => !seeds.entryPoints.includes(entry)).slice(0, GHIDRA_LIMITS.codeStarts);
280
+ // Idiom tables and the RAM under ROM that the runs never used are data for this run too; semantic data ranges come first.
281
+ // A range with an entry point of the knowledge or --entry in it is left out: that code stays code.
282
+ const fromRom = coverage === undefined ? [] : romOnlyRanges(coverage, byteAt).filter((range) => !known.some((entry) => entry >= range.start && entry <= range.end));
283
+ const dataRanges = [...seeds.dataRanges];
284
+ for (const range of [...idioms.dataRanges, ...fromRom]) if (!dataRanges.some((seed) => range.start <= seed.end && range.end >= seed.start)) dataRanges.push(range);
285
+ seeds.dataRanges = dataRanges.slice(0, GHIDRA_LIMITS.dataRanges);
286
+ const romUsed = fromRom.filter((range) => seeds.dataRanges.includes(range)).length;
287
+ const coverageUsed = fromCoverage.filter((entry) => seeds.entryPoints.includes(entry) || codeStarts.includes(entry)).length;
288
+ // Platform names are C64 names: drive code and banks get none.
289
+ const platform = space === DEFAULT_SPACE ? platformNames() : [];
290
+ const pointers = findPointers(byteAt, { start: loaded.start, end: loaded.end }, {
291
+ platform: (address) => platform.some((entry) => address >= entry.address && address <= entry.end),
292
+ sprites: listRegions(reader, undefined, "sprite", undefined, space),
293
+ });
188
294
  reader?.close();
189
295
  /** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
190
296
  const idiomsResult = () => {
191
- const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: `Idioms in ${image}` }));
297
+ const imported = withWrite((db) => importFindings(db, { ...idioms.findings, space }, { description: "Idioms in the program" }));
298
+ // The macro candidates read the code regions after both imports.
299
+ const macros = macroCandidates(space, byteAt, values["macro-batch"]);
300
+ // Platform pitfalls are C64 facts: drive code and banks get none.
301
+ const pitfallDb = openForRead();
302
+ const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, listRegions(pitfallDb, undefined, "code", undefined, space)) : [];
303
+ pitfallDb?.close();
192
304
  return {
193
305
  idioms: {
194
306
  ...idioms.counts,
@@ -199,6 +311,14 @@ async function run(argv: string[]): Promise<unknown> {
199
311
  candidates: {
200
312
  dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
201
313
  selfModified: listed(idioms.candidates.selfModified, max, formatC64Address, "a larger --max-listed"),
314
+ pointers: listed(pointers, max, showPointer, "a larger --max-listed"),
315
+ pitfalls: listed(pitfalls, max, showPitfall, "a larger --max-listed"),
316
+ macros: {
317
+ ...listed(macros.candidates, max, showMacro, "a larger --max-listed, or --macro-batch <file>"),
318
+ ...(macros.known === 0 ? {} : { known: macros.known }),
319
+ ...(macros.leftOut === 0 ? {} : { leftOut: macros.leftOut }),
320
+ ...(macros.batch === undefined ? {} : { batch: macros.batch }),
321
+ },
202
322
  },
203
323
  },
204
324
  };
@@ -210,7 +330,7 @@ async function run(argv: string[]): Promise<unknown> {
210
330
  const conflicts = coverageConflicts(db, coverage);
211
331
  db?.close();
212
332
  return {
213
- coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed },
333
+ coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed, romOnlyRanges: fromRom.length, romOnlyUsed: romUsed },
214
334
  coverageConflicts: conflicts.map(({ region, executed }) => ({
215
335
  range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
216
336
  knowledge: { type: region.type, origin: region.origin },
@@ -223,7 +343,7 @@ async function run(argv: string[]): Promise<unknown> {
223
343
  const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
224
344
  const listing = attachments[0]!.toString("utf8");
225
345
  // The listing is written only after the import: a refused import leaves no new file.
226
- const imported = record({ ...dxaFindings(result), space, ...executed }, revision, `DXA analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
346
+ const imported = record({ ...dxaFindings(result), space, ...executed }, revision, "DXA analysis of the program", runProvenance(bytes, result.toolVersion));
227
347
  if (listingPath !== undefined) {
228
348
  try {
229
349
  mkdirSync(dirname(listingPath), { recursive: true });
@@ -247,9 +367,17 @@ async function run(argv: string[]): Promise<unknown> {
247
367
  };
248
368
  }
249
369
 
250
- const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
251
- const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, `Ghidra analysis of ${image}`, { ...runProvenance(bytes, result.toolVersion), ...coverageEvidence });
370
+ const { result } = await analyzeWithGhidra({ imageKind, ...seeds, codeStarts, decompile }, bytes, localToolContext());
371
+ // Executed instruction starts that Ghidra did not decode: the instruction list and the source miss them.
372
+ const decoded = new Set(result.instructions.map((instruction) => instruction.address));
373
+ const imported = record({ ...ghidraFindings(result), space, ...executed }, revision, "Ghidra analysis of the program", runProvenance(bytes, result.toolVersion));
252
374
  const found = idiomsResult();
375
+ // A start inside a dual-use region is a known overlap, for example the second instruction of a BIT skip: one list cannot hold both.
376
+ // Read after the idioms, which write those regions.
377
+ const overlapDb = coverage === undefined ? undefined : openForRead();
378
+ const overlaps = listRegions(overlapDb, undefined, "dual-use", undefined, space);
379
+ overlapDb?.close();
380
+ const gaps = coverage === undefined ? [] : executedStarts(coverage, inProgram).filter((address) => address >= loaded.start && address <= loaded.end && !decoded.has(address) && !overlaps.some((region) => address >= region.start && address <= region.end));
253
381
  let instructions: { out: string; count: number } | undefined;
254
382
  if (values.instructions !== undefined) {
255
383
  const list = instructionList(result.instructions, coverage);
@@ -262,6 +390,7 @@ async function run(argv: string[]): Promise<unknown> {
262
390
  ...(instructions === undefined ? {} : { instructions }),
263
391
  ...found,
264
392
  ...coverageResult(),
393
+ ...(coverage === undefined ? {} : { coverageGaps: listed(gaps, max, formatC64Address, "a larger --max-listed") }),
265
394
  coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
266
395
  ...imported,
267
396
  functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
@@ -272,4 +401,4 @@ async function run(argv: string[]): Promise<unknown> {
272
401
  };
273
402
  }
274
403
 
275
- await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
404
+ await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
@@ -1,27 +1,19 @@
1
- // Runtime evidence for static analysis (D44): coverage files as entry points,
1
+ // Runtime evidence for static analysis: the coverage as entry points,
2
2
  // user/LLM code regions and JMP tables as entry points, and the check that
3
3
  // shows analyzer data over executed code.
4
4
 
5
5
  import type { DatabaseSync } from "node:sqlite";
6
6
 
7
- import { COVERAGE, instructionLength } from "#src/c64.ts";
8
- import { FULL_COVERAGE, readCoverageMaps as readMaps } from "#src/host-client/coverage.ts";
7
+ import { COVERAGE, coverageEntries, coverageSeedKinds, executedStarts } from "#src/c64.ts";
8
+
9
+ export { coverageEntries, coverageSeedKinds, executedStarts };
10
+ import { FULL_COVERAGE } from "#src/host-client/coverage.ts";
9
11
  import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
10
12
  import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
11
- import { UsageError } from "#src/script.ts";
12
13
 
13
14
  const FULL_MAP = FULL_COVERAGE;
14
- /** Instructions after which the next instruction is not reached by falling through. */
15
- const FLOW_ENDS = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
16
15
  const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
17
16
 
18
- /** Reads coverage files, one map per file (a run). */
19
- export function readCoverageMaps(paths: string[]): Uint8Array[] {
20
- return readMaps(paths, (message) => {
21
- throw new UsageError(message);
22
- });
23
- }
24
-
25
17
  /** The byte at an address of the analyzed image, or undefined outside it. */
26
18
  export type ImageByte = (address: number) => number | undefined;
27
19
 
@@ -31,22 +23,24 @@ export function imageReader(bytes: Uint8Array, kind: "prg" | "flat64k"): ImageBy
31
23
  return (address) => (address >= load && address - load + 2 < bytes.length ? bytes[address - load + 2] : undefined);
32
24
  }
33
25
 
26
+ const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
27
+ const RAM_USE = COVERAGE.ramRead | COVERAGE.ramExecute;
28
+
34
29
  /**
35
- * The executed instruction starts in RAM that begin a flow: the first after
36
- * a gap, or the first after an instruction that does not fall through. The
37
- * analyzers follow the rest of a flow themselves.
30
+ * The image bytes under ROM that the runs never used: the CPU read or ran the
31
+ * ROM there, and never read or ran the RAM. A JSR $FFE4 or an IRQ
32
+ * vector at $FFFE then leads into ROM, not into the leftover RAM bytes of a
33
+ * flat image, so the ranges are data for the analysis run.
38
34
  */
39
- export function coverageEntries(map: Uint8Array, image: ImageByte): number[] {
40
- const entries: number[] = [];
41
- let falls: number | undefined;
35
+ export function romOnlyRanges(map: Uint8Array, image: ImageByte): Array<{ start: number; end: number }> {
36
+ const ranges: Array<{ start: number; end: number }> = [];
42
37
  for (let address = 0; address < FULL_MAP; address++) {
43
- if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
44
- const opcode = image(address);
45
- if (opcode === undefined) continue;
46
- if (falls !== address) entries.push(address);
47
- falls = FLOW_ENDS.has(opcode) ? undefined : address + instructionLength(opcode);
38
+ if ((map[address]! & ROM_ACCESS) === 0 || (map[address]! & RAM_USE) !== 0 || image(address) === undefined) continue;
39
+ const last = ranges.at(-1);
40
+ if (last !== undefined && last.end === address - 1) last.end = address;
41
+ else ranges.push({ start: address, end: address });
48
42
  }
49
- return entries;
43
+ return ranges;
50
44
  }
51
45
 
52
46
  /** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
@@ -25,7 +25,7 @@ export function seedsFromKnowledge(db: DatabaseSync | undefined, extraEntryPoint
25
25
  return {
26
26
  entryPoints,
27
27
  dataRanges: listRegions(db, undefined, undefined, undefined, space)
28
- // A JMP table is code and dual-use bytes are both: neither is a data range (D42).
28
+ // A JMP table is code and dual-use bytes are both: neither is a data range.
29
29
  .filter((region) => semantic(region) && !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
30
30
  .map((region) => ({ start: region.start, end: region.end })),
31
31
  labels: symbols.map((symbol) => ({ address: symbol.address, name: symbol.name })),
@@ -46,11 +46,15 @@ export function ghidraFindings(result: GhidraResult): NormalizedFindings {
46
46
  for (const fn of result.functions) uses.set(fn.name, (uses.get(fn.name) ?? 0) + 1);
47
47
  const nameOf = (fn: GhidraResult["functions"][number]) =>
48
48
  fn.nameSource !== "seed" && uses.get(fn.name)! > 1 ? `${fn.name}_${fn.entry.toString(16).padStart(4, "0")}` : fn.name;
49
+ // A function that branches and jumps reach, and no call, is a place in a routine: a label.
50
+ const called = new Set(result.references.filter((reference) => reference.type === "call").map((reference) => reference.to));
51
+ const jumped = new Set(result.references.filter((reference) => reference.type === "jump").map((reference) => reference.to));
52
+ const kindOf = (fn: GhidraResult["functions"][number]) => (jumped.has(fn.entry) && !called.has(fn.entry) ? ("label" as const) : ("routine" as const));
49
53
  return {
50
54
  analyzer: "ghidra",
51
55
  coverage: result.coverage,
52
56
  authoritative: { symbols: result.completeness.functions, regions: result.completeness.regions, references: result.completeness.references },
53
- symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind: "routine" as const })),
57
+ symbols: result.functions.map((fn) => ({ address: fn.entry, name: nameOf(fn), kind: kindOf(fn) })),
54
58
  regions: result.regions.map((region) => ({ start: region.start, end: region.end, type: region.classification === "code" ? ("code" as const) : ("bytes" as const) })),
55
59
  references: result.references.map((reference) => ({ from: reference.from, to: reference.to, kind: reference.type })),
56
60
  };