@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
@@ -47,12 +47,15 @@
47
47
  // at an offset natural C alignment could not place it at) is rejected LOUD. An overlap is a union
48
48
  // member on a base whose layout only this function's accesses describe, and no struct at all on
49
49
  // one whose address is declared elsewhere (`recognizeStructs`).
50
- import { constAddressOf, globalCellOf } from '../ir/alias';
50
+ import { constAddressOf, constDisplacementOf, globalCellOf } from '../ir/alias';
51
51
  import { Fn, Op, Value, defOpMap } from '../ir/core';
52
+ import { placedAt } from '../ir/discipline';
52
53
  import { nextStructIndex } from '../ir/struct-names';
53
- import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
54
+ import { IrType, StructField, T, UNION_VIEW_NAMES, intWidth, scalarTypeForAccess, unionViewSigns } from '../ir/types';
54
55
  import type { StructType } from '../l3/ast';
56
+ import { type Gate, firstRejection } from '../l3/gates';
55
57
  import { RaiseUnsupportedError, StructOverlapError } from './errors';
58
+ import { storeSigned } from './storesign';
56
59
 
57
60
  /** A single observed constant-offset access to a base: the op itself and its program point, its
58
61
  * byte offset, its access width (bytes), and the signedness a recovered field would take from it.
@@ -62,10 +65,11 @@ import { RaiseUnsupportedError, StructOverlapError } from './errors';
62
65
  * like" — and a second copy of the walk would let them come to disagree about the answer while
63
66
  * each looked right on its own.
64
67
  *
65
- * SIGNEDNESS ON THE STORE HALF IS A POLICY, not a reading: a store carries none, and a word store
66
- * makes an `s32` field where a narrower one makes an unsigned one. `buildStruct` then prefers a
67
- * LOAD's own signedness at the same offset, which is the reading. raise/truncload.ts never sees
68
- * this field — its `covering-store` rule refuses a store cover before the signedness is used. */
68
+ * SIGNEDNESS ON THE STORE HALF IS A POLICY, not a reading, unless raise/storesign.ts stamped the
69
+ * store from the constant it stores: otherwise a word store makes an `s32` field where a narrower
70
+ * one makes an unsigned one. `buildStruct` then prefers a LOAD's own signedness at the same offset,
71
+ * which is the reading, over either. raise/truncload.ts never sees this field — its
72
+ * `covering-store` rule refuses a store cover before the signedness is used. */
69
73
  export interface Access {
70
74
  op: Op;
71
75
  /** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
@@ -78,6 +82,9 @@ export interface Access {
78
82
  isLoad: boolean;
79
83
  }
80
84
 
85
+ /** A store whose signedness raise/storesign.ts read off the constant it stores. */
86
+ const isStamped = (a: Access): boolean => !a.isLoad && a.op.attrs.signed !== undefined;
87
+
81
88
  /** What one walk of a function yields about its constant-offset memory accesses. */
82
89
  export interface ConstOffsetAccesses {
83
90
  /** every base's constant-offset accesses, in program order. */
@@ -124,7 +131,7 @@ export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
124
131
  at,
125
132
  off: op.attrs.off as number,
126
133
  width: op.attrs.width as number,
127
- signed: (op.attrs.width as number) === 4,
134
+ signed: storeSigned(op, op.attrs.width as number),
128
135
  isLoad: false,
129
136
  });
130
137
  break;
