romdevtools 0.29.0 → 0.40.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.
Files changed (103) hide show
  1. package/AGENTS.md +14 -5
  2. package/CHANGELOG.md +114 -12
  3. package/README.md +2 -1
  4. package/examples/gb/templates/tile_engine.c +1 -1
  5. package/examples/gbc/templates/tile_engine.c +1 -1
  6. package/examples/genesis/templates/two_plane_parallax.c +4 -4
  7. package/examples/nes/templates/tile_engine.c +1 -1
  8. package/package.json +14 -12
  9. package/src/analysis/analyze.js +263 -0
  10. package/src/analysis/decompile.js +108 -0
  11. package/src/analysis/decompiler/sleigh/6502.cspec +34 -0
  12. package/src/analysis/decompiler/sleigh/6502.ldefs +33 -0
  13. package/src/analysis/decompiler/sleigh/6502.pspec +16 -0
  14. package/src/analysis/decompiler/sleigh/6502.sla +3414 -0
  15. package/src/analysis/decompiler/sleigh/65816-snes.pspec +249 -0
  16. package/src/analysis/decompiler/sleigh/65816.cspec +17 -0
  17. package/src/analysis/decompiler/sleigh/65816.ldefs +15 -0
  18. package/src/analysis/decompiler/sleigh/65816.sla +23670 -0
  19. package/src/analysis/decompiler/sleigh/65c02.sla +4683 -0
  20. package/src/analysis/decompiler/sleigh/68000.cspec +67 -0
  21. package/src/analysis/decompiler/sleigh/68000.ldefs +68 -0
  22. package/src/analysis/decompiler/sleigh/68000.pspec +9 -0
  23. package/src/analysis/decompiler/sleigh/68000_register.cspec +118 -0
  24. package/src/analysis/decompiler/sleigh/68040.sla +81693 -0
  25. package/src/analysis/decompiler/sleigh/ARM.ldefs +377 -0
  26. package/src/analysis/decompiler/sleigh/ARM4t_le.sla +53758 -0
  27. package/src/analysis/decompiler/sleigh/ARM_v45.cspec +209 -0
  28. package/src/analysis/decompiler/sleigh/ARM_v45.pspec +40 -0
  29. package/src/analysis/decompiler/sleigh/ARMt_v45.pspec +43 -0
  30. package/src/analysis/decompiler/sleigh/HuC6280.cspec +67 -0
  31. package/src/analysis/decompiler/sleigh/HuC6280.ldefs +18 -0
  32. package/src/analysis/decompiler/sleigh/HuC6280.pspec +150 -0
  33. package/src/analysis/decompiler/sleigh/HuC6280.sla +7524 -0
  34. package/src/analysis/decompiler/sleigh/sm83.cspec +70 -0
  35. package/src/analysis/decompiler/sleigh/sm83.ldefs +29 -0
  36. package/src/analysis/decompiler/sleigh/sm83.pspec +19 -0
  37. package/src/analysis/decompiler/sleigh/sm83.sla +7695 -0
  38. package/src/analysis/decompiler/sleigh/z80.cspec +122 -0
  39. package/src/analysis/decompiler/sleigh/z80.ldefs +57 -0
  40. package/src/analysis/decompiler/sleigh/z80.pspec +43 -0
  41. package/src/analysis/decompiler/sleigh/z80.sla +20518 -0
  42. package/src/analysis/decompiler/wasm/decompile.js +2 -0
  43. package/src/analysis/decompiler/wasm/decompile.wasm +0 -0
  44. package/src/analysis/rizin.js +129 -0
  45. package/src/analysis/wasm/rizin.js +6032 -0
  46. package/src/analysis/wasm/rizin.wasm +0 -0
  47. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  48. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  49. package/src/cores/wasm/fceumm_libretro.js +1 -1
  50. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  51. package/src/cores/wasm/gambatte_libretro.js +1 -1
  52. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  53. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  54. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  55. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  56. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  57. package/src/cores/wasm/handy_libretro.js +1 -1
  58. package/src/cores/wasm/handy_libretro.wasm +0 -0
  59. package/src/cores/wasm/mgba_libretro.js +1 -1
  60. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  61. package/src/cores/wasm/prosystem_libretro.js +1 -1
  62. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  63. package/src/cores/wasm/snes9x_libretro.js +1 -1
  64. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  65. package/src/cores/wasm/stella2014_libretro.js +1 -1
  66. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  67. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  68. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  69. package/src/host/LibretroHost.js +25 -7
  70. package/src/http/routes.js +1 -1
  71. package/src/http/skill-doc.js +1 -1
  72. package/src/mcp/tools/cart-parts.js +5 -2
  73. package/src/mcp/tools/disasm.js +32 -5
  74. package/src/mcp/tools/font-map.js +3 -3
  75. package/src/mcp/tools/index.js +2 -2
  76. package/src/mcp/tools/memory.js +131 -24
  77. package/src/mcp/tools/project.js +1 -1
  78. package/src/mcp/tools/record.js +6 -7
  79. package/src/mcp/tools/reinject.js +1 -1
  80. package/src/mcp/tools/run-until.js +8 -2
  81. package/src/mcp/tools/symbols.js +10 -4
  82. package/src/mcp/tools/trace-vram-source.js +1 -1
  83. package/src/mcp/tools/watch-memory.js +50 -8
  84. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +80 -6
  85. package/src/platforms/atari2600/MENTAL_MODEL.md +6 -0
  86. package/src/platforms/atari7800/MENTAL_MODEL.md +6 -0
  87. package/src/platforms/c64/MENTAL_MODEL.md +6 -0
  88. package/src/platforms/gb/MENTAL_MODEL.md +6 -0
  89. package/src/platforms/gb/lib/c/README.md +1 -1
  90. package/src/platforms/gba/MENTAL_MODEL.md +7 -1
  91. package/src/platforms/gbc/MENTAL_MODEL.md +6 -0
  92. package/src/platforms/gbc/lib/c/README.md +1 -1
  93. package/src/platforms/genesis/MENTAL_MODEL.md +8 -2
  94. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  95. package/src/platforms/genesis/lib/wram.s +1 -1
  96. package/src/platforms/gg/MENTAL_MODEL.md +6 -0
  97. package/src/platforms/lynx/MENTAL_MODEL.md +6 -0
  98. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  99. package/src/platforms/nes/MENTAL_MODEL.md +6 -0
  100. package/src/platforms/pce/MENTAL_MODEL.md +6 -0
  101. package/src/platforms/sms/MENTAL_MODEL.md +6 -0
  102. package/src/platforms/snes/MENTAL_MODEL.md +10 -4
  103. package/src/toolchains/_worker/wasm-worker.js +5 -0
