@asmlift/core 0.1.0 → 0.3.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/README.md +18 -23
- package/package.json +1 -1
- package/src/backend/cfamily.ts +30 -4
- package/src/contracts.ts +30 -0
- package/src/declare.ts +225 -0
- package/src/detect.ts +5 -2
- package/src/frontend/format.ts +11 -3
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +206 -2
- package/src/frontend/splat.ts +305 -0
- package/src/frontend/thumb.ts +119 -6
- package/src/l3/ast.ts +8 -2
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +126 -0
- package/src/pipeline.ts +15 -4
- package/src/proto.ts +55 -0
- package/src/raise/magicdiv.ts +1 -1
- package/src/rank.ts +215 -76
- package/src/structure/structure.ts +462 -45
- package/src/symbols.ts +426 -0
- package/src/trace.ts +8 -2
package/src/frontend/mips.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { FrontendUnsupportedError } from './errors';
|
|
|
30
30
|
import { assertInputFormat } from './format';
|
|
31
31
|
import type { Frontend } from './frontend';
|
|
32
32
|
import { opaqueDest } from './opaque';
|
|
33
|
+
import { isSplatMips, parseSplatMips } from './splat';
|
|
33
34
|
import { abiSortEntryParams } from './ssa';
|
|
34
35
|
import { makeSsaBuilder } from './ssa';
|
|
35
36
|
|
|
@@ -87,6 +88,99 @@ const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
|
|
|
87
88
|
// `sll`+`addu` before the access, so no `base+index` addressing form appears in parseMem input.
|
|
88
89
|
const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm);
|
|
89
90
|
|
|
91
|
+
// A MIPS `%hi`/`%lo` relocation operand — the assembler's HI16/LO16 split that materialises the
|
|
92
|
+
// address of a named global: `%hi(SYM)`, `%lo(SYM)`, `%hi(SYM + N)`, or the memory form
|
|
93
|
+
// `%lo(SYM + N)(base)`. Splat spells global access this way and the Splat parser preserves it
|
|
94
|
+
// verbatim (frontend/splat.ts); the objdump dialect hides the symbol in a relocation, which
|
|
95
|
+
// `applyMipsGlobalRelocs` rewrites into the same `%hi`/`%lo` operands. Either way `lift` folds a
|
|
96
|
+
// `lui %hi` + its consuming `%lo` into a single `gaddr(SYM)` (the op the Thumb frontend also emits
|
|
97
|
+
// for a pool-loaded global), carrying the addend as the access offset. Returns null for a
|
|
98
|
+
// non-`%hi/%lo` operand.
|
|
99
|
+
function parseReloc(kind: 'hi' | 'lo', operand: string): { sym: string; addend: number; base?: string } | null {
|
|
100
|
+
const m = operand.match(
|
|
101
|
+
new RegExp(String.raw`^%${kind}\(\s*([A-Za-z_.$][\w.$]*)\s*(?:\+\s*(0x[0-9a-fA-F]+|\d+))?\s*\)(?:\((\w+)\))?$`),
|
|
102
|
+
);
|
|
103
|
+
if (!m) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return { sym: m[1], addend: m[2] ? parseImm(m[2]) : 0, base: m[3] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Bridge objdump global-access relocations into the `%hi`/`%lo` operands `parseReloc` reads, so the
|
|
110
|
+
// gaddr recognition recovers named globals from an object file the same way it does from Splat text.
|
|
111
|
+
// In objdump a global load shows `lui rX,0x0` with the symbol ONLY in the `R_MIPS_HI16`/`LO16`
|
|
112
|
+
// reloc records — without this the base decodes as address 0 and the access reads `*(T *)0`. Using
|
|
113
|
+
// asmData, rewrite each `lui`'s immediate to `%hi(SYM)` and its paired consumer's operand to
|
|
114
|
+
// `%lo(SYM[+N])`. Mirrors the harness's disasmToM2c rewrite, so asmlift and m2c recover the same
|
|
115
|
+
// symbols. NAMED object symbols only — a reloc against a `.rodata`/`.data` SECTION is a jump-table
|
|
116
|
+
// base (Regime B) or section-relative data, left untouched.
|
|
117
|
+
function applyMipsGlobalRelocs(instrs: Instr[], ad: AsmData): void {
|
|
118
|
+
const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
119
|
+
const his: { addr: number; sym: string }[] = [];
|
|
120
|
+
const los = new Map<number, string>(); // LO16 instruction addr → symbol
|
|
121
|
+
for (const r of ad.relocs) {
|
|
122
|
+
if (r.section !== '.text' || r.sym.startsWith('.')) {
|
|
123
|
+
continue; // section-symbol relocs are jump tables / anonymous data — not named globals
|
|
124
|
+
}
|
|
125
|
+
if (r.type === 'R_MIPS_HI16') {
|
|
126
|
+
his.push({ addr: r.offset, sym: r.sym });
|
|
127
|
+
} else if (r.type === 'R_MIPS_LO16') {
|
|
128
|
+
los.set(r.offset, r.sym);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (his.length === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
his.sort((a, b) => a.addr - b.addr);
|
|
135
|
+
const loAddrs = [...los.keys()].sort((a, b) => a - b);
|
|
136
|
+
const consumed = new Set<number>();
|
|
137
|
+
for (const hi of his) {
|
|
138
|
+
const lui = byAddr.get(hi.addr);
|
|
139
|
+
if (!lui || lui.mnemonic !== 'lui') {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Pair with the first not-yet-consumed same-symbol LO16 after the lui (GCC emits the pair with
|
|
143
|
+
// the base register threaded, so a 1:1 by-symbol-and-order match is the observed shape).
|
|
144
|
+
const loAddr = loAddrs.find((a) => a > hi.addr && !consumed.has(a) && los.get(a) === hi.sym);
|
|
145
|
+
const lo = loAddr !== undefined ? byAddr.get(loAddr) : undefined;
|
|
146
|
+
if (!lo) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// The addend N rides in the instruction fields, not the reloc record: `(HI16 imm << 16) + the
|
|
150
|
+
// LO16 instruction's signed immediate`. HI16 imm is 0 in a relocatable object.
|
|
151
|
+
const rw = rewriteLoReloc(lo, hi.sym, parseImm(lui.ops[1] ?? '0') << 16);
|
|
152
|
+
if (rw === null) {
|
|
153
|
+
continue; // an unmodelled consumer (FP load, …) — leave the pair raw; it declines downstream
|
|
154
|
+
}
|
|
155
|
+
lui.ops[1] = rw.n === 0 ? `%hi(${hi.sym})` : `%hi(${hi.sym} + 0x${rw.n.toString(16)})`;
|
|
156
|
+
consumed.add(loAddr!);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Rewrite a LO16 consumer's operand to `%lo(SYM[+N])`, returning the addend N, or null when the
|
|
161
|
+
// instruction is not a modelled global consumer (leave it raw). `hiBase` is the HI16 imm << 16.
|
|
162
|
+
function rewriteLoReloc(lo: Instr, sym: string, hiBase: number): { n: number } | null {
|
|
163
|
+
const macro = (n: number) => (n === 0 ? `%lo(${sym})` : `%lo(${sym} + 0x${n.toString(16)})`);
|
|
164
|
+
if (lo.mnemonic === 'addiu' || lo.mnemonic === 'addi') {
|
|
165
|
+
const n = hiBase + parseImm(lo.ops[2] ?? '0');
|
|
166
|
+
if (n < 0) {
|
|
167
|
+
return null; // a negative interior offset — unusual; leave raw
|
|
168
|
+
}
|
|
169
|
+
lo.ops[2] = macro(n);
|
|
170
|
+
return { n };
|
|
171
|
+
}
|
|
172
|
+
if (/^(lw|lh|lhu|lb|lbu|sw|sh|sb)$/.test(lo.mnemonic)) {
|
|
173
|
+
const mem = parseMem(lo.ops[lo.ops.length - 1] ?? '');
|
|
174
|
+
const n = hiBase + mem.off;
|
|
175
|
+
if (n < 0) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
lo.ops[lo.ops.length - 1] = `${macro(n)}(${mem.base})`;
|
|
179
|
+
return { n };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
90
184
|
interface MipsBlock {
|
|
91
185
|
startAddr: number;
|
|
92
186
|
body: Instr[]; // computation instructions (excludes the branch and its delay slot)
|
|
@@ -370,8 +464,15 @@ export function lift(
|
|
|
370
464
|
_prototypes: Prototypes = {},
|
|
371
465
|
asmData?: AsmData,
|
|
372
466
|
): Fn {
|
|
373
|
-
|
|
374
|
-
|
|
467
|
+
// Two input dialects reach this frontend: `objdump -d` text (IDO/KMC — no compiler-emitted asm),
|
|
468
|
+
// and Splat-disassembled `.s` (pmret-style N64 projects). Splat is normalised to the SAME
|
|
469
|
+
// DisasmInstr[] shape (frontend/splat.ts) so everything below is dialect-agnostic.
|
|
470
|
+
const splat = isSplatMips(asm);
|
|
471
|
+
if (!splat) {
|
|
472
|
+
assertInputFormat('mips', 'objdump', asm);
|
|
473
|
+
}
|
|
474
|
+
// ONE function only — an absent symbol declines loud (either dialect's slicer enforces this).
|
|
475
|
+
const instrs = splat ? parseSplatMips(asm, name) : parseDisasm(sliceSymbol(asm, name));
|
|
375
476
|
if (instrs.length === 0) {
|
|
376
477
|
throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
|
|
377
478
|
}
|
|
@@ -380,6 +481,12 @@ export function lift(
|
|
|
380
481
|
// exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
|
|
381
482
|
const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
|
|
382
483
|
const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
|
|
484
|
+
// Bridge global-access relocations into `%hi`/`%lo` operands (objdump dialect only — Splat text
|
|
485
|
+
// already carries them). Runs AFTER jump-table recovery so its raw `.rodata` table-base relocs are
|
|
486
|
+
// read pristine; global rewrites target NAMED symbols and never touch a jump-table base.
|
|
487
|
+
if (!splat && asmData) {
|
|
488
|
+
applyMipsGlobalRelocs(instrs, asmData);
|
|
489
|
+
}
|
|
383
490
|
// TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
|
|
384
491
|
// path cannot catch these (implicit or no register destination). `jal`/`jalr` clobber `v0`
|
|
385
492
|
// implicitly, so dropping a call fabricates `v0` from a stale value; a `jr` to anything but `ra`
|
|
@@ -451,10 +558,31 @@ export function lift(
|
|
|
451
558
|
|
|
452
559
|
const fillBlock = (b: MipsBlock, bi: number) => {
|
|
453
560
|
const ops = irBlocks[bi].ops;
|
|
561
|
+
// Pending `lui rX, %hi(SYM)` relocations awaiting their consuming `%lo` (a load/store base or an
|
|
562
|
+
// `addiu`). Block-local: the pair is emitted adjacently, so a `%lo` with no matching in-scope
|
|
563
|
+
// `%hi` (a cross-block or gp-relative access) declines LOUD rather than fabricating a base.
|
|
564
|
+
const hiReloc = new Map<string, { sym: string; addend: number }>();
|
|
565
|
+
// Materialise the address of a named global — the same `gaddr` op the Thumb frontend emits; the
|
|
566
|
+
// structurer lowers a load/store through it to `SYM` (scalar) or `((T *)&SYM)[i]` (aggregate).
|
|
567
|
+
const emitGaddr = (sym: string): Value => {
|
|
568
|
+
const g = mkValue(T.unk(32));
|
|
569
|
+
ops.push(mkOp('gaddr', { results: [g], attrs: { sym } }));
|
|
570
|
+
return g;
|
|
571
|
+
};
|
|
454
572
|
const read = (r: string): Value => {
|
|
455
573
|
if (isZero(r)) {
|
|
456
574
|
return constVal(0);
|
|
457
575
|
}
|
|
576
|
+
// A `%hi(SYM)` register read as DATA before its `%lo` completes the address is a split hi/lo
|
|
577
|
+
// relocation (the high half used alone) this frontend does not model — decline rather than
|
|
578
|
+
// treat the partial address as a value. The legit consumers (load/store/addiu `%lo`) validate
|
|
579
|
+
// `hiReloc` directly and never route the base through `read`, so this fires only on misuse.
|
|
580
|
+
const hr = hiReloc.get(r);
|
|
581
|
+
if (hr) {
|
|
582
|
+
throw new FrontendUnsupportedError(
|
|
583
|
+
`cannot lift '${name}': %hi(${hr.sym}) register used as data before a matching %lo — split hi/lo relocation not modelled`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
458
586
|
// Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
|
|
459
587
|
// (`addiu a0,sp,8` = `&local`) — not modellable without a stack abstraction. Fabricating a
|
|
460
588
|
// value for `sp` invents a PHANTOM leading parameter that shifts every real argument — a
|
|
@@ -477,6 +605,10 @@ export function lift(
|
|
|
477
605
|
return readVar(r, bi);
|
|
478
606
|
};
|
|
479
607
|
const write = (r: string, v: Value) => {
|
|
608
|
+
// Writing a register clears any pending `%hi` it held — the high-half address is gone once the
|
|
609
|
+
// register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
|
|
610
|
+
// `%hi` NOT overwritten persists across multiple `%lo` uses (the read-modify-write idiom).
|
|
611
|
+
hiReloc.delete(r);
|
|
480
612
|
if (!isZero(r)) {
|
|
481
613
|
writeVar(r, bi, v);
|
|
482
614
|
}
|
|
@@ -527,6 +659,22 @@ export function lift(
|
|
|
527
659
|
// and raise/const.ts folds the const/const pair into one 32-bit const — the form that
|
|
528
660
|
// recompiles to this exact `lui;ori`.
|
|
529
661
|
case 'lui':
|
|
662
|
+
// `lui rD, %hi(SYM)` is the high half of a global's address — record it, pending the `%lo`
|
|
663
|
+
// that completes it (below), instead of materialising a bogus numeric const. rD's SSA value
|
|
664
|
+
// is deliberately NOT written: the high half is meaningless alone, so the `gaddr` is emitted
|
|
665
|
+
// at the consuming `%lo`. A read of rD as data before that is caught by the `read` guard;
|
|
666
|
+
// an UNconsumed `%hi` (no `%lo`) is a dead `lui` whose rD is never read — the residual case
|
|
667
|
+
// (an unconsumed `%hi` reg read via a `readVar` bypass) does not occur in compiler output.
|
|
668
|
+
if (s.startsWith('%')) {
|
|
669
|
+
const hi = parseReloc('hi', s);
|
|
670
|
+
if (!hi) {
|
|
671
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': unsupported relocation immediate '${s}'`);
|
|
672
|
+
}
|
|
673
|
+
if (!isZero(d)) {
|
|
674
|
+
hiReloc.set(d, { sym: hi.sym, addend: hi.addend });
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
530
678
|
write(d, constVal((parseImm(s) << 16) >> 0));
|
|
531
679
|
break;
|
|
532
680
|
case 'addiu':
|
|
@@ -537,6 +685,24 @@ export function lift(
|
|
|
537
685
|
if (isStackPtr(d)) {
|
|
538
686
|
break;
|
|
539
687
|
}
|
|
688
|
+
// `addiu rD, rHi, %lo(SYM)` completes a global's address materialised by a `lui %hi(SYM)`:
|
|
689
|
+
// rD = &SYM (+ addend for a byte offset into the global). Emits the shared `gaddr` op.
|
|
690
|
+
if (t.startsWith('%')) {
|
|
691
|
+
const lo = parseReloc('lo', t);
|
|
692
|
+
const hr = lo ? hiReloc.get(s) : undefined;
|
|
693
|
+
if (!lo || !hr || hr.sym !== lo.sym || hr.addend !== lo.addend) {
|
|
694
|
+
throw new FrontendUnsupportedError(
|
|
695
|
+
`cannot lift '${name}': %lo relocation '${t}' with no matching in-scope %hi — split/cross-block hi/lo not modelled`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
// `&SYM` (addend 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
|
|
699
|
+
// folded byte-correctly by memAccess when this address is a load/store base; if it
|
|
700
|
+
// instead ESCAPES as a value, `assertDerefsTyped` declines it (the byte offset would
|
|
701
|
+
// element-scale in C) — see the interior-global-pointer guard there.
|
|
702
|
+
const g = emitGaddr(lo.sym);
|
|
703
|
+
lo.addend !== 0 ? emitBin('add', d, g, constVal(lo.addend)) : write(d, g);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
540
706
|
if (isZero(s)) {
|
|
541
707
|
write(d, constVal(parseImm(t)));
|
|
542
708
|
break;
|
|
@@ -706,6 +872,16 @@ export function lift(
|
|
|
706
872
|
// drop its destination register — emit an honest `opaque`: dead ⇒ it vanishes; live ⇒
|
|
707
873
|
// assertResolved fails LOUD (see frontend/opaque.ts for the policy).
|
|
708
874
|
const emitOpaqueDest = (ins: Instr) => {
|
|
875
|
+
// A `%hi`/`%lo` operand on an instruction NOT modelled as a global consumer — an FP load/store
|
|
876
|
+
// (`lwc1`/`ldc1`), or any unmodelled op — reaches here (the modelled consumers handle their own
|
|
877
|
+
// `%hi`/`%lo` and return before the default case). Dropping it to an opaque would silently
|
|
878
|
+
// delete the global access (its base is not a bare register the opaque srcReg scan can see), so
|
|
879
|
+
// decline LOUD rather than lose it.
|
|
880
|
+
if (ins.ops.some((o) => o.startsWith('%'))) {
|
|
881
|
+
throw new FrontendUnsupportedError(
|
|
882
|
+
`cannot lift '${name}': unmodelled instruction '${ins.mnemonic}' with a %hi/%lo global operand — not modelled`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
709
885
|
// storeClass: unmodelled MIPS stores — incl. the unaligned pair swl/swr and the FPU stores,
|
|
710
886
|
// whose FIRST token is a register (a SOURCE, not a dest) that would otherwise fabricate an
|
|
711
887
|
// opaque write to it while dropping the real memory write.
|
|
@@ -730,7 +906,30 @@ export function lift(
|
|
|
730
906
|
};
|
|
731
907
|
const emitUn = kit.un;
|
|
732
908
|
const emitShImm = (opc: Opcode, d: string, x: Value, sa: string) => kit.shImm(opc, d, x, parseImm(sa));
|
|
909
|
+
// Resolve a `%lo(SYM + N)(rHi)` memory operand to a global address: validate it pairs with an
|
|
910
|
+
// in-scope `%hi`, emit the `gaddr`, and return it as the base with the addend as the access
|
|
911
|
+
// offset. A non-`%lo` operand returns null (the caller falls through to the normal off(base)).
|
|
912
|
+
const globalBase = (mem: string): { base: Value; off: number } | null => {
|
|
913
|
+
if (!mem.startsWith('%')) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
const lo = parseReloc('lo', mem);
|
|
917
|
+
const hr = lo && lo.base ? hiReloc.get(lo.base) : undefined;
|
|
918
|
+
if (!lo || !lo.base || !hr || hr.sym !== lo.sym || hr.addend !== lo.addend) {
|
|
919
|
+
throw new FrontendUnsupportedError(
|
|
920
|
+
`cannot lift '${name}': %lo access '${mem}' with no matching in-scope %hi — split/cross-block hi/lo not modelled`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return { base: emitGaddr(lo.sym), off: lo.addend };
|
|
924
|
+
};
|
|
733
925
|
const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
|
|
926
|
+
const g = globalBase(mem);
|
|
927
|
+
if (g) {
|
|
928
|
+
const res = mkValue(T.unk(32));
|
|
929
|
+
ops.push(mkOp('load', { operands: [g.base], results: [res], attrs: { off: g.off, width, signed } }));
|
|
930
|
+
write(d, res);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
734
933
|
const { off, base } = parseMem(mem);
|
|
735
934
|
// A word reload from a stack slot is transparent to dataflow — the SAME value spilled — so
|
|
736
935
|
// route it through the slot SSA variable, not a `load` through `sp` (which would make `sp` a
|
|
@@ -756,6 +955,11 @@ export function lift(
|
|
|
756
955
|
write(d, res);
|
|
757
956
|
};
|
|
758
957
|
const emitStore = (srcReg: string, mem: string, width: number) => {
|
|
958
|
+
const g = globalBase(mem);
|
|
959
|
+
if (g) {
|
|
960
|
+
ops.push(mkOp('store', { operands: [g.base, read(srcReg)], attrs: { off: g.off, width } }));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
759
963
|
const { off, base } = parseMem(mem);
|
|
760
964
|
// A word spill to a stack slot (an argument home slot `sw a0,0(sp)`, or a local): record the
|
|
761
965
|
// slot's value in SSA, do NOT emit a `store` through `sp`. A never-reloaded spill (the ABI
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// asmlift — the Splat-dialect MIPS reader. Splat (the N64 disassembler that pmret/decomp.me-style
|
|
2
|
+
// projects run) emits a GNU-as flavour that neither `objdump -d` nor a compiler produce, so the
|
|
3
|
+
// shared objdump scaffolding (frontend/disasm.ts) reads nothing from it. This module normalises
|
|
4
|
+
// that dialect into the SAME `DisasmInstr[]` the objdump parser yields, so the whole MIPS frontend
|
|
5
|
+
// (delay slots, blocks, SSA, recovery) runs downstream unchanged — mirroring how the Thumb frontend
|
|
6
|
+
// grew a second dialect for pret/luvdis splits.
|
|
7
|
+
//
|
|
8
|
+
// What the dialect adds over objdump:
|
|
9
|
+
// • `glabel NAME` / `endlabel NAME` function markers (objdump uses `ADDR <sym>:` headers);
|
|
10
|
+
// • a `/* ROM VRAM BYTES */` comment prefix on every instruction (the addr lives INSIDE it, so
|
|
11
|
+
// disasm.ts's `ADDR:` line anchor never matches) — the VRAM word is the instruction address;
|
|
12
|
+
// • `$`-prefixed registers (`$v0`, `$sp`) — stripped to the bare names the frontend's guards expect;
|
|
13
|
+
// • `.L<vram>_<rom>` local labels as branch/jump TARGETS (objdump prints a resolved address) —
|
|
14
|
+
// resolved here to the target instruction's address;
|
|
15
|
+
// • constant immediate EXPRESSIONS (`(0x660104 >> 16)`, `(x & 0xFFFF)`) — the assembler's hi/lo
|
|
16
|
+
// split of a 32-bit literal, evaluated here to the plain number the decode switch parses.
|
|
17
|
+
//
|
|
18
|
+
// `%hi`/`%lo` operands (a global's address) are preserved verbatim so the MIPS frontend can fold
|
|
19
|
+
// them into a `gaddr` (frontend/mips.ts). The other GOT/PIC relocations (`%gp_rel`, `%got`, …) are
|
|
20
|
+
// declined LOUD — small-data / position-independent access is not modelled. Preserving rather than
|
|
21
|
+
// blindly evaluating is what keeps `parseImm('%hi(SYM)')` from silently becoming a NaN immediate.
|
|
22
|
+
import type { DisasmInstr } from './disasm';
|
|
23
|
+
import { FrontendUnsupportedError } from './errors';
|
|
24
|
+
|
|
25
|
+
// One instruction line: `/* ROM VRAM BYTES */ MNEMONIC OPS`. Group 1 is the VRAM address word.
|
|
26
|
+
const INSN_LINE = /^\/\*\s*[0-9A-Fa-f]+\s+([0-9A-Fa-f]+)\s+[0-9A-Fa-f]+\s*\*\/\s*(\S+)\s*(.*)$/;
|
|
27
|
+
// A Splat instruction-comment prefix anywhere in the text — the load-bearing format signal.
|
|
28
|
+
const INSN_SIGNAL = /\/\*\s*[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s*\*\//;
|
|
29
|
+
// A local-label DEFINITION on its own line (`.L800011C0_1DC0:`); the colon is required.
|
|
30
|
+
const LABEL_DEF = /^(\.[\w.$]+):$/;
|
|
31
|
+
// A GOT/PIC relocation operand this reader does not support (small-data / position-independent
|
|
32
|
+
// access) — declined loud. `%hi`/`%lo` are NOT here: they name a global's address and are preserved
|
|
33
|
+
// verbatim for the MIPS frontend to fold into a `gaddr` (see normalizeOperand / frontend/mips.ts).
|
|
34
|
+
const RELOC_OP = /%(gp_rel|gprel|got|call16|call_hi|call_lo|higher|highest|neg|tprel|dtprel)\b/i;
|
|
35
|
+
// Data directives whose bytes could encode an effect: skipping one inside a function slice would
|
|
36
|
+
// silently delete it, so they decline (mirrors the Thumb frontend's in-code-data guard).
|
|
37
|
+
const DATA_DIRECTIVE =
|
|
38
|
+
/^\.(byte|half|hword|short|2byte|word|4byte|long|dword|8byte|quad|float|double|ascii|asciz|string|incbin|space|skip|fill|zero)\b/i;
|
|
39
|
+
|
|
40
|
+
/** Does this text look like Splat-dialect MIPS? Both signals (`glabel` markers and the
|
|
41
|
+
* three-word instruction-comment prefix) are unique to Splat — objdump and compiler `.s` carry
|
|
42
|
+
* neither — so a positive match is unambiguous. */
|
|
43
|
+
export function isSplatMips(asm: string): boolean {
|
|
44
|
+
return /^\s*glabel\s+\S+/m.test(asm) || INSN_SIGNAL.test(asm);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse Splat-dialect text into one function's `DisasmInstr[]`. When `glabel` markers are present
|
|
48
|
+
* the text is sliced to exactly `name` (an absent symbol declines LOUD — emitting some other
|
|
49
|
+
* function's body under the requested name is the silent miscompile the cardinal rule forbids);
|
|
50
|
+
* a marker-less fragment is parsed whole. Branch targets are resolved against the local-label
|
|
51
|
+
* map, so an unresolved `.L` target declines here rather than crashing deep in the frontend. */
|
|
52
|
+
export function parseSplatMips(asm: string, name: string): DisasmInstr[] {
|
|
53
|
+
const lines = asm.split('\n');
|
|
54
|
+
|
|
55
|
+
// Slice to the requested function: `glabel NAME` … its `endlabel`/the next `glabel`/EOF.
|
|
56
|
+
const glabels: { line: number; sym: string }[] = [];
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const m = lines[i].match(/^\s*glabel\s+(\S+)/);
|
|
59
|
+
if (m) {
|
|
60
|
+
glabels.push({ line: i, sym: m[1] });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let slice = lines;
|
|
64
|
+
if (glabels.length > 0) {
|
|
65
|
+
const at = glabels.findIndex((g) => g.sym === name);
|
|
66
|
+
if (at === -1) {
|
|
67
|
+
throw new FrontendUnsupportedError(
|
|
68
|
+
`symbol '${name}' not found in the Splat disassembly (functions present: ${glabels.map((g) => g.sym).join(', ')})`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let end = lines.length;
|
|
72
|
+
for (let i = glabels[at].line + 1; i < lines.length; i++) {
|
|
73
|
+
if (/^\s*(endlabel|glabel)\b/.test(lines[i])) {
|
|
74
|
+
end = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
slice = lines.slice(glabels[at].line, end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Flatten to instructions, assigning any pending label(s) to the NEXT instruction's address.
|
|
82
|
+
const instrs: DisasmInstr[] = [];
|
|
83
|
+
const labelAddr = new Map<string, number>();
|
|
84
|
+
let pending: string[] = [];
|
|
85
|
+
for (const raw of slice) {
|
|
86
|
+
const line = raw.trim();
|
|
87
|
+
if (!line || line.startsWith('#')) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (/^(glabel|endlabel|dlabel|jlabel)\b/.test(line) || /^nonmatching\b/.test(line)) {
|
|
91
|
+
continue; // function/data markers and the objdiff scratch header
|
|
92
|
+
}
|
|
93
|
+
const labelDef = line.match(LABEL_DEF);
|
|
94
|
+
if (labelDef) {
|
|
95
|
+
pending.push(labelDef[1]);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (line.startsWith('.')) {
|
|
99
|
+
// A data directive in the code stream could hide an instruction/effect — decline; other
|
|
100
|
+
// bookkeeping directives (`.set`, `.align`, `.section`…) are transparent and skipped.
|
|
101
|
+
if (DATA_DIRECTIVE.test(line)) {
|
|
102
|
+
throw new FrontendUnsupportedError(
|
|
103
|
+
`cannot lift '${name}': data directive '${line}' in the code stream — skipping it would silently delete its effect`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const m = line.match(INSN_LINE);
|
|
109
|
+
if (!m) {
|
|
110
|
+
throw new FrontendUnsupportedError(
|
|
111
|
+
`cannot lift '${name}': unrecognised line in the Splat disassembly: '${line}'`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const addr = parseInt(m[1], 16);
|
|
115
|
+
const mnemonic = m[2];
|
|
116
|
+
// A data directive carrying an instruction-comment prefix (`/* … */ .word …`) would otherwise
|
|
117
|
+
// be decoded as a mnemonic and silently become an opaque — decline it like the bare form.
|
|
118
|
+
if (DATA_DIRECTIVE.test(mnemonic)) {
|
|
119
|
+
throw new FrontendUnsupportedError(
|
|
120
|
+
`cannot lift '${name}': data directive '${mnemonic}' in the code stream — skipping it would silently delete its effect`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const ops = m[3].trim() ? splitOperands(m[3].trim()).map((o) => normalizeOperand(name, o)) : [];
|
|
124
|
+
// addi/addiu SIGN-EXTEND their 16-bit immediate; Splat may spell the low half of a materialised
|
|
125
|
+
// constant as an unsigned mask (`(0x8000ABCD & 0xFFFF)` = 0xABCD), so re-sign it here to match
|
|
126
|
+
// the hardware — and the objdump path, which prints the already-signed value. Zero-extending ops
|
|
127
|
+
// (ori/andi/xori) and lui keep the unsigned value, so they are deliberately excluded.
|
|
128
|
+
if ((mnemonic === 'addiu' || mnemonic === 'addi') && ops.length === 3 && /^-?(0x[0-9a-fA-F]+|\d+)$/.test(ops[2])) {
|
|
129
|
+
ops[2] = String(signExtend16(ops[2]));
|
|
130
|
+
}
|
|
131
|
+
for (const l of pending) {
|
|
132
|
+
labelAddr.set(l, addr);
|
|
133
|
+
}
|
|
134
|
+
pending = [];
|
|
135
|
+
instrs.push({ addr, mnemonic, ops });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Resolve every branch/jump's target label to an address. A target that is not a local label of
|
|
139
|
+
// this function — an unresolvable `.L`, or a bare symbol (`j func` tail call) — declines LOUD
|
|
140
|
+
// rather than leaving `target` undefined for the frontend to crash on (`succ(undefined)`).
|
|
141
|
+
for (const ins of instrs) {
|
|
142
|
+
if (!isBranchMnemonic(ins.mnemonic)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const label = ins.ops[ins.ops.length - 1];
|
|
146
|
+
const t = label !== undefined ? labelAddr.get(label) : undefined;
|
|
147
|
+
if (t === undefined) {
|
|
148
|
+
throw new FrontendUnsupportedError(
|
|
149
|
+
`cannot lift '${name}': branch/jump target '${label ?? ''}' is not a local label in this function ` +
|
|
150
|
+
`(tail call / cross-function branch not modelled)`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
ins.target = t;
|
|
154
|
+
}
|
|
155
|
+
return instrs;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A control transfer whose last operand is a code-label target: `b`, `j`, and the conditional
|
|
159
|
+
// branches (`beq`/`bnez`/`bc1f`…). NOT `jal`/`jalr` (calls) or `jr` (register) — the MIPS frontend
|
|
160
|
+
// owns those declines; `break` is a trap, not a branch.
|
|
161
|
+
const isBranchMnemonic = (mn: string): boolean => mn === 'j' || (mn[0] === 'b' && mn !== 'break');
|
|
162
|
+
|
|
163
|
+
// Re-sign a raw 16-bit immediate: a value with bit 15 set becomes negative (two's complement),
|
|
164
|
+
// matching how addi/addiu sign-extend the field. A value already ≤ 0x7FFF is unchanged.
|
|
165
|
+
function signExtend16(s: string): number {
|
|
166
|
+
const v = parseInt(s, /^-?0x/i.test(s) ? 16 : 10) & 0xffff;
|
|
167
|
+
return v & 0x8000 ? v - 0x10000 : v;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Split an operand list on top-level commas (commas inside `(...)` — a memory operand or a
|
|
171
|
+
// constant expression — do not separate operands).
|
|
172
|
+
function splitOperands(s: string): string[] {
|
|
173
|
+
const out: string[] = [];
|
|
174
|
+
let depth = 0;
|
|
175
|
+
let cur = '';
|
|
176
|
+
for (const ch of s) {
|
|
177
|
+
if (ch === '(') {
|
|
178
|
+
depth++;
|
|
179
|
+
} else if (ch === ')') {
|
|
180
|
+
depth--;
|
|
181
|
+
}
|
|
182
|
+
if (ch === ',' && depth === 0) {
|
|
183
|
+
out.push(cur.trim());
|
|
184
|
+
cur = '';
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
cur += ch;
|
|
188
|
+
}
|
|
189
|
+
if (cur.trim()) {
|
|
190
|
+
out.push(cur.trim());
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rewrite one Splat operand into the canonical objdump spelling the frontend consumes: strip the
|
|
196
|
+
// `$` register sigil, fold a memory operand's displacement expression, evaluate a bare constant
|
|
197
|
+
// expression, preserve a `%hi`/`%lo` global reference, and decline an unsupported PIC relocation.
|
|
198
|
+
function normalizeOperand(name: string, op: string): string {
|
|
199
|
+
// `%hi(SYM)` / `%lo(SYM + N)` / `%lo(SYM)(base)` — a global's address. Preserved verbatim (with a
|
|
200
|
+
// de-sigiled base) for the MIPS frontend to fold into a `gaddr`; NOT declined like the PIC relocs.
|
|
201
|
+
const hilo = op.match(/^(%(?:hi|lo)\([^)]*\))(?:\((\$?[A-Za-z]\w*)\))?$/);
|
|
202
|
+
if (hilo) {
|
|
203
|
+
return hilo[2] ? `${hilo[1]}(${hilo[2].replace(/^\$/, '')})` : hilo[1];
|
|
204
|
+
}
|
|
205
|
+
if (RELOC_OP.test(op)) {
|
|
206
|
+
throw new FrontendUnsupportedError(
|
|
207
|
+
`cannot lift '${name}': relocation operand '${op}' (small-data / PIC data access) — not modelled`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
// Memory operand `DISP(base)` — base is a register (letter-first), DISP a constant/expression.
|
|
211
|
+
const mem = op.match(/^(.*)\((\$?[A-Za-z]\w*)\)$/);
|
|
212
|
+
if (mem) {
|
|
213
|
+
const disp = mem[1].trim();
|
|
214
|
+
const off = disp === '' ? '0' : String(evalConst(name, disp));
|
|
215
|
+
return `${off}(${mem[2].replace(/^\$/, '')})`;
|
|
216
|
+
}
|
|
217
|
+
// A bare constant expression (`(0x660104 >> 16)`) — the assembler's hi/lo literal split.
|
|
218
|
+
if (op.startsWith('(')) {
|
|
219
|
+
return String(evalConst(name, op));
|
|
220
|
+
}
|
|
221
|
+
return op.replace(/^\$/, '');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Evaluate a constant integer expression (the assembler's hi/lo split: hex/dec literals with
|
|
225
|
+
// `+ - * << >> & | ^ ~` and parentheses). Precedence-climbing; C-like precedence. A shift `>>` is
|
|
226
|
+
// LOGICAL — Splat's operands are unsigned 32-bit constants. Anything unparsable declines LOUD
|
|
227
|
+
// rather than silently yielding NaN.
|
|
228
|
+
function evalConst(name: string, expr: string): number {
|
|
229
|
+
const toks = expr.match(/0x[0-9a-fA-F]+|\d+|<<|>>|[-+*&|^()~]/g);
|
|
230
|
+
if (!toks) {
|
|
231
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': unparsable constant expression '${expr}'`);
|
|
232
|
+
}
|
|
233
|
+
const prec: Record<string, number> = { '|': 1, '^': 2, '&': 3, '<<': 4, '>>': 4, '+': 5, '-': 5, '*': 6 };
|
|
234
|
+
let p = 0;
|
|
235
|
+
const fail = () => {
|
|
236
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': unparsable constant expression '${expr}'`);
|
|
237
|
+
};
|
|
238
|
+
const primary = (): number => {
|
|
239
|
+
const t = toks[p++];
|
|
240
|
+
if (t === undefined) {
|
|
241
|
+
return fail();
|
|
242
|
+
}
|
|
243
|
+
if (t === '(') {
|
|
244
|
+
const v = expr2(0);
|
|
245
|
+
if (toks[p++] !== ')') {
|
|
246
|
+
return fail();
|
|
247
|
+
}
|
|
248
|
+
return v;
|
|
249
|
+
}
|
|
250
|
+
if (t === '-') {
|
|
251
|
+
return -unary();
|
|
252
|
+
}
|
|
253
|
+
if (t === '~') {
|
|
254
|
+
return ~unary();
|
|
255
|
+
}
|
|
256
|
+
if (/^(0x[0-9a-fA-F]+|\d+)$/.test(t)) {
|
|
257
|
+
return t.toLowerCase().startsWith('0x') ? parseInt(t, 16) : parseInt(t, 10);
|
|
258
|
+
}
|
|
259
|
+
return fail();
|
|
260
|
+
};
|
|
261
|
+
const unary = (): number => primary();
|
|
262
|
+
const expr2 = (minPrec: number): number => {
|
|
263
|
+
let left = unary();
|
|
264
|
+
for (;;) {
|
|
265
|
+
const op = toks[p];
|
|
266
|
+
if (op === undefined || prec[op] === undefined || prec[op] < minPrec) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
p++;
|
|
270
|
+
const right = expr2(prec[op] + 1);
|
|
271
|
+
switch (op) {
|
|
272
|
+
case '+':
|
|
273
|
+
left = (left + right) | 0;
|
|
274
|
+
break;
|
|
275
|
+
case '-':
|
|
276
|
+
left = (left - right) | 0;
|
|
277
|
+
break;
|
|
278
|
+
case '*':
|
|
279
|
+
left = Math.imul(left, right);
|
|
280
|
+
break;
|
|
281
|
+
case '<<':
|
|
282
|
+
left = (left << right) >>> 0;
|
|
283
|
+
break;
|
|
284
|
+
case '>>':
|
|
285
|
+
left = left >>> right;
|
|
286
|
+
break;
|
|
287
|
+
case '&':
|
|
288
|
+
left = left & right;
|
|
289
|
+
break;
|
|
290
|
+
case '|':
|
|
291
|
+
left = left | right;
|
|
292
|
+
break;
|
|
293
|
+
case '^':
|
|
294
|
+
left = left ^ right;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return left;
|
|
299
|
+
};
|
|
300
|
+
const v = expr2(0);
|
|
301
|
+
if (p !== toks.length) {
|
|
302
|
+
return fail();
|
|
303
|
+
}
|
|
304
|
+
return v;
|
|
305
|
+
}
|