@asmlift/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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 +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -3,7 +3,7 @@
3
3
  // A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
4
4
  // holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
5
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 —
6
+ // the header. Loop DISCOVERY still finds the loop — `ir/loops.ts` reads the same back-edge —
7
7
  // but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
8
8
  // the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
9
9
  // `onStack` refusal and the whole function declines with "unrecovered back-edge".
@@ -24,11 +24,13 @@
24
24
  // `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
25
25
  // gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
26
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';
27
+ import { Block, Fn, dominators, foldWriteOrder, predecessors, reachForward, successorsOf } from '../ir/core';
28
+ import { effectful } from '../ir/discipline';
29
+ import { analyzeLoops } from '../ir/loops';
28
30
  import { type Gate, firstRejection } from '../l3/gates';
29
31
 
30
32
  /** What the gates below judge: one candidate block and the block its `br` goes to. */
31
- interface LatchCandidate {
33
+ export interface LatchCandidate {
32
34
  block: Fn['blocks'][number];
33
35
  target: Fn['blocks'][number];
34
36
  dominatesBlock: boolean;
@@ -80,9 +82,9 @@ export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
80
82
  * SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
81
83
  * only ever shrink — removing a block removes it from every set it was in.)
82
84
  *
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.
85
+ * A predecessor that already branches to the target ends up with two edges into one block, each
86
+ * carrying its own arguments. Where that predecessor has no other edge the loop has no exit, and
87
+ * loop recovery spells it `while (1)` with each edge's own update copy.
86
88
  */
87
89
  export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
88
90
  let folded = 0;
@@ -124,3 +126,157 @@ export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[]
124
126
  folded++;
125
127
  }
126
128
  }