@@ -0,0 +1,263 @@
1
+ // analyze.js — MCP-facing RE analysis ops built on the Rizin WASM engine:
2
+ // control-flow graphs, deep cross-references, auto-detected functions, and a
3
+ // one-shot structural map. Complements disasm.js (da65/native, rebuildable
4
+ // output) — rizin gives the GRAPH structure da65 can't.
5
+ //
6
+ // Address model: rizin's own bin-loader sets the load address for formats it
7
+ // recognizes (iNES → 0x8000, GBA → 0x08000000, raw → 0). For platforms whose
8
+ // flat file maps 1:1 to the CPU bus (Genesis, plain binaries) the file offset
9
+ // IS the CPU address. We pass an explicit base only where it helps; the
10
+ // reported addresses are rizin virtual addresses, which match the CPU view for
11
+ // the common (unbanked / first-bank) case. Banked carts: a bank's window is
12
+ // resolved by the existing disasm mappers — analysis here is whole-file.
13
+ import { readFile } from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { runRizin, runRizinJson, RIZIN_ARCH } from "./rizin.js";
16
+ import { decompileFunction, SLEIGH_LANGID } from "./decompile.js";
17
+
18
+ /** Sniff platform from a ROM extension (mirrors disasm.js). */
19
+ export function sniffPlatform(p) {
20
+ if (/\.nes$/i.test(p)) return "nes";
21
+ if (/\.(sfc|smc)$/i.test(p)) return "snes";
22
+ if (/\.gbc$/i.test(p)) return "gbc";
23
+ if (/\.gb$/i.test(p)) return "gb";
24
+ if (/\.sms$/i.test(p)) return "sms";
25
+ if (/\.gg$/i.test(p)) return "gg";
26
+ if (/\.a26$/i.test(p)) return "atari2600";
27
+ if (/\.a78$/i.test(p)) return "atari7800";
28
+ if (/\.prg$/i.test(p)) return "c64";
29
+ if (/\.(lnx|lyx)$/i.test(p)) return "lynx";
30
+ if (/\.gba$/i.test(p)) return "gba";
31
+ if (/\.pce$/i.test(p)) return "pce";
32
+ if (/\.(gen|md|bin)$/i.test(p)) return "genesis";
33
+ return null;
34
+ }
35
+
36
+ /** rizin asm.bits per arch (analysis defaults; rizin's loader usually sets
37
+ * these for recognized formats, but raw blobs need a hint). */
38
+ const BITS = { arm: 32, m68k: 32, snes: 16 };
39
+
40
+ /** Build the common rizin invocation context for a ROM + platform. Returns
41
+ * { romBytes, arch, bits, note } — arch null means let rizin sniff. */
42
+ async function loadContext(romPath, platformOverride) {
43
+ const platform = platformOverride ?? sniffPlatform(romPath);
44
+ if (!platform) {
45
+ throw new Error(
46
+ `analyze: could not determine platform from '${path.basename(romPath)}' — pass platform explicitly`
47
+ );
48
+ }
49
+ if (!(platform in RIZIN_ARCH)) {
50
+ throw new Error(`analyze: unsupported platform '${platform}'`);
51
+ }
52
+ const arch = RIZIN_ARCH[platform];
53
+ if (arch == null) {
54
+ throw new Error(`analyze: no Rizin arch mapping for platform '${platform}'`);
55
+ }
56
+ const romBytes = new Uint8Array(await readFile(romPath));
57
+ // PCE: rizin's 6502 plugin drives the loader + standard control flow for
58
+ // function detection, but mis-decodes HuC6280 custom opcodes — CFG/xrefs are
59
+ // approximate. Accurate HuC6280 decode is the decompiler's job (SLEIGH spec).
60
+ const approx = platform === "pce";
61
+ return { platform, romBytes, arch, bits: BITS[arch], approx };
62
+ }
63
+
64
+ /** Hex-format an address the way agents expect for the platform width. */
65
+ function hx(n) { return "0x" + (n >>> 0).toString(16); }
66
+
67
+ /**
68
+ * Auto-detected function list for a ROM.
69
+ * @returns {{platform, count, functions: Array<{address, name, size, nbbs, cc, callers, callees}>}}
70
+ */
71
+ export async function analyzeFunctions(romPath, platformOverride) {
72
+ const { platform, romBytes, arch, bits } = await loadContext(romPath, platformOverride);
73
+ const fns = await runRizinJson({ romBytes, arch, bits, commands: "aaa; aflj" });
74
+ const functions = fns.map((f) => ({
75
+ address: f.offset,
76
+ addressHex: hx(f.offset),
77
+ name: f.name,
78
+ size: f.size,
79
+ nbbs: f.nbbs, // basic-block count
80
+ cc: f.cc, // cyclomatic complexity
81
+ callers: f.indegree ?? (f.codexrefs?.length ?? 0),
82
+ callees: f.outdegree ?? 0,
83
+ }));
84
+ return { platform, arch, count: functions.length, functions };
85
+ }
86
+
87
+ /**
88
+ * Control-flow graph for the function containing `address`.
89
+ * @returns {{platform, address, nodes: Array<{id,address,size,instructions,jump,fail,out}>, edges}}
90
+ */
91
+ export async function analyzeCfg(romPath, address, platformOverride) {
92
+ if (address == null) throw new Error("analyze cfg: address required");
93
+ const { platform, romBytes, arch, bits } = await loadContext(romPath, platformOverride);
94
+ // afbj = basic blocks of the function as JSON: each block has addr/size/jump/
95
+ // fail/ninstr. `jump` is the taken edge; `fail` (present only on conditional
96
+ // blocks) is the fall-through. This is the structured CFG source — `agf json`
97
+ // only gives a text body blob with untyped out_nodes.
98
+ const blocks = await runRizinJson({
99
+ romBytes, arch, bits,
100
+ commands: `aaa; af @ ${hx(address)}; afbj @ ${hx(address)}`,
101
+ });
102
+ if (!Array.isArray(blocks) || blocks.length === 0) {
103
+ return { platform, arch, address, addressHex: hx(address), nodes: [], edges: [], note: "no function/blocks at address" };
104
+ }
105
+ const nodes = blocks.map((b) => ({
106
+ id: b.addr,
107
+ address: b.addr,
108
+ addressHex: hx(b.addr),
109
+ size: b.size,
110
+ ninstr: b.ninstr,
111
+ }));
112
+ const edges = [];
113
+ for (const b of blocks) {
114
+ const conditional = b.fail != null;
115
+ if (b.jump != null) edges.push({ from: b.addr, to: b.jump, type: conditional ? "branch_true" : "jump_or_fall" });
116
+ if (b.fail != null) edges.push({ from: b.addr, to: b.fail, type: "branch_false" });
117
+ }
118
+ return {
119
+ platform, arch,
120
+ address, addressHex: hx(address),
121
+ blockCount: nodes.length,
122
+ nodes, edges,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * All cross-references TO `address` across the ROM.
128
+ * @returns {{platform, address, count, refs: Array<{from, to, type}>}}
129
+ */
130
+ export async function analyzeXrefs(romPath, address, platformOverride) {
131
+ if (address == null) throw new Error("analyze xrefs: address required");
132
+ const { platform, romBytes, arch, bits } = await loadContext(romPath, platformOverride);
133
+ let refs;
134
+ try {
135
+ refs = await runRizinJson({ romBytes, arch, bits, commands: `aaa; axtj @ ${hx(address)}` });
136
+ } catch (e) {
137
+ // axtj prints nothing (not even `[]`) when there are zero refs → our JSON
138
+ // guard throws. Treat "no JSON" as "no refs".
139
+ if (/no JSON/.test(e.message)) refs = [];
140
+ else throw e;
141
+ }
142
+ const out = (refs ?? []).map((r) => ({
143
+ from: r.from,
144
+ fromHex: hx(r.from),
145
+ to: r.to,
146
+ type: (r.type || "").toLowerCase(), // CALL / CODE / DATA / STRING
147
+ opcode: r.opcode,
148
+ }));
149
+ return { platform, arch, address, addressHex: hx(address), count: out.length, refs: out };
150
+ }
151
+
152
+ /**
153
+ * One-shot structural map: functions + strings + entrypoints from a full
154
+ * analysis pass. The "give me the shape of this ROM" call.
155
+ */
156
+ export async function analyzeStructure(romPath, platformOverride) {
157
+ const { platform, romBytes, arch, bits } = await loadContext(romPath, platformOverride);
158
+ const [fns, strings, entries] = await Promise.all([
159
+ runRizinJson({ romBytes, arch, bits, commands: "aaa; aflj" }).catch(() => []),
160
+ runRizinJson({ romBytes, arch, bits, commands: "aaa; izj" }).catch(() => []),
161
+ runRizinJson({ romBytes, arch, bits, commands: "aaa; iej" }).catch(() => []),
162
+ ]);
163
+ return {
164
+ platform, arch,
165
+ functionCount: Array.isArray(fns) ? fns.length : 0,
166
+ stringCount: Array.isArray(strings) ? strings.length : 0,
167
+ entrypoints: (Array.isArray(entries) ? entries : []).map((e) => ({ address: e.vaddr, addressHex: hx(e.vaddr) })),
168
+ functions: (Array.isArray(fns) ? fns : []).slice(0, 512).map((f) => ({
169
+ address: f.offset, addressHex: hx(f.offset), name: f.name, size: f.size, callers: f.indegree ?? 0,
170
+ })),
171
+ strings: (Array.isArray(strings) ? strings : []).slice(0, 256).map((s) => ({
172
+ address: s.vaddr, addressHex: hx(s.vaddr), value: s.string,
173
+ })),
174
+ };
175
+ }
176
+
177
+ /** Look up rizin's IO map (omlj) for the segment containing `vaddr`. Each map
178
+ * entry carries {from (vaddr base), delta (vaddr-paddr), to}. Returns
179
+ * { paddr, vbase } where paddr = vaddr-delta is the raw-file offset and vbase
180
+ * (= delta) is the address byte 0 of the file maps to on the CPU bus. */
181
+ async function vaMapping(romBytes, arch, bits, vaddr) {
182
+ let maps;
183
+ try {
184
+ maps = await runRizinJson({ romBytes, arch, bits, commands: "omlj" });
185
+ } catch { maps = []; }
186
+ for (const m of (Array.isArray(maps) ? maps : [])) {
187
+ const from = m.from ?? m.vaddr ?? 0;
188
+ const to = m.to ?? (from + (m.size ?? 0));
189
+ if (vaddr >= from && vaddr < to) return { paddr: vaddr - (m.delta ?? 0), vbase: m.delta ?? 0 };
190
+ }
191
+ return { paddr: vaddr, vbase: 0 };
192
+ }
193
+
194
+ /**
195
+ * Decompile the function containing `address` to C pseudocode (Ghidra).
196
+ * @returns {{platform, langid, address, code, warnings, qualityNote}}
197
+ */
198
+ export async function analyzeDecompile(romPath, address, platformOverride) {
199
+ if (address == null) throw new Error("analyze decompile: address required");
200
+ const platform = platformOverride ?? sniffPlatform(romPath);
201
+ if (!platform) throw new Error(`analyze decompile: unknown platform for '${path.basename(romPath)}'`);
202
+ if (!SLEIGH_LANGID[platform]) throw new Error(`analyze decompile: unsupported platform '${platform}'`);
203
+ const romBytes = new Uint8Array(await readFile(romPath));
204
+
205
+ // Use rizin's loader mapping to turn the VA (what the user sees from
206
+ // target='functions') into the file offset the raw decompiler image needs.
207
+ // PCE uses the 6502 plugin only for the map/loader (HuC6280 decode is the
208
+ // decompiler's job via SLEIGH) — its flat image bases at 0 either way.
209
+ const arch = RIZIN_ARCH[platform] ?? "6502";
210
+ const bits = { arm: 32, m68k: 32, snes: 16 }[arch];
211
+ const { paddr, vbase } = await vaMapping(romBytes, arch, bits, address);
212
+ if (paddr < 0 || paddr >= romBytes.length) {
213
+ throw new Error(
214
+ `decompile: address ${hx(address)} maps to file offset ${paddr}, outside the ` +
215
+ `${romBytes.length}-byte image for ${platform}.`
216
+ );
217
+ }
218
+ // The raw decompiler loads byte 0 at VMA 0. Code that references absolute CPU
219
+ // addresses (typical 6502: JSR/JMP to $Fxxx) only resolves if the image sits
220
+ // at the right CPU base. Rizin's map gives `vbase` when it knows the base;
221
+ // some headerless carts (2600/7800) it loads at 0, so we supply the base from
222
+ // a per-platform table. Left-pad the image by the base so file offset == CPU
223
+ // address, then decompile at the function's CPU address. Capped at 64KB (the
224
+ // 6502 family's whole address space) so a large base never over-allocates.
225
+ // Atari 2600/7800 are headerless 6502 dumps rizin loads at 0; supply the real
226
+ // CPU base so absolute references ($8000/$C000/$F000) resolve. 7800's base is
227
+ // size-dependent (16KB→$C000, 32KB→$8000); 7800 carts may carry a 128-byte
228
+ // header before the body.
229
+ let forcedBase = 0, bodyStart = 0;
230
+ if (platform === "atari2600") {
231
+ forcedBase = 0xf000;
232
+ } else if (platform === "atari7800") {
233
+ const hasHdr = romBytes.length > 128 &&
234
+ romBytes[1] === 0x41 && romBytes[2] === 0x54; // "AT"
235
+ bodyStart = hasHdr ? 128 : 0;
236
+ const body = romBytes.length - bodyStart;
237
+ forcedBase = body <= 0x4000 ? 0xc000 : body <= 0x8000 ? 0x8000 : 0x4000;
238
+ }
239
+ const base = vbase > 0 ? vbase : forcedBase;
240
+ let image = romBytes, decompAddr = paddr;
241
+ if (base > 0 && base <= 0x10000) {
242
+ const body = romBytes.subarray(bodyStart);
243
+ const padded = new Uint8Array(base + body.length);
244
+ padded.set(body, base);
245
+ image = padded;
246
+ decompAddr = base + (paddr - bodyStart); // CPU address of the function
247
+ }
248
+ const r = await decompileFunction({ platform, romBytes: image, fileOffset: decompAddr });
249
+ const QUALITY = {
250
+ gba: "excellent (ARM)", genesis: "excellent (M68K)",
251
+ gb: "good (SM83)", gbc: "good (SM83)", sms: "good (Z80)", gg: "good (Z80)", msx: "good (Z80)",
252
+ snes: "medium (65816 variable register width)", pce: "medium (HuC6280)",
253
+ nes: "rough (6502 architecture limit)", atari2600: "rough (6502)", atari7800: "rough (6502)",
254
+ c64: "rough (6502)", lynx: "rough (65C02)",
255
+ };
256
+ return {
257
+ platform, langid: r.langid,
258
+ address, addressHex: hx(address),
259
+ code: r.code,
260
+ warnings: r.warnings,
261
+ qualityNote: QUALITY[platform] ?? "unknown",
262
+ };
263
+ }
@@ -0,0 +1,108 @@
1
+ // decompile.js — drive the Ghidra decompiler WASM (romdev-analysis-decompiler)
2
+ // to turn a function into C pseudocode. Runs the decompiler's REPL one-shot
3
+ // through the isolated worker pool: mount the SLEIGH home + the ROM image,
4
+ // feed `load file <langid> <rom>; map function <addr>; decompile; print C`.
5
+ //
6
+ // The ROM is loaded as a RAW binary at VMA 0 — so the address passed must be a
7
+ // FILE OFFSET into the image we hand it, not a banked CPU address. Callers that
8
+ // have a CPU address use the disasm mappers to slice the right bank first; for
9
+ // the common flat/first-bank case the file offset equals the CPU address minus
10
+ // the platform's load base (handled in analyze.js).
11
+ import { fileURLToPath } from "node:url";
12
+ import path from "node:path";
13
+ import fs from "node:fs";
14
+ import { runIsolated } from "../toolchains/_worker/run.js";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ /** Resolve the decompiler package (or the gitignored src staging fallback). */
19
+ function decompilerPaths() {
20
+ let base;
21
+ try {
22
+ base = path.dirname(fileURLToPath(import.meta.resolve("romdev-analysis-decompiler")));
23
+ } catch { base = null; }
24
+ const candidates = [
25
+ base && { js: path.join(base, "wasm", "decompile.js"), sleigh: path.join(base, "sleigh") },
26
+ { js: path.join(__dirname, "decompiler", "wasm", "decompile.js"), sleigh: path.join(__dirname, "decompiler", "sleigh") },
27
+ ].filter(Boolean);
28
+ for (const c of candidates) if (fs.existsSync(c.js)) return c;
29
+ throw new Error(
30
+ "decompiler not found: install romdev-analysis-decompiler or run scripts/build-decompiler.sh"
31
+ );
32
+ }
33
+
34
+ /** romdev platform → Ghidra SLEIGH language id. null = not decompilable yet. */
35
+ export const SLEIGH_LANGID = {
36
+ nes: "6502:LE:16:default",
37
+ atari2600: "6502:LE:16:default",
38
+ atari7800: "6502:LE:16:default",
39
+ c64: "6502:LE:16:default",
40
+ lynx: "65C02:LE:16:default",
41
+ sms: "z80:LE:16:default",
42
+ gg: "z80:LE:16:default",
43
+ msx: "z80:LE:16:default",
44
+ gb: "SM83:LE:16:default",
45
+ gbc: "SM83:LE:16:default",
46
+ gba: "ARM:LE:32:v4t",
47
+ genesis: "68000:BE:32:default",
48
+ snes: "65816:LE:24:snes",
49
+ pce: "HuC6280:LE:16:default",
50
+ };
51
+
52
+ /**
53
+ * Decompile the function at `fileOffset` in `romBytes` for `platform`.
54
+ * @returns {{platform, langid, address, code, warnings:string[], raw:string}}
55
+ */
56
+ export async function decompileFunction({ platform, romBytes, fileOffset, name = "fn_target" }) {
57
+ const langid = SLEIGH_LANGID[platform];
58
+ if (!langid) throw new Error(`decompile: no SLEIGH language for platform '${platform}'`);
59
+ const { js, sleigh } = decompilerPaths();
60
+ const addr = "0x" + (fileOffset >>> 0).toString(16);
61
+
62
+ // REPL script. RawBinary loads at vma 0; `print C` emits the pseudocode.
63
+ const script = [
64
+ `load file ${langid} /work/rom.bin`,
65
+ `map function ${addr} ${name}`,
66
+ `decompile ${name}`,
67
+ `print C`,
68
+ `quit`,
69
+ "",
70
+ ].join("\n");
71
+
72
+ const res = await runIsolated({
73
+ gluePath: js,
74
+ argv: [],
75
+ env: { SLEIGHHOME: "/sleigh" },
76
+ stdinText: script,
77
+ hostDirMounts: [{ hostDir: sleigh, vfsDir: "/sleigh" }],
78
+ inputFiles: [{
79
+ vfsPath: "/work/rom.bin",
80
+ encoding: "base64",
81
+ data: Buffer.from(romBytes).toString("base64"),
82
+ }],
83
+ });
84
+
85
+ const raw = res.log ?? "";
86
+ // The decompiler echoes each prompt; the C body follows `print C`.
87
+ const code = extractC(raw);
88
+ const warnings = [...raw.matchAll(/\/\* WARNING: (.+?) \*\//g)].map((m) => m[1]);
89
+ if (!code) {
90
+ throw new Error(`decompile produced no C output (exit=${res.exitCode}): ${raw.slice(-400)}`);
91
+ }
92
+ return { platform, langid, address: fileOffset, addressHex: addr, code, warnings, raw };
93
+ }
94
+
95
+ /** Pull the C function text out of the REPL transcript (between `print C` and
96
+ * the next `[decomp]>` prompt). */
97
+ function extractC(transcript) {
98
+ const lines = transcript.split("\n");
99
+ const start = lines.findIndex((l) => /\[decomp\]>\s*print C\b/.test(l));
100
+ if (start === -1) return null;
101
+ const body = [];
102
+ for (let i = start + 1; i < lines.length; i++) {
103
+ if (/^\[decomp\]>/.test(lines[i])) break;
104
+ body.push(lines[i]);
105
+ }
106
+ const text = body.join("\n").trim();
107
+ return text.length ? text : null;
108
+ }
@@ -0,0 +1,34 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <compiler_spec>
4
+ <global>
5
+ <range space="RAM"/>
6
+ </global>
7
+ <stackpointer register="SP" space="RAM" growth="negative"/>
8
+ <returnaddress>
9
+ <varnode space="stack" offset="1" size="2"/>
10
+ </returnaddress>
11
+ <default_proto>
12
+ <prototype name="__stdcall" extrapop="2" stackshift="2" strategy="register">
13
+ <input>
14
+ <pentry minsize="1" maxsize="1">
15
+ <register name="A"/>
16
+ </pentry>
17
+ <pentry minsize="1" maxsize="1">
18
+ <register name="X"/>
19
+ </pentry>
20
+ <pentry minsize="1" maxsize="1">
21
+ <register name="Y"/>
22
+ </pentry>
23
+ </input>
24
+ <output>
25
+ <pentry minsize="1" maxsize="1">
26
+ <register name="A"/>
27
+ </pentry>
28
+ </output>
29
+ <unaffected>
30
+ <register name="SP"/>
31
+ </unaffected>
32
+ </prototype>
33
+ </default_proto>
34
+ </compiler_spec>
@@ -0,0 +1,33 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <language_definitions>
4
+
5
+ <language processor="6502"
6
+ endian="little"
7
+ size="16"
8
+ variant="default"
9
+ version="1.0"
10
+ slafile="6502.sla"
11
+ processorspec="6502.pspec"
12
+ manualindexfile="../manuals/6502.idx"
13
+ id="6502:LE:16:default">
14
+ <description>6502 Microcontroller Family</description>
15
+ <compiler name="default" spec="6502.cspec" id="default"/>
16
+ <external_name tool="IDA-PRO" name="m6502"/>
17
+ </language>
18
+
19
+ <language processor="65C02"
20
+ endian="little"
21
+ size="16"
22
+ variant="default"
23
+ version="1.0"
24
+ slafile="65c02.sla"
25
+ processorspec="6502.pspec"
26
+ manualindexfile="../manuals/65c02.idx"
27
+ id="65C02:LE:16:default">
28
+ <description>65C02 Microcontroller Family</description>
29
+ <compiler name="default" spec="6502.cspec" id="default"/>
30
+ <external_name tool="IDA-PRO" name="m65c02"/>
31
+ </language>
32
+
33
+ </language_definitions>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+
3
+ <processor_spec>
4
+ <programcounter register="PC"/>
5
+
6
+ <default_symbols>
7
+ <symbol name="NMI" address="FFFA" entry="true" type="code_ptr"/>
8
+ <symbol name="RES" address="FFFC" entry="true" type="code_ptr"/>
9
+ <symbol name="IRQ" address="FFFE" entry="true" type="code_ptr"/>
10
+ </default_symbols>
11
+
12
+ <default_memory_blocks>
13
+ <memory_block name="ZERO_PAGE" start_address="0x0000" length="0x0100" initialized="false"/>
14
+ <memory_block name="STACK" start_address="0x0100" length="0x0100" initialized="false"/>
15
+ </default_memory_blocks>
16
+ </processor_spec>