@henols/c64-re-tools 2.0.0-rc.8 → 2.0.0
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 +98 -115
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +43 -8
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/src/trace.ts +55 -23
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
// The c64-source script
|
|
2
|
-
// knowledge and Ghidra's instructions, which it runs itself, and checks
|
|
3
|
-
// ACME that the source assembles to the
|
|
1
|
+
// The c64-source script: generates ACME source from the program and the
|
|
2
|
+
// knowledge and Ghidra's instructions, which it runs itself, and checks
|
|
3
|
+
// with ACME that the source assembles to the program 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 is built on
|
|
6
|
+
// alone and checked. A macro use 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
|
-
import { join
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import { parseArgs } from "node:util";
|
|
11
13
|
|
|
12
|
-
import { formatC64Address, parseC64Address,
|
|
14
|
+
import { COVERAGE, coverageSeedKinds, formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
|
|
13
15
|
import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
14
|
-
import { readCoverageFiles } from "#src/host-client/coverage.ts";
|
|
15
16
|
import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
17
18
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
19
|
+
import { conflicts } from "#src/knowledge/conflicts.ts";
|
|
18
20
|
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
19
|
-
import {
|
|
21
|
+
import { identifiers } from "#src/knowledge/expression.ts";
|
|
22
|
+
import { analyzedBytes, programCoverage, requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
|
|
23
|
+
import { ANALYZER_NAME_ORDER, currentRevision, listAnalyzerNames, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols, type SymbolRow } from "#src/knowledge/read.ts";
|
|
20
24
|
import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
21
25
|
import { assemble } from "#src/native/acme.ts";
|
|
22
26
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
23
27
|
import { localToolContext } from "#src/native/local.ts";
|
|
24
28
|
import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
|
|
25
|
-
import {
|
|
29
|
+
import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
|
|
26
30
|
import { resolveProjectPath } from "#src/project.ts";
|
|
27
31
|
import { runScript, UsageError } from "#src/script.ts";
|
|
28
|
-
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
29
|
-
import { OPCODES } from "
|
|
30
|
-
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
|
|
32
|
+
import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, readEntryState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
|
|
33
|
+
import { OPCODES } from "#src/opcodes.ts";
|
|
34
|
+
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Range, type Rendered } from "./lib/source.ts";
|
|
31
35
|
|
|
32
|
-
const USAGE = `generate.ts
|
|
36
|
+
const USAGE = `generate.ts --out <dir> [options]
|
|
37
|
+
|
|
38
|
+
The program, its entry state and the coverage come from the knowledge:
|
|
39
|
+
put them in with knowledge.ts image and coverage of the c64-knowledge skill.
|
|
40
|
+
Code that no imported run executed is marked and listed; <dir>/not_executed.json
|
|
41
|
+
has all of its ranges.
|
|
33
42
|
|
|
34
|
-
<image> an image.json of the c64-unpacker skill, or a PRG, relative to
|
|
35
|
-
the project directory
|
|
36
43
|
--out <dir> the directory for the source files, relative to the project
|
|
37
44
|
--entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
|
|
38
|
-
--
|
|
39
|
-
|
|
40
|
-
has all of its ranges
|
|
41
|
-
--space <space> the knowledge space of the image: c64 (the default), drive8, or
|
|
42
|
-
bank0 to bank255; --coverage and --loader are for c64 only
|
|
45
|
+
--space <space> the knowledge space of the program: c64 (the default), drive8,
|
|
46
|
+
or bank0 to bank255; the coverage and --loader are for c64 only
|
|
43
47
|
--loader also write loader.asm and loader.prg: the loader copies the
|
|
44
|
-
segments into place, sets the entry state
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
segments into place, sets the entry state of the knowledge,
|
|
49
|
+
and jumps to the entry; it is checked in the emulator
|
|
50
|
+
--analyzer-names name an address that has no name of the user or the LLM, and
|
|
51
|
+
no platform name, with its analyzer name
|
|
47
52
|
|
|
48
53
|
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
49
54
|
|
|
50
55
|
const MAX_LISTED = 20;
|
|
56
|
+
/** Rounds of assembly that move operand expressions to numbers and macro uses to instructions. */
|
|
57
|
+
const ROUNDS = 6;
|
|
51
58
|
const PROBE_BASE = 0x1000;
|
|
52
59
|
const PROBE_SLOT = 4;
|
|
53
60
|
/** The copies of a full 64 KiB take about 140 frames. */
|
|
54
61
|
const LOADER_FRAMES = 1500;
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
segments: Range[];
|
|
59
|
-
byteAt: (address: number) => number;
|
|
60
|
-
/** The bytes that Ghidra analyzes, and how. */
|
|
61
|
-
analyzed: { bytes: Buffer; kind: "prg" | "flat64k" };
|
|
62
|
-
entry?: number;
|
|
63
|
-
/** entry_state.json next to image.json, when it exists. */
|
|
64
|
-
statePath?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readImage(path: string): Image {
|
|
68
|
-
const { bytes } = readProjectFile(path);
|
|
69
|
-
if (!path.endsWith(".json")) {
|
|
70
|
-
let range: ReturnType<typeof prgRange>;
|
|
71
|
-
try {
|
|
72
|
-
range = prgRange(bytes);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
throw new UsageError(`${path}: ${(error as Error).message}`);
|
|
75
|
-
}
|
|
76
|
-
return { path, segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]!, analyzed: { bytes, kind: "prg" } };
|
|
77
|
-
}
|
|
78
|
-
const fail = (what: string): never => {
|
|
79
|
-
throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
80
|
-
};
|
|
81
|
-
let manifest: Record<string, unknown>;
|
|
82
|
-
try {
|
|
83
|
-
manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
return fail((error as Error).message);
|
|
86
|
-
}
|
|
87
|
-
const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
|
|
88
|
-
if (typeof manifest.image !== "string") fail("it has no image");
|
|
89
|
-
if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
|
|
90
|
-
const segments = (manifest.segments as Array<Record<string, unknown>>)
|
|
91
|
-
.map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
|
|
92
|
-
.sort((a, b) => a.start - b.start);
|
|
93
|
-
segments.forEach((segment, index) => {
|
|
94
|
-
if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
|
|
95
|
-
if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
|
|
96
|
-
});
|
|
97
|
-
const directory = posix.dirname(path);
|
|
98
|
-
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
99
|
-
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
100
|
-
const image: Image = { path, segments, byteAt: (at) => memory[at]!, analyzed: { bytes: memory, kind: "flat64k" }, statePath: posix.join(directory, "entry_state.json") };
|
|
101
|
-
if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
|
|
102
|
-
return image;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** The knowledge of one space that the source uses, read once. */
|
|
106
|
-
function readKnowledge(space: Space) {
|
|
63
|
+
/** The knowledge of one space that the source uses, read once. With `analyzerNames`, an address without a name takes its first analyzer name. */
|
|
64
|
+
function readKnowledge(space: Space, analyzerNames = false) {
|
|
107
65
|
const db = openForRead();
|
|
108
66
|
try {
|
|
67
|
+
// With conflicts from a git merge, the source would depend on which of two rows comes first.
|
|
68
|
+
const found = conflicts(db).length;
|
|
69
|
+
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.`);
|
|
70
|
+
const symbols = listSymbols(db, undefined, undefined, undefined, space);
|
|
71
|
+
const second = listAnalyzerNames(db, undefined, undefined, undefined, space);
|
|
109
72
|
return {
|
|
110
73
|
revision: currentRevision(db),
|
|
111
|
-
|
|
74
|
+
program: requireProgram(db, space),
|
|
75
|
+
// The coverage is of the C64: drive code and banks have none.
|
|
76
|
+
coverage: space === DEFAULT_SPACE ? programCoverage(db)?.map : undefined,
|
|
77
|
+
symbols: analyzerNames ? withAnalyzerNames(symbols, second) : symbols,
|
|
78
|
+
// Analyzer routines are entry points for the decode, also without --analyzer-names.
|
|
79
|
+
routines: [...symbols, ...second].filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address),
|
|
112
80
|
regions: listRegions(db, undefined, undefined, undefined, space),
|
|
113
81
|
comments: listComments(db, undefined, undefined, space),
|
|
114
82
|
modules: listModules(db, undefined, undefined, space),
|
|
115
83
|
references: listReferences(db, undefined, undefined, undefined, space),
|
|
84
|
+
operands: listOperands(db, undefined, undefined, space),
|
|
85
|
+
patches: listPatches(db, undefined, undefined, undefined, space),
|
|
86
|
+
macros: listMacros(db, undefined, space),
|
|
87
|
+
macroUses: listMacroUses(db, undefined, undefined, undefined, space),
|
|
116
88
|
};
|
|
117
89
|
} finally {
|
|
118
90
|
db?.close();
|
|
119
91
|
}
|
|
120
92
|
}
|
|
121
93
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The names of the user and the LLM, and for each address without one the
|
|
96
|
+
* first analyzer name in the order heuristic, Ghidra, DXA. An analyzer
|
|
97
|
+
* name that another address has already is left out. The analyzer names keep
|
|
98
|
+
* their origin, so a platform name still wins over them.
|
|
99
|
+
*/
|
|
100
|
+
function withAnalyzerNames(symbols: SymbolRow[], second: SymbolRow[]): SymbolRow[] {
|
|
101
|
+
const named = new Set(symbols.map((symbol) => symbol.address));
|
|
102
|
+
const taken = new Set(symbols.map((symbol) => symbol.name));
|
|
103
|
+
const added: SymbolRow[] = [];
|
|
104
|
+
for (const origin of ANALYZER_NAME_ORDER) {
|
|
105
|
+
for (const row of second.filter((candidate) => candidate.origin === origin)) {
|
|
106
|
+
if (named.has(row.address) || taken.has(row.name)) continue;
|
|
107
|
+
named.add(row.address);
|
|
108
|
+
taken.add(row.name);
|
|
109
|
+
added.push(row);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...symbols, ...added].sort((a, b) => a.address - b.address);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ghidra's instruction starts and lengths inside the image, seeded from the
|
|
117
|
+
* entry, from knowledge of any origin, and from the coverage: a run's flow
|
|
118
|
+
* starts, also after a JSR, so code that an analyzer does not reach by itself
|
|
119
|
+
* is in the source.
|
|
120
|
+
*/
|
|
121
|
+
async function decode(image: ProgramImage, knowledge: ReturnType<typeof readKnowledge>, entries: number[], coverage: Uint8Array | undefined): Promise<Map<number, number>> {
|
|
124
122
|
const inImage = (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end);
|
|
125
123
|
const seeds = new Set<number>([...(image.entry === undefined ? [] : [image.entry]), ...entries]);
|
|
126
|
-
for (const
|
|
124
|
+
for (const address of knowledge.routines) seeds.add(address);
|
|
125
|
+
const flows = coverage === undefined ? { routines: [], codeStarts: [] } : coverageSeedKinds(coverage, (address) => (image.segments.some((segment) => address >= segment.start && address <= segment.end) ? image.byteAt(address) : undefined));
|
|
126
|
+
for (const address of flows.routines) seeds.add(address);
|
|
127
127
|
for (const region of knowledge.regions) {
|
|
128
128
|
if (CODE_REGION_TYPES.includes(region.type)) seeds.add(region.start);
|
|
129
129
|
if (region.type === "jumps") for (let at = region.start; at + 2 <= region.end; at += 3) seeds.add(at);
|
|
130
130
|
}
|
|
131
131
|
const entryPoints = [...seeds].filter(inImage).sort((a, b) => a - b);
|
|
132
132
|
if (entryPoints.length === 0) {
|
|
133
|
-
// An entry in no segment is a capture fault: the image is data only, and the result says entryOutside
|
|
133
|
+
// An entry in no segment is a capture fault: the image is data only, and the result says entryOutside.
|
|
134
134
|
if (image.entry !== undefined && !inImage(image.entry)) return new Map();
|
|
135
135
|
throw new UsageError("Ghidra needs an entry point: give --entry, or record a routine or a code region in knowledge with the c64-knowledge skill");
|
|
136
136
|
}
|
|
@@ -138,8 +138,15 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
|
|
|
138
138
|
.filter((region) => !CODE_REGION_TYPES.includes(region.type) && region.type !== "dual-use")
|
|
139
139
|
.flatMap((region) => image.segments.filter((segment) => region.start <= segment.end && segment.start <= region.end).map((segment) => ({ start: Math.max(region.start, segment.start), end: Math.min(region.end, segment.end) })));
|
|
140
140
|
const { result } = await analyze(
|
|
141
|
-
{
|
|
142
|
-
|
|
141
|
+
{
|
|
142
|
+
imageKind: analyzedBytes(image).kind,
|
|
143
|
+
entryPoints: entryPoints.slice(0, GHIDRA_LIMITS.entryPoints),
|
|
144
|
+
codeStarts: flows.codeStarts.filter((address) => !seeds.has(address)).slice(0, GHIDRA_LIMITS.codeStarts),
|
|
145
|
+
dataRanges: dataRanges.slice(0, GHIDRA_LIMITS.dataRanges),
|
|
146
|
+
labels: [],
|
|
147
|
+
decompile: [],
|
|
148
|
+
},
|
|
149
|
+
analyzedBytes(image).bytes,
|
|
143
150
|
localToolContext(),
|
|
144
151
|
);
|
|
145
152
|
const instructions = new Map<number, number>();
|
|
@@ -152,10 +159,14 @@ async function decode(image: Image, knowledge: ReturnType<typeof readKnowledge>,
|
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
/** Assembles files given as text with ACME; the program or the diagnostics. */
|
|
155
|
-
async function assembleFiles(
|
|
162
|
+
async function assembleFiles(
|
|
163
|
+
files: Array<{ name: string; text: string }>,
|
|
164
|
+
entry: string,
|
|
165
|
+
defines: Record<string, number> = {},
|
|
166
|
+
): Promise<{ program?: Buffer; start?: number; symbols?: AssembledSymbol[]; diagnostics: Diagnostic[] }> {
|
|
156
167
|
const contents = files.map((file) => Buffer.from(file.text, "utf8"));
|
|
157
168
|
const { result, attachments } = await assemble(
|
|
158
|
-
{ files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines
|
|
169
|
+
{ files: files.map((file, index) => ({ path: file.name, size: contents[index]!.length })), entrySource: entry, includeDirs: [], defines },
|
|
159
170
|
contents,
|
|
160
171
|
localToolContext(),
|
|
161
172
|
);
|
|
@@ -205,6 +216,121 @@ async function probe(laid: Layout): Promise<Set<string>> {
|
|
|
205
216
|
throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
|
|
206
217
|
}
|
|
207
218
|
|
|
219
|
+
/** 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. */
|
|
220
|
+
function usesInErrors(diagnostics: Diagnostic[], lineOf: ReadonlyMap<string, number>, uses: ReadonlyArray<{ start: number; macro: string }>, macroLines: Rendered["macroLines"], markLines: number): Map<number, string> {
|
|
221
|
+
const blamed = new Map<number, string>();
|
|
222
|
+
for (const diagnostic of diagnostics) {
|
|
223
|
+
if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
|
|
224
|
+
const start = lineOf.get(`${diagnostic.file}:${diagnostic.line}`);
|
|
225
|
+
if (start !== undefined) blamed.set(start, `ACME refuses it: ${diagnostic.message}`);
|
|
226
|
+
if (diagnostic.file !== "macros.asm") continue;
|
|
227
|
+
for (const [macro, lines] of macroLines) {
|
|
228
|
+
if (diagnostic.line < lines.first + markLines || diagnostic.line > lines.last + markLines) continue;
|
|
229
|
+
for (const use of uses) if (use.macro === macro) blamed.set(use.start, `ACME refuses the macro ${macro}: ${diagnostic.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return blamed;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The first address where two byte runs differ, as a reason. */
|
|
236
|
+
function firstDifference(start: number, built: (address: number) => number, original: (address: number) => number, end: number): string | undefined {
|
|
237
|
+
for (let address = start; address <= end; address++) {
|
|
238
|
+
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")}.`;
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Assembles each macro use alone at its own address, with the constants and
|
|
245
|
+
* the macros, and gives the uses that ACME refuses or assembles to other bytes
|
|
246
|
+
* than the image, with the reason. A use of another length moves no
|
|
247
|
+
* other use, so each one is checked on its own, as the opcode probe does.
|
|
248
|
+
*/
|
|
249
|
+
async function probeUses(laid: Layout, rendered: Rendered, image: ProgramImage): Promise<Map<number, string>> {
|
|
250
|
+
const blamed = new Map<number, string>();
|
|
251
|
+
const support = rendered.files.filter((file) => file.name === "constants.asm" || file.name === "macros.asm");
|
|
252
|
+
for (let round = 0; round < 3; round++) {
|
|
253
|
+
const uses = [...laid.macroUses.values()].filter((use) => !blamed.has(use.start));
|
|
254
|
+
if (uses.length === 0) return blamed;
|
|
255
|
+
// The labels of the image that the args name, with their values: the probe has no other source.
|
|
256
|
+
const labels = new Map<string, number>();
|
|
257
|
+
for (const [address, name] of laid.names) if (laid.inImage(address)) labels.set(name, address);
|
|
258
|
+
const used = [...new Set(uses.flatMap((use) => use.args.flatMap((arg) => identifiers(arg))))].filter((name) => labels.has(name)).sort();
|
|
259
|
+
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)}`];
|
|
260
|
+
const lineOf = new Map<string, number>();
|
|
261
|
+
uses.forEach((use, index) => {
|
|
262
|
+
lines.push(`!pseudopc ${formatC64Address(use.start)} {`, ` +${use.macro}${use.args.length === 0 ? "" : ` ${use.args.join(", ")}`}`);
|
|
263
|
+
lineOf.set(`probe.asm:${lines.length}`, use.start);
|
|
264
|
+
lines.push(`probe_end_${index}`, "}");
|
|
265
|
+
});
|
|
266
|
+
const built = await assembleFiles([...support, { name: "probe.asm", text: `${lines.join("\n")}\n` }], "probe.asm");
|
|
267
|
+
if (built.program === undefined) {
|
|
268
|
+
const refused = usesInErrors(built.diagnostics, lineOf, uses, rendered.macroLines, 0);
|
|
269
|
+
if (refused.size === 0) throw new WireFailure("operation-failed", `ACME refused the macro probe: ${built.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
|
|
270
|
+
for (const [start, reason] of refused) blamed.set(start, reason);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const ends = new Map((built.symbols ?? []).map((symbol) => [symbol.name, symbol.kind === "address" ? symbol.address : symbol.value]));
|
|
274
|
+
let offset = 2 + PROBE_BASE - built.start!;
|
|
275
|
+
uses.forEach((use, index) => {
|
|
276
|
+
const length = ends.get(`probe_end_${index}`)! - use.start;
|
|
277
|
+
const wanted = use.end - use.start + 1;
|
|
278
|
+
const program = built.program!;
|
|
279
|
+
const at = offset;
|
|
280
|
+
offset += length;
|
|
281
|
+
if (length !== wanted) {
|
|
282
|
+
blamed.set(use.start, `ACME assembles it to ${length} bytes, and the range has ${wanted}.`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const reason = firstDifference(use.start, (address) => program[at + address - use.start]!, image.byteAt, use.end);
|
|
286
|
+
if (reason !== undefined) blamed.set(use.start, reason);
|
|
287
|
+
});
|
|
288
|
+
return blamed;
|
|
289
|
+
}
|
|
290
|
+
throw new WireFailure("operation-failed", "ACME refused the macro probe three times.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The operand expressions and macro uses that made the assembly
|
|
295
|
+
* fail or that ACME assembled to other bytes than the image, with the
|
|
296
|
+
* reason. `markLines` is the number of lines added before the rendered text of
|
|
297
|
+
* each file. A use of another length moves every byte after it: then only the
|
|
298
|
+
* first use that differs is blamed, and no operand.
|
|
299
|
+
*/
|
|
300
|
+
function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: ProgramImage, markLines: number): { numbers: Map<number, string>; uses: Map<number, string> } {
|
|
301
|
+
const blamed = new Map<number, string>();
|
|
302
|
+
if (built.program === undefined) {
|
|
303
|
+
const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
|
|
304
|
+
for (const diagnostic of built.diagnostics) {
|
|
305
|
+
if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
|
|
306
|
+
const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
|
|
307
|
+
if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
|
|
308
|
+
}
|
|
309
|
+
const useLines = new Map(rendered.uses.map((use) => [`${use.file}:${use.line + markLines}`, use.start]));
|
|
310
|
+
return { numbers: blamed, uses: usesInErrors(built.diagnostics, useLines, rendered.uses, rendered.macroLines, markLines) };
|
|
311
|
+
}
|
|
312
|
+
const end = built.start! + built.program.length - 3;
|
|
313
|
+
const at = (address: number) => (address >= built.start! && address <= end ? built.program![2 + address - built.start!]! : -1);
|
|
314
|
+
const uses = new Map<number, string>();
|
|
315
|
+
const imageEnd = Math.max(...image.segments.map((segment) => segment.end));
|
|
316
|
+
for (const use of rendered.uses) {
|
|
317
|
+
const reason = firstDifference(use.start, at, image.byteAt, use.end);
|
|
318
|
+
if (reason === undefined) continue;
|
|
319
|
+
uses.set(use.start, reason);
|
|
320
|
+
if (end !== imageEnd) break;
|
|
321
|
+
}
|
|
322
|
+
if (uses.size > 0) return { numbers: blamed, uses };
|
|
323
|
+
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));
|
|
324
|
+
const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
|
|
325
|
+
for (const item of rendered.expressions) {
|
|
326
|
+
if (item.address + item.length - 1 > end || item.address < built.start!) continue;
|
|
327
|
+
const assembled = word((address) => built.program![2 + address - built.start!]!, item);
|
|
328
|
+
const original = word(image.byteAt, item);
|
|
329
|
+
if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
|
|
330
|
+
}
|
|
331
|
+
return { numbers: blamed, uses };
|
|
332
|
+
}
|
|
333
|
+
|
|
208
334
|
interface SegmentCheck {
|
|
209
335
|
start: string;
|
|
210
336
|
end: string;
|
|
@@ -214,7 +340,7 @@ interface SegmentCheck {
|
|
|
214
340
|
}
|
|
215
341
|
|
|
216
342
|
/** Compares the assembled program with the image, segment by segment. */
|
|
217
|
-
function check(program: Buffer, start: number, image:
|
|
343
|
+
function check(program: Buffer, start: number, image: ProgramImage): { identical: boolean; segments: SegmentCheck[] } {
|
|
218
344
|
const end = start + program.length - 3;
|
|
219
345
|
const segments = image.segments.map((segment): SegmentCheck => {
|
|
220
346
|
const shown = { start: formatC64Address(segment.start), end: formatC64Address(segment.end) };
|
|
@@ -232,6 +358,57 @@ function check(program: Buffer, start: number, image: Image): { identical: boole
|
|
|
232
358
|
return { identical: segments.every((segment) => segment.result === "identical"), segments };
|
|
233
359
|
}
|
|
234
360
|
|
|
361
|
+
/** What a build with one switch on gives. */
|
|
362
|
+
interface SwitchCheck {
|
|
363
|
+
name: string;
|
|
364
|
+
define: string;
|
|
365
|
+
patches: Array<{ start: string; end: string; bytes: number; insideNames?: string[] }>;
|
|
366
|
+
result: "fits" | "overflow" | "changes outside" | "does not assemble";
|
|
367
|
+
changedBytes?: number;
|
|
368
|
+
outside?: { count: number; first: { address: string; built: number; default: number } };
|
|
369
|
+
overflow?: Array<{ start: string; message: string }>;
|
|
370
|
+
diagnostics?: Diagnostic[];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Builds the source with one switch on and compares it with the default
|
|
375
|
+
* build: only the ranges of its patches may change, and the program keeps its
|
|
376
|
+
* start and length. A patch longer than its range stops ACME with !error.
|
|
377
|
+
*/
|
|
378
|
+
async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ name: string; text: string }>, base: { program: Buffer; start: number }, names: Layout["names"]): Promise<SwitchCheck> {
|
|
379
|
+
const shown: SwitchCheck = {
|
|
380
|
+
name,
|
|
381
|
+
define: `--define ${name}=1`,
|
|
382
|
+
patches: patches.map((patch) => {
|
|
383
|
+
const inside = patch.inner.map((address) => names.get(address)!);
|
|
384
|
+
return { start: formatC64Address(patch.start), end: formatC64Address(patch.end), bytes: patch.end - patch.start + 1, ...(inside.length === 0 ? {} : { insideNames: inside }) };
|
|
385
|
+
}),
|
|
386
|
+
result: "fits",
|
|
387
|
+
};
|
|
388
|
+
const built = await assembleFiles(files, "main.asm", { [name]: 1 });
|
|
389
|
+
if (built.program === undefined) {
|
|
390
|
+
const overflow = built.diagnostics.flatMap((diagnostic) => {
|
|
391
|
+
const found = /The patch \S+ at (\$[0-9a-f]{4}) has .*/.exec(diagnostic.message);
|
|
392
|
+
return found === null ? [] : [{ start: found[1]!, message: found[0] }];
|
|
393
|
+
});
|
|
394
|
+
if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
|
|
395
|
+
return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_LISTED) };
|
|
396
|
+
}
|
|
397
|
+
const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
|
|
398
|
+
let changed = 0;
|
|
399
|
+
let outside: SwitchCheck["outside"];
|
|
400
|
+
const end = Math.max(base.start + base.program.length, built.start! + built.program.length) - 2;
|
|
401
|
+
for (let address = Math.min(base.start, built.start!); address < end; address++) {
|
|
402
|
+
const at = (program: Buffer, start: number) => (address >= start && address < start + program.length - 2 ? program[2 + address - start]! : -1);
|
|
403
|
+
const now = at(built.program, built.start!);
|
|
404
|
+
const before = at(base.program, base.start);
|
|
405
|
+
if (now === before) continue;
|
|
406
|
+
if (inside(address)) changed++;
|
|
407
|
+
else outside = { count: (outside?.count ?? 0) + 1, first: outside?.first ?? { address: formatC64Address(address), built: now, default: before } };
|
|
408
|
+
}
|
|
409
|
+
return outside === undefined ? { ...shown, changedBytes: changed } : { ...shown, result: "changes outside", changedBytes: changed, outside };
|
|
410
|
+
}
|
|
411
|
+
|
|
235
412
|
/** Reads RAM in parts. */
|
|
236
413
|
async function readRam(client: ViceSessionClient, start: number, size: number): Promise<Buffer> {
|
|
237
414
|
const parts: Buffer[] = [];
|
|
@@ -246,18 +423,37 @@ async function readRam(client: ViceSessionClient, start: number, size: number):
|
|
|
246
423
|
* Runs the loader in the emulator to its jump to the entry and compares the
|
|
247
424
|
* machine with the entry state there. At the jump, every byte and register is
|
|
248
425
|
* set and no interrupt has run since the PLP: the 6502 takes one only after
|
|
249
|
-
* the JMP, as the original program can take one right at its entry
|
|
426
|
+
* the JMP, as the original program can take one right at its entry.
|
|
250
427
|
*/
|
|
251
|
-
|
|
428
|
+
/** What the check of the loader gives. `stop`: why the loader did not get to its copier or its jump. */
|
|
429
|
+
interface LoaderRun {
|
|
430
|
+
verified: boolean;
|
|
431
|
+
differences: string[];
|
|
432
|
+
stop?: { pc: string; port: string; cause?: string; history: string[] };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The history entries that a stop of the loader gives, so the agent sees the last instructions. */
|
|
436
|
+
const STOP_HISTORY = 16;
|
|
437
|
+
|
|
438
|
+
async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image: ProgramImage, state: EntryState, jump: number, copierSource: number, expected: (address: number) => number): Promise<LoaderRun> {
|
|
252
439
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
253
440
|
try {
|
|
254
441
|
await client.reset({ mode: "hard", run: false });
|
|
255
442
|
if (!(await autostartLoaded(client, path, program))) return { verified: false, differences: ["The loader was not in memory after autostart."] };
|
|
256
443
|
await client.keyboard(textToPetscii("RUN\n"));
|
|
444
|
+
// A stop before the copier or the jump gives $01, the last instructions and the first byte that is not as planned.
|
|
445
|
+
const stopped = async (pc: number, copied: boolean, what: string): Promise<LoaderRun> => {
|
|
446
|
+
const ram = await readRam(client, 0x0000, 0x10000);
|
|
447
|
+
const port = Buffer.from((await client.memoryRead({ address: 0x0001, size: 1, space: "c64", view: "cpu" })).data, "hex")[0]!;
|
|
448
|
+
const history = (await client.cpuHistory({ limit: STOP_HISTORY, space: "c64" })).entries.map((entry) => `${formatC64Address(entry.address)} ${entry.text}`);
|
|
449
|
+
const cause = stopCause(ram, plan, copierSource, (address) => program[2 + address - 0x0801]!, copied, expected);
|
|
450
|
+
const port01 = `$${port.toString(16).padStart(2, "0")}`;
|
|
451
|
+
return { verified: false, differences: [`The loader did not get to ${what}; it stopped at ${formatC64Address(pc)}.`], stop: { pc: formatC64Address(pc), port: port01, ...(cause === undefined ? {} : { cause }), history } };
|
|
452
|
+
};
|
|
257
453
|
const copier = await client.runUntil({ target: { kind: "address", address: plan.copier, space: "c64" }, timeoutFrames: RUN_LIMIT_FRAMES });
|
|
258
|
-
if (!copier.reached) return
|
|
454
|
+
if (!copier.reached) return await stopped(copier.pc, false, `its copier at ${formatC64Address(plan.copier)}`);
|
|
259
455
|
const atJump = await client.runUntil({ target: { kind: "address", address: jump, space: "c64" }, timeoutFrames: LOADER_FRAMES });
|
|
260
|
-
if (!atJump.reached) return
|
|
456
|
+
if (!atJump.reached) return await stopped(atJump.pc, true, `its jump to the entry at ${formatC64Address(jump)}`);
|
|
261
457
|
const memory = await readRam(client, 0x0000, 0x10000);
|
|
262
458
|
if (memory[jump] !== 0x4c || (memory[jump + 1]! | (memory[jump + 2]! << 8)) !== state.pc) {
|
|
263
459
|
return { verified: false, differences: [`The instruction at ${formatC64Address(jump)} is not JMP ${formatC64Address(state.pc)}.`] };
|
|
@@ -280,7 +476,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
280
476
|
const flags = (["n", "v", "d", "i", "z", "c"] as const).reduce((value, name, index) => value | (registers.flags[name] ? [0x80, 0x40, 0x08, 0x04, 0x02, 0x01][index]! : 0), 0);
|
|
281
477
|
differ("flags", flags, state.flags & 0xcf);
|
|
282
478
|
if (state.vic !== undefined) {
|
|
283
|
-
// The display registers and the raster line
|
|
479
|
+
// The display registers and the raster line: the loader waited for the line, so the jump comes in it or the next one.
|
|
284
480
|
const vic = Buffer.from((await client.ioRead(0xd000, 0x2f)).data, "hex");
|
|
285
481
|
for (const { register, mask } of VIC_DISPLAY_REGISTERS) differ(formatC64Address(0xd000 + register), vic[register]! & mask, state.vic.registers[register]! & mask);
|
|
286
482
|
differ("the VIC bank", Buffer.from((await client.ioRead(0xdd00, 1)).data, "hex")[0]! & 0x03, state.vic.bank);
|
|
@@ -289,7 +485,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
289
485
|
if (line !== wanted && line !== wanted + 1) differences.push(`the raster line is ${line}, not ${wanted}`);
|
|
290
486
|
}
|
|
291
487
|
if (plan.cia !== undefined) {
|
|
292
|
-
// The CIA timers
|
|
488
|
+
// The CIA timers: at the jump a running timer is 3 cycles, the JMP, before its value at the entry.
|
|
293
489
|
const chips = (await client.cia("both")).chips;
|
|
294
490
|
const word = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
295
491
|
const differWord = (what: string, got: number, wanted: number) => got !== wanted && differences.push(`${what} is ${word(got)}, not ${word(wanted)}`);
|
|
@@ -309,12 +505,37 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
309
505
|
}
|
|
310
506
|
}
|
|
311
507
|
|
|
508
|
+
/**
|
|
509
|
+
* The ROMs the program uses: from the coverage when the knowledge has
|
|
510
|
+
* it, else from $01 of the entry state, else both.
|
|
511
|
+
*/
|
|
512
|
+
function romsInUse(image: ProgramImage, coverage: Uint8Array | undefined): RomUse & { from: "coverage" | "entry state" | "none" } {
|
|
513
|
+
if (coverage !== undefined) return { ...romUseOf(coverage, COVERAGE.romExecute), from: "coverage" };
|
|
514
|
+
const port = (image.state?.cpuPort as Record<string, unknown> | undefined)?.$01;
|
|
515
|
+
if (typeof port === "number") return { ...romUseOfPort(port), from: "entry state" };
|
|
516
|
+
return { basic: true, kernal: true, from: "none" };
|
|
517
|
+
}
|
|
518
|
+
|
|
312
519
|
/** The first words of every file that the script writes. */
|
|
313
520
|
const GENERATED_MARK = "; Generated by c64-source";
|
|
314
|
-
/** The file with every range of instructions that no run executed
|
|
521
|
+
/** The file with every range of instructions that no run executed. */
|
|
315
522
|
const NOT_EXECUTED_FILE = "not_executed.json";
|
|
523
|
+
/** The line of each name and address in the source, for links from the documentation. */
|
|
524
|
+
const SOURCE_MAP_FILE = "source-map.json";
|
|
316
525
|
|
|
317
526
|
/** Removes the .asm files of an earlier run that this run does not write, and an earlier loader.prg and not_executed.json; other files stay. */
|
|
527
|
+
/**
|
|
528
|
+
* source-map.json: the line of each name and of each address that the
|
|
529
|
+
* files write, after the mark line. One entry per line of the file, so a diff
|
|
530
|
+
* of two maps stays short.
|
|
531
|
+
*/
|
|
532
|
+
function sourceMapText(rendered: Rendered, space: string, revision: string | null, files: ReadonlyArray<{ name: string }>): string {
|
|
533
|
+
const labels = rendered.labels.map((label) => JSON.stringify({ address: formatC64Address(label.address), name: label.name, file: label.file, line: label.line + 1 }));
|
|
534
|
+
const items = rendered.items.map((item) => JSON.stringify({ start: formatC64Address(item.start), end: formatC64Address(item.end), file: item.file, line: item.line + 1 }));
|
|
535
|
+
const head = JSON.stringify({ space, revision, files: files.map((file) => file.name) }).slice(0, -1);
|
|
536
|
+
return `${head},\n"labels": [\n${labels.join(",\n")}\n],\n"items": [\n${items.join(",\n")}\n]}\n`;
|
|
537
|
+
}
|
|
538
|
+
|
|
318
539
|
function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
319
540
|
const directory = resolveProjectPath(out);
|
|
320
541
|
if (!existsSync(directory)) return;
|
|
@@ -322,7 +543,7 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
|
322
543
|
if (!entry.isFile()) continue;
|
|
323
544
|
const name = entry.name;
|
|
324
545
|
const path = join(directory, name);
|
|
325
|
-
if (name === "loader.prg" || name === NOT_EXECUTED_FILE) rmSync(path, { force: true });
|
|
546
|
+
if (name === "loader.prg" || name === NOT_EXECUTED_FILE || name === SOURCE_MAP_FILE) rmSync(path, { force: true });
|
|
326
547
|
else if (name.endsWith(".asm") && !keep.has(name) && readFileSync(path, "utf8").startsWith(GENERATED_MARK)) rmSync(path, { force: true });
|
|
327
548
|
}
|
|
328
549
|
}
|
|
@@ -332,15 +553,22 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
332
553
|
args: argv,
|
|
333
554
|
strict: true,
|
|
334
555
|
allowPositionals: true,
|
|
335
|
-
options: {
|
|
556
|
+
options: {
|
|
557
|
+
out: { type: "string" },
|
|
558
|
+
entry: { type: "string", multiple: true },
|
|
559
|
+
space: { type: "string" },
|
|
560
|
+
loader: { type: "boolean" },
|
|
561
|
+
"analyzer-names": { type: "boolean" },
|
|
562
|
+
help: { type: "boolean" },
|
|
563
|
+
},
|
|
336
564
|
});
|
|
337
565
|
if (values.help) throw new UsageError("");
|
|
338
|
-
if (positionals.length !==
|
|
566
|
+
if (positionals.length !== 0) throw new UsageError("generate takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
|
|
339
567
|
if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
|
|
340
568
|
try {
|
|
341
569
|
resolveProjectPath(values.out);
|
|
342
570
|
} catch {
|
|
343
|
-
throw new UsageError(`--out must be a path relative to the project
|
|
571
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
344
572
|
}
|
|
345
573
|
const entries = (values.entry ?? []).map((value) => {
|
|
346
574
|
try {
|
|
@@ -351,16 +579,17 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
351
579
|
});
|
|
352
580
|
const space = values.space ?? DEFAULT_SPACE;
|
|
353
581
|
if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
354
|
-
if (space !== DEFAULT_SPACE &&
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const
|
|
582
|
+
if (space !== DEFAULT_SPACE && values.loader) throw new UsageError("--loader works only in space c64");
|
|
583
|
+
const knowledge = readKnowledge(space, values["analyzer-names"] === true);
|
|
584
|
+
const image = knowledge.program;
|
|
585
|
+
const coverage = knowledge.coverage;
|
|
586
|
+
if (values.loader && (image.state === undefined || image.entry === undefined)) {
|
|
587
|
+
throw new UsageError("--loader needs the entry and the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json>");
|
|
588
|
+
}
|
|
589
|
+
// Read the entry state before the long steps, so a bad state stops the run at once.
|
|
590
|
+
const state = values.loader ? readEntryState(JSON.stringify(image.state), "the entry state of the knowledge") : undefined;
|
|
591
|
+
const romUse = romsInUse(image, coverage);
|
|
592
|
+
const instructions = await decode(image, knowledge, entries, coverage);
|
|
364
593
|
const laid = layout({
|
|
365
594
|
segments: image.segments,
|
|
366
595
|
byteAt: image.byteAt,
|
|
@@ -371,50 +600,84 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
371
600
|
comments: knowledge.comments,
|
|
372
601
|
modules: knowledge.modules,
|
|
373
602
|
referenceTargets: knowledge.references.map((reference) => reference.to),
|
|
603
|
+
operands: knowledge.operands,
|
|
604
|
+
patches: knowledge.patches,
|
|
605
|
+
macros: knowledge.macros,
|
|
606
|
+
macroUses: knowledge.macroUses,
|
|
374
607
|
...(coverage === undefined ? {} : { coverage }),
|
|
375
|
-
// The platform names are C64 names: drive code and banks get none.
|
|
376
|
-
...(space === DEFAULT_SPACE ? { platform:
|
|
608
|
+
// The platform names are C64 names: drive code and banks get none. A ROM that the program does not use gives none.
|
|
609
|
+
...(space === DEFAULT_SPACE ? { platform: platformNamesFor(romUse), describe: describeAddress, decode: decodeRegister } : {}),
|
|
377
610
|
});
|
|
378
611
|
const asBytes = await probe(laid);
|
|
379
|
-
const header = [`Generated by c64-source from ${
|
|
380
|
-
const rendered = render(laid, asBytes, header);
|
|
381
|
-
const files = [...rendered.files];
|
|
612
|
+
const header = [`Generated by c64-source from knowledge revision ${knowledge.revision}${space === DEFAULT_SPACE ? "" : ` in space ${space}`}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
|
|
382
613
|
let loader: LoaderPlan | undefined;
|
|
383
|
-
// An image without its entry has no program: the source is still written and checked, but the run fails
|
|
614
|
+
// An image without its entry has no program: the source is still written and checked, but the run fails.
|
|
384
615
|
const missingEntry = entryOutsideImage(image.entry, image.segments);
|
|
385
616
|
// A loader that cannot be planned does not stop the source: the source is written and checked, and the result says why.
|
|
386
617
|
let loaderRefused: string | undefined = state === undefined ? undefined : missingEntry;
|
|
387
618
|
if (state !== undefined && missingEntry === undefined) {
|
|
388
619
|
const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
|
|
389
620
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
621
|
+
const entryName = laid.names.get(image.entry!);
|
|
622
|
+
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 knowledge revision ${knowledge.revision}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
|
|
392
623
|
} catch (error) {
|
|
393
624
|
if (!(error instanceof WireFailure)) throw error;
|
|
394
625
|
loaderRefused = error.message;
|
|
395
626
|
}
|
|
396
627
|
}
|
|
397
|
-
// Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again
|
|
398
|
-
|
|
628
|
+
// Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again.
|
|
629
|
+
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}` }));
|
|
630
|
+
const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
|
|
631
|
+
// An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and a macro use as its
|
|
632
|
+
// instructions; then the source is assembled again.
|
|
633
|
+
const numbers = new Map<number, string>();
|
|
634
|
+
const asInstructions = new Map<number, string>();
|
|
635
|
+
let rendered = render(laid, asBytes, header, numbers, asInstructions);
|
|
636
|
+
if (laid.macroUses.size > 0) for (const [start, reason] of await probeUses(laid, rendered, image)) asInstructions.set(start, reason);
|
|
637
|
+
if (asInstructions.size > 0) rendered = render(laid, asBytes, header, numbers, asInstructions);
|
|
638
|
+
let files = marked([...rendered.files, ...loaderFiles]);
|
|
639
|
+
let built = await assembleFiles(files, "main.asm");
|
|
640
|
+
for (let round = 0; round < ROUNDS && (rendered.expressions.length > 0 || rendered.uses.length > 0); round++) {
|
|
641
|
+
const blamed = blame(rendered, built, image, 1);
|
|
642
|
+
if (blamed.numbers.size === 0 && blamed.uses.size === 0) break;
|
|
643
|
+
for (const [address, reason] of blamed.numbers) numbers.set(address, reason);
|
|
644
|
+
for (const [start, reason] of blamed.uses) asInstructions.set(start, reason);
|
|
645
|
+
rendered = render(laid, asBytes, header, numbers, asInstructions);
|
|
646
|
+
files = marked([...rendered.files, ...loaderFiles]);
|
|
647
|
+
built = await assembleFiles(files, "main.asm");
|
|
648
|
+
}
|
|
399
649
|
removeOldFiles(values.out, new Set(files.map((file) => file.name)));
|
|
400
650
|
for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
|
|
651
|
+
writeProjectFile(`${values.out}/${SOURCE_MAP_FILE}`, Buffer.from(sourceMapText(rendered, space, knowledge.revision, files), "utf8"), { what: "--out" });
|
|
401
652
|
const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
402
653
|
const inData = entriesInData(entries, knowledge.regions);
|
|
403
654
|
if (rendered.notExecuted !== undefined) {
|
|
404
|
-
|
|
655
|
+
// Each range with the file of the source that holds it and the routine before it, as a report groups them.
|
|
656
|
+
const routines = knowledge.symbols.filter((symbol) => symbol.kind === "routine").sort((a, b) => a.address - b.address);
|
|
657
|
+
const placed = rendered.notExecuted.ranges.map((range) => {
|
|
658
|
+
const file = laid.pieces.find((piece) => range.start >= piece.start && range.start <= piece.end)?.file;
|
|
659
|
+
const routine = routines.filter((symbol) => symbol.address <= range.start).at(-1)?.name;
|
|
660
|
+
return { ...showRange(range), ...(file === undefined ? {} : { file }), ...(routine === undefined ? {} : { routine }) };
|
|
661
|
+
});
|
|
662
|
+
const files: Record<string, number> = {};
|
|
663
|
+
for (const range of placed) if (range.file !== undefined) files[range.file] = (files[range.file] ?? 0) + 1;
|
|
664
|
+
const all = { revision: knowledge.revision, instructions: rendered.notExecuted.instructions, files, ranges: placed };
|
|
405
665
|
writeProjectFile(`${values.out}/${NOT_EXECUTED_FILE}`, Buffer.from(`${JSON.stringify(all, null, 2)}\n`, "utf8"), { what: "--out" });
|
|
406
666
|
}
|
|
407
667
|
|
|
408
668
|
const output: Record<string, unknown> = {
|
|
409
669
|
out: values.out,
|
|
410
670
|
files: files.map((file) => file.name),
|
|
671
|
+
sourceMap: `${values.out}/${SOURCE_MAP_FILE}`,
|
|
411
672
|
revision: knowledge.revision,
|
|
412
673
|
instructions: rendered.instructions,
|
|
413
674
|
dataBytes: rendered.dataBytes,
|
|
414
675
|
...(rendered.undecodedBytes === 0 ? {} : { undecodedBytes: rendered.undecodedBytes }),
|
|
676
|
+
// The ROM names that the source leaves out, and the evidence.
|
|
677
|
+
...(space !== DEFAULT_SPACE || (romUse.basic && romUse.kernal) ? {} : { romNamesLeftOut: { roms: [...(romUse.basic ? [] : ["BASIC"]), ...(romUse.kernal ? [] : ["KERNAL"])], ...(romUse.basic || romUse.kernal ? {} : { systemArea: true }), from: romUse.from } }),
|
|
415
678
|
...(missingEntry === undefined ? {} : { entryOutside: { entry: formatC64Address(image.entry!), reason: missingEntry } }),
|
|
416
679
|
labels: { named: laid.namedLabels, generated: laid.generatedLabels },
|
|
417
|
-
// An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it
|
|
680
|
+
// An --entry in a data region decodes nothing: the result says so, and a code region in the knowledge changes it.
|
|
418
681
|
...(inData.length === 0
|
|
419
682
|
? {}
|
|
420
683
|
: { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
|
|
@@ -432,18 +695,50 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
432
695
|
},
|
|
433
696
|
}),
|
|
434
697
|
...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
|
|
698
|
+
...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
|
|
699
|
+
...(rendered.operandsAsNumbers.length === 0
|
|
700
|
+
? {}
|
|
701
|
+
: {
|
|
702
|
+
operandsAsNumbers: {
|
|
703
|
+
count: rendered.operandsAsNumbers.length,
|
|
704
|
+
listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
|
|
705
|
+
},
|
|
706
|
+
}),
|
|
707
|
+
...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
|
|
708
|
+
// A macro use that the source writes as its instructions does not fail the run: the source stays identical.
|
|
709
|
+
...(rendered.macrosAsInstructions.length === 0
|
|
710
|
+
? {}
|
|
711
|
+
: {
|
|
712
|
+
macrosAsInstructions: {
|
|
713
|
+
count: rendered.macrosAsInstructions.length,
|
|
714
|
+
listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
|
|
715
|
+
},
|
|
716
|
+
}),
|
|
717
|
+
// A patch that the source cannot write fails the run: the switch would miss a part.
|
|
718
|
+
...(laid.patchesNotWritten.length === 0
|
|
719
|
+
? {}
|
|
720
|
+
: {
|
|
721
|
+
patchesNotWritten: {
|
|
722
|
+
count: laid.patchesNotWritten.length,
|
|
723
|
+
listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
|
|
724
|
+
},
|
|
725
|
+
}),
|
|
435
726
|
};
|
|
436
727
|
// The loader includes the source, so it is built only from a source that is identical to the image.
|
|
437
728
|
const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
|
|
438
|
-
const built = await assembleFiles(files, "main.asm");
|
|
439
729
|
if (built.program === undefined) {
|
|
440
730
|
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
|
|
441
731
|
}
|
|
442
732
|
const checked = check(built.program, built.start!, image);
|
|
443
733
|
output.check = checked;
|
|
444
734
|
if (!checked.identical) return { output: { ...output, ...notBuilt("The source does not assemble to the image, and the loader includes it.") }, failed: true };
|
|
735
|
+
// Each switch on alone, against the default build; combinations are not built.
|
|
736
|
+
const switches: SwitchCheck[] = [];
|
|
737
|
+
for (const [name, patches] of switchesOf(laid)) switches.push(await checkSwitch(name, patches, files, { program: built.program, start: built.start! }, laid.names));
|
|
738
|
+
if (switches.length > 0) output.switches = switches;
|
|
739
|
+
const sourceFailed = laid.patchesNotWritten.length > 0 || switches.some((item) => item.result !== "fits");
|
|
445
740
|
if (loaderRefused !== undefined) return { output: { ...output, loader: { verified: false, reason: loaderRefused } }, failed: true };
|
|
446
|
-
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined };
|
|
741
|
+
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
|
|
447
742
|
|
|
448
743
|
const loaderBuilt = await assembleFiles(files, "loader.asm");
|
|
449
744
|
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 };
|
|
@@ -467,22 +762,25 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
467
762
|
const loaderPath = `${values.out}/loader.prg`;
|
|
468
763
|
writeProjectFile(loaderPath, program, { what: "--out" });
|
|
469
764
|
(output.files as string[]).push("loader.prg");
|
|
470
|
-
const
|
|
765
|
+
const copierSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === COPIER_SOURCE);
|
|
766
|
+
if (copierSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${COPIER_SOURCE} label. This is a defect of the c64-source script.`);
|
|
767
|
+
const copierSource = copierSymbol.kind === "address" ? copierSymbol.address : copierSymbol.value;
|
|
768
|
+
const ran = await runLoader(loaderPath, program, loader, image, state, jump, copierSource, expected);
|
|
471
769
|
output.loader = {
|
|
472
770
|
program: loaderPath,
|
|
473
771
|
copies: loader.copies.map((copy) => ({ what: copy.what, to: `${formatC64Address(copy.target.start)}-${formatC64Address(copy.target.end)}` })),
|
|
474
772
|
copier: formatC64Address(loader.copier),
|
|
475
773
|
stackByte: formatC64Address(loader.stackByte),
|
|
476
774
|
...ran,
|
|
477
|
-
// What the loader cannot set as it was at the entry
|
|
478
|
-
...(loader.
|
|
775
|
+
// What the loader cannot set as it was at the entry.
|
|
776
|
+
...(loaderLimits(loader).length === 0 ? {} : { limits: loaderLimits(loader) }),
|
|
479
777
|
};
|
|
480
|
-
return { output, failed: !ran.verified };
|
|
778
|
+
return { output, failed: !ran.verified || sourceFailed };
|
|
481
779
|
}
|
|
482
780
|
|
|
483
781
|
await runScript(
|
|
484
|
-
async () => {
|
|
485
|
-
const { output, failed } = await run(
|
|
782
|
+
async (argv) => {
|
|
783
|
+
const { output, failed } = await run(argv);
|
|
486
784
|
if (failed) process.exitCode = 1;
|
|
487
785
|
return output;
|
|
488
786
|
},
|