@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.
@@ -7,7 +7,7 @@ description: Use this skill to generate ACME source code for a C64 program from
7
7
 
8
8
  ## Purpose
9
9
 
10
- Generate ACME source from an image and the knowledge. The same knowledge gives the same source. Make the source better through the knowledge: names, comments, regions and modules. Then generate again.
10
+ Generate ACME source from an image and the knowledge. The same knowledge gives the same source. Make the source better through the knowledge: names, comments, regions, modules and macros. Change the program through patches. Then generate again.
11
11
 
12
12
  This skill does not change the knowledge.
13
13
 
@@ -58,7 +58,8 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
58
58
 
59
59
  - `main.asm` sets the address of each segment and includes the files in address order.
60
60
  - Each module in the knowledge is one file. A module starts at its address and stops at the next module or at the end of its segment. Without modules, each segment is one file, `segment_xxxx.asm`.
61
- - `constants.asm` holds the names of addresses outside the image, for example `EXTCOL = $d020`. The file puts the names in groups by memory area, for example zero page, VIC-II registers or CIA 1 registers. Each group is in address order. Drive code and banks have one group only. The side comment of a name tells what the address is. This text is the knowledge comment of the address. Without a comment, the text is the platform meaning. When the knowledge name is not the platform name, the platform name comes first. For an I/O mirror without a meaning, the text gives the register that the mirror repeats. Then `Used:` counts how the source uses the name. The kinds are `read`, `written`, `modified`, `called`, `jumped to`, `jumped through`, `branched to`, `pointer`, `address` and `table`. `address` is an immediate operand expression. `table` is an entry in a table of addresses.
61
+ - `constants.asm` holds the names of addresses outside the image, for example `EXTCOL = $d020`. The file puts the names in groups by memory area, for example zero page, VIC-II registers or CIA 1 registers. Each group is in address order. Drive code and banks have one group only. The side comment of a name tells what the address is. This text is the knowledge comment of the address. Without a comment, the text is the platform meaning. When the knowledge name is not the platform name, the platform name comes first. For an I/O mirror without a meaning, the text gives the register that the mirror repeats. Then `Used:` counts how the source uses the name. The kinds are `read`, `written`, `modified`, `called`, `jumped to`, `jumped through`, `branched to`, `pointer`, `address`, `table` and `macro argument`. `address` is an immediate operand expression. `table` is an entry in a table of addresses.
62
+ - The top of `constants.asm` sets each switch to 0, for example `!ifndef CRACK { CRACK = 0 }`. A build that defines the switch as 1 uses the patches of that switch.
62
63
  - A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. A platform name wins over a name from an analyzer (`dxa`, `ghidra` or `heuristic`), for example `EXTCOL` over `ld020`. A `user` or `llm` name wins over a platform name. An address in the image without a name gets `l_xxxx`.
63
64
  - Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
64
65
  - Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
@@ -70,9 +71,24 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
70
71
  - `!byte` with an instruction as its comment: ACME writes this instruction with a different opcode. The bytes keep the source identical.
71
72
  - `not decoded` marks bytes in a code region that Ghidra did not decode.
72
73
  - `; not executed $xxxx-$yyyy` starts code that no run of the `--coverage` files executed. The range is the same as in `not_executed.json`.
74
+ - `+inc16 ptr` is a use of a macro of the knowledge. `macros.asm` holds the macros. `main.asm` and `loader.asm` include it. The comments inside a use go to its side comment.
75
+ - `!zone patch_CRACK_100e` starts a patch of the knowledge. `!if CRACK` has the code of the patch and NOP to the end of the range. `else` has the original code. The labels inside the range come before the zone as `name = * + n`. Thus both builds have them.
73
76
 
74
77
  Do not edit the generated files. The next run writes over them.
75
78
 
79
+ ## Switches and macros
80
+
81
+ A switch changes the program only when the build defines it. Thus the source without a define stays identical to the image.
82
+
83
+ 1. The script checks the default build with `check`.
84
+ 2. Then it builds each switch on its own, with the switch as 1. It does not build two switches together.
85
+ 3. The build of a switch must have the same length as the default build. It can change bytes only in the ranges of its patches.
86
+ 4. Build the program with a switch on with the c64-assembler skill: `--define CRACK=1`.
87
+
88
+ The script writes a patch only when its range is complete items of one file. A patch cannot cut an instruction. Its switch name cannot be a name of the source.
89
+
90
+ The script writes a macro use only when the use gives the bytes of the image. First it assembles each use alone at its address. Then it checks the full build. A use that fails stays instructions, and `macrosAsInstructions` gives the reason. A label inside the range of a use also keeps the instructions, because the macro line has no place for the label.
91
+
76
92
  ## The loader