129
+
130
+ // ── EMPTY EXIT TRAMPOLINES ──────────────────────────────────────────────────────────────────────
131
+ //
132
+ // The same empty block on a loop's way OUT. `for (…) { if (t[i] == k) { r = i; break; } }` keeps
133
+ // `r` in a register the break copies into (`add r2, r1, #0`), and the copy becomes an edge argument
134
+ // of a block that holds nothing but its `br` to the code after the loop — beside a second exit edge
135
+ // (the loop's own test failing) that reaches that code directly. The loop then has two exit targets
136
+ // and none of them is a `return`, so every loop form refuses it, and the back-edge declines loud at
137
+ // `onStack`. Which exit edge carries the block is the compiler's choice: agbcc puts it on the break
138
+ // (`synthetic:break_value`) or on the latch's own exit with the break going direct (kleod
139
+ // `sub_0801E664`).
140
+ //
141
+ // The second exit need not land on the target itself. ido, gcc2.7.2kmc and mwcc lay an `else` the
142
+ // loop sits beside between the loop and its join — `if (f) { for (..) if (a(i) == k) { id = i + 100;
143
+ // break; } } else id = f + 26; out(id);` (`synthetic:break_elsetail`) — so the loop's own exit is an
144
+ // empty `b`/`j` over the `else`, and the break reaches the join only through the code its arm runs.
145
+ // Folding the block there leaves the loop two exit targets all the same — the break arm's head is
146
+ // one. What it buys is that the loop's own exit becomes the join the break arm runs on into, which
147
+ // is the exit a `break` that runs code has to leave for (`codeBreakArm`'s region must reach it). So
148
+ // the scope gate asks whether another exit edge's target REACHES the target, going forward and
149
+ // outside the loop's body.
150
+ //
151
+ // Splicing it out re-points each edge into it at its target with its own arguments, and the loop
152
+ // exits to one block: a `break` that carries its copies, which the structurer already spells. The
153
+ // arguments move soundly for the reason the latch's do: the block has no params and one op, so
154
+ // every value its `br` passes dominates it, and therefore the end of each of its predecessors.
155
+
156
+ /** One candidate trampoline: the block, where its `br` goes, and the loops it may leave. */
157
+ export interface ExitTrampoline {
158
+ block: Block;
159
+ target: Block;
160
+ /** the predecessors of `block` */
161
+ preds: readonly Block[];
162
+ /** does some loop hold every predecessor of `block` but not `block` itself, and leave by another
163
+ * edge for `target` as well — landing on it, or on a block that reaches it forward without
164
+ * re-entering the loop (`reachForward`)? */
165
+ sharedExit: boolean;
166
+ /** does every path from `target` reach a `ret` without an effect on the way? */
167
+ pureReturn: boolean;
168
+ }
169
+
170
+ export const EXIT_TRAMPOLINE_GATES: readonly Gate<ExitTrampoline>[] = [
171
+ {
172
+ id: 'trampoline-has-params',
173
+ why: 'a param is a join: the br args may read it, and they would not exist at a predecessor',
174
+ // the fold then hands a predecessor a value it does not dominate, which `verify` refuses loud
175
+ sound: false,
176
+ rejects: (c) => c.block.params.length > 0,
177
+ },
178
+ {
179
+ id: 'trampoline-does-work',
180
+ why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
181
+ sound: true,
182
+ guardedBy: 'latch.test.ts: an exit block whose sole op is a cond_br is never folded',
183
+ rejects: (c) => c.block.ops[0]?.opcode !== 'br',
184
+ },
185
+ {
186
+ id: 'trampoline-self-branch',
187
+ why: 'a block branching to itself is an infinite loop rather than a trampoline',
188
+ sound: false,
189
+ rejects: (c) => c.target === c.block,
190
+ },
191
+ {
192
+ // The SCOPE of the fold, not its soundness: an exit edge's empty block is harmless to the
193
+ // structurer wherever it is the loop's one way out, and folding it there would only re-spell
194
+ // functions that already lift.
195
+ id: 'trampoline-not-a-second-exit',
196
+ why: 'only a loop that also leaves for the same target by another edge, or for a block that runs on into it, gains from the fold an exit every edge out can leave for',
197
+ sound: false,
198
+ rejects: (c) => !c.sharedExit,
199
+ },
200
+ {
201
+ // A target that only returns already lifts: the structurer takes the edge through the
202
+ // trampoline as an early `return` and copies the pure tail into it. Kept that way, so the fold
203
+ // re-spells no function that lifted before it.
204
+ id: 'trampoline-returns-already',
205
+ why: 'an exit that only returns is an early `return` arm, which the structurer spells without the fold',
206
+ sound: false,
207
+ rejects: (c) => c.pureReturn,
208
+ },
209
+ ];
210
+
211
+ /** Every path from `b` ends in a `ret` and runs no effect on the way. */
212
+ export function returnsPurely(b: Block): boolean {
213
+ const seen = new Set<Block>();
214
+ for (const stack = [b]; stack.length > 0;) {
215
+ const x = stack.pop()!;
216
+ if (seen.has(x)) {
217
+ continue; // a join two paths share, or a cycle — either way judged on its first visit
218
+ }
219
+ seen.add(x);
220
+ if (x.ops.some(effectful)) {
221
+ return false;
222
+ }
223
+ const t = x.ops[x.ops.length - 1];
224
+ if (t?.opcode === 'ret') {
225
+ continue;
226
+ }
227
+ if (t?.opcode !== 'br' && t?.opcode !== 'cond_br') {
228
+ return false;
229
+ }
230
+ stack.push(...successorsOf(x));
231
+ }
232
+ return true;
233
+ }
234
+
235
+ /** Splice out every empty EXIT TRAMPOLINE the gates above admit; returns how many were removed.
236
+ * Iterated, as the latch fold is: one splice can make another block's exit edges shared. */
237
+ export function foldExitTrampolines(fn: Fn, gates: readonly Gate<ExitTrampoline>[] = EXIT_TRAMPOLINE_GATES): number {
238
+ let folded = 0;
239
+ for (;;) {
240
+ const preds = predecessors(fn);
241
+ const dom = dominators(fn);
242
+ const loops = [...analyzeLoops(fn, dom).byHeader.values()];
243
+ const found = fn.blocks.find((b) => {
244
+ const target = b.ops[0]?.successors[0]?.block;
245
+ if (target === undefined) {
246
+ return false;
247
+ }
248
+ const into = preds.get(b) ?? [];
249
+ const sharedExit =
250
+ into.length > 0 &&
251
+ loops.some(
252
+ (nl) =>
253
+ !nl.body.has(b) &&
254
+ into.every((p) => nl.body.has(p)) &&
255
+ nl.exitEdges.some((e) => e.to !== b && reachForward(e.to, dom, nl.body).has(target)),
256
+ );
257
+ return (
258
+ firstRejection(gates, { block: b, target, preds: into, sharedExit, pureReturn: returnsPurely(target) }) === null
259
+ );
260
+ });
261
+ if (!found) {
262
+ return folded;
263
+ }
264
+ const onward = found.ops[0].successors[0];
265
+ for (const b of fn.blocks) {
266
+ let repointed = false;
267
+ for (const op of b.ops) {
268
+ op.successors.forEach((s, i) => {
269
+ if (s.block === found) {
270
+ op.successors[i] = { block: onward.block, args: [...onward.args] };
271
+ repointed = true;
272
+ }
273
+ });
274
+ }
275
+ if (repointed) {
276
+ foldWriteOrder(fn.writeOrder, found, b);
277
+ }
278
+ }
279
+ fn.blocks = fn.blocks.filter((b) => b !== found);
280
+ folded++;
281
+ }
282
+ }
@@ -71,6 +71,7 @@
71
71
  import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
