@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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 (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -11,26 +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
+ | `--max-listed 20` | List at most this number of labels, functions or candidates. The default is 50. Use 0 to list none. |
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. |
34
38
 
35
39
  The script prints one JSON object. Addresses are `$` and four hex digits.
36
40
 
@@ -42,6 +46,25 @@ Both analyzers use the knowledge from the user and the LLM as seeds:
42
46
 
43
47
  Facts that an analyzer found are not seeds. A new analysis can then correct or remove them.
44
48
 
49
+ More seeds come from evidence:
50
+
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
+ - 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.
56
+
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`.
67
+
45
68
  ## Workflow
46
69
 
47
70
  1. Read the knowledge for the program with the c64-knowledge skill.
@@ -49,10 +72,16 @@ Facts that an analyzer found are not seeds. A new analysis can then correct or r
49
72
  3. Find the important routines. Give them names with the c64-knowledge skill.
50
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.
51
74
  5. Read the result:
52
- - `changes` counts the symbols, regions and references that the analysis `added`, `changed`, `retired` or kept `unchanged`.
53
- - DXA: `regions` lists the code and data regions, and `labels` lists the addresses that DXA named.
54
- - Ghidra: `functions` lists the routines that Ghidra found, and `decompilations` gives C-like code for the requested routines.
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.
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`).
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.
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`.
55
79
  - `conflicts` lists the places where the analyzer and the current knowledge disagree.
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.
56
85
  6. Give each important routine a name with the c64-knowledge skill. Then run the analysis again. The analyzers use the new names.
57
86
 
58
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.
@@ -61,12 +90,18 @@ The script records only structure: generated names, code and data regions, and r
61
90
 
62
91
  Automatic write:
63
92
  - code and data regions
64
- - 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`
65
94
  - references from Ghidra: call, jump, read and write.
66
95
 
67
- 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.
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.
99
+
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.
101
+
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.
68
103
 
69
- 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.
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.
70
105
 
71
106
  Do not store:
72
107
  - the listing or decompiled code
@@ -82,10 +117,15 @@ The exit status is 0 for a result, also with conflicts. The exit status is 1 for
82
117
 
83
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.
84
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.
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:
121
+ 1. Find the real targets with the c64-emulator skill, or in a coverage file.
122
+ 2. If the vector bytes are also code at another time, classify them as `dual-use`.
123
+ 3. Remove the false reference with `unreference`. It does not come back.
124
+ 4. Add a `jump` reference from the JMP to each real target, and name each target as a routine.
85
125
  - `invalid-input` with a message about an entry point: no entry point is inside the program for Ghidra. Give one with `--entry`.
86
126
  - `stale-revision`: the knowledge changed during the analysis. The script recorded nothing. Run the analysis again.
87
127
  - `operation-failed` with a message about an incomplete result or listing: the analyzer did not give a complete result. The script recorded nothing. Tell the user.
