@asmlift/core 0.8.1 → 0.10.0

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