@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
|
@@ -1,85 +1,240 @@
|
|
|
1
|
-
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge
|
|
2
|
-
// project
|
|
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) and
|
|
3
|
+
// prints one compact JSON result.
|
|
3
4
|
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
4
8
|
import type { DatabaseSync } from "node:sqlite";
|
|
5
9
|
import { parseArgs } from "node:util";
|
|
6
10
|
|
|
7
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
11
|
+
import { formatC64Address, parseC64Address, SYMBOL_NAME } from "#src/c64.ts";
|
|
12
|
+
import { ImageError, readImage } from "#src/host-client/image.ts";
|
|
13
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
14
|
+
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
15
|
+
import { joinBatches, type Batch, type JoinChoice, type JoinClash } from "#src/knowledge/join.ts";
|
|
8
16
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
9
|
-
import {
|
|
17
|
+
import { operandText } from "#src/knowledge/expression.ts";
|
|
18
|
+
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
10
19
|
import {
|
|
11
20
|
at,
|
|
21
|
+
analyzerNamed,
|
|
22
|
+
analyzerNamesAt,
|
|
23
|
+
commentsAt,
|
|
24
|
+
currentRevision,
|
|
25
|
+
listAnalyzerNames,
|
|
12
26
|
listComments,
|
|
27
|
+
listMacros,
|
|
28
|
+
listMacroUses,
|
|
29
|
+
listModules,
|
|
30
|
+
listOperands,
|
|
31
|
+
listPatches,
|
|
32
|
+
listSpaces,
|
|
13
33
|
listReferences,
|
|
14
34
|
listRegions,
|
|
35
|
+
listSuppressions,
|
|
15
36
|
listSymbols,
|
|
37
|
+
operandAt,
|
|
38
|
+
referencesFrom,
|
|
39
|
+
referencesTo,
|
|
16
40
|
search,
|
|
41
|
+
symbolAt,
|
|
42
|
+
symbolNamed,
|
|
17
43
|
type CommentRow,
|
|
44
|
+
type MacroRow,
|
|
45
|
+
type MacroUseRow,
|
|
46
|
+
type ModuleRow,
|
|
47
|
+
type OperandRow,
|
|
48
|
+
type PatchRow,
|
|
18
49
|
type ReferenceRow,
|
|
19
50
|
type RegionRow,
|
|
51
|
+
type SuppressionRow,
|
|
20
52
|
type SymbolRow,
|
|
21
53
|
} from "#src/knowledge/read.ts";
|
|
22
54
|
import {
|
|
23
55
|
COMMENT_PLACEMENTS,
|
|
56
|
+
DEFAULT_SPACE,
|
|
57
|
+
EVIDENCE_KINDS,
|
|
58
|
+
OPERAND_SELECTS,
|
|
59
|
+
ORIGINS,
|
|
24
60
|
REFERENCE_KINDS,
|
|
25
61
|
REGION_TYPES,
|
|
62
|
+
SPACE,
|
|
26
63
|
SYMBOL_KINDS,
|
|
27
64
|
type CommentPlacement,
|
|
65
|
+
type OperandSelect,
|
|
66
|
+
type Origin,
|
|
28
67
|
type ReferenceKind,
|
|
29
68
|
type RegionType,
|
|
30
69
|
type SymbolKind,
|
|
31
70
|
} from "#src/knowledge/schema.ts";
|
|
32
71
|
import {
|
|
33
72
|
addReference,
|
|
73
|
+
applyBatch,
|
|
34
74
|
classifyRegion,
|
|
75
|
+
mergeConflicts,
|
|
35
76
|
removeComment,
|
|
77
|
+
removeMacro,
|
|
78
|
+
removeMacroUse,
|
|
79
|
+
removeModule,
|
|
80
|
+
removeOperand,
|
|
81
|
+
removePatch,
|
|
36
82
|
removeReference,
|
|
37
83
|
removeSymbol,
|
|
38
84
|
renameSymbol,
|
|
39
85
|
revert,
|
|
40
86
|
setComment,
|
|
87
|
+
setMacro,
|
|
88
|
+
setMacroUse,
|
|
89
|
+
setModule,
|
|
90
|
+
setOperand,
|
|
91
|
+
setPatch,
|
|
41
92
|
unclassifyRegion,
|
|
93
|
+
type BatchOperation,
|
|
94
|
+
type BatchStep,
|
|
95
|
+
type Evidence,
|
|
96
|
+
type MergeChoice,
|
|
97
|
+
type OperandImage,
|
|
42
98
|
type WriteContext,
|
|
43
99
|
} from "#src/knowledge/write.ts";
|
|
100
|
+
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
|
+
import { isInside, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
104
|
+
import { listing, listingText, type ListingSource } from "./lib/listing.ts";
|
|
44
105
|
import { runScript, UsageError } from "#src/script.ts";
|
|
45
106
|
|
|
46
107
|
const USAGE = `knowledge.ts <command> [arguments] [options]
|
|
47
108
|
|
|
48
109
|
Read (no database needed):
|
|
49
110
|
at <address> everything known at one address
|
|
50
|
-
search <text> symbol names and comment texts
|
|
51
|
-
symbols [--start A --end A --kind K]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 (D74)
|
|
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 (D83): 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):
|
|
60
148
|
rename <address> <name> [--kind K]
|
|
61
149
|
remove-symbol <address>
|
|
62
|
-
classify <start> <end> <type>
|
|
150
|
+
classify <start> <end> <type> [--element N]
|
|
63
151
|
unclassify <start> <end>
|
|
64
152
|
comment <address> <line|side> <text>
|
|
65
153
|
uncomment <address> <line|side>
|
|
66
154
|
reference <from> <to> <kind>
|
|
67
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 (D83): 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 (D83); each map is evidence
|
|
184
|
+
of the revision, named by its path
|
|
68
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
|
|
69
199
|
|
|
70
200
|
Write options:
|
|
71
201
|
--reason <text> why, kept with the revision
|
|
72
|
-
--
|
|
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
|
|
73
206
|
--origin user|llm who decided (default llm)
|
|
74
207
|
|
|
75
|
-
Addresses are $ and four hex digits; quote them in a shell: '$2100'
|
|
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.`;
|
|
76
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 (D74). */
|
|
77
217
|
function address(value: string | undefined, what = "address"): number {
|
|
78
218
|
if (value === undefined) throw new UsageError(`${what} is missing`);
|
|
219
|
+
if (SYMBOL_NAME.test(value)) return addressOfName(value, what);
|
|
79
220
|
try {
|
|
80
221
|
return parseC64Address(value);
|
|
81
222
|
} catch {
|
|
82
|
-
throw new UsageError(`${what} must be $ followed by four hex digits, for example '$2100', not ${value}`);
|
|
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();
|
|
83
238
|
}
|
|
84
239
|
}
|
|
85
240
|
|
|
@@ -94,24 +249,111 @@ function integer(value: string | undefined, what: string): number {
|
|
|
94
249
|
}
|
|
95
250
|
|
|
96
251
|
// Output shapes: addresses as $xxxx, nothing internal.
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
252
|
+
/** The space of a row, shown only when it is not the C64 (D53). */
|
|
253
|
+
const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
|
|
254
|
+
const symbol = (row: SymbolRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
|
|
255
|
+
const region = (row: RegionRow) => ({
|
|
256
|
+
...spaceOf(row),
|
|
257
|
+
start: formatC64Address(row.start),
|
|
258
|
+
end: formatC64Address(row.end),
|
|
259
|
+
type: row.type,
|
|
260
|
+
...(row.element === undefined ? {} : { element: row.element, count: (row.end - row.start + 1) / row.element }),
|
|
261
|
+
origin: row.origin,
|
|
262
|
+
revision: row.revision,
|
|
263
|
+
});
|
|
264
|
+
const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
|
|
265
|
+
const module = (row: ModuleRow) => ({ ...spaceOf(row), start: formatC64Address(row.start), file: row.file, ...(row.title === undefined ? {} : { title: row.title }), origin: row.origin, revision: row.revision });
|
|
266
|
+
const operand = (row: OperandRow) => ({
|
|
267
|
+
...spaceOf(row),
|
|
268
|
+
address: formatC64Address(row.address),
|
|
269
|
+
select: row.select,
|
|
270
|
+
...(row.symbol === undefined ? {} : { symbol: row.symbol }),
|
|
271
|
+
...(row.offset === 0 ? {} : { offset: row.offset }),
|
|
272
|
+
...(row.expression === undefined ? {} : { expression: row.expression }),
|
|
273
|
+
text: operandText(row),
|
|
274
|
+
origin: row.origin,
|
|
275
|
+
revision: row.revision,
|
|
276
|
+
});
|
|
277
|
+
const patch = (row: PatchRow) => ({
|
|
278
|
+
...spaceOf(row),
|
|
279
|
+
start: formatC64Address(row.start),
|
|
280
|
+
end: formatC64Address(row.end),
|
|
281
|
+
bytes: row.end - row.start + 1,
|
|
282
|
+
switch: row.switch,
|
|
283
|
+
code: row.code,
|
|
284
|
+
...(row.comment === undefined ? {} : { comment: row.comment }),
|
|
285
|
+
origin: row.origin,
|
|
286
|
+
revision: row.revision,
|
|
287
|
+
});
|
|
288
|
+
const macro = (row: MacroRow) => ({ ...spaceOf(row), name: row.name, params: row.params, body: row.body, ...(row.comment === undefined ? {} : { comment: row.comment }), origin: row.origin, revision: row.revision });
|
|
289
|
+
const macroUse = (row: MacroUseRow) => ({
|
|
290
|
+
...spaceOf(row),
|
|
291
|
+
start: formatC64Address(row.start),
|
|
292
|
+
end: formatC64Address(row.end),
|
|
293
|
+
macro: row.macro,
|
|
294
|
+
args: row.args,
|
|
295
|
+
text: `+${row.macro}${row.args.length === 0 ? "" : ` ${row.args.join(", ")}`}`,
|
|
296
|
+
origin: row.origin,
|
|
297
|
+
revision: row.revision,
|
|
298
|
+
});
|
|
299
|
+
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
300
|
+
function suppression(row: SuppressionRow) {
|
|
301
|
+
const place =
|
|
302
|
+
row.entity === "symbol"
|
|
303
|
+
? { address: formatC64Address(row.start) }
|
|
304
|
+
: row.entity === "region"
|
|
305
|
+
? { start: formatC64Address(row.start), end: formatC64Address(row.end) }
|
|
306
|
+
: { from: formatC64Address(row.start), to: formatC64Address(row.to!), kind: row.kind! };
|
|
307
|
+
return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
|
|
308
|
+
}
|
|
101
309
|
|
|
102
310
|
function historyEntry(entry: HistoryEntry) {
|
|
103
311
|
const shown =
|
|
104
|
-
entry.entity === "symbol"
|
|
312
|
+
entry.entity === "symbol" || entry.entity === "analyzer-name"
|
|
105
313
|
? symbol(entry.row)
|
|
106
314
|
: entry.entity === "region"
|
|
107
315
|
? region(entry.row)
|
|
108
316
|
: entry.entity === "comment"
|
|
109
317
|
? comment(entry.row)
|
|
110
|
-
:
|
|
318
|
+
: entry.entity === "reference"
|
|
319
|
+
? reference(entry.row)
|
|
320
|
+
: entry.entity === "module"
|
|
321
|
+
? module(entry.row)
|
|
322
|
+
: entry.entity === "operand"
|
|
323
|
+
? operand(entry.row)
|
|
324
|
+
: entry.entity === "patch"
|
|
325
|
+
? patch(entry.row)
|
|
326
|
+
: entry.entity === "macro"
|
|
327
|
+
? macro(entry.row)
|
|
328
|
+
: entry.entity === "macro-use"
|
|
329
|
+
? macroUse(entry.row)
|
|
330
|
+
: suppression(entry.row);
|
|
111
331
|
const { revision: _revision, ...row } = shown;
|
|
112
332
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
113
333
|
}
|
|
114
334
|
|
|
335
|
+
/** A conflict of a git merge (D69): what is wrong, and the rows, oldest first. */
|
|
336
|
+
function showConflict(conflict: Conflict) {
|
|
337
|
+
return { conflict: CONFLICT_RULES[conflict.rule], ...spaceOf(conflict), rows: conflict.rows.map(historyEntry) };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** What merge decided for one conflict: the row that stays and the rows it closed. */
|
|
341
|
+
function showChoice(choice: MergeChoice) {
|
|
342
|
+
return { conflict: CONFLICT_RULES[choice.rule], ...spaceOf(choice), kept: historyEntry(choice.kept), closed: choice.closed.map(historyEntry) };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The reason of each revision that wrote one of the rows. */
|
|
346
|
+
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<string, string> {
|
|
347
|
+
const reasons = new Map<string, string>();
|
|
348
|
+
const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
|
|
349
|
+
for (const { revision } of rows) {
|
|
350
|
+
if (query === undefined || reasons.has(revision)) continue;
|
|
351
|
+
const found = query.get(revision) as { description: string | null } | undefined;
|
|
352
|
+
if (found?.description != null) reasons.set(revision, found.description);
|
|
353
|
+
}
|
|
354
|
+
return reasons;
|
|
355
|
+
}
|
|
356
|
+
|
|
115
357
|
function run(argv: string[]): unknown {
|
|
116
358
|
const { values, positionals } = parseArgs({
|
|
117
359
|
args: argv,
|
|
@@ -128,59 +370,52 @@ function run(argv: string[]): unknown {
|
|
|
128
370
|
reason: { type: "string" },
|
|
129
371
|
"expect-revision": { type: "string" },
|
|
130
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" },
|
|
131
394
|
help: { type: "boolean" },
|
|
132
395
|
},
|
|
133
396
|
});
|
|
134
397
|
const [command, ...args] = positionals;
|
|
135
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;
|
|
136
402
|
const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
|
|
137
403
|
|
|
138
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "history", "revisions", "revision"]);
|
|
139
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "revert"]);
|
|
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 (D79).
|
|
407
|
+
if (command === "join") return joinCommand(args, values.out, space);
|
|
140
408
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
141
409
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
142
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>");
|
|
143
413
|
const db = openForRead();
|
|
144
414
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
address: formatC64Address(known.address),
|
|
150
|
-
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
151
|
-
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
152
|
-
comments: known.comments.map(comment),
|
|
153
|
-
referencesFrom: known.referencesFrom.map(reference),
|
|
154
|
-
referencesTo: known.referencesTo.map(reference),
|
|
155
|
-
revision: known.revision,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
case "search": {
|
|
159
|
-
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
160
|
-
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"));
|
|
161
|
-
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
162
|
-
}
|
|
163
|
-
case "symbols":
|
|
164
|
-
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind")).map(symbol) };
|
|
165
|
-
case "regions":
|
|
166
|
-
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type")).map(region) };
|
|
167
|
-
case "comments":
|
|
168
|
-
return { comments: listComments(db, range).map(comment) };
|
|
169
|
-
case "references":
|
|
170
|
-
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind")).map(reference) };
|
|
171
|
-
case "history":
|
|
172
|
-
return { history: (values.name === undefined ? historyAt(db, address(args[0])) : historyNamed(db, values.name)).map(historyEntry) };
|
|
173
|
-
case "revisions": {
|
|
174
|
-
const options: { limit?: number; before?: number } = {};
|
|
175
|
-
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
176
|
-
if (values.before !== undefined) options.before = integer(values.before, "--before");
|
|
177
|
-
return { revisions: revisions(db, options) };
|
|
178
|
-
}
|
|
179
|
-
default: {
|
|
180
|
-
const changes = revision(db, integer(args[0], "revision"));
|
|
181
|
-
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
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 (D69).
|
|
418
|
+
return found.length === 0 ? answer : { ...answer, conflicts: found.map(showConflict) };
|
|
184
419
|
} finally {
|
|
185
420
|
db?.close();
|
|
186
421
|
}
|
|
@@ -188,19 +423,589 @@ function run(argv: string[]): unknown {
|
|
|
188
423
|
|
|
189
424
|
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
190
425
|
if (values.reason !== undefined) context.description = values.reason;
|
|
191
|
-
if (values["expect-revision"] !== undefined) context.expectedRevision =
|
|
426
|
+
if (values["expect-revision"] !== undefined) context.expectedRevision = values["expect-revision"];
|
|
427
|
+
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
192
428
|
// Every argument is checked before the database opens: a refused write creates no database.
|
|
193
|
-
|
|
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);
|
|
194
456
|
return withWrite(operation);
|
|
195
457
|
}
|
|
196
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
|
+
/** The lookups of a listing (D78): the program and the coverage in the knowledge (D83), the knowledge of the space and the platform names of the C64. */
|
|
472
|
+
function listingSource(db: DatabaseSync | undefined, space: string): ListingSource {
|
|
473
|
+
const image = requireProgram(db, space);
|
|
474
|
+
const coverage = programCoverage(db, space)?.map;
|
|
475
|
+
const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
|
|
476
|
+
const platform = new Map(space === DEFAULT_SPACE ? platformNames().map((entry) => [entry.address, entry.name]) : []);
|
|
477
|
+
return {
|
|
478
|
+
byteAt: (at) => (inside(at) ? image.byteAt(at) : undefined),
|
|
479
|
+
// A user or LLM name, else the platform name; an analyzer name in symbols (knowledge from before D74) gives way to it (D61).
|
|
480
|
+
nameOf: (at) => {
|
|
481
|
+
const own = symbolAt(db, at, space);
|
|
482
|
+
return own !== undefined && (own.origin === "user" || own.origin === "llm") ? own.name : (platform.get(at) ?? own?.name);
|
|
483
|
+
},
|
|
484
|
+
secondNames: (at) => analyzerNamesAt(db, at, space).map((row) => row.name),
|
|
485
|
+
regions: listRegions(db, undefined, undefined, undefined, space),
|
|
486
|
+
comments: (at) => {
|
|
487
|
+
const rows = commentsAt(db, at, space);
|
|
488
|
+
const side = rows.find((row) => row.placement === "side")?.text;
|
|
489
|
+
return { line: rows.filter((row) => row.placement === "line").flatMap((row) => row.text.split(/\r?\n/)), ...(side === undefined ? {} : { side }) };
|
|
490
|
+
},
|
|
491
|
+
operand: (at) => {
|
|
492
|
+
const row = operandAt(db, at, space);
|
|
493
|
+
return row === undefined ? undefined : operandText(row);
|
|
494
|
+
},
|
|
495
|
+
references: (at) => ({ in: referencesTo(db, at, space).length, out: referencesFrom(db, at, space).length }),
|
|
496
|
+
...(coverage === undefined ? {} : { coverage }),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
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: coverageRuns(db).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 (F-26).
|
|
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
|
+
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
637
|
+
function originFilter(value: string): Origin[] {
|
|
638
|
+
const origins = new Set<Origin>();
|
|
639
|
+
for (const part of value.split(",")) {
|
|
640
|
+
if (part === "semantic") origins.add("user").add("llm");
|
|
641
|
+
else if (part === "analyzer") origins.add("dxa").add("ghidra").add("heuristic");
|
|
642
|
+
else origins.add(oneOf(ORIGINS, part, "--origin"));
|
|
643
|
+
}
|
|
644
|
+
return [...origins];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows.
|
|
649
|
+
* A file in .c64-re-tools/local/ is refused (D82): git ignores it, and the move that keeps it changes the path.
|
|
650
|
+
*/
|
|
651
|
+
function evidenceOf(value: string): Evidence {
|
|
652
|
+
const colon = value.indexOf(":");
|
|
653
|
+
const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
|
|
654
|
+
const ref = colon < 0 ? "" : value.slice(colon + 1);
|
|
655
|
+
if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
|
|
656
|
+
if (kind === "note") return { kind, ref };
|
|
657
|
+
// A coverage map is evidence by the name that coverage imported it with (D83).
|
|
658
|
+
if (kind === "coverage") {
|
|
659
|
+
const db = openForRead();
|
|
660
|
+
try {
|
|
661
|
+
const run = coverageRuns(db).find((item) => item.name === ref);
|
|
662
|
+
if (run === undefined) throw new UsageError(`--evidence coverage:${ref} names no coverage map of the knowledge: import the map with coverage first`);
|
|
663
|
+
return { kind, ref, sha256: run.sha256 };
|
|
664
|
+
} finally {
|
|
665
|
+
db?.close();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const bytes = readProjectFile(ref).bytes;
|
|
669
|
+
const root = projectRoot();
|
|
670
|
+
if (isInside(join(root, PROJECT_DIRECTORY, LOCAL_DIRECTORY), resolveProjectPath(ref, root))) {
|
|
671
|
+
throw new UsageError(`--evidence ${kind}:${ref} is in ${PROJECT_DIRECTORY}/${LOCAL_DIRECTORY}/, which git ignores: move it to a folder that git keeps (a run folder to runs/<name>/) and give the new path`);
|
|
672
|
+
}
|
|
673
|
+
return { kind, ref, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** An image.json of c64-unpacker capture or a PRG as a write of the program (D83). */
|
|
677
|
+
function imageRequest(path: string, space: string): ImageRequest {
|
|
678
|
+
let image: ReturnType<typeof readImage>;
|
|
679
|
+
try {
|
|
680
|
+
image = readImage(path);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
const request: ImageRequest = { space, segments: image.segments, byteAt: image.byteAt };
|
|
686
|
+
if (image.entry !== undefined) request.entry = image.entry;
|
|
687
|
+
if (image.stages !== undefined) request.stages = image.stages;
|
|
688
|
+
if (image.statePath !== undefined) {
|
|
689
|
+
let bytes: Buffer | undefined;
|
|
690
|
+
try {
|
|
691
|
+
bytes = readProjectFile(image.statePath).bytes;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (!(error instanceof WireFailure && error.code === "not-found")) throw error;
|
|
694
|
+
}
|
|
695
|
+
if (bytes !== undefined) {
|
|
696
|
+
let state: unknown;
|
|
697
|
+
try {
|
|
698
|
+
state = JSON.parse(bytes.toString("utf8"));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
throw new UsageError(`${image.statePath} is not JSON: ${(error as Error).message}`);
|
|
701
|
+
}
|
|
702
|
+
if (typeof state !== "object" || state === null || Array.isArray(state)) throw new UsageError(`${image.statePath} is not an entry state of c64-unpacker capture`);
|
|
703
|
+
// The comparison with a clean reset only tells the capture what it found.
|
|
704
|
+
const { differsFromReset: _differs, ...kept } = state as Record<string, unknown>;
|
|
705
|
+
request.state = kept;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return request;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** image and coverage (D83): the program and what the runs did go into the knowledge, so no project file is made again. */
|
|
712
|
+
function programOperation(command: "image" | "coverage", args: string[], context: WriteContext, space: string): WriteOperation {
|
|
713
|
+
if (command === "image") {
|
|
714
|
+
if (args.length !== 1) throw new UsageError("image needs one image.json or PRG");
|
|
715
|
+
const request = imageRequest(args[0]!, space);
|
|
716
|
+
return (db) => {
|
|
717
|
+
const { revision, current } = setImage(db, context, request);
|
|
718
|
+
return { revision, ...spaceOf(current), bytes: current.bytes, rowsAdded: current.rowsAdded, rowsClosed: current.rowsClosed, entryChanged: current.entry };
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (args.length === 0) throw new UsageError("coverage needs one or more coverage maps");
|
|
722
|
+
const maps = args.map((path) => ({ name: path, bytes: readProjectFile(path).bytes }));
|
|
723
|
+
return (db) => {
|
|
724
|
+
const { revision, current } = addCoverage(db, context, { space, maps });
|
|
725
|
+
return { revision, ...spaceOf(current), imported: current.imported, known: current.known, ranges: current.ranges };
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The program in the knowledge (D83) that operand, patch and macro use writes
|
|
731
|
+
* are checked against (D65, D72, D73), with the platform names. A byte outside
|
|
732
|
+
* the segments of the program is not in it.
|
|
733
|
+
*/
|
|
734
|
+
function operandImage(db: DatabaseSync, space: string): OperandImage {
|
|
735
|
+
const image = requireProgram(db, space);
|
|
736
|
+
const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
|
|
737
|
+
return { byteAt: (at) => (inside(at) ? image.byteAt(at) : undefined), platform: new Map(platformNames().map((entry) => [entry.name, entry.address])) };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** An offset: a whole number in decimal or $ hex, with an optional minus sign. */
|
|
741
|
+
function offsetOf(value: unknown, what: string): number {
|
|
742
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
743
|
+
const match = typeof value === "string" ? /^(-?)(\$[0-9A-Fa-f]{1,4}|[0-9]{1,5})$/.exec(value) : null;
|
|
744
|
+
if (match === null) throw new UsageError(`${what} must be a whole number such as 1, -1 or '$1000'`);
|
|
745
|
+
const size = match[2]!.startsWith("$") ? Number.parseInt(match[2]!.slice(1), 16) : Number(match[2]);
|
|
746
|
+
return match[1] === "-" ? -size : size;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** operand <address> with one of --lo, --hi, --value or --expr. */
|
|
750
|
+
function operandOperation(args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
751
|
+
const given = (["lo", "hi", "value", "expr"] as const).filter((option) => values[option] !== undefined);
|
|
752
|
+
if (given.length !== 1) throw new UsageError("operand needs exactly one of --lo, --hi, --value or --expr");
|
|
753
|
+
const select: OperandSelect = given[0]!;
|
|
754
|
+
const request: { space: string; address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string } = { space, address: address(args[0]), select };
|
|
755
|
+
if (select === "expr") {
|
|
756
|
+
if (values.offset !== undefined) throw new UsageError("--offset works with --lo, --hi and --value, not with --expr");
|
|
757
|
+
request.expression = values.expr as string;
|
|
758
|
+
} else {
|
|
759
|
+
request.symbol = values[select] as string;
|
|
760
|
+
if (values.offset !== undefined) request.offset = offsetOf(values.offset, "--offset");
|
|
761
|
+
}
|
|
762
|
+
return (db) => {
|
|
763
|
+
const result = setOperand(db, { ...context, image: operandImage(db, space) }, request);
|
|
764
|
+
return { revision: result.revision, operand: operand(result.current), ...(result.previous === undefined ? {} : { previous: operand(result.previous) }) };
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** A comma list of params or args; an empty text is an empty list. */
|
|
769
|
+
const commaList = (value: string) => (value.trim() === "" ? [] : value.split(",").map((item) => item.trim()));
|
|
770
|
+
|
|
771
|
+
/** The text of an option, or of the project file that a --*-file option names; exactly one of them. */
|
|
772
|
+
function textOrFile(values: Record<string, unknown>, option: string, what: string): string | undefined {
|
|
773
|
+
const inline = values[option] as string | undefined;
|
|
774
|
+
const file = values[`${option}-file`] as string | undefined;
|
|
775
|
+
if (inline !== undefined && file !== undefined) throw new UsageError(`give --${option} or --${option}-file, not both`);
|
|
776
|
+
if (file !== undefined) return readProjectFile(file).bytes.toString("utf8");
|
|
777
|
+
if (inline === undefined) return undefined;
|
|
778
|
+
if (inline === "") throw new UsageError(`${what} is empty`);
|
|
779
|
+
return inline;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** patch, macro and use: the writes of the generated source (D72, D73). */
|
|
783
|
+
function sourceOperation(command: "patch" | "macro" | "use", args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
784
|
+
const comment = values.comment as string | undefined;
|
|
785
|
+
switch (command) {
|
|
786
|
+
case "patch": {
|
|
787
|
+
if (args[2] === undefined) throw new UsageError("the switch name is missing");
|
|
788
|
+
const code = textOrFile(values, "code", "--code");
|
|
789
|
+
if ((code === undefined) === (values.nop !== true)) throw new UsageError("patch needs exactly one of --code, --code-file or --nop");
|
|
790
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), switch: args[2], code: code ?? "", ...(comment === undefined ? {} : { comment }) };
|
|
791
|
+
return (db) => {
|
|
792
|
+
const result = setPatch(db, { ...context, image: operandImage(db, space) }, request);
|
|
793
|
+
return { revision: result.revision, patch: patch(result.current), ...(result.previous === undefined ? {} : { previous: patch(result.previous) }) };
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
case "macro": {
|
|
797
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
798
|
+
const body = textOrFile(values, "body", "--body");
|
|
799
|
+
if (body === undefined) throw new UsageError("macro needs --body or --body-file");
|
|
800
|
+
const request = { space, name: args[0], params: commaList((values.params as string | undefined) ?? ""), body, ...(comment === undefined ? {} : { comment }) };
|
|
801
|
+
return (db) => {
|
|
802
|
+
const result = setMacro(db, context, request);
|
|
803
|
+
return { revision: result.revision, macro: macro(result.current), ...(result.previous === undefined ? {} : { previous: macro(result.previous) }) };
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
case "use": {
|
|
807
|
+
if (args[2] === undefined) throw new UsageError("the macro name is missing");
|
|
808
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end"), macro: args[2], args: commaList((values.args as string | undefined) ?? "") };
|
|
809
|
+
return (db) => {
|
|
810
|
+
const result = setMacroUse(db, { ...context, image: operandImage(db, space) }, request);
|
|
811
|
+
return { revision: result.revision, use: macroUse(result.current), ...(result.previous === undefined ? {} : { previous: macroUse(result.previous) }) };
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
818
|
+
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
819
|
+
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
820
|
+
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
821
|
+
let parsed: unknown;
|
|
822
|
+
try {
|
|
823
|
+
parsed = JSON.parse(text);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
|
|
826
|
+
}
|
|
827
|
+
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
828
|
+
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
829
|
+
const checked = operations.some((operation) => operation.op === "operand" || operation.op === "patch" || operation.op === "use");
|
|
830
|
+
return (db) => {
|
|
831
|
+
const applied = applyBatch(db, checked ? { ...context, image: operandImage(db, space) } : context, operations);
|
|
832
|
+
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
833
|
+
const replaced = applied.steps.flatMap((step, index) =>
|
|
834
|
+
step.previous === undefined
|
|
835
|
+
? []
|
|
836
|
+
: [{ write: index + 1, op: operations[index]!.op, previous: shownPrevious(step.previous) }],
|
|
837
|
+
);
|
|
838
|
+
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** A row that a write of a batch replaced, in its output shape. */
|
|
843
|
+
function shownPrevious(row: NonNullable<BatchStep["previous"]>) {
|
|
844
|
+
if (Array.isArray(row)) return row.map(region);
|
|
845
|
+
if ("select" in row) return operand(row);
|
|
846
|
+
if ("switch" in row) return patch(row);
|
|
847
|
+
if ("body" in row) return macro(row);
|
|
848
|
+
if ("macro" in row) return macroUse(row);
|
|
849
|
+
return symbol(row);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
853
|
+
rename: ["address", "name", "kind"],
|
|
854
|
+
"remove-symbol": ["address"],
|
|
855
|
+
classify: ["start", "end", "type", "element"],
|
|
856
|
+
unclassify: ["start", "end"],
|
|
857
|
+
comment: ["address", "placement", "text"],
|
|
858
|
+
uncomment: ["address", "placement"],
|
|
859
|
+
reference: ["from", "to", "kind"],
|
|
860
|
+
unreference: ["from", "to", "kind"],
|
|
861
|
+
module: ["start", "file", "title"],
|
|
862
|
+
unmodule: ["start"],
|
|
863
|
+
operand: ["address", "select", "symbol", "offset", "expression"],
|
|
864
|
+
unoperand: ["address"],
|
|
865
|
+
patch: ["start", "end", "switch", "code", "comment"],
|
|
866
|
+
unpatch: ["start"],
|
|
867
|
+
macro: ["name", "params", "body", "comment"],
|
|
868
|
+
unmacro: ["name"],
|
|
869
|
+
use: ["start", "end", "macro", "args"],
|
|
870
|
+
unuse: ["start"],
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
/** A write of a batch as JSON again: addresses as $ and four hex digits, the C64 space left out. */
|
|
874
|
+
function batchJson(write: BatchOperation): Record<string, unknown> {
|
|
875
|
+
const out: Record<string, unknown> = {};
|
|
876
|
+
for (const [key, value] of Object.entries(write)) {
|
|
877
|
+
if (key === "space" && value === DEFAULT_SPACE) continue;
|
|
878
|
+
out[key] = ["address", "start", "end", "from", "to"].includes(key) && typeof value === "number" ? formatC64Address(value) : value;
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Reads the batches, joins them, and writes the joined batch to --out (D79). One name or one module file at two addresses writes nothing. */
|
|
884
|
+
function joinCommand(files: string[], out: string | undefined, space: string): Record<string, unknown> {
|
|
885
|
+
if (files.length < 2) throw new UsageError("join needs two or more batch files");
|
|
886
|
+
if (out === undefined) throw new UsageError("join needs --out <file>");
|
|
887
|
+
const batches: Batch[] = files.map((file) => {
|
|
888
|
+
let value: unknown;
|
|
889
|
+
try {
|
|
890
|
+
value = JSON.parse(readProjectFile(file).bytes.toString("utf8"));
|
|
891
|
+
} catch (error) {
|
|
892
|
+
throw new UsageError(`${file}: ${(error as Error).message}`);
|
|
893
|
+
}
|
|
894
|
+
const shaped = Array.isArray(value) ? { ops: value } : (value as { owner?: unknown; ops?: unknown });
|
|
895
|
+
if (typeof shaped !== "object" || shaped === null || !Array.isArray(shaped.ops)) throw new UsageError(`${file}: a batch is a list of writes, or {"owner": "$1000-$1fff", "ops": [...]}`);
|
|
896
|
+
let owner: Batch["owner"];
|
|
897
|
+
if (shaped.owner !== undefined) {
|
|
898
|
+
const parts = typeof shaped.owner === "string" ? shaped.owner.split("-") : [];
|
|
899
|
+
if (parts.length !== 2) throw new UsageError(`${file}: owner must be "<start>-<end>", for example "$1000-$1fff"`);
|
|
900
|
+
owner = { start: address(parts[0], `${file}: owner`), end: address(parts[1], `${file}: owner`) };
|
|
901
|
+
if (owner.end < owner.start) throw new UsageError(`${file}: the owner range ends before it starts`);
|
|
902
|
+
}
|
|
903
|
+
const ops = (shaped.ops as unknown[]).map((item, index) => batchOperation(item, `${file}: write ${index + 1}`, space));
|
|
904
|
+
return { file, ...(owner === undefined ? {} : { owner }), ops };
|
|
905
|
+
});
|
|
906
|
+
const owned = batches.filter((batch) => batch.owner !== undefined);
|
|
907
|
+
for (const [index, batch] of owned.entries()) {
|
|
908
|
+
const other = owned.slice(index + 1).find((next) => next.owner!.start <= batch.owner!.end && batch.owner!.start <= next.owner!.end);
|
|
909
|
+
if (other !== undefined) throw new UsageError(`the owner ranges of ${batch.file} and ${other.file} overlap; give each address one owner`);
|
|
910
|
+
}
|
|
911
|
+
const joined = joinBatches(batches);
|
|
912
|
+
const choice = (item: JoinChoice) => ({ ...item, at: typeof item.at === "number" ? formatC64Address(item.at) : item.at, ...(item.space === DEFAULT_SPACE ? { space: undefined } : {}) });
|
|
913
|
+
const clash = (item: JoinClash) => ({ ...item, ...(item.space === DEFAULT_SPACE ? { space: undefined } : {}), places: item.places.map((place) => ({ address: formatC64Address(place.address), file: place.file })) });
|
|
914
|
+
const choices = joined.choices.map(choice);
|
|
915
|
+
if (joined.clashes.length > 0) {
|
|
916
|
+
process.exitCode = 1;
|
|
917
|
+
return { joined: false, clashes: joined.clashes.map(clash), choices, remedy: "Give each name and each module file one address in the batches, then join again." };
|
|
918
|
+
}
|
|
919
|
+
const bytes = Buffer.from(`${JSON.stringify(joined.ops.map(batchJson), null, 1)}\n`, "utf8");
|
|
920
|
+
writeProjectFile(out, bytes, { what: "--out" });
|
|
921
|
+
return { joined: true, out, writes: joined.ops.length, repeated: joined.repeated, choices, apply: `knowledge.ts apply ${out}` };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
925
|
+
function batchOperation(item: unknown, at: string, space: string): BatchOperation {
|
|
926
|
+
const operation = batchFields(item, at);
|
|
927
|
+
const own = (item as Record<string, unknown>).space;
|
|
928
|
+
if (own !== undefined && typeof own !== "string") throw new UsageError(`${at}: space must be a string`);
|
|
929
|
+
return { ...operation, space: own ?? space };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** The fields of one write of a batch, with its addresses parsed. */
|
|
933
|
+
function batchFields(item: unknown, at: string): BatchOperation {
|
|
934
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
|
|
935
|
+
const fields = item as Record<string, unknown>;
|
|
936
|
+
const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
|
|
937
|
+
const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
|
|
938
|
+
if (unknown.length > 0) throw new UsageError(`${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
|
|
939
|
+
const text = (key: string) => {
|
|
940
|
+
if (typeof fields[key] !== "string") throw new UsageError(`${at}: ${key} must be a string`);
|
|
941
|
+
return fields[key] as string;
|
|
942
|
+
};
|
|
943
|
+
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
944
|
+
// A list is a JSON list of strings or a comma list.
|
|
945
|
+
const list = (key: string) => {
|
|
946
|
+
const value = fields[key];
|
|
947
|
+
if (value === undefined) return [];
|
|
948
|
+
if (typeof value === "string") return commaList(value);
|
|
949
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new UsageError(`${at}: ${key} must be a list of strings or a comma list`);
|
|
950
|
+
return (value as string[]).map((item) => item.trim());
|
|
951
|
+
};
|
|
952
|
+
const optional = (key: string) => (fields[key] === undefined ? {} : { [key]: text(key) });
|
|
953
|
+
switch (op) {
|
|
954
|
+
case "rename":
|
|
955
|
+
return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
|
|
956
|
+
case "remove-symbol":
|
|
957
|
+
return { op, address: place("address") };
|
|
958
|
+
case "classify": {
|
|
959
|
+
if (fields.element !== undefined && typeof fields.element !== "number") throw new UsageError(`${at}: element must be a number`);
|
|
960
|
+
return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
|
|
961
|
+
}
|
|
962
|
+
case "unclassify":
|
|
963
|
+
return { op, start: place("start"), end: place("end") };
|
|
964
|
+
case "comment":
|
|
965
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`), text: text("text") };
|
|
966
|
+
case "uncomment":
|
|
967
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`) };
|
|
968
|
+
case "module":
|
|
969
|
+
return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
|
|
970
|
+
case "unmodule":
|
|
971
|
+
return { op, start: place("start") };
|
|
972
|
+
case "operand": {
|
|
973
|
+
const select = oneOf(OPERAND_SELECTS, text("select"), `${at}: select`);
|
|
974
|
+
return {
|
|
975
|
+
op,
|
|
976
|
+
address: place("address"),
|
|
977
|
+
select,
|
|
978
|
+
...(fields.symbol === undefined ? {} : { symbol: text("symbol") }),
|
|
979
|
+
...(fields.offset === undefined ? {} : { offset: offsetOf(fields.offset, `${at}: offset`) }),
|
|
980
|
+
...(fields.expression === undefined ? {} : { expression: text("expression") }),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
case "unoperand":
|
|
984
|
+
return { op, address: place("address") };
|
|
985
|
+
case "patch":
|
|
986
|
+
return { op, start: place("start"), end: place("end"), switch: text("switch"), code: fields.code === undefined ? "" : text("code"), ...optional("comment") };
|
|
987
|
+
case "macro":
|
|
988
|
+
return { op, name: text("name"), params: list("params"), body: text("body"), ...optional("comment") };
|
|
989
|
+
case "unmacro":
|
|
990
|
+
return { op, name: text("name") };
|
|
991
|
+
case "use":
|
|
992
|
+
return { op, start: place("start"), end: place("end"), macro: text("macro"), args: list("args") };
|
|
993
|
+
case "unpatch":
|
|
994
|
+
case "unuse":
|
|
995
|
+
return { op, start: place("start") };
|
|
996
|
+
case "reference":
|
|
997
|
+
case "unreference":
|
|
998
|
+
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
197
1002
|
type WriteOperation = (db: DatabaseSync) => unknown;
|
|
198
1003
|
|
|
199
|
-
function writeOperation(command: string, args: string[], kind: string | undefined, context: WriteContext): WriteOperation {
|
|
1004
|
+
function writeOperation(command: string, args: string[], kind: string | undefined, element: string | undefined, context: WriteContext, title: string | undefined, space: string): WriteOperation {
|
|
200
1005
|
switch (command) {
|
|
201
1006
|
case "rename": {
|
|
202
1007
|
if (args[1] === undefined) throw new UsageError("name is missing");
|
|
203
|
-
const request: { address: number; name: string; kind?: SymbolKind } = { address: address(args[0]), name: args[1] };
|
|
1008
|
+
const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
|
|
204
1009
|
if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
|
|
205
1010
|
return (db) => {
|
|
206
1011
|
const result = renameSymbol(db, context, request);
|
|
@@ -208,45 +1013,89 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
208
1013
|
};
|
|
209
1014
|
}
|
|
210
1015
|
case "remove-symbol": {
|
|
211
|
-
const request = { address: address(args[0]) };
|
|
1016
|
+
const request = { space, address: address(args[0]) };
|
|
212
1017
|
return (db) => ({ revision: removeSymbol(db, context, request).revision });
|
|
213
1018
|
}
|
|
214
1019
|
case "classify": {
|
|
215
|
-
const request = {
|
|
1020
|
+
const request = {
|
|
1021
|
+
space,
|
|
1022
|
+
start: address(args[0], "start"),
|
|
1023
|
+
end: address(args[1], "end"),
|
|
1024
|
+
type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
|
|
1025
|
+
...(element === undefined ? {} : { element: integer(element, "--element") }),
|
|
1026
|
+
};
|
|
216
1027
|
return (db) => {
|
|
217
1028
|
const result = classifyRegion(db, context, request);
|
|
218
1029
|
return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
|
|
219
1030
|
};
|
|
220
1031
|
}
|
|
221
1032
|
case "unclassify": {
|
|
222
|
-
const request = { start: address(args[0], "start"), end: address(args[1], "end") };
|
|
1033
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
|
|
223
1034
|
return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
|
|
224
1035
|
}
|
|
225
1036
|
case "comment": {
|
|
226
1037
|
if (args.length < 3) throw new UsageError("text is missing");
|
|
227
|
-
const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
1038
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
228
1039
|
return (db) => {
|
|
229
1040
|
const result = setComment(db, context, request);
|
|
230
1041
|
return { revision: result.revision, comment: comment(result.current) };
|
|
231
1042
|
};
|
|
232
1043
|
}
|
|
233
1044
|
case "uncomment": {
|
|
234
|
-
const request = { address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
1045
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
235
1046
|
return (db) => ({ revision: removeComment(db, context, request).revision });
|
|
236
1047
|
}
|
|
237
1048
|
case "reference": {
|
|
238
|
-
const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
1049
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
239
1050
|
return (db) => {
|
|
240
1051
|
const result = addReference(db, context, request);
|
|
241
1052
|
return { revision: result.revision, reference: reference(result.current) };
|
|
242
1053
|
};
|
|
243
1054
|
}
|
|
244
1055
|
case "unreference": {
|
|
245
|
-
const request = { from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
1056
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
246
1057
|
return (db) => ({ revision: removeReference(db, context, request).revision });
|
|
247
1058
|
}
|
|
1059
|
+
case "module": {
|
|
1060
|
+
if (args[1] === undefined) throw new UsageError("file is missing");
|
|
1061
|
+
const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
|
|
1062
|
+
return (db) => {
|
|
1063
|
+
const result = setModule(db, context, request);
|
|
1064
|
+
return { revision: result.revision, module: module(result.current) };
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
case "unmodule": {
|
|
1068
|
+
const request = { space, start: address(args[0], "start") };
|
|
1069
|
+
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
1070
|
+
}
|
|
1071
|
+
case "unoperand": {
|
|
1072
|
+
const request = { space, address: address(args[0]) };
|
|
1073
|
+
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
1074
|
+
}
|
|
1075
|
+
case "unpatch": {
|
|
1076
|
+
const request = { space, start: address(args[0], "start") };
|
|
1077
|
+
return (db) => ({ revision: removePatch(db, context, request).revision });
|
|
1078
|
+
}
|
|
1079
|
+
case "unmacro": {
|
|
1080
|
+
if (args[0] === undefined) throw new UsageError("the macro name is missing");
|
|
1081
|
+
const request = { space, name: args[0] };
|
|
1082
|
+
return (db) => ({ revision: removeMacro(db, context, request).revision });
|
|
1083
|
+
}
|
|
1084
|
+
case "unuse": {
|
|
1085
|
+
const request = { space, start: address(args[0], "start") };
|
|
1086
|
+
return (db) => ({ revision: removeMacroUse(db, context, request).revision });
|
|
1087
|
+
}
|
|
1088
|
+
case "merge": {
|
|
1089
|
+
if (args.length > 0) throw new UsageError("merge takes no arguments");
|
|
1090
|
+
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
1091
|
+
return (db) => {
|
|
1092
|
+
const merged = mergeConflicts(db, context, platform);
|
|
1093
|
+
return { revision: merged.revision, choices: merged.current.map(showChoice) };
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
248
1096
|
case "revert": {
|
|
249
|
-
|
|
1097
|
+
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
1098
|
+
const request = { revision: args[0] };
|
|
250
1099
|
return (db) => ({ revision: revert(db, context, request).revision });
|
|
251
1100
|
}
|
|
252
1101
|
default:
|
|
@@ -254,4 +1103,4 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
254
1103
|
}
|
|
255
1104
|
}
|
|
256
1105
|
|
|
257
|
-
await runScript(() => run(
|
|
1106
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|