@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,616 @@
1
+ // asmlift — a function-scope static's DEFINITION, read out of the target.
2
+ //
3
+ // A relocation (PPC) or a literal-pool word (Thumb) names a function-scope static by the linker's
4
+ // name, `sprHideTbl$797` or `tide.3`, and the counter in it is the compiler's
5
+ // (frontend/reloc-symbol.ts). No declaration elsewhere in the project describes the object: the
6
+ // source DEFINED it inside the function. So a lift that names it must also define it, and
7
+ // everything a definition needs is in the target: the section says the qualifier and whether it
8
+ // has an initializer, the size says how big it is, and the bytes are the initializer.
9
+ //
10
+ // This module reads that, and what an assembler listing shows beyond it — the directives the
11
+ // compiler wrote the initializer with and the alignment it placed the object at, which are facts
12
+ // about the source's type. Which C type the definition gets is decided where the accesses are
13
+ // spelled (structure/local-statics.ts).
14
+ //
15
+ // It REFUSES rather than guesses — with the tail of a sentence the caller opens by naming the
16
+ // static — when:
17
+ // • the target does not carry the definition (the symbol is absent, or its label heads no data);
18
+ // • the object sits in a section that is not read-only data, initialized data or bss;
19
+ // • a relocation falls inside its bytes that is no pointer-width absolute address of an ordinary
20
+ // global: one narrower or wider than a pointer, or off the object's words, or of another type,
21
+ // or naming what no declaration outside the function can (a section, the compiler's anonymous
22
+ // constant, another function-scope static — reloc-symbol.ts's kinds). Such an address is
23
+ // RECORDED (`LocalObject.relocs`) and left to the reader of the object to spell or refuse
24
+ // (structure/local-statics.ts);
25
+ // • its extent cannot be read: a directive inside its data run this reader does not parse, a
26
+ // size that disagrees with the bytes under the label, or an empty object;
27
+ // • its alignment cannot be read: an `.align` form this reader does not parse, or an object file
28
+ // whose `.comment` section does not record it;
29
+ // • another function names it too. A static of an inlined same-unit function is named by every
30
+ // function the compiler inlined it into — agbcc 2.9 puts `static inline counter`'s `n.3`
31
+ // ahead of its first caller, and mwcc -inline auto has `A` and `B` both address `n$4` — so
32
+ // re-declaring it inside one of them would split one object in two. Only the functions this
33
+ // input shows can be checked, which is the limit of this refusal: a single-function input
34
+ // cannot show a second referrer, and one that ends at this function cannot show a later one.
35
+ import {
36
+ type LocalObject,
37
+ type LocalObjectReloc,
38
+ type LocalObjects,
39
+ LOCAL_OBJECT_ADDRESS_BYTES as POINTER_BYTES,
40
+ } from '../ir/core';
41
+ import type { AsmData } from './asmdata';
42
+ import { classifyRelocSymbol, localStaticName, unspellableReason } from './reloc-symbol';
43
+
44
+ /** A definition, or why there is none: the tail of a refusal sentence the caller opens with
45
+ * "names a function-scope static ('<symbol>')", so every refusal of the kind reads the same. */
46
+ export type LocalObjectRead = LocalObject | { refused: string };
47
+
48
+ const refused = (why: string): { refused: string } => ({ refused: why });
49
+
50
+ /** Why an initializer's address of `symbol` cannot be recorded, as the tail of the refusal
51
+ * sentence — or null for an ordinary global's, which a declaration outside the function names.
52
+ * Every other kind is the naming policy's own (reloc-symbol.ts), with its own sentence. */
53
+ function addressRefusal(symbol: string): string | null {
54
+ const why = unspellableReason(symbol);
55
+ return why === null ? null : `whose initializer holds an address that ${why}`;
56
+ }
57
+
58
+ // ── GNU as text (agbcc) ──────────────────────────────────────────────────────────────────────────
59
+
60
+ /** A line without its `@` comment, where the `@` is not inside a string literal. */
61
+ function code(line: string): string {
62
+ let quoted = false;
63
+ for (let i = 0; i < line.length; i++) {
64
+ const c = line[i];
65
+ if (c === '\\' && quoted) {
66
+ i++;
67
+ } else if (c === '"') {
68
+ quoted = !quoted;
69
+ } else if (c === '@' && !quoted) {
70
+ return line.slice(0, i).trim();
71
+ }
72
+ }
73
+ return line.trim();
74
+ }
75
+
76
+ /** The section a GNU as section directive switches to, or null for a line that is not one. */
77
+ function sectionSwitch(line: string): string | null {
78
+ const m = line.match(/^\.(?:section\s+([^\s,]+)|(text|data|bss)\b)/);
79
+ return m ? (m[1] ?? `.${m[2]}`) : null;
80
+ }
81
+
82
+ const GAS_SECTIONS: Readonly<Record<string, LocalObject['section']>> = {
83
+ '.rodata': 'rodata',
84
+ '.data': 'data',
85
+ '.bss': 'bss',
86
+ };
87
+
88
+ /** A GNU as integer literal: decimal, `0x` hex or `0` octal, optionally negated. */
89
+ function gasInteger(s: string): number | null {
90
+ const m = s.trim().match(/^(-?)(0x[0-9a-f]+|0[0-7]*|[1-9]\d*)$/i);
91
+ if (!m) {
92
+ return null;
93
+ }
94
+ const radix = /^0x/i.test(m[2]) ? 16 : /^0\d/.test(m[2]) ? 8 : 10;
95
+ const mag = parseInt(radix === 16 ? m[2].slice(2) : m[2], radix);
96
+ return m[1] ? -mag : mag;
97
+ }
98
+
99
+ /** The named escapes agbcc's ASM_OUTPUT_ASCII writes (agbcc/gcc/thumb.h:195-260); every other
100
+ * unprintable byte it writes as `\ooo` octal. */
101
+ const GAS_ESCAPES: Readonly<Record<string, number>> = { n: 10, t: 9, r: 13, f: 12, b: 8, '\\': 92, '"': 34 };
102
+
103
+ /** The bytes of a GNU as string literal body (between its quotes), or null for an escape this
104
+ * reader does not decode. */
105
+ function gasString(body: string): number[] | null {
106
+ const out: number[] = [];
107
+ for (let i = 0; i < body.length; i++) {
108
+ const c = body[i];
109
+ if (c !== '\\') {
110
+ out.push(c.charCodeAt(0) & 0xff);
111
+ continue;
112
+ }
113
+ const n = body[++i];
114
+ const oct = body.slice(i).match(/^[0-7]{1,3}/);
115
+ if (oct) {
116
+ out.push(parseInt(oct[0], 8) & 0xff);
117
+ i += oct[0].length - 1;
118
+ } else if (n in GAS_ESCAPES) {
119
+ out.push(GAS_ESCAPES[n]);
120
+ } else {
121
+ return null;
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /** Little-endian bytes of `value` at `width` bytes — the only byte order agbcc's target has. */
128
+ const le = (value: number, width: number): number[] =>
129
+ Array.from({ length: width }, (_, i) => (value >>> (8 * i)) & 0xff);
130
+
131
+ /** What one data directive writes: its bytes, the width of each value it writes (null for
132
+ * padding, which writes none), whether one of those values is negative, whether it writes a
133
+ * string, and the addresses among its values, at their byte offsets in the directive. */
134
+ interface DirectiveData {
135
+ bytes: number[];
136
+ width: number | null;
137
+ negative: boolean;
138
+ string: boolean;
139
+ relocs: LocalObjectReloc[];
140
+ }
141
+
142
+ /** `sym`, `sym+K` or `sym-K` — the operand GNU as relocates to a symbol's address plus a constant. */
143
+ const GAS_ADDRESS = /^([A-Za-z_.$][\w.$]*)\s*(?:([+-])\s*(\S+))?$/;
144
+
145
+ /** One data directive's data; `{ refused }` for an operand that is no number and no address this
146
+ * reader records, null when the directive is not a data directive this reader parses. */
147
+ function dataBytes(directive: string, operands: string): DirectiveData | { refused: string } | null {
148
+ const width = /^(byte)$/.test(directive)
149
+ ? 1
150
+ : /^(short|hword|2byte)$/.test(directive)
151
+ ? 2
152
+ : /^(word|long|int|4byte)$/.test(directive)
153
+ ? 4
154
+ : 0;
155
+ if (width > 0) {
156
+ const out: number[] = [];
157
+ const relocs: LocalObjectReloc[] = [];
158
+ let negative = false;
159
+ for (const op of operands.split(',')) {
160
+ const v = gasInteger(op);
161
+ if (v !== null) {
162
+ negative ||= v < 0;
163
+ out.push(...le(v, width));
164
+ continue;
165
+ }
166
+ // Not a number, so the assembler relocates the value: an address, which the bytes do not hold.
167
+ const a = op.trim().match(GAS_ADDRESS);
168
+ // agbcc writes a negative addend `garr+-0x4` (compiled: `&garr[-1]`), so the constant keeps a sign
169
+ const k = a === null ? null : a[3] === undefined ? 0 : gasInteger(a[3]);
170
+ if (a === null || k === null) {
171
+ return refused(`whose initializer holds '${op.trim()}', an expression this reader does not read as an address`);
172
+ }
173
+ if (width !== POINTER_BYTES) {
174
+ return refused(
175
+ `whose initializer holds the address '${op.trim()}' in a ${width}-byte value — not a pointer's ${POINTER_BYTES}`,
176
+ );
177
+ }
178
+ const why = addressRefusal(a[1]);
179
+ if (why !== null) {
180
+ return refused(why);
181
+ }
182
+ relocs.push({ offset: out.length, symbol: a[1], addend: a[2] === '-' ? -k : k });
183
+ out.push(...le(0, width));
184
+ }
185
+ return { bytes: out, width, negative, string: false, relocs };
186
+ }
187
+ if (/^(space|skip|zero)$/.test(directive)) {
188
+ const [n, fill, ...rest] = operands.split(',').map((s) => gasInteger(s));
189
+ return n === null || n < 0 || rest.length > 0 || fill === null
190
+ ? null
191
+ : {
192
+ bytes: new Array<number>(n).fill((fill ?? 0) & 0xff),
193
+ width: null,
194
+ negative: false,
195
+ string: false,
196
+ relocs: [],
197
+ };
198
+ }
199
+ if (/^(ascii|asciz|string)$/.test(directive)) {
200
+ const m = operands.trim().match(/^"((?:[^"\\]|\\.)*)"$/);
201
+ const s = m ? gasString(m[1]) : null;
202
+ return s === null
203
+ ? null
204
+ : { bytes: directive === 'ascii' ? s : [...s, 0], width: 1, negative: false, string: true, relocs: [] };
205
+ }
206
+ return null;
207
+ }
208
+
209
+ /** The alignment written ahead of the label on line `at`, in bytes. agbcc writes `.align N` there
210
+ * exactly when the object's alignment exceeds a byte — after the section switch, before `.type`,
211
+ * `.size` and the label (varasm.c:1327-1329 `assemble_variable`) — so none means 1. Null for an
212
+ * alignment directive this reader does not read. */
213
+ function alignAhead(lines: readonly string[], at: number): number | null {
214
+ for (let i = at - 1; i >= 0; i--) {
215
+ const line = lines[i];
216
+ if (line === '' || /^\.(type|size|globl)\b/.test(line)) {
217
+ continue;
218
+ }
219
+ const a = line.match(/^\.align\s+(\d+)\s*(?:,.*)?$/);
220
+ if (a) {
221
+ return 2 ** Number(a[1]);
222
+ }
223
+ return /^\.(balign|p2align)\b/.test(line) ? null : 1;
224
+ }
225
+ return 1;
226
+ }
227
+
228
+ /** The directives that end an object's data run without being part of it: the next object's
229
+ * alignment or bss allocation, and what ends a file or opens code. Any other directive inside the
230
+ * run is one this reader cannot size, so it refuses there rather than end the object short — an
231
+ * unsized object has no `.size` to catch the truncation (telf.h:297-316 prints one only when the
232
+ * declaration knows the size). */
233
+ const RUN_ENDS: ReadonlySet<string> = new Set([
234
+ 'align',
235
+ 'balign',
236
+ 'p2align',
237
+ 'lcomm',
238
+ 'comm',
239
+ 'ident',
240
+ 'end',
241
+ 'code',
242
+ 'thumb',
243
+ 'thumb_func',
244
+ 'arm',
245
+ ]);
246
+
247
+ /** Read a function-scope static's definition out of GNU as text (agbcc's `.s`).
248
+ *
249
+ * Two shapes, both agbcc 2.9's (varasm.c `assemble_variable`): an object with NO initializer is
250
+ * `.lcomm name.N,size` — bss, with the exact size; an initialized one is a label under
251
+ * `.section .rodata` or `.data`, followed by its data directives. `.size name.N,K` is emitted for
252
+ * a sized declarator and NOT for an unsized one (`static const u8 tide[] = {…}` has none), so the
253
+ * extent is the data run under the label, which ends at the next label, section switch,
254
+ * instruction or one of {@link RUN_ENDS}, and `.size` where present must agree with it. */
255
+ export function readGasLocalObject(asm: string, symbol: string): LocalObjectRead {
256
+ const parts = localStaticName(symbol);
257
+ if (parts === null) {
258
+ return refused('whose name has no source spelling this reader knows');
259
+ }
260
+ const { name, counter: order } = parts;
261
+ const read = readGasDefinition(asm, symbol);
262
+ return 'refused' in read ? read : { name, symbol, order, ...read };
263
+ }
264
+
265
+ /** Read an ANONYMOUS constant out of GNU as text: the data run under a compiler-generated `.L`
266
+ * label (agbcc's `.LC0`), which is how agbcc 2.9 places the image a local aggregate's constant
267
+ * initializer is copied from (`u8 a[16] = {…}` is `ldr r1,=.LC0; mov r0,sp; mov r2,#16; bl memcpy`,
268
+ * varasm.c `output_constant_def`). Nothing in the source names it, so no declaration can spell it:
269
+ * the object is carried {@link LocalObject.anonymous} and spelled only as the initializer of the
270
+ * local it is copied into (l3/constinit.ts), or refused there.
271
+ *
272
+ * Read-only data only, with the same refusals as a static's definition: a `.L` label anywhere else
273
+ * is code, or data this reader has no reason to believe is a constant image. That gate is agbcc's
274
+ * placement, not every compiler's: IDO 7.1 places the same image in `.data` (compiled). Which
275
+ * names are anonymous is reloc-symbol.ts's decision (`anon-pool`). */
276
+ export function readGasAnonymousObject(asm: string, label: string): LocalObjectRead {
277
+ if (classifyRelocSymbol(label) !== 'anon-pool') {
278
+ return refused('whose label is not a compiler-generated `.L` label');
279
+ }
280
+ const read = readGasDefinition(asm, label);
281
+ if ('refused' in read) {
282
+ return read;
283
+ }
284
+ if (read.section !== 'rodata') {
285
+ return refused(`defined in ${read.section}, which is not the read-only data a constant image sits in`);
286
+ }
287
+ return { name: label, symbol: label, order: -1, anonymous: true, ...read };
288
+ }
289
+
290
+ /** A definition's section, size, bytes and directives, read from the label down. */
291
+ function readGasDefinition(
292
+ asm: string,
293
+ symbol: string,
294
+ ): Omit<LocalObject, 'name' | 'symbol' | 'order'> | { refused: string } {
295
+ const lines = asm.split('\n').map(code);
296
+ let declaredSize: number | null = null;
297
+ let section: string | null = null;
298
+ let at = -1;
299
+ let labelSection: string | null = null;
300
+ for (let i = 0; i < lines.length; i++) {
301
+ const line = lines[i];
302
+ const lcomm = line.match(/^\.lcomm\s+([^\s,]+)\s*,\s*(\S+?)(?:\s*,.*)?$/);
303
+ if (lcomm && lcomm[1] === symbol) {
304
+ const size = gasInteger(lcomm[2]);
305
+ if (size === null || size <= 0) {
306
+ return refused(`whose '.lcomm' size '${lcomm[2]}' is not a positive number`);
307
+ }
308
+ return { section: 'bss', size, bigEndian: false };
309
+ }
310
+ const sz = line.match(/^\.size\s+([^\s,]+)\s*,\s*(\S+)$/);
311
+ if (sz && sz[1] === symbol) {
312
+ declaredSize = gasInteger(sz[2]);
313
+ }
314
+ const sw = sectionSwitch(line);
315
+ if (sw !== null) {
316
+ section = sw;
317
+ }
318
+ const lab = line.match(/^([A-Za-z_.$][\w.$]*):/);
319
+ if (lab && lab[1] === symbol) {
320
+ if (at !== -1) {
321
+ return refused('whose label is defined twice in this asm');
322
+ }
323
+ at = i;
324
+ labelSection = section;
325
+ }
326
+ }
327
+ if (at === -1) {
328
+ return refused('whose definition this asm does not carry');
329
+ }
330
+ const kind = labelSection === null ? undefined : GAS_SECTIONS[labelSection];
331
+ if (kind === undefined) {
332
+ return refused(`defined in section '${labelSection ?? '(none)'}', which is not data this reader defines`);
333
+ }
334
+ const bytes: number[] = [];
335
+ const widths = new Set<number>();
336
+ let negative = false;
337
+ const strings = new Set<boolean>();
338
+ const gaps: [number, number][] = [];
339
+ const relocs: LocalObjectReloc[] = [];
340
+ const first = lines[at].replace(/^[A-Za-z_.$][\w.$]*:\s*/, '');
341
+ for (let i = at; i < lines.length; i++) {
342
+ const line = i === at ? first : lines[i];
343
+ if (line === '') {
344
+ continue;
345
+ }
346
+ if (/^[A-Za-z_.$][\w.$]*:/.test(line) || sectionSwitch(line) !== null) {
347
+ break;
348
+ }
349
+ const d = line.match(/^\.(\w+)\s*(.*)$/);
350
+ if (!d || RUN_ENDS.has(d[1])) {
351
+ break; // an instruction, or what comes after an object
352
+ }
353
+ if (d[1] === 'type' || d[1] === 'size' || d[1] === 'globl') {
354
+ continue;
355
+ }
356
+ const b = dataBytes(d[1], d[2]);
357
+ if (b === null) {
358
+ return refused(`whose data run holds '.${d[1]}', a directive this reader does not read as bytes`);
359
+ }
360
+ if ('refused' in b) {
361
+ return b;
362
+ }
363
+ // A word lands wherever the run has reached: agbcc aligns an object's words, never one word
364
+ // inside it, so one off a pointer boundary is no pointer of the object's.
365
+ if (b.relocs.length > 0 && bytes.length % POINTER_BYTES !== 0) {
366
+ return refused(`whose initializer holds the address '${b.relocs[0].symbol}' off its ${POINTER_BYTES}-byte words`);
367
+ }
368
+ relocs.push(...b.relocs.map((r) => ({ ...r, offset: r.offset + bytes.length })));
369
+ if (b.width === null && b.bytes.length > 0 && b.bytes.every((x) => x === 0)) {
370
+ const last = gaps[gaps.length - 1];
371
+ if (last !== undefined && last[0] + last[1] === bytes.length) {
372
+ last[1] += b.bytes.length;
373
+ } else {
374
+ gaps.push([bytes.length, b.bytes.length]);
375
+ }
376
+ }
377
+ bytes.push(...b.bytes);
378
+ if (b.width !== null) {
379
+ widths.add(b.width);
380
+ negative ||= b.negative && b.width < 4;
381
+ strings.add(b.string);
382
+ }
383
+ }
384
+ if (bytes.length === 0) {
385
+ return refused('whose label heads no data');
386
+ }
387
+ if (declaredSize !== null && declaredSize !== bytes.length) {
388
+ return refused(`whose '.size' (${declaredSize}) disagrees with the ${bytes.length} bytes under its label`);
389
+ }
390
+ if (kind === 'bss') {
391
+ return bytes.every((b) => b === 0) && relocs.length === 0
392
+ ? { section: 'bss', size: bytes.length, bigEndian: false }
393
+ : refused('defined in bss with non-zero bytes');
394
+ }
395
+ const align = alignAhead(lines, at);
396
+ if (align === null) {
397
+ return refused('whose alignment directive this reader does not read');
398
+ }
399
+ // A word's negative value says nothing: agbcc's constants are 32-bit host integers (machmode.h:30),
400
+ // so an unsigned word past 0x7fffffff prints negative too, while a narrower constant keeps its
401
+ // type's extension (varasm.c:1706-1710 `immed_double_const`) — `.short 0xffff` is a u16,
402
+ // `.short -0x1` an s16. And it says so only of the ELEMENT when every value is one width: in a
403
+ // struct's mixed directives a negative belongs to one field, which says nothing of another.
404
+ const unit = widths.size === 1 && bytes.length % [...widths][0] === 0 ? [...widths][0] : undefined;
405
+ return {
406
+ section: kind,
407
+ size: bytes.length,
408
+ bytes: Uint8Array.from(bytes),
409
+ bigEndian: false,
410
+ directives: {
411
+ align,
412
+ ...(unit !== undefined ? { unit } : {}),
413
+ negative: unit !== undefined && negative,
414
+ string: strings.size === 1 && strings.has(true),
415
+ ...(widths.size > 0 && gaps.length > 0 ? { gaps } : {}),
416
+ },
417
+ ...(relocs.length > 0 ? { relocs } : {}),
418
+ };
419
+ }
420
+
421
+ /** The functions whose literal pools name `symbol` in GNU as text, by the function each pool
422
+ * follows: a pool word belongs to the most recent function start (`.thumb_func` then a label, or
423
+ * a pret `thumb_func_start NAME`).
424
+ *
425
+ * Only words in code count. A word in a data section is another object's initializer, and one
426
+ * naming a function-scope static belongs to a static of the same function, the only scope that
427
+ * can spell that name — so it is no second referrer, and it sits wherever the compiler emitted
428
+ * that object, which is ahead of the function's own code. */
429
+ export function gasPoolReferrers(asm: string, symbol: string): Set<string> {
430
+ const out = new Set<string>();
431
+ let fn: string | null = null;
432
+ let pendingFn = false;
433
+ let inCode = true;
434
+ for (const raw of asm.split('\n')) {
435
+ const line = code(raw);
436
+ const sw = sectionSwitch(line);
437
+ if (sw !== null) {
438
+ inCode = sw === '.text' || sw.startsWith('.text.');
439
+ continue;
440
+ }
441
+ const start = line.match(/^(?:non_word_aligned_)?thumb_func_start\s+(\S+)$/);
442
+ if (start) {
443
+ fn = start[1];
444
+ continue;
445
+ }
446
+ if (line === '.thumb_func') {
447
+ pendingFn = true;
448
+ continue;
449
+ }
450
+ const lab = line.match(/^([A-Za-z_.$][\w.$]*):\s*(.*)$/);
451
+ if (lab && pendingFn) {
452
+ fn = lab[1];
453
+ pendingFn = false;
454
+ }
455
+ const rest = lab ? lab[2] : line;
456
+ const w = rest.match(/^\.(?:word|4byte|long)\s+(.+)$/);
457
+ if (
458
+ inCode &&
459
+ w &&
460
+ fn !== null &&
461
+ w[1].split(',').some((op) => op.trim().match(/^[A-Za-z_.$][\w.$]*/)?.[0] === symbol)
462
+ ) {
463
+ out.add(fn);
464
+ }
465
+ }
466
+ return out;
467
+ }
468
+
469
+ // ── An object file's side table (mwcc) ───────────────────────────────────────────────────────────
470
+
471
+ const OBJECT_SECTIONS: Readonly<Record<string, LocalObject['section']>> = {
472
+ '.rodata': 'rodata',
473
+ '.sdata2': 'rodata',
474
+ '.data': 'data',
475
+ '.sdata': 'data',
476
+ '.bss': 'bss',
477
+ '.sbss': 'bss',
478
+ };
479
+
480
+ /** The relocations that fill a pointer-width word with a symbol's absolute address — the only kind
481
+ * an initializer's address constant is. Each is listed where a frontend reads its objects through
482
+ * this reader: mwcc's (frontend/ppc.ts); MIPS addresses a function-scope static section-relative
483
+ * and does not reach it. RELA types only: the addend is read off the relocation entry alone, and a
484
+ * REL type (whose addend is the word it fills) joins only with a reader of that word. */
485
+ const ABSOLUTE_WORD: ReadonlySet<string> = new Set(['R_PPC_ADDR32']);
486
+
487
+ /** Sections whose relocations describe the program for a debugger, not for the machine. */
488
+ const isDebugSection = (s: string): boolean => /^\.(debug|line)/.test(s);
489
+
490
+ /** mwcc's `.comment` section: `CodeWarrior`, a version byte (0x0a from 2.4.2 and 2.4.7, 0x08 from
491
+ * 2.3.3), a header to 0x2c, then one 8-byte record per symbol-table entry, the null one first,
492
+ * whose first word is the alignment mwcc gave that symbol (compiled on all three builds: `s64`
493
+ * records 8 and lands at 8 after an `s32`, `aligned(32)` records 32, a scalar `u8` 1). No section
494
+ * header or symbol value says the same: a section's alignment is its widest object's, and an
495
+ * object at offset 0 of one is aligned to anything. The magic is the eleven letters alone, so the
496
+ * version byte never decides whether the records are read. */
497
+ const MW_COMMENT_MAGIC = 'CodeWarrior';
498
+ const MW_COMMENT_RECORDS = 0x2c;
499
+
500
+ /** The alignment mwcc's `.comment` records for the symbol at `index`, or null when the section is
501
+ * absent, is not mwcc's, or holds another number of records than the listing has symbols — the
502
+ * index would then name another symbol's. */
503
+ function recordedAlign(ad: AsmData, index: number): number | null {
504
+ const c = ad.sections.get('.comment');
505
+ if (
506
+ c === undefined ||
507
+ String.fromCharCode(...c.subarray(0, MW_COMMENT_MAGIC.length)) !== MW_COMMENT_MAGIC ||
508
+ c.length !== MW_COMMENT_RECORDS + 8 * (ad.symbolCount + 1)
509
+ ) {
510
+ return null;
511
+ }
512
+ const at = MW_COMMENT_RECORDS + 8 * index;
513
+ const align = ((c[at] << 24) | (c[at + 1] << 16) | (c[at + 2] << 8) | c[at + 3]) >>> 0;
514
+ return align > 0 && (align & (align - 1)) === 0 ? align : null;
515
+ }
516
+
517
+ /** Read a function-scope static's definition out of an object's `objdump -s -r -t` side table:
518
+ * the symbol table gives its section, offset and size, the section contents its bytes, and mwcc's
519
+ * `.comment` its alignment. `fn` is the function reading it — a relocation naming the static from
520
+ * any other code, or from data, is another referrer. */
521
+ export function readObjectLocalObject(ad: AsmData, symbol: string, fn: string): LocalObjectRead {
522
+ const parts = localStaticName(symbol);
523
+ if (parts === null) {
524
+ return refused('whose name has no source spelling this reader knows');
525
+ }
526
+ const { name, counter: order } = parts;
527
+ const sym = ad.symbols.get(symbol);
528
+ if (sym === undefined) {
529
+ return refused("whose definition the object's symbol table does not carry");
530
+ }
531
+ const kind = OBJECT_SECTIONS[sym.section];
532
+ if (kind === undefined) {
533
+ return refused(`defined in section '${sym.section}', which is not data this reader defines`);
534
+ }
535
+ if (sym.size <= 0) {
536
+ return refused('whose symbol-table size is 0, so its extent is unknown');
537
+ }
538
+ const self = ad.symbols.get(fn);
539
+ const inside: typeof ad.relocs = [];
540
+ for (const r of ad.relocs) {
541
+ if (r.section === sym.section && r.offset >= sym.value && r.offset < sym.value + sym.size) {
542
+ inside.push(r);
543
+ }
544
+ if (r.sym !== symbol || isDebugSection(r.section)) {
545
+ continue;
546
+ }
547
+ const inSelf =
548
+ self !== undefined && r.section === self.section && r.offset >= self.value && r.offset < self.value + self.size;
549
+ if (!inSelf) {
550
+ return refused(`that ${r.section} also names at 0x${r.offset.toString(16)} — it is not this function's alone`);
551
+ }
552
+ }
553
+ const align = recordedAlign(ad, sym.index);
554
+ if (align === null) {
555
+ return refused("whose alignment the object's `.comment` section does not record");
556
+ }
557
+ const placement = { align, section: sym.section, offset: sym.value };
558
+ if (kind === 'bss') {
559
+ return inside.length === 0
560
+ ? { name, symbol, order, section: 'bss', size: sym.size, bigEndian: ad.bigEndian, placement }
561
+ : refused('defined in bss with a relocation inside it');
562
+ }
563
+ const contents = ad.sections.get(sym.section);
564
+ if (contents === undefined || sym.value + sym.size > contents.length) {
565
+ return refused(`whose bytes the side table's '${sym.section}' contents do not hold`);
566
+ }
567
+ const relocs: LocalObjectReloc[] = [];
568
+ for (const r of inside.sort((a, b) => a.offset - b.offset)) {
569
+ const at = r.offset - sym.value;
570
+ if (!ABSOLUTE_WORD.has(r.type)) {
571
+ return refused(
572
+ `whose initializer holds the address '${r.sym}' by a ${r.type} relocation — no pointer's absolute address`,
573
+ );
574
+ }
575
+ if (at % POINTER_BYTES !== 0 || at + POINTER_BYTES > sym.size) {
576
+ return refused(`whose initializer holds the address '${r.sym}' off its ${POINTER_BYTES}-byte words`);
577
+ }
578
+ const why = addressRefusal(r.sym);
579
+ if (why !== null) {
580
+ return refused(why);
581
+ }
582
+ // a RELA relocation (ABSOLUTE_WORD): the linker replaces the word, so its bytes add nothing
583
+ relocs.push({ offset: at, symbol: r.sym, addend: r.addend | 0 });
584
+ }
585
+ return {
586
+ name,
587
+ symbol,
588
+ order,
589
+ section: kind,
590
+ size: sym.size,
591
+ bytes: contents.slice(sym.value, sym.value + sym.size),
592
+ bigEndian: ad.bigEndian,
593
+ placement,
594
+ ...(relocs.length > 0 ? { relocs } : {}),
595
+ };
596
+ }
597
+
598
+ /** The statics one lift names, gathered as the frontend meets them. */
599
+ export interface LocalStatics {
600
+ /** Record a static this function names, and answer the name the IR calls it by: its linker name,
601
+ * which no C identifier and no declared symbol can equal. */
602
+ define(obj: LocalObject): string;
603
+ /** Every static, or undefined for none. */
604
+ finish(): LocalObjects | undefined;
605
+ }
606
+
607
+ export function makeLocalStatics(): LocalStatics {
608
+ const bySymbol = new Map<string, LocalObject>();
609
+ return {
610
+ define(obj) {
611
+ bySymbol.set(obj.symbol, obj);
612
+ return obj.symbol;
613
+ },
614
+ finish: () => (bySymbol.size > 0 ? bySymbol : undefined),
615
+ };
616
+ }