@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -3,27 +3,21 @@
|
|
|
3
3
|
// knowledge; a target that knowledge does not name gets l_xxxx. Data is
|
|
4
4
|
// written by its region type. The source assembles to the same bytes: the
|
|
5
5
|
// generator forces operand sizes, and writes an opcode form that ACME
|
|
6
|
-
// encodes differently as bytes (see the probe in
|
|
6
|
+
// encodes differently as bytes (see the probe in source.ts). An operand
|
|
7
7
|
// expression of the knowledge is written in place of its number. A
|
|
8
8
|
// patch is written as !if SWITCH { code } else { original }, and a
|
|
9
9
|
// macro use as one +name args line.
|
|
10
10
|
|
|
11
|
-
import { COVERAGE, formatC64Address, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
|
|
11
|
+
import { type AddressRange, COVERAGE, formatC64Address, formatC64Byte, POINTER_TABLE_TYPES, pointerTableEntries, type PointerTableType } from "#src/c64.ts";
|
|
12
12
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
13
13
|
import { identifiers, operandText, replaceIdentifiers, type OperandExpression } from "#src/knowledge/expression.ts";
|
|
14
|
-
import {
|
|
15
|
-
import { isGeneratedFile, type CommentPlacement, type Origin, type RegionType } from "#src/knowledge/schema.ts";
|
|
14
|
+
import { type CommentPlacement, isAnalyzer, isGeneratedFile, type Origin, type RegionType } from "#src/knowledge/schema.ts";
|
|
16
15
|
import type { Area } from "#src/platform.ts";
|
|
17
16
|
import { hasZeroPageForm, MNEMONICS, MODE_LENGTH, OPCODES, takesAddress, USE_KINDS, useKind, type Mode, type UseKind } from "#src/opcodes.ts";
|
|
18
17
|
|
|
19
|
-
export interface Range {
|
|
20
|
-
start: number;
|
|
21
|
-
end: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
export interface SourceInput {
|
|
25
19
|
/** In address order, without overlap. */
|
|
26
|
-
segments:
|
|
20
|
+
segments: AddressRange[];
|
|
27
21
|
/** The image byte at an address inside a segment. */
|
|
28
22
|
byteAt: (address: number) => number;
|
|
29
23
|
entry?: number;
|
|
@@ -144,7 +138,7 @@ function decodedStores(pieces: readonly Piece[], input: SourceInput): Array<{ ad
|
|
|
144
138
|
if (input.segments.some((segment) => target >= segment.start && target <= segment.end)) continue;
|
|
145
139
|
try {
|
|
146
140
|
const text = decodedText(input.decode(target, input.byteAt(load.address + 1)));
|
|
147
|
-
if (text !== "") found.push({ address: store.address, placement: "side", text: `${
|
|
141
|
+
if (text !== "") found.push({ address: store.address, placement: "side", text: `${formatC64Address(target)}: ${text}` });
|
|
148
142
|
} catch {
|
|
149
143
|
// No decoder for this register.
|
|
150
144
|
}
|
|
@@ -212,10 +206,8 @@ export interface Layout {
|
|
|
212
206
|
macrosAsInstructions: MacroAsInstructions[];
|
|
213
207
|
}
|
|
214
208
|
|
|
215
|
-
const hex2 = (value: number) => `$${value.toString(16).padStart(2, "0")}`;
|
|
216
|
-
const hex4 = (value: number) => formatC64Address(value);
|
|
217
209
|
/** ACME reads leading zeros as a 16-bit value, so a zero-page name gets two digits. */
|
|
218
|
-
const constantValue = (address: number) => (address < 0x100 ?
|
|
210
|
+
const constantValue = (address: number) => (address < 0x100 ? formatC64Byte(address) : formatC64Address(address));
|
|
219
211
|
const signed = (byte: number) => (byte < 0x80 ? byte : byte - 0x100);
|
|
220
212
|
|
|
221
213
|
function operandValue(address: number, opcode: number, byteAt: (address: number) => number): number | undefined {
|
|
@@ -240,9 +232,9 @@ export function entriesInData<R extends { start: number; end: number; type: Regi
|
|
|
240
232
|
}
|
|
241
233
|
|
|
242
234
|
/** Why an image without its entry fails: the image does not hold the program. Undefined when the entry is in a segment, or the image has no entry. */
|
|
243
|
-
export function entryOutsideImage(entry: number | undefined, segments: readonly
|
|
235
|
+
export function entryOutsideImage(entry: number | undefined, segments: readonly AddressRange[]): string | undefined {
|
|
244
236
|
if (entry === undefined || segments.some((segment) => entry >= segment.start && entry <= segment.end)) return undefined;
|
|
245
|
-
return `The entry ${
|
|
237
|
+
return `The entry ${formatC64Address(entry)} is in no segment of the image: the image does not hold the program. Capture it again with c64-unpacker loads or capture.`;
|
|
246
238
|
}
|
|
247
239
|
|
|
248
240
|
/** Splits the segments into pieces at module starts and lays out instructions and data. */
|
|
@@ -255,7 +247,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
255
247
|
};
|
|
256
248
|
|
|
257
249
|
for (const module of input.modules) {
|
|
258
|
-
if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${
|
|
250
|
+
if (isGeneratedFile(module.file)) throw new WireFailure("invalid-input", `The module at ${formatC64Address(module.start)} has the file ${module.file}, which the generator writes itself. Give the module another file.`);
|
|
259
251
|
}
|
|
260
252
|
const modules = [...input.modules].sort((a, b) => a.start - b.start);
|
|
261
253
|
const unusedModules = modules.filter((module) => !inImage(module.start)).map(({ start, file }) => ({ start, file }));
|
|
@@ -274,14 +266,14 @@ export function layout(input: SourceInput): Layout {
|
|
|
274
266
|
const end = bounds[index + 1]! - 1;
|
|
275
267
|
const module = modules.find((row) => row.start === start);
|
|
276
268
|
const file = module?.file ?? `segment_${start.toString(16).padStart(4, "0")}.asm`;
|
|
277
|
-
const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${
|
|
269
|
+
const title = module?.title?.replace(/\s*\r?\n\s*/g, " ") ?? `Segment ${formatC64Address(segment.start)}-${formatC64Address(segment.end)}`;
|
|
278
270
|
pieces.push({ start, end, file, title, segmentStart: index === 0, items: [] });
|
|
279
271
|
}
|
|
280
272
|
}
|
|
281
273
|
const files = new Map<string, number>();
|
|
282
274
|
for (const piece of pieces) {
|
|
283
275
|
const other = files.get(piece.file);
|
|
284
|
-
if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${
|
|
276
|
+
if (other !== undefined) throw new WireFailure("invalid-input", `The file ${piece.file} is used at ${formatC64Address(other)} and at ${formatC64Address(piece.start)}. Give the module another file.`);
|
|
285
277
|
files.set(piece.file, piece.start);
|
|
286
278
|
}
|
|
287
279
|
|
|
@@ -329,7 +321,6 @@ export function layout(input: SourceInput): Layout {
|
|
|
329
321
|
const knowledgeNames = new Set(input.symbols.map((symbol) => symbol.name));
|
|
330
322
|
const platform = new Map((input.platform ?? []).filter(({ name }) => !knowledgeNames.has(name) && !MNEMONICS.has(name.toLowerCase())).map(({ address, name }) => [address, name]));
|
|
331
323
|
// An analyzer name (DXA's ld020) gives way to the platform name (EXTCOL); a user or LLM name wins.
|
|
332
|
-
const isAnalyzer = (origin: Origin | undefined) => (ANALYZERS as readonly (Origin | undefined)[]).includes(origin);
|
|
333
324
|
const symbols = new Map(input.symbols.filter((symbol) => !(isAnalyzer(symbol.origin) && platform.has(symbol.address))).map((symbol) => [symbol.address, symbol.name]));
|
|
334
325
|
/** Addresses outside the image without a name of their own, inside a named platform entry of more bytes: the start of that entry. */
|
|
335
326
|
const insideEntry = new Map<number, number>();
|
|
@@ -444,7 +435,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
444
435
|
if (inside === undefined) continue;
|
|
445
436
|
accepted.delete(candidate);
|
|
446
437
|
changed = true;
|
|
447
|
-
macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${
|
|
438
|
+
macrosAsInstructions.push({ start: candidate.use.start, end: candidate.use.end, macro: candidate.use.macro, reason: `The source has a label at ${formatC64Address(inside)}, inside the use.` });
|
|
448
439
|
for (const item of candidate.items) {
|
|
449
440
|
inUse.delete(item.address);
|
|
450
441
|
if (takesAddress(OPCODES[item.opcode]!.mode) && !written.has(item.address)) use(operandValue(item.address, item.opcode, input.byteAt)!, useKind(item.opcode));
|
|
@@ -459,7 +450,7 @@ export function layout(input: SourceInput): Layout {
|
|
|
459
450
|
for (const address of [...wanted, ...outside].sort((a, b) => a - b)) {
|
|
460
451
|
const known = symbols.get(address);
|
|
461
452
|
if (known !== undefined) {
|
|
462
|
-
if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${
|
|
453
|
+
if (MNEMONICS.has(known.toLowerCase())) throw new WireFailure("invalid-input", `The symbol ${known} at ${formatC64Address(address)} has the name of an instruction, which ACME cannot read as a label. Rename the symbol with c64-knowledge.`);
|
|
463
454
|
names.set(address, known);
|
|
464
455
|
if (inImage(address)) namedLabels++;
|
|
465
456
|
continue;
|
|
@@ -488,14 +479,14 @@ export function layout(input: SourceInput): Layout {
|
|
|
488
479
|
continue;
|
|
489
480
|
}
|
|
490
481
|
if (patch.end > piece.end) {
|
|
491
|
-
refuse(`The range runs past the end of the file ${piece.file} at ${
|
|
482
|
+
refuse(`The range runs past the end of the file ${piece.file} at ${formatC64Address(piece.end)}: a patch is in one file.`);
|
|
492
483
|
continue;
|
|
493
484
|
}
|
|
494
485
|
const starts = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address : item.start)));
|
|
495
486
|
const ends = new Set(piece.items.map((item) => (item.kind === "instruction" ? item.address + item.length - 1 : item.end)));
|
|
496
487
|
if (!starts.has(patch.start) || !ends.has(patch.end)) {
|
|
497
488
|
const cut = piece.items.find((item) => item.kind === "instruction" && ((patch.start > item.address && patch.start < item.address + item.length) || (patch.end >= item.address && patch.end < item.address + item.length - 1)));
|
|
498
|
-
refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${
|
|
489
|
+
refuse(cut === undefined ? "The range does not start and end at an item of the source." : `The range cuts the instruction at ${formatC64Address((cut as InstructionItem).address)}.`);
|
|
499
490
|
continue;
|
|
500
491
|
}
|
|
501
492
|
if (sourceNames.has(patch.switch)) {
|
|
@@ -607,7 +598,7 @@ export function instructionText(opcode: number, suffix: string, expression: stri
|
|
|
607
598
|
/** The plain number of an operand: what the probe assembles, and what a comment shows. */
|
|
608
599
|
export function literalOperand(opcode: number, value: number): string {
|
|
609
600
|
const { mode } = OPCODES[opcode]!;
|
|
610
|
-
return MODE_LENGTH[mode] === 3 || mode === "rel" ?
|
|
601
|
+
return MODE_LENGTH[mode] === 3 || mode === "rel" ? formatC64Address(value) : formatC64Byte(value);
|
|
611
602
|
}
|
|
612
603
|
|
|
613
604
|
/** One instruction of each form, for the probe: the form, and the instruction bytes. */
|
|
@@ -659,9 +650,9 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
|
|
|
659
650
|
const { byteAt } = layout;
|
|
660
651
|
const cells: Cell[] = [];
|
|
661
652
|
const name = (address: number) => layout.names.get(address);
|
|
662
|
-
const expression = (target: number) => name(target) ??
|
|
663
|
-
const rtsExpression = (target: number) => (name(target) === undefined ?
|
|
664
|
-
const byteCell = (address: number, text =
|
|
653
|
+
const expression = (target: number) => name(target) ?? formatC64Address(target);
|
|
654
|
+
const rtsExpression = (target: number) => (name(target) === undefined ? formatC64Address((target - 1) & 0xffff) : `${name(target)}-1`);
|
|
655
|
+
const byteCell = (address: number, text = formatC64Byte(byteAt(address))): Cell => ({ address, length: 1, directive: "!byte", text });
|
|
665
656
|
|
|
666
657
|
if (item.region !== undefined && POINTER_TYPES.includes(item.type)) {
|
|
667
658
|
const rts = item.type === "rts-addresses" || item.type === "rts-addresses-split";
|
|
@@ -696,7 +687,7 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
|
|
|
696
687
|
const offset = address - (item.region?.start ?? item.start);
|
|
697
688
|
const last = odd && address === item.region!.end;
|
|
698
689
|
if (offset % 2 === 0 && !last && address + 1 <= item.end && !breaks.has(address + 1)) {
|
|
699
|
-
cells.push({ address, length: 2, directive: "!word", text:
|
|
690
|
+
cells.push({ address, length: 2, directive: "!word", text: formatC64Address(byteAt(address) | (byteAt(address + 1) << 8)) });
|
|
700
691
|
address++;
|
|
701
692
|
} else cells.push(byteCell(address));
|
|
702
693
|
}
|
|
@@ -721,12 +712,12 @@ function cellsOf(item: DataItem, layout: Layout, breaks: Set<number>): Cell[] {
|
|
|
721
712
|
let end = address;
|
|
722
713
|
while (end + 1 <= item.end && byteAt(end + 1) === value && !breaks.has(end + 1)) end++;
|
|
723
714
|
if (end - address + 1 >= MIN_FILL) {
|
|
724
|
-
cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${
|
|
715
|
+
cells.push({ address, length: end - address + 1, directive: "!fill", text: `${end - address + 1}, ${formatC64Byte(value)}` });
|
|
725
716
|
address = end;
|
|
726
717
|
continue;
|
|
727
718
|
}
|
|
728
719
|
}
|
|
729
|
-
cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` :
|
|
720
|
+
cells.push(byteCell(address, binary ? `%${value.toString(2).padStart(8, "0")}` : formatC64Byte(value)));
|
|
730
721
|
}
|
|
731
722
|
return cells;
|
|
732
723
|
}
|
|
@@ -779,7 +770,7 @@ function describeConstant(layout: Layout, address: number): string | undefined {
|
|
|
779
770
|
const mirrorOf = layout.describe?.(address).mirrorOf;
|
|
780
771
|
if (mirrorOf === undefined) return undefined;
|
|
781
772
|
const register = layout.platform.find((row) => row.address === mirrorOf);
|
|
782
|
-
return `Mirror of ${
|
|
773
|
+
return `Mirror of ${formatC64Address(mirrorOf)}${register === undefined ? "" : ` (${register.name})`}.`;
|
|
783
774
|
}
|
|
784
775
|
|
|
785
776
|
/** How the source uses a constant, for example "Used: read 4, written 1." */
|
|
@@ -801,7 +792,7 @@ function switchLines(layout: Layout): string[] {
|
|
|
801
792
|
const width = Math.max(...[...switches.keys()].map((name) => name.length));
|
|
802
793
|
const lines = ["; Switches: each is 0 unless the build defines it. Assemble with -DNAME=1", "; (c64-assembler --define NAME=1) to put in the patches of NAME."];
|
|
803
794
|
for (const [name, patches] of switches) {
|
|
804
|
-
const ranges = patches.map((patch) => `${
|
|
795
|
+
const ranges = patches.map((patch) => `${formatC64Address(patch.start)}-${formatC64Address(patch.end)}`).join(", ");
|
|
805
796
|
lines.push(`!ifndef ${name} { ${name} = 0 }${" ".repeat(width - name.length)} ; ${patches.length} patch${patches.length === 1 ? "" : "es"}: ${ranges}`);
|
|
806
797
|
}
|
|
807
798
|
return [...lines, ""];
|
|
@@ -829,7 +820,7 @@ function constantsText(layout: Layout): { text: string; labels: SourceLabel[] }
|
|
|
829
820
|
const next = describe?.(row.address).area;
|
|
830
821
|
if (next !== undefined && next.start !== area?.start) {
|
|
831
822
|
area = next;
|
|
832
|
-
out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${
|
|
823
|
+
out.push("", `; ${area.name[0]!.toUpperCase()}${area.name.slice(1)} ${formatC64Address(area.start)}-${formatC64Address(area.end)}`);
|
|
833
824
|
if (area.meaning !== undefined) out.push(`; ${area.meaning}`);
|
|
834
825
|
}
|
|
835
826
|
const line = `${row.name.padEnd(nameColumn)} = ${row.value.padEnd(valueColumn)}`;
|
|
@@ -865,7 +856,7 @@ export interface Rendered {
|
|
|
865
856
|
/** Instructions written as bytes, because ACME writes their form with other bytes. */
|
|
866
857
|
asBytes: Array<{ address: number; text: string }>;
|
|
867
858
|
/** With coverage: runs of instructions that no run executed. */
|
|
868
|
-
notExecuted?: { instructions: number; ranges:
|
|
859
|
+
notExecuted?: { instructions: number; ranges: AddressRange[] };
|
|
869
860
|
/** Each instruction written with an operand expression: its file and line (1 is the first line of the text). */
|
|
870
861
|
expressions: Array<{ address: number; length: number; file: string; line: number }>;
|
|
871
862
|
/** Operand expressions written as numbers, in address order. */
|
|
@@ -900,7 +891,7 @@ function macrosText(layout: Layout): { text: string; lines: Map<string, { first:
|
|
|
900
891
|
/** The lines that open a patch: its zone, the switch, the code, and the guard that stops a patch longer than its range. */
|
|
901
892
|
function patchOpening(patch: PatchBlock): string[] {
|
|
902
893
|
const bytes = patch.end - patch.start + 1;
|
|
903
|
-
const range = `${
|
|
894
|
+
const range = `${formatC64Address(patch.start)}-${formatC64Address(patch.end)}`;
|
|
904
895
|
const comment = patch.comment === undefined ? [] : patch.comment.split(/\r?\n/).map((line) => `${INDENT}; ${line}`.trimEnd());
|
|
905
896
|
return [
|
|
906
897
|
`!zone patch_${patch.switch}_${patch.start.toString(16).padStart(4, "0")} {`,
|
|
@@ -909,7 +900,7 @@ function patchOpening(patch: PatchBlock): string[] {
|
|
|
909
900
|
`${INDENT}; ${patch.switch} on: ${bytes} byte${bytes === 1 ? "" : "s"} at ${range}, NOP after the code`,
|
|
910
901
|
...comment,
|
|
911
902
|
...(patch.code === "" ? [] : patch.code.split(/\r?\n/)),
|
|
912
|
-
`${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${
|
|
903
|
+
`${INDENT}!if * - .patch_start > ${bytes} { !error "The patch ${patch.switch} at ${formatC64Address(patch.start)} has ", * - .patch_start, " bytes; the range has ${bytes}." } else { !fill ${bytes} - (* - .patch_start), $ea }`,
|
|
913
904
|
"} else {",
|
|
914
905
|
];
|
|
915
906
|
}
|
|
@@ -957,10 +948,10 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
957
948
|
result.macrosAsInstructions.sort((a, b) => a.start - b.start);
|
|
958
949
|
const unwritten = new Map(layout.operandsAsNumbers.map((row) => [row.address, row.expression]));
|
|
959
950
|
const { coverage } = layout;
|
|
960
|
-
const notExecuted = { instructions: 0, ranges: [] as
|
|
951
|
+
const notExecuted = { instructions: 0, ranges: [] as AddressRange[] };
|
|
961
952
|
|
|
962
953
|
for (const piece of layout.pieces) {
|
|
963
|
-
const out: string[] = [`; ${piece.title}`, `; ${
|
|
954
|
+
const out: string[] = [`; ${piece.title}`, `; ${formatC64Address(piece.start)}-${formatC64Address(piece.end)}`, ""];
|
|
964
955
|
/** True while the instructions before have not executed: they form one range. */
|
|
965
956
|
let running = false;
|
|
966
957
|
/** The line of the mark of the open range of instructions that did not execute. */
|
|
@@ -981,7 +972,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
981
972
|
}
|
|
982
973
|
// The mark gives the range, so a reader finds it in the full list.
|
|
983
974
|
const range = notExecuted.ranges.at(-1)!;
|
|
984
|
-
out[markLine] = `; not executed ${
|
|
975
|
+
out[markLine] = `; not executed ${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
|
|
985
976
|
notExecuted.instructions += count;
|
|
986
977
|
}
|
|
987
978
|
running = !executed;
|
|
@@ -1067,7 +1058,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1067
1058
|
if (written !== undefined && numbers.has(item.address)) asNumber(written, numbers.get(item.address)!);
|
|
1068
1059
|
if (unwritten.has(item.address)) sides.unshift(`= ${unwritten.get(item.address)!}`);
|
|
1069
1060
|
if (asBytes.has(form.key)) {
|
|
1070
|
-
const bytes = Array.from({ length: item.length }, (_, offset) =>
|
|
1061
|
+
const bytes = Array.from({ length: item.length }, (_, offset) => formatC64Byte(layout.byteAt(item.address + offset)));
|
|
1071
1062
|
out.push(withSide(`${INDENT}!byte ${bytes.join(",")}`, [literal, ...sides].join(" ")));
|
|
1072
1063
|
result.asBytes.push({ address: item.address, text: literal });
|
|
1073
1064
|
} else if (written !== undefined) {
|
|
@@ -1111,7 +1102,7 @@ export function render(layout: Layout, asBytes: ReadonlySet<string>, header: str
|
|
|
1111
1102
|
}
|
|
1112
1103
|
const main = [...header.map((line) => `; ${line}`), "", "!cpu 6510", '!source "constants.asm"', ...(layout.macros.length > 0 ? ['!source "macros.asm"'] : [])];
|
|
1113
1104
|
for (const piece of layout.pieces) {
|
|
1114
|
-
if (piece.segmentStart) main.push("", `* = ${
|
|
1105
|
+
if (piece.segmentStart) main.push("", `* = ${formatC64Address(piece.start)}`);
|
|
1115
1106
|
main.push(`!source "${piece.file}"`);
|
|
1116
1107
|
}
|
|
1117
1108
|
result.files.push({ name: "main.asm", text: `${main.join("\n")}\n` });
|
|
@@ -4,55 +4,38 @@
|
|
|
4
4
|
// also writes a loader that recreates the entry state, simulates it, and
|
|
5
5
|
// checks it in the emulator. Each switch of the patches is built on
|
|
6
6
|
// alone and checked. A macro use that does not give the bytes of the
|
|
7
|
-
// image is written as its instructions.
|
|
8
|
-
// passes,
|
|
7
|
+
// image is written as its instructions. result is PASS when every check
|
|
8
|
+
// passes, else FAIL.
|
|
9
9
|
|
|
10
10
|
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { parseArgs } from "node:util";
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { Refusal } from "#src/errors.ts";
|
|
14
|
+
import { type AddressRange, COVERAGE, coverageSeedKinds, formatC64Address, textToPetscii } from "#src/c64.ts";
|
|
15
15
|
import { autostartLoaded, RUN_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
16
16
|
import { MAX_MEMORY_READ, WireFailure } from "#src/host-client/tools.ts";
|
|
17
|
-
import {
|
|
17
|
+
import { writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
18
18
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
19
19
|
import { conflicts } from "#src/knowledge/conflicts.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { openForRead } from "#src/knowledge/database.ts";
|
|
21
21
|
import { identifiers } from "#src/knowledge/expression.ts";
|
|
22
22
|
import { analyzedBytes, programCoverage, requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
|
|
23
23
|
import { ANALYZER_NAME_ORDER, currentRevision, listAnalyzerNames, listComments, listMacros, listMacroUses, listModules, listOperands, listPatches, listReferences, listRegions, listSymbols, type SymbolRow } from "#src/knowledge/read.ts";
|
|
24
|
-
import { CODE_REGION_TYPES, DEFAULT_SPACE,
|
|
24
|
+
import { CODE_REGION_TYPES, DEFAULT_SPACE, type Space } from "#src/knowledge/schema.ts";
|
|
25
25
|
import { assemble } from "#src/native/acme.ts";
|
|
26
26
|
import { analyze } from "#src/native/ghidra/analyze.ts";
|
|
27
27
|
import { localToolContext } from "#src/native/local.ts";
|
|
28
28
|
import { GHIDRA_LIMITS, type AssembledSymbol, type Diagnostic } from "#src/native/types.ts";
|
|
29
29
|
import { decode as decodeRegister, describeAddress, platformNamesFor, romUseOf, romUseOfPort, type RomUse } from "#src/platform.ts";
|
|
30
30
|
import { resolveProjectPath } from "#src/project.ts";
|
|
31
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
32
|
-
import {
|
|
31
|
+
import { listed, OPTIONS, runScript, UsageError, type Call } from "#src/script.ts";
|
|
32
|
+
import { knowledgeNames } from "#src/knowledge/names.ts";
|
|
33
|
+
import { COPIER_SOURCE, LOADER_JUMP, loaderLimits, planLoader, rasterLine, loaderState, simulate, stopCause, VIC_DISPLAY_REGISTERS, type EntryState, type LoaderPlan } from "./lib/loader.ts";
|
|
33
34
|
import { OPCODES } from "#src/opcodes.ts";
|
|
34
|
-
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type
|
|
35
|
+
import { entriesInData, entryOutsideImage, instructionText, layout, literalOperand, probeForms, render, switchesOf, type Layout, type PatchBlock, type Rendered } from "./lib/source.ts";
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
The program, its entry state and the coverage come from the knowledge:
|
|
39
|
-
put them in with knowledge.ts image and coverage of the c64-knowledge skill.
|
|
40
|
-
Code that no imported run executed is marked and listed; <dir>/not_executed.json
|
|
41
|
-
has all of its ranges.
|
|
42
|
-
|
|
43
|
-
--out <dir> the directory for the source files, relative to the project
|
|
44
|
-
--entry <address> an entry point for Ghidra, for example '$080d' (repeatable)
|
|
45
|
-
--space <space> the knowledge space of the program: c64 (the default), drive8,
|
|
46
|
-
or bank0 to bank255; the coverage and --loader are for c64 only
|
|
47
|
-
--loader also write loader.asm and loader.prg: the loader copies the
|
|
48
|
-
segments into place, sets the entry state of the knowledge,
|
|
49
|
-
and jumps to the entry; it is checked in the emulator
|
|
50
|
-
--analyzer-names name an address that has no name of the user or the LLM, and
|
|
51
|
-
no platform name, with its analyzer name
|
|
52
|
-
|
|
53
|
-
The result is one JSON object. Addresses are $ and four hex digits.`;
|
|
54
|
-
|
|
55
|
-
const MAX_LISTED = 20;
|
|
37
|
+
/** At most this many diagnostics or differences of one check. */
|
|
38
|
+
const MAX_DIAGNOSTICS = 20;
|
|
56
39
|
/** Rounds of assembly that move operand expressions to numbers and macro uses to instructions. */
|
|
57
40
|
const ROUNDS = 6;
|
|
58
41
|
const PROBE_BASE = 0x1000;
|
|
@@ -66,7 +49,7 @@ function readKnowledge(space: Space, analyzerNames = false) {
|
|
|
66
49
|
try {
|
|
67
50
|
// With conflicts from a git merge, the source would depend on which of two rows comes first.
|
|
68
51
|
const found = conflicts(db).length;
|
|
69
|
-
if (found > 0) throw new
|
|
52
|
+
if (found > 0) throw new Refusal("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was generated. Run c64-knowledge's knowledge.ts merge, then generate again.`);
|
|
70
53
|
const symbols = listSymbols(db, undefined, undefined, undefined, space);
|
|
71
54
|
const second = listAnalyzerNames(db, undefined, undefined, undefined, space);
|
|
72
55
|
return {
|
|
@@ -392,7 +375,7 @@ async function checkSwitch(name: string, patches: PatchBlock[], files: Array<{ n
|
|
|
392
375
|
return found === null ? [] : [{ start: found[1]!, message: found[0] }];
|
|
393
376
|
});
|
|
394
377
|
if (overflow.length > 0) return { ...shown, result: "overflow", overflow };
|
|
395
|
-
return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0,
|
|
378
|
+
return { ...shown, result: "does not assemble", diagnostics: built.diagnostics.slice(0, MAX_DIAGNOSTICS) };
|
|
396
379
|
}
|
|
397
380
|
const inside = (address: number) => patches.some((patch) => address >= patch.start && address <= patch.end);
|
|
398
381
|
let changed = 0;
|
|
@@ -499,7 +482,7 @@ async function runLoader(path: string, program: Buffer, plan: LoaderPlan, image:
|
|
|
499
482
|
}
|
|
500
483
|
plan.cia.masks.forEach((mask, index) => differ(`CIA ${index + 1} interrupt mask`, chips[index]!.interruptMask, mask));
|
|
501
484
|
}
|
|
502
|
-
return { verified: differences.length === 0, differences: differences.slice(0,
|
|
485
|
+
return { verified: differences.length === 0, differences: differences.slice(0, MAX_DIAGNOSTICS) };
|
|
503
486
|
} finally {
|
|
504
487
|
await client.close();
|
|
505
488
|
}
|
|
@@ -548,46 +531,21 @@ function removeOldFiles(out: string, keep: ReadonlySet<string>): void {
|
|
|
548
531
|
}
|
|
549
532
|
}
|
|
550
533
|
|
|
551
|
-
async function
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
space: { type: "string" },
|
|
560
|
-
loader: { type: "boolean" },
|
|
561
|
-
"analyzer-names": { type: "boolean" },
|
|
562
|
-
help: { type: "boolean" },
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
if (values.help) throw new UsageError("");
|
|
566
|
-
if (positionals.length !== 0) throw new UsageError("generate takes no image: it reads the program from the knowledge (knowledge.ts image of the c64-knowledge skill puts it in)");
|
|
567
|
-
if (values.out === undefined) throw new UsageError("generate needs --out <dir>");
|
|
568
|
-
try {
|
|
569
|
-
resolveProjectPath(values.out);
|
|
570
|
-
} catch {
|
|
571
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
572
|
-
}
|
|
573
|
-
const entries = (values.entry ?? []).map((value) => {
|
|
574
|
-
try {
|
|
575
|
-
return parseC64Address(value);
|
|
576
|
-
} catch {
|
|
577
|
-
throw new UsageError(`--entry must be $ followed by four hex digits, not ${value}`);
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
const space = values.space ?? DEFAULT_SPACE;
|
|
581
|
-
if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
582
|
-
if (space !== DEFAULT_SPACE && values.loader) throw new UsageError("--loader works only in space c64");
|
|
583
|
-
const knowledge = readKnowledge(space, values["analyzer-names"] === true);
|
|
534
|
+
async function generate(call: Call): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
535
|
+
const out = call.string("out")!;
|
|
536
|
+
const entries = call.addresses("entry");
|
|
537
|
+
const max = call.count("max-listed")!;
|
|
538
|
+
const space = call.space;
|
|
539
|
+
const loaderWanted = call.flag("loader");
|
|
540
|
+
if (space !== DEFAULT_SPACE && loaderWanted) throw new UsageError("--loader works only in space c64");
|
|
541
|
+
const knowledge = readKnowledge(space, call.flag("analyzer-names"));
|
|
584
542
|
const image = knowledge.program;
|
|
585
543
|
const coverage = knowledge.coverage;
|
|
586
|
-
if (
|
|
587
|
-
throw new
|
|
544
|
+
if (loaderWanted && (image.state === undefined || image.entry === undefined)) {
|
|
545
|
+
throw new Refusal("not-found", "--loader needs the entry and the entry state of a c64-unpacker capture in the knowledge: put the capture in with knowledge.ts image <image.json>");
|
|
588
546
|
}
|
|
589
547
|
// Read the entry state before the long steps, so a bad state stops the run at once.
|
|
590
|
-
const state =
|
|
548
|
+
const state = loaderWanted ? loaderState(image.state, "the entry state of the knowledge") : undefined;
|
|
591
549
|
const romUse = romsInUse(image, coverage);
|
|
592
550
|
const instructions = await decode(image, knowledge, entries, coverage);
|
|
593
551
|
const laid = layout({
|
|
@@ -646,10 +604,10 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
646
604
|
files = marked([...rendered.files, ...loaderFiles]);
|
|
647
605
|
built = await assembleFiles(files, "main.asm");
|
|
648
606
|
}
|
|
649
|
-
removeOldFiles(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const showRange = (range:
|
|
607
|
+
removeOldFiles(out, new Set(files.map((file) => file.name)));
|
|
608
|
+
const written: Written[] = files.map((file) => writeReported(`${out}/${file.name}`, file.text, { what: "--out" }));
|
|
609
|
+
written.push(writeReported(`${out}/${SOURCE_MAP_FILE}`, sourceMapText(rendered, space, knowledge.revision, files), { what: "--out" }));
|
|
610
|
+
const showRange = (range: AddressRange) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
653
611
|
const inData = entriesInData(entries, knowledge.regions);
|
|
654
612
|
if (rendered.notExecuted !== undefined) {
|
|
655
613
|
// Each range with the file of the source that holds it and the routine before it, as a report groups them.
|
|
@@ -662,13 +620,13 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
662
620
|
const files: Record<string, number> = {};
|
|
663
621
|
for (const range of placed) if (range.file !== undefined) files[range.file] = (files[range.file] ?? 0) + 1;
|
|
664
622
|
const all = { revision: knowledge.revision, instructions: rendered.notExecuted.instructions, files, ranges: placed };
|
|
665
|
-
|
|
623
|
+
written.push(writeReported(`${out}/${NOT_EXECUTED_FILE}`, `${JSON.stringify(all, null, 2)}\n`, { what: "--out" }));
|
|
666
624
|
}
|
|
667
625
|
|
|
668
626
|
const output: Record<string, unknown> = {
|
|
669
|
-
out
|
|
670
|
-
files:
|
|
671
|
-
sourceMap: `${
|
|
627
|
+
out,
|
|
628
|
+
files: written,
|
|
629
|
+
sourceMap: `${out}/${SOURCE_MAP_FILE}`,
|
|
672
630
|
revision: knowledge.revision,
|
|
673
631
|
instructions: rendered.instructions,
|
|
674
632
|
dataBytes: rendered.dataBytes,
|
|
@@ -683,51 +641,37 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
683
641
|
: { entriesInData: inData.map(({ entry, region }) => ({ entry: formatC64Address(entry), region: { ...showRange(region), type: region.type }, origin: region.origin })) }),
|
|
684
642
|
...(rendered.asBytes.length === 0
|
|
685
643
|
? {}
|
|
686
|
-
: { asBytes:
|
|
644
|
+
: { asBytes: listed(rendered.asBytes, max, (item) => ({ address: formatC64Address(item.address), instruction: item.text })) }),
|
|
687
645
|
...(rendered.notExecuted === undefined
|
|
688
646
|
? {}
|
|
689
647
|
: {
|
|
690
|
-
notExecuted: {
|
|
691
|
-
instructions: rendered.notExecuted.instructions,
|
|
692
|
-
ranges: rendered.notExecuted.ranges.length,
|
|
693
|
-
listed: rendered.notExecuted.ranges.slice(0, MAX_LISTED).map(showRange),
|
|
694
|
-
all: `${values.out}/${NOT_EXECUTED_FILE}`,
|
|
695
|
-
},
|
|
648
|
+
notExecuted: { instructions: rendered.notExecuted.instructions, ...listed(rendered.notExecuted.ranges, max, showRange, `${out}/${NOT_EXECUTED_FILE}`) },
|
|
696
649
|
}),
|
|
697
650
|
...(laid.unusedModules.length === 0 ? {} : { unusedModules: laid.unusedModules.map((module) => ({ start: formatC64Address(module.start), file: module.file })) }),
|
|
698
651
|
...(rendered.expressions.length === 0 && rendered.operandsAsNumbers.length === 0 ? {} : { operandExpressions: rendered.expressions.length }),
|
|
699
652
|
...(rendered.operandsAsNumbers.length === 0
|
|
700
653
|
? {}
|
|
701
654
|
: {
|
|
702
|
-
operandsAsNumbers: {
|
|
703
|
-
count: rendered.operandsAsNumbers.length,
|
|
704
|
-
listed: rendered.operandsAsNumbers.slice(0, MAX_LISTED).map((item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
|
|
705
|
-
},
|
|
655
|
+
operandsAsNumbers: listed(rendered.operandsAsNumbers, max, (item) => ({ address: formatC64Address(item.address), expression: item.expression, reason: item.reason })),
|
|
706
656
|
}),
|
|
707
657
|
...(rendered.uses.length === 0 && rendered.macrosAsInstructions.length === 0 ? {} : { macroUses: rendered.uses.length }),
|
|
708
658
|
// A macro use that the source writes as its instructions does not fail the run: the source stays identical.
|
|
709
659
|
...(rendered.macrosAsInstructions.length === 0
|
|
710
660
|
? {}
|
|
711
661
|
: {
|
|
712
|
-
macrosAsInstructions: {
|
|
713
|
-
count: rendered.macrosAsInstructions.length,
|
|
714
|
-
listed: rendered.macrosAsInstructions.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
|
|
715
|
-
},
|
|
662
|
+
macrosAsInstructions: listed(rendered.macrosAsInstructions, max, (item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), macro: item.macro, reason: item.reason })),
|
|
716
663
|
}),
|
|
717
664
|
// A patch that the source cannot write fails the run: the switch would miss a part.
|
|
718
665
|
...(laid.patchesNotWritten.length === 0
|
|
719
666
|
? {}
|
|
720
667
|
: {
|
|
721
|
-
patchesNotWritten: {
|
|
722
|
-
count: laid.patchesNotWritten.length,
|
|
723
|
-
listed: laid.patchesNotWritten.slice(0, MAX_LISTED).map((item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
|
|
724
|
-
},
|
|
668
|
+
patchesNotWritten: listed(laid.patchesNotWritten, max, (item) => ({ start: formatC64Address(item.start), end: formatC64Address(item.end), switch: item.switch, reason: item.reason })),
|
|
725
669
|
}),
|
|
726
670
|
};
|
|
727
671
|
// The loader includes the source, so it is built only from a source that is identical to the image.
|
|
728
672
|
const notBuilt = (reason: string) => (state === undefined ? {} : { loader: { verified: false, reason: loaderRefused ?? reason } });
|
|
729
673
|
if (built.program === undefined) {
|
|
730
|
-
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0,
|
|
674
|
+
return { output: { ...output, check: { identical: false, diagnostics: built.diagnostics.slice(0, MAX_DIAGNOSTICS) }, ...notBuilt("The source does not assemble, and the loader includes it.") }, failed: true };
|
|
731
675
|
}
|
|
732
676
|
const checked = check(built.program, built.start!, image);
|
|
733
677
|
output.check = checked;
|
|
@@ -741,7 +685,7 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
741
685
|
if (loader === undefined || state === undefined) return { output, failed: missingEntry !== undefined || sourceFailed };
|
|
742
686
|
|
|
743
687
|
const loaderBuilt = await assembleFiles(files, "loader.asm");
|
|
744
|
-
if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0,
|
|
688
|
+
if (loaderBuilt.program === undefined) return { output: { ...output, loader: { verified: false, reason: "loader.asm does not assemble.", diagnostics: loaderBuilt.diagnostics.slice(0, MAX_DIAGNOSTICS) } }, failed: true };
|
|
745
689
|
const program = loaderBuilt.program;
|
|
746
690
|
// The simulation: the loader in memory as it loads, then every copy in its order, byte by byte.
|
|
747
691
|
const memory = new Int16Array(0x10000).fill(-1);
|
|
@@ -759,9 +703,8 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
759
703
|
const jump = jumpSymbol.kind === "address" ? jumpSymbol.address : jumpSymbol.value;
|
|
760
704
|
const wrong = simulate(memory, loader.copies, expected);
|
|
761
705
|
if (wrong !== undefined) throw new WireFailure("operation-failed", `The simulated loader writes a wrong byte at ${formatC64Address(wrong)}. This is a defect of the c64-source script.`);
|
|
762
|
-
const loaderPath = `${
|
|
763
|
-
|
|
764
|
-
(output.files as string[]).push("loader.prg");
|
|
706
|
+
const loaderPath = `${out}/loader.prg`;
|
|
707
|
+
written.push(writeReported(loaderPath, program, { what: "--out" }));
|
|
765
708
|
const copierSymbol = loaderBuilt.symbols?.find((symbol) => symbol.name === COPIER_SOURCE);
|
|
766
709
|
if (copierSymbol === undefined) throw new WireFailure("operation-failed", `The assembled loader has no ${COPIER_SOURCE} label. This is a defect of the c64-source script.`);
|
|
767
710
|
const copierSource = copierSymbol.kind === "address" ? copierSymbol.address : copierSymbol.value;
|
|
@@ -778,11 +721,27 @@ async function run(argv: string[]): Promise<{ output: Record<string, unknown>; f
|
|
|
778
721
|
return { output, failed: !ran.verified || sourceFailed };
|
|
779
722
|
}
|
|
780
723
|
|
|
781
|
-
await runScript(
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
724
|
+
await runScript({
|
|
725
|
+
name: "source",
|
|
726
|
+
summary: "Generates ACME source from the program and the knowledge, and checks with ACME that it assembles to the program byte by byte.",
|
|
727
|
+
names: knowledgeNames,
|
|
728
|
+
commands: [
|
|
729
|
+
{
|
|
730
|
+
name: "generate",
|
|
731
|
+
summary: "Writes the source files, source-map.json and not_executed.json, and checks them. result is PASS when every check passes, else FAIL.",
|
|
732
|
+
options: {
|
|
733
|
+
out: { ...OPTIONS.out, value: "<dir>", help: "the folder for the source files, relative to the project folder", required: true },
|
|
734
|
+
entry: { ...OPTIONS.entry, multiple: true, help: "an entry point for Ghidra" },
|
|
735
|
+
space: { ...OPTIONS.space, help: "the knowledge space of the program: c64 (the default), drive8, or bank0 to bank255. The coverage and --loader are for c64 only" },
|
|
736
|
+
loader: { kind: "flag", help: "also write loader.asm and loader.prg: the loader copies the segments into place, sets the entry state of the knowledge, and jumps to the entry. It is checked in the emulator" },
|
|
737
|
+
"analyzer-names": { kind: "flag", help: "name an address that has no name of the user or the LLM, and no platform name, with its analyzer name" },
|
|
738
|
+
"max-listed": OPTIONS["max-listed"],
|
|
739
|
+
},
|
|
740
|
+
notes: "The program, its entry state and the coverage come from the knowledge: put them in with knowledge.ts image and coverage of the c64-knowledge skill. Code that no imported run executed is marked and listed, and <dir>/not_executed.json has all of its ranges. Without --analyzer-names, an address without a name gets a name of the source, for example l_17e2.",
|
|
741
|
+
run: async (call) => {
|
|
742
|
+
const { output, failed } = await generate(call);
|
|
743
|
+
return { result: failed ? "FAIL" : "PASS", ...output };
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
});
|