@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.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -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 lands
12
- // at the top of the body and spells the arg's def-tree again there. Two things have to hold — the
13
- // tree gives the same answer there, and every name it reads still denotes the same value — and the
14
- // arg gates in `PREUPDATE_SINK_GATES` are those two plus the degenerate leaf that is neither.
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 `REEVAL_UNSAFE_OPS`
17
- // is the registry view that names both. ORDER: the copy opens the body, so a load in the tree would
18
- // move ahead of every store the body makes, and an effect would move against the others.
19
- // SPECULATION: the def dominates the latch, and the loop is single-latch at both call sites, but an
20
- // early-`return` arm still lets an iteration leave BEFORE the latch — so a tree the top of the body
21
- // evaluates is one that iteration never evaluated, and a trapping divide would fault where the
22
- // original returned.
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
- // One gate covers both because `REEVAL_UNSAFE_OPS` answers both, and the candidate carries no arm
25
- // information to tell them apart. KNOWN COST: a loop with NO early-return arm speculates nothing,
26
- // so a divide in its exit-arg tree is refused for a hazard it cannot have. Threading "this loop can
27
- // leave before its latch" into the candidate is what would recover it; no benchmark row asks yet.
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 at the top of the body its name holds exactly
31
- // the value the edge read. A name the body itself defines does NOT — at the top of the body it
32
- // still holds the previous iteration.
33
+ // 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 { NEGATED_ICMP, REEVAL_UNSAFE_OPS } from '../ir/opcodes';
47
- import { Stmt } from '../l3/ast';
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
- ): Set<number>;
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 at the top of the loop body — see the note above
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 at the top of the body */
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
- guardedBy: 'hazards.test.ts: ablating arg-safe-to-reevaluate admits an exit arg that reads memory',
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 holds the PREVIOUS iteration at the top of it, where the copy lands',
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 at the top of the body clobbers',
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; read from the header PARAM instead it
315
- // arrives here, with no exit slot to sink. Both spellings, and the agbcc listing, are in
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; the caller emits them at the top of the body and drops
367
- // them from the post-loop copies.
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`; and a value defined outside the loop but read on the exit edge is live-in at the
384
- // header (analysis.ts counts a successor arg as a use at the predecessor's end), which makes the
385
- // other slot fail `dest-free-inside-loop`.
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
- ): Set<number> => {
395
- const none = new Set<number>();
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
- const busyInLoop = (name: string, self: Value): boolean => {
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 false;
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 at the top of the body. Walks the def-tree
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
- const blockersOf = (a: Value): ReadonlySet<ArgBlocker> => {
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: at the top of the body its name holds exactly this
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
- if (REEVAL_UNSAFE_OPS.has(d.opcode) || respelledDefs.has(d)) {
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 dest = new Map<number, string>();
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
- destBusyInLoop: destName !== undefined && busyInLoop(destName, exit.params[j]),
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
- dest.set(j, destName!);
1052
+ cleared.set(j, { name: destName!, home });
483
1053
  }
484
1054
  });
485
- const names = new Set(dest.values());
486
- if (dest.size === 0 || names.size !== dest.size) {
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) => !dest.has(j) && readsClobbered(a, sub, names)) ? none : new Set(dest.keys());
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 at the top of the body,
508
- * on every iteration, ahead of any edge inside that body.
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