@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.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- 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 =
|
|
80
|
-
stores: Map<Op, { global: string; field: string; 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:
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
// …
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// regresses none
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
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: '
|
|
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
|
|
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
|
+
}
|