@@ -169,25 +176,35 @@ function isArray(accesses: Access[]): boolean {
169
176
  * not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
170
177
  * cover on a path the narrow read does not run — and every one of them throws here.
171
178
  *
172
- * A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Three throw sites
179
+ * A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Four throw sites
173
180
  * below reject a layout and they fall into two CLASSES, which is the split a caller reads:
174
- * • `StructOverlapError` — two accesses whose byte ranges collide. TWO of the three sites raise
175
- * it, and they carry the same "unions not modelled" text at different offsets: a second access
176
- * at an offset already taken with a different width, and one whose range straddles the field
177
- * before it (the `aligned > f.off` arm, which is the only one that can see `{s32@0, s16@2}`).
181
+ * • `StructOverlapError` — two accesses whose byte ranges collide. THREE of the four sites raise
182
+ * it, each message ending "unions not modelled": a second access
183
+ * at an offset already taken with a different width, a second LOAD of a narrow cell with the
184
+ * other extension (one field would spell one of the two reads wrong), and one whose range
185
+ * straddles the field before it (the `aligned > f.off` arm, which is the only one that can see
186
+ * `{s32@0, s16@2}`).
178
187
  * Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
179
188
  * declared outside this function's access set can keep its untyped spelling and lift
180
189
  * (`recognizeStructs` below). There is no struct to synthesize, and nothing is lost by not
181
- * synthesizing one. Any other base is declared with a union member (`buildUnionStruct`).
190
+ * synthesizing one. Any other base is declared with a union member, or a `castViews` field
191
+ * where the union would not fit (`buildUnionStruct`).
182
192
  * • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
183
193
  * at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
184
194
  * width folds to the element index `off / width` (structure/structure.ts), which for a
185
195
  * misaligned pair is a FRACTION and not C. It declines whatever the base is. */
186
- function buildStruct(name: string, accesses: Access[]): IrType {
196
+ function buildStruct(name: string, accesses: Access[], extensionViews = true): IrType {
187
197
  // One field per distinct offset; a load's signedness wins over a store's (more information).
188
198
  const byOff = new Map<number, Access>();
199
+ // The extension of every LOAD seen at an offset, kept apart from `byOff`, whose access can be a
200
+ // store (kept over an unsigned load): the verdict must not hang on the order of the accesses.
201
+ const loadedSigned = new Map<number, boolean>();
189
202
  for (const a of accesses) {
190
203
  const prev = byOff.get(a.off);
204
+ const seen = a.isLoad ? loadedSigned.get(a.off) : undefined;
205
+ if (a.isLoad && seen === undefined) {
206
+ loadedSigned.set(a.off, a.signed);
207
+ }
191
208
  if (!prev) {
192
209
  byOff.set(a.off, a);
193
210
  continue;
@@ -197,9 +214,23 @@ function buildStruct(name: string, accesses: Access[]): IrType {
197
214
  `cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
198
215
  );
199
216
  }
200
- if (a.signed && !prev.signed) {
217
+ // Two LOADS of one narrow cell with different extensions (`ldrh` and `ldrsh`) are two views of
218
+ // it, and one field types only one of them: the other read would be spelled through the wrong
219
+ // extension, a silent wrong (`u16` read as `s16` sign-extends). It is the same overlap class as
220
+ // two widths at one offset, so it goes the same way: `buildUnionStruct` declares the cell's two
221
+ // views, a declared address keeps its per-access casts. A word has no extension to disagree on.
222
+ // `extensionViews` false switches off this overlap class alone (the no-layout gate says why).
223
+ if (extensionViews && seen !== undefined && a.signed !== seen && a.width < 4) {
224
+ throw new StructOverlapError(
225
+ `cannot recover struct '${name}': the ${a.width}-byte cell at offset ${a.off} is loaded both sign- and zero-extended — unions not modelled`,
226
+ );
227
+ }
228
+ // A load reads its signedness, and a stamped store only stands in for one: a load replaces a
229
+ // stamped store, and otherwise the signed view wins, but never a stamped store's over a load's.
230
+ const replace = isStamped(prev) && a.isLoad ? true : a.signed && !prev.signed && !(isStamped(a) && prev.isLoad);
231
+ if (replace) {
201
232
  byOff.set(a.off, a);
202
- } // prefer the signed (load-derived) view
233
+ }
203
234
  }
204
235
  const dataFields: StructField[] = [...byOff.values()]
205
236
  .sort((x, y) => x.off - y.off)
@@ -253,10 +284,6 @@ function placeFields(name: string, dataFields: StructField[]): IrType {
253
284
  return T.struct(name, fields);
254
285
  }
255
286
 
256
- /** The member name of each view a union holds, by access width. A width with no entry has no view
257
- * (the deref contract admits no other scalar width either), and its overlap still declines. */
258
- const VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
259
-
260
287
  /** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
261
288
  * `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
262
289
  * written at more than one width are views of one cell, and a union is the C that says so.
@@ -281,14 +308,15 @@ const VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4:
281
308
  * one element at the cell's start is a scalar; any other is an array reaching the furthest
282
309
  * element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
283
310
  * any other is a member `field_<off>` holding a union of its views — including a single-width cell
284
- * loaded both ways. KNOWN GAP: on a base with no overlap anywhere, `buildStruct` never hands over,
285
- * and that same cell there is one field typed by its signed load, so its unsigned read is spelled sign-extended.
311
+ * loaded both ways, which `buildStruct` hands over as an overlap even on a base with no other one.
286
312
  *
287
313
  * THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
288
314
  * aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
289
315
  * agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
290
316
  * it two. A union that boundary would move — seated off it, or with the next field inside its
291
- * rounded size — declines rather than mislaying every access at or after it. An UNMEASURED
317
+ * rounded size — is never declared, since it would mislay every access at or after it. Its cell
318
+ * is a plain field of its widest view instead, read at its other views through casts, where those
319
+ * casts alias it as the union would, and otherwise it declines (`castViewCell`). An UNMEASURED
292
320
  * compiler (undefined) declines every union, since no boundary is safe to assume: one too small
293
321
  * mislays the fields after the union on agbcc, one too large drops the pad in front of them
294
322
  * everywhere else.
@@ -311,17 +339,16 @@ function buildUnionStruct(structName: string, accesses: Access[], aggregateBound
311
339
  const start = c[0].off;
312
340
  const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
313
341
  const group = c.filter((a) => a.width === width);
314
- const name = VIEW_NAMES[width];
342
+ const name = UNION_VIEW_NAMES[width];
315
343
  if (name === undefined) {
316
344
  throw new RaiseUnsupportedError(
317
345
  `cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
318
346
  );
319
347
  }
320
- // The extensions this width is LOADED with, unsigned first; a word has one type whatever its
321
- // loads say, and a width only stored gets the unsigned view.
322
- const loaded = new Set(group.filter((a) => a.isLoad).map((a) => a.signed));
323
- const signs = width === 4 ? [true] : [false, true].filter((sg) => loaded.has(sg));
324
- const views = signs.length === 0 ? [false] : signs;
348
+ const views = unionViewSigns(
349
+ width,
350
+ group.map((a) => ({ isLoad: a.isLoad, signed: a.signed, stamped: isStamped(a) })),
351
+ );
325
352
  const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
326
353
  return views.map((signed): StructField => {
327
354
  const reads = group.filter((a) => viewOf(a) === signed);
@@ -341,18 +368,46 @@ function buildUnionStruct(structName: string, accesses: Access[], aggregateBound
341
368
  }
342
369
  return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
343
370
  });
344
- dataFields.forEach((f, i) => {
371
+ const fitted = dataFields.map((f, i): StructField => {
345
372
  if (f.type.kind !== 'union') {
346
- return;
373
+ return f;
347
374
  }
348
375
  const next = dataFields[i + 1];
349
- if (f.off % f.type.size !== 0 || (next !== undefined && next.off < f.off + f.type.size)) {
376
+ if (f.off % f.type.size === 0 && (next === undefined || next.off >= f.off + f.type.size)) {
377
+ return f;
378
+ }
379
+ const widest = castViewCell(f.type.members);
380
+ if (widest === undefined) {
350
381
  throw new RaiseUnsupportedError(
351
382
  `cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
352
383
  );
353
384
  }
385
+ return { off: f.off, type: widest, name: f.name, castViews: true };
354
386
  });
355
- return placeFields(structName, dataFields);
387
+ return placeFields(structName, fitted);
388
+ }
389
+
390
+ /** The type a union's cell is declared at when the union itself cannot be laid out, or undefined
391
+ * when its views cannot be spelled as casts of that one field. The field is the widest view, the
392
+ * unsigned one where that width has two; every other view must be ALIAS-TRANSPARENT to it, so a cast access aliases the field exactly as a
393
+ * union member would: a 1-byte view (a character type, alias set 0 — c-common.c
394
+ * `c_get_alias_set`) or the field's own width with the other extension (s16 and u16 share one
395
+ * set). Any other view is refused: a `u16` read of a word cell through a cast is reused across a
396
+ * word store on agbcc, where the union reloads (`synthetic:ureread`).
397
+ *
398
+ * NO MEASURED COMPILER REACHES THE REFUSAL. Every target but agbcc has a 1-byte boundary, where a
399
+ * union of aligned views is never larger than its widest one and always fits. On agbcc a cell
400
+ * the size of the boundary is aligned and fits, a misaligned one is `refusePacked`'s, so only a
401
+ * cell of 2 bytes or fewer gets here — and its views are bytes or its own width. The refusal is
402
+ * the guard for a compiler whose boundary is wider, which the test with a boundary of 8 stands in
403
+ * for. */
404
+ function castViewCell(members: readonly StructField[]): IrType | undefined {
405
+ const width = (m: StructField): number => intWidth(m.type.kind === 'array' ? m.type.elem : m.type) ?? 0;
406
+ const widest = Math.max(...members.map(width));
407
+ const cells = members.filter((m) => width(m) === widest && m.type.kind === 'int');
408
+ const cell = cells.find((m) => m.type.kind === 'int' && !m.type.signed) ?? cells[0];
409
+ const transparent = members.every((m) => width(m) === 8 || width(m) === widest);
410
+ return transparent && cell !== undefined ? cell.type : undefined;
356
411
  }
357
412
 
358
413
  /** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
@@ -370,6 +425,305 @@ function fieldSize(t: IrType): number {
370
425
  }
371
426
  }
372
427
 
428
+ export interface DisplacedOptions {
429
+ /** leave every STORE on its materialised address, folding only the loads (`/far-cast`) */
430
+ keepStores?: boolean;
431
+ /** called when a lift without `keepStores` folds a store, which is what offers `/far-cast` */
432
+ onFoldedStore?: () => void;
433
+ }
434
+
435
+ /** The target facts the fold reads (target.ts `compilerBehaviors`). */
436
+ export interface DisplacedBehaviors {
437
+ farMemberStoreOrder?: boolean;
438
+ farMemberBasePerAccess?: { past: number };
439
+ }
440
+
441
+ /** One materialised base, as `DISPLACED_BASE_GATES` judges it. */
442
+ export interface DisplacedBase {
443
+ base: Value;
444
+ accesses: readonly Access[];
445
+ disp: { root: Value; k: number; chain: Value[] };
446
+ defs: Map<Value, Op>;
447
+ arrayBases: Set<Value>;
448
+ /** does every use of `v` go to `next` (a link of the chain), or, at the chain's head, to the
449
+ * BASE operand of a constant-offset load/store? */
450
+ onlyFeeds: (v: Value, next: Value | undefined) => boolean;
451
+ behaviors: DisplacedBehaviors;
452
+ }
453
+
454
+ /** One root and every access the fold would move onto it, as `DISPLACED_ROOT_GATES` judges it. */
455
+ export interface DisplacedRoot {
456
+ root: Value;
457
+ /** the root's own accesses plus the moved ones at their folded offsets */
458
+ merged: readonly Access[];
459
+ /** some moved store's value is computed BEFORE its address — the member's order on a target
460
+ * with `farMemberStoreOrder`, which no cast or held pointer builds */
461
+ memberStoreOrder: boolean;
462
+ }
463
+
464
+ /** The smallest displacement `address-constant` reads as an address rather than a member offset. */
465
+ const ADDRESS_CONSTANT_FLOOR = 1 << 24;
466
+
467
+ export const DISPLACED_BASE_GATES: readonly Gate<DisplacedBase>[] = [
468
+ {
469
+ id: 'not-recovery-root',
470
+ why: 'the root is typed already, a frame, global or literal address, or a variable-index array',
471
+ sound: false,
472
+ rejects: ({ disp: { root: p }, defs, arrayBases }) => {
473
+ const d = defs.get(p)?.opcode;
474
+ return (
475
+ p.type.kind !== 'unknown' ||
476
+ d === 'laddr' ||
477
+ d === 'gaddr' ||
478
+ d === 'const' ||
479
+ arrayBases.has(p) ||
480
+ globalCellOf(defs, p, 0) !== null ||
481
+ constAddressOf(defs, p, 0) !== null
482
+ );
483
+ },
484
+ },
485
+ {
486
+ // `*(u16 *)(x + 0x6000000)`, `*(u8 *)(0x6000000 + i * 2)`: a hardware or segment address added
487
+ // to an index. Read as a member it retypes the integer as a pointer to a struct with a 96 MiB
488
+ // pad. No struct on these machines comes near 16 MiB (GBA EWRAM is 256 KiB, N64 RDRAM 8 MiB,
489
+ // GameCube main RAM 24 MiB in all), and every address region a constant names starts past it.
490
+ id: 'address-constant',
491
+ why: 'a displacement of 16 MiB or more is an address constant added to an index, not a member offset',
492
+ sound: false,
493
+ rejects: ({ disp: { k } }) => Math.abs(k) >= ADDRESS_CONSTANT_FLOOR,
494
+ },
495
+ {
496
+ id: 'claimed-chain',
497
+ why: 'another struct pass already typed a value of the chain, or an indexed access reaches it',
498
+ sound: false,
499
+ rejects: ({ disp: { chain }, arrayBases }) => chain.some((v) => v.type.kind !== 'unknown' || arrayBases.has(v)),
500
+ },
501
+ {
502
+ id: 'held-address',
503
+ why: 'the address is used as a value too, so the source held it',
504
+ sound: false,
505
+ rejects: ({ disp: { chain }, onlyFeeds }) =>
506
+ !chain.every((v, i) => onlyFeeds(v, i === 0 ? undefined : chain[i - 1])),
507
+ },
508
+ {
509
+ id: 'placed-access',
510
+ why: 'a volatile or device-pinned access keeps the address it was written through',
511
+ sound: false,
512
+ rejects: ({ accesses }) => accesses.some((a) => placedAt(a.op) !== null),
513
+ },
514
+ {
515
+ id: 'negative-offset',
516
+ why: 'a folded offset below the root is no member',
517
+ sound: false,
518
+ rejects: ({ accesses, disp: { k } }) => accesses.some((a) => a.off + k < 0),
519
+ },
520
+ {
521
+ id: 'held-pointer-offsets',
522
+ why: 'one materialised base at two offsets is a held pointer where every member access gets its own',
523
+ sound: false,
524
+ rejects: ({ accesses, disp: { k }, behaviors: { farMemberBasePerAccess: perAccess } }) =>
525
+ perAccess !== undefined && Math.abs(k) > perAccess.past && new Set(accesses.map((a) => a.off)).size > 1,
526
+ },
527
+ ];
528
+
529
+ export const DISPLACED_ROOT_GATES: readonly Gate<DisplacedRoot>[] = [
530
+ {
531
+ id: 'array-shaped',
532
+ why: 'uniform-stride or single accesses are no struct evidence, unless a store shows the member order',
533
+ sound: false,
534
+ rejects: ({ merged, memberStoreOrder }) => isArray([...merged]) && !memberStoreOrder,
535
+ },
536
+ {
537
+ id: 'no-layout',
538
+ why: 'the accesses overlap or misalign, so no plain struct lays them out',
539
+ sound: false,
540
+ // A cell loaded with both extensions lays out: `recognizeStructs` gives it a view per extension
541
+ // once the fold has put its reads on one base, and leaving the root unfolded would type the
542
+ // cell by its unfolded `ldrh` alone (agbcc's `ldrsh` has no immediate form, so every signed
543
+ // halfword read is a displaced one). Not for an array-shaped set, which the fold types itself,
544
+ // as one plain struct.
545
+ // KNOWN GAP (correct C): a WIDTH overlap still rejects the root, though `recognizeStructs`
546
+ // would lay it out too. `ldrb [r0]` beside agbcc's `mov r3, #0; ldrsh [r0, r3]` keeps the
547
+ // halfword on its address, `*(s16 *)((u8 *)a0 + 0)`: the right bytes, the right extension.
548
+ rejects: ({ merged }) => {
549
+ try {
550
+ buildStruct('Struct', [...merged], isArray([...merged]));
551
+ return false;
552
+ } catch {
553
+ return true;
554
+ }
555
+ },
556
+ },
557
+ ];
558
+
559
+ /** FOLD A MATERIALISED DISPLACEMENT INTO THE ACCESS: a load/store whose base is `add(p, K)` (or a
560
+ * chain of constant adds/subs off `p`) becomes an access of `p` at `off + K`, so `recognizeStructs`
561
+ * sees it in `p`'s access set and spells it as the member it is.
562
+ *
563
+ * WHY. Every ISA has a displacement range, and a member past it is reached through an address the
564
+ * machine materialises first: Thumb's `adds rX, #38; strb [rX]` (a byte past 31, a half past 62,
565
+ * a word past 124), ido's `addiu at, a0, 32767; sb a1, 7232(at)`, KMC's and GCC 2.7.2's
566
+ * `lui at, 1; addu at, at, a0; sb a1, -25537(at)`, mwcc's `addis r5, r3, 1; stb r4, -25537(r5)`.
567
+ * Unfolded, such an access is keyed on the materialised address, never joins its struct, and is
568
+ * spelled as a cast `*((u8 *)p + 38)`. On agbcc that spelling is a different program: for a
569
+ * COMPONENT_REF target expand_assignment (gcc/expr.c :2535) expands only the base and store_field
570
+ * (:2624) evaluates the right-hand side first, while a cast (INDIRECT_REF) target is expanded whole
571
+ * (:2689) before the right-hand side (:2747). A member store of a loaded value therefore loads it
572
+ * before the `adds #K`, and the cast store after (`sa3:sub_800F0E8`'s offset-38 byte store), and
573
+ * with a constant or a parameter on the right the two still allocate registers differently. The
574
+ * cast is a source spelling too: a hoisted `u8 *d = &p->f38; *d = v;` builds the cast's object. So
575
+ * the fold is the default and `opts.keepStores` (the `/far-cast` lift) keeps every such store on
576
+ * its address for the differ to referee.
577
+ * A LOAD through a cast and through a member compile alike, and fold in both lifts — but a load
578
+ * through a HELD pointer does not: agbcc materialises one `add` per member access, even for
579
+ * adjacent members (`p->f38 + p->f39` is `add #0x26` and `add #0x27`), so one materialised base
580
+ * read or written at two distinct offsets (`add r1, #0x26; ldrb [r1]; ldrb [r1, #1]`) is the
581
+ * pointer the source held, and the member spelling cannot rebuild it (`held-pointer-offsets`).
582
+ * ido does the same past its `addiu` reach, where the assembler gives each member access its own
583
+ * `lui`/`addu` (target.ts `farMemberBasePerAccess` states the reach per compiler). On ido, KMC,
584
+ * GCC 2.7.2 and mwcc one access through a member and through a cast compile alike, a store of a
585
+ * loaded value included, and there the fold buys the type; what it can cost is the neighbours'
586
+ * spelling — a folded store no longer aliases the loads beside it, and a held mwcc pointer is
587
+ * respelled as members — which is why `/far-cast` is offered there too.
588
+ *
589
+ * THE REFUSALS ARE TWO GATE TABLES, so `pnpm bench gates --pass displaced` can count which one
590
+ * keeps a row on the cast spelling: `DISPLACED_BASE_GATES`, asked once per materialised base, and
591
+ * `DISPLACED_ROOT_GATES`, asked once per root over the bases that passed. A base the first
592
+ * refuses keeps its own accesses on its materialised address while the root's other bases are
593
+ * still asked; a root the second refuses keeps every access off it on its address.
594
+ * Every gate is a spelling choice, not a soundness rule: the fold rewrites address arithmetic
595
+ * into the same address, so no candidate it admits reads different memory, and the differ
596
+ * referees. NOT TABLED: `/far-cast`'s `keepStores` skip, which is the variation itself
597
+ * rather than a refusal. Runs right before `recognizeStructs`, after raise/truncload.ts, which
598
+ * therefore never sees a folded access. Returns the number of accesses folded. */
599
+ export function foldDisplacedAccesses(
600
+ fn: Fn,
601
+ opts: DisplacedOptions = {},
602
+ behaviors: DisplacedBehaviors = {},
603
+ voidReturn = false,
604
+ baseGates: readonly Gate<DisplacedBase>[] = DISPLACED_BASE_GATES,
605
+ rootGates: readonly Gate<DisplacedRoot>[] = DISPLACED_ROOT_GATES,
606
+ ): number {
607
+ const defs = defOpMap(fn);
608
+ const { accessesOf, arrayBases } = constOffsetAccesses(fn);
609
+ // Every use of each value: the op and operand index, or null for an edge argument.
610
+ const uses = new Map<Value, { op: Op | null; at: number }[]>();
611
+ const use = (v: Value, op: Op | null, at: number) => {
612
+ let l = uses.get(v);
613
+ if (!l) {
614
+ l = [];
615
+ uses.set(v, l);
616
+ }
617
+ l.push({ op, at });
618
+ };
619
+ // Where each op stands, for the store-order reading.
620
+ const position = new Map<Op, { block: number; at: number }>();
621
+ fn.blocks.forEach((b, block) => {
622
+ (b.ops as Op[]).forEach((op, at) => {
623
+ position.set(op, { block, at });
624
+ op.operands.forEach((v, i) => use(v, op, i));
625
+ for (const s of op.successors) {
626
+ for (const v of s.args) {
627
+ use(v, null, -1);
628
+ }
629
+ }
630
+ });
631
+ });
632
+ // A `ret` of a function declared void reads no value: the address merely sits in the return
633
+ // register when the function ends (`add r0, #0x27; strb r1, [r0]; bx lr`).
634
+ const onlyFeeds = (v: Value, next: Value | undefined): boolean =>
635
+ (uses.get(v) ?? []).every(({ op, at }) =>
636
+ op === null
637
+ ? false
638
+ : voidReturn && op.opcode === 'ret'
639
+ ? true
640
+ : next === undefined
641
+ ? (op.opcode === 'load' || op.opcode === 'store') && at === 0
642
+ : op.results[0] === next && op.opcode !== 'load' && op.opcode !== 'store',
643
+ );
644
+ // THE MEMBER'S ORDER (target.ts `farMemberStoreOrder`): the stored value is computed by a
645
+ // non-constant op in the same block before the chain's first link, AS THIS STORE'S RIGHT-HAND
646
+ // SIDE: this store is its only reader, and no memory access or call stands between it and the
647
+ // chain. A constant or a parameter on the right leaves no order to read, so it is no evidence
648
+ // either way; neither is a value an earlier statement computed (`u8 v = *q; *r = 0; p[38] = v;`),
649
+ // which the member and the cast spellings both load before the address, byte for byte. Nor is a
650
+ // value that IS a call: agbcc expands every call of a right-hand side before either target
651
+ // (MODIFY_EXPR's preexpand_calls, gcc/expr.c:6777), so `p->f60 = k()` and `p[60] = k()` are one
652
+ // object. What the right-hand side computes from a call's result still orders (`p->f60 = k() + 1`
653
+ // adds before the address, `p[60] = k() + 1` after it).
654
+ const effect = (op: Op): boolean =>
655
+ op.opcode === 'store' ||
656
+ op.opcode === 'astore' ||
657
+ op.opcode === 'load' ||
658
+ op.opcode === 'aload' ||
659
+ op.opcode === 'call' ||
660
+ op.opcode === 'opaque';
661
+ const memberOrdered = (a: Access, chain: Value[]): boolean => {
662
+ const value = a.op.operands[1];
663
+ const vdef = defs.get(value);
664
+ const first = defs.get(chain[chain.length - 1]);
665
+ const v = vdef && vdef.opcode !== 'const' && vdef.opcode !== 'call' ? position.get(vdef) : undefined;
666
+ const f = first ? position.get(first) : undefined;
667
+ return (
668
+ v !== undefined &&
669
+ f !== undefined &&
670
+ v.block === f.block &&
671
+ v.at < f.at &&
672
+ (uses.get(value) ?? []).length === 1 &&
673
+ !(fn.blocks[v.block].ops as Op[]).slice(v.at + 1, f.at).some(effect)
674
+ );
675
+ };
676
+ const byRoot = new Map<Value, { access: Access; k: number; memberOrder: boolean }[]>();
677
+ for (const [base, accesses] of accessesOf) {
678
+ const disp = constDisplacementOf(defs, base);
679
+ if (!disp) {
680
+ continue;
681
+ }
682
+ // `/far-cast`: a store kept on its address keeps the chain's value used by something other
683
+ // than a folded access, so no load sharing that address folds either.
684
+ if (opts.keepStores === true && accesses.some((a) => a.op.opcode === 'store')) {
685
+ continue;
686
+ }
687
+ // A refused base stays on its address and its root's other bases are still asked: a held
688
+ // `u16 *d = &p->f68` beside a member store `p->f38 = *q` keeps `d` and folds the store.
689
+ if (firstRejection(baseGates, { base, accesses, disp, defs, arrayBases, onlyFeeds, behaviors }) !== null) {
690
+ continue;
691
+ }
692
+ const list = byRoot.get(disp.root) ?? [];
693
+ byRoot.set(disp.root, list);
694
+ for (const access of accesses) {
695
+ const memberOrder =
696
+ behaviors.farMemberStoreOrder === true && access.op.opcode === 'store' && memberOrdered(access, disp.chain);
697
+ list.push({ access, k: disp.k, memberOrder });
698
+ }
699
+ }
700
+ let folded = 0;
701
+ for (const [root, moved] of byRoot) {
702
+ const merged = [
703
+ ...(accessesOf.get(root) ?? []),
704
+ ...moved.map(({ access, k }) => ({ ...access, off: access.off + k })),
705
+ ];
706
+ const memberStoreOrder = moved.some((m) => m.memberOrder);
707
+ if (firstRejection(rootGates, { root, merged, memberStoreOrder }) !== null) {
708
+ continue;
709
+ }
710
+ for (const { access, k } of moved) {
711
+ access.op.operands[0] = root;
712
+ access.op.attrs.off = access.off + k;
713
+ folded++;
714
+ if (access.op.opcode === 'store') {
715
+ opts.onFoldedStore?.();
716
+ }
717
+ }
718
+ // An array-shaped set the member's store order admitted: `recognizeStructs` would read it as
719
+ // an array, so the struct its evidence names is synthesized here.
720
+ if (isArray(merged)) {
721
+ root.type = T.ptr(buildStruct(`Struct${firstFreeStructIndex(fn)}`, merged));
722
+ }
723
+ }
724
+ return folded;
725
+ }
726
+
373
727
  /** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
374
728
  * before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
375
729
  * plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
@@ -399,6 +753,15 @@ export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined):
399
753
  if (base.type.kind !== 'unknown') {
400
754
  continue;
401
755
  } // already typed (not a bare recovery target)
756
+ // A FRAME OBJECT'S ADDRESS is never a recovery target: its declaration is the frame audit's
757
+ // stamp (structure.ts `laddrType` — a scalar, a `u8` array, a struct-return temp, or the union
758
+ // of the views its accesses use), and every access to it is spelled from that stamp, never
759
+ // through a type recovered here. Recovered anyway, the one-object area's accesses at several
760
+ // offsets and widths make a second, per-word union of the same bytes that nothing references
761
+ // and every candidate prints.
762
+ if (defs.get(base)?.opcode === 'laddr') {
763
+ continue;
764
+ }
402
765
 
403
766
  const accesses = accessesOf.get(base)!;
404
767
  if (isArray(accesses)) {
@@ -161,9 +161,10 @@ export const TRUNC_LOAD_GATES: readonly Gate<TruncatedLoad>[] = [
161
161
  // spellings a source has for one land here: the bare cast of a number (`*(vu16 *)0x4000004`)
162
162
  // through `constAddressOf`, and the named register (`REG_DISPSTAT`) through `globalCellOf`,
163
163
  // whose `volatile` is a declaration in the project's headers that no raise pass can read.
164
- // The two helpers are ir/alias.ts's, and `constAddressOf` is the address structure.ts's
165
- // `volatileQualifiable` asks about — so a load this rule and `qualified-access` admit is one that
166
- // predicate answers no to, by construction rather than by measurement.
164
+ // The two helpers are ir/alias.ts's. A dead read this rule and `qualified-access` admit is one
165
+ // nobody spells: a read the raw lift never used is stamped by the dead-read pin
166
+ // (frontend/dead-reads.ts) before this pass runs, `qualified-access` refuses it, and
167
+ // structure.ts `volatileQualifiable` spells only a qualified read.
167
168
  //
168
169
  // NOT `capabilities.deviceRegisters`. That window is documented as a question about SPELLING
169
170
  // and may be approximate (target.ts), so a soundness rule may not rest on it; asking whether