@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -21,6 +21,76 @@
21
21
  // narrow parameter with the `extsb`/`extsh` the frontend lifts to the same op, and the synthetic
22
22
  // `sextb`/`tos8` rows keep matching on that toolchain through this pass.
23
23
  //
24
+ // BUT THE PROLOGUE TEST IS NOT UNIVERSAL EVIDENCE, AND ON MIPS IT IS NO EVIDENCE AT ALL. Everything
25
+ // above reads the extension's POSITION, which works only where the compiler puts a declaration's
26
+ // extension somewhere a body cast's never goes. IDO 7.1 at -O2 does not: it leads the function with
27
+ // the `sll` for BOTH spellings. Two OTHER facts separate them there, and it takes both — each one
28
+ // alone is refuted by a compiled counterexample, at the row's own flags:
29
+ //
30
+ // home store widened in place
31
+ // int f(s8 x){ return x; } sw a0 sll a0,a0,0x18 the declaration
32
+ // int f(s32 x){ return (s8)x; } — sll v0,a0,0x18 a body cast
33
+ // int f(int x){ x = (signed char)x; return x; } — sll a0,a0,0x18 NOT homed
34
+ // int f(long long x){ return (signed char)x; } sw a1 sll v0,a1,0x18 NOT in place
35
+ //
36
+ // The last two lines are why the conjunction is the claim. A DEAD ABI ARGUMENT HOME STORE on its
37
+ // own is not "declared narrow" — IDO emits one wherever the incoming register value goes unused,
38
+ // whatever that value's type:
39
+ //
40
+ // int unused1(int x){ return 7; } sw a0,0(sp) / jr ra / li v0,7
41
+ // int ptr1(int *p, int y){ return y; } sw a0,0(sp) / jr ra / move v0,a1
42
+ // int ll2i(long long x){ return (int)x; } sw a0,0(sp) / sw a1,4(sp) / jr ra / move v0,a1
43
+ // int used2(int x, int y){ return x+y; } jr ra / addu v0,a0,a1 (no store at all)
44
+ //
45
+ // — so the store says the incoming register was not consumed, not that anything was declared
46
+ // narrow, and on the `long long` line that is a 64-bit parameter whose low half the extension would
47
+ // narrow to `s8`; `widened-elsewhere` is what refuses it. WIDENING IN PLACE on its own is not
48
+ // "declared narrow" either: the third line is a plain `int` the source re-assigns, and only the
49
+ // absent home store tells it from the first.
50
+ //
51
+ // The conjunction is NECESSARY-and-measured, not necessary-and-sufficient, and it errs toward
52
+ // refusing: `int m2(signed char a){ int i,s=0; for(i=0;i<10;i++) s+=arr[i]+a; return s; }` is a
53
+ // narrow declaration IDO widens into a SCRATCH register under register pressure (`sll a1,a0,0x18`),
54
+ // so this pass leaves its parameter wide and re-spells the cast. That is a worse-reading answer,
55
+ // not a wrong one.
56
+ //
57
+ // THE PAIR SAYS A NARROW DECLARATION EXISTS, NOT WHICH ONE. The WIDTH comes from the extension,
58
+ // and where a wider mask sits on that extension's result the declaration could have been the wider
59
+ // type — `int f(u16 x){ x = (signed char)x; return x; }` and `int f(s8 x){ return x & 0xffff; }`
60
+ // are ONE object at the row's own flags (`sw a0,0(sp) / sll a0,a0,0x18 / sra a0,a0,0x18 / andi
61
+ // v0,a0,0xffff`), so the asm decides nothing between them and this pass answers `s8`. Refusing on
62
+ // that disagreement is not the repair, because the spelling a refusal falls back to is a DIFFERENT
63
+ // object: `int f(int a){ return ((a << 24) >> 24) & 0xffff; }` is four words with no home store and
64
+ // both shifts in `v0`, so the refusal would cost the byte match under BOTH readings. Where the
65
+ // caller declares the parameter `proto-width` takes the tiebreak; where nobody does, the two
66
+ // readings recompile alike and only a PROTOTYPED CALL SITE of this function could tell them apart.
67
+ //
68
+ // Ungated, this pass narrows the body cast too and loses `synthetic:tos8:ido7.1`, a MATCH. The two
69
+ // MIPS GCCs are a third case again: their two spellings are one BYTE-IDENTICAL object, so nothing
70
+ // in the asm decides the width and the honest answer is to leave the extension standing.
71
+ //
72
+ // So which fact settles the width is a per-COMPILER question, asked as
73
+ // `compilerBehaviors.narrowParamWitness` and answered by `no-declaration-witness`, `unhomed-param`
74
+ // and `widened-elsewhere` below. Both facts it reads are destroyed at lift and survive as the
75
+ // frontend's `Fn.paramEvidence` (ir/core.ts).
76
+ //
77
+ // `not-prologue` STILL FIRES ON SUCH A TARGET, and it costs rather than protects there. IDO's
78
+ // scheduler interleaves the widenings of several narrow parameters with the arithmetic that
79
+ // consumes the earlier ones, and the scan below stops at the first value-reading op, so only the
80
+ // LEADING parameters are seen as prologue:
81
+ //
82
+ // int d4(s8 a, s8 b, s8 c){ return a+b+c; } sll a1 / sll a0 / sll a2 / sra a0 / sra a1 /
83
+ // addu t6,a0,a1 / sra a2 / addu v0,t6,a2
84
+ // recovered s32 d4(s8 a0, s8 a1, s32 a2) { return a0 + a1 + (s8)a2; }
85
+ // int m3(s16 a, s16 b, s16 c, s16 d){ return a*b+c*d; } all four widenings precede the first
86
+ // recovered s32 m3(s16 a0, s16 a1, s16 a2, s16 a3) multiply — all four narrow
87
+ //
88
+ // So the limit is the SCHEDULE's, not the declaration's, and the gate answers in the refusing
89
+ // direction on the parameters it cuts off. Kept because it is the sound direction and because it is
90
+ // the only gate that keeps a body cast behind real body code from being read as a prologue widening
91
+ // on the position-witness targets; NO row on either tier is known to turn on the IDO half of it —
92
+ // `d4` above is a probe, not a row, and lifting the scan is a change with its own measurement to make.
93
+ //
24
94
  // WHAT THE PROLOGUE TEST CANNOT SEE, and why the declaration settles it. The scan steps over the
