@asmlift/core 0.9.0 → 0.11.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -6,8 +6,8 @@
6
6
  // Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
7
7
  // mutation of a live value.
8
8
  import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
9
- import { deletableWhenDead, effectful, orderSensitive } from '../ir/discipline';
10
- import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
9
+ import { deadOps, effectful, orderSensitive } from '../ir/discipline';
10
+ import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
11
11
  import type { IrType } from '../ir/types';
12
12
  import { T } from '../ir/types';
13
13
 
@@ -23,8 +23,11 @@ export type MatchNode =
23
23
  args: MatchNode[];
24
24
  }
25
25
  | { bind: string } // bind this operand's VALUE to a name
26
- | { same: string } // this operand must equal a previously-bound value
27
- | { constImm: string }; // this operand must be a `const`; bind its numeric VALUE to an imm name
26
+ // this operand must equal a previously-bound value — or, when both are `const`, have its value:
27
+ // two `li rN,10` are one operand to C, as they are to the compiler that materialised both
28
+ | { same: string }
29
+ | { constImm: string } // this operand must be a `const`; bind its numeric VALUE to an imm name
30
+ | { constEq: ImmExpr }; // this operand must be a `const` whose value is this, modulo 2^32
28
31
 
29
32
  // A tiny declarative arithmetic over bound immediate names. It keeps a *computed* replacement
30
33
  // attribute DATA rather than a JS closure, so a strength-reduced idiom whose replacement is a
