@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- 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 +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -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 +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- 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 +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- 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} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- 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 +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// The reference pages of the documentation (D80): generated from the
|
|
2
|
+
// knowledge, the source map and the image, never edited. A name links to its
|
|
3
|
+
// line in the source; an address links to the entry of its name; a call site
|
|
4
|
+
// links to its line.
|
|
5
|
+
|
|
6
|
+
import { posix } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { formatC64Address, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
|
|
9
|
+
import { bitmapFrame, charsetFrame, encodePng, PATTERN_PALETTES, scaleFrame, screenText, spriteSheet, type GraphicsMode } from "#src/graphics.ts";
|
|
10
|
+
import { mermaidGraph, ownerOf, routineEdges, routineHeads, type GraphReference, type RoutineHead } from "#src/knowledge/graph.ts";
|
|
11
|
+
import type { CommentRow, ModuleRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
|
|
12
|
+
import type { RegionType } from "#src/knowledge/schema.ts";
|
|
13
|
+
import type { SourceMap } from "./source-map.ts";
|
|
14
|
+
|
|
15
|
+
/** The first line of every generated page. A page without it is a page of a person, and generate.ts never writes over it. */
|
|
16
|
+
export const GENERATED_MARK = "<!-- Generated by c64-docs";
|
|
17
|
+
|
|
18
|
+
export interface ReferenceInput {
|
|
19
|
+
/** The reference directory, relative to the project folder. */
|
|
20
|
+
out: string;
|
|
21
|
+
map: SourceMap;
|
|
22
|
+
revision: string;
|
|
23
|
+
symbols: SymbolRow[];
|
|
24
|
+
regions: RegionRow[];
|
|
25
|
+
comments: CommentRow[];
|
|
26
|
+
references: ReferenceRow[];
|
|
27
|
+
modules: ModuleRow[];
|
|
28
|
+
image?: { byteAt: (address: number) => number; inImage: (address: number) => boolean };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ReferenceFile {
|
|
32
|
+
/** Relative to the project folder. */
|
|
33
|
+
path: string;
|
|
34
|
+
bytes: Buffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Files that the source map lists but that hold no code of the program. */
|
|
38
|
+
const NOT_PIECES = new Set(["main.asm", "constants.asm", "macros.asm", "loader.asm"]);
|
|
39
|
+
const TABLE_TYPES: readonly RegionType[] = [...POINTER_TABLE_TYPES, "jumps", "words"];
|
|
40
|
+
const GRAPHICS_TYPES: readonly RegionType[] = ["sprite", "bitmap", "charset", "screen"];
|
|
41
|
+
/** A list longer than this ends with the number of the rest. */
|
|
42
|
+
const MAX_LISTED = 40;
|
|
43
|
+
/** A call graph with more edges is not readable: then it shows the calls only, and at most this many. */
|
|
44
|
+
const MAX_EDGES = 60;
|
|
45
|
+
/** A name outside the image covers the addresses that references use up to the next name, at most this many. */
|
|
46
|
+
const MAX_SPAN = 256;
|
|
47
|
+
/** The kinds of a reference that reads a table: Ghidra gives an indexed read (lda table,x) as "reference". */
|
|
48
|
+
const TABLE_READS: readonly string[] = ["read", "reference", "pointer"];
|
|
49
|
+
|
|
50
|
+
const hex = (address: number) => formatC64Address(address).toUpperCase();
|
|
51
|
+
const range = (start: number, end: number) => (start === end ? hex(start) : `${hex(start)}-${hex(end)}`);
|
|
52
|
+
const stem = (file: string) => file.replace(/\.asm$/, "");
|
|
53
|
+
|
|
54
|
+
interface Entry {
|
|
55
|
+
name: string;
|
|
56
|
+
address: number;
|
|
57
|
+
kind: string;
|
|
58
|
+
/** The page that holds the entry, relative to the project folder. */
|
|
59
|
+
page: string;
|
|
60
|
+
/** The last address that the name covers: references to it belong to the name. */
|
|
61
|
+
spanEnd: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Builds every page and image of the reference. */
|
|
65
|
+
export function buildReference(input: ReferenceInput): { files: ReferenceFile[]; counts: Record<string, number> } {
|
|
66
|
+
const { map, out } = input;
|
|
67
|
+
const pieces = map.files.filter((file) => !NOT_PIECES.has(file));
|
|
68
|
+
const pageOfFile = (file: string) => (file === "constants.asm" ? posix.join(out, "variables.md") : posix.join(out, "modules", `${stem(file)}.md`));
|
|
69
|
+
|
|
70
|
+
// --- Names and their entries ------------------------------------------------
|
|
71
|
+
const knowledgeNames = new Map(input.symbols.map((symbol) => [symbol.name, symbol]));
|
|
72
|
+
const entries = new Map<string, Entry>();
|
|
73
|
+
const sortedNames = [...new Set([...input.symbols.map((symbol) => symbol.address), ...map.labels.filter((label) => label.file === "constants.asm").map((label) => label.address)])].sort((a, b) => a - b);
|
|
74
|
+
const nextName = (address: number) => sortedNames.find((other) => other > address);
|
|
75
|
+
const regionAt = (address: number) => input.regions.find((region) => region.start <= address && region.end >= address);
|
|
76
|
+
const spanOf = (address: number, kind: string) => {
|
|
77
|
+
if (kind === "routine" || kind === "label") return address;
|
|
78
|
+
const next = nextName(address) ?? 0x10000;
|
|
79
|
+
const region = regionAt(address);
|
|
80
|
+
if (region !== undefined) return Math.min(next - 1, region.end);
|
|
81
|
+
if (input.image?.inImage(address) === true) return address;
|
|
82
|
+
const last = Math.min(next - 1, address + MAX_SPAN - 1);
|
|
83
|
+
let end = address;
|
|
84
|
+
for (const reference of input.references) if (reference.to > end && reference.to <= last) end = reference.to;
|
|
85
|
+
return end;
|
|
86
|
+
};
|
|
87
|
+
for (const label of map.labels) {
|
|
88
|
+
const symbol = knowledgeNames.get(label.name);
|
|
89
|
+
// A name that c64-source makes (l_xxxx) is no entry: it has no meaning of its own.
|
|
90
|
+
if (symbol === undefined && label.file !== "constants.asm") continue;
|
|
91
|
+
const kind = symbol?.kind ?? "platform";
|
|
92
|
+
entries.set(label.name, { name: label.name, address: label.address, kind, page: pageOfFile(label.file), spanEnd: spanOf(label.address, kind) });
|
|
93
|
+
}
|
|
94
|
+
const entryAt = new Map<number, Entry>();
|
|
95
|
+
for (const entry of entries.values()) if (!entryAt.has(entry.address)) entryAt.set(entry.address, entry);
|
|
96
|
+
const spans = [...entries.values()].filter((entry) => entry.spanEnd > entry.address).sort((a, b) => a.address - b.address);
|
|
97
|
+
const sourceNameAt = new Map<number, string>();
|
|
98
|
+
for (const label of map.labels) if (!sourceNameAt.has(label.address)) sourceNameAt.set(label.address, label.name);
|
|
99
|
+
|
|
100
|
+
// --- Links ------------------------------------------------------------------
|
|
101
|
+
const rel = (page: string, target: string) => posix.relative(posix.dirname(page), target) || posix.basename(target);
|
|
102
|
+
const codeOfName = (page: string, name: string) => {
|
|
103
|
+
const label = map.labelOf(name);
|
|
104
|
+
return label === undefined ? `\`${name}\`` : `[\`${name}\`](${rel(page, map.pathOf(label.file))}#L${label.line})`;
|
|
105
|
+
};
|
|
106
|
+
const entryOfName = (page: string, name: string, text: string) => {
|
|
107
|
+
const entry = entries.get(name);
|
|
108
|
+
return entry === undefined ? `\`${text}\`` : `[\`${text}\`](${rel(page, entry.page)}#${name})`;
|
|
109
|
+
};
|
|
110
|
+
/** A name and its address: the name to its line, the address to its entry (the convention of every page). */
|
|
111
|
+
const pair = (page: string, name: string, address: number) => `${codeOfName(page, name)} ${entryOfName(page, name, hex(address))}`;
|
|
112
|
+
const codeOfAddress = (page: string, address: number) => {
|
|
113
|
+
const line = map.lineOf(address);
|
|
114
|
+
return line === undefined ? `\`${hex(address)}\`` : `[\`${hex(address)}\`](${rel(page, map.pathOf(line.file))}#L${line.line})`;
|
|
115
|
+
};
|
|
116
|
+
/** What a reader calls an address: its name, a name plus an offset, or the address. */
|
|
117
|
+
const nameFor = (address: number): { name: string; base: number } | undefined => {
|
|
118
|
+
const own = entryAt.get(address)?.name;
|
|
119
|
+
if (own !== undefined) return { name: own, base: address };
|
|
120
|
+
const span = spans.find((entry) => entry.address < address && entry.spanEnd >= address);
|
|
121
|
+
if (span !== undefined) return { name: span.name, base: span.address };
|
|
122
|
+
const made = sourceNameAt.get(address);
|
|
123
|
+
return made === undefined ? undefined : { name: made, base: address };
|
|
124
|
+
};
|
|
125
|
+
const target = (page: string, address: number) => {
|
|
126
|
+
const named = nameFor(address);
|
|
127
|
+
if (named === undefined) return codeOfAddress(page, address);
|
|
128
|
+
const shown = named.base === address ? named.name : `${named.name}+${address - named.base}`;
|
|
129
|
+
return entries.has(named.name) ? entryOfName(page, named.name, shown) : codeOfName(page, named.name).replace(`\`${named.name}\``, `\`${shown}\``);
|
|
130
|
+
};
|
|
131
|
+
const capped = (items: string[]) => (items.length <= MAX_LISTED ? items.join(", ") : `${items.slice(0, MAX_LISTED).join(", ")} and ${items.length - MAX_LISTED} more`);
|
|
132
|
+
|
|
133
|
+
// --- The routine graph ----------------------------------------------------
|
|
134
|
+
const tableReferences: GraphReference[] = [];
|
|
135
|
+
const tables = input.regions.filter((region) => TABLE_TYPES.includes(region.type));
|
|
136
|
+
const tableEntries = new Map<RegionRow, Array<{ at: number; value: number; target?: number }>>();
|
|
137
|
+
if (input.image !== undefined) {
|
|
138
|
+
const { byteAt } = input.image;
|
|
139
|
+
for (const table of tables) {
|
|
140
|
+
const rows: Array<{ at: number; value: number; target?: number }> = [];
|
|
141
|
+
if ((POINTER_TABLE_TYPES as readonly string[]).includes(table.type)) {
|
|
142
|
+
for (const pair of pointerTableEntries({ ...table, type: table.type as PointerTableType }, byteAt)) rows.push({ at: pair.low, value: byteAt(pair.low) | (byteAt(pair.high) << 8), target: pair.target });
|
|
143
|
+
} else if (table.type === "jumps") {
|
|
144
|
+
for (let at = table.start; at + 2 <= table.end; at += 3) {
|
|
145
|
+
const value = byteAt(at + 1) | (byteAt(at + 2) << 8);
|
|
146
|
+
rows.push(byteAt(at) === 0x4c ? { at, value, target: value } : { at, value });
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
for (let at = table.start; at + 1 <= table.end; at += 2) rows.push({ at, value: byteAt(at) | (byteAt(at + 1) << 8) });
|
|
150
|
+
}
|
|
151
|
+
tableEntries.set(table, rows);
|
|
152
|
+
for (const row of rows) if (row.target !== undefined && table.type !== "words") tableReferences.push({ from: table.start, to: row.target, kind: "table" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// A table entry is an edge of the graph from each instruction that reads the table to each target in code; a table of data pointers makes no edge.
|
|
156
|
+
const symbolKinds = new Map(input.symbols.map((symbol) => [symbol.address, symbol.kind]));
|
|
157
|
+
const inCode = (address: number) =>
|
|
158
|
+
symbolKinds.get(address) === "routine" || symbolKinds.get(address) === "label" || input.regions.some((region) => (region.type === "code" || region.type === "jumps") && region.start <= address && region.end >= address);
|
|
159
|
+
const dispatches: GraphReference[] = [];
|
|
160
|
+
for (const [table, rows] of tableEntries) {
|
|
161
|
+
const readers = input.references.filter((reference) => TABLE_READS.includes(reference.kind) && reference.to >= table.start && reference.to <= table.end).map((reference) => reference.from);
|
|
162
|
+
for (const row of rows) if (row.target !== undefined && table.type !== "words" && inCode(row.target)) for (const from of new Set(readers)) dispatches.push({ from, to: row.target, kind: "table" });
|
|
163
|
+
}
|
|
164
|
+
const graphReferences: GraphReference[] = [...input.references, ...dispatches];
|
|
165
|
+
const heads = routineHeads(input.symbols, graphReferences, (address) => sourceNameAt.get(address) ?? hex(address));
|
|
166
|
+
const boundaries = input.modules.map((module) => module.start);
|
|
167
|
+
const ownerAt = (address: number) => ownerOf(heads, boundaries, address);
|
|
168
|
+
const site = (page: string, from: number) => {
|
|
169
|
+
const owner = ownerAt(from);
|
|
170
|
+
const named = owner === undefined ? undefined : entries.has(owner.name) ? entryOfName(page, owner.name, owner.name) : codeOfName(page, owner.name);
|
|
171
|
+
return named === undefined ? codeOfAddress(page, from) : `${named} ${codeOfAddress(page, from)}`;
|
|
172
|
+
};
|
|
173
|
+
const edges = routineEdges(heads, boundaries, graphReferences);
|
|
174
|
+
|
|
175
|
+
// --- File ranges ----------------------------------------------------------
|
|
176
|
+
const fileRanges = new Map<string, { start: number; end: number }>();
|
|
177
|
+
for (let address = 0; address <= 0xffff; address++) {
|
|
178
|
+
const line = map.lineOf(address);
|
|
179
|
+
if (line === undefined) continue;
|
|
180
|
+
const known = fileRanges.get(line.file);
|
|
181
|
+
if (known === undefined) fileRanges.set(line.file, { start: address, end: address });
|
|
182
|
+
else known.end = address;
|
|
183
|
+
}
|
|
184
|
+
const titleOf = (file: string) => input.modules.find((module) => module.file === file)?.title;
|
|
185
|
+
|
|
186
|
+
const header = `${GENERATED_MARK} from knowledge revision ${input.revision} and ${map.pathOf("source-map.json")}. Do not edit: change the knowledge, generate the source again, then run generate.ts. -->`;
|
|
187
|
+
const files: ReferenceFile[] = [];
|
|
188
|
+
const write = (path: string, lines: string[]) => files.push({ path, bytes: Buffer.from(`${[header, ...lines].join("\n").replace(/\n{3,}/g, "\n\n")}\n`, "utf8") });
|
|
189
|
+
|
|
190
|
+
// --- One entry --------------------------------------------------------------
|
|
191
|
+
const commentLines = (address: number) =>
|
|
192
|
+
input.comments
|
|
193
|
+
.filter((comment) => comment.address === address)
|
|
194
|
+
.sort((a, b) => (a.placement === b.placement ? 0 : a.placement === "line" ? -1 : 1))
|
|
195
|
+
.map((comment) => comment.text.replace(/\s*\r?\n\s*/g, " ").trim());
|
|
196
|
+
const incoming = (entry: Entry) => input.references.filter((reference) => reference.to >= entry.address && reference.to <= entry.spanEnd);
|
|
197
|
+
const outgoing = (head: RoutineHead) => input.references.filter((reference) => ownerAt(reference.from)?.address === head.address);
|
|
198
|
+
const entryText = (page: string, entry: Entry): string[] => {
|
|
199
|
+
const lines = ["", `<a id="${entry.name}"></a>`, `### ${codeOfName(page, entry.name)} \`${entry.spanEnd > entry.address ? range(entry.address, entry.spanEnd) : hex(entry.address)}\``, "", `*${entry.kind}*`];
|
|
200
|
+
const comments = commentLines(entry.address);
|
|
201
|
+
if (comments.length > 0) lines.push("", ...comments);
|
|
202
|
+
const facts: string[] = [];
|
|
203
|
+
const into = incoming(entry);
|
|
204
|
+
const selfHead = heads.find((candidate) => candidate.address === entry.address);
|
|
205
|
+
const fromOthers = into.filter((reference) => selfHead === undefined || ownerAt(reference.from)?.address !== selfHead.address);
|
|
206
|
+
const strong = new Set(fromOthers.filter((reference) => reference.kind !== "reference" && reference.kind !== "pointer").map((reference) => reference.from));
|
|
207
|
+
const by = (kinds: readonly string[]) =>
|
|
208
|
+
fromOthers
|
|
209
|
+
.filter((reference) => kinds.includes(reference.kind) && !((reference.kind === "reference" || reference.kind === "pointer") && strong.has(reference.from)))
|
|
210
|
+
.map((reference) => site(page, reference.from) + (reference.to === entry.address ? "" : ` (+${reference.to - entry.address})`));
|
|
211
|
+
const add = (label: string, items: string[]) => {
|
|
212
|
+
if (items.length > 0) facts.push(`- **${label}:** ${capped([...new Set(items)])}`);
|
|
213
|
+
};
|
|
214
|
+
add("Called from", by(["call"]));
|
|
215
|
+
add("Jumped to from", by(["jump"]));
|
|
216
|
+
add("Read by", by(["read"]));
|
|
217
|
+
add("Written by", by(["write"]));
|
|
218
|
+
add("Referred to by", by(["reference", "pointer"]));
|
|
219
|
+
const inTables = tableReferences.filter((reference) => reference.to === entry.address).map((reference) => {
|
|
220
|
+
const table = tables.find((region) => region.start === reference.from)!;
|
|
221
|
+
return tableLink(page, table);
|
|
222
|
+
});
|
|
223
|
+
add("Entry of", inTables);
|
|
224
|
+
const head = selfHead;
|
|
225
|
+
if (head !== undefined) {
|
|
226
|
+
const all = outgoing(head);
|
|
227
|
+
const named = new Set(all.filter((reference) => reference.kind !== "reference" && reference.kind !== "pointer").map((reference) => `${reference.from}:${reference.to}`));
|
|
228
|
+
const from = all.filter((reference) => !((reference.kind === "reference" || reference.kind === "pointer") && named.has(`${reference.from}:${reference.to}`)));
|
|
229
|
+
const to = (kinds: readonly string[]) => from.filter((reference) => kinds.includes(reference.kind) && ownerAt(reference.to)?.address !== head.address).map((reference) => target(page, reference.to));
|
|
230
|
+
add("Calls", to(["call"]));
|
|
231
|
+
add("Jumps to", to(["jump"]));
|
|
232
|
+
add("Reads", to(["read"]));
|
|
233
|
+
add("Writes", to(["write"]));
|
|
234
|
+
add("Refers to", to(["reference", "pointer"]));
|
|
235
|
+
}
|
|
236
|
+
if (facts.length > 0) lines.push("", ...facts);
|
|
237
|
+
return lines;
|
|
238
|
+
};
|
|
239
|
+
const tableAnchor = (table: RegionRow) => entryAt.get(table.start)?.name ?? `table_${table.start.toString(16).padStart(4, "0")}`;
|
|
240
|
+
function tableLink(page: string, table: RegionRow): string {
|
|
241
|
+
return `[\`${entryAt.get(table.start)?.name ?? range(table.start, table.end)}\`](${rel(page, posix.join(out, "tables.md"))}#${tableAnchor(table)})`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- Module pages -----------------------------------------------------------
|
|
245
|
+
const counts = { pages: 0, entries: 0, tables: 0, images: 0, edges: edges.length };
|
|
246
|
+
const pieceEntries = new Map<string, Entry[]>();
|
|
247
|
+
for (const entry of entries.values()) {
|
|
248
|
+
const label = map.labelOf(entry.name)!;
|
|
249
|
+
if (!pieces.includes(label.file)) continue;
|
|
250
|
+
pieceEntries.set(label.file, [...(pieceEntries.get(label.file) ?? []), entry]);
|
|
251
|
+
}
|
|
252
|
+
for (const file of pieces) {
|
|
253
|
+
const page = pageOfFile(file);
|
|
254
|
+
const span = fileRanges.get(file);
|
|
255
|
+
const own = (pieceEntries.get(file) ?? []).sort((a, b) => a.address - b.address);
|
|
256
|
+
const title = titleOf(file);
|
|
257
|
+
const lines = [`# ${file}${title === undefined ? "" : `: ${title}`}`, "", `Range \`${span === undefined ? "-" : range(span.start, span.end)}\`. Source: [${file}](${rel(page, map.pathOf(file))}). Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`];
|
|
258
|
+
if (span !== undefined) {
|
|
259
|
+
const inside = (address: number) => address >= span.start && address <= span.end;
|
|
260
|
+
const ownHeads = new Set(heads.filter((head) => inside(head.address)).map((head) => head.address));
|
|
261
|
+
let shown = edges.filter((edge) => ownHeads.has(edge.from) || ownHeads.has(edge.to));
|
|
262
|
+
const all = shown.length;
|
|
263
|
+
if (shown.length > MAX_EDGES) shown = shown.filter((edge) => edge.kind === "call").slice(0, MAX_EDGES);
|
|
264
|
+
if (shown.length > 0) {
|
|
265
|
+
lines.push("", "## Call graph", "", "A solid arrow is a call, a thick arrow a table entry, a dotted arrow a jump. Rounded nodes are in this file.");
|
|
266
|
+
if (shown.length < all) lines.push("", `The graph shows ${shown.length} of the ${all} edges: the calls only.`);
|
|
267
|
+
lines.push("", "```mermaid", mermaidGraph(heads, shown, ownHeads), "```");
|
|
268
|
+
}
|
|
269
|
+
const regions = input.regions.filter((region) => region.start <= span.end && region.end >= span.start && region.type !== "code");
|
|
270
|
+
if (regions.length > 0) {
|
|
271
|
+
lines.push("", "## Data", "", "| Range | Type | Name | Read by | Written by |", "|---|---|---|---|---|");
|
|
272
|
+
for (const region of regions) {
|
|
273
|
+
const into = input.references.filter((reference) => reference.to >= region.start && reference.to <= region.end);
|
|
274
|
+
const owners = (kind: string) => capped([...new Set(into.filter((reference) => reference.kind === kind).map((reference) => ownerAt(reference.from)?.name ?? hex(reference.from)))].map((name) => (entries.has(name) ? entryOfName(page, name, name) : `\`${name}\``)));
|
|
275
|
+
const name = entryAt.get(region.start);
|
|
276
|
+
lines.push(`| ${codeOfAddress(page, region.start)}${region.end === region.start ? "" : `-\`${hex(region.end)}\``} | ${region.type}${region.element === undefined ? "" : ` of ${region.element}`} | ${name === undefined ? "" : codeOfName(page, name.name)} | ${owners("read")} | ${owners("write")} |`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (own.length > 0) lines.push("", "## Names");
|
|
281
|
+
for (const entry of own) lines.push(...entryText(page, entry));
|
|
282
|
+
counts.entries += own.length;
|
|
283
|
+
write(page, lines);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- Variables: the names outside the image -------------------------------
|
|
287
|
+
{
|
|
288
|
+
const page = posix.join(out, "variables.md");
|
|
289
|
+
const outside = [...entries.values()].filter((entry) => map.labelOf(entry.name)?.file === "constants.asm").sort((a, b) => a.address - b.address);
|
|
290
|
+
const inside = [...entries.values()].filter((entry) => entry.kind === "variable" && map.labelOf(entry.name)?.file !== "constants.asm").sort((a, b) => a.address - b.address);
|
|
291
|
+
const lines = ["# Variables", "", `The names outside the image, with the routines that read and write them. Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`];
|
|
292
|
+
if (inside.length > 0) lines.push("", "Variables inside the image are on the page of their file:", "", ...inside.map((entry) => `- ${pair(page, entry.name, entry.address)}`));
|
|
293
|
+
for (const entry of outside) lines.push(...entryText(page, entry));
|
|
294
|
+
counts.entries += outside.length;
|
|
295
|
+
write(page, lines);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Tables -----------------------------------------------------------------
|
|
299
|
+
{
|
|
300
|
+
const page = posix.join(out, "tables.md");
|
|
301
|
+
const lines = ["# Tables", "", `Each table of addresses, jumps and words, entry by entry. An RTS table holds each target minus 1; the target column gives the target. Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`];
|
|
302
|
+
if (input.image === undefined) lines.push("", "Put the program into the knowledge with knowledge.ts image, then generate the reference again to decode the tables.");
|
|
303
|
+
for (const table of tables) {
|
|
304
|
+
const name = entryAt.get(table.start);
|
|
305
|
+
lines.push("", `<a id="${tableAnchor(table)}"></a>`, `## ${name === undefined ? `\`${range(table.start, table.end)}\`` : `${codeOfName(page, name.name)} \`${range(table.start, table.end)}\``}`, "", `*${table.type}*`);
|
|
306
|
+
const comments = commentLines(table.start);
|
|
307
|
+
if (comments.length > 0) lines.push("", ...comments);
|
|
308
|
+
const readers = [...new Set(input.references.filter((reference) => reference.to >= table.start && reference.to <= table.end && TABLE_READS.includes(reference.kind)).map((reference) => site(page, reference.from)))];
|
|
309
|
+
if (readers.length > 0) lines.push("", `- **Read by:** ${capped(readers)}`);
|
|
310
|
+
const rows = tableEntries.get(table);
|
|
311
|
+
if (rows === undefined) continue;
|
|
312
|
+
lines.push("", "| Index | At | Value | Target |", "|---|---|---|---|");
|
|
313
|
+
rows.forEach((row, index) => {
|
|
314
|
+
const named = row.target === undefined ? nameFor(row.value) : nameFor(row.target);
|
|
315
|
+
const shown = row.target === undefined ? (named?.base === row.value ? pair(page, named.name, row.value) : "") : named !== undefined && named.base === row.target ? pair(page, named.name, row.target) : target(page, row.target);
|
|
316
|
+
lines.push(`| ${index} | ${codeOfAddress(page, row.at)} | \`${hex(row.value)}\` | ${shown} |`);
|
|
317
|
+
});
|
|
318
|
+
counts.tables++;
|
|
319
|
+
}
|
|
320
|
+
write(page, lines);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Graphics -----------------------------------------------------------------
|
|
324
|
+
{
|
|
325
|
+
const page = posix.join(out, "graphics.md");
|
|
326
|
+
const graphics = input.regions.filter((region) => GRAPHICS_TYPES.includes(region.type));
|
|
327
|
+
const lines = [
|
|
328
|
+
"# Graphics",
|
|
329
|
+
"",
|
|
330
|
+
`Each sprite, bitmap, charset and screen region of the knowledge. The knowledge gives no display mode and no colours, so a sprite, bitmap or charset shows in hires and in multicolour, in pattern colours: black is pixel value 0, white is 1 in hires; in multicolour light grey is 1, grey is 2 and yellow is 3. Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`,
|
|
331
|
+
];
|
|
332
|
+
if (input.image === undefined && graphics.length > 0) lines.push("", "Put the program into the knowledge with knowledge.ts image, then generate the reference again to show the graphics.");
|
|
333
|
+
for (const region of graphics) {
|
|
334
|
+
const anchor = entryAt.get(region.start)?.name ?? `${region.type}_${region.start.toString(16).padStart(4, "0")}`;
|
|
335
|
+
const name = entryAt.get(region.start);
|
|
336
|
+
lines.push("", `<a id="${anchor}"></a>`, `## ${name === undefined ? codeOfAddress(page, region.start) : pair(page, name.name, region.start)} \`${range(region.start, region.end)}\``, "", `*${region.type}*`);
|
|
337
|
+
const comments = commentLines(region.start);
|
|
338
|
+
if (comments.length > 0) lines.push("", ...comments);
|
|
339
|
+
if (input.image === undefined) continue;
|
|
340
|
+
const bytes = Uint8Array.from({ length: region.end - region.start + 1 }, (_, offset) => input.image!.byteAt(region.start + offset));
|
|
341
|
+
if (region.type === "screen") {
|
|
342
|
+
lines.push("", "```text", ...screenText(bytes), "```");
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
for (const mode of ["hires", "multicolour"] as GraphicsMode[]) {
|
|
346
|
+
// A sheet of sprites, a charset, or a bitmap of at most 8 rows of cells, is small: each pixel shows as 2 x 2.
|
|
347
|
+
const raw = region.type === "sprite" ? spriteSheet(bytes, mode, region.element === 63 ? 63 : 64) : region.type === "charset" ? charsetFrame(bytes, mode) : bitmapFrame(bytes, mode);
|
|
348
|
+
const frame = region.type === "sprite" || region.type === "charset" || raw.height <= 64 ? scaleFrame(raw, 2) : raw;
|
|
349
|
+
const image = posix.join(out, "images", `${anchor}-${mode}.png`);
|
|
350
|
+
files.push({ path: image, bytes: encodePng(frame, PATTERN_PALETTES[mode]) });
|
|
351
|
+
lines.push("", `})`);
|
|
352
|
+
counts.images++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
write(page, lines);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// --- Memory map ---------------------------------------------------------------
|
|
359
|
+
{
|
|
360
|
+
const page = posix.join(out, "memory-map.md");
|
|
361
|
+
const lines = ["# Memory map", "", `Every region of the knowledge in address order. Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`, "", "| Range | Bytes | Type | Name | File |", "|---|---|---|---|---|"];
|
|
362
|
+
for (const region of [...input.regions].sort((a, b) => a.start - b.start)) {
|
|
363
|
+
const name = entryAt.get(region.start);
|
|
364
|
+
const line = map.lineOf(region.start);
|
|
365
|
+
const file = line === undefined || !pieces.includes(line.file) ? "" : `[${line.file}](${rel(page, pageOfFile(line.file))})`;
|
|
366
|
+
lines.push(`| ${codeOfAddress(page, region.start)}${region.end === region.start ? "" : `-\`${hex(region.end)}\``} | ${region.end - region.start + 1} | ${region.type} | ${name === undefined ? "" : pair(page, name.name, region.start)} | ${file} |`);
|
|
367
|
+
}
|
|
368
|
+
write(page, lines);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Symbols --------------------------------------------------------------------
|
|
372
|
+
{
|
|
373
|
+
const page = posix.join(out, "symbols.md");
|
|
374
|
+
const lines = ["# Names", "", `Every name in alphabetical order. The name links to its line in the source, the address to its entry. Index: [the reference](${rel(page, posix.join(out, "index.md"))}).`, "", "| Name | Address | Kind |", "|---|---|---|"];
|
|
375
|
+
const names = [...new Set([...entries.keys(), ...input.symbols.map((symbol) => symbol.name)])].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()) || a.localeCompare(b));
|
|
376
|
+
for (const name of names) {
|
|
377
|
+
const entry = entries.get(name);
|
|
378
|
+
const symbol = knowledgeNames.get(name);
|
|
379
|
+
const address = entry?.address ?? symbol!.address;
|
|
380
|
+
lines.push(`| ${codeOfName(page, name)} | ${entryOfName(page, name, hex(address))} | ${entry?.kind ?? symbol!.kind} |`);
|
|
381
|
+
}
|
|
382
|
+
write(page, lines);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Index ------------------------------------------------------------------------
|
|
386
|
+
{
|
|
387
|
+
const page = posix.join(out, "index.md");
|
|
388
|
+
const lines = [
|
|
389
|
+
"# Reference",
|
|
390
|
+
"",
|
|
391
|
+
`Generated from the knowledge and the source in [${map.directory}](${rel(page, map.directory)}). The other pages: [names](symbols.md), [variables](variables.md), [tables](tables.md), [memory map](memory-map.md) and [graphics](graphics.md).`,
|
|
392
|
+
"",
|
|
393
|
+
"| File | Range | Title | Routines | Names |",
|
|
394
|
+
"|---|---|---|---|---|",
|
|
395
|
+
];
|
|
396
|
+
for (const file of pieces) {
|
|
397
|
+
const span = fileRanges.get(file);
|
|
398
|
+
const own = pieceEntries.get(file) ?? [];
|
|
399
|
+
const routines = span === undefined ? 0 : heads.filter((head) => head.address >= span.start && head.address <= span.end).length;
|
|
400
|
+
lines.push(`| [${file}](${rel(page, pageOfFile(file))}) | \`${span === undefined ? "-" : range(span.start, span.end)}\` | ${titleOf(file) ?? ""} | ${routines} | ${own.length} |`);
|
|
401
|
+
}
|
|
402
|
+
write(page, lines);
|
|
403
|
+
}
|
|
404
|
+
counts.pages = files.filter((file) => file.path.endsWith(".md")).length;
|
|
405
|
+
return { files, counts };
|
|
406
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The source map that c64-source writes next to the source (D80): the line of
|
|
2
|
+
// each name and of each address. The documentation links to these lines.
|
|
3
|
+
|
|
4
|
+
import { posix } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { parseC64Address } from "#src/c64.ts";
|
|
7
|
+
import { WireFailure } from "#src/host-client/tools.ts";
|
|
8
|
+
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
9
|
+
import type { Space } from "#src/knowledge/schema.ts";
|
|
10
|
+
|
|
11
|
+
export const SOURCE_MAP_FILE = "source-map.json";
|
|
12
|
+
|
|
13
|
+
export interface SourceLine {
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SourceMap {
|
|
19
|
+
/** The source directory, relative to the project folder. */
|
|
20
|
+
directory: string;
|
|
21
|
+
space: Space;
|
|
22
|
+
revision: string;
|
|
23
|
+
files: string[];
|
|
24
|
+
labels: Array<SourceLine & { address: number; name: string }>;
|
|
25
|
+
/** The label line of a name. */
|
|
26
|
+
labelOf: (name: string) => (SourceLine & { address: number }) | undefined;
|
|
27
|
+
/** The line that writes an address: an instruction, a macro use or a data line. */
|
|
28
|
+
lineOf: (address: number) => SourceLine | undefined;
|
|
29
|
+
/** The project path of a source file. */
|
|
30
|
+
pathOf: (file: string) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reads <directory>/source-map.json. A missing map is a refusal with the remedy. */
|
|
34
|
+
export function readSourceMap(directory: string): SourceMap {
|
|
35
|
+
const path = posix.join(directory, SOURCE_MAP_FILE);
|
|
36
|
+
let text: string;
|
|
37
|
+
try {
|
|
38
|
+
text = readProjectFile(path).bytes.toString("utf8");
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof WireFailure && error.code === "not-found") {
|
|
41
|
+
throw new WireFailure("not-found", `There is no ${path}. Generate the source with c64-source generate.ts --out ${directory}: it writes the source map.`);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const fail = (what: string): never => {
|
|
46
|
+
throw new WireFailure("invalid-input", `${path} is not a source map of c64-source: ${what}. Generate the source again.`);
|
|
47
|
+
};
|
|
48
|
+
let raw: Record<string, unknown>;
|
|
49
|
+
try {
|
|
50
|
+
raw = JSON.parse(text) as Record<string, unknown>;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return fail((error as Error).message);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(raw.labels) || !Array.isArray(raw.items) || !Array.isArray(raw.files)) fail("it has no labels, items or files");
|
|
55
|
+
const labels = (raw.labels as Array<Record<string, unknown>>).map((label) => ({ address: parseC64Address(String(label.address)), name: String(label.name), file: String(label.file), line: Number(label.line) }));
|
|
56
|
+
const items = (raw.items as Array<Record<string, unknown>>)
|
|
57
|
+
.map((item) => ({ start: parseC64Address(String(item.start)), end: parseC64Address(String(item.end)), file: String(item.file), line: Number(item.line) }))
|
|
58
|
+
.sort((a, b) => a.start - b.start);
|
|
59
|
+
const byName = new Map(labels.map((label) => [label.name, label]));
|
|
60
|
+
return {
|
|
61
|
+
directory,
|
|
62
|
+
space: String(raw.space ?? "c64") as Space,
|
|
63
|
+
revision: String(raw.revision),
|
|
64
|
+
files: (raw.files as unknown[]).map(String),
|
|
65
|
+
labels,
|
|
66
|
+
labelOf: (name) => byName.get(name),
|
|
67
|
+
lineOf: (address) => {
|
|
68
|
+
let low = 0;
|
|
69
|
+
let high = items.length - 1;
|
|
70
|
+
while (low <= high) {
|
|
71
|
+
const middle = (low + high) >> 1;
|
|
72
|
+
const item = items[middle]!;
|
|
73
|
+
if (address < item.start) high = middle - 1;
|
|
74
|
+
else if (address > item.end) low = middle + 1;
|
|
75
|
+
else return { file: item.file, line: item.line };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
},
|
|
79
|
+
pathOf: (file) => posix.join(directory, file),
|
|
80
|
+
};
|
|
81
|
+
}
|