@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,517 @@
1
+ // asmlift — the C DEFINITION of each function-scope static a function names (ir/core.ts
2
+ // `LocalObjects`), typed from the target's own definition and the accesses the function makes.
3
+ //
4
+ // The frontend hands over what the target shows: section, size and bytes, and where the target is
5
+ // an assembler listing (agbcc's `.s`) the definition's directives too. Those say the element: the
6
+ // compiler writes each scalar of the initializer with the directive of its width, so a table of
7
+ // `.short`s is one of 16-bit elements whatever the function reads out of it, and a narrow negative
8
+ // value is only ever written for a signed type. An object file (mwcc) shows no directives, and
9
+ // there the element is the one width every access agrees on. The loads of the element's width then
10
+ // settle its signedness — they are what the function computes, and a byte holds the same bits
11
+ // declared either way, so they outrank a negative the source wrote through another type (a struct's
12
+ // signed field, a cast) — and the definition settles it where no load does. An object nothing says
13
+ // anything about is bytes.
14
+ //
15
+ // The target also shows the ALIGNMENT — the listing's `.align`, mwcc's `.comment` record — which
16
+ // decides where the object lands after the statics declared before it, and whether a DMA or GX
17
+ // buffer is where the hardware needs it. The definition this pass picks gets its compiler's own
18
+ // alignment, which the target declares ({@link StaticLayout}): a scalar to its width, an array to
19
+ // its element or the compiler's aggregate floor, a string literal to the compiler's string
20
+ // alignment. A target aligned wider — a struct, an `s64`, an `ALIGNED(32)` — keeps that alignment
21
+ // in an attribute, because a table one element off is misread by the machine and scored as a
22
+ // MATCH. A byte array the listing wrote as strings and placed at the string alignment is spelled
23
+ // with a string literal, which is what gives it that alignment.
24
+ //
25
+ // The definition is a SymbolInfo as well as a declaration, because the structurer spells an access
26
+ // through a symbol's declared shape (`tide[i]` for an array, the bare `q` for a scalar): the shape
27
+ // it spells against and the object the backend defines are one reading of the same facts. Both are
28
+ // keyed by the LINKER name the IR carries (`tide.3`), and the finished tree is renamed to the
29
+ // source's (`tide`) in one step, `nameLocalStatics`, which is where a clash between that name and
30
+ // anything else the function names can first be seen whole.
31
+ //
32
+ // An element the target RELOCATES holds an address — a table of function pointers, of data
33
+ // pointers, of `&arr[k]` — and is spelled as the address constant it is, `(u32)&sym` or
34
+ // `(u32)((u8 *)&sym + k)`, in the integer element the rest of this typing picks: such an element
35
+ // compiles to the same word and the same relocation as the pointer-typed source (agbcc and mwcc,
36
+ // compiled), so the definition needs no pointer type to put the bytes where the target has them.
37
+ //
38
+ // REFUSES, naming the static, when the target's compiler declares no layout rules, when nothing
39
+ // in the definition settles the width and the accesses disagree on it, when the width is no
40
+ // integer type's or does not divide the size, when the loads disagree on the signedness, when the
41
+ // definition is aligned narrower than its declaration here would be, when a bss static sits where
42
+ // only a scalar initialized to zero is sure to land and it is no scalar (or the input does not
43
+ // show where it sits), when an element holding an address — a static's or the compiler's
44
+ // anonymous image's — is no pointer's width;
45
+ // and, at the rename, when two statics share a source name (one block cannot declare both) or
46
+ // when the function names anything else by it — a global, a callee, a parameter, a local, itself —
47
+ // which the block-scope static would hide — and when a global an initializer holds the address of
48
+ // is hidden there by a static, parameter or local of its name.
49
+ import {
50
+ type Fn,
51
+ LOCAL_OBJECT_ADDRESS_BYTES,
52
+ type LocalObject,
53
+ type LocalObjects,
54
+ type Op,
55
+ type Value,
56
+ defOpMap,
57
+ } from '../ir/core';
58
+ import { type IrType, T } from '../ir/types';
59
+ import {
60
+ type Expr,
61
+ type InitAddress,
62
+ type SFn,
63
+ type SImage,
64
+ type SStatic,
65
+ type Stmt,
66
+ mapExprChildren,
67
+ mapStmtExprs,
68
+ mapStmtLists,
69
+ walkExprs,
70
+ } from '../l3/ast';
71
+ import { takenNames } from '../l3/hoist';
72
+ import { type SymbolInfo, accessSignedness } from '../symbols';
73
+
74
+ /** How a compiler lays out a function-scope static — facts about the COMPILER, declared per target
75
+ * (target.ts `compilerBehaviors.staticLayout`), from which the definition that puts a static where
76
+ * the target has it follows. */
77
+ export interface StaticLayout {
78
+ /** the least alignment it gives an array or a struct, whatever the element */
79
+ aggregateAlign: number;
80
+ /** the alignment it gives a declaration initialized by a string literal */
81
+ stringAlign: number;
82
+ /** where a scalar initialized to zero goes: kept in `.data`, or moved to bss — where its
83
+ * initializer is gone, and one element of zero in `.data` must then have been an aggregate */
84
+ zeroScalar: 'data' | 'bss';
85
+ /** The bss sections of such a compiler, each laying out the scalars it moved there first, in
86
+ * declaration order, and then the statics with no initializer: in reverse declaration order
87
+ * (mwcc's `.sbss`: `a = 0; b; c = 0;` at +0, +8, +4), or in the order its code first uses them
88
+ * (mwcc's `.bss`: `a; b = 0;` used a first at +4, +0). Either way the offsets say which statics
89
+ * sit where only one with the `= 0` is sure to land. */
90
+ uninitOrder?: Readonly<Record<string, 'reversed' | 'first-use'>>;
91
+ }
92
+
93
+ /** The bss statics of `objs` to write `= 0` ({@link StaticLayout.uninitOrder}), or
94
+ * the symbol of one whose placement the input does not show. In a section of either order a
95
+ * function's zero-initialized statics come first, scalars with their counters rising.
96
+ *
97
+ * Where the others follow reversed, a static with a later-declared one at a higher offset had the
98
+ * initializer. The last of the rising run cannot be told from the first of the falling one, and
99
+ * both declarations put it in the same place: it is left without.
100
+ *
101
+ * Where the others follow by first use, the leading run of `scalarShaped` statics with rising
102
+ * counters had it, save that a run that is the whole section leaves its last without, which lands
103
+ * after the rest either way. Any of that run may have had none and been used first, and the `= 0`
104
+ * puts it where it was, which the candidate's code would decide otherwise. */
105
+ function zeroInitialized(
106
+ objs: readonly LocalObject[],
107
+ uninitOrder: NonNullable<StaticLayout['uninitOrder']>,
108
+ scalarShaped: (o: LocalObject) => boolean,
109
+ ): Set<string> | { symbol: string } {
110
+ const out = new Set<string>();
111
+ const bss = objs.filter((o) => o.section === 'bss');
112
+ for (const o of bss) {
113
+ if (o.placement === undefined) {
114
+ return { symbol: o.symbol };
115
+ }
116
+ }
117
+ for (const [section, order] of Object.entries(uninitOrder)) {
118
+ const run = bss
119
+ .filter((o) => o.placement!.section === section)
120
+ .sort((a, b) => a.placement!.offset - b.placement!.offset);
121
+ if (order === 'reversed') {
122
+ for (const o of run) {
123
+ if (run.some((p) => p.placement!.offset > o.placement!.offset && p.order > o.order)) {
124
+ out.add(o.symbol);
125
+ }
126
+ }
127
+ continue;
128
+ }
129
+ let lead = 0;
130
+ while (lead < run.length && scalarShaped(run[lead]) && (lead === 0 || run[lead].order > run[lead - 1].order)) {
131
+ lead++;
132
+ }
133
+ for (const o of run.slice(0, lead === run.length ? lead - 1 : lead)) {
134
+ out.add(o.symbol);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+
140
+ /** Each static's shape for the structurer, and its definition for the backend. */
141
+ export interface LocalStaticShapes {
142
+ infos: Map<string, SymbolInfo>;
143
+ statics: SStatic[];
144
+ /** the compiler's anonymous constant images, which no declaration spells (l3/constinit.ts) */
145
+ images: SImage[];
146
+ }
147
+
148
+ /** One access through a static's address: the width it reads or writes, and a load's extension. */
149
+ interface Access {
150
+ width: number;
151
+ signed?: boolean;
152
+ }
153
+
154
+ /** The width an `aload`/`astore` reads or writes. Its `elemSize` is the element's, which for an
155
+ * element of a struct array (raise/struct-arrays.ts, `fieldOff`) is the STRIDE: the access itself
156
+ * is the field's, whose type the base's struct carries. Null for a field that type does not hold. */
157
+ function indexedWidth(op: Op): number | null {
158
+ const fieldOff = op.attrs.fieldOff as number | undefined;
159
+ if (fieldOff === undefined) {
160
+ return op.attrs.elemSize as number;
161
+ }
162
+ const bt = op.operands[0].type;
163
+ const field = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to.fields.find((f) => f.off === fieldOff) : undefined;
164
+ return field !== undefined && field.type.kind === 'int' ? field.type.width / 8 : null;
165
+ }
166
+
167
+ /** Every access through each static's address. */
168
+ function accessesOf(fn: Fn, names: ReadonlySet<string>): Map<string, Access[]> {
169
+ const defs = defOpMap(fn);
170
+ /** the static an address is computed from — its `gaddr`, through adds of anything else */
171
+ const baseOf = (v: Value, seen = new Set<Value>()): string | null => {
172
+ const d = defs.get(v);
173
+ if (d === undefined || seen.has(v)) {
174
+ return null;
175
+ }
176
+ seen.add(v);
177
+ if (d.opcode === 'gaddr') {
178
+ return names.has(d.attrs.sym as string) ? (d.attrs.sym as string) : null;
179
+ }
180
+ if (d.opcode === 'add' || d.opcode === 'sub') {
181
+ const found = d.operands.slice(0, d.opcode === 'sub' ? 1 : undefined).map((o) => baseOf(o, seen));
182
+ const hits = found.filter((f): f is string => f !== null);
183
+ return hits.length === 1 ? hits[0] : null;
184
+ }
185
+ return null;
186
+ };
187
+ const out = new Map<string, Access[]>();
188
+ for (const b of fn.blocks) {
189
+ for (const op of b.ops) {
190
+ const plain = op.opcode === 'load' || op.opcode === 'store';
191
+ const indexed = op.opcode === 'aload' || op.opcode === 'astore';
192
+ if (!plain && !indexed) {
193
+ continue;
194
+ }
195
+ const sym = baseOf(op.operands[0]);
196
+ if (sym === null) {
197
+ continue;
198
+ }
199
+ const width = plain ? (op.attrs.width as number) : indexedWidth(op);
200
+ if (width === null) {
201
+ continue;
202
+ }
203
+ const isLoad = op.opcode === 'load' || op.opcode === 'aload';
204
+ const list = out.get(sym) ?? out.set(sym, []).get(sym)!;
205
+ list.push({
206
+ width,
207
+ ...(isLoad ? { signed: accessSignedness(width, op.attrs.signed as boolean | undefined) } : {}),
208
+ });
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+
214
+ /** The elements of `obj`'s initial bytes read `width` bytes at a time in its byte order. */
215
+ function elements(obj: LocalObject, width: number, signed: boolean): number[] {
216
+ const b = obj.bytes!;
217
+ const out: number[] = [];
218
+ for (let at = 0; at < obj.size; at += width) {
219
+ let v = 0;
220
+ for (let i = 0; i < width; i++) {
221
+ v = v * 256 + b[obj.bigEndian ? at + i : at + width - 1 - i];
222
+ }
223
+ out.push(signed && v >= 2 ** (8 * width - 1) ? v - 2 ** (8 * width) : v);
224
+ }
225
+ return out;
226
+ }
227
+
228
+ /** The width of `obj`'s element: the one its definition's directives write, else the one every
229
+ * access agrees on; with no access either, a pointer's where the target relocates a word of it (an
230
+ * address table only handed to a callee, on a compiler that writes no directives: mwcc), else the
231
+ * whole object where it is aligned below the compiler's aggregate floor, as only a scalar is, and
232
+ * a byte where it is not. A relocation the pointer width does not seat is still refused, by the
233
+ * caller's check of every relocation's offset. */
234
+ function elementWidth(
235
+ obj: LocalObject,
236
+ accesses: readonly Access[],
237
+ layout: StaticLayout,
238
+ ): number | { refused: string } {
239
+ if (obj.directives?.unit !== undefined) {
240
+ return obj.directives.unit;
241
+ }
242
+ const widths = [...new Set(accesses.map((x) => x.width))];
243
+ if (widths.length > 1) {
244
+ return { refused: `whose accesses disagree on its element width (${widths.sort().join(' and ')} bytes)` };
245
+ }
246
+ if (widths.length === 1) {
247
+ return widths[0];
248
+ }
249
+ if ((obj.relocs?.length ?? 0) > 0 && obj.size % LOCAL_OBJECT_ADDRESS_BYTES === 0) {
250
+ return LOCAL_OBJECT_ADDRESS_BYTES;
251
+ }
252
+ const shown = obj.directives?.align ?? obj.placement?.align;
253
+ const scalarOnly = shown !== undefined && shown < layout.aggregateAlign;
254
+ return scalarOnly && [1, 2, 4].includes(obj.size) ? obj.size : 1;
255
+ }
256
+
257
+ /** The shape and definition of every static `fn` defines, or the refusal: the static's linker name
258
+ * and the tail of the sentence. */
259
+ export function localStaticShapes(
260
+ fn: Fn,
261
+ layout: StaticLayout | undefined,
262
+ ): LocalStaticShapes | { symbol: string; refused: string } {
263
+ const objs = fn.localObjects;
264
+ const infos = new Map<string, SymbolInfo>();
265
+ const statics: SStatic[] = [];
266
+ const images: SImage[] = [];
267
+ if (objs === undefined) {
268
+ return { infos, statics, images };
269
+ }
270
+ if (layout === undefined) {
271
+ const [first] = objs.keys();
272
+ return { symbol: first, refused: "whose layout rules this target's compiler does not declare" };
273
+ }
274
+ const access = accessesOf(fn, new Set(objs.keys()));
275
+ const widthOf = (o: LocalObject) => elementWidth(o, access.get(o.symbol) ?? [], layout);
276
+ /** Declared here as a scalar, or sized and aligned as one — the compiler aligns a scalar to at
277
+ * least its width. One declared as an array where only a zero scalar sits is refused below: an
278
+ * `s32 = 0` read a byte at a time, or only through its address, lands there as well as four
279
+ * bytes used first. */
280
+ const scalarShaped = (o: LocalObject): boolean =>
281
+ widthOf(o) === o.size || ([1, 2, 4, 8].includes(o.size) && (o.placement?.align ?? 0) >= o.size);
282
+ const zeroed =
283
+ layout.uninitOrder !== undefined
284
+ ? zeroInitialized([...objs.values()], layout.uninitOrder, scalarShaped)
285
+ : new Set<string>();
286
+ if (!(zeroed instanceof Set)) {
287
+ return { symbol: zeroed.symbol, refused: 'in bss at an offset this input does not show' };
288
+ }
289
+ // Declared in the target's declaration order, which the compiler lays the objects out by (mwcc
290
+ // reverses it in `.sbss` for statics with no initializer), so that the candidate's compiler
291
+ // places each where the target has it — except mwcc's `.bss` statics with no initializer, which
292
+ // it places in the order it first uses them: the candidate's code decides that, no declaration.
293
+ for (const obj of [...objs.values()].sort((a, b) => a.order - b.order)) {
294
+ const say = (why: string) => ({ symbol: obj.symbol, refused: why });
295
+ if (obj.anonymous) {
296
+ // The compiler's own image of a local aggregate's initializer: typed by its directives alone,
297
+ // since nothing but the copy reads it, and never declared where it sits — l3/constinit.ts
298
+ // folds it into the local the copy fills, which is the declaration that placed it.
299
+ const unit = obj.directives?.unit;
300
+ if (unit === undefined || obj.bytes === undefined) {
301
+ return say('whose image writes no one element width — a struct, or padding alone');
302
+ }
303
+ if (![1, 2, 4].includes(unit) || obj.size % unit !== 0) {
304
+ return say(`whose ${unit}-byte elements are no integer array of its ${obj.size} bytes`);
305
+ }
306
+ const signed = obj.directives!.negative;
307
+ const count = obj.size / unit;
308
+ infos.set(obj.symbol, {
309
+ name: obj.symbol,
310
+ kind: 'data',
311
+ size: obj.size,
312
+ shape: 'array',
313
+ elemSize: unit,
314
+ elemSigned: signed,
315
+ dims: [count],
316
+ const: true,
317
+ });
318
+ // Only the elements the source spelled: an element it left out is a hole, and the holes after
319
+ // the last value are dropped. Spelled out as zeros, the initializer is a different one to the
320
+ // compiler, which clears and stores where most of one is zero rather than copy an image
321
+ // (expr.c `mostly_zeros_p`) — so the printer spells the element after a hole by designator,
322
+ // where the compiler parses one (`SFn.designators`).
323
+ const init: (number | InitAddress | null)[] = elements(obj, unit, signed);
324
+ for (const [from, n] of obj.directives!.gaps ?? []) {
325
+ if (from % unit !== 0 || n % unit !== 0) {
326
+ return say(`whose padding at byte ${from} does not fall on its ${unit}-byte elements`);
327
+ }
328
+ init.fill(null, from / unit, (from + n) / unit);
329
+ }
330
+ // An element the image relocates holds an address, as a static's does — an array of pointers
331
+ // (agbcc copies one with memcpy from 16 words, compiled).
332
+ for (const r of obj.relocs ?? []) {
333
+ if (unit !== LOCAL_OBJECT_ADDRESS_BYTES || r.offset % unit !== 0) {
334
+ return say(
335
+ `whose image holds the address '${r.symbol}', which its ${unit}-byte elements cannot — ` +
336
+ `an address is ${LOCAL_OBJECT_ADDRESS_BYTES}`,
337
+ );
338
+ }
339
+ init[r.offset / unit] = { symbol: r.symbol, addend: r.addend };
340
+ }
341
+ // A string literal's image is placed at the compiler's string alignment, which a brace list of
342
+ // strings is not (agbcc: `.align 2, 0` ahead of a STRING_CST's label, none ahead of
343
+ // `{"abcde"}`'s) — the same bytes, but spelled as a literal the image moves to a stricter
344
+ // alignment and shifts the unit's `.rodata`. Nor has it a padding run: one shorter than its
345
+ // array is copied at the literal's length and the tail cleared (`char a[16] = "abc…hijklm"` is
346
+ // memcpy 15 + memset 1), so a `.space` after the `.ascii` is a brace list's image copied whole.
347
+ // So the test is asked before the trailing holes are dropped, not after.
348
+ const string =
349
+ obj.directives!.string === true &&
350
+ unit === 1 &&
351
+ obj.directives!.align >= layout.stringAlign &&
352
+ !init.includes(null);
353
+ while (init.length > 0 && init[init.length - 1] === null) {
354
+ init.pop();
355
+ }
356
+ images.push({
357
+ name: obj.symbol,
358
+ type: T.array(T.int(unit * 8, signed), count),
359
+ init,
360
+ ...(string ? { string: true as const } : {}),
361
+ });
362
+ continue;
363
+ }
364
+ const accesses = access.get(obj.symbol) ?? [];
365
+ const dir = obj.directives;
366
+ // the alignment the target shows
367
+ const shown = dir?.align ?? obj.placement?.align;
368
+ const width = widthOf(obj);
369
+ if (typeof width !== 'number') {
370
+ return say(width.refused);
371
+ }
372
+ if (![1, 2, 4].includes(width)) {
373
+ return say(`whose ${width}-byte elements are no integer type`);
374
+ }
375
+ if (obj.size % width !== 0) {
376
+ return say(`whose ${width}-byte elements do not divide its ${obj.size} bytes`);
377
+ }
378
+ // an access of another width reads through a cast, and says nothing about these elements
379
+ const signs = new Set(accesses.filter((x) => x.width === width && x.signed !== undefined).map((x) => x.signed));
380
+ if (signs.size > 1) {
381
+ return say('whose loads disagree on whether its elements are signed');
382
+ }
383
+ const signed = signs.size > 0 ? signs.has(true) : dir?.negative === true;
384
+ const count = obj.size / width;
385
+ const elem = T.int(width * 8, signed);
386
+ const init: (number | InitAddress)[] | undefined = zeroed.has(obj.symbol)
387
+ ? [0]
388
+ : obj.bytes === undefined
389
+ ? undefined
390
+ : elements(obj, width, signed);
391
+ // An element the target relocates holds an address, which only an element of a pointer's width
392
+ // can: the relocation fills exactly that many bytes.
393
+ for (const r of obj.relocs ?? []) {
394
+ if (width !== LOCAL_OBJECT_ADDRESS_BYTES || r.offset % width !== 0) {
395
+ return say(
396
+ `whose initializer holds the address '${r.symbol}', which its ${width}-byte elements cannot — ` +
397
+ `an address is ${LOCAL_OBJECT_ADDRESS_BYTES}`,
398
+ );
399
+ }
400
+ init![r.offset / width] = { symbol: r.symbol, addend: r.addend };
401
+ }
402
+ if (zeroed.has(obj.symbol) && count !== 1) {
403
+ return say(`laid out where only a scalar initialized to zero is, but of ${count} elements`);
404
+ }
405
+ // One element is a scalar — unless it is initialized data holding zero on a compiler that
406
+ // moves a zero scalar to bss, where it can only have been an aggregate, which the compiler
407
+ // keeps in .data.
408
+ const scalar =
409
+ count === 1 && !(layout.zeroScalar === 'bss' && obj.section === 'data' && init!.every((v) => v === 0));
410
+ const type: IrType = scalar ? elem : T.array(elem, count);
411
+ const string = dir?.string === true && width === 1 && !scalar && shown !== undefined && shown >= layout.stringAlign;
412
+ // The alignment the target shows against the one this definition gets from its compiler.
413
+ const own = scalar ? width : Math.max(width, layout.aggregateAlign, string ? layout.stringAlign : 1);
414
+ if (shown !== undefined && shown < own) {
415
+ return say(`whose definition is aligned to ${shown} bytes, less than the ${own} its declaration here would get`);
416
+ }
417
+ const align = shown !== undefined && shown > own ? shown : undefined;
418
+ const isConst = obj.section === 'rodata';
419
+ infos.set(obj.symbol, {
420
+ name: obj.symbol,
421
+ kind: 'data',
422
+ size: obj.size,
423
+ ...(scalar
424
+ ? { shape: 'scalar' as const, signed }
425
+ : { shape: 'array' as const, elemSize: width, elemSigned: signed, dims: [count] }),
426
+ ...(isConst ? { const: true } : {}),
427
+ });
428
+ statics.push({
429
+ name: obj.name,
430
+ type,
431
+ ...(isConst ? { const: true as const } : {}),
432
+ ...(align !== undefined ? { align } : {}),
433
+ ...(init ? { init } : {}),
434
+ ...(string ? { string: true as const } : {}),
435
+ });
436
+ }
437
+ return { infos, statics, images };
438
+ }
439
+
440
+ /** `sfn` with each static's linker name replaced by its source name — every reference, assignment
441
+ * targets and the typed-global list included — or the refusal. The tree is final here, so what the
442
+ * source name would collide with is all in it: every name its body mentions, callees included,
443
+ * plus its parameters, locals and own name. */
444
+ export function nameLocalStatics(sfn: SFn, objs: LocalObjects): SFn | { symbol: string; refused: string } {
445
+ const taken = takenNames(sfn);
446
+ for (const g of sfn.globals ?? []) {
447
+ taken.add(g.name);
448
+ }
449
+ // The globals an initializer holds the address of are named here as much as the body's: a static
450
+ // of the same name is in scope from its own declarator on, its initializer included.
451
+ const addressed = new Map<string, string>();
452
+ for (const obj of objs.values()) {
453
+ for (const r of obj.relocs ?? []) {
454
+ taken.add(r.symbol);
455
+ addressed.set(r.symbol, obj.symbol);
456
+ }
457
+ }
458
+ const bound = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
459
+ // …and the function's own parameters hide one too: a static's initializer is read in their scope.
460
+ // A local is declared after every static and hides none, but its name is the emitter's to move.
461
+ for (const [name, holder] of addressed) {
462
+ if (bound.has(name)) {
463
+ return {
464
+ symbol: holder,
465
+ refused: `whose initializer holds the address of '${name}', which a parameter or local here also names — it would hide that global`,
466
+ };
467
+ }
468
+ }
469
+ const callees = new Set<string>([sfn.name]);
470
+ for (const e of walkExprs(sfn.body)) {
471
+ if (e.k === 'call' && typeof e.fn === 'string') {
472
+ callees.add(e.fn);
473
+ }
474
+ }
475
+ const rename = new Map<string, string>();
476
+ const bySource = new Map<string, string>();
477
+ for (const obj of objs.values()) {
478
+ if (obj.anonymous) {
479
+ continue; // no source name: folded into a local's initializer at emission (l3/constinit.ts)
480
+ }
481
+ const say = (why: string) => ({ symbol: obj.symbol, refused: `whose source name '${obj.name}' ${why}` });
482
+ const other = bySource.get(obj.name);
483
+ if (other !== undefined) {
484
+ return say(`another static here ('${other}') also has — one block cannot declare both`);
485
+ }
486
+ if (callees.has(obj.name) || taken.has(obj.name)) {
487
+ const what = callees.has(obj.name) ? 'a function' : bound.has(obj.name) ? 'a parameter or local' : 'a global';
488
+ return say(`is also ${what} this one names — the static would hide it`);
489
+ }
490
+ bySource.set(obj.name, obj.symbol);
491
+ rename.set(obj.symbol, obj.name);
492
+ }
493
+ const expr = (e: Expr): Expr => {
494
+ const r = mapExprChildren(e, expr);
495
+ return (r.k === 'var' || r.k === 'addr' || r.k === 'postincr') && rename.has(r.name)
496
+ ? { ...r, name: rename.get(r.name)! }
497
+ : r;
498
+ };
499
+ const assigns = (s: Stmt): Stmt => {
500
+ const r = mapStmtLists(s, (list) => list.map(assigns));
501
+ if (r.k === 'for') {
502
+ return { ...r, init: assigns(r.init), inc: assigns(r.inc) };
503
+ }
504
+ return r.k === 'assign' && rename.has(r.name) ? { ...r, name: rename.get(r.name)! } : r;
505
+ };
506
+ return {
507
+ ...sfn,
508
+ body: sfn.body.map((s) => assigns(mapStmtExprs(s, expr))),
509
+ ...(sfn.globals
510
+ ? {
511
+ globals: sfn.globals
512
+ .map((g) => ({ ...g, name: rename.get(g.name) ?? g.name }))
513
+ .sort((a, b) => a.name.localeCompare(b.name)),
514
+ }
515
+ : {}),
516
+ };
517
+ }
@@ -86,7 +86,7 @@
86
86
  // • `sitesOf` widens where a loop emitter may MOVE a copy, but the read side does not follow. For
87
87
  // a block parameter `clobbers` falls to `liveIn` of the widened site, and a parameter's SSA def
88
88
  // point is its own block's entry, not the position the emitter actually writes it at — so a
89
- // header definition is not seen to clobber a value the sink copies to the top of the body. The
89
+ // header definition is not seen to clobber a value the sink copies into the body. The
90
90
  // slot that is actually sunk is unreachable (its exit arg is a header parameter, which
91
91
  // `loop-escape` rejects); reaching it needs a second, non-loop predecessor of the exit block
92
92
  // passing the header's definition into the same slot, and no input has been built that does.
@@ -133,7 +133,7 @@ export interface NameCoalesceDeps {
133
133
  varName: ReadonlyMap<Value, string>;
134
134
  /** name → the type it is declared with */
135
135
  varType: ReadonlyMap<string, IrType>;
136
- /** every natural loop, as its header and the blocks its body contains (structure/loops.ts) */
136
+ /** every natural loop, as its header and the blocks its body contains (ir/loops.ts) */
137
137
  loops: readonly { header: Block; body: ReadonlySet<Block> }[];
138
138
  }
139
139