@asmlift/core 0.5.0 → 0.6.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 +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -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 +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- 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 +113 -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 +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- 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 +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -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 +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -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,35 @@ 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
|
+
/** may the emitted SOURCE say "this arm runs on into the next one"? False for a language whose
|
|
27
|
+
* `case` cannot fall through (Pascal), and then Regime A declines a falling arm to if-recovery
|
|
28
|
+
* — the behaviourally identical recovery that backend CAN print. See StructureOptions. */
|
|
29
|
+
spellSwitchFallthrough: boolean;
|
|
30
|
+
/** does emitting this block's ops carry a statement beyond the ops themselves? Collapsing a
|
|
31
|
+
* test block into a `switch` re-renders its ops at their uses and emits no side effects for it,
|
|
32
|
+
* so any op that renders as a STATEMENT of its own loses that statement. Two produce one: a
|
|
33
|
+
* def-site ANCHORED merge copy (structure.ts anchorConstCopies), whose edge copy stays
|
|
34
|
+
* suppressed, and a MATERIALIZED def, whose `v = …` assignment renders only here while its uses
|
|
35
|
+
* read the bare name — leaving a local declared and never assigned. */
|
|
36
|
+
emitsOwnStatement: (blk: Block) => boolean;
|
|
37
|
+
/** Where a value is WRITTEN: its block for a parameter, its def op's block otherwise — the half
|
|
38
|
+
* of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
|
|
39
|
+
* both halves for def-site anchoring, rather than indexing the parameters a second time. */
|
|
40
|
+
blockOf: (v: Value) => Block | undefined;
|
|
41
|
+
/** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
|
|
42
|
+
* ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
|
|
43
|
+
* (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
|
|
44
|
+
* regime takes it as a dependency rather than implementing it a second time, exactly as Regime
|
|
45
|
+
* B takes `argAssignsFor`. Null ⇒ no single hoisted statement spells them; the caller declines
|
|
46
|
+
* to if-recovery. `liveAt` are the blocks whose live-in names the writes must not clobber. */
|
|
47
|
+
hoistDispatchCopies: (
|
|
48
|
+
edges: readonly { pred: Block; succ: { block: Block; args: Value[] } }[],
|
|
49
|
+
liveAt: readonly Block[],
|
|
50
|
+
) => Stmt[] | null;
|
|
27
51
|
expr: (v: Value) => Expr;
|
|
28
52
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
29
53
|
}
|
|
@@ -34,17 +58,181 @@ export interface SwitchRecoverDeps {
|
|
|
34
58
|
* - `break` every path out of the arm reaches the switch's merge (or returns / loops
|
|
35
59
|
* inside the arm). The ordinary closed arm.
|
|
36
60
|
* - `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
|
-
*
|
|
61
|
+
* Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
|
|
62
|
+
* below arranges and both regimes then re-read off the emission array (see the
|
|
63
|
+
* l3/ast.ts non-neutrality note).
|
|
39
64
|
* - `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
|
|
65
|
+
* "out to the merge". C needs a `goto` for those. Regime A declines to
|
|
66
|
+
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD. */
|
|
41
67
|
export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
|
|
42
68
|
|
|
43
69
|
export interface SwitchRecovery {
|
|
44
70
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
45
|
-
/** shared with the Regime-B (`switch_br`) path in structure.ts
|
|
46
|
-
* this returns;
|
|
71
|
+
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
72
|
+
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
73
|
+
* Regime B, which has no fallback, fails loud. */
|
|
47
74
|
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
|
|
75
|
+
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
76
|
+
* regimes read it from one definition (and one statement of what it rests on). */
|
|
77
|
+
layoutIndex: (blk: Block) => number;
|
|
78
|
+
/** where the `default:` label goes among the EMITTED arms, or `undefined` for C's last position —
|
|
79
|
+
* shared with Regime B so both regimes state those refusals once. */
|
|
80
|
+
defaultLayoutPos: (
|
|
81
|
+
defaultBlk: Block,
|
|
82
|
+
arms: readonly { entry: Block; fallsThrough: boolean }[],
|
|
83
|
+
opts: { placedByDispatch: boolean; orderIntact: boolean },
|
|
84
|
+
) => number | undefined;
|
|
85
|
+
/** ONE linear emission order for a set of arms, re-threaded so every falling arm sits directly
|
|
86
|
+
* above the one it falls into — or null when no linear order spells them. Shared with Regime B,
|
|
87
|
+
* so the chain, the adjacency it guarantees and the three refusals below it have one definition
|
|
88
|
+
* (Regime A maps null to if-recovery, Regime B to a loud StructureError). */
|
|
89
|
+
chainArms: (order: Block[], dflt: Block | null, exitOf: Map<Block, ArmExit>) => Block[] | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A block with no body of its own: no params, and one op that only LEAVES. `ret` qualifies as
|
|
93
|
+
* well as `br` because raise/retsink.ts rewrites the one into the other — a cross-jumped arm
|
|
94
|
+
* body has two dispatch preds, which is exactly the shape that makes retsink sink the merge's
|
|
95
|
+
* return into every leaf, the fall-out jumps included. */
|
|
96
|
+
function isBareExit(blk: Block): boolean {
|
|
97
|
+
return isBodyless(blk) && (blk.ops[0].opcode === 'br' || blk.ops[0].opcode === 'ret');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Are these two blocks the SAME bare exit — the same jump with the same args, or the same return
|
|
101
|
+
* of the same values? Neither has a body, so two of them are indistinguishable at emission. */
|
|
102
|
+
function sameBareExit(a: Block, c: Block): boolean {
|
|
103
|
+
if (a === c) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (!isBareExit(a) || !isBareExit(c) || a.ops[0].opcode !== c.ops[0].opcode) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const same = (x: readonly Value[], y: readonly Value[]) => x.length === y.length && x.every((v, i) => v === y[i]);
|
|
110
|
+
if (a.ops[0].opcode === 'ret') {
|
|
111
|
+
return same(a.ops[0].operands, c.ops[0].operands);
|
|
112
|
+
}
|
|
113
|
+
const [x, y] = [a, c].map((blk) => blk.ops[0].successors[0]);
|
|
114
|
+
return x.block === y.block && same(x.args, y.args);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TestInfo {
|
|
118
|
+
x: Value;
|
|
119
|
+
k: number;
|
|
120
|
+
cls: 'eq' | 'ne' | 'rel';
|
|
121
|
+
opcode: string;
|
|
122
|
+
xOnLeft: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Evaluate a test predicate for a CONCRETE scrutinee value — used to SIMULATE the decision tree and
|
|
126
|
+
// verify recovered case values (below). Returns true iff the `taken` (successors[0]) edge is followed.
|
|
127
|
+
// Signed/unsigned per the icmp opcode (PRE3, done concretely rather than via interval lattices).
|
|
128
|
+
function evalCmp(opcode: string, xOnLeft: boolean, xv: number, k: number): boolean {
|
|
129
|
+
const uns = opcode.startsWith('icmp_u');
|
|
130
|
+
const [xn, kn] = uns ? [xv >>> 0, k >>> 0] : [xv | 0, k | 0];
|
|
131
|
+
const [l, r] = xOnLeft ? [xn, kn] : [kn, xn]; // put the scrutinee where it textually appears
|
|
132
|
+
switch (opcode) {
|
|
133
|
+
case 'icmp_eq':
|
|
134
|
+
return l === r;
|
|
135
|
+
case 'icmp_ne':
|
|
136
|
+
return l !== r;
|
|
137
|
+
case 'icmp_slt':
|
|
138
|
+
case 'icmp_ult':
|
|
139
|
+
return l < r;
|
|
140
|
+
case 'icmp_sle':
|
|
141
|
+
case 'icmp_ule':
|
|
142
|
+
return l <= r;
|
|
143
|
+
case 'icmp_sgt':
|
|
144
|
+
case 'icmp_ugt':
|
|
145
|
+
return l > r;
|
|
146
|
+
case 'icmp_sge':
|
|
147
|
+
case 'icmp_uge':
|
|
148
|
+
return l >= r;
|
|
149
|
+
default:
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Which single scrutinee value does this relational test's BRANCH admit, if exactly one? A
|
|
155
|
+
// relational side is a HALF-LINE in the compare's own ordering, so it can hold one value only at
|
|
156
|
+
// a domain endpoint — which is why testing the two endpoints and their neighbours decides it,
|
|
157
|
+
// with no interval lattice. `x < 1` over an unsigned scrutinee admits `{0}` and is agbcc's
|
|
158
|
+
// spelling of `case 0` in a balanced search: `emit_case_nodes` tests the subtree BOUND, not the
|
|
159
|
+
// value, whenever the remaining range has collapsed to one. Read as navigation instead, that
|
|
160
|
+
// arm's body becomes a second default candidate and the whole tree declines.
|
|
161
|
+
//
|
|
162
|
+
// THE BRANCH, never the fall-through. Every jump in `emit_case_nodes` that lands on a case body
|
|
163
|
+
// is its test's BRANCH — for a single-valued node, LT to `node->left->code_label` and GT to
|
|
164
|
+
// `node->right->code_label`, each guarded by `node_is_bounded` on that side — while the
|
|
165
|
+
// fall-through always continues into more dispatch, so a fall-side reading has no producer in
|
|
166
|
+
// this dispatch — and none turns up in 3176 generated agbcc dispatches.
|
|
167
|
+
//
|
|
168
|
+
// TWO PREMISES ABOUT THE DOMAIN. It is the 32-bit REGISTER's, not the scrutinee's recovered
|
|
169
|
+
// type, so a narrower type has a nearer endpoint this misses — which costs a case and never
|
|
170
|
+
// invents one. And it is the WHOLE of that domain, so an ancestor that already excluded the
|
|
171
|
+
// value makes the reading wrong; PRE3 is what catches that, simulating the original tree for
|
|
172
|
+
// every recovered case value and declining unless it lands on the recorded body, exactly as it
|
|
173
|
+
// does for the `eq` cases. Null when the branch admits none, several, or the whole domain.
|
|
174
|
+
function singletonTaken(ti: TestInfo): number | null {
|
|
175
|
+
const [min, max] = ti.opcode.startsWith('icmp_u') ? [0, -1] : [-0x80000000, 0x7fffffff];
|
|
176
|
+
for (const [v, next] of [
|
|
177
|
+
[min, min + 1],
|
|
178
|
+
[max, max - 1],
|
|
179
|
+
]) {
|
|
180
|
+
if (evalCmp(ti.opcode, ti.xOnLeft, v, ti.k) && !evalCmp(ti.opcode, ti.xOnLeft, next, ti.k)) {
|
|
181
|
+
return v;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
|
|
188
|
+
* fall-through chain is emitted contiguously and takes the position of its HEAD in `order`,
|
|
189
|
+
* which is the caller's own arm-order policy — so with no fall-through every chain is a
|
|
190
|
+
* singleton and `order` comes back unchanged. `dflt` is the `default:` arm's block when it has
|
|
191
|
+
* one, and it is pinned LAST because that is where C prints the label.
|
|
192
|
+
*
|
|
193
|
+
* THREE REFUSALS (null ⇒ the caller declines), each a shape no single linear order spells:
|
|
194
|
+
* - two arms falling into the SAME arm — C drops into an arm from above along one edge only;
|
|
195
|
+
* - the `default:` arm falling into a case, since nothing is emitted below it;
|
|
196
|
+
* - a fall-through CYCLE, whose members are all fallen-into and so are never a chain head. */
|
|
197
|
+
export function chainArms(order: Block[], dflt: Block | null, exitOf: Map<Block, ArmExit>): Block[] | null {
|
|
198
|
+
const next = new Map<Block, Block>();
|
|
199
|
+
const fallenInto = new Set<Block>();
|
|
200
|
+
for (const e of [...order, ...(dflt ? [dflt] : [])]) {
|
|
201
|
+
const x = exitOf.get(e);
|
|
202
|
+
if (x?.kind !== 'fallthrough') {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (e === dflt || fallenInto.has(x.to)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
fallenInto.add(x.to);
|
|
209
|
+
next.set(e, x.to);
|
|
210
|
+
}
|
|
211
|
+
const chains: Block[][] = [];
|
|
212
|
+
const seen = new Set<Block>();
|
|
213
|
+
let intoDefault = -1;
|
|
214
|
+
for (const head of order) {
|
|
215
|
+
if (fallenInto.has(head)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const chain: Block[] = [];
|
|
219
|
+
for (let cur: Block | undefined = head; cur !== undefined && cur !== dflt && !seen.has(cur); cur = next.get(cur)) {
|
|
220
|
+
seen.add(cur);
|
|
221
|
+
chain.push(cur);
|
|
222
|
+
}
|
|
223
|
+
if (dflt !== null && next.get(chain[chain.length - 1]) === dflt) {
|
|
224
|
+
intoDefault = chains.length;
|
|
225
|
+
}
|
|
226
|
+
chains.push(chain);
|
|
227
|
+
}
|
|
228
|
+
// A cycle's every member is fallen-into, so none of them is a head and none is walked.
|
|
229
|
+
if (seen.size !== order.length) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (intoDefault >= 0) {
|
|
233
|
+
chains.push(...chains.splice(intoDefault, 1));
|
|
234
|
+
}
|
|
235
|
+
return chains.flat();
|
|
48
236
|
}
|
|
49
237
|
|
|
50
238
|
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
@@ -57,14 +245,100 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
57
245
|
isNamed,
|
|
58
246
|
isCmpOpcode,
|
|
59
247
|
switchAllowsNeqCase,
|
|
60
|
-
|
|
248
|
+
switchAllowsBoundCase,
|
|
249
|
+
switchArmsFollowLayout,
|
|
250
|
+
spellSwitchFallthrough,
|
|
251
|
+
emitsOwnStatement,
|
|
252
|
+
blockOf,
|
|
253
|
+
hoistDispatchCopies,
|
|
61
254
|
expr,
|
|
62
255
|
structureRegion,
|
|
63
256
|
} = deps;
|
|
64
257
|
|
|
258
|
+
// A block's index in `fn.blocks` as its position in the ASSEMBLY — the sole warrant for reading a
|
|
259
|
+
// source's arm order off block indices below, and true PER FRONTEND rather than of the IR:
|
|
260
|
+
// - thumb.ts and mips.ts build the list by scanning the instruction stream in address order;
|
|
261
|
+
// - ppc.ts does not. It APPENDS a synthetic return block (`synthReturn`) at the end of the list
|
|
262
|
+
// for every conditional-return branch, wherever in the stream that branch sits, so its list
|
|
263
|
+
// is not address order at all;
|
|
264
|
+
// - raising only ever REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}
|
|
265
|
+
// .ts all `filter`), never inserts or reorders, so the frontend's order is what survives.
|
|
266
|
+
// `switchArmsFollowLayout` is therefore a claim about a target's FRONTEND as much as about its
|
|
267
|
+
// compiler, and a target opts in on both — which is why PPC_MWCC, whose frontend fails the first
|
|
268
|
+
// half, does not.
|
|
269
|
+
const blockIndex = new Map(fn.blocks.map((blk, i) => [blk, i] as const));
|
|
270
|
+
const layoutIndex = (blk: Block): number => blockIndex.get(blk) ?? -1;
|
|
271
|
+
|
|
272
|
+
// Where the `default:` label goes among the arms, as a COUNT of the arms laid out before it — the
|
|
273
|
+
// same evidence the case bodies carry, read the same way. Compiled at every position of a 3- to
|
|
274
|
+
// 8-case switch, the default's block lands where the source wrote it. `undefined` ⇒ C's
|
|
275
|
+
// conventional last position, which is what every other producer of this node means.
|
|
276
|
+
//
|
|
277
|
+
// SEVEN WITHHOLDINGS, W1..W7, numbered in the order the code asks them so the prose and the code
|
|
278
|
+
// index ONE list, and stated here so both regimes state them once. W1 is the target's own opt-in;
|
|
279
|
+
// W2..W4 are about a block the DISPATCH placed rather than the arm; W5..W7 are about
|
|
280
|
+
// fall-through — W5 about the LIST the count would index, W6 and W7 about the POSITION it names.
|
|
281
|
+
//
|
|
282
|
+
// W1 the target does not read arm order off the layout at all (`switchArmsFollowLayout`).
|
|
283
|
+
// W2 a block with no body of its own is one the dispatch minted (`b .Ldefault`), and which of
|
|
284
|
+
// several such the collapse below keeps is a walk-order accident.
|
|
285
|
+
// W3 the walk already read that block as a CASE arm — a dense table sends every unwritten
|
|
286
|
+
// value's slot to the default's block, so grouping the slots gives that block an arm of its
|
|
287
|
+
// own and its index is where THAT arm sits.
|
|
288
|
+
// W4 `emit_case_nodes` ends every exhausted subtree with `emit_jump_if_reachable
|
|
289
|
+
// (default_label)` and `expand_end_case` reorders the whole dispatch, those jumps included,
|
|
290
|
+
// ahead of the arm bodies — so a jump survives as a plain FALL-THROUGH exactly when the
|
|
291
|
+
// default's body is the arm the source wrote FIRST. That reading holds only while a second
|
|
292
|
+
// subtree still names the label: a two-case chain names it once, and agbcc then lays that
|
|
293
|
+
// block right after the tests whatever the source wrote, both spellings compiling to
|
|
294
|
+
// identical instructions.
|
|
295
|
+
// W5 the chain RE-THREADED the arm order (`orderIntact` false), so the emitted list is no
|
|
296
|
+
// longer the one the layout count describes and no position in it means what the count
|
|
297
|
+
// says. This one is whole-switch because the re-threading is. A per-position reading —
|
|
298
|
+
// bracket the label between the two arms that straddle it in LAYOUT, then map that into the
|
|
299
|
+
// emitted list — is possible and unbuilt, and hard to need: a compiler that lays bodies out
|
|
300
|
+
// in source order already writes a falling arm directly above its target, so the order it
|
|
301
|
+
// declares is a chain order too.
|
|
302
|
+
// W6 the LAST emitted arm falls through, which can only be into the default (the adjacency
|
|
303
|
+
// check leaves no other target) — the label must then be last, which IS `undefined`.
|
|
304
|
+
// W7 the position lands directly after a falling arm, where printing the label would divert
|
|
305
|
+
// that arm into the default. cfamily.ts fails loud on exactly that, and this is the producer
|
|
306
|
+
// side of the same rule.
|
|
307
|
+
//
|
|
308
|
+
// A switch with a chain elsewhere keeps its evidence: the reason to withhold is the position,
|
|
309
|
+
// never "some arm somewhere falls".
|
|
310
|
+
const defaultLayoutPos = (
|
|
311
|
+
defaultBlk: Block,
|
|
312
|
+
arms: readonly { entry: Block; fallsThrough: boolean }[],
|
|
313
|
+
opts: { placedByDispatch: boolean; orderIntact: boolean },
|
|
314
|
+
): number | undefined => {
|
|
315
|
+
if (!switchArmsFollowLayout) {
|
|
316
|
+
return undefined; // W1 target does not read layout
|
|
317
|
+
}
|
|
318
|
+
if (isBareExit(defaultBlk)) {
|
|
319
|
+
return undefined; // W2 dispatch-minted bodyless block
|
|
320
|
+
}
|
|
321
|
+
if (arms.some((a) => a.entry === defaultBlk)) {
|
|
322
|
+
return undefined; // W3 already an emitted arm
|
|
323
|
+
}
|
|
324
|
+
if (opts.placedByDispatch) {
|
|
325
|
+
return undefined; // W4 dispatch ran out into it
|
|
326
|
+
}
|
|
327
|
+
if (!opts.orderIntact) {
|
|
328
|
+
return undefined; // W5 chain re-threaded the arms
|
|
329
|
+
}
|
|
330
|
+
if (arms[arms.length - 1]?.fallsThrough) {
|
|
331
|
+
return undefined; // W6 last arm falls through
|
|
332
|
+
}
|
|
333
|
+
const at = arms.filter((a) => layoutIndex(a.entry) < layoutIndex(defaultBlk)).length;
|
|
334
|
+
// W7 the label would land directly after a falling arm
|
|
335
|
+
return at > 0 && arms[at - 1].fallsThrough ? undefined : at;
|
|
336
|
+
};
|
|
337
|
+
|
|
65
338
|
// --- Regime A: comparison-tree switch recovery ----------------------------------------------------
|
|
66
339
|
// Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
|
|
67
|
-
// scrutinee identity/dominance,
|
|
340
|
+
// scrutinee identity/dominance, ARM EXITS (per site: `break`, `fallthrough` — which `chainArms`
|
|
341
|
+
// then places — or a decline), concrete interval consistency, test purity.
|
|
68
342
|
|
|
69
343
|
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
70
344
|
// `250 << 2` for a large sparse case) to a number — else null.
|
|
@@ -137,20 +411,23 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
137
411
|
}
|
|
138
412
|
};
|
|
139
413
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// the switch
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
414
|
+
// TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
|
|
415
|
+
// both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
|
|
416
|
+
//
|
|
417
|
+
// PRE4 (purity) is the second. A collapsed test block's ops re-render at whichever use inlines
|
|
418
|
+
// them, at a point the switch decides — so the question is motion, not deletion, and
|
|
419
|
+
// `ORDER_SENSITIVE_OPS` is the set that asks it. NOT the trapping divides: a use is dominated by
|
|
420
|
+
// its def, so the re-rendered op runs on a subset of the paths it already ran on — nothing is
|
|
421
|
+
// speculated. `emitsOwnStatement` covers what motion cannot save: a statement belonging to the
|
|
422
|
+
// block rather than to a use. The root is exempt from all of it — its ops are already emitted as
|
|
423
|
+
// sideEffects(b) before the switch.
|
|
424
|
+
//
|
|
425
|
+
// A block that tests the scrutinee and is NOT collapsible is still dispatch, so the walk must
|
|
426
|
+
// read it as dispatch and decline, never re-read it as a case body — that would spell an arm
|
|
427
|
+
// whose guard the dispatch has already decided.
|
|
428
|
+
const collapsible = (blk: Block): boolean =>
|
|
429
|
+
!blk.ops.some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)) && !emitsOwnStatement(blk);
|
|
430
|
+
const testInfo = (blk: Block): TestInfo | null => {
|
|
154
431
|
const term = blk.ops[blk.ops.length - 1];
|
|
155
432
|
if (term.opcode !== 'cond_br') {
|
|
156
433
|
return null;
|
|
@@ -159,9 +436,6 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
159
436
|
if (!cmp || !isCmpOpcode(cmp.opcode)) {
|
|
160
437
|
return null;
|
|
161
438
|
}
|
|
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
439
|
// Which operand is the scrutinee, which is the constant?
|
|
166
440
|
const [lo, ro] = cmp.operands;
|
|
167
441
|
const lc = evalConst(lo),
|
|
@@ -182,45 +456,16 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
182
456
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
183
457
|
};
|
|
184
458
|
|
|
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
459
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
215
460
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
216
461
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
217
462
|
// switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
|
|
218
463
|
// ordinary `break`, not as falling into the default.
|
|
219
464
|
//
|
|
220
|
-
// Region membership is `dom(blk) ∋ b
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
465
|
+
// Region membership is `dom(blk) ∋ b`: a block NOT dominated by the switch is outside this
|
|
466
|
+
// switch's region and is not walked. It IS recorded as an escape, because an arm that can leave
|
|
467
|
+
// sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
|
|
468
|
+
// record, so an arm that reaches no sibling still closes with a plain `break`.
|
|
224
469
|
//
|
|
225
470
|
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
226
471
|
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
@@ -261,7 +506,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
261
506
|
}
|
|
262
507
|
// Name what is actually missing. These three are different facts, and only the first is a shape
|
|
263
508
|
// 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) => `#${
|
|
509
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
265
510
|
if (into.size > 1) {
|
|
266
511
|
return {
|
|
267
512
|
kind: 'unstructurable',
|
|
@@ -283,15 +528,8 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
283
528
|
};
|
|
284
529
|
};
|
|
285
530
|
|
|
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
531
|
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
294
|
-
const root = testInfo(b
|
|
532
|
+
const root = testInfo(b);
|
|
295
533
|
if (!root) {
|
|
296
534
|
return null;
|
|
297
535
|
}
|
|
@@ -314,27 +552,10 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
314
552
|
// Walk the test tree. `cases`: value → case-entry block. `defaultCands`: leaves reached without an
|
|
315
553
|
// equality pin. A test-block DAG cycle, or a `!=` case when the compiler disallows it, declines.
|
|
316
554
|
const cases = new Map<number, Block>();
|
|
555
|
+
// Reached through `forwardingTarget`: agbcc's binary-search layout branches to the shared default
|
|
556
|
+
// through empty `b .Ldef` blocks, and without resolving them each becomes a DISTINCT default
|
|
557
|
+
// candidate and the whole tree declines.
|
|
317
558
|
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
559
|
// Concretely SIMULATE the decision tree for a scrutinee value `xv`, returning the leaf block it
|
|
339
560
|
// reaches (or null on an unexpected cycle). This is PRE3 done concretely: it lets us verify each
|
|
340
561
|
// recovered case value actually routes to its recorded body in the ORIGINAL tree.
|
|
@@ -342,7 +563,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
342
563
|
let cur = b;
|
|
343
564
|
const guard = new Set<Block>();
|
|
344
565
|
for (;;) {
|
|
345
|
-
const ti = testInfo(cur
|
|
566
|
+
const ti = testInfo(cur);
|
|
346
567
|
if (!ti || ti.x !== scrut) {
|
|
347
568
|
return cur;
|
|
348
569
|
} // reached a leaf (case body / default)
|
|
@@ -352,7 +573,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
352
573
|
guard.add(cur);
|
|
353
574
|
const term = cur.ops[cur.ops.length - 1];
|
|
354
575
|
const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
|
|
355
|
-
cur =
|
|
576
|
+
cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
|
|
356
577
|
}
|
|
357
578
|
};
|
|
358
579
|
const seen = new Set<Block>();
|
|
@@ -363,59 +584,76 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
363
584
|
return null;
|
|
364
585
|
} // a test-block DAG cycle → decline
|
|
365
586
|
seen.add(blk);
|
|
366
|
-
const ti = testInfo(blk
|
|
587
|
+
const ti = testInfo(blk);
|
|
367
588
|
if (!ti || ti.x !== scrut) {
|
|
368
589
|
return null;
|
|
369
590
|
} // PRE1: every test is on the SAME Value
|
|
591
|
+
if (blk !== b && !collapsible(blk)) {
|
|
592
|
+
return null;
|
|
593
|
+
} // PRE4
|
|
370
594
|
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
|
-
|
|
595
|
+
const taken = forwardingTarget(term.successors[0].block),
|
|
596
|
+
fall = forwardingTarget(term.successors[1].block);
|
|
597
|
+
const isTestOn = (child: Block) => {
|
|
598
|
+
const t = testInfo(child);
|
|
599
|
+
return !!t && t.x === scrut;
|
|
600
|
+
};
|
|
601
|
+
/** Read `child` as the BODY of case `k`. A case entry with a PHI is admitted: the dispatch
|
|
602
|
+
* edge binds those parameters, and `hoistDispatchCopies` re-emits that binding once above
|
|
603
|
+
* the `switch` (or declines the whole recovery). A fall-through chain's accumulator crosses
|
|
604
|
+
* every arm as exactly such a parameter, so refusing it here refuses the whole family. */
|
|
605
|
+
const asCase = (child: Block, k: number): boolean => {
|
|
606
|
+
if (isTestOn(child)) {
|
|
607
|
+
return false;
|
|
608
|
+
} // a case target that's a test → decline
|
|
609
|
+
if (cases.has(k)) {
|
|
610
|
+
return false;
|
|
611
|
+
} // duplicate case value → decline
|
|
612
|
+
cases.set(k, child);
|
|
613
|
+
return true;
|
|
614
|
+
};
|
|
615
|
+
/** Read `child` as a NAVIGATION edge: more dispatch to walk, or a non-test leaf, which is a
|
|
616
|
+
* default candidate. Never declines — the leaf's own reading is settled below. */
|
|
617
|
+
const asNav = (child: Block): boolean => {
|
|
618
|
+
if (isTestOn(child)) {
|
|
390
619
|
work.push(child);
|
|
391
620
|
return true;
|
|
392
621
|
}
|
|
393
|
-
defaultCands.add(child);
|
|
622
|
+
defaultCands.add(child);
|
|
394
623
|
return true;
|
|
395
624
|
};
|
|
396
625
|
if (ti.cls === 'eq') {
|
|
397
|
-
if (!
|
|
626
|
+
if (!asCase(taken, ti.k)) {
|
|
398
627
|
return null;
|
|
399
628
|
} // x==k → taken is case k
|
|
400
|
-
if (!
|
|
629
|
+
if (!asNav(fall)) {
|
|
401
630
|
return null;
|
|
402
631
|
}
|
|
403
632
|
} else if (ti.cls === 'ne') {
|
|
404
633
|
if (!switchAllowsNeqCase) {
|
|
405
634
|
return null;
|
|
406
635
|
} // per-compiler gate
|
|
407
|
-
if (!
|
|
636
|
+
if (!asCase(fall, ti.k)) {
|
|
408
637
|
return null;
|
|
409
638
|
} // x!=k → the EQUAL side (fall) is case k
|
|
410
|
-
if (!
|
|
639
|
+
if (!asNav(taken)) {
|
|
411
640
|
return null;
|
|
412
641
|
}
|
|
413
642
|
} else {
|
|
414
|
-
// relational →
|
|
415
|
-
|
|
643
|
+
// relational → navigation, except where the BRANCH has collapsed to a single value and
|
|
644
|
+
// lands on a BODY, on a compiler that declared the spelling. Two more refusals:
|
|
645
|
+
// - a bound test at the ROOT. `emit_case_nodes` emits a single-valued node's own
|
|
646
|
+
// `do_jump_if_equal` before either descent test, so a bound test always sits under
|
|
647
|
+
// another test of the same tree; one that OPENS the region did not come from this
|
|
648
|
+
// dispatch, and reading it as a case turns a comparison chain into a `switch`;
|
|
649
|
+
// - a singleton branch onto another TEST of the scrutinee, which is the search
|
|
650
|
+
// descending to pin the value. It is dispatch, so the walk reads it as dispatch —
|
|
651
|
+
// recovering it, or declining at PRE4 if it is not collapsible.
|
|
652
|
+
const k = switchAllowsBoundCase && blk !== b && !isTestOn(taken) ? singletonTaken(ti) : null;
|
|
653
|
+
if (!(k === null ? asNav(taken) : asCase(taken, k))) {
|
|
416
654
|
return null;
|
|
417
655
|
}
|
|
418
|
-
if (!
|
|
656
|
+
if (!asNav(fall)) {
|
|
419
657
|
return null;
|
|
420
658
|
}
|
|
421
659
|
}
|
|
@@ -426,14 +664,64 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
426
664
|
} // not worth a switch (m2c: ≥2 cases)
|
|
427
665
|
// The default is the single non-test leaf that is NOT a case body. 0 → no default; ≥2 distinct → decline.
|
|
428
666
|
const caseBlocks = new Set(cases.values());
|
|
429
|
-
const
|
|
430
|
-
|
|
667
|
+
const leaves = new Set([...defaultCands].filter((d) => !caseBlocks.has(d)));
|
|
668
|
+
// RESOLVE THROUGH a bare jump onto another candidate. `forwardingTarget` (ir/core.ts) stops at
|
|
669
|
+
// a `br` that carries block ARGUMENTS — skipping it would drop the values it supplies — so a
|
|
670
|
+
// leaf holding `b .Ldefault(v)` and `.Ldefault` itself arrive here as two distinct candidates
|
|
671
|
+
// even though one merely jumps to the other. They are not two defaults: the jumping leaf emits
|
|
672
|
+
// nothing of its own, and the values its `br` hands the other's parameters are the values the
|
|
673
|
+
// DISPATCH hands them. So this is `forwardingTarget`'s walk with the args-carrying step
|
|
674
|
+
// ADMITTED rather than refused, and it does not drop those values either — the step becomes one
|
|
675
|
+
// more dispatch edge, hoisted with the rest, where the hoist's disagreement rule decides
|
|
676
|
+
// whether the two paths can share one statement. (ir/core.ts lists the non-callers and why.)
|
|
677
|
+
//
|
|
678
|
+
// A cycle of such jumps has no target to resolve to and declines. Leaves that pass DIFFERENT
|
|
679
|
+
// values, or that have a body, are untouched and are still two defaults below. A SET so a leaf
|
|
680
|
+
// walked twice — L → X → D reaches X from L's walk and again from X's own turn in the loop —
|
|
681
|
+
// is recorded once.
|
|
682
|
+
const throughEdges = new Set<Block>();
|
|
683
|
+
const resolveDefault = (d: Block): Block | null => {
|
|
684
|
+
const walked = new Set<Block>();
|
|
685
|
+
let cur = d;
|
|
686
|
+
for (;;) {
|
|
687
|
+
if (walked.has(cur)) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
walked.add(cur);
|
|
691
|
+
const t = cur.ops[0];
|
|
692
|
+
if (!isBareExit(cur) || t.opcode !== 'br' || !leaves.has(t.successors[0].block)) {
|
|
693
|
+
return cur;
|
|
694
|
+
}
|
|
695
|
+
throughEdges.add(cur);
|
|
696
|
+
cur = t.successors[0].block;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const defaults: Block[] = [];
|
|
700
|
+
for (const d of leaves) {
|
|
701
|
+
const r = resolveDefault(d);
|
|
702
|
+
if (r === null) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
if (!defaults.includes(r)) {
|
|
706
|
+
defaults.push(r);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// ONE default reached by SEVERAL leaves. `balance_case_nodes`/`emit_case_nodes` give each
|
|
710
|
+
// subtree that runs out of case values its own jump to the default, so agbcc's four-case tree
|
|
711
|
+
// reaches it through two `b .Ldefault` blocks, which comparing candidates by BLOCK would count
|
|
712
|
+
// as two different defaults and decline. Two leaves are the same default when each is
|
|
713
|
+
// a bare EXIT to the same place carrying the same values: nothing about them can then differ,
|
|
714
|
+
// so the representative emits what either would. Anything else — a leaf with a body, two
|
|
715
|
+
// leaves passing different values — is still two defaults and still declines.
|
|
716
|
+
if (defaults.length > 1 && !defaults.every((d) => sameBareExit(defaults[0], d))) {
|
|
431
717
|
return null;
|
|
432
718
|
}
|
|
433
719
|
const defaultBlk = defaults[0] ?? null;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
720
|
+
// A default entry that takes BLOCK PARAMETERS is admitted on the same terms a case entry is:
|
|
721
|
+
// the dispatch edge binds them and `hoistDispatchCopies` re-emits that binding above the
|
|
722
|
+
// `switch`. The hazard it stands over is silent — without that re-emission,
|
|
723
|
+
// `switch (x) { case 1: … case 2: … }` whose fall-out edge also carried `w = 0` drops the write
|
|
724
|
+
// and looks entirely ordinary doing it — so the admission is only as good as the hoist.
|
|
437
725
|
// A default candidate that is ALSO a case body means a relational edge hit a case leaf → ambiguous.
|
|
438
726
|
if ([...defaultCands].some((d) => caseBlocks.has(d))) {
|
|
439
727
|
return null;
|
|
@@ -449,13 +737,28 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
449
737
|
return null;
|
|
450
738
|
}
|
|
451
739
|
|
|
452
|
-
// PRE2 (
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
740
|
+
// PRE2 (arm exits), per SITE. A `break` arm closes; a `fallthrough` arm runs on into the one
|
|
741
|
+
// sibling it reaches, which `chainArms` then places directly under it. `unstructurable` is a
|
|
742
|
+
// shape no single linear switch spells — a body reaching two siblings, or a sibling on one path
|
|
743
|
+
// and the switch's end on another — and Regime A declines to if-recovery, which spells every
|
|
744
|
+
// one of those edges. (Regime B reads the same verdicts and fails LOUD on either.)
|
|
456
745
|
const merge = ipdom.get(b) ?? stop;
|
|
457
746
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
458
|
-
|
|
747
|
+
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
748
|
+
const exitOf = new Map<Block, ArmExit>();
|
|
749
|
+
for (const t of siblings) {
|
|
750
|
+
exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
|
|
751
|
+
}
|
|
752
|
+
// A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
|
|
753
|
+
// for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
|
|
754
|
+
// same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
|
|
755
|
+
// backend because the backend's refusal is terminal (the whole function becomes a stub) while
|
|
756
|
+
// this one costs nothing but the `switch` spelling.
|
|
757
|
+
if (
|
|
758
|
+
[...exitOf.values()].some(
|
|
759
|
+
(e) => e.kind === 'unstructurable' || (!spellSwitchFallthrough && e.kind === 'fallthrough'),
|
|
760
|
+
)
|
|
761
|
+
) {
|
|
459
762
|
return null;
|
|
460
763
|
}
|
|
461
764
|
|
|
@@ -469,29 +772,198 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
469
772
|
}
|
|
470
773
|
}
|
|
471
774
|
|
|
472
|
-
//
|
|
473
|
-
//
|
|
775
|
+
// ARM ORDER. The case values are disjoint (PRE3), so where no arm falls through the order
|
|
776
|
+
// carries no meaning and is pure matching evidence: ascending case VALUE is the neutral
|
|
777
|
+
// spelling, and where a compiler has declared `switchArmsFollowLayout` the layout of the bodies
|
|
778
|
+
// is the SOURCE's arm order instead. A FALLING arm's position is not free, and `chainArms`
|
|
779
|
+
// below re-threads this order for those — reading it for the chain HEADS only.
|
|
780
|
+
//
|
|
781
|
+
// TWO arms the layout cannot order, both falling back rather than recovering:
|
|
782
|
+
// - two case VALUES sharing one body block share its index, so the tie is one the merge (or
|
|
783
|
+
// the source's own stacked labels) erased, and ascending value breaks it. They become ONE
|
|
784
|
+
// arm below, so the tie only orders that arm against the others. ADJACENT stacked labels
|
|
785
|
+
// never get here: `case 2: case 3:` compiles to a range test the walk reads as navigation
|
|
786
|
+
// and declines, while `case 0: case 2:` compiles to the two equality tests it recovers —
|
|
787
|
+
// both checked against agbcc;
|
|
788
|
+
// - an arm with no body of its own (`case k: break;`) has its edge resolve to the MERGE, so it
|
|
789
|
+
// inherits the merge's index and sorts after every arm that HAS a body.
|
|
790
|
+
//
|
|
791
|
+
// BOOKED, UNPAID: the grouping makes arm ORDER observable on a shape the ascending-value
|
|
792
|
+
// fallback did not have before, and IDO turns out to lay case bodies out in SOURCE order too —
|
|
793
|
+
// the same function with `case 1:` written first and last differ (.text md5 ec39af99 against
|
|
794
|
+
// 689f34ec, 144 bytes each), which is what `switchArmsFollowLayout` recovers. target.ts sets
|
|
795
|
+
// the bar for opting a compiler in at a SOURCE-level argument about its passes, not at two
|
|
796
|
+
// objects, and that argument is unpaid for IDO; nothing here changes while it is. What the
|
|
797
|
+
// grouping cannot do is make a non-agbcc row worse, because the ungrouped spelling is not a
|
|
798
|
+
// rival there: under IDO it compiles to two copies and a different ROM entirely.
|
|
474
799
|
const scrutExpr = expr(scrut);
|
|
475
|
-
const sortedCases = [...cases.entries()].sort((a, c) =>
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
800
|
+
const sortedCases = [...cases.entries()].sort((a, c) =>
|
|
801
|
+
switchArmsFollowLayout ? layoutIndex(a[1]) - layoutIndex(c[1]) || a[0] - c[0] : a[0] - c[0],
|
|
802
|
+
);
|
|
803
|
+
// TWO VALUES ONE BODY IS ONE ARM. `SwitchCase.values` stacks labels for exactly this, and the
|
|
804
|
+
// jump-table regime groups the same way (structure.ts's `armOf`). Emitting the body once per
|
|
805
|
+
// label is `structureRegion` run twice over one block: a duplication, not a spelling choice.
|
|
806
|
+
//
|
|
807
|
+
// WHY BLOCK IDENTITY IS THE KEY, and not a body-equality one like `sameBareExit` above. agbcc
|
|
808
|
+
// MERGES two written-out copies into one block — target.ts's `switchArmsFollowLayout` note
|
|
809
|
+
// says so from agbcc's own sources, SRCS compiling jump.c — and compiling both directions at
|
|
810
|
+
// TOOLCHAIN.agbccFlags says WHERE the merged block lands: at the last copy's position. So
|
|
811
|
+
// `case 0: A break; case 1: … case 2: A break;` and the grouped arm placed THERE are one
|
|
812
|
+
// object (.text md5 555abb1a), while the grouped arm placed at the first value is not
|
|
813
|
+
// (fe4d7d35). That is why the grouped spelling round-trips rather than merely reading shorter:
|
|
814
|
+
// agbcc declares `switchArmsFollowLayout`, so the arm goes exactly where the merged block sits.
|
|
815
|
+
// IDO does not merge at all — 224 bytes against the grouped 144 — so on MIPS a shared block can
|
|
816
|
+
// only have come from stacked labels. Two DISTINCT blocks with equal bodies therefore mean one
|
|
817
|
+
// arm on neither compiler: under agbcc that ROM is unreachable, under IDO it is what two arms
|
|
818
|
+
// compile to. Sound also because two dispatch edges onto ONE body cannot bind one name two
|
|
819
|
+
// ways: `hoistDispatchCopies` merges every collapsed edge's copies into a single statement
|
|
820
|
+
// above the `switch` and refuses on disagreement (structure.ts `hoistedDispatchAssigns`), so
|
|
821
|
+
// an arm reached by two case values is reached with one set of parameter values or the whole
|
|
822
|
+
// recovery declines.
|
|
823
|
+
//
|
|
824
|
+
// An arm takes the position of its FIRST value, which keeps the sort above. `defaultLayoutPos`
|
|
825
|
+
// is handed the GROUPED entry list because what it returns is an INDEX INTO the arm array, so
|
|
826
|
+
// the list it counts and the list it indexes must be one list.
|
|
827
|
+
const armsByBlock = new Map<Block, number[]>();
|
|
828
|
+
for (const [k, blk] of sortedCases) {
|
|
829
|
+
const prev = armsByBlock.get(blk);
|
|
830
|
+
if (prev) {
|
|
831
|
+
prev.push(k);
|
|
832
|
+
} else {
|
|
833
|
+
armsByBlock.set(blk, [k]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// The `default:` arm is a chain member too — an arm may run on into it, and C prints it last.
|
|
837
|
+
const dfltArm = defaultBlk !== null && defaultBlk !== merge ? defaultBlk : null;
|
|
838
|
+
const preChain = [...armsByBlock.keys()];
|
|
839
|
+
const entries = chainArms(preChain, dfltArm, exitOf);
|
|
840
|
+
if (entries === null) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
// Did the chain move anything? The arm-order POLICY above produced `preChain`; where the chain
|
|
844
|
+
// left it alone, every position still means what that policy said it meant.
|
|
845
|
+
const orderIntact = entries.every((e, i) => e === preChain[i]);
|
|
846
|
+
const fallsInto = (blk: Block): Block | null => {
|
|
847
|
+
const x = exitOf.get(blk);
|
|
848
|
+
return x?.kind === 'fallthrough' ? x.to : null;
|
|
849
|
+
};
|
|
850
|
+
// THE DISPATCH HOIST. Every edge the tree walk is about to COLLAPSE — from any test block, to
|
|
851
|
+
// a case entry, a default candidate, the merge, or another test block — carries the parallel
|
|
852
|
+
// copy that binds its target's parameters, and collapsing the tree is what would discard it.
|
|
853
|
+
// Merge those copies and emit them ONCE, above the `switch`. Placed FIRST because it is
|
|
854
|
+
// emitted first and `argAssignsFor` mints swap-cycle temp names as it goes.
|
|
855
|
+
//
|
|
856
|
+
// TWO CONDITIONS ARE THIS REGIME'S OWN, and both must hold before the emission is asked for:
|
|
857
|
+
//
|
|
858
|
+
// - AVAILABILITY AT THE ROOT. A hoisted copy is evaluated at `b`, not on its edge, so every
|
|
859
|
+
// argument it reads must be defined at a block that DOMINATES `b`. An argument computed
|
|
860
|
+
// inside a collapsed test block is not: the tree reaches that block only on some paths,
|
|
861
|
+
// while the hoist runs on all of them, and re-rendering it above the dispatch would
|
|
862
|
+
// SPECULATE it. (That is the one invariant PRE4's "a use is dominated by its def, so a
|
|
863
|
+
// collapsed op runs on a subset of the paths it already ran on" does not give the hoist,
|
|
864
|
+
// which is why the hoist asks for it here rather than inheriting it.)
|
|
865
|
+
// - The emission's own two refusals, stated at `hoistedDispatchAssigns`: edges disagreeing
|
|
866
|
+
// about one name, and a hoisted name whose value is still live at the switch or into an arm.
|
|
867
|
+
//
|
|
868
|
+
// Either way the answer is a decline to if-recovery, which spells every copy the asm performs.
|
|
869
|
+
const availableAtRoot = (v: Value): boolean => {
|
|
870
|
+
const home = blockOf(v);
|
|
871
|
+
return home !== undefined && dom.get(b)!.has(home);
|
|
872
|
+
};
|
|
873
|
+
const dispatchEdges: { pred: Block; succ: { block: Block; args: Value[] } }[] = [];
|
|
874
|
+
for (const t of new Set([...seen, ...throughEdges])) {
|
|
875
|
+
for (const e of t.ops[t.ops.length - 1].successors) {
|
|
876
|
+
if (e.block.params.length === 0) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (!e.args.every(availableAtRoot)) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
dispatchEdges.push({ pred: t, succ: e });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const hoisted = hoistDispatchCopies(dispatchEdges, [
|
|
886
|
+
b,
|
|
887
|
+
...caseBlocks,
|
|
888
|
+
...(defaultBlk ? [defaultBlk] : []),
|
|
889
|
+
...(merge ? [merge] : []),
|
|
890
|
+
]);
|
|
891
|
+
if (hoisted === null) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
// Bodies are structured in EMISSION order — `argAssignsFor` mints swap-cycle temp names as it
|
|
895
|
+
// goes, so building them out of order changes the output — and each falling arm's target is
|
|
896
|
+
// re-read off that order rather than trusted from the ordering above: this is the seam where a
|
|
897
|
+
// POSITION acquires control-flow meaning (the l3/ast.ts non-neutrality note).
|
|
898
|
+
const outCases: SwitchCase[] = [];
|
|
899
|
+
for (const [i, blk] of entries.entries()) {
|
|
900
|
+
const to = fallsInto(blk);
|
|
901
|
+
if (to !== null && to !== (entries[i + 1] ?? dfltArm)) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
// The arm fallen INTO may take block parameters, and the TWO paths that reach it are spelled
|
|
905
|
+
// in two different places: entering by its own case value takes the hoisted copy above the
|
|
906
|
+
// `switch` (`hoistDispatchCopies`), while FALLING in takes the copies the falling arm's own
|
|
907
|
+
// `br` emits as its last statements (`structureRegion(blk, to)` walks that terminator, so
|
|
908
|
+
// they are already there). Getting this wrong is SILENT, which is why it rests on the hoist
|
|
909
|
+
// rather than on a local reading of the arm.
|
|
910
|
+
//
|
|
911
|
+
// Regime B refuses the same hazard LOUD at its own `switch_br` path, on the weaker predicate
|
|
912
|
+
// that fits it (the copies `argAssignsFor` actually produced): its arms take their edge
|
|
913
|
+
// copies PER ARM, so a fall-through path would re-run them over what the falling arm
|
|
914
|
+
// computed. Hoisting is what removes that hazard, and Regime B does not hoist — booked, not
|
|
915
|
+
// built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
|
|
916
|
+
outCases.push({
|
|
917
|
+
values: armsByBlock.get(blk)!,
|
|
918
|
+
body: structureRegion(blk, to ?? merge),
|
|
919
|
+
fallsThrough: to !== null,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
481
922
|
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
482
923
|
// would carry no statement, which says nothing and is not valid C89.
|
|
483
924
|
const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
|
|
925
|
+
// The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
|
|
926
|
+
// (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
|
|
927
|
+
// into it and no other subtree jumps there — the two references the tree walk can count.
|
|
928
|
+
const dispatchTargets = [...seen].flatMap((t) =>
|
|
929
|
+
t.ops[t.ops.length - 1].successors.map((e) => forwardingTarget(e.block)),
|
|
930
|
+
);
|
|
931
|
+
// THE TWO CONJUNCTS READ THE EDGES DIFFERENTLY, and deliberately. The first is asked RAW, of
|
|
932
|
+
// the terminator's own second successor, because a surviving `b .Ldefault` block is a jump
|
|
933
|
+
// that did NOT collapse into a fall-through — resolving it away would count the jump as the
|
|
934
|
+
// running-out it is not (see the `expand_end_case` paragraph on `defaultLayoutPos`). The
|
|
935
|
+
// second is asked FORWARDED, over `dispatchTargets`, because there a forwarder is transparent:
|
|
936
|
+
// two edges that arrive through one are two references to the same block, which is exactly
|
|
937
|
+
// what "named more than once" has to count.
|
|
938
|
+
const fellThroughIntoIt = (blk: Block) =>
|
|
939
|
+
[...seen].some((t) => {
|
|
940
|
+
const succ = t.ops[t.ops.length - 1].successors;
|
|
941
|
+
return succ.length > 1 && succ[1].block === blk;
|
|
942
|
+
});
|
|
943
|
+
const namedOnlyOnce = (blk: Block) => dispatchTargets.filter((e) => e === blk).length < 2;
|
|
944
|
+
const ranOutInto = (blk: Block) => fellThroughIntoIt(blk) && namedOnlyOnce(blk);
|
|
945
|
+
// `defaultLayoutPos` owns which POSITIONS a chain makes unreadable — see its fall-through
|
|
946
|
+
// withholdings W5..W7. A chain elsewhere in the switch does not delete the evidence for
|
|
947
|
+
// where the label goes, and reading it off the emitted arms is what lets a `default:` written
|
|
948
|
+
// between two closed arms keep its place while a chain runs beside it.
|
|
949
|
+
const defaultAt = defaultBlk
|
|
950
|
+
? defaultLayoutPos(
|
|
951
|
+
defaultBlk,
|
|
952
|
+
entries.map((e, i) => ({ entry: e, fallsThrough: outCases[i].fallsThrough })),
|
|
953
|
+
{ placedByDispatch: ranOutInto(defaultBlk), orderIntact },
|
|
954
|
+
)
|
|
955
|
+
: undefined;
|
|
484
956
|
const sw: Stmt = {
|
|
485
957
|
k: 'switch',
|
|
486
958
|
scrutinee: scrutExpr,
|
|
487
959
|
cases: outCases,
|
|
488
|
-
...(defBody.length ? { default: defBody } : {}),
|
|
960
|
+
...(defBody.length ? { default: defBody, ...(defaultAt !== undefined ? { defaultAt } : {}) } : {}),
|
|
489
961
|
};
|
|
490
|
-
const out: Stmt[] = [sw];
|
|
962
|
+
const out: Stmt[] = [...hoisted, sw];
|
|
491
963
|
if (merge && merge !== stop) {
|
|
492
964
|
out.push(...structureRegion(merge, stop));
|
|
493
965
|
}
|
|
494
966
|
return out;
|
|
495
967
|
};
|
|
496
|
-
return { recognizeSwitch, analyzeArmExit };
|
|
968
|
+
return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
|
|
497
969
|
}
|