77
93
 
78
94
  `--loader` writes `loader.asm` and assembles it to `loader.prg`. The loader does these steps:
@@ -93,7 +109,7 @@ The script simulates the copies first. Then it runs `loader.prg` in the emulator
93
109
  ## Knowledge
94
110
 
95
111
  Read:
96
- - symbols, regions, comments, references, modules and operand expressions of all origins.
112
+ - symbols, regions, comments, references, modules, operand expressions, patches, macros and macro uses of all origins.
97
113
 
98
114
  Automatic write:
99
115
  - none.
@@ -111,14 +127,18 @@ One JSON object:
111
127
  - `entriesInData`: each `--entry` in a data region of the knowledge, with the `region` and its `origin`. Ghidra does not decode an entry in a data region.
112
128
  - `asBytes`: the instructions that the source has as bytes.
113
129
  - `operandExpressions`: the count of instructions that the source has with an operand expression.
114
- - `operandsAsNumbers`: the operand expressions that the source has as a number with a side comment, each with its `address`, `expression` and `reason`. An expression becomes a number when no instruction with an operand value is at the address, or when a name has no address. An expression that ACME refuses or does not assemble to the image also becomes a number. Then the script assembles the source again, at most three times.
130
+ - `operandsAsNumbers`: the operand expressions that the source has as a number with a side comment, each with its `address`, `expression` and `reason`. An expression becomes a number when no instruction with an operand value is at the address, or when a name has no address. An expression that ACME refuses or does not assemble to the image also becomes a number. Then the script assembles the source again, at most six times.
115
131
  - `unusedModules`: modules in the knowledge whose start is in no segment of the image. Often this tells that the image does not hold code that the knowledge describes.
116
132
  - `entryOutside`: the entry of `image.json` is in no segment of the image, with a `reason`. The image does not hold the program.
117
133
  - `notExecuted`: with `--coverage`, the count of instructions that no run executed, the count of their ranges, and the first ranges. `all` names the file `not_executed.json` in `--out`. It has all of the ranges. Put all of these ranges in the report.
134
+ - `macroUses`: the count of macro uses that the source has.
135
+ - `macrosAsInstructions`: the macro uses that the source has as instructions, each with its `start`, `end`, `macro` and `reason`.
136
+ - `patchesNotWritten`: the patches that the source does not have, each with its range, `switch` and `reason`.
118
137
  - `check`: `identical`, and the result for each segment. A different segment gives its first different address.
138
+ - `switches`: the build of each switch. Each item has the `define` for the c64-assembler skill, the patches and the `result` of the build with the switch on. `fits` tells that the build is good, and `changedBytes` counts the changed bytes. `overflow` tells that the code of a patch is longer than its range. `changes outside` gives the first byte outside the ranges that changed. `does not assemble` gives the diagnostics of ACME. `insideNames` lists the labels inside a patch. Code that writes to one of these labels changes the patch when the switch is on.
119
139
  - `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. `limits` lists the CIA values that the loader cannot set. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
120
140
 
121
- The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check, for an entry in no segment, and for an error. The exit status is 2 for a wrong argument.
141
+ The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check, for an entry in no segment, and for an error. A patch that the source does not have, or a switch that does not fit, also gives 1. The exit status is 2 for a wrong argument.
122
142
 
123
143
  ## Failure and conflicts
124
144
 
@@ -126,6 +146,10 @@ The exit status is 0 when the source is identical and the emulator check of the
126
146
  - `check.identical: false`: the script has a defect. Give the result to the user.
127
147
  - `operandsAsNumbers`: read each `reason`. Correct the expression with `operand` of the c64-knowledge skill, or remove it with `unoperand`.
128
148
  - `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
149
+ - `switches` with `overflow`: make the code of the patch shorter, or make the range of the patch larger, with `patch` of the c64-knowledge skill.
150
+ - `switches` with `does not assemble`: read the diagnostics and correct the code of the patch.
151
+ - `patchesNotWritten`: read each `reason`. Move the start or the end of the patch to an instruction, or give the switch another name.
152
+ - `macrosAsInstructions`: read each `reason`. Correct the macro or the args, or remove the use with `unuse`. The source stays identical, so this is not a failure.
129
153
  - `entryOutside`: the run fails, but the script still writes and checks the source. With `--loader`, `loader.verified` is `false` with the same `reason`. Capture the image again with c64-unpacker `loads` or `capture`. Make sure that a segment holds the entry.
