@asmlift/core 0.5.0 → 0.7.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -4,7 +4,8 @@
|
|
|
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 { Block, Fn, Op, Value, successorsOf } from '../ir/core';
|
|
7
|
+
import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
|
|
8
|
+
import { ORDER_SENSITIVE_OPS } from '../ir/opcodes';
|
|
8
9
|
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
9
10
|
|
|
10
11
|
export interface SwitchRecoverDeps {
|
|
@@ -18,12 +19,41 @@ export interface SwitchRecoverDeps {
|
|
|
18
19
|
/** is this opcode an integer comparison? */
|
|
19
20
|
isCmpOpcode: (opcode: string) => boolean;
|
|
20
21
|
switchAllowsNeqCase: boolean;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
/** read a relational test whose BRANCH admits exactly one scrutinee value as that case */
|
|
23
|
+
switchAllowsBoundCase: boolean;
|
|
24
|
+
/** emit the case arms in the ASSEMBLY's block-layout order rather than by ascending case value */
|
|
25
|
+
switchArmsFollowLayout: boolean;
|
|
26
|
+
/** DECLINE a recovered tree whose own layout INTERLEAVES a test with a case body. A source
|
|
27
|
+
* `switch` front-loads its whole dispatch ahead of every arm body; an if/else-if LADDER emits
|
|
28
|
+
* each test directly above its own body. See StructureOptions for the compiled evidence, for
|
|
29
|
+
* the frontend premise this shares with `switchArmsFollowLayout`, and for what the decline
|
|
30
|
+
* costs when it is wrong. */
|
|
31
|
+
switchRequiresFrontLoadedTests: boolean;
|
|
32
|
+
/** may the emitted SOURCE say "this arm runs on into the next one"? False for a language whose
|
|
33
|
+
* `case` cannot fall through (Pascal), and then Regime A declines a falling arm to if-recovery
|
|
34
|
+
* — the behaviourally identical recovery that backend CAN print. See StructureOptions. */
|
|
35
|
+
spellSwitchFallthrough: boolean;
|
|
36
|
+
/** does emitting this block's ops carry a statement beyond the ops themselves? Collapsing a
|
|
37
|
+
* test block into a `switch` re-renders its ops at their uses and emits no side effects for it,
|
|
38
|
+
* so any op that renders as a STATEMENT of its own loses that statement. Two produce one: a
|
|
39
|
+
* def-site ANCHORED merge copy (structure.ts anchorConstCopies), whose edge copy stays
|
|
40
|
+
* suppressed, and a MATERIALIZED def, whose `v = …` assignment renders only here while its uses
|
|
41
|
+
* read the bare name — leaving a local declared and never assigned. */
|
|
42
|
+
emitsOwnStatement: (blk: Block) => boolean;
|
|
43
|
+
/** Where a value is WRITTEN: its block for a parameter, its def op's block otherwise — the half
|
|
44
|
+
* of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
|
|
45
|
+
* both halves for def-site anchoring, rather than indexing the parameters a second time. */
|
|
46
|
+
blockOf: (v: Value) => Block | undefined;
|
|
47
|
+
/** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
|
|
48
|
+
* ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
|
|
49
|
+
* (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
|
|
50
|
+
* regime takes it as a dependency rather than implementing it a second time, exactly as Regime
|
|
51
|
+
* B takes `argAssignsFor`. Null ⇒ no single hoisted statement spells them; the caller declines
|
|
52
|
+
* to if-recovery. `liveAt` are the blocks whose live-in names the writes must not clobber. */
|
|
53
|
+
hoistDispatchCopies: (
|
|
54
|
+
edges: readonly { pred: Block; succ: { block: Block; args: Value[] } }[],
|
|
55
|
+
liveAt: readonly Block[],
|
|
56
|
+
) => Stmt[] | null;
|
|
27
57
|
expr: (v: Value) => Expr;
|
|
28
58
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
29
59
|
}
|
|
@@ -34,17 +64,181 @@ export interface SwitchRecoverDeps {
|
|
|
34
64
|
* - `break` every path out of the arm reaches the switch's merge (or returns / loops
|
|
35
65
|
* inside the arm). The ordinary closed arm.
|
|
36
66
|
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
37
|
-
* Only spellable when that sibling is the arm emitted NEXT
|
|
38
|
-
*
|
|
67
|
+
* Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
|
|
68
|
+
* below arranges and both regimes then re-read off the emission array (see the
|
|
69
|
+
* l3/ast.ts non-neutrality note).
|
|
39
70
|
* - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
|
|
40
|
-
* "out to the merge". C needs a `goto` for those
|
|
71
|
+
* "out to the merge". C needs a `goto` for those. Regime A declines to
|
|
72
|
+
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD. */
|
|
41
73
|
export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
|
|
42
74
|
|
|
43
75
|
export interface SwitchRecovery {
|
|
44
76
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
45
|
-
/** shared with the Regime-B (`switch_br`) path in structure.ts
|
|
46
|
-
* this returns;
|
|
77
|
+
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
78
|
+
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
79
|
+
* Regime B, which has no fallback, fails loud. */
|
|
47
80
|
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
|
|
81
|
+
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
82
|
+
* regimes read it from one definition (and one statement of what it rests on). */
|
|
83
|
+
layoutIndex: (blk: Block) => number;
|
|
84
|
+
/** where the `default:` label goes among the EMITTED arms, or `undefined` for C's last position —
|
|
85
|
+
* shared with Regime B so both regimes state those refusals once. */
|
|
86
|
+
defaultLayoutPos: (
|
|
87
|
+
defaultBlk: Block,
|
|
88
|
+
arms: readonly { entry: Block; fallsThrough: boolean }[],
|
|
89
|
+
opts: { placedByDispatch: boolean; orderIntact: boolean },
|
|
90
|
+
) => number | undefined;
|
|
91
|
+
/** ONE linear emission order for a set of arms, re-threaded so every falling arm sits directly
|
|
92
|
+
* above the one it falls into — or null when no linear order spells them. Shared with Regime B,
|
|
93
|
+
* so the chain, the adjacency it guarantees and the three refusals below it have one definition
|
|
94
|
+
* (Regime A maps null to if-recovery, Regime B to a loud StructureError). */
|
|
95
|
+
chainArms: (order: Block[], dflt: Block | null, exitOf: Map<Block, ArmExit>) => Block[] | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A block with no body of its own: no params, and one op that only LEAVES. `ret` qualifies as
|
|
99
|
+
* well as `br` because raise/retsink.ts rewrites the one into the other — a cross-jumped arm
|
|
100
|
+
* body has two dispatch preds, which is exactly the shape that makes retsink sink the merge's
|
|
101
|
+
* return into every leaf, the fall-out jumps included. */
|
|
102
|
+
function isBareExit(blk: Block): boolean {
|
|
103
|
+
return isBodyless(blk) && (blk.ops[0].opcode === 'br' || blk.ops[0].opcode === 'ret');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Are these two blocks the SAME bare exit — the same jump with the same args, or the same return
|
|
107
|
+
* of the same values? Neither has a body, so two of them are indistinguishable at emission. */
|
|
108
|
+
function sameBareExit(a: Block, c: Block): boolean {
|
|
109
|
+
if (a === c) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (!isBareExit(a) || !isBareExit(c) || a.ops[0].opcode !== c.ops[0].opcode) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const same = (x: readonly Value[], y: readonly Value[]) => x.length === y.length && x.every((v, i) => v === y[i]);
|
|
116
|
+
if (a.ops[0].opcode === 'ret') {
|
|
117
|
+
return same(a.ops[0].operands, c.ops[0].operands);
|
|
118
|
+
}
|
|
119
|
+
const [x, y] = [a, c].map((blk) => blk.ops[0].successors[0]);
|
|
120
|
+
return x.block === y.block && same(x.args, y.args);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TestInfo {
|
|
124
|
+
x: Value;
|
|
125
|
+
k: number;
|
|
126
|
+
cls: 'eq' | 'ne' | 'rel';
|
|
127
|
+
opcode: string;
|
|
128
|
+
xOnLeft: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
|
|
132
|
+
// verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
|
|
133
|
+
// Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
|
|
134
|
+
function evalCmp(opcode: string, xOnLeft: boolean, xv: number, k: number): boolean {
|
|
135
|
+
const uns = opcode.startsWith('icmp_u');
|
|
136
|
+
const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
|
|
137
|
+
const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
|
|
138
|
+
switch (opcode) {
|
|
139
|
+
case 'icmp_eq':
|
|
140
|
+
return l === r;
|
|
141
|
+
case 'icmp_ne':
|
|
142
|
+
return l !== r;
|
|
143
|
+
case 'icmp_slt':
|
|
144
|
+
case 'icmp_ult':
|
|
145
|
+
return l < r;
|
|
146
|
+
case 'icmp_sle':
|
|
147
|
+
case 'icmp_ule':
|
|
148
|
+
return l <= r;
|
|
149
|
+
case 'icmp_sgt':
|
|
150
|
+
case 'icmp_ugt':
|
|
151
|
+
return l > r;
|
|
152
|
+
case 'icmp_sge':
|
|
153
|
+
case 'icmp_uge':
|
|
154
|
+
return l >= r;
|
|
155
|
+
default:
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Which single scrutinee value does this relational test's BRANCH admit, if exactly one? A
|
|
161
|
+
// relational side is a HALF-LINE in the compare's own ordering, so it can hold one value only at
|
|
162
|
+
// a domain endpoint — which is why testing the two endpoints and their neighbours decides it,
|
|
163
|
+
// with no interval lattice. `x < 1` over an unsigned scrutinee admits `{0}` and is agbcc's
|
|
164
|
+
// spelling of `case 0` in a balanced search: `emit_case_nodes` tests the subtree BOUND, not the
|
|
165
|
+
// value, whenever the remaining range has collapsed to one. Read as navigation instead, that
|
|
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;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
|
|
194
|
+
* fall-through chain is emitted contiguously and takes the position of its HEAD in `order`,
|
|
195
|
+
* which is the caller's own arm-order policy — so with no fall-through every chain is a
|
|
196
|
+
* singleton and `order` comes back unchanged. `dflt` is the `default:` arm's block when it has
|
|
197
|
+
* one, and it is pinned LAST because that is where C prints the label.
|
|
198
|
+
*
|
|
199
|
+
* THREE REFUSALS (null ⇒ the caller declines), each a shape no single linear order spells:
|
|
200
|
+
* - two arms falling into the SAME arm — C drops into an arm from above along one edge only;
|
|
201
|
+
* - the `default:` arm falling into a case, since nothing is emitted below it;
|
|
202
|
+
* - a fall-through CYCLE, whose members are all fallen-into and so are never a chain head. */
|
|
203
|
+
export function chainArms(order: Block[], dflt: Block | null, exitOf: Map<Block, ArmExit>): Block[] | null {
|
|
204
|
+
const next = new Map<Block, Block>();
|
|
205
|
+
const fallenInto = new Set<Block>();
|
|
206
|
+
for (const e of [...order, ...(dflt ? [dflt] : [])]) {
|
|
207
|
+
const x = exitOf.get(e);
|
|
208
|
+
if (x?.kind !== 'fallthrough') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (e === dflt || fallenInto.has(x.to)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
fallenInto.add(x.to);
|
|
215
|
+
next.set(e, x.to);
|
|
216
|
+
}
|
|
217
|
+
const chains: Block[][] = [];
|
|
218
|
+
const seen = new Set<Block>();
|
|
219
|
+
let intoDefault = -1;
|
|
220
|
+
for (const head of order) {
|
|
221
|
+
if (fallenInto.has(head)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const chain: Block[] = [];
|
|
225
|
+
for (let cur: Block | undefined = head; cur !== undefined && cur !== dflt && !seen.has(cur); cur = next.get(cur)) {
|
|
226
|
+
seen.add(cur);
|
|
227
|
+
chain.push(cur);
|
|
228
|
+
}
|
|
229
|
+
if (dflt !== null && next.get(chain[chain.length - 1]) === dflt) {
|
|
230
|
+
intoDefault = chains.length;
|
|
231
|
+
}
|
|
232
|
+
chains.push(chain);
|
|
233
|
+
}
|
|
234
|
+
// A cycle's every member is fallen-into, so none of them is a head and none is walked.
|
|
235
|
+
if (seen.size !== order.length) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (intoDefault >= 0) {
|
|
239
|
+
chains.push(...chains.splice(intoDefault, 1));
|
|
240
|
+
}
|
|
241
|
+
return chains.flat();
|
|
48
242
|
}
|
|
49
243
|
|
|
50
244
|
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
@@ -57,14 +251,110 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
57
251
|
isNamed,
|
|
58
252
|
isCmpOpcode,
|
|
59
253
|
switchAllowsNeqCase,
|
|
60
|
-
|
|
254
|
+
switchAllowsBoundCase,
|
|
255
|
+
switchArmsFollowLayout,
|
|
256
|
+
switchRequiresFrontLoadedTests,
|
|
257
|
+
spellSwitchFallthrough,
|
|
258
|
+
emitsOwnStatement,
|
|
259
|
+
blockOf,
|
|
260
|
+
hoistDispatchCopies,
|
|
61
261
|
expr,
|
|
62
262
|
structureRegion,
|
|
63
263
|
} = deps;
|
|
64
264
|
|
|
265
|
+
// A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
|
|
266
|
+
// of layout below, and true PER FRONTEND rather than of the IR:
|
|
267
|
+
// - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
|
|
268
|
+
// - ppc.ts does not. It APPENDS a synthetic return block (`synthReturn`) at the end of the list
|
|
269
|
+
// for every conditional-return branch, wherever in the stream that branch sits, so its list
|
|
270
|
+
// is not address order at all;
|
|
271
|
+
// - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
|
|
272
|
+
// .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
|
|
273
|
+
// TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
|
|
274
|
+
// - `switchArmsFollowLayout` (arm ORDER, the sort below) needs the full claim — no block moved
|
|
275
|
+
// at all — because it PLACES the arms from the layout;
|
|
276
|
+
// - `switchRequiresFrontLoadedTests` (PRE5, whether to recover at all) needs only that no case
|
|
277
|
+
// BODY was moved above a dispatch test, which a compiler with a scheduler can still satisfy;
|
|
278
|
+
// `MIPS_GCC` declares `switchRequiresFrontLoadedTests` and NOT `switchArmsFollowLayout` for
|
|
279
|
+
// exactly that reason — it has a scheduler and fills delay slots (target.ts).
|
|
280
|
+
// Both are therefore claims about a target's FRONTEND as much as about its compiler, and a target
|
|
281
|
+
// opts in on both halves — which is why PPC_MWCC, whose frontend fails the frontend half outright,
|
|
282
|
+
// declares neither. Anything added below that reads `layoutIndex` inherits the frontend half and
|
|
283
|
+
// owes a statement of which strength of the compiler half it needs.
|
|
284
|
+
const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
|
|
285
|
+
const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
|
|
286
|
+
|
|
287
|
+
// Where the `default:` label goes among the arms, as a COUNT of the arms laid out before it — the
|
|
288
|
+
// same evidence the case bodies carry, read the same way. Compiled at every position of a 3- to
|
|
289
|
+
// 8-case switch, the default's block lands where the source wrote it. `undefined` ⇒ C's
|
|
290
|
+
// conventional last position, which is what every other producer of this node means.
|
|
291
|
+
//
|
|
292
|
+
// SEVEN WITHHOLDINGS, W1..W7, numbered in the order the code asks them so the prose and the code
|
|
293
|
+
// index ONE list, and stated here so both regimes state them once. W1 is the target's own opt-in;
|
|
294
|
+
// W2..W4 are about a block the DISPATCH placed rather than the arm; W5..W7 are about
|
|
295
|
+
// fall-through — W5 about the LIST the count would index, W6 and W7 about the POSITION it names.
|
|
296
|
+
//
|
|
297
|
+
// W1 the target does not read arm order off the layout at all (`switchArmsFollowLayout`).
|
|
298
|
+
// W2 a block with no body of its own is one the dispatch minted (`b .Ldefault`), and which of
|
|
299
|
+
// several such the collapse below keeps is a walk-order accident.
|
|
300
|
+
// W3 the walk already read that block as a CASE arm — a dense table sends every unwritten
|
|
301
|
+
// value's slot to the default's block, so grouping the slots gives that block an arm of its
|
|
302
|
+
// own and its index is where THAT arm sits.
|
|
303
|
+
// W4 `emit_case_nodes` ends every exhausted subtree with `emit_jump_if_reachable
|
|
304
|
+
// (default_label)` and `expand_end_case` reorders the whole dispatch, those jumps included,
|
|
305
|
+
// ahead of the arm bodies — so a jump survives as a plain FALL-THROUGH exactly when the
|
|
306
|
+
// default's body is the arm the source wrote FIRST. That reading holds only while a second
|
|
307
|
+
// subtree still names the label: a two-case chain names it once, and agbcc then lays that
|
|
308
|
+
// block right after the tests whatever the source wrote, both spellings compiling to
|
|
309
|
+
// identical instructions.
|
|
310
|
+
// W5 the chain RE-THREADED the arm order (`orderIntact` false), so the emitted list is no
|
|
311
|
+
// longer the one the layout count describes and no position in it means what the count
|
|
312
|
+
// says. This one is whole-switch because the re-threading is. A per-position reading —
|
|
313
|
+
// bracket the label between the two arms that straddle it in LAYOUT, then map that into the
|
|
314
|
+
// emitted list — is possible and unbuilt, and hard to need: a compiler that lays bodies out
|
|
315
|
+
// in source order already writes a falling arm directly above its target, so the order it
|
|
316
|
+
// declares is a chain order too.
|
|
317
|
+
// W6 the LAST emitted arm falls through, which can only be into the default (the adjacency
|
|
318
|
+
// check leaves no other target) — the label must then be last, which IS `undefined`.
|
|
319
|
+
// W7 the position lands directly after a falling arm, where printing the label would divert
|
|
320
|
+
// that arm into the default. cfamily.ts fails loud on exactly that, and this is the producer
|
|
321
|
+
// side of the same rule.
|
|
322
|
+
//
|
|
323
|
+
// A switch with a chain elsewhere keeps its evidence: the reason to withhold is the position,
|
|
324
|
+
// never "some arm somewhere falls".
|
|
325
|
+
const defaultLayoutPos = (
|
|
326
|
+
defaultBlk: Block,
|
|
327
|
+
arms: readonly { entry: Block; fallsThrough: boolean }[],
|
|
328
|
+
opts: { placedByDispatch: boolean; orderIntact: boolean },
|
|
329
|
+
): number | undefined => {
|
|
330
|
+
if (!switchArmsFollowLayout) {
|
|
331
|
+
return undefined; // W1 target does not read layout
|
|
332
|
+
}
|
|
333
|
+
if (isBareExit(defaultBlk)) {
|
|
334
|
+
return undefined; // W2 dispatch-minted bodyless block
|
|
335
|
+
}
|
|
336
|
+
if (arms.some((a) => a.entry === defaultBlk)) {
|
|
337
|
+
return undefined; // W3 already an emitted arm
|
|
338
|
+
}
|
|
339
|
+
if (opts.placedByDispatch) {
|
|
340
|
+
return undefined; // W4 dispatch ran out into it
|
|
341
|
+
}
|
|
342
|
+
if (!opts.orderIntact) {
|
|
343
|
+
return undefined; // W5 chain re-threaded the arms
|
|
344
|
+
}
|
|
345
|
+
if (arms[arms.length - 1]?.fallsThrough) {
|
|
346
|
+
return undefined; // W6 last arm falls through
|
|
347
|
+
}
|
|
348
|
+
const at = arms.filter((a) => layoutIndex(a.entry) < layoutIndex(defaultBlk)).length;
|
|
349
|
+
// W7 the label would land directly after a falling arm
|
|
350
|
+
return at > 0 && arms[at - 1].fallsThrough ? undefined : at;
|
|
351
|
+
};
|
|
352
|
+
|
|
65
353
|
// --- Regime A: comparison-tree switch recovery ----------------------------------------------------
|
|
66
|
-
// Every ambiguity declines.
|
|
67
|
-
// scrutinee identity/dominance,
|
|
354
|
+
// Every ambiguity declines. Five preconditions are enforced below, annotated PRE1..PRE5:
|
|
355
|
+
// scrutinee identity/dominance, ARM EXITS (per site: `break`, `fallthrough` — which `chainArms`
|
|
356
|
+
// then places — or a decline), concrete interval consistency, test purity, and — where the target
|
|
357
|
+
// has declared that its layout answers it — which SPELLING the source wrote.
|
|
68
358
|
|
|
69
359
|
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
70
360
|
// `250 << 2` for a large sparse case) to a number — else null.
|
|
@@ -137,20 +427,23 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
137
427
|
}
|
|
138
428
|
};
|
|
139
429
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// the switch
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
430
|
+
// TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
|
|
431
|
+
// both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
|
|
432
|
+
//
|
|
433
|
+
// PRE4 (purity) is the second. A collapsed test block's ops re-render at whichever use inlines
|
|
434
|
+
// them, at a point the switch decides — so the question is motion, not deletion, and
|
|
435
|
+
// `ORDER_SENSITIVE_OPS` is the set that asks it. NOT the trapping divides: a use is dominated by
|
|
436
|
+
// its def, so the re-rendered op runs on a subset of the paths it already ran on — nothing is
|
|
437
|
+
// speculated. `emitsOwnStatement` covers what motion cannot save: a statement belonging to the
|
|
438
|
+
// block rather than to a use. The root is exempt from all of it — its ops are already emitted as
|
|
439
|
+
// sideEffects(b) before the switch.
|
|
440
|
+
//
|
|
441
|
+
// A block that tests the scrutinee and is NOT collapsible is still dispatch, so the walk must
|
|
442
|
+
// read it as dispatch and decline, never re-read it as a case body — that would spell an arm
|
|
443
|
+
// 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);
|
|
446
|
+
const testInfo = (blk: Block): TestInfo | null => {
|
|
154
447
|
const term = blk.ops[blk.ops.length - 1];
|
|
155
448
|
if (term.opcode !== 'cond_br') {
|
|
156
449
|
return null;
|
|
@@ -159,9 +452,6 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
159
452
|
if (!cmp || !isCmpOpcode(cmp.opcode)) {
|
|
160
453
|
return null;
|
|
161
454
|
}
|
|
162
|
-
if (!isRoot && (blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode)) || emitsAnchoredWrite(blk))) {
|
|
163
|
-
return null;
|
|
164
|
-
} // PRE4 — anchored writes included: discarded with the block, while their edge copies stay suppressed
|
|
165
455
|
// Which operand is the scrutinee, which is the constant?
|
|
166
456
|
const [lo, ro] = cmp.operands;
|
|
167
457
|
const lc = evalConst(lo),
|
|
@@ -182,45 +472,16 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
182
472
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
183
473
|
};
|
|
184
474
|
|
|
185
|
-
// Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
|
|
186
|
-
// verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
|
|
187
|
-
// Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
|
|
188
|
-
const evalCmp = (opcode: string, xOnLeft: boolean, xv: number, k: number): boolean => {
|
|
189
|
-
const uns = opcode.startsWith('icmp_u');
|
|
190
|
-
const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
|
|
191
|
-
const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
|
|
192
|
-
switch (opcode) {
|
|
193
|
-
case 'icmp_eq':
|
|
194
|
-
return l === r;
|
|
195
|
-
case 'icmp_ne':
|
|
196
|
-
return l !== r;
|
|
197
|
-
case 'icmp_slt':
|
|
198
|
-
case 'icmp_ult':
|
|
199
|
-
return l < r;
|
|
200
|
-
case 'icmp_sle':
|
|
201
|
-
case 'icmp_ule':
|
|
202
|
-
return l <= r;
|
|
203
|
-
case 'icmp_sgt':
|
|
204
|
-
case 'icmp_ugt':
|
|
205
|
-
return l > r;
|
|
206
|
-
case 'icmp_sge':
|
|
207
|
-
case 'icmp_uge':
|
|
208
|
-
return l >= r;
|
|
209
|
-
default:
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
475
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
215
476
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
216
477
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
217
478
|
// switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
|
|
218
479
|
// ordinary `break`, not as falling into the default.
|
|
219
480
|
//
|
|
220
|
-
// Region membership is `dom(blk) ∋ b
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
481
|
+
// Region membership is `dom(blk) ∋ b`: a block NOT dominated by the switch is outside this
|
|
482
|
+
// switch's region and is not walked. It IS recorded as an escape, because an arm that can leave
|
|
483
|
+
// sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
|
|
484
|
+
// record, so an arm that reaches no sibling still closes with a plain `break`.
|
|
224
485
|
//
|
|
225
486
|
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
226
487
|
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
@@ -261,7 +522,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
261
522
|
}
|
|
262
523
|
// Name what is actually missing. These three are different facts, and only the first is a shape
|
|
263
524
|
// C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
|
|
264
|
-
const names = () => [...into].map((x) => `#${
|
|
525
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
265
526
|
if (into.size > 1) {
|
|
266
527
|
return {
|
|
267
528
|
kind: 'unstructurable',
|
|
@@ -283,15 +544,8 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
283
544
|
};
|
|
284
545
|
};
|
|
285
546
|
|
|
286
|
-
/** Every arm closed (`break`)? The precondition Regime A needs — it has a behaviourally identical
|
|
287
|
-
* fallback (if-recovery), so it declines on anything else instead of recovering fall-through. */
|
|
288
|
-
const allArmsClosed = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
|
|
289
|
-
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
290
|
-
return [...siblings].every((t) => analyzeArmExit(t, b, merge, siblings).kind === 'break');
|
|
291
|
-
};
|
|
292
|
-
|
|
293
547
|
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
294
|
-
const root = testInfo(b
|
|
548
|
+
const root = testInfo(b);
|
|
295
549
|
if (!root) {
|
|
296
550
|
return null;
|
|
297
551
|
}
|
|
@@ -314,27 +568,10 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
314
568
|
// Walk the test tree. `cases`: value → case-entry block. `defaultCands`: leaves reached without an
|
|
315
569
|
// equality pin. A test-block DAG cycle, or a `!=` case when the compiler disallows it, declines.
|
|
316
570
|
const cases = new Map<number, Block>();
|
|
571
|
+
// Reached through `forwardingTarget`: agbcc's binary-search layout branches to the shared default
|
|
572
|
+
// through empty `b .Ldef` blocks, and without resolving them each becomes a DISTINCT default
|
|
573
|
+
// candidate and the whole tree declines.
|
|
317
574
|
const defaultCands = new Set<Block>();
|
|
318
|
-
// Skip pure forwarding blocks — a block whose only op is an unconditional `br` (no side effects, no
|
|
319
|
-
// params). agbcc's binary-search layout branches to the shared default through such empty `b .Ldef`
|
|
320
|
-
// blocks; without skipping them each becomes a DISTINCT default candidate and the whole tree declines.
|
|
321
|
-
const skipForward = (blk: Block): Block => {
|
|
322
|
-
let cur = blk;
|
|
323
|
-
const guard = new Set<Block>();
|
|
324
|
-
// Only skip a truly empty forwarding block: a lone `br` with no params AND no successor ARGS —
|
|
325
|
-
// an edge that carries a phi arg is NOT transparent (skipping it would drop that assignment).
|
|
326
|
-
while (
|
|
327
|
-
cur.ops.length === 1 &&
|
|
328
|
-
cur.ops[0].opcode === 'br' &&
|
|
329
|
-
cur.params.length === 0 &&
|
|
330
|
-
cur.ops[0].successors[0].args.length === 0 &&
|
|
331
|
-
!guard.has(cur)
|
|
332
|
-
) {
|
|
333
|
-
guard.add(cur);
|
|
334
|
-
cur = cur.ops[0].successors[0].block;
|
|
335
|
-
}
|
|
336
|
-
return cur;
|
|
337
|
-
};
|
|
338
575
|
// Concretely SIMULATE the decision tree for a scrutinee value `xv`, returning the leaf block it
|
|
339
576
|
// reaches (or null on an unexpected cycle). This is PRE3 done concretely: it lets us verify each
|
|
340
577
|
// recovered case value actually routes to its recorded body in the ORIGINAL tree.
|
|
@@ -342,7 +579,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
342
579
|
let cur = b;
|
|
343
580
|
const guard = new Set<Block>();
|
|
344
581
|
for (;;) {
|
|
345
|
-
const ti = testInfo(cur
|
|
582
|
+
const ti = testInfo(cur);
|
|
346
583
|
if (!ti || ti.x !== scrut) {
|
|
347
584
|
return cur;
|
|
348
585
|
} // reached a leaf (case body / default)
|
|
@@ -352,7 +589,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
352
589
|
guard.add(cur);
|
|
353
590
|
const term = cur.ops[cur.ops.length - 1];
|
|
354
591
|
const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
|
|
355
|
-
cur =
|
|
592
|
+
cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
|
|
356
593
|
}
|
|
357
594
|
};
|
|
358
595
|
const seen = new Set<Block>();
|
|
@@ -363,59 +600,76 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
363
600
|
return null;
|
|
364
601
|
} // a test-block DAG cycle → decline
|
|
365
602
|
seen.add(blk);
|
|
366
|
-
const ti = testInfo(blk
|
|
603
|
+
const ti = testInfo(blk);
|
|
367
604
|
if (!ti || ti.x !== scrut) {
|
|
368
605
|
return null;
|
|
369
606
|
} // PRE1: every test is on the SAME Value
|
|
607
|
+
if (blk !== b && !collapsible(blk)) {
|
|
608
|
+
return null;
|
|
609
|
+
} // PRE4
|
|
370
610
|
const term = blk.ops[blk.ops.length - 1];
|
|
371
|
-
const taken =
|
|
372
|
-
fall =
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
611
|
+
const taken = forwardingTarget(term.successors[0].block),
|
|
612
|
+
fall = forwardingTarget(term.successors[1].block);
|
|
613
|
+
const isTestOn = (child: Block) => {
|
|
614
|
+
const t = testInfo(child);
|
|
615
|
+
return !!t && t.x === scrut;
|
|
616
|
+
};
|
|
617
|
+
/** Read `child` as the BODY of case `k`. A case entry with a PHI is admitted: the dispatch
|
|
618
|
+
* edge binds those parameters, and `hoistDispatchCopies` re-emits that binding once above
|
|
619
|
+
* the `switch` (or declines the whole recovery). A fall-through chain's accumulator crosses
|
|
620
|
+
* every arm as exactly such a parameter, so refusing it here refuses the whole family. */
|
|
621
|
+
const asCase = (child: Block, k: number): boolean => {
|
|
622
|
+
if (isTestOn(child)) {
|
|
623
|
+
return false;
|
|
624
|
+
} // a case target that's a test → decline
|
|
625
|
+
if (cases.has(k)) {
|
|
626
|
+
return false;
|
|
627
|
+
} // duplicate case value → decline
|
|
628
|
+
cases.set(k, child);
|
|
629
|
+
return true;
|
|
630
|
+
};
|
|
631
|
+
/** 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
|
+
const asNav = (child: Block): boolean => {
|
|
634
|
+
if (isTestOn(child)) {
|
|
390
635
|
work.push(child);
|
|
391
636
|
return true;
|
|
392
637
|
}
|
|
393
|
-
defaultCands.add(child);
|
|
638
|
+
defaultCands.add(child);
|
|
394
639
|
return true;
|
|
395
640
|
};
|
|
396
641
|
if (ti.cls === 'eq') {
|
|
397
|
-
if (!
|
|
642
|
+
if (!asCase(taken, ti.k)) {
|
|
398
643
|
return null;
|
|
399
644
|
} // x==k → taken is case k
|
|
400
|
-
if (!
|
|
645
|
+
if (!asNav(fall)) {
|
|
401
646
|
return null;
|
|
402
647
|
}
|
|
403
648
|
} else if (ti.cls === 'ne') {
|
|
404
649
|
if (!switchAllowsNeqCase) {
|
|
405
650
|
return null;
|
|
406
651
|
} // per-compiler gate
|
|
407
|
-
if (!
|
|
652
|
+
if (!asCase(fall, ti.k)) {
|
|
408
653
|
return null;
|
|
409
654
|
} // x!=k → the EQUAL side (fall) is case k
|
|
410
|
-
if (!
|
|
655
|
+
if (!asNav(taken)) {
|
|
411
656
|
return null;
|
|
412
657
|
}
|
|
413
658
|
} else {
|
|
414
|
-
// relational →
|
|
415
|
-
|
|
659
|
+
// relational → navigation, except where the BRANCH has collapsed to a single value and
|
|
660
|
+
// lands on a BODY, on a compiler that declared the spelling. Two more refusals:
|
|
661
|
+
// - a bound test at the ROOT. `emit_case_nodes` emits a single-valued node's own
|
|
662
|
+
// `do_jump_if_equal` before either descent test, so a bound test always sits under
|
|
663
|
+
// another test of the same tree; one that OPENS the region did not come from this
|
|
664
|
+
// dispatch, and reading it as a case turns a comparison chain into a `switch`;
|
|
665
|
+
// - a singleton branch onto another TEST of the scrutinee, which is the search
|
|
666
|
+
// descending to pin the value. It is dispatch, so the walk reads it as dispatch —
|
|
667
|
+
// recovering it, or declining at PRE4 if it is not collapsible.
|
|
668
|
+
const k = switchAllowsBoundCase && blk !== b && !isTestOn(taken) ? singletonTaken(ti) : null;
|
|
669
|
+
if (!(k === null ? asNav(taken) : asCase(taken, k))) {
|
|
416
670
|
return null;
|
|
417
671
|
}
|
|
418
|
-
if (!
|
|
672
|
+
if (!asNav(fall)) {
|
|
419
673
|
return null;
|
|
420
674
|
}
|
|
421
675
|
}
|
|
@@ -426,14 +680,64 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
426
680
|
} // not worth a switch (m2c: ≥2 cases)
|
|
427
681
|
// The default is the single non-test leaf that is NOT a case body. 0 → no default; ≥2 distinct → decline.
|
|
428
682
|
const caseBlocks = new Set(cases.values());
|
|
429
|
-
const
|
|
430
|
-
|
|
683
|
+
const leaves = new Set([...defaultCands].filter((d) => !caseBlocks.has(d)));
|
|
684
|
+
// RESOLVE THROUGH a bare jump onto another candidate. `forwardingTarget` (ir/core.ts) stops at
|
|
685
|
+
// a `br` that carries block ARGUMENTS — skipping it would drop the values it supplies — so a
|
|
686
|
+
// leaf holding `b .Ldefault(v)` and `.Ldefault` itself arrive here as two distinct candidates
|
|
687
|
+
// even though one merely jumps to the other. They are not two defaults: the jumping leaf emits
|
|
688
|
+
// nothing of its own, and the values its `br` hands the other's parameters are the values the
|
|
689
|
+
// DISPATCH hands them. So this is `forwardingTarget`'s walk with the args-carrying step
|
|
690
|
+
// ADMITTED rather than refused, and it does not drop those values either — the step becomes one
|
|
691
|
+
// more dispatch edge, hoisted with the rest, where the hoist's disagreement rule decides
|
|
692
|
+
// whether the two paths can share one statement. (ir/core.ts lists the non-callers and why.)
|
|
693
|
+
//
|
|
694
|
+
// A cycle of such jumps has no target to resolve to and declines. Leaves that pass DIFFERENT
|
|
695
|
+
// values, or that have a body, are untouched and are still two defaults below. A SET so a leaf
|
|
696
|
+
// walked twice — L → X → D reaches X from L's walk and again from X's own turn in the loop —
|
|
697
|
+
// is recorded once.
|
|
698
|
+
const throughEdges = new Set<Block>();
|
|
699
|
+
const resolveDefault = (d: Block): Block | null => {
|
|
700
|
+
const walked = new Set<Block>();
|
|
701
|
+
let cur = d;
|
|
702
|
+
for (;;) {
|
|
703
|
+
if (walked.has(cur)) {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
walked.add(cur);
|
|
707
|
+
const t = cur.ops[0];
|
|
708
|
+
if (!isBareExit(cur) || t.opcode !== 'br' || !leaves.has(t.successors[0].block)) {
|
|
709
|
+
return cur;
|
|
710
|
+
}
|
|
711
|
+
throughEdges.add(cur);
|
|
712
|
+
cur = t.successors[0].block;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const defaults: Block[] = [];
|
|
716
|
+
for (const d of leaves) {
|
|
717
|
+
const r = resolveDefault(d);
|
|
718
|
+
if (r === null) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
if (!defaults.includes(r)) {
|
|
722
|
+
defaults.push(r);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// ONE default reached by SEVERAL leaves. `balance_case_nodes`/`emit_case_nodes` give each
|
|
726
|
+
// subtree that runs out of case values its own jump to the default, so agbcc's four-case tree
|
|
727
|
+
// reaches it through two `b .Ldefault` blocks, which comparing candidates by BLOCK would count
|
|
728
|
+
// as two different defaults and decline. Two leaves are the same default when each is
|
|
729
|
+
// a bare EXIT to the same place carrying the same values: nothing about them can then differ,
|
|
730
|
+
// so the representative emits what either would. Anything else — a leaf with a body, two
|
|
731
|
+
// leaves passing different values — is still two defaults and still declines.
|
|
732
|
+
if (defaults.length > 1 && !defaults.every((d) => sameBareExit(defaults[0], d))) {
|
|
431
733
|
return null;
|
|
432
734
|
}
|
|
433
735
|
const defaultBlk = defaults[0] ?? null;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
736
|
+
// A default entry that takes BLOCK PARAMETERS is admitted on the same terms a case entry is:
|
|
737
|
+
// the dispatch edge binds them and `hoistDispatchCopies` re-emits that binding above the
|
|
738
|
+
// `switch`. The hazard it stands over is silent — without that re-emission,
|
|
739
|
+
// `switch (x) { case 1: … case 2: … }` whose fall-out edge also carried `w = 0` drops the write
|
|
740
|
+
// and looks entirely ordinary doing it — so the admission is only as good as the hoist.
|
|
437
741
|
// A default candidate that is ALSO a case body means a relational edge hit a case leaf → ambiguous.
|
|
438
742
|
if ([...defaultCands].some((d) => caseBlocks.has(d))) {
|
|
439
743
|
return null;
|
|
@@ -449,13 +753,28 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
449
753
|
return null;
|
|
450
754
|
}
|
|
451
755
|
|
|
452
|
-
// PRE2 (
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
756
|
+
// 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. `unstructurable` is a
|
|
758
|
+
// shape no single linear switch spells — a body reaching two siblings, or a sibling on one path
|
|
759
|
+
// and the switch's end on another — and Regime A declines to if-recovery, which spells every
|
|
760
|
+
// one of those edges. (Regime B reads the same verdicts and fails LOUD on either.)
|
|
456
761
|
const merge = ipdom.get(b) ?? stop;
|
|
457
762
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
458
|
-
|
|
763
|
+
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
764
|
+
const exitOf = new Map<Block, ArmExit>();
|
|
765
|
+
for (const t of siblings) {
|
|
766
|
+
exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
|
|
767
|
+
}
|
|
768
|
+
// A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
|
|
769
|
+
// for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
|
|
770
|
+
// same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
|
|
771
|
+
// backend because the backend's refusal is terminal (the whole function becomes a stub) while
|
|
772
|
+
// this one costs nothing but the `switch` spelling.
|
|
773
|
+
if (
|
|
774
|
+
[...exitOf.values()].some(
|
|
775
|
+
(e) => e.kind === 'unstructurable' || (!spellSwitchFallthrough && e.kind === 'fallthrough'),
|
|
776
|
+
)
|
|
777
|
+
) {
|
|
459
778
|
return null;
|
|
460
779
|
}
|
|
461
780
|
|
|
@@ -469,29 +788,275 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
469
788
|
}
|
|
470
789
|
}
|
|
471
790
|
|
|
472
|
-
//
|
|
473
|
-
//
|
|
791
|
+
// PRE5 (WHICH SPELLING THE SOURCE WROTE), per SITE, and only where the caller has declared
|
|
792
|
+
// that the layout answers it. Everything above establishes that this tree CAN be spelled as a
|
|
793
|
+
// `switch`; this asks whether it WAS. The two spellings are different objects, not one object
|
|
794
|
+
// two ways: a source `switch` front-loads its whole dispatch ahead of every arm body (agbcc's
|
|
795
|
+
// `expand_end_case` closes with a `reorder_insns` that moves the dispatch in front of the
|
|
796
|
+
// bodies expanded before it — the same sources `switchArmsFollowLayout` is read off), while an
|
|
797
|
+
// if/else-if ladder emits each test directly above the body it guards. So a test block laid
|
|
798
|
+
// out AFTER a case body cannot have come from a source `switch`, and recovering one here spells
|
|
799
|
+
// a ladder as a `switch` with no dual anywhere in the fan for a differ to prefer.
|
|
800
|
+
//
|
|
801
|
+
// READ PER SITE, off THIS recovery's own blocks, never off the function. A function holding a
|
|
802
|
+
// ladder and a real `switch` must keep both readings, and a function-wide OR would collapse
|
|
803
|
+
// them into one boolean and be wrong on one site by construction.
|
|
804
|
+
//
|
|
805
|
+
// WHAT IT REFUSES TO DO. The reading runs BACKWARDS, from emission to spelling, so it inherits
|
|
806
|
+
// `layoutIndex`'s premise (above) at the weaker of the two strengths stated there. A frontend
|
|
807
|
+
// or compiler that breaks it can hand this a front-loaded layout for a ladder, and then the
|
|
808
|
+
// gate simply does not fire. The converse error, a real `switch` whose layout interleaves,
|
|
809
|
+
// costs only the `switch` SPELLING: the recovery declines to if-recovery, behaviourally
|
|
810
|
+
// identical (the file header) and scored by the differ. This gate can lose a match; it can
|
|
811
|
+
// never produce a wrong answer.
|
|
812
|
+
//
|
|
813
|
+
// WHAT THE DECLINE PRODUCES, AND IT IS NOT ALWAYS A CLEAN LADDER. Recovery is RECURSIVE:
|
|
814
|
+
// declining a tree here hands the tree back to if-recovery, which then runs recovery AGAIN on
|
|
815
|
+
// each sub-tree it structures. On a flat tree that is a ladder over every case value. On a
|
|
816
|
+
// NESTED tree it is not: `corpus/agbcc-swnested.s` (a source `switch` nested inside an
|
|
817
|
+
// if/else-if ladder) declines HERE on the outer tree — correctly, the ladder arm's body really
|
|
818
|
+
// does sit between the tests — and the sub-tree then re-recovers, so the emitted C is an `if`
|
|
819
|
+
// nest holding a `switch` over a STRICT SUBSET of the source's case labels. Still behaviourally
|
|
820
|
+
// identical, and no worse than the alternative (with the reading withdrawn that same function
|
|
821
|
+
// comes back as one `switch` merging two arms the source wrote apart), but it is a THIRD
|
|
822
|
+
// spelling that neither reading would give, and nothing in the output says a dispatch was
|
|
823
|
+
// half-declined: "the decline is a clean ladder" holds at the OUTERMOST tree only. Pinned by
|
|
824
|
+
// the test "PRE5 declines a tree RECURSIVELY" on the committed fixture.
|
|
825
|
+
//
|
|
826
|
+
// WHAT THE POSITIONS ARE READ THROUGH, which is the premise that lives inside asmlift rather
|
|
827
|
+
// than in the compiler. `caseBlocks` holds `forwardingTarget` results (`asCase` is called on the
|
|
828
|
+
// resolved block), so an arm whose body is a bare jump contributes ITS TARGET's position, not
|
|
829
|
+
// its own. A forwarding target laid out ABOVE the dispatch would then read as a body above a
|
|
830
|
+
// test and decline a real `switch`.
|
|
831
|
+
//
|
|
832
|
+
// WHY THAT HAS NO INHABITANT IS MEASURED, NOT ARGUED. Two arguments that look like proofs are
|
|
833
|
+
// not: "the block is reached by a back edge" needs the block to DOMINATE the dispatch, which an
|
|
834
|
+
// `if` arm ahead of the `switch` does not, and a cross-jumped arm tail reaches it on an
|
|
835
|
+
// ordinary forward edge; "this flag's targets reorder nothing" is false — `MIPS_GCC` declares
|
|
836
|
+
// it WITH a scheduler, on the half-premise stated at `layoutIndex` above. Measured: across 28
|
|
837
|
+
// compiled probes on agbcc, gcc2.7.2 -O1 and gcc2.7.2kmc -O2 — arms cross-jumped onto a shared
|
|
838
|
+
// tail ahead of the dispatch, `goto`s out of an arm to a label above it, shared early returns,
|
|
839
|
+
// `continue`/`break` out of a switch inside a loop — no toolchain placed an arm's forwarding
|
|
840
|
+
// target above its own dispatch: every firing came back with zero bodies above the first test.
|
|
841
|
+
// Safe because no compiler in the corpus emits that shape, not because none can; whoever makes
|
|
842
|
+
// one — a new frontend, a relaxed loop recovery, a scheduler that hoists a merged tail —
|
|
843
|
+
// inherits this reader.
|
|
844
|
+
//
|
|
845
|
+
// `bodyPos` covers the CASE bodies only. `defaultBlk` is left out deliberately: it can be the
|
|
846
|
+
// dispatch's own fall-out block rather than an arm the source wrote (the W2/W4 withholdings
|
|
847
|
+
// above), so its position is one the source never chose. Both omissions can only make the gate
|
|
848
|
+
// UNDER-fire, i.e. keep a `switch`, which is the safe direction.
|
|
849
|
+
//
|
|
850
|
+
// AND THE READING REFUSES ITSELF where it has nothing to read. `layoutIndex` answers `-1` for a
|
|
851
|
+
// block absent from `fn.blocks`, which is not a position — taken as one it sorts below every
|
|
852
|
+
// real body and declines the tree holding it. That state has no inhabitant and cannot get one
|
|
853
|
+
// without a bug upstream: `ir/verify.ts` rejects a successor that is not a block of the fn and
|
|
854
|
+
// the tower runs it after the lift and after every raising pass, and `predecessorBlocks` throws
|
|
855
|
+
// by name on the same state near the top of `structure()`. So this is belt-and-braces over a
|
|
856
|
+
// VERIFIER BUG, not over an expected shape — kept because standing down costs nothing. Its test
|
|
857
|
+
// pins the UNREACHABILITY (both throws fire first); the `placed` branch itself stays
|
|
858
|
+
// unexecuted, and cannot be covered without breaking one of those two invariants.
|
|
859
|
+
if (switchRequiresFrontLoadedTests) {
|
|
860
|
+
const testPos = [...seen].map(layoutIndex);
|
|
861
|
+
const bodyPos = [...caseBlocks].map(layoutIndex);
|
|
862
|
+
const placed = [...testPos, ...bodyPos].every((i) => i >= 0);
|
|
863
|
+
if (placed && Math.max(...testPos) > Math.min(...bodyPos)) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ARM ORDER. The case values are disjoint (PRE3), so where no arm falls through the order
|
|
869
|
+
// carries no meaning and is pure matching evidence: ascending case VALUE is the neutral
|
|
870
|
+
// spelling, and where a compiler has declared `switchArmsFollowLayout` the layout of the bodies
|
|
871
|
+
// is the SOURCE's arm order instead. A FALLING arm's position is not free, and `chainArms`
|
|
872
|
+
// below re-threads this order for those — reading it for the chain HEADS only.
|
|
873
|
+
//
|
|
874
|
+
// TWO arms the layout cannot order, both falling back rather than recovering:
|
|
875
|
+
// - two case VALUES sharing one body block share its index, so the tie is one the merge (or
|
|
876
|
+
// the source's own stacked labels) erased, and ascending value breaks it. They become ONE
|
|
877
|
+
// arm below, so the tie only orders that arm against the others. ADJACENT stacked labels
|
|
878
|
+
// never get here: `case 2: case 3:` compiles to a range test the walk reads as navigation
|
|
879
|
+
// and declines, while `case 0: case 2:` compiles to the two equality tests it recovers —
|
|
880
|
+
// both checked against agbcc;
|
|
881
|
+
// - an arm with no body of its own (`case k: break;`) has its edge resolve to the MERGE, so it
|
|
882
|
+
// inherits the merge's index and sorts after every arm that HAS a body.
|
|
883
|
+
//
|
|
884
|
+
// BOOKED, UNPAID: the grouping makes arm ORDER observable on a shape the ascending-value
|
|
885
|
+
// fallback did not have before, and IDO turns out to lay case bodies out in SOURCE order too —
|
|
886
|
+
// the same function with `case 1:` written first and last differ (.text md5 ec39af99 against
|
|
887
|
+
// 689f34ec, 144 bytes each), which is what `switchArmsFollowLayout` recovers. target.ts sets
|
|
888
|
+
// the bar for opting a compiler in at a SOURCE-level argument about its passes, not at two
|
|
889
|
+
// objects, and that argument is unpaid for IDO; nothing here changes while it is. What the
|
|
890
|
+
// grouping cannot do is make a non-agbcc row worse, because the ungrouped spelling is not a
|
|
891
|
+
// rival there: under IDO it compiles to two copies and a different ROM entirely.
|
|
474
892
|
const scrutExpr = expr(scrut);
|
|
475
|
-
const sortedCases = [...cases.entries()].sort((a, c) =>
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
893
|
+
const sortedCases = [...cases.entries()].sort((a, c) =>
|
|
894
|
+
switchArmsFollowLayout ? layoutIndex(a[1]) - layoutIndex(c[1]) || a[0] - c[0] : a[0] - c[0],
|
|
895
|
+
);
|
|
896
|
+
// TWO VALUES ONE BODY IS ONE ARM. `SwitchCase.values` stacks labels for exactly this, and the
|
|
897
|
+
// jump-table regime groups the same way (structure.ts's `armOf`). Emitting the body once per
|
|
898
|
+
// label is `structureRegion` run twice over one block: a duplication, not a spelling choice.
|
|
899
|
+
//
|
|
900
|
+
// WHY BLOCK IDENTITY IS THE KEY, and not a body-equality one like `sameBareExit` above. agbcc
|
|
901
|
+
// MERGES two written-out copies into one block — target.ts's `switchArmsFollowLayout` note
|
|
902
|
+
// says so from agbcc's own sources, SRCS compiling jump.c — and compiling both directions at
|
|
903
|
+
// TOOLCHAIN.agbccFlags says WHERE the merged block lands: at the last copy's position. So
|
|
904
|
+
// `case 0: A break; case 1: … case 2: A break;` and the grouped arm placed THERE are one
|
|
905
|
+
// object (.text md5 555abb1a), while the grouped arm placed at the first value is not
|
|
906
|
+
// (fe4d7d35). That is why the grouped spelling round-trips rather than merely reading shorter:
|
|
907
|
+
// agbcc declares `switchArmsFollowLayout`, so the arm goes exactly where the merged block sits.
|
|
908
|
+
// IDO does not merge at all — 224 bytes against the grouped 144 — so on MIPS a shared block can
|
|
909
|
+
// only have come from stacked labels. Two DISTINCT blocks with equal bodies therefore mean one
|
|
910
|
+
// arm on neither compiler: under agbcc that ROM is unreachable, under IDO it is what two arms
|
|
911
|
+
// compile to. Sound also because two dispatch edges onto ONE body cannot bind one name two
|
|
912
|
+
// ways: `hoistDispatchCopies` merges every collapsed edge's copies into a single statement
|
|
913
|
+
// above the `switch` and refuses on disagreement (structure.ts `hoistedDispatchAssigns`), so
|
|
914
|
+
// an arm reached by two case values is reached with one set of parameter values or the whole
|
|
915
|
+
// recovery declines.
|
|
916
|
+
//
|
|
917
|
+
// An arm takes the position of its FIRST value, which keeps the sort above. `defaultLayoutPos`
|
|
918
|
+
// is handed the GROUPED entry list because what it returns is an INDEX INTO the arm array, so
|
|
919
|
+
// the list it counts and the list it indexes must be one list.
|
|
920
|
+
const armsByBlock = new Map<Block, number[]>();
|
|
921
|
+
for (const [k, blk] of sortedCases) {
|
|
922
|
+
const prev = armsByBlock.get(blk);
|
|
923
|
+
if (prev) {
|
|
924
|
+
prev.push(k);
|
|
925
|
+
} else {
|
|
926
|
+
armsByBlock.set(blk, [k]);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// The `default:` arm is a chain member too — an arm may run on into it, and C prints it last.
|
|
930
|
+
const dfltArm = defaultBlk !== null && defaultBlk !== merge ? defaultBlk : null;
|
|
931
|
+
const preChain = [...armsByBlock.keys()];
|
|
932
|
+
const entries = chainArms(preChain, dfltArm, exitOf);
|
|
933
|
+
if (entries === null) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
// Did the chain move anything? The arm-order POLICY above produced `preChain`; where the chain
|
|
937
|
+
// left it alone, every position still means what that policy said it meant.
|
|
938
|
+
const orderIntact = entries.every((e, i) => e === preChain[i]);
|
|
939
|
+
const fallsInto = (blk: Block): Block | null => {
|
|
940
|
+
const x = exitOf.get(blk);
|
|
941
|
+
return x?.kind === 'fallthrough' ? x.to : null;
|
|
942
|
+
};
|
|
943
|
+
// THE DISPATCH HOIST. Every edge the tree walk is about to COLLAPSE — from any test block, to
|
|
944
|
+
// a case entry, a default candidate, the merge, or another test block — carries the parallel
|
|
945
|
+
// copy that binds its target's parameters, and collapsing the tree is what would discard it.
|
|
946
|
+
// Merge those copies and emit them ONCE, above the `switch`. Placed FIRST because it is
|
|
947
|
+
// emitted first and `argAssignsFor` mints swap-cycle temp names as it goes.
|
|
948
|
+
//
|
|
949
|
+
// TWO CONDITIONS ARE THIS REGIME'S OWN, and both must hold before the emission is asked for:
|
|
950
|
+
//
|
|
951
|
+
// - AVAILABILITY AT THE ROOT. A hoisted copy is evaluated at `b`, not on its edge, so every
|
|
952
|
+
// argument it reads must be defined at a block that DOMINATES `b`. An argument computed
|
|
953
|
+
// inside a collapsed test block is not: the tree reaches that block only on some paths,
|
|
954
|
+
// while the hoist runs on all of them, and re-rendering it above the dispatch would
|
|
955
|
+
// SPECULATE it. (That is the one invariant PRE4's "a use is dominated by its def, so a
|
|
956
|
+
// collapsed op runs on a subset of the paths it already ran on" does not give the hoist,
|
|
957
|
+
// which is why the hoist asks for it here rather than inheriting it.)
|
|
958
|
+
// - The emission's own two refusals, stated at `hoistedDispatchAssigns`: edges disagreeing
|
|
959
|
+
// about one name, and a hoisted name whose value is still live at the switch or into an arm.
|
|
960
|
+
//
|
|
961
|
+
// Either way the answer is a decline to if-recovery, which spells every copy the asm performs.
|
|
962
|
+
const availableAtRoot = (v: Value): boolean => {
|
|
963
|
+
const home = blockOf(v);
|
|
964
|
+
return home !== undefined && dom.get(b)!.has(home);
|
|
965
|
+
};
|
|
966
|
+
const dispatchEdges: { pred: Block; succ: { block: Block; args: Value[] } }[] = [];
|
|
967
|
+
for (const t of new Set([...seen, ...throughEdges])) {
|
|
968
|
+
for (const e of t.ops[t.ops.length - 1].successors) {
|
|
969
|
+
if (e.block.params.length === 0) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (!e.args.every(availableAtRoot)) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
dispatchEdges.push({ pred: t, succ: e });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const hoisted = hoistDispatchCopies(dispatchEdges, [
|
|
979
|
+
b,
|
|
980
|
+
...caseBlocks,
|
|
981
|
+
...(defaultBlk ? [defaultBlk] : []),
|
|
982
|
+
...(merge ? [merge] : []),
|
|
983
|
+
]);
|
|
984
|
+
if (hoisted === null) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
// Bodies are structured in EMISSION order — `argAssignsFor` mints swap-cycle temp names as it
|
|
988
|
+
// goes, so building them out of order changes the output — and each falling arm's target is
|
|
989
|
+
// re-read off that order rather than trusted from the ordering above: this is the seam where a
|
|
990
|
+
// POSITION acquires control-flow meaning (the l3/ast.ts non-neutrality note).
|
|
991
|
+
const outCases: SwitchCase[] = [];
|
|
992
|
+
for (const [i, blk] of entries.entries()) {
|
|
993
|
+
const to = fallsInto(blk);
|
|
994
|
+
if (to !== null && to !== (entries[i + 1] ?? dfltArm)) {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
// The arm fallen INTO may take block parameters, and the TWO paths that reach it are spelled
|
|
998
|
+
// in two different places: entering by its own case value takes the hoisted copy above the
|
|
999
|
+
// `switch` (`hoistDispatchCopies`), while FALLING in takes the copies the falling arm's own
|
|
1000
|
+
// `br` emits as its last statements (`structureRegion(blk, to)` walks that terminator, so
|
|
1001
|
+
// they are already there). Getting this wrong is SILENT, which is why it rests on the hoist
|
|
1002
|
+
// rather than on a local reading of the arm.
|
|
1003
|
+
//
|
|
1004
|
+
// Regime B refuses the same hazard LOUD at its own `switch_br` path, on the weaker predicate
|
|
1005
|
+
// that fits it (the copies `argAssignsFor` actually produced): its arms take their edge
|
|
1006
|
+
// copies PER ARM, so a fall-through path would re-run them over what the falling arm
|
|
1007
|
+
// computed. Hoisting is what removes that hazard, and Regime B does not hoist — booked, not
|
|
1008
|
+
// built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
|
|
1009
|
+
outCases.push({
|
|
1010
|
+
values: armsByBlock.get(blk)!,
|
|
1011
|
+
body: structureRegion(blk, to ?? merge),
|
|
1012
|
+
fallsThrough: to !== null,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
481
1015
|
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
482
1016
|
// would carry no statement, which says nothing and is not valid C89.
|
|
483
1017
|
const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
|
|
1018
|
+
// The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
|
|
1019
|
+
// (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
|
|
1020
|
+
// into it and no other subtree jumps there — the two references the tree walk can count.
|
|
1021
|
+
const dispatchTargets = [...seen].flatMap((t) =>
|
|
1022
|
+
t.ops[t.ops.length - 1].successors.map((e) => forwardingTarget(e.block)),
|
|
1023
|
+
);
|
|
1024
|
+
// THE TWO CONJUNCTS READ THE EDGES DIFFERENTLY, and deliberately. The first is asked RAW, of
|
|
1025
|
+
// the terminator's own second successor, because a surviving `b .Ldefault` block is a jump
|
|
1026
|
+
// that did NOT collapse into a fall-through — resolving it away would count the jump as the
|
|
1027
|
+
// running-out it is not (see the `expand_end_case` paragraph on `defaultLayoutPos`). The
|
|
1028
|
+
// second is asked FORWARDED, over `dispatchTargets`, because there a forwarder is transparent:
|
|
1029
|
+
// two edges that arrive through one are two references to the same block, which is exactly
|
|
1030
|
+
// what "named more than once" has to count.
|
|
1031
|
+
const fellThroughIntoIt = (blk: Block) =>
|
|
1032
|
+
[...seen].some((t) => {
|
|
1033
|
+
const succ = t.ops[t.ops.length - 1].successors;
|
|
1034
|
+
return succ.length > 1 && succ[1].block === blk;
|
|
1035
|
+
});
|
|
1036
|
+
const namedOnlyOnce = (blk: Block) => dispatchTargets.filter((e) => e === blk).length < 2;
|
|
1037
|
+
const ranOutInto = (blk: Block) => fellThroughIntoIt(blk) && namedOnlyOnce(blk);
|
|
1038
|
+
// `defaultLayoutPos` owns which POSITIONS a chain makes unreadable — see its fall-through
|
|
1039
|
+
// withholdings W5..W7. A chain elsewhere in the switch does not delete the evidence for
|
|
1040
|
+
// where the label goes, and reading it off the emitted arms is what lets a `default:` written
|
|
1041
|
+
// between two closed arms keep its place while a chain runs beside it.
|
|
1042
|
+
const defaultAt = defaultBlk
|
|
1043
|
+
? defaultLayoutPos(
|
|
1044
|
+
defaultBlk,
|
|
1045
|
+
entries.map((e, i) => ({ entry: e, fallsThrough: outCases[i].fallsThrough })),
|
|
1046
|
+
{ placedByDispatch: ranOutInto(defaultBlk), orderIntact },
|
|
1047
|
+
)
|
|
1048
|
+
: undefined;
|
|
484
1049
|
const sw: Stmt = {
|
|
485
1050
|
k: 'switch',
|
|
486
1051
|
scrutinee: scrutExpr,
|
|
487
1052
|
cases: outCases,
|
|
488
|
-
...(defBody.length ? { default: defBody } : {}),
|
|
1053
|
+
...(defBody.length ? { default: defBody, ...(defaultAt !== undefined ? { defaultAt } : {}) } : {}),
|
|
489
1054
|
};
|
|
490
|
-
const out: Stmt[] = [sw];
|
|
1055
|
+
const out: Stmt[] = [...hoisted, sw];
|
|
491
1056
|
if (merge && merge !== stop) {
|
|
492
1057
|
out.push(...structureRegion(merge, stop));
|
|
493
1058
|
}
|
|
494
1059
|
return out;
|
|
495
1060
|
};
|
|
496
|
-
return { recognizeSwitch, analyzeArmExit };
|
|
1061
|
+
return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
|
|
497
1062
|
}
|