@asmlift/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
package/src/raise/gvn.ts CHANGED
@@ -1,4 +1,5 @@
1
- // asmlift — value numbering for OPERAND-FREE PURE definitions (today: `gaddr`).
1
+ // asmlift — value numbering for OPERAND-FREE PURE definitions (today `gaddr` and its frame-local
2
+ // twin `laddr`).
2
3
  //
3
4
  // A compiler materializes a global's address wherever it needs one. Two arms of an `if` that both
4
5
  // touch `gTable` each get their own pool load, so the frontend lifts two DISTINCT SSA values that
@@ -23,13 +24,20 @@
23
24
  // alone, so two with equal attrs are equal in every execution, on every path, always. Replacing all
24
25
  // of them with ONE definition is exact.
25
26
  //
26
- // THE ADMISSION RULE IS `gaddr`, NOT "operand-free and pure" — and the difference is the whole
27
- // safety argument, so do not relax it to the general-sounding version. `const` is ALSO operand-free
28
- // and pure, and numbering consts function-wide would be actively harmful: structure/analysis.ts
29
- // materializes a multi-use `const` that is live across a call into a named local, and its own
30
- // comment records that this exact widening ("the small-constant regression") already cost matches
31
- // once. The gate is a MATCHING policy, not a property of the opcode which is why it is not a flag
32
- // on the opcode table, where `const` would satisfy it.
27
+ // THE ADMISSION RULE IS A LIST, NOT "operand-free and pure" — and the difference is the whole
28
+ // safety argument, so do not relax it to the general-sounding version. Two more opcodes satisfy the
29
+ // general predicate and each is deliberately out, for a different reason:
30
+ //
31
+ // `const` — numbering consts function-wide would be actively HARMFUL: structure/analysis.ts
32
+ // materializes a multi-use `const` that is live across a call into a named local, and
33
+ // its own comment records that this exact widening ("the small-constant regression")
34
+ // already cost matches once.
35
+ // `undef` — numbering it would be VACUOUS: two `undef`s in one function always carry different
36
+ // `key`s (ir/opcodes.ts says so at the opcode), so "same key, therefore same value"
37
+ // never has two members to collapse.
38
+ //
39
+ // The gate is a MATCHING policy, not a property of the opcode — which is why it is not a flag on
40
+ // the opcode table, where `const` would satisfy it.
33
41
  //
34
42
  // PLACEMENT. One fresh definition per class is created in the ENTRY block, which dominates every
35
43
  // REACHABLE block — so no reachable use can precede it (unreachable blocks are excluded from the
@@ -54,18 +62,27 @@
54
62
  // function-top `p0 = (u16 *)&gBgTilemapBufs` — the same local, one level up. Both arms are pinned
55
63
  // in `test/addr-placement.test.ts`; before that they rested on a run nobody could repeat.
56
64
  //
57
- // FOUR modules now answer "is this address a local?" with independent policies — here: never;
58
- // basecse: at the function top, when reused 2+ times; l3/scopebase.ts: at the innermost scope
59
- // holding the uses; l3/argbase.ts: immediately before a call whose arguments share it. Reconciling
60
- // them is recorded debt, and the same test pins the two places they actively disagree, because a
65
+ // Several modules answer "is this address a local?" with independent policies — here: never;
66
+ // basecse: at whichever of the two positions `l3/hoist.ts` is handed (the COMMITTED call states
67
+ // the function top, its roster admissions also offer each init's first use), when the gate table
68
+ // admits the base; l3/scopebase.ts: at the innermost scope holding the uses; l3/argbase.ts:
69
+ // immediately before a call whose arguments share it. The newer placement levers — l3/nearbase.ts,
70
+ // l3/inlinebase.ts, l3/homesplit.ts — answer it too, each with its own.
71
+ // Reconciling them is recorded debt, and the same test pins the two places they actively disagree, because a
61
72
  // consolidation has to PICK rather than discover them: a `for`'s init (basecse reads it at loop
62
73
  // cadence and refuses, scopebase at the enclosing one and hoists) and a global name shadowed by a
63
74
  // local (scopebase must refuse — it re-spells the base as `&g` — while argbase may fire, because it
64
75
  // keeps the base expression verbatim).
65
76
  import { Block, Fn, Op, Value, mkOp, replaceAllUsesWith } from '../ir/core';
77
+ import type { Opcode } from '../ir/opcodes';
66
78
 
