@henols/c64-re-tools 2.1.0-rc.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/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 +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- 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 +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- 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 +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- 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 +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- 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 +1 -1
- 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 +2 -2
- 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/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge/ in the
|
|
2
|
-
// project folder (--project, or the one found from the working directory)
|
|
3
|
-
// prints one
|
|
2
|
+
// project folder (--project, or the one found from the working directory).
|
|
3
|
+
// Each command prints one JSON result.
|
|
4
4
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { DatabaseSync } from "node:sqlite";
|
|
9
|
-
import { parseArgs } from "node:util";
|
|
10
9
|
|
|
11
|
-
import { formatC64Address
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import { formatC64Address } from "#src/c64.ts";
|
|
11
|
+
import { Refusal } from "#src/errors.ts";
|
|
12
|
+
import { readImage } from "#src/host-client/image.ts";
|
|
13
|
+
import { readProjectFile, writeReported } from "#src/host-client/transfer.ts";
|
|
14
14
|
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
15
|
-
import {
|
|
16
|
-
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
15
|
+
import { openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
17
16
|
import { operandText } from "#src/knowledge/expression.ts";
|
|
18
17
|
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
18
|
+
import { joinBatches, type Batch, type JoinChoice, type JoinClash } from "#src/knowledge/join.ts";
|
|
19
|
+
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
20
|
+
import { addCoverage, coverageRuns, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
|
|
19
21
|
import {
|
|
20
|
-
at,
|
|
21
|
-
analyzerNamed,
|
|
22
22
|
analyzerNamesAt,
|
|
23
|
+
at,
|
|
23
24
|
commentsAt,
|
|
24
25
|
currentRevision,
|
|
25
26
|
listAnalyzerNames,
|
|
@@ -29,9 +30,9 @@ import {
|
|
|
29
30
|
listModules,
|
|
30
31
|
listOperands,
|
|
31
32
|
listPatches,
|
|
32
|
-
listSpaces,
|
|
33
33
|
listReferences,
|
|
34
34
|
listRegions,
|
|
35
|
+
listSpaces,
|
|
35
36
|
listSuppressions,
|
|
36
37
|
listSymbols,
|
|
37
38
|
operandAt,
|
|
@@ -39,7 +40,6 @@ import {
|
|
|
39
40
|
referencesTo,
|
|
40
41
|
search,
|
|
41
42
|
symbolAt,
|
|
42
|
-
symbolNamed,
|
|
43
43
|
type CommentRow,
|
|
44
44
|
type MacroRow,
|
|
45
45
|
type MacroUseRow,
|
|
@@ -51,23 +51,7 @@ import {
|
|
|
51
51
|
type SuppressionRow,
|
|
52
52
|
type SymbolRow,
|
|
53
53
|
} from "#src/knowledge/read.ts";
|
|
54
|
-
import {
|
|
55
|
-
COMMENT_PLACEMENTS,
|
|
56
|
-
DEFAULT_SPACE,
|
|
57
|
-
EVIDENCE_KINDS,
|
|
58
|
-
OPERAND_SELECTS,
|
|
59
|
-
ORIGINS,
|
|
60
|
-
REFERENCE_KINDS,
|
|
61
|
-
REGION_TYPES,
|
|
62
|
-
SPACE,
|
|
63
|
-
SYMBOL_KINDS,
|
|
64
|
-
type CommentPlacement,
|
|
65
|
-
type OperandSelect,
|
|
66
|
-
type Origin,
|
|
67
|
-
type ReferenceKind,
|
|
68
|
-
type RegionType,
|
|
69
|
-
type SymbolKind,
|
|
70
|
-
} from "#src/knowledge/schema.ts";
|
|
54
|
+
import { ANALYZERS, COMMENT_PLACEMENTS, DEFAULT_SPACE, EVIDENCE_KINDS, isSemantic, OPERAND_SELECTS, ORIGINS, REFERENCE_KINDS, REGION_TYPES, SEMANTIC_ORIGINS, SYMBOL_KINDS, type Origin, type OperandSelect } from "#src/knowledge/schema.ts";
|
|
71
55
|
import {
|
|
72
56
|
addReference,
|
|
73
57
|
applyBatch,
|
|
@@ -98,156 +82,15 @@ import {
|
|
|
98
82
|
type WriteContext,
|
|
99
83
|
} from "#src/knowledge/write.ts";
|
|
100
84
|
import { platformNames } from "#src/platform.ts";
|
|
101
|
-
import { addCoverage, coverageRuns, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
|
|
102
|
-
import { WireFailure } from "#src/host-client/tools.ts";
|
|
103
85
|
import { isInside, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRelative, projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
86
|
+
import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec, type SubjectSpec } from "#src/script.ts";
|
|
104
87
|
import { listing, listingText, type ListingSource } from "./lib/listing.ts";
|
|
105
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
106
|
-
|
|
107
|
-
const USAGE = `knowledge.ts <command> [arguments] [options]
|
|
108
|
-
|
|
109
|
-
Read (no database needed):
|
|
110
|
-
at <address> everything known at one address
|
|
111
|
-
search <text> symbol names, analyzer names and comment texts
|
|
112
|
-
symbols [--start A --end A --kind K --origin O] [--analyzer]
|
|
113
|
-
--analyzer: the second names that DXA, Ghidra
|
|
114
|
-
and the idioms gave
|
|
115
|
-
regions [--start A --end A --type T --origin O]
|
|
116
|
-
comments [--start A --end A --origin O]
|
|
117
|
-
references [--start A --end A --kind K --origin O]
|
|
118
|
-
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
119
|
-
modules [--start A --end A --origin O] where the files of generated source start
|
|
120
|
-
operands [--start A --end A --origin O] operand expressions of instructions
|
|
121
|
-
patches [--start A --end A --switch S --origin O] code that a switch puts in
|
|
122
|
-
switches each switch with the ranges of its patches
|
|
123
|
-
macros [--origin O] the macros of the generated source
|
|
124
|
-
uses [--start A --end A --macro M --origin O] instructions written as a macro
|
|
125
|
-
spaces the address spaces that hold knowledge
|
|
126
|
-
program the program in the knowledge: its
|
|
127
|
-
segments and entry, and the coverage maps
|
|
128
|
-
that coverage imported, with the ranges
|
|
129
|
-
history <address> | history --name <name> | history --macro <name>
|
|
130
|
-
revisions [--limit N --before R]
|
|
131
|
-
revision <revision>
|
|
132
|
-
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
133
|
-
listing <start> <end> [--out <file>]
|
|
134
|
-
the program as ACME lines with names, operand
|
|
135
|
-
expressions, comments, the access of each line
|
|
136
|
-
in the coverage (x executed, r read, w written)
|
|
137
|
-
and the references in and out; --out writes it
|
|
138
|
-
as text
|
|
139
|
-
|
|
140
|
-
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
141
|
-
drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
|
|
142
|
-
own space.
|
|
143
|
-
|
|
144
|
-
Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
|
|
145
|
-
llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
|
|
146
|
-
|
|
147
|
-
Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
148
|
-
rename <address> <name> [--kind K]
|
|
149
|
-
remove-symbol <address>
|
|
150
|
-
classify <start> <end> <type> [--element N]
|
|
151
|
-
unclassify <start> <end>
|
|
152
|
-
comment <address> <line|side> <text>
|
|
153
|
-
uncomment <address> <line|side>
|
|
154
|
-
reference <from> <to> <kind>
|
|
155
|
-
unreference <from> <to> <kind>
|
|
156
|
-
module <start> <file.asm> [--title T] a file of generated source starts here
|
|
157
|
-
unmodule <start>
|
|
158
|
-
operand <address> --lo|--hi|--value <symbol> [--offset N]
|
|
159
|
-
operand <address> --expr <text>
|
|
160
|
-
write the operand of the instruction at <address> as
|
|
161
|
-
an expression: the low or high byte of symbol + N, the
|
|
162
|
-
value symbol + N, or ACME expression text; refused when
|
|
163
|
-
it does not give the operand in the program of the
|
|
164
|
-
knowledge; give a negative offset as --offset=-1
|
|
165
|
-
unoperand <address>
|
|
166
|
-
patch <start> <end> <SWITCH> --code <text>|--code-file <file>|--nop [--comment T]
|
|
167
|
-
ACME code that replaces <start>-<end> when the build
|
|
168
|
-
defines SWITCH as 1 (c64-assembler --define SWITCH=1);
|
|
169
|
-
NOP fills the rest of the range; --nop puts only NOP
|
|
170
|
-
unpatch <start>
|
|
171
|
-
macro <name> [--params a,b] --body <text>|--body-file <file> [--comment T]
|
|
172
|
-
an ACME macro; the body names a param with a dot: .a
|
|
173
|
-
unmacro <name>
|
|
174
|
-
use <start> <end> <macro> [--args "x, y"]
|
|
175
|
-
write the instructions <start>-<end> as one use of
|
|
176
|
-
the macro, with one expression for each param
|
|
177
|
-
unuse <start>
|
|
178
|
-
image <image.json|prg> put the program into the knowledge: its bytes, and
|
|
179
|
-
for an image.json of c64-unpacker capture also the
|
|
180
|
-
entry, the entry state and the stages; with --space for
|
|
181
|
-
drive code or a cartridge bank. Only changed bytes write
|
|
182
|
-
coverage <map.bin>... join coverage maps of c64_memmap or a run into the
|
|
183
|
-
coverage of the knowledge; each map is evidence
|
|
184
|
-
of the revision, named by its path
|
|
185
|
-
revert <revision>
|
|
186
|
-
merge decide the conflicts that a git merge of two
|
|
187
|
-
computers' work left: of the rows of one conflict,
|
|
188
|
-
the newest stays; one revision for all
|
|
189
|
-
join <batch>... --out <file>
|
|
190
|
-
join the batches of agents that worked in parallel into
|
|
191
|
-
one batch for apply; writes no knowledge. A batch is a
|
|
192
|
-
list of writes, or {"owner": "$1000-$1fff", "ops": [...]}:
|
|
193
|
-
at an address in its owner range its name, comment,
|
|
194
|
-
operand and module win, and its regions come last
|
|
195
|
-
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
196
|
-
all as one revision, or none when one is refused;
|
|
197
|
-
operand, patch and use writes are checked against the
|
|
198
|
-
program of --space in the knowledge
|
|
199
|
-
|
|
200
|
-
Write options:
|
|
201
|
-
--reason <text> why, kept with the revision
|
|
202
|
-
--evidence <kind:ref> what the change rests on: coverage:<map> (a map that
|
|
203
|
-
coverage imported), test:<file>, file:<file> or
|
|
204
|
-
note:<text> (repeatable)
|
|
205
|
-
--expect-revision <R> refuse the write if knowledge changed after revision R
|
|
206
|
-
--origin user|llm who decided (default llm)
|
|
207
|
-
|
|
208
|
-
Addresses are $ and four hex digits; quote them in a shell: '$2100'. A name
|
|
209
|
-
also gives an address: a symbol name first, then an analyzer name.
|
|
210
|
-
A revision is its id, for example 20261015T214501.123-k3f9qz, or a start of
|
|
211
|
-
the id that only one revision has; 0 is the knowledge before every revision.`;
|
|
212
|
-
|
|
213
|
-
/** The space whose names give addresses: the --space of the command. */
|
|
214
|
-
let nameSpace = DEFAULT_SPACE;
|
|
215
|
-
|
|
216
|
-
/** An address as $ and four hex digits, or the address of a symbol name, else of an analyzer name. */
|
|
217
|
-
function address(value: string | undefined, what = "address"): number {
|
|
218
|
-
if (value === undefined) throw new UsageError(`${what} is missing`);
|
|
219
|
-
if (SYMBOL_NAME.test(value)) return addressOfName(value, what);
|
|
220
|
-
try {
|
|
221
|
-
return parseC64Address(value);
|
|
222
|
-
} catch {
|
|
223
|
-
throw new UsageError(`${what} must be $ followed by four hex digits, for example '$2100', or a name, not ${value}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function addressOfName(name: string, what: string): number {
|
|
228
|
-
const db = openForRead();
|
|
229
|
-
try {
|
|
230
|
-
const symbol = symbolNamed(db, name, nameSpace);
|
|
231
|
-
if (symbol !== undefined) return symbol.address;
|
|
232
|
-
const second = analyzerNamed(db, name, nameSpace);
|
|
233
|
-
if (second.length === 1) return second[0]!.address;
|
|
234
|
-
if (second.length > 1) throw new UsageError(`${what}: the analyzers give ${name} to ${second.map((row) => `${formatC64Address(row.address)} (${row.origin})`).join(" and ")}; give the address`);
|
|
235
|
-
throw new UsageError(`${what}: no symbol or analyzer name is ${name}`);
|
|
236
|
-
} finally {
|
|
237
|
-
db?.close();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
88
|
|
|
241
89
|
function oneOf<T extends string>(values: readonly T[], value: string | undefined, what: string): T {
|
|
242
90
|
if (value === undefined || !(values as readonly string[]).includes(value)) throw new UsageError(`${what} must be one of: ${values.join(", ")}`);
|
|
243
91
|
return value as T;
|
|
244
92
|
}
|
|
245
93
|
|
|
246
|
-
function integer(value: string | undefined, what: string): number {
|
|
247
|
-
if (value === undefined || !/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
|
|
248
|
-
return Number(value);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
94
|
// Output shapes: addresses as $xxxx, nothing internal.
|
|
252
95
|
/** The space of a row, shown only when it is not the C64. */
|
|
253
96
|
const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
|
|
@@ -354,120 +197,6 @@ function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<st
|
|
|
354
197
|
return reasons;
|
|
355
198
|
}
|
|
356
199
|
|
|
357
|
-
function run(argv: string[]): unknown {
|
|
358
|
-
const { values, positionals } = parseArgs({
|
|
359
|
-
args: argv,
|
|
360
|
-
allowPositionals: true,
|
|
361
|
-
strict: true,
|
|
362
|
-
options: {
|
|
363
|
-
start: { type: "string" },
|
|
364
|
-
end: { type: "string" },
|
|
365
|
-
kind: { type: "string" },
|
|
366
|
-
type: { type: "string" },
|
|
367
|
-
name: { type: "string" },
|
|
368
|
-
limit: { type: "string" },
|
|
369
|
-
before: { type: "string" },
|
|
370
|
-
reason: { type: "string" },
|
|
371
|
-
"expect-revision": { type: "string" },
|
|
372
|
-
origin: { type: "string" },
|
|
373
|
-
element: { type: "string" },
|
|
374
|
-
evidence: { type: "string", multiple: true },
|
|
375
|
-
out: { type: "string" },
|
|
376
|
-
title: { type: "string" },
|
|
377
|
-
space: { type: "string" },
|
|
378
|
-
lo: { type: "string" },
|
|
379
|
-
hi: { type: "string" },
|
|
380
|
-
value: { type: "string" },
|
|
381
|
-
expr: { type: "string" },
|
|
382
|
-
offset: { type: "string" },
|
|
383
|
-
code: { type: "string" },
|
|
384
|
-
"code-file": { type: "string" },
|
|
385
|
-
nop: { type: "boolean" },
|
|
386
|
-
comment: { type: "string" },
|
|
387
|
-
params: { type: "string" },
|
|
388
|
-
body: { type: "string" },
|
|
389
|
-
"body-file": { type: "string" },
|
|
390
|
-
args: { type: "string" },
|
|
391
|
-
switch: { type: "string" },
|
|
392
|
-
macro: { type: "string" },
|
|
393
|
-
analyzer: { type: "boolean" },
|
|
394
|
-
help: { type: "boolean" },
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
const [command, ...args] = positionals;
|
|
398
|
-
if (values.help || command === undefined) throw new UsageError("");
|
|
399
|
-
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
400
|
-
const space = values.space ?? DEFAULT_SPACE;
|
|
401
|
-
nameSpace = space;
|
|
402
|
-
const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
|
|
403
|
-
|
|
404
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "patches", "switches", "macros", "uses", "spaces", "program", "history", "revisions", "revision", "export", "listing"]);
|
|
405
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "patch", "unpatch", "macro", "unmacro", "use", "unuse", "revert", "apply", "merge", "image", "coverage"]);
|
|
406
|
-
// join reads batch files and writes one: no knowledge opens.
|
|
407
|
-
if (command === "join") return joinCommand(args, values.out, space);
|
|
408
|
-
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
409
|
-
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
410
|
-
if (read.has(command)) {
|
|
411
|
-
const origins = values.origin === undefined ? undefined : originFilter(values.origin);
|
|
412
|
-
if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
|
|
413
|
-
const db = openForRead();
|
|
414
|
-
try {
|
|
415
|
-
const answer = readAnswer(db, command, args, values, range, origins, space);
|
|
416
|
-
const found = conflicts(db);
|
|
417
|
-
// A git merge can leave conflicts: every read shows them until merge decides them.
|
|
418
|
-
return found.length === 0 ? answer : { ...answer, conflicts: found.map(showConflict) };
|
|
419
|
-
} finally {
|
|
420
|
-
db?.close();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
425
|
-
if (values.reason !== undefined) context.description = values.reason;
|
|
426
|
-
if (values["expect-revision"] !== undefined) context.expectedRevision = values["expect-revision"];
|
|
427
|
-
if (values.evidence !== undefined) context.evidence = values.evidence.map((value) => evidenceOf(value, space));
|
|
428
|
-
// Every argument is checked before the database opens: a refused write creates no database.
|
|
429
|
-
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
430
|
-
for (const option of ["lo", "hi", "value", "expr", "offset"] as const) {
|
|
431
|
-
if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
|
|
432
|
-
}
|
|
433
|
-
const only: Array<[keyof typeof values, readonly string[]]> = [
|
|
434
|
-
["code", ["patch"]],
|
|
435
|
-
["code-file", ["patch"]],
|
|
436
|
-
["nop", ["patch"]],
|
|
437
|
-
["comment", ["patch", "macro"]],
|
|
438
|
-
["params", ["macro"]],
|
|
439
|
-
["body", ["macro"]],
|
|
440
|
-
["body-file", ["macro"]],
|
|
441
|
-
["args", ["use"]],
|
|
442
|
-
];
|
|
443
|
-
for (const [option, commands] of only) {
|
|
444
|
-
if (values[option] !== undefined && !commands.includes(command)) throw new UsageError(`--${option} works only with ${commands.join(", ")}`);
|
|
445
|
-
}
|
|
446
|
-
const operation =
|
|
447
|
-
command === "image" || command === "coverage"
|
|
448
|
-
? programOperation(command, args, context, space)
|
|
449
|
-
: command === "apply"
|
|
450
|
-
? applyOperation(args, context, space)
|
|
451
|
-
: command === "operand"
|
|
452
|
-
? operandOperation(args, values, context, space)
|
|
453
|
-
: command === "patch" || command === "macro" || command === "use"
|
|
454
|
-
? sourceOperation(command, args, values, context, space)
|
|
455
|
-
: writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
456
|
-
return withWrite(operation);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
type ReadValues = {
|
|
460
|
-
analyzer?: boolean | undefined;
|
|
461
|
-
kind?: string | undefined;
|
|
462
|
-
type?: string | undefined;
|
|
463
|
-
name?: string | undefined;
|
|
464
|
-
limit?: string | undefined;
|
|
465
|
-
before?: string | undefined;
|
|
466
|
-
out?: string | undefined;
|
|
467
|
-
switch?: string | undefined;
|
|
468
|
-
macro?: string | undefined;
|
|
469
|
-
};
|
|
470
|
-
|
|
471
200
|
/** The lookups of a listing: the program and the coverage in the knowledge, the knowledge of the space and the platform names of the C64. */
|
|
472
201
|
function listingSource(db: DatabaseSync | undefined, space: string): ListingSource {
|
|
473
202
|
const image = requireProgram(db, space);
|
|
@@ -479,7 +208,7 @@ function listingSource(db: DatabaseSync | undefined, space: string): ListingSour
|
|
|
479
208
|
// A user or LLM name, else the platform name; an analyzer name in symbols (knowledge from an older version) gives way to it.
|
|
480
209
|
nameOf: (at) => {
|
|
481
210
|
const own = symbolAt(db, at, space);
|
|
482
|
-
return own !== undefined && (own.origin
|
|
211
|
+
return own !== undefined && isSemantic(own.origin) ? own.name : (platform.get(at) ?? own?.name);
|
|
483
212
|
},
|
|
484
213
|
secondNames: (at) => analyzerNamesAt(db, at, space).map((row) => row.name),
|
|
485
214
|
regions: listRegions(db, undefined, undefined, undefined, space),
|
|
@@ -497,148 +226,12 @@ function listingSource(db: DatabaseSync | undefined, space: string): ListingSour
|
|
|
497
226
|
};
|
|
498
227
|
}
|
|
499
228
|
|
|
500
|
-
/** The answer of one read command. */
|
|
501
|
-
function readAnswer(db: DatabaseSync | undefined, command: string, args: string[], values: ReadValues, range: { start: number; end: number }, origins: Origin[] | undefined, space: string): Record<string, unknown> {
|
|
502
|
-
switch (command) {
|
|
503
|
-
case "listing": {
|
|
504
|
-
if (args.length !== 2) throw new UsageError("listing needs <start> <end>");
|
|
505
|
-
const start = address(args[0], "start");
|
|
506
|
-
const end = address(args[1], "end");
|
|
507
|
-
if (end < start) throw new UsageError("the end must not be before the start");
|
|
508
|
-
const source = listingSource(db, space);
|
|
509
|
-
const lines = listing(source, start, end);
|
|
510
|
-
if (values.out !== undefined) {
|
|
511
|
-
writeProjectFile(values.out, Buffer.from(listingText(lines), "utf8"), { what: "--out" });
|
|
512
|
-
return { out: values.out, lines: lines.length };
|
|
513
|
-
}
|
|
514
|
-
return { lines: lines.map((row) => ({ ...row, address: formatC64Address(row.address) })) };
|
|
515
|
-
}
|
|
516
|
-
case "at": {
|
|
517
|
-
const known = at(db, address(args[0]), space);
|
|
518
|
-
return {
|
|
519
|
-
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
520
|
-
address: formatC64Address(known.address),
|
|
521
|
-
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
522
|
-
...(known.analyzerNames.length === 0 ? {} : { analyzerNames: known.analyzerNames.map(symbol) }),
|
|
523
|
-
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
524
|
-
comments: known.comments.map(comment),
|
|
525
|
-
referencesFrom: known.referencesFrom.map(reference),
|
|
526
|
-
referencesTo: known.referencesTo.map(reference),
|
|
527
|
-
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
528
|
-
...(known.patch === undefined ? {} : { patch: patch(known.patch) }),
|
|
529
|
-
...(known.macroUse === undefined ? {} : { macroUse: macroUse(known.macroUse) }),
|
|
530
|
-
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
531
|
-
revision: known.revision,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
case "search": {
|
|
535
|
-
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
536
|
-
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
537
|
-
return { symbols: found.symbols.map(symbol), analyzerNames: found.analyzerNames.map(symbol), comments: found.comments.map(comment) };
|
|
538
|
-
}
|
|
539
|
-
case "symbols": {
|
|
540
|
-
const kind = values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind");
|
|
541
|
-
if (values.analyzer) return { analyzerNames: listAnalyzerNames(db, range, kind, origins, space).map(symbol) };
|
|
542
|
-
return { symbols: listSymbols(db, range, kind, origins, space).map(symbol) };
|
|
543
|
-
}
|
|
544
|
-
case "regions":
|
|
545
|
-
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
546
|
-
case "comments":
|
|
547
|
-
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
548
|
-
case "references":
|
|
549
|
-
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
550
|
-
case "program": {
|
|
551
|
-
const image = programImage(db, space);
|
|
552
|
-
const ranges = db === undefined ? [] : (db.prepare("SELECT kind, count(*) AS count FROM current_coverage WHERE space = ? GROUP BY kind ORDER BY kind").all(space) as Array<{ kind: string; count: number }>);
|
|
553
|
-
return {
|
|
554
|
-
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
555
|
-
image:
|
|
556
|
-
image === undefined
|
|
557
|
-
? null
|
|
558
|
-
: {
|
|
559
|
-
segments: image.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })),
|
|
560
|
-
bytes: image.segments.reduce((sum, segment) => sum + segment.end - segment.start + 1, 0),
|
|
561
|
-
...(image.entry === undefined ? {} : { entry: formatC64Address(image.entry) }),
|
|
562
|
-
state: image.state !== undefined,
|
|
563
|
-
stages: image.stages?.length ?? 0,
|
|
564
|
-
},
|
|
565
|
-
coverage: { maps: [...new Set(coverageRuns(db).filter((run) => run.space === space).map((run) => run.name))], ranges: Object.fromEntries(ranges.map((row) => [row.kind, row.count])) },
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
case "export": {
|
|
569
|
-
const exported = {
|
|
570
|
-
revision: currentRevision(db),
|
|
571
|
-
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
572
|
-
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
573
|
-
analyzerNames: listAnalyzerNames(db, range, undefined, origins, space).map(symbol),
|
|
574
|
-
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
575
|
-
comments: listComments(db, range, origins, space).map(comment),
|
|
576
|
-
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
577
|
-
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
578
|
-
modules: listModules(db, range, origins, space).map(module),
|
|
579
|
-
operands: listOperands(db, range, origins, space).map(operand),
|
|
580
|
-
patches: listPatches(db, range, undefined, origins, space).map(patch),
|
|
581
|
-
macros: listMacros(db, origins, space).map(macro),
|
|
582
|
-
macroUses: listMacroUses(db, range, undefined, origins, space).map(macroUse),
|
|
583
|
-
};
|
|
584
|
-
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
585
|
-
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
586
|
-
return {
|
|
587
|
-
out: values.out,
|
|
588
|
-
bytes: bytes.length,
|
|
589
|
-
counts: { symbols: exported.symbols.length, analyzerNames: exported.analyzerNames.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length, operands: exported.operands.length, patches: exported.patches.length, macros: exported.macros.length, macroUses: exported.macroUses.length },
|
|
590
|
-
revision: exported.revision,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
case "suppressions": {
|
|
594
|
-
const rows = listSuppressions(db, range, space);
|
|
595
|
-
const reasons = reasonsOf(db, rows);
|
|
596
|
-
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
597
|
-
}
|
|
598
|
-
case "modules":
|
|
599
|
-
return { modules: listModules(db, range, origins, space).map(module) };
|
|
600
|
-
case "operands":
|
|
601
|
-
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
602
|
-
case "patches":
|
|
603
|
-
return { patches: listPatches(db, range, values.switch, origins, space).map(patch) };
|
|
604
|
-
case "switches": {
|
|
605
|
-
const switches = new Map<string, PatchRow[]>();
|
|
606
|
-
for (const row of listPatches(db, undefined, undefined, undefined, space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
|
|
607
|
-
return {
|
|
608
|
-
switches: [...switches].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, rows]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) })),
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
case "macros":
|
|
612
|
-
return { macros: listMacros(db, origins, space).map(macro) };
|
|
613
|
-
case "uses":
|
|
614
|
-
return { uses: listMacroUses(db, range, values.macro, origins, space).map(macroUse) };
|
|
615
|
-
case "spaces":
|
|
616
|
-
return { spaces: listSpaces(db) };
|
|
617
|
-
case "history":
|
|
618
|
-
// Each row with the reason and the evidence of the revision that wrote it.
|
|
619
|
-
return {
|
|
620
|
-
history: (values.macro !== undefined ? historyOfMacro(db, values.macro, space) : values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
621
|
-
};
|
|
622
|
-
case "revisions": {
|
|
623
|
-
const options: { limit?: number; before?: string } = {};
|
|
624
|
-
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
625
|
-
if (values.before !== undefined) options.before = values.before;
|
|
626
|
-
return { revisions: revisions(db, options) };
|
|
627
|
-
}
|
|
628
|
-
default: {
|
|
629
|
-
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
630
|
-
const changes = revision(db, args[0]);
|
|
631
|
-
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
229
|
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
637
230
|
function originFilter(value: string): Origin[] {
|
|
638
231
|
const origins = new Set<Origin>();
|
|
639
232
|
for (const part of value.split(",")) {
|
|
640
|
-
if (part === "semantic")
|
|
641
|
-
else if (part === "analyzer")
|
|
233
|
+
if (part === "semantic") for (const origin of SEMANTIC_ORIGINS) origins.add(origin);
|
|
234
|
+
else if (part === "analyzer") for (const origin of ANALYZERS) origins.add(origin);
|
|
642
235
|
else origins.add(oneOf(ORIGINS, part, "--origin"));
|
|
643
236
|
}
|
|
644
237
|
return [...origins];
|
|
@@ -684,59 +277,16 @@ function evidenceOf(value: string, space: string): Evidence {
|
|
|
684
277
|
return { kind, ref, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
685
278
|
}
|
|
686
279
|
|
|
687
|
-
/** An image.json of c64-unpacker capture or a PRG as a write of the program. */
|
|
280
|
+
/** An image.json of c64-unpacker capture or a PRG as a write of the program, with the entry state of the capture. */
|
|
688
281
|
function imageRequest(path: string, space: string): ImageRequest {
|
|
689
|
-
|
|
690
|
-
try {
|
|
691
|
-
image = readImage(path);
|
|
692
|
-
} catch (error) {
|
|
693
|
-
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
282
|
+
const image = readImage(path);
|
|
696
283
|
const request: ImageRequest = { space, segments: image.segments, byteAt: image.byteAt };
|
|
697
284
|
if (image.entry !== undefined) request.entry = image.entry;
|
|
698
285
|
if (image.stages !== undefined) request.stages = image.stages;
|
|
699
|
-
if (image.
|
|
700
|
-
let bytes: Buffer | undefined;
|
|
701
|
-
try {
|
|
702
|
-
bytes = readProjectFile(image.statePath).bytes;
|
|
703
|
-
} catch (error) {
|
|
704
|
-
if (!(error instanceof WireFailure && error.code === "not-found")) throw error;
|
|
705
|
-
}
|
|
706
|
-
if (bytes !== undefined) {
|
|
707
|
-
let state: unknown;
|
|
708
|
-
try {
|
|
709
|
-
state = JSON.parse(bytes.toString("utf8"));
|
|
710
|
-
} catch (error) {
|
|
711
|
-
throw new UsageError(`${image.statePath} is not JSON: ${(error as Error).message}`);
|
|
712
|
-
}
|
|
713
|
-
if (typeof state !== "object" || state === null || Array.isArray(state)) throw new UsageError(`${image.statePath} is not an entry state of c64-unpacker capture`);
|
|
714
|
-
// The comparison with a clean reset only tells the capture what it found.
|
|
715
|
-
const { differsFromReset: _differs, ...kept } = state as Record<string, unknown>;
|
|
716
|
-
request.state = kept;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
286
|
+
if (image.state !== undefined) request.state = image.state.raw;
|
|
719
287
|
return request;
|
|
720
288
|
}
|
|
721
289
|
|
|
722
|
-
/** image and coverage: the program and what the runs did go into the knowledge, so no project file is made again. */
|
|
723
|
-
function programOperation(command: "image" | "coverage", args: string[], context: WriteContext, space: string): WriteOperation {
|
|
724
|
-
if (command === "image") {
|
|
725
|
-
if (args.length !== 1) throw new UsageError("image needs one image.json or PRG");
|
|
726
|
-
const request = imageRequest(args[0]!, space);
|
|
727
|
-
return (db) => {
|
|
728
|
-
const { revision, current } = setImage(db, context, request);
|
|
729
|
-
return { revision, ...spaceOf(current), bytes: current.bytes, rowsAdded: current.rowsAdded, rowsClosed: current.rowsClosed, entryChanged: current.entry };
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
if (args.length === 0) throw new UsageError("coverage needs one or more coverage maps");
|
|
733
|
-
const maps = args.map((path) => ({ bytes: readProjectFile(path).bytes, name: knowledgePath(path, "coverage <map>") }));
|
|
734
|
-
return (db) => {
|
|
735
|
-
const { revision, current } = addCoverage(db, context, { space, maps });
|
|
736
|
-
return { revision, ...spaceOf(current), imported: current.imported, known: current.known, ranges: current.ranges };
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
290
|
/**
|
|
741
291
|
* The program in the knowledge that operand, patch and macro use writes
|
|
742
292
|
* are checked against, with the platform names. A byte outside
|
|
@@ -757,99 +307,6 @@ function offsetOf(value: unknown, what: string): number {
|
|
|
757
307
|
return match[1] === "-" ? -size : size;
|
|
758
308
|
}
|
|
759
309
|
|
|
760
|
-
/** operand <address> with one of --lo, --hi, --value or --expr. */
|
|
761
|
-
function operandOperation(args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
762
|
-
const given = (["lo", "hi", "value", "expr"] as const).filter((option) => values[option] !== undefined);
|
|
763
|
-
if (given.length !== 1) throw new UsageError("operand needs exactly one of --lo, --hi, --value or --expr");
|
|
764
|
-
const select: OperandSelect = given[0]!;
|
|
765
|
-
const request: { space: string; address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string } = { space, address: address(args[0]), select };
|
|
766
|
-
if (select === "expr") {
|
|
767
|
-
if (values.offset !== undefined) throw new UsageError("--offset works with --lo, --hi and --value, not with --expr");
|
|
768
|
-
request.expression = values.expr as string;
|
|
769
|
-
} else {
|
|
770
|
-
request.symbol = values[select] as string;
|
|
771
|
-
if (values.offset !== undefined) request.offset = offsetOf(values.offset, "--offset");
|
|
772
|
-
}
|
|
773
|
-
return (db) => {
|
|
774
|
-
const result = setOperand(db, { ...context, image: operandImage(db, space) }, request);
|
|
775
|
-
return { revision: result.revision, operand: operand(result.current), ...(result.previous === undefined ? {} : { previous: operand(result.previous) }) };
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/** A comma list of params or args; an empty text is an empty list. */
|
|
780
|
-
const commaList = (value: string) => (value.trim() === "" ? [] : value.split(",").map((item) => item.trim()));
|
|
781
|
-
|
|
782
|
-
/** The text of an option, or of the project file that a --*-file option names; exactly one of them. */
|
|
783
|
-
function textOrFile(values: Record<string, unknown>, option: string, what: string): string | undefined {
|
|
784
|
-
const inline = values[option] as string | undefined;
|
|
785
|
-
const file = values[`${option}-file`] as string | undefined;
|
|
786
|
-
if (inline !== undefined && file !== undefined) throw new UsageError(`give --${option} or --${option}-file, not both`);
|
|
787
|
-
if (file !== undefined) return readProjectFile(file).bytes.toString("utf8");
|
|
788
|
-
if (inline === undefined) return undefined;
|
|
789
|
-
if (inline === "") throw new UsageError(`${what} is empty`);
|
|
790
|
-
return inline;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/** patch, macro and use: the writes of the generated source. */
|
|
794
|
-
function sourceOperation(command: "patch" | "macro" | "use", args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
795
|
-
const comment = values.comment as string | undefined;
|
|
796
|
-
switch (command) {
|
|
797
|
-
case "patch": {
|
|
798
|
-
if (args[2] === undefined) throw new UsageError("the switch name is missing");
|
|
799
|
-
const code = textOrFile(values, "code", "--code");
|
|
800
|
-
if ((code === undefined) === (values.nop !== true)) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
801
|
-
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), switch: args[2], code: code ?? "", ...(comment === undefined ? {} : { comment }) };
|
|
802
|
-
return (db) => {
|
|
803
|
-
const result = setPatch(db, { ...context, image: operandImage(db, space) }, request);
|
|
804
|
-
return { revision: result.revision, patch: patch(result.current), ...(result.previous === undefined ? {} : { previous: patch(result.previous) }) };
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
case "macro": {
|
|
808
|
-
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
809
|
-
const body = textOrFile(values, "body", "--body");
|
|
810
|
-
if (body === undefined) throw new UsageError("macro needs --body or --body-file");
|
|
811
|
-
const request = { space, name: args[0], params: commaList((values.params as string | undefined) ?? ""), body, ...(comment === undefined ? {} : { comment }) };
|
|
812
|
-
return (db) => {
|
|
813
|
-
const result = setMacro(db, context, request);
|
|
814
|
-
return { revision: result.revision, macro: macro(result.current), ...(result.previous === undefined ? {} : { previous: macro(result.previous) }) };
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
case "use": {
|
|
818
|
-
if (args[2] === undefined) throw new UsageError("the macro name is missing");
|
|
819
|
-
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), macro: args[2], args: commaList((values.args as string | undefined) ?? "") };
|
|
820
|
-
return (db) => {
|
|
821
|
-
const result = setMacroUse(db, { ...context, image: operandImage(db, space) }, request);
|
|
822
|
-
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
829
|
-
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
830
|
-
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
831
|
-
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
832
|
-
let parsed: unknown;
|
|
833
|
-
try {
|
|
834
|
-
parsed = JSON.parse(text);
|
|
835
|
-
} catch (error) {
|
|
836
|
-
throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
|
|
837
|
-
}
|
|
838
|
-
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
839
|
-
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
840
|
-
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
841
|
-
return (db) => {
|
|
842
|
-
const applied = applyBatch(db, checked ? { ...context, image: operandImage(db, space) } : context, operations);
|
|
843
|
-
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write.
|
|
844
|
-
const replaced = applied.steps.flatMap((step, index) =>
|
|
845
|
-
step.previous === undefined
|
|
846
|
-
? []
|
|
847
|
-
: [{ write: index + 1, op: operations[index]!.op, previous: shownPrevious(step.previous) }],
|
|
848
|
-
);
|
|
849
|
-
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
310
|
/** A row that a write of a batch replaced, in its output shape. */
|
|
854
311
|
function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
|
|
855
312
|
if (Array.isArray(row)) return row.map(region);
|
|
@@ -891,64 +348,585 @@ function batchJson(write: BatchOperation): Record<string, unknown> {
|
|
|
891
348
|
return out;
|
|
892
349
|
}
|
|
893
350
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Commands
|
|
353
|
+
|
|
354
|
+
type WriteOperation = (db: DatabaseSync) => unknown;
|
|
355
|
+
|
|
356
|
+
/** The address and revision subjects of the commands. */
|
|
357
|
+
const ADDRESS: SubjectSpec = { name: "<address>", help: "an address or a name" };
|
|
358
|
+
const RANGE: SubjectSpec = { name: "<range>", help: "<start>-<end>, each an address or a name, for example '$1000-$10ff'" };
|
|
359
|
+
const START: SubjectSpec = { name: "<start>", help: "the start address of the row" };
|
|
360
|
+
const REVISION: SubjectSpec = { name: "<revision>", help: "a revision id, or a start of the id that only one revision has; 0 is the knowledge before every revision" };
|
|
361
|
+
|
|
362
|
+
const SPACE = { space: OPTIONS.space };
|
|
363
|
+
const FILTER = { ...SPACE, range: OPTIONS.range, origin: OPTIONS.origin };
|
|
364
|
+
const SYMBOL_KIND: OptionSpec = { kind: "choice", value: "<kind>", choices: SYMBOL_KINDS, help: `one of ${SYMBOL_KINDS.join(", ")}` };
|
|
365
|
+
const REFERENCE_KIND: OptionSpec = { kind: "choice", value: "<kind>", choices: REFERENCE_KINDS, help: `one of ${REFERENCE_KINDS.join(", ")}` };
|
|
366
|
+
const WRITE: Record<string, OptionSpec> = {
|
|
367
|
+
...SPACE,
|
|
368
|
+
by: { kind: "choice", value: "<user|llm>", choices: SEMANTIC_ORIGINS, default: "llm", help: "who decided: user, or llm (the default)" },
|
|
369
|
+
reason: OPTIONS.reason,
|
|
370
|
+
evidence: OPTIONS.evidence,
|
|
371
|
+
"expect-revision": OPTIONS["expect-revision"],
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** The rows from --range, or every address. */
|
|
375
|
+
const rangeOf = (call: Call) => call.range("range") ?? { start: 0x0000, end: 0xffff };
|
|
376
|
+
const originsOf = (call: Call) => {
|
|
377
|
+
const value = call.string("origin");
|
|
378
|
+
return value === undefined ? undefined : originFilter(value);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/** A read command: it opens the knowledge for reading, and its answer shows the conflicts that a git merge left. */
|
|
382
|
+
function readCommand(spec: Omit<CommandSpec, "run">, answer: (db: DatabaseSync | undefined, call: Call) => Record<string, unknown>): CommandSpec {
|
|
383
|
+
return {
|
|
384
|
+
...spec,
|
|
385
|
+
options: { ...SPACE, ...spec.options },
|
|
386
|
+
run: (call) => {
|
|
387
|
+
const db = openForRead();
|
|
388
|
+
try {
|
|
389
|
+
const result = answer(db, call);
|
|
390
|
+
const found = conflicts(db);
|
|
391
|
+
return found.length === 0 ? result : { ...result, conflicts: found.map(showConflict) };
|
|
392
|
+
} finally {
|
|
393
|
+
db?.close();
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
};
|
|
933
397
|
}
|
|
934
398
|
|
|
399
|
+
/** A write command: every value is checked before the knowledge opens, so a refused write makes no knowledge folder. */
|
|
400
|
+
function writeCommand(spec: Omit<CommandSpec, "run">, prepare: (call: Call) => WriteOperation): CommandSpec {
|
|
401
|
+
return {
|
|
402
|
+
...spec,
|
|
403
|
+
notes: [spec.notes, "A write is one revision. It makes .c64-re-tools/knowledge/ when it is not there."].filter((note) => note !== undefined).join("\n"),
|
|
404
|
+
options: { ...spec.options, ...WRITE },
|
|
405
|
+
run: (call) => withWrite(prepare(call)),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The write context of --by, --reason, --expect-revision and --evidence. */
|
|
410
|
+
function contextOf(call: Call): WriteContext {
|
|
411
|
+
const context: WriteContext = { origin: call.string("by") as WriteContext["origin"] };
|
|
412
|
+
const reason = call.string("reason");
|
|
413
|
+
if (reason !== undefined) context.description = reason;
|
|
414
|
+
const expected = call.string("expect-revision");
|
|
415
|
+
if (expected !== undefined) context.expectedRevision = expected;
|
|
416
|
+
const evidence = call.strings("evidence");
|
|
417
|
+
if (evidence.length > 0) context.evidence = evidence.map((value) => evidenceOf(value, call.space));
|
|
418
|
+
return context;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A write that reports the revision and the row it wrote, and the row it replaced. */
|
|
422
|
+
function shown<T, U>(result: { revision: string | null; current: T; previous?: T | undefined }, name: string, show: (row: T) => U) {
|
|
423
|
+
return { revision: result.revision, [name]: show(result.current), ...(result.previous === undefined ? {} : { previous: show(result.previous) }) };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The text of an option, or of the project file that a --*-file option names; exactly one of them. */
|
|
427
|
+
function textOrFile(call: Call, option: string): string | undefined {
|
|
428
|
+
const inline = call.string(option);
|
|
429
|
+
const file = call.string(`${option}-file`);
|
|
430
|
+
if (inline !== undefined && file !== undefined) throw new UsageError(`give --${option} or --${option}-file, not both`);
|
|
431
|
+
if (file !== undefined) return readProjectFile(file).bytes.toString("utf8");
|
|
432
|
+
return inline;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** A comma list of params or args; an empty text is an empty list. */
|
|
436
|
+
const commaList = (value: string) => (value.trim() === "" ? [] : value.split(",").map((item) => item.trim()));
|
|
437
|
+
|
|
438
|
+
const READ_COMMANDS: CommandSpec[] = [
|
|
439
|
+
readCommand({ name: "at", summary: "Everything known at one address.", subjects: [ADDRESS] }, (db, call) => {
|
|
440
|
+
const known = at(db, call.parseAddress(call.subject(0), "<address>"), call.space);
|
|
441
|
+
return {
|
|
442
|
+
...(call.space === DEFAULT_SPACE ? {} : { space: call.space }),
|
|
443
|
+
address: formatC64Address(known.address),
|
|
444
|
+
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
445
|
+
...(known.analyzerNames.length === 0 ? {} : { analyzerNames: known.analyzerNames.map(symbol) }),
|
|
446
|
+
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
447
|
+
comments: known.comments.map(comment),
|
|
448
|
+
referencesFrom: known.referencesFrom.map(reference),
|
|
449
|
+
referencesTo: known.referencesTo.map(reference),
|
|
450
|
+
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
451
|
+
...(known.patch === undefined ? {} : { patch: patch(known.patch) }),
|
|
452
|
+
...(known.macroUse === undefined ? {} : { macroUse: macroUse(known.macroUse) }),
|
|
453
|
+
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
454
|
+
revision: known.revision,
|
|
455
|
+
};
|
|
456
|
+
}),
|
|
457
|
+
readCommand(
|
|
458
|
+
{
|
|
459
|
+
name: "search",
|
|
460
|
+
summary: "Symbol names, analyzer names and comment texts that hold the text.",
|
|
461
|
+
subjects: [{ name: "<text>", help: "the text to find" }],
|
|
462
|
+
options: { origin: OPTIONS.origin, limit: { kind: "count", value: "<n>", min: 1, max: 10_000, default: 50, help: "at most n rows of each kind (default 50)" } },
|
|
463
|
+
},
|
|
464
|
+
(db, call) => {
|
|
465
|
+
const found = search(db, call.subject(0), call.count("limit")!, originsOf(call), call.space);
|
|
466
|
+
return { symbols: found.symbols.map(symbol), analyzerNames: found.analyzerNames.map(symbol), comments: found.comments.map(comment) };
|
|
467
|
+
},
|
|
468
|
+
),
|
|
469
|
+
readCommand(
|
|
470
|
+
{ name: "symbols", summary: "The symbols, or with --analyzer the second names that DXA, Ghidra and the idioms gave.", options: { ...FILTER, kind: SYMBOL_KIND, analyzer: { kind: "flag", help: "the analyzer names in place of the symbols" } } },
|
|
471
|
+
(db, call) => {
|
|
472
|
+
const kind = call.string("kind") as (typeof SYMBOL_KINDS)[number] | undefined;
|
|
473
|
+
if (call.flag("analyzer")) return { analyzerNames: listAnalyzerNames(db, rangeOf(call), kind, originsOf(call), call.space).map(symbol) };
|
|
474
|
+
return { symbols: listSymbols(db, rangeOf(call), kind, originsOf(call), call.space).map(symbol) };
|
|
475
|
+
},
|
|
476
|
+
),
|
|
477
|
+
readCommand({ name: "regions", summary: "The regions: code, data, tables and the other types.", options: { ...FILTER, type: { kind: "choice", value: "<type>", choices: REGION_TYPES, help: `one of ${REGION_TYPES.join(", ")}` } } }, (db, call) => ({
|
|
478
|
+
regions: listRegions(db, rangeOf(call), call.string("type") as (typeof REGION_TYPES)[number] | undefined, originsOf(call), call.space).map(region),
|
|
479
|
+
})),
|
|
480
|
+
readCommand({ name: "comments", summary: "The comments.", options: FILTER }, (db, call) => ({ comments: listComments(db, rangeOf(call), originsOf(call), call.space).map(comment) })),
|
|
481
|
+
readCommand({ name: "references", summary: "The references: calls, jumps, reads, writes and pointers.", options: { ...FILTER, kind: REFERENCE_KIND } }, (db, call) => ({
|
|
482
|
+
references: listReferences(db, rangeOf(call), call.string("kind") as (typeof REFERENCE_KINDS)[number] | undefined, originsOf(call), call.space).map(reference),
|
|
483
|
+
})),
|
|
484
|
+
readCommand({ name: "suppressions", summary: "The places that removals keep free of analyzer facts, with the reason of each.", options: { range: OPTIONS.range } }, (db, call) => {
|
|
485
|
+
const rows = listSuppressions(db, rangeOf(call), call.space);
|
|
486
|
+
const reasons = reasonsOf(db, rows);
|
|
487
|
+
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
488
|
+
}),
|
|
489
|
+
readCommand({ name: "modules", summary: "Where the files of the generated source start.", options: FILTER }, (db, call) => ({ modules: listModules(db, rangeOf(call), originsOf(call), call.space).map(module) })),
|
|
490
|
+
readCommand({ name: "operands", summary: "The operand expressions of instructions.", options: FILTER }, (db, call) => ({ operands: listOperands(db, rangeOf(call), originsOf(call), call.space).map(operand) })),
|
|
491
|
+
readCommand({ name: "patches", summary: "The code that a switch puts in.", options: { ...FILTER, switch: { kind: "text", value: "<switch>", help: "only the patches of this switch" } } }, (db, call) => ({
|
|
492
|
+
patches: listPatches(db, rangeOf(call), call.string("switch"), originsOf(call), call.space).map(patch),
|
|
493
|
+
})),
|
|
494
|
+
readCommand({ name: "switches", summary: "Each switch with the ranges of its patches." }, (db, call) => {
|
|
495
|
+
const switches = new Map<string, PatchRow[]>();
|
|
496
|
+
for (const row of listPatches(db, undefined, undefined, undefined, call.space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
|
|
497
|
+
return {
|
|
498
|
+
switches: [...switches].sort(([a], [b]) => (a < b ? -1 : 1)).map(([name, rows]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) })),
|
|
499
|
+
};
|
|
500
|
+
}),
|
|
501
|
+
readCommand({ name: "macros", summary: "The macros of the generated source.", options: { origin: OPTIONS.origin } }, (db, call) => ({ macros: listMacros(db, originsOf(call), call.space).map(macro) })),
|
|
502
|
+
readCommand({ name: "uses", summary: "The instructions written as a macro.", options: { ...FILTER, macro: { kind: "text", value: "<name>", help: "only the uses of this macro" } } }, (db, call) => ({
|
|
503
|
+
uses: listMacroUses(db, rangeOf(call), call.string("macro"), originsOf(call), call.space).map(macroUse),
|
|
504
|
+
})),
|
|
505
|
+
readCommand({ name: "spaces", summary: "The address spaces that hold knowledge." }, (db) => ({ spaces: listSpaces(db) })),
|
|
506
|
+
readCommand({ name: "program", summary: "The program in the knowledge: its segments and entry, and the coverage maps that coverage imported, with the ranges." }, (db, call) => {
|
|
507
|
+
const image = programImage(db, call.space);
|
|
508
|
+
const ranges = db === undefined ? [] : (db.prepare("SELECT kind, count(*) AS count FROM current_coverage WHERE space = ? GROUP BY kind ORDER BY kind").all(call.space) as Array<{ kind: string; count: number }>);
|
|
509
|
+
return {
|
|
510
|
+
...(call.space === DEFAULT_SPACE ? {} : { space: call.space }),
|
|
511
|
+
image:
|
|
512
|
+
image === undefined
|
|
513
|
+
? null
|
|
514
|
+
: {
|
|
515
|
+
segments: image.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })),
|
|
516
|
+
bytes: image.segments.reduce((sum, segment) => sum + segment.end - segment.start + 1, 0),
|
|
517
|
+
...(image.entry === undefined ? {} : { entry: formatC64Address(image.entry) }),
|
|
518
|
+
state: image.state !== undefined,
|
|
519
|
+
stages: image.stages?.length ?? 0,
|
|
520
|
+
},
|
|
521
|
+
coverage: { maps: [...new Set(coverageRuns(db).filter((run) => run.space === call.space).map((run) => run.name))], ranges: Object.fromEntries(ranges.map((row) => [row.kind, row.count])) },
|
|
522
|
+
};
|
|
523
|
+
}),
|
|
524
|
+
readCommand(
|
|
525
|
+
{
|
|
526
|
+
name: "history",
|
|
527
|
+
summary: "The rows at an address, of a name or of a macro, through all revisions, each with the reason and the evidence of its revision.",
|
|
528
|
+
subjects: [{ ...ADDRESS, optional: true }],
|
|
529
|
+
options: { name: { kind: "text", value: "<name>", help: "the history of a name in place of an address" }, macro: { kind: "text", value: "<name>", help: "the history of a macro in place of an address" } },
|
|
530
|
+
notes: "Give exactly one of <address>, --name and --macro.",
|
|
531
|
+
},
|
|
532
|
+
(db, call) => {
|
|
533
|
+
const name = call.string("name");
|
|
534
|
+
const macroName = call.string("macro");
|
|
535
|
+
if ([call.subjects.length > 0, name !== undefined, macroName !== undefined].filter(Boolean).length !== 1) throw new UsageError("history needs exactly one of <address>, --name and --macro");
|
|
536
|
+
const entries = macroName !== undefined ? historyOfMacro(db, macroName, call.space) : name !== undefined ? historyNamed(db, name, call.space) : historyAt(db, call.parseAddress(call.subject(0), "<address>"), call.space);
|
|
537
|
+
return { history: entries.map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })) };
|
|
538
|
+
},
|
|
539
|
+
),
|
|
540
|
+
readCommand(
|
|
541
|
+
{ name: "revisions", summary: "The revisions, newest first.", options: { limit: { kind: "count", value: "<n>", min: 1, max: 100_000, help: "at most n revisions" }, before: { kind: "text", value: "<revision>", help: "only the revisions before this one" } } },
|
|
542
|
+
(db, call) => {
|
|
543
|
+
const options: { limit?: number; before?: string } = {};
|
|
544
|
+
const limit = call.count("limit");
|
|
545
|
+
if (limit !== undefined) options.limit = limit;
|
|
546
|
+
const before = call.string("before");
|
|
547
|
+
if (before !== undefined) options.before = before;
|
|
548
|
+
return { revisions: revisions(db, options) };
|
|
549
|
+
},
|
|
550
|
+
),
|
|
551
|
+
readCommand({ name: "revision", summary: "The rows that one revision added and closed.", subjects: [REVISION] }, (db, call) => {
|
|
552
|
+
const changes = revision(db, call.subject(0));
|
|
553
|
+
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
554
|
+
}),
|
|
555
|
+
readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
|
|
556
|
+
const range = rangeOf(call);
|
|
557
|
+
const origins = originsOf(call);
|
|
558
|
+
const space = call.space;
|
|
559
|
+
const exported = {
|
|
560
|
+
revision: currentRevision(db),
|
|
561
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
562
|
+
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
563
|
+
analyzerNames: listAnalyzerNames(db, range, undefined, origins, space).map(symbol),
|
|
564
|
+
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
565
|
+
comments: listComments(db, range, origins, space).map(comment),
|
|
566
|
+
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
567
|
+
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
568
|
+
modules: listModules(db, range, origins, space).map(module),
|
|
569
|
+
operands: listOperands(db, range, origins, space).map(operand),
|
|
570
|
+
patches: listPatches(db, range, undefined, origins, space).map(patch),
|
|
571
|
+
macros: listMacros(db, origins, space).map(macro),
|
|
572
|
+
macroUses: listMacroUses(db, range, undefined, origins, space).map(macroUse),
|
|
573
|
+
};
|
|
574
|
+
const counts = Object.fromEntries(Object.entries(exported).flatMap(([key, value]) => (Array.isArray(value) ? [[key, value.length]] : [])));
|
|
575
|
+
return { counts, revision: exported.revision, ...writeReported(call.string("out")!, `${JSON.stringify(exported, null, 2)}\n`, { what: "--out" }) };
|
|
576
|
+
}),
|
|
577
|
+
readCommand(
|
|
578
|
+
{
|
|
579
|
+
name: "listing",
|
|
580
|
+
summary: "The program as ACME lines with names, operand expressions, comments, the access of each line in the coverage (x executed, r read, w written) and the references in and out.",
|
|
581
|
+
subjects: [RANGE],
|
|
582
|
+
options: { out: { ...OPTIONS.out, value: "<file>", help: "write the listing as text to this file in place of the lines in the result" } },
|
|
583
|
+
},
|
|
584
|
+
(db, call) => {
|
|
585
|
+
const { start, end } = call.parseRange(call.subject(0), "<range>");
|
|
586
|
+
const lines = listing(listingSource(db, call.space), start, end);
|
|
587
|
+
const out = call.string("out");
|
|
588
|
+
if (out !== undefined) return { lines: lines.length, ...writeReported(out, listingText(lines), { what: "--out" }) };
|
|
589
|
+
return { lines: lines.map((row) => ({ ...row, address: formatC64Address(row.address) })) };
|
|
590
|
+
},
|
|
591
|
+
),
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
const WRITE_COMMANDS: CommandSpec[] = [
|
|
595
|
+
writeCommand({ name: "rename", summary: "Gives an address a name.", subjects: [ADDRESS, { name: "<name>", help: "the new name: assembler label syntax" }], options: { kind: SYMBOL_KIND } }, (call) => {
|
|
596
|
+
const context = contextOf(call);
|
|
597
|
+
const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), name: call.subject(1), ...(call.string("kind") === undefined ? {} : { kind: call.string("kind") as (typeof SYMBOL_KINDS)[number] }) };
|
|
598
|
+
return (db) => shown(renameSymbol(db, context, request), "symbol", symbol);
|
|
599
|
+
}),
|
|
600
|
+
writeCommand({ name: "remove-symbol", summary: "Removes the name at an address.", subjects: [ADDRESS] }, (call) => {
|
|
601
|
+
const context = contextOf(call);
|
|
602
|
+
const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>") };
|
|
603
|
+
return (db) => ({ revision: removeSymbol(db, context, request).revision });
|
|
604
|
+
}),
|
|
605
|
+
writeCommand(
|
|
606
|
+
{
|
|
607
|
+
name: "classify",
|
|
608
|
+
summary: "Gives a range a region type.",
|
|
609
|
+
subjects: [RANGE, { name: "<type>", help: `one of ${REGION_TYPES.join(", ")}` }],
|
|
610
|
+
options: { element: { kind: "count", value: "<n>", min: 1, max: 0x10000, help: "the bytes of one element of a table: the length of the range is a multiple of it" } },
|
|
611
|
+
},
|
|
612
|
+
(call) => {
|
|
613
|
+
const context = contextOf(call);
|
|
614
|
+
const element = call.count("element");
|
|
615
|
+
const request = { space: call.space, ...call.parseRange(call.subject(0), "<range>"), type: oneOf(REGION_TYPES, call.subject(1), "<type>"), ...(element === undefined ? {} : { element }) };
|
|
616
|
+
return (db) => {
|
|
617
|
+
const result = classifyRegion(db, context, request);
|
|
618
|
+
return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
),
|
|
622
|
+
writeCommand({ name: "unclassify", summary: "Removes the region types of a range.", subjects: [RANGE] }, (call) => {
|
|
623
|
+
const context = contextOf(call);
|
|
624
|
+
const request = { space: call.space, ...call.parseRange(call.subject(0), "<range>") };
|
|
625
|
+
return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
|
|
626
|
+
}),
|
|
627
|
+
writeCommand(
|
|
628
|
+
{
|
|
629
|
+
name: "comment",
|
|
630
|
+
summary: "Writes a comment at an address: a line of its own above it, or at the end of its line.",
|
|
631
|
+
subjects: [ADDRESS, { name: "<placement>", help: `one of ${COMMENT_PLACEMENTS.join(", ")}` }, { name: "<text>", help: "the comment", list: true }],
|
|
632
|
+
},
|
|
633
|
+
(call) => {
|
|
634
|
+
const context = contextOf(call);
|
|
635
|
+
const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), placement: oneOf(COMMENT_PLACEMENTS, call.subject(1), "<placement>"), text: call.rest(2).join(" ") };
|
|
636
|
+
return (db) => {
|
|
637
|
+
const result = setComment(db, context, request);
|
|
638
|
+
return { revision: result.revision, comment: comment(result.current) };
|
|
639
|
+
};
|
|
640
|
+
},
|
|
641
|
+
),
|
|
642
|
+
writeCommand({ name: "uncomment", summary: "Removes a comment.", subjects: [ADDRESS, { name: "<placement>", help: `one of ${COMMENT_PLACEMENTS.join(", ")}` }] }, (call) => {
|
|
643
|
+
const context = contextOf(call);
|
|
644
|
+
const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), placement: oneOf(COMMENT_PLACEMENTS, call.subject(1), "<placement>") };
|
|
645
|
+
return (db) => ({ revision: removeComment(db, context, request).revision });
|
|
646
|
+
}),
|
|
647
|
+
...(["reference", "unreference"] as const).map((name) =>
|
|
648
|
+
writeCommand(
|
|
649
|
+
{
|
|
650
|
+
name,
|
|
651
|
+
summary: name === "reference" ? "Records that code at one address refers to another." : "Removes a reference.",
|
|
652
|
+
subjects: [{ name: "<from>", help: "the address of the instruction" }, { name: "<to>", help: "the address it refers to" }, { name: "<kind>", help: `one of ${REFERENCE_KINDS.join(", ")}` }],
|
|
653
|
+
},
|
|
654
|
+
(call) => {
|
|
655
|
+
const context = contextOf(call);
|
|
656
|
+
const request = { space: call.space, from: call.parseAddress(call.subject(0), "<from>"), to: call.parseAddress(call.subject(1), "<to>"), kind: oneOf(REFERENCE_KINDS, call.subject(2), "<kind>") };
|
|
657
|
+
return name === "reference"
|
|
658
|
+
? (db) => {
|
|
659
|
+
const result = addReference(db, context, request);
|
|
660
|
+
return { revision: result.revision, reference: reference(result.current) };
|
|
661
|
+
}
|
|
662
|
+
: (db) => ({ revision: removeReference(db, context, request).revision });
|
|
663
|
+
},
|
|
664
|
+
),
|
|
665
|
+
),
|
|
666
|
+
writeCommand(
|
|
667
|
+
{ name: "module", summary: "Records that a file of the generated source starts at an address.", subjects: [START, { name: "<file.asm>", help: "the source file" }], options: { title: { kind: "text", value: "<text>", help: "the title of the file" } } },
|
|
668
|
+
(call) => {
|
|
669
|
+
const context = contextOf(call);
|
|
670
|
+
const title = call.string("title");
|
|
671
|
+
const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>"), file: call.subject(1), ...(title === undefined ? {} : { title }) };
|
|
672
|
+
return (db) => {
|
|
673
|
+
const result = setModule(db, context, request);
|
|
674
|
+
return { revision: result.revision, module: module(result.current) };
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
),
|
|
678
|
+
writeCommand({ name: "unmodule", summary: "Removes the module that starts at an address.", subjects: [START] }, (call) => {
|
|
679
|
+
const context = contextOf(call);
|
|
680
|
+
const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>") };
|
|
681
|
+
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
682
|
+
}),
|
|
683
|
+
writeCommand(
|
|
684
|
+
{
|
|
685
|
+
name: "operand",
|
|
686
|
+
summary: "Writes the operand of the instruction at an address as an expression.",
|
|
687
|
+
subjects: [ADDRESS],
|
|
688
|
+
options: {
|
|
689
|
+
lo: { kind: "text", value: "<symbol>", help: "the low byte of symbol + offset" },
|
|
690
|
+
hi: { kind: "text", value: "<symbol>", help: "the high byte of symbol + offset" },
|
|
691
|
+
value: { kind: "text", value: "<symbol>", help: "the value symbol + offset" },
|
|
692
|
+
expr: { kind: "text", value: "<text>", help: "ACME expression text" },
|
|
693
|
+
offset: { kind: "text", value: "<n>", help: "with --lo, --hi or --value: a whole number such as 1, '$1000' or, as --offset=-1, a negative one" },
|
|
694
|
+
},
|
|
695
|
+
notes: "Give exactly one of --lo, --hi, --value and --expr. The write is refused when the expression does not give the operand in the program of the knowledge.",
|
|
696
|
+
},
|
|
697
|
+
(call) => {
|
|
698
|
+
const context = contextOf(call);
|
|
699
|
+
const given = (["lo", "hi", "value", "expr"] as const).filter((option) => call.string(option) !== undefined);
|
|
700
|
+
if (given.length !== 1) throw new UsageError("operand needs exactly one of --lo, --hi, --value or --expr");
|
|
701
|
+
const select: OperandSelect = given[0]!;
|
|
702
|
+
const offset = call.string("offset");
|
|
703
|
+
const request: { space: string; address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string } = { space: call.space, address: call.parseAddress(call.subject(0), "<address>"), select };
|
|
704
|
+
if (select === "expr") {
|
|
705
|
+
if (offset !== undefined) throw new UsageError("--offset works with --lo, --hi and --value, not with --expr");
|
|
706
|
+
request.expression = call.string("expr")!;
|
|
707
|
+
} else {
|
|
708
|
+
request.symbol = call.string(select)!;
|
|
709
|
+
if (offset !== undefined) request.offset = offsetOf(offset, "--offset");
|
|
710
|
+
}
|
|
711
|
+
return (db) => shown(setOperand(db, { ...context, image: operandImage(db, call.space) }, request), "operand", operand);
|
|
712
|
+
},
|
|
713
|
+
),
|
|
714
|
+
writeCommand({ name: "unoperand", summary: "Removes the operand expression at an address.", subjects: [ADDRESS] }, (call) => {
|
|
715
|
+
const context = contextOf(call);
|
|
716
|
+
const request = { space: call.space, address: call.parseAddress(call.subject(0), "<address>") };
|
|
717
|
+
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
718
|
+
}),
|
|
719
|
+
writeCommand(
|
|
720
|
+
{
|
|
721
|
+
name: "patch",
|
|
722
|
+
summary: "ACME code that replaces a range when the build defines the switch as 1 (c64-assembler --define SWITCH=1). NOP fills the rest of the range.",
|
|
723
|
+
subjects: [RANGE, { name: "<switch>", help: "the switch name" }],
|
|
724
|
+
options: {
|
|
725
|
+
code: { kind: "text", value: "<text>", help: "the ACME code" },
|
|
726
|
+
"code-file": { kind: "path", value: "<file>", help: "a project file with the ACME code" },
|
|
727
|
+
nop: { kind: "flag", help: "put only NOP" },
|
|
728
|
+
comment: { kind: "text", value: "<text>", help: "a comment for the patch" },
|
|
729
|
+
},
|
|
730
|
+
notes: "Give exactly one of --code, --code-file and --nop.",
|
|
731
|
+
},
|
|
732
|
+
(call) => {
|
|
733
|
+
const context = contextOf(call);
|
|
734
|
+
const code = textOrFile(call, "code");
|
|
735
|
+
if ((code === undefined) === !call.flag("nop")) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
736
|
+
const commentText = call.string("comment");
|
|
737
|
+
const request = { space: call.space, ...call.parseRange(call.subject(0), "<range>"), switch: call.subject(1), code: code ?? "", ...(commentText === undefined ? {} : { comment: commentText }) };
|
|
738
|
+
return (db) => shown(setPatch(db, { ...context, image: operandImage(db, call.space) }, request), "patch", patch);
|
|
739
|
+
},
|
|
740
|
+
),
|
|
741
|
+
writeCommand({ name: "unpatch", summary: "Removes the patch that starts at an address.", subjects: [START] }, (call) => {
|
|
742
|
+
const context = contextOf(call);
|
|
743
|
+
const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>") };
|
|
744
|
+
return (db) => ({ revision: removePatch(db, context, request).revision });
|
|
745
|
+
}),
|
|
746
|
+
writeCommand(
|
|
747
|
+
{
|
|
748
|
+
name: "macro",
|
|
749
|
+
summary: "Writes an ACME macro. The body names a param with a dot: .a",
|
|
750
|
+
subjects: [{ name: "<name>", help: "the macro name" }],
|
|
751
|
+
options: {
|
|
752
|
+
params: { kind: "text", value: "<a,b>", help: "the params, a comma list" },
|
|
753
|
+
body: { kind: "text", value: "<text>", help: "the body" },
|
|
754
|
+
"body-file": { kind: "path", value: "<file>", help: "a project file with the body" },
|
|
755
|
+
comment: { kind: "text", value: "<text>", help: "a comment for the macro" },
|
|
756
|
+
},
|
|
757
|
+
notes: "Give --body or --body-file.",
|
|
758
|
+
},
|
|
759
|
+
(call) => {
|
|
760
|
+
const context = contextOf(call);
|
|
761
|
+
const body = textOrFile(call, "body");
|
|
762
|
+
if (body === undefined || body === "") throw new UsageError("macro needs --body or --body-file with text");
|
|
763
|
+
const commentText = call.string("comment");
|
|
764
|
+
const request = { space: call.space, name: call.subject(0), params: commaList(call.string("params") ?? ""), body, ...(commentText === undefined ? {} : { comment: commentText }) };
|
|
765
|
+
return (db) => shown(setMacro(db, context, request), "macro", macro);
|
|
766
|
+
},
|
|
767
|
+
),
|
|
768
|
+
writeCommand({ name: "unmacro", summary: "Removes a macro that no use has.", subjects: [{ name: "<name>", help: "the macro name" }] }, (call) => {
|
|
769
|
+
const context = contextOf(call);
|
|
770
|
+
const request = { space: call.space, name: call.subject(0) };
|
|
771
|
+
return (db) => ({ revision: removeMacro(db, context, request).revision });
|
|
772
|
+
}),
|
|
773
|
+
writeCommand(
|
|
774
|
+
{
|
|
775
|
+
name: "use",
|
|
776
|
+
summary: "Writes the instructions of a range as one use of a macro.",
|
|
777
|
+
subjects: [RANGE, { name: "<macro>", help: "the macro name" }],
|
|
778
|
+
options: { args: { kind: "text", value: "<x, y>", help: "one expression for each param, a comma list" } },
|
|
779
|
+
},
|
|
780
|
+
(call) => {
|
|
781
|
+
const context = contextOf(call);
|
|
782
|
+
const request = { space: call.space, ...call.parseRange(call.subject(0), "<range>"), macro: call.subject(1), args: commaList(call.string("args") ?? "") };
|
|
783
|
+
return (db) => {
|
|
784
|
+
const result = setMacroUse(db, { ...context, image: operandImage(db, call.space) }, request);
|
|
785
|
+
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
786
|
+
};
|
|
787
|
+
},
|
|
788
|
+
),
|
|
789
|
+
writeCommand({ name: "unuse", summary: "Removes the macro use that starts at an address.", subjects: [START] }, (call) => {
|
|
790
|
+
const context = contextOf(call);
|
|
791
|
+
const request = { space: call.space, start: call.parseAddress(call.subject(0), "<start>") };
|
|
792
|
+
return (db) => ({ revision: removeMacroUse(db, context, request).revision });
|
|
793
|
+
}),
|
|
794
|
+
writeCommand(
|
|
795
|
+
{
|
|
796
|
+
name: "image",
|
|
797
|
+
summary: "Puts the program into the knowledge: its bytes, and for an image.json of a c64-unpacker capture also the entry, the entry state and the stages. Only changed bytes write.",
|
|
798
|
+
subjects: [{ name: "<file>", help: "an image.json of c64-unpacker capture or loads, or a PRG" }],
|
|
799
|
+
},
|
|
800
|
+
(call) => {
|
|
801
|
+
const context = contextOf(call);
|
|
802
|
+
const request = imageRequest(call.subject(0), call.space);
|
|
803
|
+
return (db) => {
|
|
804
|
+
const { revision, current } = setImage(db, context, request);
|
|
805
|
+
return { revision, ...spaceOf(current), bytes: current.bytes, rowsAdded: current.rowsAdded, rowsClosed: current.rowsClosed, entryChanged: current.entry };
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
),
|
|
809
|
+
writeCommand(
|
|
810
|
+
{
|
|
811
|
+
name: "coverage",
|
|
812
|
+
summary: "Joins coverage maps of c64_memmap or of a run into the coverage of the knowledge. Each map is named by its path: --evidence coverage:<path> cites it.",
|
|
813
|
+
subjects: [{ name: "<map>", help: "a 64 KiB coverage map", list: true }],
|
|
814
|
+
},
|
|
815
|
+
(call) => {
|
|
816
|
+
const context = contextOf(call);
|
|
817
|
+
const maps = call.subjects.map((path) => ({ bytes: readProjectFile(path).bytes, name: knowledgePath(path, "<map>") }));
|
|
818
|
+
return (db) => {
|
|
819
|
+
const { revision, current } = addCoverage(db, context, { space: call.space, maps });
|
|
820
|
+
return { revision, ...spaceOf(current), imported: current.imported, known: current.known, ranges: current.ranges };
|
|
821
|
+
};
|
|
822
|
+
},
|
|
823
|
+
),
|
|
824
|
+
writeCommand({ name: "revert", summary: "Undoes one revision with a new revision.", subjects: [REVISION] }, (call) => {
|
|
825
|
+
const context = contextOf(call);
|
|
826
|
+
const request = { revision: call.subject(0) };
|
|
827
|
+
return (db) => ({ revision: revert(db, context, request).revision });
|
|
828
|
+
}),
|
|
829
|
+
writeCommand({ name: "merge", summary: "Decides the conflicts that a git merge of two computers' work left: of the rows of one conflict, the newest stays. One revision for all." }, (call) => {
|
|
830
|
+
const context = contextOf(call);
|
|
831
|
+
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
832
|
+
return (db) => {
|
|
833
|
+
const merged = mergeConflicts(db, context, platform);
|
|
834
|
+
return { revision: merged.revision, choices: merged.current.map(showChoice) };
|
|
835
|
+
};
|
|
836
|
+
}),
|
|
837
|
+
writeCommand(
|
|
838
|
+
{
|
|
839
|
+
name: "apply",
|
|
840
|
+
summary: "Many writes from a JSON file, all as one revision, or none when one is refused.",
|
|
841
|
+
subjects: [{ name: "<file>", help: "the batch: a JSON list of writes, or - for standard input" }],
|
|
842
|
+
notes: 'A write is {"op": "<command>", ...}, for example {"op": "rename", "address": "$2100", "name": "update_player"}. A range is "start" and "end". A write can name its own "space". operand, patch and use writes are checked against the program of their space.',
|
|
843
|
+
},
|
|
844
|
+
(call) => {
|
|
845
|
+
const context = contextOf(call);
|
|
846
|
+
const path = call.subject(0);
|
|
847
|
+
const text = path === "-" ? readFileSync(0, "utf8") : readProjectFile(path).bytes.toString("utf8");
|
|
848
|
+
let parsed: unknown;
|
|
849
|
+
try {
|
|
850
|
+
parsed = JSON.parse(text);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
throw new Refusal("invalid-input", `the batch ${path} is not JSON: ${(error as Error).message}`);
|
|
853
|
+
}
|
|
854
|
+
if (!Array.isArray(parsed)) throw new Refusal("invalid-input", `the batch ${path} is a JSON list of writes, for example [{"op": "rename", "address": "$2100", "name": "update_player"}]`);
|
|
855
|
+
const operations = parsed.map((item, index) => batchOperation(call, item, `write ${index + 1}`));
|
|
856
|
+
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
857
|
+
return (db) => {
|
|
858
|
+
const applied = applyBatch(db, checked ? { ...context, image: operandImage(db, call.space) } : context, operations);
|
|
859
|
+
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write.
|
|
860
|
+
const replaced = applied.steps.flatMap((step, index) => (step.previous === undefined ? [] : [{ write: index + 1, op: operations[index]!.op, previous: shownPrevious(step.previous) }]));
|
|
861
|
+
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
),
|
|
865
|
+
];
|
|
866
|
+
|
|
867
|
+
/** join: the batches of agents that worked in parallel, as one batch for apply. It writes no knowledge. */
|
|
868
|
+
const JOIN: CommandSpec = {
|
|
869
|
+
name: "join",
|
|
870
|
+
summary: "Joins the batches of agents that worked in parallel into one batch for apply. It writes no knowledge.",
|
|
871
|
+
subjects: [{ name: "<batch>", help: 'a batch: a list of writes, or {"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}', list: true }],
|
|
872
|
+
options: { ...SPACE, out: { ...OPTIONS.out, value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
|
|
873
|
+
notes: "At an address in the owner range of a batch, its name, comment, operand and module win, and its regions come last. One name or one module file at two addresses writes nothing.",
|
|
874
|
+
run: (call) => {
|
|
875
|
+
if (call.subjects.length < 2) throw new UsageError("join needs two or more batch files");
|
|
876
|
+
const batches: Batch[] = call.subjects.map((file) => {
|
|
877
|
+
let value: unknown;
|
|
878
|
+
try {
|
|
879
|
+
value = JSON.parse(readProjectFile(file).bytes.toString("utf8"));
|
|
880
|
+
} catch (error) {
|
|
881
|
+
if (error instanceof SyntaxError) throw new Refusal("invalid-input", `${file} is not JSON: ${error.message}`);
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
const shaped = Array.isArray(value) ? { ops: value } : (value as { owner?: unknown; ops?: unknown });
|
|
885
|
+
if (typeof shaped !== "object" || shaped === null || !Array.isArray(shaped.ops)) throw new Refusal("invalid-input", `${file}: a batch is a list of writes, or {"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}`);
|
|
886
|
+
let owner: Batch["owner"];
|
|
887
|
+
if (shaped.owner !== undefined) {
|
|
888
|
+
const given = shaped.owner as { start?: unknown; end?: unknown };
|
|
889
|
+
if (typeof given !== "object" || given === null || typeof given.start !== "string" || typeof given.end !== "string" || Object.keys(given).length !== 2) {
|
|
890
|
+
throw new Refusal("invalid-input", `${file}: owner must be {"start": "<address>", "end": "<address>"}, for example {"start": "$1000", "end": "$1fff"}`);
|
|
891
|
+
}
|
|
892
|
+
owner = { start: call.parseAddress(given.start, `${file}: owner.start`), end: call.parseAddress(given.end, `${file}: owner.end`) };
|
|
893
|
+
if (owner.end < owner.start) throw new Refusal("invalid-input", `${file}: the owner range ends before it starts`);
|
|
894
|
+
}
|
|
895
|
+
const ops = (shaped.ops as unknown[]).map((item, index) => batchOperation(call, item, `${file}: write ${index + 1}`));
|
|
896
|
+
return { file, ...(owner === undefined ? {} : { owner }), ops };
|
|
897
|
+
});
|
|
898
|
+
const owned = batches.filter((batch) => batch.owner !== undefined);
|
|
899
|
+
for (const [index, batch] of owned.entries()) {
|
|
900
|
+
const other = owned.slice(index + 1).find((next) => next.owner!.start <= batch.owner!.end && batch.owner!.start <= next.owner!.end);
|
|
901
|
+
if (other !== undefined) throw new Refusal("invalid-input", `The owner ranges of ${batch.file} and ${other.file} overlap. Give each address one owner.`);
|
|
902
|
+
}
|
|
903
|
+
const joined = joinBatches(batches);
|
|
904
|
+
const choice = (item: JoinChoice) => ({ ...item, at: typeof item.at === "number" ? formatC64Address(item.at) : item.at, ...(item.space === DEFAULT_SPACE ? { space: undefined } : {}) });
|
|
905
|
+
if (joined.clashes.length > 0) {
|
|
906
|
+
const clash = (item: JoinClash) => `${item.what} ${item.value} at ${item.places.map((place) => `${formatC64Address(place.address)} (${place.file})`).join(" and ")}${item.space === DEFAULT_SPACE ? "" : ` in ${item.space}`}`;
|
|
907
|
+
throw new Refusal("conflict", `The batches give one ${joined.clashes.map(clash).join(", one ")}. Nothing was written. Give each name and each module file one address in the batches, then join again.`);
|
|
908
|
+
}
|
|
909
|
+
const out = call.string("out")!;
|
|
910
|
+
return { writes: joined.ops.length, repeated: joined.repeated, choices: joined.choices.map(choice), apply: `knowledge.ts apply ${out}`, ...writeReported(out, `${JSON.stringify(joined.ops.map(batchJson), null, 1)}\n`, { what: "--out" }) };
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
|
|
935
914
|
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
936
|
-
function batchOperation(
|
|
937
|
-
|
|
915
|
+
function batchOperation(call: Call, item: unknown, at: string): BatchOperation {
|
|
916
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Refusal("invalid-input", `${at}: a write is an object with op`);
|
|
938
917
|
const own = (item as Record<string, unknown>).space;
|
|
939
|
-
if (own !== undefined && typeof own !== "string") throw new
|
|
940
|
-
|
|
918
|
+
if (own !== undefined && typeof own !== "string") throw new Refusal("invalid-input", `${at}: space must be a string`);
|
|
919
|
+
const space = (own as string | undefined) ?? call.space;
|
|
920
|
+
return { ...batchFields(item as Record<string, unknown>, at, (text, what) => call.parseAddress(text, what, space)), space };
|
|
941
921
|
}
|
|
942
922
|
|
|
943
923
|
/** The fields of one write of a batch, with its addresses parsed. */
|
|
944
|
-
function batchFields(
|
|
945
|
-
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
|
|
946
|
-
const fields = item as Record<string, unknown>;
|
|
924
|
+
function batchFields(fields: Record<string, unknown>, at: string, address: (text: string, what: string) => number): BatchOperation {
|
|
947
925
|
const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
|
|
948
926
|
const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
|
|
949
|
-
if (unknown.length > 0) throw new
|
|
927
|
+
if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
|
|
950
928
|
const text = (key: string) => {
|
|
951
|
-
if (typeof fields[key] !== "string") throw new
|
|
929
|
+
if (typeof fields[key] !== "string") throw new Refusal("invalid-input", `${at}: ${key} must be a string`);
|
|
952
930
|
return fields[key] as string;
|
|
953
931
|
};
|
|
954
932
|
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
@@ -957,7 +935,7 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
957
935
|
const value = fields[key];
|
|
958
936
|
if (value === undefined) return [];
|
|
959
937
|
if (typeof value === "string") return commaList(value);
|
|
960
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new
|
|
938
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Refusal("invalid-input", `${at}: ${key} must be a list of strings or a comma list`);
|
|
961
939
|
return (value as string[]).map((item) => item.trim());
|
|
962
940
|
};
|
|
963
941
|
const optional = (key: string) => (fields[key] === undefined ? {} : { [key]: text(key) });
|
|
@@ -967,7 +945,7 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
967
945
|
case "remove-symbol":
|
|
968
946
|
return { op, address: place("address") };
|
|
969
947
|
case "classify": {
|
|
970
|
-
if (fields.element !== undefined && typeof fields.element !== "number") throw new
|
|
948
|
+
if (fields.element !== undefined && typeof fields.element !== "number") throw new Refusal("invalid-input", `${at}: element must be a number`);
|
|
971
949
|
return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
|
|
972
950
|
}
|
|
973
951
|
case "unclassify":
|
|
@@ -1010,108 +988,9 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
1010
988
|
}
|
|
1011
989
|
}
|
|
1012
990
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
|
|
1020
|
-
if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
|
|
1021
|
-
return (db) => {
|
|
1022
|
-
const result = renameSymbol(db, context, request);
|
|
1023
|
-
return { revision: result.revision, symbol: symbol(result.current), ...(result.previous === undefined ? {} : { previous: symbol(result.previous) }) };
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
case "remove-symbol": {
|
|
1027
|
-
const request = { space, address: address(args[0]) };
|
|
1028
|
-
return (db) => ({ revision: removeSymbol(db, context, request).revision });
|
|
1029
|
-
}
|
|
1030
|
-
case "classify": {
|
|
1031
|
-
const request = {
|
|
1032
|
-
space,
|
|
1033
|
-
start: address(args[0], "start"),
|
|
1034
|
-
end: address(args[1], "end"),
|
|
1035
|
-
type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
|
|
1036
|
-
...(element === undefined ? {} : { element: integer(element, "--element") }),
|
|
1037
|
-
};
|
|
1038
|
-
return (db) => {
|
|
1039
|
-
const result = classifyRegion(db, context, request);
|
|
1040
|
-
return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
case "unclassify": {
|
|
1044
|
-
const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
|
|
1045
|
-
return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
|
|
1046
|
-
}
|
|
1047
|
-
case "comment": {
|
|
1048
|
-
if (args.length < 3) throw new UsageError("text is missing");
|
|
1049
|
-
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
1050
|
-
return (db) => {
|
|
1051
|
-
const result = setComment(db, context, request);
|
|
1052
|
-
return { revision: result.revision, comment: comment(result.current) };
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
case "uncomment": {
|
|
1056
|
-
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
1057
|
-
return (db) => ({ revision: removeComment(db, context, request).revision });
|
|
1058
|
-
}
|
|
1059
|
-
case "reference": {
|
|
1060
|
-
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
1061
|
-
return (db) => {
|
|
1062
|
-
const result = addReference(db, context, request);
|
|
1063
|
-
return { revision: result.revision, reference: reference(result.current) };
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
case "unreference": {
|
|
1067
|
-
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
1068
|
-
return (db) => ({ revision: removeReference(db, context, request).revision });
|
|
1069
|
-
}
|
|
1070
|
-
case "module": {
|
|
1071
|
-
if (args[1] === undefined) throw new UsageError("file is missing");
|
|
1072
|
-
const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
|
|
1073
|
-
return (db) => {
|
|
1074
|
-
const result = setModule(db, context, request);
|
|
1075
|
-
return { revision: result.revision, module: module(result.current) };
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
case "unmodule": {
|
|
1079
|
-
const request = { space, start: address(args[0], "start") };
|
|
1080
|
-
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
1081
|
-
}
|
|
1082
|
-
case "unoperand": {
|
|
1083
|
-
const request = { space, address: address(args[0]) };
|
|
1084
|
-
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
1085
|
-
}
|
|
1086
|
-
case "unpatch": {
|
|
1087
|
-
const request = { space, start: address(args[0], "start") };
|
|
1088
|
-
return (db) => ({ revision: removePatch(db, context, request).revision });
|
|
1089
|
-
}
|
|
1090
|
-
case "unmacro": {
|
|
1091
|
-
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
1092
|
-
const request = { space, name: args[0] };
|
|
1093
|
-
return (db) => ({ revision: removeMacro(db, context, request).revision });
|
|
1094
|
-
}
|
|
1095
|
-
case "unuse": {
|
|
1096
|
-
const request = { space, start: address(args[0], "start") };
|
|
1097
|
-
return (db) => ({ revision: removeMacroUse(db, context, request).revision });
|
|
1098
|
-
}
|
|
1099
|
-
case "merge": {
|
|
1100
|
-
if (args.length > 0) throw new UsageError("merge takes no arguments");
|
|
1101
|
-
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
1102
|
-
return (db) => {
|
|
1103
|
-
const merged = mergeConflicts(db, context, platform);
|
|
1104
|
-
return { revision: merged.revision, choices: merged.current.map(showChoice) };
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
case "revert": {
|
|
1108
|
-
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
1109
|
-
const request = { revision: args[0] };
|
|
1110
|
-
return (db) => ({ revision: revert(db, context, request).revision });
|
|
1111
|
-
}
|
|
1112
|
-
default:
|
|
1113
|
-
throw new UsageError(`unknown command: ${command}`);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|
|
991
|
+
await runScript({
|
|
992
|
+
name: "knowledge",
|
|
993
|
+
summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks.",
|
|
994
|
+
names: knowledgeNames,
|
|
995
|
+
commands: [...READ_COMMANDS, ...WRITE_COMMANDS, JOIN],
|
|
996
|
+
});
|