@henols/c64-re-tools 2.0.0-rc.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -116
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +22 -6
- package/skills/c64-assembler/scripts/assemble.ts +42 -14
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +10 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +13 -4
- package/skills/c64-disk/scripts/disk.ts +7 -7
- package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +109 -0
- package/skills/c64-docs/scripts/lib/links.ts +167 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +239 -0
- package/skills/c64-emulator/SKILL.md +16 -5
- package/skills/c64-knowledge/SKILL.md +140 -18
- package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +11 -2
- package/skills/c64-memory-map/scripts/memmap.ts +2 -2
- package/skills/c64-provenance/SKILL.md +66 -11
- package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
- package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
- package/skills/c64-provenance/scripts/provenance.ts +242 -40
- package/skills/c64-reverse-engineering/SKILL.md +51 -26
- package/skills/c64-source/SKILL.md +76 -28
- package/skills/c64-source/scripts/generate.ts +425 -127
- package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +35 -18
- package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
- package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +36 -7
- package/skills/c64-testing/references/scenario-format.md +12 -8
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
- package/skills/c64-testing/scripts/run.ts +73 -21
- package/skills/c64-testing/scripts/test.ts +5 -5
- package/skills/c64-unpacker/SKILL.md +23 -9
- package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
- package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
- package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
- package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
- package/skills/c64-unpacker/scripts/unpack.ts +41 -24
- package/src/c64.ts +103 -5
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +7 -4
- package/src/cli/status.ts +70 -3
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +18 -4
- package/src/host/server.ts +10 -3
- package/src/host/tools/petcat.ts +1 -1
- package/src/host/tools/status.ts +1 -1
- package/src/host/vice/adapter.ts +4 -3
- package/src/host/vice/process.ts +2 -2
- package/src/host/vice/screen.ts +1 -42
- package/src/host/vice/session.ts +196 -23
- package/src/host-client/autostart.ts +1 -1
- package/src/host-client/connect.ts +1 -1
- package/src/host-client/coverage.ts +2 -29
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +9 -4
- package/src/host-client/tools.ts +1 -0
- package/src/host-client/transfer.ts +39 -11
- package/src/host-client/vice-session.ts +33 -16
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +70 -22
- package/src/knowledge/import.ts +64 -55
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +266 -0
- package/src/knowledge/read.ts +258 -27
- package/src/knowledge/schema.ts +199 -75
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +740 -57
- package/src/mcp/main.ts +1 -1
- package/src/mcp/output.ts +15 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools/debug.ts +97 -16
- package/src/mcp/tools/execution.ts +38 -5
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +18 -13
- package/src/mcp/tools/memory.ts +17 -8
- package/src/mcp/tools/video.ts +9 -9
- package/src/native/discover.ts +2 -2
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +1 -1
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +1 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/types.ts +14 -8
- package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
- package/src/platform.ts +70 -7
- package/src/project.ts +180 -3
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/tools.ts +2 -16
- package/src/protocol/vice.ts +124 -15
- package/src/script.ts +46 -7
- package/skills/c64-assembler/scripts/compare.ts +0 -84
- package/skills/c64-source/scripts/source.ts +0 -595
package/src/knowledge/history.ts
CHANGED
|
@@ -6,53 +6,72 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { KnowledgeError } from "./database.ts";
|
|
7
7
|
import {
|
|
8
8
|
COMMENT_COLUMNS,
|
|
9
|
+
MACRO_COLUMNS,
|
|
10
|
+
MACRO_USE_COLUMNS,
|
|
9
11
|
MODULE_COLUMNS,
|
|
12
|
+
OPERAND_COLUMNS,
|
|
13
|
+
PATCH_COLUMNS,
|
|
10
14
|
REFERENCE_COLUMNS,
|
|
11
15
|
REGION_COLUMNS,
|
|
16
|
+
resolveRevision,
|
|
12
17
|
SUPPRESSION_COLUMNS,
|
|
13
18
|
SYMBOL_COLUMNS,
|
|
14
19
|
toComment,
|
|
20
|
+
toMacro,
|
|
21
|
+
toMacroUse,
|
|
15
22
|
toModule,
|
|
23
|
+
toOperand,
|
|
24
|
+
toPatch,
|
|
16
25
|
toReference,
|
|
17
26
|
toRegion,
|
|
18
27
|
toSuppression,
|
|
19
28
|
toSymbol,
|
|
20
29
|
type CommentRow,
|
|
30
|
+
type MacroRow,
|
|
31
|
+
type MacroUseRow,
|
|
21
32
|
type ModuleRow,
|
|
33
|
+
type OperandRow,
|
|
34
|
+
type PatchRow,
|
|
22
35
|
type ReferenceRow,
|
|
23
36
|
type RegionRow,
|
|
24
37
|
type SuppressionRow,
|
|
25
38
|
type SymbolRow,
|
|
26
39
|
} from "./read.ts";
|
|
27
40
|
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
41
|
+
import { compareText } from "./text.ts";
|
|
28
42
|
import type { Evidence } from "./write.ts";
|
|
29
43
|
|
|
30
44
|
export interface Revision {
|
|
31
|
-
id:
|
|
45
|
+
id: string;
|
|
32
46
|
createdAt: string;
|
|
33
47
|
origin: Origin;
|
|
34
48
|
operation: string;
|
|
35
49
|
description?: string;
|
|
36
50
|
inputHash?: string;
|
|
37
51
|
toolVersion?: string;
|
|
38
|
-
/** What the change rests on
|
|
52
|
+
/** What the change rests on. */
|
|
39
53
|
evidence?: Evidence[];
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
43
57
|
export type HistoryEntry =
|
|
44
|
-
| { entity: "symbol"; row: SymbolRow; from:
|
|
45
|
-
| { entity: "
|
|
46
|
-
| { entity: "
|
|
47
|
-
| { entity: "
|
|
48
|
-
| { entity: "
|
|
49
|
-
| { entity: "
|
|
58
|
+
| { entity: "symbol"; row: SymbolRow; from: string; to?: string }
|
|
59
|
+
| { entity: "analyzer-name"; row: SymbolRow; from: string; to?: string }
|
|
60
|
+
| { entity: "region"; row: RegionRow; from: string; to?: string }
|
|
61
|
+
| { entity: "comment"; row: CommentRow; from: string; to?: string }
|
|
62
|
+
| { entity: "reference"; row: ReferenceRow; from: string; to?: string }
|
|
63
|
+
| { entity: "suppression"; row: SuppressionRow; from: string; to?: string }
|
|
64
|
+
| { entity: "module"; row: ModuleRow; from: string; to?: string }
|
|
65
|
+
| { entity: "operand"; row: OperandRow; from: string; to?: string }
|
|
66
|
+
| { entity: "patch"; row: PatchRow; from: string; to?: string }
|
|
67
|
+
| { entity: "macro"; row: MacroRow; from: string; to?: string }
|
|
68
|
+
| { entity: "macro-use"; row: MacroUseRow; from: string; to?: string };
|
|
50
69
|
|
|
51
70
|
type Raw = Record<string, unknown>;
|
|
52
71
|
|
|
53
72
|
function toRevision(row: Raw): Revision {
|
|
54
73
|
const revision: Revision = {
|
|
55
|
-
id: row.id as
|
|
74
|
+
id: row.id as string,
|
|
56
75
|
createdAt: row.created_at as string,
|
|
57
76
|
origin: row.origin as Origin,
|
|
58
77
|
operation: row.operation as string,
|
|
@@ -64,13 +83,14 @@ function toRevision(row: Raw): Revision {
|
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
|
|
67
|
-
const result = { entity, row, from: raw.valid_from_revision as
|
|
68
|
-
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as
|
|
86
|
+
const result = { entity, row, from: raw.valid_from_revision as string } as HistoryEntry;
|
|
87
|
+
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as string;
|
|
69
88
|
return result;
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
/** Revision ids sort in time order as text; a current row sorts after every closed one. */
|
|
72
92
|
function bySequence(a: HistoryEntry, b: HistoryEntry): number {
|
|
73
|
-
return a.from
|
|
93
|
+
return compareText(a.from, b.from) || compareText(a.to ?? "~", b.to ?? "~");
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
/** Every row, current or closed, that concerns one address of a space, oldest first. */
|
|
@@ -79,6 +99,7 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
79
99
|
const validity = ", valid_to_revision";
|
|
80
100
|
const entries: HistoryEntry[] = [];
|
|
81
101
|
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
|
|
102
|
+
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM analyzer_names WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("analyzer-name", toSymbol(raw), raw));
|
|
82
103
|
for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
83
104
|
entries.push(entry("region", toRegion(raw), raw));
|
|
84
105
|
}
|
|
@@ -92,9 +113,24 @@ export function historyAt(db: DatabaseSync | undefined, address: number, space:
|
|
|
92
113
|
entries.push(entry("suppression", toSuppression(raw), raw));
|
|
93
114
|
}
|
|
94
115
|
for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
|
|
116
|
+
for (const raw of db.prepare(`SELECT ${OPERAND_COLUMNS}${validity} FROM operands WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("operand", toOperand(raw), raw));
|
|
117
|
+
for (const raw of db.prepare(`SELECT ${PATCH_COLUMNS}${validity} FROM patches WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
118
|
+
entries.push(entry("patch", toPatch(raw), raw));
|
|
119
|
+
}
|
|
120
|
+
for (const raw of db.prepare(`SELECT ${MACRO_USE_COLUMNS}${validity} FROM macro_uses WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
121
|
+
entries.push(entry("macro-use", toMacroUse(raw), raw));
|
|
122
|
+
}
|
|
95
123
|
return entries.sort(bySequence);
|
|
96
124
|
}
|
|
97
125
|
|
|
126
|
+
/** Every row of a macro with this name, and every use of it, oldest first. */
|
|
127
|
+
export function historyOfMacro(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
128
|
+
if (db === undefined) return [];
|
|
129
|
+
const macros = (db.prepare(`SELECT ${MACRO_COLUMNS}, valid_to_revision FROM macros WHERE space = ? AND name = ?`).all(space, name) as Raw[]).map((raw) => entry("macro", toMacro(raw), raw));
|
|
130
|
+
const uses = (db.prepare(`SELECT ${MACRO_USE_COLUMNS}, valid_to_revision FROM macro_uses WHERE space = ? AND macro = ?`).all(space, name) as Raw[]).map((raw) => entry("macro-use", toMacroUse(raw), raw));
|
|
131
|
+
return [...macros, ...uses].sort(bySequence);
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
/** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
|
|
99
135
|
export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
100
136
|
if (db === undefined) return [];
|
|
@@ -104,7 +140,7 @@ export function historyNamed(db: DatabaseSync | undefined, name: string, space:
|
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
/** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
|
|
107
|
-
export function revisionNote(db: DatabaseSync | undefined, id:
|
|
143
|
+
export function revisionNote(db: DatabaseSync | undefined, id: string): { reason?: string; evidence?: Evidence[] } {
|
|
108
144
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
109
145
|
if (db === undefined || raw === undefined) return {};
|
|
110
146
|
const { description, evidence } = withEvidence(db, toRevision(raw));
|
|
@@ -113,20 +149,25 @@ export function revisionNote(db: DatabaseSync | undefined, id: number): { reason
|
|
|
113
149
|
|
|
114
150
|
/** The revision with its evidence, when it has any. */
|
|
115
151
|
function withEvidence(db: DatabaseSync, revision: Revision): Revision {
|
|
116
|
-
const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
152
|
+
const rows = db.prepare("SELECT kind, ref, sha256, space FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
117
153
|
if (rows.length === 0) return revision;
|
|
118
|
-
const evidence = rows.map((row): Evidence => ({
|
|
154
|
+
const evidence = rows.map((row): Evidence => ({
|
|
155
|
+
kind: row.kind as EvidenceKind,
|
|
156
|
+
ref: row.ref as string,
|
|
157
|
+
...(row.sha256 === null ? {} : { sha256: row.sha256 as string }),
|
|
158
|
+
...(row.space === null ? {} : { space: row.space as Space }),
|
|
159
|
+
}));
|
|
119
160
|
return { ...revision, evidence };
|
|
120
161
|
}
|
|
121
162
|
|
|
122
163
|
/** Revisions, newest first; with `before`, only revisions older than it. */
|
|
123
|
-
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?:
|
|
164
|
+
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: string } = {}): Revision[] {
|
|
124
165
|
if (db === undefined) return [];
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.prepare("SELECT * FROM revisions
|
|
128
|
-
.all(options.before
|
|
129
|
-
).map((row) => withEvidence(db, toRevision(row)));
|
|
166
|
+
const rows =
|
|
167
|
+
options.before === undefined
|
|
168
|
+
? db.prepare("SELECT * FROM revisions ORDER BY id DESC LIMIT ?").all(options.limit ?? 50)
|
|
169
|
+
: db.prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?").all(resolveRevision(db, options.before), options.limit ?? 50);
|
|
170
|
+
return (rows as Raw[]).map((row) => withEvidence(db, toRevision(row)));
|
|
130
171
|
}
|
|
131
172
|
|
|
132
173
|
export interface RevisionChanges {
|
|
@@ -138,16 +179,22 @@ export interface RevisionChanges {
|
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
/** One revision and exactly what it changed. */
|
|
141
|
-
export function revision(db: DatabaseSync | undefined,
|
|
182
|
+
export function revision(db: DatabaseSync | undefined, text: string): RevisionChanges {
|
|
183
|
+
const id = resolveRevision(db, text);
|
|
142
184
|
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
143
185
|
if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
|
|
144
186
|
const tables = [
|
|
145
187
|
["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
|
|
188
|
+
["analyzer-name", "analyzer_names", SYMBOL_COLUMNS, toSymbol],
|
|
146
189
|
["region", "regions", REGION_COLUMNS, toRegion],
|
|
147
190
|
["comment", "comments", COMMENT_COLUMNS, toComment],
|
|
148
191
|
["reference", '"references"', REFERENCE_COLUMNS, toReference],
|
|
149
192
|
["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
|
|
150
193
|
["module", "modules", MODULE_COLUMNS, toModule],
|
|
194
|
+
["operand", "operands", OPERAND_COLUMNS, toOperand],
|
|
195
|
+
["patch", "patches", PATCH_COLUMNS, toPatch],
|
|
196
|
+
["macro", "macros", MACRO_COLUMNS, toMacro],
|
|
197
|
+
["macro-use", "macro_uses", MACRO_USE_COLUMNS, toMacroUse],
|
|
151
198
|
] as const;
|
|
152
199
|
const added: HistoryEntry[] = [];
|
|
153
200
|
const closed: HistoryEntry[] = [];
|
|
@@ -161,3 +208,4 @@ export function revision(db: DatabaseSync | undefined, id: number): RevisionChan
|
|
|
161
208
|
}
|
|
162
209
|
return { revision: withEvidence(db, toRevision(raw)), added, closed };
|
|
163
210
|
}
|
|
211
|
+
|
package/src/knowledge/import.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// another analyzer are never replaced: a finding that contradicts one is
|
|
8
8
|
// reported as a conflict. Code against data is a contradiction; a different
|
|
9
9
|
// name or a finer data type for the same kind of memory is not. One exception
|
|
10
|
-
|
|
10
|
+
//: where a run executed an instruction start, a code finding replaces
|
|
11
11
|
// the data of another analyzer, never of the user, the LLM or a heuristic.
|
|
12
12
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
@@ -15,21 +15,20 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
15
15
|
|
|
16
16
|
import { KnowledgeError, transaction } from "./database.ts";
|
|
17
17
|
import {
|
|
18
|
+
analyzerNamed,
|
|
18
19
|
REFERENCE_COLUMNS,
|
|
19
20
|
referenceSuppressed,
|
|
20
21
|
regionsOverlapping,
|
|
21
22
|
suppressionsOverlapping,
|
|
22
23
|
SYMBOL_COLUMNS,
|
|
23
24
|
symbolAt,
|
|
24
|
-
symbolNamed,
|
|
25
|
-
symbolSuppressed,
|
|
26
25
|
toReference,
|
|
27
26
|
toSymbol,
|
|
28
27
|
type ReferenceRow,
|
|
29
28
|
type RegionRow,
|
|
30
29
|
type SymbolRow,
|
|
31
30
|
} from "./read.ts";
|
|
32
|
-
import { CODE_REGION_TYPES, DEFAULT_SPACE, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
31
|
+
import { CODE_REGION_TYPES, DEFAULT_SPACE, POINTER_KIND, REFERENCE_KINDS, REGION_TYPES, SYMBOL_KINDS, type Origin, type ReferenceKind, type RegionType, type Space, type SymbolKind } from "./schema.ts";
|
|
33
32
|
import { Change, checkAddress, checkSpace, hex, subtract, SYMBOL_NAME, type AddressRange, type Evidence } from "./write.ts";
|
|
34
33
|
|
|
35
34
|
export type { AddressRange };
|
|
@@ -39,7 +38,7 @@ export type Analyzer = (typeof ANALYZERS)[number];
|
|
|
39
38
|
|
|
40
39
|
export interface NormalizedFindings {
|
|
41
40
|
analyzer: Analyzer;
|
|
42
|
-
/** The address space that the analyzed image belongs to
|
|
41
|
+
/** The address space that the analyzed image belongs to; the C64 when absent. */
|
|
43
42
|
space?: Space;
|
|
44
43
|
/** The address ranges the analyzer examined; it makes claims only there. */
|
|
45
44
|
coverage: AddressRange[];
|
|
@@ -49,13 +48,13 @@ export interface NormalizedFindings {
|
|
|
49
48
|
regions: Array<{ start: number; end: number; type: RegionType }>;
|
|
50
49
|
/** Owned and covered by their source address. */
|
|
51
50
|
references: Array<{ from: number; to: number; kind: ReferenceKind }>;
|
|
52
|
-
/** True for an address where a coverage run executed an instruction start
|
|
51
|
+
/** True for an address where a coverage run executed an instruction start. */
|
|
53
52
|
executed?: (address: number) => boolean;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export interface ImportContext {
|
|
57
56
|
/** The revision the caller read; the import is refused if knowledge changed since. */
|
|
58
|
-
expectedRevision?:
|
|
57
|
+
expectedRevision?: string;
|
|
59
58
|
description?: string;
|
|
60
59
|
/** The SHA-256 of the analyzed image, as lowercase hex. */
|
|
61
60
|
inputHash?: string;
|
|
@@ -86,13 +85,16 @@ export interface ImportCounts {
|
|
|
86
85
|
changed: number;
|
|
87
86
|
retired: number;
|
|
88
87
|
unchanged: number;
|
|
89
|
-
/** Findings at a place that a user/LLM removal blocks
|
|
88
|
+
/** Findings at a place that a user/LLM removal blocks; not written. */
|
|
90
89
|
suppressed: number;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
export interface ImportResult {
|
|
94
93
|
/** The new revision, or null when nothing changed. */
|
|
95
|
-
revision:
|
|
94
|
+
revision: string | null;
|
|
95
|
+
/** The analyzer names. */
|
|
96
|
+
names: ImportCounts;
|
|
97
|
+
/** The analyzer's own rows from an older version that the run closed in symbols: only `retired` counts. */
|
|
96
98
|
symbols: ImportCounts;
|
|
97
99
|
regions: ImportCounts;
|
|
98
100
|
references: ImportCounts;
|
|
@@ -163,6 +165,7 @@ export function validateFindings(findings: NormalizedFindings): AddressRange[] {
|
|
|
163
165
|
checkAddress(reference.to, "reference target");
|
|
164
166
|
if (!covering(reference.from, reference.from)) invalid(`the reference from ${hex(reference.from)} is outside the coverage.`);
|
|
165
167
|
if (!(REFERENCE_KINDS as readonly string[]).includes(reference.kind)) invalid(`the reference kind ${reference.kind} is not known.`);
|
|
168
|
+
if (reference.kind === POINTER_KIND) invalid(`only an operand expression writes a pointer reference, not an analyzer.`);
|
|
166
169
|
}
|
|
167
170
|
return coverage;
|
|
168
171
|
}
|
|
@@ -181,68 +184,73 @@ function ownReferences(db: DatabaseSync, analyzer: Analyzer, range: AddressRange
|
|
|
181
184
|
.map((row) => toReference(row as Record<string, unknown>));
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
function
|
|
187
|
+
function ownNames(db: DatabaseSync, analyzer: Analyzer, range: AddressRange, space: Space): SymbolRow[] {
|
|
188
|
+
return db
|
|
189
|
+
.prepare(`SELECT ${SYMBOL_COLUMNS} FROM current_analyzer_names WHERE space = ? AND origin = ? AND address BETWEEN ? AND ? ORDER BY address`)
|
|
190
|
+
.all(space, analyzer, range.start, range.end)
|
|
191
|
+
.map((row) => toSymbol(row as Record<string, unknown>));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The names of an analyzer are second names: they go to analyzer_names,
|
|
196
|
+
* one for each analyzer at an address, and never to symbols. A run replaces
|
|
197
|
+
* the analyzer's own names in its coverage, and closes the analyzer's old rows
|
|
198
|
+
* in symbols there (knowledge from an older version). A name that the user or the LLM
|
|
199
|
+
* gave the address already is not kept twice. A code or data kind that
|
|
200
|
+
* disagrees with the name of the address is a conflict.
|
|
201
|
+
*/
|
|
202
|
+
function importNames(db: DatabaseSync, change: Change, findings: NormalizedFindings, coverage: AddressRange[], result: ImportResult): void {
|
|
185
203
|
const { analyzer } = findings;
|
|
186
204
|
const space = findings.space ?? DEFAULT_SPACE;
|
|
205
|
+
for (const range of coverage) {
|
|
206
|
+
for (const own of ownSymbols(db, analyzer, range, space)) {
|
|
207
|
+
change.close("symbols", "space = ? AND address = ? AND origin = ?", space, own.address, analyzer);
|
|
208
|
+
result.symbols.retired++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
187
211
|
const wanted = new Map(findings.symbols.map((symbol) => [symbol.address, symbol]));
|
|
188
212
|
const settled = new Set<number>();
|
|
189
213
|
const replaced = new Set<number>();
|
|
190
|
-
// Close first, so a name can move between addresses within one import.
|
|
191
214
|
for (const range of coverage) {
|
|
192
|
-
for (const own of
|
|
215
|
+
for (const own of ownNames(db, analyzer, range, space)) {
|
|
193
216
|
const finding = wanted.get(own.address);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
result.symbols.retired++;
|
|
198
|
-
} else if (finding.name === own.name && finding.kind === own.kind) {
|
|
217
|
+
// A seeded run gives back the name of the user or the LLM: the analyzer's own name there stays.
|
|
218
|
+
const echo = finding !== undefined && symbolAt(db, own.address, space)?.name === finding.name;
|
|
219
|
+
if (echo || (finding !== undefined && finding.name === own.name && finding.kind === own.kind)) {
|
|
199
220
|
settled.add(own.address);
|
|
200
|
-
result.
|
|
201
|
-
|
|
202
|
-
change.close("symbols", "space = ? AND address = ?", space, own.address);
|
|
203
|
-
replaced.add(own.address);
|
|
221
|
+
result.names.unchanged++;
|
|
222
|
+
continue;
|
|
204
223
|
}
|
|
224
|
+
change.close("analyzer_names", "space = ? AND address = ? AND origin = ?", space, own.address, analyzer);
|
|
225
|
+
if (finding === undefined) result.names.retired++;
|
|
226
|
+
else replaced.add(own.address);
|
|
205
227
|
}
|
|
206
228
|
}
|
|
207
229
|
for (const finding of findings.symbols) {
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
result.
|
|
211
|
-
continue;
|
|
230
|
+
const current = symbolAt(db, finding.address, space);
|
|
231
|
+
if (current !== undefined && isCode(current.kind) !== isCode(finding.kind)) {
|
|
232
|
+
result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
if (current
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
result.symbols.retired++;
|
|
218
|
-
current = undefined;
|
|
219
|
-
}
|
|
220
|
-
if (current !== undefined) {
|
|
221
|
-
// A semantic or other-analyzer row stays; only a code/data disagreement matters.
|
|
222
|
-
if (isCode(current.kind) !== isCode(finding.kind)) {
|
|
223
|
-
result.conflicts.push({ category: "symbol", address: finding.address, reason: "kind", current, finding: { name: finding.name, kind: finding.kind } });
|
|
224
|
-
}
|
|
234
|
+
if (settled.has(finding.address)) continue;
|
|
235
|
+
if (current?.name === finding.name) {
|
|
236
|
+
// The analyzer only gives back the name of the address.
|
|
237
|
+
if (replaced.has(finding.address)) result.names.retired++;
|
|
225
238
|
continue;
|
|
226
239
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
result.symbols.retired++;
|
|
232
|
-
} else if (owner !== undefined) {
|
|
233
|
-
result.conflicts.push({ category: "symbol", address: finding.address, reason: "name-taken", current: owner, finding: { name: finding.name, kind: finding.kind } });
|
|
234
|
-
if (replaced.has(finding.address)) result.symbols.retired++;
|
|
235
|
-
continue;
|
|
240
|
+
// The analyzer moved its own name from an address outside the coverage.
|
|
241
|
+
for (const moved of analyzerNamed(db, finding.name, space).filter((row) => row.origin === analyzer)) {
|
|
242
|
+
change.close("analyzer_names", "space = ? AND address = ? AND origin = ?", space, moved.address, analyzer);
|
|
243
|
+
result.names.retired++;
|
|
236
244
|
}
|
|
237
|
-
change.
|
|
238
|
-
if (replaced.has(finding.address)) result.
|
|
239
|
-
else result.
|
|
245
|
+
change.insertAnalyzerName({ space, address: finding.address, name: finding.name, kind: finding.kind, origin: analyzer });
|
|
246
|
+
if (replaced.has(finding.address)) result.names.changed++;
|
|
247
|
+
else result.names.added++;
|
|
240
248
|
}
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
/** Analyzers whose regions a heuristic finding replaces where it lies
|
|
251
|
+
/** Analyzers whose regions a heuristic finding replaces where it lies: user/LLM > heuristic > DXA/Ghidra. */
|
|
244
252
|
const YIELDS_TO_HEURISTIC = new Set<Origin>(["dxa", "ghidra"]);
|
|
245
|
-
/** Analyzers whose data gives way to a code finding over an executed instruction start
|
|
253
|
+
/** Analyzers whose data gives way to a code finding over an executed instruction start. */
|
|
246
254
|
const YIELDS_TO_EXECUTED_CODE = new Set<Origin>(["dxa", "ghidra"]);
|
|
247
255
|
|
|
248
256
|
/** True when an address in the range is an executed instruction start. */
|
|
@@ -264,7 +272,7 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
264
272
|
}
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
|
-
// Executed code is evidence about its bytes
|
|
275
|
+
// Executed code is evidence about its bytes: another analyzer's data region under a code finding gives way where a run executed an instruction start.
|
|
268
276
|
for (const region of findings.regions.filter((candidate) => isCode(candidate.type))) {
|
|
269
277
|
for (const row of regionsOverlapping(db, region.start, region.end, space)) {
|
|
270
278
|
if (row.origin === analyzer || !YIELDS_TO_EXECUTED_CODE.has(row.origin) || isCode(row.type) || row.type === "dual-use") continue;
|
|
@@ -275,7 +283,7 @@ function importRegions(db: DatabaseSync, change: Change, findings: NormalizedFin
|
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
285
|
const conflictWith = (row: RegionRow, region: { start: number; end: number; type: RegionType }) => {
|
|
278
|
-
// dual-use bytes are code at one time and data at another: neither finding contradicts them
|
|
286
|
+
// dual-use bytes are code at one time and data at another: neither finding contradicts them.
|
|
279
287
|
if (isCode(row.type) === isCode(region.type) || row.type === "dual-use") return;
|
|
280
288
|
result.conflicts.push({
|
|
281
289
|
category: "region",
|
|
@@ -381,10 +389,11 @@ export function importFindings(db: DatabaseSync, findings: NormalizedFindings, c
|
|
|
381
389
|
const coverage = validateFindings(findings);
|
|
382
390
|
return transaction(db, () => {
|
|
383
391
|
const change = new Change(db, { origin: findings.analyzer, ...context }, `import-${findings.analyzer}`);
|
|
384
|
-
const result: ImportResult = { revision: null, symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
|
|
385
|
-
|
|
392
|
+
const result: ImportResult = { revision: null, names: counts(), symbols: counts(), regions: counts(), references: counts(), conflicts: [] };
|
|
393
|
+
importNames(db, change, findings, coverage, result);
|
|
386
394
|
importRegions(db, change, findings, coverage, result);
|
|
387
395
|
importReferences(db, change, findings, coverage, result);
|
|
396
|
+
change.check();
|
|
388
397
|
result.revision = change.revision;
|
|
389
398
|
return result;
|
|
390
399
|
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Joins the batches of agents that worked in parallel, each on its own range
|
|
2
|
+
// of addresses, into one batch for apply. The owner of an address is
|
|
3
|
+
// the batch whose range holds it: its name, comment, operand and module win
|
|
4
|
+
// there; at an address that no batch owns, the first batch in the order given
|
|
5
|
+
// wins. A name that a batch gave to an address, and that the join replaced
|
|
6
|
+
// with the name of another batch, is replaced in that batch's operand
|
|
7
|
+
// expressions and macro args too. The join writes no knowledge.
|
|
8
|
+
|
|
9
|
+
import { replaceIdentifiers } from "./expression.ts";
|
|
10
|
+
import type { BatchOperation } from "./write.ts";
|
|
11
|
+
|
|
12
|
+
export interface Batch {
|
|
13
|
+
/** The file the batch came from, for the report. */
|
|
14
|
+
file: string;
|
|
15
|
+
/** The range of addresses that the batch owns. */
|
|
16
|
+
owner?: { start: number; end: number };
|
|
17
|
+
ops: BatchOperation[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One place where batches gave different values: the value that stays, and the batches whose value the join left out. */
|
|
21
|
+
export interface JoinChoice {
|
|
22
|
+
what: "name" | "comment" | "operand" | "module" | "macro";
|
|
23
|
+
space: string;
|
|
24
|
+
/** The address, or for a macro its name. */
|
|
25
|
+
at: number | string;
|
|
26
|
+
kept: { file: string; value: string };
|
|
27
|
+
left: Array<{ file: string; value: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A clash that the join cannot decide: one name, or one module file, at two addresses. */
|
|
31
|
+
export interface JoinClash {
|
|
32
|
+
what: "name" | "file";
|
|
33
|
+
value: string;
|
|
34
|
+
space: string;
|
|
35
|
+
places: Array<{ address: number; file: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JoinResult {
|
|
39
|
+
ops: BatchOperation[];
|
|
40
|
+
choices: JoinChoice[];
|
|
41
|
+
clashes: JoinClash[];
|
|
42
|
+
/** Writes that were the same in more than one batch, and that the join wrote once. */
|
|
43
|
+
repeated: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Op<K extends BatchOperation["op"]> = Extract<BatchOperation, { op: K }>;
|
|
47
|
+
const spaceOf = (op: { space?: string }) => op.space ?? "c64";
|
|
48
|
+
|
|
49
|
+
/** The value of a keyed write as text, to compare and report it. */
|
|
50
|
+
function valueOf(op: BatchOperation): string {
|
|
51
|
+
switch (op.op) {
|
|
52
|
+
case "rename":
|
|
53
|
+
return op.kind === undefined ? op.name : `${op.name} (${op.kind})`;
|
|
54
|
+
case "comment":
|
|
55
|
+
return op.text;
|
|
56
|
+
case "operand":
|
|
57
|
+
return op.expression ?? `${op.select} ${op.symbol ?? ""}${op.offset === undefined || op.offset === 0 ? "" : op.offset > 0 ? `+${op.offset}` : op.offset}`;
|
|
58
|
+
case "module":
|
|
59
|
+
return op.title === undefined ? op.file : `${op.file} (${op.title})`;
|
|
60
|
+
case "macro":
|
|
61
|
+
return `${op.name}(${(op.params ?? []).join(", ")})`;
|
|
62
|
+
default:
|
|
63
|
+
return JSON.stringify(op);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function joinBatches(batches: readonly Batch[]): JoinResult {
|
|
68
|
+
const owns = (batch: Batch, address: number) => batch.owner !== undefined && address >= batch.owner.start && address <= batch.owner.end;
|
|
69
|
+
const choices: JoinChoice[] = [];
|
|
70
|
+
const clashes: JoinClash[] = [];
|
|
71
|
+
|
|
72
|
+
/** For each key, the write that stays: the owner's last write, else the first batch's last write. */
|
|
73
|
+
function choose<K extends "rename" | "comment" | "operand" | "module" | "macro">(op: K, what: JoinChoice["what"], keyOf: (write: Op<K>) => string, at: (write: Op<K>) => number | string) {
|
|
74
|
+
const all = new Map<string, Array<{ batch: Batch; write: Op<K> }>>();
|
|
75
|
+
for (const batch of batches) {
|
|
76
|
+
for (const write of batch.ops) {
|
|
77
|
+
if (write.op !== op) continue;
|
|
78
|
+
const key = keyOf(write as Op<K>);
|
|
79
|
+
all.set(key, [...(all.get(key) ?? []), { batch, write: write as Op<K> }]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const kept = new Map<string, { batch: Batch; write: Op<K> }>();
|
|
83
|
+
for (const [key, candidates] of all) {
|
|
84
|
+
const place = at(candidates[0]!.write);
|
|
85
|
+
const owner = typeof place === "number" ? candidates.filter((candidate) => owns(candidate.batch, place)) : [];
|
|
86
|
+
// A batch can write a place twice; its last write counts, as in apply.
|
|
87
|
+
const pick = (owner.length > 0 ? owner : candidates.filter((candidate) => candidate.batch === candidates[0]!.batch)).at(-1)!;
|
|
88
|
+
kept.set(key, pick);
|
|
89
|
+
const left = candidates.filter((candidate) => candidate.batch !== pick.batch && valueOf(candidate.write) !== valueOf(pick.write));
|
|
90
|
+
const distinct = [...new Map(left.map((candidate) => [`${candidate.batch.file}|${valueOf(candidate.write)}`, candidate])).values()];
|
|
91
|
+
if (distinct.length > 0) {
|
|
92
|
+
choices.push({
|
|
93
|
+
what,
|
|
94
|
+
space: spaceOf(pick.write),
|
|
95
|
+
at: place,
|
|
96
|
+
kept: { file: pick.batch.file, value: valueOf(pick.write) },
|
|
97
|
+
left: distinct.map((candidate) => ({ file: candidate.batch.file, value: valueOf(candidate.write) })),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return kept;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const names = choose("rename", "name", (write) => `${spaceOf(write)}|${write.address}`, (write) => write.address);
|
|
105
|
+
// Name clashes: one name for two addresses after the choice.
|
|
106
|
+
const byName = new Map<string, Array<{ address: number; file: string }>>();
|
|
107
|
+
for (const { batch, write } of names.values()) {
|
|
108
|
+
const key = `${spaceOf(write)}|${write.name}`;
|
|
109
|
+
byName.set(key, [...(byName.get(key) ?? []), { address: write.address, file: batch.file }]);
|
|
110
|
+
}
|
|
111
|
+
for (const [key, places] of byName) {
|
|
112
|
+
if (places.length > 1) clashes.push({ what: "name", value: key.split("|")[1]!, space: key.split("|")[0]!, places: places.sort((a, b) => a.address - b.address) });
|
|
113
|
+
}
|
|
114
|
+
// A name that a batch gave and the join replaced: each batch's names follow the join's names.
|
|
115
|
+
const finalName = new Map<string, string>();
|
|
116
|
+
for (const write of names.values()) finalName.set(`${spaceOf(write.write)}|${write.write.address}`, write.write.name);
|
|
117
|
+
const renamedIn = new Map<Batch, Map<string, string>>();
|
|
118
|
+
for (const batch of batches) {
|
|
119
|
+
const map = new Map<string, string>();
|
|
120
|
+
for (const write of batch.ops) {
|
|
121
|
+
if (write.op !== "rename") continue;
|
|
122
|
+
const now = finalName.get(`${spaceOf(write)}|${write.address}`);
|
|
123
|
+
if (now !== undefined && now !== write.name) map.set(`${spaceOf(write)}|${write.name}`, now);
|
|
124
|
+
}
|
|
125
|
+
renamedIn.set(batch, map);
|
|
126
|
+
}
|
|
127
|
+
const follow = (batch: Batch, space: string, text: string) => {
|
|
128
|
+
const map = renamedIn.get(batch)!;
|
|
129
|
+
if (map.size === 0) return text;
|
|
130
|
+
try {
|
|
131
|
+
return replaceIdentifiers(text, (name) => map.get(`${space}|${name}`) ?? name);
|
|
132
|
+
} catch {
|
|
133
|
+
// Text that is no expression stays as the batch wrote it; apply refuses it then.
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const comments = choose("comment", "comment", (write) => `${spaceOf(write)}|${write.address}|${write.placement}`, (write) => write.address);
|
|
139
|
+
const operands = choose("operand", "operand", (write) => `${spaceOf(write)}|${write.address}`, (write) => write.address);
|
|
140
|
+
const modules = choose("module", "module", (write) => `${spaceOf(write)}|${write.start}`, (write) => write.start);
|
|
141
|
+
const macros = choose("macro", "macro", (write) => `${spaceOf(write)}|${write.name}`, (write) => write.name);
|
|
142
|
+
const byFile = new Map<string, Array<{ address: number; file: string }>>();
|
|
143
|
+
for (const { batch, write } of modules.values()) {
|
|
144
|
+
const key = `${spaceOf(write)}|${write.file}`;
|
|
145
|
+
byFile.set(key, [...(byFile.get(key) ?? []), { address: write.start, file: batch.file }]);
|
|
146
|
+
}
|
|
147
|
+
for (const [key, places] of byFile) {
|
|
148
|
+
if (places.length > 1) clashes.push({ what: "file", value: key.split("|")[1]!, space: key.split("|")[0]!, places: places.sort((a, b) => a.address - b.address) });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The order of the joined batch: removals, the regions of other batches and then the owner's (a later classify wins
|
|
152
|
+
// where they overlap), the names, the comments, the modules, the references, the macros, then the writes that use names.
|
|
153
|
+
let repeated = 0;
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
const once = (write: BatchOperation) => {
|
|
156
|
+
const key = JSON.stringify(write);
|
|
157
|
+
if (seen.has(key)) {
|
|
158
|
+
repeated++;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
seen.add(key);
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
const named = new Set([...names.values()].map(({ write }) => `${spaceOf(write)}|${write.address}`));
|
|
165
|
+
const removals: BatchOperation[] = [];
|
|
166
|
+
const foreignRegions: BatchOperation[] = [];
|
|
167
|
+
const ownRegions: BatchOperation[] = [];
|
|
168
|
+
const references: BatchOperation[] = [];
|
|
169
|
+
const rest: BatchOperation[] = [];
|
|
170
|
+
for (const batch of batches) {
|
|
171
|
+
for (const write of batch.ops) {
|
|
172
|
+
switch (write.op) {
|
|
173
|
+
case "remove-symbol":
|
|
174
|
+
// A name from any batch wins over a removal at the same address.
|
|
175
|
+
if (!named.has(`${spaceOf(write)}|${write.address}`) && once(write)) removals.push(write);
|
|
176
|
+
break;
|
|
177
|
+
case "classify":
|
|
178
|
+
case "unclassify":
|
|
179
|
+
if (once(write)) (owns(batch, write.start) ? ownRegions : foreignRegions).push(write);
|
|
180
|
+
break;
|
|
181
|
+
case "reference":
|
|
182
|
+
case "unreference":
|
|
183
|
+
if (once(write)) references.push(write);
|
|
184
|
+
break;
|
|
185
|
+
case "rename":
|
|
186
|
+
case "comment":
|
|
187
|
+
case "operand":
|
|
188
|
+
case "module":
|
|
189
|
+
case "macro":
|
|
190
|
+
break;
|
|
191
|
+
case "use":
|
|
192
|
+
if (once(write)) rest.push({ ...write, args: (write.args ?? []).map((arg) => follow(batch, spaceOf(write), arg)) });
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
if (once(write)) rest.push(write);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const kept = <T extends { batch: Batch; write: BatchOperation }>(map: Map<string, T>) => [...map.values()].map(({ write }) => write);
|
|
200
|
+
const operandWrites = [...operands.values()].map(({ batch, write }) => ({
|
|
201
|
+
...write,
|
|
202
|
+
...(write.symbol === undefined ? {} : { symbol: follow(batch, spaceOf(write), write.symbol) }),
|
|
203
|
+
...(write.expression === undefined ? {} : { expression: follow(batch, spaceOf(write), write.expression) }),
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
ops: [...removals, ...foreignRegions, ...ownRegions, ...kept(names), ...kept(comments), ...kept(modules), ...references, ...kept(macros), ...operandWrites, ...rest],
|
|
207
|
+
choices,
|
|
208
|
+
clashes,
|
|
209
|
+
repeated,
|
|
210
|
+
};
|
|
211
|
+
}
|