@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
@@ -15,29 +15,205 @@
15
15
  // (`c ? x : y`, and the branchless-compare idioms `clamp0`/`le0`/…) also converges two arms on a return
16
16
  // merge, but there the compiler emits the MERGE-VARIABLE form, which is what byte-matches — sinking it
17
17
  // would REGRESS those. The distinguishing signal is structural: a short-circuit chain converges on a
18
- // SHARED arm (the common early-exit reached from ≥2 conditions, so it has ≥2 predecessors), whereas a
19
- // simple diamond's arms each have exactly one predecessor. So sink only when some branch-predecessor of
20
- // the merge is itself shared (≥2 preds); every simple select stays a merge var.
18
+ // SHARED arm the common early-exit reached from ≥2 CONDITIONS whereas a simple diamond's arms are
19
+ // each reached from one. So sink only when some branch-predecessor of the merge is ARRIVED at from
20
+ // two places; every simple select stays a merge var.
21
+ //
22
+ // THE QUANTITY IS ARRIVALS, NOT PREDECESSORS. A FALL-THROUGH switch arm is the difference:
23
+ // `case 2: r++; case 1: r++;` gives case 1's body two predecessors — the dispatch's `beq`, and
24
+ // case 2's body running on — for a reason that has nothing to do with a chain of conditions.
25
+ // Sinking there tail-duplicates the switch's SHARED RETURN into all five of its paths, which agbcc
26
+ // then constant-folds per arm (`synthetic:sw_fall:agbcc`, 5 of its 11 objdiff points).
27
+ //
28
+ // So one arrival is SUBTRACTED, and only one kind: the previous arm of the same dispatch RUNNING
29
+ // ON into this one (`fellInto`). It is subtracted for what it IS, not for what it computed —
30
+ // "this pred computed something and ran on" is a proxy for the same intuition, and it refuses the
31
+ // shape this pass exists for, where the two arms of `if (a) { … return 0; } if (b) { … return 0; }`
32
+ // both compute and both jump to the shared exit (`retsink.test.ts`'s `TWO_ARMS`; five real-tier
33
+ // sites have it, `kleod:EntityItemDrop:agbcc` among them).
34
+ //
35
+ // "THE PREVIOUS ARM OF THE SAME DISPATCH" IS A CLAIM ABOUT A DISPATCH, so this file models one
36
+ // (`scrutOf`/`armsOf` below): two arms of two DIFFERENT tests on the SAME scrutinee. A proxy that
37
+ // does not name a dispatch is wrong in both directions, and both readings are pinned as fixtures —
38
+ // "each is the target of SOME conditional branch" reads the join of an `if` with no `else` as a
39
+ // fall-in (`IF_NO_ELSE`), and adding "…and not siblings of the same `cond_br`" still says nothing
40
+ // about WHICH dispatch, so `if (a) … if (b) …` on two different values loses its sinking. A
41
+ // function with no comparison-tree dispatch has no fall-in to subtract, which is the truth about it.
42
+ //
43
+ // The two other clauses (`FALL_IN_GATES`): `q` must arrive by an UNCONDITIONAL branch, and it must
44
+ // have a BODY. `isBodyless` (ir/core.ts) is the shared spelling of the second — a bodyless arm is
45
+ // the record gcc leaves of a decision that RAN OUT (`emit_case_nodes` mints a `b .Ldefault` per
46
+ // exhausted subtree), and it arrives rather than falls in; dropping it costs
47
+ // `synthetic:llshr:gcc2.7.2kmc` its sinking. Its parameter half is what keeps an EMPTY case arm
48
+ // (one op, but it binds the accumulator) on the fall-in side.
49
+ //
50
+ // AND THE MERGE MUST BELONG TO THAT DISPATCH (`ownedBy`). A fall-through switch can SHARE its
51
+ // return with control flow outside itself — a guard's `goto` onto the same `return` — and there
52
+ // refusing to sink is exactly wrong: the merge is left standing, Regime-A switch recovery declines
53
+ // on it, and if-recovery duplicates the tails anyway. The pred shape alone cannot tell the two
54
+ // apart (both present one fell-into arm with two preds); the SCRUTINEE can, because the guard tests
55
+ // a different value. `synthetic:sw_fallguard` is the row: MATCH, and diff:6 with the clause dropped.
56
+ //
57
+ // REGIME SCOPE — the model is `cond_br`-seeded, so it is INERT ON A JUMP TABLE. A `switch_br`
58
+ // dispatch's arms are invisible to `armsOf` and `fellInto` never fires there, so on
59
+ // `synthetic:sw_jtfall`/`sw_jtfalldesc` the pass behaves as it does where there is no dispatch at
60
+ // all. Deliberate: seeding `switch_br` too would move matching rows with no row asking for it. It
61
+ // is the second definition of "this arm falls into that one" in the tree — `switch-recover.ts`'s
62
+ // `analyzeArmExit` covers both regimes and is not reachable from `raise/` — so whoever builds the
63
+ // Regime-B hoist should route both through one recognizer rather than widen the seed here.
21
64
  //
