@asmlift/core 0.6.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/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- 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 +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
package/src/contracts.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
stmtLists,
|
|
17
17
|
walkExprs,
|
|
18
18
|
} from './l3/ast';
|
|
19
|
+
import { mentionedLocals } from './l3/mentions';
|
|
19
20
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
20
21
|
|
|
21
22
|
export class ContractError extends Error {
|
|
@@ -277,10 +278,58 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
/**
|
|
281
|
+
/** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
|
|
282
|
+
* produced.
|
|
283
|
+
*
|
|
284
|
+
* THE FAILURE THIS CATCHES is the mirror of `assertLocalsWritten` above, and the three contracts
|
|
285
|
+
* beside it do not see it. A pass that consumes a local — l3/unmerge.ts substituting a merge temp
|
|
286
|
+
* into the arms, l3/coalesce.ts folding two names into one, l3/inlinebase.ts deleting a
|
|
287
|
+
* const-address pointer — drops the name from `sfn.locals` on the strength of an in-pass count
|
|
288
|
+
* that it is no longer mentioned. If that count is ever wrong the result is not a loud variation
|
|
289
|
+
* error: it is a candidate handed to the compiler with an undeclared identifier. Normally that is
|
|
290
|
+
* a dropped candidate, but in the REAL tier the candidate is compiled inside the project's
|
|
291
|
+
* vendored translation unit, where an orphaned name that collides with a context symbol compiles
|
|
292
|
+
* and scores. Measured on the shape that inhabits it — `locals = [p]`, body `p = 0; v16 = 1;
|
|
293
|
+
* *p = v16;` — `assertResolved`, `assertDerefsTyped` and `assertLocalsWritten` all pass: the name
|
|
294
|
+
* is neither `?` nor `undefined`, it is well-typed, and the question the third one asks is the
|
|
295
|
+
* OPPOSITE one (read but never written).
|
|
296
|
+
*
|
|
297
|
+
* A DIFFERENTIAL, and that is what makes it safe to run on every respelled tree. "Every name the tree
|
|
298
|
+
* mentions is declared" is NOT the invariant and would refuse correct output everywhere:
|
|
299
|
+
* structure.ts spells a write to a scalar global as a bare `assign` whose name is declared in the
|
|
300
|
+
* project's headers and nowhere in the tree (`gBlendValue = v;` — 71 such occurrences across 22
|
|
301
|
+
* winning sources, per l3/unmerge.ts's own note), and `SFn.globals` is the symbol-map-shaped
|
|
302
|
+
* subset, not that population. So the check speaks only about names the tree ITSELF declared a
|
|
303
|
+
* moment ago and the pass then removed — a set with no legitimate inhabitant, because a pass that
|
|
304
|
+
* drops a declaration is asserting exactly this.
|
|
305
|
+
*
|
|
306
|
+
* `addr` counts, like everywhere else: `&v` names the object as surely as a read does. */
|
|
307
|
+
export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
|
|
308
|
+
const kept = new Set(after.locals.map((l) => l.name));
|
|
309
|
+
const dropped = new Set(before.locals.map((l) => l.name).filter((n) => !kept.has(n)));
|
|
310
|
+
if (!dropped.size) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// `l3/mentions.ts`'s walk, not a third copy of the node vocabulary — this is the LOUD BACKSTOP
|
|
314
|
+
// for the mistake that predicate guards, so it is the last place that should own its own.
|
|
315
|
+
// Locals only, deliberately: no L3 respell variation drops `SFn.params` (`pruneDeadParams` is L1 block
|
|
316
|
+
// params, ir/simplify.ts), so a params arm here would be a refusal with no inhabitant.
|
|
317
|
+
const found = mentionedLocals(after.body, dropped);
|
|
318
|
+
if (found.size) {
|
|
319
|
+
throw new ContractError(
|
|
320
|
+
`a respell variation deleted the declaration of ${[...found]
|
|
321
|
+
.sort()
|
|
322
|
+
.map((n) => `'${n}'`)
|
|
323
|
+
.join(', ')} in '${after.name}' ` +
|
|
324
|
+
`while the tree still names ${found.size > 1 ? 'them' : 'it'} — an undeclared identifier in the candidate`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
|
|
281
330
|
* that local's assignment has already run.
|
|
282
331
|
*
|
|
283
|
-
* THE failure a placing
|
|
332
|
+
* THE failure a placing variation can ship, and the only one the byte differ rewards: a base local whose
|
|
284
333
|
* assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
|
|
285
334
|
* (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
|
|
286
335
|
* reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
|
|
@@ -288,16 +337,16 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
288
337
|
*
|
|
289
338
|
* Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
|
|
290
339
|
* would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
|
|
291
|
-
* answer becomes a reported
|
|
340
|
+
* answer becomes a reported variation error instead of a scored spelling.
|
|
292
341
|
*
|
|
293
342
|
* IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
|
|
294
|
-
* check's call site:
|
|
343
|
+
* check's call site: respell variations that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
|
|
295
344
|
* l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
|
|
296
|
-
* produce the failure, so the check belongs on every
|
|
345
|
+
* produce the failure, so the check belongs on every respelled tree rather than on one variation's.
|
|
297
346
|
*
|
|
298
|
-
* Called ABSOLUTELY by the placing
|
|
347
|
+
* Called ABSOLUTELY by the placing variations that put an init inside a nested list, each over its own
|
|
299
348
|
* plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
|
|
300
|
-
* DIFFERENTIAL — so a placement no
|
|
349
|
+
* DIFFERENTIAL — so a placement no respelled tree ever satisfied is not judged, and a variation that
|
|
301
350
|
* mints nothing is not judged at all.
|
|
302
351
|
*
|
|
303
352
|
* A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
|
|
@@ -357,13 +406,13 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
|
|
|
357
406
|
}
|
|
358
407
|
|
|
359
408
|
/** The same guarantee across a re-spelling that MOVES statements over a placement another pass
|
|
360
|
-
* already made — `rank.ts`'s
|
|
361
|
-
* onto every
|
|
409
|
+
* already made — `rank.ts`'s stacked variations (`/initfirst`, `/pollguard`, `/pollread`), derived
|
|
410
|
+
* onto every respelled tree after the variation placed its defs, and the variation-on-variation compositions in
|
|
362
411
|
* the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
|
|
363
|
-
* `reindexWalks`) runs on a tree a placing
|
|
412
|
+
* `reindexWalks`) runs on a tree a placing variation built. `pollReads` folds a materialized re-read
|
|
364
413
|
* back into a loop condition, which is exactly such a move.
|
|
365
414
|
*
|
|
366
|
-
* A DIFFERENTIAL, which is what makes it safe on every
|
|
415
|
+
* A DIFFERENTIAL, which is what makes it safe on every variation: the walk judges the reshaped tree
|
|
367
416
|
* only where it already described the unshaped one, so a placement it cannot model (a def inside
|
|
368
417
|
* a loop body read earlier in the same body is assigned on every iteration but the first) is not
|
|
369
418
|
* judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
|
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
|
+
}
|