@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
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// factory takes its dependencies EXPLICITLY (`SwitchRecoverDeps`); `expr`/`structureRegion` are
|
|
5
5
|
// late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
|
|
6
6
|
// machinery (loops/ifs inside cases, the onStack guard).
|
|
7
|
+
import { constWord } from '../ir/bits';
|
|
7
8
|
import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
|
|
8
|
-
import {
|
|
9
|
+
import { orderSensitive } from '../ir/discipline';
|
|
9
10
|
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
10
11
|
|
|
12
|
+
/** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
|
|
13
|
+
export type SwitchBoundCase = 'taken' | 'either';
|
|
14
|
+
|
|
11
15
|
export interface SwitchRecoverDeps {
|
|
12
16
|
fn: Fn;
|
|
13
17
|
defs: Map<Value, Op>;
|
|
@@ -19,8 +23,9 @@ export interface SwitchRecoverDeps {
|
|
|
19
23
|
/** is this opcode an integer comparison? */
|
|
20
24
|
isCmpOpcode: (opcode: string) => boolean;
|
|
21
25
|
switchAllowsNeqCase: boolean;
|
|
22
|
-
/** read a relational test
|
|
23
|
-
|
|
26
|
+
/** read a relational test as a case where the values that can reach it leave exactly one on
|
|
27
|
+
* this side of it — its branch, or either side — or never (null) */
|
|
28
|
+
switchBoundCase: SwitchBoundCase | null;
|
|
24
29
|
/** emit the case arms in the ASSEMBLY's block-layout order rather than by ascending case value */
|
|
25
30
|
switchArmsFollowLayout: boolean;
|
|
26
31
|
/** DECLINE a recovered tree whose own layout INTERLEAVES a test with a case body. A source
|
|
@@ -44,6 +49,11 @@ export interface SwitchRecoverDeps {
|
|
|
44
49
|
* of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
|
|
45
50
|
* both halves for def-site anchoring, rather than indexing the parameters a second time. */
|
|
46
51
|
blockOf: (v: Value) => Block | undefined;
|
|
52
|
+
/** The header and exit of the loop the switch is being structured in, or null outside any loop.
|
|
53
|
+
* Read LIVE: the structurer's loop frame is the one current when the switch is met. An edge to
|
|
54
|
+
* the exit from inside a case arm is a `break` of that loop, which C binds to the switch instead
|
|
55
|
+
* (`analyzeArmExit`), save where the exit is the switch's own merge. */
|
|
56
|
+
enclosingLoop: () => { header: Block; exit: Block | null } | null;
|
|
47
57
|
/** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
|
|
48
58
|
* ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
|
|
49
59
|
* (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
|
|
@@ -56,6 +66,10 @@ export interface SwitchRecoverDeps {
|
|
|
56
66
|
) => Stmt[] | null;
|
|
57
67
|
expr: (v: Value) => Expr;
|
|
58
68
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
69
|
+
/** One arm's body, from `entry` to `stop`, inside the switch whose code resumes at `merge`. The
|
|
70
|
+
* structurer owns what `break;` means there (structure.ts `structureArm`), so both regimes hand
|
|
71
|
+
* it every arm and the exit verdict they read for it. */
|
|
72
|
+
structureArm: (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined) => Stmt[];
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
@@ -66,18 +80,30 @@ export interface SwitchRecoverDeps {
|
|
|
66
80
|
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
67
81
|
* Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
|
|
68
82
|
* below arranges and both regimes then re-read off the emission array (see the
|
|
69
|
-
* l3/ast.ts non-neutrality note).
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
83
|
+
* l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
|
|
84
|
+
* switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
|
|
85
|
+
* `break;` on those paths.
|
|
86
|
+
* - `unstructurable` anything else: two different siblings, or a sibling beside a block the
|
|
87
|
+
* switch does not dominate. C needs a `goto` for those. Regime A declines to
|
|
88
|
+
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
|
|
89
|
+
*
|
|
90
|
+
* `region` is the arm's own blocks: every block the walk below reached, the entry included. It is
|
|
91
|
+
* what the structurer needs to find the arm's own joins (`structure.ts` `armJoin`) and to spell a
|
|
92
|
+
* path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
|
|
93
|
+
* falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
|
|
94
|
+
* sideways, into a block the switch does not dominate, has none: its paths through such a block
|
|
95
|
+
* are not the arm's, and it structures to the CFG's joins. */
|
|
96
|
+
export type ArmExit =
|
|
97
|
+
| { kind: 'break'; region?: ReadonlySet<Block> }
|
|
98
|
+
| { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
99
|
+
| { kind: 'unstructurable'; why: string };
|
|
74
100
|
|
|
75
101
|
export interface SwitchRecovery {
|
|
76
102
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
77
103
|
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
78
104
|
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
79
105
|
* Regime B, which has no fallback, fails loud. */
|
|
80
|
-
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block
|
|
106
|
+
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>, stopAtLoop?: boolean) => ArmExit;
|
|
81
107
|
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
82
108
|
* regimes read it from one definition (and one statement of what it rests on). */
|
|
83
109
|
layoutIndex: (blk: Block) => number;
|
|
@@ -128,66 +154,126 @@ export interface TestInfo {
|
|
|
128
154
|
xOnLeft: boolean;
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
// WHICH SCRUTINEE VALUES CAN REACH A TEST, AND WHICH ONE A SIDE OF IT PINS. A comparison dispatch
|
|
158
|
+
// pins its last value with a RELATIONAL test once the tests above it have narrowed the scrutinee to
|
|
159
|
+
// one value on that side: mwcc's `switch (x) { case 0: … case 1: … default: … }` is `cmpwi r0,1;
|
|
160
|
+
// beq- case1; bge- default; cmpwi r0,0; bge- case0; b default`, where `x >= 0` is `case 0` only
|
|
161
|
+
// because `x != 1` and `x < 1` came first. agbcc's `emit_case_nodes` does the same on the branch it
|
|
162
|
+
// takes once `node_is_bounded` holds, and its `x <u 1` for `case 0` of an unsigned switch is the
|
|
163
|
+
// instance that needs nothing from the tests above: one value at an end of the 32-bit domain. Read
|
|
164
|
+
// as navigation instead, the pinned arm's body becomes a second default candidate and the whole
|
|
165
|
+
// tree declines. Which SIDE of a test may pin a case is the compiler's (`switchBoundCase`,
|
|
166
|
+
// target.ts).
|
|
167
|
+
//
|
|
168
|
+
// EXACT, not a bound. A range is the set of 32-bit register values, as disjoint signed intervals in
|
|
169
|
+
// ascending order, and every test the walk narrows it by — `==`, `!=`, and a relational test under
|
|
170
|
+
// either signedness — is an exact set over that register. An unsigned interval straddling 2^31 is
|
|
171
|
+
// two signed ones (`fromUnsigned`). So a side that comes out a singleton names the only value that
|
|
172
|
+
// CAN take it, and a side that comes out empty is one no value takes. Whether the value DOES reach
|
|
173
|
+
// the body is still PRE3's to say, simulating the original tree case by case.
|
|
174
|
+
//
|
|
175
|
+
// THE DOMAIN IS THE REGISTER's, not the scrutinee's recovered type, so a narrower type has nearer
|
|
176
|
+
// ends this misses. That costs a case and never invents one.
|
|
177
|
+
type Ranges = readonly (readonly [number, number])[];
|
|
178
|
+
|
|
179
|
+
const INT_MIN = -0x80000000;
|
|
180
|
+
const INT_MAX = 0x7fffffff;
|
|
181
|
+
const ALL: Ranges = [[INT_MIN, INT_MAX]];
|
|
182
|
+
|
|
183
|
+
/** The same opcode with its operands swapped: `k < x` is `x > k`. */
|
|
184
|
+
const MIRRORED: Record<string, string> = {
|
|
185
|
+
icmp_slt: 'icmp_sgt',
|
|
186
|
+
icmp_sgt: 'icmp_slt',
|
|
187
|
+
icmp_sle: 'icmp_sge',
|
|
188
|
+
icmp_sge: 'icmp_sle',
|
|
189
|
+
icmp_ult: 'icmp_ugt',
|
|
190
|
+
icmp_ugt: 'icmp_ult',
|
|
191
|
+
icmp_ule: 'icmp_uge',
|
|
192
|
+
icmp_uge: 'icmp_ule',
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
|
|
196
|
+
function fromUnsigned(lo: number, hi: number): Ranges {
|
|
197
|
+
if (lo > hi) {
|
|
198
|
+
return [];
|
|
157
199
|
}
|
|
200
|
+
return hi <= INT_MAX || lo > INT_MAX
|
|
201
|
+
? [[lo | 0, hi | 0]]
|
|
202
|
+
: [
|
|
203
|
+
[INT_MIN, hi | 0],
|
|
204
|
+
[lo, INT_MAX],
|
|
205
|
+
];
|
|
158
206
|
}
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// arm's body becomes a second default candidate and the whole tree declines.
|
|
167
|
-
//
|
|
168
|
-
// THE BRANCH, never the fall-through. Every jump in `emit_case_nodes` that lands on a case body
|
|
169
|
-
// is its test's BRANCH — for a single-valued node, LT to `node->left->code_label` and GT to
|
|
170
|
-
// `node->right->code_label`, each guarded by `node_is_bounded` on that side — while the
|
|
171
|
-
// fall-through always continues into more dispatch, so a fall-side reading has no producer in
|
|
172
|
-
// this dispatch — and none turns up in 3176 generated agbcc dispatches.
|
|
173
|
-
//
|
|
174
|
-
// TWO PREMISES ABOUT THE DOMAIN. It is the 32-bit REGISTER's, not the scrutinee's recovered
|
|
175
|
-
// type, so a narrower type has a nearer endpoint this misses — which costs a case and never
|
|
176
|
-
// invents one. And it is the WHOLE of that domain, so an ancestor that already excluded the
|
|
177
|
-
// value makes the reading wrong; PRE3 is what catches that, simulating the original tree for
|
|
178
|
-
// every recovered case value and declining unless it lands on the recorded body, exactly as it
|
|
179
|
-
// does for the `eq` cases. Null when the branch admits none, several, or the whole domain.
|
|
180
|
-
function singletonTaken(ti: TestInfo): number | null {
|
|
181
|
-
const [min, max] = ti.opcode.startsWith('icmp_u') ? [0, -1] : [-0x80000000, 0x7fffffff];
|
|
182
|
-
for (const [v, next] of [
|
|
183
|
-
[min, min + 1],
|
|
184
|
-
[max, max - 1],
|
|
185
|
-
]) {
|
|
186
|
-
if (evalCmp(ti.opcode, ti.xOnLeft, v, ti.k) && !evalCmp(ti.opcode, ti.xOnLeft, next, ti.k)) {
|
|
187
|
-
return v;
|
|
208
|
+
function complement(r: Ranges): Ranges {
|
|
209
|
+
const out: [number, number][] = [];
|
|
210
|
+
let next = INT_MIN;
|
|
211
|
+
for (const [lo, hi] of r) {
|
|
212
|
+
if (lo > next) {
|
|
213
|
+
out.push([next, lo - 1]);
|
|
188
214
|
}
|
|
215
|
+
next = hi + 1;
|
|
216
|
+
}
|
|
217
|
+
if (next <= INT_MAX) {
|
|
218
|
+
out.push([next, INT_MAX]);
|
|
189
219
|
}
|
|
190
|
-
return
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function intersect(a: Ranges, b: Ranges): Ranges {
|
|
224
|
+
const out: [number, number][] = [];
|
|
225
|
+
for (const [alo, ahi] of a) {
|
|
226
|
+
for (const [blo, bhi] of b) {
|
|
227
|
+
const lo = Math.max(alo, blo);
|
|
228
|
+
const hi = Math.min(ahi, bhi);
|
|
229
|
+
if (lo <= hi) {
|
|
230
|
+
out.push([lo, hi]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out.sort((x, y) => x[0] - y[0]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The register values that take the test's BRANCH. */
|
|
238
|
+
function takenBy(ti: TestInfo): Ranges {
|
|
239
|
+
const op = ti.xOnLeft ? ti.opcode : (MIRRORED[ti.opcode] ?? ti.opcode);
|
|
240
|
+
if (op === 'icmp_eq' || op === 'icmp_ne') {
|
|
241
|
+
const k = ti.k | 0;
|
|
242
|
+
return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
|
|
243
|
+
}
|
|
244
|
+
const uns = op.startsWith('icmp_u');
|
|
245
|
+
const [min, max, k] = uns ? [0, 0xffffffff, ti.k >>> 0] : [INT_MIN, INT_MAX, ti.k | 0];
|
|
246
|
+
const rel = op.slice('icmp_s'.length);
|
|
247
|
+
const [lo, hi] =
|
|
248
|
+
rel === 'lt'
|
|
249
|
+
? [min, k - 1]
|
|
250
|
+
: rel === 'le'
|
|
251
|
+
? [min, k]
|
|
252
|
+
: rel === 'gt'
|
|
253
|
+
? [k + 1, max]
|
|
254
|
+
: rel === 'ge'
|
|
255
|
+
? [k, max]
|
|
256
|
+
: [min, max];
|
|
257
|
+
return uns ? fromUnsigned(lo, hi) : lo > hi ? [] : [[lo, hi]];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** What of `path` follows the test's `taken` edge, or its fall-through. */
|
|
261
|
+
function narrow(path: Ranges, ti: TestInfo, taken: boolean): Ranges {
|
|
262
|
+
const t = takenBy(ti);
|
|
263
|
+
return intersect(path, taken ? t : complement(t));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Whether register value `v` is in `r`. PRE3's concrete simulation reads each test through this
|
|
267
|
+
* and {@link takenBy}, so the tree it walks and the ranges a bound case is read off are one
|
|
268
|
+
* definition of the icmp family, not two. */
|
|
269
|
+
function contains(r: Ranges, v: number): boolean {
|
|
270
|
+
const x = v | 0;
|
|
271
|
+
return r.some(([lo, hi]) => lo <= x && x <= hi);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The one value in `r`, if it holds exactly one. */
|
|
275
|
+
function singleton(r: Ranges): number | null {
|
|
276
|
+
return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
|
|
191
277
|
}
|
|
192
278
|
|
|
193
279
|
/** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
|
|
@@ -251,23 +337,26 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
251
337
|
isNamed,
|
|
252
338
|
isCmpOpcode,
|
|
253
339
|
switchAllowsNeqCase,
|
|
254
|
-
|
|
340
|
+
switchBoundCase,
|
|
255
341
|
switchArmsFollowLayout,
|
|
256
342
|
switchRequiresFrontLoadedTests,
|
|
257
343
|
spellSwitchFallthrough,
|
|
258
344
|
emitsOwnStatement,
|
|
259
345
|
blockOf,
|
|
346
|
+
enclosingLoop,
|
|
260
347
|
hoistDispatchCopies,
|
|
261
348
|
expr,
|
|
262
349
|
structureRegion,
|
|
350
|
+
structureArm,
|
|
263
351
|
} = deps;
|
|
264
352
|
|
|
265
353
|
// A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
|
|
266
354
|
// of layout below, and true PER FRONTEND rather than of the IR:
|
|
267
355
|
// - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
|
|
268
|
-
// - ppc.ts does
|
|
269
|
-
// for every conditional-return branch, wherever in the stream that branch sits
|
|
270
|
-
//
|
|
356
|
+
// - ppc.ts does too, and then APPENDS a synthetic return block (`synthReturn`) at the end of
|
|
357
|
+
// the list for every conditional-return branch, wherever in the stream that branch sits. So
|
|
358
|
+
// its real blocks keep address order among themselves, and each synthetic one — a bare
|
|
359
|
+
// return, never a test — sorts after all of them;
|
|
271
360
|
// - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
|
|
272
361
|
// .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
|
|
273
362
|
// TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
|
|
@@ -278,9 +367,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
278
367
|
// `MIPS_GCC` declares `switchRequiresFrontLoadedTests` and NOT `switchArmsFollowLayout` for
|
|
279
368
|
// exactly that reason — it has a scheduler and fills delay slots (target.ts).
|
|
280
369
|
// Both are therefore claims about a target's FRONTEND as much as about its compiler, and a target
|
|
281
|
-
// opts in on both halves
|
|
282
|
-
//
|
|
283
|
-
//
|
|
370
|
+
// opts in on both halves. PPC_MWCC's frontend holds the weaker half only: a synthetic return body
|
|
371
|
+
// sorting last can make PRE5 keep a `switch` it should decline, never decline one it should keep,
|
|
372
|
+
// so mwcc declares `switchRequiresFrontLoadedTests` and could not declare the placing reading.
|
|
373
|
+
// That is the frontend's limit and not the compiler's: mwcc lays its case bodies in source order
|
|
374
|
+
// (the compiled pairs at target.ts `switchArmsFollowLayout`), so a synthetic block placed at its
|
|
375
|
+
// branch's address would give PPC both halves.
|
|
376
|
+
// Anything added below that reads `layoutIndex` inherits the frontend half and owes a statement
|
|
377
|
+
// of which strength of the compiler half it needs.
|
|
284
378
|
const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
|
|
285
379
|
const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
|
|
286
380
|
|
|
@@ -357,82 +451,19 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
357
451
|
// has declared that its layout answers it — which SPELLING the source wrote.
|
|
358
452
|
|
|
359
453
|
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
360
|
-
// `250 << 2` for a large sparse case) to a number — else null.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (!d) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
if (d.opcode === 'const') {
|
|
370
|
-
return (d.attrs.value as number) | 0;
|
|
371
|
-
}
|
|
372
|
-
// Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
|
|
373
|
-
// `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
|
|
374
|
-
const operands2 = (): [number, number] | null => {
|
|
375
|
-
const a = evalConst(d.operands[0]);
|
|
376
|
-
if (a === null) {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
let c: number | null;
|
|
380
|
-
if (d.operands.length === 2) {
|
|
381
|
-
c = evalConst(d.operands[1]);
|
|
382
|
-
} else if (typeof d.attrs.imm === 'number') {
|
|
383
|
-
c = d.attrs.imm | 0;
|
|
384
|
-
} else {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
return c === null ? null : [a, c];
|
|
388
|
-
};
|
|
389
|
-
const bin = (f: (a: number, c: number) => number): number | null => {
|
|
390
|
-
const p = operands2();
|
|
391
|
-
return p === null ? null : f(p[0], p[1]) | 0;
|
|
392
|
-
};
|
|
393
|
-
const shift = (f: (a: number, c: number) => number): number | null => {
|
|
394
|
-
const p = operands2();
|
|
395
|
-
if (p === null || p[1] < 0 || p[1] >= 32) {
|
|
396
|
-
return null;
|
|
397
|
-
} // out-of-range shift amount → decline
|
|
398
|
-
return f(p[0], p[1]) | 0;
|
|
399
|
-
};
|
|
400
|
-
switch (d.opcode) {
|
|
401
|
-
case 'shl':
|
|
402
|
-
return shift((a, c) => a << c);
|
|
403
|
-
case 'shr_u':
|
|
404
|
-
return shift((a, c) => a >>> c);
|
|
405
|
-
case 'shr_s':
|
|
406
|
-
return shift((a, c) => a >> c);
|
|
407
|
-
case 'or':
|
|
408
|
-
return bin((a, c) => a | c);
|
|
409
|
-
case 'and':
|
|
410
|
-
return bin((a, c) => a & c);
|
|
411
|
-
case 'xor':
|
|
412
|
-
return bin((a, c) => a ^ c);
|
|
413
|
-
case 'add':
|
|
414
|
-
return bin((a, c) => a + c);
|
|
415
|
-
case 'sub':
|
|
416
|
-
return bin((a, c) => a - c);
|
|
417
|
-
case 'neg': {
|
|
418
|
-
const a = evalConst(d.operands[0]);
|
|
419
|
-
return a === null ? null : -a | 0;
|
|
420
|
-
}
|
|
421
|
-
case 'not': {
|
|
422
|
-
const a = evalConst(d.operands[0]);
|
|
423
|
-
return a === null ? null : ~a | 0;
|
|
424
|
-
}
|
|
425
|
-
default:
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
};
|
|
454
|
+
// `250 << 2` for a large sparse case) to a number — else null. The shared word evaluator
|
|
455
|
+
// (ir/bits.ts), which declines an opcode it has no rule for: a `neg_s` (Thumb `cmn` under a signed
|
|
456
|
+
// branch) is no word, so a guard against it is never read as a case bound. A named variable is
|
|
457
|
+
// not a constant, whatever set it.
|
|
458
|
+
const words = { defs, opaque: isNamed };
|
|
459
|
+
const evalConst = (v: Value): number | null => constWord(words, v);
|
|
429
460
|
|
|
430
461
|
// TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
|
|
431
462
|
// both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
|
|
432
463
|
//
|
|
433
464
|
// PRE4 (purity) is the second. A collapsed test block's ops re-render at whichever use inlines
|
|
434
465
|
// them, at a point the switch decides — so the question is motion, not deletion, and
|
|
435
|
-
// `
|
|
466
|
+
// `orderSensitive` is the question that asks it. NOT the trapping divides: a use is dominated by
|
|
436
467
|
// its def, so the re-rendered op runs on a subset of the paths it already ran on — nothing is
|
|
437
468
|
// speculated. `emitsOwnStatement` covers what motion cannot save: a statement belonging to the
|
|
438
469
|
// block rather than to a use. The root is exempt from all of it — its ops are already emitted as
|
|
@@ -441,8 +472,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
441
472
|
// A block that tests the scrutinee and is NOT collapsible is still dispatch, so the walk must
|
|
442
473
|
// read it as dispatch and decline, never re-read it as a case body — that would spell an arm
|
|
443
474
|
// whose guard the dispatch has already decided.
|
|
444
|
-
const collapsible = (blk: Block): boolean =>
|
|
445
|
-
!blk.ops.some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)) && !emitsOwnStatement(blk);
|
|
475
|
+
const collapsible = (blk: Block): boolean => !blk.ops.some(orderSensitive) && !emitsOwnStatement(blk);
|
|
446
476
|
const testInfo = (blk: Block): TestInfo | null => {
|
|
447
477
|
const term = blk.ops[blk.ops.length - 1];
|
|
448
478
|
if (term.opcode !== 'cond_br') {
|
|
@@ -472,6 +502,123 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
472
502
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
473
503
|
};
|
|
474
504
|
|
|
505
|
+
// Does `blk` leave the enclosing loop for its exit, other than by the loop's own bottom test (a
|
|
506
|
+
// block that also edges back to the header, which spells as the loop's condition)?
|
|
507
|
+
const breaksLoop = (blk: Block): boolean => {
|
|
508
|
+
const loop = enclosingLoop();
|
|
509
|
+
const succ = successorsOf(blk);
|
|
510
|
+
return loop !== null && loop.exit !== null && succ.includes(loop.exit) && !succ.includes(loop.header);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Does a block some arm structures edge to `target`? Walked as `structureArm` walks an arm: every
|
|
514
|
+
// block reached from the arm's entry without stepping through `merge` or back into `header`. That
|
|
515
|
+
// is wider than `analyzeArmExit`'s dominated region on purpose: when Regime A recognises a sub-tree
|
|
516
|
+
// of the comparison tree, an arm can run on into the code of a case the tree's root already took,
|
|
517
|
+
// which the dispatch does not dominate, and `structureArm` structures that code inside the arm.
|
|
518
|
+
// A block that also edges back to `header` is the loop's bottom test, which spells as the loop's
|
|
519
|
+
// condition, not as the arm's (`breaksLoop`) — save where `header` IS the merge (a top-tested
|
|
520
|
+
// loop): no bottom test is then reachable without passing the merge, and an arm's
|
|
521
|
+
// `if (c) goto out;` is a block that edges to the exit and straight back into the header.
|
|
522
|
+
const armsReach = (entries: Iterable<Block>, merge: Block | null, header: Block, target: Block): boolean => {
|
|
523
|
+
const seen = new Set<Block>(entries);
|
|
524
|
+
for (const q = [...seen]; q.length;) {
|
|
525
|
+
const succ = successorsOf(q.pop()!);
|
|
526
|
+
if (succ.includes(target) && (header === merge || !succ.includes(header))) {
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
for (const s of succ) {
|
|
530
|
+
if (s !== target && s !== merge && s !== header && !seen.has(s)) {
|
|
531
|
+
seen.add(s);
|
|
532
|
+
q.push(s);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Can a path from `from` reach `to` without passing `through`? False when `to` is null (the end
|
|
540
|
+
// of the function) or is `through` itself.
|
|
541
|
+
const reachesAvoiding = (from: Block, to: Block | null, through: Block | null): boolean => {
|
|
542
|
+
if (to === null || to === through) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const seen = new Set<Block>([from]);
|
|
546
|
+
for (const q = [from]; q.length;) {
|
|
547
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
548
|
+
if (s === to) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
if (s !== through && !seen.has(s)) {
|
|
552
|
+
seen.add(s);
|
|
553
|
+
q.push(s);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// THE LOOP-EXIT MERGE, WHERE IT SPELLS. A switch whose merge is its enclosing loop's exit puts
|
|
561
|
+
// the exit region after the `switch`, INSIDE the loop body, and reaches the loop bottom only
|
|
562
|
+
// through the arms that do not break. That is the source's text on two conditions, both read off
|
|
563
|
+
// the CFG here, and on either failing the merge-past-stop gate below declines to if-recovery:
|
|
564
|
+
// - the exit region RETURNS: no path from `merge` reaches a block that dominates it (other
|
|
565
|
+
// than `merge` itself, a loop of the exit region's own) — the enclosing loop's header, or an
|
|
566
|
+
// OUTER loop's, whose back edge the inner body cannot spell
|
|
567
|
+
// (`for (j...) { for (;;) { switch ... } out: u += prev; }` runs on to the outer latch);
|
|
568
|
+
// - every loop-bottom TEST the arms reach (a block that edges back to the header) leaves for
|
|
569
|
+
// the merge itself, which the arm then spells as its `break;` (`latchexit`). One that leaves
|
|
570
|
+
// for any other block outside the loop is past what an arm structured against `merge` can
|
|
571
|
+
// say: the merge is then the loop's exit only because an early `return` made a shared
|
|
572
|
+
// epilogue the dispatch's post-dominator, while the code the bottom test leaves for runs
|
|
573
|
+
// before it (`for (w = 0; w < 4; w++) { if (c) return w; switch ... } r(t);` on
|
|
574
|
+
// gcc2.7.2kmc).
|
|
575
|
+
// And the loop's HEADER must not leave for the merge itself. One that does makes the loop
|
|
576
|
+
// top-tested on it, with the merge as the loop's own exit, which the exit region rendered after
|
|
577
|
+
// the switch inside the body would leave nothing to reach
|
|
578
|
+
// (`for (k = 0; k < 3; k++) { if (gC == 7) goto out; switch ... }` becomes `while (gC != 7)`).
|
|
579
|
+
// A test lower in the body that leaves for the merge ahead of the dispatch spells as its own
|
|
580
|
+
// `goto` or `break`, and keeps the switch.
|
|
581
|
+
const exitMergeSpells = (from: Block, merge: Block | null): boolean => {
|
|
582
|
+
if (merge === null) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const above = dom.get(merge)!;
|
|
586
|
+
const exitSeen = new Set<Block>([merge]);
|
|
587
|
+
for (const q = [merge]; q.length;) {
|
|
588
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
589
|
+
if (s !== merge && above.has(s)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (!exitSeen.has(s)) {
|
|
593
|
+
exitSeen.add(s);
|
|
594
|
+
q.push(s);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const header = enclosingLoop()?.header ?? null;
|
|
599
|
+
if (header !== null && successorsOf(header).includes(merge)) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const seen = new Set<Block>([from]);
|
|
603
|
+
for (const q = [from]; q.length;) {
|
|
604
|
+
const succ = successorsOf(q.pop()!);
|
|
605
|
+
if (
|
|
606
|
+
header !== null &&
|
|
607
|
+
succ.includes(header) &&
|
|
608
|
+
succ.some((s) => s !== header && s !== merge && !reachesAvoiding(s, header, merge))
|
|
609
|
+
) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
for (const s of succ) {
|
|
613
|
+
if (s !== header && s !== merge && !seen.has(s)) {
|
|
614
|
+
seen.add(s);
|
|
615
|
+
q.push(s);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
};
|
|
621
|
+
|
|
475
622
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
476
623
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
477
624
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
@@ -483,22 +630,44 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
483
630
|
// sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
|
|
484
631
|
// record, so an arm that reaches no sibling still closes with a plain `break`.
|
|
485
632
|
//
|
|
633
|
+
// With `stopAtLoop` (Regime A), a FALLING arm that also reaches the merge must not break the loop
|
|
634
|
+
// the switch sits in. When the dispatch IS the loop header (agbcc and IDO rotate
|
|
635
|
+
// `while (k < 4) { k++; switch … }` so), it dominates the loop's exit and all the code after the
|
|
636
|
+
// loop, and the merge is the header itself, so dominance cannot tell the arm's region from the
|
|
637
|
+
// code past the loop. Such an arm that falls into a sibling, reaches the merge by the back edge,
|
|
638
|
+
// and leaves for the loop's exit on a third path (a `goto out`, or a sibling that IS the exit, as
|
|
639
|
+
// a `default: goto out;` makes it) would read as falling with `breaks`, Regime A would commit,
|
|
640
|
+
// and the loop-exit refusal in `structure.ts` would fail the whole function where if-recovery
|
|
641
|
+
// spells it. So it is `unstructurable`. What counts is the block the refusal fires on
|
|
642
|
+
// (`breaksLoop`): the loop's own bottom test, reached after a `continue`, exits too but is the
|
|
643
|
+
// loop's condition, and a block that also edges to the merge is the switch's own end. Regime B,
|
|
644
|
+
// which has no fallback, does not ask: it lifts a jump table whose arms share the rotated latch
|
|
645
|
+
// and its exit (`do { … switch … t++; } while (t < 15)`).
|
|
646
|
+
//
|
|
486
647
|
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
487
648
|
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
488
649
|
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
489
650
|
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
490
651
|
// back. Costly for matching, correct to run.
|
|
491
|
-
const analyzeArmExit = (
|
|
652
|
+
const analyzeArmExit = (
|
|
653
|
+
entry: Block,
|
|
654
|
+
b: Block,
|
|
655
|
+
merge: Block | null,
|
|
656
|
+
siblings: Set<Block>,
|
|
657
|
+
stopAtLoop = false,
|
|
658
|
+
): ArmExit => {
|
|
492
659
|
if (entry === merge) {
|
|
493
660
|
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
494
661
|
}
|
|
495
662
|
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
496
663
|
let toMerge = false,
|
|
497
|
-
escapes = false
|
|
664
|
+
escapes = false,
|
|
665
|
+
leavesLoop = false;
|
|
498
666
|
const seen = new Set<Block>([entry]);
|
|
499
667
|
const q = [entry];
|
|
500
668
|
while (q.length) {
|
|
501
669
|
const cur = q.pop()!;
|
|
670
|
+
leavesLoop ||= !successorsOf(cur).includes(merge as Block) && breaksLoop(cur);
|
|
502
671
|
for (const s of successorsOf(cur)) {
|
|
503
672
|
if (s === merge) {
|
|
504
673
|
toMerge = true;
|
|
@@ -515,32 +684,32 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
515
684
|
}
|
|
516
685
|
}
|
|
517
686
|
if (into.size === 0) {
|
|
518
|
-
return { kind: 'break' };
|
|
519
|
-
}
|
|
520
|
-
if (into.size === 1 && !toMerge && !escapes) {
|
|
521
|
-
return { kind: 'fallthrough', to: [...into][0] };
|
|
687
|
+
return escapes ? { kind: 'break' } : { kind: 'break', region: seen };
|
|
522
688
|
}
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
if (into.size
|
|
689
|
+
// A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
|
|
690
|
+
// spells only where nothing breakable sits between that edge and the arm, which the structurer
|
|
691
|
+
// checks where it emits it (`structure.ts` `switchBreakAt`) and refuses loud otherwise.
|
|
692
|
+
if (into.size === 1 && toMerge && leavesLoop && stopAtLoop) {
|
|
527
693
|
return {
|
|
528
694
|
kind: 'unstructurable',
|
|
529
|
-
why: `a case body
|
|
695
|
+
why: `a case body falls into sibling case ${[...into].map((x) => `#${layoutIndex(x)}`).join(', ')}, reaches the end of the switch, and leaves the loop the switch sits in`,
|
|
530
696
|
};
|
|
531
697
|
}
|
|
532
|
-
if (escapes) {
|
|
698
|
+
if (into.size === 1 && !escapes) {
|
|
699
|
+
return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
|
|
700
|
+
}
|
|
701
|
+
// Name what is actually missing. These are two different facts: the first is a shape C's
|
|
702
|
+
// fall-through cannot spell, the second leaves the switch's region sideways.
|
|
703
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
704
|
+
if (into.size > 1) {
|
|
533
705
|
return {
|
|
534
706
|
kind: 'unstructurable',
|
|
535
|
-
why: `a case body reaches sibling
|
|
707
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
536
708
|
};
|
|
537
709
|
}
|
|
538
710
|
return {
|
|
539
711
|
kind: 'unstructurable',
|
|
540
|
-
|
|
541
|
-
// asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
|
|
542
|
-
// (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
|
|
543
|
-
why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
|
|
712
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
544
713
|
};
|
|
545
714
|
};
|
|
546
715
|
|
|
@@ -588,12 +757,15 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
588
757
|
}
|
|
589
758
|
guard.add(cur);
|
|
590
759
|
const term = cur.ops[cur.ops.length - 1];
|
|
591
|
-
const taken =
|
|
760
|
+
const taken = contains(takenBy(ti), xv);
|
|
592
761
|
cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
|
|
593
762
|
}
|
|
594
763
|
};
|
|
595
764
|
const seen = new Set<Block>();
|
|
596
765
|
const work: Block[] = [b];
|
|
766
|
+
// The values that can reach each test block, written as the walk pushes it. Only a bound case
|
|
767
|
+
// reads it; the tree is a tree (a second visit declines below), so each block has one entry.
|
|
768
|
+
const pathOf = new Map<Block, Ranges>([[b, ALL]]);
|
|
597
769
|
while (work.length) {
|
|
598
770
|
const blk = work.pop()!;
|
|
599
771
|
if (seen.has(blk)) {
|
|
@@ -628,10 +800,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
628
800
|
cases.set(k, child);
|
|
629
801
|
return true;
|
|
630
802
|
};
|
|
803
|
+
const path = pathOf.get(blk)!;
|
|
631
804
|
/** Read `child` as a NAVIGATION edge: more dispatch to walk, or a non-test leaf, which is a
|
|
632
|
-
* default candidate. Never declines — the leaf's own reading is settled below.
|
|
633
|
-
|
|
805
|
+
* default candidate. Never declines — the leaf's own reading is settled below. `onTaken`
|
|
806
|
+
* says which of `blk`'s edges it is, which decides what the range narrows to. */
|
|
807
|
+
const asNav = (child: Block, onTaken: boolean): boolean => {
|
|
634
808
|
if (isTestOn(child)) {
|
|
809
|
+
pathOf.set(child, narrow(path, ti, onTaken));
|
|
635
810
|
work.push(child);
|
|
636
811
|
return true;
|
|
637
812
|
}
|
|
@@ -642,7 +817,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
642
817
|
if (!asCase(taken, ti.k)) {
|
|
643
818
|
return null;
|
|
644
819
|
} // x==k → taken is case k
|
|
645
|
-
if (!asNav(fall)) {
|
|
820
|
+
if (!asNav(fall, false)) {
|
|
646
821
|
return null;
|
|
647
822
|
}
|
|
648
823
|
} else if (ti.cls === 'ne') {
|
|
@@ -652,12 +827,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
652
827
|
if (!asCase(fall, ti.k)) {
|
|
653
828
|
return null;
|
|
654
829
|
} // x!=k → the EQUAL side (fall) is case k
|
|
655
|
-
if (!asNav(taken)) {
|
|
830
|
+
if (!asNav(taken, true)) {
|
|
656
831
|
return null;
|
|
657
832
|
}
|
|
658
833
|
} else {
|
|
659
|
-
// relational → navigation, except where
|
|
660
|
-
//
|
|
834
|
+
// relational → navigation, except where a side the compiler declared (`switchBoundCase`)
|
|
835
|
+
// admits a single value and lands on a BODY. Two more refusals:
|
|
661
836
|
// - a bound test at the ROOT. `emit_case_nodes` emits a single-valued node's own
|
|
662
837
|
// `do_jump_if_equal` before either descent test, so a bound test always sits under
|
|
663
838
|
// another test of the same tree; one that OPENS the region did not come from this
|
|
@@ -665,11 +840,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
665
840
|
// - a singleton branch onto another TEST of the scrutinee, which is the search
|
|
666
841
|
// descending to pin the value. It is dispatch, so the walk reads it as dispatch —
|
|
667
842
|
// recovering it, or declining at PRE4 if it is not collapsible.
|
|
668
|
-
const
|
|
669
|
-
|
|
843
|
+
const sides = blk !== b ? switchBoundCase : null;
|
|
844
|
+
const kTaken = sides && !isTestOn(taken) ? singleton(narrow(path, ti, true)) : null;
|
|
845
|
+
const kFall = sides === 'either' && !isTestOn(fall) ? singleton(narrow(path, ti, false)) : null;
|
|
846
|
+
if (!(kTaken === null ? asNav(taken, true) : asCase(taken, kTaken))) {
|
|
670
847
|
return null;
|
|
671
848
|
}
|
|
672
|
-
if (!asNav(fall)) {
|
|
849
|
+
if (!(kFall === null ? asNav(fall, false) : asCase(fall, kFall))) {
|
|
673
850
|
return null;
|
|
674
851
|
}
|
|
675
852
|
}
|
|
@@ -754,29 +931,73 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
754
931
|
}
|
|
755
932
|
|
|
756
933
|
// PRE2 (arm exits), per SITE. A `break` arm closes; a `fallthrough` arm runs on into the one
|
|
757
|
-
// sibling it reaches, which `chainArms` then places directly under it
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
//
|
|
934
|
+
// sibling it reaches, which `chainArms` then places directly under it — on every path, or on
|
|
935
|
+
// some while the others reach the switch's end, which the arm's own `break;` spells (`breaks`).
|
|
936
|
+
// `unstructurable` is a shape no single linear switch spells — a body reaching two siblings, or
|
|
937
|
+
// a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
|
|
938
|
+
// which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
|
|
939
|
+
// either.)
|
|
761
940
|
const merge = ipdom.get(b) ?? stop;
|
|
762
941
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
763
942
|
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
764
943
|
const exitOf = new Map<Block, ArmExit>();
|
|
765
944
|
for (const t of siblings) {
|
|
766
|
-
exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
|
|
945
|
+
exitOf.set(t, analyzeArmExit(t, b, merge, siblings, true));
|
|
767
946
|
}
|
|
768
947
|
// A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
|
|
769
948
|
// for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
|
|
770
949
|
// same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
|
|
771
950
|
// backend because the backend's refusal is terminal (the whole function becomes a stub) while
|
|
772
951
|
// this one costs nothing but the `switch` spelling.
|
|
952
|
+
//
|
|
953
|
+
// A falling arm that also breaks is admitted wherever it falls, the `default:` arm included:
|
|
954
|
+
// for `case 1: if (c) break; default: f(); break;` the ladder if-recovery writes copies `f()`
|
|
955
|
+
// into the arm and misses the object the `switch` reaches.
|
|
773
956
|
if (
|
|
774
957
|
[...exitOf.values()].some(
|
|
775
|
-
(e) => e.kind === 'unstructurable' || (
|
|
958
|
+
(e) => e.kind === 'unstructurable' || (e.kind === 'fallthrough' && !spellSwitchFallthrough),
|
|
776
959
|
)
|
|
777
960
|
) {
|
|
778
961
|
return null;
|
|
779
962
|
}
|
|
963
|
+
// AN ARM'S `break;` NEEDS THE MERGE TO BE THE SWITCH'S END IN THIS REGION. The code after the
|
|
964
|
+
// `switch` is `structureRegion(merge, stop)`, which reads as that only where every path from
|
|
965
|
+
// the dispatch reaches `stop` through the merge. A sub-tree whose post-dominator lies PAST the
|
|
966
|
+
// region's `stop` breaks that: inside a loop body whose dispatch reaches both the latch (`stop`)
|
|
967
|
+
// and an early `return` block, ipdom is the return, so an arm's path to it would read as a
|
|
968
|
+
// switch `break` while a sibling arm runs through the latch and back to the header, which
|
|
969
|
+
// fails the whole function loud. Declined here, to if-recovery, which spells each edge as the
|
|
970
|
+
// `return` or the fall to the loop bottom it is. A merge that is the enclosing loop's EXIT is
|
|
971
|
+
// the exception the structurer spells (`exitMergeSpells`): the code after the switch is then
|
|
972
|
+
// the exit region, and an arm's path to it is the arm's `break;` (`structure.ts`, the loop-exit
|
|
973
|
+
// rules).
|
|
974
|
+
if (
|
|
975
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
976
|
+
!(merge === enclosingLoop()?.exit && exitMergeSpells(b, merge)) &&
|
|
977
|
+
reachesAvoiding(b, stop, merge)
|
|
978
|
+
) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
// AN ARM'S EDGE TO THE LOOP'S EXIT, WHEN THAT EXIT IS NOT THE MERGE. A switch admitted on a
|
|
982
|
+
// falling arm's `break;` hands every arm to `structureArm`, where an edge to the enclosing loop's
|
|
983
|
+
// exit can be spelled only as the switch's own end or as a copied returning tail; any other is
|
|
984
|
+
// refused there, loud, for the whole function (`structure.ts`, "leaves its switch for a loop
|
|
985
|
+
// exit"). With an early `return` above the dispatch, the merge is the loop bottom, and a sibling
|
|
986
|
+
// arm's `goto out` to an exit that runs on to an OUTER loop's latch is such an edge. Declined
|
|
987
|
+
// here to if-recovery, which spells that `goto` as the inner loop's `break`. The exemption is
|
|
988
|
+
// the exit that is itself a `ret`; the structurer's other one, an early-return arm owning the
|
|
989
|
+
// edge, is not asked here, which costs only the `switch` spelling.
|
|
990
|
+
const loop = enclosingLoop();
|
|
991
|
+
if (
|
|
992
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
993
|
+
loop !== null &&
|
|
994
|
+
loop.exit !== null &&
|
|
995
|
+
loop.exit !== merge &&
|
|
996
|
+
loop.exit.ops[loop.exit.ops.length - 1]?.opcode !== 'ret' &&
|
|
997
|
+
armsReach(siblings, merge, loop.header, loop.exit)
|
|
998
|
+
) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
780
1001
|
|
|
781
1002
|
// PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
|
|
782
1003
|
// actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
|
|
@@ -1008,13 +1229,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1008
1229
|
// built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
|
|
1009
1230
|
outCases.push({
|
|
1010
1231
|
values: armsByBlock.get(blk)!,
|
|
1011
|
-
body:
|
|
1232
|
+
body: structureArm(blk, to ?? merge, merge, exitOf.get(blk)),
|
|
1012
1233
|
fallsThrough: to !== null,
|
|
1013
1234
|
});
|
|
1014
1235
|
}
|
|
1015
1236
|
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
1016
1237
|
// would carry no statement, which says nothing and is not valid C89.
|
|
1017
|
-
const defBody = defaultBlk ?
|
|
1238
|
+
const defBody = defaultBlk ? structureArm(defaultBlk, merge, merge, exitOf.get(defaultBlk)) : [];
|
|
1018
1239
|
// The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
|
|
1019
1240
|
// (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
|
|
1020
1241
|
// into it and no other subtree jumps there — the two references the tree walk can count.
|
|
@@ -1060,3 +1281,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1060
1281
|
};
|
|
1061
1282
|
return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
|
|
1062
1283
|
}
|
|
1284
|
+
|
|
1285
|
+
/** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test
|
|
1286
|
+
* through {@link takenBy}, so the ranges need a check that does not share their definition. */
|
|
1287
|
+
export const __testing = { takenBy, narrow, contains, ALL };
|