@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.
Files changed (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. 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
- * - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
75
- * "out to the merge". C needs a `goto` for those. Regime A declines to
76
- * if-recovery on this verdict; Regime B, having no fallback, fails LOUD. */
77
- export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
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>) => ArmExit;
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
- const evalConst = (v: Value): number | null => {
432
- if (isNamed(v)) {
433
- return null;
434
- } // a named variable is not a constant
435
- const d = defs.get(v);
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 = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
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
- // Name what is actually missing. These three are different facts, and only the first is a shape
593
- // C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
594
- const names = () => [...into].map((x) => `#${layoutIndex(x)}`).join(', ');
595
- if (into.size > 1) {
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 reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
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 case ${names()} on one path and, on another, a block the switch does not dominate`,
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
- // `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
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. `unstructurable` is a
835
- // shape no single linear switch spells — a body reaching two siblings, or a sibling on one path
836
- // and the switch's end on another — and Regime A declines to if-recovery, which spells every
837
- // one of those edges. (Regime B reads the same verdicts and fails LOUD on either.)
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' || (!spellSwitchFallthrough && e.kind === 'fallthrough'),
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: structureRegion(blk, to ?? merge),
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 ? structureRegion(defaultBlk, merge) : [];
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