romdevtools 0.41.0 → 0.43.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/CHANGELOG.md +94 -0
- package/package.json +3 -1
- package/src/analysis/analyze.js +1 -1
- package/src/analysis/backtrace.js +198 -0
- package/src/analysis/nes-ppu-runtime.js +220 -0
- package/src/analysis/nes-ppu-shim.js +263 -0
- package/src/analysis/pointer-table.js +84 -0
- package/src/analysis/recompile/emit-65816.js +105 -0
- package/src/analysis/recompile/emit-m68k.js +295 -0
- package/src/analysis/recompile/index.js +169 -0
- package/src/analysis/recompile/ir.js +123 -0
- package/src/analysis/recompile/lift-6502.js +176 -0
- package/src/analysis/recompile-65816.js +533 -0
- package/src/cheats/gamegenie.js +14 -2
- package/src/cores/registry.js +43 -51
- package/src/mcp/state.js +91 -2
- package/src/mcp/tools/cheats.js +10 -1
- package/src/mcp/tools/disasm.js +329 -21
- package/src/mcp/tools/find-references.js +93 -2
- package/src/mcp/tools/frame.js +440 -4
- package/src/mcp/tools/index.js +34 -1
- package/src/mcp/tools/lifecycle.js +53 -24
- package/src/mcp/tools/memory.js +1 -1
- package/src/mcp/tools/playtest.js +24 -1
- package/src/mcp/tools/project.js +1 -1
- package/src/mcp/tools/rendering-context.js +4 -2
- package/src/mcp/tools/watch-memory.js +87 -9
- package/src/mcp/util.js +45 -0
- package/src/playtest/playtest.js +115 -46
- package/src/toolchains/cc65/da65.js +7 -0
- package/src/cores/wasm/bluemsx_libretro.js +0 -2
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +0 -2
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +0 -2
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +0 -2
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +0 -2
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +0 -2
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +0 -2
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +0 -2
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +0 -2
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +0 -2
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +0 -2
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// 6502 → IR lifter. Turns one da65 6502 disassembly into the generic recompile
|
|
2
|
+
// IR (ir.js), tagging each instruction with an ABSTRACT op so a non-1:1 target
|
|
3
|
+
// emitter (m68k, z80, …) can translate the INTENT without knowing 6502. The 1:1
|
|
4
|
+
// emitter (65816 emulation mode) ignores the abstract tag and re-emits the
|
|
5
|
+
// mnemonic verbatim — both consume the same IR.
|
|
6
|
+
//
|
|
7
|
+
// This is the existing recompile-65816 classification logic, refactored to emit
|
|
8
|
+
// IR nodes instead of 65816 text. Same documented-opcode set, same seam
|
|
9
|
+
// detection, same refuse rules — but now source-agnostic at the boundary.
|
|
10
|
+
//
|
|
11
|
+
// Plain JS ESM + JSDoc.
|
|
12
|
+
|
|
13
|
+
import { parseDa65Line } from "../recompile-65816.js";
|
|
14
|
+
import { NES_REGISTERS } from "../../platforms/common/registers.js";
|
|
15
|
+
import {
|
|
16
|
+
ABSTRACT, COND,
|
|
17
|
+
irLabel, irReg, irBranch, irJump, irCall, irRet, irHwReg, irRefuse,
|
|
18
|
+
} from "./ir.js";
|
|
19
|
+
|
|
20
|
+
/** The 151 documented 6502 mnemonics — anything else (an undocumented opcode da65
|
|
21
|
+
* rendered, or a data byte caught in the stream) is REFUSED, not guessed. */
|
|
22
|
+
export const DOCUMENTED_6502 = new Set([
|
|
23
|
+
"adc", "and", "asl", "bcc", "bcs", "beq", "bit", "bmi", "bne", "bpl", "brk",
|
|
24
|
+
"bvc", "bvs", "clc", "cld", "cli", "clv", "cmp", "cpx", "cpy", "dec", "dex",
|
|
25
|
+
"dey", "eor", "inc", "inx", "iny", "jmp", "jsr", "lda", "ldx", "ldy", "lsr",
|
|
26
|
+
"nop", "ora", "pha", "php", "pla", "plp", "rol", "ror", "rti", "rts", "sbc",
|
|
27
|
+
"sec", "sed", "sei", "sta", "stx", "sty", "tax", "tay", "tsx", "txa", "txs",
|
|
28
|
+
"tya",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** mnemonic → abstract op, so an emitter switches on INTENT not the 6502 name.
|
|
32
|
+
* (Branches/jumps/calls/returns/hwreg are handled structurally below, not here.) */
|
|
33
|
+
const ABSTRACT_OF = {
|
|
34
|
+
lda: ABSTRACT.LOAD_ACC, ldx: ABSTRACT.LOAD_X, ldy: ABSTRACT.LOAD_Y,
|
|
35
|
+
sta: ABSTRACT.STORE_ACC, stx: ABSTRACT.STORE_X, sty: ABSTRACT.STORE_Y,
|
|
36
|
+
tax: ABSTRACT.TRANSFER, tay: ABSTRACT.TRANSFER, txa: ABSTRACT.TRANSFER,
|
|
37
|
+
tya: ABSTRACT.TRANSFER, tsx: ABSTRACT.TRANSFER, txs: ABSTRACT.TRANSFER,
|
|
38
|
+
pha: ABSTRACT.PUSH, php: ABSTRACT.PUSH, pla: ABSTRACT.PULL, plp: ABSTRACT.PULL,
|
|
39
|
+
adc: ABSTRACT.ADD, sbc: ABSTRACT.SUB, and: ABSTRACT.AND, ora: ABSTRACT.OR,
|
|
40
|
+
eor: ABSTRACT.XOR, inc: ABSTRACT.INC, dec: ABSTRACT.DEC, asl: ABSTRACT.SHL,
|
|
41
|
+
lsr: ABSTRACT.SHR, rol: ABSTRACT.ROL, ror: ABSTRACT.ROR, cmp: ABSTRACT.CMP,
|
|
42
|
+
bit: ABSTRACT.BIT, inx: ABSTRACT.INX, iny: ABSTRACT.INY, dex: ABSTRACT.DEX,
|
|
43
|
+
dey: ABSTRACT.DEY, cpx: ABSTRACT.CPX, cpy: ABSTRACT.CPY,
|
|
44
|
+
clc: ABSTRACT.CLR_FLAG, sec: ABSTRACT.SET_FLAG, cli: ABSTRACT.CLR_FLAG,
|
|
45
|
+
sei: ABSTRACT.SET_FLAG, clv: ABSTRACT.CLR_FLAG, cld: ABSTRACT.CLR_FLAG,
|
|
46
|
+
nop: ABSTRACT.NOP,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 6502 conditional-branch mnemonic → ISA-neutral condition. */
|
|
50
|
+
const BRANCH_COND = {
|
|
51
|
+
beq: COND.EQ, bne: COND.NE, bcc: COND.CC, bcs: COND.CS,
|
|
52
|
+
bmi: COND.MI, bpl: COND.PL, bvc: COND.VC, bvs: COND.VS,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect a hardware-register (MMIO) access — the seam. Returns the register's low
|
|
57
|
+
* address if the operand targets a NES PPU/APU register, else null. Only absolute
|
|
58
|
+
* operands count; immediates and zero-page never hit the register file.
|
|
59
|
+
* @param {string|undefined} operand
|
|
60
|
+
*/
|
|
61
|
+
export function seamRegister(operand) {
|
|
62
|
+
if (!operand) return null;
|
|
63
|
+
const m = operand.match(/^\$([0-9A-Fa-f]{3,4})(?:\s*,\s*[xy])?$/);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
const addr = parseInt(m[1], 16);
|
|
66
|
+
if (addr in NES_REGISTERS) return addr;
|
|
67
|
+
if ((addr >= 0x2000 && addr <= 0x2007) || (addr >= 0x4000 && addr <= 0x4017)) return addr;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Lift one parsed da65 6502 line to an IR node (or null for blank/directive/equ —
|
|
73
|
+
* those are handled by the orchestrator, which collects equs separately). The
|
|
74
|
+
* leading label, if any, rides on the returned node so branch targets resolve.
|
|
75
|
+
* @param {ReturnType<typeof parseDa65Line>} p
|
|
76
|
+
* @returns {object|null}
|
|
77
|
+
*/
|
|
78
|
+
function liftInstr(p) {
|
|
79
|
+
const label = p.label;
|
|
80
|
+
const mnem = p.mnemonic;
|
|
81
|
+
const operand = p.operand;
|
|
82
|
+
|
|
83
|
+
// Refuse the non-mechanical constructs up front (same as the original).
|
|
84
|
+
if (mnem === "sed") return irRefuse("decimal-mode (sed): BCD edge-flag semantics differ across ISAs", p.raw, label);
|
|
85
|
+
if (mnem === "jmp" && operand && operand.startsWith("(")) {
|
|
86
|
+
return irRefuse("indirect jump (jmp (addr)): target is computed — resolve with breakpoint({on:'jumptable'})", p.raw, label);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Hardware seam: any PPU/APU register access becomes an IR hwreg node.
|
|
90
|
+
const reg = seamRegister(operand);
|
|
91
|
+
if (reg != null) {
|
|
92
|
+
const isStore = mnem === "sta" || mnem === "stx" || mnem === "sty";
|
|
93
|
+
const isLoad = mnem === "lda" || mnem === "ldx" || mnem === "ldy" || mnem === "bit";
|
|
94
|
+
if (isStore) return irHwReg("write", reg, mnem, p.raw, label);
|
|
95
|
+
if (isLoad) return irHwReg("read", reg, mnem, p.raw, label);
|
|
96
|
+
return irRefuse(`hardware-register read-modify-write (${mnem} ${operand}): not in the mechanical set`, p.raw, label);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Control flow → structural IR nodes.
|
|
100
|
+
if (mnem === "jsr") return irCall(operand, p.raw, label);
|
|
101
|
+
if (mnem === "jmp") return irJump(operand, p.raw, label);
|
|
102
|
+
if (mnem === "rts") return irRet("sub", p.raw, label);
|
|
103
|
+
if (mnem === "rti") return irRet("interrupt", p.raw, label);
|
|
104
|
+
if (mnem === "brk") return irRefuse("brk: software interrupt needs explicit vector handling in v1", p.raw, label);
|
|
105
|
+
if (mnem in BRANCH_COND) return irBranch(BRANCH_COND[mnem], operand, p.raw, label);
|
|
106
|
+
|
|
107
|
+
// A documented data/ALU/transfer op → a `reg` node tagged with its abstract op.
|
|
108
|
+
if (mnem in ABSTRACT_OF) {
|
|
109
|
+
return irReg(ABSTRACT_OF[mnem], mnem, operand, p.raw, label);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Unknown mnemonic = undocumented opcode or a parse miss.
|
|
113
|
+
return irRefuse(`unrecognized/undocumented opcode '${mnem}' — not a documented 6502 instruction`, p.raw, label);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Lift a block of da65 6502 asm text into an IR program. Returns the IR node list,
|
|
118
|
+
* the collected equ definitions (address aliases), and a flag for whether the
|
|
119
|
+
* first instruction was anchored (so the orchestrator can fix the entry label).
|
|
120
|
+
* @param {string} da65Asm
|
|
121
|
+
* @returns {{ ir: Array<object>, equs: string[], instrCount: number, seamCount: number, entry: string|null }}
|
|
122
|
+
*/
|
|
123
|
+
export function lift6502(da65Asm) {
|
|
124
|
+
const lines = da65Asm.split("\n");
|
|
125
|
+
const ir = [];
|
|
126
|
+
const equs = [];
|
|
127
|
+
let instrCount = 0;
|
|
128
|
+
let seamCount = 0;
|
|
129
|
+
let entry = null;
|
|
130
|
+
const ENTRY_LABEL = "RECOMPILE_ENTRY";
|
|
131
|
+
|
|
132
|
+
for (const raw of lines) {
|
|
133
|
+
const p = parseDa65Line(raw);
|
|
134
|
+
switch (p.kind) {
|
|
135
|
+
case "blank":
|
|
136
|
+
case "comment":
|
|
137
|
+
case "directive":
|
|
138
|
+
break;
|
|
139
|
+
case "equ":
|
|
140
|
+
equs.push(`${p.label} = ${p.operand}`);
|
|
141
|
+
break;
|
|
142
|
+
case "label":
|
|
143
|
+
ir.push(irLabel(p.label));
|
|
144
|
+
break;
|
|
145
|
+
case "data":
|
|
146
|
+
ir.push(irRefuse("data/.byte in code stream (data table or undocumented opcode)", p.raw));
|
|
147
|
+
break;
|
|
148
|
+
case "instr": {
|
|
149
|
+
const node = liftInstr(p);
|
|
150
|
+
if (!node) break;
|
|
151
|
+
// Anchor the entry to the FIRST real instruction (lifted node), labeled or
|
|
152
|
+
// not — da65 only labels branch targets, so a fall-through opener is
|
|
153
|
+
// unlabeled and would otherwise let the reset skip the routine's setup.
|
|
154
|
+
if (entry == null && node.op !== "refuse") {
|
|
155
|
+
if (node.label) {
|
|
156
|
+
entry = node.label;
|
|
157
|
+
} else {
|
|
158
|
+
entry = ENTRY_LABEL;
|
|
159
|
+
ir.push(irLabel(ENTRY_LABEL));
|
|
160
|
+
}
|
|
161
|
+
} else if (entry == null && node.op === "refuse") {
|
|
162
|
+
// even a refused opener anchors the entry so the vector lands at the top
|
|
163
|
+
if (node.label) entry = node.label;
|
|
164
|
+
else { entry = ENTRY_LABEL; ir.push(irLabel(ENTRY_LABEL)); }
|
|
165
|
+
}
|
|
166
|
+
if (node.op === "hwreg") seamCount++;
|
|
167
|
+
if (node.op !== "refuse") instrCount++;
|
|
168
|
+
ir.push(node);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { ir, equs, instrCount, seamCount, entry };
|
|
176
|
+
}
|