@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -0,0 +1,410 @@
1
+ // asmlift — a local aggregate's constant INITIALIZER, recovered from the copy that fills it.
2
+ //
3
+ // `u8 a[16] = { 0, 1, 2, … };` is not a statement in the machine. A compiler that keeps the
4
+ // aggregate in memory places the initial image as a constant of its own, under a label the source
5
+ // never wrote, and copies it into the frame at entry: agbcc 2.9 writes `.LC0:` in .rodata and
6
+ // `ldr r1,=.LC0; mov r0,sp; mov r2,#16; bl memcpy` (varasm.c `output_constant_def`, expr.c
7
+ // `store_constructor` → `emit_block_move`). The lift reads that image as an ANONYMOUS object
8
+ // (ir/core.ts `LocalObject.anonymous`), so the tree holds the copy as written —
9
+ // `memcpy(sp0, .LC0, 16)` — and the image in `SFn.images`, which no declaration spells. This pass
10
+ // spells the pair back as the declaration it came from: the local's initializer, and no copy.
11
+ //
12
+ // IT RUNS AT EMISSION, NOT IN STRUCTURE. Between the two the tree belongs to the contracts and the
13
+ // respell variations, and every one of them is right about the copy as it stands: it is a call the
14
+ // asm makes (contracts.ts `assertEffectsPreserved` counts it), and it takes the local's address,
15
+ // which is what keeps dead-store elimination and the coalescers off the object. A folded tree
16
+ // would hold a local nothing mentions, which l3/dce.ts deletes — and with it the initializer the
17
+ // compiler keeps (agbcc copies `u8 a[16] = {…}` even when nothing reads `a`). So structure()
18
+ // asks this pass whether the fold is possible and refuses the function loudly where it is not,
19
+ // and the C-family emitter folds (backend/cfamily.ts), the same seam l3/slotorder.ts uses.
20
+ //
21
+ // THE FOLD IS SOUND exactly when the declaration's initializer and the copy are one program:
22
+ // • the copy is a block copy the C standard fixes (proto.ts `blockCopyRoles`), a TOP-LEVEL
23
+ // statement of the body, so it runs once and on every path that reaches it;
24
+ // • its destination is a whole local integer array of the copied size — or a reservation the
25
+ // frame rounded up by less than a word, reached by nothing but elements inside the copy and its
26
+ // whole address — not volatile, and nothing before the copy mentions it but a local assigned
27
+ // its address alone, so moving the copy to the declaration changes no value anything reads (an
28
+ // initializer runs at entry, the copy at its own position);
29
+ // • its source is the whole image, which nothing but such copies mentions, and whose size is the
30
+ // count — one image may fill several locals, since the compiler places one per identical
31
+ // constant (varasm.c `output_constant_def`), each into its own local. Several arrays in one
32
+ // frame are each their own local where the frame audit proves the boundaries between them
33
+ // (frontend/frame-objects.ts `provenParts`). KNOWN GAP: where it keeps the frame's
34
+ // address-taken bytes as one local instead — a neighbour handed to a callee nothing bounds, or
35
+ // a compiler whose placement of such arrays is not known (target.ts `addressTakenArrayOrder`)
36
+ // — the image refuses at the size check;
37
+ // • the image writes one element width (a struct's mixed widths, or padding alone, has no
38
+ // element type this pass could declare). An element it relocates holds an address, spelled as
39
+ // the address constant it is (`u32 a[16] = { (u32)&h1, … }`, which agbcc copies from the same
40
+ // `.word h1` image as the pointer array the source declared, compiled).
41
+ // The local keeps its own element type where the body reaches it by an offset or a cast — those
42
+ // were spelled against it, so its width must be the image's — and takes the image's where only
43
+ // its whole address and element accesses, which each print their own width, reach it.
44
+ //
45
+ // Thumb is the only frontend that reads an anonymous image today. The rule is asked of the tree,
46
+ // for any compiler, but the only copy it reads is a call (proto.ts `blockCopyRoles`). KNOWN GAP: an
47
+ // INLINE copy — agbcc's `ldmia`/`stmia` for a word-aligned image below its memcpy sizes, lw/sw or
48
+ // lwl/lwr+swl/swr (IDO, gcc 2.7.2, KMC), lwz/stw (mwcc) — declines, naming the copy it does not
49
+ // read; no benchmark row has one, and the MIPS and PPC frontends refuse the address-taken frame
50
+ // local before the copy is reached.
51
+ import { type IrType, typeToString } from '../ir/types';
52
+ import { blockCopyRoles } from '../proto';
53
+ import {
54
+ type Expr,
55
+ type InitAddress,
56
+ type SFn,
57
+ type SImage,
58
+ type Stmt,
59
+ mapExprChildren,
60
+ mapStmtExprs,
61
+ mentionedName,
62
+ walkExprs,
63
+ } from './ast';
64
+ import { localMentions, mentionsAnyLocal } from './mentions';
65
+
66
+ /** `e` without the casts around it. */
67
+ function bare(e: Expr): Expr {
68
+ return e.k === 'cast' ? bare(e.e) : e;
69
+ }
70
+
71
+ /** The value of `e` where it is integer arithmetic on constants alone, or null. A copy's count is
72
+ * a constant the machine may build in several instructions — agbcc spells 256 and up as an 8-bit
73
+ * immediate shifted (`mov r2,#0x80; lsl r2,r2,#1`), which the tree keeps as `128 << 1`. */
74
+ function constantValue(e: Expr): number | null {
75
+ const b = bare(e);
76
+ if (b.k === 'const') {
77
+ return b.value;
78
+ }
79
+ if (b.k !== 'bin') {
80
+ return null;
81
+ }
82
+ const l = constantValue(b.l);
83
+ const r = constantValue(b.r);
84
+ if (l === null || r === null) {
85
+ return null;
86
+ }
87
+ switch (b.op) {
88
+ case '+':
89
+ return l + r;
90
+ case '-':
91
+ return l - r;
92
+ case '*':
93
+ return l * r;
94
+ case '<<':
95
+ return r >= 0 && r < 32 ? l * 2 ** r : null;
96
+ case '|':
97
+ return l >= 0 && r >= 0 && l < 2 ** 31 && r < 2 ** 31 ? l | r : null;
98
+ default:
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /** The object `e` names whole — `x` or `&x` — or null. */
104
+ function wholeObject(e: Expr): string | null {
105
+ const b = bare(e);
106
+ return b.k === 'postincr' ? null : (mentionedName(b) ?? null);
107
+ }
108
+
109
+ /** Every mention of `name` as an expression in `body`. */
110
+ function mentions(body: readonly Stmt[], name: string): number {
111
+ let n = 0;
112
+ for (const e of walkExprs(body as Stmt[])) {
113
+ if (mentionedName(e) === name) {
114
+ n++;
115
+ }
116
+ }
117
+ return n;
118
+ }
119
+
120
+ const byteSize = (t: IrType): number | null =>
121
+ t.kind === 'int'
122
+ ? t.width / 8
123
+ : t.kind === 'array'
124
+ ? (() => {
125
+ const e = byteSize(t.elem);
126
+ return e === null ? null : e * t.count;
127
+ })()
128
+ : null;
129
+
130
+ /** `v`, an element of `width` bytes, read at the given signedness. */
131
+ function resign(v: number, width: number, signed: boolean): number {
132
+ const bits = 8 * width;
133
+ const u = ((v % 2 ** bits) + 2 ** bits) % 2 ** bits;
134
+ return signed && u >= 2 ** (bits - 1) ? u - 2 ** bits : u;
135
+ }
136
+
137
+ /** One copy's fold: the copy statement's index, the local and its declaration. */
138
+ interface Fold {
139
+ at: number;
140
+ local: string;
141
+ type: IrType;
142
+ init: (number | InitAddress | null)[];
143
+ string: boolean;
144
+ }
145
+
146
+ /** Why `image` cannot fold, as the tail of a sentence naming it, or the fold of each copy of it. */
147
+ function planFolds(sfn: SFn, image: SImage): Fold[] | string {
148
+ if (image.type.kind !== 'array' || image.type.elem.kind !== 'int') {
149
+ return 'whose image writes no one element width — a struct, or padding alone';
150
+ }
151
+ const copies: number[] = [];
152
+ sfn.body.forEach((s, i) => {
153
+ if (
154
+ s.k === 'exprstmt' &&
155
+ s.value.k === 'call' &&
156
+ wholeObject(s.value.args[blockCopyRoles(s.value.fn)?.src ?? -1] ?? { k: 'const', value: 0 }) === image.name
157
+ ) {
158
+ copies.push(i);
159
+ }
160
+ });
161
+ if (copies.length === 0) {
162
+ return (
163
+ 'which no top-level block-copy call (memcpy) takes as its source — the one copy folded into an ' +
164
+ 'initializer; an inline, element-by-element copy of the image, and any other read of it, is not'
165
+ );
166
+ }
167
+ if (mentions(sfn.body, image.name) !== copies.length) {
168
+ return 'which the body reads other than as the source of a block copy at the top level';
169
+ }
170
+ const folds: Fold[] = [];
171
+ for (const at of copies) {
172
+ const fold = planCopy(sfn, image, at);
173
+ if (typeof fold === 'string') {
174
+ return fold;
175
+ }
176
+ folds.push(fold);
177
+ }
178
+ return folds;
179
+ }
180
+
181
+ /** Why the block copy of `image` at body index `at` cannot fold, or its fold. */
182
+ function planCopy(sfn: SFn, image: SImage, at: number): Fold | string {
183
+ const elem = (image.type as Extract<IrType, { kind: 'array' }>).elem as Extract<IrType, { kind: 'int' }>;
184
+ const size = byteSize(image.type)!;
185
+ const call = (sfn.body[at] as Extract<Stmt, { k: 'exprstmt' }>).value as Extract<Expr, { k: 'call' }>;
186
+ const roles = blockCopyRoles(call.fn)!;
187
+ if (constantValue(call.args[roles.n] ?? { k: 'var', name: '' }) !== size) {
188
+ return `whose copy does not move its whole ${size} bytes`;
189
+ }
190
+ const local = wholeObject(call.args[roles.dst] ?? { k: 'const', value: 0 });
191
+ const decl = sfn.locals.find((l) => l.name === local);
192
+ if (local === null || decl === undefined) {
193
+ return 'whose copy does not fill a local of this function';
194
+ }
195
+ if (decl.init !== undefined) {
196
+ return `whose copy fills '${local}', which another image already initializes`;
197
+ }
198
+ if (decl.volatile || decl.pointeeVolatile) {
199
+ return `whose copy fills '${local}', which is volatile`;
200
+ }
201
+ const declElem = decl.type.kind === 'array' ? decl.type.elem : null;
202
+ const declSize = byteSize(decl.type);
203
+ // The copy moves the whole object — a brace initializer's image carries its zero tail — so the
204
+ // count is the object's size, and a reservation the frame audit measured wider by less than a
205
+ // word is the frame's own rounding (agbcc rounds a 6-byte array's frame to 8).
206
+ if (declElem === null || declElem.kind !== 'int' || declSize === null || declSize < size || declSize - size >= 4) {
207
+ return `whose copy fills '${local}', a ${typeToString(decl.type)}, which is not an integer array of its ${size} bytes`;
208
+ }
209
+ // A local that holds nothing but the local's address — `p0 = (u8 *)&sp0;`, the base the
210
+ // readability hoist (l3/basecse.ts) places at the head of the body, ahead of the copy — reads no
211
+ // element where it is assigned, so the copy may move past it; its uses are the local's.
212
+ const aliasAt = aliasesOf(sfn, local);
213
+ const reach = new Set([local, ...aliasAt.keys()]);
214
+ const aliasing = new Set(aliasAt.values());
215
+ const kept = sfn.body.filter((_, i) => i !== at && !aliasing.has(i));
216
+ if (
217
+ mentionsAnyLocal(
218
+ sfn.body.slice(0, at).filter((_, i) => !aliasing.has(i)),
219
+ reach,
220
+ )
221
+ ) {
222
+ return `whose copy into '${local}' is not the first thing the body does with it`;
223
+ }
224
+ const use = uses(kept, reach);
225
+ const count = size / (elem.width / 8);
226
+ // Reached as anything but elements, the local may be reached past the bytes copied — the tail a
227
+ // fill writes after a short string's copy — so the reservation must be the copy's exactly.
228
+ if (use.other && byteSize(decl.type) !== size) {
229
+ return `whose copy fills '${local}', a ${typeToString(decl.type)} the body reaches past the ${size} bytes copied`;
230
+ }
231
+ if (use.extents.some(([from, to]) => from < 0 || to > size)) {
232
+ return `whose copy fills '${local}', which the body indexes past the ${size} bytes copied`;
233
+ }
234
+ // An offset or a cast was spelled against the local's own element, so the image's must match and
235
+ // the local keeps its type. Element accesses print their own width, so where only they and the
236
+ // whole address reach it, the declaration (the frame audit may type it by bytes alone) witnesses
237
+ // nothing: the image says the element, and its signedness where it writes a negative (agbcc does
238
+ // only for a signed element), else the reads of that width do.
239
+ let own: Extract<IrType, { kind: 'int' }>;
240
+ if (use.other) {
241
+ if (declElem.width !== elem.width) {
242
+ return `whose ${elem.width / 8}-byte elements are not the ${declElem.width / 8}-byte elements the body reaches '${local}' by`;
243
+ }
244
+ own = declElem;
245
+ } else {
246
+ const signs = new Set(use.reads.filter((r) => r.width * 8 === elem.width).map((r) => r.signed));
247
+ const signed =
248
+ elem.signed ||
249
+ (signs.size === 1 ? [...signs][0] : declElem.width === elem.width && use.indexed > 0 && declElem.signed);
250
+ own = { ...elem, signed };
251
+ }
252
+ return {
253
+ at,
254
+ local,
255
+ type: { kind: 'array', elem: own, count },
256
+ // an address is the address whatever the element's signedness: its cast spells that
257
+ init: image.init.map((v) => (typeof v === 'number' ? resign(v, elem.width / 8, own.signed) : v)),
258
+ string: image.string === true && elem.width === 8,
259
+ };
260
+ }
261
+
262
+ /** The locals of `sfn` that hold `local`'s address and nothing else — assigned it once, at the top
263
+ * level of the body, and never written through `&` — each with the index of that assignment. */
264
+ function aliasesOf(sfn: SFn, local: string): Map<string, number> {
265
+ const counts = localMentions(sfn);
266
+ const out = new Map<string, number>();
267
+ sfn.body.forEach((s, i) => {
268
+ const m = s.k === 'assign' ? counts.get(s.name) : undefined;
269
+ if (
270
+ s.k === 'assign' &&
271
+ s.name !== local &&
272
+ m?.assigns === 1 &&
273
+ m.addrTaken === 0 &&
274
+ wholeObject(s.value) === local
275
+ ) {
276
+ out.set(s.name, i);
277
+ }
278
+ });
279
+ return out;
280
+ }
281
+
282
+ /** How `body` reaches the object the names in `reach` all denote whole: the byte extent of each
283
+ * direct element access with a constant index (`sp0[k]`, `((u8 *)sp0)[k]`, `p0[k]` — the index
284
+ * node carries its own width), how many of those there are in all, and whether anything reaches
285
+ * it other than as a whole call argument or a direct element access. */
286
+ function uses(
287
+ body: readonly Stmt[],
288
+ reach: ReadonlySet<string>,
289
+ ): { indexed: number; extents: [number, number][]; reads: { width: number; signed: boolean }[]; other: boolean } {
290
+ let total = 0;
291
+ let accounted = 0;
292
+ let indexed = 0;
293
+ const extents: [number, number][] = [];
294
+ const reads: { width: number; signed: boolean }[] = [];
295
+ const reaches = (e: Expr): boolean => {
296
+ const n = wholeObject(e);
297
+ return n !== null && reach.has(n);
298
+ };
299
+ for (const e of walkExprs(body as Stmt[])) {
300
+ const n = mentionedName(e);
301
+ if (n !== undefined && reach.has(n)) {
302
+ total++;
303
+ }
304
+ if (e.k === 'call') {
305
+ accounted += e.args.filter(reaches).length;
306
+ }
307
+ if (e.k === 'index' && reaches(e.base)) {
308
+ accounted++;
309
+ indexed++;
310
+ reads.push({ width: e.width, signed: e.signed });
311
+ const k = bare(e.idx);
312
+ if (k.k === 'const') {
313
+ extents.push([k.value * e.width, (k.value + 1) * e.width]);
314
+ }
315
+ }
316
+ }
317
+ return { indexed, extents, reads, other: total !== accounted };
318
+ }
319
+
320
+ /** Whether every anonymous image `sfn` holds folds into a local's initializer: null when it does,
321
+ * else the image's name and the tail of the sentence that refuses it. */
322
+ export function constantImageRefusal(sfn: SFn): { image: string; refused: string } | null {
323
+ for (const image of sfn.images ?? []) {
324
+ const plan = planFolds(sfn, image);
325
+ if (typeof plan === 'string') {
326
+ return { image: image.name, refused: plan };
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+
332
+ /** `e` with every access to a folded local that reads one of its elements AS that element spelled
333
+ * `a[k]` on the local itself. The structurer spelled the access against the type the frame audit
334
+ * gave the local — bytes, unsigned — so a read of the element type this pass gives it (`s8`, from
335
+ * the image) came out through the local's address and a cast, `((s8 *)a)[5]`. The two are one C
336
+ * value and not one compile: agbcc re-forms `sp + 0x15` for the cast where `b[5]` reads through
337
+ * the base it already holds for `b`'s copy (`ldrsb r0, [r5, r0]`). An access of another width or
338
+ * signedness keeps its cast, which is the element type it spells. */
339
+ function asElement(e: Expr, by: ReadonlyMap<string, Fold>): Expr {
340
+ const inner = mapExprChildren(e, (c) => asElement(c, by));
341
+ // …and a read of the element's BYTES at the other signedness, converted to the element's own
342
+ // type, IS the element: `(s8)((u8 *)a)[i]` is `a[i]` on `s8 a[16]`. agbcc has no `ldrsb` with an
343
+ // immediate offset, so a signed byte read through the frame base is `ldrb` and `lsl #24; asr #24`,
344
+ // which the lift reads as the unsigned byte sign-extended; C extends the element itself the same
345
+ // way. Only a conversion to exactly the element type: a wider or a narrower one is another value.
346
+ // A DECLARED array's byte (a map global's, a proven frame part's) is read signed before this, by
347
+ // the structurer (structure.ts `declaredExtendedRead`); the type this pass gives a local exists
348
+ // only here, so for those the identity is applied here.
349
+ if (inner.k === 'cast' && inner.to.kind === 'int' && inner.volatile !== true && inner.e.k === 'index') {
350
+ const read = inner.e;
351
+ const elem = elementOf(read, by);
352
+ if (elem !== undefined && elem.width === inner.to.width && elem.signed === inner.to.signed) {
353
+ return asElement({ ...read, signed: elem.signed }, by);
354
+ }
355
+ }
356
+ if (inner.k !== 'index') {
357
+ return inner;
358
+ }
359
+ const elem = elementOf(inner, by);
360
+ if (elem === undefined || elem.signed !== inner.signed) {
361
+ return inner;
362
+ }
363
+ return { ...inner, base: { k: 'var', name: wholeObject(inner.base)! }, subscript: true };
364
+ }
365
+
366
+ /** The element type of the folded local `read` reads one element of — at the element's width,
367
+ * whatever its signedness — or undefined for any other access. */
368
+ function elementOf(
369
+ read: Extract<Expr, { k: 'index' }>,
370
+ by: ReadonlyMap<string, Fold>,
371
+ ): Extract<IrType, { kind: 'int' }> | undefined {
372
+ if ((read.lead?.length ?? 0) > 0 || read.baseElem !== undefined) {
373
+ return undefined;
374
+ }
375
+ // the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
376
+ const name = wholeObject(read.base);
377
+ const fold = name === null ? undefined : by.get(name);
378
+ const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
379
+ return elem?.kind === 'int' && elem.width === read.width * 8 ? elem : undefined;
380
+ }
381
+
382
+ /** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
383
+ * the copies gone and the image with them. Throws where one does not fold: structure() refused
384
+ * that function already, so a tree that reaches here unfoldable was made so by a later pass. */
385
+ export function foldConstantImages(sfn: SFn): SFn {
386
+ if (sfn.images === undefined) {
387
+ return sfn;
388
+ }
389
+ let out: SFn = { ...sfn };
390
+ delete out.images;
391
+ for (const image of sfn.images) {
392
+ const plan = planFolds(out, image);
393
+ if (typeof plan === 'string') {
394
+ throw new Error(`cannot spell '${sfn.name}': the compiler's anonymous constant '${image.name}' ${plan}`);
395
+ }
396
+ const copies = new Set(plan.map((f) => f.at));
397
+ const by = new Map(plan.map((f) => [f.local, f]));
398
+ out = {
399
+ ...out,
400
+ body: out.body.filter((_, i) => !copies.has(i)).map((st) => mapStmtExprs(st, (e) => asElement(e, by))),
401
+ locals: out.locals.map((l) => {
402
+ const f = by.get(l.name);
403
+ return f === undefined
404
+ ? l
405
+ : { ...l, type: f.type, init: f.init, ...(f.string ? { initString: true as const } : {}) };
406
+ }),
407
+ };
408
+ }
409
+ return out;
410
+ }
package/src/l3/dce.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  // unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
26
26
  // keep is defense-in-depth for any future caller that skips that check.
27
27
  import type { Expr, SFn, Stmt } from './ast';
28
- import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
28
+ import { caseEndGotoLeaves, exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
29
29
 
30
30
  /** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
31
31
  * `addr` name counts too: taking a local's address makes every store to it observable through the
@@ -106,6 +106,24 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
106
106
  interface JumpLive {
107
107
  brk: ReadonlySet<string>;
108
108
  cont?: ReadonlySet<string>; // absent in a switch no loop encloses
109
+ /** live at a case label of the innermost switch: what its `goto` (l3/ast.ts) lands on — absent
110
+ * where no switch encloses the statement, or a loop stands between it and the switch */
111
+ caseLabel?: ReadonlySet<string>;
112
+ /** live at the end of the innermost switch arm around the statement, loops between included: what
113
+ * a case-end `goto` lands on — absent where no switch encloses it */
114
+ caseEnd?: ReadonlySet<string>;
115
+ }
116
+
117
+ /** The conservative set live throughout a loop: what is live after it and every read in it, and,
118
+ * where a case-end `goto` in its body leaves it, what is live at the arm's end that goto lands on —
119
+ * a later iteration may take it. */
120
+ function loopLiveOf(s: Stmt & { body: Stmt[] }, live: ReadonlySet<string>, jumps: JumpLive | null): Set<string> {
121
+ const loopLive = new Set(live);
122
+ allReadsInto([s], loopLive);
123
+ if (jumps?.caseEnd && caseEndGotoLeaves(s.body)) {
124
+ jumps.caseEnd.forEach((n) => loopLive.add(n));
125
+ }
126
+ return loopLive;
109
127
  }
110
128
 
111
129
  /** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
@@ -165,6 +183,26 @@ function dceBlock(
165
183
  rev.push(s);
166
184
  break;
167
185
  }
186
+ // The jump lands on the `break;` that ends its arm (l3/ast.ts, contracts.ts
187
+ // `assertCaseGotosNarrow`), whose every read the switch below already counts live.
188
+ case 'caseEndGoto': {
189
+ if (!jumps?.caseEnd) {
190
+ throw new Error('dce: a case-end goto outside a switch arm');
191
+ }
192
+ setLive(new Set(jumps.caseEnd));
193
+ rev.push(s);
194
+ break;
195
+ }
196
+ case 'goto': {
197
+ // The jump lands on a case label of the switch it sits in (l3/ast.ts, contracts.ts
198
+ // `assertCaseGotosNarrow`), whose every read the switch below already counts live.
199
+ if (!jumps?.caseLabel) {
200
+ throw new Error('dce: a goto outside the switch whose case it names');
201
+ }
202
+ setLive(new Set(jumps.caseLabel));
203
+ rev.push(s);
204
+ break;
205
+ }
168
206
  case 'break':
169
207
  case 'continue': {
170
208
  // A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
@@ -196,7 +234,7 @@ function dceBlock(
196
234
  if (mustKeep(s.cond, volatiles)) {
197
235
  rev.push({ k: 'exprstmt', value: s.cond });
198
236
  }
199
- } else if (t.out.length === 0) {
237
+ } else if (t.out.length === 0 && !keepsHelperRel(s.cond)) {
200
238
  rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
201
239
  } else {
202
240
  rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
@@ -207,9 +245,12 @@ function dceBlock(
207
245
  case 'dowhile': {
208
246
  // Conservative: any local read anywhere in the loop is live throughout it, so a
209
247
  // loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
210
- const loopLive = new Set(live);
211
- allReadsInto([s], loopLive);
212
- const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
248
+ const loopLive = loopLiveOf(s, live, jumps);
249
+ const b = dceBlock(s.body, loopLive, locals, volatiles, {
250
+ brk: loopLive,
251
+ cont: loopLive,
252
+ caseEnd: jumps?.caseEnd,
253
+ });
213
254
  const nlive = new Set(loopLive);
214
255
  for (const r of b.liveIn) {
215
256
  nlive.add(r);
@@ -219,9 +260,12 @@ function dceBlock(
219
260
  break;
220
261
  }
221
262
  case 'for': {
222
- const loopLive = new Set(live);
223
- allReadsInto([s], loopLive);
224
- const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
263
+ const loopLive = loopLiveOf(s, live, jumps);
264
+ const b = dceBlock(s.body, loopLive, locals, volatiles, {
265
+ brk: loopLive,
266
+ cont: loopLive,
267
+ caseEnd: jumps?.caseEnd,
268
+ });
225
269
  const nlive = new Set(loopLive);
226
270
  for (const r of b.liveIn) {
227
271
  nlive.add(r);
@@ -235,7 +279,7 @@ function dceBlock(
235
279
  // read anywhere in the switch as live throughout — no case-body store is ever cut.
236
280
  const swLive = new Set(live);
237
281
  allReadsInto([s], swLive);
238
- const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
282
+ const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont, caseLabel: swLive, caseEnd: swLive };
239
283
  const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
240
284
  const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
241
285
  const nlive = new Set(swLive);
@@ -43,12 +43,19 @@ import {
43
43
  exprChildren,
44
44
  exprEquals,
45
45
  exprHasEffect,
46
+ keepsHelperRel,
46
47
  mentionedName,
47
48
  stmtChildren,
48
49
  stmtExprs,
49
50
  walkExprs,
50
51
  } from './ast';
51
- import { arithConversionSignedness, declaredTypes, exprIntWidth, provablyNonNegative } from './typing';
52
+ import {
53
+ arithConversionSignedness,
54
+ declaredTypes,
55
+ exprIntWidth,
56
+ provablyNonNegative,
57
+ rendersSignedOverUnsigned,
58
+ } from './typing';
52
59
 
53
60
  const readsVar = (e: Expr, name: string): boolean =>
54
61
  mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
@@ -95,8 +102,9 @@ const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: R
95
102
  };
96
103
 
97
104
  /** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
98
- * when a width-32 cast is all that separates them: `/uns-cmp` wraps one side in `(u32)` to make
99
- * the branch unsigned, and on a zero-trip guard that side is the very const the init assigns.
105
+ * when a width-32 cast is all that separates them: the unsigned-compare spelling wraps one side in
106
+ * `(u32)` to make the branch unsigned, and on a zero-trip guard that side is the very const the
107
+ * init assigns.
100
108
  * The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
101
109
  * (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
102
110
  * the compare's rendered signedness can differ, which meaningPreserved checks separately. A
@@ -137,6 +145,25 @@ const meaningPreserved = (
137
145
  return before !== undefined && before === after;
138
146
  };
139
147
 
148
+ /** `cond` with one side read through `v`, AFTER `meaningPreserved` said yes. That answer is a
149
+ * value proof the finished tree cannot repeat: `v` holds X, but `v`'s declaration does not say
150
+ * what range X had, so rank.ts's judgment of the tree (typing.ts `signedOverUnsignedCompare`)
151
+ * would call an unsigned compare whose ORIGINAL operands were both non-negative signed, and
152
+ * withhold a spelling that compares the same values. So a compare that was value-faithful before
153
+ * the swap is marked proved: its `unsignedCmp` evidence is dropped. One that was not keeps it. */
154
+ const swapSide = (
155
+ cond: Extract<Expr, { k: 'bin' }>,
156
+ side: 'l' | 'r',
157
+ v: string,
158
+ env: ReturnType<typeof declaredTypes>,
159
+ ): Expr => {
160
+ const out: Extract<Expr, { k: 'bin' }> = { ...cond, [side]: { k: 'var', name: v } };
161
+ if (cond.unsignedCmp === true && !rendersSignedOverUnsigned(cond.l, cond.r, env)) {
162
+ delete out.unsignedCmp;
163
+ }
164
+ return out;
165
+ };
166
+
140
167
  /** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
141
168
  * tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
142
169
  * name's presence in a subtree, so the verdict carries over to the rewritten one. */
@@ -223,7 +250,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
223
250
  then = then.slice(1);
224
251
  els = els.slice(1);
225
252
  }
226
- if (then.length === 0 && els.length > 0) {
253
+ if (then.length === 0 && els.length > 0 && !keepsHelperRel(cond)) {
227
254
  const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
228
255
  if (neg !== undefined && cond.k === 'bin') {
229
256
  cond = { ...cond, op: neg };
@@ -234,16 +261,20 @@ export function initFirstGuards(sfn: SFn): SFn | null {
234
261
  // a COMMON-hoisted variable holds its const on both paths, so the condition's matching
235
262
  // const operand reads through it unconditionally — no tail gate needed.
236
263
  // A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
237
- // this site does not run `meaningPreserved`, so it may not swap in a name whose declared
238
- // type could re-render the compare's signedness — which is exactly what peeling a `(u32)`
239
- // would put on the table. A row that needs `/uns-cmp`'s spelling hoisted here brings the
240
- // gate with it.
264
+ // peeling a `(u32)` here would put a re-rendered signedness on the table with no cast left to
265
+ // say which. The name's DECLARED type can re-render it too — `if (a1 > 0)` read through a
266
+ // `u32 v` that holds the 0 is `a1 != 0` — so the swap asks `meaningPreserved` as the guard
267
+ // re-spelling does.
241
268
  for (const hv of hoisted) {
242
269
  if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
243
270
  if (cond.l.k === 'const' && cond.l.value === hv.value) {
244
- cond = { ...cond, l: { k: 'var', name: hv.name } };
271
+ if (meaningPreserved(cond.l, cond.r, 'l', hv.name, env)) {
272
+ cond = swapSide(cond, 'l', hv.name, env);
273
+ }
245
274
  } else if (cond.r.k === 'const' && cond.r.value === hv.value) {
246
- cond = { ...cond, r: { k: 'var', name: hv.name } };
275
+ if (meaningPreserved(cond.l, cond.r, 'r', hv.name, env)) {
276
+ cond = swapSide(cond, 'r', hv.name, env);
277
+ }
247
278
  }
248
279
  }
249
280
  }
@@ -283,7 +314,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
283
314
  ) {
284
315
  out.push(init);
285
316
  moved.add(init);
286
- cond = { ...cond, [side]: { k: 'var', name: init.name } };
317
+ cond = swapSide(cond, side, init.name, env);
287
318
  then = then.slice(1);
288
319
  changed = true;
289
320
  }
package/src/l3/intcell.ts CHANGED
@@ -38,7 +38,8 @@ const isCell = (x: Expr, name: string): boolean =>
38
38
  * a bare value as it stands, anything else converted. */
39
39
  function integerValue(value: Expr, name: string): Expr {
40
40
  const stored = value.k === 'cast' ? value.e : value;
41
- // `(u8 *)((u32)g + r)`: the integer sum the operand-order rule restores to a byte pointer
41
+ // `(u8 *)((u32)g + r)`: the integer sum the arithmetic table restores to a byte pointer
42
+ // (structure/pointer-spelling.ts ARITH_ROWS, `restore: 'bytes'`)
42
43
  const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
43
44
  if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
44
45
  return { ...sum, l: { k: 'var', name } };