@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -0,0 +1,399 @@
1
+ // asmlift — THE CALLER HALF OF A STRUCT RETURNED IN THE RETURN REGISTER.
2
+ //
3
+ // A callee the project declares to return a struct of one, two or four bytes, on a target whose
4
+ // rule (aggregate.ts `returnsInMemory`) hands it back in the return register — agbcc's r0 for a
5
+ // struct of one non-bitfield member, mwcc's r3 for any struct of eight bytes or less — fills that
6
+ // register with the struct's bytes as the target lays them in a word. The call plan lets the call
7
+ // write the register as its result, marked `REGISTER_STRUCT` (frontend/call-plan.ts
8
+ // `registerStructLocal` says which calls), so the frontend reads it as it reads any call's word.
9
+ //
10
+ // Here the word becomes what C says it is: the struct, assigned by the call to a local of its
11
+ // type, and re-read off that local. The call takes the local's address the way a call returning a
12
+ // struct through memory takes its destination (`sret`), so it prints `s = mk(x);`; the word is a
13
+ // load of the local at offset 0, of the struct's own size, and on a big-endian target shifted up to
14
+ // the top of the word, which is where the struct's first byte sits there (mwcc's `{u16}` member is
15
+ // `srwi r0,r3,16` off the call). Whatever the caller then takes out of the word — a truncating
16
+ // store, a shift, a mask — reads the local's bytes, and the member recovery spells it as the
17
+ // member it is: `gV = s.h;`. A 4-byte read of the local (a `{u32}`'s member, or agbcc's `{u16}`
18
+ // read whole by an add, `gH = *(s32 *)&s + gH;`) is spelled `*(s32 *)&s`, the frame-member spelling
19
+ // on every toolchain (ir/types.ts `spellsAccessType`): the value is right.
20
+ //
21
+ // WHAT THE IMAGE LEAVES OUT IS NO VALUE. A struct of one or two bytes — mwcc's; agbcc rounds every
22
+ // struct to four — fills only that much of the register, and the rest holds whatever the callee
23
+ // left. The load reads it as zero, which is right only where nothing reads it, so every reader of
24
+ // the register must need only the struct's bits (`demandedBits`): a truncating store, a shift that
25
+ // drops the rest, a mask. A function that reads past them — stores the whole word, compares it,
26
+ // returns it where its own declaration says it returns a value — declines; one whose return
27
+ // nothing declares returns no value instead, where that guessed return is the only such reader.
28
+ import { demandedBits } from '../ir/bits';
29
+ import {
30
+ type Block,
31
+ CALL_RETURN_OBJECT,
32
+ type Fn,
33
+ OWN_RETURN_OBJECT,
34
+ type Op,
35
+ REGISTER_STRUCT,
36
+ RETURNS_OBJECT,
37
+ STRUCT_HOMED,
38
+ type Value,
39
+ calleeName,
40
+ defOpMap,
41
+ mkOp,
42
+ mkValue,
43
+ replaceAllUsesWith,
44
+ } from '../ir/core';
45
+ import { qualified } from '../ir/discipline';
46
+ import { type IrType, T, typeEquals } from '../ir/types';
47
+ import { type FnProto, type Prototypes, declaresVoidReturn } from '../proto';
48
+ import type { TargetDescription } from '../target';
49
+ import { registerStructLocal } from './call-plan';
50
+ import { FrontendUnsupportedError } from './errors';
51
+
52
+ /** One call's struct, as `liftRegisterStructCalls` re-read it: the block it was assigned in, the
53
+ * local, the struct's size, the image word the frontend read, and the load of the struct's bytes
54
+ * (the word itself where no shift moves them). */
55
+ interface Image {
56
+ block: Block;
57
+ callee: string;
58
+ local: Value;
59
+ size: number;
60
+ word: Value;
61
+ loaded: Value;
62
+ /** the call as it was before it took the local, and the ops it added (`dropUnreadImage`) */
63
+ call: Op;
64
+ operands: Value[];
65
+ attrs: Op['attrs'];
66
+ added: Op[];
67
+ }
68
+
69
+ /** What `liftRegisterStructCalls` did to the function's own return. */
70
+ export interface OwnReturnOfCalls {
71
+ /** every `ret` hands back a call's struct whole, by value (`return mk(x);`) */
72
+ handedBack: boolean;
73
+ /** some call's struct is read in this function, so a word the function returns may be computed
74
+ * from it: through an op, a join, or a frame slot it is stored to and reloaded from (mwcc's home,
75
+ * an array, a local whose address escapes). The bytes do not say whether it is, and no pass can
76
+ * follow every such route through memory, so pipeline.ts `returnOwnStruct` refuses an own word
77
+ * return in any function that reads one. */
78
+ readsCallStruct: boolean;
79
+ }
80
+
81
+ /** Rewrites every call `REGISTER_STRUCT` marks into the struct assigned to a local of its declared
82
+ * type, its word re-read off that local. `prototypes` is the table the frontend planned the calls
83
+ * from, so each marked callee's declaration lays the local out; `ownDeclaration` is the function's
84
+ * own, stated or the context's, which says whether its return reads the register, and `ownStruct`
85
+ * the struct it declares it returns in that register, if it does.
86
+ *
87
+ * A FUNCTION THAT RETURNS WHAT A CALL RETURNED returns the call: where every `ret` hands back the
88
+ * very image a call to a callee declared to return the function's own struct left in the
89
+ * register, and nothing else reads that image, the call's value is the struct and each `ret`
90
+ * returns it, `return mk(x);`. Split into the local and a member copy instead, `ret.x = mk_ret.x`,
91
+ * it compiles to a frame copy the source never had (mwcc: `stw r3,8(r1)`). */
92
+ export function liftRegisterStructCalls(
93
+ fn: Fn,
94
+ prototypes: Prototypes,
95
+ target: TargetDescription,
96
+ ownDeclaration: FnProto | undefined,
97
+ ownStruct: Extract<IrType, { kind: 'struct' }> | undefined,
98
+ ): OwnReturnOfCalls {
99
+ const bigEndian = target.capabilities.endianness === 'big';
100
+ const images: Image[] = [];
101
+ const rets = fn.blocks.map((b) => b.ops[b.ops.length - 1]).filter((op) => op?.opcode === 'ret');
102
+ const marked = new Map<Value, Op>();
103
+ for (const op of fn.blocks.flatMap((b) => b.ops)) {
104
+ if (op.opcode === 'call' && op.attrs[REGISTER_STRUCT] !== undefined) {
105
+ marked.set(op.results[0], op);
106
+ }
107
+ }
108
+ if (marked.size === 0) {
109
+ return { handedBack: false, readsCallStruct: false };
110
+ }
111
+ const defs = defOpMap(fn);
112
+ const isFrameAddress = (v: Value): boolean => {
113
+ const d = defs.get(v);
114
+ return d?.opcode === 'laddr' && d.attrs[OWN_RETURN_OBJECT] !== true && d.attrs[CALL_RETURN_OBJECT] === undefined;
115
+ };
116
+ // a store of the image into a frame word is its home (`foldFrameHome`), which reads no member
117
+ const homeStore = (op: Op, word: Value): boolean =>
118
+ op.opcode === 'store' &&
119
+ op.operands[1] === word &&
120
+ op.operands[0] !== word &&
121
+ isFrameAddress(op.operands[0]) &&
122
+ !qualified(op);
123
+ const handedBack =
124
+ ownStruct !== undefined &&
125
+ rets.length > 0 &&
126
+ rets.every((r) => {
127
+ const word = r.operands.length === 1 ? r.operands[0] : undefined;
128
+ const call = word === undefined ? undefined : marked.get(word);
129
+ const type = call === undefined ? undefined : registerStructLocal(prototypes[calleeName(call)!], target);
130
+ return (
131
+ type !== undefined &&
132
+ typeEquals(type, ownStruct) &&
133
+ fn.blocks.every((b) =>
134
+ b.ops.every(
135
+ (op) =>
136
+ (op === r || homeStore(op, word!) || !op.operands.includes(word!)) &&
137
+ op.successors.every((e) => !e.args.includes(word!)),
138
+ ),
139
+ )
140
+ );
141
+ });
142
+ // a struct the source names — it has a home — is returned as that local, `return mk_ret;`; one
143
+ // with none is the call itself, `return mk(x);`
144
+ const returnedWhole = new Map<Value, Op>();
145
+ if (handedBack) {
146
+ for (const r of rets) {
147
+ const word = r.operands[0];
148
+ const call = marked.get(word)!;
149
+ const homed =
150
+ call.attrs[REGISTER_STRUCT] === STRUCT_HOMED || fn.blocks.some((b) => b.ops.some((op) => homeStore(op, word)));
151
+ if (homed) {
152
+ returnedWhole.set(word, r);
153
+ continue;
154
+ }
155
+ const { [REGISTER_STRUCT]: _, ...attrs } = call.attrs;
156
+ call.attrs = attrs;
157
+ call.results = [mkValue(ownStruct)];
158
+ r.operands = [call.results[0]];
159
+ }
160
+ }
161
+ for (const b of fn.blocks) {
162
+ for (let i = 0; i < b.ops.length; i++) {
163
+ const call = b.ops[i];
164
+ if (call.opcode !== 'call' || call.attrs[REGISTER_STRUCT] === undefined) {
165
+ continue;
166
+ }
167
+ const callee = calleeName(call);
168
+ const own = callee !== undefined && Object.hasOwn(prototypes, callee) ? prototypes[callee] : undefined;
169
+ // the type the plan marked the call by (`registerStructLocal` is its one derivation)
170
+ const type = registerStructLocal(own, target);
171
+ if (callee === undefined || type?.size === undefined) {
172
+ throw new Error(`'${fn.name}': a call marked to return a struct in a register has no declared struct`);
173
+ }
174
+ const size = type.size;
175
+ const word = call.results[0];
176
+ const { [REGISTER_STRUCT]: _, ...attrs } = call.attrs;
177
+ // a struct nothing reads is the call's statement alone, as a discarded scalar is
178
+ const read = fn.blocks.some(
179
+ (x) =>
180
+ x.ops.some((op) => op.operands.includes(word)) ||
181
+ x.ops.some((op) => op.successors.some((e) => e.args.includes(word))),
182
+ );
183
+ if (!read) {
184
+ call.attrs = attrs;
185
+ continue;
186
+ }
187
+ const object = mkOp('laddr', {
188
+ results: [mkValue(T.ptr(type))],
189
+ // one local per call, which `nth` keeps apart: two calls' structs are two objects, and one
190
+ // read of the first may come after the second
191
+ attrs: {
192
+ off: 0,
193
+ width: 1,
194
+ signed: false,
195
+ count: size,
196
+ aggregate: true,
197
+ [CALL_RETURN_OBJECT]: callee,
198
+ nth: images.length,
199
+ },
200
+ });
201
+ const local = object.results[0];
202
+ // the local's address is the call's first operand, as a hidden pointer's is, so every index
203
+ // the call names into its operands moves up one
204
+ const shifted = (key: string, step: number) =>
205
+ Array.isArray(attrs[key])
206
+ ? { [key]: (attrs[key] as number[]).map((v, k) => (k % step === 0 ? v + 1 : v)) }
207
+ : {};
208
+ call.attrs = {
209
+ ...attrs,
210
+ sret: true,
211
+ ...shifted('doubles', 1),
212
+ ...shifted('byValue', 2),
213
+ ...shifted('byValueWords', 2),
214
+ };
215
+ const operands = call.operands;
216
+ call.operands = [local, ...call.operands];
217
+ call.results = [mkValue(type)];
218
+ const lift = 32 - 8 * size;
219
+ const loaded = bigEndian && lift > 0 ? mkValue(T.unk(32)) : word;
220
+ const reads = [
221
+ mkOp('load', { operands: [local], results: [loaded], attrs: { off: 0, width: size, signed: false } }),
222
+ ];
223
+ if (loaded !== word) {
224
+ reads.push(mkOp('shl', { operands: [loaded], results: [word], attrs: { imm: lift } }));
225
+ }
226
+ b.ops.splice(i + 1, 0, ...reads);
227
+ // the locals open the entry block in the order of their calls
228
+ fn.blocks[0].ops.splice(images.length, 0, object);
229
+ if (b === fn.blocks[0]) {
230
+ i++;
231
+ }
232
+ images.push({ block: b, callee, local, size, word, loaded, call, operands, attrs, added: [object, ...reads] });
233
+ const ret = returnedWhole.get(word);
234
+ if (ret !== undefined) {
235
+ ret.operands = [local];
236
+ ret.attrs = { ...ret.attrs, [RETURNS_OBJECT]: true };
237
+ }
238
+ }
239
+ }
240
+
241
+ // THE MEMBER TAKEN BACK DOWN IS THE LOAD. A big-endian caller reads a sub-word struct's first
242
+ // member by shifting the register down by exactly what the image shifted it up (`srwi r0,r3,16`
243
+ // for a `{u16}`), and the load is narrower than the shift leaves room for, so the two shifts
244
+ // cancel: what that shift computes is the load itself.
245
+ for (const { word, loaded, size } of images) {
246
+ for (const op of fn.blocks.flatMap((x) => x.ops)) {
247
+ if (
248
+ loaded !== word &&
249
+ op.opcode === 'shr_u' &&
250
+ op.operands.length === 1 &&
251
+ op.operands[0] === word &&
252
+ op.attrs.imm === 32 - 8 * size
253
+ ) {
254
+ replaceAllUsesWith(fn, op.results[0], loaded);
255
+ }
256
+ }
257
+ }
258
+ for (const image of images) {
259
+ foldFrameHome(fn, image);
260
+ }
261
+ // what the image leaves out must be what no reader reads (`demandedBits`)
262
+ const structBits = (size: number): number =>
263
+ size >= 4 ? FULL : bigEndian ? (FULL << (32 - 8 * size)) >>> 0 : (1 << (8 * size)) - 1;
264
+ const readsPast = ({ word, size }: Image, voidOwn: boolean): boolean =>
265
+ (demandedBits(fn, word, voidOwn) & ~structBits(size)) >>> 0 !== 0;
266
+ // A RETURN NOTHING DECLARES IS NO READER OF WHAT THE IMAGE LEAVES OUT. With no own declaration,
267
+ // the `ret` reading the return register is the frontend's guess (a scalar callee's word is
268
+ // returned the same way, `v0 = g(); return v0;`), and the bytes do not decide it, as they do not
269
+ // decide a guessed arity (frontend/ssa.ts `noteStructImage`). Where that guess is the only reader
270
+ // past the struct's bits, and every `ret` hands back a call's image as it came back, the function
271
+ // returns no value: the register below a struct of one or two bytes holds none, so no C return
272
+ // can be what it holds, and `void` is the spelling those bytes compile from.
273
+ if (
274
+ ownDeclaration === undefined &&
275
+ images.some((image) => readsPast(image, false)) &&
276
+ images.every((image) => !readsPast(image, true)) &&
277
+ rets.every((r) => r.operands.length === 1 && images.some(({ word }) => word === r.operands[0]))
278
+ ) {
279
+ for (const r of rets) {
280
+ r.operands = [];
281
+ }
282
+ // …and a struct only that guess read is now one nothing reads: the call's statement alone
283
+ for (const image of [...images]) {
284
+ if (dropUnreadImage(fn, image)) {
285
+ images.splice(images.indexOf(image), 1);
286
+ }
287
+ }
288
+ }
289
+ const voidOwn = declaresVoidReturn(ownDeclaration);
290
+ for (const image of images) {
291
+ const { size, callee, word } = image;
292
+ if (readsPast(image, voidOwn)) {
293
+ // a home `foldFrameHome` left — one read outside the call's block (mwcc 2.3.3 stores the
294
+ // word whole in each arm of an `if` and reads the member in the join), or written again
295
+ // before it is read — is no read past the struct, only one this fold does not follow
296
+ if (fn.blocks.some((b) => b.ops.some((op) => homeStore(op, word) && op.attrs.width === 4))) {
297
+ throw new FrontendUnsupportedError(
298
+ `cannot lift '${fn.name}': local stack frames not supported — \`${callee}\` hands back a struct of ` +
299
+ `${size} byte${size === 1 ? '' : 's'} in ${target.returnReg}, and its frame home is written again or ` +
300
+ "read where the fold into the call's local does not reach",
301
+ );
302
+ }
303
+ throw new FrontendUnsupportedError(
304
+ `cannot lift '${fn.name}': \`${callee}\` hands back a struct of ${size} byte${size === 1 ? '' : 's'} in ` +
305
+ `${target.returnReg}, and this function reads bits of the register past it — what the callee left there is no value`,
306
+ );
307
+ }
308
+ }
309
+ return { handedBack, readsCallStruct: images.length > 0 };
310
+ }
311
+
312
+ /** Put back the call `image` was built from, where nothing outside the ops it added reads its
313
+ * word, its load or its local. */
314
+ function dropUnreadImage(fn: Fn, image: Image): boolean {
315
+ const own = new Set([image.call, ...image.added]);
316
+ const read = (v: Value): boolean =>
317
+ fn.blocks.some((b) =>
318
+ b.ops.some((op) => !own.has(op) && (op.operands.includes(v) || op.successors.some((e) => e.args.includes(v)))),
319
+ );
320
+ if (read(image.word) || read(image.loaded) || read(image.local)) {
321
+ return false;
322
+ }
323
+ image.call.operands = image.operands;
324
+ image.call.attrs = image.attrs;
325
+ image.call.results = [image.word];
326
+ for (const b of fn.blocks) {
327
+ b.ops = b.ops.filter((op) => !image.added.includes(op));
328
+ }
329
+ return true;
330
+ }
331
+
332
+ /** THE STRUCT'S FRAME HOME IS THE LOCAL. A compiler that gives the struct a home in the frame stores
333
+ * the register there as it comes back (mwcc: `bl mk; stw r3,8(r1); lhz r0,8(r1)` for `s = mk(x);
334
+ * gOut = s.a;`) and reads its members off the home, so the home's bytes are the local's. Each load
335
+ * of them is rewritten to read the local, which spells it as the member it is, and the store goes
336
+ * where nothing else touches those bytes — the C's own assignment is that store.
337
+ *
338
+ * Only what the bytes prove is folded. The home is a frame object whose address is no value — only
339
+ * ever the base of a load or a store — so nothing but those accesses reaches its bytes; the store
340
+ * writes the image whole (the word, or on big-endian the struct's own bytes, `sth`), unqualified,
341
+ * in the call's block; and only the loads that follow it there, before any other store to the
342
+ * home's bytes, and that lie inside the struct, read what it stored. A load of the pad past a
343
+ * struct of one or two bytes reads no member, and stops the walk. */
344
+ function foldFrameHome(fn: Fn, { block, local, size, word, loaded }: Image): void {
345
+ const defs = defOpMap(fn);
346
+ const frameOff = (v: Value): number | undefined => {
347
+ const d = defs.get(v);
348
+ return d?.opcode === 'laddr' &&
349
+ d.attrs[CALL_RETURN_OBJECT] === undefined &&
350
+ d.attrs[OWN_RETURN_OBJECT] !== true &&
351
+ typeof d.attrs.off === 'number'
352
+ ? d.attrs.off
353
+ : undefined;
354
+ };
355
+ const at = block.ops.findIndex(
356
+ (op) =>
357
+ op.opcode === 'store' &&
358
+ frameOff(op.operands[0]) !== undefined &&
359
+ !qualified(op) &&
360
+ ((op.operands[1] === word && op.attrs.width === 4) || (op.operands[1] === loaded && op.attrs.width === size)),
361
+ );
362
+ if (at < 0) {
363
+ return;
364
+ }
365
+ const home = block.ops[at];
366
+ const base = frameOff(home.operands[0])! + (home.attrs.off as number);
367
+ const touches = (op: Op): boolean => {
368
+ const off = frameOff(op.operands[0]);
369
+ const from = (off ?? NaN) + (op.attrs.off as number);
370
+ return from < base + (home.attrs.width as number) && from + (op.attrs.width as number) > base;
371
+ };
372
+ // every frame address in the function is only the base of an access, or nothing may be folded
373
+ const ops = fn.blocks.flatMap((b) => b.ops);
374
+ for (const op of ops) {
375
+ const bases = op.opcode === 'load' || op.opcode === 'store' ? 1 : 0;
376
+ if (op.operands.slice(bases).some((v) => frameOff(v) !== undefined)) {
377
+ return;
378
+ }
379
+ }
380
+ for (const op of block.ops.slice(at + 1)) {
381
+ if (op.opcode === 'store' && touches(op)) {
382
+ break;
383
+ }
384
+ if (op.opcode !== 'load' || !touches(op)) {
385
+ continue;
386
+ }
387
+ const from = frameOff(op.operands[0])! + (op.attrs.off as number) - base;
388
+ if (from < 0 || from + (op.attrs.width as number) > size || qualified(op)) {
389
+ break;
390
+ }
391
+ op.operands = [local, ...op.operands.slice(1)];
392
+ op.attrs = { ...op.attrs, off: from };
393
+ }
394
+ if (!ops.some((op) => op !== home && (op.opcode === 'load' || op.opcode === 'store') && touches(op))) {
395
+ block.ops.splice(block.ops.indexOf(home), 1);
396
+ }
397
+ }
398
+
399
+ const FULL = 0xffffffff;
@@ -13,13 +13,18 @@
13
13
  // name, and every refusing kind gets its own sentence naming what was seen.
