@henols/c64-re-tools 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -3
- package/skills/c64-assembler/scripts/assembler.ts +29 -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 +3 -2
- package/skills/c64-disk/scripts/disk.ts +26 -12
- package/skills/c64-disk/scripts/lib/containers.ts +10 -11
- 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 +5 -3
- package/skills/c64-docs/scripts/lib/reference.ts +2 -2
- package/skills/c64-emulator/SKILL.md +9 -2
- package/skills/c64-knowledge/SKILL.md +9 -5
- package/skills/c64-knowledge/scripts/knowledge.ts +91 -53
- package/skills/c64-knowledge/scripts/lib/listing.ts +8 -30
- package/skills/c64-memory-map/SKILL.md +2 -1
- package/skills/c64-memory-map/scripts/memory-map.ts +4 -3
- package/skills/c64-provenance/SKILL.md +6 -4
- package/skills/c64-provenance/scripts/lib/library.ts +18 -6
- package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
- package/skills/c64-provenance/scripts/provenance.ts +14 -10
- package/skills/c64-source/SKILL.md +3 -0
- package/skills/c64-source/scripts/lib/source.ts +9 -31
- package/skills/c64-source/scripts/source.ts +14 -10
- package/skills/c64-static-analysis/SKILL.md +2 -0
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
- package/skills/c64-static-analysis/scripts/static-analysis.ts +18 -8
- package/skills/c64-testing/SKILL.md +14 -3
- package/skills/c64-testing/references/recording-format.md +2 -0
- package/skills/c64-testing/references/scenario-format.md +6 -0
- package/skills/c64-testing/scripts/lib/diff-files.ts +76 -14
- package/skills/c64-testing/scripts/lib/diff.ts +3 -3
- package/skills/c64-testing/scripts/lib/recording.ts +50 -15
- package/skills/c64-testing/scripts/lib/replay.ts +12 -1
- package/skills/c64-testing/scripts/lib/runs.ts +84 -4
- package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
- package/skills/c64-testing/scripts/testing.ts +20 -7
- package/skills/c64-unpacker/SKILL.md +17 -10
- package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
- package/skills/c64-unpacker/scripts/lib/loads.ts +14 -7
- package/skills/c64-unpacker/scripts/lib/machine.ts +30 -3
- package/skills/c64-unpacker/scripts/lib/stages.ts +15 -7
- package/skills/c64-unpacker/scripts/unpacker.ts +93 -42
- package/src/host/vice/adapter.ts +4 -1
- package/src/host/vice/session.ts +148 -13
- package/src/host-client/image.ts +40 -8
- package/src/host-client/steps.ts +145 -9
- package/src/host-client/tools.ts +1 -1
- package/src/host-client/transfer.ts +33 -18
- package/src/knowledge/expression.ts +1 -1
- package/src/knowledge/history.ts +74 -4
- package/src/knowledge/import.ts +79 -7
- package/src/knowledge/names.ts +2 -1
- package/src/knowledge/program.ts +32 -18
- package/src/knowledge/read.ts +46 -0
- package/src/knowledge/schema.ts +4 -2
- package/src/knowledge/text.ts +2 -0
- package/src/knowledge/write.ts +58 -16
- package/src/mcp/inputs.ts +6 -3
- package/src/mcp/output.ts +5 -0
- package/src/mcp/server.ts +20 -4
- package/src/mcp/tools/debug.ts +36 -9
- package/src/mcp/tools/execution.ts +18 -5
- package/src/mcp/tools/machine.ts +3 -1
- package/src/mcp/tools/media.ts +14 -2
- package/src/mcp/tools/memory.ts +11 -6
- package/src/mcp/tools/video.ts +5 -3
- package/src/media.ts +42 -0
- package/src/opcodes.ts +51 -18
- package/src/project.ts +111 -3
- package/src/protocol/vice.ts +8 -2
- package/src/script.ts +55 -3
|
@@ -17,11 +17,12 @@ import { operandText } from "#src/knowledge/expression.ts";
|
|
|
17
17
|
import { historyAt, historyNamed, historyOfMacro, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
18
18
|
import { joinBatches, type Batch, type JoinChoice, type JoinClash } from "#src/knowledge/join.ts";
|
|
19
19
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
20
|
-
import { addCoverage,
|
|
20
|
+
import { addCoverage, programCoverage, programImage, requireProgram, setImage, type ImageRequest } from "#src/knowledge/program.ts";
|
|
21
21
|
import {
|
|
22
22
|
analyzerNamesAt,
|
|
23
23
|
at,
|
|
24
24
|
commentsAt,
|
|
25
|
+
coverageRuns,
|
|
25
26
|
currentRevision,
|
|
26
27
|
listAnalyzerNames,
|
|
27
28
|
listComments,
|
|
@@ -83,7 +84,7 @@ import {
|
|
|
83
84
|
} from "#src/knowledge/write.ts";
|
|
84
85
|
import { platformNames } from "#src/platform.ts";
|
|
85
86
|
import { isInside, LOCAL_DIRECTORY, PROJECT_DIRECTORY, projectRelative, projectRoot, resolveProjectPath } from "#src/project.ts";
|
|
86
|
-
import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec, type SubjectSpec } from "#src/script.ts";
|
|
87
|
+
import { listed, OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec, type SubjectSpec } from "#src/script.ts";
|
|
87
88
|
import { listing, listingText, type ListingSource } from "./lib/listing.ts";
|
|
88
89
|
|
|
89
90
|
function oneOf<T extends string>(values: readonly T[], value: string | undefined, what: string): T {
|
|
@@ -150,9 +151,18 @@ function suppression(row: SuppressionRow) {
|
|
|
150
151
|
return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/** A row of the program in a revision: memory bytes, the entry, or a coverage range. */
|
|
155
|
+
function programRow(entry: Extract<HistoryEntry, { entity: "memory" | "entry" | "coverage" }>) {
|
|
156
|
+
if (entry.entity === "memory") return { ...spaceOf(entry.row), start: formatC64Address(entry.row.address), end: formatC64Address(entry.row.address + entry.row.bytes - 1), bytes: entry.row.bytes, origin: entry.row.origin, revision: entry.row.revision };
|
|
157
|
+
if (entry.entity === "entry") return { ...spaceOf(entry.row), address: formatC64Address(entry.row.address), state: entry.row.state, stages: entry.row.stages, origin: entry.row.origin, revision: entry.row.revision };
|
|
158
|
+
return { ...spaceOf(entry.row), kind: entry.row.kind, start: formatC64Address(entry.row.start), end: formatC64Address(entry.row.end), origin: entry.row.origin, revision: entry.row.revision };
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
function historyEntry(entry: HistoryEntry) {
|
|
154
162
|
const shown =
|
|
155
|
-
entry.entity === "
|
|
163
|
+
entry.entity === "memory" || entry.entity === "entry" || entry.entity === "coverage"
|
|
164
|
+
? programRow(entry)
|
|
165
|
+
: entry.entity === "symbol" || entry.entity === "analyzer-name"
|
|
156
166
|
? symbol(entry.row)
|
|
157
167
|
: entry.entity === "region"
|
|
158
168
|
? region(entry.row)
|
|
@@ -226,13 +236,17 @@ function listingSource(db: DatabaseSync | undefined, space: string): ListingSour
|
|
|
226
236
|
};
|
|
227
237
|
}
|
|
228
238
|
|
|
229
|
-
/**
|
|
239
|
+
/** Every value of --origin, for the message of a wrong one. */
|
|
240
|
+
const ORIGIN_VALUES = `one of ${ORIGINS.join(", ")}, semantic or analyzer, or a comma list of them. semantic is ${SEMANTIC_ORIGINS.join(" and ")}. analyzer is ${ANALYZERS.slice(0, -1).join(", ")} and ${ANALYZERS.at(-1)}`;
|
|
241
|
+
|
|
242
|
+
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa, ghidra and heuristic), or a comma list. */
|
|
230
243
|
function originFilter(value: string): Origin[] {
|
|
231
244
|
const origins = new Set<Origin>();
|
|
232
245
|
for (const part of value.split(",")) {
|
|
233
246
|
if (part === "semantic") for (const origin of SEMANTIC_ORIGINS) origins.add(origin);
|
|
234
247
|
else if (part === "analyzer") for (const origin of ANALYZERS) origins.add(origin);
|
|
235
|
-
else
|
|
248
|
+
else if ((ORIGINS as readonly string[]).includes(part)) origins.add(part as Origin);
|
|
249
|
+
else throw new UsageError(`--origin cannot be ${JSON.stringify(part)}. Give ${ORIGIN_VALUES}`);
|
|
236
250
|
}
|
|
237
251
|
return [...origins];
|
|
238
252
|
}
|
|
@@ -371,6 +385,20 @@ const WRITE: Record<string, OptionSpec> = {
|
|
|
371
385
|
"expect-revision": OPTIONS["expect-revision"],
|
|
372
386
|
};
|
|
373
387
|
|
|
388
|
+
/** A list command: --max-listed limits each list of its result. */
|
|
389
|
+
const LIST = { "max-listed": OPTIONS["max-listed"] };
|
|
390
|
+
/** Where to get the rest of a list that a --range limits. */
|
|
391
|
+
const RANGE_REST = "a larger --max-listed, a smaller --range, or knowledge.ts export --out <file> for all current knowledge";
|
|
392
|
+
const MORE = "a larger --max-listed";
|
|
393
|
+
|
|
394
|
+
/** A list in a result: its count and its first --max-listed items, with rest when it is cut. */
|
|
395
|
+
function list<T, U>(call: Call, items: readonly T[], show: (item: T) => U, rest: string) {
|
|
396
|
+
return listed(items, call.count("max-listed")!, show, rest);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** All rows: the lists count every row, and --max-listed limits only what the result shows. */
|
|
400
|
+
const ALL = Number.MAX_SAFE_INTEGER;
|
|
401
|
+
|
|
374
402
|
/** The rows from --range, or every address. */
|
|
375
403
|
const rangeOf = (call: Call) => call.range("range") ?? { start: 0x0000, end: 0xffff };
|
|
376
404
|
const originsOf = (call: Call) => {
|
|
@@ -401,7 +429,8 @@ function writeCommand(spec: Omit<CommandSpec, "run">, prepare: (call: Call) => W
|
|
|
401
429
|
return {
|
|
402
430
|
...spec,
|
|
403
431
|
notes: [spec.notes, "A write is one revision. It makes .c64-re-tools/knowledge/ when it is not there."].filter((note) => note !== undefined).join("\n"),
|
|
404
|
-
|
|
432
|
+
// A command can give a WRITE option its own help or default, for example --space of revert.
|
|
433
|
+
options: Object.fromEntries(Object.entries({ ...spec.options, ...WRITE }).map(([name, option]) => [name, spec.options?.[name] ?? option])),
|
|
405
434
|
run: (call) => withWrite(prepare(call)),
|
|
406
435
|
};
|
|
407
436
|
}
|
|
@@ -459,50 +488,50 @@ const READ_COMMANDS: CommandSpec[] = [
|
|
|
459
488
|
name: "search",
|
|
460
489
|
summary: "Symbol names, analyzer names and comment texts that hold the text.",
|
|
461
490
|
subjects: [{ name: "<text>", help: "the text to find" }],
|
|
462
|
-
options: { origin: OPTIONS.origin,
|
|
491
|
+
options: { origin: OPTIONS.origin, ...LIST },
|
|
463
492
|
},
|
|
464
493
|
(db, call) => {
|
|
465
|
-
const found = search(db, call.subject(0),
|
|
466
|
-
|
|
494
|
+
const found = search(db, call.subject(0), ALL, originsOf(call), call.space);
|
|
495
|
+
const rest = "a larger --max-listed, or a longer <text>";
|
|
496
|
+
return { symbols: list(call, found.symbols, symbol, rest), analyzerNames: list(call, found.analyzerNames, symbol, rest), comments: list(call, found.comments, comment, rest) };
|
|
467
497
|
},
|
|
468
498
|
),
|
|
469
499
|
readCommand(
|
|
470
|
-
{ name: "symbols", summary: "The symbols, or with --analyzer the second names that DXA, Ghidra and the idioms gave.", options: { ...FILTER, kind: SYMBOL_KIND, analyzer: { kind: "flag", help: "the analyzer names in place of the symbols" } } },
|
|
500
|
+
{ name: "symbols", summary: "The symbols, or with --analyzer the second names that DXA, Ghidra and the idioms gave.", options: { ...FILTER, kind: SYMBOL_KIND, analyzer: { kind: "flag", help: "the analyzer names in place of the symbols" }, ...LIST } },
|
|
471
501
|
(db, call) => {
|
|
472
502
|
const kind = call.string("kind") as (typeof SYMBOL_KINDS)[number] | undefined;
|
|
473
|
-
if (call.flag("analyzer")) return { analyzerNames: listAnalyzerNames(db, rangeOf(call), kind, originsOf(call), call.space)
|
|
474
|
-
return { symbols: listSymbols(db, rangeOf(call), kind, originsOf(call), call.space)
|
|
503
|
+
if (call.flag("analyzer")) return { analyzerNames: list(call, listAnalyzerNames(db, rangeOf(call), kind, originsOf(call), call.space), symbol, RANGE_REST) };
|
|
504
|
+
return { symbols: list(call, listSymbols(db, rangeOf(call), kind, originsOf(call), call.space), symbol, RANGE_REST) };
|
|
475
505
|
},
|
|
476
506
|
),
|
|
477
|
-
readCommand({ name: "regions", summary: "The regions: code, data, tables and the other types.", options: { ...FILTER, type: { kind: "choice", value: "<type>", choices: REGION_TYPES, help: `one of ${REGION_TYPES.join(", ")}` } } }, (db, call) => ({
|
|
478
|
-
regions: listRegions(db, rangeOf(call), call.string("type") as (typeof REGION_TYPES)[number] | undefined, originsOf(call), call.space)
|
|
507
|
+
readCommand({ name: "regions", summary: "The regions: code, data, tables and the other types.", options: { ...FILTER, type: { kind: "choice", value: "<type>", choices: REGION_TYPES, help: `one of ${REGION_TYPES.join(", ")}` }, ...LIST } }, (db, call) => ({
|
|
508
|
+
regions: list(call, listRegions(db, rangeOf(call), call.string("type") as (typeof REGION_TYPES)[number] | undefined, originsOf(call), call.space), region, RANGE_REST),
|
|
479
509
|
})),
|
|
480
|
-
readCommand({ name: "comments", summary: "The comments.", options: FILTER }, (db, call) => ({ comments: listComments(db, rangeOf(call), originsOf(call), call.space)
|
|
481
|
-
readCommand({ name: "references", summary: "The references: calls, jumps, reads, writes and pointers.", options: { ...FILTER, kind: REFERENCE_KIND } }, (db, call) => ({
|
|
482
|
-
references: listReferences(db, rangeOf(call), call.string("kind") as (typeof REFERENCE_KINDS)[number] | undefined, originsOf(call), call.space)
|
|
510
|
+
readCommand({ name: "comments", summary: "The comments.", options: { ...FILTER, ...LIST } }, (db, call) => ({ comments: list(call, listComments(db, rangeOf(call), originsOf(call), call.space), comment, RANGE_REST) })),
|
|
511
|
+
readCommand({ name: "references", summary: "The references: calls, jumps, reads, writes and pointers.", options: { ...FILTER, kind: REFERENCE_KIND, ...LIST } }, (db, call) => ({
|
|
512
|
+
references: list(call, listReferences(db, rangeOf(call), call.string("kind") as (typeof REFERENCE_KINDS)[number] | undefined, originsOf(call), call.space), reference, RANGE_REST),
|
|
483
513
|
})),
|
|
484
|
-
readCommand({ name: "suppressions", summary: "The places that removals keep free of analyzer facts, with the reason of each.", options: { range: OPTIONS.range } }, (db, call) => {
|
|
514
|
+
readCommand({ name: "suppressions", summary: "The places that removals keep free of analyzer facts, with the reason of each.", options: { range: OPTIONS.range, ...LIST } }, (db, call) => {
|
|
485
515
|
const rows = listSuppressions(db, rangeOf(call), call.space);
|
|
486
|
-
const reasons = reasonsOf(db, rows);
|
|
487
|
-
return { suppressions: rows
|
|
516
|
+
const reasons = reasonsOf(db, rows.slice(0, call.count("max-listed")!));
|
|
517
|
+
return { suppressions: list(call, rows, (row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) }), RANGE_REST) };
|
|
488
518
|
}),
|
|
489
|
-
readCommand({ name: "modules", summary: "Where the files of the generated source start.", options: FILTER }, (db, call) => ({ modules: listModules(db, rangeOf(call), originsOf(call), call.space)
|
|
490
|
-
readCommand({ name: "operands", summary: "The operand expressions of instructions.", options: FILTER }, (db, call) => ({ operands: listOperands(db, rangeOf(call), originsOf(call), call.space)
|
|
491
|
-
readCommand({ name: "patches", summary: "The code that a switch puts in.", options: { ...FILTER, switch: { kind: "text", value: "<switch>", help: "only the patches of this switch" } } }, (db, call) => ({
|
|
492
|
-
patches: listPatches(db, rangeOf(call), call.string("switch"), originsOf(call), call.space)
|
|
519
|
+
readCommand({ name: "modules", summary: "Where the files of the generated source start.", options: { ...FILTER, ...LIST } }, (db, call) => ({ modules: list(call, listModules(db, rangeOf(call), originsOf(call), call.space), module, RANGE_REST) })),
|
|
520
|
+
readCommand({ name: "operands", summary: "The operand expressions of instructions.", options: { ...FILTER, ...LIST } }, (db, call) => ({ operands: list(call, listOperands(db, rangeOf(call), originsOf(call), call.space), operand, RANGE_REST) })),
|
|
521
|
+
readCommand({ name: "patches", summary: "The code that a switch puts in.", options: { ...FILTER, switch: { kind: "text", value: "<switch>", help: "only the patches of this switch" }, ...LIST } }, (db, call) => ({
|
|
522
|
+
patches: list(call, listPatches(db, rangeOf(call), call.string("switch"), originsOf(call), call.space), patch, RANGE_REST),
|
|
493
523
|
})),
|
|
494
|
-
readCommand({ name: "switches", summary: "Each switch with the ranges of its patches." }, (db, call) => {
|
|
524
|
+
readCommand({ name: "switches", summary: "Each switch with the ranges of its patches.", options: LIST }, (db, call) => {
|
|
495
525
|
const switches = new Map<string, PatchRow[]>();
|
|
496
526
|
for (const row of listPatches(db, undefined, undefined, undefined, call.space)) switches.set(row.switch, [...(switches.get(row.switch) ?? []), row]);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
};
|
|
527
|
+
const show = ([name, rows]: [string, PatchRow[]]) => ({ name, define: `--define ${name}=1`, patches: rows.map((row) => ({ start: formatC64Address(row.start), end: formatC64Address(row.end) })) });
|
|
528
|
+
return { switches: list(call, [...switches].sort(([a], [b]) => (a < b ? -1 : 1)), show, MORE) };
|
|
500
529
|
}),
|
|
501
|
-
readCommand({ name: "macros", summary: "The macros of the generated source.", options: { origin: OPTIONS.origin } }, (db, call) => ({ macros: listMacros(db, originsOf(call), call.space)
|
|
502
|
-
readCommand({ name: "uses", summary: "The instructions written as a macro.", options: { ...FILTER, macro: { kind: "text", value: "<name>", help: "only the uses of this macro" } } }, (db, call) => ({
|
|
503
|
-
uses: listMacroUses(db, rangeOf(call), call.string("macro"), originsOf(call), call.space)
|
|
530
|
+
readCommand({ name: "macros", summary: "The macros of the generated source.", options: { origin: OPTIONS.origin, ...LIST } }, (db, call) => ({ macros: list(call, listMacros(db, originsOf(call), call.space), macro, MORE) })),
|
|
531
|
+
readCommand({ name: "uses", summary: "The instructions written as a macro.", options: { ...FILTER, macro: { kind: "text", value: "<name>", help: "only the uses of this macro" }, ...LIST } }, (db, call) => ({
|
|
532
|
+
uses: list(call, listMacroUses(db, rangeOf(call), call.string("macro"), originsOf(call), call.space), macroUse, RANGE_REST),
|
|
504
533
|
})),
|
|
505
|
-
readCommand({ name: "spaces", summary: "The address spaces that hold knowledge." }, (db) => ({ spaces: listSpaces(db) })),
|
|
534
|
+
readCommand({ name: "spaces", summary: "The address spaces that hold knowledge.", options: LIST }, (db, call) => ({ spaces: list(call, listSpaces(db), (space) => space, MORE) })),
|
|
506
535
|
readCommand({ name: "program", summary: "The program in the knowledge: its segments and entry, and the coverage maps that coverage imported, with the ranges." }, (db, call) => {
|
|
507
536
|
const image = programImage(db, call.space);
|
|
508
537
|
const ranges = db === undefined ? [] : (db.prepare("SELECT kind, count(*) AS count FROM current_coverage WHERE space = ? GROUP BY kind ORDER BY kind").all(call.space) as Array<{ kind: string; count: number }>);
|
|
@@ -526,7 +555,7 @@ const READ_COMMANDS: CommandSpec[] = [
|
|
|
526
555
|
name: "history",
|
|
527
556
|
summary: "The rows at an address, of a name or of a macro, through all revisions, each with the reason and the evidence of its revision.",
|
|
528
557
|
subjects: [{ ...ADDRESS, optional: true }],
|
|
529
|
-
options: { name: { kind: "text", value: "<name>", help: "the history of a name in place of an address" }, macro: { kind: "text", value: "<name>", help: "the history of a macro in place of an address" } },
|
|
558
|
+
options: { name: { kind: "text", value: "<name>", help: "the history of a name in place of an address" }, macro: { kind: "text", value: "<name>", help: "the history of a macro in place of an address" }, ...LIST },
|
|
530
559
|
notes: "Give exactly one of <address>, --name and --macro.",
|
|
531
560
|
},
|
|
532
561
|
(db, call) => {
|
|
@@ -534,25 +563,22 @@ const READ_COMMANDS: CommandSpec[] = [
|
|
|
534
563
|
const macroName = call.string("macro");
|
|
535
564
|
if ([call.subjects.length > 0, name !== undefined, macroName !== undefined].filter(Boolean).length !== 1) throw new UsageError("history needs exactly one of <address>, --name and --macro");
|
|
536
565
|
const entries = macroName !== undefined ? historyOfMacro(db, macroName, call.space) : name !== undefined ? historyNamed(db, name, call.space) : historyAt(db, call.parseAddress(call.subject(0), "<address>"), call.space);
|
|
537
|
-
return { history: entries
|
|
566
|
+
return { history: list(call, entries, (entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) }), MORE) };
|
|
538
567
|
},
|
|
539
568
|
),
|
|
540
569
|
readCommand(
|
|
541
|
-
{ name: "revisions", summary: "The revisions, newest first.", options: {
|
|
570
|
+
{ name: "revisions", summary: "The revisions, newest first.", options: { before: { kind: "text", value: "<revision>", help: "only the revisions before this one" }, ...LIST } },
|
|
542
571
|
(db, call) => {
|
|
543
|
-
const options: { limit?: number; before?: string } = {};
|
|
544
|
-
const limit = call.count("limit");
|
|
545
|
-
if (limit !== undefined) options.limit = limit;
|
|
546
572
|
const before = call.string("before");
|
|
547
|
-
|
|
548
|
-
return { revisions:
|
|
573
|
+
const rows = revisions(db, before === undefined ? { limit: ALL } : { limit: ALL, before });
|
|
574
|
+
return { revisions: list(call, rows, (row) => row, "a larger --max-listed, or --before <revision> with the oldest revision in listed") };
|
|
549
575
|
},
|
|
550
576
|
),
|
|
551
|
-
readCommand({ name: "revision", summary: "The rows that one revision added and closed.", subjects: [REVISION] }, (db, call) => {
|
|
577
|
+
readCommand({ name: "revision", summary: "The rows that one revision added and closed: knowledge rows, and memory, entry and coverage rows of the program.", subjects: [REVISION], options: LIST }, (db, call) => {
|
|
552
578
|
const changes = revision(db, call.subject(0));
|
|
553
|
-
return { revision: changes.revision, added: changes.added
|
|
579
|
+
return { revision: changes.revision, added: list(call, changes.added, historyEntry, MORE), closed: list(call, changes.closed, historyEntry, MORE) };
|
|
554
580
|
}),
|
|
555
|
-
readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
|
|
581
|
+
readCommand({ name: "export", summary: "All current knowledge to a project file as JSON.", options: { ...FILTER, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the JSON file, relative to the project folder", required: true } } }, (db, call) => {
|
|
556
582
|
const range = rangeOf(call);
|
|
557
583
|
const origins = originsOf(call);
|
|
558
584
|
const space = call.space;
|
|
@@ -579,14 +605,14 @@ const READ_COMMANDS: CommandSpec[] = [
|
|
|
579
605
|
name: "listing",
|
|
580
606
|
summary: "The program as ACME lines with names, operand expressions, comments, the access of each line in the coverage (x executed, r read, w written) and the references in and out.",
|
|
581
607
|
subjects: [RANGE],
|
|
582
|
-
options: { out: { ...OPTIONS.out, value: "<file>", help: "write the listing as text to this file in place of the lines in the result" } },
|
|
608
|
+
options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "write the listing as text to this file in place of the lines in the result" }, ...LIST },
|
|
583
609
|
},
|
|
584
610
|
(db, call) => {
|
|
585
611
|
const { start, end } = call.parseRange(call.subject(0), "<range>");
|
|
586
612
|
const lines = listing(listingSource(db, call.space), start, end);
|
|
587
613
|
const out = call.string("out");
|
|
588
614
|
if (out !== undefined) return { lines: lines.length, ...writeReported(out, listingText(lines), { what: "--out" }) };
|
|
589
|
-
return { lines: lines
|
|
615
|
+
return { lines: list(call, lines, (row) => ({ ...row, address: formatC64Address(row.address) }), "a larger --max-listed, a smaller <range>, or --out <file> for all lines") };
|
|
590
616
|
},
|
|
591
617
|
),
|
|
592
618
|
];
|
|
@@ -821,11 +847,23 @@ const WRITE_COMMANDS: CommandSpec[] = [
|
|
|
821
847
|
};
|
|
822
848
|
},
|
|
823
849
|
),
|
|
824
|
-
writeCommand(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
850
|
+
writeCommand(
|
|
851
|
+
{
|
|
852
|
+
name: "revert",
|
|
853
|
+
summary: "Makes the knowledge the same as at one revision: all spaces, or with --space only that space. The revert is a new revision: the history stays complete.",
|
|
854
|
+
subjects: [REVISION],
|
|
855
|
+
options: { space: { kind: "space", value: "<space>", help: "revert only this space: c64, drive8, or bank0 to bank255. The other spaces do not change. Without --space, every space reverts" } },
|
|
856
|
+
},
|
|
857
|
+
(call) => {
|
|
858
|
+
const context = contextOf(call);
|
|
859
|
+
const space = call.string("space");
|
|
860
|
+
const request = { revision: call.subject(0), ...(space === undefined ? {} : { space }) };
|
|
861
|
+
return (db) => {
|
|
862
|
+
const result = revert(db, context, request);
|
|
863
|
+
return { revision: result.revision, restores: result.current.restoredRevision, ...(space === undefined ? { spaces: "all" } : { space }) };
|
|
864
|
+
};
|
|
865
|
+
},
|
|
866
|
+
),
|
|
829
867
|
writeCommand({ name: "merge", summary: "Decides the conflicts that a git merge of two computers' work left: of the rows of one conflict, the newest stays. One revision for all." }, (call) => {
|
|
830
868
|
const context = contextOf(call);
|
|
831
869
|
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
@@ -869,7 +907,7 @@ const JOIN: CommandSpec = {
|
|
|
869
907
|
name: "join",
|
|
870
908
|
summary: "Joins the batches of agents that worked in parallel into one batch for apply. It writes no knowledge.",
|
|
871
909
|
subjects: [{ name: "<batch>", help: 'a batch: a list of writes, or {"owner": {"start": "$1000", "end": "$1fff"}, "ops": [...]}', list: true }],
|
|
872
|
-
options: { ...SPACE, out: { ...OPTIONS.out, value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
|
|
910
|
+
options: { ...SPACE, out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the joined batch, relative to the project folder", required: true } },
|
|
873
911
|
notes: "At an address in the owner range of a batch, its name, comment, operand and module win, and its regions come last. One name or one module file at two addresses writes nothing.",
|
|
874
912
|
run: (call) => {
|
|
875
913
|
if (call.subjects.length < 2) throw new UsageError("join needs two or more batch files");
|
|
@@ -924,7 +962,7 @@ function batchOperation(call: Call, item: unknown, at: string): BatchOperation {
|
|
|
924
962
|
function batchFields(fields: Record<string, unknown>, at: string, address: (text: string, what: string) => number): BatchOperation {
|
|
925
963
|
const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
|
|
926
964
|
const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
|
|
927
|
-
if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
|
|
965
|
+
if (unknown.length > 0) throw new Refusal("invalid-input", `${at}: ${op} takes ${[...BATCH_FIELDS[op], "space"].join(", ")}, not ${unknown.join(", ")}`);
|
|
928
966
|
const text = (key: string) => {
|
|
929
967
|
if (typeof fields[key] !== "string") throw new Refusal("invalid-input", `${at}: ${key} must be a string`);
|
|
930
968
|
return fields[key] as string;
|
|
@@ -990,7 +1028,7 @@ function batchFields(fields: Record<string, unknown>, at: string, address: (text
|
|
|
990
1028
|
|
|
991
1029
|
await runScript({
|
|
992
1030
|
name: "knowledge",
|
|
993
|
-
summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks.",
|
|
1031
|
+
summary: "Reads and writes the knowledge of the project: names, regions, comments, references, the program and its coverage, and the source knowledge. A read needs no knowledge folder. Every command takes --space: c64 (the default), drive8 for 1541 drive code, or bank0 to bank255 for cartridge banks. revert without --space reverts all spaces.",
|
|
994
1032
|
names: knowledgeNames,
|
|
995
1033
|
commands: [...READ_COMMANDS, ...WRITE_COMMANDS, JOIN],
|
|
996
1034
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// reads files: knowledge.ts gives the lookups.
|
|
5
5
|
|
|
6
6
|
import { COVERAGE, formatC64Address, formatC64Byte } from "#src/c64.ts";
|
|
7
|
-
import { MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
|
|
7
|
+
import { instructionWith, MODE_LENGTH, OPCODES, type Mode } from "#src/opcodes.ts";
|
|
8
8
|
|
|
9
9
|
export interface ListingSource {
|
|
10
10
|
/** The image byte at an address, or undefined outside the image. */
|
|
@@ -52,36 +52,13 @@ function accessOf(coverage: Uint8Array | undefined, start: number, length: numbe
|
|
|
52
52
|
return text === "" ? "-" : text;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** The operand of an instruction
|
|
55
|
+
/** The operand value of an instruction as an expression: the name of its target where one exists, else the number. Empty for no operand. */
|
|
56
56
|
function operandOf(mode: Mode, address: number, low: number, high: number, nameOf: ListingSource["nameOf"]): string {
|
|
57
|
-
const word = low | (high << 8);
|
|
58
57
|
const named = (target: number, digits: 2 | 4) => nameOf(target) ?? (digits === 2 ? formatC64Byte(target) : formatC64Address(target));
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return `#${formatC64Byte(low)}`;
|
|
64
|
-
case "zp":
|
|
65
|
-
return named(low, 2);
|
|
66
|
-
case "zpx":
|
|
67
|
-
return `${named(low, 2)},x`;
|
|
68
|
-
case "zpy":
|
|
69
|
-
return `${named(low, 2)},y`;
|
|
70
|
-
case "abs":
|
|
71
|
-
return named(word, 4);
|
|
72
|
-
case "abx":
|
|
73
|
-
return `${named(word, 4)},x`;
|
|
74
|
-
case "aby":
|
|
75
|
-
return `${named(word, 4)},y`;
|
|
76
|
-
case "ind":
|
|
77
|
-
return `(${named(word, 4)})`;
|
|
78
|
-
case "izx":
|
|
79
|
-
return `(${named(low, 2)},x)`;
|
|
80
|
-
case "izy":
|
|
81
|
-
return `(${named(low, 2)}),y`;
|
|
82
|
-
case "rel":
|
|
83
|
-
return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
|
|
84
|
-
}
|
|
58
|
+
if (mode === "imp") return "";
|
|
59
|
+
if (mode === "imm") return formatC64Byte(low);
|
|
60
|
+
if (mode === "rel") return named((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff, 4);
|
|
61
|
+
return MODE_LENGTH[mode] === 3 ? named(low | (high << 8), 4) : named(low, 2);
|
|
85
62
|
}
|
|
86
63
|
|
|
87
64
|
/** The lines of start..end: instructions in code regions, decoded from the region start, and bytes elsewhere. */
|
|
@@ -123,8 +100,9 @@ export function listing(source: ListingSource, start: number, end: number): List
|
|
|
123
100
|
const length = MODE_LENGTH[opcode.mode];
|
|
124
101
|
const bytes = Array.from({ length: length - 1 }, (_, offset) => source.byteAt(address + 1 + offset));
|
|
125
102
|
if (address + length - 1 <= region.end && bytes.every((value) => value !== undefined)) {
|
|
103
|
+
// The operand expression of the knowledge in the syntax of the addressing mode, as c64-source writes it.
|
|
126
104
|
const operand = source.operand(address) ?? operandOf(opcode.mode, address, bytes[0] ?? 0, bytes[1] ?? 0, source.nameOf);
|
|
127
|
-
if (address >= start) lines.push(line(address, length,
|
|
105
|
+
if (address >= start) lines.push(line(address, length, instructionWith(byte, operand)));
|
|
128
106
|
address += length;
|
|
129
107
|
continue;
|
|
130
108
|
}
|
|
@@ -31,7 +31,7 @@ node <skill>/scripts/memory-map.ts export --out src/hw.asm
|
|
|
31
31
|
|---|---|
|
|
32
32
|
| `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
|
|
33
33
|
| `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F`, `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`. |
|
|
34
|
-
| `list <vic\|sid\|cia1\|cia2\|kernal\|system>` |
|
|
34
|
+
| `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | The entries of that group as a list: `count` and `listed`. `--max-listed` limits `listed`. The default is 50. |
|
|
35
35
|
| `export --out <file>` | Writes all platform names to a project file as an ACME include: one line for each name, with its meaning as a comment. A zero-page name is `NAME = $xx`. Other names are `NAME = $xxxx`. |
|
|
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.
|
|
@@ -43,6 +43,7 @@ Write a value for `decode` and `--bank` as `$` and hex digits, as a decimal numb
|
|
|
43
43
|
- 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
44
|
- A range is `<start>-<end>`, for example `'$c000-$cfff'`. In a JSON file, a range is `start` and `end`.
|
|
45
45
|
- Bytes in a JSON file are hex in a field `data`, for example `"data": "a900"`.
|
|
46
|
+
- 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
47
|
- 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
48
|
- An error is `{"error": {"code": "...", "message": "..."}}`. The message tells what is wrong and what to do.
|
|
48
49
|
- The exit status is 0 for done or PASS, 1 for FAIL, 2 for an error and 3 for INCONCLUSIVE.
|
|
@@ -6,7 +6,7 @@ import { formatC64Address, formatC64Byte } from "#src/c64.ts";
|
|
|
6
6
|
import { writeReported } from "#src/host-client/transfer.ts";
|
|
7
7
|
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
8
8
|
import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
|
|
9
|
-
import { OPTIONS, runScript, UsageError } from "#src/script.ts";
|
|
9
|
+
import { listed, OPTIONS, runScript, UsageError } from "#src/script.ts";
|
|
10
10
|
|
|
11
11
|
const show = (entry: Entry) => ({ start: formatC64Address(entry.start), ...(entry.end !== entry.start ? { end: formatC64Address(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
|
|
12
12
|
|
|
@@ -61,16 +61,17 @@ await runScript({
|
|
|
61
61
|
name: "list",
|
|
62
62
|
summary: "The registers of a chip, the KERNAL jump table, or the system areas.",
|
|
63
63
|
subjects: [{ name: "<list>", help: `one of ${Object.keys(LISTS).join(", ")}` }],
|
|
64
|
+
options: { "max-listed": OPTIONS["max-listed"] },
|
|
64
65
|
run: (call) => {
|
|
65
66
|
const list = LISTS[call.subject(0)];
|
|
66
67
|
if (list === undefined) throw new UsageError(`<list> must be one of ${Object.keys(LISTS).join(", ")}, not ${call.subject(0)}`);
|
|
67
|
-
return { entries: list().
|
|
68
|
+
return { entries: listed(list(), call.count("max-listed")!, show, "a larger --max-listed, or memory-map.ts export --out <file> for every platform name") };
|
|
68
69
|
},
|
|
69
70
|
},
|
|
70
71
|
{
|
|
71
72
|
name: "export",
|
|
72
73
|
summary: "Writes the platform names as an ACME include: NAME = $xxxx.",
|
|
73
|
-
options: { out: { ...OPTIONS.out, value: "<file>", help: "the include file, relative to the project folder", required: true } },
|
|
74
|
+
options: { out: { ...OPTIONS.out, output: "file", value: "<file>", help: "the include file, relative to the project folder", required: true } },
|
|
74
75
|
run: (call) => {
|
|
75
76
|
const names = platformNames();
|
|
76
77
|
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}`)];
|
|
@@ -38,7 +38,7 @@ node <skill>/scripts/provenance.ts leftovers --out .c64-re-tools/local/leftovers
|
|
|
38
38
|
|
|
39
39
|
| Option | Meaning |
|
|
40
40
|
|---|---|
|
|
41
|
-
| `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. |
|
|
41
|
+
| `--shift 2:-256` | Move release 2 (the first release is 1) by -256 bytes. Use it again for more releases. Give one shift for each release. |
|
|
42
42
|
| `--range '$0900-$09ff'` | Compare only the addresses in this range. |
|
|
43
43
|
| `--frames 600` | Run each release for this many frames, and give each listed difference its runtime evidence. |
|
|
44
44
|
| `--steps .c64-re-tools/local/start-game.json` | With `--frames`: a JSON list of action steps to run before the frames, for example a key to start the game. The steps are the steps of a c64-unpacker steps file. |
|
|
@@ -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,7 @@ 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
|
-
- `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. Run c64-static-analysis first, so that the knowledge has the code regions and the references.
|
|
100
|
+
- `codeRegions.unreachable`: code regions of the knowledge that no chain of call and jump references reaches from the entry. 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
101
|
- `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
102
|
|
|
102
103
|
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 +113,7 @@ Use `leftovers` to find what the program holds in addition to its working code.
|
|
|
112
113
|
| `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
114
|
| `residue` | Bytes that no run touched and that are no text, BASIC, fill or code region. `bytes` shows the first 16 bytes. |
|
|
114
115
|
| `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
|
|
116
|
+
| `deadCode` | Code regions of the knowledge that the entry does not reach, as in `codeRegions.unreachable` of `separate`, and that no run executed. |
|
|
116
117
|
|
|
117
118
|
Text from another computer can show the tools of the developer:
|
|
118
119
|
|
|
@@ -162,7 +163,8 @@ To save a routine:
|
|
|
162
163
|
- `--name` is the name of the routine and of its file. When you do not give it, the script uses the user or LLM name at the start.
|
|
163
164
|
- `--title` is the title of the routine. When you do not give it, the script uses the module title.
|
|
164
165
|
- A routine has at least 16 bytes. Its last instruction must end in the range.
|
|
165
|
-
3.
|
|
166
|
+
3. The result shows the routine file as `{out, bytes, sha256}`. `out` is relative to the project folder.
|
|
167
|
+
4. Read `leftOut`. It lists the rows that the file does not keep, and why. For example, the file does not keep an operand expression that uses a name outside the routine.
|
|
166
168
|
|
|
167
169
|
To find the routines in a program:
|
|
168
170
|
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
// its start, so match finds it in another program at another address and
|
|
5
5
|
// suggests its names there as an apply batch. Nothing here writes knowledge.
|
|
6
6
|
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join, resolve } from "node:path";
|
|
9
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
9
10
|
|
|
10
11
|
import { Refusal } from "#src/errors.ts";
|
|
12
|
+
import { writeRefusal, type Written } from "#src/host-client/transfer.ts";
|
|
11
13
|
import { formatC64Address, instructionLength } from "#src/c64.ts";
|
|
12
14
|
import { evaluate, identifiers, operandText } from "#src/knowledge/expression.ts";
|
|
13
15
|
import type { CommentRow, OperandRow, ReferenceRow, RegionRow, SymbolRow } from "#src/knowledge/read.ts";
|
|
@@ -317,16 +319,26 @@ export function readLibrary(folder: string): Routine[] {
|
|
|
317
319
|
.map((name) => parseRoutine(readFileSync(join(folder, name), "utf8"), name));
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
/**
|
|
321
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Writes a routine file into the library: a temporary file, then a rename.
|
|
324
|
+
* Refuses a name that exists unless `replace`. Reports the file as a written
|
|
325
|
+
* file: its path relative to the project folder `root`, its size and its SHA-256.
|
|
326
|
+
*/
|
|
327
|
+
export function saveRoutine(folder: string, routine: Routine, replace: boolean, root = process.cwd()): Written {
|
|
322
328
|
const file = join(folder, `${routine.name}.json`);
|
|
329
|
+
const out = relative(root, file).split(sep).join("/");
|
|
323
330
|
if (!replace && statSync(file, { throwIfNoEntry: false }) !== undefined) {
|
|
324
331
|
throw new Refusal("conflict", `The library already has the routine ${routine.name}. Give another --name, or give --replace to save over it.`);
|
|
325
332
|
}
|
|
333
|
+
const bytes = Buffer.from(routineText(routine), "utf8");
|
|
326
334
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
try {
|
|
336
|
+
writeFileSync(temporary, bytes);
|
|
337
|
+
renameSync(temporary, file);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
throw writeRefusal(error, out, "--library") ?? error;
|
|
340
|
+
}
|
|
341
|
+
return { out, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
330
342
|
}
|
|
331
343
|
|
|
332
344
|
/** An image to search: its segments and its bytes inside them. */
|
|
@@ -1,10 +1,11 @@
|
|
|
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
|
-
// region of knowledge, whether call and jump references
|
|
4
|
-
// entry; and what the program reads
|
|
5
|
-
// only: nothing here decides what is
|
|
3
|
+
// region of knowledge, whether call and jump references or the run on from
|
|
4
|
+
// its last instruction reach it from the entry; and what the program reads
|
|
5
|
+
// or executes outside the image. Evidence only: nothing here decides what is
|
|
6
|
+
// original.
|
|
6
7
|
|
|
7
|
-
import { type AddressRange, COVERAGE, executedRamCode } from "#src/c64.ts";
|
|
8
|
+
import { type AddressRange, COVERAGE, executedRamCode, instructionLength } from "#src/c64.ts";
|
|
8
9
|
|
|
9
10
|
export type Access = "executed" | "read" | "written" | "read-written" | "untouched";
|
|
10
11
|
|
|
@@ -49,6 +50,27 @@ export interface Separation {
|
|
|
49
50
|
|
|
50
51
|
const SYSTEM_PAGES_END = 0x03ff;
|
|
51
52
|
|
|
53
|
+
/** BRK, JMP, RTI, RTS, JMP (indirect) and the JAM opcodes: the CPU does not run on to the next instruction after them. */
|
|
54
|
+
const NO_FALL_THROUGH = new Set([0x00, 0x4c, 0x40, 0x60, 0x6c, 0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, 0x92, 0xb2, 0xd2, 0xf2]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The address after a code region where the CPU runs on, or undefined when
|
|
58
|
+
* its last instruction does not run on (RTS, RTI, JMP, BRK, JAM). The
|
|
59
|
+
* instructions follow one another from the start of the region.
|
|
60
|
+
*/
|
|
61
|
+
export function fallThrough(region: AddressRange, memory: Uint8Array): number | undefined {
|
|
62
|
+
let at = region.start;
|
|
63
|
+
let last = at;
|
|
64
|
+
while (at <= region.end) {
|
|
65
|
+
last = at;
|
|
66
|
+
at += instructionLength(memory[at]!);
|
|
67
|
+
}
|
|
68
|
+
return NO_FALL_THROUGH.has(memory[last]!) || at > 0xffff ? undefined : at;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The most instructions outside the code regions that the run on from a region goes through, for example an instruction in a dual-use region. */
|
|
72
|
+
const MAX_RUN_ON = 16;
|
|
73
|
+
|
|
52
74
|
/** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
|
|
53
75
|
function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): AddressRange[] {
|
|
54
76
|
const ranges: AddressRange[] = [];
|
|
@@ -92,12 +114,29 @@ export function separate(input: SeparationInput): Separation {
|
|
|
92
114
|
}
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
// Reachability: from the region that holds the entry, through call and jump references
|
|
117
|
+
// Reachability: from the region that holds the entry, through call and jump references and the run on
|
|
118
|
+
// from the last instruction of a region into the next address, to a fixed point.
|
|
96
119
|
const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
|
|
97
120
|
const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
|
|
98
121
|
const reached = new Set<number>();
|
|
99
122
|
if (input.entry !== undefined && holding(input.entry) >= 0) reached.add(holding(input.entry));
|
|
100
|
-
|
|
123
|
+
// The CPU runs the bytes after the region as instructions: through bytes of no code region (a dual-use
|
|
124
|
+
// instruction, for example) into the next code region, unless one of them does not run on.
|
|
125
|
+
const runOn = (from: number): number | undefined => {
|
|
126
|
+
let at = from;
|
|
127
|
+
for (let count = 0; count <= MAX_RUN_ON && at <= 0xffff && inImage(at); count++) {
|
|
128
|
+
if (holding(at) >= 0) return at;
|
|
129
|
+
if (NO_FALL_THROUGH.has(input.memory[at]!)) return undefined;
|
|
130
|
+
at += instructionLength(input.memory[at]!);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
};
|
|
134
|
+
const runsOn = regions.flatMap((region) => {
|
|
135
|
+
const next = fallThrough(region, input.memory);
|
|
136
|
+
const to = next === undefined ? undefined : runOn(next);
|
|
137
|
+
return to === undefined ? [] : [{ from: region.end, to }];
|
|
138
|
+
});
|
|
139
|
+
const flows = [...input.references.filter((reference) => reference.kind === "call" || reference.kind === "jump"), ...runsOn];
|
|
101
140
|
for (let grown = true; grown; ) {
|
|
102
141
|
grown = false;
|
|
103
142
|
for (const flow of flows) {
|