@asmlift/core 0.4.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 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- 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 +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -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 +649 -219
- 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 +23 -4
- 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 +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- 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 +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- 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 +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- 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,20 +19,220 @@ 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
|
}
|
|
30
54
|
|
|
55
|
+
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
56
|
+
* as C at all, and with or without a `break`.
|
|
57
|
+
*
|
|
58
|
+
* - `break` every path out of the arm reaches the switch's merge (or returns / loops
|
|
59
|
+
* inside the arm). The ordinary closed arm.
|
|
60
|
+
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
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).
|
|
64
|
+
* - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
|
|
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. */
|
|
67
|
+
export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
|
|
68
|
+
|
|
31
69
|
export interface SwitchRecovery {
|
|
32
70
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
33
|
-
/** shared with the Regime-B (`switch_br`) path in structure.ts
|
|
34
|
-
|
|
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. */
|
|
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();
|
|
35
236
|
}
|
|
36
237
|
|
|
37
238
|
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
@@ -44,14 +245,100 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
44
245
|
isNamed,
|
|
45
246
|
isCmpOpcode,
|
|
46
247
|
switchAllowsNeqCase,
|
|
47
|
-
|
|
248
|
+
switchAllowsBoundCase,
|
|
249
|
+
switchArmsFollowLayout,
|
|
250
|
+
spellSwitchFallthrough,
|
|
251
|
+
emitsOwnStatement,
|
|
252
|
+
blockOf,
|
|
253
|
+
hoistDispatchCopies,
|
|
48
254
|
expr,
|
|
49
255
|
structureRegion,
|
|
50
256
|
} = deps;
|
|
51
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
|
+
|
|
52
338
|
// --- Regime A: comparison-tree switch recovery ----------------------------------------------------
|
|
53
339
|
// Every ambiguity declines. Four preconditions are enforced below, annotated PRE1..PRE4:
|
|
54
|
-
// 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.
|
|
55
342
|
|
|
56
343
|
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
57
344
|
// `250 << 2` for a large sparse case) to a number — else null.
|
|
@@ -124,20 +411,23 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
124
411
|
}
|
|
125
412
|
};
|
|
126
413
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// the switch
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 => {
|
|
141
431
|
const term = blk.ops[blk.ops.length - 1];
|
|
142
432
|
if (term.opcode !== 'cond_br') {
|
|
143
433
|
return null;
|
|
@@ -146,9 +436,6 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
146
436
|
if (!cmp || !isCmpOpcode(cmp.opcode)) {
|
|
147
437
|
return null;
|
|
148
438
|
}
|
|
149
|
-
if (!isRoot && (blk.ops.some((op) => SIDE_EFFECTFUL.has(op.opcode)) || emitsAnchoredWrite(blk))) {
|
|
150
|
-
return null;
|
|
151
|
-
} // PRE4 — anchored writes included: discarded with the block, while their edge copies stay suppressed
|
|
152
439
|
// Which operand is the scrutinee, which is the constant?
|
|
153
440
|
const [lo, ro] = cmp.operands;
|
|
154
441
|
const lc = evalConst(lo),
|
|
@@ -169,64 +456,80 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
169
456
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
170
457
|
};
|
|
171
458
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
case 'icmp_ugt':
|
|
192
|
-
return l > r;
|
|
193
|
-
case 'icmp_sge':
|
|
194
|
-
case 'icmp_uge':
|
|
195
|
-
return l >= r;
|
|
196
|
-
default:
|
|
197
|
-
return false;
|
|
459
|
+
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
460
|
+
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
461
|
+
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
462
|
+
// switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
|
|
463
|
+
// ordinary `break`, not as falling into the default.
|
|
464
|
+
//
|
|
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`.
|
|
469
|
+
//
|
|
470
|
+
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
471
|
+
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
472
|
+
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
473
|
+
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
474
|
+
// back. Costly for matching, correct to run.
|
|
475
|
+
const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
|
|
476
|
+
if (entry === merge) {
|
|
477
|
+
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
198
478
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (s
|
|
479
|
+
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
480
|
+
let toMerge = false,
|
|
481
|
+
escapes = false;
|
|
482
|
+
const seen = new Set<Block>([entry]);
|
|
483
|
+
const q = [entry];
|
|
484
|
+
while (q.length) {
|
|
485
|
+
const cur = q.pop()!;
|
|
486
|
+
for (const s of successorsOf(cur)) {
|
|
487
|
+
if (s === merge) {
|
|
488
|
+
toMerge = true;
|
|
489
|
+
} else if (s !== entry && siblings.has(s)) {
|
|
490
|
+
into.add(s);
|
|
491
|
+
} else if (s !== entry && !seen.has(s)) {
|
|
492
|
+
if (!dom.get(s)!.has(b)) {
|
|
493
|
+
escapes = true;
|
|
213
494
|
continue;
|
|
214
495
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (inRegion(s) && !rseen.has(s)) {
|
|
219
|
-
rseen.add(s);
|
|
220
|
-
q.push(s);
|
|
221
|
-
}
|
|
496
|
+
seen.add(s);
|
|
497
|
+
q.push(s);
|
|
222
498
|
}
|
|
223
499
|
}
|
|
224
500
|
}
|
|
225
|
-
|
|
501
|
+
if (into.size === 0) {
|
|
502
|
+
return { kind: 'break' };
|
|
503
|
+
}
|
|
504
|
+
if (into.size === 1 && !toMerge && !escapes) {
|
|
505
|
+
return { kind: 'fallthrough', to: [...into][0] };
|
|
506
|
+
}
|
|
507
|
+
// Name what is actually missing. These three are different facts, and only the first is a shape
|
|
508
|
+
// C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
|
|
509
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
510
|
+
if (into.size > 1) {
|
|
511
|
+
return {
|
|
512
|
+
kind: 'unstructurable',
|
|
513
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (escapes) {
|
|
517
|
+
return {
|
|
518
|
+
kind: 'unstructurable',
|
|
519
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
kind: 'unstructurable',
|
|
524
|
+
// `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
|
|
525
|
+
// asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
|
|
526
|
+
// (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
|
|
527
|
+
why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
|
|
528
|
+
};
|
|
226
529
|
};
|
|
227
530
|
|
|
228
531
|
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
229
|
-
const root = testInfo(b
|
|
532
|
+
const root = testInfo(b);
|
|
230
533
|
if (!root) {
|
|
231
534
|
return null;
|
|
232
535
|
}
|
|
@@ -249,27 +552,10 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
249
552
|
// Walk the test tree. `cases`: value → case-entry block. `defaultCands`: leaves reached without an
|
|
250
553
|
// equality pin. A test-block DAG cycle, or a `!=` case when the compiler disallows it, declines.
|
|
251
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.
|
|
252
558
|
const defaultCands = new Set<Block>();
|
|
253
|
-
// Skip pure forwarding blocks — a block whose only op is an unconditional `br` (no side effects, no
|
|
254
|
-
// params). agbcc's binary-search layout branches to the shared default through such empty `b .Ldef`
|
|
255
|
-
// blocks; without skipping them each becomes a DISTINCT default candidate and the whole tree declines.
|
|
256
|
-
const skipForward = (blk: Block): Block => {
|
|
257
|
-
let cur = blk;
|
|
258
|
-
const guard = new Set<Block>();
|
|
259
|
-
// Only skip a truly empty forwarding block: a lone `br` with no params AND no successor ARGS —
|
|
260
|
-
// an edge that carries a phi arg is NOT transparent (skipping it would drop that assignment).
|
|
261
|
-
while (
|
|
262
|
-
cur.ops.length === 1 &&
|
|
263
|
-
cur.ops[0].opcode === 'br' &&
|
|
264
|
-
cur.params.length === 0 &&
|
|
265
|
-
cur.ops[0].successors[0].args.length === 0 &&
|
|
266
|
-
!guard.has(cur)
|
|
267
|
-
) {
|
|
268
|
-
guard.add(cur);
|
|
269
|
-
cur = cur.ops[0].successors[0].block;
|
|
270
|
-
}
|
|
271
|
-
return cur;
|
|
272
|
-
};
|
|
273
559
|
// Concretely SIMULATE the decision tree for a scrutinee value `xv`, returning the leaf block it
|
|
274
560
|
// reaches (or null on an unexpected cycle). This is PRE3 done concretely: it lets us verify each
|
|
275
561
|
// recovered case value actually routes to its recorded body in the ORIGINAL tree.
|
|
@@ -277,7 +563,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
277
563
|
let cur = b;
|
|
278
564
|
const guard = new Set<Block>();
|
|
279
565
|
for (;;) {
|
|
280
|
-
const ti = testInfo(cur
|
|
566
|
+
const ti = testInfo(cur);
|
|
281
567
|
if (!ti || ti.x !== scrut) {
|
|
282
568
|
return cur;
|
|
283
569
|
} // reached a leaf (case body / default)
|
|
@@ -287,7 +573,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
287
573
|
guard.add(cur);
|
|
288
574
|
const term = cur.ops[cur.ops.length - 1];
|
|
289
575
|
const taken = evalCmp(ti.opcode, ti.xOnLeft, xv, ti.k);
|
|
290
|
-
cur =
|
|
576
|
+
cur = forwardingTarget(term.successors[taken ? 0 : 1].block);
|
|
291
577
|
}
|
|
292
578
|
};
|
|
293
579
|
const seen = new Set<Block>();
|
|
@@ -298,59 +584,76 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
298
584
|
return null;
|
|
299
585
|
} // a test-block DAG cycle → decline
|
|
300
586
|
seen.add(blk);
|
|
301
|
-
const ti = testInfo(blk
|
|
587
|
+
const ti = testInfo(blk);
|
|
302
588
|
if (!ti || ti.x !== scrut) {
|
|
303
589
|
return null;
|
|
304
590
|
} // PRE1: every test is on the SAME Value
|
|
591
|
+
if (blk !== b && !collapsible(blk)) {
|
|
592
|
+
return null;
|
|
593
|
+
} // PRE4
|
|
305
594
|
const term = blk.ops[blk.ops.length - 1];
|
|
306
|
-
const taken =
|
|
307
|
-
fall =
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
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)) {
|
|
325
619
|
work.push(child);
|
|
326
620
|
return true;
|
|
327
621
|
}
|
|
328
|
-
defaultCands.add(child);
|
|
622
|
+
defaultCands.add(child);
|
|
329
623
|
return true;
|
|
330
624
|
};
|
|
331
625
|
if (ti.cls === 'eq') {
|
|
332
|
-
if (!
|
|
626
|
+
if (!asCase(taken, ti.k)) {
|
|
333
627
|
return null;
|
|
334
628
|
} // x==k → taken is case k
|
|
335
|
-
if (!
|
|
629
|
+
if (!asNav(fall)) {
|
|
336
630
|
return null;
|
|
337
631
|
}
|
|
338
632
|
} else if (ti.cls === 'ne') {
|
|
339
633
|
if (!switchAllowsNeqCase) {
|
|
340
634
|
return null;
|
|
341
635
|
} // per-compiler gate
|
|
342
|
-
if (!
|
|
636
|
+
if (!asCase(fall, ti.k)) {
|
|
343
637
|
return null;
|
|
344
638
|
} // x!=k → the EQUAL side (fall) is case k
|
|
345
|
-
if (!
|
|
639
|
+
if (!asNav(taken)) {
|
|
346
640
|
return null;
|
|
347
641
|
}
|
|
348
642
|
} else {
|
|
349
|
-
// relational →
|
|
350
|
-
|
|
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))) {
|
|
351
654
|
return null;
|
|
352
655
|
}
|
|
353
|
-
if (!
|
|
656
|
+
if (!asNav(fall)) {
|
|
354
657
|
return null;
|
|
355
658
|
}
|
|
356
659
|
}
|
|
@@ -361,14 +664,64 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
361
664
|
} // not worth a switch (m2c: ≥2 cases)
|
|
362
665
|
// The default is the single non-test leaf that is NOT a case body. 0 → no default; ≥2 distinct → decline.
|
|
363
666
|
const caseBlocks = new Set(cases.values());
|
|
364
|
-
const
|
|
365
|
-
|
|
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))) {
|
|
366
717
|
return null;
|
|
367
718
|
}
|
|
368
719
|
const defaultBlk = defaults[0] ?? null;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
372
725
|
// A default candidate that is ALSO a case body means a relational edge hit a case leaf → ambiguous.
|
|
373
726
|
if ([...defaultCands].some((d) => caseBlocks.has(d))) {
|
|
374
727
|
return null;
|
|
@@ -384,12 +737,28 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
384
737
|
return null;
|
|
385
738
|
}
|
|
386
739
|
|
|
387
|
-
// PRE2 (
|
|
388
|
-
//
|
|
389
|
-
//
|
|
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.)
|
|
390
745
|
const merge = ipdom.get(b) ?? stop;
|
|
391
746
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
392
|
-
|
|
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
|
+
) {
|
|
393
762
|
return null;
|
|
394
763
|
}
|
|
395
764
|
|
|
@@ -403,26 +772,198 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
403
772
|
}
|
|
404
773
|
}
|
|
405
774
|
|
|
406
|
-
//
|
|
407
|
-
//
|
|
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.
|
|
408
799
|
const scrutExpr = expr(scrut);
|
|
409
|
-
const sortedCases = [...cases.entries()].sort((a, c) =>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
922
|
+
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
923
|
+
// would carry no statement, which says nothing and is not valid C89.
|
|
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;
|
|
415
956
|
const sw: Stmt = {
|
|
416
957
|
k: 'switch',
|
|
417
958
|
scrutinee: scrutExpr,
|
|
418
959
|
cases: outCases,
|
|
419
|
-
...(
|
|
960
|
+
...(defBody.length ? { default: defBody, ...(defaultAt !== undefined ? { defaultAt } : {}) } : {}),
|
|
420
961
|
};
|
|
421
|
-
const out: Stmt[] = [sw];
|
|
962
|
+
const out: Stmt[] = [...hoisted, sw];
|
|
422
963
|
if (merge && merge !== stop) {
|
|
423
964
|
out.push(...structureRegion(merge, stop));
|
|
424
965
|
}
|
|
425
966
|
return out;
|
|
426
967
|
};
|
|
427
|
-
return { recognizeSwitch,
|
|
968
|
+
return { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms };
|
|
428
969
|
}
|