14
14
  //
15
15
  // Consumed by frontend/ppc.ts, which refuses before it recovers, and by frontend/thumb.ts's
16
- // literal-pool reader. A function-scope static is the one kind neither refuses by name: the
17
- // function DEFINES it, so both read its definition out of the target (frontend/local-object.ts) and
18
- // the lift writes it in the body under the name `localStaticName` takes apart, refusing only where
19
- // the definition cannot be read. Thumb asks about that kind and the predefined names alone rather
20
- // than taking `unspellableReason`'s answer for every kind, because a pool word is not a relocation:
21
- // agbcc packs `.L` labels into pools, and this policy calls a leading dot `section-local` — an
22
- // offset into a section, not an object — which is false about a code label in the same file.
16
+ // literal-pool reader. Two kinds are names a lift may still WRITE, because the object's definition
17
+ // is in hand and the definition is what the source wrote:
18
+ // • a function-scope static (`local-static`): the function DEFINES it, so both frontends read its
19
+ // definition out of the target (frontend/local-object.ts) and the lift writes it in the body
20
+ // under the name `localStaticName` takes apart, refusing only where the definition cannot be read;
21
+ // • the compiler's anonymous constant (`anon-pool`): no source names it, but a local aggregate's
22
+ // initializer image is what the source wrote. Thumb reads it as `LocalObject.anonymous`, which
23
+ // l3/constinit.ts spells back as that initializer or refuses. ppc still refuses `@N` by name:
24
+ // every one a lift reaches is a float or a string handed out by address, and mwcc copies an
25
+ // image inline into a frame local its frontend refuses first.
26
+ // Thumb asks about those kinds and the predefined names alone rather than taking
27
+ // `unspellableReason`'s answer for every kind, because a pool word is not a relocation.
23
28
  //
