@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
@@ -1,14 +1,23 @@
1
- // The one place that opens .c64-re-tools/knowledge.db.
1
+ // The one place that opens the knowledge. The knowledge is text in
2
+ // .c64-re-tools/knowledge/ (D69); .c64-re-tools/local/knowledge.db is an index
3
+ // that is made again from the text when the text changed.
2
4
 
3
- import { existsSync, mkdirSync } from "node:fs";
5
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
6
  import { DatabaseSync } from "node:sqlite";
5
7
  import { join } from "node:path";
6
8
 
7
- import { projectRoot } from "../project.ts";
8
- import { checkVersion, ensureSchema, schemaVersion } from "./schema.ts";
9
+ import { ensureLocal, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRoot } from "../project.ts";
10
+ import { ensureSchema } from "./schema.ts";
11
+ import { compareText, loadText, readText, REVISIONS_FILE, revisionsText, sha256, sqlName, TABLES, tableText, writeAtomic } from "./text.ts";
9
12
 
10
- export const KNOWLEDGE_DIRECTORY = ".c64-re-tools";
11
- export const KNOWLEDGE_FILE = "knowledge.db";
13
+ export const KNOWLEDGE_DIRECTORY = PROJECT_DIRECTORY;
14
+ /** The folder of the committed knowledge text. */
15
+ export const TEXT_DIRECTORY = "knowledge";
16
+ /** The index file in .c64-re-tools/local/. */
17
+ export const INDEX_FILE = "knowledge.db";
18
+ /** Git keeps the lines of both sides when two computers changed the same knowledge file. */
19
+ // eol=lf: with core.autocrlf (the Windows default) git would check the files out with CRLF, so two computers would hold other bytes.
20
+ export const GIT_ATTRIBUTES = "# The knowledge of c64-re-tools: git keeps the lines of both sides of a merge, with LF line ends on every system.\n*.jsonl merge=union text eol=lf\n.gitattributes text eol=lf\n";
12
21
 
13
22
  export const KNOWLEDGE_ERROR_CODES = [
14
23
  "invalid-input",
@@ -31,12 +40,27 @@ export class KnowledgeError extends Error {
31
40
  }
32
41
  }
33
42
 