130
154
  - `entriesInData`: an analyzer region of data hides the code at the entry. If the entry is code, record a code region at the entry with the c64-knowledge skill, for example `classify '$0500' '$05ff' code`. A region of the user or the LLM replaces the analyzer region. Then generate again.
131
155
  - `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
@@ -2,8 +2,10 @@
2
2
  // knowledge and Ghidra's instructions, which it runs itself, and checks with
3
3
  // ACME that the source assembles to the image byte by byte. With --loader it
4
4
  // also writes a loader that recreates the entry state, simulates it, and
5
- // checks it in the emulator. Exit status: 0 when every check passes, 1 when
6
- // one fails, 2 for a bad call.
5
+ // checks it in the emulator. Each switch of the patches (D72) is built on
6
+ // alone and checked. A macro use (D73) that does not give the bytes of the
7
+ // image is written as its instructions. Exit status: 0 when every check
8
+ // passes, 1 when one fails, 2 for a bad call.
7
9
 
8
10
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
9
11
  import { join, posix } from "node:path";
@@ -18,7 +20,8 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
18
20
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
19
21
  import { conflicts } from "#src/knowledge/conflicts.ts";
20
22
  import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
21
- import { currentRevision, listComments, listModules, listOperands, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
23
+ import { identifiers } from "#src/knowledge/expression.ts";
24
+ import { currentRevision, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
22
25
  import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
23
26
  import { assemble } from "#src/native/acme.ts";
24
27
  import { analyze } from "#src/native/ghidra/analyze.ts";
@@ -29,7 +32,7 @@ import { resolveProjectPath } from "#src/project.ts";
29
32
  import { runScript, UsageError } from "#src/script.ts";
30
33
  import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
31
34
  import { OPCODES } from "./opcodes.ts";
32
- import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range, type Rendered } from "./source.ts";
35
+ import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./source.ts";
33
36
 
34
37
  const USAGE = `generate.ts <image> --out <dir> [options]
35
38
 
