@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.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- 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
|
+
}
|
package/src/frontend/errors.ts
CHANGED
|
@@ -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
|
+
}
|