@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +192 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +133 -0
- package/skills/c64-source/scripts/generate.ts +503 -0
- package/skills/c64-source/scripts/loader.ts +589 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +673 -0
- package/skills/c64-static-analysis/SKILL.md +120 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/history.ts +169 -0
- package/src/knowledge/import.ts +392 -0
- package/src/knowledge/read.ts +392 -0
- package/src/knowledge/schema.ts +250 -0
- package/src/knowledge/write.ts +837 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
// ACME source from an image, current knowledge and Ghidra's instruction
|
|
2
|
+
// boundaries (D48). The same input gives the same text. Names come from
|
|
3
|
+
// knowledge; a target that knowledge does not name gets l_xxxx. Data is
|
|
4
|
+
// written by its region type. The source assembles to the same bytes: the
|
|
5
|
+
// generator forces operand sizes, and writes an opcode form that ACME
|
|
6
|
+
// encodes differently as bytes (see the probe in generate.ts). An operand
|
|
7
|
+
// expression of the knowledge is written in place of its number (D65).
|
|
8
|
+
|
|
9
|
+
import { COVERAGE, formatC64Address } from "#src/c64.ts";
|
|
10
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
11
|
+
import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
|
|
12
|
+
import { ANALYZERS } from "#src/knowledge/import.ts";
|
|
13
|
+
import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
|
|
14
|
+
import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, type Mode } from "./opcodes.ts";
|
|
15
|
+
|
|
16
|
+
export interface Range {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SourceInput {
|
|
22
|
+
/** In address order, without overlap. */
|
|
23
|
+
segments: Range[];
|
|
24
|
+
/** The image byte at an address inside a segment. */
|
|
25
|
+
byteAt: (address: number) => number;
|
|
26
|
+
entry?: number;
|
|
27
|
+
/** Where Ghidra decoded an instruction, with its length. */
|
|
28
|
+
instructions: ReadonlyMap<number, number>;
|
|
29
|
+
/** Without an origin, a symbol counts as a user or LLM name. */
|
|
30
|
+
symbols: ReadonlyArray<{ address: number; name: string; origin?: Origin }>;
|
|
31
|
+
regions: ReadonlyArray<{ start: number; end: number; type: RegionType; element?: number }>;
|
|
32
|
+
comments: ReadonlyArray<{ address: number; placement: CommentPlacement; text: string }>;
|
|
33
|
+
modules: ReadonlyArray<{ start: number; file: string; title?: string }>;
|
|
34
|
+
/** Targets of knowledge references: each one inside the image gets a label. */
|
|
35
|
+
referenceTargets: readonly number[];
|
|
36
|
+
/** Joined coverage files (D40): an instruction that no run executed is marked (D49). */
|
|
37
|
+
coverage?: Uint8Array;
|
|
38
|
+
/** Platform names (D52): the name of an address that knowledge does not name, and the last address of its entry. */
|
|
39
|
+
platform?: ReadonlyArray<{ address: number; end: number; name: string }>;
|
|
40
|
+
/** Operand expressions of the knowledge (D65), by instruction address. */
|
|
41
|
+
operands?: ReadonlyArray<OperandExpression & { address: number }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An operand expression that the source writes as a number, with its text and why. */
|
|
45
|
+
export interface OperandAsNumber {
|
|
46
|
+
address: number;
|
|
47
|
+
expression: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CODE_TYPES: readonly RegionType[] = ["code", "jumps", "dual-use"];
|
|
52
|
+
const BYTES_PER_LINE = 8;
|
|
53
|
+
const WORDS_PER_LINE = 4;
|
|
54
|
+
const CHARS_PER_LINE = 32;
|
|
55
|
+
const MIN_FILL = 16;
|
|
56
|
+
const SIDE_COLUMN = 40;
|
|
57
|
+
const INDENT = " ";
|
|
58
|
+
|
|
59
|
+
type Region = SourceInput["regions"][number];
|
|
60
|
+
|
|
61
|
+
interface InstructionItem {
|
|
62
|
+
kind: "instruction";
|
|
63
|
+
address: number;
|
|
64
|
+
length: number;
|
|
65
|
+
opcode: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface DataItem {
|
|
69
|
+
kind: "data";
|
|
70
|
+
start: number;
|
|
71
|
+
end: number;
|
|
72
|
+
/** The region type, or unclassified. In a code region, bytes that Ghidra did not decode. */
|
|
73
|
+
type: RegionType | "unclassified" | "undecoded";
|
|
74
|
+
region?: Region;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type Item = InstructionItem | DataItem;
|
|
78
|
+
|
|
79
|
+
export interface Piece {
|
|
80
|
+
start: number;
|
|
81
|
+
end: number;
|
|
82
|
+
file: string;
|
|
83
|
+
title: string;
|
|
84
|
+
/** True when the piece starts a segment: the source sets the address there. */
|
|
85
|
+
segmentStart: boolean;
|
|
86
|
+
items: Item[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Layout {
|
|
90
|
+
pieces: Piece[];
|
|
91
|
+
/** Names of addresses: inside the image a label, outside it a constant. */
|
|
92
|
+
names: Map<number, string>;
|
|
93
|
+
/** Addresses outside the image that the source names, as constants. */
|
|
94
|
+
constants: number[];
|
|
95
|
+
inImage: (address: number) => boolean;
|
|
96
|
+
byteAt: (address: number) => number;
|
|
97
|
+
comments: SourceInput["comments"];
|
|
98
|
+
coverage?: Uint8Array;
|
|
99
|
+
generatedLabels: number;
|
|
100
|
+
namedLabels: number;
|
|
101
|
+
unusedModules: Array<{ start: number; file: string }>;
|
|
102
|
+
/** The operand of an instruction as an expression with the names of this source (D65). */
|
|
103
|
+
operands: Map<number, string>;
|
|
104
|
+
/** Operand expressions that cannot be written. */
|
|
105
|
+
operandsAsNumbers: OperandAsNumber[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
109
|
+
const hex4 = (value: number) => formatC64Address(value);
|
|
110
|
+
/** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
|
|
111
|
+
const constantValue = (address: number) => (address < 0x100 ? hex2(address) : hex4(address));
|
|
112
|
+
const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
|
|
113
|
+
|
|
114
|
+
function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
|
|
115
|
+
const { mode } = OPCODES[opcode]!;
|
|
116
|
+
const low = MODE_LENGTH[mode] >= 2 ? byteAt(address + 1) : 0;
|
|
117
|
+
if (mode === "imp") return undefined;
|
|
118
|
+
if (mode === "rel") return (address + 2 + signed(low)) & 0xffff;
|
|
119
|
+
if (MODE_LENGTH[mode] === 3) return low | (byteAt(address + 2) << 8);
|
|
120
|
+
return low;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Each entry point inside a data region of the knowledge, with that region.
|
|
125
|
+
* A data region wins over an entry (D44, D48): Ghidra does not decode there,
|
|
126
|
+
* and the source has data there. A code region in the knowledge changes that (F-27).
|
|
127
|
+
*/
|
|
128
|
+
export function entriesInData<R extends { start: number; end: number; type: RegionType }>(entries: readonly number[], regions: readonly R[]): Array<{ entry: number; region: R }> {
|
|
129
|
+
return entries.flatMap((entry) => {
|
|
130
|
+
const region = regions.find((row) => entry >= row.start && entry <= row.end && !CODE_TYPES.includes(row.type));
|
|
131
|
+
return region === undefined ? [] : [{ entry, region }];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Why an image without its entry fails: the image does not hold the program (R2-02). Undefined when the entry is in a segment, or the image has no entry. */
|
|
136
|
+
export function entryOutsideImage(entry: number | undefined, segments: readonly Range[]): string | undefined {
|
|
137
|
+
if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
|
|
138
|
+
return `The entry ${hex4(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Splits the segments into pieces at module starts and lays out instructions and data. */
|
|
142
|
+
export function layout(input: SourceInput): Layout {
|
|
143
|
+
const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
|
|
144
|
+
const regionAt = (address: number) => input.regions.find((region) => address >= region.start && address <= region.end);
|
|
145
|
+
const isData = (address: number) => {
|
|
146
|
+
const region = regionAt(address);
|
|
147
|
+
return region !== undefined && !CODE_TYPES.includes(region.type);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (const module of input.modules) {
|
|
151
|
+
if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${hex4(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
|
|
152
|
+
}
|
|
153
|
+
const modules = [...input.modules].sort((a, b) => a.start - b.start);
|
|
154
|
+
const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
|
|
155
|
+
|
|
156
|
+
const pieces: Piece[] = [];
|
|
157
|
+
for (const segment of input.segments) {
|
|
158
|
+
const starts = modules.filter((module) => module.start > segment.start && module.start <= segment.end).map((module) => module.start);
|
|
159
|
+
const bounds = [segment.start, ...starts, segment.end + 1];
|
|
160
|
+
for (let index = 0; index + 1 < bounds.length; index++) {
|
|
161
|
+
const start = bounds[index]!;
|
|
162
|
+
const end = bounds[index + 1]! - 1;
|
|
163
|
+
const module = modules.find((row) => row.start === start);
|
|
164
|
+
const file = module?.file ?? `segment_${start.toString(16).padStart(4, "0")}.asm`;
|
|
165
|
+
const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${hex4(segment.start)}-${hex4(segment.end)}`;
|
|
166
|
+
pieces.push({ start, end, file, title, segmentStart: index === 0, items: [] });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const files = new Map<string, number>();
|
|
170
|
+
for (const piece of pieces) {
|
|
171
|
+
const other = files.get(piece.file);
|
|
172
|
+
if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${hex4(other)} and at ${hex4(piece.start)}. Give the module another file.`);
|
|
173
|
+
files.set(piece.file, piece.start);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const piece of pieces) {
|
|
177
|
+
let address = piece.start;
|
|
178
|
+
while (address <= piece.end) {
|
|
179
|
+
const region = regionAt(address);
|
|
180
|
+
const codeRegion = region !== undefined && CODE_TYPES.includes(region.type);
|
|
181
|
+
const length = input.instructions.get(address);
|
|
182
|
+
const opcode = input.byteAt(address);
|
|
183
|
+
const fits =
|
|
184
|
+
length !== undefined &&
|
|
185
|
+
(codeRegion || region === undefined) &&
|
|
186
|
+
MODE_LENGTH[OPCODES[opcode]!.mode] === length &&
|
|
187
|
+
address + length - 1 <= piece.end &&
|
|
188
|
+
Array.from({ length }, (_, offset) => address + offset).every((at) => !isData(at));
|
|
189
|
+
if (fits) {
|
|
190
|
+
piece.items.push({ kind: "instruction", address, length: length!, opcode });
|
|
191
|
+
address += length!;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// A data run: one region, or unclassified bytes up to the next decoded instruction.
|
|
195
|
+
const type: DataItem["type"] = codeRegion ? "undecoded" : (region?.type ?? "unclassified");
|
|
196
|
+
let end = address;
|
|
197
|
+
const same = (at: number) => {
|
|
198
|
+
if (at > piece.end) return false;
|
|
199
|
+
const next = regionAt(at);
|
|
200
|
+
if (region !== undefined && !codeRegion) return next === region;
|
|
201
|
+
if (codeRegion) return next === region && !input.instructions.has(at);
|
|
202
|
+
return next === undefined && !input.instructions.has(at);
|
|
203
|
+
};
|
|
204
|
+
while (same(end + 1)) end++;
|
|
205
|
+
const last = piece.items.at(-1);
|
|
206
|
+
if (last?.kind === "data" && last.type === type && last.region === region && last.end + 1 === address) last.end = end;
|
|
207
|
+
else piece.items.push({ kind: "data", start: address, end, type, ...(region === undefined ? {} : { region }) });
|
|
208
|
+
address = end + 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Labels: every address inside the image that an operand, a table, a symbol, a reference or the entry names.
|
|
213
|
+
const wanted = new Set<number>();
|
|
214
|
+
const want = (address: number) => inImage(address) && wanted.add(address);
|
|
215
|
+
const outside = new Set<number>();
|
|
216
|
+
// A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
|
|
217
|
+
const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
|
|
218
|
+
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
|
|
219
|
+
// An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins (D61).
|
|
220
|
+
const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
|
|
221
|
+
const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
|
|
222
|
+
/** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry (F-20). */
|
|
223
|
+
const insideEntry = new Map<number, number>();
|
|
224
|
+
const use = (address: number) => {
|
|
225
|
+
if (inImage(address)) wanted.add(address);
|
|
226
|
+
else if (symbols.has(address) || platform.has(address)) outside.add(address);
|
|
227
|
+
else {
|
|
228
|
+
const entry = input.platform?.find((row) => address > row.address && address <= row.end && !inImage(row.address) && (symbols.has(row.address) || platform.has(row.address)));
|
|
229
|
+
if (entry === undefined) return;
|
|
230
|
+
outside.add(entry.address);
|
|
231
|
+
insideEntry.set(address, entry.address);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// The names of an operand expression are found by name, a knowledge symbol before a platform name, and written with the name the source gives that address (D65).
|
|
235
|
+
const byName = new Map<string, number>();
|
|
236
|
+
for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
|
|
237
|
+
for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
|
|
238
|
+
const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
|
|
239
|
+
const expressions: Array<{ address: number; text: string }> = [];
|
|
240
|
+
const operandsAsNumbers: OperandAsNumber[] = [];
|
|
241
|
+
for (const operand of [...(input.operands ?? [])].sort((a, b) => a.address - b.address)) {
|
|
242
|
+
const expression = operandText(operand);
|
|
243
|
+
const item = instructionAt.get(operand.address);
|
|
244
|
+
const mode = item === undefined ? undefined : OPCODES[item.opcode]!.mode;
|
|
245
|
+
if (mode === undefined || mode === "imp" || mode === "rel") {
|
|
246
|
+
operandsAsNumbers.push({ address: operand.address, expression, reason: "The source has no instruction with an operand value at this address." });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const missing = identifiers(expression).filter((name) => !byName.has(name));
|
|
250
|
+
if (missing.length > 0) {
|
|
251
|
+
operandsAsNumbers.push({ address: operand.address, expression, reason: `No symbol and no platform name is ${missing.join(", ")}.` });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
expressions.push({ address: operand.address, text: expression });
|
|
255
|
+
}
|
|
256
|
+
for (const { text: expression } of expressions) for (const name of identifiers(expression)) use(byName.get(name)!);
|
|
257
|
+
// An instruction with an expression names what the expression names, not its plain target (D65).
|
|
258
|
+
const written = new Set(expressions.map((expression) => expression.address));
|
|
259
|
+
for (const piece of pieces) {
|
|
260
|
+
for (const item of piece.items) {
|
|
261
|
+
if (item.kind === "instruction") {
|
|
262
|
+
if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
for (const pointer of pointersOf(item, input.byteAt)) use(pointer);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const symbol of input.symbols) want(symbol.address);
|
|
269
|
+
for (const target of input.referenceTargets) want(target);
|
|
270
|
+
if (input.entry !== undefined) want(input.entry);
|
|
271
|
+
|
|
272
|
+
const names = new Map<number, string>();
|
|
273
|
+
const taken = new Set([...knowledgeNames, ...platform.values()]);
|
|
274
|
+
let generatedLabels = 0;
|
|
275
|
+
let namedLabels = 0;
|
|
276
|
+
for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
|
|
277
|
+
const known = symbols.get(address);
|
|
278
|
+
if (known !== undefined) {
|
|
279
|
+
if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${hex4(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
|
|
280
|
+
names.set(address, known);
|
|
281
|
+
if (inImage(address)) namedLabels++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const named = platform.get(address);
|
|
285
|
+
if (named !== undefined) {
|
|
286
|
+
names.set(address, named);
|
|
287
|
+
if (inImage(address)) namedLabels++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
let name = `l_${address.toString(16).padStart(4, "0")}`;
|
|
291
|
+
while (taken.has(name)) name = `${name}_`;
|
|
292
|
+
taken.add(name);
|
|
293
|
+
names.set(address, name);
|
|
294
|
+
generatedLabels++;
|
|
295
|
+
}
|
|
296
|
+
// The high byte of a vector is NMINV+1. A zero-page entry keeps zero-page instructions: its constant has two digits.
|
|
297
|
+
for (const [address, start] of insideEntry) names.set(address, `${names.get(start)!}+${address - start}`);
|
|
298
|
+
const operands = new Map<number, string>();
|
|
299
|
+
for (const { address, text } of expressions) {
|
|
300
|
+
const unnamed: string[] = [];
|
|
301
|
+
const written = replaceIdentifiers(text, (name) => {
|
|
302
|
+
const label = names.get(byName.get(name)!);
|
|
303
|
+
if (label === undefined) unnamed.push(name);
|
|
304
|
+
return label === undefined ? name : label.includes("+") ? `(${label})` : label;
|
|
305
|
+
});
|
|
306
|
+
if (unnamed.length === 0) operands.set(address, written);
|
|
307
|
+
else operandsAsNumbers.push({ address, expression: text, reason: `The source has no name for ${unnamed.join(", ")}.` });
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
pieces,
|
|
311
|
+
names,
|
|
312
|
+
constants: [...outside].sort((a, b) => a - b),
|
|
313
|
+
inImage,
|
|
314
|
+
byteAt: input.byteAt,
|
|
315
|
+
comments: input.comments,
|
|
316
|
+
...(input.coverage === undefined ? {} : { coverage: input.coverage }),
|
|
317
|
+
generatedLabels,
|
|
318
|
+
namedLabels,
|
|
319
|
+
unusedModules,
|
|
320
|
+
operands,
|
|
321
|
+
operandsAsNumbers: operandsAsNumbers.sort((a, b) => a.address - b.address),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The pointer region types: a table of addresses, or of addresses minus 1 for RTS. */
|
|
326
|
+
const POINTER_TYPES: ReadonlyArray<DataItem["type"]> = ["addresses", "addresses-split", "rts-addresses", "rts-addresses-split"];
|
|
327
|
+
|
|
328
|
+
/** The pairs of a pointer table: the address of each low and high byte, and the target it names. */
|
|
329
|
+
function pairsOf(region: Region, byteAt: (address: number) => number): Array<{ low: number; high: number; target: number }> {
|
|
330
|
+
const length = region.end - region.start + 1;
|
|
331
|
+
const split = region.type === "addresses-split" || region.type === "rts-addresses-split";
|
|
332
|
+
const minus = region.type === "rts-addresses" || region.type === "rts-addresses-split" ? 1 : 0;
|
|
333
|
+
const count = Math.floor(length / 2);
|
|
334
|
+
return Array.from({ length: count }, (_, index) => {
|
|
335
|
+
const low = split ? region.start + index : region.start + 2 * index;
|
|
336
|
+
const high = split ? region.start + count + index : low + 1;
|
|
337
|
+
return { low, high, target: ((byteAt(low) | (byteAt(high) << 8)) + minus) & 0xffff };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function pointersOf(item: DataItem, byteAt: (address: number) => number): number[] {
|
|
342
|
+
if (item.region === undefined || !POINTER_TYPES.includes(item.type)) return [];
|
|
343
|
+
return pairsOf(item.region, byteAt)
|
|
344
|
+
.filter((pair) => (pair.low >= item.start && pair.low <= item.end) || (pair.high >= item.start && pair.high <= item.end))
|
|
345
|
+
.map((pair) => pair.target);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** How an instruction is written: the text with a given operand expression, and the key of its opcode form for the probe. */
|
|
349
|
+
export interface InstructionForm {
|
|
350
|
+
key: string;
|
|
351
|
+
opcode: number;
|
|
352
|
+
suffix: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** The operand size suffix: +2 forces an absolute operand below $0100, +1 a zero-page operand that is a label of the image. */
|
|
356
|
+
export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: InstructionItem): InstructionForm {
|
|
357
|
+
const { mnemonic, mode } = OPCODES[item.opcode]!;
|
|
358
|
+
const value = takesAddress(mode) ? operandValue(item.address, item.opcode, layout.byteAt)! : 0;
|
|
359
|
+
let suffix = "";
|
|
360
|
+
if ((mode === "abs" || mode === "abx" || mode === "aby") && value < 0x100 && hasZeroPageForm(mnemonic, mode)) suffix = "+2";
|
|
361
|
+
if ((mode === "zp" || mode === "zpx" || mode === "zpy") && layout.inImage(value)) suffix = "+1";
|
|
362
|
+
return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The instruction text with an operand expression; the expression is a name or a $ number. */
|
|
366
|
+
export function instructionText(opcode: number, suffix: string, expression: string): string {
|
|
367
|
+
const { mnemonic, mode } = OPCODES[opcode]!;
|
|
368
|
+
const operand: Record<Mode, string> = {
|
|
369
|
+
imp: "",
|
|
370
|
+
imm: `#${expression}`,
|
|
371
|
+
zp: expression,
|
|
372
|
+
zpx: `${expression},x`,
|
|
373
|
+
zpy: `${expression},y`,
|
|
374
|
+
abs: expression,
|
|
375
|
+
abx: `${expression},x`,
|
|
376
|
+
aby: `${expression},y`,
|
|
377
|
+
ind: `(${expression})`,
|
|
378
|
+
izx: `(${expression},x)`,
|
|
379
|
+
izy: `(${expression}),y`,
|
|
380
|
+
rel: expression,
|
|
381
|
+
};
|
|
382
|
+
const text = operand[mode];
|
|
383
|
+
return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** The plain number of an operand: what the probe assembles, and what a comment shows. */
|
|
387
|
+
export function literalOperand(opcode: number, value: number): string {
|
|
388
|
+
const { mode } = OPCODES[opcode]!;
|
|
389
|
+
return MODE_LENGTH[mode] === 3 || mode === "rel" ? hex4(value) : hex2(value);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One instruction of each form, for the probe: the form, and the instruction bytes. */
|
|
393
|
+
export function probeForms(layout: Layout): Array<{ form: InstructionForm; bytes: number[] }> {
|
|
394
|
+
const forms = new Map<string, { form: InstructionForm; bytes: number[] }>();
|
|
395
|
+
for (const piece of layout.pieces) {
|
|
396
|
+
for (const item of piece.items) {
|
|
397
|
+
if (item.kind !== "instruction") continue;
|
|
398
|
+
const form = formOf(layout, item);
|
|
399
|
+
if (!forms.has(form.key)) forms.set(form.key, { form, bytes: Array.from({ length: item.length }, (_, offset) => layout.byteAt(item.address + offset)) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return [...forms.values()].sort((a, b) => (a.form.key < b.form.key ? -1 : 1));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Data cells -------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
type Directive = "!byte" | "!word" | "!fill" | "!pet" | "!scr";
|
|
408
|
+
|
|
409
|
+
interface Cell {
|
|
410
|
+
address: number;
|
|
411
|
+
length: number;
|
|
412
|
+
directive: Directive;
|
|
413
|
+
/** The value text: an expression, a fill, or the characters of a string. */
|
|
414
|
+
text: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** PETSCII bytes that ACME's !pet writes from one source character. */
|
|
418
|
+
function petsciiChar(byte: number): string | undefined {
|
|
419
|
+
if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte + 0x20);
|
|
420
|
+
if (byte >= 0xc1 && byte <= 0xda) return String.fromCharCode(byte - 0x80);
|
|
421
|
+
if (byte >= 0x20 && byte <= 0x40 && byte !== 0x22) return String.fromCharCode(byte);
|
|
422
|
+
if (byte === 0x5b || byte === 0x5d) return String.fromCharCode(byte);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Screen codes that ACME's !scr writes from one source character. */
|
|
427
|
+
function screenChar(byte: number): string | undefined {
|
|
428
|
+
if (byte === 0x00) return "@";
|
|
429
|
+
if (byte >= 0x01 && byte <= 0x1a) return String.fromCharCode(byte + 0x60);
|
|
430
|
+
if (byte >= 0x41 && byte <= 0x5a) return String.fromCharCode(byte);
|
|
431
|
+
if (byte >= 0x20 && byte <= 0x3f && byte !== 0x22) return String.fromCharCode(byte);
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const FILL_TYPES: ReadonlyArray<DataItem["type"]> = ["bytes", "workspace", "unused", "unclassified", "bitmap"];
|
|
436
|
+
|
|
437
|
+
function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
|
|
438
|
+
const { byteAt } = layout;
|
|
439
|
+
const cells: Cell[] = [];
|
|
440
|
+
const name = (address: number) => layout.names.get(address);
|
|
441
|
+
const expression = (target: number) => name(target) ?? hex4(target);
|
|
442
|
+
const rtsExpression = (target: number) => (name(target) === undefined ? hex4((target - 1) & 0xffff) : `${name(target)}-1`);
|
|
443
|
+
const byteCell = (address: number, text = hex2(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
|
|
444
|
+
|
|
445
|
+
if (item.region !== undefined && POINTER_TYPES.includes(item.type)) {
|
|
446
|
+
const rts = item.type === "rts-addresses" || item.type === "rts-addresses-split";
|
|
447
|
+
const split = item.type === "addresses-split" || item.type === "rts-addresses-split";
|
|
448
|
+
const pairs = pairsOf(item.region, byteAt);
|
|
449
|
+
const partOf = new Map<number, { pair: (typeof pairs)[number]; high: boolean }>();
|
|
450
|
+
for (const pair of pairs) {
|
|
451
|
+
partOf.set(pair.low, { pair, high: false });
|
|
452
|
+
partOf.set(pair.high, { pair, high: true });
|
|
453
|
+
}
|
|
454
|
+
for (let address = item.start; address <= item.end; address++) {
|
|
455
|
+
const part = partOf.get(address);
|
|
456
|
+
if (part === undefined) {
|
|
457
|
+
cells.push(byteCell(address));
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const text = rts ? rtsExpression(part.pair.target) : expression(part.pair.target);
|
|
461
|
+
const whole = !split && !part.high && address + 1 <= item.end && !breaks.has(address + 1);
|
|
462
|
+
if (whole) {
|
|
463
|
+
cells.push({ address, length: 2, directive: "!word", text });
|
|
464
|
+
address++;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
cells.push(byteCell(address, `${part.high ? ">" : "<"}${/^[$A-Za-z_.0-9]+$/.test(text) ? text : `(${text})`}`));
|
|
468
|
+
}
|
|
469
|
+
return cells;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (item.type === "words") {
|
|
473
|
+
const odd = (item.region === undefined ? 0 : (item.region.end - item.region.start + 1) % 2) === 1;
|
|
474
|
+
for (let address = item.start; address <= item.end; address++) {
|
|
475
|
+
const offset = address - (item.region?.start ?? item.start);
|
|
476
|
+
const last = odd && address === item.region!.end;
|
|
477
|
+
if (offset % 2 === 0 && !last && address + 1 <= item.end && !breaks.has(address + 1)) {
|
|
478
|
+
cells.push({ address, length: 2, directive: "!word", text: hex4(byteAt(address) | (byteAt(address + 1) << 8)) });
|
|
479
|
+
address++;
|
|
480
|
+
} else cells.push(byteCell(address));
|
|
481
|
+
}
|
|
482
|
+
return cells;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (item.type === "petscii" || item.type === "screen") {
|
|
486
|
+
const toChar = item.type === "petscii" ? petsciiChar : screenChar;
|
|
487
|
+
const directive = item.type === "petscii" ? "!pet" : "!scr";
|
|
488
|
+
for (let address = item.start; address <= item.end; address++) {
|
|
489
|
+
const char = toChar(byteAt(address));
|
|
490
|
+
cells.push(char === undefined ? byteCell(address) : { address, length: 1, directive, text: char });
|
|
491
|
+
}
|
|
492
|
+
return cells;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const binary = item.type === "sprite";
|
|
496
|
+
const fill = FILL_TYPES.includes(item.type) && item.region?.element === undefined;
|
|
497
|
+
for (let address = item.start; address <= item.end; address++) {
|
|
498
|
+
const value = byteAt(address);
|
|
499
|
+
if (fill) {
|
|
500
|
+
let end = address;
|
|
501
|
+
while (end + 1 <= item.end && byteAt(end + 1) === value && !breaks.has(end + 1)) end++;
|
|
502
|
+
if (end - address + 1 >= MIN_FILL) {
|
|
503
|
+
cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${hex2(value)}` });
|
|
504
|
+
address = end;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : hex2(value)));
|
|
509
|
+
}
|
|
510
|
+
return cells;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** The cells of one data item as lines: one directive per line, broken at labels, comments and elements. */
|
|
514
|
+
function dataLines(item: DataItem, layout: Layout, breaks: Set<number>): Array<{ address: number; text: string }> {
|
|
515
|
+
const cells = cellsOf(item, layout, breaks);
|
|
516
|
+
const element = item.region?.element;
|
|
517
|
+
const perLine = (directive: Directive) =>
|
|
518
|
+
directive === "!word" ? WORDS_PER_LINE : directive === "!pet" || directive === "!scr" ? CHARS_PER_LINE : item.type === "sprite" ? 3 : element !== undefined && element <= 16 ? element : BYTES_PER_LINE;
|
|
519
|
+
const lines: Array<{ address: number; text: string }> = [];
|
|
520
|
+
let group: Cell[] = [];
|
|
521
|
+
const flush = () => {
|
|
522
|
+
if (group.length === 0) return;
|
|
523
|
+
const [first] = group;
|
|
524
|
+
const directive = first!.directive;
|
|
525
|
+
const text =
|
|
526
|
+
directive === "!pet" || directive === "!scr"
|
|
527
|
+
? `${directive} "${group.map((cell) => cell.text).join("")}"`
|
|
528
|
+
: `${directive} ${group.map((cell) => cell.text).join(directive === "!fill" ? "" : ",")}`;
|
|
529
|
+
lines.push({ address: first!.address, text });
|
|
530
|
+
group = [];
|
|
531
|
+
};
|
|
532
|
+
for (const cell of cells) {
|
|
533
|
+
const first = group[0];
|
|
534
|
+
const elementStart = element !== undefined && item.region !== undefined && (cell.address - item.region.start) % element === 0;
|
|
535
|
+
if (first !== undefined && (first.directive !== cell.directive || cell.directive === "!fill" || group.length >= perLine(first.directive) || breaks.has(cell.address) || elementStart)) flush();
|
|
536
|
+
group.push(cell);
|
|
537
|
+
}
|
|
538
|
+
flush();
|
|
539
|
+
return lines;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// --- Rendering --------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
export interface Rendered {
|
|
545
|
+
files: Array<{ name: string; text: string }>;
|
|
546
|
+
instructions: number;
|
|
547
|
+
dataBytes: number;
|
|
548
|
+
undecodedBytes: number;
|
|
549
|
+
/** Instructions written as bytes, because ACME writes their form with other bytes. */
|
|
550
|
+
asBytes: Array<{ address: number; text: string }>;
|
|
551
|
+
/** With coverage: runs of instructions that no run executed. */
|
|
552
|
+
notExecuted?: { instructions: number; ranges: Range[] };
|
|
553
|
+
/** Each instruction written with an operand expression: its file and line (1 is the first line of the text). */
|
|
554
|
+
expressions: Array<{ address: number; length: number; file: string; line: number }>;
|
|
555
|
+
/** Operand expressions written as numbers, in address order (D65). */
|
|
556
|
+
operandsAsNumbers: OperandAsNumber[];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const withSide = (text: string, side: string | undefined) => (side === undefined ? text : `${text.padEnd(SIDE_COLUMN - 1)} ; ${side}`);
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The source text of each piece, the main file and the constants. `asBytes`
|
|
563
|
+
* holds the opcode form keys that the probe refused; `numbers` the operand
|
|
564
|
+
* expressions that ACME did not assemble to the image, with the reason.
|
|
565
|
+
*/
|
|
566
|
+
export function render(layout: Layout, asBytes: ReadonlySet<string>, header: string[], numbers: ReadonlyMap<number, string> = new Map()): Rendered {
|
|
567
|
+
const lineComments = new Map<number, string[]>();
|
|
568
|
+
const sideComments = new Map<number, string>();
|
|
569
|
+
for (const comment of layout.comments) {
|
|
570
|
+
if (!layout.inImage(comment.address)) continue;
|
|
571
|
+
if (comment.placement === "line") lineComments.set(comment.address, comment.text.split(/\r?\n/));
|
|
572
|
+
else sideComments.set(comment.address, comment.text.replace(/\s*\r?\n\s*/g, " "));
|
|
573
|
+
}
|
|
574
|
+
const breaks = new Set<number>([...layout.names.keys(), ...lineComments.keys(), ...sideComments.keys()]);
|
|
575
|
+
const result: Rendered = { files: [], instructions: 0, dataBytes: 0, undecodedBytes: 0, asBytes: [], expressions: [], operandsAsNumbers: [...layout.operandsAsNumbers] };
|
|
576
|
+
const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
|
|
577
|
+
const { coverage } = layout;
|
|
578
|
+
const notExecuted = { instructions: 0, ranges: [] as Range[] };
|
|
579
|
+
|
|
580
|
+
for (const piece of layout.pieces) {
|
|
581
|
+
const out: string[] = [`; ${piece.title}`, `; ${hex4(piece.start)}-${hex4(piece.end)}`, ""];
|
|
582
|
+
/** True while the instructions before have not executed: they form one range. */
|
|
583
|
+
let running = false;
|
|
584
|
+
/** The line of the mark of the open range of instructions that did not execute. */
|
|
585
|
+
let markLine = 0;
|
|
586
|
+
const head = (address: number) => {
|
|
587
|
+
for (const text of lineComments.get(address) ?? []) out.push(`; ${text}`.trimEnd());
|
|
588
|
+
const label = layout.names.get(address);
|
|
589
|
+
if (label !== undefined) out.push(label);
|
|
590
|
+
};
|
|
591
|
+
for (const item of piece.items) {
|
|
592
|
+
if (item.kind === "instruction") {
|
|
593
|
+
head(item.address);
|
|
594
|
+
const executed = coverage === undefined || (coverage[item.address]! & COVERAGE.ramExecute) !== 0;
|
|
595
|
+
if (!executed) {
|
|
596
|
+
if (running) notExecuted.ranges.at(-1)!.end = item.address + item.length - 1;
|
|
597
|
+
else {
|
|
598
|
+
notExecuted.ranges.push({ start: item.address, end: item.address + item.length - 1 });
|
|
599
|
+
markLine = out.length;
|
|
600
|
+
out.push("");
|
|
601
|
+
}
|
|
602
|
+
// The mark gives the range, so a reader finds it in the full list (D49).
|
|
603
|
+
const range = notExecuted.ranges.at(-1)!;
|
|
604
|
+
out[markLine] = `; not executed ${hex4(range.start)}-${hex4(range.end)}`;
|
|
605
|
+
notExecuted.instructions++;
|
|
606
|
+
}
|
|
607
|
+
running = !executed;
|
|
608
|
+
const sides: string[] = [];
|
|
609
|
+
if (sideComments.has(item.address)) sides.push(sideComments.get(item.address)!);
|
|
610
|
+
for (let offset = 1; offset < item.length; offset++) {
|
|
611
|
+
const inside = item.address + offset;
|
|
612
|
+
const label = layout.names.get(inside);
|
|
613
|
+
if (label !== undefined) out.push(`${label} = * + ${offset}`);
|
|
614
|
+
for (const text of lineComments.get(inside) ?? []) sides.push(text);
|
|
615
|
+
if (sideComments.has(inside)) sides.push(sideComments.get(inside)!);
|
|
616
|
+
}
|
|
617
|
+
const form = formOf(layout, item);
|
|
618
|
+
const { mode } = OPCODES[item.opcode]!;
|
|
619
|
+
const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
|
|
620
|
+
const literal = instructionText(item.opcode, "", literalOperand(item.opcode, value));
|
|
621
|
+
let written = layout.operands.get(item.address);
|
|
622
|
+
// An expression that is not written stays visible as a side comment (D65).
|
|
623
|
+
const asNumber = (expression: string, reason: string) => {
|
|
624
|
+
sides.unshift(`= ${expression}`);
|
|
625
|
+
result.operandsAsNumbers.push({ address: item.address, expression, reason });
|
|
626
|
+
written = undefined;
|
|
627
|
+
};
|
|
628
|
+
if (written !== undefined && asBytes.has(form.key)) asNumber(written, "ACME writes this instruction with other bytes, so the source has it as bytes.");
|
|
629
|
+
if (written !== undefined && numbers.has(item.address)) asNumber(written, numbers.get(item.address)!);
|
|
630
|
+
if (unwritten.has(item.address)) sides.unshift(`= ${unwritten.get(item.address)!}`);
|
|
631
|
+
if (asBytes.has(form.key)) {
|
|
632
|
+
const bytes = Array.from({ length: item.length }, (_, offset) => hex2(layout.byteAt(item.address + offset)));
|
|
633
|
+
out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
|
|
634
|
+
result.asBytes.push({ address: item.address, text: literal });
|
|
635
|
+
} else if (written !== undefined) {
|
|
636
|
+
// ACME reads an operand in brackets as indirect, so a non-immediate expression does not start with one.
|
|
637
|
+
const expression = mode !== "imm" && written.startsWith("(") ? `0+${written}` : written;
|
|
638
|
+
out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
639
|
+
result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
|
|
640
|
+
} else {
|
|
641
|
+
const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
|
|
642
|
+
out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
643
|
+
}
|
|
644
|
+
result.instructions++;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
running = false;
|
|
648
|
+
const lines = dataLines(item, layout, breaks);
|
|
649
|
+
lines.forEach((line, index) => {
|
|
650
|
+
head(line.address);
|
|
651
|
+
const note = item.type === "undecoded" && index === 0 ? "not decoded" : undefined;
|
|
652
|
+
const side = [note, sideComments.get(line.address)].filter((text) => text !== undefined).join(" ");
|
|
653
|
+
out.push(withSide(`${INDENT}${line.text}`, side === "" ? undefined : side));
|
|
654
|
+
});
|
|
655
|
+
const bytes = item.end - item.start + 1;
|
|
656
|
+
if (item.type === "undecoded") result.undecodedBytes += bytes;
|
|
657
|
+
else result.dataBytes += bytes;
|
|
658
|
+
}
|
|
659
|
+
result.files.push({ name: piece.file, text: `${out.join("\n")}\n` });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const constants = ["; Names outside the image.", ...layout.constants.map((address) => `${layout.names.get(address)} = ${constantValue(address)}`)];
|
|
663
|
+
result.files.push({ name: "constants.asm", text: `${constants.join("\n")}\n` });
|
|
664
|
+
const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"'];
|
|
665
|
+
for (const piece of layout.pieces) {
|
|
666
|
+
if (piece.segmentStart) main.push("", `* = ${hex4(piece.start)}`);
|
|
667
|
+
main.push(`!source "${piece.file}"`);
|
|
668
|
+
}
|
|
669
|
+
result.files.push({ name: "main.asm", text: `${main.join("\n")}\n` });
|
|
670
|
+
if (coverage !== undefined) result.notExecuted = notExecuted;
|
|
671
|
+
result.operandsAsNumbers.sort((a, b) => a.address - b.address);
|
|
672
|
+
return result;
|
|
673
|
+
}
|