@asmlift/core 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,296 @@
1
+ /** THE DEVICE PIN: which memory accesses of a lifted function are a device register's, and of those,
2
+ * which the recompile must make exactly where the machine did. Such an access is stamped
3
+ * `volatile` (the `device` placement, ir/discipline.ts), and the structurer spells it through a
4
+ * qualified pointer or declines.
5
+ *
6
+ * The frame-object audit (frontend/frame-objects.ts) decides WHETHER a function is pinned and
7
+ * under which policy: it is the pass that sees a frame address handed to a device that reads
8
+ * through it, and that chooses the one-object or the per-object model. This module decides WHICH
9
+ * accesses, from the policy row the audit names. Nothing here decodes, but the pass pins only
10
+ * behind a sink that audit hands it — a `readOnlyAddressSinks` register a frame address was
11
+ * stored to — and pins nothing without one, whatever the target's `deviceRegisters` window
12
+ * says. thumb.ts is the one frontend that runs that audit.
13
+ *
14
+ * EVERY DEVICE ACCESS OF A FUNCTION KEPT AS ONE OBJECT IS PINNED, as the source's `REG_*` and
15
+ * `vu32 *dmaRegs` spell them — store and load alike, because agbcc drops or moves either when it
16
+ * is plain:
17
+ * • a plain store to an address a later store overwrites, with nothing between that may alias
18
+ * it, is deleted (flow.c:2041-2052), and at -O2 a store of another type does not alias (strict
19
+ * aliasing, toplev.c:3616) — so of two transfers armed back to back through one channel, with
20
+ * only a `u16` member store between, the first one's source, destination and control stores go
21
+ * and that transfer never runs; `REG_IME = 0; … REG_IME = saved;` loses its first store the
22
+ * same way;
23
+ * • a plain load in a loop that stores nothing it may alias is invariant, so
24
+ * `while (REG_VCOUNT != 160);` is hoisted into a loop that never reads the register again.
25
+ * The address may be a literal, a literal plus a runtime index — `(vu32 *)(0x40000B0 + ch*12)`, a
26
+ * channel chosen at run time — or a phi each of whose incoming values is one of those: every way
27
+ * the lift names a device register and not a pointer loaded, passed or computed from nothing it
28
+ * can place. Over-reach costs a spelling and never an access: `volatile` keeps the accesses the
29
+ * machine made, and a pinned read is placed once, as a call is (structure/analysis.ts), so the
30
+ * qualifier adds none. The window is the target's `deviceRegisters`, which has to cover every
31
+ * register a source reaches — an address outside it stays plain; without one, it is the channels
32
+ * handed a frame address.
33
+ *
34
+ * A FUNCTION ACCEPTED OBJECT BY OBJECT pins every device read, and of its device stores only those
35
+ * of the first kind (`overwritten`). A read is pinned because a plain one is lost either way:
36
+ * nothing uses the `dmaRegs[2];` that ends a DMA macro, so the lift would drop it outright (the
37
+ * dead-read pin, frontend/dead-reads.ts, keeps such a read in every function), and a poll is
38
+ * hoisted as above, which only this pin prevents. Its other device stores stay plain, their
39
+ * qualified spelling left to `/vol-store`'s candidate (l3/volstore.ts): pinned in the structured tree they are pinned in every
40
+ * variation, and the ones that home the base or un-reduce a loop refuse a qualified base, which
41
+ * costs `synthetic:dmastride` and `synthetic:dmaptrsrc` their matches. That is also why the two
42
+ * policies stay two rows: the one-object row's every-store pin is what keeps `threeFills`' nine
43
+ * qualified stores (thumb-frontend.test.ts), and on the per-object row it costs those two matches.
44
+ *
45
+ * One store left plain this way is a GAP: a device store a pinned read of the same cell re-reads
46
+ * in a loop where nothing else may alias the cell. agbcc's loop pass takes the first access of a
47
+ * cell it meets as the cell's and compares no qualifier (loop.c:8767, rtx_equal_p at
48
+ * rtlanal.c:1066), so plain, the store moves the cell into a register and the read is served the
49
+ * stored value. The variations that home the channel's base name the store and the read through
50
+ * different addresses, which are two cells to that pass, and those are the ones a pin here would
51
+ * refuse.
52
+ *
53
+ * Under either policy, a read the compiler could not have made of a `volatile` stays plain
54
+ * (target.ts `readCouldBeVolatile`): on a compiler that sign-extends a qualified narrow read in a
55
+ * register, a sign-extending load is a plain read in the source, and qualified it recompiles to
56
+ * another instruction sequence. */
57
+ import { type RangeCtx, type StridedRange, incomingArgs, stridedRange } from '../ir/bits';
58
+ import type { Block, Op, Value } from '../ir/core';
59
+ import type { SymbolMap } from '../symbols';
60
+ import { type TargetDescription, readCouldBeVolatile } from '../target';
61
+
62
+ /** The literal addresses a function's values denote, over its IR as it stands when asked. */
63
+ export interface LiteralAddresses {
64
+ readonly defOf: ReadonlyMap<Value, Op>;
65
+ /** the value of a `const`, else undefined */
66
+ constOf(v: Value): number | undefined;
67
+ /** the literal address `v` denotes, or undefined when this cannot say */
68
+ literalAddrOf(v: Value): number | undefined;
69
+ /** every address `v` may denote, as a strided unsigned interval, or undefined when this cannot
70
+ * bound it */
71
+ addressRangeOf(v: Value): StridedRange | undefined;
72
+ }
73
+
74
+ /** `literalAddrOf`: `const` is the bare pool word, `gaddr` is the same word after the symbol map
75
+ * named it, a constant shifted by a constant is the word agbcc builds without a pool (`mov r0,
76
+ * #0x80; lsl r0, #0x13` is 0x04000000), and `add` or `sub` of a constant, on either side, is the
77
+ * base+displacement form an interior attribution or a member access produces — spellings of one
78
+ * address, which is the point: the answer must not turn on which one the assembly happened to use.
79
+ * A runtime index is not a constant, and neither is a pointer loaded from memory, a parameter or a
80
+ * phi.
81
+ *
82
+ * A NAME IS NOT AN ADDRESS. The same symbol name can sit at two addresses — a symbol map is free to
83
+ * carry one — and a `gaddr`'s `sym` can also come straight from the assembly text (`.word
84
+ * REG_DMA3SAD`), where nothing looked it up at all. So names resolve to an address here or they
85
+ * resolve to nothing: a name at more than one address vouches for neither. */
86
+ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap | undefined): LiteralAddresses {
87
+ const defOf = new Map<Value, Op>();
88
+ for (const blk of irBlocks) {
89
+ for (const op of blk.ops) {
90
+ for (const res of op.results) {
91
+ defOf.set(res, op);
92
+ }
93
+ }
94
+ }
95
+ const addrOfName = new Map<string, number | null>();
96
+ for (const [addr, infos] of symbols ?? []) {
97
+ for (const si of infos) {
98
+ addrOfName.set(si.name, addrOfName.has(si.name) ? null : addr);
99
+ }
100
+ }
101
+ const constOf = (v: Value): number | undefined => {
102
+ const d = defOf.get(v);
103
+ return d?.opcode === 'const' ? (d.attrs.value as number) : undefined;
104
+ };
105
+ const literalAddrOf = (v: Value, depth = 0): number | undefined => {
106
+ const d = defOf.get(v);
107
+ if (d === undefined || depth > 8) {
108
+ return undefined;
109
+ }
110
+ if (d.opcode === 'const') {
111
+ return d.attrs.value as number;
112
+ }
113
+ if (d.opcode === 'gaddr') {
114
+ return addrOfName.get(d.attrs.sym as string) ?? undefined;
115
+ }
116
+ if (d.opcode === 'shl') {
117
+ const shifted = constOf(d.operands[0]);
118
+ const by = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : constOf(d.operands[1]);
119
+ return shifted === undefined || by === undefined || by < 0 || by > 31 ? undefined : (shifted << by) >>> 0;
120
+ }
121
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
122
+ const [x, y] = d.operands;
123
+ const cy = constOf(y);
124
+ if (cy !== undefined) {
125
+ const base = literalAddrOf(x, depth + 1);
126
+ return base === undefined ? undefined : d.opcode === 'add' ? base + cy : base - cy;
127
+ }
128
+ const cx = d.opcode === 'add' ? constOf(x) : undefined;
129
+ if (cx !== undefined) {
130
+ const base = literalAddrOf(y, depth + 1);
131
+ return base === undefined ? undefined : base + cx;
132
+ }
133
+ }
134
+ return undefined;
135
+ };
136
+ // `addressRangeOf`: where `literalAddrOf` names one address, this names every address a value
137
+ // may hold (ir/bits.ts `stridedRange`), a placed name being the one leaf only this module reads.
138
+ const rangeCtx: RangeCtx = {
139
+ defs: defOf,
140
+ constOf,
141
+ incoming: incomingArgs(irBlocks),
142
+ entry: new Set(irBlocks[0]?.params ?? []),
143
+ pointOf: (v) => (defOf.get(v)?.opcode === 'gaddr' ? literalAddrOf(v) : undefined),
144
+ };
145
+ return {
146
+ defOf,
147
+ constOf,
148
+ literalAddrOf: (v) => literalAddrOf(v),
149
+ addressRangeOf: (v) => stridedRange(rangeCtx, v),
150
+ };
151
+ }
152
+
153
+ /** Which device stores a policy pins, beside every device read. */
154
+ type StorePin =
155
+ /** every one */
156
+ | 'every'
157
+ /** those a later store in their own block overwrites (`overwritten`) */
158
+ | 'overwritten';
159
+
160
+ /** The model the frame-object audit accepted a function under. */
161
+ export type DevicePinPolicy = 'one-object' | 'per-object';
162
+
163
+ export const DEVICE_PIN_POLICIES: Readonly<Record<DevicePinPolicy, { readonly stores: StorePin }>> = {
164
+ 'one-object': { stores: 'every' },
165
+ 'per-object': { stores: 'overwritten' },
166
+ };
167
+
168
+ /** What the frame-object audit hands this pass: the policy it accepted the function under, and the
169
+ * `readOnlyAddressSinks` registers it saw a frame address stored to. No sink, no pin. */
170
+ export interface DevicePins {
171
+ readonly policy: DevicePinPolicy;
172
+ readonly sinks: readonly number[];
173
+ }
174
+
175
+ /** The stores pinned as `overwritten` are those a later store in their own block overwrites, with
176
+ * no call between to clear flow.c's list of pending stores (flow.c:1962). A plain read of its
177
+ * bytes between does not always keep it: one agbcc forwards the stored value to (cse.c) leaves the
178
+ * store dead, and one of another type does not alias. A read it does not forward — a `char` read,
179
+ * or one it extends — keeps it, and pinning a store agbcc keeps costs a spelling. Two fills through
180
+ * one channel back to back is the shape, and plain, the first transfer is gone. */
181
+ function overwritten(op: Op, blk: Block, at: number, { literalAddrOf }: LiteralAddresses): boolean {
182
+ if (op.opcode !== 'store') {
183
+ return false;
184
+ }
185
+ const addressOf = (x: Op): number | undefined => {
186
+ const lit = literalAddrOf(x.operands[0]);
187
+ return lit === undefined ? undefined : lit + (x.attrs.off as number);
188
+ };
189
+ // Where `x` starts relative to `op`, when both name their bytes the same way.
190
+ const startOf = (x: Op): { from: number; by: number } | undefined => {
191
+ const sameBase = x.operands[0] === op.operands[0];
192
+ const from = sameBase ? (op.attrs.off as number) : addressOf(op);
193
+ const by = sameBase ? (x.attrs.off as number) : addressOf(x);
194
+ return from === undefined || by === undefined ? undefined : { from, by };
195
+ };
196
+ const width = op.attrs.width as number;
197
+ for (const later of blk.ops.slice(at + 1)) {
198
+ if (later.opcode === 'call') {
199
+ return false;
200
+ }
201
+ const s = later.opcode === 'store' ? startOf(later) : undefined;
202
+ if (s !== undefined && s.by <= s.from && s.by + (later.attrs.width as number) >= s.from + width) {
203
+ return true;
204
+ }
205
+ }
206
+ return false;
207
+ }
208
+
209
+ /** Stamp `volatile` on the device accesses `pins.policy` pins — none when `pins.sinks` is empty. */
210
+ export function pinDeviceAccesses(
211
+ irBlocks: Block[],
212
+ { policy, sinks }: DevicePins,
213
+ target: TargetDescription,
214
+ symbols: SymbolMap | undefined,
215
+ ): void {
216
+ if (sinks.length === 0) {
217
+ return;
218
+ }
219
+ const addresses = literalAddresses(irBlocks, symbols);
220
+ const { defOf, constOf, literalAddrOf } = addresses;
221
+ const { stores } = DEVICE_PIN_POLICIES[policy];
222
+ const reach = (target.capabilities.readSourceControl?.offset ?? 2) + 2;
223
+ const window = target.capabilities.deviceRegisters;
224
+ const isDevice = (a: number, w: number): boolean =>
225
+ window !== undefined ? a >= window[0] && a + w <= window[1] : sinks.some((s) => a < s + reach && a + w > s);
226
+ const incoming = new Map<Value, Value[]>();
227
+ for (const blk of irBlocks) {
228
+ for (const op of blk.ops) {
229
+ for (const sx of op.successors ?? []) {
230
+ sx.args.forEach((arg, i) => {
231
+ const param = sx.block.params[i];
232
+ if (param !== undefined) {
233
+ (incoming.get(param) ?? incoming.set(param, []).get(param)!).push(arg);
234
+ }
235
+ });
236
+ }
237
+ }
238
+ }
239
+ // The literal a pointer is a device register plus a runtime index from. `'cycle'` is a phi
240
+ // already on the walk — a pointer stepped around a loop — which contradicts nothing, so a phi
241
+ // is placed by the incoming values that are not its own back edge.
242
+ //
243
+ // Of an `add`, a constant beside a pointer this places — a register, or that phi — is the
244
+ // pointer's displacement, and the pointer places the sum: `d += 3` on a `vu32 *` is the phi plus
245
+ // 12, and 12 is no register. Beside anything else the constant is the register and the other
246
+ // operand its runtime index, `(vu32 *)(0x40000B0 + ch * 12)`.
247
+ const placed = (v: Value, onWalk: Set<Value>, depth = 0): number | 'cycle' | undefined => {
248
+ const lit = literalAddrOf(v);
249
+ if (lit !== undefined || depth > 8) {
250
+ return lit;
251
+ }
252
+ const d = defOf.get(v);
253
+ if (d?.opcode === 'add' && d.operands.length === 2) {
254
+ const [x, y] = d.operands.map((o) => placed(o, onWalk, depth + 1));
255
+ const [cx, cy] = d.operands.map(constOf);
256
+ if (cy !== undefined && x !== undefined) {
257
+ return x;
258
+ }
259
+ if (cx !== undefined && y !== undefined) {
260
+ return y;
261
+ }
262
+ return typeof x === 'number' ? x : typeof y === 'number' ? y : (x ?? y);
263
+ }
264
+ const ins = d === undefined ? incoming.get(v) : undefined;
265
+ if (ins === undefined || onWalk.has(v)) {
266
+ return ins === undefined ? undefined : 'cycle';
267
+ }
268
+ onWalk.add(v);
269
+ const each = ins.map((a) => placed(a, onWalk, depth + 1));
270
+ onWalk.delete(v);
271
+ if (each.some((a) => a === undefined || (typeof a === 'number' && !isDevice(a, 1)))) {
272
+ return undefined;
273
+ }
274
+ return each.find((a) => typeof a === 'number') ?? 'cycle';
275
+ };
276
+ // A read the compiler could not have made of a `volatile` was plain in the source.
277
+ const pins = (op: Op, blk: Block, at: number): boolean =>
278
+ op.opcode === 'load'
279
+ ? readCouldBeVolatile(target.compilerBehaviors, op)
280
+ : stores === 'every' || overwritten(op, blk, at, addresses);
281
+ for (const blk of irBlocks) {
282
+ blk.ops.forEach((op, at) => {
283
+ if (op.opcode !== 'store' && op.opcode !== 'load') {
284
+ return;
285
+ }
286
+ const base = placed(op.operands[0], new Set());
287
+ if (
288
+ typeof base === 'number' &&
289
+ isDevice(base + (op.attrs.off as number), op.attrs.width as number) &&
290
+ pins(op, blk, at)
291
+ ) {
292
+ op.attrs = { ...op.attrs, volatile: true };
293
+ }
294
+ });
295
+ }
296
+ }
@@ -45,13 +45,14 @@ export function mkEmitKit(ops: Op[], write: (reg: string, v: Value) => void): Em
45
45
 
