@asmlift/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -0,0 +1,381 @@
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
+ // REFUSES, naming the static, when the target's compiler declares no layout rules, when nothing
33
+ // in the definition settles the width and the accesses disagree on it, when the width is no
34
+ // integer type's or does not divide the size, when the loads disagree on the signedness, when the
35
+ // definition is aligned narrower than its declaration here would be, when a bss static sits where
36
+ // only a scalar initialized to zero is sure to land and it is no scalar (or the input does not
37
+ // show where it sits);
38
+ // and, at the rename, when two statics share a source name (one block cannot declare both) or
39
+ // when the function names anything else by it — a global, a callee, a parameter, a local, itself —
40
+ // which the block-scope static would hide.
41
+ import { type Fn, type LocalObject, type LocalObjects, type Op, type Value, defOpMap } from '../ir/core';
42
+ import { type IrType, T } from '../ir/types';
43
+ import {
44
+ type Expr,
45
+ type SFn,
46
+ type SStatic,
47
+ type Stmt,
48
+ mapExprChildren,
49
+ mapStmtExprs,
50
+ mapStmtLists,
51
+ walkExprs,
52
+ } from '../l3/ast';
53
+ import { takenNames } from '../l3/hoist';
54
+ import { type SymbolInfo, accessSignedness } from '../symbols';
55
+
56
+ /** How a compiler lays out a function-scope static — facts about the COMPILER, declared per target
57
+ * (target.ts `compilerBehaviors.staticLayout`), from which the definition that puts a static where
58
+ * the target has it follows. */
59
+ export interface StaticLayout {
60
+ /** the least alignment it gives an array or a struct, whatever the element */
61
+ aggregateAlign: number;
62
+ /** the alignment it gives a declaration initialized by a string literal */
63
+ stringAlign: number;
64
+ /** where a scalar initialized to zero goes: kept in `.data`, or moved to bss — where its
65
+ * initializer is gone, and one element of zero in `.data` must then have been an aggregate */
66
+ zeroScalar: 'data' | 'bss';
67
+ /** The bss sections of such a compiler, each laying out the scalars it moved there first, in
68
+ * declaration order, and then the statics with no initializer: in reverse declaration order
69
+ * (mwcc's `.sbss`: `a = 0; b; c = 0;` at +0, +8, +4), or in the order its code first uses them
70
+ * (mwcc's `.bss`: `a; b = 0;` used a first at +4, +0). Either way the offsets say which statics
71
+ * sit where only one with the `= 0` is sure to land. */
72
+ uninitOrder?: Readonly<Record<string, 'reversed' | 'first-use'>>;
73
+ }
74
+
75
+ /** The bss statics of `objs` to write `= 0` ({@link StaticLayout.uninitOrder}), or
76
+ * the symbol of one whose placement the input does not show. In a section of either order a
77
+ * function's zero-initialized statics come first, scalars with their counters rising.
78
+ *
79
+ * Where the others follow reversed, a static with a later-declared one at a higher offset had the
80
+ * initializer. The last of the rising run cannot be told from the first of the falling one, and
81
+ * both declarations put it in the same place: it is left without.
82
+ *
83
+ * Where the others follow by first use, the leading run of `scalarShaped` statics with rising
84
+ * counters had it, save that a run that is the whole section leaves its last without, which lands
85
+ * after the rest either way. Any of that run may have had none and been used first, and the `= 0`
86
+ * puts it where it was, which the candidate's code would decide otherwise. */
87
+ function zeroInitialized(
88
+ objs: readonly LocalObject[],
89
+ uninitOrder: NonNullable<StaticLayout['uninitOrder']>,
90
+ scalarShaped: (o: LocalObject) => boolean,
91
+ ): Set<string> | { symbol: string } {
92
+ const out = new Set<string>();
93
+ const bss = objs.filter((o) => o.section === 'bss');
94
+ for (const o of bss) {
95
+ if (o.placement === undefined) {
96
+ return { symbol: o.symbol };
97
+ }
98
+ }
99
+ for (const [section, order] of Object.entries(uninitOrder)) {
100
+ const run = bss
101
+ .filter((o) => o.placement!.section === section)
102
+ .sort((a, b) => a.placement!.offset - b.placement!.offset);
103
+ if (order === 'reversed') {
104
+ for (const o of run) {
105
+ if (run.some((p) => p.placement!.offset > o.placement!.offset && p.order > o.order)) {
106
+ out.add(o.symbol);
107
+ }
108
+ }
109
+ continue;
110
+ }
111
+ let lead = 0;
112
+ while (lead < run.length && scalarShaped(run[lead]) && (lead === 0 || run[lead].order > run[lead - 1].order)) {
113
+ lead++;
114
+ }
115
+ for (const o of run.slice(0, lead === run.length ? lead - 1 : lead)) {
116
+ out.add(o.symbol);
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /** Each static's shape for the structurer, and its definition for the backend. */
123
+ export interface LocalStaticShapes {
124
+ infos: Map<string, SymbolInfo>;
125
+ statics: SStatic[];
126
+ }
127
+
128
+ /** One access through a static's address: the width it reads or writes, and a load's extension. */
129
+ interface Access {
130
+ width: number;
131
+ signed?: boolean;
132
+ }
133
+
134
+ /** The width an `aload`/`astore` reads or writes. Its `elemSize` is the element's, which for an
135
+ * element of a struct array (raise/struct-arrays.ts, `fieldOff`) is the STRIDE: the access itself
136
+ * is the field's, whose type the base's struct carries. Null for a field that type does not hold. */
137
+ function indexedWidth(op: Op): number | null {
138
+ const fieldOff = op.attrs.fieldOff as number | undefined;
139
+ if (fieldOff === undefined) {
140
+ return op.attrs.elemSize as number;
141
+ }
142
+ const bt = op.operands[0].type;
143
+ const field = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to.fields.find((f) => f.off === fieldOff) : undefined;
144
+ return field !== undefined && field.type.kind === 'int' ? field.type.width / 8 : null;
145
+ }
146
+
147
+ /** Every access through each static's address. */
148
+ function accessesOf(fn: Fn, names: ReadonlySet<string>): Map<string, Access[]> {
149
+ const defs = defOpMap(fn);
150
+ /** the static an address is computed from — its `gaddr`, through adds of anything else */
151
+ const baseOf = (v: Value, seen = new Set<Value>()): string | null => {
152
+ const d = defs.get(v);
153
+ if (d === undefined || seen.has(v)) {
154
+ return null;
155
+ }
156
+ seen.add(v);
157
+ if (d.opcode === 'gaddr') {
158
+ return names.has(d.attrs.sym as string) ? (d.attrs.sym as string) : null;
159
+ }
160
+ if (d.opcode === 'add' || d.opcode === 'sub') {
161
+ const found = d.operands.slice(0, d.opcode === 'sub' ? 1 : undefined).map((o) => baseOf(o, seen));
162
+ const hits = found.filter((f): f is string => f !== null);
163
+ return hits.length === 1 ? hits[0] : null;
164
+ }
165
+ return null;
166
+ };
167
+ const out = new Map<string, Access[]>();
168
+ for (const b of fn.blocks) {
169
+ for (const op of b.ops) {
170
+ const plain = op.opcode === 'load' || op.opcode === 'store';
171
+ const indexed = op.opcode === 'aload' || op.opcode === 'astore';
172
+ if (!plain && !indexed) {
173
+ continue;
174
+ }
175
+ const sym = baseOf(op.operands[0]);
176
+ if (sym === null) {
177
+ continue;
178
+ }
179
+ const width = plain ? (op.attrs.width as number) : indexedWidth(op);
180
+ if (width === null) {
181
+ continue;
182
+ }
183
+ const isLoad = op.opcode === 'load' || op.opcode === 'aload';
184
+ const list = out.get(sym) ?? out.set(sym, []).get(sym)!;
185
+ list.push({
186
+ width,
187
+ ...(isLoad ? { signed: accessSignedness(width, op.attrs.signed as boolean | undefined) } : {}),
188
+ });
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+
194
+ /** The elements of `obj`'s initial bytes read `width` bytes at a time in its byte order. */
195
+ function elements(obj: LocalObject, width: number, signed: boolean): number[] {
196
+ const b = obj.bytes!;
197
+ const out: number[] = [];
198
+ for (let at = 0; at < obj.size; at += width) {
199
+ let v = 0;
200
+ for (let i = 0; i < width; i++) {
201
+ v = v * 256 + b[obj.bigEndian ? at + i : at + width - 1 - i];
202
+ }
203
+ out.push(signed && v >= 2 ** (8 * width - 1) ? v - 2 ** (8 * width) : v);
204
+ }
205
+ return out;
206
+ }
207
+
208
+ /** The width of `obj`'s element: the one its definition's directives write, else the one every
209
+ * access agrees on; with no access either, the whole object where it is aligned below the
210
+ * compiler's aggregate floor, as only a scalar is, and a byte where it is not. */
211
+ function elementWidth(
212
+ obj: LocalObject,
213
+ accesses: readonly Access[],
214
+ layout: StaticLayout,
215
+ ): number | { refused: string } {
216
+ if (obj.directives?.unit !== undefined) {
217
+ return obj.directives.unit;
218
+ }
219
+ const widths = [...new Set(accesses.map((x) => x.width))];
220
+ if (widths.length > 1) {
221
+ return { refused: `whose accesses disagree on its element width (${widths.sort().join(' and ')} bytes)` };
222
+ }
223
+ const shown = obj.directives?.align ?? obj.placement?.align;
224
+ const scalarOnly = shown !== undefined && shown < layout.aggregateAlign;
225
+ return widths[0] ?? (scalarOnly && [1, 2, 4].includes(obj.size) ? obj.size : 1);
226
+ }
227
+
228
+ /** The shape and definition of every static `fn` defines, or the refusal: the static's linker name
229
+ * and the tail of the sentence. */
230
+ export function localStaticShapes(
231
+ fn: Fn,
232
+ layout: StaticLayout | undefined,
233
+ ): LocalStaticShapes | { symbol: string; refused: string } {
234
+ const objs = fn.localObjects;
235
+ const infos = new Map<string, SymbolInfo>();
236
+ const statics: SStatic[] = [];
237
+ if (objs === undefined) {
238
+ return { infos, statics };
239
+ }
240
+ if (layout === undefined) {
241
+ const [first] = objs.keys();
242
+ return { symbol: first, refused: "whose layout rules this target's compiler does not declare" };
243
+ }
244
+ const access = accessesOf(fn, new Set(objs.keys()));
245
+ const widthOf = (o: LocalObject) => elementWidth(o, access.get(o.symbol) ?? [], layout);
246
+ /** Declared here as a scalar, or sized and aligned as one — the compiler aligns a scalar to at
247
+ * least its width. One declared as an array where only a zero scalar sits is refused below: an
248
+ * `s32 = 0` read a byte at a time, or only through its address, lands there as well as four
249
+ * bytes used first. */
250
+ const scalarShaped = (o: LocalObject): boolean =>
251
+ widthOf(o) === o.size || ([1, 2, 4, 8].includes(o.size) && (o.placement?.align ?? 0) >= o.size);
252
+ const zeroed =
253
+ layout.uninitOrder !== undefined
254
+ ? zeroInitialized([...objs.values()], layout.uninitOrder, scalarShaped)
255
+ : new Set<string>();
256
+ if (!(zeroed instanceof Set)) {
257
+ return { symbol: zeroed.symbol, refused: 'in bss at an offset this input does not show' };
258
+ }
259
+ // Declared in the target's declaration order, which the compiler lays the objects out by (mwcc
260
+ // reverses it in `.sbss` for statics with no initializer), so that the candidate's compiler
261
+ // places each where the target has it — except mwcc's `.bss` statics with no initializer, which
262
+ // it places in the order it first uses them: the candidate's code decides that, no declaration.
263
+ for (const obj of [...objs.values()].sort((a, b) => a.order - b.order)) {
264
+ const say = (why: string) => ({ symbol: obj.symbol, refused: why });
265
+ const accesses = access.get(obj.symbol) ?? [];
266
+ const dir = obj.directives;
267
+ // the alignment the target shows
268
+ const shown = dir?.align ?? obj.placement?.align;
269
+ const width = widthOf(obj);
270
+ if (typeof width !== 'number') {
271
+ return say(width.refused);
272
+ }
273
+ if (![1, 2, 4].includes(width)) {
274
+ return say(`whose ${width}-byte elements are no integer type`);
275
+ }
276
+ if (obj.size % width !== 0) {
277
+ return say(`whose ${width}-byte elements do not divide its ${obj.size} bytes`);
278
+ }
279
+ // an access of another width reads through a cast, and says nothing about these elements
280
+ const signs = new Set(accesses.filter((x) => x.width === width && x.signed !== undefined).map((x) => x.signed));
281
+ if (signs.size > 1) {
282
+ return say('whose loads disagree on whether its elements are signed');
283
+ }
284
+ const signed = signs.size > 0 ? signs.has(true) : dir?.negative === true;
285
+ const count = obj.size / width;
286
+ const elem = T.int(width * 8, signed);
287
+ const init = zeroed.has(obj.symbol) ? [0] : obj.bytes === undefined ? undefined : elements(obj, width, signed);
288
+ if (zeroed.has(obj.symbol) && count !== 1) {
289
+ return say(`laid out where only a scalar initialized to zero is, but of ${count} elements`);
290
+ }
291
+ // One element is a scalar — unless it is initialized data holding zero on a compiler that
292
+ // moves a zero scalar to bss, where it can only have been an aggregate, which the compiler
293
+ // keeps in .data.
294
+ const scalar =
295
+ count === 1 && !(layout.zeroScalar === 'bss' && obj.section === 'data' && init!.every((v) => v === 0));
296
+ const type: IrType = scalar ? elem : T.array(elem, count);
297
+ const string = dir?.string === true && width === 1 && !scalar && shown !== undefined && shown >= layout.stringAlign;
298
+ // The alignment the target shows against the one this definition gets from its compiler.
299
+ const own = scalar ? width : Math.max(width, layout.aggregateAlign, string ? layout.stringAlign : 1);
300
+ if (shown !== undefined && shown < own) {
301
+ return say(`whose definition is aligned to ${shown} bytes, less than the ${own} its declaration here would get`);
302
+ }
303
+ const align = shown !== undefined && shown > own ? shown : undefined;
304
+ const isConst = obj.section === 'rodata';
305
+ infos.set(obj.symbol, {
306
+ name: obj.symbol,
307
+ kind: 'data',
308
+ size: obj.size,
309
+ ...(scalar
310
+ ? { shape: 'scalar' as const, signed }
311
+ : { shape: 'array' as const, elemSize: width, elemSigned: signed, dims: [count] }),
312
+ ...(isConst ? { const: true } : {}),
313
+ });
314
+ statics.push({
315
+ name: obj.name,
316
+ type,
317
+ ...(isConst ? { const: true as const } : {}),
318
+ ...(align !== undefined ? { align } : {}),
319
+ ...(init ? { init } : {}),
320
+ ...(string ? { string: true as const } : {}),
321
+ });
322
+ }
323
+ return { infos, statics };
324
+ }
325
+
326
+ /** `sfn` with each static's linker name replaced by its source name — every reference, assignment
327
+ * targets and the typed-global list included — or the refusal. The tree is final here, so what the
328
+ * source name would collide with is all in it: every name its body mentions, callees included,
329
+ * plus its parameters, locals and own name. */
330
+ export function nameLocalStatics(sfn: SFn, objs: LocalObjects): SFn | { symbol: string; refused: string } {
331
+ const taken = takenNames(sfn);
332
+ for (const g of sfn.globals ?? []) {
333
+ taken.add(g.name);
334
+ }
335
+ const bound = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
336
+ const callees = new Set<string>([sfn.name]);
337
+ for (const e of walkExprs(sfn.body)) {
338
+ if (e.k === 'call' && typeof e.fn === 'string') {
339
+ callees.add(e.fn);
340
+ }
341
+ }
342
+ const rename = new Map<string, string>();
343
+ const bySource = new Map<string, string>();
344
+ for (const obj of objs.values()) {
345
+ const say = (why: string) => ({ symbol: obj.symbol, refused: `whose source name '${obj.name}' ${why}` });
346
+ const other = bySource.get(obj.name);
347
+ if (other !== undefined) {
348
+ return say(`another static here ('${other}') also has — one block cannot declare both`);
349
+ }
350
+ if (callees.has(obj.name) || taken.has(obj.name)) {
351
+ const what = callees.has(obj.name) ? 'a function' : bound.has(obj.name) ? 'a parameter or local' : 'a global';
352
+ return say(`is also ${what} this one names — the static would hide it`);
353
+ }
354
+ bySource.set(obj.name, obj.symbol);
355
+ rename.set(obj.symbol, obj.name);
356
+ }
357
+ const expr = (e: Expr): Expr => {
358
+ const r = mapExprChildren(e, expr);
359
+ return (r.k === 'var' || r.k === 'addr' || r.k === 'postincr') && rename.has(r.name)
360
+ ? { ...r, name: rename.get(r.name)! }
361
+ : r;
362
+ };
363
+ const assigns = (s: Stmt): Stmt => {
364
+ const r = mapStmtLists(s, (list) => list.map(assigns));
365
+ if (r.k === 'for') {
366
+ return { ...r, init: assigns(r.init), inc: assigns(r.inc) };
367
+ }
368
+ return r.k === 'assign' && rename.has(r.name) ? { ...r, name: rename.get(r.name)! } : r;
369
+ };
370
+ return {
371
+ ...sfn,
372
+ body: sfn.body.map((s) => assigns(mapStmtExprs(s, expr))),
373
+ ...(sfn.globals
374
+ ? {
375
+ globals: sfn.globals
376
+ .map((g) => ({ ...g, name: rename.get(g.name) ?? g.name }))
377
+ .sort((a, b) => a.name.localeCompare(b.name)),
378
+ }
379
+ : {}),
380
+ };
381
+ }
@@ -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.
@@ -0,0 +1,99 @@
1
+ // asmlift structurer — whether a test that READS MEMORY, and whose two arms spell the SAME
2
+ // statements, can be left out of the C. It decides nothing (both outcomes run the same code next —
3
+ // usually none, or the same edge copy into a join), and leaving it out must drop no access the
4
+ // machine made on that path. Without this the `if` reaches `l3/tailmerge.ts` and `l3/dce.ts`,
5
+ // which hoist the common statements and keep a condition that reads memory as a bare statement
6
+ // (`a1[14] >= 1;`), because they cannot tell a read the machine made there from one it made
7
+ // elsewhere.
8
+ //
9
+ // The IR can. The test is left out when every memory read in its operand cone is a load whose
10
+ // value something OUTSIDE the cone also reads, on the test's own path: a reader in a block that
11
+ // dominates the test's, or one that post-dominates it, and inside exactly the loops the test is
12
+ // in. The C then performs that read as often as the test runs, so the bare statement would be a
13
+ // second read of a value the machine loaded once. A reader on a SIBLING path is not one:
14
+ // `if (a1 != 0) *a2 = *a0; else *a0 < 0;` reads `*a0` on both paths, and without its test the
15
+ // `a1 == 0` path reads nothing. Nor is a reader after the loop the test is in: it post-dominates
16
+ // the test and runs once, where the test and its load run on every iteration, so a volatile
17
+ // register read N times would be read once. A load only this test
18
+ // reads is the machine's one access there and keeps the statement, as does a call or other effect
19
+ // in the cone. A `volatile` load needs nothing more: an on-path reader already performs the one
20
+ // access the machine made, and keeping the test beside it would add a second.
21
+ //
22
+ // A test that reads NO memory is left to the passes after this one: an empty `if` over registers
23
+ // is one `l3/dce.ts` already deletes, and one whose two arms are the same non-empty statements
24
+ // prints as the lift spelled it. Collapsing those too was measured behaviour-preserving (9 more
25
+ // GameCube functions change, 0 benchmark rows) and is left out because nothing in them is read
26
+ // twice.
27
+ //
28
+ // Where the shape comes from: CodeWarrior folds a `case` label into `default:` and keeps its test,
29
+ // so `case 1: … case 0: default: …` dispatches `beq case1; bge default; b default` (mwcc_242_81;
30
+ // ac-decomp mTG_select_tag_decide_needlework, and aSNMgr_set_appear_info_guest, whose two edges
31
+ // carry the same merge value); pikmin UpdateMgr::removeClient keeps an assert's `blt`, whose other
32
+ // side held only a dead load.
33
+ //
34
+ // RENDERING ONLY, and deliberately not a CFG rewrite ahead of the raising tower: a pre-recovery
35
+ // pass turning the same `cond_br` into a `br` costs `af:adds:ido7.1` 6/41 -> 18/41. ido guards a
36
+ // `div` with `bne v0, at` over the numerator, the MIPS lift keeps that trap check as a branch whose
37
+ // arms meet, and that compare is the out-of-block reader that offers `/escape-home` on the
38
+ // numerator, the spelling that wins.
39
+ import type { Block, Op, Value } from '../ir/core';
40
+ import { effectful } from '../ir/discipline';
41
+ import { opSig } from '../ir/opcodes';
42
+ import type { UseSite } from './analysis';
43
+ import type { NaturalLoop } from './loops';
44
+
45
+ export interface RedundantTestDeps {
46
+ defs: Map<Value, Op>;
47
+ useSitesOf: Map<Value, UseSite[]>;
48
+ dom: Map<Block, Set<Block>>;
49
+ ipdom: Map<Block, Block | null>;
50
+ loops: Iterable<NaturalLoop>;
51
+ }
52
+
53
+ /** Whether `branch`, the terminator of `at`, only re-reads what the machine read on its path. */
54
+ export function testRereadsOnly(branch: Op, at: Block, deps: RedundantTestDeps): boolean {
55
+ const { defs, useSitesOf, dom, ipdom } = deps;
56
+ const loops = [...deps.loops];
57
+ const cone = new Set<Op>();
58
+ const loads: Op[] = [];
59
+ const work = [...branch.operands];
60
+ for (let v = work.pop(); v !== undefined; v = work.pop()) {
61
+ const op = defs.get(v);
62
+ if (op === undefined || cone.has(op)) {
63
+ continue;
64
+ }
65
+ if (effectful(op)) {
66
+ return false;
67
+ }
68
+ cone.add(op);
69
+ if (opSig(op.opcode)?.reads === true) {
70
+ loads.push(op);
71
+ }
72
+ work.push(...op.operands);
73
+ }
74
+ // Dominance and post-dominance say a reader runs whenever the test does, not as often: a loop
75
+ // between them repeats one and not the other.
76
+ const sameLoops = (b: Block): boolean => loops.every((l) => l.body.has(b) === l.body.has(at));
77
+ const onPath = (b: Block): boolean => {
78
+ if (!sameLoops(b)) {
79
+ return false;
80
+ }
81
+ if (dom.get(at)?.has(b)) {
82
+ return true;
83
+ }
84
+ for (let p = ipdom.get(at); p; p = ipdom.get(p)) {
85
+ if (p === b) {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ };
91
+ return (
92
+ loads.length > 0 &&
93
+ loads.every((ld) =>
94
+ ld.results.some((r) =>
95
+ (useSitesOf.get(r) ?? []).some((u) => u.op !== branch && !cone.has(u.op) && onPath(u.blk)),
96
+ ),
97
+ )
98
+ );
99
+ }