@asmlift/core 0.8.1 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  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 +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -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,98 @@
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, globalCellOf } from '../ir/alias';
51
+ import { Fn, Op, Value, defOpMap } from '../ir/core';
48
52
  import { nextStructIndex } from '../ir/struct-names';
49
53
  import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
50
54
  import type { StructType } from '../l3/ast';
51
- import { RaiseUnsupportedError } from './errors';
55
+ import { RaiseUnsupportedError, StructOverlapError } from './errors';
52
56
 
53
- // A single observed access to a base: byte offset, access width (bytes), signedness (loads only).
54
- interface Access {
57
+ /** A single observed constant-offset access to a base: the op itself and its program point, its
58
+ * byte offset, its access width (bytes), and the signedness a recovered field would take from it.
59
+ *
60
+ * SHARED WITH raise/truncload.ts, which folds a narrow read into a cast of a wider one before this
61
+ * pass runs. The two ask the same question of a function — "what does this base's access set look
62
+ * like" — and a second copy of the walk would let them come to disagree about the answer while
63
+ * each looked right on its own.
64
+ *
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. */
69
+ export interface Access {
70
+ op: Op;
71
+ /** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
72
+ block: number;
73
+ /** index into that block's `ops`, as they stood when this was collected. */
74
+ at: number;
55
75
  off: number;
56
76
  width: number;
57
77
  signed: boolean;
78
+ isLoad: boolean;
79
+ }
80
+
81
+ /** What one walk of a function yields about its constant-offset memory accesses. */
82
+ export interface ConstOffsetAccesses {
83
+ /** every base's constant-offset accesses, in program order. */
84
+ accessesOf: Map<Value, Access[]>;
85
+ /** the bases, in FIRST-APPEARANCE order → deterministic struct names. */
86
+ order: Value[];
87
+ /** bases a VARIABLE-index access also reaches (`aload`/`astore`) — arrays, never struct-recovered. */
88
+ arrayBases: Set<Value>;
89
+ }
90
+
91
+ /** Collect every base's constant-offset accesses, plus the bases a variable-index access reaches.
92
+ * ONE walk, two consumers: `recognizeStructs` below and raise/truncload.ts. */
93
+ export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
94
+ const accessesOf = new Map<Value, Access[]>();
95
+ const arrayBases = new Set<Value>();
96
+ const order: Value[] = [];
97
+ const note = (base: Value, a: Access) => {
98
+ let list = accessesOf.get(base);
99
+ if (!list) {
100
+ list = [];
101
+ accessesOf.set(base, list);
102
+ order.push(base);
103
+ }
104
+ list.push(a);
105
+ };
106
+ fn.blocks.forEach((b, block) => {
107
+ (b.ops as Op[]).forEach((op, at) => {
108
+ switch (op.opcode) {
109
+ case 'load':
110
+ note(op.operands[0], {
111
+ op,
112
+ block,
113
+ at,
114
+ off: op.attrs.off as number,
115
+ width: op.attrs.width as number,
116
+ signed: op.attrs.signed as boolean,
117
+ isLoad: true,
118
+ });
119
+ break;
120
+ case 'store':
121
+ note(op.operands[0], {
122
+ op,
123
+ block,
124
+ at,
125
+ off: op.attrs.off as number,
126
+ width: op.attrs.width as number,
127
+ signed: (op.attrs.width as number) === 4,
128
+ isLoad: false,
129
+ });
130
+ break;
131
+ case 'aload':
132
+ case 'astore':
133
+ arrayBases.add(op.operands[0]);
134
+ break;
135
+ }
136
+ });
137
+ });
138
+ return { accessesOf, order, arrayBases };
58
139
  }
59
140
 
60
141
  // Natural C size/alignment of a recovered scalar field type (all fields here are int/ptr ≤ 4 bytes,
