@asmlift/core 0.8.1 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- 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 +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/structure/hazards.ts
CHANGED
|
@@ -8,28 +8,34 @@
|
|
|
8
8
|
// the loop's entry state so a guard about to be fused away can be checked against the test
|
|
9
9
|
// that replaces it.
|
|
10
10
|
//
|
|
11
|
-
// WHAT MAKES A SUNK COPY LEGAL. The exit edge's value is not moved, it is REBUILT: the copy
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// WHAT MAKES A SUNK COPY LEGAL. The exit edge's value is not moved, it is REBUILT: the copy spells
|
|
12
|
+
// the arg's def-tree again inside the body, at the point that tree was already computed at
|
|
13
|
+
// (`preUpdateCopyHome`) — or, for an arg the LATCH did not compute, opening it. Two things
|
|
14
|
+
// have to hold — the tree gives the same answer there, and every name it reads still denotes the
|
|
15
|
+
// same value — and the arg gates in `PREUPDATE_SINK_GATES` are those two plus the degenerate leaf
|
|
16
|
+
// that is neither.
|
|
15
17
|
//
|
|
16
|
-
// The tree must give the same answer at the new point. Two ways it might not, and
|
|
17
|
-
// is the
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// original returned.
|
|
18
|
+
// The tree must give the same answer at the new point. Two ways it might not, and
|
|
19
|
+
// `reevalUnsafe` (ir/discipline.ts) is the question that names both. ORDER: a load re-evaluated after a
|
|
20
|
+
// store the original preceded answers with what the store wrote, and an effect re-evaluated past
|
|
21
|
+
// another is out of order. SPECULATION: a tree evaluated where the original never was — the def
|
|
22
|
+
// dominates the latch, and the loop is single-latch at both call sites, but an early-`return` arm
|
|
23
|
+
// lets an iteration leave BEFORE it, and a trapping divide would fault where the original returned.
|
|
23
24
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// WHICH OF THE TWO APPLIES IS A FACT ABOUT THE POSITION, so `arg-safe-to-reevaluate` asks per slot
|
|
26
|
+
// rather than refusing the opcode. At the def's own position nothing is speculated at all: every op
|
|
27
|
+
// under the arg dominates that point, so the copy runs only on iterations that evaluated the whole
|
|
28
|
+
// tree — the question reduces to ORDER, and to ORDER only against the ops that run between each
|
|
29
|
+
// one and the copy in the asm but on the other side of it in the C (`movesPast`). Opening the body
|
|
30
|
+
// instead, both hazards are live for the whole body, and the blanket refusal is the answer.
|
|
28
31
|
//
|
|
29
32
|
// 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
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
+
// variable does: the update sits at the bottom, so anywhere ahead of it the name holds exactly the
|
|
34
|
+
// value the edge read. A name the body itself defines does NOT, wherever the copy lands ahead of
|
|
35
|
+
// the assignment that writes it — and `arg-reads-current-names` refuses every such name but one
|
|
36
|
+
// shape it can place: a def the analysis named ahead of the copy's home — in the latch, or in a
|
|
37
|
+
// block every iteration runs before it (`writtenAheadOf`) — whose statement has run on every
|
|
38
|
+
// iteration that reaches the copy.
|
|
33
39
|
//
|
|
34
40
|
// KNOWN GAP: `body` is the natural-loop body, which EXCLUDES the blocks an early-return arm owns
|
|
35
41
|
// even though their statements are emitted inside the loop. A name assigned only in such an arm is
|
|
@@ -42,9 +48,10 @@
|
|
|
42
48
|
// The maps are captured as LIVE REFERENCES, deliberately: `varName` is still being populated by
|
|
43
49
|
// the naming pipeline when the factory is created, and each hazard check reads whatever names
|
|
44
50
|
// exist at CALL time (emission runs after naming completes). Snapshotting them would break this.
|
|
45
|
-
import { Block, Op, Value } from '../ir/core';
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
51
|
+
import { Block, Op, Value, successorsOf } from '../ir/core';
|
|
52
|
+
import { effectful, orderSensitive, reevalUnsafe } from '../ir/discipline';
|
|
53
|
+
import { NEGATED_ICMP } from '../ir/opcodes';
|
|
54
|
+
import { Expr, Stmt } from '../l3/ast';
|
|
48
55
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
49
56
|
import type { UseSite } from './analysis';
|
|
50
57
|
|
|
@@ -67,6 +74,9 @@ export interface LoopHazardDeps {
|
|
|
67
74
|
* over the operands cannot see what such an op will render, so predicates that reason about
|
|
68
75
|
* the rendered expression have to treat it as opaque. LIVE, like `varName`. */
|
|
69
76
|
respelledDefs: ReadonlyMap<Op, unknown>;
|
|
77
|
+
/** where an op's expression is emitted: its own position when it renders as a statement, the one
|
|
78
|
+
* consumer it is inlined into otherwise, null when it has no one position (analysis.ts) */
|
|
79
|
+
emitPos: (op: Op) => { blk: Block; idx: number } | null;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface LoopHazards {
|
|
@@ -84,18 +94,35 @@ export interface LoopHazards {
|
|
|
84
94
|
sub: Map<Value, string>,
|
|
85
95
|
updateWrites: Set<string>,
|
|
86
96
|
region: Set<Block> | null,
|
|
97
|
+
condRepaired?: boolean,
|
|
87
98
|
): boolean;
|
|
99
|
+
testSkipsAnEffect(condV: Value, sub: Map<Value, string>): boolean;
|
|
100
|
+
preUpdateCondFold(
|
|
101
|
+
condV: Value,
|
|
102
|
+
negated: boolean,
|
|
103
|
+
header: Block,
|
|
104
|
+
backArgs: readonly Value[],
|
|
105
|
+
exitArgs: readonly Value[],
|
|
106
|
+
body: Set<Block>,
|
|
107
|
+
sub: Map<Value, string>,
|
|
108
|
+
updates: Stmt[],
|
|
109
|
+
updateWrites: Set<string>,
|
|
110
|
+
gates?: readonly Gate<PreUpdateCondCandidate>[],
|
|
111
|
+
): PreUpdateCondFold;
|
|
88
112
|
sinkablePreUpdateSlots(
|
|
89
113
|
header: Block,
|
|
90
114
|
exit: Block,
|
|
91
115
|
exitArgs: readonly Value[],
|
|
92
116
|
body: Set<Block>,
|
|
117
|
+
latch: Block,
|
|
93
118
|
sub: Map<Value, string>,
|
|
94
119
|
updateWrites: Set<string>,
|
|
95
120
|
gates?: readonly Gate<SinkCandidate>[],
|
|
96
|
-
|
|
121
|
+
staleOnZeroTrip?: (slot: number) => boolean,
|
|
122
|
+
): Map<number, Op | null>;
|
|
97
123
|
sameAtEntry(a: Value, b: Value, entry: Map<Value, Value>, negated?: boolean): boolean;
|
|
98
124
|
loopWriteSet(updates: Stmt[], bodyBlocks: Iterable<Block>, header: Block): Set<string>;
|
|
125
|
+
loopWriteSetAhead(ahead: Iterable<Block>, incoming: (b: Block) => Value[][], header: Block): Set<string>;
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
/** The names a loop update assigns (its non-identity copies) — the write set every loop-emission
|
|
@@ -103,7 +130,7 @@ export interface LoopHazards {
|
|
|
103
130
|
export const updateWriteSet = (updates: Stmt[]): Set<string> =>
|
|
104
131
|
new Set(updates.filter((st): st is Extract<Stmt, { k: 'assign' }> => st.k === 'assign').map((st) => st.name));
|
|
105
132
|
|
|
106
|
-
/** Why an exit arg cannot be REBUILT
|
|
133
|
+
/** Why an exit arg cannot be REBUILT inside the loop body — see the note above
|
|
107
134
|
* `PREUPDATE_SINK_GATES`. The walk collects EVERY one it finds, not the first: with one blocker
|
|
108
135
|
* per gate, stopping early would let ablating one gate disable another on any tree where the
|
|
109
136
|
* other's blocker happens to be found first, and the ablation would then be measuring less than
|
|
@@ -112,7 +139,7 @@ export type ArgBlocker = 'order-sensitive' | 'stale-name' | 'no-definition';
|
|
|
112
139
|
|
|
113
140
|
/** One exit slot weighed for sinking. `destName` is the name the sunk copy would write. */
|
|
114
141
|
export interface SinkCandidate {
|
|
115
|
-
/** everything that stops the arg's def-tree from being rebuilt
|
|
142
|
+
/** everything that stops the arg's def-tree from being rebuilt inside the body */
|
|
116
143
|
argBlockers: ReadonlySet<ArgBlocker>;
|
|
117
144
|
destName: string | undefined;
|
|
118
145
|
/** the names of the loop's own variables */
|
|
@@ -132,12 +159,20 @@ export const PREUPDATE_SINK_GATES: readonly Gate<SinkCandidate>[] = [
|
|
|
132
159
|
id: 'arg-safe-to-reevaluate',
|
|
133
160
|
why: 'an effect, a memory read or a trap gives a different answer where the rebuilt copy lands',
|
|
134
161
|
sound: true,
|
|
135
|
-
|
|
162
|
+
// Sound for a read, a call or a divide: rebuilt behind a store, a read answers with what the
|
|
163
|
+
// store wrote, and a call or a divide runs behind it. The analysis names a read or a call
|
|
164
|
+
// something would cross, a helper op an effect would cross, and a divide this move would carry
|
|
165
|
+
// past a memory access or an effect (`rebuiltPast`), and a named leaf is not rebuilt. What
|
|
166
|
+
// still reaches the gate through the pipeline is a divide the ISA computes in a body block
|
|
167
|
+
// that not every iteration runs, whose name would be stale where the copy lands
|
|
168
|
+
// (`arg-reads-current-names`). The rest of it is reached from a hand-built analysis, which is
|
|
169
|
+
// its guard.
|
|
170
|
+
guardedBy: 'hazards.test.ts: ablating arg-safe-to-reevaluate admits an exit arg whose read crosses a store',
|
|
136
171
|
rejects: (c) => c.argBlockers.has('order-sensitive'),
|
|
137
172
|
},
|
|
138
173
|
{
|
|
139
174
|
id: 'arg-reads-current-names',
|
|
140
|
-
why: 'a value computed in the body still
|
|
175
|
+
why: 'a value computed in the body may still hold the PREVIOUS iteration where the copy lands, unless its named def runs ahead of it on every iteration',
|
|
141
176
|
sound: true,
|
|
142
177
|
guardedBy: 'hazards.test.ts: ablating arg-reads-current-names admits an arg over a body-computed name',
|
|
143
178
|
rejects: (c) => c.argBlockers.has('stale-name'),
|
|
@@ -159,13 +194,175 @@ export const PREUPDATE_SINK_GATES: readonly Gate<SinkCandidate>[] = [
|
|
|
159
194
|
},
|
|
160
195
|
{
|
|
161
196
|
id: 'dest-free-inside-loop',
|
|
162
|
-
why: 'the name already denotes a value the loop reads, which a write
|
|
197
|
+
why: 'the name already denotes a value the loop reads, which a write inside the body clobbers',
|
|
163
198
|
sound: true,
|
|
164
199
|
guardedBy: 'hazards.test.ts: ablating dest-free-inside-loop admits a name the loop still reads',
|
|
165
200
|
rejects: (c) => c.destBusyInLoop,
|
|
166
201
|
},
|
|
167
202
|
];
|
|
168
203
|
|
|
204
|
+
/** The `++` a bottom test's pre-update read folds into, or the id of the gate that refused it —
|
|
205
|
+
* which is what the decline names, so `PREUPDATE_COND_GATES`'s reasons reach a reader apart. */
|
|
206
|
+
export type PreUpdateCondFold = { name: string; by: 1 | -1 } | { refused: string };
|
|
207
|
+
|
|
208
|
+
/** Why a bottom test's pre-update read of a loop variable cannot be spelled `n++` — see the note
|
|
209
|
+
* above `PREUPDATE_COND_GATES`. */
|
|
210
|
+
export interface PreUpdateCondCandidate {
|
|
211
|
+
/** the step the update spells: `n = n + 1` is 1, `n = n - 1` is -1, anything else null */
|
|
212
|
+
step: 1 | -1 | null;
|
|
213
|
+
/** loop variables the test reads at their pre-update value */
|
|
214
|
+
preUpdateNames: number;
|
|
215
|
+
/** how often the rendered test names THIS variable, its own `++` included */
|
|
216
|
+
mentions: number;
|
|
217
|
+
/** an op in the test renders something its operand tree does not show (a respelled def) */
|
|
218
|
+
unreadable: boolean;
|
|
219
|
+
/** a short-circuit op is reached through an op that is not one, so its arms' reach is unknown */
|
|
220
|
+
nestedConnective: boolean;
|
|
221
|
+
/** the folded leaf is evaluated on every iteration that takes the back edge */
|
|
222
|
+
onContinue: boolean;
|
|
223
|
+
/** …and on the iteration that leaves the loop */
|
|
224
|
+
onExit: boolean;
|
|
225
|
+
/** the value the update computes is read somewhere other than the back edge */
|
|
226
|
+
updateObserved: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** When a loop update may be folded into the bottom test that reads the variable BEFORE it —
|
|
230
|
+
* `do { … } while (v0 != 0 && v1++ <= 9)` in place of an update copy at the foot of the body and a
|
|
231
|
+
* test one iteration off.
|
|
232
|
+
*
|
|
233
|
+
* THE FOLD IS ABOUT WHEN THE UPDATE RUNS, and C answers that with the short-circuit operators on
|
|
234
|
+
* the path from the test's root down to the leaf. Two obligations come out of that, and they are
|
|
235
|
+
* the two gates a reader should look for first. The back edge carries `n + 1`, so every iteration
|
|
236
|
+
* that re-enters the loop must have evaluated the `++`: that is `folded-on-every-continue`, and it
|
|
237
|
+
* is what turns away a leaf under the RIGHT operand of an `||`, where a true left operand
|
|
238
|
+
* re-enters the loop having skipped it. And after the loop the variable's name is read as the
|
|
239
|
+
* value the back edge computed (`latchSub`), which the exiting iteration only leaves there if it
|
|
240
|
+
* evaluated the `++` too — `folded-on-the-exit-too`, asked only where something reads it.
|
|
241
|
+
*
|
|
242
|
+
* Neither obligation is a claim about where the machine put its own update. Where the two disagree
|
|
243
|
+
* the emitted C is still correct and the candidate merely does not match, which is the direction to
|
|
244
|
+
* be wrong in — and on the agbcc shape the fold exists for they agree, because the short-circuit
|
|
245
|
+
* fold (raise/shortcircuit.ts) is what merged the test's blocks into one in the first place.
|
|
246
|
+
*
|
|
247
|
+
* WHAT THIS TABLE DOES NOT JUDGE is the rest of the test. The fold puts the loop into a
|
|
248
|
+
* short-circuit spelling and everything else in the bottom test rides along, an EFFECT included —
|
|
249
|
+
* that question is `testSkipsAnEffect` below, asked of every `do-while` rather than only of a
|
|
250
|
+
* folded one, because the same arm holds the same call whether or not a counter moved into it.
|
|
251
|
+
*
|
|
252
|
+
* `variable-named-once` is C89's own rule rather than this pass's: an object modified between two
|
|
253
|
+
* sequence points may not be read again there, and `contracts.ts`'s `assertPostIncrUnshared` is the
|
|
254
|
+
* loud backstop for a later pass bringing a second read in.
|
|
255
|
+
*
|
|
256
|
+
* NOT THE WHOLE LIST OF REFUSALS on this decision. The gates judge the DEF TREE; `spellUpdateInCond`
|
|
257
|
+
* (structure.ts) asks the mention count again of what the lowering actually rendered, and throws
|
|
258
|
+
* its own `StructureError`.
|
|
259
|
+
*
|
|
260
|
+
* `one-pre-update-variable` is sound for a reason that lives in the CALLER: a fold sets
|
|
261
|
+
* `condRepaired`, which switches off the whole condition disjunct of `loopUpdateHazard` rather than
|
|
262
|
+
* the folded name's share of it. A second pre-update variable would then be emitted under its
|
|
263
|
+
* post-update name with every hazard reporting clean. */
|
|
264
|
+
export const PREUPDATE_COND_GATES: readonly Gate<PreUpdateCondCandidate>[] = [
|
|
265
|
+
{
|
|
266
|
+
id: 'test-is-readable',
|
|
267
|
+
why: 'an op that renders something its operand tree does not show hides whether the test names the variable again',
|
|
268
|
+
sound: true,
|
|
269
|
+
guardedBy: 'hazards.test.ts: ablating test-is-readable folds through a respelled def',
|
|
270
|
+
rejects: (c) => c.unreadable,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'one-pre-update-variable',
|
|
274
|
+
why: 'two variables read ahead of their updates would need both updates folded into one expression',
|
|
275
|
+
sound: true,
|
|
276
|
+
guardedBy: 'hazards.test.ts: ablating one-pre-update-variable repairs one variable and clobbers the other',
|
|
277
|
+
rejects: (c) => c.preUpdateNames !== 1,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'update-is-a-unit-step',
|
|
281
|
+
why: 'C has no read-then-update operator but ++ and --, so no other update has a spelling inside the test',
|
|
282
|
+
sound: true,
|
|
283
|
+
guardedBy: 'hazards.test.ts: ablating update-is-a-unit-step declines rather than minting a step-less node',
|
|
284
|
+
rejects: (c) => c.step === null,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'variable-named-once',
|
|
288
|
+
why: 'C89 leaves undefined which value a second read of an object updated in the same expression sees',
|
|
289
|
+
sound: true,
|
|
290
|
+
guardedBy: 'hazards.test.ts: ablating variable-named-once folds a test that reads the variable twice',
|
|
291
|
+
rejects: (c) => c.mentions !== 1,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'connectives-join-at-the-root',
|
|
295
|
+
why: 'a short-circuit reached through some other op decides its arms against that op, not against the test',
|
|
296
|
+
sound: true,
|
|
297
|
+
guardedBy: 'hazards.test.ts: ablating connectives-join-at-the-root folds an arm of a negated &&',
|
|
298
|
+
rejects: (c) => c.nestedConnective,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: 'folded-on-every-continue',
|
|
302
|
+
why: 'a short-circuit ahead of the leaf would skip the update on an iteration that re-enters the loop',
|
|
303
|
+
sound: true,
|
|
304
|
+
guardedBy: 'hazards.test.ts: ablating folded-on-every-continue folds a leaf under the right operand of an ||',
|
|
305
|
+
rejects: (c) => !c.onContinue,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'folded-on-the-exit-too',
|
|
309
|
+
why: 'the value after the loop is read, so the update has to run on the iteration that leaves as well',
|
|
310
|
+
sound: true,
|
|
311
|
+
guardedBy: 'hazards.test.ts: ablating folded-on-the-exit-too folds a leaf an exiting iteration skips',
|
|
312
|
+
rejects: (c) => c.updateObserved && !c.onExit,
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
/** Whether a position inside the bottom test is evaluated on the iterations where the whole test
|
|
317
|
+
* answers TRUE, and on the ones where it answers FALSE. Stated about the test's own value rather
|
|
318
|
+
* than about the loop's edges because the polarity is the caller's (`negated`). */
|
|
319
|
+
interface Reach {
|
|
320
|
+
onTrue: boolean;
|
|
321
|
+
onFalse: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const ALWAYS: Reach = { onTrue: true, onFalse: true };
|
|
325
|
+
|
|
326
|
+
/** The reach each operand of a SHORT-CIRCUIT op inherits. These two are the whole set: they are the
|
|
327
|
+
* ops `ARITH_TO_BIN` (structure.ts) renders as `&&`/`||` — a test holds the two lists together,
|
|
328
|
+
* since a connective missing from here is read as an ordinary op and its arms inherit a reach they
|
|
329
|
+
* do not have — and `Expr` has no conditional form besides them, no ternary, so every other op
|
|
330
|
+
* evaluates all of its operands whenever it is itself evaluated and they inherit its reach
|
|
331
|
+
* unchanged.
|
|
332
|
+
*
|
|
333
|
+
* In `a && b`, `b` runs only where `a` was true: a TRUE whole implies it ran, a FALSE whole does
|
|
334
|
+
* not. `a || b` is the dual.
|
|
335
|
+
*
|
|
336
|
+
* BOTH ARMS ARE STATED ABOUT THE OP'S OWN TRUTH, while `Reach` is stated about the WHOLE test's,
|
|
337
|
+
* so composing them down a chain is only valid where the two are the same value — a spine of
|
|
338
|
+
* connectives from the root. One `icmp_eq %and, 0` between them inverts the polarity and the arm
|
|
339
|
+
* answers backwards, which is `connectives-join-at-the-root`'s refusal: a connective reached
|
|
340
|
+
* through any other op is not read at all. */
|
|
341
|
+
export const SHORT_CIRCUIT_ARMS: Readonly<Record<string, (i: number, r: Reach) => Reach>> = {
|
|
342
|
+
logic_and: (i, r) => (i === 0 ? r : { onTrue: r.onTrue, onFalse: false }),
|
|
343
|
+
logic_or: (i, r) => (i === 0 ? r : { onTrue: false, onFalse: r.onFalse }),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** How many values a reach walk may visit. Neither memoises — a value two consumers read renders
|
|
347
|
+
* twice, and for `preUpdateCondFold` that count IS the fact being measured — so a shared def tree
|
|
348
|
+
* costs a walk once per path. Both answer exhaustion the refusing way, `unreadable` and "an effect
|
|
349
|
+
* may be skipped": a walk that stopped early has seen part of the test, and the part it did not
|
|
350
|
+
* see is the part a permissive answer would be about. */
|
|
351
|
+
const WALK_BUDGET = 4096;
|
|
352
|
+
|
|
353
|
+
/** The `± 1` an update spells, or null for every other update — `n = n + 1`, `n = 1 + n` and
|
|
354
|
+
* `n = n - 1` are the forms with a `++`/`--`. A cast anywhere in the value is null: the cast would
|
|
355
|
+
* have to be spelled around the update, which the operator cannot do. */
|
|
356
|
+
function unitStep(value: Expr, name: string): 1 | -1 | null {
|
|
357
|
+
if (value.k !== 'bin' || (value.op !== '+' && value.op !== '-')) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const isVar = (e: Expr): boolean => e.k === 'var' && e.name === name;
|
|
361
|
+
const one = (e: Expr): number | null => (e.k === 'const' && Math.abs(e.value) === 1 ? e.value : null);
|
|
362
|
+
const k = isVar(value.l) ? one(value.r) : value.op === '+' && isVar(value.r) ? one(value.l) : null;
|
|
363
|
+
return k === null ? null : ((value.op === '-' ? -k : k) as 1 | -1);
|
|
364
|
+
}
|
|
365
|
+
|
|
169
366
|
// `x + 0` / `x - 0` / `x | 0` are `x`. Substituting a loop variable by its init constant turns
|
|
170
367
|
// ordinary index arithmetic into exactly these, and a guard that spells the same value without
|
|
171
368
|
// the arithmetic would otherwise compare unequal.
|
|
@@ -223,8 +420,36 @@ function sameAtEntry(defs: Map<Value, Op>, a: Value, b: Value, entry: Map<Value,
|
|
|
223
420
|
return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode);
|
|
224
421
|
}
|
|
225
422
|
|
|
423
|
+
// WHERE A SUNK COPY IS REBUILT. The copy is not carried into the body, it is SPELLED AGAIN
|
|
424
|
+
// there, so the point it belongs at is the one its value was computed at: the arg's own defining
|
|
425
|
+
// op, when that op is one of `latch`'s — the block whose statements both loop emitters render
|
|
426
|
+
// inline, ahead of the update. Null for every other arg, and the copy opens the body instead.
|
|
427
|
+
// THREE args get that answer, not two: one with no position in the body at all (a block param, or
|
|
428
|
+
// a def outside the loop), and one the body DID compute, in a body block that is not the latch.
|
|
429
|
+
// The last is the narrow case and the one a wider sink would widen — the value has a position, it
|
|
430
|
+
// is just not in the block whose `sideEffects` walk is handed the copies.
|
|
431
|
+
//
|
|
432
|
+
// READING AN OP'S INDEX AS THE SOURCE'S STATEMENT ORDER IS A COMPILER CLAIM, and the project
|
|
433
|
+
// names the direction next door: target.ts's `readsStayWhereWritten` declares from compiled pairs
|
|
434
|
+
// that a compiler EMITS a read in the block the source spelled it in, and states outright that
|
|
435
|
+
// the converse — the asm's block is where the source read — is false and may not be defaulted.
|
|
436
|
+
// This is that inference one level down, inside a block, and it consults no target: on a
|
|
437
|
+
// scheduling compiler an op's index is the scheduler's order, not the source's.
|
|
438
|
+
//
|
|
439
|
+
// It owes no declaration because it buys a SPELLING, not an answer. `sideEffects` renders the
|
|
440
|
+
// whole block in that same index order, so a copy placed among those statements agrees with every
|
|
441
|
+
// one of them whatever the compiler did; and the motion that would change an ANSWER is the one
|
|
442
|
+
// `movesPast` measures, in the order it renders. Where the asm's order is not the source's, the
|
|
443
|
+
// failure is a candidate that does not match. Nothing off agbcc reaches it today — 0 sunk copies
|
|
444
|
+
// over the whole `--tier synthetic --toolchain ido7.1` run (163 rows), and the four non-agbcc
|
|
445
|
+
// `loop-preupdate` rows are all mwcc.
|
|
446
|
+
export function preUpdateCopyHome(defs: Map<Value, Op>, opBlock: Map<Op, Block>, a: Value, latch: Block): Op | null {
|
|
447
|
+
const d = defs.get(a);
|
|
448
|
+
return d !== undefined && opBlock.get(d) === latch ? d : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
226
451
|
export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
227
|
-
const { defs, varName, useSitesOf, liveIn, opBlock, materialize, respelledDefs } = deps;
|
|
452
|
+
const { defs, varName, useSitesOf, liveIn, opBlock, materialize, respelledDefs, emitPos } = deps;
|
|
228
453
|
|
|
229
454
|
// The names one loop iteration writes under its VARIABLES' names: the update copies, plus a
|
|
230
455
|
// loop-variable name a materialized body def writes IN PLACE. Adoption (seedLoopParams) makes
|
|
@@ -249,6 +474,32 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
249
474
|
return writes;
|
|
250
475
|
};
|
|
251
476
|
|
|
477
|
+
// The same set, for an iteration that leaves part-way through its body rather than at its latch:
|
|
478
|
+
// the loop-variable names written by the time control reaches the end of `ahead` (the body blocks
|
|
479
|
+
// between the header and a mid-body `break`). No update copy has run there; a name is written by
|
|
480
|
+
// a param copy on an edge INTO one of those blocks (`incoming` gives each in-edge's args), unless
|
|
481
|
+
// every such copy is an identity — the elision `argAssigns` makes — or by a materialized def in
|
|
482
|
+
// place, as `loopWriteSet` counts it. Only loop-variable names, for `loopWriteSet`'s reason: any
|
|
483
|
+
// other name is written once per iteration, so a read of it is this iteration's value.
|
|
484
|
+
const loopWriteSetAhead = (ahead: Iterable<Block>, incoming: (b: Block) => Value[][], header: Block): Set<string> => {
|
|
485
|
+
const blocks = [...ahead];
|
|
486
|
+
const writes = loopWriteSet([], blocks, header);
|
|
487
|
+
const paramNames = new Set(header.params.map((p) => varName.get(p)));
|
|
488
|
+
for (const bb of blocks) {
|
|
489
|
+
if (bb === header) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const ins = incoming(bb);
|
|
493
|
+
bb.params.forEach((p, k) => {
|
|
494
|
+
const nm = varName.get(p);
|
|
495
|
+
if (nm !== undefined && paramNames.has(nm) && ins.some((args) => varName.get(args[k]) !== nm)) {
|
|
496
|
+
writes.add(nm);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return writes;
|
|
501
|
+
};
|
|
502
|
+
|
|
252
503
|
// Does rendering `v` under `sub` read a variable that a pending loop update (`updateWrites`, the
|
|
253
504
|
// names it assigns) overwrites, via a path OTHER than a `sub`-mapped back-edge arg? Such a read is a
|
|
254
505
|
// PRE-update value the update clobbers → a read-after-write hazard when the update is emitted first.
|
|
@@ -311,8 +562,10 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
311
562
|
//
|
|
312
563
|
// AND IT DECLINES ONLY BECAUSE OF HOW THE VALUE IS SPELLED. `sinkablePreUpdateSlots` below
|
|
313
564
|
// 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;
|
|
315
|
-
//
|
|
565
|
+
// pre-update value crosses the exit as an edge ARG; a value a body OP computed is named at its
|
|
566
|
+
// def where the code past the latch's exit reads it (`escapesAheadOfUpdate`, analysis.ts, which
|
|
567
|
+
// lists where it refuses); read from the header PARAM instead it arrives here, with neither an
|
|
568
|
+
// exit slot to sink nor an op to name. All three spellings, and the agbcc listing, are in
|
|
316
569
|
// test/loop-preupdate-escape.test.ts. Routing the param one into the sink is a structure.ts
|
|
317
570
|
// change with a fan effect on every row, not an edit to this predicate.
|
|
318
571
|
const escaped = (v: Value): boolean => {
|
|
@@ -354,23 +607,192 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
354
607
|
sub: Map<Value, string>,
|
|
355
608
|
updateWrites: Set<string>,
|
|
356
609
|
region: Set<Block> | null,
|
|
610
|
+
// The bottom test's own read is spelled `n++` instead of declining (`preUpdateCondFold`), so
|
|
611
|
+
// the caller has already answered this disjunct. The other two are untouched by that fold: the
|
|
612
|
+
// update still runs, so an exit slot and an escaped body value still read the post-update name.
|
|
613
|
+
condRepaired = false,
|
|
357
614
|
): boolean =>
|
|
358
|
-
readsClobbered(condV, sub, updateWrites) ||
|
|
615
|
+
(!condRepaired && readsClobbered(condV, sub, updateWrites)) ||
|
|
359
616
|
exitArgs.some((a) => readsClobbered(a, sub, updateWrites)) ||
|
|
360
617
|
loopEscapeHazard(body, sub, updateWrites, region);
|
|
361
618
|
|
|
619
|
+
// DOES THE RENDERED BOTTOM TEST PUT AN EFFECT WHERE A SHORT CIRCUIT MAY SKIP IT? The `&&`/`||` the
|
|
620
|
+
// test renders as evaluate their right operand conditionally; the machine's branch did not, for
|
|
621
|
+
// anything the fold above may have brought into the test. Where the skipped position holds an
|
|
622
|
+
// EFFECT the emitted loop runs it fewer times than the asm does.
|
|
623
|
+
//
|
|
624
|
+
// The effect flag is the whole test and no block position is consulted, because for an effectful
|
|
625
|
+
// op the two say the same thing: every effectful op is `speculationUnsafe` (ir/discipline.ts), so
|
|
626
|
+
// raise/shortcircuit.ts never lifts one out of the arm it guards and a genuinely short-circuited
|
|
627
|
+
// effect never reaches a connective here. A memory read is exempt there — C's own short circuit
|
|
628
|
+
// re-guards it at the new point — and stays exempt here for the same reason. The reads for which
|
|
629
|
+
// re-guarding is not merely a spelling — the qualified ones (ir/discipline.ts `counted`) — are
|
|
630
|
+
// `speculationUnsafe` too, so one in the connective ran above it, and structure/analysis.ts names
|
|
631
|
+
// it there.
|
|
632
|
+
//
|
|
633
|
+
// NO SPINE CONDITION, unlike the gates that read the same `Reach`: those ask which LOOP EDGE a
|
|
634
|
+
// position is evaluated on, which an op between the connectives inverts. This asks only whether
|
|
635
|
+
// the position is evaluated EVERY time the test is, and `!(a && b)` skips `b` exactly where
|
|
636
|
+
// `a && b` does. `SHORT_CIRCUIT_ARMS` composes to that answer on its own — an arm function clears
|
|
637
|
+
// one side of the reach and nothing down the chain restores it — so the polarity never enters.
|
|
638
|
+
//
|
|
639
|
+
// A def the walk cannot read through is DESCENDED rather than refused, which is the other
|
|
640
|
+
// direction from the fold's `unreadable`: a def spelled another way still names the values its
|
|
641
|
+
// operands stand for, so an effect below one is an effect in that position. The budget is the
|
|
642
|
+
// answer given without looking, so it goes the refusing way.
|
|
643
|
+
const testSkipsAnEffect = (condV: Value, sub: Map<Value, string>): boolean => {
|
|
644
|
+
let budget = WALK_BUDGET;
|
|
645
|
+
let found = false;
|
|
646
|
+
const walk = (x: Value, r: Reach): void => {
|
|
647
|
+
if (found || sub.has(x) || varName.has(x)) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (budget-- <= 0) {
|
|
651
|
+
found = true;
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const d = defs.get(x);
|
|
655
|
+
if (d === undefined) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (effectful(d) && !(r.onTrue && r.onFalse)) {
|
|
659
|
+
found = true;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const arm = SHORT_CIRCUIT_ARMS[d.opcode];
|
|
663
|
+
d.operands.forEach((o, i) => walk(o, arm === undefined ? r : arm(i, r)));
|
|
664
|
+
};
|
|
665
|
+
walk(condV, ALWAYS);
|
|
666
|
+
return found;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// WHICH SPELLING A PRE-UPDATE READ IN THE BOTTOM TEST HAS. Returns the update to fold and the
|
|
670
|
+
// step to fold it as, or the id of the gate that refused — `PREUPDATE_COND_GATES` above carries
|
|
671
|
+
// the refusals and the argument. A fold implies the condition's hazard is REPAIRED, and nothing
|
|
672
|
+
// else: the exit slots and the escaped body values are `loopUpdateHazard`'s other two disjuncts
|
|
673
|
+
// and are unaffected by the fold, which is why they are asked for separately and why
|
|
674
|
+
// `one-pre-update-variable` keeps this to the single-variable case.
|
|
675
|
+
//
|
|
676
|
+
// The walk is `readsClobbered`'s, with two things added that the boolean does not need: it COUNTS
|
|
677
|
+
// occurrences instead of stopping at the first (C89's sequence-point rule is about the count), it
|
|
678
|
+
// tracks, per name, whether the leaf's position is reached when the whole test is TRUE and when
|
|
679
|
+
// it is FALSE (`SHORT_CIRCUIT_ARMS`). A name reached twice takes the INTERSECTION of the two
|
|
680
|
+
// positions' reach, which `variable-named-once` then refuses anyway — kept so the field means the
|
|
681
|
+
// same thing whichever gate is ablated.
|
|
682
|
+
//
|
|
683
|
+
// NOT MEMOISED, unlike `readsClobbered`'s `seen`: a value read twice renders twice, and that is
|
|
684
|
+
// the fact being counted. `WALK_BUDGET` bounds the DAG blow-up that costs, and exhausting it is
|
|
685
|
+
// `unreadable` — the same answer a def whose lowering the walk cannot see gets.
|
|
686
|
+
const preUpdateCondFold = (
|
|
687
|
+
condV: Value,
|
|
688
|
+
negated: boolean,
|
|
689
|
+
header: Block,
|
|
690
|
+
backArgs: readonly Value[],
|
|
691
|
+
exitArgs: readonly Value[],
|
|
692
|
+
body: Set<Block>,
|
|
693
|
+
sub: Map<Value, string>,
|
|
694
|
+
updates: Stmt[],
|
|
695
|
+
updateWrites: Set<string>,
|
|
696
|
+
gates: readonly Gate<PreUpdateCondCandidate>[] = PREUPDATE_COND_GATES,
|
|
697
|
+
): PreUpdateCondFold => {
|
|
698
|
+
let budget = WALK_BUDGET;
|
|
699
|
+
let unreadable = false;
|
|
700
|
+
let nestedConnective = false;
|
|
701
|
+
const mentions = new Map<string, number>();
|
|
702
|
+
const pre = new Map<string, Reach>();
|
|
703
|
+
const note = (name: string, r: Reach): void => {
|
|
704
|
+
mentions.set(name, (mentions.get(name) ?? 0) + 1);
|
|
705
|
+
if (!updateWrites.has(name)) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const was = pre.get(name);
|
|
709
|
+
pre.set(name, was === undefined ? r : { onTrue: was.onTrue && r.onTrue, onFalse: was.onFalse && r.onFalse });
|
|
710
|
+
};
|
|
711
|
+
// `spine` says the path from the root here ran through connectives only, which is the condition
|
|
712
|
+
// under which `r` is about this op's own truth as well as the whole test's.
|
|
713
|
+
const walk = (x: Value, r: Reach, spine: boolean): void => {
|
|
714
|
+
if (budget-- <= 0) {
|
|
715
|
+
unreadable = true;
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const post = sub.get(x);
|
|
719
|
+
if (post !== undefined) {
|
|
720
|
+
mentions.set(post, (mentions.get(post) ?? 0) + 1); // a post-update read: no hazard, still a mention
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const own = varName.get(x);
|
|
724
|
+
if (own !== undefined) {
|
|
725
|
+
note(own, r);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const d = defs.get(x);
|
|
729
|
+
if (d === undefined) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (respelledDefs.has(d)) {
|
|
733
|
+
unreadable = true;
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const arm = SHORT_CIRCUIT_ARMS[d.opcode];
|
|
737
|
+
if (arm !== undefined && !spine) {
|
|
738
|
+
nestedConnective = true;
|
|
739
|
+
}
|
|
740
|
+
d.operands.forEach((o, i) => walk(o, arm === undefined ? r : arm(i, r), arm !== undefined));
|
|
741
|
+
};
|
|
742
|
+
walk(condV, ALWAYS, true);
|
|
743
|
+
const [name] = [...pre.keys()];
|
|
744
|
+
const reach = name === undefined ? ALWAYS : pre.get(name)!;
|
|
745
|
+
const assigns = updates.filter((st): st is Extract<Stmt, { k: 'assign' }> => st.k === 'assign' && st.name === name);
|
|
746
|
+
const u = backArgs[header.params.findIndex((p) => varName.get(p) === name)];
|
|
747
|
+
const c: PreUpdateCondCandidate = {
|
|
748
|
+
step: assigns.length === 1 ? unitStep(assigns[0].value, name) : null,
|
|
749
|
+
preUpdateNames: pre.size,
|
|
750
|
+
mentions: mentions.get(name) ?? 0,
|
|
751
|
+
unreadable,
|
|
752
|
+
nestedConnective,
|
|
753
|
+
onContinue: negated ? reach.onFalse : reach.onTrue,
|
|
754
|
+
onExit: negated ? reach.onTrue : reach.onFalse,
|
|
755
|
+
updateObserved:
|
|
756
|
+
u === undefined || exitArgs.includes(u) || (useSitesOf.get(u) ?? []).some((s) => !body.has(s.blk)),
|
|
757
|
+
};
|
|
758
|
+
const refusal = firstRejection(gates, c);
|
|
759
|
+
if (refusal !== null) {
|
|
760
|
+
return { refused: refusal };
|
|
761
|
+
}
|
|
762
|
+
// Asked again at the return, not `c.step!`: ablating `update-is-a-unit-step` is a question about
|
|
763
|
+
// the CENSUS, and the node's shape is not negotiable — a `{ by: null }` renders as `n--` over a
|
|
764
|
+
// body the emitter has already dropped the real update from. So the ablation gets that gate's
|
|
765
|
+
// refusal back rather than a malformed fold.
|
|
766
|
+
if (c.step === null) {
|
|
767
|
+
return { refused: 'update-is-a-unit-step' };
|
|
768
|
+
}
|
|
769
|
+
return { name, by: c.step };
|
|
770
|
+
};
|
|
771
|
+
|
|
362
772
|
// Which pre-update exit copies can be REPAIRED instead of declined. The exiting edge hands a
|
|
363
773
|
// loop variable's top-of-iteration value to a merge param, and post-loop that name has moved on
|
|
364
774
|
// one iteration; emitting the copy inside the body, AHEAD of the update, restores it — the
|
|
365
775
|
// trailing-pointer idiom (`for (fast = slow = head; ...; fast = fast->next) slow = fast;`).
|
|
366
|
-
// Returns the exit slots that may move
|
|
367
|
-
//
|
|
776
|
+
// Returns the exit slots that may move, each mapped to the point the caller rebuilds it at — an
|
|
777
|
+
// op of the latch, or null for the copies that open the body — and the caller drops each one from
|
|
778
|
+
// the post-loop copies.
|
|
779
|
+
//
|
|
780
|
+
// `staleOnZeroTrip` offers the fused-guard emitter's other slots too: one whose post-loop copy
|
|
781
|
+
// would not reproduce the value the guard's exit edge carries past a loop that never ran, such as
|
|
782
|
+
// a value the body names (`t = g(i); h(4);` in `for (…)`, exiting with `t`). Moved into the body
|
|
783
|
+
// and seeded from that edge like a pre-update copy, it is the same program on both paths, and it
|
|
784
|
+
// is weighed by the same gates.
|
|
368
785
|
//
|
|
369
786
|
// The idiom reaches here at all because the compiler DID keep a second register for the trailing
|
|
370
787
|
// value and SSA construction folded the copy away, leaving the exit edge as the only place the
|
|
371
788
|
// value is still named. Where the compiler kept two loop-carried registers instead, the value is
|
|
372
789
|
// a back-edge arg and the un-rotation substitution already reads it — no repair needed.
|
|
373
790
|
//
|
|
791
|
+
// Each admitted slot comes back WITH THE POSITION IT WAS JUDGED AT, because permission and
|
|
792
|
+
// placement are one answer: `arg-safe-to-reevaluate` clears the tree against that position and no
|
|
793
|
+
// other, so an emitter free to re-derive its own would be free to place a copy where nothing
|
|
794
|
+
// cleared it — silently, since the statement is still emitted and still reads names that resolve.
|
|
795
|
+
//
|
|
374
796
|
// `PREUPDATE_SINK_GATES` holds the per-candidate refusals, ablatable one at a time. Two rules are
|
|
375
797
|
// properties of the EDGE rather than of a candidate and stay here: the exit edge is a PARALLEL
|
|
376
798
|
// copy, so splitting it across two program points must not let two slots claim one name, nor let
|
|
@@ -380,19 +802,23 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
380
802
|
// slot's destination — needs no rule, and the copies are emitted sequentially because of it.
|
|
381
803
|
// Every leaf such an expression could read is already refused by a per-candidate gate: a header
|
|
382
804
|
// param makes the other slot fail `dest-not-loop-variable`; a body-defined value is
|
|
383
|
-
// `stale-name
|
|
384
|
-
//
|
|
385
|
-
//
|
|
805
|
+
// `stale-name`, or — the one it takes as current, a def named ahead of the home — a body value
|
|
806
|
+
// under the other slot's destination, which fails `dest-free-inside-loop`; and a value defined
|
|
807
|
+
// outside the loop but read on the exit edge is live-in at the header (analysis.ts counts a
|
|
808
|
+
// successor arg as a use at the predecessor's end), which makes the other slot fail
|
|
809
|
+
// `dest-free-inside-loop` too.
|
|
386
810
|
const sinkablePreUpdateSlots = (
|
|
387
811
|
header: Block,
|
|
388
812
|
exit: Block,
|
|
389
813
|
exitArgs: readonly Value[],
|
|
390
814
|
body: Set<Block>,
|
|
815
|
+
latch: Block,
|
|
391
816
|
sub: Map<Value, string>,
|
|
392
817
|
updateWrites: Set<string>,
|
|
393
818
|
gates: readonly Gate<SinkCandidate>[] = PREUPDATE_SINK_GATES,
|
|
394
|
-
|
|
395
|
-
|
|
819
|
+
staleOnZeroTrip: (slot: number) => boolean = () => false,
|
|
820
|
+
): Map<number, Op | null> => {
|
|
821
|
+
const none = new Map<number, Op | null>();
|
|
396
822
|
const headerNames = new Set(header.params.map((p) => varName.get(p)));
|
|
397
823
|
// Is `v` defined by the loop body itself — an op in one of its blocks, or a block param?
|
|
398
824
|
// An op with no `opBlock` entry counts as INSIDE: the map is total over the function, and the
|
|
@@ -410,18 +836,35 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
410
836
|
// that check plus the body's own defs and params covers every way the name is still in use.
|
|
411
837
|
// The loop's OWN variables are left to `dest-not-loop-variable` — a header param is also a
|
|
412
838
|
// body param, so the two gates partition the names instead of overlapping.
|
|
413
|
-
|
|
839
|
+
//
|
|
840
|
+
// A value with NO name is in use under the name too when it renders through it: live into the
|
|
841
|
+
// header, it is inlined inside the loop and spells its operands' names there. agbcc hoists `k &
|
|
842
|
+
// 3` of `H[k & 3] = …` ahead of a loop that reuses `k`'s register for `s`, and the exit copy of
|
|
843
|
+
// `s`, offered `k`'s name and sunk into the body, would overwrite the `a3` that `H[a3 & 3]` reads.
|
|
844
|
+
// Reading `self` is reading the same value, not another one under its name.
|
|
845
|
+
const rendersName = (w: Value, name: string, self: Value, seen: Set<Value>): boolean => {
|
|
846
|
+
if (seen.has(w)) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
seen.add(w);
|
|
850
|
+
const n = varName.get(w);
|
|
851
|
+
if (n !== undefined) {
|
|
852
|
+
return n === name && w !== self;
|
|
853
|
+
}
|
|
854
|
+
return (defs.get(w)?.operands ?? []).some((o) => rendersName(o, name, self, seen));
|
|
855
|
+
};
|
|
856
|
+
const busyInLoop = (name: string, self: Value, alsoSelf?: Value): boolean => {
|
|
414
857
|
for (const [v, n] of varName) {
|
|
415
|
-
if (n !== name || v === self || header.params.includes(v)) {
|
|
858
|
+
if (n !== name || v === self || v === alsoSelf || header.params.includes(v)) {
|
|
416
859
|
continue;
|
|
417
860
|
}
|
|
418
861
|
if (liveIn.get(header)!.has(v) || definedInBody(v)) {
|
|
419
862
|
return true;
|
|
420
863
|
}
|
|
421
864
|
}
|
|
422
|
-
return
|
|
865
|
+
return [...liveIn.get(header)!].some((w) => !varName.has(w) && rendersName(w, name, self, new Set()));
|
|
423
866
|
};
|
|
424
|
-
// Everything that stops `a` from being REBUILT
|
|
867
|
+
// Everything that stops `a` from being REBUILT inside the body. Walks the def-tree
|
|
425
868
|
// where `exprWith(null)` will when the copy is spelled — stopping at a NAMED value, which
|
|
426
869
|
// renders as its name, and at a value with no reaching def, which renders as a gap.
|
|
427
870
|
//
|
|
@@ -434,20 +877,134 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
434
877
|
//
|
|
435
878
|
// `undef` and `laddr` render a name from their own side maps rather than `varName`, and both
|
|
436
879
|
// are position-independent — an `undef` is never assigned, a `laddr` is an address.
|
|
437
|
-
|
|
880
|
+
//
|
|
881
|
+
// Does re-evaluating `d` at the copy's position answer differently? `home` is that position
|
|
882
|
+
// (`preUpdateCopyHome`), so the ops that can tell are the ones STRICTLY BETWEEN the two, and one
|
|
883
|
+
// ahead of `d` that renders after the home (below) — and only when both are the latch's, because
|
|
884
|
+
// a def in any other block is separated from the copy by whole blocks this does not walk.
|
|
885
|
+
// `orderSensitive` is the between-set for all three ways `d` can care: a read wants no store
|
|
886
|
+
// crossed, an effect no other effect or read, and a trap none of those performed ahead of the fault.
|
|
887
|
+
//
|
|
888
|
+
// NOT A RESTATEMENT OF WHAT `materialize` ALREADY BOUNDS, which is the reading to guard against.
|
|
889
|
+
// That pass NAMES an order-sensitive value whose consumer is not adjacent to it, and a named
|
|
890
|
+
// leaf is not rebuilt, so this scan never weighs it. But it measures the distance to the edge
|
|
891
|
+
// copy's own part of the TERMINATOR and lets pass what shares that statement, while the copy is
|
|
892
|
+
// rebuilt at `home`, ahead of every part — and it lets a read pass a read.
|
|
893
|
+
//
|
|
894
|
+
// AND NOT ONLY BETWEEN. An order-sensitive op AHEAD of `d` that the rebuilt tree does not hold
|
|
895
|
+
// is crossed too when it RENDERS after the home (`rendersAfter`): inlined into an update copy at
|
|
896
|
+
// the foot of the body, it runs after the member the asm ran it before. A call there is named
|
|
897
|
+
// by the analysis (the member lies between it and its copy, in another part of the terminator,
|
|
898
|
+
// and bars it), so what still reaches this arm is a read, and a read crosses only a member that
|
|
899
|
+
// is not one: two reads commute, whichever runs first (`preupdate_exit_reads`: `u = q[1]; r = *q
|
|
900
|
+
// + 1; s = s + u;`).
|
|
901
|
+
//
|
|
902
|
+
// `latch.ops` INDEX ORDER IS EXECUTION ORDER — what `slice` reads. The one ISA fact that bends
|
|
903
|
+
// it cannot reach here: a MIPS branch-likely NULLIFIES its delay slot, so placement gives that
|
|
904
|
+
// slot its own block on the taken edge rather than a later index in the latch, and `i < 0`
|
|
905
|
+
// then refuses it as a def this block does not hold.
|
|
906
|
+
//
|
|
907
|
+
// `i < 0` carries a second refusal: a def in ANOTHER block, separated from the copy's position
|
|
908
|
+
// in the latch by whole blocks nothing here walks. Its witness is hazards.test.ts's `a read from
|
|
909
|
+
// ANOTHER body block is refused even though the arg itself is a latch op`.
|
|
910
|
+
//
|
|
911
|
+
// THE BETWEEN-SET DOES NOT EXEMPT THE TREE BEING REBUILT, where `pattern/engine.ts`'s
|
|
912
|
+
// index-order scan does exempt its own cone — a cone member is inlined into the very expression
|
|
913
|
+
// that moves, so it is no barrier to it. Two order-sensitive ops under one arg move together,
|
|
914
|
+
// but the compiler orders them inside the expression, not the asm: every corpus compiler calls
|
|
915
|
+
// before it reads (analysis.ts, the barrier scan), so a read ahead of a call in one tree comes
|
|
916
|
+
// back behind it. Left narrow on purpose; the AHEAD arm does exempt the tree, because a member
|
|
917
|
+
// ahead of `d` is weighed by this same scan from its own side.
|
|
918
|
+
//
|
|
919
|
+
// PER SLOT, and two slots sharing one order-sensitive def spell it TWICE — one `ldr` feeding
|
|
920
|
+
// two exit args becomes two reads in the emitted C. For a READ that is legal by this same scan:
|
|
921
|
+
// either nothing order-sensitive lies between the two homes, or the second slot is refused and
|
|
922
|
+
// the edge stands down whole. It costs a spelling; only a `volatile` qualifier would make the
|
|
923
|
+
// extra access observable. A variation mints one the differ referees (l3/volatileptr.ts), and
|
|
924
|
+
// the default candidate carries one on a read the device pin or the symbol map qualifies — but
|
|
925
|
+
// the analysis places such a read as it places a call (ir/discipline.ts `counted`), naming one
|
|
926
|
+
// with two uses or one that rides an edge copy, so neither reaches this scan twice, and
|
|
927
|
+
// `assertEffectsPreserved` (contracts.ts) counts both on the path whatever reaches here anyway.
|
|
928
|
+
const movesPast = (d: Op, home: Op, tree: ReadonlySet<Op>): boolean => {
|
|
929
|
+
const i = latch.ops.indexOf(d);
|
|
930
|
+
const p = latch.ops.indexOf(home);
|
|
931
|
+
if (i < 0 || i > p || latch.ops.slice(i + 1, p).some(orderSensitive)) {
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
return latch.ops
|
|
935
|
+
.slice(0, i)
|
|
936
|
+
.some((o) => orderSensitive(o) && !tree.has(o) && !(isRead(o) && isRead(d)) && rendersAfter(o, p));
|
|
937
|
+
};
|
|
938
|
+
// A memory read and nothing else: two of them commute, whichever runs first. Two qualified
|
|
939
|
+
// reads would not, and never meet in the AHEAD arm: the analysis names a qualified read that
|
|
940
|
+
// another qualified read stands between it and its render (`isBarrier`), so a qualified op ahead
|
|
941
|
+
// of `d` renders at its own index.
|
|
942
|
+
const isRead = (o: Op): boolean => orderSensitive(o) && !effectful(o);
|
|
943
|
+
// Does latch op `o` RENDER after index `p`? Where it renders is `emitPos`: the terminator's
|
|
944
|
+
// index stands for every copy it carries, and those land at or after the foot of the body; an
|
|
945
|
+
// op with no one position, or none in the latch, counts as after.
|
|
946
|
+
const rendersAfter = (o: Op, p: number): boolean => {
|
|
947
|
+
const q = emitPos(o);
|
|
948
|
+
return q === null || q.blk !== latch || q.idx > p;
|
|
949
|
+
};
|
|
950
|
+
// A body-defined name that IS current where the copy lands: a def the analysis NAMED (so it
|
|
951
|
+
// renders as a statement at its own position) that every iteration reaching the copy has just
|
|
952
|
+
// run — in the latch strictly ahead of the copy's home, which `sideEffects` renders in index
|
|
953
|
+
// order, or in another body block on every path from the header to the latch (`everyIteration`).
|
|
954
|
+
// The name holds this iteration's value, not the previous one's. That it is the ONLY write to
|
|
955
|
+
// the name is the other two conjuncts' job, which this leaves standing: a loop variable's name
|
|
956
|
+
// (`headerNames`) and a name any other value in the loop also answers to (`busyInLoop`) still
|
|
957
|
+
// refuse. So does every other body-defined name — one defined AFTER the home, in a block an
|
|
958
|
+
// iteration can skip, or not named by a def at all (a block param) — and the copy that opens
|
|
959
|
+
// the body (`home === null`) has no position to be behind. The home ITSELF counts as ahead:
|
|
960
|
+
// `sideEffects` spells a copy homed at an op after that op's own statement, so a copy of the
|
|
961
|
+
// named value its home computes reads what that statement just wrote.
|
|
962
|
+
const writtenAheadOf = (x: Value, home: Op | null): boolean => {
|
|
963
|
+
const d = defs.get(x);
|
|
964
|
+
if (home === null || d === undefined || !materialize.has(d)) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
const b = opBlock.get(d);
|
|
968
|
+
if (b !== latch) {
|
|
969
|
+
return b !== undefined && everyIteration(b);
|
|
970
|
+
}
|
|
971
|
+
return latch.ops.indexOf(d) <= latch.ops.indexOf(home);
|
|
972
|
+
};
|
|
973
|
+
// Does every path from the header to the latch, inside the body, run block `b`?
|
|
974
|
+
const everyIteration = (b: Block): boolean => {
|
|
975
|
+
if (b === header) {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
if (!body.has(b)) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
const reached = new Set<Block>([header, b]);
|
|
982
|
+
const stack = [header];
|
|
983
|
+
while (stack.length > 0) {
|
|
984
|
+
for (const s of successorsOf(stack.pop()!)) {
|
|
985
|
+
if (body.has(s) && !reached.has(s)) {
|
|
986
|
+
reached.add(s);
|
|
987
|
+
stack.push(s);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return !reached.has(latch);
|
|
992
|
+
};
|
|
993
|
+
const blockersOf = (a: Value, home: Op | null): ReadonlySet<ArgBlocker> => {
|
|
438
994
|
const seen = new Set<Value>();
|
|
439
995
|
const found = new Set<ArgBlocker>();
|
|
996
|
+
const tree = new Set<Op>();
|
|
440
997
|
const walk = (x: Value): void => {
|
|
441
998
|
if (seen.has(x)) {
|
|
442
999
|
return;
|
|
443
1000
|
}
|
|
444
1001
|
seen.add(x);
|
|
445
1002
|
if (header.params.includes(x)) {
|
|
446
|
-
return; // a loop variable:
|
|
1003
|
+
return; // a loop variable: ahead of the update its name holds exactly this
|
|
447
1004
|
}
|
|
448
1005
|
const n = varName.get(x);
|
|
449
1006
|
if (n !== undefined) {
|
|
450
|
-
if (definedInBody(x) || headerNames.has(n) || busyInLoop(n, x)) {
|
|
1007
|
+
if ((definedInBody(x) && !writtenAheadOf(x, home)) || headerNames.has(n) || busyInLoop(n, x)) {
|
|
451
1008
|
found.add('stale-name');
|
|
452
1009
|
}
|
|
453
1010
|
return;
|
|
@@ -457,45 +1014,63 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
457
1014
|
found.add('no-definition');
|
|
458
1015
|
return;
|
|
459
1016
|
}
|
|
460
|
-
|
|
461
|
-
found.add('order-sensitive');
|
|
462
|
-
}
|
|
1017
|
+
tree.add(d);
|
|
463
1018
|
d.operands.forEach(walk);
|
|
464
1019
|
};
|
|
465
1020
|
walk(a);
|
|
1021
|
+
// Weighed once the whole tree is known, because `movesPast` exempts its members. A respelled
|
|
1022
|
+
// def is opaque — what it renders is a memory read this walk never sees — so it is refused at
|
|
1023
|
+
// every position rather than measured.
|
|
1024
|
+
for (const d of tree) {
|
|
1025
|
+
if (respelledDefs.has(d) || (reevalUnsafe(d) && (home === null || movesPast(d, home, tree)))) {
|
|
1026
|
+
found.add('order-sensitive');
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
466
1029
|
return found;
|
|
467
1030
|
};
|
|
468
|
-
const
|
|
1031
|
+
const cleared = new Map<number, { name: string; home: Op | null }>();
|
|
469
1032
|
exitArgs.forEach((a, j) => {
|
|
470
|
-
if (!readsClobbered(a, sub, updateWrites)) {
|
|
1033
|
+
if (!readsClobbered(a, sub, updateWrites) && !staleOnZeroTrip(j)) {
|
|
471
1034
|
return; // no hazard on this slot — nothing to repair
|
|
472
1035
|
}
|
|
473
1036
|
const destName = varName.get(exit.params[j]);
|
|
1037
|
+
const home = preUpdateCopyHome(defs, opBlock, a, latch);
|
|
474
1038
|
const c: SinkCandidate = {
|
|
475
|
-
argBlockers: blockersOf(a),
|
|
1039
|
+
argBlockers: blockersOf(a, home),
|
|
476
1040
|
destName,
|
|
477
1041
|
headerNames,
|
|
478
1042
|
updateWrites,
|
|
479
|
-
|
|
1043
|
+
// An arg the BODY defines under the destination's name is the one write that name already
|
|
1044
|
+
// has there: its own statement rewrites the name each iteration, so its copy is `dest =
|
|
1045
|
+
// dest` and the seed is all the slot needs. An arg from outside the loop under that name
|
|
1046
|
+
// (a parameter kmc keeps in the destination's register) is read by the body, and a seed
|
|
1047
|
+
// ahead of the loop would overwrite it.
|
|
1048
|
+
destBusyInLoop:
|
|
1049
|
+
destName !== undefined && busyInLoop(destName, exit.params[j], definedInBody(a) ? a : undefined),
|
|
480
1050
|
};
|
|
481
1051
|
if (firstRejection(gates, c) === null) {
|
|
482
|
-
|
|
1052
|
+
cleared.set(j, { name: destName!, home });
|
|
483
1053
|
}
|
|
484
1054
|
});
|
|
485
|
-
const names = new Set(
|
|
486
|
-
if (
|
|
1055
|
+
const names = new Set([...cleared.values()].map((c) => c.name));
|
|
1056
|
+
if (cleared.size === 0 || names.size !== cleared.size) {
|
|
487
1057
|
return none;
|
|
488
1058
|
}
|
|
489
|
-
return exitArgs.some((a, j) => !
|
|
1059
|
+
return exitArgs.some((a, j) => !cleared.has(j) && readsClobbered(a, sub, names))
|
|
1060
|
+
? none
|
|
1061
|
+
: new Map([...cleared].map(([j, c]) => [j, c.home]));
|
|
490
1062
|
};
|
|
491
1063
|
|
|
492
1064
|
return {
|
|
493
1065
|
readsClobbered,
|
|
494
1066
|
loopEscapeHazard,
|
|
495
1067
|
loopUpdateHazard,
|
|
1068
|
+
testSkipsAnEffect,
|
|
1069
|
+
preUpdateCondFold,
|
|
496
1070
|
sinkablePreUpdateSlots,
|
|
497
1071
|
sameAtEntry: (a, b, entry, negated = false) => sameAtEntry(defs, a, b, entry, negated),
|
|
498
1072
|
loopWriteSet,
|
|
1073
|
+
loopWriteSetAhead,
|
|
499
1074
|
};
|
|
500
1075
|
}
|
|
501
1076
|
|
|
@@ -504,8 +1079,8 @@ export function makeLoopHazards(deps: LoopHazardDeps): LoopHazards {
|
|
|
504
1079
|
* `pred` after proving no value in `name`'s class has a definition able to run before that edge.
|
|
505
1080
|
* It reads each such definition's home off `paramBlock`/`opBlock`, and a SUNK pre-update exit copy
|
|
506
1081
|
* 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
|
|
508
|
-
*
|
|
1082
|
+
* model homes it after the loop, while `preUpdateCopies` really writes it INSIDE the body, on
|
|
1083
|
+
* every iteration, ahead of the update and of any edge the header dominates.
|
|
509
1084
|
*
|
|
510
1085
|
* Nothing today puts the two together — a merge inside the body that adopted the exit param's name
|
|
511
1086
|
* is a block param `definedInBody` sees, so `dest-free-inside-loop` refuses the sink before it
|