72
72
  import { type IrType, type StructField, T, scalarTypeForAccess } from '../ir/types';
73
73
  import { type Gate, firstRejection } from '../l3/gates';
74
+ import { storeSigned } from './storesign';
74
75
  import { firstFreeStructIndex } from './structs';
75
76
 
76
77
  /** One variable-index access, resolved onto (base value, member offset). */
@@ -260,7 +261,9 @@ function stripExt(v: Value, defs: Map<Value, Op>): Value {
260
261
  }
261
262
  }
262
263
 
263
- /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. */
264
+ /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. ONE link, not
265
+ * a chain, and no `sub`: the shapes this pass reads are a single `add` of a scaled index or of a
266
+ * member offset. A chain of constant displacements is ir/alias.ts `constDisplacementOf`. */
264
267
  function constAddend(op: Op, defs: Map<Value, Op>): { of: Value; k: number } | null {
265
268
  if (op.opcode !== 'add' || op.operands.length !== 2) {
266
269
  return null;
@@ -389,7 +392,7 @@ function accessesByBase(fn: Fn, defs: Map<Value, Op>): Map<Value, MemberAccess[]
389
392
  op,
390
393
  off: add === null ? 0 : add.k,
391
394
  elemSize,
392
- signed: isLoad ? (op.attrs.signed as boolean) : elemSize === 4,
395
+ signed: isLoad ? (op.attrs.signed as boolean) : storeSigned(op, elemSize),
393
396
  isLoad,
394
397
  index: op.operands[1],
395
398
  addr: add === null ? null : op.operands[0],
@@ -583,8 +586,11 @@ export function recognizeMemberArrays(
583
586
  a.op.operands[0] = g.base;
584
587
  a.op.attrs.memberOff = a.off;
585
588
  if (!a.isLoad) {
586
- // an astore carries no signedness of its own — the member's decides how the index node
587
- // types, so the store spells through the same declaration the load does
589
+ // the member's declaration decides how the index node types, so the store spells through
590
+ // the same declaration the load does, over any stamp raise/storesign.ts left. No probed
591
+ // input reaches here with a stamp that disagrees: agbcc lifts a signed narrow constant as
592
+ // `neg`/`shl`, and the MIPS shapes (`p->t[i] = -1`, `-104` beside a u16 read) take a flat
593
+ // cast. One that does should keep its stamp, and the index node cast the cell to it.
588
594
  a.op.attrs.signed = g.members.find((m) => m.off === a.off)!.signed;
589
595
  }
590
596
  }