@@ -61,7 +64,16 @@ function evalImm(e: ImmExpr, imms: Map<string, number>): number {
61
64
  // A replacement operand is either a previously-BOUND value (by name) or a SYNTHESIZED constant
62
65
  // computed from the bound immediates (e.g. the derived multiplier `2^k+1`). A synthesized const
63
66
  // is materialized as its own `const` op spliced in before the rewrite site.
64
- export type ReplaceArg = string | { constImm: ImmExpr };
67
+ // A NESTED replacement — an op of its own, spliced in before the root — lets an idiom fold to a
68
+ // two-op expression (`-(a < b)`) without a pattern per intermediate.
69
+ export type ReplaceArg = string | { constImm: ImmExpr } | Replacement;
70
+
71
+ export interface Replacement {
72
+ op: string;
73
+ args: ReplaceArg[];
74
+ attrs?: Record<string, number | ImmExpr>;
75
+ resultType?: IrType;
76
+ }
65
77
 
66
78
  export interface RewritePattern {
67
79
  id: string;
@@ -76,7 +88,7 @@ export interface RewritePattern {
76
88
  // a computed divisor proof and lives as the bespoke raise/magicdiv.ts pass instead. The
77
89
  // COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
78
90
  // multiply-by-constant idioms.
79
- replaceWith: { op: string; args: ReplaceArg[]; attrs?: Record<string, number | ImmExpr>; resultType?: IrType };
91
+ replaceWith: Replacement;
80
92
  /** Two REPLACEMENT operand names, in the order the replacement RENDERS them, whose defs C leaves
81
93
  * UNSEQUENCED against each other. A fold that collapses a multi-op idiom into one op drops its
82
94
  * operands from several uses to one, which lets the structurer inline both at that one use — and
@@ -93,12 +105,18 @@ export interface RewritePattern {
93
105
  * declaring pattern's compiler gate to that set; widening it fails loud. */
94
106
  unsequencedRightFirst?: [string, string];
95
107
  /** The compilers measured to RECOMPUTE this idiom instead of CSEing it back, when an interior op
96
- * of the match survives the fold because something outside still reads it. On one of these the
108
+ * of the match survives the fold because something outside still reads it (a read by an op `dce`
109
+ * will delete does not count, as for `foldsOnlyDyingInterior`). On one of these the
97
110
  * fold refuses that shape (`sharesInterior`, which carries the disassemblies); everywhere else
98
111
  * it is byte-neutral and refusing it costs matches, so the list is opt-in and per-compiler. A
99
112
  * compiler absent from it was either measured neutral or not measured — `validatePattern`
100
113
  * refuses a name that is not in `applies.compilers`, where the fold cannot fire anyway. */
101
114
  recomputesSharedInterior?: string[];
115
+ /** The fold fires only where its interior DIES with it, on every compiler: an interior op some
116
+ * live op outside the match still reads refuses it (a read by an op `dce` will delete does not
117
+ * count). For an idiom whose interior means something of its own to a later pass — a `bdnz`'s
118
+ * decremented counter is the loop's, not a subtraction to fold away. */
119
+ foldsOnlyDyingInterior?: true;
102
120
  }
103
121
 
104
122
  /** What the pattern layer reads off a target. STRUCTURAL on purpose: the pattern set is
@@ -145,6 +163,242 @@ export const CNTLZW_EQ0: RewritePattern = {
145
163
  replaceWith: { op: 'icmp_eq', args: ['X', { constImm: 0 }], resultType: T.u(32) },
146
164
  };
147
165
 
166
+ // ── mwcc's branch-free compares ─────────────────────────────────────────────────────────────
167
+ // CodeWarrior materialises a 0/1 compare with no branch and no set-on-compare instruction: either
168
+ // through the carry bit (the frontend reads each carry as the compare it is — frontend/ppc.ts
169
+ // `readCarry`), or with a bit trick on the operands. Each fold is an IDENTITY over every pair of
170
+ // 32-bit words (PPC `slw` by 32 is 0; mwcc-compare.test.ts and ppc-carry.test.ts evaluate both
171
+ // sides over the edge words).
172
+ // The compiled forms, at -O4,p (2.4.2 and 2.4.7 identical), a and b in r3 and r4:
173
+ //
174
+ // a <u b 2.4.x xor r0,b,a; cntlzw r0,r0; slw r0,b,r0; srwi r3,r0,31
175
+ // 2.3.3 subfc r3,b,a; subfe r0,r0,r0; neg r3,r0 -(CA - 1), CA = a >=u b
176
+ // a >=u b 2.4.x subf r0,b,a; orc r3,a,b; srwi r0,r0,1; subf r0,r0,r3; srwi r3,r0,31
177
+ // 2.3.3 subfc r0,b,a; li r0,-1; subfze r3,r0 CA itself
178
+ // a <s b 2.4.x xor r0,b,a; srawi r3,r0,1; and r0,r0,b; subf r0,r0,r3; srwi r3,r0,31
179
+ // 2.3.3 eqv r0,b,a; subfc r3,b,a; srwi r0,r0,31; addze r3,r0; clrlwi r3,r3,31
180
+ // a >=s b all srawi r6,a,31; srwi r5,b,31; subfc r0,b,a; adde r3,r6,r5
181
+ // -(a <u b) 2.4.x subfc r0,b,a; subfe r3,r0,r0 CA - 1
182
+ //
183
+ // `>` and `<=` are the same code with the operands swapped, and fold to `b < a` / `b >= a`. A
184
+ // constant operand is materialised once, into a register the trick reads twice — except in the
185
+ // `orc` form, which subtracts it as `addi rD,a,-K`, hence that form's second pattern. The signed
186
+ // and unsigned forms share no shape, so no fold reads one as the other.
187
+ const mwccOnly = { compilers: ['mwcc'] };
188
+ const C31 = { imm: 31 };
189
+
190
+ /** `shr_u(shl(b, clz(a ^ b)), 31)`: b's bit at the highest bit where a and b differ, set exactly
191
+ * when b is the larger. */
192
+ export const MWCC_CLZ_ULT: RewritePattern = {
193
+ id: 'mwcc-clz-ult',
194
+ applies: mwccOnly,
195
+ match: {
196
+ op: 'shr_u',
197
+ attrEquals: C31,
198
+ args: [
199
+ {
200
+ op: 'shl',
201
+ args: [{ bind: 'B' }, { op: 'clz', args: [{ op: 'xor', args: [{ bind: 'A' }, { same: 'B' }] }] }],
202
+ },
203
+ ],
204
+ },
205
+ replaceWith: { op: 'icmp_ult', args: ['A', 'B'], resultType: T.u(32) },
206
+ };
207
+
208
+ /** `shr_u((a | ~b) - ((a - b) >>u 1), 31)`, and its constant-b twin over `a + -K`. */
209
+ export const MWCC_ORC_UGE: RewritePattern[] = [
210
+ {
211
+ id: 'mwcc-orc-uge',
212
+ applies: mwccOnly,
213
+ match: {
214
+ op: 'shr_u',
215
+ attrEquals: C31,
216
+ args: [
217
+ {
218
+ op: 'sub',
219
+ args: [
220
+ { op: 'or', args: [{ bind: 'A' }, { op: 'not', args: [{ bind: 'B' }] }] },
221
+ { op: 'shr_u', attrEquals: { imm: 1 }, args: [{ op: 'sub', args: [{ same: 'A' }, { same: 'B' }] }] },
222
+ ],
223
+ },
224
+ ],
225
+ },
226
+ replaceWith: { op: 'icmp_uge', args: ['A', 'B'], resultType: T.u(32) },
227
+ },
228
+ {
229
+ id: 'mwcc-orc-uge-k',
230
+ applies: mwccOnly,
231
+ match: {
232
+ op: 'shr_u',
233
+ attrEquals: C31,
234
+ args: [
235
+ {
236
+ op: 'sub',
237
+ args: [
238
+ { op: 'or', args: [{ bind: 'A' }, { op: 'not', args: [{ constImm: 'k' }] }] },
239
+ {
240
+ op: 'shr_u',
241
+ attrEquals: { imm: 1 },
242
+ args: [{ op: 'add', args: [{ same: 'A' }, { constEq: { op: '-', args: [0, { imm: 'k' }] } }] }],
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ },
248
+ replaceWith: { op: 'icmp_uge', args: ['A', { constImm: { imm: 'k' } }], resultType: T.u(32) },
249
+ },
250
+ ];
251
+
252
+ /** `shr_u(((a ^ b) >>s 1) - ((a ^ b) & b), 31)`. The `and` names b, so the two xor operand orders
253
+ * are two patterns: a commutative match commits to the first order that binds. */
254
+ const xorSlt = (first: string, second: string): RewritePattern => ({
255
+ id: `mwcc-xor-slt/${first}${second}`,
256
+ applies: mwccOnly,
257
+ match: {
258
+ op: 'shr_u',
259
+ attrEquals: C31,
260
+ args: [
261
+ {
262
+ op: 'sub',
263
+ args: [
264
+ {
265
+ op: 'shr_s',
266
+ attrEquals: { imm: 1 },
267
+ args: [{ op: 'xor', ordered: true, args: [{ bind: first }, { bind: second }] }],
268
+ },
269
+ {
270
+ op: 'and',
271
+ args: [{ op: 'xor', ordered: true, args: [{ same: first }, { same: second }] }, { same: 'B' }],
272
+ },
273
+ ],
274
+ },
275
+ ],
276
+ },
277
+ replaceWith: { op: 'icmp_slt', args: ['A', 'B'], resultType: T.u(32) },
278
+ });
279
+ export const MWCC_XOR_SLT: RewritePattern[] = [xorSlt('A', 'B'), xorSlt('B', 'A')];
280
+
281
+ /** `(a >>s 31) + (b >>u 31) + (a >=u b)`, `adde` over the `subfc` carry: the sign words cancel
282
+ * when the signs agree, and decide the compare when they differ. */
283
+ export const MWCC_ADDE_SGE: RewritePattern = {
284
+ id: 'mwcc-adde-sge',
285
+ applies: mwccOnly,
286
+ match: {
287
+ op: 'add',
288
+ args: [
289
+ { op: 'icmp_uge', args: [{ bind: 'A' }, { bind: 'B' }] },
290
+ {
291
+ op: 'add',
292
+ args: [
293
+ { op: 'shr_s', attrEquals: C31, args: [{ same: 'A' }] },
294
+ { op: 'shr_u', attrEquals: C31, args: [{ same: 'B' }] },
295
+ ],
296
+ },
297
+ ],
298
+ },
299
+ replaceWith: { op: 'icmp_sge', args: ['A', 'B'], resultType: T.u(32) },
300
+ };
301
+
302
+ /** 2.3.3's `((~(a ^ b) >>u 31) + (a >=u b)) & 1`, `addze` over the `subfc` carry: one plus the
303
+ * carry when the signs agree, the carry alone when they differ. */
304
+ export const MWCC_EQV_SLT: RewritePattern = {
305
+ id: 'mwcc-eqv-slt',
306
+ applies: mwccOnly,
307
+ match: {
308
+ op: 'and',
309
+ args: [
310
+ {
311
+ op: 'add',
312
+ args: [
313
+ { op: 'icmp_uge', args: [{ bind: 'A' }, { bind: 'B' }] },
314
+ {
315
+ op: 'shr_u',
316
+ attrEquals: C31,
317
+ args: [{ op: 'not', args: [{ op: 'xor', args: [{ same: 'A' }, { same: 'B' }] }] }],
318
+ },
319
+ ],
320
+ },
321
+ { op: 'const', attrEquals: { value: 1 }, args: [] },
322
+ ],
323
+ },
324
+ replaceWith: { op: 'icmp_slt', args: ['A', 'B'], resultType: T.u(32) },
325
+ };
326
+
327
+ /** A carry less one (`subfe rD,rY,rY`): `cmp - 1` is `-(!cmp)`, and negated again (2.3.3's `neg`)
328
+ * `!cmp`. One of each per comparison of NEGATED_ICMP, since a carry is any of them (`addic -1`
329
+ * makes `!=`, `subfic 0` makes `==`); the negated form runs first, while the `- 1` is whole. */
330
+ const carryLessOne = (cmp: string): MatchNode => ({
331
+ op: 'add',
332
+ args: [
333
+ { op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
334
+ { op: 'const', attrEquals: { value: -1 }, args: [] },
335
+ ],
336
+ });
337
+ const carryNot = (cmp: string): RewritePattern => ({
338
+ id: `mwcc-carry-not/${cmp}`,
339
+ applies: mwccOnly,
340
+ match: { op: 'neg', args: [carryLessOne(cmp)] },
341
+ replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
342
+ });
343
+ const carryMask = (cmp: string): RewritePattern => ({
344
+ id: `mwcc-carry-mask/${cmp}`,
345
+ applies: mwccOnly,
346
+ match: carryLessOne(cmp),
347
+ replaceWith: { op: 'neg', args: [{ op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) }] },
348
+ });
349
+
350
+ /** `(b - a) == 0` / `!= 0` is `a == b` / `a != b`: the form 2.3.3's `!=` takes through the carry
351
+ * (`subf r3,a,b; addic r0,r3,-1; subfe r3,r0,r3`), and every build's `==` (`subf; cntlzw; srwi`),
352
+ * measured byte-exact in this operand order for both on 2.3.3 and for `==` on 2.4.2. Only where
353
+ * the difference dies: a `bdnz` tests its decremented counter this way, and a record-form `subf.`
354
+ * its result. */
355
+ const subZeroTest = (test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
356
+ id: `mwcc-sub-zerotest/${test}`,
357
+ applies: mwccOnly,
358
+ foldsOnlyDyingInterior: true,
359
+ match: {
360
+ op: test,
361
+ args: [
362
+ { op: 'sub', args: [{ bind: 'B' }, { bind: 'A' }] },
363
+ { op: 'const', attrEquals: { value: 0 }, args: [] },
364
+ ],
365
+ },
366
+ replaceWith: { op: test, args: ['A', 'B'], resultType: T.u(32) },
367
+ });
368
+
369
+ /** `-x == 0` / `-x != 0` is `x == 0` / `x != 0`: 2.3.3 materialises every `x != 0` as `neg; addic
370
+ * -1; subfe` (`a != 0`, `a > 0u`, `(a - b) != 0`), so the carry reads the negation. AFTER
371
+ * `subZeroTest`, which must not see the difference this exposes: 2.3.3's `a != b` is `subf; addic;
372
+ * subfe` with no `neg`, so `(a - b) != 0` keeps its subtraction. No dying gate: a `bdnz` counts down
373
+ * with a `sub`, never a `neg`. */
374
+ const negZeroTest = (test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
375
+ id: `mwcc-neg-zerotest/${test}`,
376
+ applies: mwccOnly,
377
+ match: {
378
+ op: test,
379
+ args: [
380
+ { op: 'neg', args: [{ bind: 'X' }] },
381
+ { op: 'const', attrEquals: { value: 0 }, args: [] },
382
+ ],
383
+ },
384
+ replaceWith: { op: test, args: ['X', { constImm: 0 }], resultType: T.u(32) },
385
+ });
386
+
387
+ /** The branch-free compare bundle. */
388
+ export const MWCC_COMPARE_PATTERNS: RewritePattern[] = [
389
+ MWCC_CLZ_ULT,
390
+ ...MWCC_ORC_UGE,
391
+ ...MWCC_XOR_SLT,
392
+ MWCC_ADDE_SGE,
393
+ MWCC_EQV_SLT,
394
+ ...Object.keys(NEGATED_ICMP).map(carryNot),
395
+ ...Object.keys(NEGATED_ICMP).map(carryMask),
396
+ subZeroTest('icmp_eq'),
397
+ subZeroTest('icmp_ne'),
398
+ negZeroTest('icmp_eq'),
399
+ negZeroTest('icmp_ne'),
400
+ ];
401
+
148
402
  // The PPC rotate mirror, as pure data: hardware only rotates LEFT, so mwcc spells `rotr(x, n)`
149
403
  // as `rotlw(x, 32 - n)`. Lowering that literally emits `x << (32 - n) | x >> (32 - (32 - n))`,
150
404
  // whose inner subtraction recompiles to an extra instruction; folding to `rotr(x, n)` lets the
@@ -359,11 +613,18 @@ export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16),
359
613
  // Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
360
614
  // with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
361
615
  //
362
- // The negation comes from THE shared table (ir/opcodes.ts NEGATED_ICMP), so this fold, the MIPS
363
- // `slt …; beqz` branch fold and the short-circuit diamond negation cannot disagree about what the
364
- // opposite of a compare is. One pattern per comparison — a data-driven fold needs a fixed
365
- // replacement opcode, so the table is unrolled into ten patterns rather than expressed as a
366
- // (nonexistent) computed-opcode replacement.
616
+ // The negation comes from THE shared tables (ir/opcodes.ts NEGATED_ICMP, and NEGATED_FCMP for a float
617
+ // compare, whose negation is the unordered twin: `!(a > b)` is true on a NaN, `a <= b` is not), so
618
+ // this fold, the MIPS `slt …; beqz` branch fold and the short-circuit diamond negation cannot
619
+ // disagree about what the opposite of a compare is. One pattern per comparison — a data-driven fold
620
+ // needs a fixed replacement opcode, so the tables are unrolled into one pattern per entry rather
621
+ // than expressed as a (nonexistent) computed-opcode replacement.
622
+ const NEGATED_CMP: Readonly<Record<string, Opcode>> = { ...NEGATED_ICMP, ...NEGATED_FCMP };
623
+ // An integer compare's result is pinned u32 (below). A float compare's is the untyped word both
624
+ // frontends give one (raise/recover.ts stamps no float compare), not the `xor`'s or the zero test's:
625
+ // mwcc's `!(a > b)` is a `cntlzw` test that CNTLZW_EQ0 has already typed u32, and a u32 result
626
+ // would declare an `int` function `u32`.
627
+ const cmpResultType = (cmp: string): IrType => (cmp in NEGATED_ICMP ? T.u(32) : T.unk(32));
367
628
  const notCmpPat = (cmp: string): RewritePattern => ({
368
629
  id: `not-${cmp}`,
369
630
  applies: {},
@@ -378,7 +639,7 @@ const notCmpPat = (cmp: string): RewritePattern => ({
378
639
  // It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
379
640
  // the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
380
641
  // patterns on one discipline instead of leaving a reader to infer which is canonical.
381
- replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
642
+ replaceWith: { op: NEGATED_CMP[cmp], args: ['A', 'B'], resultType: cmpResultType(cmp) },
382
643
  });
383
644
 
384
645
  // The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
@@ -401,15 +662,35 @@ const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern =>
401
662
  { op: 'const', attrEquals: { value: 0 }, args: [] },
402
663
  ],
403
664
  },
404
- replaceWith: { op: test === 'icmp_eq' ? NEGATED_ICMP[cmp] : cmp, args: ['A', 'B'], resultType: T.u(32) },
665
+ replaceWith: { op: test === 'icmp_eq' ? NEGATED_CMP[cmp] : cmp, args: ['A', 'B'], resultType: cmpResultType(cmp) },
405
666
  });
406
667
 
668
+ // KNOWN GAP: `!(a == b)` folds to `fcmp_une`, which C spells `a != b` — one program, which mwcc
669
+ // compiles apart (`cntlzw` of the EQ bit for the first, `xori` for the second), so it recompiles to
670
+ // the other object. Unfolded it would be no better: the zero test is CNTLZW_EQ0's u32, and a u32
671
+ // result declares an `int` function `u32`.
672
+ const notCmpPatterns = (
673
+ table: Readonly<Record<string, Opcode>>,
674
+ tests: readonly ('icmp_eq' | 'icmp_ne')[],
675
+ ): RewritePattern[] => [
676
+ ...Object.keys(table).map(notCmpPat),
677
+ ...Object.keys(table).flatMap((cmp) => tests.map((t) => cmpZeroPat(cmp, t))),
678
+ ];
679
+ /** The float compares' share of `NOT_CMP_PATTERNS`, which raise/pre-recovery.ts runs again once
680
+ * agbcc's compare helpers have folded to float compares: the idioms run before that pass, when
681
+ * `__ltsf2(a, b) >> 31 ^ 1` has no compare to negate yet. WITHOUT `cmp != 0 → cmp`, which is how
682
+ * PowerPC's record-form extract of a compare's bit reaches a branch (`mfcr; srwi. r0,r0,31;
683
+ * bnelr`): mwcc 2.4.2 and 2.4.7 compile `if (!(a < b)) gI = 1;` to that. Folded through it, `if
684
+ * (!(a0 < a1)) { gI = 1; return; }` recompiles to a plain `bltlr`; unfolded, `if (a0 < a1 == 0) {
685
+ * …; return; }` recompiles to the extract (`synthetic:fcmpbrn`). */
686
+ export const NOT_FCMP_PATTERNS: RewritePattern[] = notCmpPatterns(NEGATED_FCMP, ['icmp_eq']);
407
687
  /** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
408
- * entry is one comparison of `NEGATED_ICMP`; the bundle is what the boolean-reasoning consumers
409
- * downstream (short-circuit recovery, the structurer's condition spelling) actually match on. */
688
+ * entry is one comparison of `NEGATED_ICMP` or `NEGATED_FCMP` (a float compare's `!= 0` aside,
689
+ * `NOT_FCMP_PATTERNS`); the bundle is what the boolean-reasoning consumers downstream
690
+ * (short-circuit recovery, the structurer's condition spelling) actually match on. */
410
691
  export const NOT_CMP_PATTERNS: RewritePattern[] = [
411
- ...Object.keys(NEGATED_ICMP).map(notCmpPat),
412
- ...Object.keys(NEGATED_ICMP).flatMap((cmp) => [cmpZeroPat(cmp, 'icmp_eq'), cmpZeroPat(cmp, 'icmp_ne')]),
692
+ ...notCmpPatterns(NEGATED_ICMP, ['icmp_eq', 'icmp_ne']),
693
+ ...NOT_FCMP_PATTERNS,
413
694
  ];
414
695
 
415
696
  // The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
@@ -425,6 +706,7 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
425
706
  SDIV_POW2_2,
426
707
  ...HWMOD_PATTERNS,
427
708
  CNTLZW_EQ0,
709
+ ...MWCC_COMPARE_PATTERNS,
428
710
  // AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
429
711
  // negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
430
712
  ...NOT_CMP_PATTERNS,
@@ -450,13 +732,27 @@ interface Binds {
450
732
  interior: Set<Value>;
451
733
  }
452
734
 
735
+ /** Two constants that are the same 32-bit word (`-1` and `0xffffffff` are one register value). */
736
+ function sameWord(x: unknown, y: unknown): boolean {
737
+ return typeof x === 'number' && typeof y === 'number' && x >>> 0 === y >>> 0;
738
+ }
739
+
453
740
  function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
454
741
  if ('bind' in node) {
455
742
  b.values.set(node.bind, v);
456
743
  return true;
457
744
  }
458
745
  if ('same' in node) {
459
- return b.values.get(node.same) === v;
746
+ const bound = b.values.get(node.same);
747
+ if (bound === v) {
748
+ return true;
749
+ }
750
+ const [x, y] = [bound && defs.get(bound), defs.get(v)];
751
+ return x?.opcode === 'const' && y?.opcode === 'const' && sameWord(x.attrs.value, y.attrs.value);
752
+ }
753
+ if ('constEq' in node) {
754
+ const d = defs.get(v);
755
+ return d?.opcode === 'const' && sameWord(d.attrs.value, evalImm(node.constEq, b.imms));
460
756
  }
461
757
  if ('constImm' in node) {
462
758
  const d = defs.get(v);
@@ -711,11 +1007,11 @@ function reordersUnsequenced(
711
1007
  *
712
1008
  * Refusing leaves the raw ops standing, which is the spelling that was byte-exact before any fold
713
1009
  * existed — a worse-READING answer, never a worse-scoring one. */
714
- function sharesInterior(fn: Fn, root: Op, interior: Set<Value>, defs: Map<Value, Op>): boolean {
1010
+ function sharesInterior(fn: Fn, root: Op, interior: Set<Value>, defs: Map<Value, Op>, dead?: Set<Op>): boolean {
715
1011
  const matched = new Set([...interior].map((v) => defs.get(v)));
716
1012
  for (const b of fn.blocks) {
717
1013
  for (const o of b.ops) {
718
- if (matched.has(o)) {
1014
+ if (matched.has(o) || dead?.has(o)) {
719
1015
  continue; // a read from INSIDE the idiom is one this rewrite is replacing
720
1016
  }
721
1017
  for (const v of [...o.operands, ...o.successors.flatMap((x) => x.args)]) {
@@ -751,38 +1047,49 @@ export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget)
751
1047
  if (!tryMatch(pat.match, op.results[0], defs, binds)) {
752
1048
  continue;
753
1049
  }
754
- if (pat.recomputesSharedInterior?.includes(target.compiler) && sharesInterior(fn, op, binds.interior, defs)) {
1050
+ // Both interior gates ask one question — does an interior op outlive the fold? — and both
1051
+ // skip a reader `dce` will delete: the stage DCEs only after every pattern ran, so an op an
1052
+ // earlier fold orphaned still reads the interior here and keeps nothing alive.
1053
+ const askInterior = pat.foldsOnlyDyingInterior || pat.recomputesSharedInterior?.includes(target.compiler);
1054
+ if (askInterior && sharesInterior(fn, op, binds.interior, defs, deadOps(fn.blocks))) {
755
1055
  continue;
756
1056
  }
757
1057
  if (pat.unsequencedRightFirst && reordersUnsequenced(fn, op, pat.unsequencedRightFirst, binds, defs, pat.id)) {
758
1058
  continue;
759
1059
  }
760
- // Materialize any synthesized-constant replacement operands as their own `const` ops,
1060
+ // Materialize the replacement: synthesized constants and nested ops become their own ops,
761
1061
  // spliced in before the rewrite; bound-value operands resolve from the value binds.
762
- const rw = pat.replaceWith;
763
- const newRes = mkValue(rw.resultType ?? op.results[0].type);
764
- const consts: Op[] = [];
765
- const operands: Value[] = rw.args.map((a) => {
766
- if (typeof a === 'string') {
767
- // Attribute a malformed pattern HERE with its id — an unbound value would otherwise
768
- // become `undefined` and detonate stages later; patterns are meant to become
769
- // AI-generated data, so diagnosability is first-class.
770
- const bound = binds.values.get(a);
771
- if (!bound) {
772
- throw new Error(`pattern '${pat.id}' replaceWith references unbound value '${a}'`);
1062
+ const spliced: Op[] = [];
1063
+ const build = (rw: Replacement, type: IrType): Op => {
1064
+ const operands: Value[] = rw.args.map((a) => {
1065
+ if (typeof a === 'string') {
1066
+ // Attribute a malformed pattern HERE with its id — an unbound value would otherwise
1067
+ // become `undefined` and detonate stages later; patterns are meant to become
1068
+ // AI-generated data, so diagnosability is first-class.
1069
+ const bound = binds.values.get(a);
1070
+ if (!bound) {
1071
+ throw new Error(`pattern '${pat.id}' replaceWith references unbound value '${a}'`);
1072
+ }
1073
+ return bound;
773
1074
  }
774
- return bound;
1075
+ if ('constImm' in a) {
1076
+ const cv = mkValue(T.s());
1077
+ spliced.push(mkOp('const', { results: [cv], attrs: { value: evalImm(a.constImm, binds.imms) } }));
1078
+ return cv;
1079
+ }
1080
+ const inner = build(a, a.resultType ?? T.unk(32));
1081
+ spliced.push(inner);
1082
+ return inner.results[0];
1083
+ });
1084
+ const attrs: Record<string, number> = {};
1085
+ for (const [k, v] of Object.entries(rw.attrs ?? {})) {
1086
+ attrs[k] = typeof v === 'number' ? v : evalImm(v, binds.imms);
775
1087
  }
776
- const cv = mkValue(T.s());
777
- consts.push(mkOp('const', { results: [cv], attrs: { value: evalImm(a.constImm, binds.imms) } }));
778
- return cv;
779
- });
780
- const attrs: Record<string, number> = {};
781
- for (const [k, v] of Object.entries(rw.attrs ?? {})) {
782
- attrs[k] = typeof v === 'number' ? v : evalImm(v, binds.imms);
783
- }
784
- const newOp = mkOp(rw.op as Opcode, { operands, results: [newRes], attrs }); // pattern data boundary: verify() rejects unknowns
785
- b.ops.splice(i, 1, ...consts, newOp);
1088
+ return mkOp(rw.op as Opcode, { operands, results: [mkValue(type)], attrs }); // pattern data boundary: verify() rejects unknowns
1089
+ };
1090
+ const newOp = build(pat.replaceWith, pat.replaceWith.resultType ?? op.results[0].type);
1091
+ const newRes = newOp.results[0];
1092
+ b.ops.splice(i, 1, ...spliced, newOp);
786
1093
  replaceAllUsesWith(fn, op.results[0], newRes);
787
1094
  count++;
788
1095
  changed = true;
@@ -793,34 +1100,15 @@ export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget)
793
1100
  return count;
794
1101
  }
795
1102
 
796
- /** Remove effect-free ops whose single result is unused, to a fixed point. Deletability is
797
- * ir/discipline.ts `deletableWhenDead`, which keeps a read the lift pinned `volatile`: the machine
798
- * made it, so the recompile has to. */
1103
+ /** Remove effect-free ops whose single result is unused, to a fixed point: ir/discipline.ts
1104
+ * `deadOps`, which keeps a read the lift pinned `volatile` (the machine made it, so the recompile
1105
+ * has to) and is the same set the dead-read pin (frontend/dead-reads.ts) calls dead. */
799
1106
  export function dce(fn: Fn): void {
800
- let changed = true;
801
- while (changed) {
802
- changed = false;
803
- const used = new Set<Value>();
804
- for (const b of fn.blocks) {
805
- for (const op of b.ops) {
806
- for (const o of op.operands) {
807
- used.add(o);
808
- }
809
- for (const s of op.successors) {
810
- for (const a of s.args) {
811
- used.add(a);
812
- }
813
- }
814
- }
815
- }
816
- for (const b of fn.blocks) {
817
- const kept = b.ops.filter(
818
- (op) => !(deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])),
819
- );
820
- if (kept.length !== b.ops.length) {
821
- b.ops = kept;
822
- changed = true;
823
- }
824
- }
1107
+ const dead = deadOps(fn.blocks);
1108
+ if (dead.size === 0) {
1109
+ return;
1110
+ }
1111
+ for (const b of fn.blocks) {
1112
+ b.ops = b.ops.filter((op) => !dead.has(op));
825
1113
  }
826
1114
  }