@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -73,11 +73,15 @@ export interface BitfieldDeps {
73
73
  memberQualsAllow: (f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean) => boolean;
74
74
  }
75
75
 
76
+ /** What a recognized bitfield store assigns: an inserted value, or the literal 0 the asm does not
77
+ * carry (the ALL-ZERO form — see the note in `makeBitfieldSpelling`). */
78
+ export type BitfieldAssigned = { k: 'zero' } | { k: 'value'; v: Value };
79
+
76
80
  export interface BitfieldSpellings {
77
81
  /** extract op → the `gSym.field` read it spells */
78
82
  spelling: Map<Op, { global: string; field: string }>;
79
- /** store op → the `gSym.field = value` write it spells */
80
- stores: Map<Op, { global: string; field: string; value: Value }>;
83
+ /** store op → the `gSym.field = …` write it spells */
84
+ stores: Map<Op, { global: string; field: string; value: BitfieldAssigned }>;
81
85
  /** loads whose EVERY use is a spelled extract: the fold emits no temp for these */
82
86
  absorbed: Set<Op>;
83
87
  }
@@ -103,7 +107,7 @@ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
103
107
  // …and the WRITE side: a store the mask-and-insert idiom recognized (see the block below), with
104
108
  // the value the source assigned. THE SECOND inhabitant of "a precomputed member spelling", which
105
109
  // is what makes the shape shared rather than anticipated.
106
- const bitfieldStore = new Map<Op, { global: string; field: string; value: Value }>();
110
+ const bitfieldStore = new Map<Op, { global: string; field: string; value: BitfieldAssigned }>();
107
111
  const absorbedLoads = new Set<Op>();