22
65
  // This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
23
66
  // (the `logic_and`/`logic_or` connective plus agbcc's `(-b|b)>>31` = `b!=0` normalisation).
24
- import { Block, Fn, defOpMap, mkOp, predecessors } from '../ir/core';
67
+ import { Block, Fn, Op, Value, defOpMap, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
68
+ import { NEGATED_ICMP } from '../ir/opcodes';
69
+ import { simplifyTrivialPhis } from '../ir/simplify';
70
+ import { type Gate, firstRejection } from '../l3/gates';
25
71
 
26
72
  /** The fused short-circuit connectives (raise/shortcircuit.ts). A `cond_br` on one of these is the
27
73
  * post-fusion record of the ≥2 conditions that used to reach a shared arm. */
28
74
  const CONNECTIVES = new Set(['logic_and', 'logic_or']);
29
75
 
76
+ /** "`q` is the previous arm of the same dispatch, RUNNING ON into `target`" — the one arrival
77
+ * `arrivals` subtracts. `dispatches` is already the answer to the hard half (`siblingArms` and
78
+ * `ownedBy` in `sinkReturns`); the table is a value so each clause can be dropped and the pass
79
+ * re-run on real input. Every clause is `sound: false` — they trade BYTES, never correctness:
80
+ * admitting one wrongly spells a correct function the compiler does not re-emit, and refusing one
81
+ * wrongly does the same in the other direction. */
82
+ export interface FallInCandidate {
83
+ /** the predecessor under test */
84
+ readonly q: Block;
85
+ /** the block it would have fallen into */
86
+ readonly target: Block;
87
+ /** the scrutinees whose dispatch has `q` and `target` as arms of two DIFFERENT tests AND owns
88
+ * the return merge — empty when there is no such dispatch */
89
+ readonly dispatches: readonly Value[];
90
+ }
91
+
92
+ export const FALL_IN_GATES: readonly Gate<FallInCandidate>[] = [
93
+ {
94
+ // A DEFINITION rather than a tuning knob, and the one entry here nothing has been shown to
95
+ // move: a `cond_br` pred did not run on into this block, it chose it, so calling that a
96
+ // fall-in would be wrong about the CFG whatever it did to the bytes.
97
+ id: 'arrives-by-decision',
98
+ why: 'a `cond_br` pred CHOSE this block; that is a decision arriving, never a fall-in',
99
+ sound: false,
100
+ rejects: (c) => {
101
+ const t = terminator(c.q);
102
+ return t?.opcode !== 'br' || t.successors.length !== 1 || t.successors[0].block !== c.target;
103
+ },
104
+ },
105
+ {
106
+ // Paid for by a CORPUS row, not by a unit test: dropping it costs `synthetic:llshr:gcc2.7.2kmc`
107
+ // its sinking, and moves none of this file's fixtures.
108
+ id: 'bodyless-arm',
109
+ why: "gcc's `b .Ldefault` for an exhausted subtree is a decision that RAN OUT, not an arm",
110
+ sound: false,
111
+ rejects: (c) => isBodyless(c.q),
112
+ },
113
+ {
114
+ id: 'one-dispatch-owning-the-merge',
115
+ why: 'both arms of ONE dispatch on one scrutinee, and that dispatch owns the return merge',
116
+ sound: false,
117
+ guardedBy: 'ablating the dispatch gate reads an `if` join, and a guarded switch, as fall-ins',
118
+ rejects: (c) => c.dispatches.length === 0,
119
+ },
120
+ ];
121
+
122
+ /** The two questions the fall-in clauses ask of the function's comparison-tree dispatches. */
123
+ interface DispatchModel {
124
+ /** Is this block part of the dispatch on `s` — either one of its tests, or an arm of one? */
125
+ inDispatch(b: Block, s: Value): boolean;
126
+ /** The scrutinees for which `q` and `target` are arms of two DIFFERENT tests: the dispatches in
127
+ * which one could be the previous arm of the other. Two successors of ONE `cond_br` — the body
128
+ * and the join of an `if` with no `else` — share no such scrutinee, which is the whole point. */
129
+ siblingArms(q: Block, target: Block): Value[];
130
+ }
131
+
132
+ /** THE DISPATCH MODEL. A TEST BLOCK ends in a `cond_br` on an integer comparison of exactly one
133
+ * non-constant value against constants — the SCRUTINEE. Two test blocks belong to the same
134
+ * dispatch when they test the same scrutinee: `recognizeSwitch`'s own PRE1 ("every test is on the
135
+ * SAME Value") read at the raise level, without its dominance, purity or interval preconditions —
136
+ * those decide whether a `switch` can be SPELLED, and this pass only needs to know a decision tree
137
+ * is there. `NEGATED_ICMP` (ir/opcodes.ts) is the shared spelling of the icmp family, so an
138
+ * eleventh comparison joins this model for free.
139
+ *
140
+ * Constant folding is deliberately NOT reproduced (`switch-recover.ts evalConst` folds agbcc's
141
+ * synthesized immediates): a test whose constant side this cannot see contributes two
142
+ * non-constant operands and is skipped, which loses a subtraction rather than inventing one.
143
+ *
144
+ * Built ONCE, before `sinkReturns`' merge loop, and read-only thereafter — nothing in the loop
145
+ * writes either table, so the merge that is rewritten first sees the same dispatches as the last. */
146
+ function dispatchModel(fn: Fn, defs: Map<Value, Op>): DispatchModel {
147
+ const scrutOf = new Map<Block, Value>();
148
+ /** Arms, indexed by the block reached and the scrutinee whose test sent it there — the test
149
+ * blocks are the value, because a fall-in requires the two arms to come from DIFFERENT tests. */
150
+ const armsOf = new Map<Block, Map<Value, Set<Block>>>();
151
+ for (const b of fn.blocks) {
152
+ const t = terminator(b);
153
+ if (t?.opcode !== 'cond_br') {
154
+ continue;
155
+ }
156
+ const cmp = defs.get(t.operands[0]);
157
+ if (!cmp || !(cmp.opcode in NEGATED_ICMP)) {
158
+ continue;
159
+ }
160
+ const vars = cmp.operands.filter((o) => defs.get(o)?.opcode !== 'const');
161
+ if (vars.length !== 1) {
162
+ continue;
163
+ }
164
+ const scrut = vars[0];
165
+ scrutOf.set(b, scrut);
166
+ for (const e of t.successors) {
167
+ let byScrut = armsOf.get(e.block);
168
+ if (!byScrut) {
169
+ byScrut = new Map();
170
+ armsOf.set(e.block, byScrut);
171
+ }
172
+ const tests = byScrut.get(scrut) ?? new Set<Block>();
173
+ tests.add(b);
174
+ byScrut.set(scrut, tests);
175
+ }
176
+ }
177
+ return {
178
+ inDispatch: (b, s) => scrutOf.get(b) === s || !!armsOf.get(b)?.has(s),
179
+ siblingArms: (q, target) => {
180
+ const aq = armsOf.get(q);
181
+ const at = armsOf.get(target);
182
+ if (!aq || !at) {
183
+ return [];
184
+ }
185
+ const out: Value[] = [];
186
+ for (const [s, testsQ] of aq) {
187
+ const testsT = at.get(s);
188
+ if (testsT && [...testsQ].some((c) => [...testsT].some((d) => c !== d))) {
189
+ out.push(s);
190
+ }
191
+ }
192
+ return out;
193
+ },
194
+ };
195
+ }
196
+
30
197
  /** Tail-duplicate a return-only merge block into its unconditional-branch predecessors, but ONLY in the
31
198
  * short-circuit shape (some branch-pred is shared, or the arms are selected by a fused connective).
32
199
  * Returns whether anything changed. A "return-only" block is exactly one `ret` whose operands are all
33
200
  * its own block-params, so each predecessor already carries the returned value as a successor arg. */
34
- export function sinkReturns(fn: Fn): boolean {
201
+ export function sinkReturns(fn: Fn, gates: readonly Gate<FallInCandidate>[] = FALL_IN_GATES): boolean {
35
202
  let changed = false;
36
203
  const preds = predecessors(fn);
37
204
  const defs = defOpMap(fn);
205
+ const { inDispatch, siblingArms } = dispatchModel(fn, defs);
206
+ // WHICH READS NEED `terminator`'s UNDEFINED CASE, which is not "every read of a terminator". The
207
+ // scan over `fn.blocks` can meet a block with no ops at all, and that is the one read the guard
208
+ // is for: `ir/verify.ts` rejects an empty block and `pipeline.ts` verifies before calling this,
209
+ // but `sinkReturns` is exported and its tests build blocks by hand, where a refusal is a better
210
+ // answer than a TypeError. A read over a PREDECESSOR needs none and does not have one —
211
+ // `predecessors` is built from `successorsOf`, which is empty for a block with no terminator, so
212
+ // a bodyless block never appears in anyone's predecessor list. Once a block is known to end in a
213
+ // `br`, the rewrite below indexes its terminator directly.
38
214
  const isBrTo = (p: Block, m: Block) => {
39
- const t = p.ops[p.ops.length - 1];
40
- return t.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
215
+ const t = terminator(p);
216
+ return t?.opcode === 'br' && t.successors.length === 1 && t.successors[0].block === m;
41
217
  };
42
218
  for (const m of [...fn.blocks]) {
43
219
  if (m.ops.length !== 1) {
@@ -59,8 +235,9 @@ export function sinkReturns(fn: Fn): boolean {
59
235
  }
60
236
  // SHORT-CIRCUIT GATE, in two shapes — the chain must be visible in the CFG or in the value domain.
61
237
  //
62
- // (a) UNFUSED: at least one branch-pred is a shared block (≥2 preds of its own) — the common
63
- // early-exit reached from every condition of the chain.
238
+ // (a) UNFUSED: at least one branch-pred is ARRIVED AT from ≥2 places — the common early-exit
239
+ // reached from every condition of the chain. Everything that reaches it counts EXCEPT the
240
+ // previous arm running on (`fellInto` below).
64
241
  // (b) FUSED: `branch-shortcircuit` (raise/shortcircuit.ts) rewrites the head's condition into a
65
242
  // `logic_and`/`logic_or` and collapses the second condition block into it. That leaves both
66
243
  // arms single-pred, so (a) cannot see the chain any more — but the CONNECTIVE is now the
@@ -84,7 +261,21 @@ export function sinkReturns(fn: Fn): boolean {
84
261
  return t.opcode === 'cond_br' && CONNECTIVES.has(defs.get(t.operands[0])?.opcode ?? '');
85
262
  });
86
263
  const fusedDiamond = brPreds.length >= 2 && brPreds.some(selectedByConnective);
87
- if (!brPreds.some((p) => (preds.get(p)?.length ?? 0) >= 2) && !fusedDiamond) {
264
+ // Does the dispatch on `s` OWN this merge? Every predecessor of `m` must be part of it — one of
265
+ // its tests, or an arm of one. A `goto` from outside the switch onto the same `return` fails
266
+ // this on the scrutinee it tests, so nothing is subtracted and the merge is sunk. `ps`, not
267
+ // `brPreds`: that outside arrival is a `cond_br` (the guard's `bgt`), which never appears in
268
+ // `brPreds`.
269
+ const ownedBy = (s: Value) => ps.every((p) => inDispatch(p, s));
270
+ // `q` FELL INTO `p`: it is the previous arm of the same dispatch, running on. The clauses are
271
+ // `FALL_IN_GATES` above, argued in this file's header; `arrivals` counts every OTHER
272
+ // predecessor. Layout adjacency — `q` sitting immediately above `p`, which is what "fell
273
+ // through" means in the assembly — is NOT a clause: it moves no corpus row, and it would be an
274
+ // unpaid premise about `fn.blocks` still being address order.
275
+ const fellInto = (q: Block, target: Block) =>
276
+ firstRejection(gates, { q, target, dispatches: siblingArms(q, target).filter(ownedBy) }) === null;
277
+ const arrivals = (p: Block) => (preds.get(p) ?? []).filter((q) => !fellInto(q, p)).length;
278
+ if (!brPreds.some((p) => arrivals(p) >= 2) && !fusedDiamond) {
88
279
  continue;
89
280
  }
90
281
  for (const p of brPreds) {
@@ -98,5 +289,14 @@ export function sinkReturns(fn: Fn): boolean {
98
289
  fn.blocks = fn.blocks.filter((b) => b !== m);
99
290
  }
100
291
  }
292
+ // Sinking RETIRES in-edges. A merge also reached by a `cond_br` keeps that one — a conditional
293
+ // branch cannot carry a `ret` — and so survives with a SINGLE predecessor, where its parameter is
294
+ // no longer a join but an alias of that edge's argument. Left standing, the structurer destroys
295
+ // the alias into a local of its own (`v0 = 0; return v0;`) and Regime-A switch recovery reads the
296
+ // block as a second, distinct default candidate. The cleanup is `ir/simplify.ts`'s own; it simply
297
+ // has no other caller downstream of here.
298
+ if (changed) {
299
+ simplifyTrivialPhis(fn);
300
+ }
101
301
  return changed;
102
302
  }