88
- - `installation-incomplete`: DXA or Ghidra is not installed on this machine. Tell the user the message.
128
+ - `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
89
129
  - `notDecompiled`: Ghidra did not decompile these routines. Each entry has the reason. If the reason says that the decompiler did not start, tell the user the remedy in the reason. On macOS, Ghidra has no native decompiler until the user builds it: `./gradlew buildNatives` in the `support/gradle` directory of Ghidra.
90
130
 
91
131
  ## Handoffs
@@ -1,39 +1,151 @@
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 (D83)
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
6
  import { mkdirSync, writeFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
8
  import { parseArgs } from "node:util";
9
9
 
10
- import { formatC64Address, parseC64Address } from "#src/c64.ts";
10
+ import { COVERAGE, formatC64Address, parseC64Address, prgRange } from "#src/c64.ts";
11
11
  import { WireFailure } from "#src/host-client/tools.ts";
12
- import { readProjectFile } from "#src/host-client/transfer.ts";
12
+ import { writeProjectFile } from "#src/host-client/transfer.ts";
13
13
  import { analyze as analyzeWithDxa } from "#src/native/dxa.ts";
14
14
  import { analyze as analyzeWithGhidra } from "#src/native/ghidra/analyze.ts";
15
15
  import { localToolContext } from "#src/native/local.ts";
16
+ import { GHIDRA_LIMITS } from "#src/native/types.ts";
16
17
  import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
18
+ import { analyzedBytes, programCoverage, requireProgram } from "#src/knowledge/program.ts";
17
19
  import { importFindings, runProvenance, type Analyzer, type ImportConflict, type ImportContext, type ImportResult, type NormalizedFindings } from "#src/knowledge/import.ts";
18
- import { currentRevision } from "#src/knowledge/read.ts";
20
+ import { currentRevision, listMacros, listMacroUses, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
21
+ import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
22
+ import { platformNames } from "#src/platform.ts";
19
23
  import { resolveProjectPath } from "#src/project.ts";
20
24
  import { runScript, UsageError } from "#src/script.ts";
21
- import { dxaFindings, ghidraFindings, seedsFromKnowledge } from "./findings.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";
22
32
 
23
- const USAGE = `analyze.ts <image> [options]
33
+ const USAGE = `analyze.ts [options]
34
+
35
+ The program and the coverage come from the knowledge (D83): 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.
24
38
 
25
- <image> a PRG file, relative to the project directory
26
39
  --analyzer <name> dxa (fast first pass) or ghidra (deeper, the default)
27
- --flat64k the image is 64 KiB of memory from $0000, not a PRG
28
40
  --entry <address> an entry point, for example '$080d' (repeatable)
29
41
  --decompile <address> ghidra: a routine to decompile, at most 32 (repeatable)
30
42
  --listing <file> dxa: write the disassembly listing to this project file
43
+ --instructions <file> ghidra: write every instruction (address, bytes, mnemonic,
44
+ mode, operand, target, executed) to this project file as JSON
45
+ --max-listed <n> list at most n labels, functions or candidates (default 50,
46
+ 0 for none); the counts are always given, and knowledge has
47
+ all labels and functions
48
+ --space <space> the knowledge space of the program: c64 (the default), drive8
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
31
52
 
32
53
  Routine symbols in knowledge are entry points too. Ghidra needs at least one
33
54
  entry point; dxa finds the SYS of a BASIC start by itself. The result is one
34
55
  JSON object. Addresses are $ and four hex digits.`;
35
56
 
36
- const MAX_LISTED_FUNCTIONS = 200;
57
+ const DEFAULT_MAX_LISTED = 50;
58
+
59
+ /** The first `max` items, the count, and where to read the rest when the list is cut. */
60
+ function listed<T, U>(items: T[], max: number, show: (item: T) => U, all: string) {
61
+ return { count: items.length, listed: items.slice(0, max).map(show), ...(items.length > max ? { rest: all } : {}) };
62
+ }
63
+
64
+ /** A pitfall with $ addresses and what goes wrong (M23.7). */
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 (D65). */
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 (D73). */
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 (D73) 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
+ }
37
149
 
38
150
  /** A decompiler that did not start gets the remedy: Ghidra ships no native decompiler for macOS. */
39
151
  function withRemedy(reason: string): string {
@@ -72,10 +184,10 @@ function showConflict(conflict: ImportConflict, analyzer: Analyzer): Record<stri
72
184
 
73
185
  /**
74
186
  * Imports one analyzer's findings as one revision, refused when knowledge
75
- * 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.
76
188
  * The revision records the image hash and the analyzer version.
77
189
  */
78
- function record(findings: NormalizedFindings, revision: number, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion">) {
190
+ function record(findings: NormalizedFindings, revision: string, description: string, provenance: Pick<ImportContext, "inputHash" | "toolVersion" | "evidence">) {
79
191
  let imported: ImportResult;
80
192
  try {
81
193
  imported = withWrite((db) => importFindings(db, findings, { expectedRevision: revision, description, ...provenance }));
@@ -87,7 +199,7 @@ function record(findings: NormalizedFindings, revision: number, description: str
87
199
  }
88
200
  return {
89
201
  revision: imported.revision,
90
- 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 },
91
203
  conflicts: imported.conflicts.map((conflict) => showConflict(conflict, findings.analyzer)),
92
204
  };
93
205
  }
@@ -99,44 +211,139 @@ async function run(argv: string[]): Promise<unknown> {
99
211
  allowPositionals: true,
100
212
  options: {
101
213
  analyzer: { type: "string" },
102
- flat64k: { type: "boolean" },
103
214
  entry: { type: "string", multiple: true },
104
215
  decompile: { type: "string", multiple: true },
105
216
  listing: { type: "string" },
217
+ "max-listed": { type: "string" },
218
+ instructions: { type: "string" },
219
+ space: { type: "string" },
220
+ "macro-batch": { type: "string" },
106
221
  help: { type: "boolean" },
107
222
  },
108
223
  });
109
224
  if (values.help) throw new UsageError("");
110
- if (positionals.length !== 1) throw new UsageError("give one image");
111
- 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)");
112
226
  const entries = addresses(values.entry, "--entry");
