@asmlift/core 0.5.0 → 0.6.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 +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -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 +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- 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 +113 -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 +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- 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 +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -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 +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/structure/hazards.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
// asmlift structurer —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// asmlift structurer — the checks a loop emitter runs BEFORE it commits to a loop form, so it can
|
|
2
|
+
// decline loud instead of miscompiling. Two questions live here:
|
|
3
|
+
//
|
|
4
|
+
// • may this loop's updates be emitted before its condition/exit/post-loop reads, or would some
|
|
5
|
+
// read then see a clobbered (post-update) value that the original IR read PRE-update — and
|
|
6
|
+
// which of those reads can be REPAIRED by moving the copy into the body (`sinkable…`);
|
|
7
|
+
// • is a value the emitted form DROPS redundant — `sameAtEntry`, which reads an expression on
|
|
8
|
+
// the loop's entry state so a guard about to be fused away can be checked against the test
|
|
9
|
+
// that replaces it.
|
|
10
|
+
//
|
|
11
|
+
// WHAT MAKES A SUNK COPY LEGAL. The exit edge's value is not moved, it is REBUILT: the copy lands
|
|
12
|
+
// at the top of the body and spells the arg's def-tree again there. Two things have to hold — the
|
|
13
|
+
// tree gives the same answer there, and every name it reads still denotes the same value — and the
|
|
14
|
+
// arg gates in `PREUPDATE_SINK_GATES` are those two plus the degenerate leaf that is neither.
|
|
15
|
+
//
|
|
16
|
+
// The tree must give the same answer at the new point. Two ways it might not, and `REEVAL_UNSAFE_OPS`
|
|
17
|
+
// is the registry view that names both. ORDER: the copy opens the body, so a load in the tree would
|
|
18
|
+
// move ahead of every store the body makes, and an effect would move against the others.
|
|
19
|
+
// SPECULATION: the def dominates the latch, and the loop is single-latch at both call sites, but an
|
|
20
|
+
// early-`return` arm still lets an iteration leave BEFORE the latch — so a tree the top of the body
|
|
21
|
+
// evaluates is one that iteration never evaluated, and a trapping divide would fault where the
|
|
22
|
+
// original returned.
|
|
23
|
+
//
|
|
24
|
+
// One gate covers both because `REEVAL_UNSAFE_OPS` answers both, and the candidate carries no arm
|
|
25
|
+
// information to tell them apart. KNOWN COST: a loop with NO early-return arm speculates nothing,
|
|
26
|
+
// so a divide in its exit-arg tree is refused for a hazard it cannot have. Threading "this loop can
|
|
27
|
+
// leave before its latch" into the candidate is what would recover it; no benchmark row asks yet.
|
|
28
|
+
//
|
|
29
|
+
// And every name the rebuilt expression reads must still denote the same value there. A loop
|
|
30
|
+
// variable does: the update sits at the bottom, so at the top of the body its name holds exactly
|
|
31
|
+
// the value the edge read. A name the body itself defines does NOT — at the top of the body it
|
|
32
|
+
// still holds the previous iteration.
|
|
33
|
+
//
|
|
34
|
+
// KNOWN GAP: `body` is the natural-loop body, which EXCLUDES the blocks an early-return arm owns
|
|
35
|
+
// even though their statements are emitted inside the loop. A name assigned only in such an arm is
|
|
36
|
+
// invisible to both predicates below. Harmless while an arm leaves the function — nothing after it
|
|
37
|
+
// reads the name — and an arm that merely breaks would need it.
|
|
38
|
+
//
|
|
39
|
+
// Every check is PURE: it reads the analysis maps and decides, nothing mutates.
|
|
6
40
|
//
|
|
7
41
|
// The factory takes its dependencies EXPLICITLY (`LoopHazardDeps`), the switch-recover pattern.
|
|
8
42
|
// The maps are captured as LIVE REFERENCES, deliberately: `varName` is still being populated by
|
|
9
43
|
// the naming pipeline when the factory is created, and each hazard check reads whatever names
|
|
10
44
|
// exist at CALL time (emission runs after naming completes). Snapshotting them would break this.
|
|
11
45
|
import { Block, Op, Value } from '../ir/core';
|
|
46
|
+
import { NEGATED_ICMP, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
|
|
12
47
|
import { Stmt } from '../l3/ast';
|
|
48
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
13
49
|
import type { UseSite } from './analysis';
|
|
14
50
|
|
|
15
51
|
export interface LoopHazardDeps {
|
|
@@ -19,6 +55,18 @@ export interface LoopHazardDeps {
|
|
|
19
55
|
varName: Map<Value, string>;
|
|
20
56
|
/** every positioned use of a value (analysis.ts) */
|
|
21
57
|
useSitesOf: Map<Value, UseSite[]>;
|
|
58
|
+
/** values read at-or-after each block's entry (analysis.ts) */
|
|
59
|
+
liveIn: Map<Block, Set<Value>>;
|
|
60
|
+
/** op → the block holding it (analysis.ts) */
|
|
61
|
+
opBlock: Map<Op, Block>;
|
|
62
|
+
/** defs that emit as named temps at their own position (analysis.ts) — `loopWriteSet` reads it
|
|
63
|
+
* to see the in-place write an adopted materialized def performs. LIVE, like `varName`. */
|
|
64
|
+
materialize: Set<Op>;
|
|
65
|
+
/** Ops whose LOWERING discards their operand tree and spells something else — today the
|
|
66
|
+
* bitfield-extract fold, which renders a global member read in place of a shift pair. A walk
|
|
67
|
+
* over the operands cannot see what such an op will render, so predicates that reason about
|
|
68
|
+
* the rendered expression have to treat it as opaque. LIVE, like `varName`. */
|
|
69
|
+
respelledDefs: ReadonlyMap<Op, unknown>;
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
export interface LoopHazards {
|
|
@@ -28,7 +76,6 @@ export interface LoopHazards {
|
|
|
28
76
|
sub: Map<Value, string>,
|
|
29
77
|
updateWrites: Set<string>,
|
|
30
78
|
region?: Set<Block> | null,
|
|
31
|
-
loopParams?: Set<Value>,
|
|
32
79
|
): boolean;
|
|
33
80
|
loopUpdateHazard(
|
|
34
81
|
condV: Value,
|
|
@@ -37,8 +84,18 @@ export interface LoopHazards {
|
|
|
37
84
|
sub: Map<Value, string>,
|
|
38
85
|
updateWrites: Set<string>,
|
|
39
86
|
region: Set<Block> | null,
|
|
40
|
-
loopParams: Set<Value>,
|
|
41
87
|
): boolean;
|
|
88
|
+
sinkablePreUpdateSlots(
|
|
89
|
+
header: Block,
|
|
90
|
+
exit: Block,
|
|
91
|
+
exitArgs: readonly Value[],
|
|
92
|
+
body: Set<Block>,
|
|
93
|
+
sub: Map<Value, string>,
|
|
94
|
+
updateWrites: Set<string>,
|
|
95
|
+
gates?: readonly Gate<SinkCandidate>[],
|
|
96
|
+
): Set<number>;
|
|
97
|
+
sameAtEntry(a: Value, b: Value, entry: Map<Value, Value>, negated?: boolean): boolean;
|
|
98
|
+
loopWriteSet(updates: Stmt[], bodyBlocks: Iterable<Block>, header: Block): Set<string>;
|
|
42
99
|
}
|
|
43
100
|
|
|
44
101
|
/** The names a loop update assigns (its non-identity copies) — the write set every loop-emission
|
|
@@ -46,8 +103,151 @@ export interface LoopHazards {
|
|
|
46
103
|
export const updateWriteSet = (updates: Stmt[]): Set<string> =>
|
|
47
104
|
new Set(updates.filter((st): st is Extract<Stmt, { k: 'assign' }> => st.k === 'assign').map((st) => st.name));
|
|
48
105
|
|
|
106
|
+
/** Why an exit arg cannot be REBUILT at the top of the loop body — see the note above
|
|
107
|
+
* `PREUPDATE_SINK_GATES`. The walk collects EVERY one it finds, not the first: with one blocker
|
|
108
|
+
* per gate, stopping early would let ablating one gate disable another on any tree where the
|
|
109
|
+
* other's blocker happens to be found first, and the ablation would then be measuring less than
|
|
110
|
+
* its name says. */
|
|
111
|
+
export type ArgBlocker = 'order-sensitive' | 'stale-name' | 'no-definition';
|
|
112
|
+
|
|
113
|
+
/** One exit slot weighed for sinking. `destName` is the name the sunk copy would write. */
|
|
114
|
+
export interface SinkCandidate {
|
|
115
|
+
/** everything that stops the arg's def-tree from being rebuilt at the top of the body */
|
|
116
|
+
argBlockers: ReadonlySet<ArgBlocker>;
|
|
117
|
+
destName: string | undefined;
|
|
118
|
+
/** the names of the loop's own variables */
|
|
119
|
+
headerNames: ReadonlySet<string | undefined>;
|
|
120
|
+
/** the names the emitted update assigns */
|
|
121
|
+
updateWrites: ReadonlySet<string>;
|
|
122
|
+
/** some other value under `destName` is read inside the loop */
|
|
123
|
+
destBusyInLoop: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** When a pre-update exit copy may move into the loop body. The set-level
|
|
127
|
+
* rules — two slots wanting one name, a slot that stays behind reading a sunk name — are not in
|
|
128
|
+
* the table because they are properties of the whole edge rather than of a candidate; they live in
|
|
129
|
+
* `sinkablePreUpdateSlots` with the same refusal discipline. */
|
|
130
|
+
export const PREUPDATE_SINK_GATES: readonly Gate<SinkCandidate>[] = [
|
|
131
|
+
{
|
|
132
|
+
id: 'arg-safe-to-reevaluate',
|
|
133
|
+
why: 'an effect, a memory read or a trap gives a different answer where the rebuilt copy lands',
|
|
134
|
+
sound: true,
|
|
135
|
+
guardedBy: 'hazards.test.ts: ablating arg-safe-to-reevaluate admits an exit arg that reads memory',
|
|
136
|
+
rejects: (c) => c.argBlockers.has('order-sensitive'),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'arg-reads-current-names',
|
|
140
|
+
why: 'a value computed in the body still holds the PREVIOUS iteration at the top of it, where the copy lands',
|
|
141
|
+
sound: true,
|
|
142
|
+
guardedBy: 'hazards.test.ts: ablating arg-reads-current-names admits an arg over a body-computed name',
|
|
143
|
+
rejects: (c) => c.argBlockers.has('stale-name'),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'arg-has-a-definition',
|
|
147
|
+
why: 'a leaf with neither a name nor a def renders as a gap, which the contract catches loudly',
|
|
148
|
+
sound: false,
|
|
149
|
+
rejects: (c) => c.argBlockers.has('no-definition'),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
// The update assigns exactly the loop variables' names, so these two tests are one set today;
|
|
153
|
+
// both are spelled so the rule does not rest on that coincidence.
|
|
154
|
+
id: 'dest-not-loop-variable',
|
|
155
|
+
why: 'a copy into a name the loop itself assigns is overwritten by the update, or is a self-assignment',
|
|
156
|
+
sound: true,
|
|
157
|
+
guardedBy: 'hazards.test.ts: ablating dest-not-loop-variable admits a self-assignment',
|
|
158
|
+
rejects: (c) => c.destName === undefined || c.headerNames.has(c.destName) || c.updateWrites.has(c.destName),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'dest-free-inside-loop',
|
|
162
|
+
why: 'the name already denotes a value the loop reads, which a write at the top of the body clobbers',
|
|
163
|
+
sound: true,
|
|
164
|
+
guardedBy: 'hazards.test.ts: ablating dest-free-inside-loop admits a name the loop still reads',
|
|
165
|
+
rejects: (c) => c.destBusyInLoop,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// `x + 0` / `x - 0` / `x | 0` are `x`. Substituting a loop variable by its init constant turns
|
|
170
|
+
// ordinary index arithmetic into exactly these, and a guard that spells the same value without
|
|
171
|
+
// the arithmetic would otherwise compare unequal.
|
|
172
|
+
function fold(defs: Map<Value, Op>, v: Value): Value {
|
|
173
|
+
const d = defs.get(v);
|
|
174
|
+
if (!d || d.operands.length !== 2 || !['add', 'sub', 'or'].includes(d.opcode)) {
|
|
175
|
+
return v;
|
|
176
|
+
}
|
|
177
|
+
const z = defs.get(d.operands[1]);
|
|
178
|
+
return z?.opcode === 'const' && z.attrs?.value === 0 ? fold(defs, d.operands[0]) : v;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Does `a`, read on a loop's ENTRY values, denote the same thing as `b`? `entry` maps each header
|
|
182
|
+
// param and back-edge arg to the arg the forward edge passes, so substituting through it models
|
|
183
|
+
// the FIRST iteration — the state a guard in front of the loop tested. `negated` compares against
|
|
184
|
+
// b's logical opposite instead, for the usual case where a guard spells the loop's own test the
|
|
185
|
+
// other way round (`beq` to the exit vs `bne` to the header).
|
|
186
|
+
//
|
|
187
|
+
// Structural, not semantic: distinct ops with the same opcode, attributes and operands compare
|
|
188
|
+
// equal (two `const 0`s do), anything else does not. A false negative costs a loud decline, which
|
|
189
|
+
// is the direction to be wrong in. Memoised like `readsClobbered`'s `seen`: a value its own
|
|
190
|
+
// consumer reads twice would otherwise double the work at every level.
|
|
191
|
+
function sameAtEntry(defs: Map<Value, Op>, a: Value, b: Value, entry: Map<Value, Value>, negated = false): boolean {
|
|
192
|
+
const memo = new Map<Value, Map<Value, boolean>>();
|
|
193
|
+
const sameOp = (da: Op, db: Op, opcodeOk: boolean): boolean =>
|
|
194
|
+
opcodeOk &&
|
|
195
|
+
da.operands.length === db.operands.length &&
|
|
196
|
+
JSON.stringify(da.attrs ?? null) === JSON.stringify(db.attrs ?? null) &&
|
|
197
|
+
da.operands.every((o, i) => same(o, db.operands[i]));
|
|
198
|
+
const same = (x0: Value, y0: Value): boolean => {
|
|
199
|
+
const x = fold(defs, entry.get(x0) ?? x0);
|
|
200
|
+
const y = fold(defs, y0);
|
|
201
|
+
if (x === y) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
let row = memo.get(x);
|
|
205
|
+
if (!row) {
|
|
206
|
+
memo.set(x, (row = new Map()));
|
|
207
|
+
}
|
|
208
|
+
const hit = row.get(y);
|
|
209
|
+
if (hit !== undefined) {
|
|
210
|
+
return hit;
|
|
211
|
+
}
|
|
212
|
+
const da = defs.get(x);
|
|
213
|
+
const db = defs.get(y);
|
|
214
|
+
const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode);
|
|
215
|
+
row.set(y, r);
|
|
216
|
+
return r;
|
|
217
|
+
};
|
|
218
|
+
if (!negated) {
|
|
219
|
+
return same(a, b);
|
|
220
|
+
}
|
|
221
|
+
const da = defs.get(fold(defs, entry.get(a) ?? a));
|
|
222
|
+
const db = defs.get(fold(defs, b));
|
|
223
|
+
return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode);
|
|
224
|
+
}
|
|
225
|
+
|
|
49
226
|
export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
50
|
-
const { defs, varName, useSitesOf } = deps;
|
|
227
|
+
const { defs, varName, useSitesOf, liveIn, opBlock, materialize, respelledDefs } = deps;
|
|
228
|
+
|
|
229
|
+
// The names one loop iteration writes under its VARIABLES' names: the update copies, plus a
|
|
230
|
+
// loop-variable name a materialized body def writes IN PLACE. Adoption (seedLoopParams) makes
|
|
231
|
+
// that def's update copy an identity — elided, so `updateWriteSet(updates)` alone no longer
|
|
232
|
+
// carries the name — but the write still happens mid-body via sideEffects, and a
|
|
233
|
+
// pre-update-read check keyed on the write set is blind to it without this. Non-param
|
|
234
|
+
// materialized names stay out: a fresh temp is assigned once per iteration, so an
|
|
235
|
+
// out-of-position read of it is the current value, not a stale one (its zero-trip hazard is
|
|
236
|
+
// the kept-guard site's separate check).
|
|
237
|
+
const loopWriteSet = (updates: Stmt[], bodyBlocks: Iterable<Block>, header: Block): Set<string> => {
|
|
238
|
+
const writes = updateWriteSet(updates);
|
|
239
|
+
const paramNames = new Set(header.params.map((p) => varName.get(p)));
|
|
240
|
+
for (const bb of bodyBlocks) {
|
|
241
|
+
for (const op of bb.ops) {
|
|
242
|
+
const r = op.results[0];
|
|
243
|
+
const nm = r !== undefined && materialize.has(op) ? varName.get(r) : undefined;
|
|
244
|
+
if (nm !== undefined && paramNames.has(nm)) {
|
|
245
|
+
writes.add(nm);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return writes;
|
|
250
|
+
};
|
|
51
251
|
|
|
52
252
|
// Does rendering `v` under `sub` read a variable that a pending loop update (`updateWrites`, the
|
|
53
253
|
// names it assigns) overwrites, via a path OTHER than a `sub`-mapped back-edge arg? Such a read is a
|
|
@@ -86,12 +286,35 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
86
286
|
sub: Map<Value, string>,
|
|
87
287
|
updateWrites: Set<string>,
|
|
88
288
|
region: Set<Block> | null = null,
|
|
89
|
-
loopParams: Set<Value> = new Set(),
|
|
90
289
|
): boolean => {
|
|
91
|
-
// Body-block PARAMS escape too
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
290
|
+
// Body-block PARAMS escape too, and ONE rule covers all of them: escaped, under a name the
|
|
291
|
+
// update writes. The loop's own carried params used to be exempt outright and are not special
|
|
292
|
+
// — DELETED, not narrowed, and unnarrowable, because the only condition under the exemption
|
|
293
|
+
// was the rule's own. Anything implying NOT that is already what the rule returns; anything
|
|
294
|
+
// admitting it is the unconditional exemption back again. There being no third predicate is
|
|
295
|
+
// why `loopParams` left this file instead of being tightened inside it.
|
|
296
|
+
//
|
|
297
|
+
// Unconditional, it was a SILENT MISCOMPILE. "A post-loop read of the updated name is exactly
|
|
298
|
+
// the intended final value" holds for the BACK-EDGE ARG, which `sub` maps, and not for the
|
|
299
|
+
// PARAM, which is one update behind — and both emitters that passed a nonempty exemption set
|
|
300
|
+
// put the update at the BOTTOM of the body. The emitted C stored `i` where agbcc keeps a
|
|
301
|
+
// second register to store `i-1`; it compiled, it scored, and nothing in the tree noticed.
|
|
302
|
+
//
|
|
303
|
+
// ITS EVIDENCE IS A RIG, NOT THE CORPUS, which matters because the three disjuncts of
|
|
304
|
+
// `loopUpdateHazard` share one decline message. 121 generated loop shapes, each reference and
|
|
305
|
+
// each lift compiled natively and EXECUTED over an identical buffer: 76 wrong lifts became
|
|
306
|
+
// declines, 14 correct ones kept lifting with an identical hash. This clause then fires 0
|
|
307
|
+
// times over the klonoa checkout's 732 `.s` and over the 2737 candidates the 205 agbcc
|
|
308
|
+
// synthetic rows enumerate, where the predicate AROUND it fires 5 and 4 — on the condition, an
|
|
309
|
+
// exit arg, or an escaped op result (`synthetic:preupdate_escape` is the last of those). Those
|
|
310
|
+
// counts, and the rig, were taken at #113.
|
|
311
|
+
//
|
|
312
|
+
// AND IT DECLINES ONLY BECAUSE OF HOW THE VALUE IS SPELLED. `sinkablePreUpdateSlots` below
|
|
313
|
+
// REPAIRS this hazard, re-emitting the copy inside the body ahead of the update, whenever the
|
|
314
|
+
// pre-update value crosses the exit as an edge ARG; read from the header PARAM instead it
|
|
315
|
+
// arrives here, with no exit slot to sink. Both spellings, and the agbcc listing, are in
|
|
316
|
+
// test/loop-preupdate-escape.test.ts. Routing the param one into the sink is a structure.ts
|
|
317
|
+
// change with a fan effect on every row, not an edit to this predicate.
|
|
95
318
|
const escaped = (v: Value): boolean => {
|
|
96
319
|
for (const s of useSitesOf.get(v) ?? []) {
|
|
97
320
|
if (region ? region.has(s.blk) : !body.has(s.blk)) {
|
|
@@ -102,10 +325,9 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
102
325
|
};
|
|
103
326
|
for (const bb of body) {
|
|
104
327
|
for (const pv of bb.params) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (escaped(pv) && updateWrites.has(varName.get(pv)!)) {
|
|
328
|
+
const n = varName.get(pv); // absent while naming is still in progress: not a written name
|
|
329
|
+
|
|
330
|
+
if (n !== undefined && updateWrites.has(n) && escaped(pv)) {
|
|
109
331
|
return true;
|
|
110
332
|
}
|
|
111
333
|
}
|
|
@@ -132,11 +354,180 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
132
354
|
sub: Map<Value, string>,
|
|
133
355
|
updateWrites: Set<string>,
|
|
134
356
|
region: Set<Block> | null,
|
|
135
|
-
loopParams: Set<Value>,
|
|
136
357
|
): boolean =>
|
|
137
358
|
readsClobbered(condV, sub, updateWrites) ||
|
|
138
359
|
exitArgs.some((a) => readsClobbered(a, sub, updateWrites)) ||
|
|
139
|
-
loopEscapeHazard(body, sub, updateWrites, region
|
|
360
|
+
loopEscapeHazard(body, sub, updateWrites, region);
|
|
361
|
+
|
|
362
|
+
// Which pre-update exit copies can be REPAIRED instead of declined. The exiting edge hands a
|
|
363
|
+
// loop variable's top-of-iteration value to a merge param, and post-loop that name has moved on
|
|
364
|
+
// one iteration; emitting the copy inside the body, AHEAD of the update, restores it — the
|
|
365
|
+
// trailing-pointer idiom (`for (fast = slow = head; ...; fast = fast->next) slow = fast;`).
|
|
366
|
+
// Returns the exit slots that may move; the caller emits them at the top of the body and drops
|
|
367
|
+
// them from the post-loop copies.
|
|
368
|
+
//
|
|
369
|
+
// The idiom reaches here at all because the compiler DID keep a second register for the trailing
|
|
370
|
+
// value and SSA construction folded the copy away, leaving the exit edge as the only place the
|
|
371
|
+
// value is still named. Where the compiler kept two loop-carried registers instead, the value is
|
|
372
|
+
// a back-edge arg and the un-rotation substitution already reads it — no repair needed.
|
|
373
|
+
//
|
|
374
|
+
// `PREUPDATE_SINK_GATES` holds the per-candidate refusals, ablatable one at a time. Two rules are
|
|
375
|
+
// properties of the EDGE rather than of a candidate and stay here: the exit edge is a PARALLEL
|
|
376
|
+
// copy, so splitting it across two program points must not let two slots claim one name, nor let
|
|
377
|
+
// a slot that stays behind read a name the body now writes first.
|
|
378
|
+
//
|
|
379
|
+
// The third parallel-copy question — one sunk slot's REBUILT expression reading another sunk
|
|
380
|
+
// slot's destination — needs no rule, and the copies are emitted sequentially because of it.
|
|
381
|
+
// Every leaf such an expression could read is already refused by a per-candidate gate: a header
|
|
382
|
+
// param makes the other slot fail `dest-not-loop-variable`; a body-defined value is
|
|
383
|
+
// `stale-name`; and a value defined outside the loop but read on the exit edge is live-in at the
|
|
384
|
+
// header (analysis.ts counts a successor arg as a use at the predecessor's end), which makes the
|
|
385
|
+
// other slot fail `dest-free-inside-loop`.
|
|
386
|
+
const sinkablePreUpdateSlots = (
|
|
387
|
+
header: Block,
|
|
388
|
+
exit: Block,
|
|
389
|
+
exitArgs: readonly Value[],
|
|
390
|
+
body: Set<Block>,
|
|
391
|
+
sub: Map<Value, string>,
|
|
392
|
+
updateWrites: Set<string>,
|
|
393
|
+
gates: readonly Gate<SinkCandidate>[] = PREUPDATE_SINK_GATES,
|
|
394
|
+
): Set<number> => {
|
|
395
|
+
const none = new Set<number>();
|
|
396
|
+
const headerNames = new Set(header.params.map((p) => varName.get(p)));
|
|
397
|
+
// Is `v` defined by the loop body itself — an op in one of its blocks, or a block param?
|
|
398
|
+
// An op with no `opBlock` entry counts as INSIDE: the map is total over the function, and the
|
|
399
|
+
// safe direction for an absent one is the answer that refuses.
|
|
400
|
+
const definedInBody = (v: Value): boolean => {
|
|
401
|
+
const d = defs.get(v);
|
|
402
|
+
if (!d) {
|
|
403
|
+
return [...body].some((bb) => bb.params.includes(v));
|
|
404
|
+
}
|
|
405
|
+
const b = opBlock.get(d);
|
|
406
|
+
return b === undefined || body.has(b);
|
|
407
|
+
};
|
|
408
|
+
// Does any value OTHER than `self` under `name` live inside the loop? A value defined outside
|
|
409
|
+
// it and read anywhere in the body must be live-in at the header, the loop's only entry, so
|
|
410
|
+
// that check plus the body's own defs and params covers every way the name is still in use.
|
|
411
|
+
// The loop's OWN variables are left to `dest-not-loop-variable` — a header param is also a
|
|
412
|
+
// body param, so the two gates partition the names instead of overlapping.
|
|
413
|
+
const busyInLoop = (name: string, self: Value): boolean => {
|
|
414
|
+
for (const [v, n] of varName) {
|
|
415
|
+
if (n !== name || v === self || header.params.includes(v)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (liveIn.get(header)!.has(v) || definedInBody(v)) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
};
|
|
424
|
+
// Everything that stops `a` from being REBUILT at the top of the body. Walks the def-tree
|
|
425
|
+
// where `exprWith(null)` will when the copy is spelled — stopping at a NAMED value, which
|
|
426
|
+
// renders as its name, and at a value with no reaching def, which renders as a gap.
|
|
427
|
+
//
|
|
428
|
+
// The walk OVER-approximates what renders, which is the safe direction: `lowerDef` recurses
|
|
429
|
+
// only through `e(d.operands[...])`, so every value it reaches is one this visited. Two things
|
|
430
|
+
// would break that. A lowering reaching for a value OUTSIDE its op's operands — none does
|
|
431
|
+
// today, and it is the change to watch for. And a lowering that DISCARDS the operand tree and
|
|
432
|
+
// spells something else: `respelledDefs` is that case, refused outright below rather than
|
|
433
|
+
// walked, because what it renders is a memory read this walk would never have seen.
|
|
434
|
+
//
|
|
435
|
+
// `undef` and `laddr` render a name from their own side maps rather than `varName`, and both
|
|
436
|
+
// are position-independent — an `undef` is never assigned, a `laddr` is an address.
|
|
437
|
+
const blockersOf = (a: Value): ReadonlySet<ArgBlocker> => {
|
|
438
|
+
const seen = new Set<Value>();
|
|
439
|
+
const found = new Set<ArgBlocker>();
|
|
440
|
+
const walk = (x: Value): void => {
|
|
441
|
+
if (seen.has(x)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
seen.add(x);
|
|
445
|
+
if (header.params.includes(x)) {
|
|
446
|
+
return; // a loop variable: at the top of the body its name holds exactly this
|
|
447
|
+
}
|
|
448
|
+
const n = varName.get(x);
|
|
449
|
+
if (n !== undefined) {
|
|
450
|
+
if (definedInBody(x) || headerNames.has(n) || busyInLoop(n, x)) {
|
|
451
|
+
found.add('stale-name');
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const d = defs.get(x);
|
|
456
|
+
if (!d) {
|
|
457
|
+
found.add('no-definition');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (REEVAL_UNSAFE_OPS.has(d.opcode) || respelledDefs.has(d)) {
|
|
461
|
+
found.add('order-sensitive');
|
|
462
|
+
}
|
|
463
|
+
d.operands.forEach(walk);
|
|
464
|
+
};
|
|
465
|
+
walk(a);
|
|
466
|
+
return found;
|
|
467
|
+
};
|
|
468
|
+
const dest = new Map<number, string>();
|
|
469
|
+
exitArgs.forEach((a, j) => {
|
|
470
|
+
if (!readsClobbered(a, sub, updateWrites)) {
|
|
471
|
+
return; // no hazard on this slot — nothing to repair
|
|
472
|
+
}
|
|
473
|
+
const destName = varName.get(exit.params[j]);
|
|
474
|
+
const c: SinkCandidate = {
|
|
475
|
+
argBlockers: blockersOf(a),
|
|
476
|
+
destName,
|
|
477
|
+
headerNames,
|
|
478
|
+
updateWrites,
|
|
479
|
+
destBusyInLoop: destName !== undefined && busyInLoop(destName, exit.params[j]),
|
|
480
|
+
};
|
|
481
|
+
if (firstRejection(gates, c) === null) {
|
|
482
|
+
dest.set(j, destName!);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
const names = new Set(dest.values());
|
|
486
|
+
if (dest.size === 0 || names.size !== dest.size) {
|
|
487
|
+
return none;
|
|
488
|
+
}
|
|
489
|
+
return exitArgs.some((a, j) => !dest.has(j) && readsClobbered(a, sub, names)) ? none : new Set(dest.keys());
|
|
490
|
+
};
|
|
140
491
|
|
|
141
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
readsClobbered,
|
|
494
|
+
loopEscapeHazard,
|
|
495
|
+
loopUpdateHazard,
|
|
496
|
+
sinkablePreUpdateSlots,
|
|
497
|
+
sameAtEntry: (a, b, entry, negated = false) => sameAtEntry(defs, a, b, entry, negated),
|
|
498
|
+
loopWriteSet,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** THE WRITE RELOCATION THE UNDEF EDGE-COPY ELISION CANNOT SEE, as a postcondition on a whole
|
|
503
|
+
* function. `undefCarriesNothing` (structure.ts) drops the copy into `name` on the edge out of
|
|
504
|
+
* `pred` after proving no value in `name`'s class has a definition able to run before that edge.
|
|
505
|
+
* It reads each such definition's home off `paramBlock`/`opBlock`, and a SUNK pre-update exit copy
|
|
506
|
+
* is written somewhere else than its home says: its destination is the loop EXIT's param, so the
|
|
507
|
+
* model homes it after the loop, while `preUpdateCopies` really writes it at the top of the body,
|
|
508
|
+
* on every iteration, ahead of any edge inside that body.
|
|
509
|
+
*
|
|
510
|
+
* Nothing today puts the two together — a merge inside the body that adopted the exit param's name
|
|
511
|
+
* is a block param `definedInBody` sees, so `dest-free-inside-loop` refuses the sink before it
|
|
512
|
+
* starts. But that gate carries its own KNOWN GAP (the natural-loop body excludes an early-return
|
|
513
|
+
* arm's blocks, so a name assigned only there is invisible to it), which makes the pair a
|
|
514
|
+
* conjecture rather than a proof. Checked here so that a widening of either — a broader sink, or a
|
|
515
|
+
* naming pass that lets a body merge adopt an exit param's name — DECLINES instead of silently
|
|
516
|
+
* substituting a defined value for the undefined one the machine leaves in place.
|
|
517
|
+
*
|
|
518
|
+
* `reaches` is the caller's reachability (structure.ts's `reachFrom`), passed in so this stays a
|
|
519
|
+
* pure function of the two records. Returns the colliding name, or null. */
|
|
520
|
+
export function sunkCopyOverDroppedUndef(
|
|
521
|
+
drops: ReadonlyArray<{ name: string; pred: Block }>,
|
|
522
|
+
sunkCopies: ReadonlyArray<{ name: string; home: Block }>,
|
|
523
|
+
reaches: (home: Block, pred: Block) => boolean,
|
|
524
|
+
): string | null {
|
|
525
|
+
for (const d of drops) {
|
|
526
|
+
for (const s of sunkCopies) {
|
|
527
|
+
if (s.name === d.name && (s.home === d.pred || reaches(s.home, d.pred))) {
|
|
528
|
+
return d.name;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
142
533
|
}
|
package/src/structure/loops.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// owns. It has NOTHING to do with AST emission and is unit-testable on synthetic CFGs with no
|
|
7
7
|
// emitter and no toolchain (test/loops.test.ts).
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// Dominance itself is a substrate fact and lives in `ir/core.ts`; `analyzeLoops` takes the map as
|
|
10
|
+
// a parameter, so a caller passes `dominators(fn)` from there.
|
|
11
11
|
import { Block, Fn, predecessors, successorsOf } from '../ir/core';
|
|
12
12
|
|
|
13
13
|
/** One natural loop, keyed by its header. */
|
|
@@ -38,41 +38,6 @@ export interface LoopForest {
|
|
|
38
38
|
parent: Map<Block, Block | null>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Forward dominators (iterative data-flow). dom(b) = {b} ∪ ⋂ dom(preds). */
|
|
42
|
-
export function dominators(fn: Fn): Map<Block, Set<Block>> {
|
|
43
|
-
const preds = predecessors(fn);
|
|
44
|
-
const all = new Set(fn.blocks);
|
|
45
|
-
const dom = new Map<Block, Set<Block>>();
|
|
46
|
-
fn.blocks.forEach((b, i) => dom.set(b, i === 0 ? new Set([b]) : new Set(all)));
|
|
47
|
-
let changed = true;
|
|
48
|
-
while (changed) {
|
|
49
|
-
changed = false;
|
|
50
|
-
for (const b of fn.blocks.slice(1)) {
|
|
51
|
-
const ps = preds.get(b)!;
|
|
52
|
-
let inter: Set<Block> | null = null;
|
|
53
|
-
for (const p of ps) {
|
|
54
|
-
const dp = dom.get(p)!;
|
|
55
|
-
if (inter === null) {
|
|
56
|
-
inter = new Set(dp);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
for (const x of inter) {
|
|
60
|
-
if (!dp.has(x)) {
|
|
61
|
-
inter.delete(x);
|
|
62
|
-
}
|
|
63
|
-
} // intersect in place (spec-safe delete-in-iter)
|
|
64
|
-
}
|
|
65
|
-
const next = new Set<Block>(inter ?? []);
|
|
66
|
-
next.add(b);
|
|
67
|
-
if (!setEq(next, dom.get(b)!)) {
|
|
68
|
-
dom.set(b, next);
|
|
69
|
-
changed = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return dom;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
41
|
/** Discover every natural loop and the nesting forest. Pure over the CFG + dominators. */
|
|
77
42
|
export function analyzeLoops(fn: Fn, dom: Map<Block, Set<Block>>): LoopForest {
|
|
78
43
|
const preds = predecessors(fn);
|
|
@@ -155,15 +120,3 @@ export function analyzeLoops(fn: Fn, dom: Map<Block, Set<Block>>): LoopForest {
|
|
|
155
120
|
|
|
156
121
|
return { byHeader, parent };
|
|
157
122
|
}
|
|
158
|
-
|
|
159
|
-
function setEq<X>(a: Set<X>, b: Set<X>): boolean {
|
|
160
|
-
if (a.size !== b.size) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
for (const x of a) {
|
|
164
|
-
if (!b.has(x)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return true;
|
|
169
|
-
}
|