@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -25,8 +25,9 @@ export interface StackArgsSlot {
25
25
 
26
26
  /** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
27
27
  * `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
28
- * an indirect call, a callee with no prototype, or one whose arity fits in registers. A null
29
- * declaration can only ever lead to a refusal: the code alone cannot say where a block ENDS. */
28
+ * an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
29
+ * alone cannot say where a null declaration's block ENDS, so such a call is never handed a stack
30
+ * word; it lifts only when `survivorBound` clears every word that could open one. */
30
31
  export interface StackArgsCall<C> {
31
32
  readonly kind: 'call';
32
33
  /** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
@@ -34,6 +35,9 @@ export interface StackArgsCall<C> {
34
35
  /** The callee's name, for the refusal messages. `?` where the frontend has none. */
35
36
  readonly callee: string;
36
37
  readonly declared: readonly number[] | null;
38
+ /** The parameters the declaration lists, for the refusal messages — fewer than its words when a
39
+ * 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
40
+ readonly params?: number;
37
41
  }
38
42
 
39
43
  /** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
@@ -56,6 +60,10 @@ export interface StackArgsInput<C> {
56
60
  readonly argRegs: number;
57
61
  /** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
58
62
  readonly capturedWholeFrame: boolean;
63
+ /** The compiler keeps ONE outgoing area at the bottom of the frame, sized for its widest call and
64
+ * below every local, and never reads a word of it back after a call. Only then does a word
65
+ * loaded after a call prove itself a local — see `survivorBound`. */
66
+ readonly localsAboveOutgoingArea: boolean;
59
67
  }
60
68
 
61
69
  /** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
@@ -75,6 +83,50 @@ export interface OutgoingArgs<C> {
75
83
 
76
84
  const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
77
85
 
86
+ /** THE LOWEST FRAME WORD THAT SURVIVES A CALL, or `Infinity` when none does. A word LOADED on some
87
+ * path out of a live call before anything re-stores it held a value across that call. Under
88
+ * `localsAboveOutgoingArea` the caller never reads its outgoing area back after a call, so that
89
+ * word is a local; and since the area is one region at the frame bottom that every call shares,
90
+ * every word at or above the bound is proven not to be an argument.
91
+ *
92
+ * A backward liveness over the live blocks: a load makes its offset live, a store kills it, and a
93
+ * call is transparent — a word read after two calls survived both. Dead blocks contribute nothing:
94
+ * a load that cannot run proves nothing, and a call that cannot run is not a call it survived. */
95
+ function survivorBound<C>(
96
+ blocks: readonly StackArgsBlock<C>[],
97
+ succs: readonly (readonly number[])[],
98
+ live: ReadonlySet<number>,
99
+ ): number {
100
+ const liveIn = blocks.map(() => new Set<number>());
101
+ const liveOut = (b: number): Set<number> => new Set(succs[b].flatMap((s) => [...liveIn[s]]));
102
+ let bound = Infinity;
103
+ for (let changed = true; changed;) {
104
+ changed = false;
105
+ for (let b = blocks.length - 1; b >= 0; b--) {
106
+ if (!live.has(b)) {
107
+ continue;
108
+ }
109
+ const cur = liveOut(b);
110
+ for (const ev of [...blocks[b].events].reverse()) {
111
+ if (isCallEvent(ev)) {
112
+ for (const o of cur) {
113
+ bound = Math.min(bound, o);
114
+ }
115
+ } else if (ev.kind === 'load') {
116
+ cur.add(ev.off);
117
+ } else {
118
+ cur.delete(ev.off);
119
+ }
120
+ }
121
+ if ([...cur].some((o) => !liveIn[b].has(o))) {
122
+ liveIn[b] = cur;
123
+ changed = true;
124
+ }
125
+ }
126
+ }
127
+ return bound;
128
+ }
129
+
78
130
  // THE OUTGOING STACK-ARGUMENT AREA, AND WHO MAY CONSUME IT.
79
131
  //
80
132
  // agbcc's ACCUMULATE_OUTGOING_ARGS reserves the BOTTOM of the frame for arguments 5+ of the calls
@@ -86,7 +138,7 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
86
138
  // CreateEntity_Platform_0_0 (platform.c:734) forwards SIX arguments and came out as
87
139
  // `CreateEntity_Platform(0, 0, a0, (u16)a1)`.
88
140
  //
89
- // TWO INDEPENDENT WITNESSES MUST AGREE, and that agreement is the whole licence:
141
+ // TWO INDEPENDENT WITNESSES MUST AGREE for a call to CONSUME words — the licence:
90
142
  // * the DECLARATION says how many words a call takes. AAPCS lays arguments 5..n at [sp,#0]
91
143
  // upward, one word each, so the block is `[0, 4*(n - |argRegs|))`, contiguous from zero.
92
144
  // * the CODE says which words are staged for it — the offsets stored and not yet reloaded when
@@ -100,15 +152,27 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
100
152
  // * a variadic callee's list is a prefix — `sprintf` truthfully declares two and is handed six,
101
153
  // * a large struct return adds a hidden pointer argument that appears in no parameter list.
102
154
  //
103
- // None of those is recorded by `FnProto` or `SymbolSignature`, so an ARITY-ONLY acceptance had
104
- // all three holes: supplying a TRUE fact (`{ sprintf: { params: 2 } }`) turned a correct decline
105
- // into `return sprintf(a0, a1)` with both stack arguments deleted. Under the rule here the four
106
- // words `sprintf` is really handed are four offsets reaching the call that its declaration does
107
- // not account for, the witnesses disagree, and the answer is the decline again. And the CODE
155
+ // A `SymbolSignature` records none of those, and neither does a COUNT. A typed `FnProto` list
156
+ // records only a `long long`, which the Thumb frontend refuses past the argument registers; a
157
+ // `double` or a by-value struct has no width there, so the list states no layout at all. An
158
+ // ARITY-ONLY acceptance therefore has all three holes: supplying a TRUE fact
159
+ // (`{ sprintf: { params: 2 } }`) turns a correct decline into `return sprintf(a0, a1)` with both
160
+ // stack arguments deleted. Under the rule here the four words `sprintf` is really handed are four
161
+ // offsets reaching the call that its declaration does not account for, the witnesses disagree, and the answer is the decline again. And the CODE
108
162
  // alone cannot say where a block ENDS — a store never reloaded is an argument's signature, but
109
- // so is a dead local, which is why reading the code alone could only ever refuse (conditions (a)
163
+ // so is a dead local, which is why reading the code alone never licenses a word (conditions (a)
110
164
  // and (b) below, kept for every call no declaration covers).
111
165
  //
166
+ // A THIRD WITNESS SAYS WHERE THE AREA ENDS, and it only ever clears words. Under a compiler that
167
+ // keeps one outgoing area below every local and never reads it back after a call
168
+ // (`localsAboveOutgoingArea`), a word LOADED after a call before any re-store is a local, and so
169
+ // is every word above it (`survivorBound`). Every refusal that reads the code — NOTHING EXTRA,
170
+ // (a) and (b) — then weighs only the words beneath the bound, so an undeclared call lifts once no
171
+ // word there could open an argument block, and takes no stack word. Hand-written asm that reads
172
+ // its own outgoing argument back after a call defeats it, and lifts with that argument dropped — a
173
+ // producer assumption of the same kind as CONTIGUITY below, which is why the premise is declared
174
+ // per compiler (`target.ts` `localsAboveOutgoingArea`) and never inferred.
175
+ //
112
176
  // THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
113
177
  // * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
114
178
  // weakest thing that could still be a word this call takes must be inside the block.
@@ -118,14 +182,15 @@ const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kin
118
182
  // 0x0 in block")`; here it is a decline, and for the same reason — it is a GAP, and a gap
119
183
  // must never render as a plausible value.
120
184
  //
121
- // WHAT "NOTHING EXTRA" COSTS, because the reach is narrower than the disappearance of the old
122
- // decline suggests. A genuine SPILL that is live across a licensed call sits in the may set and is
123
- // not in the declared block, so the call refuses — and that is agbcc's commonest frame with an
124
- // outgoing area. Tolerating it means arguing that a pending word which is RELOADED later is a
125
- // local rather than argument n+1, which needs a gate and a row that gate protects; none exists.
126
- // The cost is in attribution, not correctness: the decline such a function gets names a STORE
127
- // ("[sp,#k] also reaches the call unread") rather than the capability, so a gap histogram groups
128
- // this class under that message and not under anything about stack arguments.
185
+ // A SPILL LIVE ACROSS A LICENSED CALL sits in the may set and is not in the declared block — agbcc's
186
+ // commonest frame with an outgoing area. The declaration cannot excuse it; `survivorBound` can: a
187
+ // word loaded after a call before any re-store is a local, so NOTHING EXTRA counts only the pending
188
+ // words beneath the bound. A declared block that reaches the bound contradicts it, and needs no
189
+ // check of its own: the block is contiguous words from zero, so it holds the bound's word, which
190
+ // this function loads — "this function also LOADS it" below refuses it if nothing earlier does.
191
+ // A spill with no survivor at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
192
+ // also reaches the call unread") rather than the capability, so a gap histogram groups it under
193
+ // that message.
129
194
  // The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
130
195
  // site needs: agbcc does tail-merge (`Task_BonusFlower_Spawn`, sa3 bonus_game_enemies, stores
131
196
  // argument 5 in both predecessors with the `bl` in the join), and a one-armed store — the same
@@ -148,6 +213,7 @@ export function analyzeOutgoingArgs<C>({
148
213
  localArea,
149
214
  argRegs,
150
215
  capturedWholeFrame,
216
+ localsAboveOutgoingArea,
151
217
  }: StackArgsInput<C>): OutgoingArgs<C> {
152
218
  const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
153
219
  // EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
@@ -164,19 +230,25 @@ export function analyzeOutgoingArgs<C>({
164
230
  return { blocker: null, blocks: new Map(), area: 0 };
165
231
  }
166
232
  const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
167
- const arityOf = (offs: readonly number[]) => argRegs + offs.length;
233
+ const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
234
+ const words = argRegs + offs.length;
235
+ return ev.params === undefined || ev.params === words
236
+ ? `${words} arguments`
237
+ : `${ev.params} arguments in ${words} argument words`;
238
+ };
168
239
 
169
- // THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an
170
- // object whose address a callee holds; a declared fifth argument says [sp,#0] is a DIFFERENT
171
- // callee's argument slot. Two contradictory claims about the same word, and nothing here can
172
- // decide which to believe, so the honest answer is the decline it has always been.
240
+ // THE ONE-WORD CAPTURED FRAME. `capturedWholeFrame` says the whole frame is an object whose
241
+ // address has ESCAPED this function — handed to a callee, or published to memory; the frontend
242
+ // that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
243
+ // DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
244
+ // here can decide which to believe, so the honest answer is the decline it has always been.
173
245
  if (capturedWholeFrame) {
174
246
  for (const ev of calls) {
175
247
  const offs = ev.declared;
176
248
  if (offs !== null) {
177
249
  return refuse(
178
- `callee \`${ev.callee}\` is declared with ${arityOf(offs)} arguments, so [sp,#0] is its outgoing stack argument — ` +
179
- 'but this one-word frame is an object whose address is passed to a callee, and the two name the same word',
250
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#0] is its outgoing stack argument — ` +
251
+ 'but this one-word frame is an object whose address escapes the function, and the two name the same word',
180
252
  );
181
253
  }
182
254
  }
@@ -188,6 +260,21 @@ export function analyzeOutgoingArgs<C>({
188
260
  return { blocker: null, blocks: new Map(), area: 0 };
189
261
  }
190
262
 
263
+ const succs = asmBlocks.map((): number[] => []);
264
+ for (const b of live) {
265
+ for (const q of preds[b]) {
266
+ if (live.has(q)) {
267
+ succs[q].push(b);
268
+ }
269
+ }
270
+ }
271
+ // Every word at or above this is a proven local, so it is no call's argument. The three
272
+ // code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
273
+ // that could be one, so the words they weigh are cut to those beneath the bound once, where
274
+ // they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
275
+ const bound = localsAboveOutgoingArea ? survivorBound(asmBlocks, succs, live) : Infinity;
276
+ const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
277
+
191
278
  const everySlot: number[] = [];
192
279
  for (let o = 0; o + 4 <= localArea; o += 4) {
193
280
  everySlot.push(o);
@@ -234,7 +321,7 @@ export function analyzeOutgoingArgs<C>({
234
321
  );
235
322
  for (const ev of asmBlocks[b].events) {
236
323
  if (isCallEvent(ev)) {
237
- mayAt.set(ev, new Set(may));
324
+ mayAt.set(ev, belowBound(may));
238
325
  mustAt.set(ev, new Set(must));
239
326
  storedAt.set(ev, new Set(stored));
240
327
  for (const o of ev.declared ?? []) {
@@ -297,7 +384,7 @@ export function analyzeOutgoingArgs<C>({
297
384
  const extra = asc(may).filter((o) => !offs.includes(o));
298
385
  if (missing.length > 0 || extra.length > 0) {
299
386
  return refuse(
300
- `callee \`${ev.callee}\` is declared with ${arityOf(offs)} arguments, so its outgoing stack-argument block is ${say(offs)} — but ` +
387
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so its outgoing stack-argument block is ${say(offs)} — but ` +
301
388
  (missing.length > 0
302
389
  ? `${say(missing)} is not stored on every path to the call`
303
390
  : `${say(extra)} also reaches the call unread, so the declaration does not account for every word staged here`),
@@ -317,14 +404,15 @@ export function analyzeOutgoingArgs<C>({
317
404
  // every offset live code loads back. A reload in dead code is not evidence that anything reads
318
405
  // the slot back, so it does not count.
319
406
  const reloaded = new Set<number>();
320
- const storedAnywhere = new Set<number>();
407
+ const stores = new Set<number>();
321
408
  for (const b of live) {
322
409
  for (const ev of asmBlocks[b].events) {
323
410
  if (ev.kind !== 'call') {
324
- (ev.kind === 'store' ? storedAnywhere : reloaded).add(ev.off);
411
+ (ev.kind === 'store' ? stores : reloaded).add(ev.off);
325
412
  }
326
413
  }
327
414
  }
415
+ const storedAnywhere = belowBound(stores);
328
416
  // A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
329
417
  // to a stack parameter — so after the `bl` the word holds whatever the callee left, and an
330
418
  // `ldr` off that offset reads a GAP. The dataflow above cannot catch it: the call consumes the
@@ -347,7 +435,8 @@ export function analyzeOutgoingArgs<C>({
347
435
  // (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
348
436
  // signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
349
437
  // is the layout one (the area sits at the BOTTOM of localArea, disjoint from the locals, so no
350
- // local load can land on an argument offset), which is why it is a whole-function question.
438
+ // local load can land on an argument offset), which is why it is a whole-function question, and
439
+ // why a word at or above `bound` never reaches it: the bound proves that theorem for it.
351
440
  // A LICENSED offset is excluded: its never being reloaded is explained by the call that takes it.
352
441
  for (const off of asc(storedAnywhere)) {
353
442
  if (!licensed.has(off) && !reloaded.has(off) && prefixStored(off, storedAnywhere)) {
@@ -384,7 +473,7 @@ export function analyzeOutgoingArgs<C>({
384
473
  // where the function ENDS. That is not an argument and not a local anyone reloads, so nothing
385
474
  // here can say what it is: decline rather than let it drop as a dead def.
386
475
  //
387
- // "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `preds`, not a
476
+ // "WHERE THE FUNCTION ENDS" IS A LIVE BLOCK WITH NO LIVE SUCCESSOR, read off `succs`, not a
388
477
  // terminator the caller classified. Under Thumb the two coincide — a computed PC write has no
389
478
  // static successor and the frontend throws on one long before here — but asking the CFG costs
390
479
  // nothing and removes a fact the caller could get wrong.
@@ -396,16 +485,8 @@ export function analyzeOutgoingArgs<C>({
396
485
  // declines at L2: "unrecovered back-edge into block #1 (loop-recovery declined this shape)". So
397
486
  // the loud answer is preserved by a DIFFERENT family's refusal, not by this one. Closing it needs
398
487
  // a backward "can this word still be consumed?" pass, which no row in the corpus asks for.
399
- const hasLiveSucc = asmBlocks.map(() => false);
400
- for (let b = 0; b < asmBlocks.length; b++) {
401
- if (live.has(b)) {
402
- for (const q of preds[b]) {
403
- hasLiveSucc[q] = true;
404
- }
405
- }
406
- }
407
488
  for (let b = 0; b < asmBlocks.length; b++) {
408
- if (!live.has(b) || hasLiveSucc[b]) {
489
+ if (!live.has(b) || succs[b].length > 0) {
409
490
  continue;
410
491
  }
411
492
  for (const off of asc(mayOut[b])) {