@henols/c64-re-tools 2.0.1 → 2.1.0-rc.2
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 +8 -4
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +162 -0
- package/src/native/discover.ts +8 -0
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +25 -4
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/test.ts +0 -65
package/src/knowledge/write.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Refusal } from "../errors.ts";
|
|
7
|
+
import { type AddressRange, formatC64Address, instructionLength, SPACE_PATTERN, SYMBOL_NAME } from "../c64.ts";
|
|
7
8
|
import { isoCet } from "../time.ts";
|
|
8
9
|
import { CONFLICT_RULES, conflicts, conflictTable, type Conflict, type ConflictRow } from "./conflicts.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { checkExpression, evaluate,
|
|
10
|
+
import { transaction } from "./database.ts";
|
|
11
|
+
import { checkExpression, evaluate, identifiers, operandText, replaceIdentifiers } from "./expression.ts";
|
|
11
12
|
import {
|
|
12
13
|
COMMENT_COLUMNS,
|
|
13
14
|
commentsAt,
|
|
@@ -58,33 +59,7 @@ import {
|
|
|
58
59
|
type SuppressionRow,
|
|
59
60
|
type SymbolRow,
|
|
60
61
|
} from "./read.ts";
|
|
61
|
-
import {
|
|
62
|
-
COMMENT_PLACEMENTS,
|
|
63
|
-
EVIDENCE_KINDS,
|
|
64
|
-
MAX_ELEMENT,
|
|
65
|
-
MODULE_FILE,
|
|
66
|
-
DEFAULT_SPACE,
|
|
67
|
-
GENERATED_FILES,
|
|
68
|
-
isGeneratedFile,
|
|
69
|
-
MACRO_NAME,
|
|
70
|
-
MACRO_PARAM,
|
|
71
|
-
SPACE,
|
|
72
|
-
SWITCH_NAME,
|
|
73
|
-
NO_ELEMENT_TYPES,
|
|
74
|
-
OPERAND_SELECTS,
|
|
75
|
-
POINTER_KIND,
|
|
76
|
-
REFERENCE_KINDS,
|
|
77
|
-
REGION_TYPES,
|
|
78
|
-
SYMBOL_KINDS,
|
|
79
|
-
type CommentPlacement,
|
|
80
|
-
type EvidenceKind,
|
|
81
|
-
type OperandSelect,
|
|
82
|
-
type Origin,
|
|
83
|
-
type ReferenceKind,
|
|
84
|
-
type RegionType,
|
|
85
|
-
type Space,
|
|
86
|
-
type SymbolKind,
|
|
87
|
-
} from "./schema.ts";
|
|
62
|
+
import { COMMENT_PLACEMENTS, EVIDENCE_KINDS, MAX_ELEMENT, MODULE_FILE, DEFAULT_SPACE, GENERATED_FILES, isGeneratedFile, MACRO_NAME, MACRO_PARAM, SWITCH_NAME, NO_ELEMENT_TYPES, OPERAND_SELECTS, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type CommentPlacement, type EvidenceKind, type OperandSelect, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
88
63
|
import { newRevisionId, rowId, sqlName } from "./text.ts";
|
|
89
64
|
|
|
90
65
|
/** What a change rests on: a coverage run, a test or another project file with its SHA-256, or a note. */
|
|
@@ -132,20 +107,11 @@ const MAX_PARAMS = 16;
|
|
|
132
107
|
const MAX_EVIDENCE_REF = 1000;
|
|
133
108
|
|
|
134
109
|
export function checkAddress(value: number, what = "address"): void {
|
|
135
|
-
if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new
|
|
110
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new Refusal("invalid-input", `${what} must be from $0000 to $ffff.`);
|
|
136
111
|
}
|
|
137
112
|
|
|
138
113
|
function checkOneOf<T extends string>(values: readonly T[], value: string, what: string): asserts value is T {
|
|
139
|
-
if (!(values as readonly string[]).includes(value)) throw new
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function hex(value: number): string {
|
|
143
|
-
return `$${value.toString(16).padStart(4, "0")}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface AddressRange {
|
|
147
|
-
start: number;
|
|
148
|
-
end: number;
|
|
114
|
+
if (!(values as readonly string[]).includes(value)) throw new Refusal("invalid-input", `${what} must be one of: ${values.join(", ")}.`);
|
|
149
115
|
}
|
|
150
116
|
|
|
151
117
|
/** Parts of `range` that no range in `holes` covers. */
|
|
@@ -191,14 +157,14 @@ export class Change {
|
|
|
191
157
|
// A merge of two computers' work can leave conflicts: only a merge revision writes then.
|
|
192
158
|
const found = operation === "merge" ? [] : conflicts(db);
|
|
193
159
|
if (found.length > 0) {
|
|
194
|
-
throw new
|
|
160
|
+
throw new Refusal(
|
|
195
161
|
"conflict",
|
|
196
162
|
`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
163
|
);
|
|
198
164
|
}
|
|
199
165
|
const current = currentRevision(db);
|
|
200
166
|
if (context.expectedRevision !== undefined && resolveRevision(db, context.expectedRevision) !== current) {
|
|
201
|
-
throw new
|
|
167
|
+
throw new Refusal(
|
|
202
168
|
"stale-revision",
|
|
203
169
|
`Knowledge changed since revision ${context.expectedRevision}; it is now at revision ${current}. Read it again and decide again.`,
|
|
204
170
|
);
|
|
@@ -226,7 +192,7 @@ export class Change {
|
|
|
226
192
|
check(): void {
|
|
227
193
|
if (this.revision === null) return;
|
|
228
194
|
const [found] = conflicts(this.#db);
|
|
229
|
-
if (found !== undefined) throw new
|
|
195
|
+
if (found !== undefined) throw new Refusal("conflict", `The write would leave ${describe(found)}. Nothing was written.`);
|
|
230
196
|
}
|
|
231
197
|
|
|
232
198
|
/** The id of the next row this write inserts. */
|
|
@@ -327,9 +293,9 @@ function describe(conflict: Conflict): string {
|
|
|
327
293
|
const place = (entry: ConflictRow) => {
|
|
328
294
|
if (entry.entity === "macro") return entry.row.name;
|
|
329
295
|
const row = entry.row as { address?: number; start?: number; end?: number; from?: number };
|
|
330
|
-
if (row.address !== undefined) return
|
|
331
|
-
if (row.from !== undefined && entry.entity === "reference") return
|
|
332
|
-
return row.end === undefined || row.end === row.start ?
|
|
296
|
+
if (row.address !== undefined) return formatC64Address(row.address);
|
|
297
|
+
if (row.from !== undefined && entry.entity === "reference") return formatC64Address(row.from);
|
|
298
|
+
return row.end === undefined || row.end === row.start ? formatC64Address(row.start!) : `${formatC64Address(row.start!)}-${formatC64Address(row.end)}`;
|
|
333
299
|
};
|
|
334
300
|
return `${CONFLICT_RULES[conflict.rule]} in ${conflict.space} (${[...new Set(conflict.rows.map(place))].join(", ")})`;
|
|
335
301
|
}
|
|
@@ -348,8 +314,8 @@ function liftRegionSuppressions(db: DatabaseSync, change: Change, start: number,
|
|
|
348
314
|
function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
349
315
|
for (const item of evidence ?? []) {
|
|
350
316
|
checkOneOf(EVIDENCE_KINDS, item.kind, "evidence kind");
|
|
351
|
-
if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new
|
|
352
|
-
if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new
|
|
317
|
+
if (item.ref.trim().length === 0 || item.ref.length > MAX_EVIDENCE_REF) throw new Refusal("invalid-input", `evidence has 1 to ${MAX_EVIDENCE_REF} characters after its kind.`);
|
|
318
|
+
if (item.sha256 !== undefined && !/^[0-9a-f]{64}$/.test(item.sha256)) throw new Refusal("invalid-input", "evidence sha256 must be 64 lowercase hex digits.");
|
|
353
319
|
checkSpace(item);
|
|
354
320
|
}
|
|
355
321
|
}
|
|
@@ -357,7 +323,7 @@ function checkEvidence(evidence: Evidence[] | undefined): void {
|
|
|
357
323
|
/** Runs semantic writes in one transaction, as one revision. */
|
|
358
324
|
export function write<T>(db: DatabaseSync, context: WriteContext, operation: string, work: (change: Change) => T): { revision: string | null; current: T } {
|
|
359
325
|
if (context.origin !== "user" && context.origin !== "llm") {
|
|
360
|
-
throw new
|
|
326
|
+
throw new Refusal("invalid-input", "A semantic write comes from the user or the LLM.");
|
|
361
327
|
}
|
|
362
328
|
checkEvidence(context.evidence);
|
|
363
329
|
return transaction(db, () => {
|
|
@@ -393,14 +359,14 @@ type AddressRequest = InSpace & { address: number };
|
|
|
393
359
|
const spaceOf = (request: InSpace): Space => request.space ?? DEFAULT_SPACE;
|
|
394
360
|
|
|
395
361
|
export function checkSpace(request: InSpace): void {
|
|
396
|
-
if (request.space !== undefined && !
|
|
362
|
+
if (request.space !== undefined && !SPACE_PATTERN.test(request.space)) throw new Refusal("invalid-input", "space must be c64, drive8, or bank0 to bank255.");
|
|
397
363
|
}
|
|
398
364
|
|
|
399
365
|
function checkSymbol(request: SymbolRequest): void {
|
|
400
366
|
checkSpace(request);
|
|
401
367
|
checkAddress(request.address);
|
|
402
368
|
if (!SYMBOL_NAME.test(request.name)) {
|
|
403
|
-
throw new
|
|
369
|
+
throw new Refusal("invalid-input", "A symbol name is a letter or underscore, then letters, digits or underscores (at most 64), with an optional leading dot.");
|
|
404
370
|
}
|
|
405
371
|
if (request.kind !== undefined) checkOneOf(SYMBOL_KINDS, request.kind, "kind");
|
|
406
372
|
}
|
|
@@ -412,10 +378,10 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
412
378
|
if (existing !== undefined && existing.name === request.name && existing.kind === kind) return { current: existing };
|
|
413
379
|
const owner = symbolNamed(db, request.name, space);
|
|
414
380
|
if (owner !== undefined && owner.address !== request.address) {
|
|
415
|
-
throw new
|
|
381
|
+
throw new Refusal("conflict", `The name ${request.name} already belongs to ${formatC64Address(owner.address)}. Rename or remove that symbol first.`);
|
|
416
382
|
}
|
|
417
383
|
const [patch] = listPatches(db, undefined, request.name, undefined, space);
|
|
418
|
-
if (patch !== undefined) throw new
|
|
384
|
+
if (patch !== undefined) throw new Refusal("conflict", `${request.name} is the switch of the patch at ${formatC64Address(patch.start)}-${formatC64Address(patch.end)}. Give the symbol another name.`);
|
|
419
385
|
if (existing !== undefined) change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
420
386
|
change.insertSymbol({ space, address: request.address, name: request.name, kind, origin });
|
|
421
387
|
if (existing !== undefined && existing.name !== request.name) {
|
|
@@ -430,17 +396,17 @@ function renameStep(db: DatabaseSync, change: Change, origin: Origin, request: S
|
|
|
430
396
|
function removeSymbolStep(db: DatabaseSync, change: Change, origin: Origin, request: AddressRequest): Step<null> {
|
|
431
397
|
const space = spaceOf(request);
|
|
432
398
|
const existing = symbolAt(db, request.address, space);
|
|
433
|
-
if (existing === undefined) throw new
|
|
399
|
+
if (existing === undefined) throw new Refusal("not-found", `There is no symbol at ${formatC64Address(request.address)}.`);
|
|
434
400
|
const users = operandsNaming(db, space, existing.name);
|
|
435
401
|
if (users.length > 0) {
|
|
436
|
-
throw new
|
|
402
|
+
throw new Refusal(
|
|
437
403
|
"conflict",
|
|
438
|
-
`The operand expressions at ${users.map((row) =>
|
|
404
|
+
`The operand expressions at ${users.map((row) => formatC64Address(row.address)).join(", ")} use ${existing.name}. Change or remove them first (unoperand), or rename the symbol.`,
|
|
439
405
|
);
|
|
440
406
|
}
|
|
441
407
|
const uses = usesNaming(db, space, existing.name);
|
|
442
408
|
if (uses.length > 0) {
|
|
443
|
-
throw new
|
|
409
|
+
throw new Refusal("conflict", `The macro uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")} use ${existing.name}. Change or remove them first (unuse), or rename the symbol.`);
|
|
444
410
|
}
|
|
445
411
|
// No suppression: an analyzer name is a second name and never comes back as the name.
|
|
446
412
|
change.close("symbols", "space = ? AND address = ?", space, request.address);
|
|
@@ -451,17 +417,17 @@ function checkRange(request: RangeRequest): void {
|
|
|
451
417
|
checkSpace(request);
|
|
452
418
|
checkAddress(request.start, "start");
|
|
453
419
|
checkAddress(request.end, "end");
|
|
454
|
-
if (request.end < request.start) throw new
|
|
420
|
+
if (request.end < request.start) throw new Refusal("invalid-input", "end must not be before start.");
|
|
455
421
|
}
|
|
456
422
|
|
|
457
423
|
function checkRegion(request: RegionRequest): void {
|
|
458
424
|
checkRange(request);
|
|
459
425
|
checkOneOf(REGION_TYPES, request.type, "type");
|
|
460
426
|
if (request.element === undefined) return;
|
|
461
|
-
if (NO_ELEMENT_TYPES.includes(request.type)) throw new
|
|
462
|
-
if (!Number.isInteger(request.element) || request.element < 1 || request.element > MAX_ELEMENT) throw new
|
|
427
|
+
if (NO_ELEMENT_TYPES.includes(request.type)) throw new Refusal("invalid-input", `A ${request.type} region has no element size.`);
|
|
428
|
+
if (!Number.isInteger(request.element) || request.element < 1 || request.element > MAX_ELEMENT) throw new Refusal("invalid-input", `element must be from 1 to ${MAX_ELEMENT}.`);
|
|
463
429
|
const length = request.end - request.start + 1;
|
|
464
|
-
if (length % request.element !== 0) throw new
|
|
430
|
+
if (length % request.element !== 0) throw new Refusal("invalid-input", `The region has ${length} bytes, which is not a multiple of the element size ${request.element}.`);
|
|
465
431
|
}
|
|
466
432
|
|
|
467
433
|
/** The element size of a piece of a region: kept when the piece still holds whole elements. */
|
|
@@ -499,7 +465,7 @@ function classifyStep(db: DatabaseSync, change: Change, origin: Origin, request:
|
|
|
499
465
|
function unclassifyStep(db: DatabaseSync, change: Change, origin: Origin, request: RangeRequest): Step<null> {
|
|
500
466
|
const space = spaceOf(request);
|
|
501
467
|
if (regionsOverlapping(db, request.start, request.end, space).length === 0) {
|
|
502
|
-
throw new
|
|
468
|
+
throw new Refusal("not-found", `No region covers ${formatC64Address(request.start)}-${formatC64Address(request.end)}.`);
|
|
503
469
|
}
|
|
504
470
|
clearRange(db, change, request.start, request.end, space);
|
|
505
471
|
// Suppress only the parts that no region suppression holds yet, so suppressions never overlap.
|
|
@@ -514,7 +480,7 @@ function checkComment(request: CommentRequest): string {
|
|
|
514
480
|
checkAddress(request.address);
|
|
515
481
|
checkOneOf(COMMENT_PLACEMENTS, request.placement, "placement");
|
|
516
482
|
const text = request.text.trim();
|
|
517
|
-
if (text.length === 0 || text.length > MAX_COMMENT) throw new
|
|
483
|
+
if (text.length === 0 || text.length > MAX_COMMENT) throw new Refusal("invalid-input", `A comment has 1 to ${MAX_COMMENT} characters.`);
|
|
518
484
|
return text;
|
|
519
485
|
}
|
|
520
486
|
|
|
@@ -537,7 +503,7 @@ function checkPlacement(request: PlacementRequest): void {
|
|
|
537
503
|
function uncommentStep(db: DatabaseSync, change: Change, _origin: Origin, request: PlacementRequest): Step<null> {
|
|
538
504
|
const space = spaceOf(request);
|
|
539
505
|
if (!commentsAt(db, request.address, space).some((comment) => comment.placement === request.placement)) {
|
|
540
|
-
throw new
|
|
506
|
+
throw new Refusal("not-found", `There is no ${request.placement} comment at ${formatC64Address(request.address)}.`);
|
|
541
507
|
}
|
|
542
508
|
change.close("comments", "space = ? AND address = ? AND placement = ?", space, request.address, request.placement);
|
|
543
509
|
return { current: null };
|
|
@@ -548,7 +514,7 @@ function checkReference(request: ReferenceRequest): void {
|
|
|
548
514
|
checkAddress(request.from, "from");
|
|
549
515
|
checkAddress(request.to, "to");
|
|
550
516
|
checkOneOf(REFERENCE_KINDS, request.kind, "kind");
|
|
551
|
-
if (request.kind === POINTER_KIND) throw new
|
|
517
|
+
if (request.kind === POINTER_KIND) throw new Refusal("invalid-input", "Only an operand expression writes a pointer reference: use operand and unoperand.");
|
|
552
518
|
}
|
|
553
519
|
|
|
554
520
|
function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<ReferenceRow> {
|
|
@@ -564,7 +530,7 @@ function referenceStep(db: DatabaseSync, change: Change, origin: Origin, request
|
|
|
564
530
|
function unreferenceStep(db: DatabaseSync, change: Change, origin: Origin, request: ReferenceRequest): Step<null> {
|
|
565
531
|
const space = spaceOf(request);
|
|
566
532
|
if (!referencesFrom(db, request.from, space).some((reference) => reference.to === request.to && reference.kind === request.kind)) {
|
|
567
|
-
throw new
|
|
533
|
+
throw new Refusal("not-found", `There is no ${request.kind} reference from ${formatC64Address(request.from)} to ${formatC64Address(request.to)}.`);
|
|
568
534
|
}
|
|
569
535
|
change.close('"references"', "space = ? AND from_address = ? AND to_address = ? AND kind = ?", space, request.from, request.to, request.kind);
|
|
570
536
|
change.insertSuppression({ space, entity: "reference", start: request.from, end: request.from, to: request.to, kind: request.kind, origin: origin as SuppressionRow["origin"] });
|
|
@@ -594,19 +560,14 @@ function checkOperand(request: OperandRequest): void {
|
|
|
594
560
|
checkAddress(request.address);
|
|
595
561
|
checkOneOf(OPERAND_SELECTS, request.select, "select");
|
|
596
562
|
if (request.select === "expr") {
|
|
597
|
-
if (request.symbol !== undefined || request.offset !== undefined) throw new
|
|
598
|
-
if (typeof request.expression !== "string") throw new
|
|
599
|
-
|
|
600
|
-
checkExpression(request.expression);
|
|
601
|
-
} catch (error) {
|
|
602
|
-
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
563
|
+
if (request.symbol !== undefined || request.offset !== undefined) throw new Refusal("invalid-input", "An expr operand has expression text, and no symbol or offset.");
|
|
564
|
+
if (typeof request.expression !== "string") throw new Refusal("invalid-input", "An expr operand has expression text.");
|
|
565
|
+
checkExpression(request.expression);
|
|
605
566
|
return;
|
|
606
567
|
}
|
|
607
|
-
if (request.expression !== undefined) throw new
|
|
608
|
-
if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new
|
|
609
|
-
if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new
|
|
568
|
+
if (request.expression !== undefined) throw new Refusal("invalid-input", `A ${request.select} operand has a symbol and an optional offset, and no expression text.`);
|
|
569
|
+
if (request.symbol === undefined || !SYMBOL_NAME.test(request.symbol)) throw new Refusal("invalid-input", `A ${request.select} operand names a symbol: a letter or underscore, then letters, digits or underscores.`);
|
|
570
|
+
if (request.offset !== undefined && (!Number.isInteger(request.offset) || Math.abs(request.offset) > 0xffff)) throw new Refusal("invalid-input", "offset must be a whole number from -$ffff to $ffff.");
|
|
610
571
|
}
|
|
611
572
|
|
|
612
573
|
const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${value.toString(16).padStart(size === 0x100 ? 2 : 4, "0")}`);
|
|
@@ -617,36 +578,36 @@ const shown = (value: number, size: number) => (value < 0 ? String(value) : `$${
|
|
|
617
578
|
* the platform names of the C64 space.
|
|
618
579
|
*/
|
|
619
580
|
function checkOperandValue(db: DatabaseSync, space: Space, image: OperandImage, request: OperandRequest, text: string): number[] {
|
|
620
|
-
const where =
|
|
581
|
+
const where = formatC64Address(request.address);
|
|
621
582
|
const opcode = image.byteAt(request.address);
|
|
622
|
-
if (opcode === undefined) throw new
|
|
583
|
+
if (opcode === undefined) throw new Refusal("invalid-input", `${where} is outside the image.`);
|
|
623
584
|
const length = instructionLength(opcode);
|
|
624
|
-
if (length === 1) throw new
|
|
625
|
-
if ((opcode & 0x1f) === 0x10) throw new
|
|
585
|
+
if (length === 1) throw new Refusal("invalid-input", `The instruction at ${where} (opcode ${shown(opcode, 0x100)}) has no operand.`);
|
|
586
|
+
if ((opcode & 0x1f) === 0x10) throw new Refusal("invalid-input", `The instruction at ${where} is a branch: its operand is a distance, not a value.`);
|
|
626
587
|
if ((request.select === "lo" || request.select === "hi") && length === 3) {
|
|
627
|
-
throw new
|
|
588
|
+
throw new Refusal("invalid-input", `lo and hi give one byte, but the instruction at ${where} has a 2-byte operand. Use value.`);
|
|
628
589
|
}
|
|
629
590
|
const bytes = [image.byteAt(request.address + 1), length === 3 ? image.byteAt(request.address + 2) : 0];
|
|
630
|
-
if (bytes.some((byte) => byte === undefined)) throw new
|
|
591
|
+
if (bytes.some((byte) => byte === undefined)) throw new Refusal("invalid-input", `The instruction at ${where} runs past the end of the image.`);
|
|
631
592
|
const operand = bytes[0]! | (bytes[1]! << 8);
|
|
632
593
|
const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
|
|
633
594
|
let value: number;
|
|
634
595
|
try {
|
|
635
596
|
value = evaluate(text, resolve);
|
|
636
597
|
} catch (error) {
|
|
637
|
-
if (error instanceof
|
|
598
|
+
if (error instanceof Refusal) throw new Refusal(error.code, `${where}: ${error.message}`);
|
|
638
599
|
throw error;
|
|
639
600
|
}
|
|
640
601
|
const size = length === 2 ? 0x100 : 0x10000;
|
|
641
602
|
if (value < -size / 2 || value >= size || ((value % size) + size) % size !== operand) {
|
|
642
|
-
throw new
|
|
603
|
+
throw new Refusal("invalid-input", `At ${where}, ${text} gives ${shown(value, size)}, but the operand in the image is ${shown(operand, size)}.`);
|
|
643
604
|
}
|
|
644
605
|
return identifiers(text).map((name) => resolve(name)!);
|
|
645
606
|
}
|
|
646
607
|
|
|
647
608
|
function operandStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: OperandRequest): Step<OperandRow, OperandRow> {
|
|
648
609
|
const space = spaceOf(request);
|
|
649
|
-
if (context.image === undefined) throw new
|
|
610
|
+
if (context.image === undefined) throw new Refusal("invalid-input", "An operand expression is checked against the image, and no image was given.");
|
|
650
611
|
const wanted: Omit<OperandRow, "space" | "address" | "origin" | "revision"> = { select: request.select, offset: request.offset ?? 0 };
|
|
651
612
|
if (request.symbol !== undefined) wanted.symbol = request.symbol;
|
|
652
613
|
if (request.expression !== undefined) wanted.expression = request.expression.trim();
|
|
@@ -671,7 +632,7 @@ function operandStep(db: DatabaseSync, change: Change, context: ChangeContext &
|
|
|
671
632
|
|
|
672
633
|
function unoperandStep(db: DatabaseSync, change: Change, _origin: Origin, request: AddressRequest): Step<null> {
|
|
673
634
|
const space = spaceOf(request);
|
|
674
|
-
if (operandAt(db, request.address, space) === undefined) throw new
|
|
635
|
+
if (operandAt(db, request.address, space) === undefined) throw new Refusal("not-found", `There is no operand expression at ${formatC64Address(request.address)}.`);
|
|
675
636
|
change.close("operands", "space = ? AND address = ?", space, request.address);
|
|
676
637
|
change.close('"references"', "space = ? AND from_address = ? AND kind = ?", space, request.address, POINTER_KIND);
|
|
677
638
|
return { current: null };
|
|
@@ -682,11 +643,11 @@ type ModuleRequest = InSpace & { start: number; file: string; title?: string };
|
|
|
682
643
|
function checkModule(request: ModuleRequest): void {
|
|
683
644
|
checkSpace(request);
|
|
684
645
|
checkAddress(request.start, "start");
|
|
685
|
-
if (!MODULE_FILE.test(request.file)) throw new
|
|
646
|
+
if (!MODULE_FILE.test(request.file)) throw new Refusal("invalid-input", "A module file is a name of letters, digits, underscores or hyphens (at most 60) that ends in .asm, without a directory.");
|
|
686
647
|
if (isGeneratedFile(request.file)) {
|
|
687
|
-
throw new
|
|
648
|
+
throw new Refusal("invalid-input", `c64-source writes ${GENERATED_FILES.join(", ")} itself, in any letter case, so a module cannot have the file ${request.file}. Give the module another file.`);
|
|
688
649
|
}
|
|
689
|
-
if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new
|
|
650
|
+
if (request.title !== undefined && (request.title.trim().length === 0 || request.title.length > MAX_COMMENT)) throw new Refusal("invalid-input", `A module title has 1 to ${MAX_COMMENT} characters.`);
|
|
690
651
|
}
|
|
691
652
|
|
|
692
653
|
function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: ModuleRequest): Step<ModuleRow> {
|
|
@@ -697,7 +658,7 @@ function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: M
|
|
|
697
658
|
if (existing !== undefined && existing.file === request.file && existing.title === title) return { current: existing };
|
|
698
659
|
const owner = moduleWithFile(db, request.file, space);
|
|
699
660
|
if (owner !== undefined && owner.start !== request.start) {
|
|
700
|
-
throw new
|
|
661
|
+
throw new Refusal("conflict", `The file ${request.file} already belongs to the module at ${formatC64Address(owner.start)}. Give this module another file, or remove that module first.`);
|
|
701
662
|
}
|
|
702
663
|
if (existing !== undefined) change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
703
664
|
change.insertModule({ space, start: request.start, file: request.file, ...(title === undefined ? {} : { title }), origin: origin as ModuleRow["origin"] });
|
|
@@ -706,7 +667,7 @@ function moduleStep(db: DatabaseSync, change: Change, origin: Origin, request: M
|
|
|
706
667
|
|
|
707
668
|
function unmoduleStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
708
669
|
const space = spaceOf(request);
|
|
709
|
-
if (moduleAt(db, request.start, space) === undefined) throw new
|
|
670
|
+
if (moduleAt(db, request.start, space) === undefined) throw new Refusal("not-found", `No module starts at ${formatC64Address(request.start)}.`);
|
|
710
671
|
change.close("modules", "space = ? AND start_address = ?", space, request.start);
|
|
711
672
|
return { current: null };
|
|
712
673
|
}
|
|
@@ -729,20 +690,20 @@ function checkBraces(text: string, what: string): void {
|
|
|
729
690
|
if (character === ";") break;
|
|
730
691
|
if (character === '"' || character === "'") quote = character;
|
|
731
692
|
else if (character === "{") depth++;
|
|
732
|
-
else if (character === "}" && --depth < 0) throw new
|
|
693
|
+
else if (character === "}" && --depth < 0) throw new Refusal("invalid-input", `${what} has a } with no { before it.`);
|
|
733
694
|
}
|
|
734
695
|
}
|
|
735
|
-
if (depth !== 0) throw new
|
|
696
|
+
if (depth !== 0) throw new Refusal("invalid-input", `${what} has a { with no }.`);
|
|
736
697
|
}
|
|
737
698
|
|
|
738
699
|
function checkCommentText(comment: string | undefined, what: string): void {
|
|
739
|
-
if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new
|
|
700
|
+
if (comment !== undefined && (comment.trim().length === 0 || comment.length > MAX_COMMENT)) throw new Refusal("invalid-input", `${what} has 1 to ${MAX_COMMENT} characters.`);
|
|
740
701
|
}
|
|
741
702
|
|
|
742
703
|
/** Refuses a range with a byte outside the image. */
|
|
743
704
|
function checkInImage(image: OperandImage, start: number, end: number, what: string): void {
|
|
744
705
|
for (let address = start; address <= end; address++) {
|
|
745
|
-
if (image.byteAt(address) === undefined) throw new
|
|
706
|
+
if (image.byteAt(address) === undefined) throw new Refusal("invalid-input", `${formatC64Address(address)} is outside the image: ${what} is a range of the image.`);
|
|
746
707
|
}
|
|
747
708
|
}
|
|
748
709
|
|
|
@@ -751,28 +712,28 @@ type PatchRequest = InSpace & { start: number; end: number; switch: string; code
|
|
|
751
712
|
function checkPatch(request: PatchRequest): void {
|
|
752
713
|
checkRange(request);
|
|
753
714
|
if (typeof request.switch !== "string" || !SWITCH_NAME.test(request.switch)) {
|
|
754
|
-
throw new
|
|
715
|
+
throw new Refusal("invalid-input", "A switch name is an upper-case letter, then 1 to 31 upper-case letters, digits or underscores, for example CRACK.");
|
|
755
716
|
}
|
|
756
|
-
if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new
|
|
717
|
+
if (typeof request.code !== "string" || request.code.length > MAX_CODE) throw new Refusal("invalid-input", `Patch code has at most ${MAX_CODE} characters; empty code fills the range with NOP.`);
|
|
757
718
|
checkBraces(request.code, "The patch code");
|
|
758
719
|
checkCommentText(request.comment, "A patch comment");
|
|
759
720
|
}
|
|
760
721
|
|
|
761
722
|
function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: PatchRequest): Step<PatchRow, PatchRow> {
|
|
762
723
|
const space = spaceOf(request);
|
|
763
|
-
const range = `${
|
|
764
|
-
if (context.image === undefined) throw new
|
|
724
|
+
const range = `${formatC64Address(request.start)}-${formatC64Address(request.end)}`;
|
|
725
|
+
if (context.image === undefined) throw new Refusal("invalid-input", "A patch is checked against the image, and no image was given.");
|
|
765
726
|
checkInImage(context.image, request.start, request.end, "a patch");
|
|
766
727
|
const symbol = symbolNamed(db, request.switch, space);
|
|
767
|
-
if (symbol !== undefined) throw new
|
|
768
|
-
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new
|
|
728
|
+
if (symbol !== undefined) throw new Refusal("conflict", `The switch name ${request.switch} is the name of the symbol at ${formatC64Address(symbol.address)}. Give the switch another name.`);
|
|
729
|
+
if (space === DEFAULT_SPACE && context.image.platform.has(request.switch)) throw new Refusal("conflict", `The switch name ${request.switch} is a C64 platform name. Give the switch another name.`);
|
|
769
730
|
const other = patchesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
770
731
|
if (other !== undefined) {
|
|
771
|
-
throw new
|
|
732
|
+
throw new Refusal("conflict", `The patch ${range} overlaps the patch of ${other.switch} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Change or remove that patch first (unpatch).`);
|
|
772
733
|
}
|
|
773
734
|
const [use] = macroUsesOverlapping(db, request.start, request.end, space);
|
|
774
735
|
if (use !== undefined) {
|
|
775
|
-
throw new
|
|
736
|
+
throw new Refusal("conflict", `The patch ${range} overlaps the use of the macro ${use.macro} at ${formatC64Address(use.start)}-${formatC64Address(use.end)}. Remove that use first (unuse).`);
|
|
776
737
|
}
|
|
777
738
|
const existing = patchesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
778
739
|
const code = request.code.trimEnd();
|
|
@@ -788,7 +749,7 @@ function patchStep(db: DatabaseSync, change: Change, context: ChangeContext & {
|
|
|
788
749
|
|
|
789
750
|
function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
790
751
|
const space = spaceOf(request);
|
|
791
|
-
if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new
|
|
752
|
+
if (!patchesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new Refusal("not-found", `No patch starts at ${formatC64Address(request.start)}.`);
|
|
792
753
|
change.close("patches", "space = ? AND start_address = ?", space, request.start);
|
|
793
754
|
return { current: null };
|
|
794
755
|
}
|
|
@@ -796,21 +757,21 @@ function unpatchStep(db: DatabaseSync, change: Change, _origin: Origin, request:
|
|
|
796
757
|
type MacroRequest = InSpace & { name: string; params?: string[]; body: string; comment?: string };
|
|
797
758
|
|
|
798
759
|
function checkMacroName(name: unknown, what = "A macro name"): void {
|
|
799
|
-
if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new
|
|
760
|
+
if (typeof name !== "string" || !MACRO_NAME.test(name)) throw new Refusal("invalid-input", `${what} is a letter or underscore, then at most 31 letters, digits or underscores.`);
|
|
800
761
|
}
|
|
801
762
|
|
|
802
763
|
function checkMacro(request: MacroRequest): void {
|
|
803
764
|
checkSpace(request);
|
|
804
765
|
checkMacroName(request.name);
|
|
805
766
|
const params = request.params ?? [];
|
|
806
|
-
if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new
|
|
767
|
+
if (!Array.isArray(params) || params.length > MAX_PARAMS) throw new Refusal("invalid-input", `A macro has 0 to ${MAX_PARAMS} params.`);
|
|
807
768
|
for (const param of params) {
|
|
808
769
|
if (typeof param !== "string" || !MACRO_PARAM.test(param)) {
|
|
809
|
-
throw new
|
|
770
|
+
throw new Refusal("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
771
|
}
|
|
811
772
|
}
|
|
812
|
-
if (new Set(params).size !== params.length) throw new
|
|
813
|
-
if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new
|
|
773
|
+
if (new Set(params).size !== params.length) throw new Refusal("invalid-input", "The params of a macro have different names.");
|
|
774
|
+
if (typeof request.body !== "string" || request.body.trim().length === 0 || request.body.length > MAX_CODE) throw new Refusal("invalid-input", `A macro body has 1 to ${MAX_CODE} characters.`);
|
|
814
775
|
checkBraces(request.body, "The macro body");
|
|
815
776
|
checkCommentText(request.comment, "A macro comment");
|
|
816
777
|
}
|
|
@@ -825,9 +786,9 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
|
|
|
825
786
|
if (existing !== undefined && existing.params.length !== params.length) {
|
|
826
787
|
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
827
788
|
if (uses.length > 0) {
|
|
828
|
-
throw new
|
|
789
|
+
throw new Refusal(
|
|
829
790
|
"conflict",
|
|
830
|
-
`The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) =>
|
|
791
|
+
`The macro ${request.name} has ${existing.params.length} params, and its uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")} give that many args. Remove the uses first (unuse), or keep the number of params.`,
|
|
831
792
|
);
|
|
832
793
|
}
|
|
833
794
|
}
|
|
@@ -839,9 +800,9 @@ function macroStep(db: DatabaseSync, change: Change, origin: Origin, request: Ma
|
|
|
839
800
|
|
|
840
801
|
function unmacroStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { name: string }): Step<null> {
|
|
841
802
|
const space = spaceOf(request);
|
|
842
|
-
if (macroNamed(db, request.name, space) === undefined) throw new
|
|
803
|
+
if (macroNamed(db, request.name, space) === undefined) throw new Refusal("not-found", `There is no macro ${request.name}.`);
|
|
843
804
|
const uses = listMacroUses(db, undefined, request.name, undefined, space);
|
|
844
|
-
if (uses.length > 0) throw new
|
|
805
|
+
if (uses.length > 0) throw new Refusal("conflict", `The macro ${request.name} has uses at ${uses.map((row) => formatC64Address(row.start)).join(", ")}. Remove them first (unuse).`);
|
|
845
806
|
change.close("macros", "space = ? AND name = ?", space, request.name);
|
|
846
807
|
return { current: null };
|
|
847
808
|
}
|
|
@@ -852,15 +813,10 @@ function checkMacroUse(request: MacroUseRequest): void {
|
|
|
852
813
|
checkRange(request);
|
|
853
814
|
checkMacroName(request.macro, "The macro of a use");
|
|
854
815
|
const args = request.args ?? [];
|
|
855
|
-
if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new
|
|
816
|
+
if (!Array.isArray(args) || args.length > MAX_PARAMS) throw new Refusal("invalid-input", `A macro use has 0 to ${MAX_PARAMS} args.`);
|
|
856
817
|
for (const arg of args) {
|
|
857
|
-
if (typeof arg !== "string") throw new
|
|
858
|
-
|
|
859
|
-
checkExpression(arg);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
if (error instanceof ExpressionError) throw new KnowledgeError("invalid-input", error.message);
|
|
862
|
-
throw error;
|
|
863
|
-
}
|
|
818
|
+
if (typeof arg !== "string") throw new Refusal("invalid-input", "Each arg of a macro use is expression text.");
|
|
819
|
+
checkExpression(arg);
|
|
864
820
|
}
|
|
865
821
|
}
|
|
866
822
|
|
|
@@ -880,20 +836,20 @@ function renameInUses(db: DatabaseSync, change: Change, space: Space, from: stri
|
|
|
880
836
|
function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { image?: OperandImage }, request: MacroUseRequest): Step<MacroUseRow, MacroUseRow> {
|
|
881
837
|
const space = spaceOf(request);
|
|
882
838
|
const image = context.image;
|
|
883
|
-
const range = `${
|
|
884
|
-
if (image === undefined) throw new
|
|
839
|
+
const range = `${formatC64Address(request.start)}-${formatC64Address(request.end)}`;
|
|
840
|
+
if (image === undefined) throw new Refusal("invalid-input", "A macro use is checked against the image, and no image was given.");
|
|
885
841
|
const macro = macroNamed(db, request.macro, space);
|
|
886
|
-
if (macro === undefined) throw new
|
|
842
|
+
if (macro === undefined) throw new Refusal("not-found", `There is no macro ${request.macro}. Write it first (macro).`);
|
|
887
843
|
const args = (request.args ?? []).map((arg) => arg.trim());
|
|
888
844
|
if (args.length !== macro.params.length) {
|
|
889
|
-
throw new
|
|
845
|
+
throw new Refusal("invalid-input", `The macro ${macro.name} has ${macro.params.length} params (${listText(macro.params)}), and the use gives ${args.length} args.`);
|
|
890
846
|
}
|
|
891
847
|
const resolve = (name: string) => symbolNamed(db, name, space)?.address ?? (space === DEFAULT_SPACE ? image.platform.get(name) : undefined);
|
|
892
848
|
for (const arg of args) {
|
|
893
849
|
try {
|
|
894
850
|
evaluate(arg, resolve);
|
|
895
851
|
} catch (error) {
|
|
896
|
-
if (error instanceof
|
|
852
|
+
if (error instanceof Refusal) throw new Refusal(error.code, `${range}: ${error.message}`);
|
|
897
853
|
throw error;
|
|
898
854
|
}
|
|
899
855
|
}
|
|
@@ -901,15 +857,15 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
|
|
|
901
857
|
let next = request.start;
|
|
902
858
|
while (next <= request.end) next += instructionLength(image.byteAt(next)!);
|
|
903
859
|
if (next !== request.end + 1) {
|
|
904
|
-
throw new
|
|
860
|
+
throw new Refusal("invalid-input", `A macro use is a run of whole instructions, but the instructions from ${formatC64Address(request.start)} end at ${formatC64Address(next - 1)}, not at ${formatC64Address(request.end)}.`);
|
|
905
861
|
}
|
|
906
862
|
const [patch] = patchesOverlapping(db, request.start, request.end, space);
|
|
907
863
|
if (patch !== undefined) {
|
|
908
|
-
throw new
|
|
864
|
+
throw new Refusal("conflict", `The macro use ${range} overlaps the patch of ${patch.switch} at ${formatC64Address(patch.start)}-${formatC64Address(patch.end)}. Change or remove that patch first (unpatch).`);
|
|
909
865
|
}
|
|
910
866
|
const other = macroUsesOverlapping(db, request.start, request.end, space).find((row) => row.start !== request.start);
|
|
911
867
|
if (other !== undefined) {
|
|
912
|
-
throw new
|
|
868
|
+
throw new Refusal("conflict", `The macro use ${range} overlaps the use of ${other.macro} at ${formatC64Address(other.start)}-${formatC64Address(other.end)}. Remove that use first (unuse).`);
|
|
913
869
|
}
|
|
914
870
|
const existing = macroUsesOverlapping(db, request.start, request.start, space).find((row) => row.start === request.start);
|
|
915
871
|
if (existing !== undefined && existing.end === request.end && existing.macro === request.macro && listText(existing.args) === listText(args)) return { current: existing };
|
|
@@ -922,7 +878,7 @@ function useStep(db: DatabaseSync, change: Change, context: ChangeContext & { im
|
|
|
922
878
|
|
|
923
879
|
function unuseStep(db: DatabaseSync, change: Change, _origin: Origin, request: InSpace & { start: number }): Step<null> {
|
|
924
880
|
const space = spaceOf(request);
|
|
925
|
-
if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new
|
|
881
|
+
if (!macroUsesOverlapping(db, request.start, request.start, space).some((row) => row.start === request.start)) throw new Refusal("not-found", `No macro use starts at ${formatC64Address(request.start)}.`);
|
|
926
882
|
change.close("macro_uses", "space = ? AND start_address = ?", space, request.start);
|
|
927
883
|
return { current: null };
|
|
928
884
|
}
|
|
@@ -1072,7 +1028,7 @@ export type BatchStep = Step<
|
|
|
1072
1028
|
* or none when one is refused. Every write is checked before anything opens.
|
|
1073
1029
|
*/
|
|
1074
1030
|
export function applyBatch(db: DatabaseSync, context: WriteContext, operations: BatchOperation[]): { revision: string | null; steps: BatchStep[] } {
|
|
1075
|
-
if (operations.length === 0 || operations.length > MAX_BATCH) throw new
|
|
1031
|
+
if (operations.length === 0 || operations.length > MAX_BATCH) throw new Refusal("invalid-input", `A batch has 1 to ${MAX_BATCH} writes.`);
|
|
1076
1032
|
operations.forEach((operation, index) => {
|
|
1077
1033
|
try {
|
|
1078
1034
|
switch (operation.op) {
|
|
@@ -1116,13 +1072,13 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
1116
1072
|
case "use":
|
|
1117
1073
|
return checkMacroUse(operation);
|
|
1118
1074
|
default:
|
|
1119
|
-
throw new
|
|
1075
|
+
throw new Refusal(
|
|
1120
1076
|
"invalid-input",
|
|
1121
1077
|
`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
1078
|
);
|
|
1123
1079
|
}
|
|
1124
1080
|
} catch (error) {
|
|
1125
|
-
if (error instanceof
|
|
1081
|
+
if (error instanceof Refusal) throw new Refusal(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
1126
1082
|
throw error;
|
|
1127
1083
|
}
|
|
1128
1084
|
});
|
|
@@ -1168,7 +1124,7 @@ export function applyBatch(db: DatabaseSync, context: WriteContext, operations:
|
|
|
1168
1124
|
return unuseStep(db, change, context.origin, operation);
|
|
1169
1125
|
}
|
|
1170
1126
|
} catch (error) {
|
|
1171
|
-
if (error instanceof
|
|
1127
|
+
if (error instanceof Refusal) throw new Refusal(error.code, `Write ${index + 1}: ${error.message} Nothing was written.`);
|
|
1172
1128
|
throw error;
|
|
1173
1129
|
}
|
|
1174
1130
|
}),
|
|
@@ -1303,7 +1259,7 @@ export function mergeConflicts(db: DatabaseSync, context: WriteContext, platform
|
|
|
1303
1259
|
return write(db, context, "merge", (change) => {
|
|
1304
1260
|
const choices: MergeChoice[] = [];
|
|
1305
1261
|
for (let found = conflicts(db); found.length > 0; found = conflicts(db)) {
|
|
1306
|
-
if (choices.length >= MAX_CHOICES) throw new
|
|
1262
|
+
if (choices.length >= MAX_CHOICES) throw new Refusal("invalid-database", `The merge made ${MAX_CHOICES} choices and still finds conflicts. Nothing was written.`);
|
|
1307
1263
|
const conflict = found[0]!;
|
|
1308
1264
|
const newest = conflict.rows.at(-1)!;
|
|
1309
1265
|
const older = conflict.rows.slice(0, -1);
|