46
46
  /** Emit a recovered jump-table dispatch: N case successors followed by the default (LAST), with
47
47
  * the dense 0..N-1 `cases` list derived here — the single home of the case/successor
48
- * alignment invariant. */
49
- export function pushSwitchBr(ops: Op[], scrut: Value, successors: Successor[]): void {
48
+ * alignment invariant. `exhaustive`: the scrutinee is already known to be in 0..N-1 here (see
49
+ * the opcode's attr). */
50
+ export function pushSwitchBr(ops: Op[], scrut: Value, successors: Successor[], exhaustive = false): void {
50
51
  ops.push(
51
52
  mkOp('switch_br', {
52
53
  operands: [scrut],
53
54
  successors,
54
- attrs: { cases: successors.slice(0, -1).map((_, k) => k) },
55
+ attrs: { cases: successors.slice(0, -1).map((_, k) => k), ...(exhaustive ? { exhaustive: true } : {}) },
55
56
  }),
56
57
  );
57
58
  }
@@ -6,6 +6,11 @@
6
6
  // runtime crash (TypeError/RangeError): this is a catchable, intentional "out of scope" boundary
7
7
  // signal. `PpcUnsupportedError` subclasses it (annotate-mode classification and the loud-error
8
8
  // tests use `instanceof` against this base).
9
+ /** A frontend's own refusal, handed to code it shares with other frontends: it throws the
10
+ * frontend's error class with the message as given, so a refusal the shared code makes classifies
11
+ * as the frontend's own. */
12
+ export type FrontendRefusal = (message: string) => never;
13
+
9
14
  export class FrontendUnsupportedError extends Error {
10
15
  constructor(message: string) {
11
16
  super(message);
@@ -0,0 +1,101 @@
1
+ // The edge rule for condition state: when a block may begin with the compare its predecessor left.
2
+ //
3
+ // Two frontends carry a compare across a block boundary, and their STATE and CLOBBER rules differ
4
+ // (one implicit flags register and `FLAG_SETTING` on Thumb; eight `cr` fields and the record-form
5
+ // `.` suffix on PowerPC, each also cleared by a call). The EDGE rule is the same thing on both, and it is
6
+ // where the soundness lives, so it is written once, here.
7
+
8
+ /** What an ISA tells the edge rule about its blocks. `exit` holds what each block left at its last
9
+ * instruction: the frontend writes it as it fills blocks in order, so a lookup finds only blocks
10
+ * filled EARLIER, and an unlifted predecessor answers "not known" rather than "none". */
11
+ export interface FlagsEdges<S> {
12
+ preds: readonly (readonly number[])[];
13
+ exit: ReadonlyMap<number, S>;
14
+ label: (bi: number) => string;
15
+ /** The block ends in a jump-table dispatch, so its edges are switch edges. */
16
+ dispatches: (bi: number) => boolean;
17
+ /** The block ends in a conditional branch AND this ISA's model does not carry the flags across
18
+ * that branch's edges. */
19
+ refusesConditional: (bi: number) => boolean;
20
+ }
21
+
22
+ /** The condition state a block starts with, inherited from its predecessor, or the sentence saying
23
+ * why it starts with none. One call answers both, so a refusal and its reason cannot drift apart.
24
+ *
25
+ * A block entered from exactly one predecessor begins with exactly the flags that predecessor
26
+ * left. agbcc depends on it: a function long enough to need a mid-function literal pool gets a `b`
27
+ * over the pool between a `cmp` and the branch that reads it, leaving the branch alone under a
28
+ * label. mwcc depends on it across a CONDITIONAL edge: its binary-search switch dispatch is
29
+ * `cmpwi r0,1; beq- case1; bge- default`, one compare read by the `beq-` and then by the `bge-` on
30
+ * the `beq-`'s fall-through, which starts a block of its own.
31
+ *
32
+ * What crosses the edge is the compare's SSA values, never its register names. A lone
33
+ * predecessor dominates, so those values dominate every use on this side, while re-reading `r0`
34
+ * here would pick up whatever this block redefined it to.
35
+ *
36
+ * TRANSITIVE, over as many edges as the chain has: each block runs this and writes its own exit
37
+ * state, so a compare reaches the end of a run of straight-line blocks and refuses at the first
38
+ * one that breaks the chain.
39
+ *
40
+ * NOT THE BLOCK-PARAMETER MACHINERY, though both frontends have SSA with block parameters. Three
41
+ * reasons, and the last is the one a later "improvement" would get wrong:
42
+ * * the flags are not a register anything reads, so there is nothing for `readVar` to look up.
43
+ * The compare is consumed by the terminator, never by a named operand;
44
+ * * the value a phi would carry does not exist yet on the predecessor's side. WHICH comparison
45
+ * it is comes from the branch's mnemonic, at the SUCCESSOR's terminator, while the
46
+ * predecessor is filled first. Flags are not a condition until a branch names one, so at the
47
+ * edge there is nothing yet to phi;
48
+ * * `preds.length === 1` is deliberately STRONGER than dominance. A dominating predecessor's
49
+ * flags can still be overwritten on a longer path that rejoins here, so answering this from
50
+ * the dominator tree would state a condition that holds on one path in.
51
+ *
52
+ * Refuses when either half of that sentence fails:
53
+ * * the block has no predecessor, or more than one. The flags on two paths need not agree, and
54
+ * picking one states a condition the machine does not promise;
55
+ * * the only predecessor has not been filled yet, so this pass has nothing to read;
56
+ * * the edge leaves a jump-table dispatch. The edge into a case is a switch edge, and the bounds
57
+ * guard is not the branch that made it;
58
+ * * the edge leaves a conditional branch the ISA's model does not carry across. A branch writes
59
+ * no flags on either ISA, so this is UNBUILT rather than unsound where it refuses: Thumb
60
+ * refuses it because no ARM row inhabits it, and PowerPC carries it (a `bc` writes no CR
61
+ * field) because the mwcc dispatch above does.
62
+ *
63
+ * It does NOT refuse when no compare survives to the predecessor's last instruction, because that
64
+ * is not this function's gap to report: the predecessor already wrote down what took the flags,
65
+ * and that is what crosses the edge.
66
+ *
67
+ * Not a `Gate` table, on docs/level-tower.md's structural bar rather than on cost: every refusal
68
+ * reads `exit`, which exists only because of the order the frontend fills blocks in, so its input
69
+ * cannot be prepared as a getter at any price. */
70
+ export function inheritFlags<S>(bi: number, edges: FlagsEdges<S>): S | string {
71
+ const here = edges.label(bi);
72
+ const ps = edges.preds[bi];
73
+ if (ps.length !== 1) {
74
+ return ps.length === 0
75
+ ? `no compare reaches '${here}', and it has no predecessor to inherit any from`
76
+ : `no compare crosses the edges into '${here}': ${ps.length} meet there, and the flags need not agree on all of them`;
77
+ }
78
+ const p = ps[0];
79
+ const carried = edges.exit.get(p);
80
+ // Named for the fill order that decides it, not for a loop: this is true of any predecessor not
81
+ // yet lifted, and a CFG with no cycle in it can be laid out so that one is (`f: b .L2` /
82
+ // `.L1: bge` / `.L2: cmp; b .L1`). A reverse-postorder fill is what would close this, which is
83
+ // why the sentence points at the walk.
84
+ if (carried === undefined) {
85
+ return `no compare crosses the edge into '${here}': its only predecessor '${edges.label(p)}' is lifted after it`;
86
+ }
87
+ // Asked BEFORE the conditional-branch arm. On Thumb every dispatch block also ends in a
88
+ // conditional branch (`recoverJumpTable` only recognises a bounds block ending in `bhi`/`bls`),
89
+ // so there this arm buys the truer sentence and not the verdict — `thumb-frontend.test.ts` runs
90
+ // that ablation. On PowerPC the conditional arm carries, so this arm is the one that refuses.
91
+ if (edges.dispatches(p)) {
92
+ return `no compare crosses the edge into '${here}': it leaves the jump-table dispatch in '${edges.label(p)}'`;
93
+ }
94
+ if (edges.refusesConditional(p)) {
95
+ return `no compare crosses the edge into '${here}': it leaves '${edges.label(p)}' through a conditional branch`;
96
+ }
97
+ // Whatever the predecessor left, verbatim. A reason already names the block the chain broke in,
98
+ // so it is as true here as it was there, and a run of ten straight-line blocks reports the one
99
+ // instruction that took the flags rather than the last edge it crossed.
100
+ return carried;
101
+ }