@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
package/src/l3/basecse.ts CHANGED
@@ -248,15 +248,35 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
248
248
  }
249
249
  }
250
250
 
251
- /** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local. */
252
- function rewrite(e: Expr, localFor: Map<string, string>): Expr {
251
+ /** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
252
+ *
253
+ * A `volatile` cast over a hoisted LEAF keeps its cast and takes the local under it:
254
+ * `((volatile s32 *)0x40000D4)[2]` beside a hoisted `p0 = (s32 *)0x40000D4` becomes
255
+ * `((volatile s32 *)p0)[2]`. The cast is never hoisted itself (`isHoistableBase`), since the local
256
+ * would drop its qualifier; under it the local is the same address, so the access keeps its
257
+ * qualifier and its cell, and reads the register the compiler already holds the base in. Inline, a
258
+ * qualified access at a nonzero offset makes agbcc materialize a second literal with the offset
259
+ * folded in (`.word 0x40000dc` + `ldr r0, [r0]`) where the source read `[r4, #0x8]` off the base
260
+ * it had loaded — the `dmaRegs[2];` that ends a DMA macro. `leafLocal` maps a hoisted leaf to its
261
+ * local, whichever key hoisted it. */
262
+ function rewrite(e: Expr, localFor: Map<string, string>, leafLocal: ReadonlyMap<string, string>): Expr {
253
263
  if (e.k === 'index' && isHoistableBase(e.base)) {
254
264
  const nm = localFor.get(keyOf(e.base, e.width, e.signed));
255
265
  if (nm) {
256
- return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
266
+ return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor, leafLocal) };
267
+ }
268
+ }
269
+ if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true && isHoistableLeaf(e.base.e)) {
270
+ const nm = leafLocal.get(leafId(e.base.e));
271
+ if (nm) {
272
+ return {
273
+ ...e,
274
+ base: { ...e.base, e: { k: 'var', name: nm } },
275
+ idx: rewrite(e.idx, localFor, leafLocal),
276
+ };
257
277
  }
258
278
  }
259
- return mapExprChildren(e, (c) => rewrite(c, localFor));
279
+ return mapExprChildren(e, (c) => rewrite(c, localFor, leafLocal));
260
280
  }
261
281
 
262
282
  /** One base under consideration, keyed as `(base, width, signedness)`. */
@@ -784,7 +804,15 @@ export function hoistBaseLocals(
784
804
  });
785
805
  }
786
806
 
787
- const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor)));
807
+ const leafLocal = new Map<string, string>();
808
+ for (const k of hoisted) {
809
+ const b = meta.get(k)!.base;
810
+ const leaf = leafId(b.k === 'cast' ? b.e : b);
811
+ if (!leafLocal.has(leaf)) {
812
+ leafLocal.set(leaf, localFor.get(k)!);
813
+ }
814
+ }
815
+ const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor, leafLocal)));
788
816
  // The new inits join the tree's LEADING run of base inits rather than being prepended above it:
789
817
  // when rank's /livebase re-runs this pass the head already carries the default run's, and
790
818
  // blindly prepending would spell the new base's pool load above locals the compiler loads first.
@@ -12,23 +12,24 @@
12
12
  // and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
13
13
  import { typeToString } from '../ir/types';
14
14
  import type { Expr, SFn, Stmt } from './ast';
