@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -18,6 +18,8 @@
18
18
  // gap fields the compiler had but this function never touched)
19
19
  // base @ {off2 w4} -> LOUD decline (a 4-byte field at offset 2 is not 4-aligned —
20
20
  // natural C alignment cannot place it there)
21
+ // base @ {off0 w4, off2 w2} -> struct { union { s32 word; s16 half[2]; } field_0; } (the same
22
+ // bytes at two widths — `buildUnionStruct`)
21
23
  // base @ {off0 w4, off4 w4} -> array (uniform stride — untouched)
22
24
  // base @ {off8 w4} -> array (single aligned access — no struct evidence)
23
25
  // base @ aload(index) -> array (variable index — untouched)
@@ -42,19 +44,105 @@
42
44
  // struct reproduces the observed offsets byte-for-byte and is self-describing (the same
43
45
  // discipline raise/struct-arrays.ts withPadding uses). Each accessed field must still be
44
46
  // naturally aligned to ITS OWN width (`off % width === 0`) — a genuinely packed layout (a field
45
- // at an offset natural C alignment could not place it at) is rejected LOUD, as is an
46
- // overlap/union.
47
- import { Fn, Op, Value } from '../ir/core';
47
+ // at an offset natural C alignment could not place it at) is rejected LOUD. An overlap is a union
48
+ // member on a base whose layout only this function's accesses describe, and no struct at all on
49
+ // one whose address is declared elsewhere (`recognizeStructs`).
50
+ import { constAddressOf, constDisplacementOf, globalCellOf } from '../ir/alias';
51
+ import { Fn, Op, Value, defOpMap } from '../ir/core';
52
+ import { placedAt } from '../ir/discipline';
48
53
  import { nextStructIndex } from '../ir/struct-names';
49
- import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
54
+ import { IrType, StructField, T, UNION_VIEW_NAMES, intWidth, scalarTypeForAccess, unionViewSigns } from '../ir/types';
50
55
  import type { StructType } from '../l3/ast';
51
- import { RaiseUnsupportedError } from './errors';
56
+ import { type Gate, firstRejection } from '../l3/gates';
57
+ import { RaiseUnsupportedError, StructOverlapError } from './errors';
58
+ import { storeSigned } from './storesign';
52
59
 
53
- // A single observed access to a base: byte offset, access width (bytes), signedness (loads only).
54
- interface Access {
60
+ /** A single observed constant-offset access to a base: the op itself and its program point, its
61
+ * byte offset, its access width (bytes), and the signedness a recovered field would take from it.
62
+ *
63
+ * SHARED WITH raise/truncload.ts, which folds a narrow read into a cast of a wider one before this
64
+ * pass runs. The two ask the same question of a function — "what does this base's access set look
65
+ * like" — and a second copy of the walk would let them come to disagree about the answer while
66
+ * each looked right on its own.
67
+ *
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. */
73
+ export interface Access {
74
+ op: Op;
75
+ /** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
76
+ block: number;
77
+ /** index into that block's `ops`, as they stood when this was collected. */
78
+ at: number;
55
79
  off: number;
56
80
  width: number;
57
81
  signed: boolean;
82
+ isLoad: boolean;
83
+ }
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
+
88
+ /** What one walk of a function yields about its constant-offset memory accesses. */
89
+ export interface ConstOffsetAccesses {
90
+ /** every base's constant-offset accesses, in program order. */
91
+ accessesOf: Map<Value, Access[]>;
92
+ /** the bases, in FIRST-APPEARANCE order → deterministic struct names. */
93
+ order: Value[];
94
+ /** bases a VARIABLE-index access also reaches (`aload`/`astore`) — arrays, never struct-recovered. */
95
+ arrayBases: Set<Value>;
96
+ }
97
+
98
+ /** Collect every base's constant-offset accesses, plus the bases a variable-index access reaches.
99
+ * ONE walk, two consumers: `recognizeStructs` below and raise/truncload.ts. */
100
+ export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
101
+ const accessesOf = new Map<Value, Access[]>();
102
+ const arrayBases = new Set<Value>();
103
+ const order: Value[] = [];
104
+ const note = (base: Value, a: Access) => {
105
+ let list = accessesOf.get(base);
106
+ if (!list) {
107
+ list = [];
108
+ accessesOf.set(base, list);
109
+ order.push(base);
110
+ }
111
+ list.push(a);
112
+ };
113
+ fn.blocks.forEach((b, block) => {
114
+ (b.ops as Op[]).forEach((op, at) => {
115
+ switch (op.opcode) {
116
+ case 'load':
117
+ note(op.operands[0], {
118
+ op,
119
+ block,
120
+ at,
121
+ off: op.attrs.off as number,
122
+ width: op.attrs.width as number,
123
+ signed: op.attrs.signed as boolean,
124
+ isLoad: true,
125
+ });
126
+ break;
127
+ case 'store':
128
+ note(op.operands[0], {
129
+ op,
130
+ block,
131
+ at,
132
+ off: op.attrs.off as number,
133
+ width: op.attrs.width as number,
134
+ signed: storeSigned(op, op.attrs.width as number),
135
+ isLoad: false,
136
+ });
137
+ break;
138
+ case 'aload':
139
+ case 'astore':
140
+ arrayBases.add(op.operands[0]);
141
+ break;
142
+ }
143
+ });
144
+ });
145
+ return { accessesOf, order, arrayBases };
58
146
  }