25
95
  // pure materializations agbcc interleaves among the extensions, so a constant the scheduler HOISTED
26
96
  // above a mid-body cast leaves `pb` looking like `pa`:
@@ -43,11 +113,21 @@
43
113
  // What that refusal protects is a function this pass never compiles: agbcc truncates at every
44
114
  // PROTOTYPED CALL SITE of a narrow-declared callee — `lsl/asr` ahead of the `bl`, two Thumb
45
115
  // instructions per site — so a wrong width here costs bytes the per-function differ cannot see.
116
+ //
117
+ // FUSED BEHIND A POOL LOAD. The prologue scan steps over a pool-loaded address, and a body cast
118
+ // with nothing else ahead of it — `gB = gW[(u8)a]` is `ldr r2,=gB / ldr r1,=gW / lsl r0,#24 /
119
+ // lsr r0,#22` — reaches it looking like a declaration once raise/extscale.ts has re-split the fused
120
+ // pair. The fold knows where the machine put that `lsl` and records it (`ScaleRecord.behindPool`),
121
+ // and `fused-behind-pool` reads the record: an unsigned declared parameter's `lsl` precedes every
122
+ // pool load in the benchmark's agbcc references (extscale.ts's header has the census). Only the
123
+ // fold's own extensions are judged this way; a plain cast's extension sits at its right shift,
124
+ // where its position says nothing about its `lsl`'s.
46
125
  import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
