@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
// asmlift — undo a partial-redundancy insertion: a block parameter whose every in-edge hands it the
|
|
2
|
+
// same pure expression is that expression, recomputed in the block (ISA-neutral CFG pass).
|
|
3
|
+
//
|
|
4
|
+
// agbcc's gcse PRE (gcse.c `pre_insert` :4337, at the end of a predecessor by `insert_insn_end_bb`
|
|
5
|
+
// :4210) makes an expression that is computed on SOME paths into a block fully redundant by
|
|
6
|
+
// inserting it on the others, keeps each original computation's result in the same pseudo
|
|
7
|
+
// (`pre_insert_copy_insn` :4400), and deletes the copy in the block (`pre_delete` :4508). In
|
|
8
|
+
// `for (i = 0; i < n; i++) switch (gId[i]) { … case 2: case 3: if (gK[i + 1] != 0) break; case 4: … }`
|
|
9
|
+
// (`synthetic:sw_fallpre:agbcc`) the loop's `i + 1` is inserted above the jump table, case 2 keeps
|
|
10
|
+
// its own `gK[i + 1]` sum in that register, and SSA construction reads the register as a parameter
|
|
11
|
+
// of `case 4:` fed `i + 1` by both the switch edge and the arm falling into it. The structurer
|
|
12
|
+
// spells such a parameter as a C variable assigned on each edge, and on a switch edge that a
|
|
13
|
+
// fall-through path also reaches that is an edge copy it refuses to re-run ('the case fallen into
|
|
14
|
+
// takes a value from the switch edge'). Recomputing the expression in the block removes the
|
|
15
|
+
// parameter, and with it the copy.
|
|
16
|
+
//
|
|
17
|
+
// SOUND BY SSA: every in-edge's argument is the SAME VALUE, so reading it in the block is reading
|
|
18
|
+
// it on every edge. This is ir/simplify.ts's trivial phi (`trivialPhiValue`, which this calls) with
|
|
19
|
+
// identity widened to congruence, and with its waiver: a back edge that hands the parameter itself
|
|
20
|
+
// hands whatever the last entry did. "The same" is `sameAtEntry` (ir/entry.ts) with an empty entry
|
|
21
|
+
// map: one opcode, one set of attributes, operands the same in turn, down to LEAVES that are one SSA
|
|
22
|
+
// value on every edge. The walk reads through an op only when it can be re-evaluated anywhere (`clonable`: no
|
|
23
|
+
// effect, no memory read, nothing that traps, no placement stamp), so the expression's value is a
|
|
24
|
+
// function of its leaves alone; and every leaf must be defined in a block that strictly dominates
|
|
25
|
+
// the block, so it holds the same value there. C names need no argument of their own: the
|
|
26
|
+
// recomputed expression is fresh SSA ops reading the leaves, and namecoalesce gives a leaf that is
|
|
27
|
+
// live at the block a name no other live value shares. The edge ops the fold leaves unread are
|
|
28
|
+
// deleted with whatever pure ops only they read, as the derived fold below deletes its own: KMC
|
|
29
|
+
// steps `gA[i]`'s pointer in the default arm and in the slot of the branch into the latch's second
|
|
30
|
+
// entry, and once the latch reads the step itself the default arm's dead copy would still be a body
|
|
31
|
+
// (`synthetic:sw_gotocase2:gcc2.7.2kmc`), which the structurer reads as one more case it reaches.
|
|
32
|
+
// A block the sweep leaves holding only its `br` is then threaded out of the CFG (`threadEmptied`).
|
|
33
|
+
//
|
|
34
|
+
// SCOPE, not soundness (the gates the precondition does not need, tabled as `UNPRE_GATES`):
|
|
35
|
+
// • THE BLOCK IS A JOIN OF ONE SWITCH (`not-switch-join`): one in-edge is a `switch_br`'s, at least
|
|
36
|
+
// one comes from another block, and every other comes from a block the dispatch dominates — an
|
|
37
|
+
// arm of that switch. Two joins pass:
|
|
38
|
+
// (1) A CASE FALLEN INTO (`case 4:` above). The structurer cannot spell its parameter as a
|
|
39
|
+
// variable, because the switch-edge copy would re-run on the fall path, so it declines; the
|
|
40
|
+
// fold is what lifts it.
|
|
41
|
+
// (2) THE SWITCH'S EXIT, entered by the dispatch's default edge (the bounds test's, when the
|
|
42
|
+
// source has no `default:`) and by the arms' `break`s. Its parameter IS spellable — a copy at
|
|
43
|
+
// the end of every arm and a `default:` arm holding only the copy — so here the fold changes
|
|
44
|
+
// the spelling, not whether the function lifts. `sw_fallpre`'s MATCH rests on it: PRE's
|
|
45
|
+
// `i + 1` reaches the loop latch from every edge, the default one included, and with the
|
|
46
|
+
// exit refused (only the case fallen into recomputed) the row lifts at 6/61 as `v1 = v0 + 1;`
|
|
47
|
+
// in each arm, a `default:` arm, and `v0 = v1;` where the source has `i++`. Off the bench it
|
|
48
|
+
// also re-spells functions that already lift, host-correct both ways (4 agbcc fuzz
|
|
49
|
+
// seeds): per-arm `v2 = v0 + 1` becomes the latch's `v0 = v0 + 1`, and per-arm
|
|
50
|
+
// `v1 = a1 * 3` (a source's `u = v * 3` above the switch, which every edge hands the join)
|
|
51
|
+
// one `a1 * 3` read at the join.
|
|
52
|
+
// Everywhere else the parameter is spellable too, and which spelling the source wrote the asm
|
|
53
|
+
// does not say: a sum agbcc kept in each arm is also what a source computing it in each arm
|
|
54
|
+
// compiles to. Measured at every join (full bench, the gate lifted): `synthetic:sw_fallpre` and
|
|
55
|
+
// `synthetic:jt_pre_midg` MATCH, but `synthetic:bgshare:agbcc` and `synthetic:bgswitch:agbcc` lose
|
|
56
|
+
// their MATCH, `synthetic:sw_jtloop:agbcc` goes 8/57 -> 32/58, and 21 rows move in all. Gated, the
|
|
57
|
+
// full bench moves `sw_fallpre` alone (declined -> MATCH). The wider fold would be a lift
|
|
58
|
+
// variation, and a variation never ships a function its default declines (rank.ts), so it could
|
|
59
|
+
// not have lifted this row: the gate is what lets the default carry it. `unpre.test.ts` pins both
|
|
60
|
+
// joins and an ordinary `if` join the gate keeps out.
|
|
61
|
+
// A LOOP'S LATCH passes too: the block holding the back edge to a header that dominates it.
|
|
62
|
+
// IDO computes a counted loop's loop-invariant bound `hi + 1` (`addiu v0,a1,1`) in the header
|
|
63
|
+
// and again in the delay slot of every arm's `b latch`, so the latch of `for (i = lo; i <= hi;
|
|
64
|
+
// i++) switch (..) { .. }` (`synthetic:sw_gotocase2:ido7.1`) takes `hi + 1` as a parameter every
|
|
65
|
+
// in-edge hands it. The parameter is spellable — a copy at the end of every arm — but the
|
|
66
|
+
// structurer reads a latch with a parameter as no `for` (`forPlanAt`'s `latch-params`), so the
|
|
67
|
+
// fold is what lets the `for` read the latch at all. It is a pre-structure fold on every
|
|
68
|
+
// toolchain, so it also folds a latch param no `for` reads: a `do`-`while` latch every arm hands
|
|
69
|
+
// `p + 2` (KMC's `loop_elsebreak`) is spelled as the update `p = p + 2` rather than as a copy at
|
|
70
|
+
// the end of each arm. Over the synthetic tier the latch scope alone moves 16 cells, loses no
|
|
71
|
+
// match and turns no decline: closer on six (uninit_bits ido 13 -> 6, sw_loopret gcc2.7.2
|
|
72
|
+
// 81 -> 73), further on four (loop_elsebreak gcc2.7.2kmc 16 -> 23 the most), so the scope stays.
|
|
73
|
+
// A SIBLING'S STRIDE below is kept out of the pre-structure folds by the same measurement, where
|
|
74
|
+
// the moves were one way (brk2out_ctl ido 151 -> 214). `unpre.test.ts` pins an ido-shaped latch
|
|
75
|
+
// folding and the `if` join staying out.
|
|
76
|
+
// • the argument must be an op with operands (`operand-free`). An operand-free argument (a constant, a symbol's
|
|
77
|
+
// address) is what a merge of `return 0;` arms or of a register the compiler set to one constant
|
|
78
|
+
// on several paths carries, and those are spellings other passes own. A default arm that both the
|
|
79
|
+
// switch edge and a falling case hand the constant 7 (the KMC/gcc2.7.2 shape thumb-switch.test.ts
|
|
80
|
+
// pins) therefore still declines.
|
|
81
|
+
//
|
|
82
|
+
// A SIBLING'S LEAF (`recomputeDerivedParams`, its own trace stage just ahead of this one): the
|
|
83
|
+
// congruence above with one leaf allowed to differ per edge, where on every edge it is the argument
|
|
84
|
+
// that same edge hands a sibling parameter `i`. gcc 2.7.2 strength-reduces `for (i = lo; i <= hi;
|
|
85
|
+
// i++) .. gE[i] ..` to a header that carries `i << 3` beside `i`: the guard hands it `(lo, lo << 3)`
|
|
86
|
+
// and the latch `(i + 1, (i + 1) << 3)`. SOUND BY SSA, with no induction: on edge `e` the parameter
|
|
87
|
+
// `j` receives F(a_e) and `i` receives a_e, so at every entry `j` holds F(i), and reading F(i) in the
|
|
88
|
+
// block is reading `j`. F is one `clonable` op of one shape and result type on every edge; each other
|
|
89
|
+
// operand is one value every edge shares, defined in a block that strictly dominates this one, or a
|
|
90
|
+
// constant of one value (the latch's own `3` against the entry's). An argument that is not such an op
|
|
91
|
+
// on some edge (a back edge handing the parameter itself, a load, a call) leaves the parameter alone.
|
|
92
|
+
// The edge ops the fold leaves unread are deleted with whatever pure ops only they read, so a latch's
|
|
93
|
+
// `i + 1` the dead `(i + 1) << 3` was the second reader of is not named for it.
|
|
94
|
+
//
|
|
95
|
+
// UNGATED, by measurement: a loop header is no switch join, so this is exactly the widening the
|
|
96
|
+
// `not-switch-join` gate keeps from the congruent fold. Over the synthetic tier it moves only gcc
|
|
97
|
+
// 2.7.2 and KMC cells and loses no match (retpost_tail, uninit_bits, sw_h3, sw_h4 and bcpy_ind
|
|
98
|
+
// match through it; the brk2out rows lift a few insns further, with C that reads correct). It
|
|
99
|
+
// cannot be asked per loop as structure.ts's rerun asks its readings: the pre-structure folds run
|
|
100
|
+
// once, ahead of every attempt. The recomputed F(i) has no name, so a loop test that reads it reads
|
|
101
|
+
// the op of the IV that test sees, never a value one trip stale.
|
|
102
|
+
import {
|
|
103
|
+
type Block,
|
|
104
|
+
type Fn,
|
|
105
|
+
type Op,
|
|
106
|
+
type Successor,
|
|
107
|
+
type Value,
|
|
108
|
+
defOpMap,
|
|
109
|
+
dominators,
|
|
110
|
+
foldWriteOrder,
|
|
111
|
+
forwardingTarget,
|
|
112
|
+
isBodyless,
|
|
113
|
+
mkValue,
|
|
114
|
+
opShape,
|
|
115
|
+
} from '../ir/core';
|
|
116
|
+
import { deletableWhenDead, placedAt, reevalUnsafe, spelledWhenDead } from '../ir/discipline';
|
|
117
|
+
import { sameAtEntry } from '../ir/entry';
|
|
118
|
+
import { inEdgesOf, retireParam, trivialPhiValue } from '../ir/simplify';
|
|
119
|
+
import { type IrType, typeEquals } from '../ir/types';
|
|
120
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
121
|
+
|
|
122
|
+
/** two integer types of one width, which may differ only in sign */
|
|
123
|
+
const sameIntWidth = (a: IrType, b: IrType): boolean => a.kind === 'int' && b.kind === 'int' && a.width === b.width;
|
|
124
|
+
|
|
125
|
+
/** May this op be evaluated again at another program point to the same value? */
|
|
126
|
+
const clonable = (d: Op): boolean =>
|
|
127
|
+
d.opcode !== 'call' && d.results.length === 1 && !reevalUnsafe(d) && !spelledWhenDead(d) && placedAt(d) === null;
|
|
128
|
+
|
|
129
|
+
/** A parameter the precondition admits: every in-edge (but a back edge handing the parameter itself)
|
|
130
|
+
* hands it one pure expression over leaves that strictly dominate the block. */
|
|
131
|
+
export interface RedundantParam {
|
|
132
|
+
/** one in-edge is a `switch_br`'s, another comes from elsewhere, and every other comes from a block
|
|
133
|
+
* the dispatch dominates */
|
|
134
|
+
readonly switchJoin: boolean;
|
|
135
|
+
/** the block holds a loop's back edge: one of its successors is a header that dominates it */
|
|
136
|
+
readonly latchJoin: boolean;
|
|
137
|
+
/** the expression is an op with operands, not a constant or a symbol's address */
|
|
138
|
+
readonly hasOperands: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** THE SCOPE (the header's), as data: neither rule is needed for soundness — the precondition is —
|
|
142
|
+
* so neither is `sound`, and a test ablates one with `without()` to run the fold at every join.
|
|
143
|
+
* THE RESIDUE, untabled: the precondition itself (congruent arguments over `clonable` ops, leaves
|
|
144
|
+
* that strictly dominate the block). It is what the fold IS, not a refusal of it — drop any part
|
|
145
|
+
* and the recompute reads a different value — and the gates are asked only of a parameter it
|
|
146
|
+
* admits, so a census counts scope refusals, not shapes the fold could never take. */
|
|
147
|
+
export const UNPRE_GATES: readonly Gate<RedundantParam>[] = [
|
|
148
|
+
{
|
|
149
|
+
id: 'not-switch-join',
|
|
150
|
+
why: 'outside a switch or a loop latch the parameter is spellable as a variable, and recomputing it re-spells functions that match',
|
|
151
|
+
sound: false,
|
|
152
|
+
rejects: (c) => !c.switchJoin && !c.latchJoin,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'operand-free',
|
|
156
|
+
why: 'a merged constant or symbol address is a spelling the passes that merge constants own',
|
|
157
|
+
sound: false,
|
|
158
|
+
rejects: (c) => !c.hasOperands,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/** Recompute every parameter of a switch's join (a case fallen into, or the switch's exit) or of a
|
|
163
|
+
* loop's latch whose in-edges all hand it one pure expression; returns how many parameters it
|
|
164
|
+
* removed. */
|
|
165
|
+
export function recomputeRedundantParams(fn: Fn, gates: readonly Gate<RedundantParam>[] = UNPRE_GATES): number {
|
|
166
|
+
// The CFG does not change, so dominance and the in-edge index are computed once; the def map and
|
|
167
|
+
// the homes grow with each clone.
|
|
168
|
+
const dom = dominators(fn);
|
|
169
|
+
const defs = defOpMap(fn);
|
|
170
|
+
const home = new Map<Value, Block>();
|
|
171
|
+
for (const b of fn.blocks) {
|
|
172
|
+
b.params.forEach((p) => home.set(p, b));
|
|
173
|
+
for (const op of b.ops) {
|
|
174
|
+
op.results.forEach((r) => home.set(r, b));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const inEdges = inEdgesOf(fn);
|
|
178
|
+
/** a block the switch enters — a case, or by the default edge its exit — whose every other edge
|
|
179
|
+
* comes from inside the switch */
|
|
180
|
+
const switchJoin = (ins: readonly { from: Block; op: Op }[]): boolean => {
|
|
181
|
+
const dispatch = ins.find((e) => e.op.opcode === 'switch_br');
|
|
182
|
+
return (
|
|
183
|
+
dispatch !== undefined &&
|
|
184
|
+
ins.some((e) => e.from !== dispatch.from) &&
|
|
185
|
+
ins.every((e) => e.from === dispatch.from || dom.get(e.from)!.has(dispatch.from))
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
/** a block holding a back edge: a successor that dominates it is the header of a loop it closes */
|
|
189
|
+
const latchJoin = (b: Block): boolean => b.ops[b.ops.length - 1].successors.some((s) => dom.get(b)!.has(s.block));
|
|
190
|
+
const through = (v: Value) => {
|
|
191
|
+
const d = defs.get(v);
|
|
192
|
+
return d !== undefined && clonable(d);
|
|
193
|
+
};
|
|
194
|
+
/** structural congruence: one pure expression over the same leaves */
|
|
195
|
+
const congruent = (a: Value, b: Value) => sameAtEntry(defs, a, b, { entry: new Map(), through });
|
|
196
|
+
/** the edge ops a fold stopped reading, swept once no sweep removes anything */
|
|
197
|
+
const orphaned: Op[] = [];
|
|
198
|
+
let removed = 0;
|
|
199
|
+
// A recomputed parameter can make a later block's arguments equal (a second case fallen into
|
|
200
|
+
// whose argument reads the first's parameter), so sweep until a sweep removes nothing.
|
|
201
|
+
for (let changed = true; changed;) {
|
|
202
|
+
changed = false;
|
|
203
|
+
for (const b of fn.blocks.slice(1)) {
|
|
204
|
+
const ins = inEdges.get(b) ?? [];
|
|
205
|
+
const incoming = ins.map((e) => e.succ);
|
|
206
|
+
for (let k = 0; k < b.params.length;) {
|
|
207
|
+
// THE trivial-phi predicate (ir/simplify.ts), held to congruence instead of identity: a back
|
|
208
|
+
// edge handing the parameter itself is waived there, for the reason it is waived for a
|
|
209
|
+
// trivial phi.
|
|
210
|
+
const first = trivialPhiValue(incoming, k, b.params[k], congruent);
|
|
211
|
+
const d0 = first === null ? undefined : defs.get(first);
|
|
212
|
+
const clones: Op[] = [];
|
|
213
|
+
const memo = new Map<Value, Value>();
|
|
214
|
+
let leavesDominate = true;
|
|
215
|
+
const clone = (v: Value): Value => {
|
|
216
|
+
const seen = memo.get(v);
|
|
217
|
+
if (seen !== undefined) {
|
|
218
|
+
return seen;
|
|
219
|
+
}
|
|
220
|
+
const d = defs.get(v);
|
|
221
|
+
let out = v;
|
|
222
|
+
if (d !== undefined && clonable(d)) {
|
|
223
|
+
// THE ROOT TAKES THE PARAMETER'S TYPE, which every reader of the parameter was typed
|
|
224
|
+
// against: type recovery reads a block parameter off its uses (an unsigned compare makes
|
|
225
|
+
// it `u32`) and an arm's op off its operands, so the two can differ in sign, and the
|
|
226
|
+
// declaration-signedness reconciliation reads the value that stands in the parameter's
|
|
227
|
+
// place. Typed as the arm's `s32` add, a counter compared unsigned would declare `s32`
|
|
228
|
+
// and overflow (UB) where the asm's `u32` wraps (kleod's `sub_080240F4`).
|
|
229
|
+
const p = b.params[k];
|
|
230
|
+
const r = mkValue(v === first && sameIntWidth(p.type, v.type) ? p.type : v.type);
|
|
231
|
+
const op: Op = {
|
|
232
|
+
opcode: d.opcode,
|
|
233
|
+
operands: d.operands.map(clone),
|
|
234
|
+
results: [r],
|
|
235
|
+
attrs: { ...d.attrs },
|
|
236
|
+
successors: [],
|
|
237
|
+
};
|
|
238
|
+
clones.push(op);
|
|
239
|
+
out = r;
|
|
240
|
+
} else {
|
|
241
|
+
const h = home.get(v);
|
|
242
|
+
if (h === undefined || h === b || !dom.get(b)!.has(h)) {
|
|
243
|
+
leavesDominate = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
memo.set(v, out);
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
const value = first !== null && d0 !== undefined && clonable(d0) ? clone(first) : undefined;
|
|
250
|
+
if (
|
|
251
|
+
value === undefined ||
|
|
252
|
+
!leavesDominate ||
|
|
253
|
+
firstRejection(gates, {
|
|
254
|
+
switchJoin: switchJoin(ins),
|
|
255
|
+
latchJoin: latchJoin(b),
|
|
256
|
+
hasOperands: d0!.operands.length > 0,
|
|
257
|
+
}) !== null
|
|
258
|
+
) {
|
|
259
|
+
k++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
b.ops.unshift(...clones);
|
|
263
|
+
for (const op of clones) {
|
|
264
|
+
defs.set(op.results[0], op);
|
|
265
|
+
home.set(op.results[0], b);
|
|
266
|
+
}
|
|
267
|
+
for (const s of incoming) {
|
|
268
|
+
const d = defs.get(s.args[k]);
|
|
269
|
+
if (d !== undefined) {
|
|
270
|
+
orphaned.push(d);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
retireParam(fn, b, k, value, incoming);
|
|
274
|
+
removed++;
|
|
275
|
+
changed = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
sweepOrphans(fn, orphaned);
|
|
280
|
+
return removed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Is `x` the same op as `y` apart from its operands' values: one opcode, attributes, operand count
|
|
284
|
+
* and result type? */
|
|
285
|
+
const sameShape = (x: Op, y: Op): boolean =>
|
|
286
|
+
opShape(x) === opShape(y) &&
|
|
287
|
+
x.operands.length === y.operands.length &&
|
|
288
|
+
x.results.length === 1 &&
|
|
289
|
+
typeEquals(x.results[0].type, y.results[0].type);
|
|
290
|
+
|
|
291
|
+
/** Recompute every block parameter `j` whose in-edges each hand it one pure op F over the argument
|
|
292
|
+
* that edge hands a sibling parameter `i` (A SIBLING'S LEAF, in the header): `j` is F(i). Returns
|
|
293
|
+
* how many parameters it removed. */
|
|
294
|
+
export function recomputeDerivedParams(fn: Fn): number {
|
|
295
|
+
const dom = dominators(fn);
|
|
296
|
+
const defs = defOpMap(fn);
|
|
297
|
+
const home = new Map<Value, Block>();
|
|
298
|
+
for (const b of fn.blocks) {
|
|
299
|
+
b.params.forEach((p) => home.set(p, b));
|
|
300
|
+
for (const op of b.ops) {
|
|
301
|
+
op.results.forEach((r) => home.set(r, b));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const inEdges = inEdgesOf(fn);
|
|
305
|
+
/** the edge ops a fold stopped reading, swept once every block is done */
|
|
306
|
+
const orphaned: Op[] = [];
|
|
307
|
+
let removed = 0;
|
|
308
|
+
for (const b of fn.blocks.slice(1)) {
|
|
309
|
+
const ins = inEdges.get(b) ?? [];
|
|
310
|
+
if (ins.length < 2) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const incoming = ins.map((e) => e.succ);
|
|
314
|
+
/** F(i) as fresh ops at the head of `b`, or null where `j` is no one op of `i`'s arguments */
|
|
315
|
+
const derive = (j: number): { ops: Op[]; value: Value } | null => {
|
|
316
|
+
const ds = incoming.map((s) => defs.get(s.args[j]));
|
|
317
|
+
const d0 = ds[0];
|
|
318
|
+
if (d0 === undefined || !ds.every((d) => d !== undefined && clonable(d) && sameShape(d, d0))) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
for (let i = 0; i < b.params.length; i++) {
|
|
322
|
+
if (i === j) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const ivSlot = d0.operands.map((_, k) => incoming.every((s, x) => ds[x]!.operands[k] === s.args[i]));
|
|
326
|
+
if (!ivSlot.some(Boolean)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const ops: Op[] = [];
|
|
330
|
+
const operands: Value[] = [];
|
|
331
|
+
const ok = d0.operands.every((o, k) => {
|
|
332
|
+
if (ivSlot[k]) {
|
|
333
|
+
operands.push(b.params[i]);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
const vals = ds.map((d) => d!.operands[k]);
|
|
337
|
+
if (vals.every((v) => v === o)) {
|
|
338
|
+
// one value every edge shares, which must hold that value here too
|
|
339
|
+
const h = home.get(o);
|
|
340
|
+
operands.push(o);
|
|
341
|
+
return h !== undefined && h !== b && dom.get(b)!.has(h);
|
|
342
|
+
}
|
|
343
|
+
// or a constant of one value on every edge, made again here
|
|
344
|
+
const cs = vals.map((v) => defs.get(v));
|
|
345
|
+
const c0 = cs[0];
|
|
346
|
+
if (c0?.opcode !== 'const' || !cs.every((c) => c !== undefined && c.opcode === 'const' && sameShape(c, c0))) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
const r = mkValue(o.type);
|
|
350
|
+
ops.push({ opcode: 'const', operands: [], results: [r], attrs: { ...c0.attrs }, successors: [] });
|
|
351
|
+
operands.push(r);
|
|
352
|
+
return true;
|
|
353
|
+
});
|
|
354
|
+
if (!ok) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const r = mkValue(d0.results[0].type);
|
|
358
|
+
ops.push({ opcode: d0.opcode, operands, results: [r], attrs: { ...d0.attrs }, successors: [] });
|
|
359
|
+
return { ops, value: r };
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
};
|
|
363
|
+
for (let j = 0; j < b.params.length;) {
|
|
364
|
+
const got = derive(j);
|
|
365
|
+
if (got === null) {
|
|
366
|
+
j++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
orphaned.push(...incoming.map((s) => defs.get(s.args[j])!));
|
|
370
|
+
b.ops.unshift(...got.ops);
|
|
371
|
+
for (const op of got.ops) {
|
|
372
|
+
defs.set(op.results[0], op);
|
|
373
|
+
home.set(op.results[0], b);
|
|
374
|
+
}
|
|
375
|
+
retireParam(fn, b, j, got.value, incoming);
|
|
376
|
+
removed++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
sweepOrphans(fn, orphaned);
|
|
380
|
+
return removed;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Delete each of `seeds` nothing reads any more, and then each pure op only the deleted read: never
|
|
384
|
+
* an op the fold did not orphan, which other passes own. */
|
|
385
|
+
function sweepOrphans(fn: Fn, seeds: readonly Op[]): void {
|
|
386
|
+
const defs = defOpMap(fn);
|
|
387
|
+
const uses = new Map<Value, number>();
|
|
388
|
+
const read = (v: Value, by: number) => uses.set(v, (uses.get(v) ?? 0) + by);
|
|
389
|
+
for (const b of fn.blocks) {
|
|
390
|
+
for (const op of b.ops) {
|
|
391
|
+
op.operands.forEach((o) => read(o, 1));
|
|
392
|
+
op.successors.forEach((s) => s.args.forEach((a) => read(a, 1)));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const dead = new Set<Op>();
|
|
396
|
+
const work = [...seeds];
|
|
397
|
+
while (work.length > 0) {
|
|
398
|
+
const op = work.pop()!;
|
|
399
|
+
if (dead.has(op) || !clonable(op) || !deletableWhenDead(op) || (uses.get(op.results[0]) ?? 0) > 0) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
dead.add(op);
|
|
403
|
+
for (const o of op.operands) {
|
|
404
|
+
read(o, -1);
|
|
405
|
+
const d = defs.get(o);
|
|
406
|
+
if (d !== undefined) {
|
|
407
|
+
work.push(d);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (dead.size === 0) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const emptied: Block[] = [];
|
|
415
|
+
for (const b of fn.blocks) {
|
|
416
|
+
const kept = b.ops.filter((op) => !dead.has(op));
|
|
417
|
+
if (kept.length < b.ops.length && b !== fn.blocks[0] && isBodyless({ params: b.params, ops: kept })) {
|
|
418
|
+
emptied.push(b);
|
|
419
|
+
}
|
|
420
|
+
b.ops = kept;
|
|
421
|
+
}
|
|
422
|
+
threadEmptied(fn, emptied);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Splice out each of `emptied` that the sweep left a transparent forwarder (no params, a lone `br`
|
|
426
|
+
* handing no value), every edge into it now going where it forwards.
|
|
427
|
+
*
|
|
428
|
+
* THE PASS THAT EMPTIES A BLOCK THREADS IT. Such a block held nothing but the edge copies a fold
|
|
429
|
+
* made redundant, so it stands for no code of the source: KMC's default arm that held only the
|
|
430
|
+
* pointer step (`synthetic:sw_gotocase2:gcc2.7.2kmc`), and each arm of IDO's
|
|
431
|
+
* `synthetic:forexit_old:ido7.1` that held only its copy of the latch's bound `hi + 1`. Left in the
|
|
432
|
+
* CFG, the structurer reads it as one more sibling case every `continue` falls into, or one more
|
|
433
|
+
* region on the way to the latch. An edge into it carries no args (it has no params) and its own
|
|
434
|
+
* carries none, so retargeting changes no value on any path;
|
|
435
|
+
* `foldExitTrampolines` (raise/latch.ts) folds the same block on a loop's exit, for its own scope,
|
|
436
|
+
* and this one asks no scope because the fold made it, not the compiler. A forwarder whose chain
|
|
437
|
+
* ends in a cycle of forwarders (`while (1);`) stays. */
|
|
438
|
+
function threadEmptied(fn: Fn, emptied: readonly Block[]): void {
|
|
439
|
+
const forwards = (b: Block) => isBodyless(b) && b.ops[0].opcode === 'br' && b.ops[0].successors[0].args.length === 0;
|
|
440
|
+
const gone = new Set<Block>();
|
|
441
|
+
for (const b of emptied) {
|
|
442
|
+
const onward = forwardingTarget(b);
|
|
443
|
+
if (!forwards(b) || forwards(onward)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
for (const p of fn.blocks) {
|
|
447
|
+
let repointed = false;
|
|
448
|
+
for (const op of p.ops) {
|
|
449
|
+
op.successors.forEach((s, i) => {
|
|
450
|
+
if (s.block === b) {
|
|
451
|
+
op.successors[i] = { block: onward, args: [] };
|
|
452
|
+
repointed = true;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (repointed) {
|
|
457
|
+
foldWriteOrder(fn.writeOrder, b, p);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
gone.add(b);
|
|
461
|
+
}
|
|
462
|
+
if (gone.size > 0) {
|
|
463
|
+
fn.blocks = fn.blocks.filter((b) => !gone.has(b));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// A SIBLING'S STRIDE (`strideDerivedParams`): the fact `recomputeDerivedParams` folds — a header
|
|
468
|
+
// parameter `j` holds G(i) at every entry, `i` a sibling — proved over a loop's two edges by
|
|
469
|
+
// INDUCTION where the back edge hands `j + K` rather than G of the argument it hands `i`. IDO
|
|
470
|
+
// strength-reduces `gE[i]` in `for (i = lo; i <= hi; i++)` (`synthetic:sw_gotocase2:ido7.1`) to a
|
|
471
|
+
// pointer that starts at `gE + (lo << 3)` and steps by 8 beside `i`: the entry hands the header
|
|
472
|
+
// `(gE + (lo << 3), lo)` and the back edge `(p + 8, i + 1)`. G is `B + T(x)` or `T(x)`, T one of
|
|
473
|
+
// `x << s`, `x * M` (M a constant) or `x` (the last only beside a base), and B one value from outside
|
|
474
|
+
// the loop. The entry hands `j` exactly G(x0), x0 being what it hands `i`; the back edge hands `i + D`
|
|
475
|
+
// and `j + K` with K = T(D). T is linear in 32-bit words, so G(i + D) = G(i) + T(D) with no overflow
|
|
476
|
+
// argument: if `j` = G(i) on one entry, it is G(i + D) = G(i') on the next.
|
|
477
|
+
//
|
|
478
|
+
// A FACT, NOT A FOLD, by measurement: recomputed in the header ahead of every structuring, as
|
|
479
|
+
// `recomputeDerivedParams` recomputes its params, it re-spells every IDO loop that stays `while (1)`
|
|
480
|
+
// as an index recomputed per trip, and those lift further from the bytes (synthetic:brk2out_ctl:ido7.1
|
|
481
|
+
// 151/176 -> 214/224, forexit_old 163 -> 173). So the structurer reads it only where it spells the
|
|
482
|
+
// loop as a `for` (structure.ts `forPlanAt`, A SIBLING'S STRIDE), which writes `j` nowhere and reads
|
|
483
|
+
// G(i) at each of its uses. The caller states which values are from outside the loop.
|
|
484
|
+
|
|
485
|
+
/** A header parameter that is G of a sibling at every entry (A SIBLING'S STRIDE). */
|
|
486
|
+
export interface StrideDerived {
|
|
487
|
+
/** the derived parameter's slot in the header */
|
|
488
|
+
readonly j: number;
|
|
489
|
+
/** the sibling's (the counter's) slot */
|
|
490
|
+
readonly i: number;
|
|
491
|
+
/** G's ops as the entry edge computes them, innermost first; the last one's result is G(x0) */
|
|
492
|
+
readonly ops: readonly Op[];
|
|
493
|
+
/** what the entry hands the sibling: the one leaf of G that is not from outside the loop */
|
|
494
|
+
readonly x0: Value;
|
|
495
|
+
/** G's other operands (the base, a multiplier, a shift amount), each from outside the loop */
|
|
496
|
+
readonly leaves: readonly Value[];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Every header parameter that is G of a sibling at each entry, read over the loop's one entry edge
|
|
500
|
+
* and one back edge (A SIBLING'S STRIDE). `outside` says a value is defined in a block that strictly
|
|
501
|
+
* dominates the header. */
|
|
502
|
+
export function strideDerivedParams(
|
|
503
|
+
defs: ReadonlyMap<Value, Op>,
|
|
504
|
+
header: Block,
|
|
505
|
+
entry: Successor,
|
|
506
|
+
back: Successor,
|
|
507
|
+
outside: (v: Value) => boolean,
|
|
508
|
+
): StrideDerived[] {
|
|
509
|
+
const constOf = (v: Value): number | null => {
|
|
510
|
+
const d = defs.get(v);
|
|
511
|
+
return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value | 0 : null;
|
|
512
|
+
};
|
|
513
|
+
/** `v` is `p + c`, `c` a constant: `c` */
|
|
514
|
+
const stepOf = (v: Value, p: Value): number | null => {
|
|
515
|
+
const d = defs.get(v);
|
|
516
|
+
if (d?.opcode !== 'add' || d.operands.length !== 2) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const [a, b] = d.operands;
|
|
520
|
+
return a === p ? constOf(b) : b === p ? constOf(a) : null;
|
|
521
|
+
};
|
|
522
|
+
/** `v` is T(x0): its ops, its constant operands, and T(d) */
|
|
523
|
+
const term = (v: Value, x0: Value, d: number): { ops: Op[]; leaves: Value[]; k: number } | null => {
|
|
524
|
+
if (v === x0) {
|
|
525
|
+
return { ops: [], leaves: [], k: d };
|
|
526
|
+
}
|
|
527
|
+
const t = defs.get(v);
|
|
528
|
+
if (t === undefined || !clonable(t) || (t.opcode === 'shl' && t.operands[0] !== x0)) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
if (t.opcode === 'shl') {
|
|
532
|
+
const amount = t.operands.length === 1 ? t.attrs.imm : t.operands.length === 2 ? constOf(t.operands[1]) : null;
|
|
533
|
+
return typeof amount !== 'number' || amount < 0 || amount > 31
|
|
534
|
+
? null
|
|
535
|
+
: { ops: [t], leaves: t.operands.slice(1), k: (d << amount) | 0 };
|
|
536
|
+
}
|
|
537
|
+
if (t.opcode === 'mul' && t.operands.length === 2) {
|
|
538
|
+
const at = t.operands.indexOf(x0);
|
|
539
|
+
const m = at < 0 ? null : constOf(t.operands[1 - at]);
|
|
540
|
+
return m === null ? null : { ops: [t], leaves: [t.operands[1 - at]], k: Math.imul(d, m) };
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
};
|
|
544
|
+
/** `j` as G of the sibling `i`, or null */
|
|
545
|
+
const fit = (j: number, i: number): StrideDerived | null => {
|
|
546
|
+
const k = stepOf(back.args[j], header.params[j]);
|
|
547
|
+
const d = i === j ? null : stepOf(back.args[i], header.params[i]);
|
|
548
|
+
if (k === null || d === null || d === 0) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const x0 = entry.args[i];
|
|
552
|
+
const g = entry.args[j];
|
|
553
|
+
let found: { ops: Op[]; leaves: Value[]; k: number } | null = null;
|
|
554
|
+
const bare = term(g, x0, d);
|
|
555
|
+
if (bare !== null && bare.ops.length > 0) {
|
|
556
|
+
found = bare;
|
|
557
|
+
} else {
|
|
558
|
+
const add = defs.get(g);
|
|
559
|
+
if (add?.opcode === 'add' && add.operands.length === 2 && clonable(add)) {
|
|
560
|
+
for (const at of [0, 1]) {
|
|
561
|
+
const t = term(add.operands[at], x0, d);
|
|
562
|
+
const base = add.operands[1 - at];
|
|
563
|
+
if (t !== null && base !== x0 && outside(base)) {
|
|
564
|
+
found = { ops: [...t.ops, add], leaves: [...t.leaves, base], k: t.k };
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return found !== null && found.k === k && found.leaves.every(outside)
|
|
571
|
+
? { j, i, ops: found.ops, x0, leaves: found.leaves }
|
|
572
|
+
: null;
|
|
573
|
+
};
|
|
574
|
+
const slots = header.params.map((_, x) => x);
|
|
575
|
+
// A sibling that is itself G of another is asked last. The one reader, structure.ts's `for`, drops
|
|
576
|
+
// every slot in `strides` from its copies, so no such slot is the counter its update writes, and it
|
|
577
|
+
// takes a stride only as G of that counter's param: asking the other slots first asks exactly the
|
|
578
|
+
// slots that can be the counter, and a pair asked later is one the `for` would refuse anyway, so the
|
|
579
|
+
// order turns no lift into a decline. (KMC strength-reduces both `i << 3` and `gE + (i << 3)` beside
|
|
580
|
+
// `i`, and the pointer is `x + gE` of the shift as well as G of `i`.)
|
|
581
|
+
const strides = new Set(slots.filter((j) => slots.some((i) => fit(j, i) !== null)));
|
|
582
|
+
const order = [...slots.filter((x) => !strides.has(x)), ...slots.filter((x) => strides.has(x))];
|
|
583
|
+
const out: StrideDerived[] = [];
|
|
584
|
+
for (const j of strides) {
|
|
585
|
+
for (const i of order) {
|
|
586
|
+
const sd = fit(j, i);
|
|
587
|
+
if (sd !== null) {
|
|
588
|
+
out.push(sd);
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return out;
|
|
594
|
+
}
|