@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.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- 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 +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// asmlift — what the jump-table recognizers share: the MIPS and PowerPC idiom matchers below, and,
|
|
2
|
+
// for all three frontends, what an elided dispatch leaves in its registers (at the end of this file).
|
|
3
|
+
//
|
|
4
|
+
// Both frontends recover a dense switch from its dispatch idiom BEFORE the CFG is built, because
|
|
5
|
+
// the case count decides the block leaders: the bounds branch becomes a `switch_br`, and the
|
|
6
|
+
// dispatch block it guards is pruned unread. Two facts about that pruning are ISA-neutral, and live
|
|
7
|
+
// here so a fix to one reaches both:
|
|
8
|
+
//
|
|
9
|
+
// - THE DISPATCH BLOCK HAS ONE ENTRY. A branch landing inside it skips the bounds check, and the
|
|
10
|
+
// block it enters is the one pruned (`entersBetween`). Nor may one land between the bounds
|
|
11
|
+
// compare and the branch that reads it: that path brings its own compare, and the `switch_br`
|
|
12
|
+
// would bound it by one it never ran (PowerPC's `boundsCompare`; MIPS's `mipsBounds` reads each
|
|
13
|
+
// edge into the block instead, because IDO puts the `sltiu` in their delay slots). The pruning is
|
|
14
|
+
// then CHECKED on the built CFG, not assumed (`refuseReachableDispatch`): a table's case is an
|
|
15
|
+
// edge too, and no table existed while the recognizer looked, so a case landing in its own or
|
|
16
|
+
// another table's dispatch block keeps that block reachable, and its indirect jump would lift as
|
|
17
|
+
// whatever the frontend reads it as (MIPS reads a non-`ra` `jr` as a return).
|
|
18
|
+
// - A TABLE BASE FORMED OUTSIDE THAT BLOCK IS NOT PRUNED WITH IT. KMC and gcc 2.7.2 hoist the
|
|
19
|
+
// `lui ; addiu` pair into a loop's preheader, mwcc 2.3.3 hoists `lis ; addi` to the entry, and a
|
|
20
|
+
// register allocator puts the base wherever it likes — a temporary in a leaf, a callee-saved
|
|
21
|
+
// register across a call, an argument register once its argument is dead. The recognizer can only
|
|
22
|
+
// GUESS which pair that is, because it reads instructions, not dataflow. So the pair is lifted
|
|
23
|
+
// like any address — its high half paired with its low half through SSA — and what the low half
|
|
24
|
+
// completes is not a value but a TABLE BASE (`TableBases`): a definition no C expression spells,
|
|
25
|
+
// refused wherever it is read and wherever it reaches a merge, as a pending high half is
|
|
26
|
+
// (frontend/high-half.ts). The dispatch's one claim on it is a PROOF answered by SSA once every
|
|
27
|
+
// block is filled: every path into the bounds block brings that very base in the register the
|
|
28
|
+
// dispatch reads, and no call destroyed it on the way (`SsaBuilder.everyReachingDef`). Keyed by
|
|
29
|
+
// value, not by register — the same reason high-half.ts gives — so a second pair, a rewrite, a
|
|
30
|
+
// path around the pair, a call that clobbers it and a callee-saved restore all answer "no" here,
|
|
31
|
+
// by the SSA's own bookkeeping rather than by a count of the register's mentions.
|
|
32
|
+
import { type Block, type Op, type Value, mkOp, mkValue } from '../ir/core';
|
|
33
|
+
import { T } from '../ir/types';
|
|
34
|
+
import { type FnProto, declaresVoidReturn } from '../proto';
|
|
35
|
+
import type { CallDeclarations } from './call-plan';
|
|
36
|
+
import type { FrontendRefusal } from './errors';
|
|
37
|
+
import type { SsaBuilder } from './ssa';
|
|
38
|
+
|
|
39
|
+
/** Whether any branch lands after the word at `afterAddr`, up to and including the word at
|
|
40
|
+
* `lastAddr`. Asked of a dispatch block (after its bounds branch, up to its indirect jump), where
|
|
41
|
+
* such a path skips the bounds check and enters the block a recovered table prunes, and of the
|
|
42
|
+
* words between a bounds compare and its branch, where it brings a compare of its own. */
|
|
43
|
+
export const entersBetween = (
|
|
44
|
+
instrs: ReadonlyArray<{ target?: number }>,
|
|
45
|
+
afterAddr: number,
|
|
46
|
+
lastAddr: number,
|
|
47
|
+
): boolean => instrs.some((x) => x.target !== undefined && x.target > afterAddr && x.target <= lastAddr);
|
|
48
|
+
|
|
49
|
+
/** Refuse a function whose CFG, pruned to what the entry reaches, still holds a block ending in a
|
|
50
|
+
* recovered dispatch's indirect jump. Recovery replaces that jump with the bounds block's
|
|
51
|
+
* `switch_br` and lifts nothing of the block it ends, so the block must be unreachable; a branch
|
|
52
|
+
* or a table case into it says otherwise. `blocks` are the reachable blocks, `recovered` the
|
|
53
|
+
* addresses of the recovered indirect jumps. */
|
|
54
|
+
export function refuseReachableDispatch(
|
|
55
|
+
name: string,
|
|
56
|
+
blocks: ReadonlyArray<{ branch: { addr: number; mnemonic: string } | null }>,
|
|
57
|
+
recovered: ReadonlySet<number>,
|
|
58
|
+
fail: FrontendRefusal,
|
|
59
|
+
): void {
|
|
60
|
+
for (const { branch } of blocks) {
|
|
61
|
+
if (branch !== null && recovered.has(branch.addr)) {
|
|
62
|
+
fail(
|
|
63
|
+
`cannot lift '${name}': the recovered jump table's '${branch.mnemonic}' at 0x${branch.addr.toString(16)} ` +
|
|
64
|
+
`is still reachable — a branch or a table case enters its dispatch block past the bounds check`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Where a jump table is: the symbol a base's relocations name, and the byte offset into it. */
|
|
71
|
+
export interface TableKey {
|
|
72
|
+
sym: string;
|
|
73
|
+
addend: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sameKey = (a: TableKey, b: TableKey) => a.sym === b.sym && a.addend === b.addend;
|
|
77
|
+
|
|
78
|
+
/** The one table base a function's candidate pairs agree on, or null. The recognizer's GUESS for a
|
|
79
|
+
* base formed outside the dispatch block: several distinct candidates leave it undecided, and the
|
|
80
|
+
* lift's proof (`TableBases.prove`) is what makes the guess safe to act on. */
|
|
81
|
+
export const agreedTable = (candidates: readonly TableKey[]): TableKey | null =>
|
|
82
|
+
candidates.length > 0 && candidates.every((c) => sameKey(c, candidates[0])) ? candidates[0] : null;
|
|
83
|
+
|
|
84
|
+
/** A recovered dispatch whose table base its own block does not form. */
|
|
85
|
+
export interface HoistedDispatch {
|
|
86
|
+
/** the register the dispatch reads the base from */
|
|
87
|
+
reg: string;
|
|
88
|
+
table: TableKey;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TableBases {
|
|
92
|
+
/** Whether the low half completing `key` makes a table base: some recovered dispatch reads that
|
|
93
|
+
* table through a hoisted base. Every other pair is an ordinary address. */
|
|
94
|
+
wants(key: TableKey): boolean;
|
|
95
|
+
/** Record `v` as the address of the table `key`, completed by the instruction at `addr`. */
|
|
96
|
+
record(v: Value, key: TableKey, addr: number): void;
|
|
97
|
+
/** True when `v` is a table base — for a caller that must EXCLUDE one (a guessed call's argument
|
|
98
|
+
* count must not take a base parked in an argument register for an argument). */
|
|
99
|
+
has(v: Value): boolean;
|
|
100
|
+
/** Reading a register AS A VALUE. A table base is not one, so this refuses. */
|
|
101
|
+
guardRead(name: string, reg: string, v: Value): Value;
|
|
102
|
+
/** The dispatch whose `switch_br` block `b` emits reads `d.reg` as the base of `d.table` at the end
|
|
103
|
+
* of that block. Checked by `prove`. */
|
|
104
|
+
demand(d: HoistedDispatch, b: number, jumpAddr: number): void;
|
|
105
|
+
/** Every demand holds: each path into its block brings the very table base it reads, undestroyed.
|
|
106
|
+
* Asked once every block is filled. */
|
|
107
|
+
prove(name: string, ssa: Pick<SsaBuilder, 'everyReachingDef'>): void;
|
|
108
|
+
/** The last line of defence, as for the high halves: a base reaching an operand, a block
|
|
109
|
+
* parameter or a successor argument has been read as a value through a join, which no
|
|
110
|
+
* `guardRead` saw. Checked on the finished function. */
|
|
111
|
+
assertNoneEscaped(name: string, blocks: Block[]): void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function makeTableBases(hoisted: Iterable<HoistedDispatch>, fail: FrontendRefusal): TableBases {
|
|
115
|
+
const wanted = [...hoisted].map((d) => d.table);
|
|
116
|
+
const bases = new Map<Value, TableKey & { addr: number }>();
|
|
117
|
+
const demands: Array<{ d: HoistedDispatch; b: number; jumpAddr: number }> = [];
|
|
118
|
+
const hex = (a: number) => `0x${a.toString(16)}`;
|
|
119
|
+
const refuse = (name: string, v: Value, how: string): never => {
|
|
120
|
+
const t = bases.get(v)!;
|
|
121
|
+
return fail(
|
|
122
|
+
`cannot lift '${name}': the jump table '${t.sym}'+${t.addend}'s base, formed at ${hex(t.addr)}, ${how} — ` +
|
|
123
|
+
`a table's address is the dispatch's alone, and no C expression spells it`,
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
wants: (key) => wanted.some((w) => sameKey(w, key)),
|
|
128
|
+
record: (v, key, addr) => void bases.set(v, { ...key, addr }),
|
|
129
|
+
has: (v) => bases.has(v),
|
|
130
|
+
guardRead(name, reg, v) {
|
|
131
|
+
return bases.has(v) ? refuse(name, v, `is read from ${reg} as a value`) : v;
|
|
132
|
+
},
|
|
133
|
+
demand: (d, b, jumpAddr) => void demands.push({ d, b, jumpAddr }),
|
|
134
|
+
prove(name, ssa) {
|
|
135
|
+
for (const { d, b, jumpAddr } of demands) {
|
|
136
|
+
const isBase = (v: Value) => {
|
|
137
|
+
const t = bases.get(v);
|
|
138
|
+
return t !== undefined && sameKey(t, d.table);
|
|
139
|
+
};
|
|
140
|
+
if (!ssa.everyReachingDef(d.reg, b, isBase)) {
|
|
141
|
+
fail(
|
|
142
|
+
`cannot lift '${name}': the dispatch at ${hex(jumpAddr)} reads the base of jump table '${d.table.sym}'` +
|
|
143
|
+
`+${d.table.addend} from ${d.reg}, and ${d.reg} does not hold that table's address on every path to ` +
|
|
144
|
+
`it — a path around the base, a rewrite, or a call that destroys it`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
assertNoneEscaped(name, blocks) {
|
|
150
|
+
for (const b of blocks) {
|
|
151
|
+
for (const v of [
|
|
152
|
+
...b.params,
|
|
153
|
+
...b.ops.flatMap((op) => [...op.operands, ...op.successors.flatMap((s) => s.args)]),
|
|
154
|
+
]) {
|
|
155
|
+
if (bases.has(v)) {
|
|
156
|
+
refuse(name, v, 'reaches a use through a merge');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// WHAT AN ELIDED DISPATCH LEAVES BEHIND. Every recognizer prunes the dispatch block unread, so the
|
|
165
|
+
// registers its idiom writes — the scaled index, the table's base, the entry's address, the loaded
|
|
166
|
+
// code address, PowerPC's `ctr` — are written on no path the lift models. The default path never
|
|
167
|
+
// ran the idiom, so nothing may be written at the bounds block; the case edges did, so the case
|
|
168
|
+
// targets are where it is written: at the ENTRY of each, before its first instruction. A register
|
|
169
|
+
// holds what the idiom's LAST write of it left:
|
|
170
|
+
// - the index shift (`lsl`/`sll`/`slwi rI,rS,2`) leaves `rS << 2`, a value C spells. mwcc 2.4.2
|
|
171
|
+
// at -O4 reuses it in the case bodies (`p[i] = 7` in a switch on `i` is `stwx r0,r4,r5` with r5
|
|
172
|
+
// the dispatch's own `slwi`), so it is modelled, not refused. The shift reads the scrutinee as
|
|
173
|
+
// the switch does: every recognizer refuses an idiom that rewrites it before the shift, and the
|
|
174
|
+
// switch reads it where the bounds test did, so the scrutinee at a case's entry is the one the
|
|
175
|
+
// shift read — on a target only the table's case edges enter (EXACT);
|
|
176
|
+
// - every other write leaves an address no C expression spells, so it is a MARKER: an `undef`
|
|
177
|
+
// value that `refuseStaleIdiomReads` declines wherever an operand demands it.
|
|
178
|
+
// A target some other edge also enters (the default, a hole naming the default, a loop's back
|
|
179
|
+
// edge, a fallthrough from another case: MIXED) cannot tell the paths apart at its entry, so its
|
|
180
|
+
// markers stand for what the other paths brought too, and a read of one declines. Its scaled index
|
|
181
|
+
// may stand: mwcc 2.4.2 at -O4 keeps it live into such targets — a do-while case re-entered on its
|
|
182
|
+
// back edge (`lwzx r0,r4,r3` with r3 the dispatch's `slwi r3,r3,2`), a case falling into the next
|
|
183
|
+
// with `slwi r31,r27,2` still in r31. It is the shift of the scrutinee where the `switch_br` read
|
|
184
|
+
// it, not where the target reads it (a back edge brings whatever the case left in the scrutinee's
|
|
185
|
+
// register), and it stands only where every other path in brings that same scaled index
|
|
186
|
+
// (`unsettledScaledIndices`); elsewhere it is a marker. A target two tables enter, and the
|
|
187
|
+
// function's entry, whose call edge no `preds` lists, make every register a marker.
|
|
188
|
+
|
|
189
|
+
/** What an elided dispatch leaves in a register it writes: the scrutinee scaled by four, or an
|
|
190
|
+
* address no C expression spells. */
|
|
191
|
+
export type IdiomLeft = 'scaled' | 'opaque';
|
|
192
|
+
|
|
193
|
+
/** The registers an elided dispatch's idiom writes, in program order, each with whether it is the
|
|
194
|
+
* index shift: what each register holds after it, decided by its last writer. */
|
|
195
|
+
export function idiomLeaves(writes: ReadonlyArray<{ reg: string; scaled: boolean }>): Map<string, IdiomLeft> {
|
|
196
|
+
const left = new Map<string, IdiomLeft>();
|
|
197
|
+
for (const { reg, scaled } of writes) {
|
|
198
|
+
left.set(reg, scaled ? 'scaled' : 'opaque');
|
|
199
|
+
}
|
|
200
|
+
return left;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A table whose dispatch block was elided, by block index. */
|
|
204
|
+
export interface ElidedDispatch {
|
|
205
|
+
/** the block emitting the `switch_br` */
|
|
206
|
+
at: number;
|
|
207
|
+
/** the bounds-checked scrutinee's register */
|
|
208
|
+
scrutReg: string;
|
|
209
|
+
/** the case targets, one per case */
|
|
210
|
+
cases: readonly number[];
|
|
211
|
+
/** the default target, or null when the out-of-range path is a return of its own */
|
|
212
|
+
defaultBlock: number | null;
|
|
213
|
+
leaves: ReadonlyMap<string, IdiomLeft>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A MARKER ({@link staleMarker}): what the idiom left in `reg` that no C expression spells, and
|
|
217
|
+
* whether the dispatch that left it was kept (its block lifted, holding the code the compiler
|
|
218
|
+
* placed in it) or elided. A read of one declines ({@link refuseStaleIdiomReads}). */
|
|
219
|
+
export interface StaleMarker {
|
|
220
|
+
kind: 'marker';
|
|
221
|
+
reg: string;
|
|
222
|
+
form: 'kept' | 'elided';
|
|
223
|
+
/** Whether the marker stands for the register on EVERY path that brings it ({@link parkedIn}): a
|
|
224
|
+
* kept dispatch's, and an EXACT target's. A MIXED target's marker also replaced what the other
|
|
225
|
+
* paths into it brought, so it is read like any value, and its read declines. */
|
|
226
|
+
parks: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The scaled index a case target is entered with in `reg` ({@link enterCaseTarget}): no marker
|
|
230
|
+
* but a value a case may read, recorded for {@link scaledIndexIn} and, on a MIXED target, to be
|
|
231
|
+
* settled. */
|
|
232
|
+
export interface StaleScaledIndex {
|
|
233
|
+
kind: 'scaled';
|
|
234
|
+
reg: string;
|
|
235
|
+
/** the dispatch's block: two tables' scaled indices are different values */
|
|
236
|
+
table: number;
|
|
237
|
+
/** the scrutinee the shift reads, where the dispatch's `switch_br` read it */
|
|
238
|
+
of: Value;
|
|
239
|
+
/** On a MIXED target: the `shl` defining it, the frontend's key for `reg`, and the predecessors
|
|
240
|
+
* ({@link CaseEntry.others}) every path into whose end must bring the same scaled index. Where
|
|
241
|
+
* one does not, `refuseStaleIdiomReads` declines a read of it and makes the `shl` a marker. */
|
|
242
|
+
settle?: { op: Op; key: string; others: readonly number[] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** What an elided or kept dispatch left in a register, by the value standing for it: a marker no
|
|
246
|
+
* read may reach, or the scaled index a read may reach but a guessed arity may not count. */
|
|
247
|
+
export type StaleIdiomRead = StaleMarker | StaleScaledIndex;
|
|
248
|
+
|
|
249
|
+
/** What a case target's entry holds from an elided dispatch: the scrutinee's register (read for a
|
|
250
|
+
* `scaled` entry) and each register the idiom wrote. */
|
|
251
|
+
export interface CaseEntry {
|
|
252
|
+
scrutReg: string;
|
|
253
|
+
leaves: ReadonlyMap<string, IdiomLeft>;
|
|
254
|
+
/** only the table's case edges enter the target */
|
|
255
|
+
exact: boolean;
|
|
256
|
+
/** the block emitting the table's `switch_br` */
|
|
257
|
+
at: number;
|
|
258
|
+
/** A MIXED target's predecessors other than the table's case edges — `at` itself when the target
|
|
259
|
+
* is also the default — each of whose paths must bring the scaled index for it to stand there
|
|
260
|
+
* (`StaleScaledIndex.settle`). Empty on an EXACT target. */
|
|
261
|
+
others: readonly number[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Each case target's entry: EXACT (every edge into it is one table's case edge) and MIXED (other
|
|
265
|
+
* edges enter it too) keep that table's `leaves`, the scaled index settled later on a MIXED one; a
|
|
266
|
+
* target two tables enter makes every register they wrote a marker. Block 0 does too: the call
|
|
267
|
+
* that enters a function is an edge no `preds` lists. */
|
|
268
|
+
export function caseEntries(
|
|
269
|
+
tables: readonly ElidedDispatch[],
|
|
270
|
+
preds: ReadonlyArray<readonly number[]>,
|
|
271
|
+
): Map<number, CaseEntry> {
|
|
272
|
+
const owners = new Map<number, ElidedDispatch[]>();
|
|
273
|
+
for (const t of tables) {
|
|
274
|
+
for (const c of new Set(t.cases)) {
|
|
275
|
+
owners.set(c, [...(owners.get(c) ?? []), t]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const out = new Map<number, CaseEntry>();
|
|
279
|
+
for (const [b, ts] of owners) {
|
|
280
|
+
const [t] = ts;
|
|
281
|
+
const own = ts.length === 1 && b !== 0;
|
|
282
|
+
const exact = own && b !== t.defaultBlock && preds[b].every((p) => p === t.at);
|
|
283
|
+
out.set(
|
|
284
|
+
b,
|
|
285
|
+
own
|
|
286
|
+
? {
|
|
287
|
+
scrutReg: t.scrutReg,
|
|
288
|
+
leaves: t.leaves,
|
|
289
|
+
exact,
|
|
290
|
+
at: t.at,
|
|
291
|
+
others: exact ? [] : [...new Set(preds[b])].filter((p) => p !== t.at || b === t.defaultBlock),
|
|
292
|
+
}
|
|
293
|
+
: {
|
|
294
|
+
scrutReg: t.scrutReg,
|
|
295
|
+
leaves: new Map(ts.flatMap((u) => [...u.leaves.keys()]).map((r) => [r, 'opaque' as const])),
|
|
296
|
+
exact,
|
|
297
|
+
at: t.at,
|
|
298
|
+
others: [],
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Write what an elided dispatch left at the entry of case target `entry`'s block, before its first
|
|
306
|
+
* instruction: `scrutinee` reads the switch's scrutinee there (before any of these writes, which may
|
|
307
|
+
* overwrite its register), `dispatchScrutinee` at the end of the dispatch's block `entry.at` (null
|
|
308
|
+
* while that block is unfilled, where a MIXED target's scaled index is a marker instead), `write`
|
|
309
|
+
* defines a register in the block. Each marker is recorded in `stale` against the register it
|
|
310
|
+
* stands in, for {@link refuseStaleIdiomReads}, and the scaled index as its own kind, for
|
|
311
|
+
* {@link scaledIndexIn} and, on a MIXED target, to be settled. */
|
|
312
|
+
export function enterCaseTarget(
|
|
313
|
+
entry: CaseEntry,
|
|
314
|
+
ops: Op[],
|
|
315
|
+
io: {
|
|
316
|
+
key: (reg: string) => string;
|
|
317
|
+
scrutinee: (reg: string) => Value;
|
|
318
|
+
dispatchScrutinee: (reg: string) => Value | null;
|
|
319
|
+
write: (reg: string, v: Value) => void;
|
|
320
|
+
},
|
|
321
|
+
stale: Map<Value, StaleIdiomRead>,
|
|
322
|
+
): void {
|
|
323
|
+
// On an EXACT target the two reads are one value: its only predecessor is the dispatch's block.
|
|
324
|
+
const of = [...entry.leaves.values()].includes('scaled')
|
|
325
|
+
? entry.exact
|
|
326
|
+
? io.scrutinee(entry.scrutReg)
|
|
327
|
+
: io.dispatchScrutinee(entry.scrutReg)
|
|
328
|
+
: null;
|
|
329
|
+
const shl = of === null ? null : mkOp('shl', { operands: [of], results: [mkValue(T.unk(32))], attrs: { imm: 2 } });
|
|
330
|
+
if (shl !== null) {
|
|
331
|
+
ops.push(shl);
|
|
332
|
+
}
|
|
333
|
+
for (const [r, left] of entry.leaves) {
|
|
334
|
+
if (left === 'scaled' && shl !== null) {
|
|
335
|
+
const [scaled] = shl.results;
|
|
336
|
+
io.write(r, scaled);
|
|
337
|
+
const settle = entry.exact ? {} : { settle: { op: shl, key: io.key(r), others: entry.others } };
|
|
338
|
+
stale.set(scaled, { kind: 'scaled', reg: r, table: entry.at, of: of!, ...settle });
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
io.write(r, staleMarker(ops, io.key(r), { reg: r, form: 'elided', parks: entry.exact }, stale));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** A marker for what the unlifted idiom left in `read.reg`, keyed `key` in the SSA. */
|
|
346
|
+
export function staleMarker(
|
|
347
|
+
ops: Op[],
|
|
348
|
+
key: string,
|
|
349
|
+
read: Omit<StaleMarker, 'kind'>,
|
|
350
|
+
stale: Map<Value, StaleIdiomRead>,
|
|
351
|
+
): Value {
|
|
352
|
+
const op = mkOp('undef', { results: [mkValue(T.unk(32))], attrs: { key } });
|
|
353
|
+
ops.push(op);
|
|
354
|
+
stale.set(op.results[0], { kind: 'marker', ...read });
|
|
355
|
+
return op.results[0];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Whether `v`, read from `reg`, is the marker the idiom left in that very register on every path
|
|
359
|
+
* that brings it — where the compiler parked the dispatch's own value, which no call is handed and
|
|
360
|
+
* no return hands back. A guessed arity stops below it (`CallSite.parked`). A return reaching only
|
|
361
|
+
* it returns nothing where the frontend already asks whether a value reaches the return register
|
|
362
|
+
* (PowerPC's r3, MIPS's v0, each written by the idiom on mwcc and gcc). Thumb asks no such thing — a
|
|
363
|
+
* return with nothing written in r0 hands back the caller's r0 — so its return reads the marker, and
|
|
364
|
+
* the read declines unless the function is declared void. A copy into another register is
|
|
365
|
+
* something the case did with it, and a MIXED target's marker stands in for what another path
|
|
366
|
+
* brought too (`StaleIdiomRead.parks`): both are read like any value. `key` is the frontend's
|
|
367
|
+
* register spelling. */
|
|
368
|
+
export const parkedIn =
|
|
369
|
+
(stale: ReadonlyMap<Value, StaleIdiomRead>, key: (reg: string) => string = (r) => r) =>
|
|
370
|
+
(v: Value, reg: string): boolean => {
|
|
371
|
+
const s = stale.get(v);
|
|
372
|
+
return s?.kind === 'marker' && s.parks && key(s.reg) === key(reg);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/** Whether `v`, read from `reg`, is the scrutinee scaled by four that an elided dispatch left in that
|
|
376
|
+
* very register (`enterCaseTarget`). A case reads it freely (`p[i]`), but a guessed arity may not
|
|
377
|
+
* count it: mwcc 2.4.2 keeps the dispatch's `slwi r4,r3,2` live into its `p[i]` cases, and an
|
|
378
|
+
* undeclared `g(2)` there is `li r3,2; bl g`, the bytes `g(2, i * 4)` compiles to too. */
|
|
379
|
+
export const scaledIndexIn =
|
|
380
|
+
(stale: ReadonlyMap<Value, StaleIdiomRead>, key: (reg: string) => string = (r) => r) =>
|
|
381
|
+
(v: Value, reg: string): boolean => {
|
|
382
|
+
const s = stale.get(v);
|
|
383
|
+
return s?.kind === 'scaled' && key(s.reg) === key(reg);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/** A DISPATCH'S STALE IDIOM REGISTERS MAY NOT BE READ. The dispatch idiom is not lifted, so a marker
|
|
387
|
+
* ({@link staleMarker}) stands for the table's address, an entry's address or the case's own code
|
|
388
|
+
* address, none of which a C expression spells. Run on the filled SSA, before `finish` collapses a
|
|
389
|
+
* join of an undefined value into its other argument: a marker some operand demands, directly or
|
|
390
|
+
* through the joins that carry it, declines. A `ret` operand demands nothing under a declared
|
|
391
|
+
* `void` return, which drops it (pipeline.ts `dropVoidReturnOperands`). A MIXED target's scaled
|
|
392
|
+
* index is settled first ({@link unsettledScaledIndices}): one that does not stand is a marker,
|
|
393
|
+
* read or not. `fail` is the frontend's own refusal. Last, `calls` refuses a guessed arity's
|
|
394
|
+
* trailing pass-through where the function lifts only through this modelling: a parked ceiling that
|
|
395
|
+
* hid a register, or a case reading a scaled index that stands
|
|
396
|
+
* (`CallDeclarations.refuseDispatchUnlockedPassThrough`). */
|
|
397
|
+
export function refuseStaleIdiomReads(
|
|
398
|
+
name: string,
|
|
399
|
+
irBlocks: readonly Block[],
|
|
400
|
+
stale: ReadonlyMap<Value, StaleIdiomRead>,
|
|
401
|
+
ssa: Pick<SsaBuilder, 'everyReachingDef'>,
|
|
402
|
+
own: FnProto | undefined,
|
|
403
|
+
fail: FrontendRefusal,
|
|
404
|
+
calls: Pick<CallDeclarations, 'refuseDispatchUnlockedPassThrough'>,
|
|
405
|
+
): void {
|
|
406
|
+
if (stale.size === 0) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const incoming = new Map<Value, Value[]>();
|
|
410
|
+
for (const blk of irBlocks) {
|
|
411
|
+
for (const op of blk.ops) {
|
|
412
|
+
for (const s of op.successors) {
|
|
413
|
+
s.args.forEach((a, i) => {
|
|
414
|
+
const p = s.block.params[i];
|
|
415
|
+
incoming.set(p, [...(incoming.get(p) ?? []), a]);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const unsettled = unsettledScaledIndices(irBlocks, stale, incoming, ssa);
|
|
421
|
+
const demanded = new Set<Value>();
|
|
422
|
+
const work: Value[] = [];
|
|
423
|
+
const demand = (v: Value) => {
|
|
424
|
+
if (!demanded.has(v)) {
|
|
425
|
+
demanded.add(v);
|
|
426
|
+
work.push(v);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const voidReturn = declaresVoidReturn(own);
|
|
430
|
+
for (const blk of irBlocks) {
|
|
431
|
+
for (const op of blk.ops) {
|
|
432
|
+
if (!(op.opcode === 'ret' && voidReturn)) {
|
|
433
|
+
op.operands.forEach(demand);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
while (work.length > 0) {
|
|
438
|
+
(incoming.get(work.pop()!) ?? []).forEach(demand);
|
|
439
|
+
}
|
|
440
|
+
const read = [...stale].flatMap(([v, s]) =>
|
|
441
|
+
demanded.has(v) && (s.kind === 'marker' || unsettled.has(v))
|
|
442
|
+
? [s.kind === 'marker' ? s : { reg: s.reg, form: 'elided' as const }]
|
|
443
|
+
: [],
|
|
444
|
+
)[0];
|
|
445
|
+
if (read !== undefined) {
|
|
446
|
+
const { reg, form } = read;
|
|
447
|
+
const why =
|
|
448
|
+
"which the unlifted dispatch idiom wrote — the lift would read the value from before the dispatch, not the table's " +
|
|
449
|
+
'index, address or target';
|
|
450
|
+
// one literal per form: the decline classifier (apps/web declines.ts) keys on the text up to `reads`
|
|
451
|
+
fail(
|
|
452
|
+
form === 'kept'
|
|
453
|
+
? `cannot lift '${name}': a case of the kept jump table reads ${reg}, ${why}`
|
|
454
|
+
: `cannot lift '${name}': a case of the elided jump table reads ${reg}, ${why}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
calls.refuseDispatchUnlockedPassThrough(
|
|
458
|
+
[...stale].some(([v, s]) => s.kind === 'scaled' && demanded.has(v) && !unsettled.has(v)),
|
|
459
|
+
);
|
|
460
|
+
// Read by nothing, so no C spells it; a marker keeps the SSA honest, as the shift's scrutinee need
|
|
461
|
+
// not reach a target some other path enters.
|
|
462
|
+
for (const v of unsettled) {
|
|
463
|
+
const s = stale.get(v) as StaleScaledIndex;
|
|
464
|
+
Object.assign(s.settle!.op, { opcode: 'undef', operands: [], attrs: { key: s.settle!.key } });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The scaled indices written at MIXED targets that do not stand. One stands where every path into
|
|
469
|
+
* the end of each of its target's other predecessors brings, in its register, a scaled index of the
|
|
470
|
+
* same table shifted from the same scrutinee — directly or through joins all of whose inputs do —
|
|
471
|
+
* with no call destroying it (`SsaBuilder.everyReachingDef`). Those paths include ones through
|
|
472
|
+
* other MIXED targets, so it is a greatest fixpoint: every guess stands until a path refutes it, and
|
|
473
|
+
* a refuted one refutes whatever leaned on it. What stands is then exactly what the register holds
|
|
474
|
+
* on every path into the target: the table's case edges bring the shift the target computes, and
|
|
475
|
+
* each other edge brings a value computing that same shift. The scrutinee is compared through
|
|
476
|
+
* joins all of whose other inputs are it, which the trivial-phi pass has yet to fold. */
|
|
477
|
+
function unsettledScaledIndices(
|
|
478
|
+
irBlocks: readonly Block[],
|
|
479
|
+
stale: ReadonlyMap<Value, StaleIdiomRead>,
|
|
480
|
+
incoming: ReadonlyMap<Value, readonly Value[]>,
|
|
481
|
+
ssa: Pick<SsaBuilder, 'everyReachingDef'>,
|
|
482
|
+
): Set<Value> {
|
|
483
|
+
const pending = [...stale].flatMap(([v, s]) => (s.kind === 'scaled' && s.settle !== undefined ? [{ v, s }] : []));
|
|
484
|
+
const refuted = new Set<Value>();
|
|
485
|
+
if (pending.length === 0) {
|
|
486
|
+
return refuted;
|
|
487
|
+
}
|
|
488
|
+
// the entry block's parameters are the caller's values, whatever its back edges bring
|
|
489
|
+
const entryParams = new Set(irBlocks[0].params);
|
|
490
|
+
const joined = (v: Value) => (entryParams.has(v) ? undefined : incoming.get(v));
|
|
491
|
+
const resolve = (v: Value, seen: Set<Value>): Value => {
|
|
492
|
+
const ins = joined(v);
|
|
493
|
+
if (ins === undefined || seen.has(v)) {
|
|
494
|
+
return v;
|
|
495
|
+
}
|
|
496
|
+
seen.add(v);
|
|
497
|
+
const others = new Set(ins.map((i) => resolve(i, seen)).filter((u) => u !== v));
|
|
498
|
+
return others.size === 1 ? [...others][0] : v;
|
|
499
|
+
};
|
|
500
|
+
const brings = (s: StaleScaledIndex) => {
|
|
501
|
+
const of = resolve(s.of, new Set());
|
|
502
|
+
const seen = new Set<Value>();
|
|
503
|
+
const ok = (u: Value): boolean => {
|
|
504
|
+
const t = stale.get(u);
|
|
505
|
+
if (t?.kind === 'scaled' && t.table === s.table && !refuted.has(u) && resolve(t.of, new Set()) === of) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
const ins = joined(u);
|
|
509
|
+
if (ins === undefined) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
if (seen.has(u)) {
|
|
513
|
+
return true; // a cycle of joins adds no input the first visit did not judge
|
|
514
|
+
}
|
|
515
|
+
seen.add(u);
|
|
516
|
+
return ins.every(ok);
|
|
517
|
+
};
|
|
518
|
+
return ok;
|
|
519
|
+
};
|
|
520
|
+
for (let changed = true; changed;) {
|
|
521
|
+
changed = false;
|
|
522
|
+
for (const { v, s } of pending) {
|
|
523
|
+
if (!refuted.has(v) && !s.settle!.others.every((p) => ssa.everyReachingDef(s.settle!.key, p, brings(s)))) {
|
|
524
|
+
refuted.add(v);
|
|
525
|
+
changed = true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return refuted;
|
|
530
|
+
}
|