47
126
  import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
48
127
  import { T } from '../ir/types';
49
128
  import { type Gate, firstRejection } from '../l3/gates';
50
129
  import { type FnProto, declaredWidth } from '../proto';
130
+ import type { NarrowParamWitness } from '../target';
51
131
 
52
132
  /** What the gates below judge: one entry parameter and the extension that reads it. */
53
133
  export interface NarrowParamCandidate {
@@ -63,6 +143,15 @@ export interface NarrowParamCandidate {
63
143
  uses: number;
64
144
  /** the width the caller's own prototype declares for this parameter, if it declares one */
65
145
  declared: number | undefined;
146
+ /** the extension is one raise/extscale.ts re-split from a fused pair whose `shl` the machine ran
147
+ * behind a pool load — see FUSED BEHIND A POOL LOAD */
148
+ fusedBehindPool: boolean;
149
+ /** what this compiler's object shows for a narrow declaration (target.ts `narrowParamWitness`) */
150
+ witness: NarrowParamWitness;
151
+ /** the machine stored this parameter to a stack slot nothing reads back (`ParamObservation`) */
152
+ homed: boolean;
153
+ /** the machine put the parameter's own widened value back in its argument register (`ParamObservation`) */
154
+ selfRedefined: boolean;
66
155
  }
67
156
 
68
157
  export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
@@ -103,11 +192,41 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
103
192
  },
104
193
  {
105
194
  id: 'not-prologue',
106
- why: 'an extension behind body code is where the SOURCE wrote the cast',
195
+ why: 'an extension behind body code is where the SOURCE wrote the cast — and on a schedule that interleaves them, the refusing answer',
107
196
  sound: true,
108
197
  guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
109
198
  rejects: (c) => !c.inPrologue,
110
199
  },
200
+ {
201
+ id: 'no-declaration-witness',
202
+ why: 'this compiler spells a narrow declaration and a body cast the same way, so the asm decides nothing',
203
+ sound: true,
204
+ guardedBy:
205
+ 'param-width.test.ts: a compiler whose two spellings are one object refuses the narrowing, measured or not',
206
+ rejects: (c) => c.witness === 'none',
207
+ },
208
+ {
209
+ id: 'unhomed-param',
210
+ why: 'this compiler homes a narrow DECLARED parameter, so the absent home store proves the declaration was wide',
211
+ sound: true,
212
+ guardedBy: 'param-width.test.ts: a homing compiler refuses the parameter it did not home',
213
+ rejects: (c) => c.witness === 'home-store-and-in-place' && !c.homed,
214
+ },
215
+ {
216
+ id: 'widened-elsewhere',
217
+ why: 'this compiler widens a narrow DECLARED parameter in the argument register itself, so a widening that lands in a scratch register is not a declaration',
218
+ sound: true,
219
+ guardedBy:
220
+ 'param-width.test.ts: \u2026and refuses one it homed but widened SOMEWHERE ELSE \u2014 that is a 64-bit half',
221
+ rejects: (c) => c.witness === 'home-store-and-in-place' && !c.selfRedefined,
222
+ },
223
+ {
224
+ id: 'fused-behind-pool',
225
+ why: 'a fused cast behind a pool load is body code if unsigned, and either width is the same object if signed',
226
+ sound: true,
227
+ guardedBy: 'extscale.test.ts: a body cast behind nothing but a pool load keeps its parameter wide',
228
+ rejects: (c) => c.fusedBehindPool,
229
+ },
111
230
  ];
112
231
 
