@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- 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 +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -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 +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- 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 +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- 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/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- 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 +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
|
@@ -4,6 +4,7 @@
|
|
|
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';
|
|
9
10
|
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
@@ -48,6 +49,11 @@ export interface SwitchRecoverDeps {
|
|
|
48
49
|
* of "where is this defined" `defs` does not answer. Taken from `structure.ts`, which builds
|
|
49
50
|
* both halves for def-site anchoring, rather than indexing the parameters a second time. */
|
|
50
51
|
blockOf: (v: Value) => Block | undefined;
|
|
52
|
+
/** The header and exit of the loop the switch is being structured in, or null outside any loop.
|
|
53
|
+
* Read LIVE: the structurer's loop frame is the one current when the switch is met. An edge to
|
|
54
|
+
* the exit from inside a case arm is a `break` of that loop, which C binds to the switch instead
|
|
55
|
+
* (`analyzeArmExit`), save where the exit is the switch's own merge. */
|
|
56
|
+
enclosingLoop: () => { header: Block; exit: Block | null } | null;
|
|
51
57
|
/** THE DISPATCH HOIST: the copies of every edge the tree walk collapsed, merged and re-emitted
|
|
52
58
|
* ONCE ahead of the `switch`. `structure.ts hoistedDispatchAssigns` owns the emission
|
|
53
59
|
* (suppression, identity elision, `undef`, the write-order sort, `sequentialize`), so this
|
|
@@ -60,6 +66,10 @@ export interface SwitchRecoverDeps {
|
|
|
60
66
|
) => Stmt[] | null;
|
|
61
67
|
expr: (v: Value) => Expr;
|
|
62
68
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
69
|
+
/** One arm's body, from `entry` to `stop`, inside the switch whose code resumes at `merge`. The
|
|
70
|
+
* structurer owns what `break;` means there (structure.ts `structureArm`), so both regimes hand
|
|
71
|
+
* it every arm and the exit verdict they read for it. */
|
|
72
|
+
structureArm: (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined) => Stmt[];
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
@@ -70,18 +80,30 @@ export interface SwitchRecoverDeps {
|
|
|
70
80
|
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
71
81
|
* Only spellable when that sibling is the arm emitted NEXT, which `chainArms`
|
|
72
82
|
* 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
|
-
|
|
83
|
+
* l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
|
|
84
|
+
* switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
|
|
85
|
+
* `break;` on those paths.
|
|
86
|
+
* - `unstructurable` anything else: two different siblings, or a sibling beside a block the
|
|
87
|
+
* switch does not dominate. C needs a `goto` for those. Regime A declines to
|
|
88
|
+
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
|
|
89
|
+
*
|
|
90
|
+
* `region` is the arm's own blocks: every block the walk below reached, the entry included. It is
|
|
91
|
+
* what the structurer needs to find the arm's own joins (`structure.ts` `armJoin`) and to spell a
|
|
92
|
+
* path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
|
|
93
|
+
* falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
|
|
94
|
+
* sideways, into a block the switch does not dominate, has none: its paths through such a block
|
|
95
|
+
* are not the arm's, and it structures to the CFG's joins. */
|
|
96
|
+
export type ArmExit =
|
|
97
|
+
| { kind: 'break'; region?: ReadonlySet<Block> }
|
|
98
|
+
| { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
99
|
+
| { kind: 'unstructurable'; why: string };
|
|
78
100
|
|
|
79
101
|
export interface SwitchRecovery {
|
|
80
102
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
81
103
|
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
82
104
|
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
83
105
|
* Regime B, which has no fallback, fails loud. */
|
|
84
|
-
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block
|
|
106
|
+
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>, stopAtLoop?: boolean) => ArmExit;
|
|
85
107
|
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
86
108
|
* regimes read it from one definition (and one statement of what it rests on). */
|
|
87
109
|
layoutIndex: (blk: Block) => number;
|
|
@@ -321,9 +343,11 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
321
343
|
spellSwitchFallthrough,
|
|
322
344
|
emitsOwnStatement,
|
|
323
345
|
blockOf,
|
|
346
|
+
enclosingLoop,
|
|
324
347
|
hoistDispatchCopies,
|
|
325
348
|
expr,
|
|
326
349
|
structureRegion,
|
|
350
|
+
structureArm,
|
|
327
351
|
} = deps;
|
|
328
352
|
|
|
329
353
|
// A block's index in `fn.blocks` as its position in the ASSEMBLY — the warrant for every reading
|
|
@@ -427,75 +451,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
427
451
|
// has declared that its layout answers it — which SPELLING the source wrote.
|
|
428
452
|
|
|
429
453
|
// 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
|
-
};
|
|
454
|
+
// `250 << 2` for a large sparse case) to a number — else null. The shared word evaluator
|
|
455
|
+
// (ir/bits.ts), which declines an opcode it has no rule for: a `neg_s` (Thumb `cmn` under a signed
|
|
456
|
+
// branch) is no word, so a guard against it is never read as a case bound. A named variable is
|
|
457
|
+
// not a constant, whatever set it.
|
|
458
|
+
const words = { defs, opaque: isNamed };
|
|
459
|
+
const evalConst = (v: Value): number | null => constWord(words, v);
|
|
499
460
|
|
|
500
461
|
// TWO QUESTIONS about a block, asked separately because the answers diverge and the walk needs
|
|
501
462
|
// both: WHAT does it test (`testInfo`), and may this recovery DISCARD it (`collapsible`)?
|
|
@@ -541,6 +502,123 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
541
502
|
return { x, k, cls, opcode: cmp.opcode, xOnLeft };
|
|
542
503
|
};
|
|
543
504
|
|
|
505
|
+
// Does `blk` leave the enclosing loop for its exit, other than by the loop's own bottom test (a
|
|
506
|
+
// block that also edges back to the header, which spells as the loop's condition)?
|
|
507
|
+
const breaksLoop = (blk: Block): boolean => {
|
|
508
|
+
const loop = enclosingLoop();
|
|
509
|
+
const succ = successorsOf(blk);
|
|
510
|
+
return loop !== null && loop.exit !== null && succ.includes(loop.exit) && !succ.includes(loop.header);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Does a block some arm structures edge to `target`? Walked as `structureArm` walks an arm: every
|
|
514
|
+
// block reached from the arm's entry without stepping through `merge` or back into `header`. That
|
|
515
|
+
// is wider than `analyzeArmExit`'s dominated region on purpose: when Regime A recognises a sub-tree
|
|
516
|
+
// of the comparison tree, an arm can run on into the code of a case the tree's root already took,
|
|
517
|
+
// which the dispatch does not dominate, and `structureArm` structures that code inside the arm.
|
|
518
|
+
// A block that also edges back to `header` is the loop's bottom test, which spells as the loop's
|
|
519
|
+
// condition, not as the arm's (`breaksLoop`) — save where `header` IS the merge (a top-tested
|
|
520
|
+
// loop): no bottom test is then reachable without passing the merge, and an arm's
|
|
521
|
+
// `if (c) goto out;` is a block that edges to the exit and straight back into the header.
|
|
522
|
+
const armsReach = (entries: Iterable<Block>, merge: Block | null, header: Block, target: Block): boolean => {
|
|
523
|
+
const seen = new Set<Block>(entries);
|
|
524
|
+
for (const q = [...seen]; q.length;) {
|
|
525
|
+
const succ = successorsOf(q.pop()!);
|
|
526
|
+
if (succ.includes(target) && (header === merge || !succ.includes(header))) {
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
for (const s of succ) {
|
|
530
|
+
if (s !== target && s !== merge && s !== header && !seen.has(s)) {
|
|
531
|
+
seen.add(s);
|
|
532
|
+
q.push(s);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Can a path from `from` reach `to` without passing `through`? False when `to` is null (the end
|
|
540
|
+
// of the function) or is `through` itself.
|
|
541
|
+
const reachesAvoiding = (from: Block, to: Block | null, through: Block | null): boolean => {
|
|
542
|
+
if (to === null || to === through) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const seen = new Set<Block>([from]);
|
|
546
|
+
for (const q = [from]; q.length;) {
|
|
547
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
548
|
+
if (s === to) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
if (s !== through && !seen.has(s)) {
|
|
552
|
+
seen.add(s);
|
|
553
|
+
q.push(s);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// THE LOOP-EXIT MERGE, WHERE IT SPELLS. A switch whose merge is its enclosing loop's exit puts
|
|
561
|
+
// the exit region after the `switch`, INSIDE the loop body, and reaches the loop bottom only
|
|
562
|
+
// through the arms that do not break. That is the source's text on two conditions, both read off
|
|
563
|
+
// the CFG here, and on either failing the merge-past-stop gate below declines to if-recovery:
|
|
564
|
+
// - the exit region RETURNS: no path from `merge` reaches a block that dominates it (other
|
|
565
|
+
// than `merge` itself, a loop of the exit region's own) — the enclosing loop's header, or an
|
|
566
|
+
// OUTER loop's, whose back edge the inner body cannot spell
|
|
567
|
+
// (`for (j...) { for (;;) { switch ... } out: u += prev; }` runs on to the outer latch);
|
|
568
|
+
// - every loop-bottom TEST the arms reach (a block that edges back to the header) leaves for
|
|
569
|
+
// the merge itself, which the arm then spells as its `break;` (`latchexit`). One that leaves
|
|
570
|
+
// for any other block outside the loop is past what an arm structured against `merge` can
|
|
571
|
+
// say: the merge is then the loop's exit only because an early `return` made a shared
|
|
572
|
+
// epilogue the dispatch's post-dominator, while the code the bottom test leaves for runs
|
|
573
|
+
// before it (`for (w = 0; w < 4; w++) { if (c) return w; switch ... } r(t);` on
|
|
574
|
+
// gcc2.7.2kmc).
|
|
575
|
+
// And the loop's HEADER must not leave for the merge itself. One that does makes the loop
|
|
576
|
+
// top-tested on it, with the merge as the loop's own exit, which the exit region rendered after
|
|
577
|
+
// the switch inside the body would leave nothing to reach
|
|
578
|
+
// (`for (k = 0; k < 3; k++) { if (gC == 7) goto out; switch ... }` becomes `while (gC != 7)`).
|
|
579
|
+
// A test lower in the body that leaves for the merge ahead of the dispatch spells as its own
|
|
580
|
+
// `goto` or `break`, and keeps the switch.
|
|
581
|
+
const exitMergeSpells = (from: Block, merge: Block | null): boolean => {
|
|
582
|
+
if (merge === null) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const above = dom.get(merge)!;
|
|
586
|
+
const exitSeen = new Set<Block>([merge]);
|
|
587
|
+
for (const q = [merge]; q.length;) {
|
|
588
|
+
for (const s of successorsOf(q.pop()!)) {
|
|
589
|
+
if (s !== merge && above.has(s)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (!exitSeen.has(s)) {
|
|
593
|
+
exitSeen.add(s);
|
|
594
|
+
q.push(s);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const header = enclosingLoop()?.header ?? null;
|
|
599
|
+
if (header !== null && successorsOf(header).includes(merge)) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const seen = new Set<Block>([from]);
|
|
603
|
+
for (const q = [from]; q.length;) {
|
|
604
|
+
const succ = successorsOf(q.pop()!);
|
|
605
|
+
if (
|
|
606
|
+
header !== null &&
|
|
607
|
+
succ.includes(header) &&
|
|
608
|
+
succ.some((s) => s !== header && s !== merge && !reachesAvoiding(s, header, merge))
|
|
609
|
+
) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
for (const s of succ) {
|
|
613
|
+
if (s !== header && s !== merge && !seen.has(s)) {
|
|
614
|
+
seen.add(s);
|
|
615
|
+
q.push(s);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
};
|
|
621
|
+
|
|
544
622
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
545
623
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
546
624
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
@@ -552,22 +630,44 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
552
630
|
// sideways does not fall into the next case — and only the FALL-THROUGH verdict consults that
|
|
553
631
|
// record, so an arm that reaches no sibling still closes with a plain `break`.
|
|
554
632
|
//
|
|
633
|
+
// With `stopAtLoop` (Regime A), a FALLING arm that also reaches the merge must not break the loop
|
|
634
|
+
// the switch sits in. When the dispatch IS the loop header (agbcc and IDO rotate
|
|
635
|
+
// `while (k < 4) { k++; switch … }` so), it dominates the loop's exit and all the code after the
|
|
636
|
+
// loop, and the merge is the header itself, so dominance cannot tell the arm's region from the
|
|
637
|
+
// code past the loop. Such an arm that falls into a sibling, reaches the merge by the back edge,
|
|
638
|
+
// and leaves for the loop's exit on a third path (a `goto out`, or a sibling that IS the exit, as
|
|
639
|
+
// a `default: goto out;` makes it) would read as falling with `breaks`, Regime A would commit,
|
|
640
|
+
// and the loop-exit refusal in `structure.ts` would fail the whole function where if-recovery
|
|
641
|
+
// spells it. So it is `unstructurable`. What counts is the block the refusal fires on
|
|
642
|
+
// (`breaksLoop`): the loop's own bottom test, reached after a `continue`, exits too but is the
|
|
643
|
+
// loop's condition, and a block that also edges to the merge is the switch's own end. Regime B,
|
|
644
|
+
// which has no fallback, does not ask: it lifts a jump table whose arms share the rotated latch
|
|
645
|
+
// and its exit (`do { … switch … t++; } while (t < 15)`).
|
|
646
|
+
//
|
|
555
647
|
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
556
648
|
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
557
649
|
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
558
650
|
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
559
651
|
// back. Costly for matching, correct to run.
|
|
560
|
-
const analyzeArmExit = (
|
|
652
|
+
const analyzeArmExit = (
|
|
653
|
+
entry: Block,
|
|
654
|
+
b: Block,
|
|
655
|
+
merge: Block | null,
|
|
656
|
+
siblings: Set<Block>,
|
|
657
|
+
stopAtLoop = false,
|
|
658
|
+
): ArmExit => {
|
|
561
659
|
if (entry === merge) {
|
|
562
660
|
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
563
661
|
}
|
|
564
662
|
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
565
663
|
let toMerge = false,
|
|
566
|
-
escapes = false
|
|
664
|
+
escapes = false,
|
|
665
|
+
leavesLoop = false;
|
|
567
666
|
const seen = new Set<Block>([entry]);
|
|
568
667
|
const q = [entry];
|
|
569
668
|
while (q.length) {
|
|
570
669
|
const cur = q.pop()!;
|
|
670
|
+
leavesLoop ||= !successorsOf(cur).includes(merge as Block) && breaksLoop(cur);
|
|
571
671
|
for (const s of successorsOf(cur)) {
|
|
572
672
|
if (s === merge) {
|
|
573
673
|
toMerge = true;
|
|
@@ -584,32 +684,32 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
584
684
|
}
|
|
585
685
|
}
|
|
586
686
|
if (into.size === 0) {
|
|
587
|
-
return { kind: 'break' };
|
|
588
|
-
}
|
|
589
|
-
if (into.size === 1 && !toMerge && !escapes) {
|
|
590
|
-
return { kind: 'fallthrough', to: [...into][0] };
|
|
687
|
+
return escapes ? { kind: 'break' } : { kind: 'break', region: seen };
|
|
591
688
|
}
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
if (into.size
|
|
689
|
+
// A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
|
|
690
|
+
// spells only where nothing breakable sits between that edge and the arm, which the structurer
|
|
691
|
+
// checks where it emits it (`structure.ts` `switchBreakAt`) and refuses loud otherwise.
|
|
692
|
+
if (into.size === 1 && toMerge && leavesLoop && stopAtLoop) {
|
|
596
693
|
return {
|
|
597
694
|
kind: 'unstructurable',
|
|
598
|
-
why: `a case body
|
|
695
|
+
why: `a case body falls into sibling case ${[...into].map((x) => `#${layoutIndex(x)}`).join(', ')}, reaches the end of the switch, and leaves the loop the switch sits in`,
|
|
599
696
|
};
|
|
600
697
|
}
|
|
601
|
-
if (escapes) {
|
|
698
|
+
if (into.size === 1 && !escapes) {
|
|
699
|
+
return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
|
|
700
|
+
}
|
|
701
|
+
// Name what is actually missing. These are two different facts: the first is a shape C's
|
|
702
|
+
// fall-through cannot spell, the second leaves the switch's region sideways.
|
|
703
|
+
const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
|
|
704
|
+
if (into.size > 1) {
|
|
602
705
|
return {
|
|
603
706
|
kind: 'unstructurable',
|
|
604
|
-
why: `a case body reaches sibling
|
|
707
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
605
708
|
};
|
|
606
709
|
}
|
|
607
710
|
return {
|
|
608
711
|
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`,
|
|
712
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
613
713
|
};
|
|
614
714
|
};
|
|
615
715
|
|
|
@@ -831,29 +931,73 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
831
931
|
}
|
|
832
932
|
|
|
833
933
|
// 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
|
-
//
|
|
934
|
+
// sibling it reaches, which `chainArms` then places directly under it — on every path, or on
|
|
935
|
+
// some while the others reach the switch's end, which the arm's own `break;` spells (`breaks`).
|
|
936
|
+
// `unstructurable` is a shape no single linear switch spells — a body reaching two siblings, or
|
|
937
|
+
// a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
|
|
938
|
+
// which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
|
|
939
|
+
// either.)
|
|
838
940
|
const merge = ipdom.get(b) ?? stop;
|
|
839
941
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
840
942
|
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
841
943
|
const exitOf = new Map<Block, ArmExit>();
|
|
842
944
|
for (const t of siblings) {
|
|
843
|
-
exitOf.set(t, analyzeArmExit(t, b, merge, siblings));
|
|
945
|
+
exitOf.set(t, analyzeArmExit(t, b, merge, siblings, true));
|
|
844
946
|
}
|
|
845
947
|
// A language with no fall-through in its `case` (Pascal) cannot print a falling arm at all, so
|
|
846
948
|
// for it a falling arm is exactly as unspellable as an `unstructurable` one — and takes the
|
|
847
949
|
// same exit: if-recovery, which that backend prints fine. Asked HERE rather than left to the
|
|
848
950
|
// backend because the backend's refusal is terminal (the whole function becomes a stub) while
|
|
849
951
|
// this one costs nothing but the `switch` spelling.
|
|
952
|
+
//
|
|
953
|
+
// A falling arm that also breaks is admitted wherever it falls, the `default:` arm included:
|
|
954
|
+
// for `case 1: if (c) break; default: f(); break;` the ladder if-recovery writes copies `f()`
|
|
955
|
+
// into the arm and misses the object the `switch` reaches.
|
|
850
956
|
if (
|
|
851
957
|
[...exitOf.values()].some(
|
|
852
|
-
(e) => e.kind === 'unstructurable' || (
|
|
958
|
+
(e) => e.kind === 'unstructurable' || (e.kind === 'fallthrough' && !spellSwitchFallthrough),
|
|
853
959
|
)
|
|
854
960
|
) {
|
|
855
961
|
return null;
|
|
856
962
|
}
|
|
963
|
+
// AN ARM'S `break;` NEEDS THE MERGE TO BE THE SWITCH'S END IN THIS REGION. The code after the
|
|
964
|
+
// `switch` is `structureRegion(merge, stop)`, which reads as that only where every path from
|
|
965
|
+
// the dispatch reaches `stop` through the merge. A sub-tree whose post-dominator lies PAST the
|
|
966
|
+
// region's `stop` breaks that: inside a loop body whose dispatch reaches both the latch (`stop`)
|
|
967
|
+
// and an early `return` block, ipdom is the return, so an arm's path to it would read as a
|
|
968
|
+
// switch `break` while a sibling arm runs through the latch and back to the header, which
|
|
969
|
+
// fails the whole function loud. Declined here, to if-recovery, which spells each edge as the
|
|
970
|
+
// `return` or the fall to the loop bottom it is. A merge that is the enclosing loop's EXIT is
|
|
971
|
+
// the exception the structurer spells (`exitMergeSpells`): the code after the switch is then
|
|
972
|
+
// the exit region, and an arm's path to it is the arm's `break;` (`structure.ts`, the loop-exit
|
|
973
|
+
// rules).
|
|
974
|
+
if (
|
|
975
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
976
|
+
!(merge === enclosingLoop()?.exit && exitMergeSpells(b, merge)) &&
|
|
977
|
+
reachesAvoiding(b, stop, merge)
|
|
978
|
+
) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
// AN ARM'S EDGE TO THE LOOP'S EXIT, WHEN THAT EXIT IS NOT THE MERGE. A switch admitted on a
|
|
982
|
+
// falling arm's `break;` hands every arm to `structureArm`, where an edge to the enclosing loop's
|
|
983
|
+
// exit can be spelled only as the switch's own end or as a copied returning tail; any other is
|
|
984
|
+
// refused there, loud, for the whole function (`structure.ts`, "leaves its switch for a loop
|
|
985
|
+
// exit"). With an early `return` above the dispatch, the merge is the loop bottom, and a sibling
|
|
986
|
+
// arm's `goto out` to an exit that runs on to an OUTER loop's latch is such an edge. Declined
|
|
987
|
+
// here to if-recovery, which spells that `goto` as the inner loop's `break`. The exemption is
|
|
988
|
+
// the exit that is itself a `ret`; the structurer's other one, an early-return arm owning the
|
|
989
|
+
// edge, is not asked here, which costs only the `switch` spelling.
|
|
990
|
+
const loop = enclosingLoop();
|
|
991
|
+
if (
|
|
992
|
+
[...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.breaks) &&
|
|
993
|
+
loop !== null &&
|
|
994
|
+
loop.exit !== null &&
|
|
995
|
+
loop.exit !== merge &&
|
|
996
|
+
loop.exit.ops[loop.exit.ops.length - 1]?.opcode !== 'ret' &&
|
|
997
|
+
armsReach(siblings, merge, loop.header, loop.exit)
|
|
998
|
+
) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
857
1001
|
|
|
858
1002
|
// PRE3 (concrete interval consistency): a `case k` is only sound if the ORIGINAL tree
|
|
859
1003
|
// actually routes x==k to its recorded body. A relational guard can make an `x==k` test DEAD (e.g.
|
|
@@ -1085,13 +1229,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1085
1229
|
// built, and no row asks for it (`sw_jtfall`/`sw_jtfalldesc` match today).
|
|
1086
1230
|
outCases.push({
|
|
1087
1231
|
values: armsByBlock.get(blk)!,
|
|
1088
|
-
body:
|
|
1232
|
+
body: structureArm(blk, to ?? merge, merge, exitOf.get(blk)),
|
|
1089
1233
|
fallsThrough: to !== null,
|
|
1090
1234
|
});
|
|
1091
1235
|
}
|
|
1092
1236
|
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
1093
1237
|
// would carry no statement, which says nothing and is not valid C89.
|
|
1094
|
-
const defBody = defaultBlk ?
|
|
1238
|
+
const defBody = defaultBlk ? structureArm(defaultBlk, merge, merge, exitOf.get(defaultBlk)) : [];
|
|
1095
1239
|
// The `default:` arm is an ARM: where its block is laid out is read exactly as a case body's is
|
|
1096
1240
|
// (`defaultLayoutPos`). The dispatch placed that block itself when the last test simply RAN OUT
|
|
1097
1241
|
// into it and no other subtree jumps there — the two references the tree walk can count.
|
package/src/symbols.ts
CHANGED
|
@@ -189,6 +189,12 @@ export interface SymbolInfo {
|
|
|
189
189
|
* DECLARATION differs — `#define name body` instead of `extern T name;` — which is why the
|
|
190
190
|
* body is carried rather than reconstructed. */
|
|
191
191
|
macroBody?: string;
|
|
192
|
+
/** This name is a FILE-SCOPE GLOBAL REGISTER VARIABLE, and this is the register's `__asm__` name
|
|
193
|
+
* (`target.globalRegisterVariables`): the lift reads and writes a callee-saved register no
|
|
194
|
+
* prologue saved as this global. `gName` and `gName = v` mean what they mean for an `extern`, and
|
|
195
|
+
* `&gName` is an error, as for any register variable. Only the DECLARATION differs —
|
|
196
|
+
* `register T name __asm__("r8");` instead of `extern T name;`. */
|
|
197
|
+
globalRegister?: string;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
/** THE one reading of {@link SymbolInfo.dims} for spelling C, shared by the access side
|