108
112
  if (symCtx && littleEndian && spellBitfieldMembers) {
109
113
  // the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
@@ -190,6 +194,34 @@ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
190
194
  // single-use and unmaterialized, because the fold DELETES all of them — a second reader would
191
195
  // keep the temp and the emitted C would do the work twice.
192
196
  //
197
+ // THE ALL-ZERO FORM HAS NO `or`: agbcc's `expmed.c` skips the insert when the assigned value
198
+ // is all-zero (`:557-558`, `:606-608`), so `gSym.field = 0;` lowers to
199
+ // `store(A, and(load(A), ~W))` and the store's value operand IS the keep.
200
+ //
201
+ // ITS EVIDENCE IS THE MASK, NOT THE VALUE. `= 0` is also spellable raw, as
202
+ // `*(u8 *)&gSym = KEEP & *(u8 *)&gSym`, so keying on "the inserted value is 0" would name a
203
+ // member wherever the two spellings are the SAME OBJECT. Measured, pinned agbcc, `u8 a:4;
204
+ // u8 b:4`: the DECLARED store of the LOW nibble complements in the 32-bit domain — `~0xF` =
205
+ // -16, which no Thumb `mov #imm8` encodes, so `mov #0x10; neg` — where the byte-domain raw
206
+ // spelling narrows the same keep to one encodable `mov #0xF0`. The HIGH nibble's two
207
+ // spellings are byte-identical (`mov #0xf` on both sides). So the fold admits the zero form
208
+ // only where the keep mask is that 32-bit complement; the gate below says what each clause
209
+ // kills.
210
+ //
211
+ // THE MASK'S ENCODING IS NOT ITS DEFINING OP. `mov;neg` is only how the complement is built
212
+ // where it misses `mov #imm8`; agbcc spells the same complement for `u16 a : 12` as a pool
213
+ // `ldr` of `-0x1000` (compiled). A rule keyed on "defined by a `neg`" would refuse that real
214
+ // inhabitant, so the gate reads the mask's VALUE. The price: the Thumb frontend lowers
215
+ // `bic Rd,Rm` to `and(Rd, ~Rm)`, so a hand-written `mov #0xf; bic` arrives as the same IR as
216
+ // the accepted `mov #0x10; neg` and is admitted. agbcc emits no `bic` for this idiom
217
+ // (compiled: the or-form clear is `mov #0x4; neg`), and `/no-bitfield` co-enumerates the raw
218
+ // spelling, so that is one extra candidate for the differ, not a lost one.
219
+ //
220
+ // TARGET COUPLING, stated because the code cannot: every argument here is a THUMB encoding
221
+ // argument, and the fold's only target guard is `littleEndian`. That is sound only because
222
+ // armv4t+agbcc is the one little-endian target in `target.ts`; a second one inherits none of
223
+ // this reasoning.
224
+ //
193
225
  // TRUNCATION is what makes an UNMASKED insert legal, and only sometimes: C truncates the
194
226
  // assigned value to the field width, while the asm's `or` writes every bit of `v << lo` that
195
227
  // the STORE keeps. The two agree when the field ends the stored cell — bits above it are
@@ -239,24 +271,33 @@ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
239
271
  const width = op.attrs.width as number;
240
272
  const cell = globalCellOf(defs, op.operands[0], op.attrs.off as number);
241
273
  const si = cell ? symCtx.info(cell.name) : undefined;
242
- const orOp = defs.get(op.operands[1]);
274
+ const valOp = defs.get(op.operands[1]);
275
+ // the ALL-ZERO form: no `or`, the keep `and` IS the store's value. Single-use and
276
+ // unmaterialized go untested here because the shared `andOp` gate tests the same op.
277
+ const zeroForm = valOp?.opcode === 'and';
243
278
  if (
244
279
  !cell ||
245
280
  si?.shape !== 'struct' ||
246
281
  si.volatile ||
247
- orOp?.opcode !== 'or' ||
248
- orOp.operands.length !== 2 ||
249
- materialize.has(orOp) ||
250
- (useSitesOf.get(orOp.results[0]) ?? []).length !== 1
282
+ (!zeroForm &&
283
+ (valOp?.opcode !== 'or' ||
284
+ valOp.operands.length !== 2 ||
285
+ materialize.has(valOp) ||
286
+ (useSitesOf.get(valOp.results[0]) ?? []).length !== 1))
251
287
  ) {
252
288
  continue;
253
289
  }
254
290
  const cellBits = width * 8;
255
291
  const cellMask = width >= 4 ? -1 : (1 << cellBits) - 1;
256
- for (const [keepV, insV] of [
257
- [orOp.operands[0], orOp.operands[1]],
258
- [orOp.operands[1], orOp.operands[0]],
259
- ] as const) {
292
+ // The `or` form's two pairs are the commutativity of `|`; the zero form contributes one,
293
+ // with `null` for the insert the asm does not contain.
294
+ const pairs: readonly (readonly [Value, Value | null])[] = zeroForm
295
+ ? [[op.operands[1], null]]
296
+ : [
297
+ [valOp!.operands[0], valOp!.operands[1]],
298
+ [valOp!.operands[1], valOp!.operands[0]],
299
+ ];
300
+ for (const [keepV, insV] of pairs) {
260
301
  const andOp = defs.get(keepV);
261
302
  if (
262
303
  andOp?.opcode !== 'and' ||
@@ -298,30 +339,89 @@ export function makeBitfieldSpelling(deps: BitfieldDeps): BitfieldSpellings {
298
339
  if ((((w >= 32 ? -1 : (1 << w) - 1) << lo) & cellMask) !== clear) {
299
340
  continue;
300
341
  }
301
- // …and the insert must be exactly that value seated at `lo`.
302
- const shifted = defs.get(insV);
303
- const value =
304
- lo === 0
305
- ? insV
306
- : shifted?.opcode === 'shl' && shifted.operands.length === 1 && shifted.attrs.imm === lo
307
- ? shifted.operands[0]
308
- : null;
309
- if (
310
- value === null ||
311
- (lo !== 0 && (materialize.has(shifted!) || (useSitesOf.get(insV) ?? []).length !== 1))
312
- ) {
342
+ // …AND THE STORE'S WIDTH MUST BE THE ACCESS AN ASSIGNMENT TO THOSE BITS COMPILES TO:
343
+ // the NARROWEST aligned 1/2/4-byte cell that contains the whole window. agbcc picks the
344
+ // access from the field's own BITS, not from its container's declared type — measured,
345
+ // pinned agbcc, one compile per row:
346
+ //
347
+ // u16 a : 2 at bit 0 → `ldrb`/`strb` (narrower than the `u16` container)
348
+ // u32 a : 12 at bit 0 → `ldrh`/`strh` (narrower than the `u32` container)
349
+ // u32 x : 8 at bit 12 → `ldr`/`str` (WIDER: bits 12-19 straddle bytes 1-2)
350
+ // u32 a : 17 / a : 20 → `ldr`/`str` (WIDER: no 3-byte access exists)
351
+ //
352
+ // Both directions matter and only one of them is intuitive. Without the narrowing half a
353
+ // `strh` over `gState.hearts` (two bits inside byte 0) spells a member from bytes that
354
+ // spelling cannot reproduce. Without the WIDENING half every field whose bits straddle
355
+ // an aligned pair — the common packed-header shape — is refused, because the compiler
356
+ // had no choice but a word.
357
+ //
358
+ // So the test reads the WINDOW and never the map's `size`. `size` is the field's byte
359
+ // SPAN (@gba-kit/debug-info emits `ceil((bitsIntoByte + bitWidth) / 8)`), which for the
360
+ // two widening rows is 2 and 3 — neither an access this machine has, and 3 an access no
361
+ // machine has. Any bound expressed in `size` is therefore wrong for a whole band of real
362
+ // fields however it rounds. `bitOffset`/`bitWidth` state the same fact exactly.
363
+ const loBit = cell.byte * 8 + lo;
364
+ const cellWidth = [1, 2, 4].find(
365
+ (n) => Math.floor(loBit / (n * 8)) === Math.floor((loBit + w - 1) / (n * 8)),
366
+ );
367
+ if (width !== cellWidth) {
368
+ continue;
369
+ }
370
+ // THE ZERO FORM'S EVIDENCE RULE: accept only a keep mask no RAW spelling of the same
371
+ // clear can produce. Three clauses, each measured with the pinned agbcc:
372
+ //
373
+ // · `mask === ~clear` — the 32-bit complement of the window. `~0xF` = -16 for a low
374
+ // nibble, `-0x1000` for `u16 a : 12`: neither is encodable in the byte domain, so a
375
+ // raw spelling narrows to a different object. A pool word like `0xFFFF00F0` clears
376
+ // the same nibble but zeroes bits ABOVE the cell, so it is some other function whose
377
+ // clear is a coincidence.
378
+ // · `(mask & cellMask) !== mask` — a WORD cell is refused outright. `cellMask` has bit
379
+ // 31 clear at widths 1-2, so `mask === ~clear` already forces an outside bit there
380
+ // and this can only bite at width 4. It costs nothing: compiled, `gS.a = 0;` for
381
+ // `u32 a : 20` and `*(s32 *)&gS &= -1048576;` are byte-identical.
382
+ // · `(mask & cellMask) !== 0` — a mask keeping NO bit of the cell is not a
383
+ // read-modify-write. agbcc compiles `g.f8 = 0;`, a field filling its own byte, to
384
+ // `mov #0x0; strb`, so the candidate could not reproduce its own input.
385
+ //
386
+ // The rule refuses a real population as collateral: agbcc narrows the HIGH nibble's
387
+ // clear to the in-cell `mov #0xf`, byte-identical to the raw spelling, so a declared
388
+ // store does NOT always complement in 32 bits. Only the converse is needed.
389
+ //
390
+ // NONE of this may extend to the `or` form, where the insert seated at `lo` is the
391
+ // evidence and agbcc does spell an end-of-cell clear with the narrow in-cell constant.
392
+ // `zeroForm`, the discriminant that built `pairs`, is what keeps the two apart.
393
+ if (zeroForm && (mask !== ~clear || (mask & cellMask) === mask || (mask & cellMask) === 0)) {
313
394
  continue;
314
395
  }
315
- if (lo + w !== cellBits && provableBits(bits, value) > w) {
316
- continue; // C would truncate bits the asm's `or` writes
396
+ // …and the insert must be exactly that value seated at `lo`. The zero form skips it:
397
+ // no insert to seat, and no bit the (absent) `or` writes for C's truncation to disagree
398
+ // with.
399
+ let assigned: BitfieldAssigned = { k: 'zero' };
400
+ if (insV !== null) {
401
+ const shifted = defs.get(insV);
402
+ const inserted =
403
+ lo === 0
404
+ ? insV
405
+ : shifted?.opcode === 'shl' && shifted.operands.length === 1 && shifted.attrs.imm === lo
406
+ ? shifted.operands[0]
407
+ : null;
408
+ if (
409
+ inserted === null ||
410
+ (lo !== 0 && (materialize.has(shifted!) || (useSitesOf.get(insV) ?? []).length !== 1))
411
+ ) {
412
+ continue;
413
+ }
414
+ if (lo + w !== cellBits && provableBits(bits, inserted) > w) {
415
+ continue; // C would truncate bits the asm's `or` writes
416
+ }
417
+ assigned = { k: 'value', v: inserted };
317
418
  }
419
+ // …and a DECLARED field must occupy exactly the window.
318
420
  const fld = symCtx
319
421
  .fieldsOf(cell.name)
320
- ?.find(
321
- (f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === cell.byte * 8 + lo && f.signed !== undefined,
322
- );
422
+ ?.find((f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === loBit && f.signed !== undefined);
323
423
  if (fld && memberQualsAllow(fld, si.const, true)) {
324
- bitfieldStore.set(op, { global: cell.name, field: fld.name, value });
424
+ bitfieldStore.set(op, { global: cell.name, field: fld.name, value: assigned });
325
425
  }
326
426
  break;
327
427
  }
@@ -123,12 +123,12 @@ export function bareArrayElement(si: SymbolInfo, width: number, signed: boolean)
123
123
  // reduce to: `(r<<11) + (i<<1)` and `((r<<10) + i) << 1` differ only in where the element scale
124
124
  // sits, which is exactly what the division removes. So this runs on the byte residual and
125
125
  // arrayAccess does not call it — see the note at that site for what the two spellings measure.
126
- // `packages/cli/test/matching/array-rank-axis.test.ts` compiles both halves of that.
126
+ // `packages/cli/test/matching/array-rank-variation.test.ts` compiles both halves of that.
127
127
  //
128
128
  // The recovered address is the SAME address either way (C scales `[r]` by the declared row size,
129
129
  // which is the constant the arithmetic multiplied by), so this is a spelling, not a re-addressing.
130
130
  //
131
- // AND IT IS AN AXIS, NOT A DEFAULT — the evidence above says the residual carries a ROW, and it
131
+ // AND IT IS A VARIATION, NOT A DEFAULT — the evidence above says the residual carries a ROW, and it
132
132
  // does not say which of the two spellings that reach it wrote one. The cast form this replaces
133
133
  // (`*(T *)((r<<11) + (i<<1) + (u32)&g)`) compiles to the SAME shift structure and differs only in
134
134
  // scheduling on agbcc, kmc and mwcc, and is byte-identical on IDO — where the flat sum is
@@ -149,7 +149,7 @@ function addTerms(e: Expr, sign: 1 | -1, into: { e: Expr; sign: 1 | -1 }[]): voi
149
149
 
150
150
  /** `x` when `t` is the NON-CONSTANT value `x` scaled by exactly `stride` (`x * stride` or
151
151
  * `x << log2(stride)`), else null. A constant term is never a recovered subscript: both
152
- * spellings of a constant row index compile identically, so nothing referees the choice. */
152
+ * spellings of a constant row index compile identically, so nothing referees the question. */
153
153
  function scaledBy(t: Expr, stride: number): Expr | null {
154
154
  if (t.k !== 'bin') {
155
155
  return null;
@@ -195,6 +195,32 @@ export function declaredSubscripts(
195
195
  if (inner === null || inner.length === 0) {
196
196
  return null;
197
197
  }
198
+ return subscriptsFromExtents(inner, residual, width, true);
199
+ }
200
+
201
+ /** THE split of a BYTE residual into a multidimensional array's subscripts, shared by the two
202
+ * declarations that can state a rank: a GLOBAL's ({@link declaredSubscripts}) and an array
203
+ * MEMBER's (structure.ts `pointeeElement`). One copy, because the two must not come to disagree
204
+ * about which term is a row.
205
+ *
206
+ * `needRecovered` is the one place they legitimately differ, and it is a fact about the
207
+ * FALLBACK rather than about the arithmetic. For a global, a residual holding no term at a
208
+ * leading stride means there is nothing to recover, and today's flat `gSym[i]` already spells
209
+ * the very same address — so the split is refused and nothing is lost. An array MEMBER of a rank
210
+ * the project's own header declares has NO such fallback: `->x[i]` on a `u8 x[6][8]` is a ROW,
211
+ * which does not type-check, so the leading subscripts have to be spelled even when every one of
212
+ * them comes out the literal 0 (`->x[0][i]`, the same address the byte arithmetic reached).
213
+ *
214
+ * REFUSES (null) when: any leading stride is not strictly larger than the one below it (an
215
+ * extent of 1 makes two positions indistinguishable, so the split would be a guess); a stride is
216
+ * not a safe integer; `needRecovered` and no term is a non-constant multiple of a leading stride;
217
+ * or what remains does not divide into whole elements. */
218
+ export function subscriptsFromExtents(
219
+ inner: number[],
220
+ residual: Expr,
221
+ width: number,
222
+ needRecovered: boolean,
223
+ ): { lead: Expr[]; idx: Expr } | null {
198
224
  const strides: number[] = [];
199
225
  for (let p = 0; p < inner.length; p++) {
200
226
  strides.push(inner.slice(p).reduce((a, b) => a * b, width));
@@ -217,7 +243,7 @@ export function declaredSubscripts(
217
243
  }
218
244
  return { k: 'const', value: 0 };
219
245
  });
220
- if (taken.size === 0) {
246
+ if (needRecovered && taken.size === 0) {
221
247
  return null;
222
248
  }
223
249
  let sum: Expr = { k: 'const', value: 0 };
@@ -52,18 +52,34 @@
52
52
  // still holds the value the iteration started with — `structure.ts` refuses to let a merge outside
53
53
  // the loop adopt it for exactly that reason (`carriesPreUpdate`), and refuses to let an inner
54
54
  // loop's variable adopt an enclosing one's, which the inner loop would then mutate every iteration.
55
- // `loop-escape` restates both over classes: a class holding a loop header's parameter may absorb
56
- // only values the loop's body contains. Two loops' variables always fail it a nested pair because
57
- // the outer variable's home is outside the inner body, a disjoint pair in both directions — so the
58
- // enclosing-loop rule needs no gate of its own.
55
+ // That second refusal has ONE exception there, `enclosingCarrierName`: a value the frontend's
56
+ // write-order record shows crossing into the inner loop in the register it already had, and its
57
+ // `carriedByBothLoops` clause, this gate's premise stated per site one that every outer back
58
+ // edge hands back as a value the inner loop carries. So the walk can already hand this pass a class
59
+ // holding BOTH headers' parameters; what it cannot hand it is a class the outer loop overwrites
60
+ // behind the inner one's back. `loop-escape` restates both refusals over classes: a class holding
61
+ // a loop header's parameter may absorb only values the loop's body contains. Two loops' variables
62
+ // always fail it — a nested pair because the outer variable's home is outside the inner body, a
63
+ // disjoint pair in both directions — so the enclosing-loop rule needs no gate of its own here.
59
64
  //
60
- // It is NOT marked sound, because it has not been shown to be, and it is BLUNTER than the rule it
61
- // restates: `carriesPreUpdate` branches on which emitter owns the latch and names four shapes that
62
- // are not the hazard, none of which this has. Dropping it over 773 benchmark rows improves 12 and
63
- // regresses none, measured at #55 `nestedloop` is `int s = 0; … s += i*j`, one accumulator the
64
- // pass currently emits as two. Lifting `carriesPreUpdate` itself to name classes is the work that would take those
65
- // 12 rows; it needs the class-level closure, since a merge can reach a loop variable's name through
66
- // an edge that carried no loop variable at all.
65
+ // It is BLUNTER than the rule it restates: `carriesPreUpdate` branches on which emitter owns the
66
+ // latch and names four shapes that are not the hazard, none of which this has. It IS load-bearing,
67
+ // and the measurement that says so is not a benchmark one: dropping it over 773 rows improves 12
68
+ // and regresses none (#55), and `nestedloop` is `int s = 0; … s += i*j`, one accumulator the pass
69
+ // emits as two but with it dropped, two generated functions COMPUTE SOMETHING ELSE: an inner
70
+ // loop's variable adopts the enclosing loop's carrier and then overwrites it every iteration. Both
71
+ // are frozen as literal IR in `test/loop-escape-witnesses.ts`, and `namecoalesce.test.ts` asserts
72
+ // the escape copy the gate forces on one side and its absence on the other. A byte score cannot
73
+ // see that failure, which is the whole reason that fuzz exists. So the 12 rows are real and so is the hazard, and taking them
74
+ // still needs `carriesPreUpdate` lifted to name classes: the class-level closure, since a merge can
75
+ // reach a loop variable's name through an edge that carried no loop variable at all.
76
+ //
77
+ // WHY THE FLAG IS `false` ANYWAY, recorded here because this is where it is set. By `gates.ts`'s
78
+ // definition — remove it and some candidate is WRONG — this gate qualifies, and the two frozen
79
+ // witnesses are the proof. It stays `false` on the bluntness above: what is shown wrong is the
80
+ // SHAPE this rejects, not the rule as written, which also refuses merges `carriesPreUpdate` would
81
+ // allow. Flipping it costs a line in `namecoalesce-fuzz`'s `OUT_OF_REACH` — the ablating arm has no
82
+ // witness inside `SEEDS` — and buys no coverage that `namecoalesce.test.ts` does not already hold.
67
83
  //
68
84
  // TWO KNOWN GAPS, both on the READ side of a relocated write:
69
85
  //
@@ -160,7 +176,7 @@ export const NAME_COALESCE_GATES: readonly Gate<NameMerge>[] = [
160
176
  },
161
177
  {
162
178
  id: 'type',
163
- why: 'the survivor keeps its own declared type, so the two must agree',
179
+ why: 'a merged variable has one declared type, so the types of the two names must agree',
164
180
  sound: true,
165
181
  guardedBy: 'namecoalesce.test.ts: ablating type merges two names the declarations disagree about',
166
182
  rejects: (c) => !c.sameType,
@@ -177,8 +193,11 @@ export const NAME_COALESCE_GATES: readonly Gate<NameMerge>[] = [
177
193
  },
178
194
  {
179
195
  id: 'loop-escape',
180
- why: 'outside the loop a loop variable’s name holds the value from BEFORE the update',
196
+ why: 'outside the loop a loop variable’s name holds the value from before the update',
181
197
  sound: false,
198
+ // Not required of an unsound gate, and named anyway: the differential fuzz never ablates this
199
+ // one, so the frozen witnesses are its only evidence and `gate-contract` pins their test.
200
+ guardedBy: 'namecoalesce.test.ts: ablating loop-escape lets an inner loop clobber the enclosing loop variable',
182
201
  rejects: (c) => c.loopEscapes,
183
202
  },
184
203
  ];
@@ -0,0 +1,95 @@
1
+ // Which `return;` statements the SOURCE wrote, read off the assembly.
2
+ //
3
+ // A void `return;` is a control transfer to the epilogue, and a compiler spells one as an
4
+ // unconditional `b <epilogue>`. So where the epilogue is a block of its OWN — nothing in it but the
5
+ // `ret` — its in-edges are the ways the body ends, and only one kind of arrival is a statement:
6
+ //
7
+ // - an unconditional branch instruction into it → a `return;` the source wrote.
8
+ // - FALLING into it → the body simply running out, which spells
9
+ // nothing.
10
+ // - a conditional branch's own edge → nothing either. Unoptimised code tests into a
11
+ // BODY label and never into the epilogue, so a
12
+ // `bxx` landing there is jump-optimisation having
13
+ // rerouted a branch the source did not write — and
14
+ // by then both spellings are one object anyway.
15
+ //
16
+ // The statement being decided is the one at the END of the body, so a fall-through in-edge normally
17
+ // settles it: that edge is the body running out, and it wrote no `return;`. A branch in-edge
18
+ // alongside it is a `return;`, just not this one — it ends an arm, and `l3/tailret.ts` may delete a
19
+ // return only in TAIL position, so an arm the function continues past keeps its own. An arm that IS
20
+ // in tail position is one the compiler must branch over regardless (the block laid out before the
21
+ // epilogue is the one that falls in, and only one block can be), so its `return;` and its `}`
22
+ // compile to the same instruction. That is what makes the answer safe per BLOCK.
23
+ //
24
+ // ONE KIND OF BRANCH IN-EDGE OVERRULES THE FALLING ONE: a branch out of an EMPTY block. That arm
25
+ // holds nothing but the `return;`, so dropping it does not shorten a statement list, it flips the
26
+ // branch sense — and the two senses are two different objects. Measured on `if (*p & 1) return; …`
27
+ // at agbcc -O0: the sense that writes the `return;` is 40 bytes, `beq` over a `b <epilogue>`; the
28
+ // flipped one is 36, `bne` over the body with no `b` at all. So that `b` is in the object because
29
+ // the source wrote a `return;`, and the falling edge beside it licenses nothing.
30
+ //
31
+ // With no fall-through in-edge, a branch in-edge is the only evidence there is, and it says the
32
+ // source wrote a `return;` — keep it. With neither, nothing reached the epilogue by a written
33
+ // transfer at all and there is nothing to spell. NO in-edge at all says the same thing without
34
+ // needing the epilogue to be a block of its own: a body that never branched anywhere ended by
35
+ // running out. That is the single-block function, which on an OBJECT has no epilogue label to
36
+ // split it — the shape the `.s` path never shows.
37
+ //
38
+ // A `ret` SUNK onto one edge (`raise/retsink.ts`, `raise/tailsink.ts`) carries that edge's own
39
+ // fall-through fact and answers for itself, which beats anything its block's in-edges could say:
40
+ // those are about reaching the statements above the return, not about reaching the epilogue. An
41
+ // epilogue block that ALSO holds statements and did not come from sinking is not asked at all, for
42
+ // the same reason.
43
+ //
44
+ // NOT DECIDED HERE: a `while` whose only exit branches straight to the epilogue. That `b` is the
45
+ // loop's `}`, and the same instruction to the same address is what a trailing `return;` compiles to
46
+ // — the two objects differ only by an empty forwarder block between them, which is a fact about
47
+ // branch chains rather than about return spelling. It is kept, which is the side that can never
48
+ // delete a return the object needs.
49
+ //
50
+ // The in-edges the reading is about are the ways the body ENDS, so only LIVE ones count. The thumb
51
+ // frontend hands over unreachable blocks on purpose, and one laid out just before the epilogue would
52
+ // otherwise contribute a fall-through no execution takes — deleting the `return;` every real edge
53
+ // branched in with.
54
+ //
55
+ // This decides SPELLING only; `l3/tailret.ts` owns whether a marked return is safe to delete.
56
+ import type { Block, Fn } from '../ir/core';
57
+ import { predecessors, reachableBlocks } from '../ir/core';
58
+
59
+ /** The arrival an in-edge stands for: a `return;` the source wrote, or the body running out. */
60
+ const isWrittenBranch = (p: Block): boolean => {
61
+ const term = p.ops[p.ops.length - 1];
62
+ return term.opcode === 'br' && term.attrs.fallthrough !== true;
63
+ };
64
+ const isFallThrough = (p: Block): boolean => {
65
+ const term = p.ops[p.ops.length - 1];
66
+ return term.opcode === 'br' && term.attrs.fallthrough === true;
67
+ };
68
+ /** A branch out of an EMPTY block is an arm with nothing else in it: the `return;` it stands for is
69
+ * the only statement that arm would hold, so it has nowhere else to live and is never dropped. */
70
+ const isBareBranch = (p: Block): boolean => p.ops.length === 1 && isWrittenBranch(p);
71
+
72
+ /** The blocks whose `ret` the assembly shows no `return;` for. */
73
+ export function unspelledEpilogues(fn: Fn): Set<Block> {
74
+ const preds = predecessors(fn);
75
+ const live = reachableBlocks(fn);
76
+ const out = new Set<Block>();
77
+ for (const b of fn.blocks) {
78
+ const term = b.ops[b.ops.length - 1];
79
+ if (term?.opcode !== 'ret') {
80
+ continue;
81
+ }
82
+ const inEdges = (preds.get(b) ?? []).filter((p) => live.has(p));
83
+ // Nothing arrived here from anywhere: the body never left its one block, so it transferred
84
+ // control to no epilogue and spelled no `return;`.
85
+ if (inEdges.length === 0) {
86
+ out.add(b);
87
+ continue;
88
+ }
89
+ const fellIn = inEdges.some(isFallThrough) && !inEdges.some(isBareBranch);
90
+ if (term.attrs.fallthrough === true || (b.ops.length === 1 && (fellIn || !inEdges.some(isWrittenBranch)))) {
91
+ out.add(b);
92
+ }
93
+ }
94
+ return out;
95
+ }