24
29
  // A kind is listed only when it behaves differently: the decomp projects' generated labels
25
30
  // (`lbl_1_bss_2464`, `fn_1_458`) are ordinary identifiers that the project's own headers declare
@@ -55,6 +60,9 @@ export type RelocSymbolKind =
55
60
  * {@link classifyRelocSymbol}. */
56
61
  const LOCAL_STATIC = /^([A-Za-z_][A-Za-z0-9_]*)[.$](\d+)$/;
57
62
 
63
+ /** A GNU assembler local label, `.L` and the rest of an identifier: `.LC0`, `.L5`. */
64
+ const GAS_LOCAL = /^\.L[\w.$]*$/;
65
+
58
66
  /** The identifiers a compiler defines inside every function, whose objects it names like a static. */
59
67
  const PREDEFINED = new Set(['__func__', '__FUNCTION__', '__PRETTY_FUNCTION__']);
60
68
 
@@ -69,6 +77,13 @@ export function classifyRelocSymbol(sym: string): RelocSymbolKind {
69
77
  if (sym.startsWith('@')) {
70
78
  return 'anon-pool'; // `@193` — mwcc's anonymous string/constant pool entries
71
79
  }
80
+ // `.LC0` — a GNU assembler LOCAL label (the `.L` prefix keeps it out of the symbol table), which
81
+ // is how gcc, and so agbcc, names an anonymous constant (varasm.c `output_constant_def`). Asked
82
+ // before `section-local`, whose leading dot it shares: it labels a datum, not a section. A `.L`
83
+ // label may label code as well; reading the definition is what tells the two apart.
84
+ if (GAS_LOCAL.test(sym)) {
85
+ return 'anon-pool';
86
+ }
72
87
  if (sym.startsWith('.')) {
73
88
  return 'section-local'; // `...bss.0`, `.rodata` — an offset into a section
74
89
  }