@@ -50,8 +53,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
50
53
  The result is one JSON object. Addresses are $ and four hex digits.`;
51
54
 
52
55
  const MAX_LISTED = 20;
53
- /** Rounds of assembly that move operand expressions to numbers (D65). */
54
- const OPERAND_ROUNDS = 3;
56
+ /** Rounds of assembly that move operand expressions to numbers (D65) and macro uses to instructions (D73). */
57
+ const ROUNDS = 6;
55
58
  const PROBE_BASE = 0x1000;
56
59
  const PROBE_SLOT = 4;
57
60
  /** The copies of a full 64 KiB take about 140 frames. */
@@ -81,6 +84,9 @@ function readKnowledge(space: Space) {
81
84
  modules: listModules(db, undefined, undefined, space),
82
85
  references: listReferences(db, undefined, undefined, undefined, space),
83
86
  operands: listOperands(db, undefined, undefined, space),
87
+ patches: listPatches(db, undefined, undefined, undefined, space),
88
+ macros: listMacros(db, undefined, space),
89
+ macroUses: listMacroUses(db, undefined, undefined, undefined, space),
84
90
  };
85
91
  } finally {
86
92
  db?.close();
@@ -120,10 +126,14 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
120
126
  }
121
127
 
122
128
  /** Assembles files given as text with ACME; the program or the diagnostics. */
123
- async function assembleFiles(files: Array<{ name: string; text: string }>, entry: string): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
129
+ async function assembleFiles(
130
+ files: Array<{ name: string; text: string }>,
131
+ entry: string,
132
+ defines: Record<string, number> = {},
133
+ ): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
124
134
  const contents = files.map((file) => Buffer.from(file.text, "utf8"));
125
135
  const { result, attachments } = await assemble(
126
- { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines: {} },
136
+ { files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines },
127
137
  contents,
128
138
  localToolContext(),
129
139
  );
@@ -173,12 +183,88 @@ async function probe(laid: Layout): Promise<Set<string>> {
173
183
  throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
174
184
  }
175
185
 
186
+ /** The macro uses that an ACME error names: its line, or a line of the macro body in macros.asm, which every use of that macro shares. */
187
+ function usesInErrors(diagnostics: Diagnostic[], lineOf: ReadonlyMap<string, number>, uses: ReadonlyArray<{ start: number; macro: string }>, macroLines: Rendered["macroLines"], markLines: number): Map<number, string> {
188
+ const blamed = new Map<number, string>();
189
+ for (const diagnostic of diagnostics) {
190
+ if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
191
+ const start = lineOf.get(`${diagnostic.file}:${diagnostic.line}`);
192
+ if (start !== undefined) blamed.set(start, `ACME refuses it: ${diagnostic.message}`);
193
+ if (diagnostic.file !== "macros.asm") continue;
194
+ for (const [macro, lines] of macroLines) {
195
+ if (diagnostic.line < lines.first + markLines || diagnostic.line > lines.last + markLines) continue;
196
+ for (const use of uses) if (use.macro === macro) blamed.set(use.start, `ACME refuses the macro ${macro}: ${diagnostic.message}`);
197
+ }
198
+ }
199
+ return blamed;
200
+ }
201
+
202
+ /** The first address where two byte runs differ, as a reason. */
203
+ function firstDifference(start: number, built: (address: number) => number, original: (address: number) => number, end: number): string | undefined {
204
+ for (let address = start; address <= end; address++) {
205
+ if (built(address) !== original(address)) return `ACME assembles it to other bytes than the image: at ${formatC64Address(address)} it gives $${built(address).toString(16).padStart(2, "0")}, and the image has $${original(address).toString(16).padStart(2, "0")}.`;
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ /**
211
+ * Assembles each macro use alone at its own address, with the constants and
212
+ * the macros, and gives the uses that ACME refuses or assembles to other bytes
213
+ * than the image, with the reason (D73). A use of another length moves no
214
+ * other use, so each one is checked on its own, as the opcode probe does.
215
+ */
216
+ async function probeUses(laid: Layout, rendered: Rendered, image: Image): Promise<Map<number, string>> {
217
+ const blamed = new Map<number, string>();
218
+ const support = rendered.files.filter((file) => file.name === "constants.asm" || file.name === "macros.asm");
219
+ for (let round = 0; round < 3; round++) {
220
+ const uses = [...laid.macroUses.values()].filter((use) => !blamed.has(use.start));
221
+ if (uses.length === 0) return blamed;
222
+ // The labels of the image that the args name, with their values: the probe has no other source.
223
+ const labels = new Map<string, number>();
224
+ for (const [address, name] of laid.names) if (laid.inImage(address)) labels.set(name, address);
225
+ const used = [...new Set(uses.flatMap((use) => use.args.flatMap((arg) => identifiers(arg))))].filter((name) => labels.has(name)).sort();
226
+ const lines = ["!cpu 6510", '!source "constants.asm"', '!source "macros.asm"', ...used.map((name) => `${name} = ${labels.get(name)! < 0x100 ? `$${labels.get(name)!.toString(16).padStart(2, "0")}` : formatC64Address(labels.get(name)!)}`), `* = ${formatC64Address(PROBE_BASE)}`];
227
+ const lineOf = new Map<string, number>();
228
+ uses.forEach((use, index) => {
229
+ lines.push(`!pseudopc ${formatC64Address(use.start)} {`, ` +${use.macro}${use.args.length === 0 ? "" : ` ${use.args.join(", ")}`}`);
230
+ lineOf.set(`probe.asm:${lines.length}`, use.start);
231
+ lines.push(`probe_end_${index}`, "}");
232
+ });
233
+ const built = await assembleFiles([...support, { name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
234
+ if (built.program === undefined) {
235
+ const refused = usesInErrors(built.diagnostics, lineOf, uses, rendered.macroLines, 0);
236
+ if (refused.size === 0) throw new WireFailure("operation-failed", `ACME refused the macro probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
237
+ for (const [start, reason] of refused) blamed.set(start, reason);
238
+ continue;
239
+ }
240
+ const ends = new Map((built.symbols ?? []).map((symbol) => [symbol.name, symbol.kind === "address" ? symbol.address : symbol.value]));
241
+ let offset = 2 + PROBE_BASE - built.start!;
242
+ uses.forEach((use, index) => {
243
+ const length = ends.get(`probe_end_${index}`)! - use.start;
244
+ const wanted = use.end - use.start + 1;
245
+ const program = built.program!;
246
+ const at = offset;
247
+ offset += length;
248
+ if (length !== wanted) {
249
+ blamed.set(use.start, `ACME assembles it to ${length} bytes, and the range has ${wanted}.`);
250
+ return;
251
+ }
252
+ const reason = firstDifference(use.start, (address) => program[at + address - use.start]!, image.byteAt, use.end);
253
+ if (reason !== undefined) blamed.set(use.start, reason);
254
+ });
255
+ return blamed;
256
+ }
257
+ throw new WireFailure("operation-failed", "ACME refused the macro probe three times.");
258
+ }
259
+
176
260
  /**
177
- * The operand expressions that made the assembly fail or that ACME assembled
178
- * to other bytes than the image, with the reason (D65). `markLines` is the
179
- * number of lines added before the rendered text of each file.
261
+ * The operand expressions (D65) and macro uses (D73) that made the assembly
262
+ * fail or that ACME assembled to other bytes than the image, with the
263
+ * reason. `markLines` is the number of lines added before the rendered text of
264
+ * each file. A use of another length moves every byte after it: then only the
265
+ * first use that differs is blamed, and no operand.
180
266
  */
