@asmlift/core 0.5.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
// asmlift structurer — the ANALYSIS phase. Pure derivation over the lifted fn — nothing here
|
|
2
2
|
// mutates the IR or depends on naming/emission state:
|
|
3
3
|
// • use-site registry — every use of a value, POSITIONED (op + block + index);
|
|
4
|
-
// • per-block SSA value liveness (backward dataflow) — consumed by
|
|
5
|
-
//
|
|
6
|
-
// • the effect-ordering model — which
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
// • per-block SSA value liveness (backward dataflow) — consumed by structure.ts and
|
|
5
|
+
// structure/namecoalesce.ts;
|
|
6
|
+
// • the effect-ordering model — which defs must MATERIALIZE as named temps at their own
|
|
7
|
+
// program position instead of inlining at their use (calls/loads for effect order, plus
|
|
8
|
+
// the pure defs the homing rules claim);
|
|
9
|
+
// • the HOMING-VARIATION ENUMERATION GATES — one export per structure variation, each answering
|
|
10
|
+
// "does this function hold a value the variation would home at all" so rank.ts can skip a
|
|
11
|
+
// variation whose candidate would only duplicate the default. Each mirrors its variation's scope inside `analyze`
|
|
12
|
+
// and states where it DIVERGES from it, in which direction, and what that costs.
|
|
13
|
+
import { disjointConstSlots, globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
14
|
+
import {
|
|
15
|
+
Block,
|
|
16
|
+
Fn,
|
|
17
|
+
Op,
|
|
18
|
+
Successor,
|
|
19
|
+
Value,
|
|
20
|
+
defOpMap,
|
|
21
|
+
dominators,
|
|
22
|
+
mergeClasses,
|
|
23
|
+
predecessors,
|
|
24
|
+
successorsOf,
|
|
25
|
+
} from '../ir/core';
|
|
26
|
+
import { EFFECTFUL_OPS, MEM_BASE_OPS, ORDER_SENSITIVE_OPS, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
|
|
10
27
|
|
|
11
28
|
export interface UseSite {
|
|
12
29
|
blk: Block;
|
|
@@ -14,6 +31,632 @@ export interface UseSite {
|
|
|
14
31
|
op: Op;
|
|
15
32
|
}
|
|
16
33
|
|
|
34
|
+
/** Is the op's own value an ADDRESS — a pointer or array whose standalone rendering must carry a
|
|
35
|
+
* cast? Homed, `add(p, 8)` renders `(u16 *)(gPtr + 8)`: the cast lands outside the sum, so a byte
|
|
36
|
+
* offset becomes element arithmetic and the address moves. The VALUE-side counterpart of
|
|
37
|
+
* `coneHoldsAddr`, which is a different question rather than a weaker one — that walk crosses
|
|
38
|
+
* reads, so a rule whose clientele IS values over a load cannot ask it, and a pointer loaded from
|
|
39
|
+
* memory has no gaddr of its own for it to find. Each caller's refusal says which half it buys. */
|
|
40
|
+
function rendersAsAddress(op: Op): boolean {
|
|
41
|
+
const t = op.results[0]?.type;
|
|
42
|
+
return t?.kind === 'ptr' || t?.kind === 'array';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Values C evaluates only under a `&&`/`||`: the SECOND operand of every `logic_and`/`logic_or`
|
|
46
|
+
* and its transitive operand cone. raise/shortcircuit.ts lifts that cone into the block ABOVE the
|
|
47
|
+
* branch on the contract that the structurer inlines it back under C's own short circuit, so the
|
|
48
|
+
* def block of anything in it is a FOLD ARTIFACT — a rule that names one of these values there
|
|
49
|
+
* emits it above the guard the source wrote (`p != 0 && *p != 0` becoming `v0 = *p;` above its own
|
|
50
|
+
* null check). Asked by the def-block placement rule and by the merge-feed-home scope. */
|
|
51
|
+
function shortCircuitGuardedValues(fn: Fn, defOf: Map<Value, Op>): Set<Value> {
|
|
52
|
+
const guarded = new Set<Value>();
|
|
53
|
+
const work: Value[] = [];
|
|
54
|
+
for (const b of fn.blocks) {
|
|
55
|
+
for (const op of b.ops) {
|
|
56
|
+
const rhs = op.opcode === 'logic_and' || op.opcode === 'logic_or' ? op.operands[1] : undefined;
|
|
57
|
+
if (rhs !== undefined) {
|
|
58
|
+
work.push(rhs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
while (work.length) {
|
|
63
|
+
const v = work.pop()!;
|
|
64
|
+
if (!guarded.has(v)) {
|
|
65
|
+
guarded.add(v);
|
|
66
|
+
work.push(...(defOf.get(v)?.operands ?? []));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return guarded;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Any gaddr/laddr in the op's operand cone (the op included). Rendered standalone, an address
|
|
73
|
+
* computation over one loses the memAccess's inline byte-stride cast — the value changes, so a
|
|
74
|
+
* homing rule asking this refuses the cone (the cast-aware machinery in l3/basecse.ts,
|
|
75
|
+
* scopebase.ts and nearbase.ts serves those bases instead). The walk deliberately crosses loads —
|
|
76
|
+
* a gaddr reachable only through a load's address keeps its cast at that load's own deref, so
|
|
77
|
+
* over-refusal there costs a candidate, never soundness. That over-refusal is why the
|
|
78
|
+
* derived-read-home variation asks its own pair instead (an address in the cone OUTSIDE a read, plus
|
|
79
|
+
* `rendersAsAddress` on the value): reads over named globals are its whole clientele. */
|
|
80
|
+
function coneHoldsAddr(op0: Op, defOf: Map<Value, Op>): boolean {
|
|
81
|
+
const seen = new Set<Value>();
|
|
82
|
+
const cone = [op0];
|
|
83
|
+
while (cone.length) {
|
|
84
|
+
const d = cone.pop()!;
|
|
85
|
+
if (d.opcode === 'gaddr' || d.opcode === 'laddr') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
for (const x of d.operands) {
|
|
89
|
+
if (!seen.has(x)) {
|
|
90
|
+
seen.add(x);
|
|
91
|
+
const dd = defOf.get(x);
|
|
92
|
+
if (dd) {
|
|
93
|
+
cone.push(dd);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** THE ADDRESS-HOME VARIATION'S SCOPE: every value whose MERGE CLASS (ir/core.ts `mergeClasses`) is
|
|
102
|
+
* used only as the base of memory accesses, and at 2+ of them.
|
|
103
|
+
*
|
|
104
|
+
* WHY THE CLASS AND NOT THE VALUE. The question the variation asks is about a REGISTER — did the
|
|
105
|
+
* machine derive one address and dereference it at several sites — and a register that survives a
|
|
106
|
+
* branch merge is spelled in functional-form SSA as an edge argument plus a block parameter. So a
|
|
107
|
+
* base each arm derives and the join then reads is N+1 SSA values with one base use apiece, none
|
|
108
|
+
* of which reaches the 2-access threshold, while the register it describes reaches N+1 of them.
|
|
109
|
+
* Counting per value also mis-reads the edge itself: the terminator that carries the argument is
|
|
110
|
+
* a consumer that is not a memory access, so the per-value rule refuses on the very edge that
|
|
111
|
+
* makes the base shared.
|
|
112
|
+
*
|
|
113
|
+
* An edge argument is therefore NOT a use that leaves the class — it feeds the parameter beside
|
|
114
|
+
* it, which `mergeClasses` has already unioned in. Everything else still disqualifies the whole
|
|
115
|
+
* class: a store's value slot, an `aload` index, arithmetic. The home is justified by the
|
|
116
|
+
* shared-base reuse alone, as it always was.
|
|
117
|
+
*
|
|
118
|
+
* `ignoreRet` skips `ret` operands: `analyze` passes its own `returnsVoid` (where a ret operand
|
|
119
|
+
* is a phantom the use registry already drops), and rank.ts's enumeration gate passes true
|
|
120
|
+
* unconditionally, because it cannot know — see `hasHomeableSharedAddress`.
|
|
121
|
+
*
|
|
122
|
+
* Block PARAMETERS are members like any other and can qualify here. Neither caller homes one —
|
|
123
|
+
* both filter by a DEF — but they must be counted, or the join half of every class is invisible.
|
|
124
|
+
*
|
|
125
|
+
* IT IS NOT A SUPERSET OF THE PER-VALUE RULE IT REPLACED, and that is the price rather than a
|
|
126
|
+
* bug: a value whose CLASS MATE escapes is now refused where counting the value alone admitted
|
|
127
|
+
* it. Measured by running both predicates over every function in the four checkouts — 1487
|
|
128
|
+
* lifting functions, both symbol-map configurations — the widening admits new values in 25
|
|
129
|
+
* functions map-less and 8 map-ful, and REMOVES exactly one value from each of four functions
|
|
130
|
+
* map-less (kleod `sub_0804EB64`, marioparty3 `func_8010E344_25A994_rockin_raceway`,
|
|
131
|
+
* snowboardkids2 `func_8006FDA0_709A0` and `func_8006FDC8_709C8`), none map-ful. The function-
|
|
132
|
+
* level ENUMERATION gate is a clean superset over the same corpus, which is why a census taken
|
|
133
|
+
* at that scope reports no loss; this is the finer one.
|
|
134
|
+
*
|
|
135
|
+
* NO LOOP-HEADER REFUSAL OF ITS OWN, AND THE REASON IS A GUARD THAT WAS ALREADY THERE. The
|
|
136
|
+
* scope's plan mandated one — refuse a class reaching a loop-header PARAMETER, because
|
|
137
|
+
* `structure.ts`'s `carriesPreUpdate` makes a loop variable's name denote different values at
|
|
138
|
+
* different points. Two things are true and only together do they answer it.
|
|
139
|
+
*
|
|
140
|
+
* The FIRST is about which classes the widening adds. Of the 25+8 functions above exactly one
|
|
141
|
+
* carries a class reaching a loop-header parameter (marioparty3
|
|
142
|
+
* `func_80112508_523648_filesel`, 2 values, map-less), and neither value is one the variation could
|
|
143
|
+
* materialize; `addr-home.test.ts` pins both halves. A loop-carried pointer INDUCTION is refused
|
|
144
|
+
* by its own increment, a non-base use of a class member. The loop-header class that does survive
|
|
145
|
+
* has a back-edge value READ FROM MEMORY, so its only def is a `load` and the enumeration gate
|
|
146
|
+
* excludes it. So the widening adds no loop-header home, and the planned guard has no inhabitant
|
|
147
|
+
* among the values it would have been written for.
|
|
148
|
+
*
|
|
149
|
+
* The SECOND is what stands there whatever the scope rule says, and it is NOT one of those two:
|
|
150
|
+
* the third scope's own `!multiBlockHeaders.has(b)` seat refusal, which predates this widening
|
|
151
|
+
* and refuses to materialize AT a multi-block loop header regardless of which class the value is
|
|
152
|
+
* in. It is load-bearing and measurable: replace that clause with `true` and marioparty3
|
|
153
|
+
* `func_8010923C_18E46C_cosmic_coaster` stops structuring at all — it throws the
|
|
154
|
+
* `carriesPreUpdate` StructureError this paragraph is about — while `func_80033910_34510` and
|
|
155
|
+
* `func_80033970_34570` go from variation-inert to changing their source. So the hazard is real; it is
|
|
156
|
+
* answered by WHERE a home may sit rather than by WHICH class may have one, which is a refusal
|
|
157
|
+
* the widening does not touch and the plan's class guard would have restated. Note what that
|
|
158
|
+
* implies about the enumeration gate: it deliberately
|
|
159
|
+
* omits the seat refusal (see `hasHomeableSharedAddress`), which is safe only because the seat
|
|
160
|
+
* refusal is applied HERE, in the scope, on every candidate the gate enumerates. */
|
|
161
|
+
export function sharedBaseClasses(fn: Fn, ignoreRet: boolean): Set<Value> {
|
|
162
|
+
const classes = mergeClasses(fn);
|
|
163
|
+
const baseUses = new Map<Value, Set<Op>>();
|
|
164
|
+
const otherUse = new Set<Value>();
|
|
165
|
+
for (const b of fn.blocks) {
|
|
166
|
+
for (const op of b.ops) {
|
|
167
|
+
if (ignoreRet && op.opcode === 'ret') {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
op.operands.forEach((o, i) => {
|
|
171
|
+
// BASE SLOT ONLY. `store p, p` — the address written as its own data — needs no clause
|
|
172
|
+
// here and had one: the j>0 pass over the SAME operand list puts `p` into `otherUse`
|
|
173
|
+
// regardless, and `otherUse` disqualifies the whole class below, so a base-slot entry for
|
|
174
|
+
// an escaping value can never be read. Measured before removing it: over 1487 lifted
|
|
175
|
+
// corpus functions the shape occurs at 10 sites and the predicate's output is identical
|
|
176
|
+
// with the clause and without it, in both `ignoreRet` senses. What keeps the escape out is
|
|
177
|
+
// therefore the `otherUse` sweep, and that is what `analysis.test.ts` pins — a clause that
|
|
178
|
+
// cannot change an answer is not a guard, it is a claim that one exists.
|
|
179
|
+
if (i === 0 && MEM_BASE_OPS.has(op.opcode)) {
|
|
180
|
+
(baseUses.get(o) ?? baseUses.set(o, new Set()).get(o)!).add(op);
|
|
181
|
+
} else {
|
|
182
|
+
otherUse.add(o);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
for (const s of op.successors) {
|
|
186
|
+
s.args.forEach((a, i) => {
|
|
187
|
+
// Class-internal only up to the parameter list: an argument past the end binds nothing,
|
|
188
|
+
// so `mergeClasses` never unioned it and it escapes like any other use.
|
|
189
|
+
if (i >= s.block.params.length) {
|
|
190
|
+
otherUse.add(a);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const out = new Set<Value>();
|
|
197
|
+
for (const b of fn.blocks) {
|
|
198
|
+
for (const v of [...b.params, ...b.ops.flatMap((op) => op.results)]) {
|
|
199
|
+
const members = classes.get(v) ?? [v];
|
|
200
|
+
if (members.some((m) => otherUse.has(m))) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const consumers = new Set<Op>();
|
|
204
|
+
for (const m of members) {
|
|
205
|
+
for (const c of baseUses.get(m) ?? []) {
|
|
206
|
+
consumers.add(c);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (consumers.size >= 2) {
|
|
210
|
+
out.add(v);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Is `d` a def one of the homing variations could seat in a local — a def at all, and a PURE
|
|
218
|
+
* non-memory one that is not a `const`? The three enumeration gates below share this filter and
|
|
219
|
+
* each then adds its own cone/address/shape refusals.
|
|
220
|
+
*
|
|
221
|
+
* Named for what it tests rather than for the shape it excludes: a `const` IS a pure non-memory
|
|
222
|
+
* def, and it is out because a re-derived const is re-materialization — the compiler's own
|
|
223
|
+
* behaviour — not because it computes anything a home would preserve. */
|
|
224
|
+
function isHomeableDef(d: Op | undefined): d is Op {
|
|
225
|
+
return !!d && d.opcode !== 'const' && d.opcode !== 'call' && d.opcode !== 'load' && d.opcode !== 'aload';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** THE READ CONE of a pure def: the `load`/`aload` defs its operand tree stands on. The walk stops
|
|
229
|
+
* AT a read (a read's own address computation is the read's business, not this walk's) and
|
|
230
|
+
* REFUSES — null — as soon as the cone holds a `call`, whose re-render would re-execute it, or a
|
|
231
|
+
* `gaddr`/`laddr`, whose standalone rendering loses the memAccess's inline byte-stride cast.
|
|
232
|
+
*
|
|
233
|
+
* Both callers gate `load`/`aload`/`call` out of `op0` itself before asking, and neither reads the
|
|
234
|
+
* order the reads come back in — one takes `length`/`every`, the other builds a `Set`. */
|
|
235
|
+
function readCone(op0: Op, defOf: Map<Value, Op>): Op[] | null {
|
|
236
|
+
const reads: Op[] = [];
|
|
237
|
+
const seen = new Set<Value>();
|
|
238
|
+
const cone = [op0];
|
|
239
|
+
while (cone.length) {
|
|
240
|
+
const d = cone.pop()!;
|
|
241
|
+
if (d.opcode === 'load' || d.opcode === 'aload') {
|
|
242
|
+
reads.push(d);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (d.opcode === 'call' || d.opcode === 'gaddr' || d.opcode === 'laddr') {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
for (const x of d.operands) {
|
|
249
|
+
if (!seen.has(x)) {
|
|
250
|
+
seen.add(x);
|
|
251
|
+
const dd = defOf.get(x);
|
|
252
|
+
if (dd) {
|
|
253
|
+
cone.push(dd);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return reads;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** rank.ts's enumeration gate for the `/addr-home` variation: does the function HAVE a value the
|
|
262
|
+
* variation would home — a non-const pure def whose merge class is a shared base, with no gaddr/laddr in
|
|
263
|
+
* its cone? Mirrors the variation's scope rule in `analyze` (the same `sharedBaseClasses` call), minus
|
|
264
|
+
* the loop-header seat refusal (that needs the loop model; a false positive costs one
|
|
265
|
+
* duplicate-collapsed candidate, never a wrong one).
|
|
266
|
+
*
|
|
267
|
+
* `ignoreRet` is true here: a `ret` operand may be a void phantom, which `analyze` skips under
|
|
268
|
+
* `returnsVoid` and this gate cannot know. For a genuinely returned base the variation's own rule
|
|
269
|
+
* still refuses, costing one duplicate-collapsed candidate — the same trade as the loop-header
|
|
270
|
+
* divergence. */
|
|
271
|
+
export function hasHomeableSharedAddress(fn: Fn): boolean {
|
|
272
|
+
const defOf = defOpMap(fn);
|
|
273
|
+
for (const v of sharedBaseClasses(fn, true)) {
|
|
274
|
+
const d = defOf.get(v);
|
|
275
|
+
if (isHomeableDef(d) && !coneHoldsAddr(d, defOf)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** rank.ts's enumeration gate for the `/expr-home` variation: does the function HAVE a value the
|
|
283
|
+
* variation would home — a pure non-const def with 2+ distinct consumers, at least one of them inside
|
|
284
|
+
* a loop the def sits outside, cone-free? Loops here are LAYOUT ranges (a successor at an
|
|
285
|
+
* equal-or-earlier block position closes one) where the variation's own rule uses the dominator model,
|
|
286
|
+
* and consumers here come from op operands only, where the rule counts `useSitesOf` and so counts
|
|
287
|
+
* branch args too — unlike hasHomeableSharedAddress this therefore diverges in BOTH directions. A
|
|
288
|
+
* false positive costs one duplicate-collapsed candidate. A false negative silently skips the arm:
|
|
289
|
+
* on IR whose block layout does not follow dominance, which every frontend avoids by laying blocks
|
|
290
|
+
* out in address order (a natural loop's back edge points backward), and on a value EITHER of
|
|
291
|
+
* whose two consumers is a branch arg, which the rule would home and this never enumerates. The
|
|
292
|
+
* second is unwitnessed over the 856-row bench the variation was measured on (#97), and costs a
|
|
293
|
+
* missing candidate, never a wrong one. */
|
|
294
|
+
export function hasLoopSharedPureValue(fn: Fn): boolean {
|
|
295
|
+
const defOf = defOpMap(fn);
|
|
296
|
+
const pos = new Map<Block, number>(fn.blocks.map((b, i) => [b, i]));
|
|
297
|
+
const ranges: [number, number][] = [];
|
|
298
|
+
for (const b of fn.blocks) {
|
|
299
|
+
for (const sx of successorsOf(b)) {
|
|
300
|
+
if (pos.get(sx)! <= pos.get(b)!) {
|
|
301
|
+
ranges.push([pos.get(sx)!, pos.get(b)!]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (ranges.length === 0) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const consumers = new Map<Value, Set<Op>>();
|
|
309
|
+
const opPos = new Map<Op, number>();
|
|
310
|
+
for (const b of fn.blocks) {
|
|
311
|
+
for (const op of b.ops) {
|
|
312
|
+
opPos.set(op, pos.get(b)!);
|
|
313
|
+
for (const o of op.operands) {
|
|
314
|
+
(consumers.get(o) ?? consumers.set(o, new Set()).get(o)!).add(op);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const [v, cs] of consumers) {
|
|
319
|
+
const d = defOf.get(v);
|
|
320
|
+
if (!isHomeableDef(d)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const dp = opPos.get(d)!;
|
|
324
|
+
if (
|
|
325
|
+
cs.size >= 2 &&
|
|
326
|
+
ranges.some(
|
|
327
|
+
([lo, hi]) => (dp < lo || dp > hi) && [...cs].some((c) => opPos.get(c)! >= lo && opPos.get(c)! <= hi),
|
|
328
|
+
) &&
|
|
329
|
+
!coneHoldsAddr(d, defOf)
|
|
330
|
+
) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** rank.ts's enumeration gate for the `/derived-home` variation: does the function HAVE a value the
|
|
338
|
+
* variation would home — a pure non-const, non-pointer def with 2+ consumers standing on a same-block
|
|
339
|
+
* memory read that is used nowhere else and reaches it with no write in between, and with no call
|
|
340
|
+
* or standalone address in the cone? A TRUE here DOUBLES the whole structuring cross for the
|
|
341
|
+
* function — not one candidate — so every refusal cheap enough to state without the positioned
|
|
342
|
+
* model is mirrored here; only the loop-header seat stays out.
|
|
343
|
+
*
|
|
344
|
+
* Diverges in BOTH directions, like `hasLoopSharedPureValue` and unlike `hasHomeableSharedAddress`.
|
|
345
|
+
* Over: the write scan is straight-line within the one block, where the rule's is cycle-aware, so
|
|
346
|
+
* a write reaching only around a back edge is invisible here — a false positive costs a doubled
|
|
347
|
+
* cross whose every candidate the source dedup then collapses. Under: use counting here is by SLOT
|
|
348
|
+
* over operands and successor args, where `analyze` drops a void function's `ret` operand — so a
|
|
349
|
+
* read whose second use is a suppressed return is refused here and admitted there, silently
|
|
350
|
+
* skipping the alternative. That shape is a void function returning the very halfword it read, which no
|
|
351
|
+
* caller can observe; the variation's clientele reads to compute, not to return. */
|
|
352
|
+
export function hasDerivedReadHome(fn: Fn): boolean {
|
|
353
|
+
const defOf = defOpMap(fn);
|
|
354
|
+
const consumers = new Map<Value, Set<Op>>();
|
|
355
|
+
const useSlots = new Map<Value, number>();
|
|
356
|
+
const blockOf = new Map<Op, Block>();
|
|
357
|
+
const idxOf = new Map<Op, number>();
|
|
358
|
+
for (const b of fn.blocks) {
|
|
359
|
+
b.ops.forEach((op, i) => {
|
|
360
|
+
blockOf.set(op, b);
|
|
361
|
+
idxOf.set(op, i);
|
|
362
|
+
const use = (v: Value) => {
|
|
363
|
+
(consumers.get(v) ?? consumers.set(v, new Set()).get(v)!).add(op);
|
|
364
|
+
useSlots.set(v, (useSlots.get(v) ?? 0) + 1);
|
|
365
|
+
};
|
|
366
|
+
for (const o of op.operands) {
|
|
367
|
+
use(o);
|
|
368
|
+
}
|
|
369
|
+
for (const s of op.successors) {
|
|
370
|
+
for (const a of s.args) {
|
|
371
|
+
use(a);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** any op that writes memory strictly between two ops of one block — the rule's `memWriteBetween`
|
|
377
|
+
* over the straight line the same-block requirement already pins */
|
|
378
|
+
const writeBetween = (b: Block, lo: number, hi: number): boolean =>
|
|
379
|
+
b.ops.slice(lo + 1, hi).some((x) => EFFECTFUL_OPS.has(x.opcode));
|
|
380
|
+
const standsOnRead = (op0: Op): boolean => {
|
|
381
|
+
const reads = readCone(op0, defOf);
|
|
382
|
+
if (reads === null) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const b = blockOf.get(op0)!;
|
|
386
|
+
return (
|
|
387
|
+
reads.length > 0 &&
|
|
388
|
+
reads.every(
|
|
389
|
+
(r) =>
|
|
390
|
+
blockOf.get(r) === b &&
|
|
391
|
+
(useSlots.get(r.results[0]) ?? 0) === 1 &&
|
|
392
|
+
!writeBetween(b, idxOf.get(r)!, idxOf.get(op0)!),
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
for (const [v, cs] of consumers) {
|
|
397
|
+
const d = defOf.get(v);
|
|
398
|
+
if (cs.size >= 2 && isHomeableDef(d) && !rendersAsAddress(d) && standsOnRead(d)) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Natural loops: a back edge is `latch → header` with the header dominating the latch, and the
|
|
406
|
+
* body is the backward closure from the latch. Shared by the rules inside `analyze` and by the
|
|
407
|
+
* `/merge-home` gate below, so the two cannot disagree about what "inside a loop" means. */
|
|
408
|
+
function naturalLoops(
|
|
409
|
+
fn: Fn,
|
|
410
|
+
dom: Map<Block, Set<Block>>,
|
|
411
|
+
predsOf: Map<Block, Block[]>,
|
|
412
|
+
): { header: Block; body: Set<Block> }[] {
|
|
413
|
+
const loops: { header: Block; body: Set<Block> }[] = [];
|
|
414
|
+
for (const latch of fn.blocks) {
|
|
415
|
+
for (const header of successorsOf(latch)) {
|
|
416
|
+
if (!dom.get(latch)?.has(header)) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const body = new Set<Block>([header]);
|
|
420
|
+
const work = [latch];
|
|
421
|
+
while (work.length) {
|
|
422
|
+
const x = work.pop()!;
|
|
423
|
+
if (!body.has(x)) {
|
|
424
|
+
body.add(x);
|
|
425
|
+
work.push(...(predsOf.get(x) ?? []));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
loops.push({ header, body });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return loops;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** THE MERGE-FEED-HOME variation's scope (rank.ts `/merge-home`, AnalyzeOptions.homeMergeFeeds): the
|
|
435
|
+
* pure defs it materializes.
|
|
436
|
+
*
|
|
437
|
+
* A merge parameter whose incoming edges render ONE value's expression twice or more is a value
|
|
438
|
+
* the source computed once ABOVE the branch and the copy machinery then sank into every arm —
|
|
439
|
+
* `s32 m = (b & 1) ? 0x400 : 0;` becomes the whole `neg/orr/asr #31/and` chain in each arm, which
|
|
440
|
+
* agbcc if-converts per arm instead of holding the value in a register across the branch. Homed at
|
|
441
|
+
* the def, it renders once and the arms read the name.
|
|
442
|
+
*
|
|
443
|
+
* Neither the value's use count nor its multi-render alone is the evidence: the sibling scopes'
|
|
444
|
+
* clientele (a value re-derived at two ordinary uses) is a value the compiler DOES re-materialize
|
|
445
|
+
* for free. What a merge slot adds is that the two renders are mutually exclusive ARMS of one
|
|
446
|
+
* branch. Which side the source spelled is still not derivable (nothing stops a source from
|
|
447
|
+
* writing the expression twice), so this is a differ-refereed candidate variation, never a default.
|
|
448
|
+
*
|
|
449
|
+
* Refusals:
|
|
450
|
+
* • a value whose cone holds a gaddr/laddr (`coneHoldsAddr`) — rendered standalone an `&g + i`
|
|
451
|
+
* loses the memAccess's inline byte-stride cast, the same soundness half the sibling scopes
|
|
452
|
+
* state.
|
|
453
|
+
* • a value that is ITSELF an address (`rendersAsAddress`). A SCOPE boundary rather than that
|
|
454
|
+
* same hazard: what it adds over `coneHoldsAddr` is the pointers with no gaddr/laddr anywhere
|
|
455
|
+
* in their cone — derived from, or loaded from, a pointer param — and there the backend does
|
|
456
|
+
* re-derive the byte cast (`v0 = (u8 *)*a1 + 6;` then `v0 = (u8 *)v0 + 2;`). Address-shaped
|
|
457
|
+
* bases belong to the cast-aware machinery in l3/basecse.ts, scopebase.ts and nearbase.ts;
|
|
458
|
+
* this variation does not offer a second spelling of them.
|
|
459
|
+
* • an op whose answer depends on WHERE it runs, or that can TRAP — `REEVAL_UNSAFE_OPS`, read
|
|
460
|
+
* from the registry rather than re-listed. Both halves carry: for a read WHERE it happens is
|
|
461
|
+
* the read rules' question, and a homed divide becomes an unconditional statement at a def
|
|
462
|
+
* block raise/shortcircuit.ts may have made, on paths C's own `&&` would have re-guarded —
|
|
463
|
+
* the KNOWN GAP `ir/opcodes.ts` books against `HOIST_UNSAFE_OPS`.
|
|
464
|
+
* • an `undef` — the variation's premise is a value the source COMPUTED once above the branch, and
|
|
465
|
+
* an uninitialised register was never computed at all: homed, it spells `v0 = uninit_r5;`,
|
|
466
|
+
* a copy of a value nothing wrote, which no asm can have.
|
|
467
|
+
* • a value in a `&&`/`||`'s guarded cone WHOSE CONE HOLDS a re-evaluation-unsafe op —
|
|
468
|
+
* raise/shortcircuit.ts put that cone above the branch on the contract that the structurer
|
|
469
|
+
* inlines it back under C's own short circuit, so its def block is a FOLD ARTIFACT rather than
|
|
470
|
+
* the block the asm computed in. Naming it there emits `v0 = *p | 1;` above `p != 0`, and
|
|
471
|
+
* `v0 = a2 / a0 | 1;` above the `a0 != 0` the source divided under — the trap half is the one
|
|
472
|
+
* that turns a re-spelling into a fault. Narrowed to that cone deliberately: a PURE guarded
|
|
473
|
+
* value re-spelled above the connective computes the same thing on the same paths, and
|
|
474
|
+
* refusing it outright costs `synthetic:modpow2:ido7.1` 4 → 6 for no soundness gain.
|
|
475
|
+
* • a def block or a join INSIDE A LOOP. A loop-carried merge parameter's "definition above the
|
|
476
|
+
* branch" is the loop's entry initializer, whose placement `/defsite/loop-entry` already
|
|
477
|
+
* decides; homing it here would answer the same question a second time, from a rule that
|
|
478
|
+
* cannot see the loop's kept guard. It has NO corpus reach — the gate admits the same 16 rows
|
|
479
|
+
* with it on and off, in both symbol-map configurations — so what it buys is fan on
|
|
480
|
+
* loop-shaped functions the corpus does not carry. klonoa's LoadBGTilemapData (89 blocks, 25
|
|
481
|
+
* joins with params) is refused by this clause and no other, and lifting it takes that
|
|
482
|
+
* function's map-less enumeration 75264 → 150528 candidates, 102s → 207s. A null there is
|
|
483
|
+
* this refusal, not an absent idiom.
|
|
484
|
+
* The `analyze` scope adds nothing to this list — the whole predicate is here, so the variation's
|
|
485
|
+
* enumeration gate can run it rather than approximate it. */
|
|
486
|
+
function mergeFeedHomes(fn: Fn, dom: Map<Block, Set<Block>>, defOf: Map<Value, Op>, inLoop: Set<Block>): Set<Op> {
|
|
487
|
+
// Every block's incoming COPY SITES — the places its edge assignments render, each once. Neither
|
|
488
|
+
// the predecessor list nor the raw edge list: `predecessors` (ir/core.ts) lists a block once per
|
|
489
|
+
// successor EDGE, so walking it against that block's own successors visits a join two of one
|
|
490
|
+
// block's edges reach k² times, and a value carried on ONE of them then tallies 2.
|
|
491
|
+
//
|
|
492
|
+
// The shape to follow is structure.ts's Regime B: it groups the `switch_br`'s CASE slots by
|
|
493
|
+
// target block (two naming one block are ONE arm carrying both `case` labels, args required to
|
|
494
|
+
// agree), then emits the DEFAULT edge as an entry of its own with its own copies — so a block a
|
|
495
|
+
// case slot and the default both name renders them TWICE. A `cond_br`'s two arms each emit their
|
|
496
|
+
// own even when both name the join. Over-counting a site costs a candidate that homes a value
|
|
497
|
+
// rendered once, which loses on score; under-counting costs the candidate outright.
|
|
498
|
+
const copySitesOf = new Map<Block, Successor[]>();
|
|
499
|
+
const addSite = (sx: Successor): void => {
|
|
500
|
+
const at = copySitesOf.get(sx.block);
|
|
501
|
+
if (at) {
|
|
502
|
+
at.push(sx);
|
|
503
|
+
} else {
|
|
504
|
+
copySitesOf.set(sx.block, [sx]);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
for (const b of fn.blocks) {
|
|
508
|
+
const term = b.ops[b.ops.length - 1];
|
|
509
|
+
const succ = term?.successors ?? [];
|
|
510
|
+
if (term?.opcode === 'switch_br' && succ.length > 0) {
|
|
511
|
+
const seen = new Set<Block>();
|
|
512
|
+
for (const sx of succ.slice(0, -1)) {
|
|
513
|
+
if (!seen.has(sx.block)) {
|
|
514
|
+
seen.add(sx.block);
|
|
515
|
+
addSite(sx);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
addSite(succ[succ.length - 1]!);
|
|
519
|
+
} else {
|
|
520
|
+
for (const sx of succ) {
|
|
521
|
+
addSite(sx);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const joins = fn.blocks.filter(
|
|
526
|
+
(m) => m.params.length > 0 && (copySitesOf.get(m)?.length ?? 0) >= 2 && !inLoop.has(m),
|
|
527
|
+
);
|
|
528
|
+
if (joins.length === 0) {
|
|
529
|
+
return new Set<Op>();
|
|
530
|
+
}
|
|
531
|
+
const blockOf = new Map<Op, Block>();
|
|
532
|
+
for (const b of fn.blocks) {
|
|
533
|
+
for (const op of b.ops) {
|
|
534
|
+
blockOf.set(op, b);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Every value an edge ARGUMENT renders — the argument itself plus its inlined operand cone,
|
|
538
|
+
// memoized per value so one walk serves every slot. The walk stops DESCENDING at an
|
|
539
|
+
// order-sensitive def: under a call or a memory read the value renders at that op's own position,
|
|
540
|
+
// never at this edge. It crosses a trapping divide, which nothing materializes — that renders
|
|
541
|
+
// inline at the copy site and its operands render there with it. The stopping op is still
|
|
542
|
+
// recorded, so a membership test over the cone sees it.
|
|
543
|
+
const coneCache = new Map<Value, Set<Value>>();
|
|
544
|
+
const coneOf = (root: Value): Set<Value> => {
|
|
545
|
+
const hit = coneCache.get(root);
|
|
546
|
+
if (hit) {
|
|
547
|
+
return hit;
|
|
548
|
+
}
|
|
549
|
+
const acc = new Set<Value>();
|
|
550
|
+
const stack = [root];
|
|
551
|
+
while (stack.length) {
|
|
552
|
+
const x = stack.pop()!;
|
|
553
|
+
if (acc.has(x)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
acc.add(x);
|
|
557
|
+
const memo = coneCache.get(x);
|
|
558
|
+
if (memo) {
|
|
559
|
+
for (const y of memo) {
|
|
560
|
+
acc.add(y);
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const d = defOf.get(x);
|
|
565
|
+
if (d && !ORDER_SENSITIVE_OPS.has(d.opcode)) {
|
|
566
|
+
stack.push(...d.operands);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
coneCache.set(root, acc);
|
|
570
|
+
return acc;
|
|
571
|
+
};
|
|
572
|
+
const scGuarded = shortCircuitGuardedValues(fn, defOf);
|
|
573
|
+
/** Does anything in the value's cone answer differently, or trap, at another program point? The
|
|
574
|
+
* cone renders INSIDE the home, so both halves move with it — a read answers whichever stores
|
|
575
|
+
* ran before it, and a divide that only ever ran under a guard divides by zero above it. */
|
|
576
|
+
const coneHoldsReevalUnsafe = (v: Value): boolean =>
|
|
577
|
+
[...coneOf(v)].some((x) => {
|
|
578
|
+
const d = defOf.get(x);
|
|
579
|
+
return d !== undefined && REEVAL_UNSAFE_OPS.has(d.opcode);
|
|
580
|
+
});
|
|
581
|
+
/** may this op's result be materialized at its def — and is this variation the one to do it? */
|
|
582
|
+
const eligible = (op: Op): boolean => {
|
|
583
|
+
const v = op.results[0];
|
|
584
|
+
return (
|
|
585
|
+
v !== undefined &&
|
|
586
|
+
!REEVAL_UNSAFE_OPS.has(op.opcode) &&
|
|
587
|
+
op.opcode !== 'undef' &&
|
|
588
|
+
!(scGuarded.has(v) && coneHoldsReevalUnsafe(v)) &&
|
|
589
|
+
!rendersAsAddress(op) &&
|
|
590
|
+
!coneHoldsAddr(op, defOf)
|
|
591
|
+
);
|
|
592
|
+
};
|
|
593
|
+
const out = new Set<Op>();
|
|
594
|
+
for (const m of joins) {
|
|
595
|
+
for (let i = 0; i < m.params.length; i++) {
|
|
596
|
+
const tally = new Map<Value, number>();
|
|
597
|
+
const carried = new Set<Value>();
|
|
598
|
+
for (const sx of copySitesOf.get(m)!) {
|
|
599
|
+
const a = sx.args[i];
|
|
600
|
+
if (a === undefined) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
carried.add(a);
|
|
604
|
+
for (const v of coneOf(a)) {
|
|
605
|
+
tally.set(v, (tally.get(v) ?? 0) + 1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// The slot's feeders: rendered from 2+ of its edges, CARRIED unchanged by at least one of
|
|
609
|
+
// them, by a def this join's every arm reaches.
|
|
610
|
+
//
|
|
611
|
+
// Both counts carry a different half of the evidence. The 2+ renders are the duplication;
|
|
612
|
+
// what the carried edge adds is that the value IS the merge variable on some path — the
|
|
613
|
+
// register the compiler had to reserve across the branch either way, so the sunk spelling is
|
|
614
|
+
// strictly more work than the asm shows. Without it the scope reaches every shared
|
|
615
|
+
// SUBEXPRESSION of two arms, which agbcc computes per arm — `armkeep` is the row that prices
|
|
616
|
+
// that class, and `subexpr` the fixture that holds the line (armkeep's own arms share only a
|
|
617
|
+
// block PARAMETER, which has no def op to home, so the row never reaches this clause).
|
|
618
|
+
// Only a DOMINATING def is one definition the source could have written above the branch.
|
|
619
|
+
const feeders: Op[] = [];
|
|
620
|
+
for (const [v, n] of tally) {
|
|
621
|
+
const d = n >= 2 && carried.has(v) ? defOf.get(v) : undefined;
|
|
622
|
+
const b = d ? blockOf.get(d) : undefined;
|
|
623
|
+
if (d && b && b !== m && dom.get(m)!.has(b) && !inLoop.has(b) && eligible(d)) {
|
|
624
|
+
feeders.push(d);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Only the MAXIMAL ones. Where a slot has enough edges for two of its arguments to qualify
|
|
628
|
+
// and one sits inside the other's cone, homing both spells `v0 = a1 & 1; v1 = v0 | 8;` where
|
|
629
|
+
// the source spelled one expression. The inner one renders INSIDE the outer's home, so its
|
|
630
|
+
// own is redundant; a feeder that is not covered stays, which is what keeps a second slot's
|
|
631
|
+
// smaller feeder its home.
|
|
632
|
+
const covered = new Set<Value>();
|
|
633
|
+
for (const d of feeders) {
|
|
634
|
+
for (const v of coneOf(d.results[0])) {
|
|
635
|
+
if (v !== d.results[0]) {
|
|
636
|
+
covered.add(v);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const d of feeders) {
|
|
641
|
+
if (!covered.has(d.results[0])) {
|
|
642
|
+
out.add(d);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** rank.ts's enumeration gate for the `/merge-home` variation: does the function HAVE a value the
|
|
651
|
+
* variation would home? The scope itself rather than a restatement of it, so the gate and the rule cannot
|
|
652
|
+
* drift apart — admitting still says only that the rule fires, not that the tree changes. */
|
|
653
|
+
export function hasMergeFeedHome(fn: Fn): boolean {
|
|
654
|
+
const dom = dominators(fn);
|
|
655
|
+
const predsOf = predecessors(fn);
|
|
656
|
+
const inLoop = new Set(naturalLoops(fn, dom, predsOf).flatMap((L) => [...L.body]));
|
|
657
|
+
return mergeFeedHomes(fn, dom, defOpMap(fn), inLoop).size > 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
17
660
|
export interface StructureAnalysis {
|
|
18
661
|
/** every positioned use of a value; a value absent here is dead */
|
|
19
662
|
useSitesOf: Map<Value, UseSite[]>;
|
|
@@ -21,7 +664,8 @@ export interface StructureAnalysis {
|
|
|
21
664
|
opBlock: Map<Op, Block>;
|
|
22
665
|
/** SSA values live at each block's entry */
|
|
23
666
|
liveIn: Map<Block, Set<Value>>;
|
|
24
|
-
/**
|
|
667
|
+
/** defs that must emit as named temps at their own position — calls/loads for effect order,
|
|
668
|
+
* plus the pure defs the homing rules claim */
|
|
25
669
|
materialize: Set<Op>;
|
|
26
670
|
/** cached forward reachability (successors-transitive, excluding the start block itself) */
|
|
27
671
|
reachFrom: (b: Block) => Set<Block>;
|
|
@@ -29,7 +673,7 @@ export interface StructureAnalysis {
|
|
|
29
673
|
* transitively; null = several places / unresolvable (callers treat conservatively) */
|
|
30
674
|
emitPos: (op: Op) => { blk: Block; idx: number } | null;
|
|
31
675
|
/** may an op `isWrite` accepts execute between `def` and a statement at `render`, on any
|
|
32
|
-
* def-avoiding path — the fold-ordering gate (see
|
|
676
|
+
* def-avoiding path — the fold-ordering gate (see `makeMemWriteBetween`) */
|
|
33
677
|
memWriteBetween: (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean) => boolean;
|
|
34
678
|
}
|
|
35
679
|
|
|
@@ -38,7 +682,10 @@ export interface AnalyzeOptions {
|
|
|
38
682
|
* rebuilt. Absent ⇒ the global-aware alias rule below cannot resolve anything and every write
|
|
39
683
|
* bars, exactly as before it existed. */
|
|
40
684
|
defs?: Map<Value, Op>;
|
|
41
|
-
/**
|
|
685
|
+
/** Dominator sets, when the caller already holds them (structure() does) — consumed by the
|
|
686
|
+
* live-across-a-loop rule's back-edge detection. Absent ⇒ that rule stands down. */
|
|
687
|
+
dom?: Map<Block, Set<Block>>;
|
|
688
|
+
/** THE value-home variation (rank.ts `/reread-globals`). A read of a named global is barred from
|
|
42
689
|
* rendering at its use by any write in between — even a store to an unrelated global, which
|
|
43
690
|
* cannot possibly change what it sees. That over-conservatism is what invents the locals the
|
|
44
691
|
* round-5 dogfood measured as its highest-cost defect ("hoists what agbcc re-reads"):
|
|
@@ -46,97 +693,295 @@ export interface AnalyzeOptions {
|
|
|
46
693
|
* gA = v; gB = v; with `s32 v = gValue;` where the source said `gA = gValue; gB = gValue;`
|
|
47
694
|
*
|
|
48
695
|
* With this on, the barrier scan for a load whose address resolves to a named global uses THE
|
|
49
|
-
* shared disjointness query (ir/alias.ts) instead of "any write at all". Materializing
|
|
50
|
-
*
|
|
696
|
+
* shared disjointness query (ir/alias.ts) instead of "any write at all". Materializing a
|
|
697
|
+
* global's READ is always sound (the deref and its cast render at the def's position — contrast
|
|
698
|
+
* homeSharedAddresses below, where the materialized value is an address and soundness is
|
|
699
|
+
* scoped), so today's spelling is never wrong — only sometimes not the one the compiler was
|
|
700
|
+
* given.
|
|
51
701
|
* Which side matches is genuinely per-function (the same dogfood watched agbcc go both ways
|
|
52
|
-
* inside ONE function), so this is a differ-refereed candidate
|
|
702
|
+
* inside ONE function), so this is a differ-refereed candidate variation, never a default.
|
|
703
|
+
*
|
|
704
|
+
* SCOPE, since `readsStayWhereWritten` below now answers the same question — read once and
|
|
705
|
+
* reuse, or read per use — with the opposite default: this variation owns renders in the read's OWN
|
|
706
|
+
* block, where nothing about placement is in evidence and both spellings really do compile.
|
|
707
|
+
* A read whose every render sits in a STRICTLY DOMINATED block is the default's, and on a
|
|
708
|
+
* target that declares it the variation cannot produce the re-read spelling there (the rule
|
|
709
|
+
* materializes first). That is a pre-emption, not a conflict: a per-arm source read compiles to
|
|
710
|
+
* a per-arm load on that compiler, so the sunk spelling is one it did not emit from this asm. */
|
|
53
711
|
rereadGlobals?: boolean;
|
|
54
712
|
/** "does the project declare this global volatile?" — a read of a volatile object may NOT be
|
|
55
|
-
* duplicated or moved, so the
|
|
713
|
+
* duplicated or moved, so the variation above refuses on one. Answers false for a symbol the map
|
|
56
714
|
* does not carry (and for no map at all), which is the same posture the multi-render rule has
|
|
57
715
|
* always had: without a declaration nothing here can know, and the differ referees the extra
|
|
58
|
-
* load. Where the map DOES know, the
|
|
716
|
+
* load. Where the map DOES know, the variation is silent about it rather than wrong. */
|
|
59
717
|
volatileGlobal?: (name: string) => boolean;
|
|
718
|
+
/** The in-place-join variation (rank.ts `/inplace`). A load whose result is a `cond_br` successor
|
|
719
|
+
* ARG feeds a merge: rendered inline it has no name, so the merge param mints a fresh variable
|
|
720
|
+
* and BOTH arms must assign it. Materialized, the naming walk can home the merge in the load's
|
|
721
|
+
* own variable, the identity arm elides, and the `if` renders one-sided — `v = *p; if (v > 31)
|
|
722
|
+
* v = 32;` — which is also what reuses the load's register when recompiled. Whether the source
|
|
723
|
+
* spelled the temp or the overwrite is not derivable from the asm, so this is a differ-refereed
|
|
724
|
+
* candidate variation, never a default. Plain `br` args (loop-carried values) are out of scope:
|
|
725
|
+
* their homes are the loop-param machinery's question. */
|
|
726
|
+
materializeJoinFeeds?: boolean;
|
|
727
|
+
/** The address-home variation (rank.ts `/addr-home`). A pure computed address the asm derived ONCE
|
|
728
|
+
* and dereferenced at 2+ sites renders, by default, re-derived at each use — and the loads
|
|
729
|
+
* through it re-read per use — where the original source may have spelled a pointer local plus
|
|
730
|
+
* scalar temps (`u8 *entry = (u8 *)((a0 << 2) + base); type = entry[1]; idx = entry[0];`).
|
|
731
|
+
* The two spellings are codegen-visible (the re-derive folds each deref offset into its OWN
|
|
732
|
+
* pool literal; the home shares one base register across `[rN, #k]` accesses) and which one
|
|
733
|
+
* the source used is not derivable from asm, so this is a differ-refereed candidate variation,
|
|
734
|
+
* never a default. With it on: a non-const pure value whose MERGE CLASS is consumed ONLY as
|
|
735
|
+
* the base operand of 2+ memory accesses materializes (`sharedBaseClasses` — the class is what
|
|
736
|
+
* makes a base the arms derive and the join dereferences one register rather than three
|
|
737
|
+
* single-use values), and so does a multi-render `load` through such a base.
|
|
738
|
+
* Both rules only ADD materialization, which preserves semantics FOR THE VALUES THE SCOPE
|
|
739
|
+
* ADMITS — the gaddr/laddr cone refusal is the soundness half of that claim (rendered
|
|
740
|
+
* standalone, an address cone's value changes: the byte-stride cast lives at the use — see
|
|
741
|
+
* coneHoldsAddr; those bases stay with l3/basecse.ts, scopebase.ts and nearbase.ts), and the
|
|
742
|
+
* multi-block-loop-header seat refusal is the decline-avoidance half (same as
|
|
743
|
+
* liveAcrossLoop's). An L3 respell variation could not host this one: by structuring's end the loads
|
|
744
|
+
* have already rendered per-use inside separate arms, and only this phase's positioned
|
|
745
|
+
* memory model can merge them into pre-branch temps. */
|
|
746
|
+
homeSharedAddresses?: boolean;
|
|
747
|
+
/** The loop-expression-home variation (rank.ts `/expr-home`). A pure computed value defined outside
|
|
748
|
+
* a loop and consumed by 2+ distinct ops, at least one of them inside that loop, is one the
|
|
749
|
+
* compiler holds in a (callee-saved) register across the iterations — it never re-derives per
|
|
750
|
+
* use in a loop —
|
|
751
|
+
* where the default renders it re-derived at each use; the source may have spelled a typed
|
|
752
|
+
* local (`u32 size = 16 << t;` driving a loop bound, a product and a shift). The home's
|
|
753
|
+
* declared type is the IR value's recovered type, so a u32 value's compares stay unsigned
|
|
754
|
+
* through the local. Straight-line multi-use values stay OUT (the small-constant class the
|
|
755
|
+
* const-across-call scope's note records); shared memory-access bases are `/addr-home`'s.
|
|
756
|
+
* Same refusals as that variation: gaddr/laddr cones and multi-block-loop-header seats. Adding
|
|
757
|
+
* materialization preserves semantics for the admitted values, exactly as above. */
|
|
758
|
+
homeLoopExprs?: boolean;
|
|
759
|
+
/** The derived-read-home variation (rank.ts `/derived-home`). A memory read whose value is not used
|
|
760
|
+
* directly but through a pure computation with 2+ consumers puts the home on the WRONG node:
|
|
761
|
+
* the read materializes (its consumer resolves no single render position) and the computation
|
|
762
|
+
* then re-derives from that local at every use, where the asm computed it ONCE — the read's
|
|
763
|
+
* register died at the computation and the DERIVED value is what a register carried on
|
|
764
|
+
* (`eor r1,r1,r0` keeps `0x3FF ^ REG_KEYINPUT`, never the raw halfword). With this on, a pure
|
|
765
|
+
* non-const value with 2+ consumers whose operand cone bottoms out at a memory read
|
|
766
|
+
* materializes instead; the read then renders exactly once, inside the home.
|
|
767
|
+
*
|
|
768
|
+
* A differ-refereed variation, not a fix: agbcc CSEs a re-derived expression back to one
|
|
769
|
+
* instruction often enough that both spellings do compile, and which one the source spelled is
|
|
770
|
+
* not derivable. What the cone's read supplies is the evidence the straight-line case otherwise
|
|
771
|
+
* lacks — `/expr-home` takes a loop as proof the value stayed in a register, and a
|
|
772
|
+
* freely-re-derivable value (the small-constant class) has no such proof at all, but a value
|
|
773
|
+
* standing on a read the source could not repeat is one the asm computed from a single access.
|
|
774
|
+
*
|
|
775
|
+
* Refusals, all of them about the ONE access the home must reproduce. A cone read used anywhere
|
|
776
|
+
* but the cone (its second use resolves a second render position, so the read would render
|
|
777
|
+
* twice). A cone read outside this value's own block, barred from it by a write, or barred by a
|
|
778
|
+
* read outside the cone (moving the access across a branch, into a loop, or past another access
|
|
779
|
+
* changes which paths read, how often, and in what order). A cone crossing a `call` (homing
|
|
780
|
+
* would move a side effect). A standalone gaddr/laddr in the cone, or a value that is ITSELF an
|
|
781
|
+
* address (rendered standalone the byte-stride cast lands outside the sum — see
|
|
782
|
+
* `rendersAsAddress`). And the multi-block-loop-header seat the sibling variations refuse. */
|
|
783
|
+
homeDerivedReads?: boolean;
|
|
784
|
+
/** The merge-feed-home variation (rank.ts `/merge-home`). A pure value one join's incoming edges
|
|
785
|
+
* render into the SAME parameter slot from 2+ places materializes at its def: the copy machinery
|
|
786
|
+
* has no name to reference, so the default re-derives the whole expression per arm
|
|
787
|
+
* (`m = (-(b & 1) | b & 1) >> 31 & 0x400;` in both) and agbcc if-converts each copy. The scope
|
|
788
|
+
* and every refusal it states are `mergeFeedHomes` above, which the enumeration gate also runs. */
|
|
789
|
+
homeMergeFeeds?: boolean;
|
|
790
|
+
/** DEF-BLOCK PLACEMENT for memory reads — WHERE the read happens, not where the value lives.
|
|
791
|
+
* The sibling of the homing variations above: there the question is which register or offset holds a
|
|
792
|
+
* value, here which BLOCK performs the read. A read whose every render sits in a block its own
|
|
793
|
+
* block STRICTLY DOMINATES has no rule at all above — those variations want 2+ consumers or a shared
|
|
794
|
+
* base — so it sinks and each arm re-reads it: a second load either way, plus a second pool
|
|
795
|
+
* literal when the address folded to a constant.
|
|
796
|
+
*
|
|
797
|
+
* A compiler behavior (TargetDescription.compilerBehaviors `readsStayWhereWritten`), not
|
|
798
|
+
* a differ-refereed variation, and that distinction is the argument: a variation exists where the asm
|
|
799
|
+
* UNDERDETERMINES the source, some pass having collapsed two spellings onto one output
|
|
800
|
+
* (`/uns-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
|
|
801
|
+
* block the source spelled it in, re-spelling it at the block the asm read in reproduces that
|
|
802
|
+
* asm while the sunk spelling is one it emits only for a source that read per arm — nothing for
|
|
803
|
+
* the differ to referee, and the extra candidate is pure cost. Which compilers may declare
|
|
804
|
+
* that, and on what evidence, is at the target field; absent ⇒ the rule stands down entirely.
|
|
805
|
+
*
|
|
806
|
+
* Materializing is the conservative DIRECTION — back to the read's own def position, never
|
|
807
|
+
* forward past a write — so it adds no barrier scan; SINKING is what needs one (the
|
|
808
|
+
* multi-render rule below) and that scan stays. It is NOT sound by construction: the def block
|
|
809
|
+
* is the asm's read block only while nothing moved the def and a branch really does lie
|
|
810
|
+
* between, which is what the refusals are for. Getting that wrong emits a read on a path the
|
|
811
|
+
* asm never ran it on.
|
|
812
|
+
*
|
|
813
|
+
* Five refusals:
|
|
814
|
+
* • a multi-block loop HEADER, whose test-at-top condition seats no temp — `multiBlockHeaders`
|
|
815
|
+
* • a FALL-THROUGH between def and render, where the dominance is the frontend's
|
|
816
|
+
* block-per-label and no branch separates them — `fallThroughSeam`
|
|
817
|
+
* • a loop PREHEADER of the loop the renders are in, where loop invariant motion parks a read
|
|
818
|
+
* the source wrote in the BODY — `preheaderOfRenderLoop`
|
|
819
|
+
* • a read C evaluates only under a `&&`/`||`, whose def block raise/shortcircuit.ts made —
|
|
820
|
+
* `shortCircuitGuarded`
|
|
821
|
+
* • a read that IS a block parameter's incoming copy, where the seat manufactures a copy the
|
|
822
|
+
* compiler never emitted — `onlyFeedsBlockParams` */
|
|
823
|
+
readsStayWhereWritten?: boolean;
|
|
60
824
|
}
|
|
61
825
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
826
|
+
/** THE def-block placement rule's loop refusal: is `b` a PREHEADER of some loop whose body holds
|
|
827
|
+
* one of the render blocks — outside the body, and a predecessor of the header?
|
|
828
|
+
*
|
|
829
|
+
* Loop invariant motion (loop.c, which agbcc does compile and run at -O2) is the pass whose
|
|
830
|
+
* landing spot the source could not have spelled: it parks the read BELOW the loop guard, where
|
|
831
|
+
* a read the source wrote above the loop sits above it. Compiled,
|
|
832
|
+
* `for (i=0;i<n;i++) t += gK*i;` emits `mov r2,#0 / cmp r2,r3 / bge .L4 / ldr r0,.L8 /
|
|
833
|
+
* ldr r4,[r0]` — guard first, read after — while hoisting `gK` into a local above the loop by
|
|
834
|
+
* hand puts both `ldr`s ahead of the guard. So a read in the preheader is evidence of a read in
|
|
835
|
+
* the BODY, and inferring def-block placement there spells the one source the asm rules out.
|
|
836
|
+
* (With an aliasing store in the loop agbcc hoists only the address constant and leaves the
|
|
837
|
+
* `ldr` in the body — same conclusion, weaker premise.)
|
|
838
|
+
*
|
|
839
|
+
* Narrow on purpose: a loop merely lying between def and render is not this shape. */
|
|
840
|
+
function preheaderOfRenderLoop(
|
|
841
|
+
loopBodies: readonly { header: Block; body: Set<Block> }[],
|
|
842
|
+
b: Block,
|
|
843
|
+
renders: readonly Block[],
|
|
844
|
+
): boolean {
|
|
845
|
+
return loopBodies.some(
|
|
846
|
+
(L) => !L.body.has(b) && successorsOf(b).includes(L.header) && renders.some((x) => L.body.has(x)),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** THE def-block placement rule's seam refusal: does a FALL-THROUGH alone put a render below
|
|
851
|
+
* `b` — a chain of unconditional `br` edges, each into a block whose only predecessor is the
|
|
852
|
+
* one before it?
|
|
853
|
+
*
|
|
854
|
+
* The frontends start a block at every LABEL, so a label nothing branches to cuts one straight
|
|
855
|
+
* line of asm in two and the upper half dominates the lower with no control flow between. The
|
|
856
|
+
* rule's premise is that the compiler will not move a read ACROSS a branch, which says nothing
|
|
857
|
+
* there — while WITHIN a straight line agbcc picks the order itself. Compiled, klonoa's
|
|
858
|
+
* StreamCmd_SetMusicParams (a stray `sub_0804E9AC:` between its last `ldrh r2,[r4]` and the
|
|
859
|
+
* `bl` consuming it) assembles byte-identical to its object from the inlined read and four
|
|
860
|
+
* bytes off from the named one, which swaps that `ldrh` with the `ldr r0,pool` beside it. Of
|
|
861
|
+
* the rule's 20 firings over the 464 klonoa agbcc functions, 13 were across such a seam. */
|
|
862
|
+
function fallThroughSeam(predsOf: Map<Block, Block[]>, b: Block, renders: readonly Block[]): boolean {
|
|
863
|
+
const seen = new Set<Block>([b]);
|
|
864
|
+
for (let cur = b; ;) {
|
|
865
|
+
const t = cur.ops[cur.ops.length - 1];
|
|
866
|
+
if (t?.opcode !== 'br') {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
const next = t.successors[0].block;
|
|
870
|
+
if (predsOf.get(next)!.length !== 1 || seen.has(next)) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
if (renders.includes(next)) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
seen.add(next);
|
|
877
|
+
cur = next;
|
|
81
878
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** THE def→render path discipline. May an op `isWrite` accepts execute between `def` and a
|
|
882
|
+
* statement at `render`, on any def-avoiding path? The def block's tail, the render block's head,
|
|
883
|
+
* and every between-block on a path; a path re-crossing the def is the NEXT dynamic instance and
|
|
884
|
+
* does not count. Path-based on purpose: `fn.blocks` is ADDRESS order, so a linear-position scan
|
|
885
|
+
* misses a block laid out after the render that executes between def and render on the taken path
|
|
886
|
+
* (an audit round broke exactly that way). */
|
|
887
|
+
function makeMemWriteBetween(deps: {
|
|
888
|
+
opBlock: Map<Op, Block>;
|
|
889
|
+
opIndex: Map<Op, number>;
|
|
890
|
+
reachAvoiding: (from: Block, avoid: Block) => Set<Block>;
|
|
891
|
+
}): (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean) => boolean {
|
|
892
|
+
const { opBlock, opIndex, reachAvoiding } = deps;
|
|
893
|
+
return (def: Op, render: { blk: Block; idx: number }, isWrite: (x: Op) => boolean): boolean => {
|
|
894
|
+
const b = opBlock.get(def)!;
|
|
895
|
+
const oi = opIndex.get(def)!;
|
|
896
|
+
const wDirty = (list: Op[], from: number, to: number): boolean => {
|
|
897
|
+
for (let k = from; k < to; k++) {
|
|
898
|
+
if (isWrite(list[k])) {
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return false;
|
|
903
|
+
};
|
|
904
|
+
// Same block: the only def-avoiding path is the straight line between the two indices
|
|
905
|
+
// (leaving and re-entering the block re-crosses the def). A render BEFORE the def cannot
|
|
906
|
+
// happen — within a block, uses follow defs — and falls through to the path walk, whose
|
|
907
|
+
// answer is the conservative one.
|
|
908
|
+
if (render.blk === b && oi < render.idx) {
|
|
909
|
+
return wDirty(b.ops, oi + 1, render.idx);
|
|
910
|
+
}
|
|
911
|
+
if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(render.blk.ops, 0, render.idx)) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
for (const x of reachAvoiding(b, b)) {
|
|
915
|
+
if (x === render.blk && !reachAvoiding(render.blk, b).has(render.blk)) {
|
|
916
|
+
continue; // acyclic render block: head checked
|
|
917
|
+
}
|
|
918
|
+
if (x !== render.blk && !reachAvoiding(x, b).has(render.blk)) {
|
|
919
|
+
continue; // not on a def→render path
|
|
920
|
+
}
|
|
921
|
+
if (wDirty(x.ops, 0, x.ops.length)) {
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** INTERDEPENDENT PARALLEL-COPY ARGS. A terminator's successor args are ONE parallel copy
|
|
930
|
+
* (argAssigns), whose every read means the PRE-copy value. An arg whose def-tree reads a SIBLING
|
|
931
|
+
* arg of the same edge cannot read it by name there — so the sibling's whole expression is
|
|
932
|
+
* re-derived inside this arg's copy, and `sequentialize` spills old-value temps to untangle the
|
|
933
|
+
* order: arithmetic the compiler performed once, emitted per reader (the coupled-recurrence loop,
|
|
934
|
+
* `a += b*c; d += a;`). The read sibling is the register the copy machinery cannot represent —
|
|
935
|
+
* its def is the one the caller materializes. The walk crosses pure defs only (a call/load render
|
|
936
|
+
* is those rules' question) and stops at params, which always carry a name. */
|
|
937
|
+
function copyInterdependentValues(fn: Fn, defOf: Map<Value, Op>): Set<Value> {
|
|
938
|
+
const copyInterdependent = new Set<Value>();
|
|
939
|
+
const pureReads = (root: Value, acc: Set<Value>) => {
|
|
940
|
+
const stack = [root];
|
|
941
|
+
while (stack.length) {
|
|
942
|
+
const x = stack.pop()!;
|
|
943
|
+
const d = defOf.get(x);
|
|
944
|
+
if (!d || acc.has(x)) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
acc.add(x);
|
|
948
|
+
if (d.opcode !== 'call' && d.opcode !== 'load' && d.opcode !== 'aload') {
|
|
949
|
+
stack.push(...d.operands);
|
|
99
950
|
}
|
|
100
951
|
}
|
|
101
|
-
}
|
|
102
|
-
/** True if `def`'s value is still needed after a call — a call lies strictly between the def and
|
|
103
|
-
* one of its `consumers`. Such a value survives in a callee-saved register (a local), which is
|
|
104
|
-
* what materializing it reproduces. */
|
|
105
|
-
const liveAcrossCall = (def: Op, consumers: Op[]): boolean => {
|
|
106
|
-
const dp = linPos(def);
|
|
107
|
-
const usePos = consumers.map(linPos);
|
|
108
|
-
return callPos.some((c) => c > dp && usePos.some((u) => u > c));
|
|
109
952
|
};
|
|
110
953
|
for (const b of fn.blocks) {
|
|
111
|
-
b.ops.
|
|
112
|
-
if (
|
|
113
|
-
|
|
954
|
+
for (const s of b.ops[b.ops.length - 1]?.successors ?? []) {
|
|
955
|
+
if (s.args.length < 2) {
|
|
956
|
+
continue;
|
|
114
957
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
arr.push(site);
|
|
120
|
-
} else {
|
|
121
|
-
useSitesOf.set(v, [site]);
|
|
958
|
+
for (const w of s.args) {
|
|
959
|
+
const d = defOf.get(w);
|
|
960
|
+
if (!d || d.opcode === 'call' || d.opcode === 'load' || d.opcode === 'aload') {
|
|
961
|
+
continue;
|
|
122
962
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
963
|
+
const reads = new Set<Value>();
|
|
964
|
+
for (const o of d.operands) {
|
|
965
|
+
pureReads(o, reads);
|
|
966
|
+
}
|
|
967
|
+
for (const v of s.args) {
|
|
968
|
+
if (v !== w && reads.has(v)) {
|
|
969
|
+
copyInterdependent.add(v);
|
|
970
|
+
}
|
|
130
971
|
}
|
|
131
972
|
}
|
|
132
|
-
}
|
|
973
|
+
}
|
|
133
974
|
}
|
|
975
|
+
return copyInterdependent;
|
|
976
|
+
}
|
|
134
977
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
978
|
+
/** PER-BLOCK LIVENESS of SSA values — backward dataflow. Successor args count as uses at the END
|
|
979
|
+
* of the predecessor (they render in the predecessor's argAssigns), so liveIn(B) means precisely
|
|
980
|
+
* "read at-or-after B's entry". Consumed by the coalescing interference check: merging two values
|
|
981
|
+
* that are ever simultaneously live into one variable name is the textbook silent clobber.
|
|
982
|
+
*
|
|
983
|
+
* `returnsVoid` suppresses the phantom `ret` operand, the same way the use registry does. */
|
|
984
|
+
function blockLiveIn(fn: Fn, returnsVoid: boolean): Map<Block, Set<Value>> {
|
|
140
985
|
const liveIn = new Map<Block, Set<Value>>();
|
|
141
986
|
for (const b of fn.blocks) {
|
|
142
987
|
liveIn.set(b, new Set());
|
|
@@ -177,17 +1022,22 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
177
1022
|
}
|
|
178
1023
|
}
|
|
179
1024
|
}
|
|
1025
|
+
return liveIn;
|
|
1026
|
+
}
|
|
180
1027
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1028
|
+
/** FORWARD REACHABILITY over the CFG, in the two flavours the materialization rules ask for.
|
|
1029
|
+
*
|
|
1030
|
+
* `reachFrom` is the plain successors-transitive set, excluding the start block itself, cached per
|
|
1031
|
+
* factory — which is why this is a factory: the cache belongs to one `analyze` call.
|
|
1032
|
+
*
|
|
1033
|
+
* `reachAvoiding` never passes THROUGH `avoid` — the def-block-avoiding form for per-iteration
|
|
1034
|
+
* path checks: a path that re-enters the def's block re-executes the def, so writes on it belong
|
|
1035
|
+
* to the NEXT dynamic instance (which re-renders anyway) and must not count against this one.
|
|
1036
|
+
* Uncached (per-decision graphs are small). */
|
|
1037
|
+
function makeReach(): {
|
|
1038
|
+
reachFrom: (b: Block) => Set<Block>;
|
|
1039
|
+
reachAvoiding: (from: Block, avoid: Block) => Set<Block>;
|
|
1040
|
+
} {
|
|
191
1041
|
const reachCache = new Map<Block, Set<Block>>();
|
|
192
1042
|
const reachFrom = (b: Block): Set<Block> => {
|
|
193
1043
|
let r = reachCache.get(b);
|
|
@@ -207,7 +1057,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
207
1057
|
reachCache.set(b, r);
|
|
208
1058
|
return r;
|
|
209
1059
|
};
|
|
210
|
-
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding
|
|
1060
|
+
// Reachability that never passes THROUGH `avoid` — the def-block-avoiding form for
|
|
211
1061
|
// per-iteration path checks: a path that re-enters the def's block re-executes the def, so
|
|
212
1062
|
// writes on it belong to the NEXT dynamic instance (which re-renders anyway) and must not
|
|
213
1063
|
// count against this one. Uncached (per-decision graphs are small).
|
|
@@ -228,6 +1078,133 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
228
1078
|
}
|
|
229
1079
|
return r;
|
|
230
1080
|
};
|
|
1081
|
+
return { reachFrom, reachAvoiding };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {}): StructureAnalysis {
|
|
1085
|
+
const {
|
|
1086
|
+
defs,
|
|
1087
|
+
dom,
|
|
1088
|
+
rereadGlobals = false,
|
|
1089
|
+
volatileGlobal,
|
|
1090
|
+
materializeJoinFeeds = false,
|
|
1091
|
+
homeSharedAddresses = false,
|
|
1092
|
+
homeLoopExprs = false,
|
|
1093
|
+
homeDerivedReads = false,
|
|
1094
|
+
homeMergeFeeds = false,
|
|
1095
|
+
readsStayWhereWritten = false,
|
|
1096
|
+
} = opts;
|
|
1097
|
+
// ── use registry ────────────────────────────────────────────────────────────────────────
|
|
1098
|
+
// Every use of a value, POSITIONED: the consuming op and its block/index. Successor args are
|
|
1099
|
+
// uses AT the terminator (they render in argAssigns at block end). A void function's `ret`
|
|
1100
|
+
// operand is a phantom, not a real use — skipping it lets a call whose result ONLY flows into
|
|
1101
|
+
// the suppressed return read as a dead (side-effect) call, so `sideEffects()` emits it.
|
|
1102
|
+
// One operand SLOT = one entry (an op reading a value twice records two uses — that count is
|
|
1103
|
+
// what decides whether an inlined call would EXECUTE twice).
|
|
1104
|
+
const useSitesOf = new Map<Value, UseSite[]>();
|
|
1105
|
+
const opIndex = new Map<Op, number>();
|
|
1106
|
+
const opBlock = new Map<Op, Block>();
|
|
1107
|
+
const blockPos = new Map<Block, number>();
|
|
1108
|
+
for (const b of fn.blocks) {
|
|
1109
|
+
blockPos.set(b, blockPos.size);
|
|
1110
|
+
b.ops.forEach((op, i) => {
|
|
1111
|
+
opIndex.set(op, i);
|
|
1112
|
+
opBlock.set(op, b);
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
// Linear program position (block order, then op order): a call "between" a def and a use is one
|
|
1116
|
+
// whose position lies strictly between them. This is a PROXY for "a call on a def→use path", not
|
|
1117
|
+
// the real dataflow: it checks position order only, not reachability. It can therefore FALSE-
|
|
1118
|
+
// POSITIVE — a call in a forward SIBLING branch (never traversed on the def→use path) still sits
|
|
1119
|
+
// between them by position (e.g. `def; if(c){call;ret} else {…use…use}`), and back-edges are not
|
|
1120
|
+
// modelled either. That is SAFE ONLY BECAUSE the caller materializes exactly `const` ops: a const
|
|
1121
|
+
// is a relocation-invariant leaf whose def dominates every use, so binding it to a local is
|
|
1122
|
+
// UNCONDITIONALLY semantics-preserving on every path — a false positive costs at most a match (an
|
|
1123
|
+
// extra `v =` the compiler would have re-inlined), caught by the zero-lost gate, never wrong C.
|
|
1124
|
+
// RE-VERIFY this before widening the whitelist to any value that is not path-independent or that
|
|
1125
|
+
// carries a use-site cast (an address computation `&g + i`), for which a false positive is unsound.
|
|
1126
|
+
const linPos = (op: Op): number => blockPos.get(opBlock.get(op)!)! * 1e6 + opIndex.get(op)!;
|
|
1127
|
+
const callPos: number[] = [];
|
|
1128
|
+
for (const b of fn.blocks) {
|
|
1129
|
+
for (const op of b.ops) {
|
|
1130
|
+
if (op.opcode === 'call') {
|
|
1131
|
+
callPos.push(linPos(op));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// Which values ride a branch edge as a successor ARG, in two scopes with two different readers.
|
|
1136
|
+
//
|
|
1137
|
+
// `branchArgFed` is EVERY multi-successor terminator's, and it is a correctness fact: an edge
|
|
1138
|
+
// argument is emitted INSIDE the arm it belongs to, so a value that renders only there runs only
|
|
1139
|
+
// on that path. For a call that is an effect the IR performs unconditionally and the C performs
|
|
1140
|
+
// sometimes, and a `switch_br` does it as readily as a `cond_br`. Plain `br` args are excluded
|
|
1141
|
+
// because their edge copy is on the one path that reaches the successor, so nothing conditional
|
|
1142
|
+
// happens to them.
|
|
1143
|
+
//
|
|
1144
|
+
// `condBrArgFed` is the `/inplace` variation's narrower reading — a preference about where a load's
|
|
1145
|
+
// value is homed, whose own scope note is on AnalyzeOptions.materializeJoinFeeds.
|
|
1146
|
+
const branchArgFed = new Set<Value>();
|
|
1147
|
+
const condBrArgFed = new Set<Value>();
|
|
1148
|
+
for (const b of fn.blocks) {
|
|
1149
|
+
for (const op of b.ops) {
|
|
1150
|
+
if (op.successors.length > 1) {
|
|
1151
|
+
for (const s of op.successors) {
|
|
1152
|
+
for (const a of s.args) {
|
|
1153
|
+
branchArgFed.add(a);
|
|
1154
|
+
if (op.opcode === 'cond_br') {
|
|
1155
|
+
condBrArgFed.add(a);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
/** True if `def`'s value is still needed after a call — a call lies strictly between the def and
|
|
1163
|
+
* one of its `consumers`. Such a value survives in a callee-saved register (a local), which is
|
|
1164
|
+
* what materializing it reproduces. */
|
|
1165
|
+
const liveAcrossCall = (def: Op, consumers: Op[]): boolean => {
|
|
1166
|
+
const dp = linPos(def);
|
|
1167
|
+
const usePos = consumers.map(linPos);
|
|
1168
|
+
return callPos.some((c) => c > dp && usePos.some((u) => u > c));
|
|
1169
|
+
};
|
|
1170
|
+
for (const b of fn.blocks) {
|
|
1171
|
+
b.ops.forEach((op, i) => {
|
|
1172
|
+
if (returnsVoid && op.opcode === 'ret') {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const site: UseSite = { blk: b, idx: i, op };
|
|
1176
|
+
const add = (v: Value) => {
|
|
1177
|
+
const arr = useSitesOf.get(v);
|
|
1178
|
+
if (arr) {
|
|
1179
|
+
arr.push(site);
|
|
1180
|
+
} else {
|
|
1181
|
+
useSitesOf.set(v, [site]);
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
for (const u of op.operands) {
|
|
1185
|
+
add(u);
|
|
1186
|
+
}
|
|
1187
|
+
for (const s of op.successors) {
|
|
1188
|
+
for (const a of s.args) {
|
|
1189
|
+
add(a);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const liveIn = blockLiveIn(fn, returnsVoid);
|
|
1196
|
+
|
|
1197
|
+
// ── the effect-ordering model — inline-at-use barriers ────────────────────────────────────
|
|
1198
|
+
// `expr()` renders a def's computation AT ITS USE, which silently MOVES it: a call executes
|
|
1199
|
+
// once per rendered copy (`foo(a0)+foo(a0)`), a load reads memory at the render point (it can
|
|
1200
|
+
// textually sink past an aliasing store). The model: a call/load/aload def may inline ONLY
|
|
1201
|
+
// when rendering cannot change behavior — exactly one render position, and the program-order
|
|
1202
|
+
// gap between def and render crosses no memory write (loads) / no memory access at all (calls,
|
|
1203
|
+
// whose own reads+writes must not reorder against anything). Every other case gets a NAMED
|
|
1204
|
+
// TEMP assigned at the def's own program position (sideEffects) — which is precisely the
|
|
1205
|
+
// register the compiler used.
|
|
1206
|
+
const materialize = new Set<Op>();
|
|
1207
|
+
const { reachFrom, reachAvoiding } = makeReach();
|
|
231
1208
|
// Where a value's expression is ultimately EMITTED: the anchored consumer (statement op,
|
|
232
1209
|
// terminator, materialized def) it inlines into, transitively through single-use pure ops.
|
|
233
1210
|
// null = renders in several places / unresolvable (treated conservatively by the caller).
|
|
@@ -258,7 +1235,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
258
1235
|
};
|
|
259
1236
|
// EVERY position a value's expression renders at — `emitPos` generalized to the whole set (it
|
|
260
1237
|
// answers one place or gives up), by following ALL consumers transitively. That matters for
|
|
261
|
-
// the value-home
|
|
1238
|
+
// the value-home variation: a pure expression with two consumers (`gOut = e; return e;`) has no single
|
|
262
1239
|
// emit position, so `emitPos` answers null and every memory read feeding it is forced into a
|
|
263
1240
|
// local — even when re-reading at both places is provably equivalent. Null only for a genuine
|
|
264
1241
|
// cycle (defensive: SSA use-def is acyclic through ops), which the caller treats as unresolvable.
|
|
@@ -301,47 +1278,193 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
301
1278
|
emitPosSetCache.set(op, res);
|
|
302
1279
|
return res;
|
|
303
1280
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1281
|
+
const memWriteBetween = makeMemWriteBetween({ opBlock, opIndex, reachAvoiding });
|
|
1282
|
+
const defOf = defs ?? defOpMap(fn);
|
|
1283
|
+
const copyInterdependent = copyInterdependentValues(fn, defOf);
|
|
1284
|
+
// ── natural loops, for the live-across-a-loop rule ────────────────────────────────────────
|
|
1285
|
+
// From the caller's dominators (a back edge is `latch → header` with the header dominating the
|
|
1286
|
+
// latch); the body is the backward closure from the latch. With no `dom` the rule stands
|
|
1287
|
+
// down — the same posture as the `defs`-carried rules.
|
|
1288
|
+
const predsOf = predecessors(fn);
|
|
1289
|
+
const loopBodies = dom ? naturalLoops(fn, dom, predsOf) : [];
|
|
1290
|
+
/** The def's value enters some loop's header live and every consumer sits outside that loop,
|
|
1291
|
+
* as does the def: the value is carried ACROSS the loop, not into it. */
|
|
1292
|
+
// Never for a def in a MULTI-BLOCK loop header: a test-at-top `while`'s condition has no seat
|
|
1293
|
+
// for a materialized temp (the structurer's headerPure gate), so materializing there trades a
|
|
1294
|
+
// structuring function for a decline. Self-loop headers stay eligible — their kept-guard
|
|
1295
|
+
// do-while form hosts the temp.
|
|
1296
|
+
const multiBlockHeaders = new Set(loopBodies.filter((L) => L.body.size > 1).map((L) => L.header));
|
|
1297
|
+
const liveAcrossLoop = (def: Op, r: Value, consumers: Op[]): boolean =>
|
|
1298
|
+
!multiBlockHeaders.has(opBlock.get(def)!) &&
|
|
1299
|
+
loopBodies.some(
|
|
1300
|
+
(L) =>
|
|
1301
|
+
liveIn.get(L.header)!.has(r) &&
|
|
1302
|
+
!L.body.has(opBlock.get(def)!) &&
|
|
1303
|
+
consumers.every((c) => !L.body.has(opBlock.get(c)!)),
|
|
1304
|
+
);
|
|
1305
|
+
/** Would naming THIS op's own result change its value? Yes when the result is an ADDRESS built
|
|
1306
|
+
* over a gaddr/laddr: rendered standalone an `&g + i` loses the memAccess's inline byte-stride
|
|
1307
|
+
* cast, and the cast-aware base machinery in l3/ serves those bases instead. Asked by the rules
|
|
1308
|
+
* that home an address; a rule homing the SCALAR a load returns is not this case — the name
|
|
1309
|
+
* holds the loaded value and the address stays inline at the deref, which is why the load rules
|
|
1310
|
+
* (live-across-a-loop, join feeds, /addr-home's, def-block placement) do not ask it. */
|
|
1311
|
+
const addressCone = (op0: Op): boolean => coneHoldsAddr(op0, defOf);
|
|
1312
|
+
// ── the address-home variation's scope ────────────────────────────────────────────────────
|
|
1313
|
+
// Over the MERGE CLASS, so a base the arms derive and the join dereferences counts as the one
|
|
1314
|
+
// register it is — see `sharedBaseClasses`. Computed once, and only under the variation.
|
|
1315
|
+
const sharedBaseClass = homeSharedAddresses ? sharedBaseClasses(fn, returnsVoid) : new Set<Value>();
|
|
1316
|
+
// The same question asked of the VALUE ALONE, which is what the two variations below need: their
|
|
1317
|
+
// "shared bases stay the address-home scope's" exclusion is a hand-off between scopes, and
|
|
1318
|
+
// widening it to the class would make them refuse values the address-home scope only claims
|
|
1319
|
+
// when its own variation is ON — a candidate lost with no candidate gained. Where both variations run the
|
|
1320
|
+
// two scopes may claim one value, which is a no-op: `materialize` is a set and the address-home
|
|
1321
|
+
// scope, running first, is what registers `addressHomedBases`.
|
|
1322
|
+
const usedOnlyAsSharedBase = (v: Value): boolean => {
|
|
1323
|
+
const sites = useSitesOf.get(v) ?? [];
|
|
1324
|
+
const consumers = new Set(sites.map((s) => s.op));
|
|
1325
|
+
return (
|
|
1326
|
+
consumers.size >= 2 &&
|
|
1327
|
+
[...consumers].every(
|
|
1328
|
+
(c) => MEM_BASE_OPS.has(c.opcode) && c.operands[0] === v && !c.operands.some((o, i) => i > 0 && o === v),
|
|
1329
|
+
)
|
|
1330
|
+
);
|
|
1331
|
+
};
|
|
1332
|
+
// The merge-feed-home variation's ops, settled BEFORE the fixpoint: the scope reads the IR alone (no
|
|
1333
|
+
// render positions, no `materialize`), so it cannot change as the set grows. Unlike the rules
|
|
1334
|
+
// that stand down without the caller's `dom`, this one computes its own: rank.ts has already
|
|
1335
|
+
// admitted the candidate on `hasMergeFeedHome`, which runs the same scope, so standing down here
|
|
1336
|
+
// would be a refusal nothing reports.
|
|
1337
|
+
let mergeFeedOps = new Set<Op>();
|
|
1338
|
+
if (homeMergeFeeds) {
|
|
1339
|
+
const mdom = dom ?? dominators(fn);
|
|
1340
|
+
const mloops = mdom === dom ? loopBodies : naturalLoops(fn, mdom, predsOf);
|
|
1341
|
+
mergeFeedOps = mergeFeedHomes(fn, mdom, defOf, new Set(mloops.flatMap((L) => [...L.body])));
|
|
1342
|
+
}
|
|
1343
|
+
/** result values the address-home variation materialized — the load rule's admission key */
|
|
1344
|
+
const addressHomedBases = new Set<Value>();
|
|
1345
|
+
/** The loop-expression-home variation's scope: 2+ distinct consumers of the value, at least one of
|
|
1346
|
+
* them inside a loop the def's block is outside (loop model = the caller's dominators; absent ⇒
|
|
1347
|
+
* never).
|
|
1348
|
+
*
|
|
1349
|
+
* The two counts carry different halves of the evidence. ONE consumer inside the loop is what
|
|
1350
|
+
* says the compiler held the value in a callee-saved register across the iterations — it does
|
|
1351
|
+
* not re-derive per use in a loop, and a value crossing the loop boundary is pinned for the
|
|
1352
|
+
* whole nest. The SECOND consumer, anywhere, is what makes the home observable at all: a
|
|
1353
|
+
* single-use value inlines at its one use with the same bytes either way, so homing it can only
|
|
1354
|
+
* add a copy. Values consumed only OUTSIDE any loop are the straight-line class `/derived-home`
|
|
1355
|
+
* serves on its own evidence. */
|
|
1356
|
+
const loopSharedConsumers = (v: Value, defBlk: Block): boolean => {
|
|
1357
|
+
const consumers = [...new Set((useSitesOf.get(v) ?? []).map((s) => s.op))];
|
|
1358
|
+
return (
|
|
1359
|
+
consumers.length >= 2 &&
|
|
1360
|
+
loopBodies.some((L) => !L.body.has(defBlk) && consumers.some((c) => L.body.has(opBlock.get(c)!)))
|
|
1361
|
+
);
|
|
1362
|
+
};
|
|
1363
|
+
/** The derived-read-home variation's scope: does `op0` stand on a memory READ that may render at
|
|
1364
|
+
* `op0`'s own position?
|
|
1365
|
+
*
|
|
1366
|
+
* The cone walk STOPS at a read — its address stays inline at the deref, so nothing below it is
|
|
1367
|
+
* ever rendered standalone — and refuses on anything else that cannot move to `op0`: a `call`
|
|
1368
|
+
* (a side effect), and a gaddr/laddr reached OUTSIDE a read's address (rendered standalone an
|
|
1369
|
+
* `&g + i` loses the memAccess's byte-stride cast). `coneHoldsAddr` cannot answer this: it
|
|
1370
|
+
* crosses reads, so it refuses every value standing on a named global's load — which is this
|
|
1371
|
+
* variation's whole clientele.
|
|
1372
|
+
*
|
|
1373
|
+
* At least one read is required. A value derivable from locals and constants alone is one the
|
|
1374
|
+
* compiler re-materializes for free, and homing it only adds copies — the small-constant class
|
|
1375
|
+
* the const scope's note records.
|
|
1376
|
+
*
|
|
1377
|
+
* Each read must then sit in `op0`'s OWN BLOCK, and reach `op0`'s position with nothing that
|
|
1378
|
+
* writes memory able to execute in between — homing renders the read at `op0`, so both are
|
|
1379
|
+
* about moving it there.
|
|
1380
|
+
*
|
|
1381
|
+
* A read OUTSIDE the cone bars it too. The default's "loads never bar a load" holds for reads
|
|
1382
|
+
* the compiler leaves unsequenced inside ONE expression, which is what the cone's own reads
|
|
1383
|
+
* become; a foreign read renders in a different statement, so moving past it reorders two
|
|
1384
|
+
* accesses — for two MMIO cells (this variation's clientele) an observable swap, as when `A`'s
|
|
1385
|
+
* derived value sits below `B`'s and homing both puts `B`'s read first.
|
|
1386
|
+
*
|
|
1387
|
+
* And each read's value must go NOWHERE BUT the cone: exactly one use site. Homing resolves a
|
|
1388
|
+
* render position for a read that had none, so a SECOND use resolves a second one, and the
|
|
1389
|
+
* multi-render load rule then inlines the read at BOTH — two accesses where the asm has one
|
|
1390
|
+
* `ldrh`, which for a volatile cell is precisely the duplication `volatileGlobal` refuses. Two
|
|
1391
|
+
* homed values over one read is the same shape from the other side (each is the other's second
|
|
1392
|
+
* use), so one test covers both. This is what makes "renders once, inside the home" a property
|
|
1393
|
+
* rather than an aspiration: without it the variation silently doubles a hardware read.
|
|
1394
|
+
*
|
|
1395
|
+
* The same block is what makes the variation's claim true at all: the register handoff it reproduces
|
|
1396
|
+
* is one straight-line run of the asm, `ldrh` into `eor` into three uses. Across blocks WHICH
|
|
1397
|
+
* BLOCK reads is `readsStayWhereWritten`'s question, not this one, and answering it here goes
|
|
1398
|
+
* wrong in both directions — a value below a branch pulls the read into an arm that may not
|
|
1399
|
+
* run, a value inside a loop pulls it in to run per iteration. Neither is a write, so no
|
|
1400
|
+
* barrier sees either; for an ordinary cell they are worse spellings, and for a volatile one
|
|
1401
|
+
* they are a missing access and a duplicated one. */
|
|
1402
|
+
const standsOnMovableRead = (op0: Op, blk: Block): boolean => {
|
|
1403
|
+
const reads = readCone(op0, defOf);
|
|
1404
|
+
if (reads === null) {
|
|
320
1405
|
return false;
|
|
321
|
-
};
|
|
322
|
-
// Same block: the only def-avoiding path is the straight line between the two indices
|
|
323
|
-
// (leaving and re-entering the block re-crosses the def). A render BEFORE the def cannot
|
|
324
|
-
// happen — within a block, uses follow defs — and falls through to the path walk, whose
|
|
325
|
-
// answer is the conservative one.
|
|
326
|
-
if (render.blk === b && oi < render.idx) {
|
|
327
|
-
return wDirty(b.ops, oi + 1, render.idx);
|
|
328
1406
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
1407
|
+
const at = { blk, idx: opIndex.get(op0)! };
|
|
1408
|
+
const coneReads = new Set(reads);
|
|
1409
|
+
const bars = (x: Op): boolean =>
|
|
1410
|
+
EFFECTFUL_OPS.has(x.opcode) || ((x.opcode === 'load' || x.opcode === 'aload') && !coneReads.has(x));
|
|
1411
|
+
return (
|
|
1412
|
+
reads.length > 0 &&
|
|
1413
|
+
reads.every(
|
|
1414
|
+
(r) =>
|
|
1415
|
+
opBlock.get(r) === blk && (useSitesOf.get(r.results[0])?.length ?? 0) === 1 && !memWriteBetween(r, at, bars),
|
|
1416
|
+
)
|
|
1417
|
+
);
|
|
1418
|
+
};
|
|
1419
|
+
/** 2+ distinct consuming ops — the multi-use the pure-op rule reads as a reused register. */
|
|
1420
|
+
const multiConsumer = (v: Value): boolean => new Set((useSitesOf.get(v) ?? []).map((s) => s.op)).size >= 2;
|
|
1421
|
+
/** THE def-block placement rule's short-circuit refusal: values C evaluates only under a
|
|
1422
|
+
* `&&`/`||` — the SECOND operand of every `logic_and`/`logic_or`, and everything it reads.
|
|
1423
|
+
*
|
|
1424
|
+
* raise/shortcircuit.ts recovers a connective by hoisting the guarded arm's whole pure body,
|
|
1425
|
+
* memory reads included, into the block ABOVE the branch (its value form and its control-flow
|
|
1426
|
+
* form both splice that body into the head). ir/opcodes.ts states the safety argument as the
|
|
1427
|
+
* reason a read is deliberately absent from HOIST_UNSAFE_OPS: the structurer inlines it back
|
|
1428
|
+
* into the `&&`/`||` right-hand side, where C's own short circuit re-guards it. So for a value
|
|
1429
|
+
* in that cone the def block is a FOLD ARTIFACT rather than the block the asm read in, and the
|
|
1430
|
+
* whole premise this rule reads placement under does not hold there. Naming it also breaks the
|
|
1431
|
+
* re-guard: `p != 0 && *p != 0` would emit `v0 = *p;` above its own null check.
|
|
1432
|
+
*
|
|
1433
|
+
* An operand[0] cone is unconditional and keeps the rule; only the guarded side is collected. */
|
|
1434
|
+
const shortCircuitGuarded = readsStayWhereWritten ? shortCircuitGuardedValues(fn, defOf) : new Set<Value>();
|
|
1435
|
+
/** THE def-block placement rule's copy refusal: is every use of the value a successor ARGUMENT,
|
|
1436
|
+
* i.e. is the value nothing but a block parameter's incoming copy?
|
|
1437
|
+
*
|
|
1438
|
+
* Then the parameter IS the read's home. Inlined, the edge assignment is the read
|
|
1439
|
+
* (`v1 = mplay->tracks;`); materialized it becomes two names and a copy between them
|
|
1440
|
+
* (`v0 = mplay->tracks; … v1 = v0;`) where the asm loaded straight into the register the
|
|
1441
|
+
* parameter became — `ldr r1,[r4,#0x2C]` once, never a `mov`. Six of klonoa's matched m4a
|
|
1442
|
+
* functions are that shape, and the manufactured copy costs more than the placement gains:
|
|
1443
|
+
* scored against their own objects, dropping the refusal takes m4aMPlayVolumeControl 26→33,
|
|
1444
|
+
* m4aMPlayPitchControl 36→39, m4aMPlayLFOSpeedSet 28→31 and FadeOutBody 69→73.
|
|
1445
|
+
*
|
|
1446
|
+
* Note what this does NOT claim: those reads really do sit above the loop guard in the asm, so
|
|
1447
|
+
* the placement inference was right and the SPELLING is what fails. Seating the read above the
|
|
1448
|
+
* guard AND as the loop variable is loop-init hoisting, a capability this rule does not have. */
|
|
1449
|
+
const argUsedValues = new Set<Value>();
|
|
1450
|
+
const operandUsedValues = new Set<Value>();
|
|
1451
|
+
// Both sets serve that rule alone, so they are built only where it can fire — the same posture
|
|
1452
|
+
// `condBrArgFed` takes above (every other target pays nothing for a behavior it never declares).
|
|
1453
|
+
if (readsStayWhereWritten) {
|
|
1454
|
+
for (const b of fn.blocks) {
|
|
1455
|
+
for (const op of b.ops) {
|
|
1456
|
+
for (const v of op.operands) {
|
|
1457
|
+
operandUsedValues.add(v);
|
|
1458
|
+
}
|
|
1459
|
+
for (const sc of op.successors) {
|
|
1460
|
+
for (const v of sc.args) {
|
|
1461
|
+
argUsedValues.add(v);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
341
1464
|
}
|
|
342
1465
|
}
|
|
343
|
-
|
|
344
|
-
|
|
1466
|
+
}
|
|
1467
|
+
const onlyFeedsBlockParams = (v: Value): boolean => argUsedValues.has(v) && !operandUsedValues.has(v);
|
|
345
1468
|
// Decide in REVERSE program order so a consumer's own materialization is settled before any
|
|
346
1469
|
// producer asks for its emit position (SSA: uses follow defs in dominance/layout order) — and
|
|
347
1470
|
// iterate to a fixpoint for IR whose block layout does not follow dominance (hand-built IR):
|
|
@@ -374,15 +1497,78 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
374
1497
|
// across `foo(...)` calls). WITHOUT a call in its live range the const is instead cheaply
|
|
375
1498
|
// re-materialized at each use (a bare `movs r, #0` per init), so materializing it would
|
|
376
1499
|
// ADD pointless copies and MISS — hence the call gate (the small-constant regression).
|
|
377
|
-
// Cheap deref casts still land on the `index` node at the use, preserving byte strides
|
|
378
|
-
// NON-const
|
|
379
|
-
//
|
|
1500
|
+
// Cheap deref casts still land on the `index` node at the use, preserving byte strides.
|
|
1501
|
+
// Second scope: a NON-const read by a sibling parallel-copy arg (`copyInterdependent`) —
|
|
1502
|
+
// the one place a pure value's inlining is not value-identical rendering but a
|
|
1503
|
+
// re-derivation the copy machinery is forced into. Address cones are excluded from it
|
|
1504
|
+
// (an `&g + i` rendered standalone loses the memAccess's inline `(u8 *)` cast —
|
|
1505
|
+
// cast-aware base materialization is separate), and consts are not in it at all: a
|
|
1506
|
+
// re-derived const is re-materialization, which is the compiler's own behavior.
|
|
380
1507
|
const pr = op.results[0];
|
|
381
1508
|
if (op.opcode === 'const' && pr && useSitesOf.has(pr)) {
|
|
382
1509
|
const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
|
|
383
1510
|
if (cons.length > 1 && liveAcrossCall(op, cons)) {
|
|
384
1511
|
materialize.add(op);
|
|
385
1512
|
}
|
|
1513
|
+
} else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !addressCone(op)) {
|
|
1514
|
+
materialize.add(op);
|
|
1515
|
+
}
|
|
1516
|
+
// Folding the FOUR VARIATION scopes below into one predicate-parameterized scope is BOOKED
|
|
1517
|
+
// in docs/level-tower.md and deliberately unpaid; what it cannot absorb is named there,
|
|
1518
|
+
// along with the price the fourth one added to it (the gate duplication).
|
|
1519
|
+
// Third scope, under the address-home variation only (see AnalyzeOptions.homeSharedAddresses):
|
|
1520
|
+
// a non-const pure value whose MERGE CLASS is used only as the base of 2+ memory
|
|
1521
|
+
// accesses.
|
|
1522
|
+
if (
|
|
1523
|
+
homeSharedAddresses &&
|
|
1524
|
+
op.opcode !== 'const' &&
|
|
1525
|
+
pr &&
|
|
1526
|
+
sharedBaseClass.has(pr) &&
|
|
1527
|
+
!addressCone(op) &&
|
|
1528
|
+
!multiBlockHeaders.has(b)
|
|
1529
|
+
) {
|
|
1530
|
+
materialize.add(op);
|
|
1531
|
+
addressHomedBases.add(pr);
|
|
1532
|
+
}
|
|
1533
|
+
// Fourth scope, under the loop-expression-home variation (AnalyzeOptions.homeLoopExprs): a
|
|
1534
|
+
// pure non-const value with 2+ distinct consumers, at least one of them inside a loop the
|
|
1535
|
+
// def sits outside. Shared bases stay the previous scope's (its load rule needs the
|
|
1536
|
+
// registration).
|
|
1537
|
+
if (
|
|
1538
|
+
homeLoopExprs &&
|
|
1539
|
+
op.opcode !== 'const' &&
|
|
1540
|
+
pr &&
|
|
1541
|
+
!usedOnlyAsSharedBase(pr) &&
|
|
1542
|
+
loopSharedConsumers(pr, b) &&
|
|
1543
|
+
!addressCone(op) &&
|
|
1544
|
+
!multiBlockHeaders.has(b)
|
|
1545
|
+
) {
|
|
1546
|
+
materialize.add(op);
|
|
1547
|
+
}
|
|
1548
|
+
// Fifth scope, under the derived-read-home variation (AnalyzeOptions.homeDerivedReads): a
|
|
1549
|
+
// pure non-const value with 2+ consumers standing on a memory read. Shared bases stay
|
|
1550
|
+
// the third scope's and values consumed across a loop the fourth's; what this one adds
|
|
1551
|
+
// is the straight-line case, which neither reaches.
|
|
1552
|
+
if (
|
|
1553
|
+
homeDerivedReads &&
|
|
1554
|
+
op.opcode !== 'const' &&
|
|
1555
|
+
pr &&
|
|
1556
|
+
!usedOnlyAsSharedBase(pr) &&
|
|
1557
|
+
!rendersAsAddress(op) &&
|
|
1558
|
+
multiConsumer(pr) &&
|
|
1559
|
+
standsOnMovableRead(op, b) &&
|
|
1560
|
+
!multiBlockHeaders.has(b)
|
|
1561
|
+
) {
|
|
1562
|
+
materialize.add(op);
|
|
1563
|
+
}
|
|
1564
|
+
// Sixth scope, under the merge-feed-home variation (AnalyzeOptions.homeMergeFeeds) —
|
|
1565
|
+
// `mergeFeedHomes` above. The only one of the FOUR VARIATION scopes that admits a `const`; the
|
|
1566
|
+
// other const clientele is the first scope, a const live across a call. For the sibling
|
|
1567
|
+
// variations a re-derived const is re-materialization, the compiler's own behavior, while a
|
|
1568
|
+
// const the arms of a branch merge is one it held in a register across them
|
|
1569
|
+
// (`mov r5, #0` once, not per arm).
|
|
1570
|
+
if (homeMergeFeeds && mergeFeedOps.has(op)) {
|
|
1571
|
+
materialize.add(op);
|
|
386
1572
|
}
|
|
387
1573
|
continue;
|
|
388
1574
|
}
|
|
@@ -390,7 +1576,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
390
1576
|
if (!r || !useSitesOf.has(r)) {
|
|
391
1577
|
continue;
|
|
392
1578
|
} // dead call → exprstmt (unchanged)
|
|
393
|
-
// Under the value-home
|
|
1579
|
+
// Under the value-home variation: which named global cell this op reads, if any. A constant-
|
|
394
1580
|
// offset `load` only — an `aload`'s runtime index names no single cell, and a call reads
|
|
395
1581
|
// everything. Null ⇒ every write bars, exactly as before.
|
|
396
1582
|
const cell =
|
|
@@ -398,6 +1584,10 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
398
1584
|
? globalCellOf(defs, op.operands[0], op.attrs.off as number)
|
|
399
1585
|
: null;
|
|
400
1586
|
const barsThisRead = cell && defs && !volatileGlobal?.(cell.name) ? mayWriteGlobal(defs, cell.name) : null;
|
|
1587
|
+
if (materializeJoinFeeds && op.opcode === 'load' && condBrArgFed.has(r)) {
|
|
1588
|
+
materialize.add(op);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
401
1591
|
const sites = useSitesOf.get(r)!;
|
|
402
1592
|
const consumers = [...new Set(sites.map((s) => s.op))];
|
|
403
1593
|
const isCall = op.opcode === 'call';
|
|
@@ -406,13 +1596,79 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
406
1596
|
materialize.add(op);
|
|
407
1597
|
continue;
|
|
408
1598
|
}
|
|
1599
|
+
// …and ONCE means once on EVERY path the asm runs it on, which the multi-site rule above
|
|
1600
|
+
// does not say. `anchored` calls a terminator a single render position, but a branch's edge
|
|
1601
|
+
// copies are emitted inside the ARMS, so a call whose only consumer is an edge argument
|
|
1602
|
+
// renders in one arm and is skipped on the others. Compiled: `s32 t = f2(a); if (a > 0) {
|
|
1603
|
+
// return t; } return 0;` gives `bl f2` ahead of the `cmp`, and inlined at the edge the
|
|
1604
|
+
// recovered C calls `f2` only in the `else`; a `switch_br` arm hides it the same way.
|
|
1605
|
+
// Materializing puts it back at the position the asm executed it. Sole-use only in
|
|
1606
|
+
// practice — a second use already materialized above — and it is 0 of 2288 sa3 functions,
|
|
1607
|
+
// 2 of 412 klonoa ones.
|
|
1608
|
+
if (isCall && branchArgFed.has(r)) {
|
|
1609
|
+
materialize.add(op);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
// Live across a LOOP neither side belongs to: the access ran before the loop and the
|
|
1613
|
+
// value crossed it in a callee-saved register (hipress's `keep = p[1]`, homed in r8 and
|
|
1614
|
+
// touched only by `mov`). Rendering at the use would re-schedule the access to the far
|
|
1615
|
+
// side of the loop — the same refusal liveAcrossCall makes for a call, applied to a loop.
|
|
1616
|
+
if (liveAcrossLoop(op, r, consumers)) {
|
|
1617
|
+
materialize.add(op);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
// ── DEF-BLOCK PLACEMENT (readsStayWhereWritten; see AnalyzeOptions) ──────────────────
|
|
1621
|
+
// Every render in a block this one strictly dominates, with a branch between ⇒ the asm
|
|
1622
|
+
// read once above that branch, and re-spelling the read there reproduces it. ONE render
|
|
1623
|
+
// suffices — the short-circuit-into-a-call shape has exactly one — and an unresolvable
|
|
1624
|
+
// render position refuses, as everywhere else. Both memory reads, spelled positively: a
|
|
1625
|
+
// `call` is the enclosing arm's other member and has its own execute-once rules above.
|
|
1626
|
+
if (
|
|
1627
|
+
(op.opcode === 'load' || op.opcode === 'aload') &&
|
|
1628
|
+
readsStayWhereWritten &&
|
|
1629
|
+
dom &&
|
|
1630
|
+
!multiBlockHeaders.has(b) &&
|
|
1631
|
+
!shortCircuitGuarded.has(r) &&
|
|
1632
|
+
!onlyFeedsBlockParams(r)
|
|
1633
|
+
) {
|
|
1634
|
+
const at = consumers.map((c) => emitPos(c));
|
|
1635
|
+
const rb = at.some((p) => p === null) ? null : [...new Set(at.map((p) => p!.blk))];
|
|
1636
|
+
if (
|
|
1637
|
+
rb &&
|
|
1638
|
+
rb.length > 0 &&
|
|
1639
|
+
rb.every((x) => x !== b && dom.get(x)!.has(b)) &&
|
|
1640
|
+
!preheaderOfRenderLoop(loopBodies, b, rb) &&
|
|
1641
|
+
!fallThroughSeam(predsOf, b, rb)
|
|
1642
|
+
) {
|
|
1643
|
+
materialize.add(op);
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// Address-home variation: a multi-render load THROUGH a base this variation homed re-reads what the
|
|
1648
|
+
// asm read once into the register the home just reproduced — home the value too, at the
|
|
1649
|
+
// load's own position. Only through variation-homed bases (the fixpoint's later sweep sees them
|
|
1650
|
+
// even though reverse order visits the load first); the general multi-render re-read stays
|
|
1651
|
+
// the default rule below. addressHomedBases registers on the same sweep that materializes the
|
|
1652
|
+
// base — safe because no other rule can pre-empt a value the variation would home: base-slot-only
|
|
1653
|
+
// means no successor-arg use (outside copyInterdependent's read set), and the variation's scope
|
|
1654
|
+
// excludes consts, the const arm's only clientele.
|
|
1655
|
+
if (
|
|
1656
|
+
homeSharedAddresses &&
|
|
1657
|
+
op.opcode === 'load' &&
|
|
1658
|
+
consumers.length > 1 &&
|
|
1659
|
+
addressHomedBases.has(op.operands[0]) &&
|
|
1660
|
+
!multiBlockHeaders.has(b)
|
|
1661
|
+
) {
|
|
1662
|
+
materialize.add(op);
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
409
1665
|
// A MULTI-RENDER load re-reads memory at each render — which is exactly what the original
|
|
410
1666
|
// per-use source spelling did (`while (*s != EOS) *d = *s;` reads *s twice per iteration),
|
|
411
1667
|
// so it is sound iff every render still sees the def-time memory: NO write anywhere
|
|
412
1668
|
// between the def and ANY render (cycle-aware, conservative write set). Otherwise a temp.
|
|
413
1669
|
//
|
|
414
|
-
// WHERE it renders. Without the
|
|
415
|
-
// single position (its own value renders in several places) refuses. With the
|
|
1670
|
+
// WHERE it renders. Without the variation: one position per consumer, and a consumer with no
|
|
1671
|
+
// single position (its own value renders in several places) refuses. With the variation a load
|
|
416
1672
|
// resolves the whole SET instead — the second half of the value-home defect, where the
|
|
417
1673
|
// local is invented not by a barrier but because the pure expression downstream is itself
|
|
418
1674
|
// duplicated (`gOut = (gValue << 1) + gValue; return (gValue << 1) + gValue;`). Never for a
|
|
@@ -428,8 +1684,7 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
428
1684
|
continue;
|
|
429
1685
|
}
|
|
430
1686
|
if (poss.length > 1) {
|
|
431
|
-
const
|
|
432
|
-
const isWrite = barsThisRead ?? ((x: Op) => MW.has(x.opcode));
|
|
1687
|
+
const isWrite = barsThisRead ?? ((x: Op) => EFFECTFUL_OPS.has(x.opcode));
|
|
433
1688
|
if (poss.some((p) => memWriteBetween(op, p!, isWrite))) {
|
|
434
1689
|
materialize.add(op);
|
|
435
1690
|
}
|
|
@@ -443,23 +1698,16 @@ export function analyze(fn: Fn, returnsVoid: boolean, opts: AnalyzeOptions = {})
|
|
|
443
1698
|
// exactly as it originally chose to. Loads never bar a load (reads don't conflict).
|
|
444
1699
|
const samePos = (q: { blk: Block; idx: number } | null) => q !== null && q.blk === pos.blk && q.idx === pos.idx;
|
|
445
1700
|
const isBarrier = (x: Op): boolean => {
|
|
446
|
-
// Value-home
|
|
1701
|
+
// Value-home variation: a store/astore this read is PROVABLY disjoint from (a different named
|
|
447
1702
|
// global) does not sequence against it, so the read may still render at its use.
|
|
448
1703
|
if (barsThisRead && (x.opcode === 'store' || x.opcode === 'astore') && !barsThisRead(x)) {
|
|
449
1704
|
return false;
|
|
450
1705
|
}
|
|
451
1706
|
if (x.opcode === 'store') {
|
|
452
|
-
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const lo = op.attrs.off as number,
|
|
457
|
-
lw = op.attrs.width as number;
|
|
458
|
-
const so = x.attrs.off as number,
|
|
459
|
-
sw = x.attrs.width as number;
|
|
460
|
-
if (so + sw <= lo || lo + lw <= so) {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
1707
|
+
// A store to a PROVABLY-DISJOINT slot of the same base never aliases the load
|
|
1708
|
+
// (`disjointConstSlots`, ir/alias.ts). Anything less certain bars.
|
|
1709
|
+
if (!isCall && op.opcode === 'load' && disjointConstSlots(op, x)) {
|
|
1710
|
+
return false;
|
|
463
1711
|
}
|
|
464
1712
|
return true;
|
|
465
1713
|
}
|