59
147
 
60
148
  // Natural C size/alignment of a recovered scalar field type (all fields here are int/ptr ≤ 4 bytes,
@@ -78,27 +166,71 @@ function isArray(accesses: Access[]): boolean {
78
166
 
79
167
  /** Build the struct type for a base whose accesses are NOT array-shaped. Unaccessed leading/
80
168
  * interior gaps are FILLED with `u8[N]` pads so the declared struct reproduces the observed
81
- * offsets. Throws LOUD only on a layout natural C alignment cannot reproduce: two accesses
82
- * overlapping in bytes (a union — same offset with differing widths, OR distinct offsets whose
83
- * ranges collide), or a field at an offset its own natural alignment could not place it at (a
84
- * PACKED layout). */
85
- function buildStruct(name: string, accesses: Access[]): IrType {
169
+ * offsets. Throws LOUD only on a layout no plain struct reproduces: two accesses overlapping in
170
+ * bytes (same offset with differing widths, OR distinct offsets whose ranges collide), or a field
171
+ * at an offset its own natural alignment could not place it at (a PACKED layout).
172
+ *
173
+ * ONE CLASS OF OVERLAP NEVER REACHES HERE: a narrow LOAD covering the low-order end of a wider
174
+ * access that ran before it on every path is a cast of that access rather than a second field, and
175
+ * raise/truncload.ts folds it into one before this pass runs. What is left is what the asm does
176
+ * not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
177
+ * cover on a path the narrow read does not run — and every one of them throws here.
178
+ *
179
+ * A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Four throw sites
180
+ * below reject a layout and they fall into two CLASSES, which is the split a caller reads:
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}`).
187
+ * Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
188
+ * declared outside this function's access set can keep its untyped spelling and lift
189
+ * (`recognizeStructs` below). There is no struct to synthesize, and nothing is lost by not
190
+ * synthesizing one. Any other base is declared with a union member, or a `castViews` field
191
+ * where the union would not fit (`buildUnionStruct`).
192
+ * • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
193
+ * at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
194
+ * width folds to the element index `off / width` (structure/structure.ts), which for a
195
+ * misaligned pair is a FRACTION and not C. It declines whatever the base is. */
196
+ function buildStruct(name: string, accesses: Access[], extensionViews = true): IrType {
86
197
  // One field per distinct offset; a load's signedness wins over a store's (more information).
87
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>();
88
202
  for (const a of accesses) {
89
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
+ }
90
208
  if (!prev) {
91
209
  byOff.set(a.off, a);
92
210
  continue;
93
211
  }
94
212
  if (prev.width !== a.width) {
95
- throw new RaiseUnsupportedError(
213
+ throw new StructOverlapError(
96
214
  `cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
97
215
  );