34
- export function knowledgePath(root: string = projectRoot()): string {
35
- return join(root, KNOWLEDGE_DIRECTORY, KNOWLEDGE_FILE);
43
+ /** The folder of the knowledge text: .c64-re-tools/knowledge/. */
44
+ export function textPath(root: string = projectRoot()): string {
45
+ return join(root, KNOWLEDGE_DIRECTORY, TEXT_DIRECTORY);
46
+ }
47
+
48
+ /** The index: .c64-re-tools/local/knowledge.db. */
49
+ export function indexPath(root: string = projectRoot()): string {
50
+ return join(root, KNOWLEDGE_DIRECTORY, LOCAL_DIRECTORY, INDEX_FILE);
51
+ }
52
+
53
+ /** Refuses a project that has only the knowledge.db of an older c64-re-tools. */
54
+ function refuseOldLayout(root: string): void {
55
+ if (!existsSync(textPath(root)) && existsSync(join(root, KNOWLEDGE_DIRECTORY, INDEX_FILE))) {
56
+ throw new KnowledgeError(
57
+ "unsupported-schema",
58
+ `${KNOWLEDGE_DIRECTORY}/${INDEX_FILE} is the knowledge of an older c64-re-tools. This c64-re-tools keeps the knowledge as text in ${KNOWLEDGE_DIRECTORY}/${TEXT_DIRECTORY}/. Convert it one time with distribution/convert-knowledge.ts of the c64-re-tools source repository.`,
59
+ );
60
+ }
36
61
  }
37
62
 
38
63
  function configure(db: DatabaseSync): void {
39
- // A committed write must be complete in knowledge.db itself: rollback journal, never WAL.
40
64
  db.exec("PRAGMA journal_mode = DELETE; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
41
65
  }
42
66
 
@@ -47,28 +71,23 @@ function open(path: string, readOnly: boolean): DatabaseSync {
47
71
  configure(db);
48
72
  db.prepare("SELECT count(*) FROM sqlite_master").get();
49
73
  } catch (error) {
50
- throw new KnowledgeError("invalid-database", `${KNOWLEDGE_DIRECTORY}/${KNOWLEDGE_FILE} is not a readable SQLite database: ${(error as Error).message}`);
74
+ throw new KnowledgeError("invalid-database", `${KNOWLEDGE_DIRECTORY}/${LOCAL_DIRECTORY}/${INDEX_FILE} is not a readable SQLite database: ${(error as Error).message} Delete it: it is made again from the knowledge text.`);
51
75
  }
52
76
  return db;
53
77
  }
54
78
 
55
- /**
56
- * Opens the project's knowledge database read-only, as it is: a read never
57
- * migrates and never writes. Returns undefined when the database does not
58
- * exist or has no schema yet: that reads as empty knowledge, and a read
59
- * creates nothing. Refuses a schema version other than this build's.
60
- */
61
- export function openForRead(root: string = projectRoot()): DatabaseSync | undefined {
62
- const path = knowledgePath(root);
63
- if (!existsSync(path)) return undefined;
64
- const db = open(path, true);
79
+ /** Opens the index for a write and gives it the schema of this build. A file that is no database is made again. */
80
+ function openIndex(root: string): DatabaseSync {
81
+ ensureLocal(root);
82
+ let db: DatabaseSync;
65
83
  try {
66
- const version = schemaVersion(db);
67
- if (version === 0) {
68
- db.close();
69
- return undefined;
70
- }
71
- checkVersion(version);
84
+ db = open(indexPath(root), false);
85
+ } catch {
86
+ writeFileSync(indexPath(root), "");
87
+ db = open(indexPath(root), false);
88
+ }
89
+ try {
90
+ ensureSchema(db);
72
91
  } catch (error) {
73
92
  db.close();
74
93
  throw error;
@@ -76,16 +95,75 @@ export function openForRead(root: string = projectRoot()): DatabaseSync | undefi
76
95
  return db;
77
96
  }
78
97
 
98
+ function storedHashes(db: DatabaseSync): Record<string, string> {
99
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'text_hashes'").get() as { value: string } | undefined;
100
+ return row === undefined ? {} : (JSON.parse(row.value) as Record<string, string>);
101
+ }
102
+
103
+ function storeHashes(db: DatabaseSync, hashes: Record<string, string>): void {
104
+ const sorted = Object.fromEntries(Object.entries(hashes).sort(([a], [b]) => compareText(a, b)));
105
+ db.prepare("INSERT INTO meta (key, value) VALUES ('text_hashes', ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value").run(JSON.stringify(sorted));
106
+ }
107
+
108
+ /** The SHA-256 of each knowledge file, by file name, in name order. */
109
+ function textHashes(files: ReadonlyMap<string, { sha256: string }>): Record<string, string> {
110
+ return Object.fromEntries([...files].map(([name, file]): [string, string] => [name, file.sha256]).sort(([a], [b]) => compareText(a, b)));
111
+ }
112
+
113
+ const sameHashes = (a: Record<string, string>, b: Record<string, string>) => JSON.stringify(Object.entries(a).sort()) === JSON.stringify(Object.entries(b).sort());
114
+
79
115
  /**
80
- * Opens the project's knowledge database for a write, creating
81
- * .c64-re-tools/knowledge.db and its schema when needed. Refuses a schema
82
- * version other than this build's.
116
+ * Makes the index equal to the text when a knowledge file has another
117
+ * SHA-256 than the index holds, or a file came or went. Runs inside a write
118
+ * transaction, so two processes never make the index at the same time.
119
+ */
120
+ function freshen(db: DatabaseSync, root: string): void {
121
+ const files = readText(textPath(root));
122
+ const hashes = textHashes(files);
123
+ if (sameHashes(hashes, storedHashes(db))) return;
124
+ db.exec("DELETE FROM evidence");
125
+ for (const spec of TABLES) db.exec(`DELETE FROM ${sqlName(spec.table)}`);
126
+ db.exec("DELETE FROM revisions");
127
+ loadText(db, files);
128
+ storeHashes(db, hashes);
129
+ }
130
+
131
+ /** The project of each index opened for a write: its writes go to the text too. */
132
+ const projects = new WeakMap<DatabaseSync, string>();
133
+
134
+ /**
135
+ * Opens the project's knowledge read-only, made fresh from the text first. A
136
+ * read never changes the knowledge. Returns undefined when the project has no
137
+ * knowledge text: that reads as empty knowledge, and a read creates nothing.
138
+ */
139
+ export function openForRead(root: string = projectRoot()): DatabaseSync | undefined {
140
+ refuseOldLayout(root);
141
+ if (!existsSync(textPath(root))) return undefined;
142
+ const index = openIndex(root);
143
+ try {
144
+ // The lock is taken only when the text changed: a read does not wait for a long write that keeps the index fresh.
145
+ if (!sameHashes(textHashes(readText(textPath(root))), storedHashes(index))) transaction(index, () => freshen(index, root));
146
+ } finally {
147
+ index.close();
148
+ }
149
+ return open(indexPath(root), true);
150
+ }
151
+
152
+ /**
153
+ * Opens the project's knowledge for a write: it creates
154
+ * .c64-re-tools/knowledge/ with its .gitattributes when needed, and the index.
155
+ * Every committed transaction on the database writes the changed knowledge
156
+ * files.
83
157
  */
84
158
  export function openForWrite(root: string = projectRoot()): DatabaseSync {
85
- mkdirSync(join(root, KNOWLEDGE_DIRECTORY), { recursive: true });
86
- const db = open(knowledgePath(root), false);
159
+ refuseOldLayout(root);
160
+ mkdirSync(textPath(root), { recursive: true });
161
+ const attributes = join(textPath(root), ".gitattributes");
162
+ if (!existsSync(attributes)) writeFileSync(attributes, GIT_ATTRIBUTES);
163
+ const db = openIndex(root);
164
+ projects.set(db, root);
87
165
  try {
88
- ensureSchema(db);
166
+ transaction(db, () => undefined);
89
167
  } catch (error) {
90
168
  db.close();
91
169
  throw error;
@@ -94,13 +172,14 @@ export function openForWrite(root: string = projectRoot()): DatabaseSync {
94
172
  }
95
173
 
96
174
  /**
97
- * Runs one write on the project's knowledge database and closes it. When the
98
- * project has no knowledge.db yet, `work` first runs on an empty database in
99
- * memory: a write that is refused there creates no .c64-re-tools/. `work`
100
- * must change nothing outside the database it gets.
175
+ * Runs one write on the project's knowledge and closes it. When the project
176
+ * has no knowledge yet, `work` first runs on an empty database in memory: a
177
+ * write that is refused there creates no .c64-re-tools/. `work` must change
178
+ * nothing outside the database it gets.
101
179
  */
102
180
  export function withWrite<T>(work: (db: DatabaseSync) => T, root: string = projectRoot()): T {
103
- if (!existsSync(knowledgePath(root))) {
181
+ refuseOldLayout(root);
182
+ if (!existsSync(textPath(root))) {
104
183
  const trial = new DatabaseSync(":memory:");
105
184
  try {
106
185
  configure(trial);
@@ -118,11 +197,44 @@ export function withWrite<T>(work: (db: DatabaseSync) => T, root: string = proje
118
197
  }
119
198
  }
120
199
 
121
- /** Runs `work` in one immediate transaction: all of it commits, or none of it does. */
200
+ /** The newest revision id, or "0" when there is none. */
201
+ function newest(db: DatabaseSync): string {
202
+ return (db.prepare("SELECT max(id) AS id FROM revisions").get() as { id: string | null }).id ?? "0";
203
+ }
204
+
205
+ /**
206
+ * Writes the knowledge files that the revisions after `before` changed, then
207
+ * keeps their SHA-256 in the index. The text is written before the index
208
+ * commits: when the commit fails, the next open makes the index from the text.
209
+ */
210
+ function writeText(db: DatabaseSync, root: string, before: string): void {
211
+ const directory = textPath(root);
212
+ const hashes = storedHashes(db);
213
+ const write = (file: string, text: string) => {
214
+ writeAtomic(join(directory, file), text);
215
+ hashes[file] = sha256(text);
216
+ };
217
+ write(REVISIONS_FILE, revisionsText(db));
218
+ for (const spec of TABLES) {
219
+ const changed = db.prepare(`SELECT 1 FROM ${sqlName(spec.table)} WHERE valid_from_revision > ? OR valid_to_revision > ? LIMIT 1`).get(before, before);
220
+ if (changed !== undefined) write(spec.file, tableText(db, spec));
221
+ }
222
+ storeHashes(db, hashes);
223
+ }
224
+
225
+ /**
226
+ * Runs `work` in one immediate transaction: all of it commits, or none of it
227
+ * does. On the knowledge of a project, the index is made fresh from the text
228
+ * first, and a work that adds a revision writes the changed knowledge files.
229
+ */
122
230
  export function transaction<T>(db: DatabaseSync, work: () => T): T {
231
+ const root = projects.get(db);
123
232
  db.exec("BEGIN IMMEDIATE");
124
233
  try {
234
+ if (root !== undefined) freshen(db, root);
235
+ const before = root === undefined ? "0" : newest(db);
125
236
  const result = work();
237
+ if (root !== undefined && newest(db) !== before) writeText(db, root, before);
126
238
  db.exec("COMMIT");
127
239
  return result;
128
240
  } catch (error) {
@@ -0,0 +1,209 @@
1
+ // Operand expressions (D65): the part of ACME's expression syntax that names
2
+ // an operand by symbols and numbers. Knowledge checks an expression against
3
+ // the image with it, and c64-source writes it with the names of its layout.
4
+ //
5
+ // Priorities are ACME's, from high to low: unary - and !, then * / %, + -,
6
+ // << >> >>>, unary < and >, &, XOR, |. A unary < or > takes everything of a
7
+ // higher priority after it, so <a+1 is the low byte of a+1.
8
+
9
+ /** An expression that does not parse, or that names a symbol with no value. */
10
+ export class ExpressionError extends Error {
11
+ override name = "ExpressionError";
12
+ }
13
+
14
+ /** The selectors of an operand expression: a byte of a symbol, its value, or free expression text. */
15
+ export const OPERAND_SELECTS = ["lo", "hi", "value", "expr"] as const;
16
+ export type OperandSelect = (typeof OPERAND_SELECTS)[number];
17
+
18
+ /** An operand expression as knowledge stores it: a symbol with an offset, or text for expr. */
19
+ export interface OperandExpression {
20
+ select: OperandSelect;
21
+ symbol?: string;
22
+ offset?: number;
23
+ expression?: string;
24
+ }
25
+
26
+ type Node =
27
+ | { kind: "number"; value: number }
28
+ | { kind: "name"; name: string }
29
+ | { kind: "unary"; operator: string; operand: Node }
30
+ | { kind: "binary"; operator: string; left: Node; right: Node };
31
+
32
+ /** Binary operators by priority (higher binds tighter); unary < and > sit at LOW_HIGH. */
33
+ const BINARY: Record<string, number> = { "*": 10, "/": 10, "%": 10, "+": 9, "-": 9, "<<": 8, ">>": 8, ">>>": 8, "&": 4, XOR: 3, "|": 2 };
34
+ const LOW_HIGH = 7;
35
+ const NAME = /^[A-Za-z_.][A-Za-z0-9_]*/;
36
+ const MAX_TEXT = 200;
37
+
38
+ interface Token {
39
+ text: string;
40
+ at: number;
41
+ }
42
+
43
+ /** The expression as tokens with their offsets; context decides % (binary literal or remainder). */
44
+ function tokenize(text: string): Token[] {
45
+ const tokens: Token[] = [];
46
+ let at = 0;
47
+ let operandNext = true;
48
+ while (at < text.length) {
49
+ const rest = text.slice(at);
50
+ const space = /^\s+/.exec(rest);
51
+ if (space) {
52
+ at += space[0].length;
53
+ continue;
54
+ }
55
+ const number: RegExpExecArray | null = operandNext ? /^(\$[0-9A-Fa-f]+|%[01]+|[0-9]+)/.exec(rest) : null;
56
+ const match: RegExpExecArray | null = number ?? /^(>>>|<<|>>|XOR\b)/.exec(rest) ?? NAME.exec(rest) ?? /^[-+*/%&|!<>()]/.exec(rest);
57
+ if (!match) throw new ExpressionError(`"${text}" has an unknown character at position ${at + 1}: "${rest[0]}".`);
58
+ const token: string = match[0];
59
+ tokens.push({ text: token, at });
60
+ at += token.length;
61
+ // After an operand or a ), an operator comes next; after anything else, an operand.
62
+ operandNext = !(number !== null || (NAME.test(token) && token !== "XOR") || token === ")");
63
+ }
64
+ return tokens;
65
+ }
66
+
67
+ function parse(text: string): Node {
68
+ if (text.trim().length === 0) throw new ExpressionError("An expression is empty.");
69
+ if (text.length > MAX_TEXT) throw new ExpressionError(`An expression has at most ${MAX_TEXT} characters.`);
70
+ const tokens = tokenize(text);
71
+ let index = 0;
72
+ const peek = () => tokens[index]?.text;
73
+ const fail = (what: string): never => {
74
+ throw new ExpressionError(`"${text}" ${what}.`);
75
+ };
76
+
77
+ const operand = (): Node => {
78
+ const token = tokens[index++]?.text ?? fail("ends where an operand is missing");
79
+ if (token === "(") {
80
+ const inner = binary(0);
81
+ if (tokens[index++]?.text !== ")") fail("has a ( with no )");
82
+ return inner;
83
+ }
84
+ if (token === "-" || token === "!") return { kind: "unary", operator: token, operand: operand() };
85
+ if (token === "<" || token === ">") return { kind: "unary", operator: token, operand: binary(LOW_HIGH + 1) };
86
+ if (token.startsWith("$")) return { kind: "number", value: Number.parseInt(token.slice(1), 16) };
87
+ if (token.startsWith("%")) return { kind: "number", value: Number.parseInt(token.slice(1), 2) };
88
+ if (/^[0-9]/.test(token)) return { kind: "number", value: Number.parseInt(token, 10) };
89
+ if (NAME.test(token) && token !== "XOR") return { kind: "name", name: token };
90
+ return fail(`has "${token}" where an operand must be`);
91
+ };
92
+
93
+ const binary = (minimum: number): Node => {
94
+ let left = operand();
95
+ for (;;) {
96
+ const operator = peek();
97
+ const priority = operator === undefined ? undefined : BINARY[operator];
98
+ if (priority === undefined || priority < minimum) return left;
99
+ index++;
100
+ left = { kind: "binary", operator: operator!, left, right: binary(priority + 1) };
101
+ }
102
+ };
103
+
104
+ const tree = binary(0);
105
+ if (index < tokens.length) fail(`has "${tokens[index]!.text}" after the end of the expression`);
106
+ return tree;
107
+ }
108
+
109
+ function names(node: Node, into: string[]): string[] {
110
+ if (node.kind === "name" && !into.includes(node.name)) into.push(node.name);
111
+ if (node.kind === "unary") names(node.operand, into);
112
+ if (node.kind === "binary") {
113
+ names(node.left, into);
114
+ names(node.right, into);
115
+ }
116
+ return into;
117
+ }
118
+
119
+ function value(node: Node, resolve: (name: string) => number | undefined): number {
120
+ switch (node.kind) {
121
+ case "number":
122
+ return node.value;
123
+ case "name": {
124
+ const found = resolve(node.name);
125
+ if (found === undefined) throw new ExpressionError(`"${node.name}" is no symbol of the knowledge and no platform name.`);
126
+ return found;
127
+ }
128
+ case "unary": {
129
+ const inner = value(node.operand, resolve);
130
+ if (node.operator === "-") return -inner;
131
+ if (node.operator === "!") return ~inner;
132
+ return node.operator === "<" ? inner & 0xff : (inner >> 8) & 0xff;
133
+ }
134
+ case "binary": {
135
+ const left = value(node.left, resolve);
136
+ const right = value(node.right, resolve);
137
+ switch (node.operator) {
138
+ case "*":
139
+ return left * right;
140
+ case "/":
141
+ case "%":
142
+ if (right === 0) throw new ExpressionError("An expression divides by zero.");
143
+ return node.operator === "/" ? Math.trunc(left / right) : left % right;
144
+ case "+":
145
+ return left + right;
146
+ case "-":
147
+ return left - right;
148
+ case "<<":
149
+ return left << right;
150
+ case ">>":
151
+ return left >> right;
152
+ case ">>>":
153
+ return left >>> right;
154
+ case "&":
155
+ return left & right;
156
+ case "XOR":
157
+ return left ^ right;
158
+ default:
159
+ return left | right;
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ /** Throws ExpressionError when the text is no expression of this syntax. */
166
+ export function checkExpression(text: string): void {
167
+ parse(text);
168
+ }
169
+
170
+ /** The value of the expression; `resolve` gives the value of a name, or undefined. */
171
+ export function evaluate(text: string, resolve: (name: string) => number | undefined): number {
172
+ return value(parse(text), resolve);
173
+ }
174
+
175
+ /** The names the expression uses, each once, in their order. */
176
+ export function identifiers(text: string): string[] {
177
+ return names(parse(text), []);
178
+ }
179
+
180
+ /**
181
+ * The text with each name replaced by `replace(name)` (a name it returns
182
+ * unchanged stays). Spaces and other tokens stay as written.
183
+ */
184
+ export function replaceIdentifiers(text: string, replace: (name: string) => string): string {
185
+ parse(text);
186
+ let out = "";
187
+ let from = 0;
188
+ for (const token of tokenize(text)) {
189
+ if (!NAME.test(token.text) || token.text === "XOR") continue;
190
+ out += text.slice(from, token.at) + replace(token.text);
191
+ from = token.at + token.text.length;
192
+ }
193
+ return out + text.slice(from);
194
+ }
195
+
196
+ function offsetText(offset: number): string {
197
+ const size = Math.abs(offset);
198
+ return `${offset < 0 ? "-" : "+"}${size < 10 ? String(size) : `$${size.toString(16)}`}`;
199
+ }
200
+
201
+ /** ACME text of an operand expression, for example <irq_handler, >(sprite_bank+$1000) or bar_colours-1. */
202
+ export function operandText(operand: OperandExpression): string {
203
+ if (operand.select === "expr") return operand.expression!;
204
+ const offset = operand.offset ?? 0;
205
+ const sum = offset === 0 ? operand.symbol! : `${operand.symbol!}${offsetText(offset)}`;
206
+ if (operand.select === "value") return sum;
207
+ const selector = operand.select === "lo" ? "<" : ">";
208
+ return offset === 0 ? `${selector}${sum}` : `${selector}(${sum})`;
209
+ }
@@ -0,0 +1,104 @@
1
+ // The routine graph of the knowledge (D78, D80): which routine calls, jumps
2
+ // to or dispatches through a table to which other routine, and the same
3
+ // graph as a mermaid flowchart. The callers give the rows, so the graph needs
4
+ // no database and the documentation and the knowledge script draw one graph.
5
+
6
+ import { formatC64Address } from "../c64.ts";
7
+ import type { ReferenceKind, SymbolKind } from "./schema.ts";
8
+
9
+ /** How one routine reaches another: a call, a jump, or an entry of a table of addresses. */
10
+ export type EdgeKind = "call" | "jump" | "table";
11
+
12
+ export interface GraphReference {
13
+ from: number;
14
+ to: number;
15
+ kind: ReferenceKind | "table";
16
+ }
17
+
18
+ export interface RoutineHead {
19
+ address: number;
20
+ name: string;
21
+ }
22
+
23
+ export interface GraphEdge {
24
+ from: number;
25
+ to: number;
26
+ /** The strongest kind of the references that make the edge: a call, then a table entry, then a jump. */
27
+ kind: EdgeKind;
28
+ /** The references that make the edge. */
29
+ count: number;
30
+ }
31
+
32
+ const STRENGTH: Record<EdgeKind, number> = { call: 3, table: 2, jump: 1 };
33
+
34
+ /**
35
+ * The routine heads: every routine of the knowledge, and every call or table
36
+ * target, which another routine enters at its start. A head without a name of
37
+ * the knowledge gets the name that `nameOf` gives.
38
+ */
39
+ export function routineHeads(symbols: ReadonlyArray<{ address: number; name: string; kind: SymbolKind }>, references: readonly GraphReference[], nameOf: (address: number) => string): RoutineHead[] {
40
+ const names = new Map(symbols.map((symbol) => [symbol.address, symbol.name]));
41
+ const heads = new Set(symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address));
42
+ for (const reference of references) if (reference.kind === "call" || reference.kind === "table") heads.add(reference.to);
43
+ return [...heads].sort((a, b) => a - b).map((address) => ({ address, name: names.get(address) ?? nameOf(address) }));
44
+ }
45
+
46
+ /**
47
+ * The head that holds an address: the nearest head at or below it, when no
48
+ * boundary (a module start) lies between the two.
49
+ */
50
+ export function ownerOf(heads: readonly RoutineHead[], boundaries: readonly number[], address: number): RoutineHead | undefined {
51
+ let low = 0;
52
+ let high = heads.length - 1;
53
+ let found: RoutineHead | undefined;
54
+ while (low <= high) {
55
+ const middle = (low + high) >> 1;
56
+ if (heads[middle]!.address <= address) {
57
+ found = heads[middle];
58
+ low = middle + 1;
59
+ } else high = middle - 1;
60
+ }
61
+ if (found === undefined) return undefined;
62
+ return boundaries.some((boundary) => boundary > found.address && boundary <= address) ? undefined : found;
63
+ }
64
+
65
+ /** The edges between routines: each call, jump or table reference from one head's code to another head; a jump inside a routine is no edge. */
66
+ export function routineEdges(heads: readonly RoutineHead[], boundaries: readonly number[], references: readonly GraphReference[]): GraphEdge[] {
67
+ const edges = new Map<string, GraphEdge>();
68
+ for (const reference of references) {
69
+ if (reference.kind !== "call" && reference.kind !== "jump" && reference.kind !== "table") continue;
70
+ const from = ownerOf(heads, boundaries, reference.from);
71
+ const to = ownerOf(heads, boundaries, reference.to);
72
+ if (from === undefined || to === undefined || from.address === to.address) continue;
73
+ // A jump into the middle of another routine is an edge to that routine.
74
+ const key = `${from.address}:${to.address}`;
75
+ const edge = edges.get(key);
76
+ if (edge === undefined) edges.set(key, { from: from.address, to: to.address, kind: reference.kind, count: 1 });
77
+ else {
78
+ edge.count++;
79
+ if (STRENGTH[reference.kind] > STRENGTH[edge.kind]) edge.kind = reference.kind;
80
+ }
81
+ }
82
+ return [...edges.values()].sort((a, b) => a.from - b.from || a.to - b.to);
83
+ }
84
+
85
+ const ARROWS: Record<EdgeKind, string> = { call: "-->", table: "==>", jump: "-.->" };
86
+
87
+ /**
88
+ * A mermaid flowchart of the edges: a node per head with its name and
89
+ * address, a solid arrow for a call, a thick one for a table entry and a
90
+ * dotted one for a jump. The heads in `own` are drawn as rounded nodes.
91
+ */
92
+ export function mermaidGraph(heads: readonly RoutineHead[], edges: readonly GraphEdge[], own: ReadonlySet<number> = new Set()): string {
93
+ const used = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
94
+ for (const address of own) used.add(address);
95
+ const id = (address: number) => `n${address.toString(16).padStart(4, "0")}`;
96
+ const lines = ["flowchart LR"];
97
+ for (const head of heads) {
98
+ if (!used.has(head.address)) continue;
99
+ const label = `${head.name}<br/>${formatC64Address(head.address).toUpperCase()}`;
100
+ lines.push(own.has(head.address) ? ` ${id(head.address)}("${label}")` : ` ${id(head.address)}["${label}"]`);
101
+ }
102
+ for (const edge of edges) lines.push(` ${id(edge.from)} ${ARROWS[edge.kind]} ${id(edge.to)}`);
103
+ return lines.join("\n");
104
+ }