113
232
  /** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
@@ -125,13 +244,20 @@ function useCount(fn: Fn, v: Value): number {
125
244
  }
126
245
 
127
246
  /** Type an entry parameter at the width its prologue extension proves, and drop the extension.
128
- * `self` is the prototype the caller supplied for THIS function, if any. Returns the number of
129
- * parameters narrowed. */
247
+ * `witness` is what this compiler's object shows for a narrow declaration (target.ts
248
+ * `narrowParamWitness`); `self` is the prototype the caller supplied for THIS function, if any;
249
+ * `fusedBehindPool` is raise/extscale.ts's record of the extensions it re-split behind a pool load
250
+ * (`ScaleRecord.behindPool`). Returns the number of parameters narrowed. */
130
251
  export function narrowEntryParams(
131
252
  fn: Fn,
253
+ witness: NarrowParamWitness,
132
254
  self?: FnProto,
133
255
  gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
256
+ fusedBehindPool: ReadonlySet<Op> = new Set(),
134
257
  ): number {
258
+ // ABSENT ⇒ NEITHER OBSERVATION, which is the refusing direction on a target that reads them: a
259
+ // function nobody measured (parsed IR, a hand-built fn) is never narrowed on evidence never taken.
260
+ const evidence = fn.paramEvidence;
135
261
  const entry = fn.blocks[0];
136
262
  const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
137
263
  const entryIsJoin = fn.blocks.some((b) => successorsOf(b).includes(entry));
@@ -166,6 +292,10 @@ export function narrowEntryParams(
166
292
  inPrologue: prologue.has(op),
167
293
  uses: useCount(fn, p),
168
294
  declared: declared[entry.params.indexOf(p)],
295
+ fusedBehindPool: fusedBehindPool.has(op),
296
+ witness,
297
+ homed: evidence?.get(p)?.deadHome ?? false,
298
+ selfRedefined: evidence?.get(p)?.selfRedefined ?? false,
169
299
  };
170
300
  if (firstRejection(gates, c) !== null) {
171
301
  continue;
@@ -18,12 +18,21 @@ import type { TargetDescription } from '../target';
18
18
  import { recognizeArrays } from './arrays';
19
19
  import { recognizeConsts } from './const';
20
20
  import { recognizeDivPow2 } from './divpow2';
21
+ import {
22
+ type PoolOrder,
23
+ type ScaleRecord,
24
+ emptyScaleRecord,
25
+ foldScaledExtensions,
26
+ foldsShiftPairCasts,
27
+ poolOrderOf,
28
+ restoreUnclaimedScales,
29
+ } from './extscale';
21
30
  import { numberPureValues } from './gvn';
22
31
  import { recognizeMagicDivision } from './magicdiv';
23
32
  import { recognizeMemberArrays } from './memberarrays';
24
33
  import { rerootNarrowReads } from './narrow';
25
34
  import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
26
- import { narrowEntryParams } from './paramwidth';
35
+ import { PARAM_WIDTH_GATES, narrowEntryParams } from './paramwidth';
27
36
  import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
28
37
  import { recognizeSoftDiv } from './softdiv';
29
38
  import { recognizeStructArrays } from './struct-arrays';
@@ -34,7 +43,7 @@ import { recognizeStructs } from './structs';
34
43
  * A field here selects between spellings its pass can already produce; none of them relaxes a
35
44
  * soundness refusal, and each recognizer documents its own. */
36
45
  export interface PreRecoveryOptions {
37
- /** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree axis. */
46
+ /** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree question. */
38
47
  shortCircuit?: BranchShortCircuitOptions;
39
48
  }
40
49
 
@@ -44,9 +53,18 @@ export interface PreRecoveryOptions {
44
53
  * by then divpow2 has deleted a block, both short-circuit folds have rewritten edges, and the
45
54
  * eight `dce: true` passes have changed op counts. So the driver reads it here and hands it down. */
46
55
  export interface PreRecoveryFacts {
47
- /** the join shape of every block, for raise/narrowlocal.ts's `edge-extends`. Blocks a later pass
48
- * creates are absent, and absent reads as "no diamond" the refusing direction. */
56
+ /** the join shape of every block, for raise/narrowlocal.ts's `edge-extends` and, downstream of
57
+ * pre-recovery entirely, raise/retsink.ts's `pre-diamond`. Blocks a later pass creates are
58
+ * absent, and absent reads as "no diamond" — the refusing direction, in both readers. */
49
59
  mergeShapes: Map<Block, MergeShape>;
60
+ /** which entry-block ops the machine ran after its first pool-loaded address, for
61
+ * raise/extscale.ts's behind-a-pool-load record. Read HERE because `addrnum`, the first pass,
62
+ * hoists duplicated addresses to the head of the entry block and that order is gone after it. */
63
+ poolOrder: PoolOrder;
64
+ /** THE ONE FACT A PASS WRITES rather than the lift: what raise/extscale.ts's fold made, for the
65
+ * two passes after it that read it — paramwidth's `fused-behind-pool` gate and
66
+ * `extscale-restore`. Empty until the fold runs. */
67
+ scales: ScaleRecord;
50
68
  }
51
69
 
52
70
  export interface PreRecoveryPass {
@@ -55,7 +73,9 @@ export interface PreRecoveryPass {
55
73
  /** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR.
56
74
  * `self` is the prototype the caller supplied for the function being raised — read only by
57
75
  * parameter-width, which checks its inference against a declared width. `lifted` is the
58
- * pre-pass CFG snapshot — read only by narrow-local. */
76
+ * pre-pass snapshot — read by narrow-local (its CFG) and scaled-extension (its entry order) —
77
+ * plus the fold's record, which scaled-extension writes and parameter-width and
78
+ * scaled-extension-restore read. */
59
79
  run: (
60
80
  fn: Fn,
61
81
  self: FnProto | undefined,
@@ -70,9 +90,10 @@ export interface PreRecoveryPass {
70
90
  }
71
91
 
72
92
  /** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
73
- * address-numbering → const-materialize → magic-division → pow2-division → soft-division → array-legalize →
74
- * struct-arraymember-array → struct-pointershort-circuitbranch-short-circuitnarrow-reads
75
- * narrow-local → parameter-width. See each recognizer's file for the rationale. */
93
+ * address-numbering → const-materialize → magic-division → pow2-division → soft-division →
94
+ * scaled-extensionarray-legalize → struct-arraymember-arraystruct-pointershort-circuit
95
+ * branch-short-circuit → narrow-reads → narrow-local → parameter-width scaled-extension-restore.
96
+ * See each recognizer's file for the rationale. */
76
97
  export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
77
98
  // FIRST: collapsing duplicate address definitions removes block params every later recognizer
78
99
  // would otherwise have to reason around, and it can only shrink the value graph.
@@ -81,10 +102,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
81
102
  // Numbering alone is not enough and not safe to ship alone: collapsing the duplicates leaves a
82
103
  // block param whose edges now all carry one value, and the structurer still destroys THAT into
83
104
  // a local (it only reuses a name a carrier already has, and an inlined `gaddr` has none).
84
- // Numbering alone costs kleod:UpdateHUDCounterDisplay its match, so the pair is the atomic
85
- // unit, expressed as a body rather than a sum of two unrelated counts. It is NOT monotone,
86
- // which is worth knowing before tuning either half: dropping the cleanup IMPROVES
87
- // kleod:ConfigureEntityBehavior and kleod:CountCollectedGems, neither of them near matching.
105
+ // Numbering alone cost kleod:UpdateHUDCounterDisplay its match (measured on kleod's
106
+ // kl-eod-decomp rows, retired 2026-09-13), so the pair is the atomic unit, expressed as a body
107
+ // rather than a sum of two unrelated counts. It was NOT monotone, which is worth knowing before
108
+ // tuning either half: dropping the cleanup IMPROVED kleod:ConfigureEntityBehavior and
109
+ // kleod:CountCollectedGems, neither of them near matching then.
88
110
  run: (fn) => {
89
111
  const n = numberPureValues(fn);
90
112
  return n + simplifyTrivialPhis(fn);
@@ -100,6 +122,17 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
100
122
  // beside magicdiv so that a reader looking for division recovery finds both together.
101
123
  { id: 'divpow2', run: recognizeDivPow2, dce: true },
102
124
  { id: 'softdiv', run: (fn) => recognizeSoftDiv(fn), dce: false, gate: (t) => !t.capabilities.hwDivide },
125
+ // AFTER `const`, which folds a shift pair over a constant to the constant it computes, and BEFORE
126
+ // the three array recognizers, whose input this pass produces: `shl(ext(x), k)` is an element
127
+ // scale they legalize and the fused pair is not. `dce: true` — the `shl` a fold leaves readerless.
128
+ // It reads `lifted.poolOrder`, the entry block's order before `addrnum` hoisted its addresses,
129
+ // and writes `lifted.scales`.
130
+ {
131
+ id: 'extscale',
132
+ run: (fn, _self, _opts, _target, lifted) => foldScaledExtensions(fn, lifted.poolOrder, lifted.scales),
133
+ dce: true,
134
+ gate: foldsShiftPairCasts,
135
+ },
103
136
  { id: 'arrays', run: recognizeArrays, dce: true },
104
137
  // struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
105
138
  // discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
@@ -128,28 +161,35 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
128
161
  // counts 0 fused heads across the 782 rows that lift map-lessly, so the hazard has no producer
129
162
  // there. The reverse direction never could: the value form replaces its head's `cond_br` with a
130
163
  // `br`, which this pass never matches.
164
+ // The `target` argument is read by ONE conjunct of ONE gate, as at `narrowlocal` below:
165
+ // `read-behind-effect`'s "a local costs a second load", compiled on all five bench toolchains and
166
+ // true on agbcc alone (raise/shortcircuit.ts, target.ts `reloadsLocalReread`).
131
167
  {
132
168
  id: 'branch-shortcircuit',
133
- run: (fn, _self, opts) => recognizeBranchShortCircuit(fn, opts.shortCircuit),
169
+ run: (fn, _self, opts, target) =>
170
+ recognizeBranchShortCircuit(fn, {
171
+ ...opts.shortCircuit,
172
+ reloadsLocalReread: target.compilerBehaviors.reloadsLocalReread,
173
+ }),
134
174
  dce: true,
135
175
  },
136
176
  // LAST, and the position IS load-bearing: this pass reads the CFG's edge arguments to find a
137
177
  // loop variable's next value, and both short-circuit folds above rewrite the very edges it reads.
138
178
  // `dce: false` — the rewrite orphans nothing, since the operand it drops keeps its other use.
139
179
  { id: 'narrow', run: rerootNarrowReads, dce: false },
140
- // The two WIDTH passes, last and in either order relative to each other: each only DELETES an
141
- // extension and retypes the parameter that fed it, so every recognizer above sees the shape it
142
- // was written against and neither can match a shape the other creates. They are disjoint by
143
- // construction — `narrowlocal` refuses an entry parameter, `paramwidth` reads only entry
144
- // parameters — and `narrowlocal` cannot take an extension `narrow` above wants either, since a
145
- // parameter carrying BOTH a `zext` and a `sext` has two readers and is refused.
180
+ // The two WIDTH passes, last of the recognizers and in either order relative to each other: each
181
+ // only DELETES an extension and retypes the parameter that fed it, so every recognizer above sees
182
+ // the shape it was written against and neither can match a shape the other creates. They are
183
+ // disjoint by construction — `narrowlocal` refuses an entry parameter, `paramwidth` reads only
184
+ // entry parameters — and `narrowlocal` cannot take an extension `narrow` above wants either, since
185
+ // a parameter carrying BOTH a `zext` and a `sext` has two readers and is refused.
146
186
  // `dce: false` on both — the extension each drops is spliced out in place, and its result has no
147
187
  // other reader.
148
188
  // The `target` argument is read by ONE conjunct of ONE gate — see raise/narrowlocal.ts's
149
189
  // `NarrowLocalOptions`. A whole-pass `gate` would be wrong: the pass's SOUND rules are claims
150
190
  // about C and run everywhere; only the join-shape evidence is a claim about gcc 2.x's optimizer.
151
- // That same conjunct is why this is the one pass reading `lifted`: every pass above it can
152
- // rewrite the CFG whose shape it judges.
191
+ // That same conjunct is why it reads `lifted.mergeShapes`: every pass above it can rewrite the
192
+ // CFG whose shape it judges.
153
193
  {
154
194
  id: 'narrowlocal',
155
195
  run: (fn, _self, _opts, target, lifted) =>
@@ -161,20 +201,54 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
161
201
  ),
162
202
  dce: false,
163
203
  },
164
- { id: 'paramwidth', run: (fn, self) => narrowEntryParams(fn, self), dce: false },
204
+ // The `target` argument is read by TWO of this pass's gates, and unlike `narrowlocal`'s single
205
+ // conjunct it is not a tuning knob: `narrowParamWitness` says WHICH fact in the object settles a
206
+ // parameter's declared width on this compiler, and a target that names none refuses outright.
207
+ {
208
+ id: 'paramwidth',
209
+ run: (fn, self, _opts, target, lifted) =>
210
+ narrowEntryParams(
211
+ fn,
212
+ target.compilerBehaviors.narrowParamWitness ?? 'none',
213
+ self,
214
+ PARAM_WIDTH_GATES,
215
+ lifted.scales.behindPool,
216
+ ),
217
+ dce: false,
218
+ },
219
+ // LAST, after every pass that can CLAIM what `extscale` exposed — the two width passes above take
220
+ // an extension, the array recognizers a scale. What none of them took goes back to the pair the
221
+ // frontend lifted (raise/extscale.ts, WHAT NOBODY CLAIMED). `dce: true` — the extension it leaves
222
+ // readerless.
223
+ {
224
+ id: 'extscale-restore',
225
+ run: (fn, _self, _opts, _target, lifted) => restoreUnclaimedScales(fn, lifted.scales),
226
+ dce: true,
227
+ gate: foldsShiftPairCasts,
228
+ },
165
229
  ];
166
230
 
167
231
  /** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
168
232
  * `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
169
- * hook), if given. */
233
+ * hook), if given.
234
+ *
235
+ * RETURNS the facts it read, because a pass AFTER pre-recovery needs one of them too:
236
+ * `raise/retsink.ts`'s `pre-diamond` asks whether a return merge was a diamond in the ROM, and
237
+ * by its turn `raise/shortcircuit.ts` has manufactured diamonds that were not. The map is the
238
+ * driver's to compute — it is the only place that sees `fn` before the first pass — so handing it
239
+ * back is cheaper and truer than recomputing something later that cannot be recomputed. */
170
240
  export function runPreRecovery(
171
241
  fn: Fn,
172
242
  target: TargetDescription,
173
243
  afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
174
244
  self?: FnProto,
175
245
  opts: PreRecoveryOptions = {},
176
- ): void {
177
- const lifted: PreRecoveryFacts = { mergeShapes: mergeShapes(fn) };
246
+ ): PreRecoveryFacts {
247
+ const lifted: PreRecoveryFacts = {
248
+ mergeShapes: mergeShapes(fn),
249
+ poolOrder: poolOrderOf(fn),
250
+ scales: emptyScaleRecord(),
251
+ };
178
252
  for (const pass of PRE_RECOVERY_PASSES) {
179
253
  if (pass.gate && !pass.gate(target)) {
180
254
  continue;
@@ -187,4 +261,5 @@ export function runPreRecovery(
187
261
  afterPass?.(pass, result);
188
262
  }
189
263
  }
264
+ return lifted;
190
265
  }