@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -6,7 +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 { EFFECTFUL_OPS, NEGATED_ICMP, ORDER_SENSITIVE_OPS, type Opcode, isDceSafe } from '../ir/opcodes';
9
+ import { deadOps, effectful, orderSensitive } from '../ir/discipline';
10
+ import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
10
11
  import type { IrType } from '../ir/types';
11
12
  import { T } from '../ir/types';
12
13
 
@@ -22,8 +23,11 @@ export type MatchNode =
22
23
  args: MatchNode[];
23
24
  }
24
25
  | { bind: string } // bind this operand's VALUE to a name
25
- | { same: string } // this operand must equal a previously-bound value
26
- | { 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
27
31
 
28
32
  // A tiny declarative arithmetic over bound immediate names. It keeps a *computed* replacement
29
33
  // attribute DATA rather than a JS closure, so a strength-reduced idiom whose replacement is a
@@ -60,7 +64,16 @@ function evalImm(e: ImmExpr, imms: Map<string, number>): number {
60
64
  // A replacement operand is either a previously-BOUND value (by name) or a SYNTHESIZED constant
61
65
  // computed from the bound immediates (e.g. the derived multiplier `2^k+1`). A synthesized const
62
66
  // is materialized as its own `const` op spliced in before the rewrite site.
63
- 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
+ }
64
77
 
65
78
  export interface RewritePattern {
66
79
  id: string;
@@ -75,7 +88,7 @@ export interface RewritePattern {
75
88
  // a computed divisor proof and lives as the bespoke raise/magicdiv.ts pass instead. The
76
89
  // COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
77
90
  // multiply-by-constant idioms.
78
- replaceWith: { op: string; args: ReplaceArg[]; attrs?: Record<string, number | ImmExpr>; resultType?: IrType };
91
+ replaceWith: Replacement;
79
92
  /** Two REPLACEMENT operand names, in the order the replacement RENDERS them, whose defs C leaves
80
93
  * UNSEQUENCED against each other. A fold that collapses a multi-op idiom into one op drops its
81
94
  * operands from several uses to one, which lets the structurer inline both at that one use — and
@@ -92,12 +105,18 @@ export interface RewritePattern {
92
105
  * declaring pattern's compiler gate to that set; widening it fails loud. */
93
106
  unsequencedRightFirst?: [string, string];
94
107
  /** The compilers measured to RECOMPUTE this idiom instead of CSEing it back, when an interior op
95
- * 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
96
110
  * fold refuses that shape (`sharesInterior`, which carries the disassemblies); everywhere else
97
111
  * it is byte-neutral and refusing it costs matches, so the list is opt-in and per-compiler. A
98
112
  * compiler absent from it was either measured neutral or not measured — `validatePattern`
99
113
  * refuses a name that is not in `applies.compilers`, where the fold cannot fire anyway. */
100
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;
101
120
  }
102
121
 
103
122
  /** What the pattern layer reads off a target. STRUCTURAL on purpose: the pattern set is
@@ -144,6 +163,242 @@ export const CNTLZW_EQ0: RewritePattern = {
144
163
  replaceWith: { op: 'icmp_eq', args: ['X', { constImm: 0 }], resultType: T.u(32) },
145
164
  };
146
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
+
147
402
  // The PPC rotate mirror, as pure data: hardware only rotates LEFT, so mwcc spells `rotr(x, n)`
148
403
  // as `rotlw(x, 32 - n)`. Lowering that literally emits `x << (32 - n) | x >> (32 - (32 - n))`,
149
404
  // whose inner subtraction recompiles to an extra instruction; folding to `rotr(x, n)` lets the
@@ -358,11 +613,18 @@ export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16),
358
613
  // Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
359
614
  // with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
360
615
  //
361
- // The negation comes from THE shared table (ir/opcodes.ts NEGATED_ICMP), so this fold, the MIPS
362
- // `slt …; beqz` branch fold and the short-circuit diamond negation cannot disagree about what the
363
- // opposite of a compare is. One pattern per comparison — a data-driven fold needs a fixed
364
- // replacement opcode, so the table is unrolled into ten patterns rather than expressed as a
365
- // (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));
366
628
  const notCmpPat = (cmp: string): RewritePattern => ({
367
629
  id: `not-${cmp}`,
368
630
  applies: {},
@@ -377,7 +639,7 @@ const notCmpPat = (cmp: string): RewritePattern => ({
377
639
  // It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
378
640
  // the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
379
641
  // patterns on one discipline instead of leaving a reader to infer which is canonical.
380
- replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
642
+ replaceWith: { op: NEGATED_CMP[cmp], args: ['A', 'B'], resultType: cmpResultType(cmp) },
381
643
  });
382
644
 
383
645
  // The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
@@ -400,15 +662,35 @@ const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern =>
400
662
  { op: 'const', attrEquals: { value: 0 }, args: [] },
401
663
  ],
402
664
  },
403
- 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) },
404
666
  });
405
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']);
406
687
  /** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
407
- * entry is one comparison of `NEGATED_ICMP`; the bundle is what the boolean-reasoning consumers
408
- * 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. */
409
691
  export const NOT_CMP_PATTERNS: RewritePattern[] = [
410
- ...Object.keys(NEGATED_ICMP).map(notCmpPat),
411
- ...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,
412
694
  ];
413
695
 
414
696
  // The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
@@ -424,6 +706,7 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
424
706
  SDIV_POW2_2,
425
707
  ...HWMOD_PATTERNS,
426
708
  CNTLZW_EQ0,
709
+ ...MWCC_COMPARE_PATTERNS,
427
710
  // AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
428
711
  // negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
429
712
  ...NOT_CMP_PATTERNS,
@@ -449,13 +732,27 @@ interface Binds {
449
732
  interior: Set<Value>;
450
733
  }
451
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
+
452
740
  function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
453
741
  if ('bind' in node) {
454
742
  b.values.set(node.bind, v);
455
743
  return true;
456
744
  }
457
745
  if ('same' in node) {
458
- 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));
459
756
  }