@@ -78,10 +159,30 @@ function isArray(accesses: Access[]): boolean {
78
159
 
79
160
  /** Build the struct type for a base whose accesses are NOT array-shaped. Unaccessed leading/
80
161
  * 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). */
162
+ * offsets. Throws LOUD only on a layout no plain struct reproduces: two accesses overlapping in
163
+ * bytes (same offset with differing widths, OR distinct offsets whose ranges collide), or a field
164
+ * at an offset its own natural alignment could not place it at (a PACKED layout).
165
+ *
166
+ * ONE CLASS OF OVERLAP NEVER REACHES HERE: a narrow LOAD covering the low-order end of a wider
167
+ * access that ran before it on every path is a cast of that access rather than a second field, and
168
+ * raise/truncload.ts folds it into one before this pass runs. What is left is what the asm does
169
+ * not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
170
+ * cover on a path the narrow read does not run — and every one of them throws here.
171
+ *
172
+ * A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Three throw sites
173
+ * 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}`).
178
+ * Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
179
+ * declared outside this function's access set can keep its untyped spelling and lift
180
+ * (`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`).
182
+ * • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
183
+ * at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
184
+ * width folds to the element index `off / width` (structure/structure.ts), which for a
185
+ * misaligned pair is a FRACTION and not C. It declines whatever the base is. */
85
186
  function buildStruct(name: string, accesses: Access[]): IrType {
86
187
  // One field per distinct offset; a load's signedness wins over a store's (more information).
87
188
  const byOff = new Map<number, Access>();
@@ -92,7 +193,7 @@ function buildStruct(name: string, accesses: Access[]): IrType {
92
193
  continue;
93
194
  }
94
195
  if (prev.width !== a.width) {
95
- throw new RaiseUnsupportedError(
196
+ throw new StructOverlapError(
96
197
  `cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
97
198
  );
98
199
  }
