@asmlift/core 0.3.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +125 -2
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +15 -2
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +301 -26
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +118 -4
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +5 -2
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +172 -20
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +399 -31
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
package/src/frontend/ssa.ts
CHANGED
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
// CFG (predecessors per block) and, per block, emits ops through `readVar`/`writeVar`; this
|
|
4
4
|
// module materialises block-argument phis at joins and back-edges.
|
|
5
5
|
//
|
|
6
|
+
// `preds` is an EDGE list, not a block list: it carries one entry per CFG edge, so a `switch_br`
|
|
7
|
+
// with several case values reaching one block appears there several times. Both readings are
|
|
8
|
+
// needed and they are not interchangeable — phi wiring wants the distinct predecessor BLOCKS (one
|
|
9
|
+
// value each), while the args it appends belong to the EDGES (every one of them). `distinctPreds`
|
|
10
|
+
// names the first; `appendSuccessorArg` walks the second. (ir/core.ts `predecessors` and
|
|
11
|
+
// structure.ts `predecessorBlocks` have the same duality, and structure.ts already dedups ad hoc
|
|
12
|
+
// at its two join sites.)
|
|
13
|
+
//
|
|
6
14
|
// Protocol: create the builder, then fill blocks in index order. For each block, emit its
|
|
7
15
|
// computation via read/writeVar, push its terminator op last (successors referencing
|
|
8
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
9
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
10
|
-
import { Block, Fn,
|
|
18
|
+
import { Block, Fn, Value, mkValue } from '../ir/core';
|
|
19
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
11
20
|
import { T } from '../ir/types';
|
|
12
21
|
|
|
13
22
|
export interface SsaBuilder {
|
|
@@ -27,6 +36,7 @@ export interface SsaBuilder {
|
|
|
27
36
|
finish(): void;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/** `preds` is per-EDGE (see the module header): one entry per CFG edge into each block. */
|
|
30
40
|
export function makeSsaBuilder(name: string, blockCount: number, preds: number[][]): SsaBuilder {
|
|
31
41
|
const irBlocks: Block[] = Array.from({ length: blockCount }, () => ({ params: [] as Value[], ops: [] }));
|
|
32
42
|
const fn: Fn = { name, blocks: irBlocks };
|
|
@@ -38,6 +48,9 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
38
48
|
const phiBlock = new Map<Value, number>();
|
|
39
49
|
const paramReg = new Map<Value, string>();
|
|
40
50
|
|
|
51
|
+
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
52
|
+
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
53
|
+
|
|
41
54
|
const writeVar = (reg: string, b: number, v: Value) => defs[b].set(reg, v);
|
|
42
55
|
const readVar = (reg: string, b: number): Value => defs[b].get(reg) ?? readRecursive(reg, b);
|
|
43
56
|
|
|
@@ -55,7 +68,10 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
55
68
|
incompletePhis[b].set(reg, phi);
|
|
56
69
|
return phi;
|
|
57
70
|
}
|
|
58
|
-
|
|
71
|
+
// DISTINCT predecessor blocks: a switch_br reaching this block on several case values is one
|
|
72
|
+
// predecessor with several edges, and it supplies ONE value — counting the edges instead would
|
|
73
|
+
// manufacture a join (and a phi) where there is none.
|
|
74
|
+
const ps = distinctPreds(b);
|
|
59
75
|
if (ps.length === 0) {
|
|
60
76
|
// live-in with no predecessor: an incoming argument register → function parameter.
|
|
61
77
|
const p = mkValue(T.unk(32));
|
|
@@ -75,16 +91,24 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
75
91
|
return phi;
|
|
76
92
|
};
|
|
77
93
|
const addPhiOperands = (reg: string, b: number) => {
|
|
78
|
-
for (const p of
|
|
94
|
+
for (const p of distinctPreds(b)) {
|
|
79
95
|
appendSuccessorArg(p, b, readVar(reg, p));
|
|
80
96
|
}
|
|
81
97
|
};
|
|
82
|
-
// Append `arg` to predecessor p
|
|
98
|
+
// Append `arg` to EVERY successor edge of predecessor p that targets block b.
|
|
99
|
+
//
|
|
100
|
+
// A predecessor normally has one edge to a given successor, but a `switch_br` has as many as it
|
|
101
|
+
// has case values, and two cases sharing a body (`case 1: case 2:`) is ordinary C. Block args
|
|
102
|
+
// belong to the EDGE, so each of those edges needs its own copy: appending to just the first (a
|
|
103
|
+
// `find`) left the others short, while `preds` listing the block once per edge made the loop run
|
|
104
|
+
// k times and pile k copies onto that same first edge. Both halves of that — every edge, once per
|
|
105
|
+
// predecessor BLOCK — have to hold together, which is why they are fixed in one place.
|
|
83
106
|
const appendSuccessorArg = (p: number, b: number, arg: Value) => {
|
|
84
107
|
const term = irBlocks[p].ops[irBlocks[p].ops.length - 1];
|
|
85
|
-
const s
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
for (const s of term.successors) {
|
|
109
|
+
if (s.block === irBlocks[b]) {
|
|
110
|
+
s.args.push(arg);
|
|
111
|
+
}
|
|
88
112
|
}
|
|
89
113
|
};
|
|
90
114
|
const sealBlock = (b: number) => {
|
|
@@ -128,7 +152,7 @@ export function makeSsaBuilder(name: string, blockCount: number, preds: number[]
|
|
|
128
152
|
filled[b] = true;
|
|
129
153
|
sealReadyBlocks();
|
|
130
154
|
},
|
|
131
|
-
finish: () => simplifyTrivialPhis(fn, phiBlock),
|
|
155
|
+
finish: () => simplifyTrivialPhis(fn, (p) => phiBlock.delete(p)),
|
|
132
156
|
};
|
|
133
157
|
}
|
|
134
158
|
|
|
@@ -167,48 +191,3 @@ export function abiSortEntryParams(
|
|
|
167
191
|
}
|
|
168
192
|
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
169
193
|
}
|
|
170
|
-
|
|
171
|
-
// Remove block-parameters that are really trivial phis: those whose incoming operands (across
|
|
172
|
-
// every predecessor edge, ignoring self-references from a back-edge) are all the same single
|
|
173
|
-
// value. Such a parameter carries no join information — a loop-invariant register or a value
|
|
174
|
-
// defined before the join — so it is replaced by that value and the corresponding argument
|
|
175
|
-
// dropped from each predecessor's terminator. Iterated to fixpoint because removing one phi
|
|
176
|
-
// can make another trivial.
|
|
177
|
-
function simplifyTrivialPhis(fn: Fn, phiBlock: Map<Value, number>): void {
|
|
178
|
-
const edgesTo = (b: Block): Successor[] => {
|
|
179
|
-
const out: Successor[] = [];
|
|
180
|
-
for (const pb of fn.blocks) {
|
|
181
|
-
for (const op of pb.ops) {
|
|
182
|
-
for (const s of op.successors) {
|
|
183
|
-
if (s.block === b) {
|
|
184
|
-
out.push(s);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return out;
|
|
190
|
-
};
|
|
191
|
-
let changed = true;
|
|
192
|
-
while (changed) {
|
|
193
|
-
changed = false;
|
|
194
|
-
for (const b of fn.blocks) {
|
|
195
|
-
const incoming = edgesTo(b);
|
|
196
|
-
for (let i = b.params.length - 1; i >= 0; i--) {
|
|
197
|
-
const param = b.params[i];
|
|
198
|
-
const operands = incoming.map((s) => s.args[i]);
|
|
199
|
-
const distinct = [...new Set(operands.filter((v) => v !== param))];
|
|
200
|
-
if (distinct.length !== 1) {
|
|
201
|
-
continue;
|
|
202
|
-
} // a genuine join (or unreachable) — keep it
|
|
203
|
-
const v = distinct[0];
|
|
204
|
-
replaceAllUsesWith(fn, param, v);
|
|
205
|
-
b.params.splice(i, 1);
|
|
206
|
-
for (const s of incoming) {
|
|
207
|
-
s.args.splice(i, 1);
|
|
208
|
-
}
|
|
209
|
-
phiBlock.delete(param);
|
|
210
|
-
changed = true;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
package/src/frontend/thumb.ts
CHANGED
|
@@ -30,14 +30,74 @@ import { opaqueDest } from './opaque';
|
|
|
30
30
|
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder } from './ssa';
|
|
31
31
|
|
|
32
32
|
interface Instr {
|
|
33
|
+
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
34
|
+
* consumer matches one name. */
|
|
33
35
|
mnemonic: string;
|
|
34
36
|
ops: string[];
|
|
37
|
+
/** the spelling the input file actually used, present only when normalisation changed it.
|
|
38
|
+
* Messages must use this: a decline naming `ldrsh` for a file containing `ldsh` sends the
|
|
39
|
+
* reader looking for an instruction that is not there. */
|
|
40
|
+
asWritten?: string;
|
|
35
41
|
}
|
|
36
42
|
interface AsmBlock {
|
|
37
43
|
label: string;
|
|
38
44
|
instrs: Instr[];
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
// Alternative mnemonic spellings, normalised at the single point where an instruction enters the
|
|
48
|
+
// IR. Each maps to a name the decode switch below already handles.
|
|
49
|
+
//
|
|
50
|
+
// These are not "similar" instructions — each pair is ONE instruction with two accepted spellings.
|
|
51
|
+
// The ARM7TDMI Technical Reference Manual (ARM DDI 0029G) gives a single encoding for each:
|
|
52
|
+
// Figure 1-6 "Thumb instruction set formats" lists Format 08 "Load and store sign-extended byte and
|
|
53
|
+
// halfword" (0101 H S 1 Ro Rb Rd) and Format 15 "Multiple load and store" (1100 L Rb Rlist), and
|
|
54
|
+
// Table 1-7 "Thumb instruction set summary" spells them `LDRSH Rd, [Rb, Ro]`, `LDRSB Rd, [Rb, Ro]`,
|
|
55
|
+
// `LDMIA Rb!, <reglist>` and `STMIA Rb!, <reglist>`. Older ARM7TDMI documentation used LDSH/LDSB,
|
|
56
|
+
// which is where the short spellings come from; the stack-suffix forms (FD, EA) are the same
|
|
57
|
+
// instructions named after the stack discipline they implement.
|
|
58
|
+
//
|
|
59
|
+
// Confirmed with this project's own toolchain — same encoding, and gba-kit executes them with the
|
|
60
|
+
// same architectural effect (sign extension, transfers, base writeback):
|
|
61
|
+
//
|
|
62
|
+
// ldsh / ldrsh 885e / 885e ldm / ldmia / ldmfd 01c9 / 01c9 / 01c9
|
|
63
|
+
// ldsb / ldrsb 8856 / 8856 stm / stmia / stmea 01c1 / 01c1 / 01c1
|
|
64
|
+
//
|
|
65
|
+
// Measured on the Klonoa: Empire of Dreams disassembly (luvdis, 469 .s files): `ldsh` 292 and
|
|
66
|
+
// `ldsb` 180 against `ldrsh` 0 and `ldrsb` 12 — the same tool emits both spellings for the signed
|
|
67
|
+
// byte load — and `ldm` 12 / `stm` 34 against `ldmia` 0 / `stmia` 0. The UAL names this frontend
|
|
68
|
+
// cased for the multiple forms never appear in that corpus at all.
|
|
69
|
+
//
|
|
70
|
+
// These are PURE SYNONYMS — identical operands — which is why they belong in a table here rather
|
|
71
|
+
// than in decode arms like MIPS's `move` or PPC's `slwi`. That distinction, and why there is no
|
|
72
|
+
// shared alias helper across the three frontends, is written up once in ./opaque.ts.
|
|
73
|
+
//
|
|
74
|
+
// The LOAD aliases matter more than the store ones, and the asymmetry is worth knowing: an
|
|
75
|
+
// unrecognised `stm*` matches `opaquePolicy.storeClass` and fails LOUD, but an unrecognised `ldm*`
|
|
76
|
+
// does not — it reaches opaqueDest, which takes ops[0] (the BASE) as the destination, so the opaque
|
|
77
|
+
// is dead, DCE removes it, and the load silently vanishes. Measured: `ldmfd r1, {r0}; bx lr` lifted
|
|
78
|
+
// to `return a0;` where the answer is `return *a0;`. Every load spelling ARMv4T Thumb accepts is
|
|
79
|
+
// therefore listed. (`ldmed`/`ldmea` are NOT: they mean IB/DB, which Thumb-1 does not have, and
|
|
80
|
+
// `as` rejects them — so they cannot appear.)
|
|
81
|
+
//
|
|
82
|
+
// `stmfd` is deliberately absent, and the asymmetry is real rather than an oversight: `stmfd` is
|
|
83
|
+
// `stmdb`, and ARMv4T Thumb has neither — `as` rejects both with "selected processor does not
|
|
84
|
+
// support ... in Thumb mode". There is nothing to normalise it TO.
|
|
85
|
+
//
|
|
86
|
+
// Null-prototype so that an inherited key (`constructor`, `toString`) cannot be mistaken for an
|
|
87
|
+
// entry. Unreachable from real assembly, but the lookup should not depend on that.
|
|
88
|
+
const LEGACY_MNEMONICS: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
|
|
89
|
+
ldsh: 'ldrsh',
|
|
90
|
+
ldsb: 'ldrsb',
|
|
91
|
+
ldm: 'ldmia',
|
|
92
|
+
ldmfd: 'ldmia',
|
|
93
|
+
stm: 'stmia',
|
|
94
|
+
stmea: 'stmia',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function canonicalMnemonic(mn: string): string {
|
|
98
|
+
return LEGACY_MNEMONICS[mn] ?? mn;
|
|
99
|
+
}
|
|
100
|
+
|
|
41
101
|
// Map a Thumb conditional-branch mnemonic to the icmp opcode for "branch taken". The signed forms
|
|
42
102
|
// (`blt`/`ble`/`bgt`/`bge`) follow a signed `cmp`; the UNSIGNED forms carry the carry/borrow sense:
|
|
43
103
|
// `bhi` = unsigned > (higher), `bls` = unsigned <= (lower-or-same), `bcc`/`blo` = unsigned <
|
|
@@ -119,6 +179,48 @@ const imm = (s: string) => parseInt(s.replace(/^#/, ''), s.includes('0x') ? 16 :
|
|
|
119
179
|
// detection sees them, and any consumer that needs the exact list rejects the leftover `-` token
|
|
120
180
|
// loudly rather than treating the fused range as one phantom register.
|
|
121
181
|
const REG_NUM: Record<string, number> = { sp: 13, lr: 14, pc: 15 };
|
|
182
|
+
|
|
183
|
+
// Thumb-1 data-processing mnemonics that write the condition flags when their destination is a LOW
|
|
184
|
+
// register — which is all of them on this ISA, `s`-suffix or not (the assembler picks the encoding).
|
|
185
|
+
// Used to invalidate a pending compare: see the decode loop. `cmp`/`cmn`/`tst` are absent on purpose
|
|
186
|
+
// — they set flags but define no register, and `cmp` is the very instruction that seeds the pending
|
|
187
|
+
// compare. Loads, stores, push/pop, `bl` and the high-register forms leave the flags alone.
|
|
188
|
+
const FLAG_SETTING = new Set([
|
|
189
|
+
'mov',
|
|
190
|
+
'movs',
|
|
191
|
+
'add',
|
|
192
|
+
'adds',
|
|
193
|
+
'sub',
|
|
194
|
+
'subs',
|
|
195
|
+
'lsl',
|
|
196
|
+
'lsls',
|
|
197
|
+
'lsr',
|
|
198
|
+
'lsrs',
|
|
199
|
+
'asr',
|
|
200
|
+
'asrs',
|
|
201
|
+
'neg',
|
|
202
|
+
'negs',
|
|
203
|
+
'rsb',
|
|
204
|
+
'rsbs',
|
|
205
|
+
'mvn',
|
|
206
|
+
'mvns',
|
|
207
|
+
'bic',
|
|
208
|
+
'bics',
|
|
209
|
+
'ror',
|
|
210
|
+
'rors',
|
|
211
|
+
'mul',
|
|
212
|
+
'muls',
|
|
213
|
+
'and',
|
|
214
|
+
'ands',
|
|
215
|
+
'orr',
|
|
216
|
+
'orrs',
|
|
217
|
+
'eor',
|
|
218
|
+
'eors',
|
|
219
|
+
'adc',
|
|
220
|
+
'adcs',
|
|
221
|
+
'sbc',
|
|
222
|
+
'sbcs',
|
|
223
|
+
]);
|
|
122
224
|
const regNum = (r: string) => (r[0] === 'r' ? Number(r.slice(1)) : REG_NUM[r]);
|
|
123
225
|
function expandRegList(tokens: string[]): string[] {
|
|
124
226
|
const out: string[] = [];
|
|
@@ -305,7 +407,14 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
305
407
|
continue;
|
|
306
408
|
}
|
|
307
409
|
dataLabel = null; // a real instruction ends a data run
|
|
308
|
-
|
|
410
|
+
const canon = canonicalMnemonic(m[1]);
|
|
411
|
+
flat.push({
|
|
412
|
+
instr: {
|
|
413
|
+
mnemonic: canon,
|
|
414
|
+
ops: m[2] ? splitOperands(m[2]) : [],
|
|
415
|
+
...(canon === m[1] ? {} : { asWritten: m[1] }),
|
|
416
|
+
},
|
|
417
|
+
});
|
|
309
418
|
}
|
|
310
419
|
if (
|
|
311
420
|
armLabels.has(name) ||
|
|
@@ -608,6 +717,41 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
608
717
|
`cannot lift '${name}': block '${mixed.label}' interleaves raw data (.${subwordData.get(mixed.label)}) with instructions`,
|
|
609
718
|
);
|
|
610
719
|
}
|
|
720
|
+
// Two labels on the same instruction (`.LCB80:` immediately followed by `.L7:`) make the first
|
|
721
|
+
// an ALIAS of the second, not a block of its own — agbcc emits exactly that when a long-jump
|
|
722
|
+
// helper label lands on an existing one. The empty block is dropped just below, so a branch
|
|
723
|
+
// naming the alias would afterwards resolve to nothing and decline as a dangling target. Point
|
|
724
|
+
// those branches at the block the label actually names, before anything reads the CFG.
|
|
725
|
+
// A label naming DATA is emphatically NOT an alias, and this is the guard the whole pass turns
|
|
726
|
+
// on. Decode pushes an empty block for a literal-pool / jump-table label too, so aliasing them
|
|
727
|
+
// blindly would silently retarget `beq .Lpool` at whatever code happens to follow the pool —
|
|
728
|
+
// marker-free, plausible, wrong C where the frontend used to decline. Every agbcc pool is a
|
|
729
|
+
// label on data, so that is the common case, not an exotic one. A data label therefore neither
|
|
730
|
+
// aliases nor is aliased THROUGH: scanning past one for a later code block would silently jump
|
|
731
|
+
// over the data.
|
|
732
|
+
const isDataLabel = (l: string) => dataWords.has(l) || subwordData.has(l);
|
|
733
|
+
const aliasOf = new Map<string, string>();
|
|
734
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
735
|
+
if (blocks[i].instrs.length > 0 || isDataLabel(blocks[i].label)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
let j = i + 1;
|
|
739
|
+
while (j < blocks.length && blocks[j].instrs.length === 0 && !isDataLabel(blocks[j].label)) {
|
|
740
|
+
j++;
|
|
741
|
+
}
|
|
742
|
+
const next = blocks[j];
|
|
743
|
+
if (next && next.instrs.length > 0) {
|
|
744
|
+
aliasOf.set(blocks[i].label, next.label);
|
|
745
|
+
} // otherwise a trailing or data-fronted label: left dangling so a branch to it still declines
|
|
746
|
+
}
|
|
747
|
+
for (const b of aliasOf.size ? blocks : []) {
|
|
748
|
+
for (const ins of b.instrs) {
|
|
749
|
+
const k = ins.ops.length - 1;
|
|
750
|
+
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && k >= 0) {
|
|
751
|
+
ins.ops[k] = aliasOf.get(ins.ops[k]) ?? ins.ops[k];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
611
755
|
let live = blocks.filter((b) => b.instrs.length > 0);
|
|
612
756
|
// Alignment-pad NOPs a splitter emits around returns and literal pools: `lsls r0, r0, #0`
|
|
613
757
|
// is the 0x0000 halfword, `mov r8, r8` is 0x46C0, plus a literal `nop`. A block made ONLY
|
|
@@ -784,21 +928,47 @@ function recoverJumpTable(
|
|
|
784
928
|
disp: AsmBlock,
|
|
785
929
|
dataWords: Map<string, string[]>,
|
|
786
930
|
blockLabels: Set<string>,
|
|
931
|
+
longDefault?: string,
|
|
787
932
|
): JumpTable | null {
|
|
788
|
-
// bounds: last two instrs
|
|
933
|
+
// bounds: last two instrs are `cmp rX,#M` then the out-of-range guard, in one of two spellings.
|
|
934
|
+
//
|
|
935
|
+
// direct cmp rX,#M ; bhi DEF → fall through to the dispatch
|
|
936
|
+
// long jump cmp rX,#M ; bls DISP ; b DEF → branch TO the dispatch, long-branch the default
|
|
937
|
+
//
|
|
938
|
+
// The second is what agbcc emits whenever the default is out of a conditional branch's reach —
|
|
939
|
+
// Thumb-1 `B<cond>` carries a signed 8-bit HALFWORD offset, so ±256 BYTES, about 128
|
|
940
|
+
// instructions — which on a real switch it usually is: five of the six benchmark
|
|
941
|
+
// functions with a table use it, and only the sixth uses the direct form. `longDefault` is the
|
|
942
|
+
// target of that trailing `b`, read by the caller from the block after `bounds`.
|
|
789
943
|
const bi = bounds.instrs;
|
|
790
|
-
const
|
|
944
|
+
const guard = bi[bi.length - 1],
|
|
791
945
|
cmp = bi[bi.length - 2];
|
|
792
|
-
if (!
|
|
946
|
+
if (!guard || !cmp || cmp.mnemonic !== 'cmp') {
|
|
793
947
|
return null;
|
|
794
948
|
}
|
|
949
|
+
let defaultLabel: string;
|
|
950
|
+
if (longDefault === undefined) {
|
|
951
|
+
if (guard.mnemonic !== 'bhi') {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
defaultLabel = guard.ops[0];
|
|
955
|
+
} else {
|
|
956
|
+
// The `bls` must name THIS dispatch block, or the guard belongs to some other branch and the
|
|
957
|
+
// `b` we picked up is not its default.
|
|
958
|
+
if (guard.mnemonic !== 'bls' || guard.ops[0] !== disp.label) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
defaultLabel = longDefault;
|
|
962
|
+
}
|
|
795
963
|
const scrutReg = cmp.ops[0];
|
|
796
964
|
const m = cmp.ops[1];
|
|
797
965
|
if (!m?.startsWith('#')) {
|
|
798
966
|
return null;
|
|
799
967
|
}
|
|
800
968
|
const n = imm(m) + 1; // cases 0..M → N = M+1
|
|
801
|
-
|
|
969
|
+
if (n < 1) {
|
|
970
|
+
return null; // a bound that admits no case at all is not a dispatch — fail closed
|
|
971
|
+
}
|
|
802
972
|
|
|
803
973
|
// disp: exactly the 5-op idiom, threading a single index register from `lsl rY,rX,#2`.
|
|
804
974
|
const d = disp.instrs;
|
|
@@ -835,11 +1005,28 @@ function recoverJumpTable(
|
|
|
835
1005
|
}
|
|
836
1006
|
|
|
837
1007
|
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
838
|
-
|
|
839
|
-
|
|
1008
|
+
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
1009
|
+
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
1010
|
+
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
1011
|
+
// rather than fixed speculatively.
|
|
1012
|
+
//
|
|
1013
|
+
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
1014
|
+
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
1015
|
+
// pointer in beside whatever else the function needed, and which slot it lands in is an artifact
|
|
1016
|
+
// of emission order. Reading only a bare label whose pool held exactly ONE word declined six real
|
|
1017
|
+
// benchmark functions whose table pointer merely sat later in the pool. Same fix m2c made in
|
|
1018
|
+
// `a7c5c2d`, and the same shared POOL_LABEL grammar the const/gaddr resolvers use, so the three
|
|
1019
|
+
// cannot disagree about what `.L21+0x4` addresses.
|
|
1020
|
+
const pm = ptrLabel.match(POOL_LABEL);
|
|
1021
|
+
const ptrWords = pm ? dataWords.get(pm[1]) : undefined;
|
|
1022
|
+
if (!pm || !ptrWords) {
|
|
840
1023
|
return null;
|
|
841
1024
|
}
|
|
842
|
-
const
|
|
1025
|
+
const ptrOff = pm[2] ? Number(pm[2]) : 0;
|
|
1026
|
+
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
1027
|
+
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
1028
|
+
}
|
|
1029
|
+
const caseLabels = dataWords.get(ptrWords[ptrOff / 4].trim());
|
|
843
1030
|
if (!caseLabels || caseLabels.length !== n) {
|
|
844
1031
|
return null;
|
|
845
1032
|
} // table length must equal the bound
|
|
@@ -875,26 +1062,49 @@ export function lift(
|
|
|
875
1062
|
const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
|
|
876
1063
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
877
1064
|
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
878
|
-
|
|
1065
|
+
// How many branches name each label — not just whether any does, because the long-jump bounds
|
|
1066
|
+
// form legitimately branches to its own dispatch block exactly once.
|
|
1067
|
+
const branchRefs = new Map<string, number>();
|
|
879
1068
|
for (const b of rawBlocks) {
|
|
880
1069
|
for (const ins of b.instrs) {
|
|
881
1070
|
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && ins.ops.length) {
|
|
882
|
-
|
|
1071
|
+
const t = ins.ops[ins.ops.length - 1];
|
|
1072
|
+
branchRefs.set(t, (branchRefs.get(t) ?? 0) + 1);
|
|
883
1073
|
}
|
|
884
1074
|
}
|
|
885
1075
|
}
|
|
886
1076
|
const tables = new Map<AsmBlock, JumpTable>(); // bounds block → recovered table
|
|
887
|
-
const elided = new Set<AsmBlock>(); // dispatch blocks removed from the CFG
|
|
1077
|
+
const elided = new Set<AsmBlock>(); // dispatch (and long-jump default) blocks removed from the CFG
|
|
888
1078
|
rawBlocks.forEach((d, i) => {
|
|
889
1079
|
const last = d.instrs[d.instrs.length - 1];
|
|
890
|
-
if (last
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1080
|
+
if (!last || last.mnemonic !== 'mov' || last.ops[0] !== 'pc' || last.ops[1] === 'lr') {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const refs = branchRefs.get(d.label) ?? 0;
|
|
1084
|
+
const prev = rawBlocks[i - 1];
|
|
1085
|
+
// Direct form: the dispatch is reached ONLY by falling through from its bounds predecessor. A
|
|
1086
|
+
// `b disp` from anywhere else would leave a dangling edge after elision, so decline (→ loud-fail).
|
|
1087
|
+
if (prev && refs === 0) {
|
|
1088
|
+
const jt = recoverJumpTable(prev, d, dataWords, blockLabels);
|
|
895
1089
|
if (jt) {
|
|
896
|
-
tables.set(
|
|
1090
|
+
tables.set(prev, jt);
|
|
897
1091
|
elided.add(d);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Long-jump form: `bounds` (cmp; bls DISP), then a lone `b DEF` block, then the dispatch. The
|
|
1096
|
+
// dispatch is entered by exactly that one `bls` and nothing else, and the `b DEF` block —
|
|
1097
|
+
// synthetically labelled, so unnameable and unreachable once the bounds block dispatches — is
|
|
1098
|
+
// elided WITH it. Leaving it would make it a parameterless predecessor of the default block,
|
|
1099
|
+
// and wiring a phi through it fabricates an entry parameter (the phantom-param miscompile).
|
|
1100
|
+
const boundsB = rawBlocks[i - 2];
|
|
1101
|
+
const prevNamed = prev ? (branchRefs.get(prev.label) ?? 0) > 0 : false;
|
|
1102
|
+
if (refs === 1 && prev && boundsB && !prevNamed && prev.instrs.length === 1 && prev.instrs[0].mnemonic === 'b') {
|
|
1103
|
+
const jt = recoverJumpTable(boundsB, d, dataWords, blockLabels, prev.instrs[0].ops[0]);
|
|
1104
|
+
if (jt) {
|
|
1105
|
+
tables.set(boundsB, jt);
|
|
1106
|
+
elided.add(d);
|
|
1107
|
+
elided.add(prev);
|
|
898
1108
|
}
|
|
899
1109
|
}
|
|
900
1110
|
});
|
|
@@ -1028,7 +1238,7 @@ export function lift(
|
|
|
1028
1238
|
// adjustments have no low-register data destination, so they fall through harmlessly;
|
|
1029
1239
|
// terminators are handled in the terminator section below.
|
|
1030
1240
|
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
1031
|
-
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[] }) => {
|
|
1241
|
+
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[]; asWritten?: string }) => {
|
|
1032
1242
|
// storeClass: unmodelled Thumb stores are str*/stm* — `stmia rN!, {…}`'s dest token `r0!`
|
|
1033
1243
|
// fails isReg, so without this it would be skipped as "no reg dest", silently deleting the
|
|
1034
1244
|
// memory writes AND the base writeback. push/pop stay transparent frame ops (they don't match).
|
|
@@ -1040,6 +1250,7 @@ export function lift(
|
|
|
1040
1250
|
storeClass: /^(str|stm)/,
|
|
1041
1251
|
skipSafe: /^(push|pop|nop)$/,
|
|
1042
1252
|
context: name,
|
|
1253
|
+
display: ins.asWritten,
|
|
1043
1254
|
});
|
|
1044
1255
|
if (!od) {
|
|
1045
1256
|
return;
|
|
@@ -1047,7 +1258,7 @@ export function lift(
|
|
|
1047
1258
|
const operands = od.srcRegs.map((r) => readVar(r, bi));
|
|
1048
1259
|
const res = mkValue(T.unk(32));
|
|
1049
1260
|
// carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'rsb'")`)
|
|
1050
|
-
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.mnemonic } }));
|
|
1261
|
+
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.asWritten ?? ins.mnemonic } }));
|
|
1051
1262
|
writeVar(od.dst, bi, res);
|
|
1052
1263
|
};
|
|
1053
1264
|
// 2-operand ALU form `op rD, op2` (rD = rD ⟨op⟩ op2). `op2` is an immediate (`#N`) or a
|
|
@@ -1076,6 +1287,22 @@ export function lift(
|
|
|
1076
1287
|
if (classifyXfer(ins)) {
|
|
1077
1288
|
continue;
|
|
1078
1289
|
}
|
|
1290
|
+
// A Thumb-1 data-processing instruction on LOW registers writes the condition flags whether or
|
|
1291
|
+
// not the mnemonic carries the `s` (agbcc spells `adds r0,r0,r3` as `add r0,r0,r3`, and the
|
|
1292
|
+
// assembler picks the flag-setting encoding) — so an instruction between a `cmp` and its branch
|
|
1293
|
+
// REPLACES the flags the branch will test. Folding the earlier `cmp` in anyway would emit a
|
|
1294
|
+
// condition on the wrong operands: silently wrong C with no marker. Drop the pending compare
|
|
1295
|
+
// and let the terminator's existing "no reaching compare in its block" decline fire — the loud
|
|
1296
|
+
// answer, since modelling arithmetic flags is a capability asmlift does not have.
|
|
1297
|
+
//
|
|
1298
|
+
// The HIGH-register forms (`mov rD,rH`, `add rD,rH`) do NOT set flags and stay transparent,
|
|
1299
|
+
// which is what keeps agbcc's callee-saved shuffling from tripping this. Measured free: across
|
|
1300
|
+
// every agbcc row in the benchmark, no conditional-branch block has ANY instruction between its
|
|
1301
|
+
// compare and the branch — compilers keep the pair adjacent. The inhabitant this guards is
|
|
1302
|
+
// hand-written asm in the playground, where there is no oracle to catch a lie.
|
|
1303
|
+
if (FLAG_SETTING.has(ins.mnemonic) && /^r[0-7]$/.test(reg(ins.ops[0] ?? ''))) {
|
|
1304
|
+
pendingCmp = null;
|
|
1305
|
+
}
|
|
1079
1306
|
const [a, b, c] = ins.ops;
|
|
1080
1307
|
switch (ins.mnemonic) {
|
|
1081
1308
|
case 'mov':
|
|
@@ -1215,9 +1442,18 @@ export function lift(
|
|
|
1215
1442
|
// rejoin below also tolerates a split list defensively. Thumb-1 LDMIA skips the
|
|
1216
1443
|
// writeback when rN is itself in the list (the loaded value wins) — modelled; any
|
|
1217
1444
|
// malformed shape degrades to the loud opaque.
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1445
|
+
// There is NO no-writeback form in Thumb-1, so the `!` is decoration and must not drive
|
|
1446
|
+
// the model. Four sources agree:
|
|
1447
|
+
// * ARM DDI 0029G Table 1-7 gives the canonical syntax as `LDMIA Rb!, <reglist>` and
|
|
1448
|
+
// `STMIA Rb!, <reglist>` — the `!` is part of the mnemonic, not an option, and
|
|
1449
|
+
// Figure 1-6 Format 15 has no bit that could encode its absence;
|
|
1450
|
+
// * GNU as assembles `ldm r1,{r0}` and `ldm r1!,{r0}` to the same halfword, 0xc901,
|
|
1451
|
+
// and warns "this instruction will write back the base register";
|
|
1452
|
+
// * gba-kit executes both with the base advanced by 4;
|
|
1453
|
+
// * GBATEK, THUMB.15: "Both STM and LDM are incrementing the Base Register".
|
|
1454
|
+
// An earlier version of this comment called the `!`-less spelling "the valid
|
|
1455
|
+
// no-writeback form — same transfers, base unchanged", which is false, and the code
|
|
1456
|
+
// below acted on it. A missing register list is malformed → loud opaque.
|
|
1221
1457
|
const baseTok = a;
|
|
1222
1458
|
const writeback = !!baseTok?.endsWith('!');
|
|
1223
1459
|
if (baseTok === undefined || b === undefined || !b.startsWith('{')) {
|
|
@@ -1244,6 +1480,30 @@ export function lift(
|
|
|
1244
1480
|
emitOpaqueDest(ins);
|
|
1245
1481
|
break;
|
|
1246
1482
|
}
|
|
1483
|
+
// An STM whose base is in its own list, but is not the LOWEST entry, stores a value this
|
|
1484
|
+
// frontend must not guess — because the available references DISAGREE about what it is.
|
|
1485
|
+
//
|
|
1486
|
+
// ARM: UNPREDICTABLE, "the stored value cannot be relied upon".
|
|
1487
|
+
// GNU as: warns "value stored for rN is UNKNOWN".
|
|
1488
|
+
// GBATEK: version-specific — "Store OLD base if Rb is FIRST entry in Rlist,
|
|
1489
|
+
// otherwise store NEW base (STM/ARMv4), always store OLD base (STM/ARMv5)".
|
|
1490
|
+
// mGBA: stores the OLD base unconditionally, on an ARMv4T core — its STM_LOOP
|
|
1491
|
+
// reads gprs[i] during the loop and the writeback runs after it.
|
|
1492
|
+
//
|
|
1493
|
+
// So GBATEK's ARMv4 rule and the reference emulator's behaviour do not agree, and no
|
|
1494
|
+
// hardware test result was found either way. This frontend used to emit the old base,
|
|
1495
|
+
// i.e. it silently picked one side of that disagreement. Declining is the contract:
|
|
1496
|
+
// where the architecture declines to define a value, so do we.
|
|
1497
|
+
//
|
|
1498
|
+
// (One site in the Klonoa corpus, in unreachable code after a `pop`/`bx`, and it already
|
|
1499
|
+
// declines for an unrelated pc-relative-pool reason — so this costs nothing today.)
|
|
1500
|
+
if (ins.mnemonic === 'stmia' && list.some((r) => reg(r) === baseReg) && reg(list[0]) !== baseReg) {
|
|
1501
|
+
throw new FrontendUnsupportedError(
|
|
1502
|
+
`cannot lift '${name}': stm with the base register in its own list, not as the lowest ` +
|
|
1503
|
+
`entry — the value stored for that register is UNPREDICTABLE and differs between ` +
|
|
1504
|
+
`ARMv4 (new base) and ARMv5 (old base)`,
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1247
1507
|
// SNAPSHOT the base ONCE: hardware performs every transfer from the ORIGINAL base, but
|
|
1248
1508
|
// a base-in-list ldmia overwrites that register mid-list — re-reading it per iteration
|
|
1249
1509
|
// loaded the siblings from the freshly-loaded value instead (silent wrong addresses,
|
|
@@ -1260,10 +1520,12 @@ export function lift(
|
|
|
1260
1520
|
irb.ops.push(mkOp('store', { operands: [base0, readData(reg(r), bi)], attrs: { off: 4 * i, width: 4 } }));
|
|
1261
1521
|
}
|
|
1262
1522
|
});
|
|
1263
|
-
// Writeback advances the base by 4×count
|
|
1264
|
-
//
|
|
1523
|
+
// Writeback advances the base by 4×count. It is suppressed ONLY for an ldmia whose base
|
|
1524
|
+
// is in its own list — the loaded value wins. GBATEK, THUMB.15: "no writeback
|
|
1525
|
+
// (LDM/ARMv4/ARMv5; at this point, THUMB opcodes work different than ARM opcodes)".
|
|
1526
|
+
// The `!` is NOT what decides it: see above, there is no encoding without writeback.
|
|
1265
1527
|
const wroteBase = ins.mnemonic === 'ldmia' && list.some((r) => reg(r) === baseReg);
|
|
1266
|
-
if (
|
|
1528
|
+
if (!wroteBase) {
|
|
1267
1529
|
const adv = mkValue(T.unk(32));
|
|
1268
1530
|
irb.ops.push(mkOp('add', { operands: [base0, constVal(4 * list.length, bi)], results: [adv] }));
|
|
1269
1531
|
writeVar(baseReg, bi, adv);
|
|
@@ -1477,7 +1739,20 @@ export function lift(
|
|
|
1477
1739
|
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
1478
1740
|
} else if (kind === 'return') {
|
|
1479
1741
|
// bx lr / pop {…,pc} / mov pc,lr
|
|
1480
|
-
|
|
1742
|
+
//
|
|
1743
|
+
// A `bx rN` BRANCHES THROUGH rN, so at that instruction rN holds the RETURN ADDRESS. When rN
|
|
1744
|
+
// is the return-VALUE register the two uses collide, and the address wins by definition —
|
|
1745
|
+
// whatever value was in r0 is gone, so the function cannot be returning one. agbcc spells an
|
|
1746
|
+
// interworking return that way (`push {lr}` … `pop {r0}; bx r0`), and reading r0 as a value
|
|
1747
|
+
// there invents a return the machine provably cannot make: a phantom `return`, a non-`void`
|
|
1748
|
+
// signature that would contradict the project's own prototype, and a live range that keeps
|
|
1749
|
+
// otherwise-dead computation alive.
|
|
1750
|
+
//
|
|
1751
|
+
// The other return forms are untouched, because none of them writes the return register:
|
|
1752
|
+
// `bx lr` and `bx r1`/`bx r2` branch through a different one, and `pop {…,pc}` / `mov pc,lr`
|
|
1753
|
+
// load PC directly. Only the register actually branched through is disqualified.
|
|
1754
|
+
const viaReturnReg = last.mnemonic === 'bx' && last.ops[0] === target.returnReg;
|
|
1755
|
+
irb.ops.push(mkOp('ret', { operands: viaReturnReg ? [] : [readVar(target.returnReg, bi)] }));
|
|
1481
1756
|
} else if (kind === 'uncond') {
|
|
1482
1757
|
irb.ops.push(mkOp('br', { successors: [succ(last.ops[0])] }));
|
|
1483
1758
|
} else if (kind === 'cond') {
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -130,11 +130,55 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
130
130
|
return (OPCODES as Record<string, OpSig | undefined>)[opcode];
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** The comparison whose result is the logical NEGATION of each `icmp_*` — `!(a < b)` is `a >= b`.
|
|
134
|
+
*
|
|
135
|
+
* Unlike EFFECTFUL_OPS/HOIST_UNSAFE_OPS below, this is AUTHORED data seated beside the registry,
|
|
136
|
+
* not a view derived from it: nothing in `OPCODES` states which comparison opposes which. What is
|
|
137
|
+
* derived is its SYMMETRY — the five involutive pairs are expanded both ways, so `neg(neg(c)) === c`
|
|
138
|
+
* holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
|
|
139
|
+
* plainly inverted condition in the emitted C). Completeness against the icmp family is the part
|
|
140
|
+
* construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
|
|
141
|
+
* added to `OPCODES` would otherwise degrade three consumers three different ways.
|
|
142
|
+
*
|
|
143
|
+
* It lives here for the reason HOIST_UNSAFE_OPS does: every consumer that has to say "the opposite
|
|
144
|
+
* of this compare" reads THIS one — the MIPS frontend's `slt …; beqz` branch-when-false fold, the
|
|
145
|
+
* short-circuit recognizer's diamond negation, and the idiom layer's `cmp ^ 1` fold — so they
|
|
146
|
+
* cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
147
|
+
* shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
|
|
148
|
+
* true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
|
|
149
|
+
* vocabulary — deliberately separate, because signedness lives in the operand types there, so the
|
|
150
|
+
* two tables are not candidates for further consolidation. */
|
|
151
|
+
const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
152
|
+
['icmp_eq', 'icmp_ne'],
|
|
153
|
+
['icmp_slt', 'icmp_sge'],
|
|
154
|
+
['icmp_sgt', 'icmp_sle'],
|
|
155
|
+
['icmp_ult', 'icmp_uge'],
|
|
156
|
+
['icmp_ugt', 'icmp_ule'],
|
|
157
|
+
];
|
|
158
|
+
export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries(
|
|
159
|
+
ICMP_NEGATION_PAIRS.flatMap(([a, b]) => [
|
|
160
|
+
[a, b],
|
|
161
|
+
[b, a],
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
|
|
133
165
|
/** Ops with an observable side effect — the derived view raise/shortcircuit.ts consumes. */
|
|
134
166
|
export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
135
167
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
|
|
136
168
|
);
|
|
137
169
|
|
|
170
|
+
/** Ops that may not be REORDERED across other code — `EFFECTFUL_OPS` plus `opaque`.
|
|
171
|
+
*
|
|
172
|
+
* `effects` is overloaded on two axes, and `opaque` is exactly the op that separates them: a dead
|
|
173
|
+
* `opaque` MUST stay deletable (`isDceSafe` below says so deliberately — giving it `effects: true`
|
|
174
|
+
* would strand dead opaques after every pattern rewrite, and they would surface as ASMLIFT_ERROR
|
|
175
|
+
* gaps in functions that emit cleanly today), while a LIVE one is an instruction asmlift could not
|
|
176
|
+
* model and must not be moved past anything. So "deletable when dead" and "movable when live" are
|
|
177
|
+
* different questions and get different views, both derived here rather than re-spelled per
|
|
178
|
+
* consumer — structure/analysis.ts and structure/structure.ts each carry their own inline copy of
|
|
179
|
+
* this membership, which is how the two models drifted apart in the first place. */
|
|
180
|
+
export const HOIST_UNSAFE_OPS: ReadonlySet<string> = new Set([...EFFECTFUL_OPS, 'opaque']);
|
|
181
|
+
|
|
138
182
|
/** May a dead result of this opcode be deleted? Registered, no observable effects, not control
|
|
139
183
|
* flow. Deliberately includes `opaque` — a dead opaque vanishing is designed behavior. */
|
|
140
184
|
export function isDceSafe(opcode: string): boolean {
|