@henols/c64-re-tools 2.0.0-rc.11 → 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/README.md +7 -5
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +11 -2
- package/skills/c64-assembler/scripts/assemble.ts +4 -4
- package/skills/c64-basic/SKILL.md +8 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +10 -3
- package/skills/c64-disk/scripts/disk.ts +4 -4
- package/skills/c64-emulator/SKILL.md +4 -0
- package/skills/c64-knowledge/SKILL.md +62 -12
- package/skills/c64-knowledge/scripts/knowledge.ts +352 -102
- package/skills/c64-memory-map/SKILL.md +7 -0
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +56 -3
- package/skills/c64-provenance/scripts/library.ts +584 -0
- package/skills/c64-provenance/scripts/provenance.ts +201 -9
- package/skills/c64-reverse-engineering/SKILL.md +16 -2
- package/skills/c64-source/SKILL.md +38 -7
- package/skills/c64-source/scripts/generate.ts +213 -29
- package/skills/c64-source/scripts/loader.ts +3 -1
- package/skills/c64-source/scripts/opcodes.ts +19 -0
- package/skills/c64-source/scripts/source.ts +387 -35
- package/skills/c64-static-analysis/SKILL.md +11 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +95 -8
- package/skills/c64-static-analysis/scripts/macros.ts +233 -0
- package/skills/c64-testing/SKILL.md +8 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/checklist.ts +2 -2
- package/skills/c64-testing/scripts/run.ts +4 -4
- package/skills/c64-testing/scripts/scenario.ts +1 -1
- package/skills/c64-testing/scripts/test.ts +4 -4
- package/skills/c64-unpacker/SKILL.md +11 -4
- package/skills/c64-unpacker/scripts/unpack.ts +4 -4
- package/src/cli/main.ts +5 -3
- package/src/host-client/image.ts +1 -1
- package/src/host-client/transfer.ts +34 -10
- package/src/host-client/vice-session.ts +9 -9
- package/src/knowledge/conflicts.ts +157 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/history.ts +54 -20
- package/src/knowledge/import.ts +3 -2
- package/src/knowledge/read.ts +169 -20
- package/src/knowledge/schema.ts +115 -72
- package/src/knowledge/text.ts +391 -0
- package/src/knowledge/write.ts +508 -51
- package/src/mcp/output.ts +14 -7
- package/src/mcp/tools/debug.ts +8 -6
- package/src/mcp/tools/media.ts +17 -13
- package/src/mcp/tools/memory.ts +9 -5
- package/src/mcp/tools/video.ts +6 -5
- package/src/platform.ts +28 -5
- package/src/project.ts +166 -3
- package/src/protocol/tools.ts +1 -1
- package/src/script.ts +46 -7
|
@@ -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";
|
|
@@ -16,24 +18,26 @@ import { ImageError, readImage as readSharedImage, type Image } from "#src/host-
|
|
|
16
18
|
import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
17
19
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
18
20
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
21
|
+
import { conflicts } from "#src/knowledge/conflicts.ts";
|
|
19
22
|
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
20
|
-
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";
|
|
21
25
|
import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
22
26
|
import { assemble } from "#src/native/acme.ts";
|
|
23
27
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
24
28
|
import { localToolContext } from "#src/native/local.ts";
|
|
25
29
|
import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
|
|
26
|
-
import { platformNames } from "#src/platform.ts";
|
|
30
|
+
import { describeAddress, platformNames } from "#src/platform.ts";
|
|
27
31
|
import { resolveProjectPath } from "#src/project.ts";
|
|
28
32
|
import { runScript, UsageError } from "#src/script.ts";
|
|
29
33
|
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
30
34
|
import { OPCODES } from "./opcodes.ts";
|
|
31
|
-
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";
|
|
32
36
|
|
|
33
37
|
const USAGE = `generate.ts <image> --out <dir> [options]
|
|
34
38
|
|
|
35
39
|
<image> an image.json of the c64-unpacker skill, or a PRG, relative to
|
|
36
|
-
the project
|
|
40
|
+
the project folder
|
|
37
41
|
--out <dir> the directory for the source files, relative to the project
|
|
38
42
|
--entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
|
|
39
43
|
--coverage <file> a coverage map of 64 KiB from c64_memmap (repeatable): code
|
|
@@ -49,8 +53,8 @@ const USAGE = `generate.ts <image> --out <dir> [options]
|
|
|
49
53
|
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
50
54
|
|
|
51
55
|
const MAX_LISTED = 20;
|
|
52
|
-
/** Rounds of assembly that move operand expressions to numbers (D65). */
|
|
53
|
-
const
|
|
56
|
+
/** Rounds of assembly that move operand expressions to numbers (D65) and macro uses to instructions (D73). */
|
|
57
|
+
const ROUNDS = 6;
|
|
54
58
|
const PROBE_BASE = 0x1000;
|
|
55
59
|
const PROBE_SLOT = 4;
|
|
56
60
|
/** The copies of a full 64 KiB take about 140 frames. */
|
|
@@ -69,6 +73,9 @@ function readImage(path: string): Image {
|
|
|
69
73
|
function readKnowledge(space: Space) {
|
|
70
74
|
const db = openForRead();
|
|
71
75
|
try {
|
|
76
|
+
// With conflicts from a git merge, the source would depend on which of two rows comes first (D69).
|
|
77
|
+
const found = conflicts(db).length;
|
|
78
|
+
if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was generated. Run c64-knowledge's knowledge.ts merge, then generate again.`);
|
|
72
79
|
return {
|
|
73
80
|
revision: currentRevision(db),
|
|
74
81
|
symbols: listSymbols(db, undefined, undefined, undefined, space),
|
|
@@ -77,6 +84,9 @@ function readKnowledge(space: Space) {
|
|
|
77
84
|
modules: listModules(db, undefined, undefined, space),
|
|
78
85
|
references: listReferences(db, undefined, undefined, undefined, space),
|
|
79
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),
|
|
80
90
|
};
|
|
81
91
|
} finally {
|
|
82
92
|
db?.close();
|
|
@@ -116,10 +126,14 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
|
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
/** Assembles files given as text with ACME; the program or the diagnostics. */
|
|
119
|
-
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[] }> {
|
|
120
134
|
const contents = files.map((file) => Buffer.from(file.text, "utf8"));
|
|
121
135
|
const { result, attachments } = await assemble(
|
|
122
|
-
{ 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 },
|
|
123
137
|
contents,
|
|
124
138
|
localToolContext(),
|
|
125
139
|
);
|
|
@@ -169,12 +183,88 @@ async function probe(laid: Layout): Promise<Set<string>> {
|
|
|
169
183
|
throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
|
|
170
184
|
}
|
|
171
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
|
+
|
|
172
260
|
/**
|
|
173
|
-
* The operand expressions
|
|
174
|
-
* to other bytes than the image, with the
|
|
175
|
-
* 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.
|
|
176
266
|
*/
|
|
177
|
-
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> } {
|
|
178
268
|
const blamed = new Map<number, string>();
|
|
179
269
|
if (built.program === undefined) {
|
|
180
270
|
const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
|
|
@@ -183,9 +273,20 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
|
|
|
183
273
|
const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
|
|
184
274
|
if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
|
|
185
275
|
}
|
|
186
|
-
|
|
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) };
|
|
187
278
|
}
|
|
188
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 };
|
|
189
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));
|
|
190
291
|
const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
|
|
191
292
|
for (const item of rendered.expressions) {
|
|
@@ -194,7 +295,7 @@ function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFile
|
|
|
194
295
|
const original = word(image.byteAt, item);
|
|
195
296
|
if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
|
|
196
297
|
}
|
|
197
|
-
return blamed;
|
|
298
|
+
return { numbers: blamed, uses };
|
|
198
299
|
}
|
|
199
300
|
|
|
200
301
|
interface SegmentCheck {
|
|
@@ -224,6 +325,57 @@ function check(program: Buffer, start: number, image: Image): { identical: boole
|
|
|
224
325
|
return { identical: segments.every((segment) => segment.result === "identical"), segments };
|
|
225
326
|
}
|
|
226
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
|
+
|
|
227
379
|
/** Reads RAM in parts. */
|
|
228
380
|
async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
|
|
229
381
|
const parts: Buffer[] = [];
|
|
@@ -332,7 +484,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
332
484
|
try {
|
|
333
485
|
resolveProjectPath(values.out);
|
|
334
486
|
} catch {
|
|
335
|
-
throw new UsageError(`--out must be a path relative to the project
|
|
487
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
336
488
|
}
|
|
337
489
|
const entries = (values.entry ?? []).map((value) => {
|
|
338
490
|
try {
|
|
@@ -364,9 +516,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
364
516
|
modules: knowledge.modules,
|
|
365
517
|
referenceTargets: knowledge.references.map((reference) => reference.to),
|
|
366
518
|
operands: knowledge.operands,
|
|
519
|
+
patches: knowledge.patches,
|
|
520
|
+
macros: knowledge.macros,
|
|
521
|
+
macroUses: knowledge.macroUses,
|
|
367
522
|
...(coverage === undefined ? {} : { coverage }),
|
|
368
523
|
// The platform names are C64 names: drive code and banks get none.
|
|
369
|
-
...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
|
|
524
|
+
...(space === DEFAULT_SPACE ? { platform: platformNames(), describe: describeAddress } : {}),
|
|
370
525
|
});
|
|
371
526
|
const asBytes = await probe(laid);
|
|
372
527
|
const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
|
|
@@ -379,7 +534,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
379
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) }));
|
|
380
535
|
try {
|
|
381
536
|
const entryName = laid.names.get(image.entry!);
|
|
382
|
-
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."] });
|
|
383
538
|
} catch (error) {
|
|
384
539
|
if (!(error instanceof WireFailure)) throw error;
|
|
385
540
|
loaderRefused = error.message;
|
|
@@ -388,16 +543,21 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
388
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).
|
|
389
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}` }));
|
|
390
545
|
const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
|
|
391
|
-
// 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).
|
|
392
548
|
const numbers = new Map<number, string>();
|
|
393
|
-
|
|
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);
|
|
394
553
|
let files = marked([...rendered.files, ...loaderFiles]);
|
|
395
554
|
let built = await assembleFiles(files, "main.asm");
|
|
396
|
-
for (let round = 0; round <
|
|
555
|
+
for (let round = 0; round < ROUNDS && (rendered.expressions.length > 0 || rendered.uses.length > 0); round++) {
|
|
397
556
|
const blamed = blame(rendered, built, image, 1);
|
|
398
|
-
if (blamed.size === 0) break;
|
|
399
|
-
for (const [address, reason] of blamed) numbers.set(address, reason);
|
|
400
|
-
|
|
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);
|
|
401
561
|
files = marked([...rendered.files, ...loaderFiles]);
|
|
402
562
|
built = await assembleFiles(files, "main.asm");
|
|
403
563
|
}
|
|
@@ -446,6 +606,25 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
446
606
|
listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
|
|
447
607
|
},
|
|
448
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
|
+
}),
|
|
449
628
|
};
|
|
450
629
|
// The loader includes the source, so it is built only from a source that is identical to the image.
|
|
451
630
|
const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
|
|
@@ -455,8 +634,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
455
634
|
const checked = check(built.program, built.start!, image);
|
|
456
635
|
output.check = checked;
|
|
457
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");
|
|
458
642
|
if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
|
|
459
|
-
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
|
|
643
|
+
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
|
|
460
644
|
|
|
461
645
|
const loaderBuilt = await assembleFiles(files, "loader.asm");
|
|
462
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 };
|
|
@@ -490,12 +674,12 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
490
674
|
// What the loader cannot set as it was at the entry (D63).
|
|
491
675
|
...(loader.cia === undefined || loader.cia.limits.length === 0 ? {} : { limits: loader.cia.limits }),
|
|
492
676
|
};
|
|
493
|
-
return { output, failed: !ran.verified };
|
|
677
|
+
return { output, failed: !ran.verified || sourceFailed };
|
|
494
678
|
}
|
|
495
679
|
|
|
496
680
|
await runScript(
|
|
497
|
-
async () => {
|
|
498
|
-
const { output, failed } = await run(
|
|
681
|
+
async (argv) => {
|
|
682
|
+
const { output, failed } = await run(argv);
|
|
499
683
|
if (failed) process.exitCode = 1;
|
|
500
684
|
return output;
|
|
501
685
|
},
|
|
@@ -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 {",
|
|
@@ -59,3 +59,22 @@ export function takesAddress(mode: Mode): boolean {
|
|
|
59
59
|
export function isZeroPageMode(mode: Mode): boolean {
|
|
60
60
|
return mode === "zp" || mode === "zpx" || mode === "zpy" || mode === "izx" || mode === "izy";
|
|
61
61
|
}
|
|
62
|
+
|
|
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", "macro argument"] as const;
|
|
65
|
+
export type UseKind = (typeof USE_KINDS)[number];
|
|
66
|
+
|
|
67
|
+
const WRITES = new Set(["sta", "stx", "sty", "sax", "sha", "shx", "shy", "tas"]);
|
|
68
|
+
const MODIFIES = new Set(["inc", "dec", "asl", "lsr", "rol", "ror", "slo", "rla", "sre", "rra", "dcp", "isc"]);
|
|
69
|
+
|
|
70
|
+
/** The use of the operand address of an instruction that takes an address. Through (zp),y and (zp,x) the address is a pointer. */
|
|
71
|
+
export function useKind(opcode: number): UseKind {
|
|
72
|
+
const { mnemonic, mode } = OPCODES[opcode]!;
|
|
73
|
+
if (mode === "izx" || mode === "izy") return "pointer";
|
|
74
|
+
if (mode === "rel") return "branched to";
|
|
75
|
+
if (mnemonic === "jsr") return "called";
|
|
76
|
+
if (mnemonic === "jmp") return mode === "ind" ? "jumped through" : "jumped to";
|
|
77
|
+
if (WRITES.has(mnemonic)) return "written";
|
|
78
|
+
if (MODIFIES.has(mnemonic)) return "modified";
|
|
79
|
+
return "read";
|
|
80
|
+
}
|