@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// asmlift — re-rolling a loop test the compiler peeled into the loop's entry (ISA-neutral CFG pass).
|
|
2
|
+
//
|
|
3
|
+
// `for (i = 0; i < 16; i++) { if (t[i] == 7) { hit(); break; } } out();` — agbcc rotates the loop so
|
|
4
|
+
// it is entered at the increment, and evaluates the body's test for i = 0 in the ENTRY block:
|
|
5
|
+
//
|
|
6
|
+
// E: ldrb r1, [t] ; t[0]
|
|
7
|
+
// cmp r1, #7
|
|
8
|
+
// beq .Lhit ; the first iteration's test
|
|
9
|
+
// H: add r2, #1 ; i++
|
|
10
|
+
// cmp r2, #15
|
|
11
|
+
// bhi .Lout
|
|
12
|
+
// T: ldrb r0, [t + i]
|
|
13
|
+
// cmp r0, #7
|
|
14
|
+
// bne H ; the same test, every later iteration
|
|
15
|
+
// .Lhit: …
|
|
16
|
+
//
|
|
17
|
+
// The arm `.Lhit` then has two predecessors, one outside the loop, and no loop spelling owns it. But
|
|
18
|
+
// E's test IS T's, at the values E hands the loop: it is the loop's first iteration. Rotating the
|
|
19
|
+
// loop back — T becomes the header, entered from E with E's values and from H with H's — makes
|
|
20
|
+
// E's test redundant and leaves `.Lhit` one predecessor:
|
|
21
|
+
//
|
|
22
|
+
// E: br T(A) T(q): test(q) ? X : H(q) H(p): …; cond ? out : T(B)
|
|
23
|
+
//
|
|
24
|
+
// Sound when T computes what E computes on E's values. The SHAPE comes first and is not tabled — it
|
|
25
|
+
// is what names E, H, T and X at all: one forward predecessor E and one latch T, distinct from H,
|
|
26
|
+
// both ending in a `cond_br`, T's only predecessor H by one edge, no params on T, and T's other edge
|
|
27
|
+
// leaving the loop for the X that E's other edge reaches too. Then the admission, `REROLL_GATES`:
|
|
28
|
+
// • T reads nothing H defines except its back-edge arguments B, which are H's and distinct (so,
|
|
29
|
+
// renamed to T's own params, its ops are a function of the values it hands round);
|
|
30
|
+
// • E's condition and arguments into X are T's, with B replaced by E's arguments A into H — the
|
|
31
|
+
// question guard fusion asks of a rotated loop's guard, answered by the one predicate both read
|
|
32
|
+
// (`sameAtEntry`, ir/entry.ts), with its identities on (`str[0]` is `str[i]` at `i = 0`);
|
|
33
|
+
// • T holds no effect and no read outside what was compared: it now runs whole on the first
|
|
34
|
+
// iteration as well, where E's own copies of anything else would still run;
|
|
35
|
+
// • E runs no effect after the first op its test reads, so the test reads the same memory moved to
|
|
36
|
+
// the head of T as it did at the end of E.
|
|
37
|
+
// An effect in E's compared slice needs no rule of its own: the comparison matches it only with an
|
|
38
|
+
// op of T of the same opcode, which the third rule refuses, or with itself, which stays.
|
|
39
|
+
// Every check that fails leaves the function as it was.
|
|
40
|
+
//
|
|
41
|
+
// The rotation is ISA-neutral; the top test it then restores (`restoreTopTest`) is not. That rule
|
|
42
|
+
// reads the peel as a top-tested source, a GCC fact measured on agbcc alone, the one profile the
|
|
43
|
+
// re-roll fires on; no other profile has been probed for a peeled do-while. Where one peels it, the
|
|
44
|
+
// cost is a `for` spelled where the source was a do-while, never wrong C.
|
|
45
|
+
import { Block, Fn, Op, Successor, Value, dominators, mkValue, predecessors, successorsOf } from '../ir/core';
|
|
46
|
+
import { effectful, spelledWhenDead } from '../ir/discipline';
|
|
47
|
+
import { type EntryReading, sameAtEntry } from '../ir/entry';
|
|
48
|
+
import { analyzeLoops } from '../ir/loops';
|
|
49
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
50
|
+
import { intWidth } from '../ir/types';
|
|
51
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
52
|
+
import { foldConstPair } from './const';
|
|
53
|
+
|
|
54
|
+
/** One loop whose shape admits a re-roll, and the facts its admission reads. */
|
|
55
|
+
export interface PeeledTest {
|
|
56
|
+
/** does T read a value H defines that is not one of its back-edge args? */
|
|
57
|
+
readsHeader: boolean;
|
|
58
|
+
/** is a back-edge arg defined in T itself? */
|
|
59
|
+
backArgInLatch: boolean;
|
|
60
|
+
/** does T hand H one value for two params? */
|
|
61
|
+
backArgRepeated: boolean;
|
|
62
|
+
/** are E's test and its args into X T's own, read on E's values? */
|
|
63
|
+
sameTest: boolean;
|
|
64
|
+
/** does T run an effect, or a read outside its compared slice? */
|
|
65
|
+
latchRunsMore: boolean;
|
|
66
|
+
/** does E run an effect after the first op its compared slice reads? */
|
|
67
|
+
entryWritesAfterRead: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const REROLL_GATES: readonly Gate<PeeledTest>[] = [
|
|
71
|
+
{
|
|
72
|
+
id: 'latch-reads-header',
|
|
73
|
+
why: 'the rotated latch heads the loop, where a header value other than its back-edge args does not exist',
|
|
74
|
+
sound: true,
|
|
75
|
+
guardedBy: 'reroll.test.ts: a latch that reads a header value it does not hand round is left alone',
|
|
76
|
+
rejects: (c) => c.readsHeader,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'back-arg-in-latch',
|
|
80
|
+
why: 'the header hands the rotated latch its back-edge args, so they must be the header’s values',
|
|
81
|
+
sound: true,
|
|
82
|
+
guardedBy: 'reroll.test.ts: a back-edge arg the latch computes itself is left alone',
|
|
83
|
+
rejects: (c) => c.backArgInLatch,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'back-arg-repeated',
|
|
87
|
+
why: 'one value handed round to two params renames to one of them, and the other loses its entry value',
|
|
88
|
+
sound: true,
|
|
89
|
+
guardedBy: 'reroll.test.ts: a latch that hands one value to two header params is left alone',
|
|
90
|
+
rejects: (c) => c.backArgRepeated,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'not-the-entry-test',
|
|
94
|
+
why: 'only an entry test that is the latch’s own on the entry values is the first iteration',
|
|
95
|
+
sound: true,
|
|
96
|
+
guardedBy: 'reroll.test.ts: an entry test that differs from the loop’s at its first values is left alone',
|
|
97
|
+
rejects: (c) => !c.sameTest,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'latch-runs-more',
|
|
101
|
+
why: 'the rotated latch runs whole on the first iteration, where the entry’s own copy of anything else still runs',
|
|
102
|
+
sound: true,
|
|
103
|
+
guardedBy: [
|
|
104
|
+
'reroll.test.ts: a latch that runs an effect besides its test is left alone — the entry’s peeled copy stays',
|
|
105
|
+
'reroll.test.ts: a latch that reads memory outside its test is left alone — the first iteration would read it once more',
|
|
106
|
+
],
|
|
107
|
+
rejects: (c) => c.latchRunsMore,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'entry-writes-after-read',
|
|
111
|
+
why: 'the entry’s test moves to the head of the latch, past whatever the entry runs after reading it',
|
|
112
|
+
sound: true,
|
|
113
|
+
guardedBy: 'reroll.test.ts: an entry that writes memory after its test reads it is left alone',
|
|
114
|
+
rejects: (c) => c.entryWritesAfterRead,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/** Re-roll every peeled loop test the conditions above admit; returns how many. */
|
|
119
|
+
export function rerollPeeledTests(
|
|
120
|
+
fn: Fn,
|
|
121
|
+
gates: readonly Gate<PeeledTest>[] = REROLL_GATES,
|
|
122
|
+
topGates: readonly Gate<FoldedTopTest>[] = TOP_TEST_GATES,
|
|
123
|
+
): number {
|
|
124
|
+
let done = 0;
|
|
125
|
+
// one at a time: each rotation changes the loop forest
|
|
126
|
+
while (rerollOne(fn, gates, topGates)) {
|
|
127
|
+
done++;
|
|
128
|
+
}
|
|
129
|
+
return done;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rerollOne(fn: Fn, gates: readonly Gate<PeeledTest>[], topGates: readonly Gate<FoldedTopTest>[]): boolean {
|
|
133
|
+
const preds = predecessors(fn);
|
|
134
|
+
const defs = new Map<Value, Op>();
|
|
135
|
+
const defBlock = new Map<Value, Block>();
|
|
136
|
+
for (const b of fn.blocks) {
|
|
137
|
+
b.params.forEach((p) => defBlock.set(p, b));
|
|
138
|
+
for (const op of b.ops) {
|
|
139
|
+
op.results.forEach((r) => {
|
|
140
|
+
defs.set(r, op);
|
|
141
|
+
defBlock.set(r, b);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const forest = analyzeLoops(fn, dominators(fn));
|
|
146
|
+
for (const nl of forest.byHeader.values()) {
|
|
147
|
+
const H = nl.header;
|
|
148
|
+
if (nl.selfLoop || nl.latches.length !== 1 || nl.forwardPreds.length !== 1) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const T = nl.latches[0];
|
|
152
|
+
const E = nl.forwardPreds[0];
|
|
153
|
+
const tTerm = T.ops[T.ops.length - 1];
|
|
154
|
+
const eTerm = E.ops[E.ops.length - 1];
|
|
155
|
+
if (
|
|
156
|
+
T === H ||
|
|
157
|
+
tTerm.opcode !== 'cond_br' ||
|
|
158
|
+
eTerm.opcode !== 'cond_br' ||
|
|
159
|
+
(preds.get(T) ?? []).some((p) => p !== H) ||
|
|
160
|
+
successorsOf(H).filter((s) => s === T).length !== 1 ||
|
|
161
|
+
T.params.length > 0
|
|
162
|
+
) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const tBack = tTerm.successors.findIndex((s) => s.block === H);
|
|
166
|
+
const tOut = 1 - tBack;
|
|
167
|
+
const eIn = eTerm.successors.findIndex((s) => s.block === H);
|
|
168
|
+
const eOut = 1 - eIn;
|
|
169
|
+
if (tBack < 0 || eIn < 0 || tTerm.successors[tOut].block !== eTerm.successors[eOut].block) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const X = tTerm.successors[tOut].block;
|
|
173
|
+
if (nl.body.has(X) || X === E) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const B = tTerm.successors[tBack].args;
|
|
177
|
+
const A = eTerm.successors[eIn].args;
|
|
178
|
+
const tReads = [
|
|
179
|
+
...T.ops.flatMap((op) => op.operands),
|
|
180
|
+
...T.ops.flatMap((op) => op.successors.flatMap((s) => s.args)),
|
|
181
|
+
];
|
|
182
|
+
// compare E's test with T's at E's values: T's read with each B[k] as E's A[k], through the ops
|
|
183
|
+
// of T and E only — a read anywhere else may see other memory
|
|
184
|
+
const reading: EntryReading = {
|
|
185
|
+
entry: new Map(B.map((v, k) => [v, A[k]])),
|
|
186
|
+
through: (v) => defBlock.get(v) === T || defBlock.get(v) === E,
|
|
187
|
+
normalize: true,
|
|
188
|
+
};
|
|
189
|
+
const tXArgs = tTerm.successors[tOut].args;
|
|
190
|
+
const eXArgs = eTerm.successors[eOut].args;
|
|
191
|
+
// What the comparison covers: in each block, the ops its condition and its args into X are
|
|
192
|
+
// computed from. Only E's copies of those are deleted below.
|
|
193
|
+
const slice = sliceOf(defs, E, [eTerm.operands[0], ...eXArgs]);
|
|
194
|
+
const tSlice = sliceOf(defs, T, [tTerm.operands[0], ...tXArgs]);
|
|
195
|
+
const firstRead = E.ops.findIndex((op) => slice.has(op));
|
|
196
|
+
const site: PeeledTest = {
|
|
197
|
+
readsHeader: tReads.some((v) => defBlock.get(v) === H && !B.includes(v)),
|
|
198
|
+
backArgInLatch: B.some((v) => defBlock.get(v) === T),
|
|
199
|
+
backArgRepeated: new Set(B).size !== B.length,
|
|
200
|
+
sameTest:
|
|
201
|
+
// the sense: which arm of each condition leaves for X
|
|
202
|
+
sameAtEntry(defs, tTerm.operands[0], eTerm.operands[0], reading, tOut !== eOut) &&
|
|
203
|
+
tXArgs.length === eXArgs.length &&
|
|
204
|
+
tXArgs.every((a, k) => sameAtEntry(defs, a, eXArgs[k], reading)),
|
|
205
|
+
latchRunsMore: T.ops.some((op) => op !== tTerm && (effectful(op) || (spelledWhenDead(op) && !tSlice.has(op)))),
|
|
206
|
+
entryWritesAfterRead: firstRead >= 0 && E.ops.slice(firstRead, -1).some((op) => !slice.has(op) && effectful(op)),
|
|
207
|
+
};
|
|
208
|
+
if (firstRejection(gates, site) !== null) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// ROTATE: T(q) heads the loop; H hands it B, E hands it A, and T hands H its own params.
|
|
212
|
+
const q = B.map((v) => mkValue(v.type));
|
|
213
|
+
const rename = new Map<Value, Value>(B.map((v, k) => [v, q[k]]));
|
|
214
|
+
const sub = (v: Value) => rename.get(v) ?? v;
|
|
215
|
+
T.params = q;
|
|
216
|
+
for (const op of T.ops) {
|
|
217
|
+
op.operands = op.operands.map(sub);
|
|
218
|
+
for (const s of op.successors) {
|
|
219
|
+
s.args = s.args.map(sub);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const op of H.ops) {
|
|
223
|
+
for (const s of op.successors) {
|
|
224
|
+
if (s.block === T) {
|
|
225
|
+
s.args = [...B];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
rotateWriteOrder(fn, { E, H, T, B, q });
|
|
230
|
+
eTerm.opcode = 'br';
|
|
231
|
+
eTerm.operands = [];
|
|
232
|
+
eTerm.successors = [{ block: T, args: [...A] }];
|
|
233
|
+
eTerm.attrs = {};
|
|
234
|
+
// the test E no longer reads — only its compared slice: any other op of E is E's own code
|
|
235
|
+
for (let changed = true; changed;) {
|
|
236
|
+
changed = false;
|
|
237
|
+
const used = new Set(
|
|
238
|
+
fn.blocks.flatMap((b) => b.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)])),
|
|
239
|
+
);
|
|
240
|
+
E.ops = E.ops.filter((op) => {
|
|
241
|
+
const dead = slice.has(op) && !effectful(op) && op.results.every((r) => !used.has(r));
|
|
242
|
+
changed ||= dead;
|
|
243
|
+
return !dead;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// T now heads the loop: lay it out where H was, so the loop reads in its own order
|
|
247
|
+
fn.blocks = fn.blocks.filter((b) => b !== T);
|
|
248
|
+
fn.blocks.splice(fn.blocks.indexOf(H), 0, T);
|
|
249
|
+
// H's params now take T's own params on their one in-edge: each is that value under a second name
|
|
250
|
+
simplifyTrivialPhis(fn);
|
|
251
|
+
restoreTopTest(fn, { E, T, H }, topGates);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** One re-rolled loop whose exit test could head it again, and the facts its admission reads. */
|
|
258
|
+
export interface FoldedTopTest {
|
|
259
|
+
/** does the exit edge hand the exit block a value that another edge into it does not hand the
|
|
260
|
+
* same parameter? The loop's own value, which the head and an arm out of the test both hand,
|
|
261
|
+
* does not count. */
|
|
262
|
+
exitValueDiffers: boolean;
|
|
263
|
+
/** does the exit test or its edge read a value of the loop's blocks other than those H hands
|
|
264
|
+
* round, or does a block after the exit read one at all? */
|
|
265
|
+
readsLoopValue: boolean;
|
|
266
|
+
/** the exit test on the entry values: `stays` (provably not taken), `leaves`, `guarded` (not a
|
|
267
|
+
* constant, but the test of the guard the loop is entered under, on the same values and leaving
|
|
268
|
+
* for the same exit), or `unknown` (not a constant: a memory read, a call, a parameter, an opcode
|
|
269
|
+
* `EVAL` does not fold) */
|
|
270
|
+
atEntry: 'stays' | 'leaves' | 'guarded' | 'unknown';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const TOP_TEST_GATES: readonly Gate<FoldedTopTest>[] = [
|
|
274
|
+
{
|
|
275
|
+
id: 'exit-value-differs',
|
|
276
|
+
why: 'an exit whose edges hand it different values becomes a second copy of it after the loop, a spelling measured worse',
|
|
277
|
+
sound: false,
|
|
278
|
+
rejects: (c) => c.exitValueDiffers,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'reads-loop-value',
|
|
282
|
+
why: 'the new head is reached before the loop body, where the body’s own values do not exist yet',
|
|
283
|
+
sound: true,
|
|
284
|
+
guardedBy: 'reroll.test.ts: each top-test refusal above is its rule’s alone',
|
|
285
|
+
rejects: (c) => c.readsLoopValue,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'not-staying-at-entry',
|
|
289
|
+
why: 'a head test that could leave on entry would skip the first iteration the asm always runs',
|
|
290
|
+
sound: true,
|
|
291
|
+
guardedBy: 'reroll.test.ts: each top-test refusal above is its rule’s alone',
|
|
292
|
+
rejects: (c) => c.atEntry === 'leaves' || c.atEntry === 'unknown',
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
/** Integer opcodes `evalConst` folds, as int32; `add` and `or` through raise/const.ts's `foldConstPair`. */
|
|
297
|
+
const EVAL: Readonly<Record<string, (a: number, b: number) => number>> = {
|
|
298
|
+
add: (a, b) => foldConstPair('add', a, b)!,
|
|
299
|
+
or: (a, b) => foldConstPair('or', a, b)!,
|
|
300
|
+
sub: (a, b) => (a - b) | 0,
|
|
301
|
+
and: (a, b) => a & b,
|
|
302
|
+
xor: (a, b) => a ^ b,
|
|
303
|
+
icmp_eq: (a, b) => +(a === b),
|
|
304
|
+
icmp_ne: (a, b) => +(a !== b),
|
|
305
|
+
icmp_slt: (a, b) => +(a < b),
|
|
306
|
+
icmp_sle: (a, b) => +(a <= b),
|
|
307
|
+
icmp_sgt: (a, b) => +(a > b),
|
|
308
|
+
icmp_sge: (a, b) => +(a >= b),
|
|
309
|
+
icmp_ult: (a, b) => +(a >>> 0 < b >>> 0),
|
|
310
|
+
icmp_ule: (a, b) => +(a >>> 0 <= b >>> 0),
|
|
311
|
+
icmp_ugt: (a, b) => +(a >>> 0 > b >>> 0),
|
|
312
|
+
icmp_uge: (a, b) => +(a >>> 0 >= b >>> 0),
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** RESTORE THE TOP TEST THE COMPILER FOLDED AWAY. A loop `rerollOne` just rotated is now
|
|
316
|
+
*
|
|
317
|
+
* E: br T(A) T(q): test(q) ? X : H H: …; B = next(q); exit(B) ? Y : T(B)
|
|
318
|
+
*
|
|
319
|
+
* which spells `while (1) { …; if (exit) break; }` — and that compiles as a do-while, whose test is
|
|
320
|
+
* at the bottom. But the peel is evidence that the source tested at the TOP: agbcc's `while`/`for`
|
|
321
|
+
* emit the exit test ahead of the body (c-parse.in), `expand_end_loop` rotates it to the bottom
|
|
322
|
+
* and enters at it (stmt.c), and `duplicate_loop_exit_test` copies it in front of the loop
|
|
323
|
+
* (jump.c), where a constant start folds it away and leaves the first body test in E. A do-while
|
|
324
|
+
* has no such jump and no peel. So where H's exit test, read on the entry values A, provably stays
|
|
325
|
+
* in, it moves to a new head C:
|
|
326
|
+
*
|
|
327
|
+
* E: br C(A) C(c): exit(c) ? Y : T(c) T: test(c) ? X : H H: …; br C(next(c))
|
|
328
|
+
*
|
|
329
|
+
* which runs the same iterations: on entry the test is false, and every later time it is H's own
|
|
330
|
+
* test on H's values. Where the start or the bound is not a constant, the copied test does not
|
|
331
|
+
* fold and stays in front as a guard G of E (`for (i = 0; i < n; i++)` keeps `0 < n`): the same
|
|
332
|
+
* evidence, and the same move, once G is proven to be that test on those values (`entryGuard`);
|
|
333
|
+
* then C's test is G's, and G goes. Refuses by `TOP_TEST_GATES`; a refusal keeps the loop as
|
|
334
|
+
* rotated. */
|
|
335
|
+
function restoreTopTest(
|
|
336
|
+
fn: Fn,
|
|
337
|
+
loop: { E: Block; T: Block; H: Block },
|
|
338
|
+
gates: readonly Gate<FoldedTopTest>[],
|
|
339
|
+
): boolean {
|
|
340
|
+
const { E, T, H } = loop;
|
|
341
|
+
const hTerm = H.ops[H.ops.length - 1];
|
|
342
|
+
const eTerm = E.ops[E.ops.length - 1];
|
|
343
|
+
const preds = predecessors(fn);
|
|
344
|
+
if (
|
|
345
|
+
hTerm?.opcode !== 'cond_br' ||
|
|
346
|
+
eTerm?.opcode !== 'br' ||
|
|
347
|
+
eTerm.successors[0].block !== T ||
|
|
348
|
+
(preds.get(H) ?? []).some((p) => p !== T) ||
|
|
349
|
+
(preds.get(T) ?? []).some((p) => p !== E && p !== H)
|
|
350
|
+
) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const back = hTerm.successors.findIndex((s) => s.block === T);
|
|
354
|
+
if (back < 0 || hTerm.successors[1 - back].block === T) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const exit = hTerm.successors[1 - back];
|
|
358
|
+
const B = hTerm.successors[back].args;
|
|
359
|
+
const A = eTerm.successors[0].args;
|
|
360
|
+
const inLoop = new Set<Value>([
|
|
361
|
+
...T.params,
|
|
362
|
+
...H.params,
|
|
363
|
+
...[T, H].flatMap((b) => b.ops.flatMap((op) => op.results)),
|
|
364
|
+
]);
|
|
365
|
+
const defs = new Map<Value, Op>(
|
|
366
|
+
fn.blocks.flatMap((b) => b.ops.flatMap((op) => op.results.map((r) => [r, op] as const))),
|
|
367
|
+
);
|
|
368
|
+
// what moves: the ops of H the exit test is computed from, minus those that compute what H hands round
|
|
369
|
+
const kept = sliceOf(defs, H, B);
|
|
370
|
+
const moved = new Set([...sliceOf(defs, H, [hTerm.operands[0]])].filter((op) => !kept.has(op)));
|
|
371
|
+
const movedResults = new Set([...moved].flatMap((op) => op.results));
|
|
372
|
+
// …and the test is all they compute: where another op of the loop reads one, the loop stays rotated
|
|
373
|
+
const readers = [T, H].flatMap((b) => b.ops).filter((op) => op !== hTerm && !moved.has(op));
|
|
374
|
+
if (
|
|
375
|
+
readers.some((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)].some((v) => movedResults.has(v)))
|
|
376
|
+
) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
// the exit test on the entry values: each B[k] read as A[k], through E's ops and the moved ones
|
|
380
|
+
const entry = new Map<Value, Value>(B.map((v, k) => [v, A[k]]));
|
|
381
|
+
const evalConst = (v0: Value): number | null => {
|
|
382
|
+
const v = entry.get(v0) ?? v0;
|
|
383
|
+
const d = defs.get(v);
|
|
384
|
+
if (d === undefined || intWidth(v.type) !== 32) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
if (d.opcode === 'const') {
|
|
388
|
+
return typeof d.attrs.value === 'number' ? d.attrs.value | 0 : null;
|
|
389
|
+
}
|
|
390
|
+
const f = EVAL[d.opcode];
|
|
391
|
+
if (!f || d.operands.length !== 2 || !(moved.has(d) || E.ops.includes(d))) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const x = evalConst(d.operands[0]);
|
|
395
|
+
const y = evalConst(d.operands[1]);
|
|
396
|
+
return x === null || y === null ? null : f(x, y);
|
|
397
|
+
};
|
|
398
|
+
const cond = evalConst(hTerm.operands[0]);
|
|
399
|
+
const guard = cond === null ? entryGuard(fn, preds, defs, { E, H, exit, back, B, A, moved }) : null;
|
|
400
|
+
// a value the exit edge hands, or another edge into the exit: the loop's own `k`th value is one
|
|
401
|
+
// value however it is named, as H hands it round (B) or as T receives it
|
|
402
|
+
const own = (v: Value): Value | number => {
|
|
403
|
+
const k = Math.max(B.indexOf(v), T.params.indexOf(v));
|
|
404
|
+
return k < 0 ? v : k;
|
|
405
|
+
};
|
|
406
|
+
const intoExit = fn.blocks
|
|
407
|
+
.flatMap((b) => b.ops[b.ops.length - 1]?.successors ?? [])
|
|
408
|
+
.filter((s) => s.block === exit.block && s !== exit && s !== guard?.out);
|
|
409
|
+
// the blocks from the exit on, which C now enters and T no longer dominates: a loop value read
|
|
410
|
+
// there would be read one iteration on
|
|
411
|
+
const after = new Set<Block>();
|
|
412
|
+
for (const stack = [exit.block]; stack.length > 0;) {
|
|
413
|
+
const x = stack.pop()!;
|
|
414
|
+
if (x !== T && x !== H && !after.has(x)) {
|
|
415
|
+
after.add(x);
|
|
416
|
+
stack.push(...successorsOf(x));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const readAfter = [...after].flatMap((b) =>
|
|
420
|
+
b.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)]),
|
|
421
|
+
);
|
|
422
|
+
const site: FoldedTopTest = {
|
|
423
|
+
exitValueDiffers: exit.args.some((v, k) => intoExit.some((s) => own(s.args[k]) !== own(v))),
|
|
424
|
+
readsLoopValue:
|
|
425
|
+
[...[...moved].flatMap((op) => op.operands), hTerm.operands[0], ...exit.args].some(
|
|
426
|
+
(v) => inLoop.has(v) && !movedResults.has(v) && !B.includes(v),
|
|
427
|
+
) || readAfter.some((v) => inLoop.has(v) && !movedResults.has(v)),
|
|
428
|
+
atEntry:
|
|
429
|
+
cond !== null ? ((cond !== 0) === (back === 0) ? 'stays' : 'leaves') : guard !== null ? 'guarded' : 'unknown',
|
|
430
|
+
};
|
|
431
|
+
if (firstRejection(gates, site) !== null) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
if (guard !== null) {
|
|
435
|
+
dropGuard(fn, guard);
|
|
436
|
+
}
|
|
437
|
+
const c = B.map((v) => mkValue(v.type));
|
|
438
|
+
const rename = new Map<Value, Value>(B.map((v, k) => [v, c[k]]));
|
|
439
|
+
const sub = (v: Value) => rename.get(v) ?? v;
|
|
440
|
+
const headOps = H.ops.filter((op) => moved.has(op));
|
|
441
|
+
for (const op of headOps) {
|
|
442
|
+
op.operands = op.operands.map(sub);
|
|
443
|
+
}
|
|
444
|
+
const C: Block = {
|
|
445
|
+
params: c,
|
|
446
|
+
ops: [
|
|
447
|
+
...headOps,
|
|
448
|
+
{
|
|
449
|
+
opcode: 'cond_br',
|
|
450
|
+
operands: [sub(hTerm.operands[0])],
|
|
451
|
+
results: [],
|
|
452
|
+
attrs: { ...hTerm.attrs },
|
|
453
|
+
successors: hTerm.successors.map((s, k) =>
|
|
454
|
+
k === back ? { block: T, args: [...c] } : { block: s.block, args: s.args.map(sub) },
|
|
455
|
+
),
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
H.ops = [
|
|
460
|
+
...H.ops.filter((op) => op !== hTerm && !moved.has(op)),
|
|
461
|
+
{ opcode: 'br', operands: [], results: [], attrs: {}, successors: [{ block: C, args: [...B] }] },
|
|
462
|
+
];
|
|
463
|
+
eTerm.successors = [{ block: C, args: [...A] }];
|
|
464
|
+
// write order: E and H wrote T's keys on their edges, and now write C's; C writes nothing itself
|
|
465
|
+
const order = fn.writeOrder;
|
|
466
|
+
if (order !== undefined) {
|
|
467
|
+
for (const pred of [E, H]) {
|
|
468
|
+
const rec = order.lastWrite.get(pred);
|
|
469
|
+
T.params.forEach((tk, k) => {
|
|
470
|
+
const at = rec?.get(tk);
|
|
471
|
+
rec?.delete(tk);
|
|
472
|
+
if (at !== undefined) {
|
|
473
|
+
rec?.set(c[k], at);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (order.writes.has(T)) {
|
|
478
|
+
order.writes.set(C, 0);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
fn.blocks.splice(fn.blocks.indexOf(T), 0, C);
|
|
482
|
+
// T's params now take C's on their one in-edge
|
|
483
|
+
simplifyTrivialPhis(fn);
|
|
484
|
+
rejoinSunkReturns(predecessors(fn), T, exit.block, new Set([C, T, H]));
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** AN ARM THAT FELL INTO THE EXIT'S RETURN BREAKS TO IT. Where the code after the loop is the bare
|
|
489
|
+
* epilogue, return-sinking (raise/retsink.ts) has already given the arm out of the test its own copy
|
|
490
|
+
* of the `ret`, marked as the fall-through it was: the arm ran out into the epilogue the exit
|
|
491
|
+
* branches to. Rotated, that reads as `if (v0 >= n) return; … } gHit = 1;`; restored, the arm is an
|
|
492
|
+
* early `return` inside the `while`, a spelling agbcc compiles to another object, while the arm
|
|
493
|
+
* running out into the loop's exit is the `break` the layout shows. So where every `ret` in the
|
|
494
|
+
* arm's region is such a fall-through, returning the values the exit's own lone `ret` returns, it
|
|
495
|
+
* branches to the exit again: the same instruction stream, and a region the loop reads as a
|
|
496
|
+
* code-running `break` (`codeBreakArm`) — which it only takes with no other way in. */
|
|
497
|
+
function rejoinSunkReturns(preds: ReadonlyMap<Block, Block[]>, T: Block, exit: Block, loop: ReadonlySet<Block>): void {
|
|
498
|
+
const xt = exit.ops[exit.ops.length - 1];
|
|
499
|
+
const tTerm = T.ops[T.ops.length - 1];
|
|
500
|
+
const outs = tTerm?.opcode === 'cond_br' ? tTerm.successors.filter((s) => !loop.has(s.block)) : [];
|
|
501
|
+
if (exit.ops.length !== 1 || xt.opcode !== 'ret' || exit.params.length > 0 || outs.length !== 1) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const X = outs[0].block;
|
|
505
|
+
const region = new Set<Block>();
|
|
506
|
+
const rets: Op[] = [];
|
|
507
|
+
for (const stack = [X]; stack.length > 0;) {
|
|
508
|
+
const x = stack.pop()!;
|
|
509
|
+
if (x === exit || loop.has(x)) {
|
|
510
|
+
return; // already a break, or not an arm
|
|
511
|
+
}
|
|
512
|
+
if (region.has(x)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
region.add(x);
|
|
516
|
+
const t = x.ops[x.ops.length - 1];
|
|
517
|
+
if (t?.opcode === 'ret') {
|
|
518
|
+
rets.push(t);
|
|
519
|
+
} else if (t?.opcode !== 'br' && t?.opcode !== 'cond_br') {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
stack.push(...successorsOf(x));
|
|
523
|
+
}
|
|
524
|
+
const { fallthrough: _, ...xAttrs } = xt.attrs;
|
|
525
|
+
const rejoins = (r: Op) => {
|
|
526
|
+
const { fallthrough, ...attrs } = r.attrs;
|
|
527
|
+
return (
|
|
528
|
+
fallthrough === true &&
|
|
529
|
+
JSON.stringify(attrs) === JSON.stringify(xAttrs) &&
|
|
530
|
+
r.operands.length === xt.operands.length &&
|
|
531
|
+
r.operands.every((v, k) => v === xt.operands[k])
|
|
532
|
+
);
|
|
533
|
+
};
|
|
534
|
+
const entered = (x: Block) => (preds.get(x) ?? []).every((p) => region.has(p) || (x === X && p === T));
|
|
535
|
+
if (rets.length === 0 || !rets.every(rejoins) || ![...region].every(entered)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
for (const r of rets) {
|
|
539
|
+
r.opcode = 'br';
|
|
540
|
+
r.operands = [];
|
|
541
|
+
r.attrs = { fallthrough: true };
|
|
542
|
+
r.successors = [{ block: exit, args: [] }];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The guard a loop is entered under, where the compiler kept the exit test it copied ahead of the
|
|
547
|
+
* loop (`duplicate_loop_exit_test`, jump.c) rather than fold it: `for (i = 0; i < n; i++)` with `n`
|
|
548
|
+
* unknown leaves `0 < n` in front of the peeled first test. E's one predecessor G decides between
|
|
549
|
+
* E and the loop's exit, on the exit test read at the entry values A (`sameAtEntry`, the predicate
|
|
550
|
+
* guard fusion reads) and with the args the exit edge hands on those values. Then the new head runs
|
|
551
|
+
* G's own test on G's own values, and G's branch is redundant — provided nothing between them runs:
|
|
552
|
+
* E's ops now run on the way out as well, and the test reads memory where G read it, so E holds no
|
|
553
|
+
* effect and no read. */
|
|
554
|
+
interface EntryGuard {
|
|
555
|
+
G: Block;
|
|
556
|
+
gTerm: Op;
|
|
557
|
+
/** G's edge into E, and its edge into the exit */
|
|
558
|
+
into: Successor;
|
|
559
|
+
out: Successor;
|
|
560
|
+
}
|
|
561
|
+
function entryGuard(
|
|
562
|
+
fn: Fn,
|
|
563
|
+
preds: ReadonlyMap<Block, Block[]>,
|
|
564
|
+
defs: ReadonlyMap<Value, Op>,
|
|
565
|
+
r: {
|
|
566
|
+
E: Block;
|
|
567
|
+
H: Block;
|
|
568
|
+
exit: Successor;
|
|
569
|
+
back: number;
|
|
570
|
+
B: readonly Value[];
|
|
571
|
+
A: readonly Value[];
|
|
572
|
+
moved: ReadonlySet<Op>;
|
|
573
|
+
},
|
|
574
|
+
): EntryGuard | null {
|
|
575
|
+
const { E, H, exit, back, B, A, moved } = r;
|
|
576
|
+
const ep = preds.get(E) ?? [];
|
|
577
|
+
const G = ep[0];
|
|
578
|
+
if (ep.length !== 1 || G === E || G === H) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
const gTerm = G.ops[G.ops.length - 1];
|
|
582
|
+
if (gTerm?.opcode !== 'cond_br') {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const gIn = gTerm.successors.findIndex((s) => s.block === E);
|
|
586
|
+
if (gIn < 0 || gTerm.successors[1 - gIn].block !== exit.block) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
if (E.ops.slice(0, -1).some((op) => effectful(op) || spelledWhenDead(op))) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const hTerm = H.ops[H.ops.length - 1];
|
|
593
|
+
const gOut = gTerm.successors[1 - gIn];
|
|
594
|
+
const blockOf = new Map<Op, Block>(fn.blocks.flatMap((b) => b.ops.map((op) => [op, b] as const)));
|
|
595
|
+
const reading: EntryReading = {
|
|
596
|
+
entry: new Map(B.map((v, k) => [v, A[k]])),
|
|
597
|
+
through: (v) => {
|
|
598
|
+
const d = defs.get(v);
|
|
599
|
+
const b = d === undefined ? undefined : blockOf.get(d);
|
|
600
|
+
return d !== undefined && (moved.has(d) || b === E || b === G);
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
// the senses: the exit test leaves on its `1 - back`th edge, the guard on its `1 - gIn`th
|
|
604
|
+
const same =
|
|
605
|
+
sameAtEntry(defs, hTerm.operands[0], gTerm.operands[0], reading, 1 - back !== 1 - gIn) &&
|
|
606
|
+
exit.args.length === gOut.args.length &&
|
|
607
|
+
exit.args.every((a, k) => sameAtEntry(defs, a, gOut.args[k], reading));
|
|
608
|
+
return same ? { G, gTerm, into: gTerm.successors[gIn], out: gOut } : null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** G enters E unconditionally; the ops only its test read go with it. */
|
|
612
|
+
function dropGuard(fn: Fn, { G, gTerm, into }: EntryGuard): void {
|
|
613
|
+
const cond = gTerm.operands[0];
|
|
614
|
+
gTerm.opcode = 'br';
|
|
615
|
+
gTerm.operands = [];
|
|
616
|
+
gTerm.successors = [into];
|
|
617
|
+
gTerm.attrs = {};
|
|
618
|
+
const slice = sliceOf(new Map(G.ops.flatMap((op) => op.results.map((v) => [v, op] as const))), G, [cond]);
|
|
619
|
+
for (let changed = true; changed;) {
|
|
620
|
+
changed = false;
|
|
621
|
+
const used = new Set(
|
|
622
|
+
fn.blocks.flatMap((b) => b.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)])),
|
|
623
|
+
);
|
|
624
|
+
G.ops = G.ops.filter((op) => {
|
|
625
|
+
const dead = slice.has(op) && !effectful(op) && op.results.every((v) => !used.has(v));
|
|
626
|
+
changed ||= dead;
|
|
627
|
+
return !dead;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** THE WRITE-ORDER BOOKKEEPING THE ROTATION OWES (ir/core.ts `WriteOrder`). A missing `lastWrite`
|
|
633
|
+
* entry on a measured block says "this block left the key as it was", which the structurer reads as
|
|
634
|
+
* a licence to name a nested loop's parameter after the enclosing header's, so every edge the
|
|
635
|
+
* rotation re-targets keeps saying who wrote each key. T's param `q[k]` stands for the key H's
|
|
636
|
+
* `p[k]` did: it receives the same values on the same register.
|
|
637
|
+
* • E → T(A) is E's old edge into H, re-targeted: E's records for `p[k]` are its records for `q[k]`.
|
|
638
|
+
* • H → T(B) is the old back edge T → H(B), seen from the other end. T's copies on it (a record of
|
|
639
|
+
* T's for `p[k]`) land at the end of H, as `foldWriteOrder` puts a folded block's writes. Where T
|
|
640
|
+
* made none and `B[k]` is not `p[k]`, the key took a new value before T, so H wrote it — at an
|
|
641
|
+
* ordinal among H's own writes the record never measured; it takes H's last, the bound every
|
|
642
|
+
* later write sorts after. Saying "written" where it may not have been costs a spelling at most;
|
|
643
|
+
* the reverse licenses a name. A `B[k]` that is `p[k]` passes through: no entry, as before.
|
|
644
|
+
* • T → H(q) passes T's own params on: T writes none of those keys, so its records for `p[k]` go. */
|
|
645
|
+
function rotateWriteOrder(fn: Fn, r: { E: Block; H: Block; T: Block; B: readonly Value[]; q: readonly Value[] }): void {
|
|
646
|
+
const order = fn.writeOrder;
|
|
647
|
+
if (order === undefined) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const { E, H, T, B, q } = r;
|
|
651
|
+
const p = H.params;
|
|
652
|
+
const eRec = order.lastWrite.get(E);
|
|
653
|
+
if (eRec !== undefined) {
|
|
654
|
+
p.forEach((pk, k) => {
|
|
655
|
+
const at = eRec.get(pk);
|
|
656
|
+
eRec.delete(pk);
|
|
657
|
+
if (at !== undefined) {
|
|
658
|
+
eRec.set(q[k], at);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
const hBase = order.writes.get(H);
|
|
663
|
+
const tWrites = order.writes.get(T);
|
|
664
|
+
if (hBase === undefined || tWrites === undefined) {
|
|
665
|
+
return; // H or T unmeasured: nobody measured this fn, and nothing reads its absences
|
|
666
|
+
}
|
|
667
|
+
const tRec = order.lastWrite.get(T);
|
|
668
|
+
const hRec = order.lastWrite.get(H) ?? new Map<Value, number>();
|
|
669
|
+
p.forEach((pk, k) => {
|
|
670
|
+
const at = tRec?.get(pk);
|
|
671
|
+
tRec?.delete(pk);
|
|
672
|
+
if (at !== undefined) {
|
|
673
|
+
hRec.set(q[k], hBase + at);
|
|
674
|
+
} else if (B[k] !== pk && hBase > 0) {
|
|
675
|
+
hRec.set(q[k], hBase - 1);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
order.lastWrite.set(H, hRec);
|
|
679
|
+
order.writes.set(H, hBase + tWrites);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** The ops in `blk` the values `roots` are computed from. */
|
|
683
|
+
function sliceOf(defs: ReadonlyMap<Value, Op>, blk: Block, roots: readonly Value[]): Set<Op> {
|
|
684
|
+
const out = new Set<Op>();
|
|
685
|
+
const walk = (v: Value) => {
|
|
686
|
+
const d = defs.get(v);
|
|
687
|
+
if (d !== undefined && blk.ops.includes(d) && !out.has(d)) {
|
|
688
|
+
out.add(d);
|
|
689
|
+
d.operands.forEach(walk);
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
roots.forEach(walk);
|
|
693
|
+
return out;
|
|
694
|
+
}
|