@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henols/c64-re-tools",
3
- "version": "2.0.0-rc.12",
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 `constants.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.
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
- : suppression(entry.row);
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
- if (values.image !== undefined && command !== "operand" && command !== "apply") throw new UsageError("--image works only with operand and apply");
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
- : writeOperation(command, args, values.kind, values.element, context, values.title, space);
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 = { kind?: string | undefined; type?: string | undefined; name?: string | undefined; limit?: string | undefined; before?: string | undefined; out?: string | undefined };
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), with the platform
419
- * names. A byte outside the segments of the image is no operand.
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 each operand expression against the image: give --image <image.json|prg>`);
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 withImage = operations.some((operation) => operation.op === "operand") ? { ...context, image: operandImage(imagePath, "apply with operand writes") } : context;
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: Array.isArray(step.previous) ? step.previous.map(region) : "select" in step.previous ? operand(step.previous) : symbol(step.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