181
- function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): Map<number, string> {
267
+ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): { numbers: Map<number, string>; uses: Map<number, string> } {
182
268
  const blamed = new Map<number, string>();
183
269
  if (built.program === undefined) {
184
270
  const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
@@ -187,9 +273,20 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
187
273
  const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
188
274
  if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
189
275
  }
190
- return blamed;
276
+ const useLines = new Map(rendered.uses.map((use) => [`${use.file}:${use.line + markLines}`, use.start]));
277
+ return { numbers: blamed, uses: usesInErrors(built.diagnostics, useLines, rendered.uses, rendered.macroLines, markLines) };
191
278
  }
192
279
  const end = built.start! + built.program.length - 3;
280
+ const at = (address: number) => (address >= built.start! && address <= end ? built.program![2 + address - built.start!]! : -1);
281
+ const uses = new Map<number, string>();
282
+ const imageEnd = Math.max(...image.segments.map((segment) => segment.end));
283
+ for (const use of rendered.uses) {
284
+ const reason = firstDifference(use.start, at, image.byteAt, use.end);
285
+ if (reason === undefined) continue;
286
+ uses.set(use.start, reason);
287
+ if (end !== imageEnd) break;
288
+ }
289
+ if (uses.size > 0) return { numbers: blamed, uses };
193
290
  const word = (at: (address: number) => number, item: Rendered["expressions"][number]) => (item.length === 3 ? at(item.address + 1) | (at(item.address + 2) << 8) : at(item.address + 1));
194
291
  const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
195
292
  for (const item of rendered.expressions) {
@@ -198,7 +295,7 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
198
295
  const original = word(image.byteAt, item);
199
296
  if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
200
297
  }
201
- return blamed;
298
+ return { numbers: blamed, uses };
202
299
  }
203
300
 