15
- import { exprChildren, isLoop, mapExprChildren, stmtChildren, stmtExprs } from './ast';
15
+ import { exprChildren, isLoop, mapExprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
16
16
  import { type Gate, firstRejection } from './gates';
17
17
 
18
18
  function namesIn(e: Expr, out: Set<string>): void {
19
- // `addr` names a GLOBAL (`&gSym`) or a LOCAL — the structurer renders an `laddr` frame object
20
- // as `&sp0`, an addr node over a name that IS in `sfn.locals`. Both are collected, because a
21
- // name mentioned only through `&` still has a live range: a span that ignored its `addr`
22
- // mentions would be SHORT, and a short span is a clobber where a long one is only a missed
23
- // merge. Collecting a global name costs nothing — it is not in `sfn.locals`, so no pair is
24
- // ever built for it.
19
+ // EVERY mention, through `mentionedName`'s vocabulary rather than this file's own: a span that
20
+ // misses one is SHORT, and a short span is a clobber where a long one is only a missed merge.
21
+ // `&sp0` is a LOCAL's mention — the structurer renders an `laddr` frame object as an addr node
22
+ // over a name that IS in `sfn.locals` — and `n++` in a bottom test is a counter's only mention
23
+ // inside its own loop. A global name comes along for the ride and costs nothing — it is not in
24
+ // `sfn.locals`, so no pair is ever built for it.
25
25
  //
26
- // `rename` DISAGREES WITH THIS, and knowingly: it rewrites only `var` leaves, so a local
27
- // absorbed while mentioned through `&` leaves that mention standing against a declaration the
28
- // merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
26
+ // `rename` DISAGREES WITH THIS, and knowingly: it rewrites `var` and `postincr` leaves, so a
27
+ // local absorbed while mentioned through `&` leaves that mention standing against a declaration
28
+ // the merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
29
29
  // cleanup — so today's behaviour is pinned exactly in coalesce.test.ts ('rename') rather than
30
30
  // repaired here.
31
- if (e.k === 'var' || e.k === 'addr') out.add(e.name);
31
+ const n = mentionedName(e);
32
+ if (n !== undefined) out.add(n);
32
33
  for (const c of exprChildren(e)) namesIn(c, out);
33
34
  }
34
35
 
@@ -62,7 +63,14 @@ export interface Span {
62
63
  * arithmetic over the variable is its own history. The read must be a `var`: `mentions` counts an
63
64
  * `addr` too, and `&a` is not a read of `a`. */
64
65
  const readsVarArithmetically = (e: Expr, n: string): boolean => {
65
- if (e.k === 'index' || e.k === 'field' || e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
66
+ if (
67
+ e.k === 'index' ||
68
+ e.k === 'field' ||
69
+ e.k === 'call' ||
70
+ e.k === 'marker' ||
71
+ e.k === 'postincr' ||
72
+ e.k === 'addr'
73
+ ) {
66
74
  return false;
67
75
  }
68
76
  return (e.k === 'var' && e.name === n) || exprChildren(e).some((c) => readsVarArithmetically(c, n));
@@ -155,7 +163,7 @@ function spans(body: Stmt[]): Map<string, Span> {
155
163
  }
156
164
  function rename(body: Stmt[], from: string, to: string): Stmt[] {
157
165
  const inExpr = (e: Expr): Expr =>
158
- e.k === 'var' && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
166
+ (e.k === 'var' || e.k === 'postincr') && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
159
167
  const inStmt = (s: Stmt): Stmt => {
160
168
  const r = { ...s } as Record<string, unknown>;
161
169
  if (s.k === 'assign' && s.name === from) r.name = to;
@@ -0,0 +1,382 @@
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
+ if (inner.k !== 'index' || (inner.lead?.length ?? 0) > 0 || inner.baseElem !== undefined) {
342
+ return inner;
343
+ }
344
+ // the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
345
+ const name = wholeObject(inner.base);
346
+ const fold = name === null ? undefined : by.get(name);
347
+ const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
348
+ if (elem?.kind !== 'int' || elem.width !== inner.width * 8 || elem.signed !== inner.signed) {
349
+ return inner;
350
+ }
351
+ return { ...inner, base: { k: 'var', name: name! }, subscript: true };
352
+ }
353
+
354
+ /** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
355
+ * the copies gone and the image with them. Throws where one does not fold: structure() refused
356
+ * that function already, so a tree that reaches here unfoldable was made so by a later pass. */
357
+ export function foldConstantImages(sfn: SFn): SFn {
358
+ if (sfn.images === undefined) {
359
+ return sfn;
360
+ }
361
+ let out: SFn = { ...sfn };
362
+ delete out.images;
363
+ for (const image of sfn.images) {
364
+ const plan = planFolds(out, image);
365
+ if (typeof plan === 'string') {
366
+ throw new Error(`cannot spell '${sfn.name}': the compiler's anonymous constant '${image.name}' ${plan}`);
367
+ }
368
+ const copies = new Set(plan.map((f) => f.at));
369
+ const by = new Map(plan.map((f) => [f.local, f]));
370
+ out = {
371
+ ...out,
372
+ body: out.body.filter((_, i) => !copies.has(i)).map((st) => mapStmtExprs(st, (e) => asElement(e, by))),
373
+ locals: out.locals.map((l) => {
374
+ const f = by.get(l.name);
375
+ return f === undefined
376
+ ? l
377
+ : { ...l, type: f.type, init: f.init, ...(f.string ? { initString: true as const } : {}) };
378
+ }),
379
+ };
380
+ }
381
+ return out;
382
+ }
package/src/l3/dce.ts CHANGED
@@ -25,13 +25,15 @@
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 { 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
32
- * escaped pointer, so an address-taken local is never dead here. */
32
+ * escaped pointer, so an address-taken local is never dead here. A `postincr` reads its name
33
+ * before it writes it, so it pins the store that reaches it — `n++` in a loop test is the only
34
+ * reader of the `n = 0` above the loop. */
33
35
  function readsInto(e: Expr, out: Set<string>): void {
34
- if (e.k === 'var' || e.k === 'addr') {
36
+ if (e.k === 'var' || e.k === 'addr' || e.k === 'postincr') {
35
37
  out.add(e.name);
36
38
  }
37
39
  for (const c of exprChildren(e)) {
@@ -48,6 +50,7 @@ function reads(e: Expr): Set<string> {
48
50
  /** True if `e` carries any reason NOT to speculatively delete its assignment/condition:
49
51
  * - `call` — may write memory/globals (a side effect);
50
52
  * - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
53
+ * - `postincr` — its write is the loop update; deleting the assignment would delete the update;
51
54
  * - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
52
55
  * value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
53
56
  * - a memory load (`index`/`field`) — a deref's volatility is unknowable here, so a possibly-effectful read
@@ -56,7 +59,7 @@ function reads(e: Expr): Set<string> {
56
59
  * observable access the machine performed; deleting the dead assignment would delete the read.
57
60
  * A dead assignment whose value contains any of these is kept. */
58
61
  function mustKeep(e: Expr, volatiles: ReadonlySet<string>): boolean {
59
- if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
62
+ if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'index' || e.k === 'field') {
60
63
  return true;
61
64
  }
62
65
  if (e.k === 'var' && (e.name === '?' || volatiles.has(e.name))) {
@@ -95,6 +98,16 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
95
98
  }
96
99
  }
97
100
 
101
+ /** Where a `break` and a `continue` at this depth land, as the set live there. A jump does not fall
102
+ * through to the statement after it: the live set at a jump is its TARGET's, not what the rest of
103
+ * the enclosing block reads. `break` targets the innermost loop or switch, `continue` the innermost
104
+ * loop; each is that statement's conservative in-scope set (everything read anywhere in it plus
105
+ * what is live after it), which also covers the loop's own next test and update. */
106
+ interface JumpLive {
107
+ brk: ReadonlySet<string>;
108
+ cont?: ReadonlySet<string>; // absent in a switch no loop encloses
109
+ }
110
+
98
111
  /** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
99
112
  * returns the rewritten block and the set live on entry. */
100
113
  function dceBlock(
@@ -102,6 +115,7 @@ function dceBlock(
102
115
  liveOut: ReadonlySet<string>,
103
116
  locals: ReadonlySet<string>,
104
117
  volatiles: ReadonlySet<string>,
118
+ jumps: JumpLive | null,
105
119
  ): { out: Stmt[]; liveIn: Set<string> } {
106
120
  const live = new Set(liveOut);
107
121
  const rev: Stmt[] = [];
@@ -153,12 +167,19 @@ function dceBlock(
153
167
  }
154
168
  case 'break':
155
169
  case 'continue': {
170
+ // A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
171
+ // statements after the `if` overwrite `v` for the next iteration. Leaving `live` as the
172
+ // fall-through set would judge the carried copy dead and drop it.
173
+ const target = s.k === 'break' ? jumps?.brk : jumps?.cont;
174
+ if (target) {
175
+ setLive(new Set(target));
176
+ }
156
177
  rev.push(s);
157
178
  break;
158
179
  }
159
180
  case 'if': {
160
- const t = dceBlock(s.then, live, locals, volatiles);
161
- const e = dceBlock(s.else, live, locals, volatiles);
181
+ const t = dceBlock(s.then, live, locals, volatiles, jumps);
182
+ const e = dceBlock(s.else, live, locals, volatiles, jumps);
162
183
  const nlive = new Set<string>();
163
184
  for (const r of reads(s.cond)) {
164
185
  nlive.add(r);
@@ -175,7 +196,7 @@ function dceBlock(
175
196
  if (mustKeep(s.cond, volatiles)) {
176
197
  rev.push({ k: 'exprstmt', value: s.cond });
177
198
  }
178
- } else if (t.out.length === 0) {
199
+ } else if (t.out.length === 0 && !keepsHelperRel(s.cond)) {
179
200
  rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
180
201
  } else {
181
202
  rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
@@ -188,7 +209,7 @@ function dceBlock(
188
209
  // loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
189
210
  const loopLive = new Set(live);
190
211
  allReadsInto([s], loopLive);
191
- const b = dceBlock(s.body, loopLive, locals, volatiles);
212
+ const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
192
213
  const nlive = new Set(loopLive);
193
214
  for (const r of b.liveIn) {
194
215
  nlive.add(r);
@@ -200,7 +221,7 @@ function dceBlock(
200
221
  case 'for': {
201
222
  const loopLive = new Set(live);
202
223
  allReadsInto([s], loopLive);
203
- const b = dceBlock(s.body, loopLive, locals, volatiles);
224
+ const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
204
225
  const nlive = new Set(loopLive);
205
226
  for (const r of b.liveIn) {
206
227
  nlive.add(r);
@@ -214,8 +235,9 @@ function dceBlock(
214
235
  // read anywhere in the switch as live throughout — no case-body store is ever cut.
215
236
  const swLive = new Set(live);
216
237
  allReadsInto([s], swLive);
217
- const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles).out }));
218
- const def = s.default ? dceBlock(s.default, swLive, locals, volatiles).out : s.default;
238
+ const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
239
+ const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
240
+ const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
219
241
  const nlive = new Set(swLive);
220
242
  for (const r of reads(s.scrutinee)) {
221
243
  nlive.add(r);
@@ -263,7 +285,7 @@ export function eliminateDeadStores(sfn: SFn): SFn {
263
285
  allAddrNamesInto(sfn.body, addressTaken);
264
286
  const volatiles = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
265
287
  const locals = new Set(sfn.locals.filter((l) => !l.volatile && !addressTaken.has(l.name)).map((l) => l.name));
266
- const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles).out;
288
+ const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles, null).out;
267
289
  const used = new Set<string>();
268
290
  referencedNames(body, used);
269
291
  return { ...sfn, body, locals: sfn.locals.filter((l) => used.has(l.name) || l.volatile) };
package/src/l3/gates.ts CHANGED
@@ -19,8 +19,9 @@ export interface Gate<Ctx> {
19
19
  /** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
20
20
  * heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
21
21
  readonly sound: boolean;
22
- /** the test that fails when this gate is removed — required for a sound gate */
23
- readonly guardedBy?: string;
22
+ /** the test that fails when this gate is removed — required for a sound gate; a rule with two
23
+ * halves names one test per half */
24
+ readonly guardedBy?: string | readonly string[];
24
25
  /** true ⇒ REJECT this candidate */
25
26
  readonly rejects: (c: Ctx) => boolean;
26
27
  }
@@ -67,6 +68,11 @@ export function ablateHeuristic<Ctx>(gates: readonly Gate<Ctx>[], id: string): r
67
68
  return without(gates, id);
68
69
  }
69
70
 
71
+ /** Every test a gate names as its guard, one per half of the rule. */
72
+ export function guardsOf<Ctx>(g: Gate<Ctx>): readonly string[] {
73
+ return typeof g.guardedBy === 'string' ? [g.guardedBy] : (g.guardedBy ?? []);
74
+ }
75
+
70
76
  /** Structural defects in a gate table — the part checkable without running the pass. Returns
71
77
  * findings rather than throwing, so core stays free of a test-framework import. */
72
78
  export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
@@ -84,7 +90,7 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
84
90
  out.push(`gate '${g.id}' has no usable \`why\``);
85
91
  }
86
92
  // the one rule that costs something to declare
87
- if (g.sound && !g.guardedBy?.trim()) {
93
+ if (g.sound && !guardsOf(g).some((t) => t.trim())) {
88
94
  out.push(`gate '${g.id}' is marked sound but names no guard`);
89
95
  }
90
96
  }