@@ -108,31 +209,38 @@ function buildStruct(name: string, accesses: Access[]): IrType {
108
209
  // cannot reproduce, so it is rejected LOUD (never a silently-wrong struct). The GAP before a
109
210
  // field (an unaccessed leading/interior member) is legal: it is filled with a `u8[N]` pad below.
110
211
  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
- }
212
+ refusePacked(name, f.off, fieldSize(f.type));
213
+ }
214
+ return placeFields(name, dataFields);
215
+ }
216
+
217
+ function refusePacked(name: string, off: number, size: number): void {
218
+ if (off % sizeAlign(size) !== 0) {
219
+ throw new RaiseUnsupportedError(
220
+ `cannot recover struct '${name}': field at offset ${off} (width ${size}) is not naturally aligned — packed layout not modelled`,
221
+ );
117
222
  }
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.
223
+ }
224
+
225
+ /** Seat `dataFields` (sorted by offset) under natural C alignment, inserting an explicit `u8[N]`
226
+ * PAD only for a gap the alignment itself does NOT already cover (the same self-describing
227
+ * discipline as raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C
228
+ * alignment would put it after the running cursor:
229
+ * • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
230
+ * C's own inter-field alignment reproduces the layout.
231
+ * • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
232
+ * never read): insert a `u8[off - cursor]` pad so the field lands exactly.
233
+ * • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
234
+ * the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
235
+ * reject LOUD, never a silently-mislaid field. */
236
+ function placeFields(name: string, dataFields: StructField[]): IrType {
129
237
  const fields: StructField[] = [];
130
238
  let cursor = 0;
131
239
  let pad = 0;
132
240
  for (const f of dataFields) {
133
- const aligned = roundUp(cursor, sizeAlign(accessWidth(f)));
241
+ const aligned = roundUp(cursor, sizeAlign(fieldSize(f.type)));
134
242
  if (aligned > f.off) {
135
- throw new RaiseUnsupportedError(
243
+ throw new StructOverlapError(
136
244
  `cannot recover struct '${name}': field at offset ${f.off} overlaps the prior field (aligned to ${aligned}) — unions not modelled`,
137
245
  );
138
246
  }
@@ -140,69 +248,144 @@ function buildStruct(name: string, accesses: Access[]): IrType {
140
248
  fields.push({ off: cursor, type: T.array(T.u(8), f.off - cursor), name: `_pad${pad++}` });
141
249
  }
142
250
  fields.push(f);
143
- cursor = f.off + accessWidth(f);
251
+ cursor = f.off + fieldSize(f.type);
144
252
  }
145
253
  return T.struct(name, fields);
146
254
  }
147
255
 
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;
151
- }
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' };
152
259
 
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);
168
- }
169
- list.push(a);
170
- };
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;
192
- }
260
+ /** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
261
+ * `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
262
+ * written at more than one width are views of one cell, and a union is the C that says so.
263
+ *
264
+ * NOT A CAST, and the compiler is why. `((u16 *)p)[k]` beside `*p` spells the same two accesses,
265
+ * but a cast-punned access is outside C's aliasing rules and agbcc (-O2) applies them: a narrow
266
+ * read, a word store and the same narrow read again compiles, through casts, to ONE `ldrh` whose
267
+ * value is reused, where the union spelling reloads after the `str` — as the asm the union was
268
+ * compiled from does (`synthetic:ureread`). gcc2.7.2kmc, ido7.1 and mwcc_242_81 reload for both.
269
+ * The direction is one-way: a union access aliases every view of its cell, so the union spelling
270
+ * keeps every access the asm performed and the cast cannot be the better candidate — which is why
271
+ * this is the default and not a variation.
272
+ *
273
+ * THE LAYOUT. Every access must be naturally aligned (a packed one declines, as in
274
+ * `buildStruct`), and access widths are powers of two, so an aligned access never straddles the
275
+ * boundary of a wider aligned one: two accesses overlap only when the wider CONTAINS the narrower.
276
+ * A cell is therefore one widest access and every access inside it, and it gets a VIEW per width —
277
+ * `word`, `half`, `byte` — and, for a narrow width LOADED with both extensions, per extension —
278
+ * `uhalf` and `shalf` — because a view's type IS its extension and one view for both reads one of
279
+ * them wrong. A store carries no extension and writes through its width's only view, or the
280
+ * unsigned one where there are two. A view that holds
281
+ * one element at the cell's start is a scalar; any other is an array reaching the furthest
282
+ * element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
283
+ * 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.
286
+ *
287
+ * THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
288
+ * aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
289
+ * agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
290
+ * 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
292
+ * compiler (undefined) declines every union, since no boundary is safe to assume: one too small
293
+ * mislays the fields after the union on agbcc, one too large drops the pad in front of them
294
+ * everywhere else.
295
+ *
296
+ * A cell at a width no view is named for declines; the frontends emit none. */
297
+ function buildUnionStruct(structName: string, accesses: Access[], aggregateBoundary: number | undefined): IrType {
298
+ for (const a of accesses) {
299
+ refusePacked(structName, a.off, a.width);
300
+ }
301
+ const clusters: Access[][] = [];
302
+ for (const a of [...accesses].sort((x, y) => x.off - y.off || y.width - x.width)) {
303
+ const cur = clusters.at(-1);
304
+ if (cur !== undefined && a.off < cur[0].off + cur[0].width) {
305
+ cur.push(a);
306
+ } else {
307
+ clusters.push([a]);
193
308
  }
194
309
  }
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]);
310
+ const dataFields = clusters.map((c): StructField => {
311
+ const start = c[0].off;
312
+ const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
313
+ const group = c.filter((a) => a.width === width);
314
+ const name = VIEW_NAMES[width];
315
+ if (name === undefined) {
316
+ throw new RaiseUnsupportedError(
317
+ `cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
318
+ );
203
319
  }
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;
325
+ const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
326
+ return views.map((signed): StructField => {
327
+ const reads = group.filter((a) => viewOf(a) === signed);
328
+ const count = Math.max(...reads.map((a) => a.off - start)) / width + 1;
329
+ const elem = scalarTypeForAccess(width, signed);
330
+ const viewName = views.length > 1 ? `${signed ? 's' : 'u'}${name}` : name;
331
+ return { off: 0, type: count === 1 ? elem : T.array(elem, count), name: viewName };
332
+ });
333
+ });
334
+ if (members.length === 1) {
335
+ return { off: start, type: members[0].type, name: `field_${start}` };
204
336
  }
337
+ if (aggregateBoundary === undefined) {
338
+ throw new RaiseUnsupportedError(
339
+ `cannot recover struct '${structName}': the union at offset ${start} needs this compiler's aggregate boundary, which is unmeasured`,
340
+ );
341
+ }
342
+ return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
343
+ });
344
+ dataFields.forEach((f, i) => {
345
+ if (f.type.kind !== 'union') {
346
+ return;
347
+ }
348
+ const next = dataFields[i + 1];
349
+ if (f.off % f.type.size !== 0 || (next !== undefined && next.off < f.off + f.type.size)) {
350
+ throw new RaiseUnsupportedError(
351
+ `cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
352
+ );
353
+ }
354
+ });
355
+ return placeFields(structName, dataFields);
356
+ }
357
+
358
+ /** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
359
+ * size its compiler gives it). */
360
+ function fieldSize(t: IrType): number {
361
+ switch (t.kind) {
362
+ case 'int':
363
+ return t.width / 8;
364
+ case 'array':
365
+ return t.count * fieldSize(t.elem);
366
+ case 'union':
367
+ return t.size;
368
+ default:
369
+ return 4;
205
370
  }
371
+ }
372
+
373
+ /** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
374
+ * before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
375
+ * plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
376
+ * compiler's struct/union size boundary, undefined where nobody measured it (`buildUnionStruct`). */
377
+ export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined): number {
378
+ const { accessesOf, order, arrayBases } = constOffsetAccesses(fn);
379
+
380
+ // Does this base's address have a source of truth OUTSIDE this function's access set? Consulted
381
+ // only when synthesis refuses an OVERLAP: see the caller below. Two kinds qualify, and both are
382
+ // read with the helpers raise/truncload.ts asks the same question with — one concept, one
383
+ // reading, and `globalCellOf` is the whole of that reading: it answers for `&gSym + K` as well as
384
+ // for the bare `&gSym`, and an interior address is its own base here (`constOffsetAccesses` keys
385
+ // on the operand Value), so a narrower opcode test would decline the shape `fixed-cell` admits.
386
+ const defs = defOpMap(fn);
387
+ const addressDeclaredElsewhere = (base: Value): boolean =>
388
+ globalCellOf(defs, base, 0) !== null || constAddressOf(defs, base, 0) !== null;
206
389
 
207
390
  // The NAME counter is seeded from the names already in the graph, not from this pass's own
208
391
  // success count: raise/memberarrays.ts runs first and mints `Struct<N>` types of its own, and two
@@ -224,19 +407,47 @@ export function recognizeStructs(fn: Fn): number {
224
407
  try {
225
408
  base.type = T.ptr(buildStruct(`Struct${name}`, accesses));
226
409
  } 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)) {
410
+ // A base whose ADDRESS is already declared somewhere other than this function's access set is
411
+ // not a struct this pass has to synthesize, so failing to synthesize one for it is not a
412
+ // reason to decline the function — PROVIDED the refusal is the OVERLAP one (its own class,
413
+ // never a substring of the message), whose residue is per-access spellings that stand on their
414
+ // own. Two kinds of base qualify:
415
+ // • a NAMED global (`globalCellOf`) — its declaration belongs to the project's own headers,
416
+ // and its constant-offset accesses render at L3 through the symbol context (member
417
+ // spelling when the map knows the layout, the honest cast spelling when it does not). The
418
+ // inhabitant is agbcc FUSING two adjacent u8 compares into one ldrh — `s.level == 8 &&
419
+ // s.world == 6` reads offset 12 at widths 1 AND 2, which is not a union, just two
420
+ // spellings of declared bytes.
421
+ // • a LITERAL ADDRESS (`constAddressOf`) — a cell the hardware placed, which L3 renders as
422
+ // a cast at each access's OWN width (`*(u16 *)0x4000004`). There is no layout to
423
+ // reconcile: two widths at one literal address are two casts of one address, which is
424
+ // what the source wrote. The inhabitant is a memory-mapped I/O register the source writes
425
+ // as a halfword and as a word (`sa3:Sio32MultiLoadMain`, `synthetic:unidev`).
426
+ // This is the DUAL of raise/truncload.ts's `fixed-cell` gate, not a relaxation of it: that
427
+ // gate refuses to FOLD the two accesses into one cast of a wider load, because a device read
428
+ // is not a read of the bytes around it. Both readings agree that the two accesses stay two.
429
+ //
430
+ // THE PACKED REFUSAL IS NOT FORGIVEN FOR ANY BASE. Its residue is not two spellings: a
431
+ // misaligned access renders through the element index `off / width`, which is a fraction and
432
+ // not C, so forgiving it would trade a loud decline for an uncompilable candidate. THIS layer
433
+ // is the one that can name the layout, which is why the refusal a reader sees is this one.
434
+ // structure/structure.ts `displacementIndex` refuses the fraction a second time where it
435
+ // would be computed, for the bases that never reach this pass at all (already typed, or a
436
+ // forgiven overlap); that message can only describe the access, not the layout.
437
+ //
438
+ // An ANONYMOUS base (a loaded pointer, a parameter) has no other source of truth: its layout
439
+ // is what this function's accesses say, and an overlap there says UNION. It is declared as
440
+ // one (`buildUnionStruct`), which declines in turn on what it cannot lay out. The two
441
+ // declared kinds above keep their casts although `buildUnionStruct`'s aliasing argument
442
+ // applies to them too: no row re-reads across a store there, and a union is the wrong
443
+ // declaration for both inhabitants (docs/level-tower.md, "A union").
444
+ if (!(e instanceof StructOverlapError)) {
445
+ throw e;
446
+ }
447
+ if (addressDeclaredElsewhere(base)) {
237
448
  continue;
238
449
  }
239
- throw e;
450
+ base.type = T.ptr(buildUnionStruct(`Struct${name}`, accesses, aggregateBoundary));
240
451
  }
241
452
  name++;
242
453
  count++;
@@ -249,13 +460,15 @@ export function recognizeStructs(fn: Fn): number {
249
460
  * `ir/struct-names.ts`, which the three struct minters share; this names the prefix. */
250
461
  export function firstFreeStructIndex(fn: Fn): number {
251
462
  return nextStructIndex(
252
- [...collectStructs(fn)].map((s) => s.name),
463
+ [...collectStructs(fn, true)].map((s) => s.name),
253
464
  'Struct',
254
465
  );
255
466
  }
256
467
 
257
468
  /** 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.
469
+ * value), deduped by name and sorted, for the backend to declare above the function. A struct the
470
+ * headers own (`declared`, ir/types.ts) is left out — the declarations block defines it — unless
471
+ * `withDeclared` asks for every name the graph uses, which is what a name allocator needs.
259
472
  *
260
473
  * "Mentions", not "references": this walks `fn.blocks` at the moment structuring runs, and the
261
474
  * result is CACHED on the SFn (`structure.ts`) and carried by every later `{...sfn}` pass, so a
@@ -266,11 +479,11 @@ export function firstFreeStructIndex(fn: Fn): number {
266
479
  * is recorded here rather than fixed speculatively. The fix, if a pass ever makes it reachable, is
267
480
  * the one l3/symbol-refs.ts already applies to symbol references: derive at the consumption point
268
481
  * (`backend/cfamily.ts` structDecls) instead of caching. */
269
- export function collectStructs(fn: Fn): StructType[] {
482
+ export function collectStructs(fn: Fn, withDeclared = false): StructType[] {
270
483
  const seen = new Map<string, StructType>();
271
484
  const consider = (t: IrType) => {
272
485
  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)) {
486
+ if (s && s.kind === 'struct' && (withDeclared || s.declared === undefined) && !seen.has(s.name)) {
274
487
  seen.set(s.name, { name: s.name, fields: s.fields, size: s.size });
275
488
  }
276
489
  };