@asmlift/core 0.9.0 → 0.11.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// factory takes its dependencies EXPLICITLY (`SwitchRecoverDeps`); `expr`/`structureRegion` are
|
|
5
5
|
// late-bound callbacks into the emission phase, so case bodies reuse the ordinary structuring
|
|
6
6
|
// machinery (loops/ifs inside cases, the onStack guard).
|
|
7
|
+
import { constWord } from '../ir/bits';
|
|
7
8
|
import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
|
|
8
9
|
import { orderSensitive } from '../ir/discipline';
|
|
10
|
+
import { SWAPPED_ICMP } from '../ir/opcodes';
|
|
9
11
|
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
10
12
|
|
|
11
13
|
/** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
|
|
@@ -48,6 +50,11 @@ export interface SwitchRecoverDeps {
|
|
|
48
50
|
* of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
|
|
49
51
|
* both halves for def-site anchoring, rather than indexing the parameters a second time. */
|
|
50
52
|
blockOf: (v: Value) => Block | undefined;
|
|
53
|
+
/** The header and exit of the loop the switch is being structured in, or null outside any loop.
|
|
54
|
+
* Read LIVE: the structurer's loop frame is the one current when the switch is met. An edge to
|
|
55
|
+
* the exit from inside a case arm is a `break` of that loop, which C binds to the switch instead
|
|
56
|
+
* (`analyzeArmExit`), save where the exit is the switch's own merge. */
|
|
57
|
+
enclosingLoop: () => { header: Block; exit: Block | null } | null;
|
|
51
58
|
/** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
|
|
52
59
|
* ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
|
|
53
60
|
* (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
|
|
@@ -60,6 +67,10 @@ export interface SwitchRecoverDeps {
|
|
|
60
67
|
) => Stmt[] | null;
|
|
61
68
|
expr: (v: Value) => Expr;
|
|
62
69
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
70
|
+
/** One arm's body, from `entry` to `stop`, inside the switch whose code resumes at `merge`. The
|
|
71
|
+
* structurer owns what `break;` means there (structure.ts `structureArm`), so both regimes hand
|
|
72
|
+
* it every arm and the exit verdict they read for it. */
|
|
73
|
+
structureArm: (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined) => Stmt[];
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
@@ -70,18 +81,54 @@ export interface SwitchRecoverDeps {
|
|
|
70
81
|
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
71
82
|
* Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
|
|
72
83
|
* below arranges and both regimes then re-read off the emission array (see the
|
|
73
|
-
* l3/ast.ts non-neutrality note).
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
84
|
+
* l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
|
|
85
|
+
* switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
|
|
86
|
+
* `break;` on those paths.
|
|
87
|
+
* - `goto` a `fallthrough` exit whose sibling another arm already falls into: the same
|
|
88
|
+
* paths, ending in `goto` to that sibling's case label (l3/ast.ts) instead of
|
|
89
|
+
* running on into it. Only `jumpSharedFallTargets` below makes one, and only
|
|
90
|
+
* Regime B asks it.
|
|
91
|
+
* - `unstructurable` anything else: two different siblings, or a sibling beside a block the
|
|
92
|
+
* switch does not dominate. C needs a `goto` for those. Regime A declines to
|
|
93
|
+
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
|
|
94
|
+
*
|
|
95
|
+
* `region` is the arm's own blocks: every block the walk below reached, the entry included. It is
|
|
96
|
+
* what the structurer needs to find the arm's own joins (`structure.ts` `armJoin`) and to spell a
|
|
97
|
+
* path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
|
|
98
|
+
* falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
|
|
99
|
+
* sideways, into a block the switch does not dominate, has none: its paths through such a block
|
|
100
|
+
* are not the arm's, and it structures to the CFG's joins. It names those blocks instead
|
|
101
|
+
* (`escapedTo`, the first block outside the region on each such path), which decide whether its
|
|
102
|
+
* body can still be copied into the arms that fall into it (`copySharedFallTargets`). An arm that
|
|
103
|
+
* took such a copy carries the SIBLING's `escapedTo`, not its own first outside block: the blocks
|
|
104
|
+
* where the copy steps sideways, which is all the copies read of it. */
|
|
105
|
+
export type ArmExit =
|
|
106
|
+
| { kind: 'break'; region?: ReadonlySet<Block>; escapedTo?: ReadonlySet<Block> }
|
|
107
|
+
| { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
108
|
+
| { kind: 'goto'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
109
|
+
| { kind: 'unstructurable'; why: string };
|
|
78
110
|
|
|
79
111
|
export interface SwitchRecovery {
|
|
80
|
-
|
|
112
|
+
/** `mergeAt`, given the tree's case and default blocks, may name the switch's end ahead of the
|
|
113
|
+
* CFG's (structure.ts's join past early returns); `undefined` keeps `ipdom ?? stop`. */
|
|
114
|
+
recognizeSwitch: (
|
|
115
|
+
b: Block,
|
|
116
|
+
stop: Block | null,
|
|
117
|
+
mergeAt?: (arms: readonly Block[]) => Block | undefined,
|
|
118
|
+
) => Stmt[] | null;
|
|
81
119
|
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
82
120
|
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
83
121
|
* Regime B, which has no fallback, fails loud. */
|
|
84
|
-
analyzeArmExit: (
|
|
122
|
+
analyzeArmExit: (
|
|
123
|
+
entry: Block,
|
|
124
|
+
b: Block,
|
|
125
|
+
merge: Block | null,
|
|
126
|
+
siblings: Set<Block>,
|
|
127
|
+
stopAtLoop?: boolean,
|
|
128
|
+
jumps?: ReadonlySet<Block>,
|
|
129
|
+
) => ArmExit;
|
|
130
|
+
/** may the copies write out a sibling that leaves sideways into `s`? (`copySharedFallTargets`) */
|
|
131
|
+
forwardToMerge: (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>) => boolean;
|
|
85
132
|
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
86
133
|
* regimes read it from one definition (and one statement of what it rests on). */
|
|
87
134
|
layoutIndex: (blk: Block) => number;
|
|
@@ -158,18 +205,6 @@ const INT_MIN = -0x80000000;
|
|
|
158
205
|
const INT_MAX = 0x7fffffff;
|
|
159
206
|
const ALL: Ranges = [[INT_MIN, INT_MAX]];
|
|
160
207
|
|
|
161
|
-
/** The same opcode with its operands swapped: `k < x` is `x > k`. */
|
|
162
|
-
const MIRRORED: Record<string, string> = {
|
|
163
|
-
icmp_slt: 'icmp_sgt',
|
|
164
|
-
icmp_sgt: 'icmp_slt',
|
|
165
|
-
icmp_sle: 'icmp_sge',
|
|
166
|
-
icmp_sge: 'icmp_sle',
|
|
167
|
-
icmp_ult: 'icmp_ugt',
|
|
168
|
-
icmp_ugt: 'icmp_ult',
|
|
169
|
-
icmp_ule: 'icmp_uge',
|
|
170
|
-
icmp_uge: 'icmp_ule',
|
|
171
|
-
};
|
|
172
|
-
|
|
173
208
|
/** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
|
|
174
209
|
function fromUnsigned(lo: number, hi: number): Ranges {
|
|
175
210
|
if (lo > hi) {
|
|
@@ -214,7 +249,7 @@ function intersect(a: Ranges, b: Ranges): Ranges {
|
|
|
214
249
|
|
|
215
250
|
/** The register values that take the test's BRANCH. */
|
|
216
251
|
function takenBy(ti: TestInfo): Ranges {
|
|
217
|
-
const op = ti.xOnLeft ? ti.opcode : (
|
|
252
|
+
const op = ti.xOnLeft ? ti.opcode : (SWAPPED_ICMP[ti.opcode] ?? ti.opcode);
|
|
218
253
|
if (op === 'icmp_eq' || op === 'icmp_ne') {
|
|
219
254
|
const k = ti.k | 0;
|
|
220
255
|
return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
|
|
@@ -254,6 +289,204 @@ function singleton(r: Ranges): number | null {
|
|
|
254
289
|
return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
|
|
255
290
|
}
|
|
256
291
|
|
|
292
|
+
/** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as copies of it. A compiler that
|
|
293
|
+
* cross-jumps (agbcc's and gcc 2.7.2's `jump.c` cross-jumping, KMC's with it) finds two arms
|
|
294
|
+
* ending in the same code as a sibling arm and keeps one copy, so `case 1: g(v); gOut = v;
|
|
295
|
+
* break; case 2: gOut = v; break; case 3: h(v); gOut = v; break;` comes back with case 1 and
|
|
296
|
+
* case 3 both entering case 2's `gOut = v`. C drops into an arm from above along one edge only,
|
|
297
|
+
* so read as written that is two fall-throughs into one arm and `chainArms` refuses it — but the
|
|
298
|
+
* source had one fall-through at most.
|
|
299
|
+
*
|
|
300
|
+
* WHICH ENTRANT FALLS IS NOT IN THE ASM, so it is a ranked choice (`/tail-copies`,
|
|
301
|
+
* rank-variations.ts), never a fixed default. The cross-jump keeps ONE copy of the tail and
|
|
302
|
+
* says nothing about which arm the source wrote it under, and one compiler at one set of flags
|
|
303
|
+
* answers both ways: KMC `-O2` and gcc 2.7.2 `-O1` lay arm bodies out in SOURCE order, and a
|
|
304
|
+
* source whose fall-through arm is written directly above its target in case order recompiles
|
|
305
|
+
* only with that arm falling, while one whose arms are written out of case order (`case 3`,
|
|
306
|
+
* `case 2`, `case 1`) recompiles nearer with every entrant holding its own copy. Neither the
|
|
307
|
+
* layout nor the case order decides it: the copy the cross-jump kept is not the source's
|
|
308
|
+
* fall-through, so the arm that physically runs into the tail can be the wrong faller too.
|
|
309
|
+
*
|
|
310
|
+
* - `faller` (the default): at most one entrant keeps its fall-through, the one the caller's
|
|
311
|
+
* arm-order policy (`order`, before the chain moves anything) puts DIRECTLY ABOVE the sibling —
|
|
312
|
+
* on `switchArmsFollowLayout` targets (agbcc) its layout neighbour, elsewhere the arm before it in
|
|
313
|
+
* case order. Every other entrant copies; no entrant above it ⇒ every entrant copies.
|
|
314
|
+
* - not `faller`: every entrant copies, whichever arm sits above the sibling.
|
|
315
|
+
*
|
|
316
|
+
* Either way each copying entrant gets the sibling's region written out again under its own arm:
|
|
317
|
+
* its exit becomes a closed `break` over the two regions.
|
|
318
|
+
*
|
|
319
|
+
* A `default:` SIBLING takes the LAST arm of `order` as the arm above it: that is where `chainArms`
|
|
320
|
+
* puts a default that an arm falls into, at the end. It is not always where the label is printed —
|
|
321
|
+
* a target that prints `default:` at its layout position (`defaultLayoutPos`) can put it between two
|
|
322
|
+
* cases, and then the arm the source wrote above it is not `order`'s last, and its entrants all copy
|
|
323
|
+
* (a measured agbcc shape of that kind still recompiles to its target).
|
|
324
|
+
*
|
|
325
|
+
* A `default:` SIBLING MAY BE CODE THE SOURCE WROTE AFTER THE SWITCH. Where the other arms
|
|
326
|
+
* `return` from a void function they reach the same epilogue the default block does, so
|
|
327
|
+
* post-dominance ends the switch there, and arms that `break` to the code after the switch read as
|
|
328
|
+
* falling into the default block. The copies then write that code into each of them: the same
|
|
329
|
+
* program, not the source's spelling (which would end the switch at the default block and spell
|
|
330
|
+
* the other arms `return`, a reading nothing makes yet).
|
|
331
|
+
*
|
|
332
|
+
* ONLY OVER A CLOSED SIBLING. The copy is the sibling's region walked again inside the entrant's
|
|
333
|
+
* arm, so it is spelled only where that region ends the switch: the sibling's own exit a `break`
|
|
334
|
+
* WITH a region — it reaches no sibling of its own and leaves through no block the switch does
|
|
335
|
+
* not dominate. A sibling that falls on stays two-into-one for `chainArms` to refuse. PURE: the
|
|
336
|
+
* closed exit is exactly what `analyzeArmExit` reads for the entrant once the sibling is no
|
|
337
|
+
* longer a sibling — its walk reaches the sibling's blocks, and they reach the merge and nothing
|
|
338
|
+
* else — so it is assembled here rather than walked again.
|
|
339
|
+
*
|
|
340
|
+
* OR A SIBLING THAT LEAVES SIDEWAYS ONLY FORWARD. A closed sibling with no region stepped into
|
|
341
|
+
* blocks the switch does not dominate (`escapedTo`). Its body is still the switch's end on every
|
|
342
|
+
* path where each of those blocks runs only on to the merge or a `ret` — never back into the
|
|
343
|
+
* dispatch, a sibling (the entrants among them) or round a cycle; a path out through the
|
|
344
|
+
* enclosing loop's exit is one more path to a `ret`. `sidewaysEnds` answers that per block
|
|
345
|
+
* (`forwardToMerge`, the caller's to hand in; absent, no such sibling is copied). Nothing in it is
|
|
346
|
+
* particular to one compiler: it is asked of every jump table whose shared tail lost its region
|
|
347
|
+
* because a block on the tail's way to the merge is also reached from outside the switch. Where it
|
|
348
|
+
* shows:
|
|
349
|
+
* - agbcc's gcse PRE puts the next switch's index in the dispatch block, which then no longer
|
|
350
|
+
* dominates the default the bounds test reaches too, and the default's tail runs on through
|
|
351
|
+
* blocks other arms cross-jumped into (`synthetic:jt_xjdefpre`, kleod EntityCreate);
|
|
352
|
+
* - with no PRE at all (IDO 7.1 has no gcse), the tail runs on into a second jump table after the
|
|
353
|
+
* switch that the other branch of an `if` around it reaches too, and from there to the switch's
|
|
354
|
+
* end: in a `for` whose tail `continue`s (switch-arms.test.ts, the ido7.1 `jl_cont` fixture),
|
|
355
|
+
* or where a `return` in the tail makes the epilogue the merge, and the copies then write that
|
|
356
|
+
* whole second switch into each entrant.
|
|
357
|
+
* Each copying entrant then has no region either: like every arm that leaves sideways, it
|
|
358
|
+
* structures to the CFG's joins, which walks the sibling's blocks once inside it and stops at the
|
|
359
|
+
* merge.
|
|
360
|
+
*
|
|
361
|
+
* `onFallerKept` fires each time `faller` leaves an entrant its fall-through — exactly where the
|
|
362
|
+
* two settings of `faller` return different exits. Where it never fires over a whole structuring,
|
|
363
|
+
* the other setting builds the same tree, which is what rank.ts reads to skip `/tail-copies`.
|
|
364
|
+
* `onCopied` fires with the sibling each time an entrant takes a copy of a CASE arm (not the
|
|
365
|
+
* `default:` arm's), which is where `jumpSharedFallTargets` can read a `goto` instead: rank.ts's
|
|
366
|
+
* witness for `/case-goto`, and the siblings that variation hands it. */
|
|
367
|
+
export function copySharedFallTargets(
|
|
368
|
+
order: readonly Block[],
|
|
369
|
+
dflt: Block | null,
|
|
370
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
371
|
+
faller: boolean,
|
|
372
|
+
onFallerKept?: () => void,
|
|
373
|
+
sidewaysEnds?: (s: Block) => boolean,
|
|
374
|
+
onCopied?: (to: Block) => void,
|
|
375
|
+
): Map<Block, ArmExit> {
|
|
376
|
+
const out = new Map(exitOf);
|
|
377
|
+
for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, faller)) {
|
|
378
|
+
const own = exitOf.get(to);
|
|
379
|
+
if (own?.kind !== 'break') {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const { region, escapedTo } = own;
|
|
383
|
+
if (
|
|
384
|
+
region === undefined &&
|
|
385
|
+
(escapedTo === undefined || sidewaysEnds === undefined || ![...escapedTo].every(sidewaysEnds))
|
|
386
|
+
) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const [e, x] of movers(onFallerKept)) {
|
|
390
|
+
if (to !== dflt) {
|
|
391
|
+
onCopied?.(to);
|
|
392
|
+
}
|
|
393
|
+
out.set(
|
|
394
|
+
e,
|
|
395
|
+
region === undefined
|
|
396
|
+
? { kind: 'break', escapedTo: escapedTo! }
|
|
397
|
+
: { kind: 'break', region: new Set([...x.region, ...region]) },
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as one fall-through and `goto`s to the
|
|
405
|
+
* sibling's case label — where the copies above do not take them (they are read first, and this
|
|
406
|
+
* is read over what they leave). It is the source's own spelling of the shape: `case 1: if (c)
|
|
407
|
+
* continue; goto L; case 2: if (d) continue; goto L; case 3: case 4: L: …` (kleod sub_0800D188's
|
|
408
|
+
* `goto block_1643;`), which every compiler that does not cross-jump a copy into it compiles with
|
|
409
|
+
* the sibling's code once. Unlike the copies it needs nothing of the sibling: the sibling's own exit
|
|
410
|
+
* is read for itself and may fall on, break, or leave sideways, since the jump does not write it
|
|
411
|
+
* out again.
|
|
412
|
+
*
|
|
413
|
+
* WHO FALLS is read from the one place the copies ask it too (`sharedFallTargets`), but with no
|
|
414
|
+
* choice in it: the entrant the caller's arm-order policy puts directly above the sibling always
|
|
415
|
+
* keeps its fall-through, and every other entrant jumps. A `goto` from that arm would land on the
|
|
416
|
+
* label directly below it, a path its plain fall-through already spells, so it is a jump goto-free
|
|
417
|
+
* C does not need (asmlift-101 §2.7). `/tail-copies`' setting of `faller` asks which entrant the
|
|
418
|
+
* SOURCE wrote the body under, a question a jump does not have: it writes no body.
|
|
419
|
+
*
|
|
420
|
+
* ONLY INTO A CASE ARM: the sibling is in `order`, never `dflt`, since only a case arm carries a
|
|
421
|
+
* label (l3/ast.ts `SwitchCase.label`) — and, given `only`, only into those siblings (`/case-goto`
|
|
422
|
+
* hands it the case arms the copies wrote back).
|
|
423
|
+
*
|
|
424
|
+
* WHAT MAKES THE JUMP THE SAME PROGRAM is the exit it replaces. A `goto` exit is a `fallthrough`
|
|
425
|
+
* exit with its last edge spelled as a jump: the arm is structured to stop at the sibling's entry
|
|
426
|
+
* exactly as a falling arm is (structure.ts `structureArm`), so every path that leaves the arm's
|
|
427
|
+
* body at its end is a path into the sibling, with the edge's own copies already run, and the
|
|
428
|
+
* `goto` is the arm's last top-level statement. The jump lands right after the sibling's case
|
|
429
|
+
* labels, which is where a fall-through lands; the emission refuses a sibling that opens with
|
|
430
|
+
* switch-edge copies, as it does for a fall-through: entering it by its own case value runs those,
|
|
431
|
+
* and the jump, landing above them, would run them again. No loop stands between the jump and the
|
|
432
|
+
* label: both are the switch's, in the loop the switch is in. */
|
|
433
|
+
export function jumpSharedFallTargets(
|
|
434
|
+
order: readonly Block[],
|
|
435
|
+
dflt: Block | null,
|
|
436
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
437
|
+
only?: ReadonlySet<Block>,
|
|
438
|
+
): Map<Block, ArmExit> {
|
|
439
|
+
const out = new Map(exitOf);
|
|
440
|
+
for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, true)) {
|
|
441
|
+
if (to === dflt || !order.includes(to) || (only !== undefined && !only.has(to))) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const [e, x] of movers()) {
|
|
445
|
+
out.set(e, { kind: 'goto', to, breaks: x.breaks, region: x.region });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
type Entrant = [Block, Extract<ArmExit, { kind: 'fallthrough' }>];
|
|
452
|
+
|
|
453
|
+
/** WHO FALLS, the one answer both rewrites above read: each arm two or more arms fall into (`to`,
|
|
454
|
+
* in the order its first entrant comes in `order` and then `dflt`), and `movers` — its entrants
|
|
455
|
+
* other than the one that keeps its fall-through, with their exits, which each rewrite spells its
|
|
456
|
+
* own way (a copy, a jump). The keeper is the arm the caller's arm-order policy puts directly
|
|
457
|
+
* above `to`, where `faller` is set (`dflt` sits below the last of `order`); `movers` fires
|
|
458
|
+
* `onFallerKept` when that arm is an entrant, which is where the two settings of `faller` differ.
|
|
459
|
+
* It fires on the call, so a rewrite that refuses `to` before asking reports nothing. */
|
|
460
|
+
function sharedFallTargets(
|
|
461
|
+
order: readonly Block[],
|
|
462
|
+
dflt: Block | null,
|
|
463
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
464
|
+
faller: boolean,
|
|
465
|
+
): { to: Block; movers: (onFallerKept?: () => void) => Entrant[] }[] {
|
|
466
|
+
const entrants = new Map<Block, Entrant[]>();
|
|
467
|
+
for (const e of [...order, ...(dflt ? [dflt] : [])]) {
|
|
468
|
+
const x = exitOf.get(e);
|
|
469
|
+
if (x?.kind === 'fallthrough') {
|
|
470
|
+
entrants.set(x.to, [...(entrants.get(x.to) ?? []), [e, x]]);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return [...entrants]
|
|
474
|
+
.filter(([, from]) => from.length >= 2)
|
|
475
|
+
.map(([to, from]) => {
|
|
476
|
+
const at = to === dflt ? order.length : order.indexOf(to);
|
|
477
|
+
const above = faller && at > 0 ? order[at - 1] : undefined;
|
|
478
|
+
return {
|
|
479
|
+
to,
|
|
480
|
+
movers: (onFallerKept?: () => void) => {
|
|
481
|
+
if (above !== undefined && from.some(([e]) => e === above)) {
|
|
482
|
+
onFallerKept?.();
|
|
483
|
+
}
|
|
484
|
+
return from.filter(([e]) => e !== above);
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
257
490
|
/** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
|
|
258
491
|
* fall-through chain is emitted contiguously and takes the position of its HEAD in `order`,
|
|
259
492
|
* which is the caller's own arm-order policy — so with no fall-through every chain is a
|
|
@@ -321,9 +554,11 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
321
554
|
spellSwitchFallthrough,
|
|
322
555
|
emitsOwnStatement,
|
|
323
556
|
blockOf,
|
|
557
|
+
enclosingLoop,
|
|
324
558
|
hoistDispatchCopies,
|
|
325
559
|
expr,
|
|
326
560
|
structureRegion,
|
|
561
|
+
structureArm,
|
|
327
562
|
} = deps;
|
|
328
563
|
|
|
329
564
|
// A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
|
|
@@ -333,8 +568,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
333
568
|
// the list for every conditional-return branch, wherever in the stream that branch sits. So
|
|
334
569
|
// its real blocks keep address order among themselves, and each synthetic one — a bare
|
|
335
570
|
// return, never a test — sorts after all of them;
|
|
336
|
-
// - raising
|
|
337
|
-
//
|
|
571
|
+
// - raising mostly REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}.ts
|
|
572
|
+
// all `filter`), but not only: raise/reroll.ts moves a rerolled loop's new head ahead of the
|
|
573
|
+
// old one and inserts a block of its own before it. So this order is the frontend's only
|
|
574
|
+
// where no reroll fired. A reader that needs the asm's own order wherever a pass rewrote the
|
|
575
|
+
// list reads the frontend's record instead (ir/core.ts `AsmLayout`, which the return-tail
|
|
576
|
+
// chooser reads); this one has not been moved onto it.
|
|
338
577
|
// TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
|
|
339
578
|
// - `switchArmsFollowLayout` (arm ORDER, the sort below) needs the full claim — no block moved
|
|
340
579
|
// at all — because it PLACES the arms from the layout;
|
|
@@ -427,75 +666,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
427
666
|
// has declared that its layout answers it — which SPELLING the source wrote.
|
|
428
667
|
|
|
429
668
|
// Fold a value that is a compile-time constant (a `const`, or a synthesized immediate like agbcc's
|
|
430
|
-
// `250 << 2` for a large sparse case) to a number — else null.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (!d) {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
if (d.opcode === 'const') {
|
|
440
|
-
return (d.attrs.value as number) | 0;
|
|
441
|
-
}
|
|
442
|
-
// Resolve the two operands of a binary op to constants (2-operand → both; 1-operand → operand +
|
|
443
|
-
// `imm` attr, which MUST be present, else decline — a missing imm would wrongly fold `and x` to 0).
|
|
444
|
-
const operands2 = (): [number, number] | null => {
|
|
445
|
-
const a = evalConst(d.operands[0]);
|
|
446
|
-
if (a === null) {
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
let c: number | null;
|
|
450
|
-
if (d.operands.length === 2) {
|
|
451
|
-
c = evalConst(d.operands[1]);
|
|
452
|
-
} else if (typeof d.attrs.imm === 'number') {
|
|
453
|
-
c = d.attrs.imm | 0;
|
|
454
|
-
} else {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
return c === null ? null : [a, c];
|
|
458
|
-
};
|
|
459
|
-
const bin = (f: (a: number, c: number) => number): number | null => {
|
|
460
|
-
const p = operands2();
|
|
461
|
-
return p === null ? null : f(p[0], p[1]) | 0;
|
|
462
|
-
};
|
|
463
|
-
const shift = (f: (a: number, c: number) => number): number | null => {
|
|
464
|
-
const p = operands2();
|
|
465
|
-
if (p === null || p[1] < 0 || p[1] >= 32) {
|
|
466
|
-
return null;
|
|
467
|
-
} // out-of-range shift amount → decline
|
|
468
|
-
return f(p[0], p[1]) | 0;
|
|
469
|
-
};
|
|
470
|
-
switch (d.opcode) {
|
|
471
|
-
case 'shl':
|
|
472
|
-
return shift((a, c) => a << c);
|
|
473
|
-
case 'shr_u':
|
|
474
|
-
return shift((a, c) => a >>> c);
|
|
475
|
-
case 'shr_s':
|
|
476
|
-
return shift((a, c) => a >> c);
|
|
477
|
-
case 'or':
|
|
478
|
-
return bin((a, c) => a | c);
|
|
479
|
-
case 'and':
|
|
480
|
-
return bin((a, c) => a & c);
|
|
481
|
-
case 'xor':
|
|
482
|
-
return bin((a, c) => a ^ c);
|
|
483
|
-
case 'add':
|
|
484
|
-
return bin((a, c) => a + c);
|
|
485
|
-
case 'sub':
|
|
486
|
-
return bin((a, c) => a - c);
|
|
487
|
-
case 'neg': {
|
|
488
|
-
const a = evalConst(d.operands[0]);
|
|
489
|
-
return a === null ? null : -a | 0;
|
|
490
|
-
}
|
|
491
|
-
case 'not': {
|
|
492
|
-
const a = evalConst(d.operands[0]);
|
|
493
|
-
return a === null ? null : ~a | 0;
|
|
494
|
-
}
|
|
495
|
-
default:
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
};
|
|
669
|
+
// `250 << 2` for a large sparse case) to a number — else null. The shared word evaluator
|
|
670
|
+
// (ir/bits.ts), which declines an opcode it has no rule for: a `neg_s` (Thumb `cmn` under a signed
|
|
671
|
+
// branch) is no word, so a guard against it is never read as a case bound. A named variable is
|
|
672
|
+
// not a constant, whatever set it.
|
|
673
|
+
const words = { defs, opaque: isNamed };
|
|
674
|
+
const evalConst = (v: Value): number | null => constWord(words, v);
|
|
499
675
|
|
|
500
676
|
// TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
|
|
501
677
|
// both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
|
|
@@ -541,6 +717,191 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
541
717
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
542
718
|
};
|
|
543
719
|
|
|
720
|
+
// Does `blk` leave the enclosing loop for its exit, other than by the loop's own bottom test (a
|
|
721
|
+
// block that also edges back to the header, which spells as the loop's condition)?
|
|
722
|
+
const breaksLoop = (blk: Block): boolean => {
|
|
723
|
+
const loop = enclosingLoop();
|
|
724
|
+
const succ = successorsOf(blk);
|
|
725
|
+
return loop !== null && loop.exit !== null && succ.includes(loop.exit) && !succ.includes(loop.header);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// Does a block some arm structures edge to `target`? Walked as `structureArm` walks an arm: every
|
|
729
|
+
// block reached from the arm's entry without stepping through `merge` or back into `header`. That
|
|
730
|
+
// is wider than `analyzeArmExit`'s dominated region on purpose: when Regime A recognises a sub-tree
|
|
731
|
+
// of the comparison tree, an arm can run on into the code of a case the tree's root already took,
|
|
732
|
+
// which the dispatch does not dominate, and `structureArm` structures that code inside the arm.
|
|
733
|
+
// A block that also edges back to `header` is the loop's bottom test, which spells as the loop's
|
|
734
|
+
// condition, not as the arm's (`breaksLoop`) — save where `header` IS the merge (a top-tested
|
|
735
|
+
// loop): no bottom test is then reachable without passing the merge, and an arm's
|
|
736
|
+
// `if (c) goto out;` is a block that edges to the exit and straight back into the header.
|
|
737
|
+
const armsReach = (entries: Iterable<Block>, merge: Block | null, header: Block, target: Block): boolean => {
|
|
738
|
+
const seen = new Set<Block>(entries);
|
|
739
|
+
for (const q = [...seen]; q.length;) {
|
|
740
|
+
const succ = successorsOf(q.pop()!);
|
|
741
|
+
if (succ.includes(target) && (header === merge || !succ.includes(header))) {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
for (const s of succ) {
|
|
745
|
+
if (s !== target && s !== merge && s !== header && !seen.has(s)) {
|
|
746
|
+
seen.add(s);
|
|
747
|
+
q.push(s);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// Can a path from `from` reach `to` without passing `through`? False when `to` is null (the end
|
|
755
|
+
// of the function) or is `through` itself.
|
|
756
|
+
const reachesAvoiding = (from: Block, to: Block | null, through: Block | null): boolean => {
|
|
757
|
+
if (to === null || to === through) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
const seen = new Set<Block>([from]);
|
|
761
|
+
for (const q = [from]; q.length;) {
|
|
762
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
763
|
+
if (s === to) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
if (s !== through && !seen.has(s)) {
|
|
767
|
+
seen.add(s);
|
|
768
|
+
q.push(s);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return false;
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// THE LOOP-EXIT MERGE, WHERE IT SPELLS. A switch whose merge is its enclosing loop's exit puts
|
|
776
|
+
// the exit region after the `switch`, INSIDE the loop body, and reaches the loop bottom only
|
|
777
|
+
// through the arms that do not break. That is the source's text on two conditions, both read off
|
|
778
|
+
// the CFG here, and on either failing the merge-past-stop gate below declines to if-recovery:
|
|
779
|
+
// - the exit region RETURNS: no path from `merge` reaches a block that dominates it (other
|
|
780
|
+
// than `merge` itself, a loop of the exit region's own) — the enclosing loop's header, or an
|
|
781
|
+
// OUTER loop's, whose back edge the inner body cannot spell
|
|
782
|
+
// (`for (j...) { for (;;) { switch ... } out: u += prev; }` runs on to the outer latch);
|
|
783
|
+
// - every loop-bottom TEST the arms reach (a block that edges back to the header) leaves for
|
|
784
|
+
// the merge itself, which the arm then spells as its `break;` (`latchexit`). One that leaves
|
|
785
|
+
// for any other block outside the loop is past what an arm structured against `merge` can
|
|
786
|
+
// say: the merge is then the loop's exit only because an early `return` made a shared
|
|
787
|
+
// epilogue the dispatch's post-dominator, while the code the bottom test leaves for runs
|
|
788
|
+
// before it (`for (w = 0; w < 4; w++) { if (c) return w; switch ... } r(t);` on
|
|
789
|
+
// gcc2.7.2kmc).
|
|
790
|
+
// And the loop's HEADER must not leave for the merge itself. One that does makes the loop
|
|
791
|
+
// top-tested on it, with the merge as the loop's own exit, which the exit region rendered after
|
|
792
|
+
// the switch inside the body would leave nothing to reach
|
|
793
|
+
// (`for (k = 0; k < 3; k++) { if (gC == 7) goto out; switch ... }` becomes `while (gC != 7)`).
|
|
794
|
+
// A test lower in the body that leaves for the merge ahead of the dispatch spells as its own
|
|
795
|
+
// `goto` or `break`, and keeps the switch.
|
|
796
|
+
const exitMergeSpells = (from: Block, merge: Block | null): boolean => {
|
|
797
|
+
if (merge === null) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const above = dom.get(merge)!;
|
|
801
|
+
const exitSeen = new Set<Block>([merge]);
|
|
802
|
+
for (const q = [merge]; q.length;) {
|
|
803
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
804
|
+
if (s !== merge && above.has(s)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
if (!exitSeen.has(s)) {
|
|
808
|
+
exitSeen.add(s);
|
|
809
|
+
q.push(s);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const header = enclosingLoop()?.header ?? null;
|
|
814
|
+
if (header !== null && successorsOf(header).includes(merge)) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
const seen = new Set<Block>([from]);
|
|
818
|
+
for (const q = [from]; q.length;) {
|
|
819
|
+
const succ = successorsOf(q.pop()!);
|
|
820
|
+
if (
|
|
821
|
+
header !== null &&
|
|
822
|
+
succ.includes(header) &&
|
|
823
|
+
succ.some((s) => s !== header && s !== merge && !reachesAvoiding(s, header, merge))
|
|
824
|
+
) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
for (const s of succ) {
|
|
828
|
+
if (s !== header && s !== merge && !seen.has(s)) {
|
|
829
|
+
seen.add(s);
|
|
830
|
+
q.push(s);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// A SIDEWAYS PATH THAT ONLY RETURNS. An arm can step into a block the switch does not dominate
|
|
838
|
+
// and still go nowhere but the end of the function: every path from `s` runs forward to a `ret`,
|
|
839
|
+
// never back into the switch, its merge or a sibling, and never round a cycle. Such an arm spells
|
|
840
|
+
// the escaping path in place as its `return;` (`case 0: if (c) { k(a); return; } gOut = 1;`
|
|
841
|
+
// falling into `case 1:`), and a falling arm's join already reads that path as a jump
|
|
842
|
+
// (`regionJoins`, "a `return k` through a shared epilogue"). It is a property of the ARM, not of
|
|
843
|
+
// any loop, so it is asked of every jump-table switch (Regime B; `analyzeArmExit` says why not of
|
|
844
|
+
// Regime A's comparison trees). Where it shows:
|
|
845
|
+
// - in a loop body, where an arm `return`s into the one epilogue past the loop (agbcc and IDO
|
|
846
|
+
// when the code after the loop returns early too, mwcc always) and the switch ends at the
|
|
847
|
+
// body's own join (structure.ts, THE LOOP CLAMP);
|
|
848
|
+
// - outside any loop, where an arm `goto`s a returning tail it shares with code the switch does
|
|
849
|
+
// not dominate: a guard above the switch cross-jumped into the same `k(7); return;`, a shared
|
|
850
|
+
// `out:` label, the other branch of an `if` around the switch, or a switch with no merge.
|
|
851
|
+
// The cycle test is a walk of the paths themselves: a block met again while it is still on the
|
|
852
|
+
// path being walked is a cycle, reducible or not (an IDO `goto mid;` into a `while` body makes an
|
|
853
|
+
// irreducible one, which a dominance test does not see). An edge to a block that dominates its
|
|
854
|
+
// source is refused too, since that block may lie above `s` where the walk never put it.
|
|
855
|
+
//
|
|
856
|
+
// WITH `mergeEnds` it is the copies' question (`forwardToMerge`): a path may also END at the
|
|
857
|
+
// merge, and nothing more is admitted. A path out through the enclosing loop's exit is one more
|
|
858
|
+
// forward path to a `ret`, wherever it falls on the path: the copy writes the code past the loop
|
|
859
|
+
// and its `return;` inside the case arm. A step round the loop to its header needs no test of its
|
|
860
|
+
// own: the header dominates every block of the loop, so the dominance test refuses it from inside,
|
|
861
|
+
// and the cycle test from a path that entered at the header.
|
|
862
|
+
//
|
|
863
|
+
// There the SIBLING and CYCLE refusals are conservative, not needed for soundness: forced off, a
|
|
864
|
+
// tail that steps into a sibling (agbcc, the `jt_xjdefpre` shape with `if (v == 9) return;` in the
|
|
865
|
+
// tail, so the epilogue is the merge and the empty `case 4:` is a sibling) or round a cycle lifts
|
|
866
|
+
// correct, that sibling's body or the loop written out in each entrant. They spare that
|
|
867
|
+
// duplication; dropping one needs a differential over the shapes it then admits.
|
|
868
|
+
const onlyReturns = (
|
|
869
|
+
s: Block,
|
|
870
|
+
b: Block,
|
|
871
|
+
merge: Block | null,
|
|
872
|
+
siblings: ReadonlySet<Block>,
|
|
873
|
+
mergeEnds = false,
|
|
874
|
+
): boolean => {
|
|
875
|
+
const done = new Set<Block>();
|
|
876
|
+
const onPath = new Set<Block>([s]);
|
|
877
|
+
const stack: { x: Block; next: number }[] = [{ x: s, next: 0 }];
|
|
878
|
+
while (stack.length) {
|
|
879
|
+
const top = stack[stack.length - 1];
|
|
880
|
+
const succ = successorsOf(top.x);
|
|
881
|
+
if (top.next === 0 && succ.length === 0 && top.x.ops[top.x.ops.length - 1]?.opcode !== 'ret') {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (top.next === succ.length) {
|
|
885
|
+
stack.pop();
|
|
886
|
+
onPath.delete(top.x);
|
|
887
|
+
done.add(top.x);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const y = succ[top.next++];
|
|
891
|
+
if (mergeEnds && y === merge && merge !== null) {
|
|
892
|
+
continue; // a path that reaches the switch's end
|
|
893
|
+
}
|
|
894
|
+
if (y === b || y === merge || siblings.has(y) || onPath.has(y) || dom.get(top.x)!.has(y)) {
|
|
895
|
+
return false; // back into the switch, or a cycle
|
|
896
|
+
}
|
|
897
|
+
if (!done.has(y)) {
|
|
898
|
+
onPath.add(y);
|
|
899
|
+
stack.push({ x: y, next: 0 });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return true;
|
|
903
|
+
};
|
|
904
|
+
|
|
544
905
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
545
906
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
546
907
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
@@ -552,30 +913,62 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
552
913
|
// sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
|
|
553
914
|
// record, so an arm that reaches no sibling still closes with a plain `break`.
|
|
554
915
|
//
|
|
916
|
+
// With `stopAtLoop` (Regime A), a FALLING arm that also reaches the merge must not break the loop
|
|
917
|
+
// the switch sits in. When the dispatch IS the loop header (agbcc and IDO rotate
|
|
918
|
+
// `while (k < 4) { k++; switch … }` so), it dominates the loop's exit and all the code after the
|
|
919
|
+
// loop, and the merge is the header itself, so dominance cannot tell the arm's region from the
|
|
920
|
+
// code past the loop. Such an arm that falls into a sibling, reaches the merge by the back edge,
|
|
921
|
+
// and leaves for the loop's exit on a third path (a `goto out`, or a sibling that IS the exit, as
|
|
922
|
+
// a `default: goto out;` makes it) would read as falling with `breaks`, Regime A would commit,
|
|
923
|
+
// and the loop-exit refusal in `structure.ts` would fail the whole function where if-recovery
|
|
924
|
+
// spells it. So it is `unstructurable`. What counts is the block the refusal fires on
|
|
925
|
+
// (`breaksLoop`): the loop's own bottom test, reached after a `continue`, exits too but is the
|
|
926
|
+
// loop's condition, and a block that also edges to the merge is the switch's own end. Regime B,
|
|
927
|
+
// which has no fallback, does not ask: it lifts a jump table whose arms share the rotated latch
|
|
928
|
+
// and its exit (`do { … switch … t++; } while (t < 15)`).
|
|
929
|
+
//
|
|
555
930
|
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
556
931
|
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
557
932
|
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
558
933
|
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
559
934
|
// back. Costly for matching, correct to run.
|
|
560
|
-
|
|
935
|
+
//
|
|
936
|
+
// `jumps` are blocks an edge into which is a jump that ends its path where it sits, whatever the
|
|
937
|
+
// switch: the latch of a loop spelled `for`, which the structurer writes as `continue;`. Such an
|
|
938
|
+
// edge is neither a fall into a sibling nor an escape, and an arm that IS one is a bare jump.
|
|
939
|
+
const analyzeArmExit = (
|
|
940
|
+
entry: Block,
|
|
941
|
+
b: Block,
|
|
942
|
+
merge: Block | null,
|
|
943
|
+
siblings: Set<Block>,
|
|
944
|
+
stopAtLoop = false,
|
|
945
|
+
jumps: ReadonlySet<Block> = new Set(),
|
|
946
|
+
): ArmExit => {
|
|
561
947
|
if (entry === merge) {
|
|
562
948
|
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
563
949
|
}
|
|
950
|
+
if (jumps.has(entry)) {
|
|
951
|
+
return { kind: 'break', region: new Set([entry]) };
|
|
952
|
+
}
|
|
564
953
|
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
954
|
+
const escapedTo = new Set<Block>(); // the blocks outside the switch's region it steps into
|
|
565
955
|
let toMerge = false,
|
|
566
|
-
|
|
956
|
+
leavesLoop = false;
|
|
567
957
|
const seen = new Set<Block>([entry]);
|
|
568
958
|
const q = [entry];
|
|
569
959
|
while (q.length) {
|
|
570
960
|
const cur = q.pop()!;
|
|
961
|
+
leavesLoop ||= !successorsOf(cur).includes(merge as Block) && breaksLoop(cur);
|
|
571
962
|
for (const s of successorsOf(cur)) {
|
|
572
963
|
if (s === merge) {
|
|
573
964
|
toMerge = true;
|
|
965
|
+
} else if (jumps.has(s)) {
|
|
966
|
+
continue;
|
|
574
967
|
} else if (s !== entry && siblings.has(s)) {
|
|
575
968
|
into.add(s);
|
|
576
969
|
} else if (s !== entry && !seen.has(s)) {
|
|
577
970
|
if (!dom.get(s)!.has(b)) {
|
|
578
|
-
|
|
971
|
+
escapedTo.add(s);
|
|
579
972
|
continue;
|
|
580
973
|
}
|
|
581
974
|
seen.add(s);
|
|
@@ -584,36 +977,50 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
584
977
|
}
|
|
585
978
|
}
|
|
586
979
|
if (into.size === 0) {
|
|
587
|
-
return { kind: 'break' };
|
|
588
|
-
}
|
|
589
|
-
if (into.size === 1 && !toMerge && !escapes) {
|
|
590
|
-
return { kind: 'fallthrough', to: [...into][0] };
|
|
980
|
+
return escapedTo.size > 0 ? { kind: 'break', escapedTo } : { kind: 'break', region: seen };
|
|
591
981
|
}
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
if (into.size
|
|
982
|
+
// A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
|
|
983
|
+
// spells only where nothing breakable sits between that edge and the arm, which the structurer
|
|
984
|
+
// checks where it emits it (`structure.ts` `switchBreakAt`) and refuses loud otherwise.
|
|
985
|
+
if (into.size === 1 && toMerge && leavesLoop && stopAtLoop) {
|
|
596
986
|
return {
|
|
597
987
|
kind: 'unstructurable',
|
|
598
|
-
why: `a case body
|
|
988
|
+
why: `a case body falls into sibling case ${[...into].map((x) => `#${layoutIndex(x)}`).join(', ')}, reaches the end of the switch, and leaves the loop the switch sits in`,
|
|
599
989
|
};
|
|
600
990
|
}
|
|
601
|
-
|
|
991
|
+
// A sideways path that only RETURNS does not stop the fall-through (`onlyReturns`): it is a
|
|
992
|
+
// jump that ends where it sits, as a `ret` inside the arm is. Regime B only. Regime A
|
|
993
|
+
// (`stopAtLoop`) commits to its `switch` on gates that read an arm's `breaks` alone, and has
|
|
994
|
+
// if-recovery to fall back on, so a falling arm beside an escape stays `unstructurable` there:
|
|
995
|
+
// admitted, it commits Regime A to switches whose emission then refuses the whole function (an
|
|
996
|
+
// escape to a `goto out` past a loop exit, or out of a nested switch) where if-recovery spells
|
|
997
|
+
// them.
|
|
998
|
+
if (
|
|
999
|
+
into.size === 1 &&
|
|
1000
|
+
(escapedTo.size === 0 || (!stopAtLoop && [...escapedTo].every((s) => onlyReturns(s, b, merge, siblings))))
|
|
1001
|
+
) {
|
|
1002
|
+
return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
|
|
1003
|
+
}
|
|
1004
|
+
// Name what is actually missing. These are two different facts: the first is a shape C's
|
|
1005
|
+
// fall-through cannot spell, the second leaves the switch's region sideways.
|
|
1006
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
1007
|
+
if (into.size > 1) {
|
|
602
1008
|
return {
|
|
603
1009
|
kind: 'unstructurable',
|
|
604
|
-
why: `a case body reaches sibling
|
|
1010
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
605
1011
|
};
|
|
606
1012
|
}
|
|
607
1013
|
return {
|
|
608
1014
|
kind: 'unstructurable',
|
|
609
|
-
|
|
610
|
-
// asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
|
|
611
|
-
// (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
|
|
612
|
-
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`,
|
|
1015
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
613
1016
|
};
|
|
614
1017
|
};
|
|
615
1018
|
|
|
616
|
-
const recognizeSwitch = (
|
|
1019
|
+
const recognizeSwitch = (
|
|
1020
|
+
b: Block,
|
|
1021
|
+
stop: Block | null,
|
|
1022
|
+
mergeAt?: (arms: readonly Block[]) => Block | undefined,
|
|
1023
|
+
): Stmt[] | null => {
|
|
617
1024
|
const root = testInfo(b);
|
|
618
1025
|
if (!root) {
|
|
619
1026
|
return null;
|
|
@@ -831,29 +1238,73 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
831
1238
|
}
|
|
832
1239
|
|
|
833
1240
|
// PRE2 (arm exits), per SITE. A `break` arm closes; a `fallthrough` arm runs on into the one
|
|
834
|
-
// sibling it reaches, which `chainArms` then places directly under it
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
|
|
1241
|
+
// sibling it reaches, which `chainArms` then places directly under it — on every path, or on
|
|
1242
|
+
// some while the others reach the switch's end, which the arm's own `break;` spells (`breaks`).
|
|
1243
|
+
// `unstructurable` is a shape no single linear switch spells — a body reaching two siblings, or
|
|
1244
|
+
// a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
|
|
1245
|
+
// which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
|
|
1246
|
+
// either.)
|
|
839
1247
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
1248
|
+
const merge = mergeAt?.([...targets]) ?? ipdom.get(b) ?? stop;
|
|
840
1249
|
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
841
1250
|
const exitOf = new Map<Block, ArmExit>();
|
|
842
1251
|
for (const t of siblings) {
|
|
843
|
-
exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
|
|
1252
|
+
exitOf.set(t, analyzeArmExit(t, b, merge, siblings, true));
|
|
844
1253
|
}
|
|
845
1254
|
// A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
|
|
846
1255
|
// for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
|
|
847
1256
|
// same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
|
|
848
1257
|
// backend because the backend's refusal is terminal (the whole function becomes a stub) while
|
|
849
1258
|
// this one costs nothing but the `switch` spelling.
|
|
1259
|
+
//
|
|
1260
|
+
// A falling arm that also breaks is admitted wherever it falls, the `default:` arm included:
|
|
1261
|
+
// for `case 1: if (c) break; default: f(); break;` the ladder if-recovery writes copies `f()`
|
|
1262
|
+
// into the arm and misses the object the `switch` reaches.
|
|
850
1263
|
if (
|
|
851
1264
|
[...exitOf.values()].some(
|
|
852
|
-
(e) => e.kind === 'unstructurable' || (
|
|
1265
|
+
(e) => e.kind === 'unstructurable' || (e.kind === 'fallthrough' && !spellSwitchFallthrough),
|
|
853
1266
|
)
|
|
854
1267
|
) {
|
|
855
1268
|
return null;
|
|
856
1269
|
}
|
|
1270
|
+
// AN ARM'S `break;` NEEDS THE MERGE TO BE THE SWITCH'S END IN THIS REGION. The code after the
|
|
1271
|
+
// `switch` is `structureRegion(merge, stop)`, which reads as that only where every path from
|
|
1272
|
+
// the dispatch reaches `stop` through the merge. A sub-tree whose post-dominator lies PAST the
|
|
1273
|
+
// region's `stop` breaks that: inside a loop body whose dispatch reaches both the latch (`stop`)
|
|
1274
|
+
// and an early `return` block, ipdom is the return, so an arm's path to it would read as a
|
|
1275
|
+
// switch `break` while a sibling arm runs through the latch and back to the header, which
|
|
1276
|
+
// fails the whole function loud. Declined here, to if-recovery, which spells each edge as the
|
|
1277
|
+
// `return` or the fall to the loop bottom it is. A merge that is the enclosing loop's EXIT is
|
|
1278
|
+
// the exception the structurer spells (`exitMergeSpells`): the code after the switch is then
|
|
1279
|
+
// the exit region, and an arm's path to it is the arm's `break;` (`structure.ts`, the loop-exit
|
|
1280
|
+
// rules).
|
|
1281
|
+
if (
|
|
1282
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
1283
|
+
!(merge === enclosingLoop()?.exit && exitMergeSpells(b, merge)) &&
|
|
1284
|
+
reachesAvoiding(b, stop, merge)
|
|
1285
|
+
) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
// AN ARM'S EDGE TO THE LOOP'S EXIT, WHEN THAT EXIT IS NOT THE MERGE. A switch admitted on a
|
|
1289
|
+
// falling arm's `break;` hands every arm to `structureArm`, where an edge to the enclosing loop's
|
|
1290
|
+
// exit can be spelled only as the switch's own end or as a copied returning tail; any other is
|
|
1291
|
+
// refused there, loud, for the whole function (`structure.ts`, "leaves its switch for a loop
|
|
1292
|
+
// exit"). With an early `return` above the dispatch, the merge is the loop bottom, and a sibling
|
|
1293
|
+
// arm's `goto out` to an exit that runs on to an OUTER loop's latch is such an edge. Declined
|
|
1294
|
+
// here to if-recovery, which spells that `goto` as the inner loop's `break`. The exemption is
|
|
1295
|
+
// the exit that is itself a `ret`; the structurer's other one, an early-return arm owning the
|
|
1296
|
+
// edge, is not asked here, which costs only the `switch` spelling.
|
|
1297
|
+
const loop = enclosingLoop();
|
|
1298
|
+
if (
|
|
1299
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
1300
|
+
loop !== null &&
|
|
1301
|
+
loop.exit !== null &&
|
|
1302
|
+
loop.exit !== merge &&
|
|
1303
|
+
loop.exit.ops[loop.exit.ops.length - 1]?.opcode !== 'ret' &&
|
|
1304
|
+
armsReach(siblings, merge, loop.header, loop.exit)
|
|
1305
|
+
) {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
857
1308
|
|
|
858
1309
|
// PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
|
|
859
1310
|
// actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
|
|
@@ -1006,6 +1457,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1006
1457
|
// The `default:` arm is a chain member too — an arm may run on into it, and C prints it last.
|
|
1007
1458
|
const dfltArm = defaultBlk !== null && defaultBlk !== merge ? defaultBlk : null;
|
|
1008
1459
|
const preChain = [...armsByBlock.keys()];
|
|
1460
|
+
// NO COPIES HERE. Arms a cross-jump sent into one sibling's body do not linearize, and Regime
|
|
1461
|
+
// B reads them back as copies (structure.ts, THE COPIES) because its only alternative is a loud
|
|
1462
|
+
// failure. Regime A's alternative is if-recovery, which LIFTS: a copy reading here would replace
|
|
1463
|
+
// one lift with another, the choice between two legal spellings of one CFG that only a ranked
|
|
1464
|
+
// variation may make (docs/level-tower.md). On comparison trees agbcc and KMC cross-jumped,
|
|
1465
|
+
// the switch spelling measured nearer the target on some and further on others, and no corpus
|
|
1466
|
+
// row reaches this point, so the refusal stands and if-recovery answers.
|
|
1009
1467
|
const entries = chainArms(preChain, dfltArm, exitOf);
|
|
1010
1468
|
if (entries === null) {
|
|
1011
1469
|
return null;
|
|
@@ -1085,13 +1543,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1085
1543
|
// built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
|
|
1086
1544
|
outCases.push({
|
|
1087
1545
|
values: armsByBlock.get(blk)!,
|
|
1088
|
-
body:
|
|
1546
|
+
body: structureArm(blk, to ?? merge, merge, exitOf.get(blk)),
|
|
1089
1547
|
fallsThrough: to !== null,
|
|
1090
1548
|
});
|
|
1091
1549
|
}
|
|
1092
1550
|
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
1093
1551
|
// would carry no statement, which says nothing and is not valid C89.
|
|
1094
|
-
const defBody = defaultBlk ?
|
|
1552
|
+
const defBody = defaultBlk ? structureArm(defaultBlk, merge, merge, exitOf.get(defaultBlk)) : [];
|
|
1095
1553
|
// The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
|
|
1096
1554
|
// (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
|
|
1097
1555
|
// into it and no other subtree jumps there — the two references the tree walk can count.
|
|
@@ -1135,7 +1593,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1135
1593
|
}
|
|
1136
1594
|
return out;
|
|
1137
1595
|
};
|
|
1138
|
-
|
|
1596
|
+
/** `copySharedFallTargets`' `sidewaysEnds`: does every path from `s`, a block outside the
|
|
1597
|
+
* switch's region, run only forward to `merge` or a `ret` (`onlyReturns` with the merge as an
|
|
1598
|
+
* end)? */
|
|
1599
|
+
const forwardToMerge = (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>): boolean =>
|
|
1600
|
+
merge !== null && onlyReturns(s, b, merge, siblings, true);
|
|
1601
|
+
return { recognizeSwitch, analyzeArmExit, forwardToMerge, layoutIndex, defaultLayoutPos, chainArms };
|
|
1139
1602
|
}
|
|
1140
1603
|
|
|
1141
1604
|
/** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test
|