@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
@@ -0,0 +1,345 @@
1
+ // A NARROWING EXTENSION FUSED WITH THE LEFT SHIFT THAT SCALES IT.
2
+ //
3
+ // agbcc lowers `(u8)x` to a shift pair, `lsl #24; lsr #24` (the CAST_PATTERNS idiom,
4
+ // pattern/engine.ts). Scale the narrowed value and gcc's combiner merges the right half of the pair
5
+ // with the scale, so `(u8)x << 3` is TWO instructions, not three:
6
+ //
7
+ // lsl r0, r0, #0x18 %1 = shl %x {imm=24}
8
+ // lsr r0, r0, #0x15 %2 = shr_u %1 {imm=21} == zext8(x) << 3
9
+ //
10
+ // Exactly: `(x << L) >>u R` for `0 < R < L` keeps x's low `32 - L` bits and lands them at bit
11
+ // `L - R`, which is `zext(x, 32 - L) << (L - R)`; `>>s` is the same with `sext`. What the fold buys
12
+ // is the two facts the raw pair hides from every pass below — and ONLY those: the spelling it
13
+ // prints when neither is claimed is not byte-neutral, so that spelling never ships (WHAT NOBODY
14
+ // CLAIMED, below):
15
+ //
16
+ // • THE WIDTH. A declared `u8 idx` is extended in the prologue, and that extension is what
17
+ // raise/paramwidth.ts reads to type the parameter. When the extension is fused, the prologue
18
+ // holds a bare `shl` instead: paramwidth sees no extension of this parameter, AND its prologue
19
+ // scan stops at that `shl`, so every LATER parameter's extension reads as body code too. Typed
20
+ // `u32`, the function recompiles with both shifts at the use and the whole schedule moves.
21
+ // • THE SCALE. `shl(ext(x), k)` is the element-scaled index raise/arrays.ts and
22
+ // raise/struct-arrays.ts legalize; `shr_u(shl(x, 24), 21)` is an opaque byte offset to both.
23
+ //
24
+ // WHERE THE EXTENSION GOES is the one real decision here, and it is read off the machine, not
25
+ // chosen. The two halves of the pair are not scheduled together. Compiled with this benchmark's
26
+ // agbcc, the same body over a narrow and a wide parameter:
27
+ //
28
+ // void pa(u8 a, u8 b) { gA |= 4; gB = (u32)&gT + a * 4; gC = b; }
29
+ // lsl r0,#24 / lsl r1,#24 / lsr r1,#24 / …body… / lsr r0,#22 / add …
30
+ // void pb(u32 a, u8 b) { gA |= 4; gB = (u32)&gT + (u8)a * 4; gC = b; }
31
+ // lsl r1,#24 / lsr r1,#24 / …body… / lsl r0,#24 / lsr r0,#22 / add …
32
+ //
33
+ // The declaration's extension starts in the prologue and only its SECOND half is merged into the
34
+ // use; a cast in the body is lowered at the use, both halves together. So the extension is placed
35
+ // where its `shl` stood and the scale where the right shift stood: a prologue `lsl` yields a
36
+ // prologue extension, which paramwidth's own gates then judge. Those gates keep a body cast wide
37
+ // only when body code that READS a value comes first — `not-prologue` steps over materializations,
38
+ // a pool-loaded address among them — which is why the fold records a pair it finds behind one
39
+ // (THE BODY CAST BEHIND A POOL LOAD, below). Placing both at the right shift instead throws `pa`'s
40
+ // prologue evidence away, so `pa` reads as `pb` and stays wide — measured on the benchmark's sa3
41
+ // rows whose first parameter is a declared `u8` scaled this way:
42
+ // `sa2__sub_8007858` 39/60 anchored against 44/61 at the right shift, and `sa2__sub_8007958` 64/87
43
+ // against 66/88.
44
+ //
45
+ // The SIGNED form does not carry that evidence on agbcc: `void pd(s16 a, …) { … a * 2 … }` and its
46
+ // wide twin `(s16)a * 2` compile to the SAME object, both halves at the use — the ambiguity of
47
+ // paramwidth's `pc` pair — so the placement rule is the same either way, and where the pair sits the
48
+ // judgement is paramwidth's.
49
+ //
50
+ // REFUSES — the pair is left as it is — when:
51
+ // • `R >= L`. `R == L` is the plain cast, which CAST_PATTERNS already folded; `R > L` keeps
52
+ // `32 - R` bits of `x << L` and scales nothing — a bitfield EXTRACT, structure/bitfields.ts's
53
+ // shape — so the two readings are disjoint by the shift amounts alone;
54
+ // • `32 - L` is not a C cast width (8 or 16) — `lsl #28; lsr #26` is `(x & 15) << 2`, which no
55
+ // cast spells;
56
+ // • either shift is the two-register form (no `imm`) or `R` is 0;
57
+ // • the target is not one CAST_PATTERNS applies to. Same predicate, because the fused pair is
58
+ // that cast's lowering with a scale merged in: where `(u8)x` is not a shift pair (IDO and GCC
59
+ // `andi`), `(u8)x << k` is not one either, and a `sll; srl` there is some other arithmetic.
60
+ //
61
+ // • another narrowing of the same value, at the same width and with the same SIGN, sits in the
62
+ // pair's block — a second right shift of a `shl(x, L)`, fused or plain, or the extension
63
+ // CAST_PATTERNS made of a plain one. agbcc extends a value once per block per signedness:
64
+ // `p[(u8)a * 4] = 1; return (u8)a;` is one `lsl #24; lsr #24` and a `lsl #4` at the use, and no
65
+ // fused pair. So a same-sign sibling means the source wrote the shift itself —
66
+ //
67
+ // u32 r6(u32 a, u32 *p) { u32 t = a << 24; p[t >> 22] = 1; return t >> 24; }
68
+ // lsl r0,#24 / lsr r2,r0,#20 / … / lsr r0,r0,#24
69
+ //
70
+ // which folded, struct-arrays spelled `a1[(u8)a0].field_0`: objdiff 3 where the raw pair is
71
+ // byte-exact. Opposite signs share only the `lsl` — `p[(u8)a] = (s8)a` is `lsl #24; lsr #22 …
72
+ // asr #24` — and both of those ARE casts, so they fold. The test is per BLOCK because the
73
+ // sharing is: the same two casts in two blocks lower twice, each with its own `lsl`
74
+ // (`if (c) p[1] = (u8)a; return p[(u8)a];`), and the fused one folds; and a `shl` shared
75
+ // ACROSS blocks is a declaration too — sa3's `sub_8010184` indexes `s->hitboxes[hbIndex]`
76
+ // twice over an `s16 hbIndex`: one `lsl #16`, read by an `asr #13` in each of two blocks. The
77
+ // idiom patterns turn a plain pair into its extension in place, so the test reads the same
78
+ // before them as after, and raise/globalshape.ts, which reads the lift, agrees with the fold
79
+ // about every pair.
80
+ //
81
+ // THE BODY CAST BEHIND A POOL LOAD folds, and the fold records it: the pair's `shl` reads an entry
82
+ // parameter and the machine ran it after a pool-loaded address. Compiled with this benchmark's
83
+ // agbcc, a body cast whose only predecessors are pool loads puts nothing between them and the
84
+ // extension that paramwidth's scan stops at, so the extension reads as a declaration:
85
+ //
86
+ // void pc1(u32 a) { gB = (u32)&gT + (u8)a * 4; } ldr r2,=gB / lsl r0,#24 / lsr r0,#22
87
+ //
88
+ // narrowed to `u8 a0` (objdiff 2) where the raw pair is MATCH. An UNSIGNED declared parameter's
89
+ // `lsl` comes before any pool load, symbol or numeric: all 44 over the benchmark's agbcc
90
+ // references. A signed one need not (27 before, 2 after): the signed form carries no placement
91
+ // evidence (above), and its narrow and wide spellings compile to the same object. So only the
92
+ // WIDTH is refused, by raise/paramwidth.ts's `fused-behind-pool` reading the record; the SCALE stays
93
+ // for the array passes: `gB = gW[(u8)a]` is objdiff 2 as the raw pair and byte-exact as
94
+ // `gW[(u8)a0]`, and sa3's `DemoPlayAlloc` (`gDemoRecordings[demoIndex]` over an `s16`) is spelled
95
+ // as the subscript it is.
96
+ // The order is read off the LIFTED function (`poolOrderOf`, which says why). NOT caught: a NUMERIC
97
+ // pool word lifts to `const`, the same op a `movs` does, and paramwidth's own header says why a
98
+ // `movs` ahead of the pair decides nothing — so a body cast behind only a numeric pool load
99
+ // (`& 0xfff` over a wide parameter in a loop) still narrows.
100
+ //
101
+ // One extension per (`shl`, signedness): two folded scales of one sign off one `shl` — in two
102
+ // blocks, `sub_8010184`'s shape — share it, and opposite signs get one each. The `shl` dies when
103
+ // every reader was folded; one that keeps a raw reader (`p[t >> 22] = 1; return t;`) stays beside
104
+ // the extension.
105
+ //
106
+ // WHAT NOBODY CLAIMED goes back (`restoreUnclaimedScales`, the last pre-recovery pass). An
107
+ // extension no width pass took and a scale no array pass took print as `(u8)a1 << 12`, and that is
108
+ // NOT the same object as the lifted `a1 << 24 >> 12`: straight-line they compile alike (the `pb`
109
+ // pair above), but kleod's `SetWorldMapTilePalette` — two `u8` parameters, the first used in agbcc's
110
+ // shifted domain so paramwidth's scan stops at its `shl` and refuses the second's extension — scores
111
+ // 54/93 with the raw pair and 59/91 with the cast, the loop's r4/r6 allocation swapped. So every
112
+ // scale the fold made that still reads its extension is rewritten in place to the pair it replaced;
113
+ // what survives the pass is exactly what a consumer claimed, and everywhere else the output is the
114
+ // one the lift alone produces.
115
+ //
116
+ // That is a decision about the DEFAULT, not a finding that the raw spelling is better: the unclaimed cast
117
+ // has both signs. Over the 55 corpus functions the fold fires on, scored on the benchmark's own
118
+ // path, restoring moves four scores — `SetWorldMapTilePalette` 59/91 → 54/93 and sa3
119
+ // `UnpackSaveSector` 347 → 346 better, sa3 `ClearSave` 189 → 191 and `CompleteSave` 185 → 187
120
+ // worse, all four to what the lift alone scores (and sa3 `ValidateSave`'s denominator, 213/378 →
121
+ // 213/379). A spelling with both signs is the differ's to referee (a ranked variation), and none is
122
+ // built; the default is the one that asserts nothing the lift did not.
123
+ import { type Block, type Fn, type Op, type Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
124
+ import { CAST_WIDTHS } from '../ir/opcodes';
125
+ import { CAST_PATTERNS, patternApplies } from '../pattern/engine';
126
+ import type { TargetDescription } from '../target';
127
+
128
+ /** `shr_{u,s}(shl(src, L), R)` read as `ext(src, 32 - L) << (L - R)`. */
129
+ export interface ScaledExtension {
130
+ /** the value being narrowed — the `shl`'s operand */
131
+ src: Value;
132
+ /** the cast width, `32 - L` */
133
+ width: number;
134
+ /** `shr_s` (sign-extend) rather than `shr_u` */
135
+ signed: boolean;
136
+ /** the scale's shift amount, `L - R` */
137
+ shift: number;
138
+ /** the `shl` — where the extension begins */
139
+ inner: Op;
140
+ }
141
+
142
+ /** The fused pair's SHAPE rooted at `op`, or null — the header's refusals on the amounts and the
143
+ * two-register form. The sibling refusal is {@link foldablePairs}', the target's the caller's. */
144
+ export function scaledExtensionOf(op: Op | undefined, defs: Map<Value, Op>): ScaledExtension | null {
145
+ if (!op || (op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
146
+ return null;
147
+ }
148
+ const inner = defs.get(op.operands[0]);
149
+ const r = op.attrs.imm;
150
+ const l = inner?.attrs.imm;
151
+ if (inner?.opcode !== 'shl' || inner.operands.length !== 1 || typeof r !== 'number' || typeof l !== 'number') {
152
+ return null;
153
+ }
154
+ if (!CAST_WIDTHS.has(32 - l) || r <= 0 || r >= l) {
155
+ return null;
156
+ }
157
+ return { src: inner.operands[0], width: 32 - l, signed: op.opcode === 'shr_s', shift: l - r, inner };
158
+ }
159
+
160
+ /** Does `target` lower BOTH narrowing casts to a shift pair? The conjunction is what the two
161
+ * consumers need: a fused pair carries either signedness and this fold re-splits it without
162
+ * knowing which. It is a stricter question than either cast pattern's own gate, because
163
+ * `pattern/engine.ts` measures the halves separately — MIPS spells `(s8)x` as a shift pair and
164
+ * `(u8)x` as `andi`, so every MIPS compiler answers `false` here while folding the signed half. */
165
+ export const foldsShiftPairCasts = (target: TargetDescription): boolean =>
166
+ CAST_PATTERNS.every((p) => patternApplies(p, target));
167
+
168
+ /** Where the MACHINE put each entry-block op relative to its first pool-loaded address, read off a
169
+ * function whose entry block is still in lifted order. See THE BODY CAST BEHIND A POOL LOAD. */
170
+ export interface PoolOrder {
171
+ /** the function's parameters */
172
+ entryParams: ReadonlySet<Value>;
173
+ /** the entry block's ops that come after a `gaddr` */
174
+ afterPoolLoad: ReadonlySet<Op>;
175
+ }
176
+
177
+ /** Read {@link PoolOrder} off `fn`. Valid only BEFORE raise/gvn.ts's `addrnum` runs: that pass
178
+ * hoists a duplicated address to the head of the entry block, after which a `gaddr`'s position no
179
+ * longer says where the machine loaded it. The idiom patterns before it leave both a `gaddr` and a
180
+ * fused pair's `shl` where they were — a plain cast's pair is folded at its right half, which is
181
+ * why this fact is read for the FUSED form only. */
182
+ export function poolOrderOf(fn: Fn): PoolOrder {
183
+ const entry = fn.blocks[0];
184
+ const afterPoolLoad = new Set<Op>();
185
+ let seen = false;
186
+ for (const op of entry?.ops ?? []) {
187
+ if (seen) {
188
+ afterPoolLoad.add(op);
189
+ }
190
+ seen ||= op.opcode === 'gaddr';
191
+ }
192
+ return { entryParams: new Set(entry?.params ?? []), afterPoolLoad };
193
+ }
194
+
195
+ /** Is `o` a narrowing of `m.src` at `m.width` with `m`'s signedness — a right shift of a
196
+ * `shl(src, 32 - width)`, fused or plain, or the extension CAST_PATTERNS made of a plain one? The
197
+ * answer is the same before the idiom patterns and after them: they turn a plain pair into its
198
+ * extension at the pair's own position. */
199
+ function narrowsLike(o: Op, m: ScaledExtension, defs: Map<Value, Op>): boolean {
200
+ if (o.opcode === (m.signed ? 'sext' : 'zext')) {
201
+ return o.operands[0] === m.src && o.attrs.width === m.width;
202
+ }
203
+ if (o.opcode !== (m.signed ? 'shr_s' : 'shr_u') || o.operands.length !== 1) {
204
+ return false;
205
+ }
206
+ const l = 32 - m.width;
207
+ const r = o.attrs.imm;
208
+ const d = defs.get(o.operands[0]);
209
+ return (
210
+ d?.opcode === 'shl' &&
211
+ d.operands.length === 1 &&
212
+ d.operands[0] === m.src &&
213
+ d.attrs.imm === l &&
214
+ typeof r === 'number' &&
215
+ r > 0 &&
216
+ r <= l
217
+ );
218
+ }
219
+
220
+ /** Every fused pair in `fn` AS THE FOLD TAKES IT, keyed by its right shift: the shape, minus a pair
221
+ * with a same-sign sibling in its block. THE ONE READING — the fold and raise/globalshape.ts's
222
+ * stride reader both ask here, and both before any pair is rewritten; a reader that took a pair the
223
+ * fold leaves raw would license an element scale that no pass below legalizes. The TARGET half of
224
+ * the gate is the caller's: the pass list gates the fold, and globalshape asks
225
+ * {@link foldsShiftPairCasts} before it reads. */
226
+ export function foldablePairs(fn: Fn, defs: Map<Value, Op> = defOpMap(fn)): Map<Op, ScaledExtension> {
227
+ const out = new Map<Op, ScaledExtension>();
228
+ for (const b of fn.blocks) {
229
+ for (const op of b.ops) {
230
+ const m = scaledExtensionOf(op, defs);
231
+ if (m !== null && !b.ops.some((o) => o !== op && narrowsLike(o, m, defs))) {
232
+ out.set(op, m);
233
+ }
234
+ }
235
+ }
236
+ return out;
237
+ }
238
+
239
+ /** What the fold made, for the two passes that read it later: raise/paramwidth.ts's
240
+ * `fused-behind-pool` gate and {@link restoreUnclaimedScales}. One per pre-recovery run
241
+ * (`PreRecoveryFacts.scales`), keyed by op identity. */
242
+ export interface ScaleRecord {
243
+ /** each scale the fold made → the extension it reads and the pair it replaced */
244
+ folded: Map<Op, { ext: Op; l: number; r: number }>;
245
+ /** the extensions over an entry parameter whose `shl` the machine ran behind a pool load */
246
+ behindPool: Set<Op>;
247
+ }
248
+
249
+ export const emptyScaleRecord = (): ScaleRecord => ({ folded: new Map(), behindPool: new Set() });
250
+
251
+ /** Rewrite every fused pair to `shl(ext(src), shift)`, the extension spliced in at the `shl`'s
252
+ * position and the scale at the right shift's, and write what it made into `record`. Returns the
253
+ * number of pairs folded; the `shl`s left without a reader are the pass driver's DCE. `order` is
254
+ * the lifted-order fact the driver read before `addrnum` (`PreRecoveryFacts.poolOrder`); the
255
+ * default reads it off `fn` itself, which is right only while `fn`'s entry block is still in lifted
256
+ * order — a test's parsed IR. */
257
+ export function foldScaledExtensions(
258
+ fn: Fn,
259
+ order: PoolOrder = poolOrderOf(fn),
260
+ record: ScaleRecord = emptyScaleRecord(),
261
+ ): number {
262
+ const defs = defOpMap(fn);
263
+ const blockOf = new Map<Op, Block>();
264
+ for (const b of fn.blocks) {
265
+ for (const op of b.ops) {
266
+ blockOf.set(op, b);
267
+ }
268
+ }
269
+ const exts = new Map<Op, Map<boolean, Op>>();
270
+ let folded = 0;
271
+ for (const [op, m] of foldablePairs(fn, defs)) {
272
+ const bySign = exts.get(m.inner) ?? exts.set(m.inner, new Map()).get(m.inner)!;
273
+ let ext = bySign.get(m.signed);
274
+ if (ext === undefined) {
275
+ ext = mkOp(m.signed ? 'sext' : 'zext', {
276
+ operands: [m.src],
277
+ results: [mkValue(m.inner.results[0].type)],
278
+ attrs: { width: m.width },
279
+ });
280
+ const home = blockOf.get(m.inner)!;
281
+ home.ops.splice(home.ops.indexOf(m.inner), 0, ext);
282
+ if (order.entryParams.has(m.src) && order.afterPoolLoad.has(m.inner)) {
283
+ record.behindPool.add(ext);
284
+ }
285
+ bySign.set(m.signed, ext);
286
+ }
287
+ const scaled = mkOp('shl', {
288
+ operands: [ext.results[0]],
289
+ results: [mkValue(op.results[0].type)],
290
+ attrs: { imm: m.shift },
291
+ });
292
+ const b = blockOf.get(op)!;
293
+ b.ops.splice(b.ops.indexOf(op), 1, scaled);
294
+ replaceAllUsesWith(fn, op.results[0], scaled.results[0]);
295
+ record.folded.set(scaled, { ext, l: 32 - m.width, r: 32 - m.width - m.shift });
296
+ folded++;
297
+ }
298
+ return folded;
299
+ }
300
+
301
+ /** Put back the machine's own pair wherever no pass below claimed what the fold exposed. Returns
302
+ * the number of scales restored; an extension left without a reader is the driver's DCE.
303
+ *
304
+ * A scale is CLAIMED when it no longer reads the fold's extension — raise/paramwidth.ts or
305
+ * raise/narrowlocal.ts retyped the value and dropped the extension, so the scale now reads that
306
+ * value — or when it is gone, legalized into an element index by raise/arrays.ts or
307
+ * raise/struct-arrays.ts. Anything else still reads the extension, and is rewritten in place (same
308
+ * result value, same position) to `shr(shl(src, L), R)`, with the `shl` where the extension stood:
309
+ * the ops the frontend lifted, in the order it lifted them. */
310
+ export function restoreUnclaimedScales(fn: Fn, record: ScaleRecord): number {
311
+ const shls = new Map<Op, Op>();
312
+ let restored = 0;
313
+ for (const b of fn.blocks) {
314
+ for (const op of [...b.ops]) {
315
+ const f = record.folded.get(op);
316
+ if (f === undefined || op.opcode !== 'shl' || op.operands[0] !== f.ext.results[0]) {
317
+ continue;
318
+ }
319
+ // The pair is rebuilt from the amounts RECORDED at the fold, so they must still be the ones
320
+ // the two ops carry: a pass that re-scaled the `shl` or re-widened the extension in place
321
+ // would make the recorded pair compute a different value. None does today; refuse, not guess.
322
+ const extended = f.ext.opcode === 'zext' || f.ext.opcode === 'sext';
323
+ if (!extended || f.ext.attrs.width !== 32 - f.l || op.attrs.imm !== f.l - f.r) {
324
+ continue;
325
+ }
326
+ let shl = shls.get(f.ext);
327
+ if (shl === undefined) {
328
+ const home = fn.blocks.find((x) => x.ops.includes(f.ext))!;
329
+ shl = mkOp('shl', {
330
+ operands: [f.ext.operands[0]],
331
+ results: [mkValue(f.ext.results[0].type)],
332
+ attrs: { imm: f.l },
333
+ });
334
+ home.ops.splice(home.ops.indexOf(f.ext), 0, shl);
335
+ shls.set(f.ext, shl);
336
+ }
337
+ op.opcode = f.ext.opcode === 'sext' ? 'shr_s' : 'shr_u';
338
+ op.operands = [shl.results[0]];
339
+ op.attrs = { imm: f.r };
340
+ record.folded.delete(op);
341
+ restored++;
342
+ }
343
+ }
344
+ return restored;
345
+ }
@@ -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
  );