460
757
  if ('constImm' in node) {
461
758
  const d = defs.get(v);
@@ -589,17 +886,18 @@ function validatePattern(pat: RewritePattern): void {
589
886
  * just its def, and the question has to be asked over the cones: one pure `+ 1` between an effect
590
887
  * and the fold's operand is the difference between `f() % g()` and `(f() + 1) % g()`, and the
591
888
  * second one reorders exactly as the first does. Both cones become operands of ONE expression,
592
- * where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts)
593
- * exempts exactly this case — "a sibling effect inlined into the SAME statement is not a reorder,
594
- * the recompiling compiler orders unsequenced operands of one expression exactly as it originally
595
- * chose to". That premise holds only when the expression asmlift re-spells is the one the source
596
- * wrote. A fold INVENTS an expression, so it must check.
889
+ * where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts,
890
+ * the barrier scan) keeps two operands of one statement inline only in the order every corpus
891
+ * compiler gives back — a call before the reads beside it — but it judges the program the fold
892
+ * leaves, after the idiom's own ops are gone. The fold INVENTS the expression, so it checks the
893
+ * cones it joins itself.
597
894
  *
598
- * A cone's members are weighed by ORDER_SENSITIVE_OPS, not EFFECTFUL_OPS: a memory read answers
895
+ * A cone's members are weighed by `orderSensitive`, not `effectful`: a memory read answers
599
896
  * whichever stores ran before it, so hoisting a `load` over a `call` — one asmlift may itself be
600
897
  * passing the loaded pointer to — changes the answer as surely as swapping two calls. Two READS
601
- * are the exception and commute, the same fact the structurer states as "a load never bars a
602
- * load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted.
898
+ * are the exception and commute, the same fact the structurer states as "a plain load never bars
899
+ * a load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted. Two `volatile`
900
+ * device reads do not commute, and the structurer names the first of them itself.
603
901
  *
604
902
  * Refuses only what it must — the alternative spelling (the idiom written out) names the operands
605
903
  * and states the order, so a refusal is a loud, correct, slightly-worse-scoring answer. */
@@ -654,20 +952,18 @@ function reordersUnsequenced(
654
952
  return cone;
655
953
  };
656
954
  const [lc, rc] = names.map(coneOf);
657
- const sensitive = (c: Set<Op>) => [...c].filter((o) => ORDER_SENSITIVE_OPS.has(o.opcode));
955
+ const sensitive = (c: Set<Op>) => [...c].filter(orderSensitive);
658
956
  for (const l of sensitive(lc)) {
659
957
  for (const r of sensitive(rc)) {
660
958
  // One op standing in BOTH cones is one evaluation and cannot be sequenced against itself; two
661
959
  // READS commute; and a right-cone op the machine ALREADY runs first loses nothing.
662
- if (l === r || at.get(l)! >= at.get(r)! || !(EFFECTFUL_OPS.has(l.opcode) || EFFECTFUL_OPS.has(r.opcode))) {
960
+ if (l === r || at.get(l)! >= at.get(r)! || !(effectful(l) || effectful(r))) {
663
961
  continue;
664
962
  }
665
963
  // The left one runs first, so an inlined `A op B` would swap them — UNLESS a sibling effect
666
964
  // stands between, which the inline-at-use model refuses to cross, forcing a named temp at the
667
965
  // def's own position. A cone member is no sibling: it is inlined into this very expression.
668
- if (
669
- !blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => EFFECTFUL_OPS.has(o.opcode) && !lc.has(o) && !rc.has(o))
670
- ) {
966
+ if (!blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => effectful(o) && !lc.has(o) && !rc.has(o))) {
671
967
  return true;
672
968
  }
673
969
  }
@@ -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;
1074
+ }
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;
773
1079
  }
774
- return bound;
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,31 +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
- * derived from the ONE effect table in ir/opcodes.ts. */
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. */
798
1106
  export function dce(fn: Fn): void {
799
- let changed = true;
800
- while (changed) {
801
- changed = false;
802
- const used = new Set<Value>();
803
- for (const b of fn.blocks) {
804
- for (const op of b.ops) {
805
- for (const o of op.operands) {
806
- used.add(o);
807
- }
808
- for (const s of op.successors) {
809
- for (const a of s.args) {
810
- used.add(a);
811
- }
812
- }
813
- }
814
- }
815
- for (const b of fn.blocks) {
816
- const kept = b.ops.filter((op) => !(isDceSafe(op.opcode) && op.results.length === 1 && !used.has(op.results[0])));
817
- if (kept.length !== b.ops.length) {
818
- b.ops = kept;
819
- changed = true;
820
- }
821
- }
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));
822
1113
  }
823
1114
  }