@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
@@ -29,9 +29,10 @@
29
29
  // REGISTERS the list occupies, and `irWidthOf` gives the IR width one parameter ARRIVES at once a
30
30
  // frontend has paired those registers up.
31
31
  import { type Op, type Value, mkOp } from './ir/core';
32
- import { FLOAT_OPS, type Opcode, WIDE_BITS } from './ir/opcodes';
32
+ import { FLOAT_COMPUTING, FLOAT_TO_INT, INT_TO_FLOAT, NEGATED_FCMP, type Opcode, WIDE_BITS } from './ir/opcodes';
33
33
  import { type IrType, intWidth } from './ir/types';
34
34
  import { type Prototypes, wordsOf } from './proto';
35
+ import type { TargetDescription } from './target';
35
36
 
36
37
  export interface RuntimeHelper {
37
38
  /** the op this helper computes, where asmlift has one. Absent for a helper it can only SIGN. */
@@ -40,6 +41,19 @@ export interface RuntimeHelper {
40
41
  params: readonly number[];
41
42
  /** the returned width: 0 (void), 32, or 64 (a register PAIR). */
42
43
  returns: 0 | 32 | 64;
44
+ /** A THREE-WAY FLOAT COMPARE: its result's SIGN orders its two operands (negative, zero, positive
45
+ * for less, equal, greater), and this is the value it returns where either is a NaN. A compare
46
+ * folds through the integer test its caller applies to the result (raise/floathelpers.ts), so it
47
+ * carries this in place of an `op`. */
48
+ unordered?: 1 | -1;
49
+ /** THE RELATION THE SOURCE SPELLED, for a compare helper whose relation and its negation are both
50
+ * C operators — `__eqsf2` for `==`, `__nesf2` for `!=`. agbcc's gcc folds `!(a == b)` into
51
+ * `a != b` before it picks a helper, so the helper names the relation the source wrote, and a
52
+ * lift that spells its negation (or the negation's `!`) recompiles to the other helper. The fold
53
+ * stamps it on the compare as `attrs.relation` (raise/floathelpers.ts), and the structurer keeps
54
+ * that relation's spelling by choosing the arm order instead (l3/ast.ts `Expr.helperRel`). The
55
+ * ordered four need none: `!(a > b)` keeps `__gtsf2`, and no other helper computes it. */
56
+ relation?: 'fcmp_oeq' | 'fcmp_une';
43
57
  }
44
58
 
45
59
  /** The entry a target's table holds for `callee`, or undefined. THE ONE READER, because the table
@@ -118,19 +132,34 @@ export function helperOp(opcode: Opcode, helper: string, operands: readonly Valu
118
132
  return mkOp(opcode, { operands: [...operands], results: [result], attrs: { helper } });
119
133
  }
120
134
 
121
- /** Whether a helper computes on floats: its op is a float op, so its operands and result are floats
122
- * of the widths its signature states, carried in integer registers by a soft-float runtime. Every
123
- * reader of the table asks this and not the width, because the two families fold into different
124
- * IR: an integer helper into its op over the same values, a float helper into a float op over
125
- * values re-typed as floats (`raise/floathelpers.ts`). */
135
+ /** Whether a helper computes on floats: its op is a float op, a conversion or a compare, so the
136
+ * operands and the result that op takes as floats are floats of the widths its signature states,
137
+ * carried in integer registers by a soft-float runtime. Every reader of the table asks this and
138
+ * not the width, because the two families fold into different IR: an integer helper into its op
139
+ * over the same values, a float helper into its op over values re-typed as floats where the op
140
+ * takes one (`raise/floathelpers.ts`). */
126
141
  export function isFloatHelper(h: RuntimeHelper): boolean {
127
- return h.op !== undefined && FLOAT_OPS.has(h.op);
142
+ return h.unordered !== undefined || (h.op !== undefined && FLOAT_COMPUTING.has(h.op));
143
+ }
144
+
145
+ /** Which sides of a float helper are floats: its operands (one kind for all of them, in every op
146
+ * here) and its result. A conversion has an integer on one side; a compare returns one. */
147
+ export function floatSidesOf(h: RuntimeHelper): { operands: boolean; result: boolean } {
148
+ const op = h.op ?? '';
149
+ return {
150
+ operands: !INT_TO_FLOAT.has(op),
151
+ result: h.unordered === undefined && !FLOAT_TO_INT.has(op) && NEGATED_FCMP[op] === undefined,
152
+ };
128
153
  }
129
154
 
130
155
  /** Signatures for a target's helpers, in the WORD arity the frontend's prototype lookup speaks.
131
156
  * Consumed behind any caller-supplied prototype — the project's own headers win. */
132
- export function helperPrototypes(table: Readonly<Record<string, RuntimeHelper>> | undefined): Prototypes {
133
- return Object.fromEntries(Object.entries(table ?? {}).map(([sym, h]) => [sym, { params: wordsOf(h.params) }]));
157
+ export function helperPrototypes(
158
+ target: Pick<TargetDescription, 'runtimeHelpers' | 'evenWordAlignedArgs'>,
159
+ ): Prototypes {
160
+ return Object.fromEntries(
161
+ Object.entries(target.runtimeHelpers ?? {}).map(([sym, h]) => [sym, { params: wordsOf(h.params, target) }]),
162
+ );
134
163
  }
135
164
 
136
165
  /** agbcc's runtime, which is GCC 2.9's libgcc for ARM.
@@ -142,20 +171,32 @@ export function helperPrototypes(table: Readonly<Record<string, RuntimeHelper>>
142
171
  * operand SET-UP instead — `asr rN,rM,#31` per half for signed, `mov rN,#0` for unsigned. The
143
172
  * DIVISIONS do split, and their entries say so.
144
173
  *
145
- * THE DOUBLE ARITHMETIC IS HERE, AND NOTHING ELSE OF THE SOFT FLOAT. `optabs.c:4022`
146
- * `init_floating_libfuncs` names DFmode's add, sub, mul, div (4129-4140) and neg (4155), and
147
- * `thumb.md` has no DF pattern but the move, so every one of them is a call; the ABI is a long
148
- * long's (thumb.h:632, 655). `raise/floathelpers.ts` folds them to the float ops over a `double`.
174
+ * THE FLOAT ARITHMETIC OF BOTH PRECISIONS IS HERE, AND FOLDS. `optabs.c:4022`
175
+ * `init_floating_libfuncs` names each mode's add, sub, mul, div (4129-4140) and neg (4155), and
176
+ * `thumb.md` has no SF or DF pattern but the move, so every one of them is a call; a single
177
+ * travels as an int's word and a double as a long long's pair (thumb.h:632, 655).
178
+ * `raise/floathelpers.ts` folds them to the float ops over a `float` or a `double`.
149
179
  *
150
- * NAMING THEM IS A TRADE, and these five make it: a call the fold refuses declines where an
151
- * unnamed helper would publish as a pass-through that matches for free. So a function whose
152
- * double arithmetic feeds a compare (`__gtdf2`…), a conversion (`__fixdfsi`…) or any other call
153
- * declines, where unnamed it would print `__muldf3(); return __fixdfsi();`. The compares and
154
- * conversions of both precisions stay out, because the IR has no int<->float op to fold one into;
155
- * the single-precision arithmetic stays out because its pass-throughs are what the float rows
156
- * score on (`synthetic:fadd:agbcc`), and declining them is the same trade, to be made with a
157
- * measurement of those rows. Making it is a table row: the fold reads each value's width off the
158
- * signature. */
180
+ * NAMING THEM IS A TRADE, and these ten make it: a call the fold refuses declines where an unnamed
181
+ * helper would publish as a pass-through. So a function whose float arithmetic feeds a compare, a
182
+ * conversion or an ordinary call declines, where unnamed it would print `__muldf3(); g();`. For
183
+ * the singles that pass-through is wrong C: unnamed, `__mulsf3`'s arity is GUESSED (frontend/ssa.ts
184
+ * `fallbackArgc`), and a guess drops what the function passes on unchanged — `a * b + c` as
185
+ * `__addsf3(__mulsf3(), a2)`, `a + b` as `__addsf3()`.
186
+ *
187
+ * THE CONVERSIONS FOLD TOO, to the cast each computes (`fptosi`, `sitofp`, `fpext` …), with an
188
+ * integer on one side that `raise/floathelpers.ts` takes as any integer. THE COMPARES fold through
189
+ * the test their caller applies to the integer they return, by libgcc's contract for it
190
+ * (`unordered`), and one whose result is read any other way refuses (`refuseUnmodelledHelpers`).
191
+ * Unnamed, each would be the same guess, dropping the argument passed on unchanged and matching
192
+ * byte for byte: `(int)x` as `__fixsfsi()`, `(float)i` as `__floatsisf()`, `a > b` as
193
+ * `__gtsf2() > 0`. The names are the ones agbcc
194
+ * emits for every compare, every conversion between `int`, `unsigned`, `long long` and either
195
+ * precision, and every widening and narrowing between the two precisions (compiled evidence: one
196
+ * function per operation at the canonical flags). There is no conversion of an unsigned integer
197
+ * INTO a float: agbcc open-codes `(float)u` around `__floatsisf` and `__addsf3`. `__cmpdi2`, which
198
+ * it also calls converting an `unsigned long long`, is an integer compare and outside this
199
+ * family. */
159
200
  export const AGBCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
160
201
  // 32-bit software division — the ops `raise/softdiv.ts` rewrites, gated on the target having no
161
202
  // hardware divider, which is what those four are about.
@@ -180,6 +221,45 @@ export const AGBCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
180
221
  __muldf3: { op: 'fmul', params: [64, 64], returns: 64 },
181
222
  __divdf3: { op: 'fdiv', params: [64, 64], returns: 64 },
182
223
  __negdf2: { op: 'fneg', params: [64], returns: 64 },
224
+ // Single arithmetic, one float in one register each way (thumb.h:632's word).
225
+ __addsf3: { op: 'fadd', params: [32, 32], returns: 32 },
226
+ __subsf3: { op: 'fsub', params: [32, 32], returns: 32 },
227
+ __mulsf3: { op: 'fmul', params: [32, 32], returns: 32 },
228
+ __divsf3: { op: 'fdiv', params: [32, 32], returns: 32 },
229
+ __negsf2: { op: 'fneg', params: [32], returns: 32 },
230
+ // The compares, one int out of two floats of either precision, each libgcc's three-way
231
+ // `__fpcmp_parts` when both are ordered (fp-bit.c:1117-1188). On a NaN each returns the value its
232
+ // own relation tests FALSE on — `== 0`, `< 0`, `<= 0` are false on 1, `> 0`, `>= 0` on -1 — except
233
+ // `__nesf2`, whose 1 makes `!=` true (fp-bit.c:1214-1309). agbcc tests the result against 0 with
234
+ // the relation the helper is named for.
235
+ __eqsf2: { params: [32, 32], returns: 32, unordered: 1, relation: 'fcmp_oeq' },
236
+ __nesf2: { params: [32, 32], returns: 32, unordered: 1, relation: 'fcmp_une' },
237
+ __gtsf2: { params: [32, 32], returns: 32, unordered: -1 },
238
+ __gesf2: { params: [32, 32], returns: 32, unordered: -1 },
239
+ __ltsf2: { params: [32, 32], returns: 32, unordered: 1 },
240
+ __lesf2: { params: [32, 32], returns: 32, unordered: 1 },
241
+ __eqdf2: { params: [64, 64], returns: 32, unordered: 1, relation: 'fcmp_oeq' },
242
+ __nedf2: { params: [64, 64], returns: 32, unordered: 1, relation: 'fcmp_une' },
243
+ __gtdf2: { params: [64, 64], returns: 32, unordered: -1 },
244
+ __gedf2: { params: [64, 64], returns: 32, unordered: -1 },
245
+ __ltdf2: { params: [64, 64], returns: 32, unordered: 1 },
246
+ __ledf2: { params: [64, 64], returns: 32, unordered: 1 },
247
+ // The conversions: to and from a word, to and from a long long, and between the precisions.
248
+ // `__fix*` truncates toward zero, as C's conversion out of a float does.
249
+ __fixsfsi: { op: 'fptosi', params: [32], returns: 32 },
250
+ __fixunssfsi: { op: 'fptoui', params: [32], returns: 32 },
251
+ __floatsisf: { op: 'sitofp', params: [32], returns: 32 },
252
+ __fixdfsi: { op: 'fptosi', params: [64], returns: 32 },
253
+ __fixunsdfsi: { op: 'fptoui', params: [64], returns: 32 },
254
+ __floatsidf: { op: 'sitofp', params: [32], returns: 64 },
255
+ __fixsfdi: { op: 'fptosi', params: [32], returns: 64 },
256
+ __fixunssfdi: { op: 'fptoui', params: [32], returns: 64 },
257
+ __floatdisf: { op: 'sitofp', params: [64], returns: 32 },
258
+ __fixdfdi: { op: 'fptosi', params: [64], returns: 64 },
259
+ __fixunsdfdi: { op: 'fptoui', params: [64], returns: 64 },
260
+ __floatdidf: { op: 'sitofp', params: [64], returns: 64 },
261
+ __extendsfdf2: { op: 'fpext', params: [32], returns: 64 },
262
+ __truncdfsf2: { op: 'fptrunc', params: [64], returns: 32 },
183
263
  };
