@henols/c64-re-tools 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +6 -3
- package/skills/c64-assembler/scripts/assembler.ts +19 -11
- package/skills/c64-assembler/scripts/lib/compare.ts +4 -3
- package/skills/c64-basic/SKILL.md +1 -0
- package/skills/c64-disk/SKILL.md +1 -0
- package/skills/c64-disk/scripts/disk.ts +2 -2
- package/skills/c64-disk/scripts/lib/containers.ts +4 -4
- package/skills/c64-docs/SKILL.md +2 -1
- package/skills/c64-docs/scripts/docs.ts +1 -1
- package/skills/c64-docs/scripts/lib/links.ts +2 -0
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +12 -6
- package/skills/c64-knowledge/scripts/knowledge.ts +41 -25
- package/skills/c64-knowledge/scripts/lib/listing.ts +16 -31
- package/skills/c64-memory-map/SKILL.md +3 -0
- package/skills/c64-memory-map/scripts/memory-map.ts +23 -4
- package/skills/c64-provenance/SKILL.md +5 -3
- package/skills/c64-provenance/scripts/lib/library.ts +28 -5
- package/skills/c64-provenance/scripts/lib/separate.ts +34 -6
- package/skills/c64-provenance/scripts/provenance.ts +22 -10
- package/skills/c64-source/SKILL.md +2 -1
- package/skills/c64-source/scripts/lib/source.ts +25 -33
- package/skills/c64-source/scripts/source.ts +13 -17
- package/skills/c64-static-analysis/SKILL.md +6 -3
- package/skills/c64-static-analysis/scripts/lib/findings.ts +23 -8
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +38 -25
- package/skills/c64-static-analysis/scripts/static-analysis.ts +31 -19
- package/skills/c64-testing/SKILL.md +9 -5
- package/skills/c64-testing/references/scenario-format.md +5 -3
- package/skills/c64-testing/scripts/lib/diff-files.ts +41 -10
- package/skills/c64-testing/scripts/lib/recording.ts +3 -3
- package/skills/c64-testing/scripts/lib/replay.ts +1 -1
- package/skills/c64-testing/scripts/lib/runs.ts +45 -9
- package/skills/c64-testing/scripts/lib/scenario.ts +130 -81
- package/skills/c64-testing/scripts/testing.ts +9 -6
- package/skills/c64-unpacker/SKILL.md +10 -7
- package/skills/c64-unpacker/scripts/lib/loads.ts +15 -6
- package/skills/c64-unpacker/scripts/lib/machine.ts +61 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +21 -17
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +144 -11
- package/src/host-client/image.ts +27 -13
- package/src/host-client/steps.ts +41 -5
- package/src/host-client/tools.ts +4 -3
- package/src/host-client/transfer.ts +22 -29
- package/src/knowledge/history.ts +55 -2
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/program.ts +31 -2
- package/src/knowledge/read.ts +7 -4
- package/src/knowledge/schema.ts +3 -2
- package/src/knowledge/text.ts +1 -0
- package/src/knowledge/write.ts +86 -42
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +1 -1
- package/src/mcp/server.ts +9 -1
- package/src/mcp/tools/debug.ts +31 -4
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +5 -1
- package/src/mcp/tools/memory.ts +4 -3
- package/src/media.ts +57 -0
- package/src/opcodes.ts +81 -18
- package/src/platform.ts +14 -1
- package/src/project.ts +118 -3
- package/src/protocol/vice.ts +4 -1
- package/src/script.ts +118 -5
|
@@ -36,6 +36,8 @@ node <skill>/scripts/memory-map.ts export --out src/hw.asm
|
|
|
36
36
|
|
|
37
37
|
Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
|
|
38
38
|
|
|
39
|
+
`at` and `decode` also take a platform name, for example `SCROLY` or `CHROUT`, in upper or lower case. A platform name comes before a name of the knowledge. `list` and `export` show the platform names.
|
|
40
|
+
|
|
39
41
|
### Calling the script
|
|
40
42
|
|
|
41
43
|
- Call a script as `node <skill>/scripts/<script>.ts <command> [subjects] [options]`. The command comes first.
|
|
@@ -43,6 +45,7 @@ Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal numb
|
|
|
43
45
|
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
44
46
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
45
47
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
48
|
+
- The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
|
|
46
49
|
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
47
50
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
48
51
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// writes the platform names to a project file as an ACME include.
|
|
4
4
|
|
|
5
5
|
import { formatC64Address, formatC64Byte } from "#src/c64.ts";
|
|
6
|
+
import { isRefusal, Refusal } from "#src/errors.ts";
|
|
6
7
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
7
8
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
8
9
|
import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
|
|
@@ -10,6 +11,24 @@ import { listed, OPTIONS, runScript, UsageError } from "#src/script.ts";
|
|
|
10
11
|
|
|
11
12
|
const show = (entry: Entry) => ({ start: formatC64Address(entry.start), ...(entry.end !== entry.start ? { end: formatC64Address(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
|
|
12
13
|
|
|
14
|
+
/** The platform names by name in upper case: memory-map knows them also outside a project. */
|
|
15
|
+
const PLATFORM = new Map(platformNames().map((entry) => [entry.name.toUpperCase(), entry.address]));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The address of a name: a platform name (a register, a KERNAL or BASIC entry,
|
|
19
|
+
* a system variable) in any letter case first, else a name of the knowledge.
|
|
20
|
+
*/
|
|
21
|
+
function names(name: string, space: string): number {
|
|
22
|
+
const platform = PLATFORM.get(name.toUpperCase());
|
|
23
|
+
if (platform !== undefined) return platform;
|
|
24
|
+
try {
|
|
25
|
+
return knowledgeNames(name, space);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!isRefusal(error) || error.code !== "not-found") throw error;
|
|
28
|
+
throw new Refusal("not-found", `${name} is not a C64 platform name and not a name in the knowledge. Give a $ address, a platform name that memory-map.ts list or export shows (for example SCROLY or CHROUT), or a name that knowledge.ts symbols lists.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
const LISTS: Record<string, () => Entry[]> = {
|
|
14
33
|
vic: () => IO[0]!.registers,
|
|
15
34
|
sid: () => IO[1]!.registers,
|
|
@@ -22,12 +41,12 @@ const LISTS: Record<string, () => Entry[]> = {
|
|
|
22
41
|
await runScript({
|
|
23
42
|
name: "memory-map",
|
|
24
43
|
summary: "The platform meaning of C64 addresses and of values in platform registers.",
|
|
25
|
-
names
|
|
44
|
+
names,
|
|
26
45
|
commands: [
|
|
27
46
|
{
|
|
28
47
|
name: "at",
|
|
29
48
|
summary: "The platform meaning of each address: its name, area and meaning.",
|
|
30
|
-
subjects: [{ name: "<address>", help: "an address or a name of the knowledge", list: true }],
|
|
49
|
+
subjects: [{ name: "<address>", help: "an address, a platform name such as SCROLY, or a name of the knowledge", list: true }],
|
|
31
50
|
run: (call) => ({
|
|
32
51
|
addresses: call.subjects.map((text) => {
|
|
33
52
|
const found = lookup(call.parseAddress(text, "<address>"));
|
|
@@ -47,7 +66,7 @@ await runScript({
|
|
|
47
66
|
name: "decode",
|
|
48
67
|
summary: "The meaning of a value in a platform register.",
|
|
49
68
|
subjects: [
|
|
50
|
-
{ name: "<register>", help: "the register: $0001, $d011, $d016, $d018, $d010, $d015-$d01f, $dc0d, $dd00 or $dd0d" },
|
|
69
|
+
{ name: "<register>", help: "the register, as an address or a platform name: $0001, $d011 (SCROLY), $d016, $d018, $d010, $d015-$d01f, $dc0d, $dd00 or $dd0d" },
|
|
51
70
|
{ name: "<value>", help: "the value of the register: $ and hex, decimal, or % and binary, 0 to 255" },
|
|
52
71
|
],
|
|
53
72
|
options: { bank: { kind: "number", value: "<value>", help: "the value of $dd00, for the VIC-II bank of a $d018 value", max: 0xff } },
|
|
@@ -71,7 +90,7 @@ await runScript({
|
|
|
71
90
|
{
|
|
72
91
|
name: "export",
|
|
73
92
|
summary: "Writes the platform names as an ACME include: NAME = $xxxx.",
|
|
74
|
-
options: { out: { ...OPTIONS.out, value: "<file>", help: "the include file, relative to the project folder", required: true } },
|
|
93
|
+
options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the include file, relative to the project folder", required: true } },
|
|
75
94
|
run: (call) => {
|
|
76
95
|
const names = platformNames();
|
|
77
96
|
const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${item.address < 0x100 ? formatC64Byte(item.address) : formatC64Address(item.address)}`.padEnd(24) + `; ${item.meaning}`)];
|
|
@@ -56,6 +56,7 @@ With `--frames`, a PRG starts with autostart and RUN. An image starts from its b
|
|
|
56
56
|
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
57
57
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
58
58
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
59
|
+
- The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
|
|
59
60
|
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
60
61
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
61
62
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -96,7 +97,8 @@ Use `separate` when you have only one release. It reads the program, its stages
|
|
|
96
97
|
- `ranges`: the program in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. The last stage is the program. An earlier stage is a depacker, an intro or a loader. Only a capture of c64-unpacker `capture` has stages. For a program from `loads` or a PRG, `noStages` tells this.
|
|
97
98
|
- `access`: from the coverage of the knowledge: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs. A `read` byte right after executed code, or at a return address, can be a dummy read of the 6502 that the program does not use.
|
|
98
99
|
- `codeRegions.total`: the number of code regions in the knowledge.
|
|
99
|
-
- `
|
|
100
|
+
- `roots`: the addresses where the reachability starts. They are the entry, and the NMI, RESET and IRQ vectors at `$FFFA`-`$FFFF` when the image holds them. An interrupt handler starts from its vector, because no call or jump goes to it. `from` tells the source of each root.
|
|
101
|
+
- `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from a root. A region also reaches the code after it when its last instruction is not RTS, RTI, JMP, BRK or JAM: the CPU continues there. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
|
|
100
102
|
- `outside`: RAM outside the program that the program `read` or `executed` but did not write, and code it `generated`. A loader or an earlier stage put the bytes that the program did not write. A `generated` range can have gaps of two bytes or less: these are operand bytes.
|
|
101
103
|
|
|
102
104
|
Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed` (default 50). `--out` writes all of them and the result shows the file as `{out, bytes, sha256}`. Run more coverage runs for parts that are `untouched`, and add them with `coverage` of the c64-knowledge skill, before you decide.
|
|
@@ -112,7 +114,7 @@ Use `leftovers` to find what the program holds in addition to its working code.
|
|
|
112
114
|
| `fill` | One byte that repeats, at least 16 times, or a pattern of two to four bytes that repeats, at least 32 bytes in total. `pattern` is the repeated bytes. |
|
|
113
115
|
| `residue` | Bytes that no run touched and that are no text, BASIC, fill or code region. `bytes` shows the first 16 bytes. |
|
|
114
116
|
| `copies` | Bytes that are the same as earlier bytes of the program, at least 32 bytes. `copyOf` is the address of the earlier bytes. |
|
|
115
|
-
| `deadCode` | Code regions of the knowledge that
|
|
117
|
+
| `deadCode` | Code regions of the knowledge that no root reaches, as in `codeRegions.unreachable` of `separate`, and that no run executed. |
|
|
116
118
|
|
|
117
119
|
Text from another computer can show the tools of the developer:
|
|
118
120
|
|
|
@@ -193,7 +195,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
|
|
|
193
195
|
|
|
194
196
|
The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. `releases` gives the segments and the `shift` of each release. With `--frames`, each difference also has `runtime`, and `run` tells if each release started, or why not.
|
|
195
197
|
|
|
196
|
-
For `leftovers`: the lists of findings, with `entry` and the number of `stages`. `
|
|
198
|
+
For `leftovers`: the lists of findings, with `entry`, `roots` and the number of `stages`. `noStages` and `noCoverage` tell which knowledge is missing. Without a root, `roots` tells why `deadCode` is empty.
|
|
197
199
|
|
|
198
200
|
The runtime evidence and the leftovers are evidence only. The script does not decide which bytes are original.
|
|
199
201
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// suggests its names there as an apply batch. Nothing here writes knowledge.
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { Refusal } from "#src/errors.ts";
|
|
12
12
|
import { writeRefusal, type Written } from "#src/host-client/transfer.ts";
|
|
@@ -14,7 +14,7 @@ import { formatC64Address, instructionLength } from "#src/c64.ts";
|
|
|
14
14
|
import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
|
|
15
15
|
import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
|
|
16
16
|
import { COMMENT_PLACEMENTS, type CommentPlacement, isSemantic, OPERAND_SELECTS, type OperandSelect, REFERENCE_KINDS, type ReferenceKind, REGION_TYPES, type RegionType, SYMBOL_KINDS, type SymbolKind } from "#src/knowledge/schema.ts";
|
|
17
|
-
import { PROJECT_DIRECTORY } from "#src/project.ts";
|
|
17
|
+
import { checkOutputPath, isInside, PROJECT_DIRECTORY } from "#src/project.ts";
|
|
18
18
|
|
|
19
19
|
/** A routine shorter than this is not saved and not searched: its bytes occur by chance. */
|
|
20
20
|
export const MIN_ROUTINE = 16;
|
|
@@ -291,14 +291,37 @@ export function parseRoutine(text: string, file: string): Routine {
|
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Refuses a library folder to write that is inside the project `root`, also
|
|
296
|
+
* through a symbolic link, when the output check of every script refuses it
|
|
297
|
+
* (checkOutputPath): for example a folder in the knowledge. A folder outside
|
|
298
|
+
* the project gets no check.
|
|
299
|
+
*/
|
|
300
|
+
function checkLibraryOutput(path: string, root: string): void {
|
|
301
|
+
let existing = path;
|
|
302
|
+
while (statSync(existing, { throwIfNoEntry: false }) === undefined && dirname(existing) !== existing) existing = dirname(existing);
|
|
303
|
+
const real = join(realpathSync(existing), relative(existing, path));
|
|
304
|
+
const realRoot = realpathSync(root);
|
|
305
|
+
for (const [base, target] of [[root, path], [realRoot, real]] as const) {
|
|
306
|
+
if (isInside(base, target)) {
|
|
307
|
+
// A project path shows with "/" on every system, as the messages give it.
|
|
308
|
+
checkOutputPath(relative(base, target).split(sep).join("/") || ".", "folder", { root: base, what: "--library" });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
/**
|
|
295
315
|
* The library folder that --library names, relative to the working directory
|
|
296
316
|
* like --project (D71). A library is a plain folder, never a project. With
|
|
297
|
-
* `create`, a missing folder is made
|
|
317
|
+
* `create`, a missing folder is made; a folder inside the project must pass
|
|
318
|
+
* the output check of every script, which refuses the knowledge.
|
|
298
319
|
*/
|
|
299
320
|
export function libraryFolder(folder: string, options: { create: boolean; cwd?: string }): string {
|
|
300
321
|
if (folder.length === 0) throw new Refusal("invalid-input", "The library folder must not be empty.");
|
|
301
|
-
const
|
|
322
|
+
const root = options.cwd ?? process.cwd();
|
|
323
|
+
const path = resolve(root, folder);
|
|
324
|
+
if (options.create) checkLibraryOutput(path, root);
|
|
302
325
|
const stat = statSync(path, { throwIfNoEntry: false });
|
|
303
326
|
if (stat === undefined) {
|
|
304
327
|
if (!options.create) throw new Refusal("not-found", `There is no library folder ${folder}. Save a routine to it first with routine.`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Evidence from one release: for each range of the image, the unpack
|
|
2
2
|
// stage that wrote it and the access class from coverage; for each code
|
|
3
3
|
// region of knowledge, whether call and jump references or the run on from
|
|
4
|
-
// its last instruction reach it from the entry
|
|
4
|
+
// its last instruction reach it from a root (the entry, and the NMI, RESET
|
|
5
|
+
// and IRQ vectors at $fffa-$ffff when the image holds them); and what the program reads
|
|
5
6
|
// or executes outside the image. Evidence only: nothing here decides what is
|
|
6
7
|
// original.
|
|
7
8
|
|
|
@@ -30,13 +31,21 @@ export interface EvidenceRange extends AddressRange {
|
|
|
30
31
|
|
|
31
32
|
export interface CodeRegionEvidence extends AddressRange {
|
|
32
33
|
type: string;
|
|
33
|
-
/** Absent when the image has no entry. */
|
|
34
|
+
/** Absent when the image has no root: no entry and no hardware vector. */
|
|
34
35
|
reachable?: boolean;
|
|
35
36
|
/** Absent without coverage. */
|
|
36
37
|
executed?: boolean;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** An address where the reachability starts, and why: the entry, or a hardware vector in the image. */
|
|
41
|
+
export interface Root {
|
|
42
|
+
address: number;
|
|
43
|
+
from: "entry" | "NMI vector $fffa" | "RESET vector $fffc" | "IRQ vector $fffe";
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export interface Separation {
|
|
47
|
+
/** The roots of the reachability, in the order entry, NMI, RESET, IRQ. */
|
|
48
|
+
roots: Root[];
|
|
40
49
|
ranges: EvidenceRange[];
|
|
41
50
|
codeRegions: CodeRegionEvidence[];
|
|
42
51
|
/**
|
|
@@ -50,6 +59,24 @@ export interface Separation {
|
|
|
50
59
|
|
|
51
60
|
const SYSTEM_PAGES_END = 0x03ff;
|
|
52
61
|
|
|
62
|
+
/** The hardware vectors of the 6510: the CPU reads them from $fffa-$ffff, the RAM when the KERNAL is banked out. */
|
|
63
|
+
const HARDWARE_VECTORS = [
|
|
64
|
+
{ at: 0xfffa, from: "NMI vector $fffa" },
|
|
65
|
+
{ at: 0xfffc, from: "RESET vector $fffc" },
|
|
66
|
+
{ at: 0xfffe, from: "IRQ vector $fffe" },
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The roots of the reachability: the entry, and each hardware vector whose
|
|
71
|
+
* two bytes are in the image. An interrupt handler runs by its vector, and
|
|
72
|
+
* no call or jump reference reaches it.
|
|
73
|
+
*/
|
|
74
|
+
export function reachabilityRoots(segments: readonly AddressRange[], memory: Uint8Array, entry?: number): Root[] {
|
|
75
|
+
const inImage = (address: number) => segments.some((segment) => address >= segment.start && address <= segment.end);
|
|
76
|
+
const vectors = HARDWARE_VECTORS.filter((vector) => inImage(vector.at) && inImage(vector.at + 1)).map((vector): Root => ({ address: memory[vector.at]! | (memory[vector.at + 1]! << 8), from: vector.from }));
|
|
77
|
+
return [...(entry === undefined ? [] : [{ address: entry, from: "entry" as const }]), ...vectors];
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/** BRK, JMP, RTI, RTS, JMP (indirect) and the JAM opcodes: the CPU does not run on to the next instruction after them. */
|
|
54
81
|
const NO_FALL_THROUGH = new Set([0x00, 0x4c, 0x40, 0x60, 0x6c, 0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
|
|
55
82
|
|
|
@@ -114,12 +141,13 @@ export function separate(input: SeparationInput): Separation {
|
|
|
114
141
|
}
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
// Reachability: from the
|
|
144
|
+
// Reachability: from the regions that hold a root, through call and jump references and the run on
|
|
118
145
|
// from the last instruction of a region into the next address, to a fixed point.
|
|
119
146
|
const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
|
|
120
147
|
const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
|
|
148
|
+
const roots = reachabilityRoots(input.segments, input.memory, input.entry);
|
|
121
149
|
const reached = new Set<number>();
|
|
122
|
-
|
|
150
|
+
for (const root of roots) if (holding(root.address) >= 0) reached.add(holding(root.address));
|
|
123
151
|
// The CPU runs the bytes after the region as instructions: through bytes of no code region (a dual-use
|
|
124
152
|
// instruction, for example) into the next code region, unless one of them does not run on.
|
|
125
153
|
const runOn = (from: number): number | undefined => {
|
|
@@ -154,7 +182,7 @@ export function separate(input: SeparationInput): Separation {
|
|
|
154
182
|
ran = false;
|
|
155
183
|
for (let address = region.start; address <= region.end && !ran; address++) ran = executed[address] === 1;
|
|
156
184
|
}
|
|
157
|
-
return { start: region.start, end: region.end, type: region.type, ...(
|
|
185
|
+
return { start: region.start, end: region.end, type: region.type, ...(roots.length === 0 ? {} : { reachable: reached.has(index) }), ...(ran === undefined ? {} : { executed: ran }) };
|
|
158
186
|
});
|
|
159
187
|
|
|
160
188
|
const read: number[] = [];
|
|
@@ -174,5 +202,5 @@ export function separate(input: SeparationInput): Separation {
|
|
|
174
202
|
for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
|
|
175
203
|
return true;
|
|
176
204
|
};
|
|
177
|
-
return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
|
|
205
|
+
return { roots, ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
|
|
178
206
|
}
|
|
@@ -41,13 +41,13 @@ import {
|
|
|
41
41
|
import { CODE_REGION_TYPES, SEMANTIC_ORIGINS } from "#src/knowledge/schema.ts";
|
|
42
42
|
import { platformNames } from "#src/platform.ts";
|
|
43
43
|
import { projectRoot } from "#src/project.ts";
|
|
44
|
-
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
44
|
+
import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
|
|
45
45
|
import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec } from "#src/script.ts";
|
|
46
46
|
import { compareReleases, shiftSuggestion, suggestShifts, type Release } from "./lib/compare.ts";
|
|
47
47
|
import { findRoutine, libraryFolder, readLibrary, routineFrom, saveRoutine, suggest } from "./lib/library.ts";
|
|
48
48
|
import { watchRelease, type Start } from "./lib/runtime.ts";
|
|
49
49
|
import { findLeftovers } from "./lib/leftovers.ts";
|
|
50
|
-
import { separate } from "./lib/separate.ts";
|
|
50
|
+
import { separate, type Root } from "./lib/separate.ts";
|
|
51
51
|
|
|
52
52
|
const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
|
|
53
53
|
|
|
@@ -107,14 +107,17 @@ async function compare(call: Call): Promise<unknown> {
|
|
|
107
107
|
const shifts = new Map<number, number>();
|
|
108
108
|
for (const text of call.strings("shift")) {
|
|
109
109
|
const match = /^(\d+):([+-]?\d+)$/.exec(text);
|
|
110
|
-
if (match === null
|
|
110
|
+
if (match === null) throw new UsageError(`--shift must be <release number>:<bytes>, for example 2:-256, not ${text}`);
|
|
111
|
+
if (Number(match[1]) < 1 || Number(match[1]) > positionals.length) {
|
|
112
|
+
throw new UsageError(`--shift ${text}: there is no release ${match[1]}. This compare has ${positionals.length} releases: give a release number from 1 to ${positionals.length}`);
|
|
113
|
+
}
|
|
111
114
|
if (shifts.has(Number(match[1]))) throw new UsageError(`--shift gives release ${match[1]} more than one shift (${shifts.get(Number(match[1]))} and ${match[2]}). Give one --shift for each release`);
|
|
112
115
|
shifts.set(Number(match[1]), Number(match[2]));
|
|
113
116
|
}
|
|
114
117
|
const frames = call.count("frames");
|
|
115
118
|
const stepsFile = call.string("steps");
|
|
116
119
|
if (stepsFile !== undefined && frames === undefined) throw new UsageError("--steps works only with --frames");
|
|
117
|
-
const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps");
|
|
120
|
+
const steps: ActionStep[] = stepsFile === undefined ? [] : readActionSteps(stepsFile, "--steps", knowledgeAt());
|
|
118
121
|
const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
|
|
119
122
|
const releases = loaded.map((item) => item.release);
|
|
120
123
|
const range = call.range("range");
|
|
@@ -236,6 +239,11 @@ function programEvidence() {
|
|
|
236
239
|
return { program, stages, coverage, found };
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
/** The roots of the reachability as a person reads them. */
|
|
243
|
+
const showRoots = (roots: readonly Root[]) => roots.map((root) => ({ address: formatC64Address(root.address), from: root.from }));
|
|
244
|
+
|
|
245
|
+
const NO_ROOTS = "none: the program in the knowledge has no entry, and the image does not hold the hardware vectors at $fffa-$ffff, so no code region is reachable or unreachable.";
|
|
246
|
+
|
|
239
247
|
const NO_STAGES = "The program in the knowledge has no stage log. Only a capture of c64-unpacker gives one. A capture of loads holds one part of a program and has none.";
|
|
240
248
|
|
|
241
249
|
/** separate: evidence from the program in the knowledge, with all ranges in --out and the first ones in the result. */
|
|
@@ -248,7 +256,8 @@ function separateCommand(call: Call): unknown {
|
|
|
248
256
|
const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
|
|
249
257
|
const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
|
|
250
258
|
const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
|
|
251
|
-
const
|
|
259
|
+
const roots = showRoots(found.roots);
|
|
260
|
+
const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify({ roots, ranges, codeRegions, outside }, null, 1)}\n`, { what: "--out" });
|
|
252
261
|
const rest = out === undefined ? undefined : out;
|
|
253
262
|
const list = <T>(items: T[]) => listed(items, max, undefined, rest);
|
|
254
263
|
const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
@@ -263,10 +272,12 @@ function separateCommand(call: Call): unknown {
|
|
|
263
272
|
}),
|
|
264
273
|
...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
|
|
265
274
|
ranges: list(ranges),
|
|
275
|
+
// The roots of the reachability: the entry and the hardware vectors in the image.
|
|
276
|
+
roots: roots.length === 0 ? NO_ROOTS : roots,
|
|
266
277
|
// total counts every code region; unreachable is a list like the others.
|
|
267
278
|
codeRegions: {
|
|
268
279
|
total: codeRegions.length,
|
|
269
|
-
...(
|
|
280
|
+
...(roots.length === 0 ? {} : { unreachable: list(codeRegions.filter((region) => region.reachable === false)) }),
|
|
270
281
|
},
|
|
271
282
|
...(coverage === undefined ? {} : { outside: { read: list(outside.read), executed: list(outside.executed), generated: list(outside.generated) } }),
|
|
272
283
|
...(written === undefined ? {} : written),
|
|
@@ -307,7 +318,8 @@ function leftoversCommand(call: Call): unknown {
|
|
|
307
318
|
const written = out === undefined ? undefined : writeReported(out, `${JSON.stringify(all, null, 1)}\n`, { what: "--out" });
|
|
308
319
|
const list = <T>(items: T[]) => listed(items, max, undefined, out ?? "a larger --max-listed, or --out <file>");
|
|
309
320
|
return {
|
|
310
|
-
...(program.entry === undefined ? {
|
|
321
|
+
...(program.entry === undefined ? {} : { entry: formatC64Address(program.entry) }),
|
|
322
|
+
roots: found.roots.length === 0 ? NO_ROOTS : showRoots(found.roots),
|
|
311
323
|
...(stages === undefined ? { noStages: NO_STAGES } : { stages: stages.length }),
|
|
312
324
|
...(coverage === undefined ? { noCoverage: "The knowledge has no coverage, so residue is empty and no finding has access. Add coverage with coverage of the c64-knowledge skill." } : {}),
|
|
313
325
|
text: list(all.text.map((item) => (item.text.length > SHOWN_TEXT ? { ...item, text: `${item.text.slice(0, SHOWN_TEXT)} ...`, length: item.text.length } : item))),
|
|
@@ -462,14 +474,14 @@ const COMMANDS: CommandSpec[] = [
|
|
|
462
474
|
{
|
|
463
475
|
name: "separate",
|
|
464
476
|
summary: "Evidence from the program in the knowledge: the unpack stage that wrote each range, its access in the coverage, whether call and jump references, or the run on from the code before it, reach each code region from the entry, and what the program reads or executes outside its segments.",
|
|
465
|
-
options: { out: { ...OPTIONS.out, value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
|
|
477
|
+
options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all ranges to this file" }, "max-listed": OPTIONS["max-listed"] },
|
|
466
478
|
run: separateCommand,
|
|
467
479
|
},
|
|
468
480
|
{
|
|
469
481
|
name: "leftovers",
|
|
470
482
|
summary: "What the program in the knowledge holds besides its working code: texts in PETSCII, screen codes, ASCII or with bit 7 set (Apple II), assembler source, BASIC lines away from the BASIC start, fills, untouched bytes outside code, copies of earlier bytes, and code regions that nothing reaches and no run executed.",
|
|
471
483
|
options: {
|
|
472
|
-
out: { ...OPTIONS.out, value: "<file>", help: "write all findings to this file" },
|
|
484
|
+
out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write all findings to this file" },
|
|
473
485
|
"min-text": { kind: "count", value: "<n>", min: 4, max: 999, default: 8, help: "the shortest text to find (default 8)" },
|
|
474
486
|
"max-listed": OPTIONS["max-listed"],
|
|
475
487
|
},
|
|
@@ -492,7 +504,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
492
504
|
summary: "Finds each routine of the library in the program of the knowledge: the opcodes and other fixed bytes equal, and the addresses into the routine moved by one shift.",
|
|
493
505
|
options: {
|
|
494
506
|
library: { ...LIBRARY, help: "the library folder, relative to the project folder, for example ../library" },
|
|
495
|
-
out: { ...OPTIONS.out, value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
|
|
507
|
+
out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the names of the routines at the found addresses as a batch for knowledge.ts apply" },
|
|
496
508
|
"max-listed": OPTIONS["max-listed"],
|
|
497
509
|
},
|
|
498
510
|
run: matchCommand,
|
|
@@ -47,6 +47,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
|
|
|
47
47
|
- An address is `$` and 1 to 4 hex digits, for example `'$c000'` or `'$fb'`, or a name. A name is a symbol, else an analyzer name that only one analyzer gives. Quote an address in a shell.
|
|
48
48
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
49
49
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
50
|
+
- The script examines each `--out` path before it starts work. It refuses a path in `.c64-re-tools/knowledge/` and the index of the knowledge, because only c64-knowledge writes them. It refuses a folder where the command writes a file, and a file where the command writes a folder.
|
|
50
51
|
- The result is one JSON object on one line. A file that the script writes shows as `{"out", "bytes", "sha256"}`. A list shows as `{"count", "listed"}`, and `rest` tells where to get the rest of a long list.
|
|
51
52
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
52
53
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -79,7 +80,7 @@ When the knowledge has coverage, code that no run executed gets the line `; not
|
|
|
79
80
|
- `constants.asm` holds the names of addresses outside the program, for example `EXTCOL = $d020`. The file puts the names in groups by memory area, for example zero page, VIC-II registers or CIA 1 registers. Each group is in address order. Drive code and banks have one group only. The side comment of a name tells what the address is. This text is the knowledge comment of the address. Without a comment, the text is the platform meaning. When the knowledge name is not the platform name, the platform name comes first. For an I/O mirror without a meaning, the text gives the register that the mirror repeats. Then `Used:` counts how the source uses the name. The kinds are `read`, `written`, `modified`, `called`, `jumped to`, `jumped through`, `branched to`, `pointer`, `address`, `table` and `macro argument`. `address` is an immediate operand expression. `table` is an entry in a table of addresses.
|
|
80
81
|
- The top of `constants.asm` sets each switch to 0, for example `!ifndef CRACK { CRACK = 0 }`. A build that defines the switch as 1 uses the patches of that switch.
|
|
81
82
|
- A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. A platform name wins over a name from an analyzer (`dxa`, `ghidra` or `heuristic`), for example `EXTCOL` over `ld020`. A `user` or `llm` name wins over a platform name. An address in the program without a name gets `l_xxxx`.
|
|
82
|
-
- The source uses the names of a ROM only when the program uses that ROM. When the knowledge has coverage, a ROM is in use when a run executed code of it. A read of ROM bytes as data does not count. Without coverage, `$01` of the entry state tells which ROMs are in. When the program uses neither ROM, the zero page and pages 2 and 3 get no system names. `romNamesLeftOut` gives the ROMs that the source did not name, and the evidence.
|
|
83
|
+
- The source uses the names of a ROM only when the program uses that ROM. When the knowledge has coverage, a ROM is in use when a run executed code of it. A read of ROM bytes as data does not count. Without coverage, `$01` of the entry state tells which ROMs are in. When the program uses neither ROM, the zero page and pages 2 and 3 get no system names. `romNamesLeftOut` gives the ROMs that the source did not name, and the evidence. An operand expression or a macro use with such a name gets that reason.
|
|
83
84
|
- `sta $d011 ; $d011: yScroll 3, rows 25, displayEnabled` is a store of an immediate to a register that the c64-memory-map skill decodes. The side comment gives the decoded value. A side comment in the knowledge replaces it.
|
|
84
85
|
- Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
|
|
85
86
|
- Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
|
|
@@ -13,7 +13,7 @@ import { WireFailure } from "#src/host-client/tools.ts";
|
|
|
13
13
|
import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
|
|
14
14
|
import { type CommentPlacement, isAnalyzer, isGeneratedFile, type Origin, type RegionType } from "#src/knowledge/schema.ts";
|
|
15
15
|
import type { Area } from "#src/platform.ts";
|
|
16
|
-
import { hasZeroPageForm,
|
|
16
|
+
import { hasZeroPageForm, instructionWith, labelProblem, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type UseKind } from "#src/opcodes.ts";
|
|
17
17
|
|
|
18
18
|
export interface SourceInput {
|
|
19
19
|
/** In address order, without overlap. */
|
|
@@ -34,6 +34,8 @@ export interface SourceInput {
|
|
|
34
34
|
coverage?: Uint8Array;
|
|
35
35
|
/** Platform names: the name of an address that knowledge does not name, the last address of its entry, and what it is. */
|
|
36
36
|
platform?: ReadonlyArray<{ address: number; end: number; name: string; meaning?: string }>;
|
|
37
|
+
/** The platform names that `platform` leaves out, because the program does not use their ROM: the reason of an expression that names one tells it. */
|
|
38
|
+
platformLeftOut?: ReadonlySet<string>;
|
|
37
39
|
/** The platform area of an address, and the register that an I/O mirror repeats: constants.asm groups and describes its names with them. */
|
|
38
40
|
describe?: (address: number) => { area: Area; mirrorOf?: number };
|
|
39
41
|
/** What a value in a platform register means (c64-memory-map): an immediate stored to such a register gets it as a side comment. Throws for a register without a decoder. */
|
|
@@ -319,7 +321,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
319
321
|
const outside = new Set<number>();
|
|
320
322
|
// A platform name that knowledge gives to another address, or that reads as an instruction, is not used.
|
|
321
323
|
const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
|
|
322
|
-
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) &&
|
|
324
|
+
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && labelProblem(name) === undefined).map(({ address, name }) => [address, name]));
|
|
323
325
|
// An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
|
|
324
326
|
const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
|
|
325
327
|
/** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
|
|
@@ -355,6 +357,17 @@ export function layout(input: SourceInput): Layout {
|
|
|
355
357
|
for (const row of input.platform ?? []) if (!byName.has(row.name)) byName.set(row.name, row.address);
|
|
356
358
|
for (const symbol of input.symbols) byName.set(symbol.name, symbol.address);
|
|
357
359
|
const instructionAt = new Map(pieces.flatMap((piece) => piece.items.flatMap((item) => (item.kind === "instruction" ? [[item.address, item] as const] : []))));
|
|
360
|
+
/** Why names of an expression are not names of the source: not known, or platform names of a ROM that the program does not use. */
|
|
361
|
+
const unknownNames = (missing: readonly string[], remedy: string) => {
|
|
362
|
+
const leftOut = missing.filter((name) => input.platformLeftOut?.has(name) === true);
|
|
363
|
+
const unknown = missing.filter((name) => !leftOut.includes(name));
|
|
364
|
+
const are = (names: readonly string[]) => `${names.join(", ")} ${names.length === 1 ? "is" : "are"}`;
|
|
365
|
+
return [
|
|
366
|
+
...(unknown.length === 0 ? [] : [`${are(unknown)} not a symbol of the knowledge or a platform name.`]),
|
|
367
|
+
...(leftOut.length === 0 ? [] : [`${are(leftOut)} a platform name of a ROM that the program does not use, so the source leaves ${leftOut.length === 1 ? "it" : "them"} out (romNamesLeftOut).`]),
|
|
368
|
+
remedy,
|
|
369
|
+
].join(" ");
|
|
370
|
+
};
|
|
358
371
|
// A macro use is a run of whole instructions of one file, of a macro with one param for each arg, whose args name known names.
|
|
359
372
|
const macrosByName = new Map((input.macros ?? []).map((macro) => [macro.name, macro]));
|
|
360
373
|
const macrosAsInstructions: MacroAsInstructions[] = [];
|
|
@@ -384,7 +397,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
384
397
|
}
|
|
385
398
|
const missing = [...new Set(use.args.flatMap((arg) => identifiers(arg)))].filter((name) => !byName.has(name));
|
|
386
399
|
if (missing.length > 0) {
|
|
387
|
-
fallBack(
|
|
400
|
+
fallBack(unknownNames(missing, "Name the address with knowledge.ts rename, or give other args with knowledge.ts use."));
|
|
388
401
|
continue;
|
|
389
402
|
}
|
|
390
403
|
candidates.push({ use, items: run as InstructionItem[] });
|
|
@@ -403,7 +416,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
403
416
|
}
|
|
404
417
|
const missing = identifiers(expression).filter((name) => !byName.has(name));
|
|
405
418
|
if (missing.length > 0) {
|
|
406
|
-
operandsAsNumbers.push({ address: operand.address, expression, reason:
|
|
419
|
+
operandsAsNumbers.push({ address: operand.address, expression, reason: unknownNames(missing, "Name the address with knowledge.ts rename, or write the operand again with knowledge.ts operand.") });
|
|
407
420
|
continue;
|
|
408
421
|
}
|
|
409
422
|
// An immediate operand builds the address of a name; any other operand uses the address as the instruction does.
|
|
@@ -450,7 +463,8 @@ export function layout(input: SourceInput): Layout {
|
|
|
450
463
|
for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
|
|
451
464
|
const known = symbols.get(address);
|
|
452
465
|
if (known !== undefined) {
|
|
453
|
-
|
|
466
|
+
const problem = labelProblem(known);
|
|
467
|
+
if (problem !== undefined) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)}: ${problem} Rename the symbol with c64-knowledge.`);
|
|
454
468
|
names.set(address, known);
|
|
455
469
|
if (inImage(address)) namedLabels++;
|
|
456
470
|
continue;
|
|
@@ -574,27 +588,6 @@ export function formOf(layout: Pick<Layout, "inImage" | "byteAt">, item: Instruc
|
|
|
574
588
|
return { key: `${item.opcode.toString(16).padStart(2, "0")}${suffix}`, opcode: item.opcode, suffix };
|
|
575
589
|
}
|
|
576
590
|
|
|
577
|
-
/** The instruction text with an operand expression; the expression is a name or a $ number. */
|
|
578
|
-
export function instructionText(opcode: number, suffix: string, expression: string): string {
|
|
579
|
-
const { mnemonic, mode } = OPCODES[opcode]!;
|
|
580
|
-
const operand: Record<Mode, string> = {
|
|
581
|
-
imp: "",
|
|
582
|
-
imm: `#${expression}`,
|
|
583
|
-
zp: expression,
|
|
584
|
-
zpx: `${expression},x`,
|
|
585
|
-
zpy: `${expression},y`,
|
|
586
|
-
abs: expression,
|
|
587
|
-
abx: `${expression},x`,
|
|
588
|
-
aby: `${expression},y`,
|
|
589
|
-
ind: `(${expression})`,
|
|
590
|
-
izx: `(${expression},x)`,
|
|
591
|
-
izy: `(${expression}),y`,
|
|
592
|
-
rel: expression,
|
|
593
|
-
};
|
|
594
|
-
const text = operand[mode];
|
|
595
|
-
return text === "" ? `${mnemonic}${suffix}` : `${mnemonic}${suffix} ${text}`;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
591
|
/** The plain number of an operand: what the probe assembles, and what a comment shows. */
|
|
599
592
|
export function literalOperand(opcode: number, value: number): string {
|
|
600
593
|
const { mode } = OPCODES[opcode]!;
|
|
@@ -1040,13 +1033,14 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1040
1033
|
out.push(`${label} = * + ${offset}`);
|
|
1041
1034
|
result.labels.push({ address: inside, name: label, file: piece.file, line: out.length });
|
|
1042
1035
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1036
|
+
// A comment at an operand byte (knowledge of an older version) shows with its address, as in the listing of c64-knowledge.
|
|
1037
|
+
for (const text of lineComments.get(inside) ?? []) sides.push(`${formatC64Address(inside)}: ${text}`);
|
|
1038
|
+
if (sideComments.has(inside)) sides.push(`${formatC64Address(inside)}: ${sideComments.get(inside)!}`);
|
|
1045
1039
|
}
|
|
1046
1040
|
const form = formOf(layout, item);
|
|
1047
1041
|
const { mode } = OPCODES[item.opcode]!;
|
|
1048
1042
|
const value = mode === "imp" ? 0 : operandValue(item.address, item.opcode, layout.byteAt)!;
|
|
1049
|
-
const literal =
|
|
1043
|
+
const literal = instructionWith(item.opcode, literalOperand(item.opcode, value));
|
|
1050
1044
|
let written = layout.operands.get(item.address);
|
|
1051
1045
|
// An expression that is not written stays visible as a side comment.
|
|
1052
1046
|
const asNumber = (expression: string, reason: string) => {
|
|
@@ -1062,13 +1056,11 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1062
1056
|
out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
|
|
1063
1057
|
result.asBytes.push({ address: item.address, text: literal });
|
|
1064
1058
|
} else if (written !== undefined) {
|
|
1065
|
-
|
|
1066
|
-
const expression = mode !== "imm" && written.startsWith("(") ? `0+${written}` : written;
|
|
1067
|
-
out.push(withSide(`${INDENT}${instructionText(item.opcode, form.suffix, expression)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
1059
|
+
out.push(withSide(`${INDENT}${instructionWith(item.opcode, written, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
1068
1060
|
result.expressions.push({ address: item.address, length: item.length, file: piece.file, line: out.length });
|
|
1069
1061
|
} else {
|
|
1070
1062
|
const expression = takesAddress(mode) ? (layout.names.get(value) ?? literalOperand(item.opcode, value)) : literalOperand(item.opcode, value);
|
|
1071
|
-
out.push(withSide(`${INDENT}${
|
|
1063
|
+
out.push(withSide(`${INDENT}${instructionWith(item.opcode, expression, form.suffix)}`, sides.length === 0 ? undefined : sides.join(" ")));
|
|
1072
1064
|
}
|
|
1073
1065
|
mapLine(item.address, item.address + item.length - 1);
|
|
1074
1066
|
result.instructions++;
|