204
301
  interface SegmentCheck {
@@ -228,6 +325,57 @@ function check(program: Buffer, start: number, image: Image): { identical: boole
228
325
  return { identical: segments.every((segment) => segment.result === "identical"), segments };
229
326
  }
230
327
 
328
+ /** What a build with one switch on gives (D72). */
329
+ interface SwitchCheck {
330
+ name: string;
331
+ define: string;
332
+ patches: Array<{ start: string; end: string; bytes: number; insideNames?: string[] }>;
333
+ result: "fits" | "overflow" | "changes outside" | "does not assemble";
334
+ changedBytes?: number;
335
+ outside?: { count: number; first: { address: string; built: number; default: number } };
336
+ overflow?: Array<{ start: string; message: string }>;
337
+ diagnostics?: Diagnostic[];
338
+ }
339
+
340
+ /**
341
+ * Builds the source with one switch on and compares it with the default
342
+ * build: only the ranges of its patches may change, and the program keeps its
343
+ * start and length. A patch longer than its range stops ACME with !error.
344
+ */
345
+ async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ name: string; text: string }>, base: { program: Buffer; start: number }, names: Layout["names"]): Promise<SwitchCheck> {
346
+ const shown: SwitchCheck = {
347
+ name,
348
+ define: `--define ${name}=1`,
349
+ patches: patches.map((patch) => {
350
+ const inside = patch.inner.map((address) => names.get(address)!);
351
+ return { start: formatC64Address(patch.start), end: formatC64Address(patch.end), bytes: patch.end - patch.start + 1, ...(inside.length === 0 ? {} : { insideNames: inside }) };
352
+ }),
353
+ result: "fits",
354
+ };
355
+ const built = await assembleFiles(files, "main.asm", { [name]: 1 });
356
+ if (built.program === undefined) {
357
+ const overflow = built.diagnostics.flatMap((diagnostic) => {
358
+ const found = /The patch \S+ at (\$[0-9a-f]{4}) has .*/.exec(diagnostic.message);
359
+ return found === null ? [] : [{ start: found[1]!, message: found[0] }];
360
+ });
361
+ if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
362
+ return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_LISTED) };
363
+ }
364
+ const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
365
+ let changed = 0;
366
+ let outside: SwitchCheck["outside"];
367
+ const end = Math.max(base.start + base.program.length, built.start! + built.program.length) - 2;
368
+ for (let address = Math.min(base.start, built.start!); address < end; address++) {
369
+ const at = (program: Buffer, start: number) => (address >= start && address < start + program.length - 2 ? program[2 + address - start]! : -1);
370
+ const now = at(built.program, built.start!);
371
+ const before = at(base.program, base.start);
372
+ if (now === before) continue;
373
+ if (inside(address)) changed++;
374
+ else outside = { count: (outside?.count ?? 0) + 1, first: outside?.first ?? { address: formatC64Address(address), built: now, default: before } };
375
+ }
376
+ return outside === undefined ? { ...shown, changedBytes: changed } : { ...shown, result: "changes outside", changedBytes: changed, outside };
377
+ }
378
+
231
379
  /** Reads RAM in parts. */
232
380
  async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
233
381
  const parts: Buffer[] = [];
@@ -368,6 +516,9 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
368
516
  modules: knowledge.modules,
369
517
  referenceTargets: knowledge.references.map((reference) => reference.to),
370
518
  operands: knowledge.operands,
519
+ patches: knowledge.patches,
520
+ macros: knowledge.macros,
521
+ macroUses: knowledge.macroUses,
371
522
  ...(coverage === undefined ? {} : { coverage }),
372
523
  // The platform names are C64 names: drive code and banks get none.
373
524
  ...(space === DEFAULT_SPACE ? { platform: platformNames(), describe: describeAddress } : {}),
@@ -383,7 +534,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
383
534
  const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
