@henols/c64-re-tools 2.0.0-rc.9 → 2.0.1
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 +97 -116
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -5
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +25 -5
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +8 -2
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/package.ts +13 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
package/src/knowledge/write.ts
CHANGED
|
@@ -3,33 +3,56 @@
|
|
|
3
3
|
|
|
4
4
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
5
|
|
|
6
|
-
import { SYMBOL_NAME } from "../c64.ts";
|
|
6
|
+
import { instructionLength, SYMBOL_NAME } from "../c64.ts";
|
|
7
7
|
import { isoCet } from "../time.ts";
|
|
8
|
+
import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
|
|
8
9
|
import { KnowledgeError, transaction } from "./database.ts";
|
|
10
|
+
import { checkExpression, evaluate, ExpressionError, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
|
|
9
11
|
import {
|
|
10
12
|
COMMENT_COLUMNS,
|
|
11
13
|
commentsAt,
|
|
12
14
|
currentRevision,
|
|
15
|
+
listMacroUses,
|
|
16
|
+
listOperands,
|
|
17
|
+
listPatches,
|
|
18
|
+
listText,
|
|
19
|
+
MACRO_COLUMNS,
|
|
20
|
+
MACRO_USE_COLUMNS,
|
|
21
|
+
macroNamed,
|
|
22
|
+
macroUsesOverlapping,
|
|
13
23
|
MODULE_COLUMNS,
|
|
14
24
|
moduleAt,
|
|
15
25
|
moduleWithFile,
|
|
26
|
+
OPERAND_COLUMNS,
|
|
27
|
+
operandAt,
|
|
28
|
+
PATCH_COLUMNS,
|
|
29
|
+
patchesOverlapping,
|
|
16
30
|
REFERENCE_COLUMNS,
|
|
17
31
|
referencesFrom,
|
|
18
32
|
REGION_COLUMNS,
|
|
19
33
|
regionsOverlapping,
|
|
34
|
+
resolveRevision,
|
|
20
35
|
SUPPRESSION_COLUMNS,
|
|
21
36
|
suppressionsOverlapping,
|
|
22
37
|
SYMBOL_COLUMNS,
|
|
23
38
|
symbolAt,
|
|
24
39
|
symbolNamed,
|
|
25
40
|
toComment,
|
|
41
|
+
toMacro,
|
|
42
|
+
toMacroUse,
|
|
26
43
|
toModule,
|
|
44
|
+
toOperand,
|
|
45
|
+
toPatch,
|
|
27
46
|
toReference,
|
|
28
47
|
toRegion,
|
|
29
48
|
toSuppression,
|
|
30
49
|
toSymbol,
|
|
31
50
|
type CommentRow,
|
|
51
|
+
type MacroRow,
|
|
52
|
+
type MacroUseRow,
|
|
32
53
|
type ModuleRow,
|
|
54
|
+
type OperandRow,
|
|
55
|
+
type PatchRow,
|
|
33
56
|
type ReferenceRow,
|
|
34
57
|
type RegionRow,
|
|
35
58
|
type SuppressionRow,
|
|
@@ -43,25 +66,34 @@ import {
|
|
|
43
66
|
DEFAULT_SPACE,
|
|
44
67
|
GENERATED_FILES,
|
|
45
68
|
isGeneratedFile,
|
|
69
|
+
MACRO_NAME,
|
|
70
|
+
MACRO_PARAM,
|
|
46
71
|
SPACE,
|
|
72
|
+
SWITCH_NAME,
|
|
47
73
|
NO_ELEMENT_TYPES,
|
|
74
|
+
OPERAND_SELECTS,
|
|
75
|
+
POINTER_KIND,
|
|
48
76
|
REFERENCE_KINDS,
|
|
49
77
|
REGION_TYPES,
|
|
50
78
|
SYMBOL_KINDS,
|
|
51
79
|
type CommentPlacement,
|
|
52
80
|
type EvidenceKind,
|
|
81
|
+
type OperandSelect,
|
|
53
82
|
type Origin,
|
|
54
83
|
type ReferenceKind,
|
|
55
84
|
type RegionType,
|
|
56
85
|
type Space,
|
|
57
86
|
type SymbolKind,
|
|
58
87
|
} from "./schema.ts";
|
|
88
|
+
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
59
89
|
|
|
60
|
-
/** What a change rests on
|
|
90
|
+
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
61
91
|
export interface Evidence {
|
|
62
92
|
kind: EvidenceKind;
|
|
63
93
|
ref: string;
|
|
64
94
|
sha256?: string;
|
|
95
|
+
/** The space that a coverage map was imported into; none is the C64, as for a row. */
|
|
96
|
+
space?: Space;
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
/** Who writes and why. Semantic writes come from the user or the LLM, never as an analyzer. */
|
|
@@ -70,14 +102,22 @@ export interface WriteContext {
|
|
|
70
102
|
/** A short reason, kept with the revision for later review. */
|
|
71
103
|
description?: string;
|
|
72
104
|
/** The revision the caller read; the write is refused if knowledge changed since. */
|
|
73
|
-
expectedRevision?:
|
|
105
|
+
expectedRevision?: string;
|
|
74
106
|
/** Kept with the revision when the write changes something. */
|
|
75
107
|
evidence?: Evidence[];
|
|
108
|
+
/** The image that an operand, patch or macro use write is checked against. */
|
|
109
|
+
image?: OperandImage;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The bytes of the image and the platform names (C64 space only) that operand expressions, patches and macro uses are checked against. */
|
|
113
|
+
export interface OperandImage {
|
|
114
|
+
byteAt(address: number): number | undefined;
|
|
115
|
+
platform: ReadonlyMap<string, number>;
|
|
76
116
|
}
|
|
77
117
|
|
|
78
118
|
/** The revision a write created, or null when nothing needed to change. */
|
|
79
119
|
export interface WriteResult<T, P = never> {
|
|
80
|
-
revision:
|
|
120
|
+
revision: string | null;
|
|
81
121
|
current: T;
|
|
82
122
|
/** The current rows of another origin that this write replaced, as they were before it. */
|
|
83
123
|
previous?: P;
|
|
@@ -85,6 +125,10 @@ export interface WriteResult<T, P = never> {
|
|
|
85
125
|
|
|
86
126
|
export { SYMBOL_NAME };
|
|
87
127
|
const MAX_COMMENT = 4000;
|
|
128
|
+
/** The most characters of patch code and of a macro body. */
|
|
129
|
+
const MAX_CODE = 8000;
|
|
130
|
+
/** The most parameters of a macro. */
|
|
131
|
+
const MAX_PARAMS = 16;
|
|
88
132
|
const MAX_EVIDENCE_REF = 1000;
|
|
89
133
|
|
|
90
134
|
export function checkAddress(value: number, what = "address"): void {
|
|
@@ -122,7 +166,7 @@ export function subtract(range: AddressRange, holes: AddressRange[]): AddressRan
|
|
|
122
166
|
export interface ChangeContext {
|
|
123
167
|
origin: Origin;
|
|
124
168
|
description?: string;
|
|
125
|
-
expectedRevision?:
|
|
169
|
+
expectedRevision?: string;
|
|
126
170
|
inputHash?: string;
|
|
127
171
|
toolVersion?: string;
|
|
128
172
|
evidence?: Evidence[];
|
|
@@ -133,7 +177,9 @@ export interface ChangeContext {
|
|
|
133
177
|
* only when the first actual change happens, so a no-op leaves no revision.
|
|
134
178
|
*/
|
|
135
179
|
export class Change {
|
|
136
|
-
revision:
|
|
180
|
+
revision: string | null = null;
|
|
181
|
+
/** The rows this write inserted, for their ids. */
|
|
182
|
+
#rows = 0;
|
|
137
183
|
readonly #db: DatabaseSync;
|
|
138
184
|
readonly #context: ChangeContext;
|
|
139
185
|
readonly #operation: string;
|
|
@@ -142,8 +188,16 @@ export class Change {
|
|
|
142
188
|
this.#db = db;
|
|
143
189
|
this.#context = context;
|
|
144
190
|
this.#operation = operation;
|
|
191
|
+
// A merge of two computers' work can leave conflicts: only a merge revision writes then.
|
|
192
|
+
const found = operation === "merge" ? [] : conflicts(db);
|
|
193
|
+
if (found.length > 0) {
|
|
194
|
+
throw new KnowledgeError(
|
|
195
|
+
"conflict",
|
|
196
|
+
`The knowledge has ${found.length} conflict${found.length === 1 ? "" : "s"} from a git merge, for example ${describe(found[0]!)}. Nothing was written. Run knowledge.ts merge to decide them, then write again.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
145
199
|
const current = currentRevision(db);
|
|
146
|
-
if (context.expectedRevision !== undefined && context.expectedRevision !== current) {
|
|
200
|
+
if (context.expectedRevision !== undefined && resolveRevision(db, context.expectedRevision) !== current) {
|
|
147
201
|
throw new KnowledgeError(
|
|
148
202
|
"stale-revision",
|
|
149
203
|
`Knowledge changed since revision ${context.expectedRevision}; it is now at revision ${current}. Read it again and decide again.`,
|
|
@@ -151,24 +205,33 @@ export class Change {
|
|
|
151
205
|
}
|
|
152
206
|
}
|
|
153
207
|
|
|
154
|
-
/** The id of this write's revision, created on first use with its evidence. */
|
|
155
|
-
id():
|
|
208
|
+
/** The id of this write's revision, created on first use with its evidence (an id that sorts after every revision). */
|
|
209
|
+
id(): string {
|
|
156
210
|
if (this.revision !== null) return this.revision;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
211
|
+
const now = new Date();
|
|
212
|
+
const id = newRevisionId(currentRevision(this.#db), now);
|
|
213
|
+
this.#db
|
|
214
|
+
.prepare("INSERT INTO revisions (id, created_at, origin, operation, input_hash, tool_version, description) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
215
|
+
.run(id, isoCet(now), this.#context.origin, this.#operation, this.#context.inputHash ?? null, this.#context.toolVersion ?? null, this.#context.description ?? null);
|
|
216
|
+
this.revision = id;
|
|
217
|
+
const evidence = this.#db.prepare("INSERT INTO evidence (revision, kind, ref, sha256, space) VALUES (?, ?, ?, ?, ?)");
|
|
218
|
+
for (const item of this.#context.evidence ?? []) evidence.run(id, item.kind, item.ref, item.sha256 ?? null, item.space ?? null);
|
|
219
|
+
return id;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Refuses a write that leaves two current rows where one is allowed: the
|
|
224
|
+
* index has no unique indexes, because a merged text can hold such rows.
|
|
225
|
+
*/
|
|
226
|
+
check(): void {
|
|
227
|
+
if (this.revision === null) return;
|
|
228
|
+
const [found] = conflicts(this.#db);
|
|
229
|
+
if (found !== undefined) throw new KnowledgeError("conflict", `The write would leave ${describe(found)}. Nothing was written.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The id of the next row this write inserts. */
|
|
233
|
+
#rowId(): string {
|
|
234
|
+
return rowId(this.id(), ++this.#rows);
|
|
172
235
|
}
|
|
173
236
|
|
|
174
237
|
/** Closes the current rows of `table` that `where` selects. */
|
|
@@ -177,40 +240,100 @@ export class Change {
|
|
|
177
240
|
}
|
|
178
241
|
|
|
179
242
|
insertSymbol(row: Omit<SymbolRow, "revision">): void {
|
|
180
|
-
this.#db.prepare("INSERT INTO symbols (space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)").run(row.space, row.address, row.name, row.kind, row.origin, this.id());
|
|
243
|
+
this.#db.prepare("INSERT INTO symbols (id, space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.name, row.kind, row.origin, this.id());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A second name of an address from an analyzer. */
|
|
247
|
+
insertAnalyzerName(row: Omit<SymbolRow, "revision">): void {
|
|
248
|
+
this.#db.prepare("INSERT INTO analyzer_names (id, space, address, name, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.name, row.kind, row.origin, this.id());
|
|
181
249
|
}
|
|
182
250
|
|
|
183
251
|
insertRegion(row: Omit<RegionRow, "revision">): void {
|
|
184
252
|
this.#db
|
|
185
|
-
.prepare("INSERT INTO regions (space, start_address, end_address, type, element, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
186
|
-
.run(row.space, row.start, row.end, row.type, row.element ?? null, row.origin, this.id());
|
|
253
|
+
.prepare("INSERT INTO regions (id, space, start_address, end_address, type, element, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
254
|
+
.run(this.#rowId(), row.space, row.start, row.end, row.type, row.element ?? null, row.origin, this.id());
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
insertComment(row: Omit<CommentRow, "revision">): void {
|
|
190
258
|
this.#db
|
|
191
|
-
.prepare("INSERT INTO comments (space, address, placement, text, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)")
|
|
192
|
-
.run(row.space, row.address, row.placement, row.text, row.origin, this.id());
|
|
259
|
+
.prepare("INSERT INTO comments (id, space, address, placement, text, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
260
|
+
.run(this.#rowId(), row.space, row.address, row.placement, row.text, row.origin, this.id());
|
|
193
261
|
}
|
|
194
262
|
|
|
195
263
|
insertReference(row: Omit<ReferenceRow, "revision">): void {
|
|
196
264
|
this.#db
|
|
197
|
-
.prepare('INSERT INTO "references" (space, from_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)')
|
|
198
|
-
.run(row.space, row.from, row.to, row.kind, row.origin, this.id());
|
|
265
|
+
.prepare('INSERT INTO "references" (id, space, from_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
266
|
+
.run(this.#rowId(), row.space, row.from, row.to, row.kind, row.origin, this.id());
|
|
199
267
|
}
|
|
200
268
|
|
|
201
269
|
insertModule(row: Omit<ModuleRow, "revision">): void {
|
|
202
270
|
this.#db
|
|
203
|
-
.prepare("INSERT INTO modules (space, start_address, file, title, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)")
|
|
204
|
-
.run(row.space, row.start, row.file, row.title ?? null, row.origin, this.id());
|
|
271
|
+
.prepare("INSERT INTO modules (id, space, start_address, file, title, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
272
|
+
.run(this.#rowId(), row.space, row.start, row.file, row.title ?? null, row.origin, this.id());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
insertOperand(row: Omit<OperandRow, "revision">): void {
|
|
276
|
+
this.#db
|
|
277
|
+
.prepare("INSERT INTO operands (id, space, address, selector, symbol, offset, expression, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
278
|
+
.run(this.#rowId(), row.space, row.address, row.select, row.symbol ?? null, row.offset, row.expression ?? null, row.origin, this.id());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
insertPatch(row: Omit<PatchRow, "revision">): void {
|
|
282
|
+
this.#db
|
|
283
|
+
.prepare("INSERT INTO patches (id, space, start_address, end_address, switch_name, code, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
284
|
+
.run(this.#rowId(), row.space, row.start, row.end, row.switch, row.code, row.comment ?? null, row.origin, this.id());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
insertMacro(row: Omit<MacroRow, "revision">): void {
|
|
288
|
+
this.#db
|
|
289
|
+
.prepare("INSERT INTO macros (id, space, name, params, body, comment, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
290
|
+
.run(this.#rowId(), row.space, row.name, listText(row.params), row.body, row.comment ?? null, row.origin, this.id());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
insertMacroUse(row: Omit<MacroUseRow, "revision">): void {
|
|
294
|
+
this.#db
|
|
295
|
+
.prepare("INSERT INTO macro_uses (id, space, start_address, end_address, macro, args, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
296
|
+
.run(this.#rowId(), row.space, row.start, row.end, row.macro, listText(row.args), row.origin, this.id());
|
|
205
297
|
}
|
|
206
298
|
|
|
207
299
|
insertSuppression(row: Omit<SuppressionRow, "revision">): void {
|
|
208
300
|
this.#db
|
|
209
|
-
.prepare("INSERT INTO suppressions (space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
210
|
-
.run(row.space, row.entity, row.start, row.end, row.to ?? null, row.kind ?? null, row.origin, this.id());
|
|
301
|
+
.prepare("INSERT INTO suppressions (id, space, entity, start_address, end_address, to_address, kind, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
302
|
+
.run(this.#rowId(), row.space, row.entity, row.start, row.end, row.to ?? null, row.kind ?? null, row.origin, this.id());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Bytes of the program, as hex digits. */
|
|
306
|
+
insertMemory(row: { space: Space; address: number; bytes: string; origin: Origin }): void {
|
|
307
|
+
this.#db.prepare("INSERT INTO memory (id, space, address, bytes, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?)").run(this.#rowId(), row.space, row.address, row.bytes, row.origin, this.id());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The entry of the program, with the machine state and the unpacker stages as JSON text when it has them. */
|
|
311
|
+
insertEntry(row: { space: Space; address: number; state: string | null; stages: string | null; origin: Origin }): void {
|
|
312
|
+
this.#db
|
|
313
|
+
.prepare("INSERT INTO entries (id, space, address, state, stages, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
314
|
+
.run(this.#rowId(), row.space, row.address, row.state, row.stages, row.origin, this.id());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** One coverage range of one kind. */
|
|
318
|
+
insertCoverage(row: { space: Space; kind: string; start: number; end: number; origin: Origin }): void {
|
|
319
|
+
this.#db
|
|
320
|
+
.prepare("INSERT INTO coverage (id, space, kind, start_address, end_address, origin, valid_from_revision) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
321
|
+
.run(this.#rowId(), row.space, row.kind, row.start, row.end, row.origin, this.id());
|
|
211
322
|
}
|
|
212
323
|
}
|
|
213
324
|
|
|
325
|
+
/** A conflict in words: its rule, its space and the addresses of its rows. */
|
|
326
|
+
function describe(conflict: Conflict): string {
|
|
327
|
+
const place = (entry: ConflictRow) => {
|
|
328
|
+
if (entry.entity === "macro") return entry.row.name;
|
|
329
|
+
const row = entry.row as { address?: number; start?: number; end?: number; from?: number };
|
|
330
|
+
if (row.address !== undefined) return hex(row.address);
|
|
331
|
+
if (row.from !== undefined && entry.entity === "reference") return hex(row.from);
|
|
332
|
+
return row.end === undefined || row.end === row.start ? hex(row.start!) : `${hex(row.start!)}-${hex(row.end)}`;
|
|
333
|
+
};
|
|
334
|
+
return `${CONFLICT_RULES[conflict.rule]} in ${conflict.space} (${[...new Set(conflict.rows.map(place))].join(", ")})`;
|
|
335
|
+
}
|
|
336
|
+
|
|
214
337
|
/** Closes the current region suppressions over [start, end] and keeps their parts outside it: a user/LLM region there decides now. */
|
|
215
338
|
function liftRegionSuppressions(db: DatabaseSync, change: Change, start: number, end: number, space: Space): void {
|
|
216
339
|
const overlapping = suppressionsOverlapping(db, "region", start, end, space);
|
|
@@ -227,11 +350,12 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
|
227
350
|
checkOneOf(EVIDENCE_KINDS, item.kind, "evidence kind");
|
|
228
351
|
if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new KnowledgeError("invalid-input", `evidence has 1 to ${MAX_EVIDENCE_REF} characters after its kind.`);
|
|
229
352
|
if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new KnowledgeError("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
|
|
353
|
+
checkSpace(item);
|
|
230
354
|
}
|
|
231
355
|
}
|
|
232
356
|
|
|
233
357
|
/** Runs semantic writes in one transaction, as one revision. */
|
|
234
|
-
function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision:
|
|
358
|
+
export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
235
359
|
if (context.origin !== "user" && context.origin !== "llm") {
|
|
236
360
|
throw new KnowledgeError("invalid-input", "A semantic write comes from the user or the LLM.");
|
|
237
361
|
}
|
|
@@ -239,6 +363,7 @@ function write<T>(db: DatabaseSync, context: WriteContext, operation: string, wo
|
|
|
239
363
|
return transaction(db, () => {
|
|
240
364
|
const change = new Change(db, context, operation);
|
|
241
365
|
const current = work(change);
|
|
366
|
+
change.check();
|
|
242
367
|
return { revision: change.revision, current };
|
|
243
368
|
});
|
|
244
369
|
}
|
|
@@ -249,13 +374,13 @@ interface Step<T, P = never> {
|
|
|
249
374
|
previous?: P;
|
|
250
375
|
}
|
|
251
376
|
|
|
252
|
-
const result = <T, P>(written: { revision:
|
|
377
|
+
const result = <T, P>(written: { revision: string | null; current: Step<T, P> }): WriteResult<T, P> =>
|
|
253
378
|
written.current.previous === undefined ? { revision: written.revision, current: written.current.current } : { revision: written.revision, current: written.current.current, previous: written.current.previous };
|
|
254
379
|
|
|
255
380
|
// Each write has a check, which refuses bad input before anything opens, and a
|
|
256
|
-
// step, which runs inside a change. A single write and a batch share both
|
|
381
|
+
// step, which runs inside a change. A single write and a batch share both.
|
|
257
382
|
|
|
258
|
-
/** Every request names its space
|
|
383
|
+
/** Every request names its space; without one it is the C64. */
|
|
259
384
|
type InSpace = { space?: Space };
|
|
260
385
|
type SymbolRequest = InSpace & { address: number; name: string; kind?: SymbolKind };
|
|
261
386
|
type RegionRequest = InSpace & { start: number; end: number; type: RegionType; element?: number };
|
|
@@ -289,8 +414,14 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
289
414
|
if (owner !== undefined && owner.address !== request.address) {
|
|
290
415
|
throw new KnowledgeError("conflict", `The name ${request.name} already belongs to ${hex(owner.address)}. Rename or remove that symbol first.`);
|
|
291
416
|
}
|
|
417
|
+
const [patch] = listPatches(db, undefined, request.name, undefined, space);
|
|
418
|
+
if (patch !== undefined) throw new KnowledgeError("conflict", `${request.name} is the switch of the patch at ${hex(patch.start)}-${hex(patch.end)}. Give the symbol another name.`);
|
|
292
419
|
if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
293
420
|
change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
|
|
421
|
+
if (existing !== undefined && existing.name !== request.name) {
|
|
422
|
+
renameInOperands(db, change, space, existing.name, request.name);
|
|
423
|
+
renameInUses(db, change, space, existing.name, request.name);
|
|
424
|
+
}
|
|
294
425
|
change.close("suppressions", "space = ? AND entity = 'symbol' AND start_address = ?", space, request.address);
|
|
295
426
|
const current = symbolAt(db, request.address, space)!;
|
|
296
427
|
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
@@ -298,9 +429,21 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
298
429
|
|
|
299
430
|
function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, request: AddressRequest): Step<null> {
|
|
300
431
|
const space = spaceOf(request);
|
|
301
|
-
|
|
432
|
+
const existing = symbolAt(db, request.address, space);
|
|
433
|
+
if (existing === undefined) throw new KnowledgeError("not-found", `There is no symbol at ${hex(request.address)}.`);
|
|
434
|
+
const users = operandsNaming(db, space, existing.name);
|
|
435
|
+
if (users.length > 0) {
|
|
436
|
+
throw new KnowledgeError(
|
|
437
|
+
"conflict",
|
|
438
|
+
`The operand expressions at ${users.map((row) => hex(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const uses = usesNaming(db, space, existing.name);
|
|
442
|
+
if (uses.length > 0) {
|
|
443
|
+
throw new KnowledgeError("conflict", `The macro uses at ${uses.map((row) => hex(row.start)).join(", ")} use ${existing.name}. Change or remove them first (unuse), or rename the symbol.`);
|
|
444
|
+
}
|
|
445
|
+
// No suppression: an analyzer name is a second name and never comes back as the name.
|
|
302
446
|
change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
303
|
-
change.insertSuppression({ space, entity: "symbol", start: request.address, end: request.address, origin: origin as SuppressionRow["origin"] });
|
|
304
447
|
return { current: null };
|
|
305
448
|
}
|
|
306
449
|
|
|
@@ -405,6 +548,7 @@ function checkReference(request: ReferenceRequest): void {
|
|
|
405
548
|
checkAddress(request.from, "from");
|
|
406
549
|
checkAddress(request.to, "to");
|
|
407
550
|
checkOneOf(REFERENCE_KINDS, request.kind, "kind");
|
|
551
|
+
if (request.kind === POINTER_KIND) throw new KnowledgeError("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
|
|
408
552
|
}
|
|
409
553
|
|
|
410
554
|
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
|
|
@@ -427,6 +571,112 @@ function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, reque
|
|
|
427
571
|
return { current: null };
|
|
428
572
|
}
|
|
429
573
|
|
|
574
|
+
type OperandRequest = InSpace & { address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string };
|
|
575
|
+
|
|
576
|
+
/** Current operand expressions that use a name, as a symbol or inside expression text. */
|
|
577
|
+
function operandsNaming(db: DatabaseSync, space: Space, name: string): OperandRow[] {
|
|
578
|
+
return listOperands(db, undefined, undefined, space).filter((row) => row.symbol === name || (row.expression !== undefined && identifiers(row.expression).includes(name)));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** A rename of a symbol renames it in every operand expression that uses it; the rows keep their origin. */
|
|
582
|
+
function renameInOperands(db: DatabaseSync, change: Change, space: Space, from: string, to: string): void {
|
|
583
|
+
for (const row of operandsNaming(db, space, from)) {
|
|
584
|
+
change.close("operands", "space = ? AND address = ?", space, row.address);
|
|
585
|
+
const renamed: Omit<OperandRow, "revision"> = { ...row };
|
|
586
|
+
if (row.symbol === from) renamed.symbol = to;
|
|
587
|
+
if (row.expression !== undefined) renamed.expression = replaceIdentifiers(row.expression, (name) => (name === from ? to : name));
|
|
588
|
+
change.insertOperand(renamed);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function checkOperand(request: OperandRequest): void {
|
|
593
|
+
checkSpace(request);
|
|
594
|
+
checkAddress(request.address);
|
|
595
|
+
checkOneOf(OPERAND_SELECTS, request.select, "select");
|
|
596
|
+
if (request.select === "expr") {
|
|
597
|
+
if (request.symbol !== undefined || request.offset !== undefined) throw new KnowledgeError("invalid-input", "An expr operand has expression text, and no symbol or offset.");
|
|
598
|
+
if (typeof request.expression !== "string") throw new KnowledgeError("invalid-input", "An expr operand has expression text.");
|
|
599
|
+
try {
|
|
600
|
+
checkExpression(request.expression);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (request.expression !== undefined) throw new KnowledgeError("invalid-input", `A ${request.select} operand has a symbol and an optional offset, and no expression text.`);
|
|
608
|
+
if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new KnowledgeError("invalid-input", `A ${request.select} operand names a symbol: a letter or underscore, then letters, digits or underscores.`);
|
|
609
|
+
if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new KnowledgeError("invalid-input", "offset must be a whole number from -$ffff to $ffff.");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Refuses an expression whose value is not the operand in the image, and
|
|
616
|
+
* returns the addresses of its names. Symbols of the knowledge come first, then
|
|
617
|
+
* the platform names of the C64 space.
|
|
618
|
+
*/
|
|
619
|
+
function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage, request: OperandRequest, text: string): number[] {
|
|
620
|
+
const where = hex(request.address);
|
|
621
|
+
const opcode = image.byteAt(request.address);
|
|
622
|
+
if (opcode === undefined) throw new KnowledgeError("invalid-input", `${where} is outside the image.`);
|
|
623
|
+
const length = instructionLength(opcode);
|
|
624
|
+
if (length === 1) throw new KnowledgeError("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
625
|
+
if ((opcode & 0x1f) === 0x10) throw new KnowledgeError("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
626
|
+
if ((request.select === "lo" || request.select === "hi") && length === 3) {
|
|
627
|
+
throw new KnowledgeError("invalid-input", `lo and hi give one byte, but the instruction at ${where} has a 2-byte operand. Use value.`);
|
|
628
|
+
}
|
|
629
|
+
const bytes = [image.byteAt(request.address + 1), length === 3 ? image.byteAt(request.address + 2) : 0];
|
|
630
|
+
if (bytes.some((byte) => byte === undefined)) throw new KnowledgeError("invalid-input", `The instruction at ${where} runs past the end of the image.`);
|
|
631
|
+
const operand = bytes[0]! | (bytes[1]! << 8);
|
|
632
|
+
const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
|
|
633
|
+
let value: number;
|
|
634
|
+
try {
|
|
635
|
+
value = evaluate(text, resolve);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${where}: ${error.message}`);
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
const size = length === 2 ? 0x100 : 0x10000;
|
|
641
|
+
if (value < -size / 2 || value >= size || ((value % size) + size) % size !== operand) {
|
|
642
|
+
throw new KnowledgeError("invalid-input", `At ${where}, ${text} gives ${shown(value, size)}, but the operand in the image is ${shown(operand, size)}.`);
|
|
643
|
+
}
|
|
644
|
+
return identifiers(text).map((name) => resolve(name)!);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function operandStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: OperandRequest): Step<OperandRow, OperandRow> {
|
|
648
|
+
const space = spaceOf(request);
|
|
649
|
+
if (context.image === undefined) throw new KnowledgeError("invalid-input", "An operand expression is checked against the image, and no image was given.");
|
|
650
|
+
const wanted: Omit<OperandRow, "space" | "address" | "origin" | "revision"> = { select: request.select, offset: request.offset ?? 0 };
|
|
651
|
+
if (request.symbol !== undefined) wanted.symbol = request.symbol;
|
|
652
|
+
if (request.expression !== undefined) wanted.expression = request.expression.trim();
|
|
653
|
+
const targets = new Set(checkOperandValue(db, space, context.image, request, operandText(wanted)));
|
|
654
|
+
const existing = operandAt(db, request.address, space);
|
|
655
|
+
if (existing !== undefined && existing.select === wanted.select && existing.symbol === wanted.symbol && existing.offset === wanted.offset && existing.expression === wanted.expression) {
|
|
656
|
+
return { current: existing };
|
|
657
|
+
}
|
|
658
|
+
const origin = context.origin as OperandRow["origin"];
|
|
659
|
+
if (existing !== undefined) change.close("operands", "space = ? AND address = ?", space, request.address);
|
|
660
|
+
change.insertOperand({ space, address: request.address, ...wanted, origin });
|
|
661
|
+
const pointers = referencesFrom(db, request.address, space).filter((reference) => reference.kind === POINTER_KIND);
|
|
662
|
+
for (const reference of pointers) {
|
|
663
|
+
if (!targets.has(reference.to)) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.address, reference.to, POINTER_KIND);
|
|
664
|
+
}
|
|
665
|
+
for (const to of targets) {
|
|
666
|
+
if (!pointers.some((reference) => reference.to === to)) change.insertReference({ space, from: request.address, to, kind: POINTER_KIND, origin });
|
|
667
|
+
}
|
|
668
|
+
const current = operandAt(db, request.address, space)!;
|
|
669
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function unoperandStep(db: DatabaseSync, change: Change, _origin: Origin, request: AddressRequest): Step<null> {
|
|
673
|
+
const space = spaceOf(request);
|
|
674
|
+
if (operandAt(db, request.address, space) === undefined) throw new KnowledgeError("not-found", `There is no operand expression at ${hex(request.address)}.`);
|
|
675
|
+
change.close("operands", "space = ? AND address = ?", space, request.address);
|
|
676
|
+
change.close('"references"', "space = ? AND from_address = ? AND kind = ?", space, request.address, POINTER_KIND);
|
|
677
|
+
return { current: null };
|
|
678
|
+
}
|
|
679
|
+
|
|
430
680
|
type ModuleRequest = InSpace & { start: number; file: string; title?: string };
|
|
431
681
|
|
|
432
682
|
function checkModule(request: ModuleRequest): void {
|
|
@@ -442,7 +692,7 @@ function checkModule(request: ModuleRequest): void {
|
|
|
442
692
|
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
|
|
443
693
|
const space = spaceOf(request);
|
|
444
694
|
const existing = moduleAt(db, request.start, space);
|
|
445
|
-
// A module write without a title keeps the title it has
|
|
695
|
+
// A module write without a title keeps the title it has.
|
|
446
696
|
const title = request.title === undefined ? existing?.title : request.title.trim();
|
|
447
697
|
if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
|
|
448
698
|
const owner = moduleWithFile(db, request.file, space);
|
|
@@ -461,6 +711,222 @@ function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request
|
|
|
461
711
|
return { current: null };
|
|
462
712
|
}
|
|
463
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Refuses ACME text whose braces do not pair outside comments and strings:
|
|
716
|
+
* c64-source writes patch code and a macro body inside { }.
|
|
717
|
+
*/
|
|
718
|
+
function checkBraces(text: string, what: string): void {
|
|
719
|
+
let depth = 0;
|
|
720
|
+
for (const line of text.split("\n")) {
|
|
721
|
+
let quote: string | undefined;
|
|
722
|
+
for (let index = 0; index < line.length; index++) {
|
|
723
|
+
const character = line[index]!;
|
|
724
|
+
if (quote !== undefined) {
|
|
725
|
+
if (character === "\\") index++;
|
|
726
|
+
else if (character === quote) quote = undefined;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (character === ";") break;
|
|
730
|
+
if (character === '"' || character === "'") quote = character;
|
|
731
|
+
else if (character === "{") depth++;
|
|
732
|
+
else if (character === "}" && --depth < 0) throw new KnowledgeError("invalid-input", `${what} has a } with no { before it.`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (depth !== 0) throw new KnowledgeError("invalid-input", `${what} has a { with no }.`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function checkCommentText(comment: string | undefined, what: string): void {
|
|
739
|
+
if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new KnowledgeError("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Refuses a range with a byte outside the image. */
|
|
743
|
+
function checkInImage(image: OperandImage, start: number, end: number, what: string): void {
|
|
744
|
+
for (let address = start; address <= end; address++) {
|
|
745
|
+
if (image.byteAt(address) === undefined) throw new KnowledgeError("invalid-input", `${hex(address)} is outside the image: ${what} is a range of the image.`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
type PatchRequest = InSpace & { start: number; end: number; switch: string; code: string; comment?: string };
|
|
750
|
+
|
|
751
|
+
function checkPatch(request: PatchRequest): void {
|
|
752
|
+
checkRange(request);
|
|
753
|
+
if (typeof request.switch !== "string" || !SWITCH_NAME.test(request.switch)) {
|
|
754
|
+
throw new KnowledgeError("invalid-input", "A switch name is an upper-case letter, then 1 to 31 upper-case letters, digits or underscores, for example CRACK.");
|
|
755
|
+
}
|
|
756
|
+
if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new KnowledgeError("invalid-input", `Patch code has at most ${MAX_CODE} characters; empty code fills the range with NOP.`);
|
|
757
|
+
checkBraces(request.code, "The patch code");
|
|
758
|
+
checkCommentText(request.comment, "A patch comment");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: PatchRequest): Step<PatchRow, PatchRow> {
|
|
762
|
+
const space = spaceOf(request);
|
|
763
|
+
const range = `${hex(request.start)}-${hex(request.end)}`;
|
|
764
|
+
if (context.image === undefined) throw new KnowledgeError("invalid-input", "A patch is checked against the image, and no image was given.");
|
|
765
|
+
checkInImage(context.image, request.start, request.end, "a patch");
|
|
766
|
+
const symbol = symbolNamed(db, request.switch, space);
|
|
767
|
+
if (symbol !== undefined) throw new KnowledgeError("conflict", `The switch name ${request.switch} is the name of the symbol at ${hex(symbol.address)}. Give the switch another name.`);
|
|
768
|
+
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new KnowledgeError("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
|
|
769
|
+
const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
770
|
+
if (other !== undefined) {
|
|
771
|
+
throw new KnowledgeError("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${hex(other.start)}-${hex(other.end)}. Change or remove that patch first (unpatch).`);
|
|
772
|
+
}
|
|
773
|
+
const [use] = macroUsesOverlapping(db, request.start, request.end, space);
|
|
774
|
+
if (use !== undefined) {
|
|
775
|
+
throw new KnowledgeError("conflict", `The patch ${range} overlaps the use of the macro ${use.macro} at ${hex(use.start)}-${hex(use.end)}. Remove that use first (unuse).`);
|
|
776
|
+
}
|
|
777
|
+
const existing = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
778
|
+
const code = request.code.trimEnd();
|
|
779
|
+
// A patch write without a comment keeps the comment it has, as a module keeps its title.
|
|
780
|
+
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
781
|
+
if (existing !== undefined && existing.end === request.end && existing.switch === request.switch && existing.code === code && existing.comment === comment) return { current: existing };
|
|
782
|
+
const origin = context.origin as PatchRow["origin"];
|
|
783
|
+
if (existing !== undefined) change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
784
|
+
change.insertPatch({ space, start: request.start, end: request.end, switch: request.switch, code, ...(comment === undefined ? {} : { comment }), origin });
|
|
785
|
+
const current = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
786
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
790
|
+
const space = spaceOf(request);
|
|
791
|
+
if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No patch starts at ${hex(request.start)}.`);
|
|
792
|
+
change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
793
|
+
return { current: null };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
type MacroRequest = InSpace & { name: string; params?: string[]; body: string; comment?: string };
|
|
797
|
+
|
|
798
|
+
function checkMacroName(name: unknown, what = "A macro name"): void {
|
|
799
|
+
if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new KnowledgeError("invalid-input", `${what} is a letter or underscore, then at most 31 letters, digits or underscores.`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function checkMacro(request: MacroRequest): void {
|
|
803
|
+
checkSpace(request);
|
|
804
|
+
checkMacroName(request.name);
|
|
805
|
+
const params = request.params ?? [];
|
|
806
|
+
if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
|
|
807
|
+
for (const param of params) {
|
|
808
|
+
if (typeof param !== "string" || !MACRO_PARAM.test(param)) {
|
|
809
|
+
throw new KnowledgeError("invalid-input", "A macro param is a lower-case letter or underscore, then lower-case letters, digits or underscores, without a dot. The body names it with a dot: .dst.");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (new Set(params).size !== params.length) throw new KnowledgeError("invalid-input", "The params of a macro have different names.");
|
|
813
|
+
if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new KnowledgeError("invalid-input", `A macro body has 1 to ${MAX_CODE} characters.`);
|
|
814
|
+
checkBraces(request.body, "The macro body");
|
|
815
|
+
checkCommentText(request.comment, "A macro comment");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: MacroRequest): Step<MacroRow, MacroRow> {
|
|
819
|
+
const space = spaceOf(request);
|
|
820
|
+
const existing = macroNamed(db, request.name, space);
|
|
821
|
+
const params = request.params ?? [];
|
|
822
|
+
const body = request.body.trimEnd();
|
|
823
|
+
const comment = request.comment === undefined ? existing?.comment : request.comment.trim();
|
|
824
|
+
if (existing !== undefined && listText(existing.params) === listText(params) && existing.body === body && existing.comment === comment) return { current: existing };
|
|
825
|
+
if (existing !== undefined && existing.params.length !== params.length) {
|
|
826
|
+
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
827
|
+
if (uses.length > 0) {
|
|
828
|
+
throw new KnowledgeError(
|
|
829
|
+
"conflict",
|
|
830
|
+
`The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) => hex(row.start)).join(", ")} give that many args. Remove the uses first (unuse), or keep the number of params.`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (existing !== undefined) change.close("macros", "space = ? AND name = ?", space, request.name);
|
|
835
|
+
change.insertMacro({ space, name: request.name, params, body, ...(comment === undefined ? {} : { comment }), origin: origin as MacroRow["origin"] });
|
|
836
|
+
const current = macroNamed(db, request.name, space)!;
|
|
837
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function unmacroStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { name: string }): Step<null> {
|
|
841
|
+
const space = spaceOf(request);
|
|
842
|
+
if (macroNamed(db, request.name, space) === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.name}.`);
|
|
843
|
+
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
844
|
+
if (uses.length > 0) throw new KnowledgeError("conflict", `The macro ${request.name} has uses at ${uses.map((row) => hex(row.start)).join(", ")}. Remove them first (unuse).`);
|
|
845
|
+
change.close("macros", "space = ? AND name = ?", space, request.name);
|
|
846
|
+
return { current: null };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
type MacroUseRequest = InSpace & { start: number; end: number; macro: string; args?: string[] };
|
|
850
|
+
|
|
851
|
+
function checkMacroUse(request: MacroUseRequest): void {
|
|
852
|
+
checkRange(request);
|
|
853
|
+
checkMacroName(request.macro, "The macro of a use");
|
|
854
|
+
const args = request.args ?? [];
|
|
855
|
+
if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new KnowledgeError("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
|
|
856
|
+
for (const arg of args) {
|
|
857
|
+
if (typeof arg !== "string") throw new KnowledgeError("invalid-input", "Each arg of a macro use is expression text.");
|
|
858
|
+
try {
|
|
859
|
+
checkExpression(arg);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Current macro uses that name a symbol in an arg. */
|
|
868
|
+
function usesNaming(db: DatabaseSync, space: Space, name: string): MacroUseRow[] {
|
|
869
|
+
return listMacroUses(db, undefined, undefined, undefined, space).filter((row) => row.args.some((arg) => identifiers(arg).includes(name)));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** A rename of a symbol renames it in the args of every macro use; the rows keep their origin. */
|
|
873
|
+
function renameInUses(db: DatabaseSync, change: Change, space: Space, from: string, to: string): void {
|
|
874
|
+
for (const row of usesNaming(db, space, from)) {
|
|
875
|
+
change.close("macro_uses", "space = ? AND start_address = ?", space, row.start);
|
|
876
|
+
change.insertMacroUse({ ...row, args: row.args.map((arg) => replaceIdentifiers(arg, (name) => (name === from ? to : name))) });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: MacroUseRequest): Step<MacroUseRow, MacroUseRow> {
|
|
881
|
+
const space = spaceOf(request);
|
|
882
|
+
const image = context.image;
|
|
883
|
+
const range = `${hex(request.start)}-${hex(request.end)}`;
|
|
884
|
+
if (image === undefined) throw new KnowledgeError("invalid-input", "A macro use is checked against the image, and no image was given.");
|
|
885
|
+
const macro = macroNamed(db, request.macro, space);
|
|
886
|
+
if (macro === undefined) throw new KnowledgeError("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
|
|
887
|
+
const args = (request.args ?? []).map((arg) => arg.trim());
|
|
888
|
+
if (args.length !== macro.params.length) {
|
|
889
|
+
throw new KnowledgeError("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
|
|
890
|
+
}
|
|
891
|
+
const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
|
|
892
|
+
for (const arg of args) {
|
|
893
|
+
try {
|
|
894
|
+
evaluate(arg, resolve);
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", `${range}: ${error.message}`);
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
checkInImage(image, request.start, request.end, "a macro use");
|
|
901
|
+
let next = request.start;
|
|
902
|
+
while (next <= request.end) next += instructionLength(image.byteAt(next)!);
|
|
903
|
+
if (next !== request.end + 1) {
|
|
904
|
+
throw new KnowledgeError("invalid-input", `A macro use is a run of whole instructions, but the instructions from ${hex(request.start)} end at ${hex(next - 1)}, not at ${hex(request.end)}.`);
|
|
905
|
+
}
|
|
906
|
+
const [patch] = patchesOverlapping(db, request.start, request.end, space);
|
|
907
|
+
if (patch !== undefined) {
|
|
908
|
+
throw new KnowledgeError("conflict", `The macro use ${range} overlaps the patch of ${patch.switch} at ${hex(patch.start)}-${hex(patch.end)}. Change or remove that patch first (unpatch).`);
|
|
909
|
+
}
|
|
910
|
+
const other = macroUsesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
911
|
+
if (other !== undefined) {
|
|
912
|
+
throw new KnowledgeError("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${hex(other.start)}-${hex(other.end)}. Remove that use first (unuse).`);
|
|
913
|
+
}
|
|
914
|
+
const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
915
|
+
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
|
|
916
|
+
const origin = context.origin as MacroUseRow["origin"];
|
|
917
|
+
if (existing !== undefined) change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
918
|
+
change.insertMacroUse({ space, start: request.start, end: request.end, macro: request.macro, args, origin });
|
|
919
|
+
const current = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start)!;
|
|
920
|
+
return existing !== undefined && existing.origin !== origin ? { current, previous: existing } : { current };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function unuseStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
924
|
+
const space = spaceOf(request);
|
|
925
|
+
if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new KnowledgeError("not-found", `No macro use starts at ${hex(request.start)}.`);
|
|
926
|
+
change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
927
|
+
return { current: null };
|
|
928
|
+
}
|
|
929
|
+
|
|
464
930
|
/** Names the symbol at an address (one current symbol per address, unique names). */
|
|
465
931
|
export function renameSymbol(db: DatabaseSync, context: WriteContext, request: SymbolRequest): WriteResult<SymbolRow, SymbolRow> {
|
|
466
932
|
checkSymbol(request);
|
|
@@ -505,7 +971,7 @@ export function removeReference(db: DatabaseSync, context: WriteContext, request
|
|
|
505
971
|
return result(write(db, context, "remove-reference", (change) => unreferenceStep(db, change, context.origin, request)));
|
|
506
972
|
}
|
|
507
973
|
|
|
508
|
-
/** Starts a file of the generated source at an address
|
|
974
|
+
/** Starts a file of the generated source at an address; one module per address and per file. */
|
|
509
975
|
export function setModule(db: DatabaseSync, context: WriteContext, request: ModuleRequest): WriteResult<ModuleRow> {
|
|
510
976
|
checkModule(request);
|
|
511
977
|
return result(write(db, context, "set-module", (change) => moduleStep(db, change, context.origin, request)));
|
|
@@ -517,7 +983,62 @@ export function removeModule(db: DatabaseSync, context: WriteContext, request: I
|
|
|
517
983
|
return result(write(db, context, "remove-module", (change) => unmoduleStep(db, change, context.origin, request)));
|
|
518
984
|
}
|
|
519
985
|
|
|
520
|
-
/**
|
|
986
|
+
/**
|
|
987
|
+
* Writes the operand expression of the instruction at an address, after
|
|
988
|
+
* a check against the image, and a pointer reference to each name it uses.
|
|
989
|
+
*/
|
|
990
|
+
export function setOperand(db: DatabaseSync, context: WriteContext, request: OperandRequest): WriteResult<OperandRow, OperandRow> {
|
|
991
|
+
checkOperand(request);
|
|
992
|
+
return result(write(db, context, "set-operand", (change) => operandStep(db, change, context, request)));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export function removeOperand(db: DatabaseSync, context: WriteContext, request: AddressRequest): WriteResult<null> {
|
|
996
|
+
checkSpace(request);
|
|
997
|
+
checkAddress(request.address);
|
|
998
|
+
return result(write(db, context, "remove-operand", (change) => unoperandStep(db, change, context.origin, request)));
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Writes code that replaces [start, end] when the build defines the switch
|
|
1003
|
+
*, after a check that the range is in the image and overlaps no other
|
|
1004
|
+
* patch or macro use.
|
|
1005
|
+
*/
|
|
1006
|
+
export function setPatch(db: DatabaseSync, context: WriteContext, request: PatchRequest): WriteResult<PatchRow, PatchRow> {
|
|
1007
|
+
checkPatch(request);
|
|
1008
|
+
return result(write(db, context, "set-patch", (change) => patchStep(db, change, context, request)));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function removePatch(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
|
|
1012
|
+
checkSpace(request);
|
|
1013
|
+
checkAddress(request.start, "start");
|
|
1014
|
+
return result(write(db, context, "remove-patch", (change) => unpatchStep(db, change, context.origin, request)));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Writes a macro of the generated source. */
|
|
1018
|
+
export function setMacro(db: DatabaseSync, context: WriteContext, request: MacroRequest): WriteResult<MacroRow, MacroRow> {
|
|
1019
|
+
checkMacro(request);
|
|
1020
|
+
return result(write(db, context, "set-macro", (change) => macroStep(db, change, context.origin, request)));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function removeMacro(db: DatabaseSync, context: WriteContext, request: InSpace & { name: string }): WriteResult<null> {
|
|
1024
|
+
checkSpace(request);
|
|
1025
|
+
checkMacroName(request.name);
|
|
1026
|
+
return result(write(db, context, "remove-macro", (change) => unmacroStep(db, change, context.origin, request)));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** Writes the instructions in [start, end] as one use of a macro, after a check against the image. */
|
|
1030
|
+
export function setMacroUse(db: DatabaseSync, context: WriteContext, request: MacroUseRequest): WriteResult<MacroUseRow, MacroUseRow> {
|
|
1031
|
+
checkMacroUse(request);
|
|
1032
|
+
return result(write(db, context, "set-macro-use", (change) => useStep(db, change, context, request)));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export function removeMacroUse(db: DatabaseSync, context: WriteContext, request: InSpace & { start: number }): WriteResult<null> {
|
|
1036
|
+
checkSpace(request);
|
|
1037
|
+
checkAddress(request.start, "start");
|
|
1038
|
+
return result(write(db, context, "remove-macro-use", (change) => unuseStep(db, change, context.origin, request)));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** One write of a batch, named as the command that does it alone. */
|
|
521
1042
|
export type BatchOperation =
|
|
522
1043
|
| ({ op: "rename" } & SymbolRequest)
|
|
523
1044
|
| ({ op: "remove-symbol" } & AddressRequest)
|
|
@@ -528,18 +1049,29 @@ export type BatchOperation =
|
|
|
528
1049
|
| ({ op: "reference" } & ReferenceRequest)
|
|
529
1050
|
| ({ op: "unreference" } & ReferenceRequest)
|
|
530
1051
|
| ({ op: "module" } & ModuleRequest)
|
|
531
|
-
| ({ op: "unmodule" } & InSpace & { start: number })
|
|
1052
|
+
| ({ op: "unmodule" } & InSpace & { start: number })
|
|
1053
|
+
| ({ op: "operand" } & OperandRequest)
|
|
1054
|
+
| ({ op: "unoperand" } & AddressRequest)
|
|
1055
|
+
| ({ op: "patch" } & PatchRequest)
|
|
1056
|
+
| ({ op: "unpatch" } & InSpace & { start: number })
|
|
1057
|
+
| ({ op: "macro" } & MacroRequest)
|
|
1058
|
+
| ({ op: "unmacro" } & InSpace & { name: string })
|
|
1059
|
+
| ({ op: "use" } & MacroUseRequest)
|
|
1060
|
+
| ({ op: "unuse" } & InSpace & { start: number });
|
|
532
1061
|
|
|
533
1062
|
export const MAX_BATCH = 10_000;
|
|
534
1063
|
|
|
535
1064
|
/** The result of each write of a batch, in order. */
|
|
536
|
-
export type BatchStep = Step<
|
|
1065
|
+
export type BatchStep = Step<
|
|
1066
|
+
SymbolRow | RegionRow[] | CommentRow | ReferenceRow | ModuleRow | OperandRow | PatchRow | MacroRow | MacroUseRow | null,
|
|
1067
|
+
SymbolRow | RegionRow[] | OperandRow | PatchRow | MacroRow | MacroUseRow
|
|
1068
|
+
>;
|
|
537
1069
|
|
|
538
1070
|
/**
|
|
539
|
-
* Applies many writes as one revision in one transaction
|
|
1071
|
+
* Applies many writes as one revision in one transaction: all of them,
|
|
540
1072
|
* or none when one is refused. Every write is checked before anything opens.
|
|
541
1073
|
*/
|
|
542
|
-
export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision:
|
|
1074
|
+
export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision: string | null; steps: BatchStep[] } {
|
|
543
1075
|
if (operations.length === 0 || operations.length > MAX_BATCH) throw new KnowledgeError("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
|
|
544
1076
|
operations.forEach((operation, index) => {
|
|
545
1077
|
try {
|
|
@@ -565,8 +1097,29 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
565
1097
|
case "unmodule":
|
|
566
1098
|
checkSpace(operation);
|
|
567
1099
|
return checkAddress(operation.start, "start");
|
|
1100
|
+
case "operand":
|
|
1101
|
+
return checkOperand(operation);
|
|
1102
|
+
case "unoperand":
|
|
1103
|
+
checkSpace(operation);
|
|
1104
|
+
return checkAddress(operation.address);
|
|
1105
|
+
case "patch":
|
|
1106
|
+
return checkPatch(operation);
|
|
1107
|
+
case "unpatch":
|
|
1108
|
+
case "unuse":
|
|
1109
|
+
checkSpace(operation);
|
|
1110
|
+
return checkAddress(operation.start, "start");
|
|
1111
|
+
case "macro":
|
|
1112
|
+
return checkMacro(operation);
|
|
1113
|
+
case "unmacro":
|
|
1114
|
+
checkSpace(operation);
|
|
1115
|
+
return checkMacroName(operation.name);
|
|
1116
|
+
case "use":
|
|
1117
|
+
return checkMacroUse(operation);
|
|
568
1118
|
default:
|
|
569
|
-
throw new KnowledgeError(
|
|
1119
|
+
throw new KnowledgeError(
|
|
1120
|
+
"invalid-input",
|
|
1121
|
+
`op must be one of rename, remove-symbol, classify, unclassify, comment, uncomment, reference, unreference, module, unmodule, operand, unoperand, patch, unpatch, macro, unmacro, use, unuse.`,
|
|
1122
|
+
);
|
|
570
1123
|
}
|
|
571
1124
|
} catch (error) {
|
|
572
1125
|
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
@@ -597,6 +1150,22 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
597
1150
|
return moduleStep(db, change, context.origin, operation);
|
|
598
1151
|
case "unmodule":
|
|
599
1152
|
return unmoduleStep(db, change, context.origin, operation);
|
|
1153
|
+
case "operand":
|
|
1154
|
+
return operandStep(db, change, context, operation);
|
|
1155
|
+
case "unoperand":
|
|
1156
|
+
return unoperandStep(db, change, context.origin, operation);
|
|
1157
|
+
case "patch":
|
|
1158
|
+
return patchStep(db, change, context, operation);
|
|
1159
|
+
case "unpatch":
|
|
1160
|
+
return unpatchStep(db, change, context.origin, operation);
|
|
1161
|
+
case "macro":
|
|
1162
|
+
return macroStep(db, change, context.origin, operation);
|
|
1163
|
+
case "unmacro":
|
|
1164
|
+
return unmacroStep(db, change, context.origin, operation);
|
|
1165
|
+
case "use":
|
|
1166
|
+
return useStep(db, change, context, operation);
|
|
1167
|
+
case "unuse":
|
|
1168
|
+
return unuseStep(db, change, context.origin, operation);
|
|
600
1169
|
}
|
|
601
1170
|
} catch (error) {
|
|
602
1171
|
if (error instanceof KnowledgeError) throw new KnowledgeError(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
@@ -608,12 +1177,32 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
608
1177
|
}
|
|
609
1178
|
|
|
610
1179
|
/** Row content without its revision, as a comparable key. */
|
|
1180
|
+
/** The tables of the program and the columns that make a row. */
|
|
1181
|
+
const PROGRAM_TABLES = [
|
|
1182
|
+
["memory", "space, address, bytes, origin"],
|
|
1183
|
+
["entries", "space, address, state, stages, origin"],
|
|
1184
|
+
["coverage", "space, kind, start_address, end_address, origin"],
|
|
1185
|
+
] as const;
|
|
1186
|
+
|
|
611
1187
|
const KEYS = {
|
|
612
1188
|
symbols: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.space}|${row.address}|${row.name}|${row.kind}|${row.origin}` },
|
|
1189
|
+
analyzer_names: { columns: SYMBOL_COLUMNS, map: toSymbol, key: (row: SymbolRow) => `${row.space}|${row.address}|${row.name}|${row.kind}|${row.origin}` },
|
|
613
1190
|
regions: { columns: REGION_COLUMNS, map: toRegion, key: (row: RegionRow) => `${row.space}|${row.start}|${row.end}|${row.type}|${row.element ?? ""}|${row.origin}` },
|
|
614
1191
|
comments: { columns: COMMENT_COLUMNS, map: toComment, key: (row: CommentRow) => `${row.space}|${row.address}|${row.placement}|${row.text}|${row.origin}` },
|
|
615
1192
|
references: { columns: REFERENCE_COLUMNS, map: toReference, key: (row: ReferenceRow) => `${row.space}|${row.from}|${row.to}|${row.kind}|${row.origin}` },
|
|
616
1193
|
modules: { columns: MODULE_COLUMNS, map: toModule, key: (row: ModuleRow) => `${row.space}|${row.start}|${row.file}|${row.title ?? ""}|${row.origin}` },
|
|
1194
|
+
operands: {
|
|
1195
|
+
columns: OPERAND_COLUMNS,
|
|
1196
|
+
map: toOperand,
|
|
1197
|
+
key: (row: OperandRow) => `${row.space}|${row.address}|${row.select}|${row.symbol ?? ""}|${row.offset}|${row.expression ?? ""}|${row.origin}`,
|
|
1198
|
+
},
|
|
1199
|
+
patches: {
|
|
1200
|
+
columns: PATCH_COLUMNS,
|
|
1201
|
+
map: toPatch,
|
|
1202
|
+
key: (row: PatchRow) => `${row.space}|${row.start}|${row.end}|${row.switch}|${row.code}|${row.comment ?? ""}|${row.origin}`,
|
|
1203
|
+
},
|
|
1204
|
+
macros: { columns: MACRO_COLUMNS, map: toMacro, key: (row: MacroRow) => `${row.space}|${row.name}|${listText(row.params)}|${row.body}|${row.comment ?? ""}|${row.origin}` },
|
|
1205
|
+
macro_uses: { columns: MACRO_USE_COLUMNS, map: toMacroUse, key: (row: MacroUseRow) => `${row.space}|${row.start}|${row.end}|${row.macro}|${listText(row.args)}|${row.origin}` },
|
|
617
1206
|
suppressions: {
|
|
618
1207
|
columns: SUPPRESSION_COLUMNS,
|
|
619
1208
|
map: toSuppression,
|
|
@@ -625,22 +1214,19 @@ const KEYS = {
|
|
|
625
1214
|
* Makes the current knowledge equal to what it was at `revision`, as a new
|
|
626
1215
|
* revision. History is never erased; restored rows keep their origin.
|
|
627
1216
|
*/
|
|
628
|
-
export function revert(db: DatabaseSync, context: WriteContext, request: { revision:
|
|
1217
|
+
export function revert(db: DatabaseSync, context: WriteContext, request: { revision: string }): WriteResult<{ restoredRevision: string }> {
|
|
629
1218
|
return write(db, context, "revert", (change) => {
|
|
630
|
-
const
|
|
631
|
-
if (!Number.isInteger(request.revision) || request.revision < 0 || request.revision > current) {
|
|
632
|
-
throw new KnowledgeError("not-found", `There is no revision ${request.revision}; knowledge is at revision ${current}.`);
|
|
633
|
-
}
|
|
1219
|
+
const restored = resolveRevision(db, request.revision);
|
|
634
1220
|
const at = "valid_from_revision <= ? AND (valid_to_revision IS NULL OR valid_to_revision > ?)";
|
|
635
1221
|
for (const [table, spec] of Object.entries(KEYS)) {
|
|
636
1222
|
const name = table === "references" ? '"references"' : table;
|
|
637
|
-
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(
|
|
1223
|
+
const then = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
638
1224
|
const now = db.prepare(`SELECT ${spec.columns}, id FROM ${name} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
|
|
639
1225
|
const key = (row: Record<string, unknown>) => (spec.key as (row: unknown) => string)(spec.map(row as never));
|
|
640
1226
|
const thenKeys = new Set(then.map(key));
|
|
641
1227
|
const nowKeys = new Set(now.map(key));
|
|
642
1228
|
// Close first, so restored rows never collide with the current-row rules.
|
|
643
|
-
for (const row of now) if (!thenKeys.has(key(row))) change.close(name, "id = ?", row.id as
|
|
1229
|
+
for (const row of now) if (!thenKeys.has(key(row))) change.close(name, "id = ?", row.id as string);
|
|
644
1230
|
for (const row of then) {
|
|
645
1231
|
if (nowKeys.has(key(row))) continue;
|
|
646
1232
|
if (table === "suppressions") {
|
|
@@ -651,13 +1237,110 @@ export function revert(db: DatabaseSync, context: WriteContext, request: { revis
|
|
|
651
1237
|
change.insertModule(spec.map(row as never) as ModuleRow);
|
|
652
1238
|
continue;
|
|
653
1239
|
}
|
|
1240
|
+
if (table === "operands") {
|
|
1241
|
+
change.insertOperand(spec.map(row as never) as OperandRow);
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (table === "patches") {
|
|
1245
|
+
change.insertPatch(spec.map(row as never) as PatchRow);
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (table === "macros") {
|
|
1249
|
+
change.insertMacro(spec.map(row as never) as MacroRow);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (table === "macro_uses") {
|
|
1253
|
+
change.insertMacroUse(spec.map(row as never) as MacroUseRow);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
654
1256
|
const mapped = spec.map(row as never) as SymbolRow & RegionRow & CommentRow & ReferenceRow;
|
|
655
1257
|
if (table === "symbols") change.insertSymbol(mapped);
|
|
1258
|
+
else if (table === "analyzer_names") change.insertAnalyzerName(mapped);
|
|
656
1259
|
else if (table === "regions") change.insertRegion(mapped);
|
|
657
1260
|
else if (table === "comments") change.insertComment(mapped);
|
|
658
1261
|
else change.insertReference(mapped);
|
|
659
1262
|
}
|
|
660
1263
|
}
|
|
661
|
-
|
|
1264
|
+
// The program: a row is the same when all its values are the same.
|
|
1265
|
+
for (const [table, columns] of PROGRAM_TABLES) {
|
|
1266
|
+
const then = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE ${at}`).all(restored, restored) as Array<Record<string, unknown>>;
|
|
1267
|
+
const now = db.prepare(`SELECT ${columns}, id FROM ${table} WHERE valid_to_revision IS NULL`).all() as Array<Record<string, unknown>>;
|
|
1268
|
+
const key = (row: Record<string, unknown>) => JSON.stringify(columns.split(", ").map((column) => row[column]));
|
|
1269
|
+
const thenKeys = new Set(then.map(key));
|
|
1270
|
+
const nowKeys = new Set(now.map(key));
|
|
1271
|
+
for (const row of now) if (!thenKeys.has(key(row))) change.close(table, "id = ?", row.id as string);
|
|
1272
|
+
for (const row of then) {
|
|
1273
|
+
if (nowKeys.has(key(row))) continue;
|
|
1274
|
+
const values = row as never;
|
|
1275
|
+
if (table === "memory") change.insertMemory(values);
|
|
1276
|
+
else if (table === "entries") change.insertEntry(values);
|
|
1277
|
+
else change.insertCoverage({ ...(values as { space: Space; kind: string; origin: Origin }), start: row.start_address as number, end: row.end_address as number });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return { restoredRevision: restored };
|
|
662
1281
|
});
|
|
663
1282
|
}
|
|
1283
|
+
|
|
1284
|
+
/** What a merge decided for one conflict: the newest row stays and the older rows close. */
|
|
1285
|
+
export interface MergeChoice {
|
|
1286
|
+
rule: Conflict["rule"];
|
|
1287
|
+
space: Space;
|
|
1288
|
+
kept: ConflictRow;
|
|
1289
|
+
closed: ConflictRow[];
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** The most choices one merge makes; more means the knowledge text is broken. */
|
|
1293
|
+
const MAX_CHOICES = 100_000;
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Decides every conflict of a git merge in one merge revision. Of the
|
|
1297
|
+
* rows of one conflict, the row of the newest revision stays and the others
|
|
1298
|
+
* close; an older region keeps its parts outside the newer regions. Where an
|
|
1299
|
+
* operand expression closes, the pointer references of its address follow the
|
|
1300
|
+
* expression that stays. `platform` gives the C64 platform names.
|
|
1301
|
+
*/
|
|
1302
|
+
export function mergeConflicts(db: DatabaseSync, context: WriteContext, platform: ReadonlyMap<string, number>): WriteResult<MergeChoice[]> {
|
|
1303
|
+
return write(db, context, "merge", (change) => {
|
|
1304
|
+
const choices: MergeChoice[] = [];
|
|
1305
|
+
for (let found = conflicts(db); found.length > 0; found = conflicts(db)) {
|
|
1306
|
+
if (choices.length >= MAX_CHOICES) throw new KnowledgeError("invalid-database", `The merge made ${MAX_CHOICES} choices and still finds conflicts. Nothing was written.`);
|
|
1307
|
+
const conflict = found[0]!;
|
|
1308
|
+
const newest = conflict.rows.at(-1)!;
|
|
1309
|
+
const older = conflict.rows.slice(0, -1);
|
|
1310
|
+
const table = sqlName(conflictTable(conflict.rule));
|
|
1311
|
+
for (const entry of older) change.close(table, "id = ?", entry.id);
|
|
1312
|
+
if (conflict.rule === "region" || conflict.rule === "suppression-region") keepOutside(change, conflict, conflict.rule === "region");
|
|
1313
|
+
if (conflict.rule === "operand") followOperand(db, change, newest.row as OperandRow, platform);
|
|
1314
|
+
choices.push({ rule: conflict.rule, space: conflict.space, kept: newest, closed: older });
|
|
1315
|
+
}
|
|
1316
|
+
return choices;
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** Puts back the parts of the older ranges of a conflict that no newer range covers. */
|
|
1321
|
+
function keepOutside(change: Change, conflict: Conflict, regions: boolean): void {
|
|
1322
|
+
const newer: AddressRange[] = [];
|
|
1323
|
+
for (const entry of [...conflict.rows].reverse()) {
|
|
1324
|
+
const row = entry.row as RegionRow & SuppressionRow;
|
|
1325
|
+
if (newer.length > 0) {
|
|
1326
|
+
for (const piece of subtract({ start: row.start, end: row.end }, newer)) {
|
|
1327
|
+
if (regions) change.insertRegion({ space: row.space, ...piece, type: row.type, ...elementOf(piece, row.element), origin: row.origin });
|
|
1328
|
+
else change.insertSuppression({ space: row.space, entity: "region", ...piece, origin: row.origin });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
newer.push({ start: row.start, end: row.end });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/** Makes the pointer references of an address the ones of the operand expression that stays there. */
|
|
1336
|
+
function followOperand(db: DatabaseSync, change: Change, kept: OperandRow, platform: ReadonlyMap<string, number>): void {
|
|
1337
|
+
const resolve = (name: string) => symbolNamed(db, name, kept.space)?.address ?? (kept.space === DEFAULT_SPACE ? platform.get(name) : undefined);
|
|
1338
|
+
const targets = new Set(identifiers(operandText(kept)).flatMap((name) => resolve(name) ?? []));
|
|
1339
|
+
const pointers = referencesFrom(db, kept.address, kept.space).filter((reference) => reference.kind === POINTER_KIND);
|
|
1340
|
+
for (const reference of pointers) {
|
|
1341
|
+
if (!targets.has(reference.to)) change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", kept.space, kept.address, reference.to, POINTER_KIND);
|
|
1342
|
+
}
|
|
1343
|
+
for (const to of targets) {
|
|
1344
|
+
if (!pointers.some((reference) => reference.to === to)) change.insertReference({ space: kept.space, from: kept.address, to, kind: POINTER_KIND, origin: kept.origin });
|
|
1345
|
+
}
|
|
1346
|
+
}
|