@asmlift/core 0.6.0 → 0.7.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 (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
@@ -164,10 +164,12 @@ import type { SFn } from '../l3/ast';
164
164
  import { type Gate, firstRejection } from '../l3/gates';
165
165
  import type { SymbolInfo } from '../symbols';
166
166
  import type { TargetDescription } from '../target';
167
+ import { type ScaledExtension, foldablePairs, foldsShiftPairCasts } from './extscale';
167
168
 
168
169
  /** One additive term of an address residual: `v` scaled by `scale`, or a pure constant.
169
- * `scaleOp` is the op that DID the scaling (a `shl`/`mul`), which is what carries the position
170
- * the order licence reads; a term at scale 1 has none. */
170
+ * `scaleOp` is the op that DID the scaling (a `shl`/`mul`, or the right shift of a fused cast —
171
+ * see `scaleOf`), which is what carries the position the order licence reads; a term at scale 1
172
+ * has none. */
171
173
  interface Term {
172
174
  scale: number;
173
175
  /** null ⇒ a constant term, whose value is `konst` */
@@ -618,8 +620,18 @@ function useIndex(fn: Fn): Map<Value, Op[]> {
618
620
  }
619
621
 
620
622
  /** `x * K` / `x << k` read as a scale, or scale 1 for anything else. A CONSTANT operand makes the
621
- * whole term constant instead (`const << 2` is a displacement, not a subscript). */
622
- function scaleOf(v: Value, defs: Map<Value, Op>): Term {
623
+ * whole term constant instead (`const << 2` is a displacement, not a subscript).
624
+ *
625
+ * …and a narrowing cast FUSED with its scale, `shr(shl(x, 24), 21)` — `(u8)x << 3` after agbcc's
626
+ * combiner merged the pair's right half into the scale. Its RIGHT shift is the scaling op:
627
+ * compiled, `gTbl[i]` over a `u8 i` is `lsl` / `ldr` / `lsr` and `((u16 *)gTbl)[i]` is `lsl` /
628
+ * `lsr` / `ldr`, so the right half is where the order fork shows. A constant under the pair keeps
629
+ * the scale-1 reading — `const` folds that pair to its value before anything spells it.
630
+ *
631
+ * `fused` is raise/extscale.ts's own `foldablePairs`, or null where the target keeps the fold off,
632
+ * so a pair is read as a scale exactly when the fold will take it: a scale read anywhere else would
633
+ * license an element no pass legalizes. */
634
+ function scaleOf(v: Value, defs: Map<Value, Op>, fused: Map<Op, ScaledExtension> | null): Term {
623
635
  const d = defs.get(v);
624
636
  const constOf = (x: Value): number | null => {
625
637
  const dx = defs.get(x);
@@ -638,6 +650,10 @@ function scaleOf(v: Value, defs: Map<Value, Op>): Term {
638
650
  ? { scale: 1 << k, v: d.operands[0], konst: 0, scaleOp: d }
639
651
  : { scale: 1, v, konst: 0, scaleOp: null };
640
652
  }
653
+ const pair = d === undefined ? undefined : fused?.get(d);
654
+ if (d !== undefined && pair !== undefined && constOf(pair.src) === null) {
655
+ return { scale: 1 << pair.shift, v: pair.src, konst: 0, scaleOp: d };
656
+ }
641
657
  if (d?.opcode === 'mul') {
642
658
  for (const [a, b] of [
643
659
  [d.operands[0], d.operands[1]],
@@ -655,7 +671,7 @@ function scaleOf(v: Value, defs: Map<Value, Op>): Term {
655
671
  /** The additive terms of a byte residual. Only `add` is opened: a `sub` at the top of the tree
656
672
  * makes a term's sign depend on the walk, and a NEGATIVE stride is not an array subscript this
657
673
  * spelling can express, so it refuses rather than dropping the sign. */
658
- function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
674
+ function residualTerms(root: Value, defs: Map<Value, Op>, fused: Map<Op, ScaledExtension> | null): Term[] | null {
659
675
  const out: Term[] = [];
660
676
  const walk = (v: Value, depth: number): boolean => {
661
677
  if (depth > 16) {
@@ -668,7 +684,7 @@ function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
668
684
  if (d?.opcode === 'add') {
669
685
  return walk(d.operands[0], depth + 1) && walk(d.operands[1], depth + 1);
670
686
  }
671
- out.push(scaleOf(v, defs));
687
+ out.push(scaleOf(v, defs, fused));
672
688
  return true;
673
689
  };
674
690
  return walk(root, 0) ? out : null;
@@ -691,19 +707,23 @@ function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
691
707
  * say in the type. */
692
708
  function accessesBySymbol(
693
709
  fn: Fn,
710
+ target: TargetDescription,
694
711
  gates: readonly Gate<AddressUse>[],
695
712
  ): Map<string, ElementAccess[] | { refusedBy: string }>;
696
713
  function accessesBySymbol(
697
714
  fn: Fn,
715
+ target: TargetDescription,
698
716
  gates: readonly Gate<AddressUse>[],
699
717
  interiorIsEvidence: true,
700
718
  ): Map<string, Access[] | { refusedBy: string }>;
701
719
  function accessesBySymbol(
702
720
  fn: Fn,
721
+ target: TargetDescription,
703
722
  gates: readonly Gate<AddressUse>[],
704
723
  interiorIsEvidence = false,
705
724
  ): Map<string, Access[] | { refusedBy: string }> {
706
725
  const defs = defOpMap(fn);
726
+ const fused = foldsShiftPairCasts(target) ? foldablePairs(fn, defs) : null;
707
727
  const uses = useIndex(fn);
708
728
  const out = new Map<string, Access[] | { refusedBy: string }>();
709
729
  const refuse = (sym: string, id: string): void => void out.set(sym, { refusedBy: id });
@@ -728,7 +748,7 @@ function accessesBySymbol(
728
748
  for (const u of gUses) {
729
749
  const isAdd = u.opcode === 'add';
730
750
  const other = isAdd ? (u.operands[0] === base ? u.operands[1] : u.operands[0]) : undefined;
731
- const terms = other === undefined ? null : residualTerms(other, defs);
751
+ const terms = other === undefined ? null : residualTerms(other, defs, fused);
732
752
  const consumers = (isAdd ? (uses.get(u.results[0]) ?? []) : []).map((m) => {
733
753
  const isLoad = m.opcode === 'load' && m.operands[0] === u.results[0];
734
754
  const isStore = m.opcode === 'store' && m.operands[0] === u.results[0];
@@ -903,8 +923,8 @@ function extentsOf(strides: number[]): number[] | null {
903
923
  * same addresses whichever one a consumer picked up.
904
924
  *
905
925
  * This exists because the SPELLING and the DECLARATION are decided from different derivations —
906
- * rank.ts derives the declaration dictionary once off the probe's lift and the spelling per
907
- * symbol variant off that variant's own — and a candidate that spells from one and declares from
926
+ * rank.ts derives the declaration dictionary once off the shared lift and the spelling per
927
+ * lift off that lift's own — and a candidate that spells from one and declares from
908
928
  * the other addresses a different object than the assembly did, compiling either way. */
909
929
  export function sameDerivedShape(a: SymbolInfo | undefined, b: SymbolInfo | undefined): boolean {
910
930
  if (a === undefined || b === undefined) {
@@ -934,7 +954,7 @@ export function inferGlobalArrays(
934
954
  return out;
935
955
  }
936
956
  const pos = positions(fn);
937
- for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
957
+ for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
938
958
  const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
939
959
  if (si !== null) {
940
960
  out.set(sym, si);
@@ -968,7 +988,7 @@ export function orderLicensedGlobals(
968
988
  return out;
969
989
  }
970
990
  const pos = positions(fn);
971
- for (const [sym, accs] of accessesBySymbol(fn, gates.address, true)) {
991
+ for (const [sym, accs] of accessesBySymbol(fn, target, gates.address, true)) {
972
992
  if (Array.isArray(accs) && firstRejection(gates.shape, evidenceOf(accs, pos)) === null) {
973
993
  out.add(sym);
974
994
  }
@@ -991,7 +1011,7 @@ export function arrayShapeRefusals(
991
1011
  return out;
992
1012
  }
993
1013
  const pos = positions(fn);
994
- for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
1014
+ for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
995
1015
  out.set(sym, Array.isArray(accs) ? firstRejection(gates.shape, evidenceOf(accs, pos)) : accs.refusedBy);
996
1016
  }
997
1017
  return out;
package/src/raise/gvn.ts CHANGED
@@ -64,9 +64,9 @@
64
64
  //
65
65
  // Several modules answer "is this address a local?" with independent policies — here: never;
66
66
  // basecse: at whichever of the two positions `l3/hoist.ts` is handed (the COMMITTED call states
67
- // the function top, its roster admissions also offer each init's first use), when the gate table
67
+ // the function top, its roster's hoists also offer each init's first use), when the gate table
68
68
  // admits the base; l3/scopebase.ts: at the innermost scope holding the uses; l3/argbase.ts:
69
- // immediately before a call whose arguments share it. The newer placement levers — l3/nearbase.ts,
69
+ // immediately before a call whose arguments share it. The newer placement variations — l3/nearbase.ts,
70
70
  // l3/inlinebase.ts, l3/homesplit.ts — answer it too, each with its own.
71
71
  // Reconciling them is recorded debt, and the same test pins the two places they actively disagree, because a
72
72
  // consolidation has to PICK rather than discover them: a `for`'s init (basecse reads it at loop
@@ -56,7 +56,7 @@ function magicS(d: number): { M: number; s: number } {
56
56
  }
57
57
 
58
58
  /** Forward UNSIGNED magic generator (Hacker's Delight §10-8). Returns the multiplier `M`, shift `s`,
59
- * and the `add` indicator (true ⇒ the "add-correction" variant that needs an extra `+x` term —
59
+ * and the `add` indicator (true ⇒ the "add-correction" form that needs an extra `+x` term —
60
60
  * matched by matchUnsignedAddCorrection, not the simple `mulhu>>s` shape). */
61
61
  function magicU(d: number): { M: number; s: number; add: boolean } {
62
62
  const u = (x: number) => x >>> 0;
@@ -312,7 +312,7 @@ function matchSignedMagic(ctx: Ctx, mul: Op): Match | null {
312
312
  }
313
313
 
314
314
  /** Match the SIMPLE unsigned magic-division DAG `shr_u(mulhu(x, M), s)` (no correction) and
315
- * reconstruct+verify the divisor. The add-correction variant (`t + ((x−t)>>1)`) is not matched
315
+ * reconstruct+verify the divisor. The add-correction form (`t + ((x−t)>>1)`) is not matched
316
316
  * here — its `mulhu` result feeds a `sub`, not a direct `shr_u`, so this matcher naturally declines it. */
317
317
  function matchUnsignedSimpleMagic(ctx: Ctx, mul: Op): Match | null {
318
318
  const bound = bindMulOperands(ctx, mul);
@@ -32,16 +32,16 @@
32
32
  // produces no site here. `synthetic:basefold` is that control — its reference IS the cast spelling
33
33
  // — and it MATCHES today, untouched.
34
34
  //
35
- // A DEFAULT, NOT A RANKED AXIS, and the reason is a measurement rather than a preference. An axis
35
+ // A DEFAULT, NOT A RANKED VARIATION, and the reason is a measurement rather than a preference. A variation
36
36
  // exists where two source spellings collapse onto one asm, so nothing but the differ can separate
37
37
  // them. Here the asm separates them itself — line one folds `+K` into the memory operand and
38
38
  // produces NO site — so `add(P, K)` feeding an off-0 scaled access is evidence AGAINST the spelling
39
- // asmlift emits today, not a coin flip. And an axis at this level is a LIFT variant (`/setup-args`'s
39
+ // asmlift emits today, not a coin flip. And a variation at this level is a LIFT variation (`/setup-args`'s
40
40
  // position in rank.ts), doubling the whole candidate product for every function with a site: swept
41
41
  // over 2288 sa3 and 412 klonoa functions in both symbol-map configurations, the gates below admit
42
42
  // two bases, in one function, which declines for an unrelated reason — so the price would be paid
43
43
  // to referee a question the corpus never poses. What is NOT settled is line two, and that is a
44
- // different question: where a base LOCAL goes is the L3 base-local levers' business, taken over
44
+ // different question: where a base LOCAL goes is the L3 base-local variations' business, taken over
45
45
  // whatever type the recovery mints, not a second answer to the TYPE this pass decides.
46
46
  //
47
47
  // WHY THE TRAILING COUNT IS A GATE RATHER THAN A GUESS, and it is the struct's SIZE that makes it
@@ -52,7 +52,7 @@
52
52
  // only once the object is big enough. So a count is not a fact to derive but a free parameter with
53
53
  // a byte-observable LOWER BOUND, and inventing one invents a size. The count is therefore read off
54
54
  // the loop that walks the member — `boundedCount` below — and the whole base DECLINES when no
55
- // counted loop states one. An INTERIOR member's count is not a choice at all: it is forced by the
55
+ // counted loop states one. An INTERIOR member's count is not an open question at all: it is forced by the
56
56
  // member that follows it.
57
57
  //
58
58
  // THE SIBLING PASSES, and why this is a third one rather than a case inside either.
@@ -364,8 +364,24 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
364
364
  * is its jump has none.
365
365
  *
366
366
  * Over-refusal here is free by construction: it returns the carrier to the wide-local-plus-cast
367
- * spelling this pass emits without the clause. */
368
- function armIsOneSet(b: Block): boolean {
367
+ * spelling this pass emits without the clause.
368
+ *
369
+ * A SECOND READER, AND WHY THE PREDICATE IS SHARED RATHER THAN RE-DERIVED. `raise/retsink.ts`'s
370
+ * `arms-are-one-set` asks this same question of the same optimizer for a different purpose: a
371
+ * merge-variable select whose arms this guard would have collapsed never comes back as a diamond,
372
+ * so a TARGET holding one was written with early returns. Compiled both ways with agbcc -O2
373
+ * -mthumb and committed (`test/corpus/agbcc-select-{merge,early}.s`), the three bullets above hold
374
+ * term for term in that direction too: `selcomp` (`v = a + b`, one SET) loses its diamond in the
375
+ * merge spelling and keeps it in the early one; `selload` (`v = *p`) keeps a diamond in BOTH,
376
+ * carrying no information; `selcomp3` (three ops) likewise. ONE DIFFERENCE IS WORTH NAMING: the
377
+ * constant bullet's "refusing the foldable case costs nothing" is an argument about THIS pass's
378
+ * fallback, and it does not transfer — `selk3` (`v = a + 3`) is one `adds`, so `jump.c`'s own guard
379
+ * counts one SET where this predicate counts two, and retsink's refusal of it costs a candidate
380
+ * there rather than nothing. That divergence is argued from the optimizer rather than compiled:
381
+ * `selk3` is deliberately outside the committed pair. It still never costs an ANSWER (every
382
+ * clause in that table is `sound: false`), so the shared conservative predicate is the right one
383
+ * until a row asks for the cost model neither pass has. */
384
+ export function armIsOneSet(b: Block): boolean {
369
385
  return (
370
386
  !b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
371
387
  );
@@ -43,6 +43,15 @@
43
43
  // What that refusal protects is a function this pass never compiles: agbcc truncates at every
44
44
  // PROTOTYPED CALL SITE of a narrow-declared callee — `lsl/asr` ahead of the `bl`, two Thumb
45
45
  // instructions per site — so a wrong width here costs bytes the per-function differ cannot see.
46
+ //
47
+ // FUSED BEHIND A POOL LOAD. The prologue scan steps over a pool-loaded address, and a body cast
48
+ // with nothing else ahead of it — `gB = gW[(u8)a]` is `ldr r2,=gB / ldr r1,=gW / lsl r0,#24 /
49
+ // lsr r0,#22` — reaches it looking like a declaration once raise/extscale.ts has re-split the fused
50
+ // pair. The fold knows where the machine put that `lsl` and records it (`ScaleRecord.behindPool`),
51
+ // and `fused-behind-pool` reads the record: an unsigned declared parameter's `lsl` precedes every
52
+ // pool load in the benchmark's agbcc references (extscale.ts's header has the census). Only the
53
+ // fold's own extensions are judged this way; a plain cast's extension sits at its right shift,
54
+ // where its position says nothing about its `lsl`'s.
46
55
  import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
47
56
  import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
48
57
  import { T } from '../ir/types';
@@ -63,6 +72,9 @@ export interface NarrowParamCandidate {
63
72
  uses: number;
64
73
  /** the width the caller's own prototype declares for this parameter, if it declares one */
65
74
  declared: number | undefined;
75
+ /** the extension is one raise/extscale.ts re-split from a fused pair whose `shl` the machine ran
76
+ * behind a pool load — see FUSED BEHIND A POOL LOAD */
77
+ fusedBehindPool: boolean;
66
78
  }
67
79
 
68
80
  export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
@@ -108,6 +120,13 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
108
120
  guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
109
121
  rejects: (c) => !c.inPrologue,
110
122
  },
123
+ {
124
+ id: 'fused-behind-pool',
125
+ why: 'a fused cast behind a pool load is body code if unsigned, and either width is the same object if signed',
126
+ sound: true,
127
+ guardedBy: 'extscale.test.ts: a body cast behind nothing but a pool load keeps its parameter wide',
128
+ rejects: (c) => c.fusedBehindPool,
129
+ },
111
130
  ];
112
131
 
113
132
  /** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
@@ -125,12 +144,14 @@ function useCount(fn: Fn, v: Value): number {
125
144
  }
126
145
 
127
146
  /** 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. */
147
+ * `self` is the prototype the caller supplied for THIS function, if any; `fusedBehindPool` is
148
+ * raise/extscale.ts's record of the extensions it re-split behind a pool load
149
+ * (`ScaleRecord.behindPool`). Returns the number of parameters narrowed. */
130
150
  export function narrowEntryParams(
131
151
  fn: Fn,
132
152
  self?: FnProto,
133
153
  gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
154
+ fusedBehindPool: ReadonlySet<Op> = new Set(),
134
155
  ): number {
135
156
  const entry = fn.blocks[0];
136
157
  const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
@@ -166,6 +187,7 @@ export function narrowEntryParams(
166
187
  inPrologue: prologue.has(op),
167
188
  uses: useCount(fn, p),
168
189
  declared: declared[entry.params.indexOf(p)],
190
+ fusedBehindPool: fusedBehindPool.has(op),
169
191
  };
170
192
  if (firstRejection(gates, c) !== null) {
171
193
  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,44 @@ 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
+ {
205
+ id: 'paramwidth',
206
+ run: (fn, self, _opts, _target, lifted) => narrowEntryParams(fn, self, PARAM_WIDTH_GATES, lifted.scales.behindPool),
207
+ dce: false,
208
+ },
209
+ // LAST, after every pass that can CLAIM what `extscale` exposed — the two width passes above take
210
+ // an extension, the array recognizers a scale. What none of them took goes back to the pair the
211
+ // frontend lifted (raise/extscale.ts, WHAT NOBODY CLAIMED). `dce: true` — the extension it leaves
212
+ // readerless.
213
+ {
214
+ id: 'extscale-restore',
215
+ run: (fn, _self, _opts, _target, lifted) => restoreUnclaimedScales(fn, lifted.scales),
216
+ dce: true,
217
+ gate: foldsShiftPairCasts,
218
+ },
165
219
  ];
166
220
 
167
221
  /** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
168
222
  * `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
169
- * hook), if given. */
223
+ * hook), if given.
224
+ *
225
+ * RETURNS the facts it read, because a pass AFTER pre-recovery needs one of them too:
226
+ * `raise/retsink.ts`'s `pre-diamond` asks whether a return merge was a diamond in the ROM, and
227
+ * by its turn `raise/shortcircuit.ts` has manufactured diamonds that were not. The map is the
228
+ * driver's to compute — it is the only place that sees `fn` before the first pass — so handing it
229
+ * back is cheaper and truer than recomputing something later that cannot be recomputed. */
170
230
  export function runPreRecovery(
171
231
  fn: Fn,
172
232
  target: TargetDescription,
173
233
  afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
174
234
  self?: FnProto,
175
235
  opts: PreRecoveryOptions = {},
176
- ): void {
177
- const lifted: PreRecoveryFacts = { mergeShapes: mergeShapes(fn) };
236
+ ): PreRecoveryFacts {
237
+ const lifted: PreRecoveryFacts = {
238
+ mergeShapes: mergeShapes(fn),
239
+ poolOrder: poolOrderOf(fn),
240
+ scales: emptyScaleRecord(),
241
+ };
178
242
  for (const pass of PRE_RECOVERY_PASSES) {
179
243
  if (pass.gate && !pass.gate(target)) {
180
244
  continue;
@@ -187,4 +251,5 @@ export function runPreRecovery(
187
251
  afterPass?.(pass, result);
188
252
  }
189
253
  }
254
+ return lifted;
190
255
  }