@henols/c64-re-tools 2.0.0-rc.10 → 2.0.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-knowledge/SKILL.md +28 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +131 -10
- package/skills/c64-source/SKILL.md +8 -1
- package/skills/c64-source/scripts/generate.ts +64 -51
- package/skills/c64-source/scripts/loader.ts +29 -11
- package/skills/c64-source/scripts/source.ts +83 -5
- package/skills/c64-static-analysis/SKILL.md +1 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +19 -1
- package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
- package/src/c64.ts +2 -2
- package/src/host-client/image.ts +63 -0
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/history.ts +7 -1
- package/src/knowledge/import.ts +2 -1
- package/src/knowledge/read.ts +52 -2
- package/src/knowledge/schema.ts +22 -1
- package/src/knowledge/write.ts +179 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/c64-re-tools",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.11",
|
|
4
4
|
"description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Henrik Olsson",
|
|
@@ -47,6 +47,8 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
47
47
|
| Add or remove a reference | `reference '$2000' '$2100' call` / `unreference ...` |
|
|
48
48
|
| Places that a removal blocks | `suppressions` with `--start` and `--end` |
|
|
49
49
|
| Start a file of generated source | `module '$2000' player.asm --title 'Player movement'` / `unmodule '$2000'` |
|
|
50
|
+
| Write the operand of an instruction as an expression | `operand '$8079' --lo irq_handler --image unpacked/image.json` / `unoperand '$8079'` |
|
|
51
|
+
| Operand expressions | `operands` with `--start` and `--end` |
|
|
50
52
|
| Files of generated source | `modules` |
|
|
51
53
|
| The address spaces that hold knowledge | `spaces` |
|
|
52
54
|
| History | `history '$2100'`, `history --name update_player`, `revisions`, `revision 12` |
|
|
@@ -54,7 +56,7 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
54
56
|
|
|
55
57
|
Every command takes `--space`: `c64` (the default), `drive8` for code in the 1541 drive, or `bank0` to `bank255` for cartridge banks. Drive code and banks use the same addresses as the C64, so each space has its own knowledge. A name is unique inside one space. In a batch, a write can name its own `space`.
|
|
56
58
|
|
|
57
|
-
Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Reference kinds are `call`, `jump`, `read`, `write` and `reference
|
|
59
|
+
Symbol kinds are `label`, `routine`, `variable`, `data` and `vector`. Reference kinds are `call`, `jump`, `read`, `write`, `reference` and `pointer`. Only an operand expression writes a `pointer` reference.
|
|
58
60
|
|
|
59
61
|
Region types:
|
|
60
62
|
|
|
@@ -110,6 +112,8 @@ The ops and their fields:
|
|
|
110
112
|
| `reference`, `unreference` | `from`, `to`, `kind` |
|
|
111
113
|
| `module` | `start`, `file`, `title` (optional) |
|
|
112
114
|
| `unmodule` | `start` |
|
|
115
|
+
| `operand` | `address`, `select` (`lo`, `hi`, `value` or `expr`), `symbol` and `offset` (optional), or `expression` for `expr` |
|
|
116
|
+
| `unoperand` | `address` |
|
|
113
117
|
|
|
114
118
|
A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
115
119
|
|
|
@@ -117,6 +121,28 @@ A `line` comment is a line of its own above the address. A `side` comment is at
|
|
|
117
121
|
|
|
118
122
|
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm` and `constants.asm` itself. Thus a module cannot have one of these names, in upper or lower case. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
119
123
|
|
|
124
|
+
## Operand expressions
|
|
125
|
+
|
|
126
|
+
An operand expression tells the c64-source skill how to write the operand of one instruction. The source then has a name, not a number. When code or data changes its length, the operand follows the address that moved.
|
|
127
|
+
|
|
128
|
+
Give one selector:
|
|
129
|
+
|
|
130
|
+
| Option | Operand | Source |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `--lo <symbol>` | The low byte of the symbol plus the offset | `lda #<irq_handler` |
|
|
133
|
+
| `--hi <symbol>` | The high byte of the symbol plus the offset | `lda #>(sprite_bank+$1000)` |
|
|
134
|
+
| `--value <symbol>` | The symbol plus the offset | `lda bar_colours-1,x` |
|
|
135
|
+
| `--expr <text>` | ACME expression text with names and numbers | `lda #(sprite_bank + 5*64) / 64` |
|
|
136
|
+
|
|
137
|
+
- `--offset` adds a whole number to the symbol. Write a negative offset as `--offset=-1`.
|
|
138
|
+
- A name is a symbol of the knowledge or a platform name of the c64-memory-map skill.
|
|
139
|
+
- `--expr` takes `$`, `%` and decimal numbers, names, brackets, the operators `+ - * / % << >> >>> & | XOR`, and `<`, `>`, `-` and `!` before an operand.
|
|
140
|
+
- The script compares the value of the expression with the operand in the image. If they are not equal, the script refuses the write. Give the image with `--image`: an `image.json` of the c64-unpacker skill or a PRG. `apply` with an `operand` write also needs `--image`.
|
|
141
|
+
- Each name in the expression gets a `pointer` reference from the instruction. Thus `at` on the named address shows the code that uses the address.
|
|
142
|
+
- A `rename` of a symbol changes the name in each expression. The knowledge refuses `remove-symbol` while an expression uses the symbol. Use `unoperand` first.
|
|
143
|
+
|
|
144
|
+
Write an operand expression for each immediate that is a byte of an address. Also write one for each table read through the byte before the table. The pointer candidates of the c64-static-analysis skill show many of these places.
|
|
145
|
+
|
|
120
146
|
## Removals
|
|
121
147
|
|
|
122
148
|
`remove-symbol`, `unclassify` and `unreference` block the place for DXA and Ghidra. An analysis does not add a symbol at that address, a region in that range or that reference again. This applies to every removed fact, also to a fact from the LLM.
|
|
@@ -146,7 +172,7 @@ The script records the origin `llm`. Use `--origin user` only when the user deci
|
|
|
146
172
|
|
|
147
173
|
For a read: the current or historical knowledge in compact JSON.
|
|
148
174
|
|
|
149
|
-
For a write: the new revision and the current value after the write. If the write replaces a symbol
|
|
175
|
+
For a write: the new revision and the current value after the write. If the write replaces a symbol, a region or an operand expression of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change. For `apply`, `replaced` lists each write that replaced a row. Each item has the number of the write, its `op` and its `previous`.
|
|
150
176
|
|
|
151
177
|
The exit status is 0 for a result. The exit status is 1 for an error from the knowledge, for example `conflict`. The exit status is 2 for a wrong argument.
|
|
152
178
|
|
|
@@ -7,14 +7,17 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
|
|
9
9
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
|
+
import { ImageError, readImage } from "#src/host-client/image.ts";
|
|
10
11
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
12
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
13
|
+
import { operandText } from "#src/knowledge/expression.ts";
|
|
12
14
|
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
13
15
|
import {
|
|
14
16
|
at,
|
|
15
17
|
currentRevision,
|
|
16
18
|
listComments,
|
|
17
19
|
listModules,
|
|
20
|
+
listOperands,
|
|
18
21
|
listSpaces,
|
|
19
22
|
listReferences,
|
|
20
23
|
listRegions,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
search,
|
|
24
27
|
type CommentRow,
|
|
25
28
|
type ModuleRow,
|
|
29
|
+
type OperandRow,
|
|
26
30
|
type ReferenceRow,
|
|
27
31
|
type RegionRow,
|
|
28
32
|
type SuppressionRow,
|
|
@@ -32,12 +36,14 @@ import {
|
|
|
32
36
|
COMMENT_PLACEMENTS,
|
|
33
37
|
DEFAULT_SPACE,
|
|
34
38
|
EVIDENCE_KINDS,
|
|
39
|
+
OPERAND_SELECTS,
|
|
35
40
|
ORIGINS,
|
|
36
41
|
REFERENCE_KINDS,
|
|
37
42
|
REGION_TYPES,
|
|
38
43
|
SPACE,
|
|
39
44
|
SYMBOL_KINDS,
|
|
40
45
|
type CommentPlacement,
|
|
46
|
+
type OperandSelect,
|
|
41
47
|
type Origin,
|
|
42
48
|
type ReferenceKind,
|
|
43
49
|
type RegionType,
|
|
@@ -49,17 +55,21 @@ import {
|
|
|
49
55
|
classifyRegion,
|
|
50
56
|
removeComment,
|
|
51
57
|
removeModule,
|
|
58
|
+
removeOperand,
|
|
52
59
|
removeReference,
|
|
53
60
|
removeSymbol,
|
|
54
61
|
renameSymbol,
|
|
55
62
|
revert,
|
|
56
63
|
setComment,
|
|
57
64
|
setModule,
|
|
65
|
+
setOperand,
|
|
58
66
|
unclassifyRegion,
|
|
59
67
|
type BatchOperation,
|
|
60
68
|
type Evidence,
|
|
69
|
+
type OperandImage,
|
|
61
70
|
type WriteContext,
|
|
62
71
|
} from "#src/knowledge/write.ts";
|
|
72
|
+
import { platformNames } from "#src/platform.ts";
|
|
63
73
|
import { runScript, UsageError } from "#src/script.ts";
|
|
64
74
|
|
|
65
75
|
const USAGE = `knowledge.ts <command> [arguments] [options]
|
|
@@ -73,6 +83,7 @@ Read (no database needed):
|
|
|
73
83
|
references [--start A --end A --kind K --origin O]
|
|
74
84
|
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
75
85
|
modules [--start A --end A --origin O] where the files of generated source start
|
|
86
|
+
operands [--start A --end A --origin O] operand expressions of instructions
|
|
76
87
|
spaces the address spaces that hold knowledge
|
|
77
88
|
history <address> | history --name <name>
|
|
78
89
|
revisions [--limit N --before N]
|
|
@@ -97,9 +108,18 @@ Write (one revision each; creates the database when needed):
|
|
|
97
108
|
unreference <from> <to> <kind>
|
|
98
109
|
module <start> <file.asm> [--title T] a file of generated source starts here
|
|
99
110
|
unmodule <start>
|
|
111
|
+
operand <address> --lo|--hi|--value <symbol> [--offset N] --image <file>
|
|
112
|
+
operand <address> --expr <text> --image <file>
|
|
113
|
+
write the operand of the instruction at <address> as
|
|
114
|
+
an expression: the low or high byte of symbol + N, the
|
|
115
|
+
value symbol + N, or ACME expression text; refused when
|
|
116
|
+
it does not give the operand in the image (image.json
|
|
117
|
+
or PRG); give a negative offset as --offset=-1
|
|
118
|
+
unoperand <address>
|
|
100
119
|
revert <revision>
|
|
101
120
|
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
102
|
-
all as one revision, or none when one is refused
|
|
121
|
+
all as one revision, or none when one is refused;
|
|
122
|
+
operand writes need --image <file>
|
|
103
123
|
|
|
104
124
|
Write options:
|
|
105
125
|
--reason <text> why, kept with the revision
|
|
@@ -144,6 +164,17 @@ const region = (row: RegionRow) => ({
|
|
|
144
164
|
});
|
|
145
165
|
const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
|
|
146
166
|
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 });
|
|
167
|
+
const operand = (row: OperandRow) => ({
|
|
168
|
+
...spaceOf(row),
|
|
169
|
+
address: formatC64Address(row.address),
|
|
170
|
+
select: row.select,
|
|
171
|
+
...(row.symbol === undefined ? {} : { symbol: row.symbol }),
|
|
172
|
+
...(row.offset === 0 ? {} : { offset: row.offset }),
|
|
173
|
+
...(row.expression === undefined ? {} : { expression: row.expression }),
|
|
174
|
+
text: operandText(row),
|
|
175
|
+
origin: row.origin,
|
|
176
|
+
revision: row.revision,
|
|
177
|
+
});
|
|
147
178
|
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
148
179
|
function suppression(row: SuppressionRow) {
|
|
149
180
|
const place =
|
|
@@ -167,7 +198,9 @@ function historyEntry(entry: HistoryEntry) {
|
|
|
167
198
|
? reference(entry.row)
|
|
168
199
|
: entry.entity === "module"
|
|
169
200
|
? module(entry.row)
|
|
170
|
-
:
|
|
201
|
+
: entry.entity === "operand"
|
|
202
|
+
? operand(entry.row)
|
|
203
|
+
: suppression(entry.row);
|
|
171
204
|
const { revision: _revision, ...row } = shown;
|
|
172
205
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
173
206
|
}
|
|
@@ -205,6 +238,12 @@ function run(argv: string[]): unknown {
|
|
|
205
238
|
out: { type: "string" },
|
|
206
239
|
title: { type: "string" },
|
|
207
240
|
space: { type: "string" },
|
|
241
|
+
lo: { type: "string" },
|
|
242
|
+
hi: { type: "string" },
|
|
243
|
+
value: { type: "string" },
|
|
244
|
+
expr: { type: "string" },
|
|
245
|
+
offset: { type: "string" },
|
|
246
|
+
image: { type: "string" },
|
|
208
247
|
help: { type: "boolean" },
|
|
209
248
|
},
|
|
210
249
|
});
|
|
@@ -214,8 +253,8 @@ function run(argv: string[]): unknown {
|
|
|
214
253
|
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
215
254
|
const space = values.space ?? DEFAULT_SPACE;
|
|
216
255
|
|
|
217
|
-
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "spaces", "history", "revisions", "revision", "export"]);
|
|
218
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "revert", "apply"]);
|
|
256
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "spaces", "history", "revisions", "revision", "export"]);
|
|
257
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "revert", "apply"]);
|
|
219
258
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
220
259
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
221
260
|
if (read.has(command)) {
|
|
@@ -234,6 +273,7 @@ function run(argv: string[]): unknown {
|
|
|
234
273
|
comments: known.comments.map(comment),
|
|
235
274
|
referencesFrom: known.referencesFrom.map(reference),
|
|
236
275
|
referencesTo: known.referencesTo.map(reference),
|
|
276
|
+
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
237
277
|
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
238
278
|
revision: known.revision,
|
|
239
279
|
};
|
|
@@ -261,13 +301,14 @@ function run(argv: string[]): unknown {
|
|
|
261
301
|
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
262
302
|
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
263
303
|
modules: listModules(db, range, origins, space).map(module),
|
|
304
|
+
operands: listOperands(db, range, origins, space).map(operand),
|
|
264
305
|
};
|
|
265
306
|
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
266
307
|
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
267
308
|
return {
|
|
268
309
|
out: values.out,
|
|
269
310
|
bytes: bytes.length,
|
|
270
|
-
counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length },
|
|
311
|
+
counts: { symbols: exported.symbols.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 },
|
|
271
312
|
revision: exported.revision,
|
|
272
313
|
};
|
|
273
314
|
}
|
|
@@ -278,6 +319,8 @@ function run(argv: string[]): unknown {
|
|
|
278
319
|
}
|
|
279
320
|
case "modules":
|
|
280
321
|
return { modules: listModules(db, range, origins, space).map(module) };
|
|
322
|
+
case "operands":
|
|
323
|
+
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
281
324
|
case "spaces":
|
|
282
325
|
return { spaces: listSpaces(db) };
|
|
283
326
|
case "history":
|
|
@@ -307,7 +350,16 @@ function run(argv: string[]): unknown {
|
|
|
307
350
|
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
308
351
|
// Every argument is checked before the database opens: a refused write creates no database.
|
|
309
352
|
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
310
|
-
const
|
|
353
|
+
for (const option of ["lo", "hi", "value", "expr", "offset"] as const) {
|
|
354
|
+
if (values[option] !== undefined && command !== "operand") throw new UsageError(`--${option} works only with operand`);
|
|
355
|
+
}
|
|
356
|
+
if (values.image !== undefined && command !== "operand" && command !== "apply") throw new UsageError("--image works only with operand and apply");
|
|
357
|
+
const operation =
|
|
358
|
+
command === "apply"
|
|
359
|
+
? applyOperation(args, context, space, values.image)
|
|
360
|
+
: command === "operand"
|
|
361
|
+
? operandOperation(args, values, context, space)
|
|
362
|
+
: writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
311
363
|
return withWrite(operation);
|
|
312
364
|
}
|
|
313
365
|
|
|
@@ -332,8 +384,54 @@ function evidenceOf(value: string): Evidence {
|
|
|
332
384
|
return { kind, ref, sha256: createHash("sha256").update(readProjectFile(ref).bytes).digest("hex") };
|
|
333
385
|
}
|
|
334
386
|
|
|
387
|
+
/**
|
|
388
|
+
* The image that operand writes are checked against (D65), with the platform
|
|
389
|
+
* names. A byte outside the segments of the image is no operand.
|
|
390
|
+
*/
|
|
391
|
+
function operandImage(path: string | undefined, what: string): OperandImage {
|
|
392
|
+
if (path === undefined) throw new UsageError(`${what} checks each operand expression against the image: give --image <image.json|prg>`);
|
|
393
|
+
let image: ReturnType<typeof readImage>;
|
|
394
|
+
try {
|
|
395
|
+
image = readImage(path);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
const inside = (at: number) => image.segments.some((segment) => at >= segment.start && at <= segment.end);
|
|
401
|
+
return { byteAt: (at) => (inside(at) ? image.byteAt(at) : undefined), platform: new Map(platformNames().map((entry) => [entry.name, entry.address])) };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** An offset: a whole number in decimal or $ hex, with an optional minus sign. */
|
|
405
|
+
function offsetOf(value: unknown, what: string): number {
|
|
406
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
407
|
+
const match = typeof value === "string" ? /^(-?)(\$[0-9A-Fa-f]{1,4}|[0-9]{1,5})$/.exec(value) : null;
|
|
408
|
+
if (match === null) throw new UsageError(`${what} must be a whole number such as 1, -1 or '$1000'`);
|
|
409
|
+
const size = match[2]!.startsWith("$") ? Number.parseInt(match[2]!.slice(1), 16) : Number(match[2]);
|
|
410
|
+
return match[1] === "-" ? -size : size;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** operand <address> with one of --lo, --hi, --value or --expr. */
|
|
414
|
+
function operandOperation(args: string[], values: Record<string, unknown>, context: WriteContext, space: string): WriteOperation {
|
|
415
|
+
const given = (["lo", "hi", "value", "expr"] as const).filter((option) => values[option] !== undefined);
|
|
416
|
+
if (given.length !== 1) throw new UsageError("operand needs exactly one of --lo, --hi, --value or --expr");
|
|
417
|
+
const select: OperandSelect = given[0]!;
|
|
418
|
+
const request: { space: string; address: number; select: OperandSelect; symbol?: string; offset?: number; expression?: string } = { space, address: address(args[0]), select };
|
|
419
|
+
if (select === "expr") {
|
|
420
|
+
if (values.offset !== undefined) throw new UsageError("--offset works with --lo, --hi and --value, not with --expr");
|
|
421
|
+
request.expression = values.expr as string;
|
|
422
|
+
} else {
|
|
423
|
+
request.symbol = values[select] as string;
|
|
424
|
+
if (values.offset !== undefined) request.offset = offsetOf(values.offset, "--offset");
|
|
425
|
+
}
|
|
426
|
+
const image = operandImage(values.image as string | undefined, "operand");
|
|
427
|
+
return (db) => {
|
|
428
|
+
const result = setOperand(db, { ...context, image }, request);
|
|
429
|
+
return { revision: result.revision, operand: operand(result.current), ...(result.previous === undefined ? {} : { previous: operand(result.previous) }) };
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
335
433
|
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
336
|
-
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
434
|
+
function applyOperation(args: string[], context: WriteContext, space: string, imagePath: string | undefined): WriteOperation {
|
|
337
435
|
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
338
436
|
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
339
437
|
let parsed: unknown;
|
|
@@ -344,11 +442,14 @@ function applyOperation(args: string[], context: WriteContext, space: string): W
|
|
|
344
442
|
}
|
|
345
443
|
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
346
444
|
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
445
|
+
const withImage = operations.some((operation) => operation.op === "operand") ? { ...context, image: operandImage(imagePath, "apply with operand writes") } : context;
|
|
347
446
|
return (db) => {
|
|
348
|
-
const applied = applyBatch(db,
|
|
447
|
+
const applied = applyBatch(db, withImage, operations);
|
|
349
448
|
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
350
449
|
const replaced = applied.steps.flatMap((step, index) =>
|
|
351
|
-
step.previous === undefined
|
|
450
|
+
step.previous === undefined
|
|
451
|
+
? []
|
|
452
|
+
: [{ write: index + 1, op: operations[index]!.op, previous: Array.isArray(step.previous) ? step.previous.map(region) : "select" in step.previous ? operand(step.previous) : symbol(step.previous) }],
|
|
352
453
|
);
|
|
353
454
|
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
354
455
|
};
|
|
@@ -365,6 +466,8 @@ const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
|
365
466
|
unreference: ["from", "to", "kind"],
|
|
366
467
|
module: ["start", "file", "title"],
|
|
367
468
|
unmodule: ["start"],
|
|
469
|
+
operand: ["address", "select", "symbol", "offset", "expression"],
|
|
470
|
+
unoperand: ["address"],
|
|
368
471
|
};
|
|
369
472
|
|
|
370
473
|
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
@@ -406,7 +509,21 @@ function batchFields(item: unknown, at: string): BatchOperation {
|
|
|
406
509
|
return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
|
|
407
510
|
case "unmodule":
|
|
408
511
|
return { op, start: place("start") };
|
|
409
|
-
|
|
512
|
+
case "operand": {
|
|
513
|
+
const select = oneOf(OPERAND_SELECTS, text("select"), `${at}: select`);
|
|
514
|
+
return {
|
|
515
|
+
op,
|
|
516
|
+
address: place("address"),
|
|
517
|
+
select,
|
|
518
|
+
...(fields.symbol === undefined ? {} : { symbol: text("symbol") }),
|
|
519
|
+
...(fields.offset === undefined ? {} : { offset: offsetOf(fields.offset, `${at}: offset`) }),
|
|
520
|
+
...(fields.expression === undefined ? {} : { expression: text("expression") }),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
case "unoperand":
|
|
524
|
+
return { op, address: place("address") };
|
|
525
|
+
case "reference":
|
|
526
|
+
case "unreference":
|
|
410
527
|
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
411
528
|
}
|
|
412
529
|
}
|
|
@@ -480,6 +597,10 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
480
597
|
const request = { space, start: address(args[0], "start") };
|
|
481
598
|
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
482
599
|
}
|
|
600
|
+
case "unoperand": {
|
|
601
|
+
const request = { space, address: address(args[0]) };
|
|
602
|
+
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
603
|
+
}
|
|
483
604
|
case "revert": {
|
|
484
605
|
const request = { revision: integer(args[0], "revision") };
|
|
485
606
|
return (db) => ({ revision: revert(db, context, request).revision });
|
|
@@ -58,6 +58,8 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
|
|
|
58
58
|
- `lda+2 $0012` is an absolute operand below $0100. `+1` is a zero-page operand with a label.
|
|
59
59
|
- `sta NMINV+1` is an address inside a platform entry of more than one byte, for example the high byte of a vector.
|
|
60
60
|
- `name = * + 1` is a label inside an instruction, for example for code that changes its own operand.
|
|
61
|
+
- `lda #<irq_handler` and `lda bar_colours-1,x` are operand expressions of the knowledge. The script writes each name of an expression with the name of its address in the source. An operand expression in a non-immediate mode that starts with a bracket gets `0+` before it, because ACME reads `(...)` as indirect.
|
|
62
|
+
- `; = <irq_handler` at the end of an instruction with a number: the source cannot use this operand expression. `operandsAsNumbers` gives the reason.
|
|
61
63
|
- `!byte` with an instruction as its comment: ACME writes this instruction with a different opcode. The bytes keep the source identical.
|
|
62
64
|
- `not decoded` marks bytes in a code region that Ghidra did not decode.
|
|
63
65
|
- `; not executed $xxxx-$yyyy` starts code that no run of the `--coverage` files executed. The range is the same as in `not_executed.json`.
|
|
@@ -75,6 +77,8 @@ Do not edit the generated files. The next run writes over them.
|
|
|
75
77
|
5. It sets the CIA timers, their latches and the interrupt masks from `io` and `cia` in the entry state. A running timer gets the value that counts down to the timer value of the entry at the entry.
|
|
76
78
|
6. It sets $00/$01, the stack pointer, A, X, Y and the flags, and jumps to the entry.
|
|
77
79
|
|
|
80
|
+
The copy table, the copier and the jump to the entry use labels, not numbers. `loader_from_xxxx` and `loader_end_xxxx` are around each packed segment, and `loader_to_xxxx` is its first address in place. The jump goes to the label of the entry. Thus a segment can change its length, and the loader still copies it and jumps to the entry that moved. The display values, the raster line and the CIA values stay numbers. They are the entry state, not addresses of the program.
|
|
81
|
+
|
|
78
82
|
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the SID, the time of day of the CIAs, the CIA interrupt flags or the interrupt registers of the VIC-II. `loader.limits` names each CIA value that the loader cannot set as it was at the entry. A program that reads the SID voice 3 for random numbers can still differ in a scenario. A raster sync does not change a timer or the SID. Then observe after the program sets up its own random numbers.
|
|
79
83
|
|
|
80
84
|
The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01, the registers, the display registers of the VIC-II, the VIC bank and the raster line with the entry state there. It also compares the CIA timers, latches and interrupt masks. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
|
|
@@ -82,7 +86,7 @@ The script simulates the copies first. Then it runs `loader.prg` in the emulator
|
|
|
82
86
|
## Knowledge
|
|
83
87
|
|
|
84
88
|
Read:
|
|
85
|
-
- symbols, regions, comments, references and
|
|
89
|
+
- symbols, regions, comments, references, modules and operand expressions of all origins.
|
|
86
90
|
|
|
87
91
|
Automatic write:
|
|
88
92
|
- none.
|
|
@@ -99,6 +103,8 @@ One JSON object:
|
|
|
99
103
|
- `labels`: `named` labels from the knowledge and `generated` labels `l_xxxx`.
|
|
100
104
|
- `entriesInData`: each `--entry` in a data region of the knowledge, with the `region` and its `origin`. Ghidra does not decode an entry in a data region.
|
|
101
105
|
- `asBytes`: the instructions that the source has as bytes.
|
|
106
|
+
- `operandExpressions`: the count of instructions that the source has with an operand expression.
|
|
107
|
+
- `operandsAsNumbers`: the operand expressions that the source has as a number with a side comment, each with its `address`, `expression` and `reason`. An expression becomes a number when no instruction with an operand value is at the address, or when a name has no address. An expression that ACME refuses or does not assemble to the image also becomes a number. Then the script assembles the source again, at most three times.
|
|
102
108
|
- `unusedModules`: modules in the knowledge whose start is in no segment of the image. Often this tells that the image does not hold code that the knowledge describes.
|
|
103
109
|
- `entryOutside`: the entry of `image.json` is in no segment of the image, with a `reason`. The image does not hold the program.
|
|
104
110
|
- `notExecuted`: with `--coverage`, the count of instructions that no run executed, the count of their ranges, and the first ranges. `all` names the file `not_executed.json` in `--out`. It has all of the ranges. Put all of these ranges in the report.
|
|
@@ -111,6 +117,7 @@ The exit status is 0 when the source is identical and the emulator check of the
|
|
|
111
117
|
|
|
112
118
|
- `check.diagnostics`: ACME did not assemble the source. A name in the knowledge can be wrong for ACME. Read the diagnostics and change the name with c64-knowledge.
|
|
113
119
|
- `check.identical: false`: the script has a defect. Give the result to the user.
|
|
120
|
+
- `operandsAsNumbers`: read each `reason`. Correct the expression with `operand` of the c64-knowledge skill, or remove it with `unoperand`.
|
|
114
121
|
- `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
|
|
115
122
|
- `entryOutside`: the run fails, but the script still writes and checks the source. With `--loader`, `loader.verified` is `false` with the same `reason`. Capture the image again with c64-unpacker `loads` or `capture`. Make sure that a segment holds the entry.
|
|
116
123
|
- `entriesInData`: an analyzer region of data hides the code at the entry. If the entry is code, record a code region at the entry with the c64-knowledge skill, for example `classify '$0500' '$05ff' code`. A region of the user or the LLM replaces the analyzer region. Then generate again.
|
|
@@ -12,11 +12,12 @@ import { parseArgs } from "node:util";
|
|
|
12
12
|
import { formatC64Address, parseC64Address, prgRange, textToPetscii } from "#src/c64.ts";
|
|
13
13
|
import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
14
14
|
import { readCoverageFiles } from "#src/host-client/coverage.ts";
|
|
15
|
+
import { ImageError, readImage as readSharedImage, type Image } from "#src/host-client/image.ts";
|
|
15
16
|
import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
16
17
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
17
18
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
18
19
|
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
19
|
-
import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
20
|
+
import { currentRevision, listComments, listModules, listOperands, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
20
21
|
import { CODE_REGION_TYPES, DEFAULT_SPACE, SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
21
22
|
import { assemble } from "#src/native/acme.ts";
|
|
22
23
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
@@ -27,7 +28,7 @@ import { resolveProjectPath } from "#src/project.ts";
|
|
|
27
28
|
import { runScript, UsageError } from "#src/script.ts";
|
|
28
29
|
import { LOADER_JUMP, planLoader, rasterLine, readEntryState, simulate, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./loader.ts";
|
|
29
30
|
import { OPCODES } from "./opcodes.ts";
|
|
30
|
-
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range } from "./source.ts";
|
|
31
|
+
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, type Layout, type Range, type Rendered } from "./source.ts";
|
|
31
32
|
|
|
32
33
|
const USAGE = `generate.ts <image> --out <dir> [options]
|
|
33
34
|
|
|
@@ -48,58 +49,20 @@ const USAGE = `generate.ts <image> --out <dir> [options]
|
|
|
48
49
|
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
49
50
|
|
|
50
51
|
const MAX_LISTED = 20;
|
|
52
|
+
/** Rounds of assembly that move operand expressions to numbers (D65). */
|
|
53
|
+
const OPERAND_ROUNDS = 3;
|
|
51
54
|
const PROBE_BASE = 0x1000;
|
|
52
55
|
const PROBE_SLOT = 4;
|
|
53
56
|
/** The copies of a full 64 KiB take about 140 frames. */
|
|
54
57
|
const LOADER_FRAMES = 1500;
|
|
55
58
|
|
|
56
|
-
interface Image {
|
|
57
|
-
path: string;
|
|
58
|
-
segments: Range[];
|
|
59
|
-
byteAt: (address: number) => number;
|
|
60
|
-
/** The bytes that Ghidra analyzes, and how. */
|
|
61
|
-
analyzed: { bytes: Buffer; kind: "prg" | "flat64k" };
|
|
62
|
-
entry?: number;
|
|
63
|
-
/** entry_state.json next to image.json, when it exists. */
|
|
64
|
-
statePath?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
59
|
function readImage(path: string): Image {
|
|
68
|
-
const { bytes } = readProjectFile(path);
|
|
69
|
-
if (!path.endsWith(".json")) {
|
|
70
|
-
let range: ReturnType<typeof prgRange>;
|
|
71
|
-
try {
|
|
72
|
-
range = prgRange(bytes);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
throw new UsageError(`${path}: ${(error as Error).message}`);
|
|
75
|
-
}
|
|
76
|
-
return { path, segments: [{ start: range.start, end: range.end }], byteAt: (address) => range.body[address - range.start]!, analyzed: { bytes, kind: "prg" } };
|
|
77
|
-
}
|
|
78
|
-
const fail = (what: string): never => {
|
|
79
|
-
throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
|
|
80
|
-
};
|
|
81
|
-
let manifest: Record<string, unknown>;
|
|
82
60
|
try {
|
|
83
|
-
|
|
61
|
+
return readSharedImage(path);
|
|
84
62
|
} catch (error) {
|
|
85
|
-
|
|
63
|
+
if (error instanceof ImageError) throw new UsageError(error.message);
|
|
64
|
+
throw error;
|
|
86
65
|
}
|
|
87
|
-
const address = (value: unknown, what: string) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail(`${what} is not a $ address`));
|
|
88
|
-
if (typeof manifest.image !== "string") fail("it has no image");
|
|
89
|
-
if (!Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it has no segments");
|
|
90
|
-
const segments = (manifest.segments as Array<Record<string, unknown>>)
|
|
91
|
-
.map((segment, index) => ({ start: address(segment.start, `segment ${index + 1} start`), end: address(segment.end, `segment ${index + 1} end`) }))
|
|
92
|
-
.sort((a, b) => a.start - b.start);
|
|
93
|
-
segments.forEach((segment, index) => {
|
|
94
|
-
if (segment.end < segment.start) fail(`the segment at ${formatC64Address(segment.start)} ends before it starts`);
|
|
95
|
-
if (index > 0 && segment.start <= segments[index - 1]!.end) fail(`the segments at ${formatC64Address(segments[index - 1]!.start)} and ${formatC64Address(segment.start)} overlap`);
|
|
96
|
-
});
|
|
97
|
-
const directory = posix.dirname(path);
|
|
98
|
-
const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
|
|
99
|
-
if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
|
|
100
|
-
const image: Image = { path, segments, byteAt: (at) => memory[at]!, analyzed: { bytes: memory, kind: "flat64k" }, statePath: posix.join(directory, "entry_state.json") };
|
|
101
|
-
if (manifest.entry !== undefined) image.entry = address(manifest.entry, "entry");
|
|
102
|
-
return image;
|
|
103
66
|
}
|
|
104
67
|
|
|
105
68
|
/** The knowledge of one space that the source uses, read once. */
|
|
@@ -113,6 +76,7 @@ function readKnowledge(space: Space) {
|
|
|
113
76
|
comments: listComments(db, undefined, undefined, space),
|
|
114
77
|
modules: listModules(db, undefined, undefined, space),
|
|
115
78
|
references: listReferences(db, undefined, undefined, undefined, space),
|
|
79
|
+
operands: listOperands(db, undefined, undefined, space),
|
|
116
80
|
};
|
|
117
81
|
} finally {
|
|
118
82
|
db?.close();
|
|
@@ -205,6 +169,34 @@ async function probe(laid: Layout): Promise<Set<string>> {
|
|
|
205
169
|
throw new WireFailure("operation-failed", "ACME refused the opcode probe three times.");
|
|
206
170
|
}
|
|
207
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The operand expressions that made the assembly fail or that ACME assembled
|
|
174
|
+
* to other bytes than the image, with the reason (D65). `markLines` is the
|
|
175
|
+
* number of lines added before the rendered text of each file.
|
|
176
|
+
*/
|
|
177
|
+
function blame(rendered: Rendered, built: Awaited<ReturnType<typeof assembleFiles>>, image: Image, markLines: number): Map<number, string> {
|
|
178
|
+
const blamed = new Map<number, string>();
|
|
179
|
+
if (built.program === undefined) {
|
|
180
|
+
const lines = new Map(rendered.expressions.map((item) => [`${item.file}:${item.line + markLines}`, item.address]));
|
|
181
|
+
for (const diagnostic of built.diagnostics) {
|
|
182
|
+
if (diagnostic.severity !== "error" || diagnostic.file === undefined || diagnostic.line === undefined) continue;
|
|
183
|
+
const address = lines.get(`${diagnostic.file}:${diagnostic.line}`);
|
|
184
|
+
if (address !== undefined) blamed.set(address, `ACME refuses it: ${diagnostic.message}`);
|
|
185
|
+
}
|
|
186
|
+
return blamed;
|
|
187
|
+
}
|
|
188
|
+
const end = built.start! + built.program.length - 3;
|
|
189
|
+
const word = (at: (address: number) => number, item: Rendered["expressions"][number]) => (item.length === 3 ? at(item.address + 1) | (at(item.address + 2) << 8) : at(item.address + 1));
|
|
190
|
+
const show = (value: number, length: number) => `$${value.toString(16).padStart(length === 3 ? 4 : 2, "0")}`;
|
|
191
|
+
for (const item of rendered.expressions) {
|
|
192
|
+
if (item.address + item.length - 1 > end || item.address < built.start!) continue;
|
|
193
|
+
const assembled = word((address) => built.program![2 + address - built.start!]!, item);
|
|
194
|
+
const original = word(image.byteAt, item);
|
|
195
|
+
if (assembled !== original) blamed.set(item.address, `ACME assembles it to ${show(assembled, item.length)}, and the image has ${show(original, item.length)}.`);
|
|
196
|
+
}
|
|
197
|
+
return blamed;
|
|
198
|
+
}
|
|
199
|
+
|
|
208
200
|
interface SegmentCheck {
|
|
209
201
|
start: string;
|
|
210
202
|
end: string;
|
|
@@ -371,14 +363,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
371
363
|
comments: knowledge.comments,
|
|
372
364
|
modules: knowledge.modules,
|
|
373
365
|
referenceTargets: knowledge.references.map((reference) => reference.to),
|
|
366
|
+
operands: knowledge.operands,
|
|
374
367
|
...(coverage === undefined ? {} : { coverage }),
|
|
375
368
|
// The platform names are C64 names: drive code and banks get none.
|
|
376
369
|
...(space === DEFAULT_SPACE ? { platform: platformNames() } : {}),
|
|
377
370
|
});
|
|
378
371
|
const asBytes = await probe(laid);
|
|
379
372
|
const header = [`Generated by c64-source from ${image.path}${space === DEFAULT_SPACE ? "" : ` in space ${space}`} and knowledge revision ${knowledge.revision}.`, "Change names, comments, regions and modules in knowledge, then generate again."];
|
|
380
|
-
const rendered = render(laid, asBytes, header);
|
|
381
|
-
const files = [...rendered.files];
|
|
382
373
|
let loader: LoaderPlan | undefined;
|
|
383
374
|
// An image without its entry has no program: the source is still written and checked, but the run fails (R2-02).
|
|
384
375
|
const missingEntry = entryOutsideImage(image.entry, image.segments);
|
|
@@ -387,15 +378,29 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
387
378
|
if (state !== undefined && missingEntry === undefined) {
|
|
388
379
|
const segments = image.segments.map((segment) => ({ ...segment, files: laid.pieces.filter((piece) => piece.start >= segment.start && piece.start <= segment.end).map((piece) => piece.file) }));
|
|
389
380
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
381
|
+
const entryName = laid.names.get(image.entry!);
|
|
382
|
+
loader = planLoader({ segments, byteAt: image.byteAt, state, entry: image.entry!, ...(entryName === undefined ? {} : { entryName }), header: [`Loader generated by c64-source from ${image.path}.`, "It copies the segments into place, sets the entry state and jumps to the entry."] });
|
|
392
383
|
} catch (error) {
|
|
393
384
|
if (!(error instanceof WireFailure)) throw error;
|
|
394
385
|
loaderRefused = error.message;
|
|
395
386
|
}
|
|
396
387
|
}
|
|
397
388
|
// Every generated file starts with the mark, so a later run knows its own files and removes the ones it does not write again (F-25).
|
|
398
|
-
|
|
389
|
+
const marked = (list: Array<{ name: string; text: string }>) => list.map((file) => ({ ...file, text: `${GENERATED_MARK}. Do not edit: change the knowledge and generate again.\n${file.text}` }));
|
|
390
|
+
const loaderFiles = loader === undefined ? [] : [{ name: "loader.asm", text: loader.text }];
|
|
391
|
+
// An operand expression that ACME refuses, or assembles to other bytes, is written as its number, and the source is assembled again (D65).
|
|
392
|
+
const numbers = new Map<number, string>();
|
|
393
|
+
let rendered = render(laid, asBytes, header, numbers);
|
|
394
|
+
let files = marked([...rendered.files, ...loaderFiles]);
|
|
395
|
+
let built = await assembleFiles(files, "main.asm");
|
|
396
|
+
for (let round = 0; round < OPERAND_ROUNDS && rendered.expressions.length > 0; round++) {
|
|
397
|
+
const blamed = blame(rendered, built, image, 1);
|
|
398
|
+
if (blamed.size === 0) break;
|
|
399
|
+
for (const [address, reason] of blamed) numbers.set(address, reason);
|
|
400
|
+
rendered = render(laid, asBytes, header, numbers);
|
|
401
|
+
files = marked([...rendered.files, ...loaderFiles]);
|
|
402
|
+
built = await assembleFiles(files, "main.asm");
|
|
403
|
+
}
|
|
399
404
|
removeOldFiles(values.out, new Set(files.map((file) => file.name)));
|
|
400
405
|
for (const file of files) writeProjectFile(`${values.out}/${file.name}`, Buffer.from(file.text, "utf8"), { what: "--out" });
|
|
401
406
|
const showRange = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
@@ -432,10 +437,18 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
432
437
|
},
|
|
433
438
|
}),
|
|
434
439
|
...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
|
|
440
|
+
...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
|
|
441
|
+
...(rendered.operandsAsNumbers.length === 0
|
|
442
|
+
? {}
|
|
443
|
+
: {
|
|
444
|
+
operandsAsNumbers: {
|
|
445
|
+
count: rendered.operandsAsNumbers.length,
|
|
446
|
+
listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
435
449
|
};
|
|
436
450
|
// The loader includes the source, so it is built only from a source that is identical to the image.
|
|
437
451
|
const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
|
|
438
|
-
const built = await assembleFiles(files, "main.asm");
|
|
439
452
|
if (built.program === undefined) {
|
|
440
453
|
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_LISTED) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
|
|
441
454
|
}
|