@asmlift/core 0.4.0 → 0.6.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 (87) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +113 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
@@ -0,0 +1,371 @@
1
+ // L3 re-spelling: spell a leaf base's constant subscript as a struct MEMBER, so the offset stays
2
+ // in the instruction's displacement instead of folding into the address the compiler materializes.
3
+ //
4
+ // THE COMPILER FACT this exists for is the one `l3/basecse.ts`'s header states in the imperative
5
+ // and cannot act on: on a target that declares `compilerBehaviors.foldsConstAddrOffset`, a
6
+ // constant SUBSCRIPT folds into the literal the address materializes, while an aggregate MEMBER
7
+ // offset stays in the memory operand.
8
+ //
9
+ // ((u16 *)0x3003468)[7] → .word 0x3003476 + ldrh r0, [r0]
10
+ // ((struct S *)0x3003468)->m14 → .word 0x3003468 + ldrh r0, [r0, #0xe]
11
+ //
12
+ // Both denote the same cell. Which one the source wrote is not derivable from the C, but the ASM
13
+ // says which one the compiler was given: an offset that reached the load's own displacement got
14
+ // there because nothing folded it, and only a member (or a named base — that is `/basefold`'s
15
+ // spelling) leaves it there. `l3/ast.ts`'s `index.operandOff` carries that displacement down from
16
+ // the lift, because the fold at L3 (`idx = idxVal + off / width`, structure/structure.ts) makes
17
+ // the two indistinguishable afterwards.
18
+ //
19
+ // THE ROSTER ALREADY OFFERS THE OTHER SOURCE OF THE SAME SHAPE. `/basefold` reads the identical
20
+ // evidence and answers it with a NAMED BASE (`u16 *p = (u16 *)C; p[7]`), which also keeps the
21
+ // displacement. The member is the second source and was in no fan; the two are different C and
22
+ // different register pressure, so both ride and the differ referees.
23
+ //
24
+ // SCOPE — a LEAF base only (a bare `addr` or a bare `const`), which is `l3/basecse.ts`'s
25
+ // `isHoistableBase` population. A computed base (`((u16 *)v0)[8]`) is out: the address is already
26
+ // held somewhere, so nothing folded into a literal and the evidence says nothing — that shape is
27
+ // `/addr-home`'s (structure/analysis.ts). It is a GATE rather than a collection filter so the
28
+ // refusal is attributable: `firstRejection` names it.
29
+ //
30
+ // NO DEVICE-REGISTER REFUSAL, which is a priced decision and not an omission. A base inside the
31
+ // target's declared `deviceRegisters` window is admitted like any other, so this pass will offer
32
+ // `((struct Off0 *)&REG_DMA3SAD)->m8` — a struct declared over the register file, carrying no
33
+ // qualifier. Three measurements say a refusal there buys nothing for that price.
34
+ //
35
+ // REACH. Every device-window base reaching this table is already refused by `no-operand-off`
36
+ // (49 of them map-less, 2 map-ful, over all four checkouts — 1417 structured functions map-less,
37
+ // 398 map-ful): the DMA idiom writes offset 0 as well as offset 8, and offset 0 leaves no
38
+ // displacement. A window refusal removes ZERO further bases in either configuration.
39
+ //
40
+ // PREMISE. It would be refusing a dropped qualifier, and there is none to drop: `/volatile`
41
+ // wraps the base in a CAST that `non-leaf-base` refuses, so the two levers cannot compose and
42
+ // the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
43
+ // `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
44
+ // scores zero whichever way it is spelled.
45
+ //
46
+ // CONFIGURATION. A numeric window is read through `addrConst`, and a symbol map turns a device
47
+ // address into a named `addr` no range can see. So such a refusal fires map-less and not
48
+ // map-ful on the SAME function — and the real tier runs map-ful. A refusal a symbol map
49
+ // switches off is not a refusal.
50
+ //
51
+ // What referees it instead is the differ, and `offmember.test.ts` pins the price on real compiled
52
+ // asm: on a DMACNT spin that never touches DMASAD — the one shape `no-operand-off` does not cover
53
+ // — the member spelling is offered and LOSES, 52 candidates, best
54
+ // `unsigned/derived-home/livebase/volatile: 4` against `/offmember`'s 5. A row where it WINS over
55
+ // MMIO is what would earn a gate here, together with a window reading that survives a symbol map.
56
+ //
57
+ // NOT AN EXTENSION OF raise/struct-arrays.ts. That pass mints an element struct off the `mul`/`shl`
58
+ // STRIDE idiom in the machine code, and this shape has no scale at all — the subscript is a
59
+ // constant and the base is a leaf, so there is no stride to read. It is a SPELLING of a tree
60
+ // structuring already produced, which is what puts it in `l3/` beside the other re-spellings.
61
+ //
62
+ // SOUNDNESS. The member offset is `idx * width`, the access's own byte offset from the base — never
63
+ // `operandOff`, which is EVIDENCE about where the offset travelled and is only part of it when the
64
+ // address carried the rest. So the respelled node addresses the same cell by construction, and the
65
+ // one way it could not is a layout C cannot reproduce — a member C would seat somewhere other than
66
+ // where the asm read it. `unspellable-layout` refuses that, and it is the table's only sound gate.
67
+ //
68
+ // REFUSAL. `spellOperandMembers` returns `null` when no base was admitted, so a function with no
69
+ // eligible site contributes no candidate and costs nothing.
70
+ //
71
+ // WHAT THE DECLARATION GOVERNS, and it is narrower than it looks: the CONSTANT SUBSCRIPTS this
72
+ // pass grouped under one base expression, and nothing else. A sibling
73
+ // access through the same address that this pass has no member spelling for — a variable
74
+ // subscript, a `lead`-prefixed one — is left exactly as it was, keeping its own cast, and a
75
+ // struct-array element off the same numeric constant reaches the address through a different base
76
+ // expression and so a different key entirely. So one address really can leave here spelled two
77
+ // ways, and `offmember.test.ts` emits exactly this pair rather than describing it:
78
+ // `((struct Off0 *)50345232)->m16` beside `((s32 *)50345232)[a0]`.
79
+ //
80
+ // THAT IS UGLY AND IT IS NOT UNSOUND, and only the second half is a gate's business. Every
81
+ // access carries its own cast, so no access reads a byte it did not read before; the seating check
82
+ // below judges the accesses the declaration is built FROM, which are exactly the accesses
83
+ // respelled through it, so its argument is over a total population of what it governs. The
84
+ // contradiction is between two FICTIONAL TYPES over one address, which is a readability claim —
85
+ // `quality`'s clientele, not a gate's.
86
+ //
87
+ // AND THE TIGHTER RULE IS PRICED, so it is not re-derived from scratch. Refusing a base whose
88
+ // siblings this pass cannot spell removes `a:gCallbackQueue` from
89
+ // `kleod:ProcessInputAndUpdateEntities` (asserted by dumping that row's winning source both ways:
90
+ // 9 `struct Off*` declarations become 2, and the survivor is `gUnk_03004C20`) and COSTS that row
91
+ // score, while protecting no row anywhere in the artifact. NO BRACKET IS QUOTED, and that is a
92
+ // discipline rather than a gap: a bracket is a claim about the whole tree, so BOTH endpoints move
93
+ // whenever anything else does. Read the unablated endpoint off the committed artifact — where
94
+ // `kleod:ProcessInputAndUpdateEntities` scores 211 — and re-measure the ablated one before writing
95
+ // a pair anywhere.
96
+ // "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
97
+ // extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
98
+ // move at all. THAT POPULATION IS A QUERY, not a count to keep in step by hand — the rows of
99
+ // `apps/benchmark/results/results.json` whose `asmlift.candidateLabel` contains `offmember` — and
100
+ // it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
101
+ // `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
102
+ // `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
103
+ // `sa3:sub_803213C` all still MATCH, `kleod:RollRandomLevelVariant` 18,
104
+ // `kleod:CountCollectedGems` 290, `kleod:UpdateWorldMapNodeAnim` 157, each the artifact's own
105
+ // number. A gate needs a row it protects. (One side effect worth recording: with the rule on, PI's
106
+ // winner becomes `unsigned/setup-args/no-ptr-elem/offmember` — an arm that wins no REAL row wins
107
+ // there. It is an ablation's artifact, not a reason to ship either.)
108
+ //
109
+ // ALL-OR-NOTHING PER FUNCTION, and that is a PRICE rather than a property. Every admitted base is
110
+ // respelled together in one candidate, so a function with two admitted bases where the target
111
+ // folded one and kept the other in the operand has no reachable spelling — the coverage hole
112
+ // `l3/ptrfield.ts` measures for its fields. `l3/basecse.ts` is NOT a second witness to it but the
113
+ // counterexample: its `UNFOLDED_GATES` cuts across `LIVEBASE_BLOCK_GATES` rather than refining it,
114
+ // so a partial answer IS reachable there — a proper nonempty subset on 13 of the agbcc rows the
115
+ // artifact carried when that census ran,
116
+ // one of three bases on `synthetic:dmascope` — and that is the way out of this hole too: not a
117
+ // per-base fork, but a SECOND PREDICATE separating the bases the differ has to choose between.
118
+ // The per-base fork is 2^n and the family's standing price for forking ten refusal sites per site
119
+ // was 1024x, so the arbitrary subsets stay unreachable until a row demands one.
120
+ // Measured over klonoa's 69
121
+ // lifting functions (lift → idioms → raise → structure, one pass per configuration): 36 admitted
122
+ // bases over 25 functions map-less and 38 over 24 map-ful, so the multi-base functions really are
123
+ // the majority of the surplus and a missing subset has somewhere to live. The other three
124
+ // checkouts admit 32 bases over 32 functions (af), 10 over 10 (marioparty3) and 2 over 2
125
+ // (snowboardkids2), all map-less.
126
+ import { nextStructIndex } from '../ir/struct-names';
127
+ import { type IrType, T, scalarTypeForAccess } from '../ir/types';
128
+ import type { Expr, SFn, StructType } from './ast';
129
+ import { mapExprChildren, mapStmtExprs, walkExprs } from './ast';
130
+ import { type Gate, firstRejection } from './gates';
131
+
132
+ /** One observed constant-subscript access through a base. */
133
+ interface Site {
134
+ /** the member's byte offset from the base — `idx * width`, the address the node denotes */
135
+ off: number;
136
+ width: number;
137
+ signed: boolean;
138
+ /** the displacement the instruction carried, when it carried one (l3/ast.ts `operandOff`) */
139
+ operandOff?: number;
140
+ }
141
+
142
+ /** The identity of an access's base, for grouping. Leaf bases key by value; everything else keys
143
+ * by its printed shape, so a non-leaf base still reaches the table and is refused by NAME. */
144
+ const baseKey = (e: Expr): string =>
145
+ e.k === 'addr' ? `a:${e.name}` : e.k === 'const' ? `c:${e.value}` : `x:${JSON.stringify(e)}`;
146
+
147
+ /** What the gates judge: one base and every constant-subscript access through it. */
148
+ export interface OffmemberBase {
149
+ key: string;
150
+ /** the base is a bare `addr`/`const` — the population whose address the compiler materializes */
151
+ leafBase: boolean;
152
+ /** some access through this base carries no memory-operand displacement, so nothing says its
153
+ * offset was ever anywhere but the address */
154
+ missingOperandOff: boolean;
155
+ /** some access's subscript is WIDER than the displacement the instruction carried — the address
156
+ * expression held the rest, and how that split maps back onto one member is not measured */
157
+ indexCarriesMore: boolean;
158
+ /** the accesses cannot be declared as a plain C struct seating each at its own offset */
159
+ unspellableLayout: boolean;
160
+ }
161
+
162
+ export const OFFMEMBER_GATES: readonly Gate<OffmemberBase>[] = [
163
+ {
164
+ id: 'non-leaf-base',
165
+ why: 'a computed base is already held somewhere, so nothing folded into a literal',
166
+ // A REACH argument, which is why it claims no soundness and owes no guard — and it is the
167
+ // only thing standing between this pass and a dropped qualifier, which the header's
168
+ // no-device-refusal paragraph is the other half of. A `/volatile` base reaches L3 as a CAST
169
+ // over the constant, so this gate excludes it as a side effect of excluding every computed
170
+ // base, and the natural widening ("a cast of a leaf const is still a leaf") would respell a
171
+ // qualified access as an unqualified member with no test failing. Nothing downstream would
172
+ // object. So the widening is forbidden HERE, at this gate, and a round that wants it owes a
173
+ // qualifier-preserving spelling first.
174
+ sound: false,
175
+ rejects: (c) => !c.leafBase,
176
+ },
177
+ {
178
+ id: 'no-operand-off',
179
+ why: 'an offset that never reached the instruction is not evidence of a member',
180
+ sound: false,
181
+ rejects: (c) => c.missingOperandOff,
182
+ },
183
+ {
184
+ id: 'index-carries-more',
185
+ why: 'the address carried part of the offset, and that decomposition is unmeasured',
186
+ sound: false,
187
+ rejects: (c) => c.indexCarriesMore,
188
+ },
189
+ {
190
+ id: 'unspellable-layout',
191
+ why: 'a struct C cannot seat at the observed offsets would address different bytes',
192
+ sound: true,
193
+ guardedBy: 'offmember.test.ts: a base whose accesses no plain struct can seat is refused',
194
+ rejects: (c) => c.unspellableLayout,
195
+ },
196
+ ];
197
+
198
+ /** The members this base is spelled through: one field per distinct offset. Shared by the seating
199
+ * PREDICATE and the layout BUILDER so the two cannot judge one field set and declare another —
200
+ * and, since `seatable` now refuses an offset whose views disagree on width OR signedness, every
201
+ * surviving offset has exactly one view and the pick below is an identity on an admitted base.
202
+ * It is kept because this runs on ABLATED tables too, where the gate is gone and something still
203
+ * has to choose; preferring the signed view keeps that choice the widest one. */
204
+ function membersOf(sites: readonly Site[]): Site[] {
205
+ const byOff = new Map<number, Site>();
206
+ for (const s of sites) {
207
+ const prev = byOff.get(s.off);
208
+ if (!prev || (s.signed && !prev.signed)) {
209
+ byOff.set(s.off, s);
210
+ }
211
+ }
212
+ return [...byOff.values()].sort((a, b) => a.off - b.off);
213
+ }
214
+
215
+ /** Can plain C seat every member at the offset the asm read it at, AND does the member read the
216
+ * same value the access did?
217
+ *
218
+ * FOUR ways it cannot, and the first three are the same defect — a field the declaration would
219
+ * place somewhere other than where the access reads: a NEGATIVE offset, which no member has; two
220
+ * views of ONE offset at different widths (a union); and two offsets whose byte ranges collide.
221
+ *
222
+ * THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
223
+ * offset at one width but
224
+ * DIFFERENT SIGNEDNESS (`ldrb` and `ldrsb` at the same address). One member has one type, so
225
+ * respelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
226
+ * signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
227
+ * change rather than a spelling change, and the differ can referee it only by luck: a masked or
228
+ * compared result compiles to the same bytes while the published C says something the asm does
229
+ * not. C spells it as a union, which is not a member, so it is refused exactly as the width union
230
+ * is. `l3/basecse.ts` keys `(base, width, signedness)` for the same reason, and `l3/typing.ts`
231
+ * states the rule in the imperative: signedness counts wherever the access extends.
232
+ *
233
+ * Natural ALIGNMENT is not among them, and that is an invariant rather than an omission: a
234
+ * member's offset here is the node's own `idx * width`, so it is a multiple of its width by
235
+ * construction and C's own alignment can always place it. A clause for it would have no
236
+ * inhabitant.
237
+ *
238
+ * This is what `unspellable-layout` asks, and the gate is the ONLY refusal — `layoutFor` below
239
+ * builds whatever it is handed, so ablating the gate really does emit the mislaid struct rather
240
+ * than quietly declining beside it. */
241
+ function seatable(sites: readonly Site[]): boolean {
242
+ let cursor = 0;
243
+ const views = new Map<number, Site>();
244
+ for (const s of sites) {
245
+ const v = views.get(s.off);
246
+ if (v !== undefined && (v.width !== s.width || v.signed !== s.signed)) {
247
+ return false;
248
+ }
249
+ views.set(s.off, s);
250
+ }
251
+ for (const m of membersOf(sites)) {
252
+ if (m.off < cursor) {
253
+ return false;
254
+ }
255
+ cursor = m.off + m.width;
256
+ }
257
+ return true;
258
+ }
259
+
260
+ /** The struct declaration for a base: each member at its own offset, gaps filled with an explicit
261
+ * `u8[N]` pad so the declaration reproduces the observed offsets on its own — the discipline
262
+ * raise/structs.ts and raise/struct-arrays.ts share. No trailing pad and no `size`: the spelling
263
+ * is `->m`, so `sizeof` is never taken. */
264
+ function layoutFor(name: string, sites: readonly Site[]): StructType {
265
+ const fields: StructType['fields'] = [];
266
+ let cursor = 0;
267
+ let pad = 0;
268
+ for (const m of membersOf(sites)) {
269
+ if (cursor < m.off) {
270
+ fields.push({ off: cursor, type: T.array(T.u(8), m.off - cursor), name: `_pad${pad++}` });
271
+ }
272
+ fields.push({ off: m.off, type: scalarTypeForAccess(m.width, m.signed), name: `m${m.off}` });
273
+ cursor = m.off + m.width;
274
+ }
275
+ return { name, fields };
276
+ }
277
+
278
+ /** Every CONSTANT-SUBSCRIPT `index` access, grouped by base, in first-appearance order — the
279
+ * accesses this pass has a member spelling for, which are exactly the ones the declaration it
280
+ * builds will govern. A sibling with a variable subscript or a `lead` is not one of them and is
281
+ * not counted: it keeps its own cast and this pass never rewrites it (see the header's
282
+ * "WHAT THE DECLARATION GOVERNS"). */
283
+ function collect(sfn: SFn): { order: string[]; sites: Map<string, Site[]> } {
284
+ const order: string[] = [];
285
+ const sites = new Map<string, Site[]>();
286
+ for (const e of walkExprs(sfn.body)) {
287
+ if (e.k !== 'index' || e.idx.k !== 'const' || e.lead !== undefined) {
288
+ continue;
289
+ }
290
+ const k = baseKey(e.base);
291
+ let list = sites.get(k);
292
+ if (!list) {
293
+ order.push(k);
294
+ list = [];
295
+ sites.set(k, list);
296
+ }
297
+ list.push({ off: e.idx.value * e.width, width: e.width, signed: e.signed, operandOff: e.operandOff });
298
+ }
299
+ return { order, sites };
300
+ }
301
+
302
+ /** The keys `gates` admits, with the struct each one is spelled through. */
303
+ function admit(
304
+ sfn: SFn,
305
+ gates: readonly Gate<OffmemberBase>[],
306
+ firstName: number,
307
+ ): Map<string, { struct: StructType; type: IrType }> {
308
+ const { order, sites } = collect(sfn);
309
+ const out = new Map<string, { struct: StructType; type: IrType }>();
310
+ let n = firstName;
311
+ for (const key of order) {
312
+ const list = sites.get(key)!;
313
+ const rejected = firstRejection(gates, {
314
+ key,
315
+ leafBase: key.startsWith('a:') || key.startsWith('c:'),
316
+ missingOperandOff: list.some((s) => s.operandOff === undefined),
317
+ indexCarriesMore: list.some((s) => s.operandOff !== undefined && s.operandOff !== s.off),
318
+ unspellableLayout: !seatable(list),
319
+ });
320
+ if (rejected === null) {
321
+ const struct = layoutFor(`Off${n}`, list);
322
+ out.set(key, { struct, type: T.ptr(T.struct(struct.name, struct.fields)) });
323
+ n++;
324
+ }
325
+ }
326
+ return out;
327
+ }
328
+
329
+ /** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
330
+ * The pass needs nothing else from the target: the fold this exists for is `foldsConstAddrOffset`
331
+ * and rank.ts asks that before offering the axis at all. */
332
+ export interface OffmemberOpts {
333
+ readonly gates?: readonly Gate<OffmemberBase>[];
334
+ }
335
+
336
+ /** The census without the rewrite, for a caller comparing what two tables would admit. */
337
+ export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly string[] {
338
+ return [...admit(sfn, opts.gates ?? OFFMEMBER_GATES, 0).keys()];
339
+ }
340
+
341
+ /** Re-spell every admitted base's constant subscripts as members of a synthesized struct.
342
+ * `null` when nothing is admitted — the axis then contributes no candidate. */
343
+ export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
344
+ // Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
345
+ // shared with the other two minters, which also records that this scan returns 0 on every
346
+ // corpus function). The prefix is what keeps this pass clear of raise/structs.ts's `Struct<N>`
347
+ // and raise/struct-arrays.ts's `Elem<N>`; the monotone scan is what keeps it clear of itself.
348
+ const seed = nextStructIndex(
349
+ (sfn.structs ?? []).map((s) => s.name),
350
+ 'Off',
351
+ );
352
+ const admitted = admit(sfn, opts.gates ?? OFFMEMBER_GATES, seed);
353
+ if (admitted.size === 0) {
354
+ return null;
355
+ }
356
+ const rewrite = (e: Expr): Expr => {
357
+ if (e.k === 'index' && e.idx.k === 'const' && e.lead === undefined) {
358
+ const hit = admitted.get(baseKey(e.base));
359
+ if (hit) {
360
+ return { k: 'field', base: { k: 'cast', to: hit.type, e: e.base }, name: `m${e.idx.value * e.width}` };
361
+ }
362
+ }
363
+ return mapExprChildren(e, rewrite);
364
+ };
365
+ const structs = [...(sfn.structs ?? []), ...[...admitted.values()].map((a) => a.struct)].sort((a, b) =>
366
+ a.name.localeCompare(b.name),
367
+ );
368
+ // `mapStmtExprs` recurses into nested statement lists, so one call per top-level statement
369
+ // covers the whole body.
370
+ return { ...sfn, structs, body: sfn.body.map((st) => mapStmtExprs(st, rewrite)) };
371
+ }
@@ -0,0 +1,96 @@
1
+ // L3 re-spelling lever: park incoming ARGUMENTS first in the entry straight-line prefix.
2
+ //
3
+ // A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
4
+ // compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
5
+ // to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
6
+ // (materialized statements first, edge copies last), while the compiler may have parked BEFORE
7
+ // any of those statements ran (hipress homes its counter in ip before loading a byte into the
8
+ // vacated r1). Both orders are legitimate C for the same asm; this lever emits the park-first
9
+ // sibling and the differ referees.
10
+ //
11
+ // SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
12
+ // leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
13
+ // scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
14
+ // re-spelled), so a constant initializer qualifies as a park and a param leaf is NOT required —
15
+ // what this lever moves is the leading run's ORDER, and a constant is as free of position as a
16
+ // parked register is; and a park never crosses a statement that writes a name it reads, reads or
17
+ // writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
18
+ // parks and of everything else — is preserved. Declines (null) when nothing moves.
19
+ //
20
+ // WHAT THE DESTINATION TEST IS, stated because it is narrower than "a local": the target is checked
21
+ // against the PARAMS alone, so every other name an `assign` can carry qualifies — and structure.ts
22
+ // spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
23
+ // The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
24
+ // name the park reads) through an ALIAS rather than by name is invisible to them — the same
25
+ // name-keyed model every lever at this level defers aliasing to.
26
+ //
27
+ // The kmc hipress residual is this axis's OTHER projection — its keep-load renders first while
28
+ // gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
29
+ // ordering lever rather than growing a sibling.
30
+ import type { Expr, SFn, Stmt } from './ast';
31
+ import { exprChildren, exprHasEffect } from './ast';
32
+
33
+ type Assign = Extract<Stmt, { k: 'assign' }>;
34
+
35
+ // `addr` counts as touching its name: `foo(&v)` may read or write v through the pointer, so a
36
+ // park must treat it exactly like a direct read-and-write of v.
37
+ const readVars = (e: Expr, acc: Set<string> = new Set()): Set<string> => {
38
+ if (e.k === 'var' || e.k === 'addr') {
39
+ acc.add(e.name);
40
+ }
41
+ for (const c of exprChildren(e)) {
42
+ readVars(c, acc);
43
+ }
44
+ return acc;
45
+ };
46
+
47
+ const pureOverParams = (e: Expr, params: ReadonlySet<string>): boolean => {
48
+ switch (e.k) {
49
+ case 'var':
50
+ return params.has(e.name);
51
+ case 'const':
52
+ return true;
53
+ case 'un':
54
+ case 'cast':
55
+ return pureOverParams(e.e, params);
56
+ case 'bin':
57
+ return pureOverParams(e.l, params) && pureOverParams(e.r, params);
58
+ default:
59
+ return false;
60
+ }
61
+ };
62
+
63
+ export function parkParamsFirst(sfn: SFn): SFn | null {
64
+ const params = new Set(sfn.params.map((p) => p.name));
65
+ let n = 0;
66
+ while (n < sfn.body.length && sfn.body[n].k === 'assign') {
67
+ n++;
68
+ }
69
+ const prefix = sfn.body.slice(0, n) as Assign[];
70
+ const parks: Assign[] = [];
71
+ const rest: Assign[] = [];
72
+ for (const st of prefix) {
73
+ // `rest` is exactly what this park would cross — a REFUSED earlier park is in it, so a later
74
+ // park is re-checked against it like any other crossed statement.
75
+ if (!params.has(st.name) && pureOverParams(st.value, params)) {
76
+ const reads = readVars(st.value);
77
+ if (
78
+ rest.every(
79
+ (c) =>
80
+ !exprHasEffect(c.value) && // a call can write anything an escaped address reaches
81
+ !reads.has(c.name) &&
82
+ c.name !== st.name &&
83
+ !readVars(c.value).has(st.name),
84
+ )
85
+ ) {
86
+ parks.push(st);
87
+ continue;
88
+ }
89
+ }
90
+ rest.push(st);
91
+ }
92
+ if (parks.length === 0 || parks.every((p, i) => prefix[i] === p)) {
93
+ return null; // nothing to move, or the parks already lead the prefix
94
+ }
95
+ return { ...sfn, body: [...parks, ...rest, ...sfn.body.slice(n)] };
96
+ }
@@ -0,0 +1,154 @@
1
+ // L3 poll-shape re-spelling levers: `pollGuards` regrows an empty bottom-tested loop's guard;
2
+ // `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
3
+ // its own trace argument below.
4
+ //
5
+ // do { } while (dma[2] & 0x80000000); → if (dma[2] & 0x80000000) { do { } while (…); }
6
+ //
7
+ // For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
8
+ // into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
9
+ // the choice leaves no instruction trace, only a register-allocation ripple: the extra
10
+ // source-level read raises the condition operands' ref counts, which re-orders the allocator's
11
+ // priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
12
+ // form the source spelled is unrecoverable from the bytes; both are emitted and the differ
13
+ // referees.
14
+ //
15
+ // SCOPE (decline over approximate): only a `dowhile` with an EMPTY body regrows a guard —
16
+ // there the two forms have IDENTICAL evaluation traces (each evaluates the condition until its
17
+ // first falsy result; the regrown guard IS the first bottom-test, not an extra one), so
18
+ // volatile reads, calls, any effect in the condition all count the same. A NON-empty body is
19
+ // where the forms genuinely differ (the body runs at least once vs at least zero times), which
20
+ // is why it never wraps. Declines (null) when no empty do-while exists.
21
+ import type { Expr, SFn, Stmt } from './ast';
22
+ import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs } from './ast';
23
+
24
+ export function pollGuards(sfn: SFn): SFn | null {
25
+ let changed = false;
26
+ const rewrite = (s: Stmt): Stmt => {
27
+ if (s.k === 'dowhile' && s.body.length === 0) {
28
+ changed = true;
29
+ return { k: 'if', cond: s.cond, then: [s], else: [] };
30
+ }
31
+ return mapStmtLists(s, (list) => list.map(rewrite));
32
+ };
33
+ const body = sfn.body.map(rewrite);
34
+ return changed ? { ...sfn, body } : null;
35
+ }
36
+
37
+ // L3 re-spelling lever: a materialized POLL re-reads in its own condition.
38
+ //
39
+ // v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
40
+ //
41
+ // The structurer materializes a loop-carried load into a named temp with a pre-loop read and a
42
+ // per-iteration re-read; the source may have spelled the read INSIDE the condition of an
43
+ // empty-bodied `while`. The two forms have IDENTICAL evaluation traces — the old form reads once
44
+ // before plus once per iteration, the new form reads once per condition evaluation, and both
45
+ // count 1 + iterations — so volatile reads COUNT the same; the condition gates below (call-free,
46
+ // no volatile-rooted or raw derefs) are what make the ORDER identical too: with no other
47
+ // observable effect in the condition, there is nothing for the embedded read to reorder against.
48
+ // What differs is bytes: the pre-read + temp spelling materializes an extra register and
49
+ // instruction the in-condition spelling does not.
50
+ //
51
+ // SCOPE (decline over approximate): each admission is one named predicate below and carries its own
52
+ // refusal's reason; the temp must additionally be the function's OWN non-volatile LOCAL, because a
53
+ // bare global's assigns are stores other code observes and its declaration cannot be dropped.
54
+ // Declines (null) when no poll matches.
55
+ const countVar = (e: Expr, n: string): number =>
56
+ (e.k === 'var' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countVar(c, n), 0);
57
+
58
+ const countAddr = (e: Expr, n: string): number =>
59
+ (e.k === 'addr' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countAddr(c, n), 0);
60
+
61
+ /** call- and marker-free: no effect the fold could move or duplicate. */
62
+ const pure = (e: Expr): boolean => e.k !== 'call' && e.k !== 'marker' && exprChildren(e).every(pure);
63
+
64
+ /** The var a deref's base stands on, through casts only — null for anything else (a raw address, an
65
+ * arithmetic base), which is exactly the set `condDerefsPlain` refuses. */
66
+ const rootVar = (e: Expr): string | null => (e.k === 'var' ? e.name : e.k === 'cast' ? rootVar(e.e) : null);
67
+
68
+ /** ORDER-safety for the condition's other reads: a deref there must be rooted at a var declared
69
+ * non-volatile — a volatile-rooted or raw-address deref is (or may be) an OBSERVABLE read the
70
+ * fold would unsequence against X inside one expression, where the original sequenced them. */
71
+ const condDerefsPlain = (e: Expr, volatileLocals: ReadonlySet<string>): boolean => {
72
+ if (e.k === 'index' || e.k === 'field') {
73
+ const rv = rootVar(e.base);
74
+ if (rv === null || volatileLocals.has(rv)) {
75
+ return false;
76
+ }
77
+ }
78
+ return exprChildren(e).every((c) => condDerefsPlain(c, volatileLocals));
79
+ };
80
+
81
+ /** Every mention of `n` in a statement list — reads, `&n`, and assign targets, at any depth. */
82
+ const occurs = (list: Stmt[], n: string): number =>
83
+ list.reduce(
84
+ (a, st) =>
85
+ a +
86
+ stmtExprs(st).reduce((x, e) => x + countVar(e, n) + countAddr(e, n), 0) +
87
+ (st.k === 'assign' && st.name === n ? 1 : 0) +
88
+ occurs(stmtChildren(st), n),
89
+ 0,
90
+ );
91
+
92
+ /** The loop body must be EXACTLY the one re-read: a single assign of the same variable to the same
93
+ * expression. Any other statement there is one the fold would delete along with the loop's body. */
94
+ const bodyIsTheSoleReread = (w: Extract<Stmt, { k: 'while' }>, a: Extract<Stmt, { k: 'assign' }>): boolean =>
95
+ w.body.length === 1 && w.body[0].k === 'assign' && w.body[0].name === a.name && exprEquals(w.body[0].value, a.value);
96
+
97
+ /** The condition must read the variable EXACTLY once, as a bare var: a second read would double X's
98
+ * per-iteration evaluation, and an `&v` is not a read at all and cannot be substituted. */
99
+ const condReadsVarOnce = (cond: Expr, n: string): boolean => countVar(cond, n) === 1 && countAddr(cond, n) === 0;
100
+
101
+ /** X must not mention the variable it is assigned to — the folded form evaluates X in a condition
102
+ * where that variable no longer exists. */
103
+ const valueIsSelfFree = (value: Expr, n: string): boolean => countVar(value, n) === 0 && countAddr(value, n) === 0;
104
+
105
+ /** The condition carries no effect and no observable read of its own, which is what makes the
106
+ * embedded X unsequenceable against anything: call/marker-free, every deref `condDerefsPlain`. */
107
+ const condFoldable = (cond: Expr, volatileLocals: ReadonlySet<string>): boolean =>
108
+ pure(cond) && condDerefsPlain(cond, volatileLocals);
109
+
110
+ /** The pattern owns EVERY occurrence of the variable: both assign targets and the one condition
111
+ * read, three in all, counted over the whole function and counting `&v`. Its declaration is dropped
112
+ * with the temp, so anything else mentioning it would be left naming a variable that is gone. */
113
+ const ownsEveryOccurrence = (body: Stmt[], n: string): boolean => occurs(body, n) === 3;
114
+
115
+ export function pollReads(sfn: SFn): SFn | null {
116
+ const ownPlain = new Set(
117
+ sfn.locals.filter((l) => l.volatile !== true && l.pointeeVolatile !== true).map((l) => l.name),
118
+ );
119
+ const volatileLocals = new Set(
120
+ sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
121
+ );
122
+ const subst = (e: Expr, n: string, x: Expr): Expr =>
123
+ e.k === 'var' && e.name === n ? x : mapExprChildren(e, (c) => subst(c, n, x));
124
+ const dropped = new Set<string>();
125
+ const rewriteList = (list: Stmt[]): Stmt[] => {
126
+ const out: Stmt[] = [];
127
+ for (let i = 0; i < list.length; i++) {
128
+ const a = list[i];
129
+ const w = list[i + 1];
130
+ if (
131
+ a.k === 'assign' &&
132
+ ownPlain.has(a.name) &&
133
+ w !== undefined &&
134
+ w.k === 'while' &&
135
+ bodyIsTheSoleReread(w, a) &&
136
+ condReadsVarOnce(w.cond, a.name) &&
137
+ valueIsSelfFree(a.value, a.name) &&
138
+ pure(a.value) &&
139
+ condFoldable(w.cond, volatileLocals) &&
140
+ ownsEveryOccurrence(sfn.body, a.name)
141
+ ) {
142
+ out.push({ k: 'while', cond: subst(w.cond, a.name, a.value), body: [] });
143
+ dropped.add(a.name);
144
+ i++;
145
+ continue;
146
+ }
147
+ out.push(recurse(a));
148
+ }
149
+ return out;
150
+ };
151
+ const recurse = (s0: Stmt): Stmt => mapStmtLists(s0, rewriteList);
152
+ const body = rewriteList(sfn.body);
153
+ return dropped.size > 0 ? { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) } : null;
154
+ }