@henols/c64-re-tools 2.0.0-rc.12 → 2.0.0-rc.13
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.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +2 -0
- package/skills/c64-knowledge/SKILL.md +43 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +227 -16
- package/skills/c64-reverse-engineering/SKILL.md +1 -0
- package/skills/c64-source/SKILL.md +29 -5
- package/skills/c64-source/scripts/generate.ts +203 -23
- package/skills/c64-source/scripts/loader.ts +3 -1
- package/skills/c64-source/scripts/opcodes.ts +1 -1
- package/skills/c64-source/scripts/source.ts +303 -25
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +89 -2
- package/skills/c64-static-analysis/scripts/macros.ts +233 -0
- package/src/knowledge/conflicts.ts +12 -0
- package/src/knowledge/history.ts +30 -1
- package/src/knowledge/read.ts +128 -1
- package/src/knowledge/schema.ts +48 -2
- package/src/knowledge/text.ts +38 -11
- package/src/knowledge/write.ts +368 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/c64-re-tools",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.13",
|
|
4
4
|
"description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Henrik Olsson",
|
|
@@ -52,6 +52,8 @@ The script prints one JSON object. The target CPU is the 6510 and the output is
|
|
|
52
52
|
4. If `assembled` is `false`, read `diagnostics`. Each one has a severity, a file, a line and a message. Correct the source and run the script again.
|
|
53
53
|
5. To run the program, give the PRG path to the c64-emulator skill.
|
|
54
54
|
|
|
55
|
+
The source of the c64-source skill has switches. Each switch is 0 when the build does not define it. `--define CRACK=1` puts in the patches of the switch `CRACK`. With `--compare`, only the bytes of those patches are different.
|
|
56
|
+
|
|
55
57
|
## Knowledge
|
|
56
58
|
|
|
57
59
|
Automatic write:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-knowledge
|
|
3
|
-
description: Use this skill to read, write, correct and review the durable knowledge of a C64 project in .c64-re-tools/knowledge/. It gives the knowledge at an address, and it names symbols, classifies memory regions, sets comments and records references. It also shows the history of each change, and it can revert to an earlier revision. Do not use it for live emulator state or for static analysis.
|
|
3
|
+
description: Use this skill to read, write, correct and review the durable knowledge of a C64 project in .c64-re-tools/knowledge/. It gives the knowledge at an address, and it names symbols, classifies memory regions, sets comments and records references. It records patches and macros for the generated source. It also shows the history of each change, and it can revert to an earlier revision. Do not use it for live emulator state or for static analysis.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# c64-knowledge
|
|
@@ -9,7 +9,7 @@ description: Use this skill to read, write, correct and review the durable knowl
|
|
|
9
9
|
|
|
10
10
|
Read, write, correct and review the durable understanding of a C64 project. The knowledge is text in `.c64-re-tools/knowledge/` in the project folder: one JSON line for each row. Commit that folder with git. The script keeps an index of the text in `.c64-re-tools/local/knowledge.db` and makes the index again when the text changes. Do not commit the index.
|
|
11
11
|
|
|
12
|
-
This skill is the only write path for conclusions of the user and the LLM. It does not run an emulator or an analyzer.
|
|
12
|
+
This skill is the only write path for conclusions of the user and the LLM. It does not run an emulator or an analyzer. Write the knowledge only with the script. The script checks each write. Do not write or edit the files in `.c64-re-tools/knowledge/` or the index yourself.
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
@@ -47,9 +47,14 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
47
47
|
| Start a file of generated source | `module '$2000' player.asm --title 'Player movement'` / `unmodule '$2000'` |
|
|
48
48
|
| Write the operand of an instruction as an expression | `operand '$8079' --lo irq_handler --image unpacked/image.json` / `unoperand '$8079'` |
|
|
49
49
|
| Operand expressions | `operands` with `--start` and `--end` |
|
|
50
|
+
| Replace a range with code when a switch is on | `patch '$100e' '$1010' CRACK --code-file crack.asm --image unpacked/image.json` / `unpatch '$100e'` |
|
|
51
|
+
| Patches and switches | `patches` with `--switch`, and `switches` |
|
|
52
|
+
| Write a macro | `macro inc16 --params dst --body-file inc16.asm` / `unmacro inc16` |
|
|
53
|
+
| Write instructions as one use of a macro | `use '$1005' '$100a' inc16 --args ptr --image unpacked/image.json` / `unuse '$1005'` |
|
|
54
|
+
| Macros and their uses | `macros`, and `uses` with `--start`, `--end` and `--macro` |
|
|
50
55
|
| Files of generated source | `modules` |
|
|
51
56
|
| The address spaces that hold knowledge | `spaces` |
|
|
52
|
-
| History | `history '$2100'`, `history --name update_player`, `revisions`, `revision 20261015T2145` |
|
|
57
|
+
| History | `history '$2100'`, `history --name update_player`, `history --macro inc16`, `revisions`, `revision 20261015T2145` |
|
|
53
58
|
| Go back to an earlier state | `revert 20261015T2145` |
|
|
54
59
|
| Decide the conflicts after a git merge | `merge --reason 'merge of two computers'` |
|
|
55
60
|
|
|
@@ -122,14 +127,19 @@ The ops and their fields:
|
|
|
122
127
|
| `unmodule` | `start` |
|
|
123
128
|
| `operand` | `address`, `select` (`lo`, `hi`, `value` or `expr`), `symbol` and `offset` (optional), or `expression` for `expr` |
|
|
124
129
|
| `unoperand` | `address` |
|
|
130
|
+
| `patch` | `start`, `end`, `switch`, `code` (empty for NOP only), `comment` (optional) |
|
|
131
|
+
| `unpatch`, `unuse` | `start` |
|
|
132
|
+
| `macro` | `name`, `params` (a list or a comma list), `body`, `comment` (optional) |
|
|
133
|
+
| `unmacro` | `name` |
|
|
134
|
+
| `use` | `start`, `end`, `macro`, `args` (a list or a comma list) |
|
|
125
135
|
|
|
126
|
-
A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
136
|
+
A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. `apply` with an `operand`, `patch` or `use` write needs `--image`. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
127
137
|
|
|
128
138
|
The c64-provenance skill writes a batch with `match --out`. The batch holds the names of library routines at their addresses in this program. Read the `batch.leftOut` of its result first. Then apply the batch with the command in its `apply`.
|
|
129
139
|
|
|
130
140
|
## Modules
|
|
131
141
|
|
|
132
|
-
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm` and `
|
|
142
|
+
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm`, `constants.asm` and `macros.asm` itself. Thus a module cannot have one of these names, in upper or lower case. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
133
143
|
|
|
134
144
|
## Operand expressions
|
|
135
145
|
|
|
@@ -153,6 +163,34 @@ Give one selector:
|
|
|
153
163
|
|
|
154
164
|
Write an operand expression for each immediate that is a byte of an address. Also write one for each table read through the byte before the table. The pointer candidates of the c64-static-analysis skill show many of these places.
|
|
155
165
|
|
|
166
|
+
## Code switches
|
|
167
|
+
|
|
168
|
+
A patch is ACME code that replaces a range of the image when the build defines its switch as 1. Use patches to change the program, for example for a crack, a trainer or a debug hook. The default build does not define the switch. Thus the c64-source skill still makes a source that is identical to the image.
|
|
169
|
+
|
|
170
|
+
- A switch name is upper case, for example `CRACK` or `TRAINER`. It cannot be the name of a symbol or a platform name.
|
|
171
|
+
- Many patches can have the same switch. The c64-source skill builds each switch on its own. It does not build two switches together.
|
|
172
|
+
- The code must fit in the range. NOP ($EA) fills the bytes after the code. `--nop` gives a range of NOP only.
|
|
173
|
+
- `--code` gives the code as text. `--code-file` reads the code from a project file. The code can use the labels and the macros of the source, and local labels with a dot, for example `.skip`.
|
|
174
|
+
- The braces in the code must be pairs, because the source puts the code inside `{ }`.
|
|
175
|
+
- A patch cannot overlap another patch or a macro use.
|
|
176
|
+
- Give the image with `--image`. Each byte of the range must be in the image.
|
|
177
|
+
- A `patch` write without `--comment` keeps the comment that the patch has.
|
|
178
|
+
|
|
179
|
+
Build the program with a switch on with the c64-assembler skill: `--define CRACK=1`.
|
|
180
|
+
|
|
181
|
+
## Macros
|
|
182
|
+
|
|
183
|
+
A macro writes a run of instructions as one line of the source, for example `+inc16 ptr`. Use macros to make the source shorter and easier to read.
|
|
184
|
+
|
|
185
|
+
- `macro` writes the ACME macro: its name, its params and its body. `--params` gives the params as a comma list, without a dot. The body uses each param with a dot, for example `.dst`.
|
|
186
|
+
- `use` writes the instructions from the start to the end as one use of a macro. `--args` gives one expression for each param, as a comma list. Each name in an arg is a symbol of the knowledge or a platform name.
|
|
187
|
+
- The range of a use is complete instructions. Give the image with `--image`.
|
|
188
|
+
- A `rename` of a symbol changes the name in each arg. The knowledge refuses `remove-symbol` while an arg uses the symbol.
|
|
189
|
+
- The knowledge refuses `unmacro` while the macro has uses. It also refuses a change of the number of params while the macro has uses.
|
|
190
|
+
- A use cannot overlap a patch or another use.
|
|
191
|
+
|
|
192
|
+
The c64-source skill writes a use only when the use gives the bytes of the image. If not, it writes the instructions and gives the reason. The c64-static-analysis skill finds uses of known 6502 idioms and writes them to a batch with `--macro-batch`.
|
|
193
|
+
|
|
156
194
|
## Removals
|
|
157
195
|
|
|
158
196
|
`remove-symbol`, `unclassify` and `unreference` block the place for DXA and Ghidra. An analysis does not add a symbol at that address, a region in that range or that reference again. This applies to every removed fact, also to a fact from the LLM.
|
|
@@ -13,13 +13,16 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
|
|
|
13
13
|
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
14
14
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
15
15
|
import { operandText } from "#src/knowledge/expression.ts";
|
|
16
|
-
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
16
|
+
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
17
17
|
import {
|
|
18
18
|
at,
|
|
19
19
|
currentRevision,
|
|
20
20
|
listComments,
|
|
21
|
+
listMacros,
|
|
22
|
+
listMacroUses,
|
|
21
23
|
listModules,
|
|
22
24
|
listOperands,
|
|
25
|
+
listPatches,
|
|
23
26
|
listSpaces,
|
|
24
27
|
listReferences,
|
|
25
28
|
listRegions,
|
|
@@ -27,8 +30,11 @@ import {
|
|
|
27
30
|
listSymbols,
|
|
28
31
|
search,
|
|
29
32
|
type CommentRow,
|
|
33
|
+
type MacroRow,
|
|
34
|
+
type MacroUseRow,
|
|
30
35
|
type ModuleRow,
|
|
31
36
|
type OperandRow,
|
|
37
|
+
type PatchRow,
|
|
32
38
|
type ReferenceRow,
|
|
33
39
|
type RegionRow,
|
|
34
40
|
type SuppressionRow,
|
|
@@ -57,17 +63,24 @@ import {
|
|
|
57
63
|
classifyRegion,
|
|
58
64
|
mergeConflicts,
|
|
59
65
|
removeComment,
|
|
66
|
+
removeMacro,
|
|
67
|
+
removeMacroUse,
|
|
60
68
|
removeModule,
|
|
61
69
|
removeOperand,
|
|
70
|
+
removePatch,
|
|
62
71
|
removeReference,
|
|
63
72
|
removeSymbol,
|
|
64
73
|
renameSymbol,
|
|
65
74
|
revert,
|
|
66
75
|
setComment,
|
|
76
|
+
setMacro,
|
|
77
|
+
setMacroUse,
|
|
67
78
|
setModule,
|
|
68
79
|
setOperand,
|
|
80
|
+
setPatch,
|
|
69
81
|
unclassifyRegion,
|
|
70
82
|
type BatchOperation,
|
|
83
|
+
type BatchStep,
|
|
71
84
|
type Evidence,
|
|
72
85
|
type MergeChoice,
|
|
73
86
|
type OperandImage,
|
|
@@ -88,8 +101,12 @@ Read (no database needed):
|
|
|
88
101
|
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
89
102
|
modules [--start A --end A --origin O] where the files of generated source start
|
|
90
103
|
operands [--start A --end A --origin O] operand expressions of instructions
|
|
104
|
+
patches [--start A --end A --switch S --origin O] code that a switch puts in
|
|
105
|
+
switches each switch with the ranges of its patches
|
|
106
|
+
macros [--origin O] the macros of the generated source
|
|
107
|
+
uses [--start A --end A --macro M --origin O] instructions written as a macro
|
|
91
108
|
spaces the address spaces that hold knowledge
|
|
92
|
-
history <address> | history --name <name>
|
|
109
|
+
history <address> | history --name <name> | history --macro <name>
|
|
93
110
|
revisions [--limit N --before R]
|
|
94
111
|
revision <revision>
|
|
95
112
|
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
@@ -120,13 +137,25 @@ Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
|
120
137
|
it does not give the operand in the image (image.json
|
|
121
138
|
or PRG); give a negative offset as --offset=-1
|
|
122
139
|
unoperand <address>
|
|
140
|
+
patch <start> <end> <SWITCH> --code <text>|--code-file <file>|--nop [--comment T] --image <file>
|
|
141
|
+
ACME code that replaces <start>-<end> when the build
|
|
142
|
+
defines SWITCH as 1 (c64-assembler --define SWITCH=1);
|
|
143
|
+
NOP fills the rest of the range; --nop puts only NOP
|
|
144
|
+
unpatch <start>
|
|
145
|
+
macro <name> [--params a,b] --body <text>|--body-file <file> [--comment T]
|
|
146
|
+
an ACME macro; the body names a param with a dot: .a
|
|
147
|
+
unmacro <name>
|
|
148
|
+
use <start> <end> <macro> [--args "x, y"] --image <file>
|
|
149
|
+
write the instructions <start>-<end> as one use of
|
|
150
|
+
the macro, with one expression for each param
|
|
151
|
+
unuse <start>
|
|
123
152
|
revert <revision>
|
|
124
153
|
merge decide the conflicts that a git merge of two
|
|
125
154
|
computers' work left: of the rows of one conflict,
|
|
126
155
|
the newest stays; one revision for all
|
|
127
156
|
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
128
157
|
all as one revision, or none when one is refused;
|
|
129
|
-
operand writes need --image <file>
|
|
158
|
+
operand, patch and use writes need --image <file>
|
|
130
159
|
|
|
131
160
|
Write options:
|
|
132
161
|
--reason <text> why, kept with the revision
|
|
@@ -184,6 +213,28 @@ const operand = (row: OperandRow) => ({
|
|
|
184
213
|
origin: row.origin,
|
|
185
214
|
revision: row.revision,
|
|
186
215
|
});
|
|
216
|
+
const patch = (row: PatchRow) => ({
|
|
217
|
+
...spaceOf(row),
|
|
218
|
+
start: formatC64Address(row.start),
|
|
219
|
+
end: formatC64Address(row.end),
|
|
220
|
+
bytes: row.end - row.start + 1,
|
|
221
|
+
switch: row.switch,
|
|
222
|
+
code: row.code,
|
|
223
|
+
...(row.comment === undefined ? {} : { comment: row.comment }),
|
|
224
|
+
origin: row.origin,
|
|
225
|
+
revision: row.revision,
|
|
226
|
+
});
|
|
227
|
+
const macro = (row: MacroRow) => ({ ...spaceOf(row), name: row.name, params: row.params, body: row.body, ...(row.comment === undefined ? {} : { comment: row.comment }), origin: row.origin, revision: row.revision });
|
|
228
|
+
const macroUse = (row: MacroUseRow) => ({
|
|
229
|
+
...spaceOf(row),
|
|
230
|
+
start: formatC64Address(row.start),
|
|
231
|
+
end: formatC64Address(row.end),
|
|
232
|
+
macro: row.macro,
|
|
233
|
+
args: row.args,
|
|
234
|
+
text: `+${row.macro}${row.args.length === 0 ? "" : ` ${row.args.join(", ")}`}`,
|
|
235
|
+
origin: row.origin,
|
|
236
|
+
revision: row.revision,
|
|
237
|
+
});
|
|
187
238
|
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
188
239
|
function suppression(row: SuppressionRow) {
|
|
189
240
|
const place =
|
|
@@ -209,7 +260,13 @@ function historyEntry(entry: HistoryEntry) {
|
|
|
209
260
|
? module(entry.row)
|
|
210
261
|
: entry.entity === "operand"
|
|
211
262
|
? operand(entry.row)
|
|
212
|
-
:
|
|
263
|
+
: entry.entity === "patch"
|
|
264
|
+
? patch(entry.row)
|
|
265
|
+
: entry.entity === "macro"
|
|
266
|
+
? macro(entry.row)
|
|
267
|
+
: entry.entity === "macro-use"
|
|
268
|
+
? macroUse(entry.row)
|
|
269
|
+
: suppression(entry.row);
|
|
213
270
|
const { revision: _revision, ...row } = shown;
|
|
214
271
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
215
272
|
}
|
|
@@ -263,6 +320,16 @@ function run(argv: string[]): unknown {
|
|
|
263
320
|
expr: { type: "string" },
|
|
264
321
|
offset: { type: "string" },
|
|
265
322
|
image: { type: "string" },
|
|
323
|
+
code: { type: "string" },
|
|
324
|
+
"code-file": { type: "string" },
|
|
325
|
+
nop: { type: "boolean" },
|
|
326
|
+
comment: { type: "string" },
|
|
327
|
+
params: { type: "string" },
|
|
328
|
+
body: { type: "string" },
|
|
329
|
+
"body-file": { type: "string" },
|
|
330
|
+
args: { type: "string" },
|
|
331
|
+
switch: { type: "string" },
|
|
332
|
+
macro: { type: "string" },
|
|
266
333
|
help: { type: "boolean" },
|
|
267
334
|
},
|
|
268
335
|
});
|
|
@@ -272,8 +339,8 @@ function run(argv: string[]): unknown {
|
|
|
272
339
|
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
273
340
|
const space = values.space ?? DEFAULT_SPACE;
|
|
274
341
|
|
|
275
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "spaces", "history", "revisions", "revision", "export"]);
|
|
276
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "revert", "apply", "merge"]);
|
|
342
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "patches", "switches", "macros", "uses", "spaces", "history", "revisions", "revision", "export"]);
|
|
343
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "patch", "unpatch", "macro", "unmacro", "use", "unuse", "revert", "apply", "merge"]);
|
|
277
344
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
278
345
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
279
346
|
if (read.has(command)) {
|
|
@@ -299,17 +366,41 @@ function run(argv: string[]): unknown {
|
|
|
299
366
|
for (const option of ["lo", "hi", "value", "expr", "offset"] as const) {
|
|
300
367
|
if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
|
|
301
368
|
}
|
|
302
|
-
|
|
369
|
+
const only: Array<[keyof typeof values, readonly string[]]> = [
|
|
370
|
+
["image", ["operand", "patch", "use", "apply"]],
|
|
371
|
+
["code", ["patch"]],
|
|
372
|
+
["code-file", ["patch"]],
|
|
373
|
+
["nop", ["patch"]],
|
|
374
|
+
["comment", ["patch", "macro"]],
|
|
375
|
+
["params", ["macro"]],
|
|
376
|
+
["body", ["macro"]],
|
|
377
|
+
["body-file", ["macro"]],
|
|
378
|
+
["args", ["use"]],
|
|
379
|
+
];
|
|
380
|
+
for (const [option, commands] of only) {
|
|
381
|
+
if (values[option] !== undefined && !commands.includes(command)) throw new UsageError(`--${option} works only with ${commands.join(", ")}`);
|
|
382
|
+
}
|
|
303
383
|
const operation =
|
|
304
384
|
command === "apply"
|
|
305
385
|
? applyOperation(args, context, space, values.image)
|
|
306
386
|
: command === "operand"
|
|
307
387
|
? operandOperation(args, values, context, space)
|
|
308
|
-
:
|
|
388
|
+
: command === "patch" || command === "macro" || command === "use"
|
|
389
|
+
? sourceOperation(command, args, values, context, space)
|
|
390
|
+
: writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
309
391
|
return withWrite(operation);
|
|
310
392
|
}
|
|
311
393
|
|
|
312
|
-
type ReadValues = {
|
|
394
|
+
type ReadValues = {
|
|
395
|
+
kind?: string | undefined;
|
|
396
|
+
type?: string | undefined;
|
|
397
|
+
name?: string | undefined;
|
|
398
|
+
limit?: string | undefined;
|
|
399
|
+
before?: string | undefined;
|
|
400
|
+
out?: string | undefined;
|
|
401
|
+
switch?: string | undefined;
|
|
402
|
+
macro?: string | undefined;
|
|
403
|
+
};
|
|
313
404
|
|
|
314
405
|
/** The answer of one read command. */
|
|
315
406
|
function readAnswer(db: DatabaseSync | undefined, command: string, args: string[], values: ReadValues, range: { start: number; end: number }, origins: Origin[] | undefined, space: string): Record<string, unknown> {
|
|
@@ -325,6 +416,8 @@ function readAnswer(db: DatabaseSync | undefined, command: string, args: string[
|
|
|
325
416
|
referencesFrom: known.referencesFrom.map(reference),
|
|
326
417
|
referencesTo: known.referencesTo.map(reference),
|
|
327
418
|
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
419
|
+
...(known.patch === undefined ? {} : { patch: patch(known.patch) }),
|
|
420
|
+
...(known.macroUse === undefined ? {} : { macroUse: macroUse(known.macroUse) }),
|
|
328
421
|
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
329
422
|
revision: known.revision,
|
|
330
423
|
};
|
|
@@ -353,13 +446,16 @@ function readAnswer(db: DatabaseSync | undefined, command: string, args: string[
|
|
|
353
446
|
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
354
447
|
modules: listModules(db, range, origins, space).map(module),
|
|
355
448
|
operands: listOperands(db, range, origins, space).map(operand),
|
|
449
|
+
patches: listPatches(db, range, undefined, origins, space).map(patch),
|
|
450
|
+
macros: listMacros(db, origins, space).map(macro),
|
|
451
|
+
macroUses: listMacroUses(db, range, undefined, origins, space).map(macroUse),
|
|
356
452
|
};
|
|
357
453
|
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
358
454
|
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
359
455
|
return {
|
|
360
456
|
out: values.out,
|
|
361
457
|
bytes: bytes.length,
|
|
362
|
-
counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length, operands: exported.operands.length },
|
|
458
|
+
counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length, operands: exported.operands.length, patches: exported.patches.length, macros: exported.macros.length, macroUses: exported.macroUses.length },
|
|
363
459
|
revision: exported.revision,
|
|
364
460
|
};
|
|
365
461
|
}
|
|
@@ -372,12 +468,25 @@ function readAnswer(db: DatabaseSync | undefined, command: string, args: string[
|
|
|
372
468
|
return { modules: listModules(db, range, origins, space).map(module) };
|
|
373
469
|
case "operands":
|
|
374
470
|
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
471
|
+
case "patches":
|
|
472
|
+
return { patches: listPatches(db, range, values.switch, origins, space).map(patch) };
|
|
473
|
+
case "switches": {
|
|
474
|
+
const switches = new Map<string, PatchRow[]>();
|
|
475
|
+
for (const row of listPatches(db, undefined, undefined, undefined, space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
|
|
476
|
+
return {
|
|
477
|
+
switches: [...switches].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, rows]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) })),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
case "macros":
|
|
481
|
+
return { macros: listMacros(db, origins, space).map(macro) };
|
|
482
|
+
case "uses":
|
|
483
|
+
return { uses: listMacroUses(db, range, values.macro, origins, space).map(macroUse) };
|
|
375
484
|
case "spaces":
|
|
376
485
|
return { spaces: listSpaces(db) };
|
|
377
486
|
case "history":
|
|
378
487
|
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
379
488
|
return {
|
|
380
|
-
history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
489
|
+
history: (values.macro !== undefined ? historyOfMacro(db, values.macro, space) : values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
381
490
|
};
|
|
382
491
|
case "revisions": {
|
|
383
492
|
const options: { limit?: number; before?: string } = {};
|
|
@@ -415,11 +524,12 @@ function evidenceOf(value: string): Evidence {
|
|
|
415
524
|
}
|
|
416
525
|
|
|
417
526
|
/**
|
|
418
|
-
* The image that operand writes are checked against (D65
|
|
419
|
-
* names. A byte outside the segments of the image
|
|
527
|
+
* The image that operand, patch and macro use writes are checked against (D65,
|
|
528
|
+
* D72, D73), with the platform names. A byte outside the segments of the image
|
|
529
|
+
* is not in the image.
|
|
420
530
|
*/
|
|
421
531
|
function operandImage(path: string | undefined, what: string): OperandImage {
|
|
422
|
-
if (path === undefined) throw new UsageError(`${what} checks
|
|
532
|
+
if (path === undefined) throw new UsageError(`${what} checks the write against the image: give --image <image.json|prg>`);
|
|
423
533
|
let image: ReturnType<typeof readImage>;
|
|
424
534
|
try {
|
|
425
535
|
image = readImage(path);
|
|
@@ -460,6 +570,57 @@ function operandOperation(args: string[], values: Record<string, unknown>, conte
|
|
|
460
570
|
};
|
|
461
571
|
}
|
|
462
572
|
|
|
573
|
+
/** A comma list of params or args; an empty text is an empty list. */
|
|
574
|
+
const commaList = (value: string) => (value.trim() === "" ? [] : value.split(",").map((item) => item.trim()));
|
|
575
|
+
|
|
576
|
+
/** The text of an option, or of the project file that a --*-file option names; exactly one of them. */
|
|
577
|
+
function textOrFile(values: Record<string, unknown>, option: string, what: string): string | undefined {
|
|
578
|
+
const inline = values[option] as string | undefined;
|
|
579
|
+
const file = values[`${option}-file`] as string | undefined;
|
|
580
|
+
if (inline !== undefined && file !== undefined) throw new UsageError(`give --${option} or --${option}-file, not both`);
|
|
581
|
+
if (file !== undefined) return readProjectFile(file).bytes.toString("utf8");
|
|
582
|
+
if (inline === undefined) return undefined;
|
|
583
|
+
if (inline === "") throw new UsageError(`${what} is empty`);
|
|
584
|
+
return inline;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** patch, macro and use: the writes of the generated source (D72, D73). */
|
|
588
|
+
function sourceOperation(command: "patch" | "macro" | "use", args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
589
|
+
const comment = values.comment as string | undefined;
|
|
590
|
+
switch (command) {
|
|
591
|
+
case "patch": {
|
|
592
|
+
if (args[2] === undefined) throw new UsageError("the switch name is missing");
|
|
593
|
+
const code = textOrFile(values, "code", "--code");
|
|
594
|
+
if ((code === undefined) === (values.nop !== true)) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
595
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), switch: args[2], code: code ?? "", ...(comment === undefined ? {} : { comment }) };
|
|
596
|
+
const image = operandImage(values.image as string | undefined, "patch");
|
|
597
|
+
return (db) => {
|
|
598
|
+
const result = setPatch(db, { ...context, image }, request);
|
|
599
|
+
return { revision: result.revision, patch: patch(result.current), ...(result.previous === undefined ? {} : { previous: patch(result.previous) }) };
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
case "macro": {
|
|
603
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
604
|
+
const body = textOrFile(values, "body", "--body");
|
|
605
|
+
if (body === undefined) throw new UsageError("macro needs --body or --body-file");
|
|
606
|
+
const request = { space, name: args[0], params: commaList((values.params as string | undefined) ?? ""), body, ...(comment === undefined ? {} : { comment }) };
|
|
607
|
+
return (db) => {
|
|
608
|
+
const result = setMacro(db, context, request);
|
|
609
|
+
return { revision: result.revision, macro: macro(result.current), ...(result.previous === undefined ? {} : { previous: macro(result.previous) }) };
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
case "use": {
|
|
613
|
+
if (args[2] === undefined) throw new UsageError("the macro name is missing");
|
|
614
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), macro: args[2], args: commaList((values.args as string | undefined) ?? "") };
|
|
615
|
+
const image = operandImage(values.image as string | undefined, "use");
|
|
616
|
+
return (db) => {
|
|
617
|
+
const result = setMacroUse(db, { ...context, image }, request);
|
|
618
|
+
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
463
624
|
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
464
625
|
function applyOperation(args: string[], context: WriteContext, space: string, imagePath: string | undefined): WriteOperation {
|
|
465
626
|
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
@@ -472,19 +633,30 @@ function applyOperation(args: string[], context: WriteContext, space: string, im
|
|
|
472
633
|
}
|
|
473
634
|
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
474
635
|
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
475
|
-
const
|
|
636
|
+
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
637
|
+
const withImage = checked ? { ...context, image: operandImage(imagePath, "apply with operand, patch or use writes") } : context;
|
|
476
638
|
return (db) => {
|
|
477
639
|
const applied = applyBatch(db, withImage, operations);
|
|
478
640
|
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
479
641
|
const replaced = applied.steps.flatMap((step, index) =>
|
|
480
642
|
step.previous === undefined
|
|
481
643
|
? []
|
|
482
|
-
: [{ write: index + 1, op: operations[index]!.op, previous:
|
|
644
|
+
: [{ write: index + 1, op: operations[index]!.op, previous: shownPrevious(step.previous) }],
|
|
483
645
|
);
|
|
484
646
|
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
485
647
|
};
|
|
486
648
|
}
|
|
487
649
|
|
|
650
|
+
/** A row that a write of a batch replaced, in its output shape. */
|
|
651
|
+
function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
|
|
652
|
+
if (Array.isArray(row)) return row.map(region);
|
|
653
|
+
if ("select" in row) return operand(row);
|
|
654
|
+
if ("switch" in row) return patch(row);
|
|
655
|
+
if ("body" in row) return macro(row);
|
|
656
|
+
if ("macro" in row) return macroUse(row);
|
|
657
|
+
return symbol(row);
|
|
658
|
+
}
|
|
659
|
+
|
|
488
660
|
const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
489
661
|
rename: ["address", "name", "kind"],
|
|
490
662
|
"remove-symbol": ["address"],
|
|
@@ -498,6 +670,12 @@ const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
|
498
670
|
unmodule: ["start"],
|
|
499
671
|
operand: ["address", "select", "symbol", "offset", "expression"],
|
|
500
672
|
unoperand: ["address"],
|
|
673
|
+
patch: ["start", "end", "switch", "code", "comment"],
|
|
674
|
+
unpatch: ["start"],
|
|
675
|
+
macro: ["name", "params", "body", "comment"],
|
|
676
|
+
unmacro: ["name"],
|
|
677
|
+
use: ["start", "end", "macro", "args"],
|
|
678
|
+
unuse: ["start"],
|
|
501
679
|
};
|
|
502
680
|
|
|
503
681
|
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
@@ -520,6 +698,15 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
520
698
|
return fields[key] as string;
|
|
521
699
|
};
|
|
522
700
|
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
701
|
+
// A list is a JSON list of strings or a comma list.
|
|
702
|
+
const list = (key: string) => {
|
|
703
|
+
const value = fields[key];
|
|
704
|
+
if (value === undefined) return [];
|
|
705
|
+
if (typeof value === "string") return commaList(value);
|
|
706
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new UsageError(`${at}: ${key} must be a list of strings or a comma list`);
|
|
707
|
+
return (value as string[]).map((item) => item.trim());
|
|
708
|
+
};
|
|
709
|
+
const optional = (key: string) => (fields[key] === undefined ? {} : { [key]: text(key) });
|
|
523
710
|
switch (op) {
|
|
524
711
|
case "rename":
|
|
525
712
|
return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
|
|
@@ -552,6 +739,17 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
552
739
|
}
|
|
553
740
|
case "unoperand":
|
|
554
741
|
return { op, address: place("address") };
|
|
742
|
+
case "patch":
|
|
743
|
+
return { op, start: place("start"), end: place("end"), switch: text("switch"), code: fields.code === undefined ? "" : text("code"), ...optional("comment") };
|
|
744
|
+
case "macro":
|
|
745
|
+
return { op, name: text("name"), params: list("params"), body: text("body"), ...optional("comment") };
|
|
746
|
+
case "unmacro":
|
|
747
|
+
return { op, name: text("name") };
|
|
748
|
+
case "use":
|
|
749
|
+
return { op, start: place("start"), end: place("end"), macro: text("macro"), args: list("args") };
|
|
750
|
+
case "unpatch":
|
|
751
|
+
case "unuse":
|
|
752
|
+
return { op, start: place("start") };
|
|
555
753
|
case "reference":
|
|
556
754
|
case "unreference":
|
|
557
755
|
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
@@ -631,6 +829,19 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
631
829
|
const request = { space, address: address(args[0]) };
|
|
632
830
|
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
633
831
|
}
|
|
832
|
+
case "unpatch": {
|
|
833
|
+
const request = { space, start: address(args[0], "start") };
|
|
834
|
+
return (db) => ({ revision: removePatch(db, context, request).revision });
|
|
835
|
+
}
|
|
836
|
+
case "unmacro": {
|
|
837
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
838
|
+
const request = { space, name: args[0] };
|
|
839
|
+
return (db) => ({ revision: removeMacro(db, context, request).revision });
|
|
840
|
+
}
|
|
841
|
+
case "unuse": {
|
|
842
|
+
const request = { space, start: address(args[0], "start") };
|
|
843
|
+
return (db) => ({ revision: removeMacroUse(db, context, request).revision });
|
|
844
|
+
}
|
|
634
845
|
case "merge": {
|
|
635
846
|
if (args.length > 0) throw new UsageError("merge takes no arguments");
|
|
636
847
|
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
@@ -33,6 +33,7 @@ One repository can hold several programs, with one project folder for each progr
|
|
|
33
33
|
- From the root of the repository, give `--project <folder>` to each skill script, and `project` to each emulator tool with a path.
|
|
34
34
|
- Write all files in the project folder. Do not write files at the root of the repository.
|
|
35
35
|
- Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
|
|
36
|
+
- Write the knowledge only with the c64-knowledge script. Do not write or edit the files in `.c64-re-tools/knowledge/` yourself.
|
|
36
37
|
|
|
37
38
|
## Workflow
|
|
38
39
|
|