113
227
  const decompile = addresses(values.decompile, "--decompile");
114
228
  if (decompile.length > 32) throw new UsageError("--decompile takes at most 32 routines");
229
+ const maxListed = values["max-listed"] ?? String(DEFAULT_MAX_LISTED);
230
+ if (!/^\d{1,5}$/.test(maxListed)) throw new UsageError(`--max-listed must be a whole number, not ${maxListed}`);
231
+ const max = Number(maxListed);
115
232
  const analyzer = values.analyzer ?? "ghidra";
116
233
  if (analyzer !== "dxa" && analyzer !== "ghidra") throw new UsageError(`--analyzer must be dxa or ghidra, not ${analyzer}`);
117
234
  if (analyzer === "dxa" && decompile.length > 0) throw new UsageError("--decompile works only with --analyzer ghidra");
118
235
  if (analyzer === "ghidra" && values.listing !== undefined) throw new UsageError("--listing works only with --analyzer dxa");
236
+ if (analyzer === "dxa" && values.instructions !== undefined) throw new UsageError("--instructions works only with --analyzer ghidra");
119
237
  let listingPath: string | undefined;
120
238
  if (values.listing !== undefined) {
121
239
  try {
122
240
  listingPath = resolveProjectPath(values.listing);
123
241
  } catch {
124
- 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"]}`);
125
251
  }
126
252
  }
127
253
 
254
+ const space = values.space ?? DEFAULT_SPACE;
255
+ if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
128
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 (D53).
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.
263
+ const byteAt = imageReader(bytes, imageKind);
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 (D58).
266
+ const executed = coverage === undefined ? {} : { executed: (address: number) => (coverage[address]! & COVERAGE.ramExecute) !== 0 };
267
+ const loaded = imageKind === "flat64k" ? { start: 0x0000, end: 0xffff } : prgRange(bytes);
268
+ const idioms = findIdioms(byteAt, { start: loaded.start, end: loaded.end }, covered);
269
+
129
270
  const revision = currentRevision(reader);
130
- const seeds = seedsFromKnowledge(reader, entries);
271
+ const seeds = seedsFromKnowledge(reader, entries, space);
272
+ // Knowledge routines and --entry first, then code regions and JMP tables, then idioms, then coverage, up to the seed bound (D44).
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 (D74); 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])];
278
+ seeds.entryPoints = ordered.slice(0, GHIDRA_LIMITS.entryPoints).sort((a, b) => a - b);
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
+ });
131
294
  reader?.close();