98
216
  }
99
- 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) {
100
232
  byOff.set(a.off, a);
101
- } // prefer the signed (load-derived) view
233
+ }
102
234
  }
103
235
  const dataFields: StructField[] = [...byOff.values()]
104
236
  .sort((x, y) => x.off - y.off)
@@ -108,31 +240,38 @@ function buildStruct(name: string, accesses: Access[]): IrType {
108
240
  // cannot reproduce, so it is rejected LOUD (never a silently-wrong struct). The GAP before a
109
241
  // field (an unaccessed leading/interior member) is legal: it is filled with a `u8[N]` pad below.
110
242
  for (const f of dataFields) {
111
- const sz = accessWidth(f);
112
- if (f.off % sizeAlign(sz) !== 0) {
113
- throw new RaiseUnsupportedError(
114
- `cannot recover struct '${name}': field at offset ${f.off} (width ${sz}) is not naturally aligned — packed layout not modelled`,
115
- );
116
- }
243
+ refusePacked(name, f.off, fieldSize(f.type));
117
244
  }
118
- // Place fields under natural C alignment, inserting an explicit `u8[N]` PAD only for a gap the
119
- // alignment itself does NOT already cover (the same self-describing discipline as
120
- // raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C alignment
121
- // would put it after the running cursor:
122
- // • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
123
- // C's own inter-field alignment reproduces the layout.
124
- // • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
125
- // never read): insert a `u8[off - cursor]` pad so the field lands exactly.
126
- // • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
127
- // the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
128
- // reject LOUD, never a silently-mislaid field.
245
+ return placeFields(name, dataFields);
246
+ }
247
+
248
+ function refusePacked(name: string, off: number, size: number): void {
249
+ if (off % sizeAlign(size) !== 0) {
250
+ throw new RaiseUnsupportedError(
251
+ `cannot recover struct '${name}': field at offset ${off} (width ${size}) is not naturally aligned — packed layout not modelled`,
252
+ );
253
+ }
254
+ }
255
+
256
+ /** Seat `dataFields` (sorted by offset) under natural C alignment, inserting an explicit `u8[N]`
257
+ * PAD only for a gap the alignment itself does NOT already cover (the same self-describing
258
+ * discipline as raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C
259
+ * alignment would put it after the running cursor:
260
+ * • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
261
+ * C's own inter-field alignment reproduces the layout.
262
+ * • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
263
+ * never read): insert a `u8[off - cursor]` pad so the field lands exactly.
264
+ * • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
265
+ * the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
266
+ * reject LOUD, never a silently-mislaid field. */
267
+ function placeFields(name: string, dataFields: StructField[]): IrType {
129
268
  const fields: StructField[] = [];
130
269
  let cursor = 0;
131
270
  let pad = 0;
132
271
  for (const f of dataFields) {
133
- const aligned = roundUp(cursor, sizeAlign(accessWidth(f)));
272
+ const aligned = roundUp(cursor, sizeAlign(fieldSize(f.type)));
134
273
  if (aligned > f.off) {
135
- throw new RaiseUnsupportedError(
274
+ throw new StructOverlapError(
136
275
  `cannot recover struct '${name}': field at offset ${f.off} overlaps the prior field (aligned to ${aligned}) — unions not modelled`,
137
276
  );
138
277
  }
@@ -140,69 +279,467 @@ function buildStruct(name: string, accesses: Access[]): IrType {
140
279
  fields.push({ off: cursor, type: T.array(T.u(8), f.off - cursor), name: `_pad${pad++}` });
141
280
  }
142
281
  fields.push(f);
143
- cursor = f.off + accessWidth(f);
282
+ cursor = f.off + fieldSize(f.type);
144
283
  }
145
284
  return T.struct(name, fields);
146
285
  }
147
286
 
148
- // Width in bytes of a recovered field's type (int width/8; pointer is word-sized 4).
149
- function accessWidth(f: StructField): number {
150
- return f.type.kind === 'int' ? f.type.width / 8 : 4;
287
+ /** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
288
+ * `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
289
+ * written at more than one width are views of one cell, and a union is the C that says so.
290
+ *
291
+ * NOT A CAST, and the compiler is why. `((u16 *)p)[k]` beside `*p` spells the same two accesses,
292
+ * but a cast-punned access is outside C's aliasing rules and agbcc (-O2) applies them: a narrow
293
+ * read, a word store and the same narrow read again compiles, through casts, to ONE `ldrh` whose
294
+ * value is reused, where the union spelling reloads after the `str` — as the asm the union was
295
+ * compiled from does (`synthetic:ureread`). gcc2.7.2kmc, ido7.1 and mwcc_242_81 reload for both.
296
+ * The direction is one-way: a union access aliases every view of its cell, so the union spelling
297
+ * keeps every access the asm performed and the cast cannot be the better candidate — which is why
298
+ * this is the default and not a variation.
299
+ *
300
+ * THE LAYOUT. Every access must be naturally aligned (a packed one declines, as in
301
+ * `buildStruct`), and access widths are powers of two, so an aligned access never straddles the
302
+ * boundary of a wider aligned one: two accesses overlap only when the wider CONTAINS the narrower.
303
+ * A cell is therefore one widest access and every access inside it, and it gets a VIEW per width —
304
+ * `word`, `half`, `byte` — and, for a narrow width LOADED with both extensions, per extension —
305
+ * `uhalf` and `shalf` — because a view's type IS its extension and one view for both reads one of
306
+ * them wrong. A store carries no extension and writes through its width's only view, or the
307
+ * unsigned one where there are two. A view that holds
308
+ * one element at the cell's start is a scalar; any other is an array reaching the furthest
309
+ * element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
310
+ * any other is a member `field_<off>` holding a union of its views — including a single-width cell
311
+ * loaded both ways, which `buildStruct` hands over as an overlap even on a base with no other one.
312
+ *
313
+ * THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
314
+ * aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
315
+ * agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
316
+ * it two. A union that boundary would move — seated off it, or with the next field inside its
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
320
+ * compiler (undefined) declines every union, since no boundary is safe to assume: one too small
321
+ * mislays the fields after the union on agbcc, one too large drops the pad in front of them
322
+ * everywhere else.
323
+ *
324
+ * A cell at a width no view is named for declines; the frontends emit none. */
325
+ function buildUnionStruct(structName: string, accesses: Access[], aggregateBoundary: number | undefined): IrType {
326
+ for (const a of accesses) {
327
+ refusePacked(structName, a.off, a.width);
328
+ }
329
+ const clusters: Access[][] = [];
330
+ for (const a of [...accesses].sort((x, y) => x.off - y.off || y.width - x.width)) {
331
+ const cur = clusters.at(-1);
332
+ if (cur !== undefined && a.off < cur[0].off + cur[0].width) {
333
+ cur.push(a);
334
+ } else {
335
+ clusters.push([a]);
336
+ }
337
+ }
338
+ const dataFields = clusters.map((c): StructField => {
339
+ const start = c[0].off;
340
+ const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
341
+ const group = c.filter((a) => a.width === width);
342
+ const name = UNION_VIEW_NAMES[width];
343
+ if (name === undefined) {
344
+ throw new RaiseUnsupportedError(
345
+ `cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
346
+ );
347
+ }
348
+ const views = unionViewSigns(
349
+ width,
350
+ group.map((a) => ({ isLoad: a.isLoad, signed: a.signed, stamped: isStamped(a) })),
351
+ );
352
+ const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
353
+ return views.map((signed): StructField => {
354
+ const reads = group.filter((a) => viewOf(a) === signed);
355
+ const count = Math.max(...reads.map((a) => a.off - start)) / width + 1;
356
+ const elem = scalarTypeForAccess(width, signed);
357
+ const viewName = views.length > 1 ? `${signed ? 's' : 'u'}${name}` : name;
358
+ return { off: 0, type: count === 1 ? elem : T.array(elem, count), name: viewName };
359
+ });
360
+ });
361
+ if (members.length === 1) {
362
+ return { off: start, type: members[0].type, name: `field_${start}` };
363
+ }
364
+ if (aggregateBoundary === undefined) {
365
+ throw new RaiseUnsupportedError(
366
+ `cannot recover struct '${structName}': the union at offset ${start} needs this compiler's aggregate boundary, which is unmeasured`,
367
+ );
368
+ }
369
+ return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
370
+ });
371
+ const fitted = dataFields.map((f, i): StructField => {
372
+ if (f.type.kind !== 'union') {
373
+ return f;
374
+ }
375
+ const next = dataFields[i + 1];
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) {
381
+ throw new RaiseUnsupportedError(
382
+ `cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
383
+ );
384
+ }
385
+ return { off: f.off, type: widest, name: f.name, castViews: true };
386
+ });
387
+ return placeFields(structName, fitted);
151
388
  }
152
389
 
153
- /** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
154
- * before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
155
- * plain pointer. Returns the number of bases recovered as structs. */
156
- export function recognizeStructs(fn: Fn): number {
157
- // Collect each base's constant-offset accesses, and the set of bases that are ALSO array bases
158
- // (used by a variable-index aload/astore) — those are arrays, excluded from struct recovery.
159
- const accessesOf = new Map<Value, Access[]>();
160
- const arrayBases = new Set<Value>();
161
- const order: Value[] = []; // first-appearance order → deterministic struct names
162
- const note = (base: Value, a: Access) => {
163
- let list = accessesOf.get(base);
164
- if (!list) {
165
- list = [];
166
- accessesOf.set(base, list);
167
- order.push(base);
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;
411
+ }
412
+
413
+ /** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
414
+ * size its compiler gives it). */
415
+ function fieldSize(t: IrType): number {
416
+ switch (t.kind) {
417
+ case 'int':
418
+ return t.width / 8;
419
+ case 'array':
420
+ return t.count * fieldSize(t.elem);
421
+ case 'union':
422
+ return t.size;
423
+ default:
424
+ return 4;
425
+ }
426
+ }
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);
168
616
  }
169
- list.push(a);
617
+ l.push({ op, at });
170
618
  };
171
- for (const b of fn.blocks) {
172
- for (const op of b.ops as Op[]) {
173
- switch (op.opcode) {
174
- case 'load':
175
- note(op.operands[0], {
176
- off: op.attrs.off as number,
177
- width: op.attrs.width as number,
178
- signed: op.attrs.signed as boolean,
179
- });
180
- break;
181
- case 'store':
182
- note(op.operands[0], {
183
- off: op.attrs.off as number,
184
- width: op.attrs.width as number,
185
- signed: (op.attrs.width as number) === 4,
186
- });
187
- break;
188
- case 'aload':
189
- case 'astore':
190
- arrayBases.add(op.operands[0]);
191
- break;
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
+ }
192
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 });
193
698
  }
194
699
  }
195
-
196
- // Which values are the address of a NAMED global (`gaddr`)? Consulted only when synthesis
197
- // DECLINES: see the catch below.
198
- const namedGlobal = new Set<Value>();
199
- for (const b of fn.blocks) {
200
- for (const op of b.ops as Op[]) {
201
- if (op.opcode === 'gaddr') {
202
- namedGlobal.add(op.results[0]);
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?.();
203
716
  }
204
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
+ }
205
723
  }
724
+ return folded;
725
+ }
726
+
727
+ /** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
728
+ * before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
729
+ * plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
730
+ * compiler's struct/union size boundary, undefined where nobody measured it (`buildUnionStruct`). */
731
+ export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined): number {
732
+ const { accessesOf, order, arrayBases } = constOffsetAccesses(fn);
733
+
734
+ // Does this base's address have a source of truth OUTSIDE this function's access set? Consulted
735
+ // only when synthesis refuses an OVERLAP: see the caller below. Two kinds qualify, and both are
736
+ // read with the helpers raise/truncload.ts asks the same question with — one concept, one
737
+ // reading, and `globalCellOf` is the whole of that reading: it answers for `&gSym + K` as well as
738
+ // for the bare `&gSym`, and an interior address is its own base here (`constOffsetAccesses` keys
739
+ // on the operand Value), so a narrower opcode test would decline the shape `fixed-cell` admits.
740
+ const defs = defOpMap(fn);
741
+ const addressDeclaredElsewhere = (base: Value): boolean =>
742
+ globalCellOf(defs, base, 0) !== null || constAddressOf(defs, base, 0) !== null;
206
743
 
207
744
  // The NAME counter is seeded from the names already in the graph, not from this pass's own
208
745
  // success count: raise/memberarrays.ts runs first and mints `Struct<N>` types of its own, and two
@@ -216,6 +753,15 @@ export function recognizeStructs(fn: Fn): number {
216
753
  if (base.type.kind !== 'unknown') {
217
754
  continue;
218
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
+ }
219
765
 
220
766
  const accesses = accessesOf.get(base)!;
221
767
  if (isArray(accesses)) {
@@ -224,19 +770,47 @@ export function recognizeStructs(fn: Fn): number {
224
770
  try {
225
771
  base.type = T.ptr(buildStruct(`Struct${name}`, accesses));
226
772
  } catch (e) {
227
- // A NAMED global whose accesses synthesis cannot reconcile is not a reason to decline the
228
- // function: its declaration belongs to the project's own headers, and its constant-offset
229
- // accesses render at L3 through the symbol context (member spelling when the map knows the
230
- // layout, the honest cast spelling when it does not). The inhabitant is agbcc FUSING two
231
- // adjacent u8 compares into one ldrh — `s.level == 8 && s.world == 6` reads offset 12 at
232
- // widths 1 AND 2, which is not a union, just two spellings of declared bytes. An ANONYMOUS
233
- // base (a loaded pointer, a parameter) has no other source of truth, so for it the decline
234
- // stands exactly as before — this catch narrows nothing for the shapes that already worked,
235
- // because a base synthesis succeeds on takes the same path it always took.
236
- if (e instanceof RaiseUnsupportedError && namedGlobal.has(base)) {
773
+ // A base whose ADDRESS is already declared somewhere other than this function's access set is
774
+ // not a struct this pass has to synthesize, so failing to synthesize one for it is not a
775
+ // reason to decline the function — PROVIDED the refusal is the OVERLAP one (its own class,
776
+ // never a substring of the message), whose residue is per-access spellings that stand on their
777
+ // own. Two kinds of base qualify:
778
+ // • a NAMED global (`globalCellOf`) — its declaration belongs to the project's own headers,
779
+ // and its constant-offset accesses render at L3 through the symbol context (member
780
+ // spelling when the map knows the layout, the honest cast spelling when it does not). The
781
+ // inhabitant is agbcc FUSING two adjacent u8 compares into one ldrh — `s.level == 8 &&
782
+ // s.world == 6` reads offset 12 at widths 1 AND 2, which is not a union, just two
783
+ // spellings of declared bytes.
784
+ // • a LITERAL ADDRESS (`constAddressOf`) — a cell the hardware placed, which L3 renders as
785
+ // a cast at each access's OWN width (`*(u16 *)0x4000004`). There is no layout to
786
+ // reconcile: two widths at one literal address are two casts of one address, which is
787
+ // what the source wrote. The inhabitant is a memory-mapped I/O register the source writes
788
+ // as a halfword and as a word (`sa3:Sio32MultiLoadMain`, `synthetic:unidev`).
789
+ // This is the DUAL of raise/truncload.ts's `fixed-cell` gate, not a relaxation of it: that
790
+ // gate refuses to FOLD the two accesses into one cast of a wider load, because a device read
791
+ // is not a read of the bytes around it. Both readings agree that the two accesses stay two.
792
+ //
793
+ // THE PACKED REFUSAL IS NOT FORGIVEN FOR ANY BASE. Its residue is not two spellings: a
794
+ // misaligned access renders through the element index `off / width`, which is a fraction and
795
+ // not C, so forgiving it would trade a loud decline for an uncompilable candidate. THIS layer
796
+ // is the one that can name the layout, which is why the refusal a reader sees is this one.
797
+ // structure/structure.ts `displacementIndex` refuses the fraction a second time where it
798
+ // would be computed, for the bases that never reach this pass at all (already typed, or a
799
+ // forgiven overlap); that message can only describe the access, not the layout.
800
+ //
801
+ // An ANONYMOUS base (a loaded pointer, a parameter) has no other source of truth: its layout
802
+ // is what this function's accesses say, and an overlap there says UNION. It is declared as
803
+ // one (`buildUnionStruct`), which declines in turn on what it cannot lay out. The two
804
+ // declared kinds above keep their casts although `buildUnionStruct`'s aliasing argument
805
+ // applies to them too: no row re-reads across a store there, and a union is the wrong
806
+ // declaration for both inhabitants (docs/level-tower.md, "A union").
807
+ if (!(e instanceof StructOverlapError)) {
808
+ throw e;
809
+ }
810
+ if (addressDeclaredElsewhere(base)) {
237
811
  continue;
238
812
  }
239
- throw e;
813
+ base.type = T.ptr(buildUnionStruct(`Struct${name}`, accesses, aggregateBoundary));
240
814
  }
241
815
  name++;
242
816
  count++;
@@ -249,13 +823,15 @@ export function recognizeStructs(fn: Fn): number {
249
823
  * `ir/struct-names.ts`, which the three struct minters share; this names the prefix. */
250
824
  export function firstFreeStructIndex(fn: Fn): number {
251
825
  return nextStructIndex(
252
- [...collectStructs(fn)].map((s) => s.name),
826
+ [...collectStructs(fn, true)].map((s) => s.name),
253
827
  'Struct',
254
828
  );
255
829
  }
256
830
 
257
831
  /** The distinct struct types this function's L2 GRAPH mentions (unwrapping struct pointers on every
258
- * value), deduped by name and sorted, for the backend to declare above the function.
832
+ * value), deduped by name and sorted, for the backend to declare above the function. A struct the
833
+ * headers own (`declared`, ir/types.ts) is left out — the declarations block defines it — unless
834
+ * `withDeclared` asks for every name the graph uses, which is what a name allocator needs.
259
835
  *
260
836
  * "Mentions", not "references": this walks `fn.blocks` at the moment structuring runs, and the
261
837
  * result is CACHED on the SFn (`structure.ts`) and carried by every later `{...sfn}` pass, so a
@@ -266,11 +842,11 @@ export function firstFreeStructIndex(fn: Fn): number {
266
842
  * is recorded here rather than fixed speculatively. The fix, if a pass ever makes it reachable, is
267
843
  * the one l3/symbol-refs.ts already applies to symbol references: derive at the consumption point
268
844
  * (`backend/cfamily.ts` structDecls) instead of caching. */
269
- export function collectStructs(fn: Fn): StructType[] {
845
+ export function collectStructs(fn: Fn, withDeclared = false): StructType[] {
270
846
  const seen = new Map<string, StructType>();
271
847
  const consider = (t: IrType) => {
272
848
  const s = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : t.kind === 'struct' ? t : null;
273
- if (s && s.kind === 'struct' && !seen.has(s.name)) {
849
+ if (s && s.kind === 'struct' && (withDeclared || s.declared === undefined) && !seen.has(s.name)) {
274
850
  seen.set(s.name, { name: s.name, fields: s.fields, size: s.size });
275
851
  }
276
852
  };