384
535
  try {
385
536
  const entryName = laid.names.get(image.entry!);
386
- loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
537
+ loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), macros: laid.macros.length > 0, header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
387
538
  } catch (error) {
388
539
  if (!(error instanceof WireFailure)) throw error;
389
540
  loaderRefused = error.message;
@@ -392,16 +543,21 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
392
543
  // Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
393
544
  const marked = (list: Array<{ name: string; text: string }>) => list.map((file) => ({ ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` }));
394
545
  const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
395
- // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and the source is assembled again (D65).
546
+ // An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and a macro use as its
547
+ // instructions; then the source is assembled again (D65, D73).
396
548
  const numbers = new Map<number, string>();
397
- let rendered = render(laid, asBytes, header, numbers);
549
+ const asInstructions = new Map<number, string>();
550
+ let rendered = render(laid, asBytes, header, numbers, asInstructions);
551
+ if (laid.macroUses.size > 0) for (const [start, reason] of await probeUses(laid, rendered, image)) asInstructions.set(start, reason);
552
+ if (asInstructions.size > 0) rendered = render(laid, asBytes, header, numbers, asInstructions);
398
553
  let files = marked([...rendered.files, ...loaderFiles]);
399
554
  let built = await assembleFiles(files, "main.asm");
400
- for (let round = 0; round < OPERAND_ROUNDS && rendered.expressions.length > 0; round++) {
555
+ for (let round = 0; round < ROUNDS && (rendered.expressions.length > 0 || rendered.uses.length > 0); round++) {
401
556
  const blamed = blame(rendered, built, image, 1);
402
- if (blamed.size === 0) break;
403
- for (const [address, reason] of blamed) numbers.set(address, reason);
404
- rendered = render(laid, asBytes, header, numbers);
557
+ if (blamed.numbers.size === 0 && blamed.uses.size === 0) break;
558
+ for (const [address, reason] of blamed.numbers) numbers.set(address, reason);
559
+ for (const [start, reason] of blamed.uses) asInstructions.set(start, reason);
560
+ rendered = render(laid, asBytes, header, numbers, asInstructions);
405
561
  files = marked([...rendered.files, ...loaderFiles]);
406
562
  built = await assembleFiles(files, "main.asm");
407
563
  }
@@ -450,6 +606,25 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
450
606
  listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
451
607
  },
452
608
  }),
609
+ ...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
610
+ // A macro use that the source writes as its instructions does not fail the run: the source stays identical (D73).
611
+ ...(rendered.macrosAsInstructions.length === 0
612
+ ? {}
613
+ : {
614
+ macrosAsInstructions: {
615
+ count: rendered.macrosAsInstructions.length,
616
+ listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
617
+ },
618
+ }),
619
+ // A patch that the source cannot write fails the run: the switch would miss a part (D72).
620
+ ...(laid.patchesNotWritten.length === 0
621
+ ? {}
622
+ : {
623
+ patchesNotWritten: {
624
+ count: laid.patchesNotWritten.length,
625
+ listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
626
+ },
627
+ }),
453
628
  };
454
629
  // The loader includes the source, so it is built only from a source that is identical to the image.
455
630
  const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
@@ -459,8 +634,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
459
634
  const checked = check(built.program, built.start!, image);
460
635
  output.check = checked;
461
636
  if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
637
+ // Each switch on alone, against the default build (D72); combinations are not built.
638
+ const switches: SwitchCheck[] = [];
639
+ for (const [name, patches] of switchesOf(laid)) switches.push(await checkSwitch(name, patches, files, { program: built.program, start: built.start! }, laid.names));
640
+ if (switches.length > 0) output.switches = switches;
641
+ const sourceFailed = laid.patchesNotWritten.length > 0 || switches.some((item) => item.result !== "fits");
462
642
  if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
463
- if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
643
+ if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
464
644
 
465
645
  const loaderBuilt = await assembleFiles(files, "loader.asm");
466
646
  if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_LISTED) } }, failed: true };
@@ -494,7 +674,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
494
674
  // What the loader cannot set as it was at the entry (D63).
495
675
  ...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
496
676
  };
497
- return { output, failed: !ran.verified };
677
+ return { output, failed: !ran.verified || sourceFailed };
498
678
  }
499
679
 
500
680
  await runScript(
@@ -357,6 +357,8 @@ export interface LoaderInput {
357
357
  /** The label of the entry in the source: the loader jumps to it, so the jump follows the code when it moves (D66). */
358
358
  entryName?: string;
359
359
  header: string[];
360
+ /** The source has macros.asm (D73): the loader includes it after the constants. */
361
+ macros?: boolean;
360
362
  }
361
363
 
362
364
  /** The label of a block (D66): its first byte and the byte after it in the loader file, and its first address where it runs. */
@@ -419,7 +421,7 @@ export function planLoader(input: LoaderInput): LoaderPlan {
419
421
  const copyBlocks = order.map((index) => blockOf[index]!);
420
422
 
421
423
  const stackByte = 0x0100 + state.sp;
422
- const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', "", `* = ${hex4(LOAD)}`];
424
+ const lines: string[] = [...input.header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(input.macros ? ['!source "macros.asm"'] : []), "", `* = ${hex4(LOAD)}`];
423
425
  lines.push(` !byte $0b,$08,$0a,$00,$9e,${[...String(STUB)].map((digit) => hex2(digit.charCodeAt(0))).join(",")},$00,$00,$00 ; 10 SYS ${STUB}`);
424
426
  lines.push(
425
427
  "!zone loader_stub {",
@@ -61,7 +61,7 @@ export function isZeroPageMode(mode: Mode): boolean {
61
61
  }
62
62
 
63
63
  /** How an instruction uses the address of its operand, in the order a summary lists them. */
64
- export const USE_KINDS = ["read", "written", "modified", "called", "jumped to", "jumped through", "branched to", "pointer", "address", "table"] as const;
64
+ export const USE_KINDS = ["read", "written", "modified", "called", "jumped to", "jumped through", "branched to", "pointer", "address", "table", "macro argument"] as const;
65
65
  export type UseKind = (typeof USE_KINDS)[number];
66
66
 
67
67
  const WRITES = new Set(["sta", "stx", "sty", "sax", "sha", "shx", "shy", "tas"]);