132
-
133
- const imageKind = values.flat64k ? "flat64k" : "prg";
134
- const bytes = readProjectFile(image).bytes;
295
+ /** Imports the idioms as their own revision, after the analyzer's, and gives their part of the result. */
296
+ const idiomsResult = () => {
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 (D73).
299
+ const macros = macroCandidates(space, byteAt, values["macro-batch"]);
300
+ // Platform pitfalls are C64 facts: drive code and banks get none (M23.7).
301
+ const pitfallDb = openForRead();
302
+ const pitfalls = space === DEFAULT_SPACE ? findPitfalls(byteAt, listRegions(pitfallDb, undefined, "code", undefined, space)) : [];
303
+ pitfallDb?.close();
304
+ return {
305
+ idioms: {
306
+ ...idioms.counts,
307
+ revision: imported.revision,
308
+ changes: { regions: imported.regions, references: imported.references },
309
+ conflicts: imported.conflicts.map((conflict) => showConflict(conflict, "heuristic")),
310
+ // Candidates are not knowledge: a larger --max-listed gives more of them.
311
+ candidates: {
312
+ dop: listed(idioms.candidates.dop, max, formatC64Address, "a larger --max-listed"),
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
+ },
322
+ },
323
+ },
324
+ };
325
+ };
326
+ /** The coverage part of a result: its seeds, and the analyzer data that it shows to be code. */
327
+ const coverageResult = () => {
328
+ if (coverage === undefined) return {};
329
+ const db = openForRead();
330
+ const conflicts = coverageConflicts(db, coverage);
331
+ db?.close();
332
+ return {
333
+ coverageSeeds: { entryPoints: fromCoverage.length, used: coverageUsed, romOnlyRanges: fromRom.length, romOnlyUsed: romUsed },
334
+ coverageConflicts: conflicts.map(({ region, executed }) => ({
335
+ range: { start: formatC64Address(region.start), end: formatC64Address(region.end) },
336
+ knowledge: { type: region.type, origin: region.origin },
337
+ executed,
338
+ problem: "coverage shows code",
339
+ })),
340
+ };
341
+ };
135
342
  if (analyzer === "dxa") {
136
343
  const { result, attachments } = await analyzeWithDxa({ imageKind, ...seeds }, bytes, localToolContext());
137
344
  const listing = attachments[0]!.toString("utf8");
138
345
  // The listing is written only after the import: a refused import leaves no new file.
139
- const imported = record(dxaFindings(result), revision, `DXA analysis of ${image}`, runProvenance(bytes, result.toolVersion));
346
+ const imported = record({ ...dxaFindings(result), space, ...executed }, revision, "DXA analysis of the program", runProvenance(bytes, result.toolVersion));
140
347
  if (listingPath !== undefined) {
141
348
  try {
142
349
  mkdirSync(dirname(listingPath), { recursive: true });
@@ -146,25 +353,47 @@ async function run(argv: string[]): Promise<unknown> {
146
353
  throw new WireFailure("operation-failed", `The script did not write the listing to ${values.listing}: ${(error as Error).message}. ${recorded}`);
147
354
  }
148
355
  }
356
+ const found = idiomsResult();
149
357
  return {
150
358
  analyzer: "dxa",
359
+ ...(space === DEFAULT_SPACE ? {} : { space }),
360
+ ...found,
361
+ ...coverageResult(),
151
362
  coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
152
363
  ...imported,
153
364
  regions: result.regions.map((region) => ({ start: formatC64Address(region.start), end: formatC64Address(region.end), classification: region.classification })),
154
- labels: result.labels.slice(0, MAX_LISTED_FUNCTIONS).map((label) => ({ address: formatC64Address(label.address), name: label.name })),
155
- ...(result.labels.length > MAX_LISTED_FUNCTIONS ? { moreLabels: result.labels.length - MAX_LISTED_FUNCTIONS } : {}),
365
+ labels: listed(result.labels, max, (label) => ({ address: formatC64Address(label.address), name: label.name }), "c64-knowledge: knowledge.ts symbols"),
156
366
  ...(listingPath === undefined ? {} : { listing: values.listing }),
157
367
  };
158
368
  }
159
369
 
160
- const { result } = await analyzeWithGhidra({ imageKind, ...seeds, decompile }, bytes, localToolContext());
161
- const imported = record(ghidraFindings(result), revision, `Ghidra analysis of ${image}`, runProvenance(bytes, result.toolVersion));
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));
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));
381
+ let instructions: { out: string; count: number } | undefined;
382
+ if (values.instructions !== undefined) {
383
+ const list = instructionList(result.instructions, coverage);
384
+ writeProjectFile(values.instructions, Buffer.from(`${JSON.stringify(list, null, 1)}\n`), { what: "--instructions" });
385
+ instructions = { out: values.instructions, count: list.length };
386
+ }
162
387
  return {
163
388
  analyzer: "ghidra",
389
+ ...(space === DEFAULT_SPACE ? {} : { space }),
390
+ ...(instructions === undefined ? {} : { instructions }),
391
+ ...found,
392
+ ...coverageResult(),
393
+ ...(coverage === undefined ? {} : { coverageGaps: listed(gaps, max, formatC64Address, "a larger --max-listed") }),
164
394
  coverage: result.coverage.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
165
395
  ...imported,
166
- functions: result.functions.slice(0, MAX_LISTED_FUNCTIONS).map((fn) => ({ entry: formatC64Address(fn.entry), name: fn.name })),
167
- ...(result.functions.length > MAX_LISTED_FUNCTIONS ? { moreFunctions: result.functions.length - MAX_LISTED_FUNCTIONS } : {}),
396
+ functions: listed(result.functions, max, (fn) => ({ entry: formatC64Address(fn.entry), name: fn.name }), "c64-knowledge: knowledge.ts symbols --kind routine"),
168
397
  decompilations: result.decompilations.map((item) => ({ entry: formatC64Address(item.entry), text: item.text, ...(item.truncated ? { truncated: true } : {}) })),
169
398
  ...((result.notDecompiled ?? []).length === 0
170
399
  ? {}
@@ -172,4 +401,4 @@ async function run(argv: string[]): Promise<unknown> {
172
401
  };
173
402
  }
174
403
 
175
- await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
404
+ await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
@@ -0,0 +1,72 @@
1
+ // Runtime evidence for static analysis (D44): the coverage as entry points,
2
+ // user/LLM code regions and JMP tables as entry points, and the check that
3
+ // shows analyzer data over executed code.
4
+
5
+ import type { DatabaseSync } from "node:sqlite";
6
+
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";
11
+ import { listRegions, type RegionRow } from "#src/knowledge/read.ts";
12
+ import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
13
+
14
+ const FULL_MAP = FULL_COVERAGE;
15
+ const ANALYZER_ORIGINS = new Set(["dxa", "ghidra", "heuristic"]);
16
+
17
+ /** The byte at an address of the analyzed image, or undefined outside it. */
18
+ export type ImageByte = (address: number) => number | undefined;
19
+
20
+ export function imageReader(bytes: Uint8Array, kind: "prg" | "flat64k"): ImageByte {
21
+ if (kind === "flat64k") return (address) => bytes[address];
22
+ const load = bytes[0]! | (bytes[1]! << 8);
23
+ return (address) => (address >= load && address - load + 2 < bytes.length ? bytes[address - load + 2] : undefined);
24
+ }
25
+
26
+ const ROM_ACCESS = COVERAGE.romRead | COVERAGE.romExecute;
27
+ const RAM_USE = COVERAGE.ramRead | COVERAGE.ramExecute;
28
+
29
+ /**
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 (R2-07). 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.
34
+ */
35
+ export function romOnlyRanges(map: Uint8Array, image: ImageByte): Array<{ start: number; end: number }> {
36
+ const ranges: Array<{ start: number; end: number }> = [];
37
+ for (let address = 0; address < FULL_MAP; address++) {
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 });
42
+ }
43
+ return ranges;
44
+ }
45
+
46
+ /** Entry points from semantic knowledge beyond routines: the start of each code region, and each entry of a JMP table with its target. */
47
+ export function codeEntries(db: DatabaseSync | undefined, image: ImageByte, space: Space = DEFAULT_SPACE): number[] {
48
+ const entries: number[] = [];
49
+ for (const region of listRegions(db, undefined, undefined, undefined, space).filter((row) => row.origin === "user" || row.origin === "llm")) {
50
+ if (region.type === "code") entries.push(region.start);
51
+ if (region.type !== "jumps") continue;
52
+ for (let at = region.start; at + 2 <= region.end; at += 3) {
53
+ entries.push(at);
54
+ const low = image(at + 1);
55
+ const high = image(at + 2);
56
+ if (image(at) === 0x4c && low !== undefined && high !== undefined) entries.push(low | (high << 8));
57
+ }
58
+ }
59
+ return entries;
60
+ }
61
+
62
+ /** Analyzer data regions that hold executed instruction starts: the coverage shows code there. */
63
+ export function coverageConflicts(db: DatabaseSync | undefined, map: Uint8Array): Array<{ region: RegionRow; executed: number }> {
64
+ const conflicts: Array<{ region: RegionRow; executed: number }> = [];
65
+ for (const region of listRegions(db)) {
66
+ if (!ANALYZER_ORIGINS.has(region.origin) || CODE_REGION_TYPES.includes(region.type) || region.type === "dual-use") continue;
67
+ let executed = 0;
68
+ for (let address = region.start; address <= region.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) executed++;
69
+ if (executed > 0) conflicts.push({ region, executed });
70
+ }
71
+ return conflicts;
72
+ }