@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- 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 +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- 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 +28 -25
- 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/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- 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/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- 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 +342 -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 +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- 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/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/raise/const.ts
CHANGED
|
@@ -3,17 +3,33 @@
|
|
|
3
3
|
// A RISC target builds a 32-bit literal in two halves: a high-half load (MIPS `lui`, PPC `lis`) then a
|
|
4
4
|
// low-half `ori`/`addiu`. The frontends lift that pair faithfully as `or(const(hi<<16), const(lo))` /
|
|
5
5
|
// `add(const(hi<<16), const(lo))` — a live binary op over two `const` ops — because neither frontend can
|
|
6
|
-
// see across the two instructions. This pass folds
|
|
6
|
+
// see across the two instructions. This pass folds such a const/const `or`/`add` into a single `const`,
|
|
7
7
|
// which is the form that (a) type-recovers as one 32-bit literal and (b) recompiles to the exact
|
|
8
8
|
// `lui;ori` / `lis;ori` pair. Without it a magic-division reciprocal or an address literal is never a
|
|
9
9
|
// single value the later passes can reason about.
|
|
10
10
|
//
|
|
11
|
+
// THE CLIENTELE IS "A VALUE THE TARGET MATERIALISES IN TWO INSTRUCTIONS", and it is wider than the
|
|
12
|
+
// RISC pair above in both directions, and both halves are measured:
|
|
13
|
+
// - It is NOT ARM-free. On Thumb a literal is a pool word plus an immediate `add`, which is exactly a
|
|
14
|
+
// two-instruction materialisation and lifts as the same const/const pair. Ablating the whole pass
|
|
15
|
+
// costs three agbcc BYTE-MATCHES — `synthetic:dmafield` MATCH -> diff:29, `synthetic:fieldbase`
|
|
16
|
+
// MATCH -> diff:22, `synthetic:bgfixed` MATCH -> diff:2 (measured, whole pass off). Do not gate
|
|
17
|
+
// this pass on a RISC target.
|
|
18
|
+
// - The pair is NOT confined to ONE block. `synthetic:mergepool:gcc2.7.2kmc` lifts a genuine
|
|
19
|
+
// `lui;ori` as `or(const 65536, const 9029)` whose two halves are defined in DIFFERENT blocks
|
|
20
|
+
// (instrumented: `b0=0 b1=1`), because gcc hoisted the high half above the branch. A guard
|
|
21
|
+
// tightened to a literal same-block test loses folds the corpus depends on.
|
|
22
|
+
// What the refusal below actually excludes is narrower than either: an operand a terminator hands to a
|
|
23
|
+
// successor's block-parameter — a REGISTER the compiler held live across a branch — where the pair is
|
|
24
|
+
// ALSO not recognisable as a hi/lo pair and the result is not an address. See its site for the cost.
|
|
25
|
+
//
|
|
11
26
|
// This cannot be a data-`RewritePattern`: the fold's result is COMPUTED from the two operands' values,
|
|
12
27
|
// which the pattern engine's numeric-exact `attrEquals` cannot express. So it lives here as an always-on
|
|
13
28
|
// recognizer, run before type recovery. Value-preserving and local; a single left-to-right pass suffices
|
|
14
29
|
// (SSA guarantees each const is defined before the op that consumes it, and a folded result feeds
|
|
15
30
|
// forward for any chained materialisation).
|
|
16
|
-
import { Fn, Op, defOpMap, mkOp } from '../ir/core';
|
|
31
|
+
import { Fn, Op, Value, defOpMap, mkOp } from '../ir/core';
|
|
32
|
+
import { MEM_BASE_OPS } from '../ir/opcodes';
|
|
17
33
|
|
|
18
34
|
// The binary opcodes whose const/const form is a constant. `>> 0` normalises to a signed 32-bit result
|
|
19
35
|
// (hardware wraparound): `|` already yields int32, `+` may exceed it and is truncated to match `addu`/`add`.
|
|
@@ -22,11 +38,123 @@ const FOLD: Record<string, (a: number, b: number) => number> = {
|
|
|
22
38
|
add: (a, b) => (a + b) >> 0,
|
|
23
39
|
};
|
|
24
40
|
|
|
41
|
+
/** Is `v` a RISC HIGH HALF — what one `lui`/`lis`/`addis` puts in a register? `hi << 16`, for a
|
|
42
|
+
* NON-ZERO `hi`. Zero is excluded deliberately, and that exclusion is what makes this test
|
|
43
|
+
* trustworthy as a positive: `lui rD, 0` is a no-op no compiler emits, so a `const 0` is never a
|
|
44
|
+
* half being materialised — it is an initialised register, exactly the shape the refusal below
|
|
45
|
+
* exists to protect.
|
|
46
|
+
*
|
|
47
|
+
* IT COVERS THE RISC HALF OF THE CLIENTELE ONLY, because the 16/16 split is an ISA fact: censused
|
|
48
|
+
* over the corpus's const/const fold sites, all 19 RISC ones (gcc2.7.2kmc / mwcc_242_81 / ido7.1)
|
|
49
|
+
* are recognisable to it and 0 of the 20 agbcc ones are — an ARM pool word is an arbitrary 32-bit
|
|
50
|
+
* value (`0x03001C00`, low half `0x1C00`) and can never pass. The ARM half of the clientele is
|
|
51
|
+
* protected by `memBases` and by simply not being edge-carried; four agbcc sites on three rows
|
|
52
|
+
* (`dmascope` ×2, `dmascope2`, `dmafield`'s `add(…,112)`) sit behind neither carve-out. If a row
|
|
53
|
+
* ever needs the buy-back on ARM, the half-width belongs on the target description —
|
|
54
|
+
* `PreRecoveryPass` already threads `target`, and `softdiv` gates on `capabilities.hwDivide` —
|
|
55
|
+
* rather than as a second constant here. */
|
|
56
|
+
const isHighHalf = (v: number): boolean => (v & 0xffff) === 0 && v !== 0;
|
|
57
|
+
|
|
58
|
+
/** Is `v` a LOW HALF — what one `ori`/`addi`/`addiu` can supply? `ori` takes an UNSIGNED 16-bit
|
|
59
|
+
* immediate and `addi`/`addiu` a SIGNED one, so the admissible range is the union: mwcc completes an
|
|
60
|
+
* `addis` with a NEGATIVE `addi` whenever bit 15 of the low half is set (`0x12350000 + -25924` is
|
|
61
|
+
* `0x12345ABC`). Deliberately NOT split per-opcode (`or` unsigned, `add` signed): that narrowing is
|
|
62
|
+
* true of the ISA, reaches 0 rows of the corpus, and no fixture reddens when it is removed, so it
|
|
63
|
+
* would be an unpinned clause. The half-width belongs on the target description if a row needs it. */
|
|
64
|
+
const isLowHalf = (v: number): boolean => v >= -0x8000 && v <= 0xffff;
|
|
65
|
+
|
|
66
|
+
/** The constant a foldable const/const pair denotes, or `null` when `opcode` is not one this pass
|
|
67
|
+
* folds. Exported because `structure.ts` repairs the refusal's residue at RENDER time and must
|
|
68
|
+
* print exactly what an unrefused fold here would have produced — the opcode set and the int32
|
|
69
|
+
* normalisation are one decision, so they live in one place. */
|
|
70
|
+
export function foldConstPair(opcode: string, a: number, b: number): number | null {
|
|
71
|
+
const f = FOLD[opcode];
|
|
72
|
+
return f ? f(a, b) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Does this opcode's const/const form denote a constant? The membership half of `foldConstPair`,
|
|
76
|
+
* for callers that must classify an op before they have its operands' values. */
|
|
77
|
+
export const isConstFoldOpcode = (opcode: string): boolean => opcode in FOLD;
|
|
78
|
+
|
|
79
|
+
/** THE EVIDENCE THE FOLD WOULD OTHERWISE DESTROY, stamped on the literal it produces.
|
|
80
|
+
*
|
|
81
|
+
* Three Thumb shapes put two accesses a constant distance apart, and the assembly tells them
|
|
82
|
+
* apart: two pool words (two independent `const` ops), one pool word plus memory-operand
|
|
83
|
+
* displacements (`l3/ast.ts`'s `operandOff`), and one pool word plus an `add` to the register
|
|
84
|
+
* that already held the first address (`ldr r3,=X; strh [r3]; adds r3,#2; strh [r3]`). The third
|
|
85
|
+
* lifts as `add(const X, const 2)` — indistinguishable, once folded, from a literal `X + 2` the
|
|
86
|
+
* compiler materialised in two instructions, which is this pass's whole clientele. So the fold
|
|
87
|
+
* still happens and the distinction is recorded, exactly as `structure.ts` records `operandOff`
|
|
88
|
+
* before its own fold (see the `operandOff` note in l3/ast.ts).
|
|
89
|
+
*
|
|
90
|
+
* WHAT IT ASSERTS is narrow and is a fact about REGISTERS, not about C: at this instruction the
|
|
91
|
+
* machine held the base address in a register, used it as an address, and advanced it by `step`
|
|
92
|
+
* bytes to reach another address it also used. THE READER decides what to spell — `l3/advance.ts`
|
|
93
|
+
* offers a pointer local advanced in place, which is the only C spelling that reproduces the
|
|
94
|
+
* `add` on a compiler that folds a constant subscript into the memory operand, and which is
|
|
95
|
+
* INERT (same bytes as the subscript) wherever the pointee is not `volatile`.
|
|
96
|
+
*
|
|
97
|
+
* THE THREE REFUSALS, each of which makes the stamp mean something else:
|
|
98
|
+
* • `or`, not `add` — a hi/lo `or` is a literal being assembled, never a pointer being moved.
|
|
99
|
+
* • the ADDEND is itself a memory base. Then both halves are addresses and neither is the step;
|
|
100
|
+
* the shape is not an advance and the stamp would name an arbitrary one of them.
|
|
101
|
+
* • either the base or the result is never used AS AN ADDRESS. A register that only feeds
|
|
102
|
+
* arithmetic is a value, and `X + 2` over two values is a literal by every reading.
|
|
103
|
+
* A zero step is dropped too: it names no advance, and `l3/ast.ts`'s readers all test
|
|
104
|
+
* `!== undefined` rather than truthiness, so a stamped 0 would read as a real one. */
|
|
105
|
+
function advanceEvidence(op: Op, a: number, c: number, memBases: ReadonlySet<Value>): { advancedBy?: number } {
|
|
106
|
+
if (op.opcode !== 'add' || !memBases.has(op.results[0])) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
const baseIdx = memBases.has(op.operands[0]) ? 0 : memBases.has(op.operands[1]) ? 1 : -1;
|
|
110
|
+
if (baseIdx < 0 || memBases.has(op.operands[1 - baseIdx])) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
const step = baseIdx === 0 ? c : a;
|
|
114
|
+
return step === 0 ? {} : { advancedBy: step };
|
|
115
|
+
}
|
|
116
|
+
|
|
25
117
|
/** Fold each const/const `or`/`add` into one `const`, in place. Returns whether anything changed. The
|
|
26
118
|
* now-dead source consts are left for DCE (they may still have other uses; liveness is not our concern). */
|
|
27
119
|
export function recognizeConsts(fn: Fn): boolean {
|
|
28
120
|
let changed = false;
|
|
29
121
|
const defs = defOpMap(fn);
|
|
122
|
+
// The two facts the CLIENTELE REFUSAL below reads, both collected in one walk.
|
|
123
|
+
// `edgeSlots` — for every value a terminator hands to a successor, WHICH block-parameters it
|
|
124
|
+
// feeds. Being in the map at all is "a register the compiler held live across a branch": in
|
|
125
|
+
// functional-form SSA the machine had this value in a register at the branch and the join
|
|
126
|
+
// reads it back. WHICH parameter is the second question, and the buy-back below needs it.
|
|
127
|
+
// `memBases` — every value used as a memory base, i.e. the values that ARE addresses.
|
|
128
|
+
const edgeSlots = new Map<Value, Set<Value>>();
|
|
129
|
+
const memBases = new Set<Value>();
|
|
130
|
+
for (const b of fn.blocks) {
|
|
131
|
+
for (const op of b.ops) {
|
|
132
|
+
for (const sc of op.successors) {
|
|
133
|
+
sc.args.forEach((v, i) => {
|
|
134
|
+
const param = sc.block.params[i];
|
|
135
|
+
if (param === undefined) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const slots = edgeSlots.get(v);
|
|
139
|
+
if (slots) {
|
|
140
|
+
slots.add(param);
|
|
141
|
+
} else {
|
|
142
|
+
edgeSlots.set(v, new Set([param]));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (MEM_BASE_OPS.has(op.opcode) && op.operands.length > 0) {
|
|
147
|
+
memBases.add(op.operands[0]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Do these two values reach the SAME block-parameter — i.e. are they two arms' feeds of one
|
|
152
|
+
* merge? See the refusal for why that is what tells an accumulator from a shared high half. */
|
|
153
|
+
const meetAtSameParam = (x: Value, y: Value): boolean => {
|
|
154
|
+
const sx = edgeSlots.get(x);
|
|
155
|
+
const sy = edgeSlots.get(y);
|
|
156
|
+
return !!sx && !!sy && [...sx].some((k) => sy.has(k));
|
|
157
|
+
};
|
|
30
158
|
const constOf = (op: Op | undefined): number | null =>
|
|
31
159
|
op && op.opcode === 'const' ? (op.attrs.value as number) : null;
|
|
32
160
|
for (const b of fn.blocks) {
|
|
@@ -41,8 +169,80 @@ export function recognizeConsts(fn: Fn): boolean {
|
|
|
41
169
|
if (a === null || c === null) {
|
|
42
170
|
continue;
|
|
43
171
|
}
|
|
172
|
+
// ── THE REFUSAL: this shape is not a literal being materialised ───────────────────────────
|
|
173
|
+
// An operand a terminator ALSO hands to a successor's block-parameter is a register the compiler
|
|
174
|
+
// held live across the branch, whose value on this path happens to be a constant. agbcc's
|
|
175
|
+
// `s = 0; ... if (c) s += 1;` lifts as `add(%s = const 0, const 1)` in the taken arm, where
|
|
176
|
+
// `%s` is also the value bb0 hands the join. Folding it to `const 1` deletes the accumulator's
|
|
177
|
+
// last reference, so every later level sees an arm that materialises a literal and spells it
|
|
178
|
+
// as one (`v = 1;` with an `else v = 0;`) instead of the `s += 1` the target records — and the
|
|
179
|
+
// enumeration gate for the shipped `/merge-home` variation, which is what would have spelled the
|
|
180
|
+
// hoisted init, reads FALSE because the merge feed it looks for is gone.
|
|
181
|
+
//
|
|
182
|
+
// The mapping is a FUNCTION, not an open question, so this is a default and not a variation: a register
|
|
183
|
+
// carried across a branch is not a literal being materialised, whichever compiler produced it.
|
|
184
|
+
//
|
|
185
|
+
// IT IS A PROXY, and the two carve-outs are where it is bought back. Edge-carrying is evidence
|
|
186
|
+
// of a register, not proof, and the same `add(const 0, const K)` still folds wherever nothing
|
|
187
|
+
// carries the zero (6 sites on 5 marioparty3/snowboardkids2 rows) — defensible, since with no
|
|
188
|
+
// merge there is no home to hoist and the incident cannot occur, but the rule is narrower than
|
|
189
|
+
// "never fold a const/const pair over a branch".
|
|
190
|
+
// - `hiLoPair` — the pass's OWN clientele beats the proxy. mwcc materialises `0x12345678` as
|
|
191
|
+
// `lis; addi` and shares the `lis` across a branch whenever the high half is live at the
|
|
192
|
+
// join, so the genuine pair IS edge-carried and the proxy refuses it: measured, a
|
|
193
|
+
// `base = 0x12340000; if (c) q[0] = base|0x5678; else q[1] = base|0x9ABC; *p = base;` row
|
|
194
|
+
// emitted `*a2 = 305397760 + 22136;` on mwcc_242_81 where every other toolchain emitted the
|
|
195
|
+
// folded literal. The literal is then no longer ONE value for `recognizeMagicDivision`, type
|
|
196
|
+
// recovery or the symbol map — the same never-enumerated failure this refusal exists to fix,
|
|
197
|
+
// one level down.
|
|
198
|
+
//
|
|
199
|
+
// `feedsSameMerge` IS A CONDITION ON THAT BUY-BACK: read as a bare "is this a hi/lo pair"
|
|
200
|
+
// it re-opens the very incident this refusal exists for. An accumulator's `const 0` init
|
|
201
|
+
// passes `isLowHalf`, so `s = 0; if (c) s += 0x10000;` — one 16.16 fixed-point step — is a
|
|
202
|
+
// hi/lo pair by the letter of the test, and its MIRROR `s = 0x10000; if (c) s += 1;` is
|
|
203
|
+
// one whichever operand is read as the half. Folded, a two-arm `s += 0x10000` row scores
|
|
204
|
+
// **diff:1** on mwcc_242_81 with `hasMergeFeedHome` FALSE; refused, **MATCH** with the
|
|
205
|
+
// gate TRUE. What separates the two shapes is WHERE THE VALUES GO rather than their bit
|
|
206
|
+
// patterns: an accumulator's init and its updated copy are two arms' feeds of ONE block
|
|
207
|
+
// parameter — exactly the merge `/merge-home` exists to home — whereas a shared high half
|
|
208
|
+
// reaches the join while the COMPLETED literal is stored or returned, never merged with
|
|
209
|
+
// the half it was built from. Measured FREE: with it in, 770 synthetic + 252 real rows are
|
|
210
|
+
// byte-identical in post-recovery IR, in emitted source and in gap list to the branch
|
|
211
|
+
// without it.
|
|
212
|
+
// - `memBases` — an address literal, `0x03001C00 + 1206` reached through one arm's base
|
|
213
|
+
// register. It decides 0 folds over the corpus's 806 lifted rows and is here as a
|
|
214
|
+
// statement of scope; `hiLoPair` is the clause that carries real traffic. Deliberately NOT
|
|
215
|
+
// transitive and NOT extended to call arguments: an address literal escaping as a call
|
|
216
|
+
// argument is a shape the refusal HELPS (a probe row scored diff:7 -> MATCH with it
|
|
217
|
+
// firing), so widening this test would give that back.
|
|
218
|
+
//
|
|
219
|
+
// NOT CONVERTIBLE TO A `Gate` TABLE, and the reason is the type rather than the minutes.
|
|
220
|
+
// `firstRejection` reads a table as a DISJUNCTION of independent refusals; the test below is
|
|
221
|
+
// a CONJUNCTION with two buy-backs, and "refuse unless exempted" decomposes only by making
|
|
222
|
+
// every term re-carry the whole conjunction. The census would then report one opaque id and
|
|
223
|
+
// stay silent about WHICH exemption fired — the only question the paragraphs above ask. Said
|
|
224
|
+
// here because a selector that ranks passes by refusal count or by instrument minutes points
|
|
225
|
+
// at this file (`grep -n "raise/const.ts" docs/level-tower.md`), and both readings are wrong
|
|
226
|
+
// about it for different reasons.
|
|
227
|
+
//
|
|
228
|
+
// A REFUSAL IS ALSO A SCHEDULING DECISION, and that coupling is invisible at this site:
|
|
229
|
+
// `pre-recovery.ts` registers this pass `dce: true` and runs `dce(fn)` only when the pass
|
|
230
|
+
// returns TRUTHY, so a function whose ONLY const/const pair is refused gets no DCE here at
|
|
231
|
+
// all — and `addrnum` above is `dce: false`, so this is the first pass that can schedule one.
|
|
232
|
+
// Measured inert: forcing the cancelled `dce(fn)` removes 0 ops on every invocation of every
|
|
233
|
+
// reachable row (`sinkacc`, 3 invocations). Worth knowing before this refusal is widened.
|
|
234
|
+
const edgeCarried = edgeSlots.has(op.operands[0]) || edgeSlots.has(op.operands[1]);
|
|
235
|
+
const feedsSameMerge =
|
|
236
|
+
meetAtSameParam(op.results[0], op.operands[0]) || meetAtSameParam(op.results[0], op.operands[1]);
|
|
237
|
+
const hiLoPair = ((isHighHalf(a) && isLowHalf(c)) || (isHighHalf(c) && isLowHalf(a))) && !feedsSameMerge;
|
|
238
|
+
if (edgeCarried && !hiLoPair && !memBases.has(op.results[0])) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
44
241
|
// Reuse the SAME result Value → every existing use already points at it (no RAUW needed).
|
|
45
|
-
const folded = mkOp('const', {
|
|
242
|
+
const folded = mkOp('const', {
|
|
243
|
+
results: [op.results[0]],
|
|
244
|
+
attrs: { value: fold(a, c), ...advanceEvidence(op, a, c, memBases) },
|
|
245
|
+
});
|
|
46
246
|
b.ops.splice(i, 1, folded);
|
|
47
247
|
defs.set(op.results[0], folded); // keep the def map current so a chained fold sees this const
|
|
48
248
|
changed = true;
|
package/src/raise/divpow2.ts
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
// SELF-VERIFYING. asmlift emits a plain `x / 2^k` and the target compiler regenerates ITS own
|
|
32
32
|
// lowering; a wrong divisor recompiles to different bytes and shows up as a nonmatch, never as a
|
|
33
33
|
// false match. The residual exposure is a lost match, not a miscompile — on a compiler that lowers
|
|
34
|
-
// `/2^k` branchlessly, a diamond of this shape came from hand-written biasing, and
|
|
34
|
+
// `/2^k` branchlessly, a diamond of this shape came from hand-written biasing, and rewriting it
|
|
35
35
|
// costs a match that used to land. Measured positive on ido7.1 (two flips), agbcc and gcc2.7.2kmc
|
|
36
|
-
// (modpow2 stays byte-exact through the
|
|
36
|
+
// (modpow2 stays byte-exact through the rewrite); mwcc_242_81 and gcc2.7.2 have no inhabitant, so
|
|
37
37
|
// they are unmeasured rather than clean.
|
|
38
38
|
//
|
|
39
39
|
// It is deliberately IDENTITY-OR-DECLINE about the shape (the bias constant must be exactly
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// A NARROWING EXTENSION FUSED WITH THE LEFT SHIFT THAT SCALES IT.
|
|
2
|
+
//
|
|
3
|
+
// agbcc lowers `(u8)x` to a shift pair, `lsl #24; lsr #24` (the CAST_PATTERNS idiom,
|
|
4
|
+
// pattern/engine.ts). Scale the narrowed value and gcc's combiner merges the right half of the pair
|
|
5
|
+
// with the scale, so `(u8)x << 3` is TWO instructions, not three:
|
|
6
|
+
//
|
|
7
|
+
// lsl r0, r0, #0x18 %1 = shl %x {imm=24}
|
|
8
|
+
// lsr r0, r0, #0x15 %2 = shr_u %1 {imm=21} == zext8(x) << 3
|
|
9
|
+
//
|
|
10
|
+
// Exactly: `(x << L) >>u R` for `0 < R < L` keeps x's low `32 - L` bits and lands them at bit
|
|
11
|
+
// `L - R`, which is `zext(x, 32 - L) << (L - R)`; `>>s` is the same with `sext`. What the fold buys
|
|
12
|
+
// is the two facts the raw pair hides from every pass below — and ONLY those: the spelling it
|
|
13
|
+
// prints when neither is claimed is not byte-neutral, so that spelling never ships (WHAT NOBODY
|
|
14
|
+
// CLAIMED, below):
|
|
15
|
+
//
|
|
16
|
+
// • THE WIDTH. A declared `u8 idx` is extended in the prologue, and that extension is what
|
|
17
|
+
// raise/paramwidth.ts reads to type the parameter. When the extension is fused, the prologue
|
|
18
|
+
// holds a bare `shl` instead: paramwidth sees no extension of this parameter, AND its prologue
|
|
19
|
+
// scan stops at that `shl`, so every LATER parameter's extension reads as body code too. Typed
|
|
20
|
+
// `u32`, the function recompiles with both shifts at the use and the whole schedule moves.
|
|
21
|
+
// • THE SCALE. `shl(ext(x), k)` is the element-scaled index raise/arrays.ts and
|
|
22
|
+
// raise/struct-arrays.ts legalize; `shr_u(shl(x, 24), 21)` is an opaque byte offset to both.
|
|
23
|
+
//
|
|
24
|
+
// WHERE THE EXTENSION GOES is the one real decision here, and it is read off the machine, not
|
|
25
|
+
// chosen. The two halves of the pair are not scheduled together. Compiled with this benchmark's
|
|
26
|
+
// agbcc, the same body over a narrow and a wide parameter:
|
|
27
|
+
//
|
|
28
|
+
// void pa(u8 a, u8 b) { gA |= 4; gB = (u32)&gT + a * 4; gC = b; }
|
|
29
|
+
// lsl r0,#24 / lsl r1,#24 / lsr r1,#24 / …body… / lsr r0,#22 / add …
|
|
30
|
+
// void pb(u32 a, u8 b) { gA |= 4; gB = (u32)&gT + (u8)a * 4; gC = b; }
|
|
31
|
+
// lsl r1,#24 / lsr r1,#24 / …body… / lsl r0,#24 / lsr r0,#22 / add …
|
|
32
|
+
//
|
|
33
|
+
// The declaration's extension starts in the prologue and only its SECOND half is merged into the
|
|
34
|
+
// use; a cast in the body is lowered at the use, both halves together. So the extension is placed
|
|
35
|
+
// where its `shl` stood and the scale where the right shift stood: a prologue `lsl` yields a
|
|
36
|
+
// prologue extension, which paramwidth's own gates then judge. Those gates keep a body cast wide
|
|
37
|
+
// only when body code that READS a value comes first — `not-prologue` steps over materializations,
|
|
38
|
+
// a pool-loaded address among them — which is why the fold records a pair it finds behind one
|
|
39
|
+
// (THE BODY CAST BEHIND A POOL LOAD, below). Placing both at the right shift instead throws `pa`'s
|
|
40
|
+
// prologue evidence away, so `pa` reads as `pb` and stays wide — measured on the benchmark's sa3
|
|
41
|
+
// rows whose first parameter is a declared `u8` scaled this way:
|
|
42
|
+
// `sa2__sub_8007858` 39/60 anchored against 44/61 at the right shift, and `sa2__sub_8007958` 64/87
|
|
43
|
+
// against 66/88.
|
|
44
|
+
//
|
|
45
|
+
// The SIGNED form does not carry that evidence on agbcc: `void pd(s16 a, …) { … a * 2 … }` and its
|
|
46
|
+
// wide twin `(s16)a * 2` compile to the SAME object, both halves at the use — the ambiguity of
|
|
47
|
+
// paramwidth's `pc` pair — so the placement rule is the same either way, and where the pair sits the
|
|
48
|
+
// judgement is paramwidth's.
|
|
49
|
+
//
|
|
50
|
+
// REFUSES — the pair is left as it is — when:
|
|
51
|
+
// • `R >= L`. `R == L` is the plain cast, which CAST_PATTERNS already folded; `R > L` keeps
|
|
52
|
+
// `32 - R` bits of `x << L` and scales nothing — a bitfield EXTRACT, structure/bitfields.ts's
|
|
53
|
+
// shape — so the two readings are disjoint by the shift amounts alone;
|
|
54
|
+
// • `32 - L` is not a C cast width (8 or 16) — `lsl #28; lsr #26` is `(x & 15) << 2`, which no
|
|
55
|
+
// cast spells;
|
|
56
|
+
// • either shift is the two-register form (no `imm`) or `R` is 0;
|
|
57
|
+
// • the target is not one CAST_PATTERNS applies to. Same predicate, because the fused pair is
|
|
58
|
+
// that cast's lowering with a scale merged in: where `(u8)x` is not a shift pair (IDO and GCC
|
|
59
|
+
// `andi`), `(u8)x << k` is not one either, and a `sll; srl` there is some other arithmetic.
|
|
60
|
+
//
|
|
61
|
+
// • another narrowing of the same value, at the same width and with the same SIGN, sits in the
|
|
62
|
+
// pair's block — a second right shift of a `shl(x, L)`, fused or plain, or the extension
|
|
63
|
+
// CAST_PATTERNS made of a plain one. agbcc extends a value once per block per signedness:
|
|
64
|
+
// `p[(u8)a * 4] = 1; return (u8)a;` is one `lsl #24; lsr #24` and a `lsl #4` at the use, and no
|
|
65
|
+
// fused pair. So a same-sign sibling means the source wrote the shift itself —
|
|
66
|
+
//
|
|
67
|
+
// u32 r6(u32 a, u32 *p) { u32 t = a << 24; p[t >> 22] = 1; return t >> 24; }
|
|
68
|
+
// lsl r0,#24 / lsr r2,r0,#20 / … / lsr r0,r0,#24
|
|
69
|
+
//
|
|
70
|
+
// which folded, struct-arrays spelled `a1[(u8)a0].field_0`: objdiff 3 where the raw pair is
|
|
71
|
+
// byte-exact. Opposite signs share only the `lsl` — `p[(u8)a] = (s8)a` is `lsl #24; lsr #22 …
|
|
72
|
+
// asr #24` — and both of those ARE casts, so they fold. The test is per BLOCK because the
|
|
73
|
+
// sharing is: the same two casts in two blocks lower twice, each with its own `lsl`
|
|
74
|
+
// (`if (c) p[1] = (u8)a; return p[(u8)a];`), and the fused one folds; and a `shl` shared
|
|
75
|
+
// ACROSS blocks is a declaration too — sa3's `sub_8010184` indexes `s->hitboxes[hbIndex]`
|
|
76
|
+
// twice over an `s16 hbIndex`: one `lsl #16`, read by an `asr #13` in each of two blocks. The
|
|
77
|
+
// idiom patterns turn a plain pair into its extension in place, so the test reads the same
|
|
78
|
+
// before them as after, and raise/globalshape.ts, which reads the lift, agrees with the fold
|
|
79
|
+
// about every pair.
|
|
80
|
+
//
|
|
81
|
+
// THE BODY CAST BEHIND A POOL LOAD folds, and the fold records it: the pair's `shl` reads an entry
|
|
82
|
+
// parameter and the machine ran it after a pool-loaded address. Compiled with this benchmark's
|
|
83
|
+
// agbcc, a body cast whose only predecessors are pool loads puts nothing between them and the
|
|
84
|
+
// extension that paramwidth's scan stops at, so the extension reads as a declaration:
|
|
85
|
+
//
|
|
86
|
+
// void pc1(u32 a) { gB = (u32)&gT + (u8)a * 4; } ldr r2,=gB / lsl r0,#24 / lsr r0,#22
|
|
87
|
+
//
|
|
88
|
+
// narrowed to `u8 a0` (objdiff 2) where the raw pair is MATCH. An UNSIGNED declared parameter's
|
|
89
|
+
// `lsl` comes before any pool load, symbol or numeric: all 44 over the benchmark's agbcc
|
|
90
|
+
// references. A signed one need not (27 before, 2 after): the signed form carries no placement
|
|
91
|
+
// evidence (above), and its narrow and wide spellings compile to the same object. So only the
|
|
92
|
+
// WIDTH is refused, by raise/paramwidth.ts's `fused-behind-pool` reading the record; the SCALE stays
|
|
93
|
+
// for the array passes: `gB = gW[(u8)a]` is objdiff 2 as the raw pair and byte-exact as
|
|
94
|
+
// `gW[(u8)a0]`, and sa3's `DemoPlayAlloc` (`gDemoRecordings[demoIndex]` over an `s16`) is spelled
|
|
95
|
+
// as the subscript it is.
|
|
96
|
+
// The order is read off the LIFTED function (`poolOrderOf`, which says why). NOT caught: a NUMERIC
|
|
97
|
+
// pool word lifts to `const`, the same op a `movs` does, and paramwidth's own header says why a
|
|
98
|
+
// `movs` ahead of the pair decides nothing — so a body cast behind only a numeric pool load
|
|
99
|
+
// (`& 0xfff` over a wide parameter in a loop) still narrows.
|
|
100
|
+
//
|
|
101
|
+
// One extension per (`shl`, signedness): two folded scales of one sign off one `shl` — in two
|
|
102
|
+
// blocks, `sub_8010184`'s shape — share it, and opposite signs get one each. The `shl` dies when
|
|
103
|
+
// every reader was folded; one that keeps a raw reader (`p[t >> 22] = 1; return t;`) stays beside
|
|
104
|
+
// the extension.
|
|
105
|
+
//
|
|
106
|
+
// WHAT NOBODY CLAIMED goes back (`restoreUnclaimedScales`, the last pre-recovery pass). An
|
|
107
|
+
// extension no width pass took and a scale no array pass took print as `(u8)a1 << 12`, and that is
|
|
108
|
+
// NOT the same object as the lifted `a1 << 24 >> 12`: straight-line they compile alike (the `pb`
|
|
109
|
+
// pair above), but kleod's `SetWorldMapTilePalette` — two `u8` parameters, the first used in agbcc's
|
|
110
|
+
// shifted domain so paramwidth's scan stops at its `shl` and refuses the second's extension — scores
|
|
111
|
+
// 54/93 with the raw pair and 59/91 with the cast, the loop's r4/r6 allocation swapped. So every
|
|
112
|
+
// scale the fold made that still reads its extension is rewritten in place to the pair it replaced;
|
|
113
|
+
// what survives the pass is exactly what a consumer claimed, and everywhere else the output is the
|
|
114
|
+
// one the lift alone produces.
|
|
115
|
+
//
|
|
116
|
+
// That is a decision about the DEFAULT, not a finding that the raw spelling is better: the unclaimed cast
|
|
117
|
+
// has both signs. Over the 55 corpus functions the fold fires on, scored on the benchmark's own
|
|
118
|
+
// path, restoring moves four scores — `SetWorldMapTilePalette` 59/91 → 54/93 and sa3
|
|
119
|
+
// `UnpackSaveSector` 347 → 346 better, sa3 `ClearSave` 189 → 191 and `CompleteSave` 185 → 187
|
|
120
|
+
// worse, all four to what the lift alone scores (and sa3 `ValidateSave`'s denominator, 213/378 →
|
|
121
|
+
// 213/379). A spelling with both signs is the differ's to referee (a ranked variation), and none is
|
|
122
|
+
// built; the default is the one that asserts nothing the lift did not.
|
|
123
|
+
import { type Block, type Fn, type Op, type Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
124
|
+
import { CAST_WIDTHS } from '../ir/opcodes';
|
|
125
|
+
import { CAST_PATTERNS, patternApplies } from '../pattern/engine';
|
|
126
|
+
import type { TargetDescription } from '../target';
|
|
127
|
+
|
|
128
|
+
/** `shr_{u,s}(shl(src, L), R)` read as `ext(src, 32 - L) << (L - R)`. */
|
|
129
|
+
export interface ScaledExtension {
|
|
130
|
+
/** the value being narrowed — the `shl`'s operand */
|
|
131
|
+
src: Value;
|
|
132
|
+
/** the cast width, `32 - L` */
|
|
133
|
+
width: number;
|
|
134
|
+
/** `shr_s` (sign-extend) rather than `shr_u` */
|
|
135
|
+
signed: boolean;
|
|
136
|
+
/** the scale's shift amount, `L - R` */
|
|
137
|
+
shift: number;
|
|
138
|
+
/** the `shl` — where the extension begins */
|
|
139
|
+
inner: Op;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The fused pair's SHAPE rooted at `op`, or null — the header's refusals on the amounts and the
|
|
143
|
+
* two-register form. The sibling refusal is {@link foldablePairs}', the target's the caller's. */
|
|
144
|
+
export function scaledExtensionOf(op: Op | undefined, defs: Map<Value, Op>): ScaledExtension | null {
|
|
145
|
+
if (!op || (op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const inner = defs.get(op.operands[0]);
|
|
149
|
+
const r = op.attrs.imm;
|
|
150
|
+
const l = inner?.attrs.imm;
|
|
151
|
+
if (inner?.opcode !== 'shl' || inner.operands.length !== 1 || typeof r !== 'number' || typeof l !== 'number') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!CAST_WIDTHS.has(32 - l) || r <= 0 || r >= l) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return { src: inner.operands[0], width: 32 - l, signed: op.opcode === 'shr_s', shift: l - r, inner };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Does `target` lower a narrowing cast to a shift pair — the gate the fold shares with the cast
|
|
161
|
+
* idiom itself. */
|
|
162
|
+
export const foldsShiftPairCasts = (target: TargetDescription): boolean =>
|
|
163
|
+
CAST_PATTERNS.every((p) => patternApplies(p, target));
|
|
164
|
+
|
|
165
|
+
/** Where the MACHINE put each entry-block op relative to its first pool-loaded address, read off a
|
|
166
|
+
* function whose entry block is still in lifted order. See THE BODY CAST BEHIND A POOL LOAD. */
|
|
167
|
+
export interface PoolOrder {
|
|
168
|
+
/** the function's parameters */
|
|
169
|
+
entryParams: ReadonlySet<Value>;
|
|
170
|
+
/** the entry block's ops that come after a `gaddr` */
|
|
171
|
+
afterPoolLoad: ReadonlySet<Op>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read {@link PoolOrder} off `fn`. Valid only BEFORE raise/gvn.ts's `addrnum` runs: that pass
|
|
175
|
+
* hoists a duplicated address to the head of the entry block, after which a `gaddr`'s position no
|
|
176
|
+
* longer says where the machine loaded it. The idiom patterns before it leave both a `gaddr` and a
|
|
177
|
+
* fused pair's `shl` where they were — a plain cast's pair is folded at its right half, which is
|
|
178
|
+
* why this fact is read for the FUSED form only. */
|
|
179
|
+
export function poolOrderOf(fn: Fn): PoolOrder {
|
|
180
|
+
const entry = fn.blocks[0];
|
|
181
|
+
const afterPoolLoad = new Set<Op>();
|
|
182
|
+
let seen = false;
|
|
183
|
+
for (const op of entry?.ops ?? []) {
|
|
184
|
+
if (seen) {
|
|
185
|
+
afterPoolLoad.add(op);
|
|
186
|
+
}
|
|
187
|
+
seen ||= op.opcode === 'gaddr';
|
|
188
|
+
}
|
|
189
|
+
return { entryParams: new Set(entry?.params ?? []), afterPoolLoad };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Is `o` a narrowing of `m.src` at `m.width` with `m`'s signedness — a right shift of a
|
|
193
|
+
* `shl(src, 32 - width)`, fused or plain, or the extension CAST_PATTERNS made of a plain one? The
|
|
194
|
+
* answer is the same before the idiom patterns and after them: they turn a plain pair into its
|
|
195
|
+
* extension at the pair's own position. */
|
|
196
|
+
function narrowsLike(o: Op, m: ScaledExtension, defs: Map<Value, Op>): boolean {
|
|
197
|
+
if (o.opcode === (m.signed ? 'sext' : 'zext')) {
|
|
198
|
+
return o.operands[0] === m.src && o.attrs.width === m.width;
|
|
199
|
+
}
|
|
200
|
+
if (o.opcode !== (m.signed ? 'shr_s' : 'shr_u') || o.operands.length !== 1) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const l = 32 - m.width;
|
|
204
|
+
const r = o.attrs.imm;
|
|
205
|
+
const d = defs.get(o.operands[0]);
|
|
206
|
+
return (
|
|
207
|
+
d?.opcode === 'shl' &&
|
|
208
|
+
d.operands.length === 1 &&
|
|
209
|
+
d.operands[0] === m.src &&
|
|
210
|
+
d.attrs.imm === l &&
|
|
211
|
+
typeof r === 'number' &&
|
|
212
|
+
r > 0 &&
|
|
213
|
+
r <= l
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Every fused pair in `fn` AS THE FOLD TAKES IT, keyed by its right shift: the shape, minus a pair
|
|
218
|
+
* with a same-sign sibling in its block. THE ONE READING — the fold and raise/globalshape.ts's
|
|
219
|
+
* stride reader both ask here, and both before any pair is rewritten; a reader that took a pair the
|
|
220
|
+
* fold leaves raw would license an element scale that no pass below legalizes. The TARGET half of
|
|
221
|
+
* the gate is the caller's: the pass list gates the fold, and globalshape asks
|
|
222
|
+
* {@link foldsShiftPairCasts} before it reads. */
|
|
223
|
+
export function foldablePairs(fn: Fn, defs: Map<Value, Op> = defOpMap(fn)): Map<Op, ScaledExtension> {
|
|
224
|
+
const out = new Map<Op, ScaledExtension>();
|
|
225
|
+
for (const b of fn.blocks) {
|
|
226
|
+
for (const op of b.ops) {
|
|
227
|
+
const m = scaledExtensionOf(op, defs);
|
|
228
|
+
if (m !== null && !b.ops.some((o) => o !== op && narrowsLike(o, m, defs))) {
|
|
229
|
+
out.set(op, m);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** What the fold made, for the two passes that read it later: raise/paramwidth.ts's
|
|
237
|
+
* `fused-behind-pool` gate and {@link restoreUnclaimedScales}. One per pre-recovery run
|
|
238
|
+
* (`PreRecoveryFacts.scales`), keyed by op identity. */
|
|
239
|
+
export interface ScaleRecord {
|
|
240
|
+
/** each scale the fold made → the extension it reads and the pair it replaced */
|
|
241
|
+
folded: Map<Op, { ext: Op; l: number; r: number }>;
|
|
242
|
+
/** the extensions over an entry parameter whose `shl` the machine ran behind a pool load */
|
|
243
|
+
behindPool: Set<Op>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const emptyScaleRecord = (): ScaleRecord => ({ folded: new Map(), behindPool: new Set() });
|
|
247
|
+
|
|
248
|
+
/** Rewrite every fused pair to `shl(ext(src), shift)`, the extension spliced in at the `shl`'s
|
|
249
|
+
* position and the scale at the right shift's, and write what it made into `record`. Returns the
|
|
250
|
+
* number of pairs folded; the `shl`s left without a reader are the pass driver's DCE. `order` is
|
|
251
|
+
* the lifted-order fact the driver read before `addrnum` (`PreRecoveryFacts.poolOrder`); the
|
|
252
|
+
* default reads it off `fn` itself, which is right only while `fn`'s entry block is still in lifted
|
|
253
|
+
* order — a test's parsed IR. */
|
|
254
|
+
export function foldScaledExtensions(
|
|
255
|
+
fn: Fn,
|
|
256
|
+
order: PoolOrder = poolOrderOf(fn),
|
|
257
|
+
record: ScaleRecord = emptyScaleRecord(),
|
|
258
|
+
): number {
|
|
259
|
+
const defs = defOpMap(fn);
|
|
260
|
+
const blockOf = new Map<Op, Block>();
|
|
261
|
+
for (const b of fn.blocks) {
|
|
262
|
+
for (const op of b.ops) {
|
|
263
|
+
blockOf.set(op, b);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const exts = new Map<Op, Map<boolean, Op>>();
|
|
267
|
+
let folded = 0;
|
|
268
|
+
for (const [op, m] of foldablePairs(fn, defs)) {
|
|
269
|
+
const bySign = exts.get(m.inner) ?? exts.set(m.inner, new Map()).get(m.inner)!;
|
|
270
|
+
let ext = bySign.get(m.signed);
|
|
271
|
+
if (ext === undefined) {
|
|
272
|
+
ext = mkOp(m.signed ? 'sext' : 'zext', {
|
|
273
|
+
operands: [m.src],
|
|
274
|
+
results: [mkValue(m.inner.results[0].type)],
|
|
275
|
+
attrs: { width: m.width },
|
|
276
|
+
});
|
|
277
|
+
const home = blockOf.get(m.inner)!;
|
|
278
|
+
home.ops.splice(home.ops.indexOf(m.inner), 0, ext);
|
|
279
|
+
if (order.entryParams.has(m.src) && order.afterPoolLoad.has(m.inner)) {
|
|
280
|
+
record.behindPool.add(ext);
|
|
281
|
+
}
|
|
282
|
+
bySign.set(m.signed, ext);
|
|
283
|
+
}
|
|
284
|
+
const scaled = mkOp('shl', {
|
|
285
|
+
operands: [ext.results[0]],
|
|
286
|
+
results: [mkValue(op.results[0].type)],
|
|
287
|
+
attrs: { imm: m.shift },
|
|
288
|
+
});
|
|
289
|
+
const b = blockOf.get(op)!;
|
|
290
|
+
b.ops.splice(b.ops.indexOf(op), 1, scaled);
|
|
291
|
+
replaceAllUsesWith(fn, op.results[0], scaled.results[0]);
|
|
292
|
+
record.folded.set(scaled, { ext, l: 32 - m.width, r: 32 - m.width - m.shift });
|
|
293
|
+
folded++;
|
|
294
|
+
}
|
|
295
|
+
return folded;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Put back the machine's own pair wherever no pass below claimed what the fold exposed. Returns
|
|
299
|
+
* the number of scales restored; an extension left without a reader is the driver's DCE.
|
|
300
|
+
*
|
|
301
|
+
* A scale is CLAIMED when it no longer reads the fold's extension — raise/paramwidth.ts or
|
|
302
|
+
* raise/narrowlocal.ts retyped the value and dropped the extension, so the scale now reads that
|
|
303
|
+
* value — or when it is gone, legalized into an element index by raise/arrays.ts or
|
|
304
|
+
* raise/struct-arrays.ts. Anything else still reads the extension, and is rewritten in place (same
|
|
305
|
+
* result value, same position) to `shr(shl(src, L), R)`, with the `shl` where the extension stood:
|
|
306
|
+
* the ops the frontend lifted, in the order it lifted them. */
|
|
307
|
+
export function restoreUnclaimedScales(fn: Fn, record: ScaleRecord): number {
|
|
308
|
+
const shls = new Map<Op, Op>();
|
|
309
|
+
let restored = 0;
|
|
310
|
+
for (const b of fn.blocks) {
|
|
311
|
+
for (const op of [...b.ops]) {
|
|
312
|
+
const f = record.folded.get(op);
|
|
313
|
+
if (f === undefined || op.opcode !== 'shl' || op.operands[0] !== f.ext.results[0]) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// The pair is rebuilt from the amounts RECORDED at the fold, so they must still be the ones
|
|
317
|
+
// the two ops carry: a pass that re-scaled the `shl` or re-widened the extension in place
|
|
318
|
+
// would make the recorded pair compute a different value. None does today; refuse, not guess.
|
|
319
|
+
const extended = f.ext.opcode === 'zext' || f.ext.opcode === 'sext';
|
|
320
|
+
if (!extended || f.ext.attrs.width !== 32 - f.l || op.attrs.imm !== f.l - f.r) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
let shl = shls.get(f.ext);
|
|
324
|
+
if (shl === undefined) {
|
|
325
|
+
const home = fn.blocks.find((x) => x.ops.includes(f.ext))!;
|
|
326
|
+
shl = mkOp('shl', {
|
|
327
|
+
operands: [f.ext.operands[0]],
|
|
328
|
+
results: [mkValue(f.ext.results[0].type)],
|
|
329
|
+
attrs: { imm: f.l },
|
|
330
|
+
});
|
|
331
|
+
home.ops.splice(home.ops.indexOf(f.ext), 0, shl);
|
|
332
|
+
shls.set(f.ext, shl);
|
|
333
|
+
}
|
|
334
|
+
op.opcode = f.ext.opcode === 'sext' ? 'shr_s' : 'shr_u';
|
|
335
|
+
op.operands = [shl.results[0]];
|
|
336
|
+
op.attrs = { imm: f.r };
|
|
337
|
+
record.folded.delete(op);
|
|
338
|
+
restored++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return restored;
|
|
342
|
+
}
|