@asmlift/core 0.7.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +45 -19
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
package/src/frontend/disasm.ts
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
// parses GNU-as text, not objdump, so it does not route through here.
|
|
3
3
|
import { FrontendUnsupportedError } from './errors';
|
|
4
4
|
|
|
5
|
+
/** The objdump function-header line, exported through the two readers below so that nothing can
|
|
6
|
+
* disagree about where a function starts. GREEDY to the LAST `>`: a C++ template symbol contains
|
|
7
|
+
* `>` of its own (`invoke__Q23zen20NumberPicCallBack<i>FP7P2DPane`), and a header a pattern cannot
|
|
8
|
+
* see is worse than one it misreads — the PRECEDING function's slice runs on through it, and the
|
|
9
|
+
* prologue split swallows it. */
|
|
10
|
+
const HEADER_LINE = /^([0-9a-f]+)\s+<(.+)>:\s*$/i;
|
|
11
|
+
const HEADER_SEARCH = new RegExp(HEADER_LINE.source, 'im');
|
|
12
|
+
|
|
13
|
+
/** Character offset of the FIRST function header in an objdump listing, or -1 — everything before
|
|
14
|
+
* it is the listing's own prologue (`target.o: file format …`, section headings). */
|
|
15
|
+
export function firstFunctionHeader(disasm: string): number {
|
|
16
|
+
return disasm.search(HEADER_SEARCH);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The ADDRESS the first function header gives, or undefined for headerless input (a raw
|
|
20
|
+
* instruction fragment, which says where it starts only by its first line). It is the one thing
|
|
21
|
+
* that tells a word with nothing before it apart from a word the listing does not spell, and
|
|
22
|
+
* those two have opposite answers wherever what precedes a word decides how it may be read. */
|
|
23
|
+
export function symbolStart(disasm: string): number | undefined {
|
|
24
|
+
const m = disasm.match(HEADER_SEARCH);
|
|
25
|
+
return m ? parseInt(m[1], 16) : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
/** Slice a multi-symbol objdump listing down to ONE function's lines. objdump marks each
|
|
6
29
|
* function with an `ADDR <sym>:` header line; when headers are present the input is sliced to
|
|
7
30
|
* exactly the requested symbol — and an ABSENT symbol declines LOUD, because emitting some
|
|
@@ -11,9 +34,9 @@ export function sliceSymbol(disasm: string, symbol: string): string {
|
|
|
11
34
|
const lines = disasm.split('\n');
|
|
12
35
|
const headers: { line: number; sym: string }[] = [];
|
|
13
36
|
for (let i = 0; i < lines.length; i++) {
|
|
14
|
-
const m = lines[i].match(
|
|
37
|
+
const m = lines[i].match(HEADER_LINE);
|
|
15
38
|
if (m) {
|
|
16
|
-
headers.push({ line: i, sym: m[
|
|
39
|
+
headers.push({ line: i, sym: m[2] });
|
|
17
40
|
}
|
|
18
41
|
}
|
|
19
42
|
if (headers.length === 0) {
|
|
@@ -29,38 +52,136 @@ export function sliceSymbol(disasm: string, symbol: string): string {
|
|
|
29
52
|
return lines.slice(headers[at].line, end).join('\n');
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/** A relocation objdump printed (with `-r`) under the instruction whose operand field it fills.
|
|
56
|
+
* All three fields carry meaning the rest of the listing does not:
|
|
57
|
+
* the TYPE says WHICH field — an `@ha` immediate half (`R_PPC_ADDR16_HA`), an `@l` half
|
|
58
|
+
* (`R_PPC_ADDR16_LO`), a small-data memory base (`R_PPC_EMB_SDA21`), a call target
|
|
59
|
+
* (`R_PPC_REL24`) — and the mnemonic cannot stand in for it; the ADDEND is part of the address,
|
|
60
|
+
* so `SYM` and `SYM+0x4` are different words; the SYMBOL is the name. */
|
|
61
|
+
export interface DisasmReloc {
|
|
62
|
+
type: string;
|
|
63
|
+
sym: string;
|
|
64
|
+
addend: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
/** One disassembled instruction. `target` is a decoded branch-target address (objdump prints the
|
|
33
|
-
* target as `10 <sym+0x10>` in the last operand); `
|
|
34
|
-
* (PPC `-r` output), absent otherwise. */
|
|
68
|
+
* target as `10 <sym+0x10>` in the last operand); `reloc` is the relocation objdump attached to
|
|
69
|
+
* this instruction (PPC `-r` output), absent otherwise. */
|
|
35
70
|
export interface DisasmInstr {
|
|
36
71
|
addr: number;
|
|
37
72
|
mnemonic: string;
|
|
38
73
|
ops: string[];
|
|
39
74
|
target?: number;
|
|
40
|
-
|
|
75
|
+
reloc?: DisasmReloc;
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
export interface DisasmOptions {
|
|
44
79
|
/** Attach relocation lines (`ADDR: R_* <sym>[+addend]`) to the PRECEDING instruction — the
|
|
45
|
-
* callee symbol for a `bl` whose encoded offset is a 0 placeholder
|
|
46
|
-
*
|
|
80
|
+
* callee symbol for a `bl` whose encoded offset is a 0 placeholder, the named global behind a
|
|
81
|
+
* printed-as-0 immediate or memory base (PPC `-r` output). Tested BEFORE the instruction regex,
|
|
82
|
+
* which would otherwise mis-read `R_PPC_…` as a mnemonic. */
|
|
47
83
|
relocs?: boolean;
|
|
48
84
|
/** Strip branch-prediction hint suffixes glued onto the mnemonic (`blt-`, `bge+`, `bgelr-`).
|
|
49
85
|
* The suffix is a prediction hint, not a different instruction — without stripping, the
|
|
50
86
|
* mnemonic misses the cond tables and the branch is silently dropped. */
|
|
51
87
|
hintSuffixes?: boolean;
|
|
88
|
+
/** What an all-zero word decodes to on this architecture (MIPS: `nop` — `sll zero,zero,0`).
|
|
89
|
+
* objdump prints a run of zero words as a bare `...` instead of the words themselves, and the
|
|
90
|
+
* words it stands for are real program text: a `mflo` hazard pad sits between a multiply and
|
|
91
|
+
* the branch that reads it. Without a decoding they cannot be recovered, so the listing is
|
|
92
|
+
* refused rather than parsed one word short (a zero word is not an instruction at all on
|
|
93
|
+
* PowerPC, whose frontend therefore supplies none). */
|
|
94
|
+
zeroWord?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Fixed instruction width, in bytes, of every ISA that reaches this reader (MIPS, PowerPC), so an
|
|
98
|
+
* address gap is a whole number of words. Thumb is variable-width and has a reader of its own. */
|
|
99
|
+
const WORD = 4;
|
|
100
|
+
|
|
101
|
+
/** objdump's elision of a run of zero words: a bare `...` on its own line (`-d` prints it only
|
|
102
|
+
* for zeroes; `-z` prints the words instead). */
|
|
103
|
+
const ELISION_LINE = /^\s*\.\.\.\s*$/;
|
|
104
|
+
|
|
105
|
+
/** An instruction line's address column, whatever the mnemonic column holds. A line that carries
|
|
106
|
+
* an address carries a WORD of the function, so one this reader cannot decode must not be
|
|
107
|
+
* skipped: the words after it would keep their addresses while the list lost one, and every
|
|
108
|
+
* reader of that list — the delay slot at `branch + 4` above all — would be answering about a
|
|
109
|
+
* word that is not there. */
|
|
110
|
+
const ADDRESSED_LINE = /^\s*[0-9a-f]+:\s/i;
|
|
111
|
+
|
|
112
|
+
/** Replace an elision with the zero words it stands for: from the word after the last instruction
|
|
113
|
+
* parsed up to (not including) the address of the line that ends the run. Every way the run's
|
|
114
|
+
* extent is unknowable refuses by name — a run of unknown length is exactly the silent hole this
|
|
115
|
+
* exists to remove. A run with NO line after it is not a hole: it is the padding past the last
|
|
116
|
+
* word objdump printed, it bounds nothing, and nothing is invented for it. */
|
|
117
|
+
function expandElision(out: DisasmInstr[], next: number, zeroWord: string | undefined): void {
|
|
118
|
+
const prev = out[out.length - 1];
|
|
119
|
+
const hex = (a: number) => `0x${a.toString(16)}`;
|
|
120
|
+
if (!prev) {
|
|
121
|
+
throw new FrontendUnsupportedError(
|
|
122
|
+
`objdump elided a run of zero words ('...') before the first instruction of the listing, ` +
|
|
123
|
+
`ending at ${hex(next)}: where the run begins is unknown`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const from = prev.addr + WORD;
|
|
127
|
+
if (zeroWord === undefined) {
|
|
128
|
+
throw new FrontendUnsupportedError(
|
|
129
|
+
`objdump elided a run of zero words ('...') at ${hex(from)}, but a zero word is not a ` +
|
|
130
|
+
`decodable instruction on this architecture`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (next <= from || (next - from) % WORD !== 0) {
|
|
134
|
+
throw new FrontendUnsupportedError(
|
|
135
|
+
`objdump elided a run of zero words ('...') between ${hex(from)} and ${hex(next)}, which is ` +
|
|
136
|
+
`not a whole number of instruction words`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
for (let addr = from; addr < next; addr += WORD) {
|
|
140
|
+
out.push({ addr, mnemonic: zeroWord, ops: [] });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Attach a parsed relocation to the instruction it belongs to. The binding is POSITIONAL —
|
|
145
|
+
* objdump prints a relocation directly beneath its instruction — and both ways that assumption
|
|
146
|
+
* can break fail LOUD, because each one silently relocates the wrong operand: an offset outside
|
|
147
|
+
* the preceding instruction's four bytes means the listing is not the assumed shape (the offset
|
|
148
|
+
* points at the relocated FIELD, so a 16-bit immediate's offset is the instruction's address + 2),
|
|
149
|
+
* and a second relocation on one instruction would overwrite the first, leaving one symbol
|
|
150
|
+
* standing for two. */
|
|
151
|
+
function attachReloc(out: DisasmInstr[], offset: number, reloc: DisasmReloc): void {
|
|
152
|
+
const ins = out[out.length - 1];
|
|
153
|
+
if (!ins || offset < ins.addr || offset >= ins.addr + 4) {
|
|
154
|
+
throw new FrontendUnsupportedError(
|
|
155
|
+
`relocation '${reloc.type} ${reloc.sym}' at 0x${offset.toString(16)} does not fall inside ` +
|
|
156
|
+
(ins ? `the preceding instruction ('${ins.mnemonic}' at 0x${ins.addr.toString(16)})` : 'any instruction'),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (ins.reloc) {
|
|
160
|
+
throw new FrontendUnsupportedError(
|
|
161
|
+
`two relocations on one instruction ('${ins.mnemonic}' at 0x${ins.addr.toString(16)}): ` +
|
|
162
|
+
`'${ins.reloc.type} ${ins.reloc.sym}' and '${reloc.type} ${reloc.sym}'`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
ins.reloc = reloc;
|
|
52
166
|
}
|
|
53
167
|
|
|
54
168
|
/** Parse objdump `-d --no-show-raw-insn` output into a flat instruction list with addresses. */
|
|
55
169
|
export function parseDisasm(disasm: string, opts: DisasmOptions = {}): DisasmInstr[] {
|
|
56
170
|
const out: DisasmInstr[] = [];
|
|
171
|
+
let elided = false;
|
|
57
172
|
for (const raw of disasm.split('\n')) {
|
|
173
|
+
if (ELISION_LINE.test(raw)) {
|
|
174
|
+
elided = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
58
177
|
if (opts.relocs) {
|
|
59
|
-
const rel = raw.match(/^\s+[0-9a-f]
|
|
178
|
+
const rel = raw.match(/^\s+([0-9a-f]+):\s+(R_\w+)\s+([^\s+-]+)(?:\s*([+-])\s*(0x[0-9a-f]+|\d+))?\s*$/i);
|
|
60
179
|
if (rel) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
180
|
+
attachReloc(out, parseInt(rel[1], 16), {
|
|
181
|
+
type: rel[2],
|
|
182
|
+
sym: rel[3],
|
|
183
|
+
addend: rel[5] ? parseImm(rel[5]) * (rel[4] === '-' ? -1 : 1) : 0,
|
|
184
|
+
});
|
|
64
185
|
continue;
|
|
65
186
|
}
|
|
66
187
|
}
|
|
@@ -68,9 +189,18 @@ export function parseDisasm(disasm: string, opts: DisasmOptions = {}): DisasmIns
|
|
|
68
189
|
? raw.match(/^\s*([0-9a-f]+):\s+([a-z][a-z0-9._]*)([-+]?)\s*(.*?)\s*$/i)
|
|
69
190
|
: raw.match(/^\s*([0-9a-f]+):\s+([a-z][a-z0-9._]*)\s*(.*?)\s*$/i);
|
|
70
191
|
if (!m) {
|
|
192
|
+
if (ADDRESSED_LINE.test(raw)) {
|
|
193
|
+
throw new FrontendUnsupportedError(
|
|
194
|
+
`objdump line '${raw.trim()}' carries an address but no instruction this reader can decode`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
71
197
|
continue;
|
|
72
198
|
}
|
|
73
199
|
const addr = parseInt(m[1], 16);
|
|
200
|
+
if (elided) {
|
|
201
|
+
expandElision(out, addr, opts.zeroWord);
|
|
202
|
+
elided = false;
|
|
203
|
+
}
|
|
74
204
|
const mnemonic = m[2]; // hint suffix (group 3), when parsed, is dropped
|
|
75
205
|
const opsStr = opts.hintSuffixes ? m[4] : m[3];
|
|
76
206
|
const ops = opsStr
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// asmlift — the HIGH HALF of a relocated address, shared by the MIPS and PowerPC frontends.
|
|
2
|
+
//
|
|
3
|
+
// Both ISAs materialise a global's address in two instructions: a high-half producer (`lis
|
|
4
|
+
// rD,SYM@ha`, `lui rD,%hi(SYM)`) and a low-half consumer (`addi rD,rHi,SYM@l`, `lw rD,%lo(SYM)(rHi)`)
|
|
5
|
+
// that completes it. In a RELOCATABLE object neither printed immediate carries the address — it
|
|
6
|
+
// lives entirely in the two relocation records — so the high half is NOT A VALUE but a placeholder
|
|
7
|
+
// that means nothing until its matching low half completes it. Letting one reach the IR as an
|
|
8
|
+
// ordinary number hands the pipeline a plausible address that compiles and is simply wrong, so the
|
|
9
|
+
// invariant is absolute: a placeholder is either consumed by its low half or refuses.
|
|
10
|
+
//
|
|
11
|
+
// KEYED BY VALUE, NOT BY REGISTER, and the difference is the whole point. A register-keyed map
|
|
12
|
+
// answers "was a high half put in rX?", which is not the question; the question is "does the half
|
|
13
|
+
// reach THIS read?". Those differ whenever a redefinition on any path erases the entry, or a
|
|
14
|
+
// sibling path the reader never takes writes the register — and the register-keyed reading then
|
|
15
|
+
// falls through to whatever def reached before the producer. Keying by the SSA value makes the
|
|
16
|
+
// pairing a PROOF, answered by SSA rather than by a side map with an invalidation discipline every
|
|
17
|
+
// future writer must maintain.
|
|
18
|
+
//
|
|
19
|
+
// WHAT EACH ISA KEEPS FOR ITSELF is the ADDEND, and it is a relocation-format fact, not a
|
|
20
|
+
// preference. PowerPC objects are RELA: the addend rides on the relocation record. MIPS objects are
|
|
21
|
+
// REL: the record has no addend field and the value is split across the two instruction immediates
|
|
22
|
+
// (`(hi_imm << 16) + (s16)lo_imm`), so the MIPS fold computes the high half's contribution itself.
|
|
23
|
+
// Either way `addend` is "the part of the address this producer contributed", and the consumer adds
|
|
24
|
+
// its own.
|
|
25
|
+
import type { Block, Value } from '../ir/core';
|
|
26
|
+
|
|
27
|
+
/** A pending high half: what it names, what it contributed, and the instruction that produced it,
|
|
28
|
+
* so a refusal can point a reader at it. Whether it has been consumed is deliberately NOT here: it
|
|
29
|
+
* is this module's own bookkeeping, set by `pair`, so no frontend can fold a pair and forget to
|
|
30
|
+
* mark it — forgetting would refuse a well-formed function with nothing to catch it. */
|
|
31
|
+
export interface HighHalfInfo {
|
|
32
|
+
sym: string;
|
|
33
|
+
/** This producer's contribution to the address — RELA carries it, REL computes it. */
|
|
34
|
+
addend: number;
|
|
35
|
+
addr: number;
|
|
36
|
+
mnemonic: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How one ISA spells the two halves in a refusal, and how it fails loud. */
|
|
40
|
+
export interface HighHalfDialect {
|
|
41
|
+
/** The high-half marker as the ISA's asm spells it: `@ha` (PowerPC), `%hi` (MIPS). */
|
|
42
|
+
hi: string;
|
|
43
|
+
/** The indefinite article `hi` takes, so one shared sentence reads right in both ISAs' asm. */
|
|
44
|
+
hiArticle: string;
|
|
45
|
+
/** The low-half marker: `@l` (PowerPC), `%lo` (MIPS). */
|
|
46
|
+
lo: string;
|
|
47
|
+
/** The frontend's designed loud-failure signal (`PpcUnsupportedError`, `FrontendUnsupportedError`). */
|
|
48
|
+
fail(message: string): never;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface HighHalves {
|
|
52
|
+
/** Record the placeholder `v` as the high half `info`. The producer writes `v` as an ordinary SSA
|
|
53
|
+
* definition, which is what lets SSA answer the pairing question later. */
|
|
54
|
+
record(v: Value, info: HighHalfInfo): void;
|
|
55
|
+
/** THE PAIRING. `v` is what the low half's base register holds HERE; it must be the very
|
|
56
|
+
* placeholder a producer defined, and the two halves must name the same symbol (`alsoMatches`
|
|
57
|
+
* adds the per-ISA rest of the match — PowerPC's RELA addend). Anything else refuses, naming
|
|
58
|
+
* `reg` and what it really holds. The half is CONSUMED on the way out, which is why this is a
|
|
59
|
+
* call and not a public flag: the mark and the proof cannot come apart. */
|
|
60
|
+
pair(site: string, reg: string, v: Value, loSym: string, alsoMatches?: (hi: HighHalfInfo) => boolean): HighHalfInfo;
|
|
61
|
+
/** True when `v` is a placeholder — for a caller that must EXCLUDE one (a call's argument count
|
|
62
|
+
* must not treat a half parked in an argument register as an argument). */
|
|
63
|
+
has(v: Value): boolean;
|
|
64
|
+
/** Reading a register AS A VALUE. A register holding a high half is not one, so this refuses
|
|
65
|
+
* rather than handing back a plausible number standing for an address. */
|
|
66
|
+
guardRead(name: string, reg: string, v: Value): Value;
|
|
67
|
+
/** A high half no low half ever completed. Finishing the lift would SILENTLY DROP the address it
|
|
68
|
+
* was building — every low-half consumer a frontend does not model lands here, which is what
|
|
69
|
+
* keeps "not modelled" from turning into "not emitted". */
|
|
70
|
+
assertAllConsumed(name: string): void;
|
|
71
|
+
/** The LAST line of defence, because `guardRead` sees only the reads a frontend routes through
|
|
72
|
+
* it. SSA builds a block parameter's incoming arguments with reads of its own: at a merge of a
|
|
73
|
+
* path that holds a half and a path that does not, the read returns the PARAMETER — an ordinary
|
|
74
|
+
* value — while the placeholder is appended to the predecessor's successor arguments. So the
|
|
75
|
+
* finished function is checked once: a placeholder anywhere in the IR refuses. */
|
|
76
|
+
assertNoneEscaped(name: string, blocks: Block[]): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function makeHighHalves(d: HighHalfDialect): HighHalves {
|
|
80
|
+
const halves = new Map<Value, { info: HighHalfInfo; consumed: boolean }>();
|
|
81
|
+
return {
|
|
82
|
+
record: (v, info) => void halves.set(v, { info, consumed: false }),
|
|
83
|
+
has: (v) => halves.has(v),
|
|
84
|
+
pair(site, reg, v, loSym, alsoMatches) {
|
|
85
|
+
const entry = halves.get(v);
|
|
86
|
+
const hi = entry && entry.info.sym === loSym && (alsoMatches?.(entry.info) ?? true) ? entry.info : undefined;
|
|
87
|
+
if (!hi) {
|
|
88
|
+
d.fail(
|
|
89
|
+
`${site} carries the '${d.lo}' half of '${loSym}' but ${reg} ` +
|
|
90
|
+
(entry
|
|
91
|
+
? `holds the high half of '${entry.info.sym}'`
|
|
92
|
+
: `holds no high half here — a reused register, a missing '${d.hi}', or ${d.hiArticle} ` +
|
|
93
|
+
`'${d.hi}' that reaches this instruction only through a merge or a loop header, where ` +
|
|
94
|
+
`what the register holds is the block parameter standing for the join and not the half`) +
|
|
95
|
+
` — this frontend will not guess at the pair`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
entry!.consumed = true;
|
|
99
|
+
return hi;
|
|
100
|
+
},
|
|
101
|
+
guardRead(name, reg, v) {
|
|
102
|
+
const hi = halves.get(v)?.info;
|
|
103
|
+
if (hi) {
|
|
104
|
+
d.fail(
|
|
105
|
+
`cannot lift '${name}': ${reg} holds the high half of '${hi.sym}' (the '${hi.mnemonic}' at ` +
|
|
106
|
+
`0x${hi.addr.toString(16)}) and is read as a value — only its matching '${d.lo}' half may consume it`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return v;
|
|
110
|
+
},
|
|
111
|
+
assertAllConsumed(name) {
|
|
112
|
+
// Lowest address first, so a function with several dangling halves names the one a reader
|
|
113
|
+
// meets first in the listing rather than whichever the map happened to iterate to.
|
|
114
|
+
const dangling = [...halves.values()]
|
|
115
|
+
.filter((h) => !h.consumed)
|
|
116
|
+
.map((h) => h.info)
|
|
117
|
+
.sort((a, b) => a.addr - b.addr)[0];
|
|
118
|
+
if (dangling) {
|
|
119
|
+
d.fail(
|
|
120
|
+
`cannot lift '${name}': '${dangling.mnemonic}' at 0x${dangling.addr.toString(16)} carries the ` +
|
|
121
|
+
`'${d.hi}' half of '${dangling.sym}' and no modelled instruction consumes its '${d.lo}' half — ` +
|
|
122
|
+
`the address is never completed`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
assertNoneEscaped(name, blocks) {
|
|
127
|
+
const escaped = (v: Value): void => {
|
|
128
|
+
const hi = halves.get(v)?.info;
|
|
129
|
+
if (hi) {
|
|
130
|
+
d.fail(
|
|
131
|
+
`cannot lift '${name}': the high half of '${hi.sym}' (the '${hi.mnemonic}' at ` +
|
|
132
|
+
`0x${hi.addr.toString(16)}) reaches a merge with values that are not it — what the register ` +
|
|
133
|
+
`holds there is not a value this frontend can write down`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const b of blocks) {
|
|
138
|
+
b.params.forEach(escaped);
|
|
139
|
+
for (const op of b.ops) {
|
|
140
|
+
op.operands.forEach(escaped);
|
|
141
|
+
op.results.forEach(escaped);
|
|
142
|
+
for (const s of op.successors) {
|
|
143
|
+
s.args.forEach(escaped);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|