@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
|
@@ -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
|
|
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 `
|
|
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
|
|
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
|
|
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.
|
|
6
|
-
//
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
178
|
-
* to other bytes than the image, with the
|
|
179
|
-
* number of lines added before the rendered text of
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
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"]);
|