@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.20

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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +137 -0
  14. package/skills/c64-docs/references/page-types.md +113 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +193 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +89 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -60
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +63 -0
  81. package/src/host-client/image.ts +66 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +203 -32
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +137 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -8
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
@@ -0,0 +1,155 @@
1
+ // The links of a page written by a person (D80). A name in backticks links to
2
+ // its line in the source; the address after it links to the entry of the name
3
+ // in the reference, or, for an address inside the routine or the data of the
4
+ // name ("draw_room $0AAF", a place in draw_room), to the line of that address. A link that points into the source or the reference is
5
+ // managed: link.ts writes it again when a line moves. Other links stay as the
6
+ // person wrote them; link.ts only checks that their targets exist.
7
+
8
+ import { posix } from "node:path";
9
+
10
+ import { anchorsOf, linksOf, proseLines } from "./markdown.ts";
11
+
12
+ export interface LinkContext {
13
+ /** The page, relative to the project folder. */
14
+ page: string;
15
+ /** The address of a name of the knowledge or of the source; undefined for a word that is no name. */
16
+ addressOf: (name: string) => number | undefined;
17
+ /** The project path and line of the label of a name, as path#Lnn. */
18
+ codeOf: (name: string) => string | undefined;
19
+ /** The project path and anchor of the entry of a name, as path#name. */
20
+ entryOf: (name: string) => string | undefined;
21
+ /** The last address of the routine or data of a name: an address up to it is a place inside the name. */
22
+ bodyEnd: (name: string) => number | undefined;
23
+ /** The project path and line that writes an address, as path#Lnn. */
24
+ lineOf: (address: number) => string | undefined;
25
+ /** The name of the routine or data that holds an address, for a message. */
26
+ placeOf: (address: number) => string | undefined;
27
+ /** True for a project path that link.ts manages: a file of the source or a page of the reference. */
28
+ managed: (path: string) => boolean;
29
+ }
30
+
31
+ export type ProblemKind = "unknown-name" | "wrong-address" | "missing-file" | "missing-anchor" | "missing-line" | "outside-project" | "stale-reference";
32
+
33
+ export interface Problem {
34
+ page: string;
35
+ /** 1 is the first line. */
36
+ line: number;
37
+ kind: ProblemKind;
38
+ message: string;
39
+ }
40
+
41
+ const NAME = String.raw`([A-Za-z_][A-Za-z0-9_]*)(\+(?:\$[0-9A-Fa-f]{1,4}|[0-9]+))?`;
42
+ const ADDRESS = String.raw`\$([0-9A-Fa-f]{2,4})(?:-\$[0-9A-Fa-f]{2,4})?`;
43
+ const TICK = "`";
44
+ /**
45
+ * A name in backticks, linked or not, with an optional address in backticks
46
+ * after it, linked or not. Groups: 1 the [ of a linked name, 2 the name, 3 its
47
+ * offset, 4 the target of the name, 5 the space or bracket between them, 6 the
48
+ * address text, 7 its start digits, 8 the target of the address.
49
+ */
50
+ const MENTION = new RegExp(
51
+ String.raw`(\[)?${TICK}${NAME}${TICK}(?:\]\(([^)\s]*)\))?(?:(\s*\(\s*|\s+)\[?${TICK}(${ADDRESS})${TICK}(?:\]\(([^)\s]*)\))?)?`,
52
+ "g",
53
+ );
54
+
55
+ const relative = (page: string, target: string) => {
56
+ const [path, fragment] = target.split("#");
57
+ const shown = posix.relative(posix.dirname(page), path!) || posix.basename(path!);
58
+ return fragment === undefined ? shown : `${shown}#${fragment}`;
59
+ };
60
+ /** The project path of a link target, without its fragment; undefined for a link out of the project (a URL, or a page anchor only). */
61
+ export function targetPath(page: string, target: string): string | undefined {
62
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#")) return undefined;
63
+ return posix.normalize(posix.join(posix.dirname(page), decodeURIComponent(target.split("#")[0]!)));
64
+ }
65
+
66
+ /** Writes the links of the name mentions of a page; returns the new text, the number of links changed and the problems. */
67
+ export function linkPage(text: string, context: LinkContext): { text: string; changed: number; problems: Problem[] } {
68
+ const problems: Problem[] = [];
69
+ let changed = 0;
70
+ const lines = proseLines(text).map((line, index) => {
71
+ if (!line.prose) return line.text;
72
+ return line.text.replace(MENTION, (whole: string, open: string | undefined, name: string, offset: string | undefined, nameTarget: string | undefined, space: string | undefined, addressText: string | undefined, digits: string | undefined, addressTarget: string | undefined, at: number, all: string) => {
73
+ // "[`x`]" without a target is no link of ours: leave it.
74
+ if (open !== undefined && nameTarget === undefined) return whole;
75
+ const ownLink = (target: string | undefined) => target !== undefined && !context.managed(targetPath(context.page, target) ?? "");
76
+ const address = context.addressOf(name);
77
+ const report = (kind: ProblemKind, message: string) => problems.push({ page: context.page, line: index + 1, kind, message });
78
+ // In a list "`a`/`b` `$1000`/`$1010`" the address belongs to another name: the name gets its link, the address stays as it is.
79
+ const namePart = open === undefined ? `${TICK}${name}${offset ?? ""}${TICK}` : `[${TICK}${name}${offset ?? ""}${TICK}](${nameTarget})`;
80
+ const listed = addressText !== undefined && (all[at - 1] === "/" || all[at + whole.length] === "/");
81
+ const rest = listed ? whole.slice(namePart.length) : "";
82
+ const claimed = addressText !== undefined;
83
+ if (listed) addressText = digits = addressTarget = undefined;
84
+ if (address === undefined) {
85
+ // A word in backticks is no claim of a name; with an address after it, or with a link of ours, it is.
86
+ if (claimed || (nameTarget !== undefined && !ownLink(nameTarget))) {
87
+ report("unknown-name", `\`${name}\` is no name of the knowledge or the source. Apply the name to the knowledge, or correct the page.`);
88
+ }
89
+ return whole;
90
+ }
91
+ const delta = offset === undefined ? 0 : offset.startsWith("+$") ? Number.parseInt(offset.slice(2), 16) : Number.parseInt(offset.slice(1), 10);
92
+ const given = digits === undefined ? undefined : Number.parseInt(digits, 16);
93
+ const place = given !== undefined && given !== address + delta && given > address && given <= (context.bodyEnd(name) ?? address);
94
+ if (given !== undefined && given !== address + delta && !place) {
95
+ const expected = `$${(address + delta).toString(16).toUpperCase().padStart(4, "0")}`;
96
+ const holder = context.placeOf(given!);
97
+ report("wrong-address", `\`${name}${offset ?? ""}\` is ${expected} in the knowledge, and the page gives ${addressText}${holder === undefined || holder === name ? "" : `, a place in \`${holder}\``}. Correct the name or the address.${digits!.length <= 2 ? ` For a value, write \`${name}\` = \`${addressText}\`.` : ""}`);
98
+ }
99
+ const shownName = `${TICK}${name}${offset ?? ""}${TICK}`;
100
+ const code = context.codeOf(name);
101
+ const nameLink = ownLink(nameTarget) ? `[${shownName}](${nameTarget})` : code === undefined ? shownName : `[${shownName}](${relative(context.page, code)})`;
102
+ let result = nameLink;
103
+ if (addressText !== undefined) {
104
+ const entry = place ? context.lineOf(given!) : context.entryOf(name);
105
+ const shownAddress = `${TICK}${addressText}${TICK}`;
106
+ result += space! + (ownLink(addressTarget) ? `[${shownAddress}](${addressTarget})` : entry === undefined ? shownAddress : `[${shownAddress}](${relative(context.page, entry)})`);
107
+ }
108
+ result += rest;
109
+ if (result !== whole) changed++;
110
+ return result;
111
+ });
112
+ });
113
+ return { text: lines.join("\n"), changed, problems };
114
+ }
115
+
116
+ /** The files that a check reads: their text, or undefined when the file does not exist. */
117
+ export interface Files {
118
+ exists: (path: string) => boolean;
119
+ /** The text of a project file, or undefined when it is no file. */
120
+ text: (path: string) => string | undefined;
121
+ inside: (path: string) => boolean;
122
+ }
123
+
124
+ /** Checks that each link and image of a page reaches a file, and its anchor or line. */
125
+ export function checkLinks(page: string, text: string, files: Files): Problem[] {
126
+ const problems: Problem[] = [];
127
+ const anchors = new Map<string, Set<string>>();
128
+ const anchorsAt = (path: string) => {
129
+ if (!anchors.has(path)) anchors.set(path, anchorsOf(path === page ? text : (files.text(path) ?? "")));
130
+ return anchors.get(path)!;
131
+ };
132
+ for (const link of linksOf(text)) {
133
+ const report = (kind: ProblemKind, message: string) => problems.push({ page, line: link.line, kind, message });
134
+ const fragment = link.target.includes("#") ? link.target.slice(link.target.indexOf("#") + 1) : undefined;
135
+ if (/^[a-z][a-z0-9+.-]*:/i.test(link.target)) continue;
136
+ const path = link.target.startsWith("#") ? page : targetPath(page, link.target)!;
137
+ if (!files.inside(path)) {
138
+ report("outside-project", `${link.target} leaves the project folder.`);
139
+ continue;
140
+ }
141
+ if (!files.exists(path)) {
142
+ report("missing-file", `${link.target}: there is no ${path}.`);
143
+ continue;
144
+ }
145
+ if (fragment === undefined || fragment === "") continue;
146
+ const line = /^L(\d+)(?:-L?\d+)?$/.exec(fragment);
147
+ if (path.endsWith(".md")) {
148
+ if (!anchorsAt(path).has(fragment)) report("missing-anchor", `${link.target}: ${path} has no anchor ${fragment}.`);
149
+ } else if (line !== null) {
150
+ const count = (files.text(path) ?? "").split("\n").length;
151
+ if (Number(line[1]) > count) report("missing-line", `${link.target}: ${path} has ${count} lines.`);
152
+ }
153
+ }
154
+ return problems;
155
+ }
@@ -0,0 +1,79 @@
1
+ // What the documentation scripts read in a Markdown page: the lines outside
2
+ // code blocks and HTML comments, the anchors that a link can name, and the
3
+ // links and images with their targets.
4
+
5
+ /** The lines of a page and, for each one, whether it is prose: outside fenced code (also mermaid) and outside HTML comments. */
6
+ export function proseLines(text: string): Array<{ text: string; prose: boolean }> {
7
+ const lines = text.split("\n");
8
+ let fence: string | undefined;
9
+ let comment = false;
10
+ return lines.map((line) => {
11
+ const opening = /^\s*(`{3,}|~{3,})/.exec(line);
12
+ if (fence !== undefined) {
13
+ if (opening !== null && opening[1]!.startsWith(fence[0]!) && opening[1]!.length >= fence.length && line.trim() === opening[1]) fence = undefined;
14
+ return { text: line, prose: false };
15
+ }
16
+ if (opening !== null) {
17
+ fence = opening[1]!;
18
+ return { text: line, prose: false };
19
+ }
20
+ if (comment) {
21
+ if (line.includes("-->")) comment = false;
22
+ return { text: line, prose: false };
23
+ }
24
+ if (line.trimStart().startsWith("<!--")) {
25
+ comment = !line.includes("-->");
26
+ return { text: line, prose: false };
27
+ }
28
+ return { text: line, prose: true };
29
+ });
30
+ }
31
+
32
+ /** The id that GitHub and VS Code give a heading: lower case, without punctuation, spaces as hyphens. */
33
+ export function headingSlug(heading: string): string {
34
+ const text = heading
35
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
36
+ .replace(/<[^>]+>/g, "")
37
+ .replace(/`/g, "")
38
+ .trim()
39
+ .toLowerCase();
40
+ return text.replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
41
+ }
42
+
43
+ /** The anchors of a page: each <a id> or <a name>, and each heading slug, with -1, -2 for a slug that comes again. */
44
+ export function anchorsOf(text: string): Set<string> {
45
+ const anchors = new Set<string>();
46
+ const seen = new Map<string, number>();
47
+ for (const line of proseLines(text)) {
48
+ for (const match of line.text.matchAll(/<a\s+(?:id|name)="([^"]+)"/g)) anchors.add(match[1]!);
49
+ if (!line.prose) continue;
50
+ const heading = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line.text);
51
+ if (heading === null) continue;
52
+ const slug = headingSlug(heading[1]!);
53
+ const count = seen.get(slug) ?? 0;
54
+ seen.set(slug, count + 1);
55
+ anchors.add(count === 0 ? slug : `${slug}-${count}`);
56
+ }
57
+ return anchors;
58
+ }
59
+
60
+ export interface Link {
61
+ /** 1 is the first line. */
62
+ line: number;
63
+ image: boolean;
64
+ target: string;
65
+ }
66
+
67
+ /** The links and images of the prose lines; links inside inline code are not links. */
68
+ export function linksOf(text: string): Link[] {
69
+ const links: Link[] = [];
70
+ proseLines(text).forEach((line, index) => {
71
+ if (!line.prose) return;
72
+ // Inline code that holds no link text: `[x](y)` inside one pair of backticks is code, not a link.
73
+ const outside = line.text.replace(/`[^`]*`/g, (code) => (code.includes("](") ? " ".repeat(code.length) : code));
74
+ for (const match of outside.matchAll(/(!?)\[(?:[^\]`]|`[^`]*`)*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
75
+ links.push({ line: index + 1, image: match[1] === "!", target: match[2]! });
76
+ }
77
+ });
78
+ return links;
79
+ }
@@ -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("", `![${anchor} in ${mode}](${rel(page, image)})`);
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
+ }