@asmlift/core 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -0,0 +1,277 @@
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 drops it outright, and a poll
37
+ * is hoisted as above. Its other device stores stay plain, their qualified spelling left to
38
+ * `/vol-store`'s candidate (l3/volstore.ts): pinned in the structured tree they are pinned in every
39
+ * variation, and the ones that home the base or un-reduce a loop refuse a qualified base, which
40
+ * costs `synthetic:dmastride` and `synthetic:dmaptrsrc` their matches. That is also why the two
41
+ * policies stay two rows: the one-object row's every-store pin is what keeps `threeFills`' nine
42
+ * qualified stores (thumb-frontend.test.ts), and on the per-object row it costs those two matches.
43
+ *
44
+ * One store left plain this way is a GAP: a device store a pinned read of the same cell re-reads
45
+ * in a loop where nothing else may alias the cell. agbcc's loop pass takes the first access of a
46
+ * cell it meets as the cell's and compares no qualifier (loop.c:8767, rtx_equal_p at
47
+ * rtlanal.c:1066), so plain, the store moves the cell into a register and the read is served the
48
+ * stored value. The variations that home the channel's base name the store and the read through
49
+ * different addresses, which are two cells to that pass, and those are the ones a pin here would
50
+ * refuse.
51
+ *
52
+ * Under either policy, a read the compiler could not have made of a `volatile` stays plain
53
+ * (target.ts `readCouldBeVolatile`): on a compiler that sign-extends a qualified narrow read in a
54
+ * register, a sign-extending load is a plain read in the source, and qualified it recompiles to
55
+ * another instruction sequence. */
56
+ import type { Block, Op, Value } from '../ir/core';
57
+ import type { SymbolMap } from '../symbols';
58
+ import { type TargetDescription, readCouldBeVolatile } from '../target';
59
+
60
+ /** The literal addresses a function's values denote, over its IR as it stands when asked. */
61
+ export interface LiteralAddresses {
62
+ readonly defOf: ReadonlyMap<Value, Op>;
63
+ /** the value of a `const`, else undefined */
64
+ constOf(v: Value): number | undefined;
65
+ /** the literal address `v` denotes, or undefined when this cannot say */
66
+ literalAddrOf(v: Value): number | undefined;
67
+ }
68
+
69
+ /** `literalAddrOf`: `const` is the bare pool word, `gaddr` is the same word after the symbol map
70
+ * named it, a constant shifted by a constant is the word agbcc builds without a pool (`mov r0,
71
+ * #0x80; lsl r0, #0x13` is 0x04000000), and `add` or `sub` of a constant, on either side, is the
72
+ * base+displacement form an interior attribution or a member access produces — spellings of one
73
+ * address, which is the point: the answer must not turn on which one the assembly happened to use.
74
+ * A runtime index is not a constant, and neither is a pointer loaded from memory, a parameter or a
75
+ * phi.
76
+ *
77
+ * A NAME IS NOT AN ADDRESS. The same symbol name can sit at two addresses — a symbol map is free to
78
+ * carry one — and a `gaddr`'s `sym` can also come straight from the assembly text (`.word
79
+ * REG_DMA3SAD`), where nothing looked it up at all. So names resolve to an address here or they
80
+ * resolve to nothing: a name at more than one address vouches for neither. */
81
+ export function literalAddresses(irBlocks: readonly Block[], symbols: SymbolMap | undefined): LiteralAddresses {
82
+ const defOf = new Map<Value, Op>();
83
+ for (const blk of irBlocks) {
84
+ for (const op of blk.ops) {
85
+ for (const res of op.results) {
86
+ defOf.set(res, op);
87
+ }
88
+ }
89
+ }
90
+ const addrOfName = new Map<string, number | null>();
91
+ for (const [addr, infos] of symbols ?? []) {
92
+ for (const si of infos) {
93
+ addrOfName.set(si.name, addrOfName.has(si.name) ? null : addr);
94
+ }
95
+ }
96
+ const constOf = (v: Value): number | undefined => {
97
+ const d = defOf.get(v);
98
+ return d?.opcode === 'const' ? (d.attrs.value as number) : undefined;
99
+ };
100
+ const literalAddrOf = (v: Value, depth = 0): number | undefined => {
101
+ const d = defOf.get(v);
102
+ if (d === undefined || depth > 8) {
103
+ return undefined;
104
+ }
105
+ if (d.opcode === 'const') {
106
+ return d.attrs.value as number;
107
+ }
108
+ if (d.opcode === 'gaddr') {
109
+ return addrOfName.get(d.attrs.sym as string) ?? undefined;
110
+ }
111
+ if (d.opcode === 'shl') {
112
+ const shifted = constOf(d.operands[0]);
113
+ const by = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : constOf(d.operands[1]);
114
+ return shifted === undefined || by === undefined || by < 0 || by > 31 ? undefined : (shifted << by) >>> 0;
115
+ }
116
+ if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
117
+ const [x, y] = d.operands;
118
+ const cy = constOf(y);
119
+ if (cy !== undefined) {
120
+ const base = literalAddrOf(x, depth + 1);
121
+ return base === undefined ? undefined : d.opcode === 'add' ? base + cy : base - cy;
122
+ }
123
+ const cx = d.opcode === 'add' ? constOf(x) : undefined;
124
+ if (cx !== undefined) {
125
+ const base = literalAddrOf(y, depth + 1);
126
+ return base === undefined ? undefined : base + cx;
127
+ }
128
+ }
129
+ return undefined;
130
+ };
131
+ return { defOf, constOf, literalAddrOf: (v) => literalAddrOf(v) };
132
+ }
133
+
134
+ /** Which device stores a policy pins, beside every device read. */
135
+ type StorePin =
136
+ /** every one */
137
+ | 'every'
138
+ /** those a later store in their own block overwrites (`overwritten`) */
139
+ | 'overwritten';
140
+
141
+ /** The model the frame-object audit accepted a function under. */
142
+ export type DevicePinPolicy = 'one-object' | 'per-object';
143
+
144
+ export const DEVICE_PIN_POLICIES: Readonly<Record<DevicePinPolicy, { readonly stores: StorePin }>> = {
145
+ 'one-object': { stores: 'every' },
146
+ 'per-object': { stores: 'overwritten' },
147
+ };
148
+
149
+ /** What the frame-object audit hands this pass: the policy it accepted the function under, and the
150
+ * `readOnlyAddressSinks` registers it saw a frame address stored to. No sink, no pin. */
151
+ export interface DevicePins {
152
+ readonly policy: DevicePinPolicy;
153
+ readonly sinks: readonly number[];
154
+ }
155
+
156
+ /** The stores pinned as `overwritten` are those a later store in their own block overwrites, with
157
+ * no call between to clear flow.c's list of pending stores (flow.c:1962). A plain read of its
158
+ * bytes between does not always keep it: one agbcc forwards the stored value to (cse.c) leaves the
159
+ * store dead, and one of another type does not alias. A read it does not forward — a `char` read,
160
+ * or one it extends — keeps it, and pinning a store agbcc keeps costs a spelling. Two fills through
161
+ * one channel back to back is the shape, and plain, the first transfer is gone. */
162
+ function overwritten(op: Op, blk: Block, at: number, { literalAddrOf }: LiteralAddresses): boolean {
163
+ if (op.opcode !== 'store') {
164
+ return false;
165
+ }
166
+ const addressOf = (x: Op): number | undefined => {
167
+ const lit = literalAddrOf(x.operands[0]);
168
+ return lit === undefined ? undefined : lit + (x.attrs.off as number);
169
+ };
170
+ // Where `x` starts relative to `op`, when both name their bytes the same way.
171
+ const startOf = (x: Op): { from: number; by: number } | undefined => {
172
+ const sameBase = x.operands[0] === op.operands[0];
173
+ const from = sameBase ? (op.attrs.off as number) : addressOf(op);
174
+ const by = sameBase ? (x.attrs.off as number) : addressOf(x);
175
+ return from === undefined || by === undefined ? undefined : { from, by };
176
+ };
177
+ const width = op.attrs.width as number;
178
+ for (const later of blk.ops.slice(at + 1)) {
179
+ if (later.opcode === 'call') {
180
+ return false;
181
+ }
182
+ const s = later.opcode === 'store' ? startOf(later) : undefined;
183
+ if (s !== undefined && s.by <= s.from && s.by + (later.attrs.width as number) >= s.from + width) {
184
+ return true;
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+
190
+ /** Stamp `volatile` on the device accesses `pins.policy` pins — none when `pins.sinks` is empty. */
191
+ export function pinDeviceAccesses(
192
+ irBlocks: Block[],
193
+ { policy, sinks }: DevicePins,
194
+ target: TargetDescription,
195
+ symbols: SymbolMap | undefined,
196
+ ): void {
197
+ if (sinks.length === 0) {
198
+ return;
199
+ }
200
+ const addresses = literalAddresses(irBlocks, symbols);
201
+ const { defOf, constOf, literalAddrOf } = addresses;
202
+ const { stores } = DEVICE_PIN_POLICIES[policy];
203
+ const reach = (target.capabilities.readSourceControl?.offset ?? 2) + 2;
204
+ const window = target.capabilities.deviceRegisters;
205
+ const isDevice = (a: number, w: number): boolean =>
206
+ window !== undefined ? a >= window[0] && a + w <= window[1] : sinks.some((s) => a < s + reach && a + w > s);
207
+ const incoming = new Map<Value, Value[]>();
208
+ for (const blk of irBlocks) {
209
+ for (const op of blk.ops) {
210
+ for (const sx of op.successors ?? []) {
211
+ sx.args.forEach((arg, i) => {
212
+ const param = sx.block.params[i];
213
+ if (param !== undefined) {
214
+ (incoming.get(param) ?? incoming.set(param, []).get(param)!).push(arg);
215
+ }
216
+ });
217
+ }
218
+ }
219
+ }
220
+ // The literal a pointer is a device register plus a runtime index from. `'cycle'` is a phi
221
+ // already on the walk — a pointer stepped around a loop — which contradicts nothing, so a phi
222
+ // is placed by the incoming values that are not its own back edge.
223
+ //
224
+ // Of an `add`, a constant beside a pointer this places — a register, or that phi — is the
225
+ // pointer's displacement, and the pointer places the sum: `d += 3` on a `vu32 *` is the phi plus
226
+ // 12, and 12 is no register. Beside anything else the constant is the register and the other
227
+ // operand its runtime index, `(vu32 *)(0x40000B0 + ch * 12)`.
228
+ const placed = (v: Value, onWalk: Set<Value>, depth = 0): number | 'cycle' | undefined => {
229
+ const lit = literalAddrOf(v);
230
+ if (lit !== undefined || depth > 8) {
231
+ return lit;
232
+ }
233
+ const d = defOf.get(v);
234
+ if (d?.opcode === 'add' && d.operands.length === 2) {
235
+ const [x, y] = d.operands.map((o) => placed(o, onWalk, depth + 1));
236
+ const [cx, cy] = d.operands.map(constOf);
237
+ if (cy !== undefined && x !== undefined) {
238
+ return x;
239
+ }
240
+ if (cx !== undefined && y !== undefined) {
241
+ return y;
242
+ }
243
+ return typeof x === 'number' ? x : typeof y === 'number' ? y : (x ?? y);
244
+ }
245
+ const ins = d === undefined ? incoming.get(v) : undefined;
246
+ if (ins === undefined || onWalk.has(v)) {
247
+ return ins === undefined ? undefined : 'cycle';
248
+ }
249
+ onWalk.add(v);
250
+ const each = ins.map((a) => placed(a, onWalk, depth + 1));
251
+ onWalk.delete(v);
252
+ if (each.some((a) => a === undefined || (typeof a === 'number' && !isDevice(a, 1)))) {
253
+ return undefined;
254
+ }
255
+ return each.find((a) => typeof a === 'number') ?? 'cycle';
256
+ };
257
+ // A read the compiler could not have made of a `volatile` was plain in the source.
258
+ const pins = (op: Op, blk: Block, at: number): boolean =>
259
+ op.opcode === 'load'
260
+ ? readCouldBeVolatile(target.compilerBehaviors, op)
261
+ : stores === 'every' || overwritten(op, blk, at, addresses);
262
+ for (const blk of irBlocks) {
263
+ blk.ops.forEach((op, at) => {
264
+ if (op.opcode !== 'store' && op.opcode !== 'load') {
265
+ return;
266
+ }
267
+ const base = placed(op.operands[0], new Set());
268
+ if (
269
+ typeof base === 'number' &&
270
+ isDevice(base + (op.attrs.off as number), op.attrs.width as number) &&
271
+ pins(op, blk, at)
272
+ ) {
273
+ op.attrs = { ...op.attrs, volatile: true };
274
+ }
275
+ });
276
+ }
277
+ }
@@ -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
+ }
@@ -0,0 +1,167 @@
1
+ // asmlift — the argument ABI at a function's boundary, both register files, read once for the two
2
+ // frontends that lift hardware floating point (frontend/mips.ts, frontend/ppc.ts). The homes are
3
+ // target data (`TargetDescription.fpu`); this is where a lift is held to them.
4
+ //
5
+ // WHY A FRONTEND NEEDS THIS AT ALL, and why decoding the arithmetic alone is not a smaller version
6
+ // of it: a register of the FPU's file read before any write mints an entry parameter like any other
7
+ // key (frontend/ssa.ts), and a value left in the float return register is not the integer one. With
8
+ // the decode and neither of these, `float fadd(float a, float b){ return a + b; }` lifts on every
9
+ // FPU target as `void fadd(s32 a0, s32 a1) { return; }` — two phantom integer parameters and a dead
10
+ // add. `docs/floating-point.md` §2 measures that.
11
+ import { type IrType, T } from '../ir/types';
12
+ import type { TargetDescription } from '../target';
13
+ import { FrontendUnsupportedError } from './errors';
14
+ import type { ArgSlots } from './ssa';
15
+
16
+ export type Fpu = NonNullable<TargetDescription['fpu']>;
17
+
18
+ /** The one float type a function's FPU registers hold: double where it decodes a double op, single
19
+ * where it decodes a single one, of no stated precision (`T.fUnstated`) where every op it decodes
20
+ * carries none, and null where it decodes both. `widthOf` is the frontend's reading of a mnemonic
21
+ * (null for one that carries no precision, undefined for one it does not decode). */
22
+ export function fpPrecision(
23
+ instrs: readonly { mnemonic: string }[],
24
+ widthOf: (mnemonic: string) => 32 | 64 | null | undefined,
25
+ ): IrType | null {
26
+ const widths = new Set(instrs.map((ins) => widthOf(ins.mnemonic)).filter((w) => w !== null && w !== undefined));
27
+ return widths.size > 1 ? null : widths.has(64) ? T.f64() : widths.has(32) ? T.f32() : T.fUnstated();
28
+ }
29
+
30
+ /** Refuse a function computing in both precisions, where the frontend DECODES a float instruction of
31
+ * it, so a refusal the stream reaches first keeps its own reason. The SSA builder types a register
32
+ * per key, not per value, and the rounding between the precisions (a conversion, PowerPC's `frsp`,
33
+ * or a single op over a double operand) is not modelled. It throws here, so its throw site is the
34
+ * one `declines.test.ts` harvests. */
35
+ export function refuseBothPrecisions(name: string, mnemonic: string): never {
36
+ throw new FrontendUnsupportedError(
37
+ `cannot lift '${name}': '${mnemonic}' is in a function that computes in ` +
38
+ 'both single and double precision, and the rounding between them is not modelled',
39
+ );
40
+ }
41
+
42
+ /** THE ARGUMENT SLOTS OF BOTH REGISTER FILES, as the one `ArgSlots` the MIPS and PowerPC frontends
43
+ * hand to `mintArgSlotHoles` and `abiSortEntryParams` (frontend/ssa.ts). Naming is positional
44
+ * (`a0`, `a1`, … in sort order), so the ORDER `slotOf` gives and the HOLES `holes` fills are two
45
+ * uses of one reading — kept in one place because under `'leading'` the files share a slot
46
+ * sequence, and a hole filled from the wrong file is a signature that compiles and binds its
47
+ * caller's arguments one slot off.
48
+ *
49
+ * `slotOf` is the slot, with one exception: under `'separate'` a float argument ranks after every
50
+ * integer one. THAT ORDER IS A SPELLING, NOT A READING. Under the PowerPC EABI the two files count
51
+ * independently, so `float g(int *p, float b)` and `float g(float b, int *p)` compile to one object
52
+ * and nothing in it says which the source wrote. Either spelling reproduces the bytes in a unit
53
+ * that does not declare the function; this one is fixed so the output is deterministic. KNOWN GAP:
54
+ * a unit that does declare it — a project context — decides the order, and only the C++ backend
55
+ * reads that declaration (`bindSpecParams`). In C the int-first spelling of
56
+ * `float mix(float a, int n);` is a redeclaration mwcc refuses: a noncompile, not a wrong program.
57
+ * Ranking by the function's own declared parameters here would close it, once `proto.ts` can
58
+ * size a float. A key in neither file is no slot (null), and sorts after every argument.
59
+ *
60
+ * `holes` applies FOUR RULES to the keys the entry reads, each a refusal or a parameter the ABI
61
+ * proves:
62
+ * 1. A register of the FPU file that arrives at the entry is a float ARGUMENT, or the lift is
63
+ * refused. Nothing else in that file carries a caller's value: a read of `$f4` before any write
64
+ * is uninitialised storage or code this frontend has not followed, and minting a parameter for
65
+ * it would fabricate an argument.
66
+ * 2. Every slot below the highest one read is a parameter too, read or not. Arguments take their
67
+ * registers in order, so `r5` is the third argument whatever became of the first two, and a
68
+ * signature holding only the registers read would bind every later argument one slot low.
69
+ * 3. …and it is minted from the file the ABI puts that slot in. Under `'separate'` each file fills
70
+ * its own holes. Under `'leading'` (MIPS o32) a float argument is in `argRegs[k]` only while
71
+ * arguments 0..k are all floating, so a hole below the highest float read is a float, and one
72
+ * above it an integer — `float f(float a, float b, int c, int d){ return d ? a : b; }` reads
73
+ * `$f12`, `$f14` and `a3`, and its third argument is `a2`. Which file a hole comes from thus
74
+ * depends on what the function reads, which is why `ArgSlots` asks for the holes of a read
75
+ * set rather than for the key of a slot: `int ib(int a, int b){ return b; }` reads only `a1`,
76
+ * and its first argument is `a0`, not `$f12`.
77
+ * 4. Under `'leading'` a float argument still takes the INTEGER slots it shadows — one for a
78
+ * single, two for a double (`floatType`) — so an integer argument register read in one of those
79
+ * slots is a contradiction the ABI does not produce, and is refused rather than ranked. */
80
+ export function fpuArgSlots(
81
+ name: string,
82
+ fpu: Fpu | undefined,
83
+ argRegs: readonly string[],
84
+ isFpKey: (key: string) => boolean,
85
+ floatType: IrType | null = T.f32(),
86
+ ): ArgSlots {
87
+ // How many integer slots a float argument takes under `'leading'`: IDO reads the integer after a
88
+ // double two slots on (`double di(double a, int b)` reads b from a2).
89
+ const words = floatType?.kind === 'float' && floatType.width === 64 ? 2 : 1;
90
+ const slotOf = (key: string): { slot: number; float: boolean } | null => {
91
+ const gpr = argRegs.indexOf(key);
92
+ if (gpr >= 0) {
93
+ return { slot: gpr, float: false };
94
+ }
95
+ const fp = fpu?.argRegs.indexOf(key) ?? -1;
96
+ return fp >= 0 ? { slot: fpu?.slots === 'leading' ? fp * words : fp, float: true } : null;
97
+ };
98
+ return {
99
+ slotOf: (key) => {
100
+ const s = slotOf(key);
101
+ return s === null ? null : s.float && fpu?.slots === 'separate' ? argRegs.length + s.slot : s.slot;
102
+ },
103
+ holes: (readKeys) => {
104
+ for (const k of readKeys.filter(isFpKey)) {
105
+ if (!fpu?.argRegs.includes(k)) {
106
+ throw new FrontendUnsupportedError(
107
+ `cannot lift '${name}': ${k} is read before this function writes it, and no floating-point argument ` +
108
+ `arrives there — not modelled`,
109
+ );
110
+ }
111
+ }
112
+ const read = readKeys.map(slotOf).filter((s) => s !== null);
113
+ const top = (float: boolean) => Math.max(-1, ...read.filter((s) => s.float === float).map((s) => s.slot));
114
+ const floatTop = top(true);
115
+ const intTop = top(false);
116
+ if (fpu?.slots === 'leading') {
117
+ // The last integer slot the floats read shadow; a double shadows two.
118
+ const floatEnd = floatTop < 0 ? -1 : floatTop + words - 1;
119
+ const shadowed = read.find((s) => !s.float && s.slot <= floatEnd);
120
+ if (shadowed) {
121
+ const k = shadowed.slot;
122
+ throw new FrontendUnsupportedError(
123
+ `cannot lift '${name}': ${argRegs[k]} and ${fpu.argRegs[Math.floor(k / words)]} both carry argument ` +
124
+ `slot ${k} — a floating-point argument takes the integer slots it shadows, so the ABI does not ` +
125
+ 'produce this — not modelled',
126
+ );
127
+ }
128
+ return [
129
+ ...fpu.argRegs.slice(0, Math.max(0, floatTop / words)),
130
+ ...argRegs.slice(floatEnd + 1, Math.max(floatEnd + 1, intTop)),
131
+ ];
132
+ }
133
+ return [...(fpu?.argRegs.slice(0, Math.max(0, floatTop)) ?? []), ...argRegs.slice(0, Math.max(0, intTop))];
134
+ },
135
+ };
136
+ }
137
+
138
+ /** Whether a function RETURNS a float: some instruction the frontend decodes writes the float return
139
+ * register, in a block the entry REACHES. Decided over the whole function, before any `ret` is
140
+ * emitted, because a function has one return type on every path — a path that leaves the register
141
+ * untouched returns the float argument that arrived there (PowerPC's `f1` is both), or refuses by
142
+ * `fpuArgSlots`' rule 1. Reachable blocks only: `addi r3,r3,1; blr; fmr f1,f2` returns `r3`, and
143
+ * the `fmr` past the `blr` is on no path of it.
144
+ *
145
+ * And then the INTEGER return register is scratch, even where the function writes it. Both halves
146
+ * rest on one fact: the arithmetic is ALL the decode admits, so a float reaches nothing but another
147
+ * float op or the return — every move to memory, to the integer file or through a compare or a
148
+ * conversion is refused. A write to the float return register that did not feed the return would
149
+ * therefore be dead in the source, and the compiler does not emit dead arithmetic; an integer
150
+ * result, meanwhile, could only have come from a float by a conversion, which refuses. IDO's
151
+ * unrolled `float pw(float a, int n)` is the witness for the second half: it counts in `v0`.
152
+ *
153
+ * THAT FACT IS WHAT HOLDS THIS RULE UP, and the next layer removes it: a layer that lets a float
154
+ * reach memory, the integer file or a compare must first decide the return from the value that
155
+ * reaches each `ret`, not from a mnemonic scan (`fpu-lift.test.ts` pins the function that breaks
156
+ * it, `docs/floating-point.md` §6). */
157
+ export function writesFloatReturn(
158
+ instrs: readonly { mnemonic: string; ops: string[] }[],
159
+ decodes: ReadonlySet<string>,
160
+ destKey: (tok: string) => string | null,
161
+ fpu: Fpu | undefined,
162
+ ): boolean {
163
+ return (
164
+ fpu !== undefined &&
165
+ instrs.some((ins) => decodes.has(ins.mnemonic) && ins.ops[0] !== undefined && destKey(ins.ops[0]) === fpu.returnReg)
166
+ );
167
+ }