@asmlift/core 0.9.0 → 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 (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -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
  *
@@ -892,7 +892,7 @@ export interface AnalyzeOptions {
892
892
  * A compiler behavior (TargetDescription.compilerBehaviors `readsStayWhereWritten`), not
893
893
  * a differ-refereed variation, and that distinction is the argument: a variation exists where the asm
894
894
  * 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
895
+ * (`/signed-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
896
896
  * block the source spelled it in, re-spelling it at the block the asm read in reproduces that
897
897
  * asm while the sunk spelling is one it emits only for a source that read per arm — nothing for
898
898
  * the differ to referee, and the extra candidate is pure cost. Which compilers may declare
@@ -1521,6 +1521,9 @@ interface LoopRules {
1521
1521
  /** The def's value enters some loop's header live and every consumer sits outside that loop, as
1522
1522
  * does the def: the value is carried ACROSS the loop, not into it. */
1523
1523
  liveAcrossLoop(def: Op, r: Value, consumers: Op[]): boolean;
1524
+ /** `liveAcrossLoop`, and neither the def nor any consumer sits in any loop, enclosing ones
1525
+ * included: never a value the compiler could have hoisted out of one. */
1526
+ carriedPastLoops(def: Op, r: Value, consumers: Op[]): boolean;
1524
1527
  /** Does `r`, computed by `op` in `L`'s body, read a loop variable the update overwrites? */
1525
1528
  readsPreUpdate(L: BottomTestedLoop, op: Op, r: Value): boolean;
1526
1529
  escapesAheadOfUpdate(op: Op, r: Value, consumers: Op[]): boolean;
@@ -1551,6 +1554,9 @@ function makeLoopRules(a: {
1551
1554
  !L.body.has(opBlock.get(def)!) &&
1552
1555
  consumers.every((c) => !L.body.has(opBlock.get(c)!)),
1553
1556
  );
1557
+ const carriedPastLoops = (def: Op, r: Value, consumers: Op[]): boolean =>
1558
+ liveAcrossLoop(def, r, consumers) &&
1559
+ loopBodies.every((L) => !L.body.has(opBlock.get(def)!) && consumers.every((c) => !L.body.has(opBlock.get(c)!)));
1554
1560
  /** THE PRE-UPDATE ESCAPE. A value a bottom-tested loop computes and the code after it reads,
1555
1561
  * whose expression reads a loop variable — `do { s = &f->v; f = f->next; } while (--n); *out =
1556
1562
  * s;`. Inlined at the post-loop read it re-reads that variable's NAME, which by then holds the
@@ -1710,6 +1716,7 @@ function makeLoopRules(a: {
1710
1716
  multiBlockHeaders,
1711
1717
  bottomTested,
1712
1718
  liveAcrossLoop,
1719
+ carriedPastLoops,
1713
1720
  readsPreUpdate,
1714
1721
  escapesAheadOfUpdate,
1715
1722
  rebuiltPast,
@@ -2042,6 +2049,20 @@ function namePureOp(rules: MaterializeRules, state: MaterializeState, op: Op, b:
2042
2049
  } else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !scopes.addressCone(op)) {
2043
2050
  materialize.add(op);
2044
2051
  }
2052
+ // A PURE VALUE CARRIED ACROSS A LOOP, computed and read outside every loop, as `nameAccess` names
2053
+ // a read (`carriedPastLoops`; a value read inside a loop is `/expr-home`'s question, where loop.c's
2054
+ // invariant motion may have put it). `j = n * 3; for (…) { if (t[i] == 7) { gHit = j; break; } }`
2055
+ // computes `j` ahead of the loop and holds it in a register to the break arm, which sits outside
2056
+ // the natural loop. Inlined, `gHit = n * 3` moves the computation into the arm, and every profile
2057
+ // compiles it there (IDO scores the two alike). A `const` is re-materialized where it is used, and
2058
+ // an address loses its inline byte-stride cast standing alone and is what a compiler CSEs and
2059
+ // moves freely, so neither is named.
2060
+ if (op.opcode !== 'const' && pr && useSitesOf.has(pr) && !scopes.addressCone(op) && !rendersAsAddress(op)) {
2061
+ const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
2062
+ if (loops.carriedPastLoops(op, pr, cons)) {
2063
+ materialize.add(op);
2064
+ }
2065
+ }
2045
2066
  // A float PRODUCT read by a float add or subtract, directly or through a negation, is named
2046
2067
  // on a compiler that contracts (target.ts `contractsFloatProducts`): it fuses only WITHIN
2047
2068
  // one expression, so the inline `a * b + c` recompiles to one `fmadds` — one rounding, a
@@ -2248,7 +2269,7 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
2248
2269
  // order is the compiler's to choose (`gY = VCOUNT - TM0CNT_L` reads VCOUNT first); a
2249
2270
  // qualified read spelled as a bare statement because nothing uses it is an access too.
2250
2271
  if (once && !isCall && (x.opcode === 'load' || x.opcode === 'aload') && counted(x)) {
2251
- return true;
2272
+ return !interchangeableReads(rules, x, op);
2252
2273
  }
2253
2274
  if (!isCall) {
2254
2275
  return false;
@@ -2261,6 +2282,148 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
2261
2282
  return false;
2262
2283
  }
2263
2284
 
2285
+ const COMMUTATIVE_OPS: ReadonlySet<string> = new Set(['add', 'mul', 'and', 'or', 'xor']);
2286
+
2287
+ /** One op's identity apart from its operands: opcode, result count and attributes, keys sorted. */
2288
+ const opShape = (op: Op): string =>
2289
+ `${op.opcode}/${op.results.length}/${JSON.stringify(Object.entries(op.attrs).sort(([a], [b]) => a.localeCompare(b)))}`;
2290
+
2291
+ /** Do two values name the same address: one value, or two `gaddr`s of one symbol? */
2292
+ function sameAddress(defOf: Map<Value, Op>, a: Value, b: Value): boolean {
2293
+ if (a === b) {
2294
+ return true;
2295
+ }
2296
+ const da = defOf.get(a);
2297
+ const db = defOf.get(b);
2298
+ return (
2299
+ da !== undefined &&
2300
+ db !== undefined &&
2301
+ da.opcode === 'gaddr' &&
2302
+ db.opcode === 'gaddr' &&
2303
+ opShape(da) === opShape(db)
2304
+ );
2305
+ }
2306
+
2307
+ /** Are two qualified reads of ONE cell interchangeable in the one expression they feed: the same
2308
+ * access of the same address, each the sole use of its value up a chain of ops alike but for it,
2309
+ * the two chains meeting as the two operands of one commutative op? Then the expression's value is
2310
+ * the same whichever read the compiler makes first, and its order is the compiler's to choose:
2311
+ * `gR + gR` over `extern volatile u32 gR;` and `*gPV + *gPV` over `u16 *volatile gPV` read the
2312
+ * object twice, in an order the source never stated, so naming the first read into a temp spells
2313
+ * an order the source did not have (ido 7.1 loads `gR + gR` in the other order). The meeting op
2314
+ * may also be a sum both reads enter with one sign, past a reassociation (`sameSignInOneSum`).
2315
+ * `gR - gR` is not symmetric — the order decides which read is the minuend — and stays sequenced.
2316
+ * `a` is the later read, `b` the earlier one placed past it. */
2317
+ function interchangeableReads(rules: MaterializeRules, a: Op, b: Op): boolean {
2318
+ const { defOf } = rules;
2319
+ if (
2320
+ a === b ||
2321
+ opShape(a) !== opShape(b) ||
2322
+ a.operands.length !== b.operands.length ||
2323
+ !a.operands.every((v, i) => sameAddress(defOf, v, b.operands[i]))
2324
+ ) {
2325
+ return false;
2326
+ }
2327
+ return mirroredChains(rules, a, b) || sameSignInOneSum(rules, b, a);
2328
+ }
2329
+
2330
+ /** The two reads' sole-use chains are alike but for them and meet as the two operands of one
2331
+ * commutative op: `(gR << 1) * (gR << 1)`. */
2332
+ function mirroredChains(rules: MaterializeRules, a: Op, b: Op): boolean {
2333
+ const { useSitesOf } = rules.uses;
2334
+ let ca = a;
2335
+ let cb = b;
2336
+ for (let depth = 0; depth < 8; depth++) {
2337
+ const va = ca.results[0];
2338
+ const vb = cb.results[0];
2339
+ const ua = va === undefined ? undefined : useSitesOf.get(va);
2340
+ const ub = vb === undefined ? undefined : useSitesOf.get(vb);
2341
+ if (ua?.length !== 1 || ub?.length !== 1) {
2342
+ return false;
2343
+ }
2344
+ const na = ua[0].op;
2345
+ const nb = ub[0].op;
2346
+ if (na === nb) {
2347
+ return (
2348
+ COMMUTATIVE_OPS.has(na.opcode) &&
2349
+ na.successors.length === 0 &&
2350
+ na.operands.length === 2 &&
2351
+ ((na.operands[0] === va && na.operands[1] === vb) || (na.operands[0] === vb && na.operands[1] === va))
2352
+ );
2353
+ }
2354
+ if (
2355
+ opShape(na) !== opShape(nb) ||
2356
+ na.successors.length > 0 ||
2357
+ nb.successors.length > 0 ||
2358
+ (MEM_BASE_OPS.has(na.opcode) && counted(na)) ||
2359
+ na.operands.length !== nb.operands.length ||
2360
+ !na.operands.every((v, i) => (v === va ? nb.operands[i] === vb : v === nb.operands[i] && v !== vb))
2361
+ ) {
2362
+ return false;
2363
+ }
2364
+ ca = na;
2365
+ cb = nb;
2366
+ }
2367
+ return false;
2368
+ }
2369
+
2370
+ /** Each read's sole-use chain up through `add`/`sub` ops, as the ops it passes and the read's sign in
2371
+ * each: entering an `add`, or a `sub` as its minuend, keeps the sign; entering a `sub` as its
2372
+ * subtrahend flips it. The value of every op on the chain is then that sign times the read, plus
2373
+ * terms the read is not in. */
2374
+ function additiveChain(rules: MaterializeRules, read: Op): Map<Op, { sign: number; operand: number }> {
2375
+ const { useSitesOf } = rules.uses;
2376
+ const chain = new Map<Op, { sign: number; operand: number }>();
2377
+ let v = read.results[0];
2378
+ let sign = 1;
2379
+ for (let depth = 0; depth < 8 && v !== undefined; depth++) {
2380
+ const uses = useSitesOf.get(v);
2381
+ if (uses?.length !== 1) {
2382
+ break;
2383
+ }
2384
+ const n = uses[0].op;
2385
+ const operand = n.operands.indexOf(v);
2386
+ if (
2387
+ (n.opcode !== 'add' && n.opcode !== 'sub') ||
2388
+ n.operands.length !== 2 ||
2389
+ n.results.length !== 1 ||
2390
+ n.successors.length > 0 ||
2391
+ n.operands.lastIndexOf(v) !== operand
2392
+ ) {
2393
+ break;
2394
+ }
2395
+ sign = n.opcode === 'sub' && operand === 1 ? -sign : sign;
2396
+ chain.set(n, { sign, operand });
2397
+ v = n.results[0];
2398
+ }
2399
+ return chain;
2400
+ }
2401
+
2402
+ /** The two reads meet in one sum with the same sign: their sole-use chains run through `add`/`sub`
2403
+ * ops only, to an op one reaches through each operand, where each counts with the same sign. The
2404
+ * sum is then the same whichever read is made first, however the compiler reassociated it:
2405
+ * ido 7.1 compiles `(gR - x) + (gR - x)` as `((gR - x) + gR) - x`, where one read passes a `sub`
2406
+ * before the `add` they meet in and the other does not, so the chains are not mirrored. `gR - gR`
2407
+ * meets with opposite signs and stays sequenced. Wrapping arithmetic keeps the sum associative.
2408
+ * The value is then right in either order, but the match is not: the earlier read must also be
2409
+ * the one the expression spells first, entering the meeting op as its left operand, or a compiler
2410
+ * that reads operands left to right reverses the asm's two loads. mwcc 2.4.2 reads
2411
+ * `gMS.f + ((t + gMS.f) + t)`'s right-hand `gMS.f` first, so spelling both in one expression
2412
+ * there puts the asm's second load first. */
2413
+ function sameSignInOneSum(rules: MaterializeRules, earlier: Op, later: Op): boolean {
2414
+ const ofEarlier = additiveChain(rules, earlier);
2415
+ if (ofEarlier.size === 0) {
2416
+ return false;
2417
+ }
2418
+ for (const [n, inLater] of additiveChain(rules, later)) {
2419
+ const inEarlier = ofEarlier.get(n);
2420
+ if (inEarlier !== undefined) {
2421
+ return inEarlier.operand === 0 && inLater.operand === 1 && inEarlier.sign === inLater.sign;
2422
+ }
2423
+ }
2424
+ return false;
2425
+ }
2426
+
2264
2427
  /** The rules for a memory access — a `call`, `load` or `aload` — in order; the first that names
2265
2428
  * the op ends the walk. */
2266
2429
  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
+ };
@@ -1,17 +1,29 @@
1
1
  // THE GLOBAL-ACCESS ADDRESS DECOMPOSITION — the pure half of the symbol-map access spellings.
2
2
  //
3
- // Everything here is a function of an `Expr`, a `SymbolInfo` and a width: no structurer state, no
4
- // naming walk, no loop context. It answers three questions the access spellings in structure.ts
5
- // ask over and over — is this address `&gSym` plus something; does that something divide into
6
- // whole elements; and do the terms of it name the DECLARED subscripts of a multidimensional array
7
- // — and it answers them the same way for every caller, which is the point of the split: the
8
- // rank-pinning fallback and the declared-subscript recovery share `bareArrayElement`, so the two
9
- // cannot disagree about what a bare element spelling is.
3
+ // Everything here is a function of an `Expr`, the map's declarations and a width: no structurer
4
+ // state, no naming walk, no loop context. It answers three questions the access spellings in
5
+ // structure.ts ask over and over — is this address `&gSym` plus something; does that something
6
+ // divide into whole elements; and do the terms of it name the DECLARED subscripts of a
7
+ // multidimensional array — and it answers them the same way for every caller, which is the point
8
+ // of the split: the rank-pinning fallback and the declared-subscript recovery share
9
+ // `bareArrayElement`, so the two cannot disagree about what a bare element spelling is. The same
10
+ // holds for what the map declares a named member to be (`ptrMemberDecl`), which the access folds
11
+ // and the pointer spelling (pointer-spelling.ts) both read.
10
12
  import type { Expr } from '../l3/ast';
11
- import { type SymbolInfo, arrayInnerExtents } from '../symbols';
13
+ import {
14
+ type DeclaredField,
15
+ type SymbolInfo,
16
+ type SymbolStructField,
17
+ arrayInnerExtents,
18
+ declaredFields,
19
+ isArrayField,
20
+ isBitfieldField,
21
+ isPtrField,
22
+ pointeeFields,
23
+ } from '../symbols';
12
24
 
13
25
  // `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
14
- // (`(u32)&gSym` — see `intifyAddr` in structure.ts's lowerDef): both spell the same link-time
26
+ // (`(u32)&gSym` — see `ARITH_ROWS` in pointer-spelling.ts): both spell the same link-time
15
27
  // constant, so the fold rules match through the cast and every access that CAN spell a named
16
28
  // element still does.
17
29
  // WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
@@ -298,3 +310,93 @@ export function addOffset(idx: Expr, n: number): Expr {
298
310
  ? { k: 'const', value: idx.value + n }
299
311
  : { k: 'bin', op: '+', l: idx, r: { k: 'const', value: n } };
300
312
  }
313
+
314
+ /** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
315
+ * The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
316
+ * qualifier the name reintroduces changes what the compiler emits:
317
+ * • `volatile` makes the access observable — the load may no longer be folded or reordered,
318
+ * which is a different instruction sequence (measured: 6 insns where the cast form was 5);
319
+ * • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
320
+ * Either way the honest spelling is the cast form, so the member simply is not nameable here. */
321
+ export function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
322
+ if (f.volatile) {
323
+ return false;
324
+ }
325
+ return !(isStore && (f.const || containerConst));
326
+ }
327
+
328
+ /** The member a `width`-byte access at byte `byte` of a struct global names, `gSym.member`: one at
329
+ * that offset and of that size, no array or bitfield, whose qualifiers allow the access. An ARRAY
330
+ * member would match a byte access by (offset, size) and spell `.x`, which is not an lvalue of
331
+ * that width; a plain read of a BITFIELD's bytes is not a read of its bits. */
332
+ export function structMemberAt(
333
+ si: SymbolInfo,
334
+ byte: number,
335
+ width: number,
336
+ isStore: boolean,
337
+ ): DeclaredField | undefined {
338
+ if (si.shape !== 'struct') {
339
+ return undefined;
340
+ }
341
+ const f = declaredFields(si.layout)?.find(
342
+ (x) => x.offset === byte && x.size === width && !isArrayField(x) && !isBitfieldField(x),
343
+ );
344
+ return f !== undefined && memberQualsAllow(f, si.const, isStore) ? f : undefined;
345
+ }
346
+
347
+ /** The members a symbol's declaration seats: a struct global's own, or a pointer global's
348
+ * pointee's. */
349
+ export interface MemberLookup {
350
+ fieldsOf(name: string): DeclaredField[] | null;
351
+ }
352
+
353
+ /** {@link MemberLookup.fieldsOf} over `info`, MEMOIZED per symbol. `declaredFields` validates every
354
+ * member and returns a fresh sorted copy on every call, and pointer-spelling.ts's `roleOf`
355
+ * asks it for every member operand of every arithmetic op lowered, so an uncached lookup is an
356
+ * O(n log n) allocation on a hot path — inside a `structure()` a ranked run repeats once per
357
+ * candidate, 17,856 times on the largest fan. */
358
+ export function memoFieldsOf(info: (name: string) => SymbolInfo | undefined): MemberLookup['fieldsOf'] {
359
+ const cache = new Map<string, DeclaredField[] | null>();
360
+ return (name) => {
361
+ const hit = cache.get(name);
362
+ if (hit !== undefined) {
363
+ return hit;
364
+ }
365
+ const si = info(name);
366
+ const fields =
367
+ si?.shape === 'struct' ? declaredFields(si.layout) : si?.shape === 'pointer' ? pointeeFields(si.pointee) : null;
368
+ cache.set(name, fields);
369
+ return fields;
370
+ };
371
+ }
372
+
373
+ /** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
374
+ * does the declaration say about this member", for rules that must reason about a member's type
375
+ * after the access rules have already spelled it. Both named spellings resolve, through the same
376
+ * shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
377
+ * `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
378
+ * recovered-struct spelling, which no map declares — resolves to null, and so does any base that
379
+ * is not a map-shaped global, which is what makes every caller refuse rather than guess.
380
+ *
381
+ * The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
382
+ * spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
383
+ * is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
384
+ * down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
385
+ * is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
386
+ * of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
387
+ * the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
388
+ * it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
389
+ export function declaredMemberOf(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
390
+ if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
391
+ return null;
392
+ }
393
+ return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
394
+ }
395
+
396
+ /** The map member a `field` node names when the declaration makes it a POINTER — {@link
397
+ * isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
398
+ * disagree about what a member is. */
399
+ export function ptrMemberDecl(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
400
+ const f = declaredMemberOf(x, sym);
401
+ return f !== null && isPtrField(f) ? f : null;
402
+ }