@henols/c64-re-tools 2.0.0-rc.6 → 2.0.0-rc.8
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 +1 -1
- package/skills/c64-basic/SKILL.md +2 -2
- package/skills/c64-basic/scripts/basic.ts +1 -1
- package/skills/c64-disk/SKILL.md +3 -3
- package/skills/c64-disk/scripts/disk.ts +2 -1
- package/skills/c64-emulator/SKILL.md +4 -1
- package/skills/c64-knowledge/SKILL.md +2 -2
- package/skills/c64-knowledge/scripts/knowledge.ts +5 -2
- package/skills/c64-provenance/SKILL.md +10 -5
- package/skills/c64-provenance/scripts/compare.ts +35 -6
- package/skills/c64-provenance/scripts/provenance.ts +35 -15
- package/skills/c64-provenance/scripts/separate.ts +12 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -2
- package/skills/c64-source/SKILL.md +18 -11
- package/skills/c64-source/scripts/generate.ts +55 -10
- package/skills/c64-source/scripts/loader.ts +253 -6
- package/skills/c64-source/scripts/source.ts +46 -11
- package/skills/c64-static-analysis/SKILL.md +4 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +13 -6
- package/skills/c64-static-analysis/scripts/coverage.ts +4 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +14 -9
- package/skills/c64-testing/SKILL.md +1 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/scenario.ts +9 -7
- package/skills/c64-unpacker/SKILL.md +11 -9
- package/skills/c64-unpacker/scripts/loads.ts +16 -4
- package/skills/c64-unpacker/scripts/machine.ts +13 -1
- package/skills/c64-unpacker/scripts/stages.ts +66 -3
- package/skills/c64-unpacker/scripts/unpack.ts +135 -42
- package/src/c64.ts +19 -5
- package/src/cli/status.ts +26 -3
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/petcat.ts +5 -2
- package/src/host/tools/status.ts +6 -3
- package/src/host/vice/adapter.ts +37 -9
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +7 -2
- package/src/host/vice/screen.ts +3 -2
- package/src/host/vice/session.ts +68 -15
- package/src/host-client/autostart.ts +11 -1
- package/src/host-client/connect.ts +10 -10
- package/src/host-client/coverage.ts +19 -10
- package/src/host-client/tools.ts +6 -6
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/schema.ts +7 -0
- package/src/knowledge/write.ts +5 -0
- package/src/mcp/tools/debug.ts +9 -5
- package/src/mcp/tools/machine.ts +4 -1
- package/src/mcp/tools/media.ts +7 -1
- package/src/mcp/tools/video.ts +7 -2
- package/src/native/discover.ts +2 -1
- package/src/platform.ts +3 -2
- package/src/protocol/tools.ts +19 -4
- package/src/protocol/vice.ts +56 -5
package/package.json
CHANGED
|
@@ -67,7 +67,7 @@ The exit status is 0 for a PRG. The exit status is 1 for `assembled: false`, for
|
|
|
67
67
|
## Failure and conflicts
|
|
68
68
|
|
|
69
69
|
- `assembled: false`: the source has errors. This is a normal result. Correct the source.
|
|
70
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
70
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
71
71
|
- `invalid-input`: an option or a path is wrong. Read the message and correct the call.
|
|
72
72
|
|
|
73
73
|
## Handoffs
|
|
@@ -54,9 +54,9 @@ The exit status is 0 for a listing. The exit status is 1 for `decoded: false` an
|
|
|
54
54
|
|
|
55
55
|
## Failure and conflicts
|
|
56
56
|
|
|
57
|
-
- `decoded: false`: the file is not a BASIC program. This is a normal result. Usually the file is machine code. Give it to the c64-static-analysis skill.
|
|
57
|
+
- `decoded: false`: the file is not a BASIC program. This is a normal result. Usually the file is machine code. Give it to the c64-static-analysis skill. A memory dump also gives `decoded: false`: its first two bytes are not a load address, and its bytes go past `$ffff`.
|
|
58
58
|
- An empty `handoffs` list: the BASIC does not start machine code with SYS or USR. Do not make up a handoff. Look for a POKE into a vector or a LOAD of another file in the listing.
|
|
59
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
59
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
60
60
|
- `not-found` or `invalid-input`: the path or an argument is wrong. Read the message and correct the call.
|
|
61
61
|
|
|
62
62
|
## Handoffs
|
|
@@ -22,7 +22,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
22
22
|
const result = await decodeBasic({ program });
|
|
23
23
|
if (!result.decoded) return { output: { decoded: false, reason: result.reason }, failed: true };
|
|
24
24
|
const size = statSync(resolveProjectPath(program)).size - 2;
|
|
25
|
-
const end =
|
|
25
|
+
const end = result.loadAddress + size - 1;
|
|
26
26
|
return {
|
|
27
27
|
output: {
|
|
28
28
|
decoded: true,
|
package/skills/c64-disk/SKILL.md
CHANGED
|
@@ -29,7 +29,7 @@ node <skill>/scripts/disk.ts <action> <image> [name] [--out <file>]
|
|
|
29
29
|
| Need | Command |
|
|
30
30
|
|---|---|
|
|
31
31
|
| The disk name, the free blocks and the files | `directory original/game.d64` |
|
|
32
|
-
| The free and used sectors of each track | `bam original/game.d64` |
|
|
32
|
+
| The free and used sectors of each track, and the used sectors that no file uses | `bam original/game.d64` |
|
|
33
33
|
| The type, size and first block of one file | `entry original/game.d64 GAME` |
|
|
34
34
|
| The track and sector of each block of one file | `chain original/game.d64 GAME` |
|
|
35
35
|
| Extract one file | `read original/game.d64 GAME --out extracted/game.prg` |
|
|
@@ -47,7 +47,7 @@ File names show the characters that the C64 shows in a directory list. A byte th
|
|
|
47
47
|
1. Run `directory`. Find the file in `entries`. Each entry has a `name`, a `type`, a size in `blocks` and the `closed` and `locked` flags.
|
|
48
48
|
2. Run `read` with an output path to extract the file. A PRG file keeps its 2-byte load address at the start.
|
|
49
49
|
3. Run `entry` or `chain` only when the disk structure is important, for example for a loader that reads sectors directly.
|
|
50
|
-
4. Run `bam` only to find used sectors that no file uses.
|
|
50
|
+
4. Run `bam` only to find used sectors that no file uses. `unreferenced` lists them. The list does not include the header, the directory and the BAM blocks. These sectors can hold data that a loader reads directly.
|
|
51
51
|
|
|
52
52
|
## Knowledge
|
|
53
53
|
|
|
@@ -79,7 +79,7 @@ The exit status is 0 for a result with the file. The exit status is 1 for `found
|
|
|
79
79
|
|
|
80
80
|
- `found: false`: the image has no file with that name. This is a normal result. Read the directory again and compare the names.
|
|
81
81
|
- `media-error`: the image has a defect or is not a disk image. The message tells the problem, for example a sector chain that loops. Tell the user. Do not change the image.
|
|
82
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
82
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
83
83
|
- `invalid-input`: an argument or a path is wrong. Read the message and correct the call.
|
|
84
84
|
|
|
85
85
|
A file that is not `closed` was not written fully. Its data can be incomplete.
|
|
@@ -16,7 +16,8 @@ import { ContainerError, parseCrt, parseT64, tapeFile } from "./containers.ts";
|
|
|
16
16
|
const USAGE = `disk.ts <action> <image> [name] [--out <file>]
|
|
17
17
|
|
|
18
18
|
directory <image> disk name, id, free blocks and the files
|
|
19
|
-
bam <image> free and used sectors of each track
|
|
19
|
+
bam <image> free and used sectors of each track, and the
|
|
20
|
+
used sectors that no file holds
|
|
20
21
|
entry <image> <name> the directory entry of one file
|
|
21
22
|
chain <image> <name> the track and sector of each block of one file
|
|
22
23
|
read <image> <name> --out <file> write the bytes of one file into the project
|
|
@@ -64,12 +64,15 @@ For a large result, give `out` with a path in the project. `c64_memory_read`, `c
|
|
|
64
64
|
|
|
65
65
|
Write operations need a stopped CPU. Stop the CPU with `c64_execution` action `pause` first.
|
|
66
66
|
|
|
67
|
-
`c64_reset` with `run` false stops the CPU at the first instruction of the reset routine. The result gives that address in `pc`.
|
|
67
|
+
`c64_reset` with `run` false stops the CPU at the first instruction of the reset routine. The result gives that address in `pc`. A reset and `c64_autostart` set `c64_timing` to zero.
|
|
68
|
+
|
|
69
|
+
A cartridge from `c64_autostart` stays in the C64 through every reset, as on a real C64. `c64_status` gives `cartridge: true` while the C64 has it, and `cartridge: false` when it does not. `c64_autostart` of a file that is not a cartridge removes the cartridge first. `c64_autostart` also sets both joysticks to the center first. `joysticksCentred` gives the ports that it changed.
|
|
68
70
|
|
|
69
71
|
Step, next and until-return work only in space `c64`. The emulator cannot stop the disk drive CPU after one instruction. To find when the drive executes an address, use `c64_run_until` with an address target in space `drive8`, or a breakpoint in space `drive8`. The drive can execute a few more instructions before it stops. The computer completes its current instruction. The `pc` in the result is the drive address where it stopped.
|
|
70
72
|
|
|
71
73
|
`c64_keyboard` puts keys in the keyboard queue of the KERNAL. A program that reads the keyboard hardware directly does not see them. Many games do this. The emulator has no command that presses one key of the keyboard matrix. For such a program:
|
|
72
74
|
|
|
75
|
+
- After `c64_window` or a snapshot restore, the profiler of VICE keeps the routines that ran before the restore. Then `c64_profile` can give the cycles of the program to such a routine, for example `$fd50`. Do not trust the routine addresses of `c64_profile` after a restore.
|
|
73
76
|
- VICE 3.10 and older have a known flaw: bits 5-7 of `$DC00` and `$DC01` are always 0. Then `emulatorFlaw` in the result of `c64_keyboard` and `c64_joystick` gives the flaw. With this flaw, a wait for any key that reads all keyboard columns at one time ends without a key. A wait for one specific key, or a compare of the full byte of a joystick port, never ends. Tell the user about the flaw. The next VICE release and the VICE nightly builds do not have it. Joystick port 1 uses the same lines as the keyboard.
|
|
74
77
|
- For one specific key, for example F1 or RUN/STOP, open the window and ask the user to press the key.
|
|
75
78
|
- Do not change the program to make it accept a key.
|
|
@@ -115,7 +115,7 @@ A `line` comment is a line of its own above the address. A `side` comment is at
|
|
|
115
115
|
|
|
116
116
|
## Modules
|
|
117
117
|
|
|
118
|
-
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
118
|
+
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm` and `constants.asm` itself. Thus a module cannot have one of these names, in upper or lower case. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
119
119
|
|
|
120
120
|
## Removals
|
|
121
121
|
|
|
@@ -146,7 +146,7 @@ The script records the origin `llm`. Use `--origin user` only when the user deci
|
|
|
146
146
|
|
|
147
147
|
For a read: the current or historical knowledge in compact JSON.
|
|
148
148
|
|
|
149
|
-
For a write: the new revision and the current value after the write. If the write replaces a symbol or a region of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change.
|
|
149
|
+
For a write: the new revision and the current value after the write. If the write replaces a symbol or a region of a different origin, `previous` gives that row as it was before the write. For example, an `llm` write over a `user` name gives `previous`. Read `previous` and tell the user about the change. For `apply`, `replaced` lists each write that replaced a row. Each item has the number of the write, its `op` and its `previous`.
|
|
150
150
|
|
|
151
151
|
The exit status is 0 for a result. The exit status is 1 for an error from the knowledge, for example `conflict`. The exit status is 2 for a wrong argument.
|
|
152
152
|
|
|
@@ -346,8 +346,11 @@ function applyOperation(args: string[], context: WriteContext, space: string): W
|
|
|
346
346
|
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
347
347
|
return (db) => {
|
|
348
348
|
const applied = applyBatch(db, context, operations);
|
|
349
|
-
// replaced:
|
|
350
|
-
|
|
349
|
+
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
350
|
+
const replaced = applied.steps.flatMap((step, index) =>
|
|
351
|
+
step.previous === undefined ? [] : [{ write: index + 1, op: operations[index]!.op, previous: Array.isArray(step.previous) ? step.previous.map(region) : symbol(step.previous) }],
|
|
352
|
+
);
|
|
353
|
+
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
351
354
|
};
|
|
352
355
|
}
|
|
353
356
|
|
|
@@ -40,12 +40,16 @@ node <skill>/scripts/provenance.ts separate unpacked/image.json --coverage cover
|
|
|
40
40
|
|
|
41
41
|
The script puts each PRG at its load address. An image has bytes only in its segments. The script compares the addresses where all releases have bytes. It prints one JSON object.
|
|
42
42
|
|
|
43
|
+
With `--shift`, the addresses of `segments`, `common` and `differences` are the release addresses plus the `shift` of the release. The `start` and `end` of `runtime` are the addresses in the release itself, without the shift.
|
|
44
|
+
|
|
43
45
|
With `--run`, a PRG starts with autostart and RUN. An image starts from its bytes: the script writes the segments and the entry state from `entry_state.json` into the emulator, and starts at the entry. Watchpoints on each difference stop the run at its first access.
|
|
44
46
|
|
|
45
47
|
## Workflow
|
|
46
48
|
|
|
47
49
|
1. Run the script on the releases.
|
|
48
|
-
2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with `--shift`.
|
|
50
|
+
2. Read `identicalShare`. If it is low and `suggestedShifts` is present, the releases are at different addresses. Run the script again with a `--shift` of `suggestedShifts`. `shiftsNotSuggested` tells why a found shift does not fit in `$0000-$ffff`.
|
|
51
|
+
|
|
52
|
+
`suggestedShifts` has at most 3 shifts for each release. The shift with the most `matchingSequences` is first. Data that did not move and code that moved give different shifts. Try each shift. `current: true` marks the shift of this run.
|
|
49
53
|
3. Read `differences`. Each one has an address range, the bytes of each release, and `groups`: the releases that have the same bytes there.
|
|
50
54
|
4. To know how the program uses the differences, run the script again with `--run` and the frames that get the program to the part you examine. Read `runtime` of each difference:
|
|
51
55
|
- `first`: the first access in that release: `read`, `write`, `execute` or `none`. An instruction that reads and then writes the same byte, for example INC, counts as `read`. The operand bytes of an executed instruction count as `execute`, for example the value of `LDA #$03`.
|
|
@@ -65,10 +69,11 @@ Use `separate` when you have only one release. It gives evidence, not a decision
|
|
|
65
69
|
|
|
66
70
|
- `ranges`: the image in ranges with the same stage and access. `stage` is the last unpack stage that wrote the range. In `image.json`, the last stage is the program. An earlier stage is a depacker, an intro or a loader. Only an image from c64-unpacker `capture` has stages. For an image from `loads`, `noStages` tells this.
|
|
67
71
|
- `access`: from the joined `--coverage` files: `executed`, `read`, `written`, `read-written` or `untouched`. `untouched` bytes did not take part in the runs.
|
|
72
|
+
- `codeRegions.total`: the number of code regions in the knowledge.
|
|
68
73
|
- `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.
|
|
69
|
-
- `outside`: RAM outside the image 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.
|
|
74
|
+
- `outside`: RAM outside the image 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.
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
Each list has `count` and `listed`. `listed` has the first ranges, at most `--max-listed`. `--out` writes all of them. Run more coverage runs for parts that are `untouched` before you decide.
|
|
72
77
|
|
|
73
78
|
## Knowledge
|
|
74
79
|
|
|
@@ -79,7 +84,7 @@ Record a confirmed conclusion as a normal comment, region or symbol with the c64
|
|
|
79
84
|
|
|
80
85
|
## Result
|
|
81
86
|
|
|
82
|
-
The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. With `--run`, each difference also has `runtime`, and `run` tells if each release started, or why not.
|
|
87
|
+
The differing ranges with the bytes of each release and the groups that agree, and the share of identical bytes. `common` gives the ranges where all releases have bytes. `releases` gives the segments and the `shift` of each release. With `--run`, each difference also has `runtime`, and `run` tells if each release started, or why not.
|
|
83
88
|
|
|
84
89
|
The runtime evidence is evidence only. The script does not decide which bytes are original.
|
|
85
90
|
|
|
@@ -91,7 +96,7 @@ The exit status is 0 for a result. The exit status is 1 for an error. The exit s
|
|
|
91
96
|
- Do not call a byte original only because one release has it. A majority of releases is also not proof.
|
|
92
97
|
- `common` is empty: the releases do not cover the same addresses. Use `--shift`, or compare a range that all releases cover.
|
|
93
98
|
- `run.releases` shows `started: false`: a PRG without a BASIC start, or an image without `entry_state.json`. The reason tells which. The other releases still give evidence.
|
|
94
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own emulator script.
|
|
99
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own emulator script.
|
|
95
100
|
- `moreDifferences: true`: the script shows only the first differences. Use `--from` and `--to` to see the others.
|
|
96
101
|
- `not-found`: the project has no file at that path.
|
|
97
102
|
- `invalid-input`: a file, an address or a shift is wrong. A path that goes out of the project directory is also wrong, also through a link. Read the message.
|
|
@@ -93,11 +93,15 @@ function addressesOf(release: Release): number[] {
|
|
|
93
93
|
return release.segments.flatMap((segment) => Array.from({ length: segment.end - segment.start + 1 }, (_, offset) => segment.start + offset));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Most shifts that suggestShifts gives: data that did not move can outvote code that did (b2/04). */
|
|
97
|
+
export const MAX_SUGGESTED_SHIFTS = 3;
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
|
-
* The
|
|
98
|
-
*
|
|
100
|
+
* The shifts that put `other` over `base` best: the most common address
|
|
101
|
+
* differences between equal 8-byte sequences, most votes first. A shift with
|
|
102
|
+
* less than a tenth of the best one's votes is chance. Empty when nothing matches.
|
|
99
103
|
*/
|
|
100
|
-
export function
|
|
104
|
+
export function suggestShifts(base: Release, other: Release): Array<{ shift: number; matches: number }> {
|
|
101
105
|
const WINDOW = 8;
|
|
102
106
|
const key = (release: Release, addresses: number[], at: number) =>
|
|
103
107
|
String.fromCharCode(...addresses.slice(at, at + WINDOW).map((address) => release.byteAt(address)));
|
|
@@ -117,7 +121,32 @@ export function suggestShift(base: Release, other: Release): { shift: number; ma
|
|
|
117
121
|
votes.set(shift, (votes.get(shift) ?? 0) + 1);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
const ranked = [...votes].map(([shift, matches]) => ({ shift, matches })).sort((a, b) => b.matches - a.matches || Math.abs(a.shift) - Math.abs(b.shift));
|
|
125
|
+
return ranked.filter((found) => found.matches * 10 >= ranked[0]!.matches).slice(0, MAX_SUGGESTED_SHIFTS);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type ShiftSuggestion = { release: number; shift: number; matchingSequences: number } | { release: number; matchingSequences: number; reason: string };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The --shift to suggest for release `index` (0 = the first) from `found`,
|
|
132
|
+
* the best shift of that release over the first one (F-25). The shift is the
|
|
133
|
+
* full value for --shift: `applied` holds the shifts already given. A shift
|
|
134
|
+
* that moves a segment out of $0000-$ffff is not suggested: with two releases,
|
|
135
|
+
* the opposite shift of the first release is suggested when it fits, else the
|
|
136
|
+
* result gives the reason.
|
|
137
|
+
*/
|
|
138
|
+
export function shiftSuggestion(releases: Release[], applied: number[], index: number, found: { shift: number; matches: number }): ShiftSuggestion {
|
|
139
|
+
const outside = (release: Release, by: number) => release.segments.find((segment) => segment.start + by < 0 || segment.end + by > 0xffff);
|
|
140
|
+
const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
|
|
141
|
+
const why = (number: number, by: number, segment: Range) =>
|
|
142
|
+
`release ${number} moved by ${by} puts ${hex(segment.start)}-${hex(segment.end)} ${segment.start + by < 0 ? "below $0000" : "past $ffff"}`;
|
|
143
|
+
const own = outside(releases[index]!, found.shift);
|
|
144
|
+
if (own === undefined) return { release: index + 1, shift: applied[index]! + found.shift, matchingSequences: found.matches };
|
|
145
|
+
const reasons = [why(index + 1, found.shift, own)];
|
|
146
|
+
if (releases.length === 2) {
|
|
147
|
+
const base = outside(releases[0]!, -found.shift);
|
|
148
|
+
if (base === undefined) return { release: 1, shift: applied[0]! - found.shift, matchingSequences: found.matches };
|
|
149
|
+
reasons.push(why(1, -found.shift, base));
|
|
150
|
+
}
|
|
151
|
+
return { release: index + 1, matchingSequences: found.matches, reason: `No shift fits: ${reasons.join(", and ")}.` };
|
|
123
152
|
}
|
|
@@ -18,7 +18,7 @@ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
|
18
18
|
import { listReferences, listRegions } from "#src/knowledge/read.ts";
|
|
19
19
|
import { CODE_REGION_TYPES } from "#src/knowledge/schema.ts";
|
|
20
20
|
import { runScript, UsageError } from "#src/script.ts";
|
|
21
|
-
import { compareReleases,
|
|
21
|
+
import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./compare.ts";
|
|
22
22
|
import { watchRelease, type ImageState, type Start } from "./runtime.ts";
|
|
23
23
|
import { separate } from "./separate.ts";
|
|
24
24
|
|
|
@@ -36,7 +36,7 @@ separate gives evidence from one release: the unpack stage that wrote each range
|
|
|
36
36
|
its access in the --coverage files (64 KiB each, repeatable), whether call and
|
|
37
37
|
jump references in knowledge reach each code region from the entry, and what the
|
|
38
38
|
program reads or executes outside the image. --out writes all ranges to a file;
|
|
39
|
-
the result lists at most --max-listed of
|
|
39
|
+
the result lists at most --max-listed of each list (default 50) and its count.
|
|
40
40
|
|
|
41
41
|
A release is a PRG, placed at its load address, or an image.json of the
|
|
42
42
|
c64-unpacker skill, compared inside its segments. Paths are relative to the
|
|
@@ -204,16 +204,23 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
204
204
|
const compared = comparison.identicalBytes + comparison.differingBytes;
|
|
205
205
|
const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
|
|
206
206
|
// A poor match at the load addresses can mean that the releases are placed differently.
|
|
207
|
+
// Each release gets its best shifts: data that stayed can outvote code that moved, so one is not enough (b2/04).
|
|
208
|
+
const applied = loaded.map((item) => item.shift);
|
|
207
209
|
const suggestions =
|
|
208
210
|
identicalShare < 0.5
|
|
209
|
-
? releases.slice(1).flatMap((release, index) =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
? releases.slice(1).flatMap((release, index) =>
|
|
212
|
+
suggestShifts(releases[0]!, release).map((found) => {
|
|
213
|
+
const suggestion = shiftSuggestion(releases, applied, index + 1, found);
|
|
214
|
+
// The shift that the run already has: its votes tell how good the present alignment is.
|
|
215
|
+
return "shift" in suggestion && suggestion.shift === applied[suggestion.release - 1] ? { ...suggestion, current: true } : suggestion;
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
213
218
|
: [];
|
|
219
|
+
const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
|
|
220
|
+
const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
|
|
214
221
|
|
|
215
222
|
// The runtime evidence: one emulator for each release, the differences at the release's own addresses.
|
|
216
|
-
const runtime = new Map<string, Array<{ first: string; changed: boolean }>>();
|
|
223
|
+
const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
|
|
217
224
|
const runs: Record<string, unknown> = {};
|
|
218
225
|
if (frames !== undefined && comparison.differences.length > 0) {
|
|
219
226
|
for (const item of loaded) {
|
|
@@ -229,7 +236,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
229
236
|
if (!watched.started) runs[name] = { started: false, reason: watched.reason };
|
|
230
237
|
else {
|
|
231
238
|
runs[name] = { started: true };
|
|
232
|
-
runtime.set(name, watched.accesses);
|
|
239
|
+
runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
|
|
233
240
|
}
|
|
234
241
|
} finally {
|
|
235
242
|
await client.close();
|
|
@@ -238,7 +245,7 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
return {
|
|
241
|
-
releases: loaded.map((item) => ({ name: item.release.name, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
|
|
248
|
+
releases: loaded.map((item) => ({ name: item.release.name, shift: item.shift, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
|
|
242
249
|
common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
|
|
243
250
|
identicalBytes: comparison.identicalBytes,
|
|
244
251
|
differingBytes: comparison.differingBytes,
|
|
@@ -248,10 +255,21 @@ async function run(argv: string[]): Promise<unknown> {
|
|
|
248
255
|
end: formatC64Address(difference.end),
|
|
249
256
|
values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
|
|
250
257
|
groups: difference.groups,
|
|
251
|
-
|
|
258
|
+
// runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
|
|
259
|
+
...(runtime.size === 0
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
runtime: Object.fromEntries(
|
|
263
|
+
[...runtime].map(([name, accesses]) => {
|
|
264
|
+
const { start, end, first, changed } = accesses[index]!;
|
|
265
|
+
return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
}),
|
|
252
269
|
})),
|
|
253
270
|
...(comparison.moreDifferences ? { moreDifferences: true } : {}),
|
|
254
|
-
...(
|
|
271
|
+
...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
|
|
272
|
+
...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
|
|
255
273
|
...(frames === undefined
|
|
256
274
|
? {}
|
|
257
275
|
: { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
|
|
@@ -288,6 +306,7 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
|
|
|
288
306
|
const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
|
|
289
307
|
const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
|
|
290
308
|
const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
|
|
309
|
+
const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
|
|
291
310
|
if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ release: path, ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
|
|
292
311
|
const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
293
312
|
const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
|
|
@@ -301,12 +320,13 @@ function separateCommand(path: string, coveragePaths: string[], out: string | un
|
|
|
301
320
|
bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
|
|
302
321
|
}),
|
|
303
322
|
...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
|
|
304
|
-
ranges:
|
|
323
|
+
ranges: listed(ranges),
|
|
324
|
+
// total counts every code region; unreachable is a list like the others (b2/08).
|
|
305
325
|
codeRegions: {
|
|
306
|
-
|
|
307
|
-
...(loaded.entry === undefined ? {} : { unreachable: codeRegions.filter((region) => region.reachable === false) }),
|
|
326
|
+
total: codeRegions.length,
|
|
327
|
+
...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
|
|
308
328
|
},
|
|
309
|
-
...(coverage === undefined ? {} : { outside }),
|
|
329
|
+
...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
|
|
310
330
|
...(out === undefined ? {} : { out }),
|
|
311
331
|
};
|
|
312
332
|
}
|
|
@@ -42,18 +42,20 @@ export interface Separation {
|
|
|
42
42
|
/**
|
|
43
43
|
* RAM outside the image and outside pages 0-3, from the coverage: `read`
|
|
44
44
|
* and `executed` the program never wrote (a loader or an earlier stage put
|
|
45
|
-
* those bytes there), and `generated` it wrote and then executed
|
|
45
|
+
* those bytes there), and `generated` it wrote and then executed, in
|
|
46
|
+
* pieces of code with gaps of up to two operand bytes.
|
|
46
47
|
*/
|
|
47
48
|
outside: { read: Range[]; executed: Range[]; generated: Range[] };
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const SYSTEM_PAGES_END = 0x03ff;
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
/** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
|
|
54
|
+
function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): Range[] {
|
|
53
55
|
const ranges: Range[] = [];
|
|
54
56
|
for (const address of addresses) {
|
|
55
57
|
const last = ranges.at(-1);
|
|
56
|
-
if (last !== undefined && last.end
|
|
58
|
+
if (last !== undefined && address - last.end - 1 <= gap && joins(last.end, address)) last.end = address;
|
|
57
59
|
else ranges.push({ start: address, end: address });
|
|
58
60
|
}
|
|
59
61
|
return ranges;
|
|
@@ -128,5 +130,11 @@ export function separate(input: SeparationInput): Separation {
|
|
|
128
130
|
else if (!written && (input.coverage[address]! & COVERAGE.ramRead) !== 0) read.push(address);
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
// The image has no bytes of generated code, so only its opcode bytes count as executed. VICE marks only
|
|
134
|
+
// opcode bytes: a gap of at most two operand bytes outside the image stays inside one piece of code (F-28).
|
|
135
|
+
const outsideImage = (from: number, to: number) => {
|
|
136
|
+
for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
|
|
132
140
|
}
|
|
@@ -92,7 +92,7 @@ In stage 5, if you cannot get to a part of the program, ask the user how to get
|
|
|
92
92
|
1. Acceptance: each criterion of stage 1 and its result, and each exception that the user accepted.
|
|
93
93
|
2. The program: what the program does, in a few sentences.
|
|
94
94
|
3. Modules: the file, the address range and the title of each module.
|
|
95
|
-
4. Untested code: the `
|
|
95
|
+
4. Untested code: all ranges in the `not_executed.json` file of c64-source. Tell why no run executed them, if you know.
|
|
96
96
|
5. Open questions: what the evidence does not explain.
|
|
97
97
|
|
|
98
98
|
Keep observed facts apart from your conclusions.
|
|
@@ -133,7 +133,7 @@ A clear answer for the scope, with the durable conclusions in the project knowle
|
|
|
133
133
|
|
|
134
134
|
## Failure and conflicts
|
|
135
135
|
|
|
136
|
-
- A skill reports `installation-incomplete`, or a message that the host runtime does not run: stop the whole task. Give the message to the user and wait for the user. Do not continue with other skills. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
136
|
+
- A skill reports `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the whole task. Give the message to the user and wait for the user. Do not continue with other skills. Do not make a replacement for the tool or the host runtime, for example your own disk parser, disassembler or emulator script.
|
|
137
137
|
- If the evidence does not support a conclusion, keep the unknown and get more evidence. Do not make up a meaning.
|
|
138
138
|
- Before you replace current knowledge from the user or the LLM, read its history and the evidence for it.
|
|
139
139
|
- A conflict from the static analysis: find the correct answer at run time with the c64-emulator skill.
|
|
@@ -32,7 +32,7 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
|
|
|
32
32
|
|---|---|
|
|
33
33
|
| `--out src` | The directory for the source files. The script writes over the files that it generated before. |
|
|
34
34
|
| `--entry '$080d'` | An entry point for Ghidra. Use it again for more entry points. The entry of `image.json`, the routines and the code regions in the knowledge are entry points too. |
|
|
35
|
-
| `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` with `out`. Use it again for more runs. Code that no run executed gets the line `; not executed
|
|
35
|
+
| `--coverage coverage/attract.bin` | A coverage file of 64 KiB from `c64_memmap` with `out`. Use it again for more runs. Code that no run executed gets the line `; not executed` with its range. `notExecuted` lists it. |
|
|
36
36
|
| `--space drive8` | Generate the source of drive code or a cartridge bank from the knowledge of that space. `--coverage` and `--loader` are only for `c64`, and platform names are only for `c64`. |
|
|
37
37
|
| `--loader` | Also write `loader.asm` and `loader.prg`. This needs `image.json` with an entry, and `entry_state.json`. |
|
|
38
38
|
|
|
@@ -52,14 +52,15 @@ node <skill>/scripts/generate.ts unpacked/image.json --out src --loader
|
|
|
52
52
|
- `main.asm` sets the address of each segment and includes the files in address order.
|
|
53
53
|
- Each module in the knowledge is one file. A module starts at its address and stops at the next module or at the end of its segment. Without modules, each segment is one file, `segment_xxxx.asm`.
|
|
54
54
|
- `constants.asm` holds the names of addresses outside the image, for example `EXTCOL = $d020`.
|
|
55
|
-
- A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. An address in the image without a name gets `l_xxxx`.
|
|
55
|
+
- A label or a constant gets the name of the address in the knowledge. Without a knowledge name, it gets the platform name of the c64-memory-map skill, for example `EXTCOL` or `CHROUT`. A platform name wins over a name from an analyzer (`dxa`, `ghidra` or `heuristic`), for example `EXTCOL` over `ld020`. A `user` or `llm` name wins over a platform name. An address in the image without a name gets `l_xxxx`.
|
|
56
56
|
- Code regions, and the instructions that Ghidra decodes outside of data regions, become instructions.
|
|
57
57
|
- Data regions become data by their type: `!byte`, `!word`, `!pet`, `!scr`, `!fill`, tables of labels, and sprite bytes in binary.
|
|
58
58
|
- `lda+2 $0012` is an absolute operand below $0100. `+1` is a zero-page operand with a label.
|
|
59
|
+
- `sta NMINV+1` is an address inside a platform entry of more than one byte, for example the high byte of a vector.
|
|
59
60
|
- `name = * + 1` is a label inside an instruction, for example for code that changes its own operand.
|
|
60
61
|
- `!byte` with an instruction as its comment: ACME writes this instruction with a different opcode. The bytes keep the source identical.
|
|
61
62
|
- `not decoded` marks bytes in a code region that Ghidra did not decode.
|
|
62
|
-
- `; not executed` starts code that no run of the `--coverage` files executed.
|
|
63
|
+
- `; not executed $xxxx-$yyyy` starts code that no run of the `--coverage` files executed. The range is the same as in `not_executed.json`.
|
|
63
64
|
|
|
64
65
|
Do not edit the generated files. The next run writes over them.
|
|
65
66
|
|
|
@@ -70,11 +71,13 @@ Do not edit the generated files. The next run writes over them.
|
|
|
70
71
|
1. `10 SYS 2061` starts it.
|
|
71
72
|
2. It moves its copier to two free pages. The copier runs with interrupts off and with all RAM visible.
|
|
72
73
|
3. It copies each segment and the pages $0002-$03ff of the entry state into place. The script selects an order in which no copy writes over the packed bytes of a copy that has not run yet.
|
|
73
|
-
4. It sets
|
|
74
|
+
4. It sets the display registers of the VIC-II and the VIC bank from `io` in the entry state. Then it waits for the raster line of the entry.
|
|
75
|
+
5. It sets the CIA timers, their latches and the interrupt masks from `io` and `cia` in the entry state. A running timer gets the value that counts down to the timer value of the entry at the entry.
|
|
76
|
+
6. It sets $00/$01, the stack pointer, A, X, Y and the flags, and jumps to the entry.
|
|
74
77
|
|
|
75
|
-
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the
|
|
78
|
+
The flags go through the free stack byte below the stack pointer (`stackByte`). The loader changes this byte. The loader does not set the SID, the time of day of the CIAs, the CIA interrupt flags or the interrupt registers of the VIC-II. `loader.limits` names each CIA value that the loader cannot set as it was at the entry. A program that reads the SID voice 3 for random numbers can still differ in a scenario. A raster sync does not change a timer or the SID. Then observe after the program sets up its own random numbers.
|
|
76
79
|
|
|
77
|
-
The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01
|
|
80
|
+
The script simulates the copies first. Then it runs `loader.prg` in the emulator to its jump to the entry. It compares the memory, $00/$01, the registers, the display registers of the VIC-II, the VIC bank and the raster line with the entry state there. It also compares the CIA timers, latches and interrupt masks. `loader.verified: true` tells that all of them are equal. An interrupt can run right at the entry, as in the original program.
|
|
78
81
|
|
|
79
82
|
## Knowledge
|
|
80
83
|
|
|
@@ -94,22 +97,26 @@ One JSON object:
|
|
|
94
97
|
- `files`: the files in the output directory.
|
|
95
98
|
- `instructions`, `dataBytes` and `undecodedBytes`.
|
|
96
99
|
- `labels`: `named` labels from the knowledge and `generated` labels `l_xxxx`.
|
|
100
|
+
- `entriesInData`: each `--entry` in a data region of the knowledge, with the `region` and its `origin`. Ghidra does not decode an entry in a data region.
|
|
97
101
|
- `asBytes`: the instructions that the source has as bytes.
|
|
98
|
-
- `unusedModules`: modules
|
|
99
|
-
- `
|
|
102
|
+
- `unusedModules`: modules in the knowledge whose start is in no segment of the image. Often this tells that the image does not hold code that the knowledge describes.
|
|
103
|
+
- `entryOutside`: the entry of `image.json` is in no segment of the image, with a `reason`. The image does not hold the program.
|
|
104
|
+
- `notExecuted`: with `--coverage`, the count of instructions that no run executed, the count of their ranges, and the first ranges. `all` names the file `not_executed.json` in `--out`. It has all of the ranges. Put all of these ranges in the report.
|
|
100
105
|
- `check`: `identical`, and the result for each segment. A different segment gives its first different address.
|
|
101
|
-
- `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
|
|
106
|
+
- `loader`: the program, the copies in their order, the copier page, `stackByte`, `verified` and `differences`. `limits` lists the CIA values that the loader cannot set. When the script cannot make the loader, `loader` has `verified: false` and a `reason`. The loader includes the source. Thus a source that does not assemble to the image also stops the loader.
|
|
102
107
|
|
|
103
|
-
The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check and for an error. The exit status is 2 for a wrong argument.
|
|
108
|
+
The exit status is 0 when the source is identical and the emulator check of the loader passes. The exit status is 1 for a failed check, for an entry in no segment, and for an error. The exit status is 2 for a wrong argument.
|
|
104
109
|
|
|
105
110
|
## Failure and conflicts
|
|
106
111
|
|
|
107
112
|
- `check.diagnostics`: ACME did not assemble the source. A name in the knowledge can be wrong for ACME. Read the diagnostics and change the name with c64-knowledge.
|
|
108
113
|
- `check.identical: false`: the script has a defect. Give the result to the user.
|
|
109
114
|
- `invalid-input` that names a symbol or a module: change that symbol or module in the knowledge.
|
|
115
|
+
- `entryOutside`: the run fails, but the script still writes and checks the source. With `--loader`, `loader.verified` is `false` with the same `reason`. Capture the image again with c64-unpacker `loads` or `capture`. Make sure that a segment holds the entry.
|
|
116
|
+
- `entriesInData`: an analyzer region of data hides the code at the entry. If the entry is code, record a code region at the entry with the c64-knowledge skill, for example `classify '$0500' '$05ff' code`. A region of the user or the LLM replaces the analyzer region. Then generate again.
|
|
110
117
|
- `loader.verified: false` with a `reason`: the script did not make the loader. The script still writes and checks the source. When no copy order or no free pages exist, or the loader needs too many copies, the reason gives the ranges. Capture the image again with fewer segments, or write the loader by hand.
|
|
111
118
|
- `loader.verified: false` with `differences`: the loader ran, and `differences` gives the bytes and registers that are not equal.
|
|
112
|
-
- `installation-incomplete`, or a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disassembler or emulator script.
|
|
119
|
+
- `installation-incomplete`, or `machine-unavailable` with a message that the host runtime does not run: stop the task. Give the message to the user and wait for the user. The message gives the remedy. Do not make a replacement for the tool or the host runtime, for example your own disassembler or emulator script.
|
|
113
120
|
|
|
114
121
|
## Handoffs
|
|
115
122
|
|