67
- /** Ops whose result depends on `attrs` alone — no operands, no memory, no control flow. */
68
- const NUMBERABLE = new Set(['gaddr', 'laddr']); // laddr: same argument operand-free, pure, attr-keyed
79
+ /** Ops whose result depends on `attrs` alone — no operands, no memory, no control flow. The LIST,
80
+ * not the predicate (see the module note); `satisfies` makes a typo a compile error rather than an
81
+ * entry that silently matches nothing. `laddr` earns its place on the same argument `gaddr` does —
82
+ * operand-free, pure, attr-keyed. */
83
+ const NUMBERABLE = ['gaddr', 'laddr'] as const satisfies readonly Opcode[];
84
+ type NumberableOpcode = (typeof NUMBERABLE)[number];
85
+ const isNumberable = (opcode: string): opcode is NumberableOpcode => (NUMBERABLE as readonly string[]).includes(opcode);
69
86
 
70
87
  /** The value-number key: the opcode plus every attribute, in a stable order. */
71
88
  function keyOf(op: Op): string {
@@ -109,7 +126,7 @@ export function numberPureValues(fn: Fn): number {
109
126
  return;
110
127
  }
111
128
  for (const op of b.ops) {
112
- if (NUMBERABLE.has(op.opcode) && op.results.length === 1) {
129
+ if (isNumberable(op.opcode) && op.results.length === 1) {
113
130
  const k = keyOf(op);
114
131
  groups.set(k, [...(groups.get(k) ?? []), { op, block: bi }]);
115
132
  }
@@ -128,9 +145,7 @@ export function numberPureValues(fn: Fn): number {
128
145
  // block, so no path is left reading a definition this pass has moved.
129
146
  const survivor = dups[0].op;
130
147
  const value: Value = { type: survivor.results[0].type };
131
- hoisted.push(
132
- mkOp(survivor.opcode as Parameters<typeof mkOp>[0], { results: [value], attrs: { ...survivor.attrs } }),
133
- );
148
+ hoisted.push(mkOp(survivor.opcode as NumberableOpcode, { results: [value], attrs: { ...survivor.attrs } }));
134
149
  for (const d of dups) {
135
150
  replaceAllUsesWith(fn, d.op.results[0], value);
136
151
  removed++;
@@ -0,0 +1,126 @@
1
+ // asmlift — empty-latch folding (F-CFG-class structural pass; successor-aware, ISA-neutral).
2
+ //
3
+ // A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
4
+ // holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
5
+ // The loop is unchanged; its latch has just become a separate empty block between the exit test and
6
+ // the header. Loop DISCOVERY still finds the loop — `structure/loops.ts` reads the same back-edge —
7
+ // but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
8
+ // the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
9
+ // `onStack` refusal and the whole function declines with "unrecovered back-edge".
10
+ //
11
+ // Splicing the block out — every predecessor edge re-pointed at the header, carrying the latch's
12
+ // own edge arguments — restores the single-latch loop, which is a canonicalization the emitter
13
+ // already handles rather than a new case inside it.
14
+ //
15
+ // The arguments move soundly because the block has no params and one op: a value the latch's `br`
16
+ // passes dominates the latch, so it dominates the end of every predecessor of the latch too.
17
+ //
18
+ // WHY DOMINANCE, AND NOT "the target can reach this block". The two disagree on a loop PREHEADER —
19
+ // the same empty forwarding block seen from the other side, which dominates its header instead of
20
+ // the reverse. Reachability alone cannot refuse one, because an INNER loop's preheader sitting
21
+ // inside an OUTER loop is reachable from the inner header round the outer back-edge. Folding a
22
+ // preheader hands the structurer a guard branching straight at the header — the guarded-self-loop
23
+ // shape, where the structurer either fuses under its guard proof or keeps the guard as its own
24
+ // `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
25
+ // gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
26
+ // unrelated branch as a loop guard — which is the `target-dominates` entry's own note below.
27
+ import { Fn, dominators, foldWriteOrder } from '../ir/core';
28
+ import { type Gate, firstRejection } from '../l3/gates';
29
+
30
+ /** What the gates below judge: one candidate block and the block its `br` goes to. */
31
+ interface LatchCandidate {
32
+ block: Fn['blocks'][number];
33
+ target: Fn['blocks'][number];
34
+ dominatesBlock: boolean;
35
+ }
36
+
37
+ export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
38
+ {
39
+ id: 'latch-has-params',
40
+ why: "a param means the block JOINS edges, so its br args are not this one edge's copy",
41
+ sound: false,
42
+ rejects: (c) => c.block.params.length > 0,
43
+ },
44
+ {
45
+ // WHAT THIS RULE ACTUALLY DECIDES, which is narrower than its id suggests. A block carrying real
46
+ // work never reaches the table at all: `foldEmptyLatches`' pre-check reads `ops[0].successors[0]`,
47
+ // so a first op that is a `store` (or any non-terminator) has no target and is refused there.
48
+ // What is left for this rule is the block whose sole op IS a terminator but not a `br` — a
49
+ // `cond_br`, whose SECOND successor the fold would discard along with the block.
50
+ id: 'latch-does-work',
51
+ why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
52
+ sound: true,
53
+ guardedBy: 'latch.test.ts: a latch whose sole op is a cond_br is refused — folding it would drop its second arm',
54
+ // `br` is a terminator, so a block whose FIRST op is one holds nothing but that branch.
55
+ rejects: (c) => c.block.ops[0]?.opcode !== 'br',
56
+ },
57
+ {
58
+ id: 'self-branch',
59
+ why: 'every block dominates itself, so this is an infinite loop rather than a trampoline',
60
+ sound: false,
61
+ rejects: (c) => c.target === c.block,
62
+ },
63
+ {
64
+ id: 'target-dominates',
65
+ why: "a preheader is the same empty block from the other side; folding it re-shapes another block's branch into a loop guard",
66
+ // NOT `sound`, and the burden it used to carry now lives elsewhere: the guarded-self-loop
67
+ // emitter refuses to fuse an unproven guard — it keeps its `if`, or declines loud — and a
68
+ // multi-block loop's guard has no fusion path to lose it to at all. So ablating this gate
69
+ // re-shapes the C without making it wrong, which is a heuristic by the Gate contract. The named
70
+ // test pins that second layer.
71
+ sound: false,
72
+ guardedBy: 'latch.test.ts: ablating the dominance gate hands a guard to the kept-guard loop emitter',
73
+ rejects: (c) => !c.dominatesBlock,
74
+ },
75
+ ];
76
+
77
+ /** Splice out every EMPTY LATCH the gates above admit; returns how many were removed.
78
+ *
79
+ * Iterated, because folding one latch can make its predecessor into one: the predecessor's
80
+ * SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
81
+ * only ever shrink — removing a block removes it from every set it was in.)
82
+ *
83
+ * A predecessor that already branches to the target ends up with two edges into one block. That is
84
+ * a block whose every edge continues the loop, so it has no exit and loop recovery declines — the
85
+ * same loud decline it gave before the fold.
86
+ */
87
+ export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
88
+ let folded = 0;
89
+ for (;;) {
90
+ const dom = dominators(fn);
91
+ const latch = fn.blocks.find((b) => {
92
+ // THE PRE-CHECK IS WHAT REFUSES A WORK-CARRYING BLOCK. A block whose first op computes
93
+ // something — a `store`, an arithmetic op — is not a terminator and so has no successors, and
94
+ // a candidate with no target is not judged at all. The gates below therefore only ever see a
95
+ // block whose FIRST op is a terminator; `latch-does-work` is the one that then insists it be
96
+ // an unconditional `br`.
97
+ const target = b.ops[0]?.successors[0]?.block;
98
+ return (
99
+ target !== undefined &&
100
+ firstRejection(gates, { block: b, target, dominatesBlock: dom.get(b)!.has(target) }) === null
101
+ );
102
+ });
103
+ if (!latch) {
104
+ return folded;
105
+ }
106
+ const onward = latch.ops[0].successors[0];
107
+ for (const b of fn.blocks) {
108
+ let repointed = false;
109
+ for (const op of b.ops) {
110
+ op.successors.forEach((s, i) => {
111
+ if (s.block === latch) {
112
+ op.successors[i] = { block: onward.block, args: [...onward.args] };
113
+ repointed = true;
114
+ }
115
+ });
116
+ }
117
+ // The copies the latch stood for now happen at the end of this predecessor — after its own
118
+ // writes, which the write-order record has to keep saying (ir/core.ts `foldWriteOrder`).
119
+ if (repointed) {
120
+ foldWriteOrder(fn.writeOrder, latch, b);
121
+ }
122
+ }
123
+ fn.blocks = fn.blocks.filter((b) => b !== latch);
124
+ folded++;
125
+ }
126
+ }