184
264
 
185
265
  /** CodeWarrior's PowerPC runtime (`Runtime.PPCEABI.H`), as the GameCube projects vendor it.
@@ -220,6 +300,44 @@ export const PPC_MWCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> =
220
300
  __shr2u: { op: 'shr_u', params: [64, 32], returns: 64 },
221
301
  };
222
302
 
303
+ /** A compiler runtime's OUT-OF-LINE REGISTER SAVE AND RESTORE ROUTINES: a prologue that saves a run
304
+ * of callee-saved registers by calling into the runtime instead of storing each one inline. A
305
+ * compiler fact, like the table above, and it hangs off `TargetDescription` for the same reason:
306
+ * which compiler emits the family, at which level, is measured per build, and a frontend that
307
+ * hard-coded the names would recognise them on a target whose runtime has no such routine.
308
+ *
309
+ * A call to one is FRAME BOOKKEEPING, not a call: it writes no register but the link register, and
310
+ * the frame address in `frameReg` is the only thing it reads (frontend/ppc.ts `saveRoutine`). */
311
+ export interface RegisterSaveRoutines {
312
+ /** the register a routine takes its frame address in. The save area is the words just BELOW it. */
313
+ frameReg: string;
314
+ /** every routine name, with three groups: `save` or `rest`, the register file (`gpr`/`fpr`), and
315
+ * the first register the routine moves — it moves that one up to the file's last, 31. */
316
+ name: RegExp;
317
+ }
318
+
319
+ /** The routine a callee symbol names under `routines`, or null when it names none. */
320
+ export function registerSaveRoutineOf(
321
+ routines: RegisterSaveRoutines | undefined,
322
+ sym: string,
323
+ ): { sym: string; restore: boolean; fpr: boolean; first: number } | null {
324
+ const m = routines?.name.exec(sym);
325
+ return m ? { sym: m[0], restore: m[1] === 'rest', fpr: m[2] === 'fpr', first: Number(m[3]) } : null;
326
+ }
327
+
328
+ /** CodeWarrior's `_savegpr_NN` / `_restgpr_NN` and `_savefpr_NN` / `_restfpr_NN`, called after
329
+ * `addi r11,r1,N`. `_savegpr_NN` stores rNN..r31 into the 4*(32-NN) bytes just below r11, and
330
+ * `_savefpr_NN` stores fNN..f31 into the 8*(32-NN) bytes below it.
331
+ *
332
+ * COMPILED, per build: 2.4.2 and 2.4.7 emit the family at every level (`-O4,p`, `-O4,s`, `-O0,p`).
333
+ * 2.3.3 never does at Pikmin's `-O4,p`, where it saves with `stmw` and inline `stfd`, but does at
334
+ * `-O0,p` and `-O4,s`; all three builds share `PPC_MWCC`, so the description carries it. The bench's
335
+ * feature tagger reads this same pattern (apps/benchmark `features.ts`). */
336
+ export const PPC_MWCC_SAVE_ROUTINES: RegisterSaveRoutines = {
337
+ frameReg: 'r11',
338
+ name: /^_(save|rest)(gpr|fpr)_(\d+)$/,
339
+ };
340
+
223
341
  /** IDO 7.1's runtime for the 64-bit operations MIPS II has no instruction for, a family of its own
224
342
  * (`__ll_*` for signed, `__ull_*` for unsigned): a scan for `__*di3` reports zero on it.
225
343
  *
@@ -28,6 +28,7 @@ import {
28
28
  defOpMap,
29
29
  dominators,
30
30
  mergeClasses,
31
+ opShape,
31
32
  predecessors,
32
33
  successorsOf,
33
34
  } from '../ir/core';
@@ -892,7 +893,7 @@ export interface AnalyzeOptions {
892
893
  * A compiler behavior (TargetDescription.compilerBehaviors `readsStayWhereWritten`), not
893
894
  * a differ-refereed variation, and that distinction is the argument: a variation exists where the asm
894
895
  * UNDERDETERMINES the source, some pass having collapsed two spellings onto one output
895
- * (`/uns-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
896
+ * (`/signed-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
896
897
  * block the source spelled it in, re-spelling it at the block the asm read in reproduces that
897
898
  * asm while the sunk spelling is one it emits only for a source that read per arm — nothing for
898
899
  * the differ to referee, and the extra candidate is pure cost. Which compilers may declare
@@ -1006,12 +1007,18 @@ function makeMemWriteBetween(deps: {
1006
1007
  if (wDirty(b.ops, oi + 1, b.ops.length) || wDirty(render.blk.ops, 0, render.idx)) {
1007
1008
  return true;
1008
1009
  }
1009
- for (const x of reachAvoiding(b, b)) {
1010
- if (x === render.blk && !reachAvoiding(render.blk, b).has(render.blk)) {
1011
- continue; // acyclic render block: head checked
1012
- }
1013
- if (x !== render.blk && !reachAvoiding(x, b).has(render.blk)) {
1014
- continue; // not on a def→render path
1010
+ // The between-blocks: every `x` the def's block reaches without re-crossing it from which the
1011
+ // render block is reached again in at least one step, by the same def-avoiding walk. The render
1012
+ // block itself counts only where it reaches itself so (its head is checked above); any other
1013
+ // block only where it lies on a def→render path.
1014
+ const forward = reachAvoiding(b, b);
1015
+ if (!forward.has(render.blk)) {
1016
+ return false; // every block on a def→render path is one the def's block reaches
1017
+ }
1018
+ const onPath = blocksReaching(forward, render.blk, b);
1019
+ for (const x of forward) {
1020
+ if (!successorsOf(x).some((s) => s !== b && onPath.has(s))) {
1021
+ continue; // not on a def→render path (or, for the render block, acyclic: head checked)
1015
1022
  }
1016
1023
  if (wDirty(x.ops, 0, x.ops.length)) {
1017
1024
  return true;
@@ -1021,6 +1028,49 @@ function makeMemWriteBetween(deps: {
1021
1028
  };
1022
1029
  }
1023
1030
 
1031
+ /** The blocks of `within` that reach `target` in zero or more steps without passing `avoid`,
1032
+ * `target` included: one backward walk over `within`'s own edges.
1033
+ *
1034
+ * `makeMemWriteBetween` asks, for each block `x` the def's block reaches, whether `target` is in
1035
+ * `reachAvoiding(x, avoid)`. That is a forward walk per `x`, quadratic in the function's size per
1036
+ * question, and the question is asked per op `analyze` names (`nameAccess`): Klonoa's
1037
+ * sub_0800D188 (1,783 blocks) spent most of its structuring in those walks. `target` is in
1038
+ * `reachAvoiding(x, avoid)` exactly where some successor of `x` other than `avoid` is in this set,
1039
+ * since every block on such a path after `x` is in `within` (`within` being everything the def's
1040
+ * block reaches without passing `avoid`, from which `x` was drawn). */
1041
+ function blocksReaching(within: ReadonlySet<Block>, target: Block, avoid: Block): Set<Block> {
1042
+ const into = new Map<Block, Block[]>();
1043
+ for (const y of within) {
1044
+ if (y === avoid) {
1045
+ continue;
1046
+ }
1047
+ for (const s of successorsOf(y)) {
1048
+ if (s !== avoid && within.has(s)) {
1049
+ const list = into.get(s);
1050
+ if (list === undefined) {
1051
+ into.set(s, [y]);
1052
+ } else {
1053
+ list.push(y);
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ const out = new Set<Block>();
1059
+ if (target === avoid) {
1060
+ return out;
1061
+ }
1062
+ out.add(target);
1063
+ for (const work = [target]; work.length;) {
1064
+ for (const y of into.get(work.pop()!) ?? []) {
1065
+ if (!out.has(y)) {
1066
+ out.add(y);
1067
+ work.push(y);
1068
+ }
1069
+ }
1070
+ }
1071
+ return out;
1072
+ }
1073
+
1024
1074
  /** INTERDEPENDENT PARALLEL-COPY ARGS. A terminator's successor args are ONE parallel copy
1025
1075
  * (argAssigns), whose every read means the PRE-copy value. An arg whose def-tree reads a SIBLING
1026
1076
  * arg of the same edge cannot read it by name there — so the sibling's whole expression is
@@ -1521,6 +1571,9 @@ interface LoopRules {
1521
1571
  /** The def's value enters some loop's header live and every consumer sits outside that loop, as
1522
1572
  * does the def: the value is carried ACROSS the loop, not into it. */
1523
1573
  liveAcrossLoop(def: Op, r: Value, consumers: Op[]): boolean;
1574
+ /** `liveAcrossLoop`, and neither the def nor any consumer sits in any loop, enclosing ones
1575
+ * included: never a value the compiler could have hoisted out of one. */
1576
+ carriedPastLoops(def: Op, r: Value, consumers: Op[]): boolean;
1524
1577
  /** Does `r`, computed by `op` in `L`'s body, read a loop variable the update overwrites? */
1525
1578
  readsPreUpdate(L: BottomTestedLoop, op: Op, r: Value): boolean;
1526
1579
  escapesAheadOfUpdate(op: Op, r: Value, consumers: Op[]): boolean;
@@ -1551,6 +1604,9 @@ function makeLoopRules(a: {
1551
1604
  !L.body.has(opBlock.get(def)!) &&
1552
1605
  consumers.every((c) => !L.body.has(opBlock.get(c)!)),
1553
1606
  );
1607
+ const carriedPastLoops = (def: Op, r: Value, consumers: Op[]): boolean =>
1608
+ liveAcrossLoop(def, r, consumers) &&
1609
+ loopBodies.every((L) => !L.body.has(opBlock.get(def)!) && consumers.every((c) => !L.body.has(opBlock.get(c)!)));
1554
1610
  /** THE PRE-UPDATE ESCAPE. A value a bottom-tested loop computes and the code after it reads,
1555
1611
  * whose expression reads a loop variable — `do { s = &f->v; f = f->next; } while (--n); *out =
1556
1612
  * s;`. Inlined at the post-loop read it re-reads that variable's NAME, which by then holds the
@@ -1710,6 +1766,7 @@ function makeLoopRules(a: {
1710
1766
  multiBlockHeaders,
1711
1767
  bottomTested,
1712
1768
  liveAcrossLoop,
1769
+ carriedPastLoops,
1713
1770
  readsPreUpdate,
1714
1771
  escapesAheadOfUpdate,
1715
1772
  rebuiltPast,
@@ -2042,6 +2099,20 @@ function namePureOp(rules: MaterializeRules, state: MaterializeState, op: Op, b:
2042
2099
  } else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !scopes.addressCone(op)) {
2043
2100
  materialize.add(op);
2044
2101
  }
2102
+ // A PURE VALUE CARRIED ACROSS A LOOP, computed and read outside every loop, as `nameAccess` names
2103
+ // a read (`carriedPastLoops`; a value read inside a loop is `/expr-home`'s question, where loop.c's
2104
+ // invariant motion may have put it). `j = n * 3; for (…) { if (t[i] == 7) { gHit = j; break; } }`
2105
+ // computes `j` ahead of the loop and holds it in a register to the break arm, which sits outside
2106
+ // the natural loop. Inlined, `gHit = n * 3` moves the computation into the arm, and every profile
2107
+ // compiles it there (IDO scores the two alike). A `const` is re-materialized where it is used, and
2108
+ // an address loses its inline byte-stride cast standing alone and is what a compiler CSEs and
2109
+ // moves freely, so neither is named.
2110
+ if (op.opcode !== 'const' && pr && useSitesOf.has(pr) && !scopes.addressCone(op) && !rendersAsAddress(op)) {
2111
+ const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
2112
+ if (loops.carriedPastLoops(op, pr, cons)) {
2113
+ materialize.add(op);
2114
+ }
2115
+ }
2045
2116
  // A float PRODUCT read by a float add or subtract, directly or through a negation, is named
2046
2117
  // on a compiler that contracts (target.ts `contractsFloatProducts`): it fuses only WITHIN
2047
2118
  // one expression, so the inline `a * b + c` recompiles to one `fmadds` — one rounding, a
@@ -2248,7 +2319,7 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
2248
2319
  // order is the compiler's to choose (`gY = VCOUNT - TM0CNT_L` reads VCOUNT first); a
2249
2320
  // qualified read spelled as a bare statement because nothing uses it is an access too.
2250
2321
  if (once && !isCall && (x.opcode === 'load' || x.opcode === 'aload') && counted(x)) {
2251
- return true;
2322
+ return !interchangeableReads(rules, x, op);
2252
2323
  }
2253
2324
  if (!isCall) {
2254
2325
  return false;
@@ -2261,6 +2332,144 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
2261
2332
  return false;
2262
2333
  }
2263
2334
 
2335
+ const COMMUTATIVE_OPS: ReadonlySet<string> = new Set(['add', 'mul', 'and', 'or', 'xor']);
2336
+
2337
+ /** Do two values name the same address: one value, or two `gaddr`s of one symbol? */
2338
+ function sameAddress(defOf: Map<Value, Op>, a: Value, b: Value): boolean {
2339
+ if (a === b) {
2340
+ return true;
2341
+ }
2342
+ const da = defOf.get(a);
2343
+ const db = defOf.get(b);
2344
+ return (
2345
+ da !== undefined &&
2346
+ db !== undefined &&
2347
+ da.opcode === 'gaddr' &&
2348
+ db.opcode === 'gaddr' &&
2349
+ opShape(da) === opShape(db)
2350
+ );
2351
+ }
2352
+
2353
+ /** Are two qualified reads of ONE cell interchangeable in the one expression they feed: the same
2354
+ * access of the same address, each the sole use of its value up a chain of ops alike but for it,
2355
+ * the two chains meeting as the two operands of one commutative op? Then the expression's value is
2356
+ * the same whichever read the compiler makes first, and its order is the compiler's to choose:
2357
+ * `gR + gR` over `extern volatile u32 gR;` and `*gPV + *gPV` over `u16 *volatile gPV` read the
2358
+ * object twice, in an order the source never stated, so naming the first read into a temp spells
2359
+ * an order the source did not have (ido 7.1 loads `gR + gR` in the other order). The meeting op
2360
+ * may also be a sum both reads enter with one sign, past a reassociation (`sameSignInOneSum`).
2361
+ * `gR - gR` is not symmetric — the order decides which read is the minuend — and stays sequenced.
2362
+ * `a` is the later read, `b` the earlier one placed past it. */
2363
+ function interchangeableReads(rules: MaterializeRules, a: Op, b: Op): boolean {
2364
+ const { defOf } = rules;
2365
+ if (
2366
+ a === b ||
2367
+ opShape(a) !== opShape(b) ||
2368
+ a.operands.length !== b.operands.length ||
2369
+ !a.operands.every((v, i) => sameAddress(defOf, v, b.operands[i]))
2370
+ ) {
2371
+ return false;
2372
+ }
2373
+ return mirroredChains(rules, a, b) || sameSignInOneSum(rules, b, a);
2374
+ }
2375
+
2376
+ /** The two reads' sole-use chains are alike but for them and meet as the two operands of one
2377
+ * commutative op: `(gR << 1) * (gR << 1)`. */
2378
+ function mirroredChains(rules: MaterializeRules, a: Op, b: Op): boolean {
2379
+ const { useSitesOf } = rules.uses;
2380
+ let ca = a;
2381
+ let cb = b;
2382
+ for (let depth = 0; depth < 8; depth++) {
2383
+ const va = ca.results[0];
2384
+ const vb = cb.results[0];
2385
+ const ua = va === undefined ? undefined : useSitesOf.get(va);
2386
+ const ub = vb === undefined ? undefined : useSitesOf.get(vb);
2387
+ if (ua?.length !== 1 || ub?.length !== 1) {
2388
+ return false;
2389
+ }
2390
+ const na = ua[0].op;
2391
+ const nb = ub[0].op;
2392
+ if (na === nb) {
2393
+ return (
2394
+ COMMUTATIVE_OPS.has(na.opcode) &&
2395
+ na.successors.length === 0 &&
2396
+ na.operands.length === 2 &&
2397
+ ((na.operands[0] === va && na.operands[1] === vb) || (na.operands[0] === vb && na.operands[1] === va))
2398
+ );
2399
+ }
2400
+ if (
2401
+ opShape(na) !== opShape(nb) ||
2402
+ na.successors.length > 0 ||
2403
+ nb.successors.length > 0 ||
2404
+ (MEM_BASE_OPS.has(na.opcode) && counted(na)) ||
2405
+ na.operands.length !== nb.operands.length ||
2406
+ !na.operands.every((v, i) => (v === va ? nb.operands[i] === vb : v === nb.operands[i] && v !== vb))
2407
+ ) {
2408
+ return false;
2409
+ }
2410
+ ca = na;
2411
+ cb = nb;
2412
+ }
2413
+ return false;
2414
+ }
2415
+
2416
+ /** Each read's sole-use chain up through `add`/`sub` ops, as the ops it passes and the read's sign in
2417
+ * each: entering an `add`, or a `sub` as its minuend, keeps the sign; entering a `sub` as its
2418
+ * subtrahend flips it. The value of every op on the chain is then that sign times the read, plus
2419
+ * terms the read is not in. */
2420
+ function additiveChain(rules: MaterializeRules, read: Op): Map<Op, { sign: number; operand: number }> {
2421
+ const { useSitesOf } = rules.uses;
2422
+ const chain = new Map<Op, { sign: number; operand: number }>();
2423
+ let v = read.results[0];
2424
+ let sign = 1;
2425
+ for (let depth = 0; depth < 8 && v !== undefined; depth++) {
2426
+ const uses = useSitesOf.get(v);
2427
+ if (uses?.length !== 1) {
2428
+ break;
2429
+ }
2430
+ const n = uses[0].op;
2431
+ const operand = n.operands.indexOf(v);
2432
+ if (
2433
+ (n.opcode !== 'add' && n.opcode !== 'sub') ||
2434
+ n.operands.length !== 2 ||
2435
+ n.results.length !== 1 ||
2436
+ n.successors.length > 0 ||
2437
+ n.operands.lastIndexOf(v) !== operand
2438
+ ) {
2439
+ break;
2440
+ }
2441
+ sign = n.opcode === 'sub' && operand === 1 ? -sign : sign;
2442
+ chain.set(n, { sign, operand });
2443
+ v = n.results[0];
2444
+ }
2445
+ return chain;
2446
+ }
2447
+
2448
+ /** The two reads meet in one sum with the same sign: their sole-use chains run through `add`/`sub`
2449
+ * ops only, to an op one reaches through each operand, where each counts with the same sign. The
2450
+ * sum is then the same whichever read is made first, however the compiler reassociated it:
2451
+ * ido 7.1 compiles `(gR - x) + (gR - x)` as `((gR - x) + gR) - x`, where one read passes a `sub`
2452
+ * before the `add` they meet in and the other does not, so the chains are not mirrored. `gR - gR`
2453
+ * meets with opposite signs and stays sequenced. Wrapping arithmetic keeps the sum associative.
2454
+ * The value is then right in either order, but the match is not: the earlier read must also be
2455
+ * the one the expression spells first, entering the meeting op as its left operand, or a compiler
2456
+ * that reads operands left to right reverses the asm's two loads. mwcc 2.4.2 reads
2457
+ * `gMS.f + ((t + gMS.f) + t)`'s right-hand `gMS.f` first, so spelling both in one expression
2458
+ * there puts the asm's second load first. */
2459
+ function sameSignInOneSum(rules: MaterializeRules, earlier: Op, later: Op): boolean {
2460
+ const ofEarlier = additiveChain(rules, earlier);
2461
+ if (ofEarlier.size === 0) {
2462
+ return false;
2463
+ }
2464
+ for (const [n, inLater] of additiveChain(rules, later)) {
2465
+ const inEarlier = ofEarlier.get(n);
2466
+ if (inEarlier !== undefined) {
2467
+ return inEarlier.operand === 0 && inLater.operand === 1 && inEarlier.sign === inLater.sign;
2468
+ }
2469
+ }
2470
+ return false;
2471
+ }
2472
+
2264
2473
  /** The rules for a memory access — a `call`, `load` or `aload` — in order; the first that names
2265
2474
  * the op ends the walk. */
2266
2475
  function nameAccess(rules: MaterializeRules, state: MaterializeState, op: Op, b: Block): void {
@@ -0,0 +1,23 @@
1
+ // asmlift structurer — the IR's integer arithmetic opcodes and the L3 operator each one renders
2
+ // as. The float ops are FLOAT_TO_BIN in structure.ts, which says why they are not here.
3
+ import type { BinOp } from '../l3/ast';
4
+
5
+ export const ARITH_TO_BIN: Record<string, BinOp> = {
6
+ add: '+',
7
+ sub: '-',
8
+ mul: '*',
9
+ sdiv: '/',
10
+ // the UNSIGNED quotient/remainder — the C backend spells them `/`/`%` over an operand it casts
11
+ // unsigned (l3/ast.ts BinOp, backend/cfamily.ts C_SPELLING)
12
+ udiv: '/u',
13
+ smod: '%',
14
+ umod: '%u',
15
+ or: '|',
16
+ and: '&',
17
+ xor: '^',
18
+ shl: '<<',
19
+ shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
20
+ shr_s: '>>',
21
+ logic_and: '&&',
22
+ logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
23
+ };