@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
package/src/pattern/engine.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface RewritePattern {
|
|
|
67
67
|
// `applies` is DATA, consumed generically by patternApplies — NOT an `arch ==` branch.
|
|
68
68
|
// `isa` pins the ISA; `compilers` pins which COMPILERS emit this idiom (the same shift-sequence
|
|
69
69
|
// for `/2` is produced by agbcc AND gcc, so a compiler LIST, not a single arch, is the honest
|
|
70
|
-
// predicate); `capabilities` is a hardware predicate. An absent
|
|
70
|
+
// predicate); `capabilities` is a hardware predicate. An absent field means "don't constrain on it".
|
|
71
71
|
applies: { isa?: string; compilers?: string[]; capabilities?: Partial<{ hwDivide: boolean; hwFloat: boolean }> };
|
|
72
72
|
match: MatchNode; // rooted at the op result to replace
|
|
73
73
|
// NOTE: a RELATIONAL guard (a `where` clause constraining the bound immediates, e.g. "two shift
|
|
@@ -91,16 +91,30 @@ export interface RewritePattern {
|
|
|
91
91
|
* is one inhabitant. The direction was measured on ONE compiler, so `validatePattern` pins the
|
|
92
92
|
* declaring pattern's compiler gate to that set; widening it fails loud. */
|
|
93
93
|
unsequencedRightFirst?: [string, string];
|
|
94
|
+
/** The compilers measured to RECOMPUTE this idiom instead of CSEing it back, when an interior op
|
|
95
|
+
* of the match survives the fold because something outside still reads it. On one of these the
|
|
96
|
+
* fold refuses that shape (`sharesInterior`, which carries the disassemblies); everywhere else
|
|
97
|
+
* it is byte-neutral and refusing it costs matches, so the list is opt-in and per-compiler. A
|
|
98
|
+
* compiler absent from it was either measured neutral or not measured — `validatePattern`
|
|
99
|
+
* refuses a name that is not in `applies.compilers`, where the fold cannot fire anyway. */
|
|
100
|
+
recomputesSharedInterior?: string[];
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
/**
|
|
103
|
+
/** What the pattern layer reads off a target. STRUCTURAL on purpose: the pattern set is
|
|
104
|
+
* serializable data and does not import `target.ts`, so a `TargetDescription` satisfies this by
|
|
105
|
+
* shape. It is the layer's ONE channel — `patternApplies` and `applyPattern` both take it, so a
|
|
106
|
+
* caller that raises through the patterns cannot hand one of them a target and the other nothing. */
|
|
107
|
+
export interface PatternTarget {
|
|
108
|
+
id: string;
|
|
109
|
+
compiler: string;
|
|
110
|
+
capabilities: { hwDivide: boolean; hwFloat: boolean };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Does this pattern apply to `target`? Every DECLARED field must match: the ISA (so an idiom can be
|
|
97
114
|
* pinned to one frontend), the compiler set (so an idiom fires only for the compilers that emit
|
|
98
115
|
* it — the reason MIPS+IDO and MIPS+GCC are distinguishable despite one frontend), and every
|
|
99
|
-
* declared capability. An omitted
|
|
100
|
-
export function patternApplies(
|
|
101
|
-
p: RewritePattern,
|
|
102
|
-
target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
|
|
103
|
-
): boolean {
|
|
116
|
+
* declared capability. An omitted field is unconstrained. */
|
|
117
|
+
export function patternApplies(p: RewritePattern, target: PatternTarget): boolean {
|
|
104
118
|
if (p.applies.isa && p.applies.isa !== target.id) {
|
|
105
119
|
return false;
|
|
106
120
|
}
|
|
@@ -172,7 +186,7 @@ export const SDIV_POW2_2: RewritePattern = {
|
|
|
172
186
|
// out as its own definition. Folding the triple back to one `smod`/`umod` gives recovery and the
|
|
173
187
|
// structurer the operator the source wrote, and re-emitting `%` reproduces the triple byte-exact.
|
|
174
188
|
//
|
|
175
|
-
// This is NOT the `capabilities.hwDivide`
|
|
189
|
+
// This is NOT the `capabilities.hwDivide` field: MIPS also divides in hardware and needs no fold at
|
|
176
190
|
// all, because `div` leaves the remainder in `hi` and the frontend reads it straight out. The
|
|
177
191
|
// narrower fact is a hardware divide that yields the QUOTIENT ONLY; `isa: 'ppc'` STANDS IN for it
|
|
178
192
|
// until a second ISA earns the capability, and `compilers` carries the measured half, that mwcc's
|
|
@@ -271,9 +285,27 @@ export const MUL_CONST_PATTERNS: RewritePattern[] = [MUL_SHIFT_ADD, MUL_SHIFT_SU
|
|
|
271
285
|
// lsr/asr #24`). The naive lift prints `x << 24 >> 24` — but C's `>>` over the s32-typed value is
|
|
272
286
|
// ARITHMETIC, so the UNSIGNED case recompiles with `asr` where the target has `lsr`: a miscompile
|
|
273
287
|
// (tou8/zextb/tou16 nonmatch). Folding to a cast op both fixes that and reads correctly; recompiling
|
|
274
|
-
// `(u8)x` reproduces `lsl;lsr`.
|
|
275
|
-
//
|
|
276
|
-
//
|
|
288
|
+
// `(u8)x` reproduces `lsl;lsr`. `k = 32 - w`.
|
|
289
|
+
//
|
|
290
|
+
// THE TWO SIGNEDNESSES ARE GATED SEPARATELY, because MIPS spells them differently and the pair of
|
|
291
|
+
// gates is the measurement. Compiled at each row's own flags — IDO 7.1 `-mips2 -O2 -32 -non_shared
|
|
292
|
+
// -G 0`, KMC gcc `-mips3 -O2`, gcc 2.7.2 `-mips3 -O1`:
|
|
293
|
+
//
|
|
294
|
+
// (s8)x sll v0,a0,0x18 ; sra v0,v0,0x18 (s16)x sll 0x10 ; sra 0x10 all three
|
|
295
|
+
// (u8)x andi v0,a0,0xff all three
|
|
296
|
+
//
|
|
297
|
+
// So the SIGN-extend is a shift pair on MIPS exactly as on agbcc, and folding it is byte-neutral
|
|
298
|
+
// there: the fold changes the printed spelling from `x << 24 >> 24` to `(s8)x` and recompiles to
|
|
299
|
+
// the same two instructions. The ZERO-extend is not — `andi` is not a shift pair, and folding an
|
|
300
|
+
// IDO `srl` to a `zext` would re-spell it as `andi`, a miscompile. So `zextPat` is pinned to agbcc
|
|
301
|
+
// alone, and the row that pin protects is `synthetic:zextb:ido7.1` — whose target IS that `andi`.
|
|
302
|
+
//
|
|
303
|
+
// mwcc/PowerPC is in NEITHER list: it has `extsb`/`extsh`, which the frontend lifts straight to
|
|
304
|
+
// `sext`, so the shift-pair shape is not one it emits and no measurement licenses the fold there.
|
|
305
|
+
/** The compilers measured to lower a SIGNED narrowing cast to a shift pair — see the pair of
|
|
306
|
+
* disassemblies above. A compiler outside this list keeps the raw shifts. */
|
|
307
|
+
export const SEXT_SHIFT_PAIR_COMPILERS = ['agbcc', 'ido', 'gcc'];
|
|
308
|
+
|
|
277
309
|
const zextPat = (w: number, k: number): RewritePattern => ({
|
|
278
310
|
id: `zext${w}`,
|
|
279
311
|
applies: { compilers: ['agbcc'] },
|
|
@@ -282,7 +314,10 @@ const zextPat = (w: number, k: number): RewritePattern => ({
|
|
|
282
314
|
});
|
|
283
315
|
const sextPat = (w: number, k: number): RewritePattern => ({
|
|
284
316
|
id: `sext${w}`,
|
|
285
|
-
applies: { compilers:
|
|
317
|
+
applies: { compilers: SEXT_SHIFT_PAIR_COMPILERS },
|
|
318
|
+
// IDO alone emits the `sll` twice when the pair's own `sll` has a second reader; agbcc and both
|
|
319
|
+
// MIPS GCCs CSE it back, byte-identically. `sharesInterior` carries the three-spelling table.
|
|
320
|
+
recomputesSharedInterior: ['ido'],
|
|
286
321
|
match: { op: 'shr_s', attrEquals: { imm: k }, args: [{ op: 'shl', attrEquals: { imm: k }, args: [{ bind: 'X' }] }] },
|
|
287
322
|
replaceWith: { op: 'sext', args: ['X'], attrs: { width: w } },
|
|
288
323
|
});
|
|
@@ -296,7 +331,8 @@ const sextPat = (w: number, k: number): RewritePattern => ({
|
|
|
296
331
|
* and never reaches the bitfield member recognizer (structure.ts, which matches the raw
|
|
297
332
|
* `shr(shl(load))` shape only). Honest output, not a miscompile — the field just keeps the cast
|
|
298
333
|
* spelling at those widths. Teaching the recognizer a zext/sext arm is the coverage extension if
|
|
299
|
-
* a row ever needs it.
|
|
334
|
+
* a row ever needs it. The SIGNED half of that shadow reaches MIPS too, where the symbol maps are:
|
|
335
|
+
* measured over the whole corpus, no row's emitted C moves for it (`pnpm bench sweep`). */
|
|
300
336
|
export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16), sextPat(8, 24), sextPat(16, 16)];
|
|
301
337
|
|
|
302
338
|
// ── boolean-negation idiom ───────────────────────────────────────────────────────────────────
|
|
@@ -408,6 +444,9 @@ const COMMUTATIVE = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_
|
|
|
408
444
|
interface Binds {
|
|
409
445
|
values: Map<string, Value>;
|
|
410
446
|
imms: Map<string, number>;
|
|
447
|
+
/** Every value matched by an `op` node of the pattern, root included — the ops this rewrite is
|
|
448
|
+
* about to REPLACE. `sharesInterior` below turns it into the fold's own legality condition. */
|
|
449
|
+
interior: Set<Value>;
|
|
411
450
|
}
|
|
412
451
|
|
|
413
452
|
function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
|
|
@@ -430,6 +469,7 @@ function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): bo
|
|
|
430
469
|
if (!d || d.opcode !== node.op) {
|
|
431
470
|
return false;
|
|
432
471
|
}
|
|
472
|
+
b.interior.add(v);
|
|
433
473
|
if (node.attrEquals) {
|
|
434
474
|
for (const [k, val] of Object.entries(node.attrEquals)) {
|
|
435
475
|
if (d.attrs[k] !== val) {
|
|
@@ -459,7 +499,7 @@ function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): bo
|
|
|
459
499
|
[0, 1],
|
|
460
500
|
[1, 0],
|
|
461
501
|
] as const) {
|
|
462
|
-
const trial: Binds = { values: new Map(b.values), imms: new Map(b.imms) };
|
|
502
|
+
const trial: Binds = { values: new Map(b.values), imms: new Map(b.imms), interior: new Set(b.interior) };
|
|
463
503
|
if (tryMatch(node.args[0], d.operands[i], defs, trial) && tryMatch(node.args[1], d.operands[j], defs, trial)) {
|
|
464
504
|
for (const [k, val] of trial.values) {
|
|
465
505
|
b.values.set(k, val);
|
|
@@ -467,6 +507,9 @@ function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): bo
|
|
|
467
507
|
for (const [k, val] of trial.imms) {
|
|
468
508
|
b.imms.set(k, val);
|
|
469
509
|
}
|
|
510
|
+
for (const val of trial.interior) {
|
|
511
|
+
b.interior.add(val);
|
|
512
|
+
}
|
|
470
513
|
return true;
|
|
471
514
|
}
|
|
472
515
|
}
|
|
@@ -523,6 +566,17 @@ function validatePattern(pat: RewritePattern): void {
|
|
|
523
566
|
);
|
|
524
567
|
}
|
|
525
568
|
}
|
|
569
|
+
const recompiles = pat.recomputesSharedInterior;
|
|
570
|
+
if (recompiles?.length) {
|
|
571
|
+
const on = pat.applies.compilers;
|
|
572
|
+
const inert = on === undefined ? [] : recompiles.filter((c) => !on.includes(c));
|
|
573
|
+
if (inert.length) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`pattern '${pat.id}' names [${inert.join(', ')}] in 'recomputesSharedInterior' but does not apply to ` +
|
|
576
|
+
`${on === undefined ? 'them' : `[${on.join(', ')}]`} — the refusal could never fire, so the declaration is inert`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
526
580
|
validated.add(pat);
|
|
527
581
|
}
|
|
528
582
|
|
|
@@ -621,8 +675,66 @@ function reordersUnsequenced(
|
|
|
621
675
|
return false;
|
|
622
676
|
}
|
|
623
677
|
|
|
624
|
-
/**
|
|
625
|
-
|
|
678
|
+
/** Would this fold RECOMPUTE the idiom rather than re-spell it — leave an INTERIOR op standing
|
|
679
|
+
* because something outside the match still reads it, while the replacement computes the same
|
|
680
|
+
* thing again?
|
|
681
|
+
*
|
|
682
|
+
* Every pattern here is licensed by a compiled pair showing that the replacement's C recompiles to
|
|
683
|
+
* the SAME instructions. That pair is measured on the shape where the fold's interior ops DIE with
|
|
684
|
+
* it — `dce` below removes exactly the ops nothing else reads. An interior op with a surviving
|
|
685
|
+
* reader does not die, and whether the recompiling compiler then emits the work twice or CSEs it
|
|
686
|
+
* back is a COMPILER fact, so it was compiled rather than reasoned. One function, three spellings,
|
|
687
|
+
* each toolchain at its own canonical flags:
|
|
688
|
+
*
|
|
689
|
+
* int r1(int x){ int y = x << 24; return (y >> 24) + y; }
|
|
690
|
+
*
|
|
691
|
+
* ido7.1 raw `(a0 << 24 >> 24) + (a0 << 24)` 4 words == the object of the C above
|
|
692
|
+
* folded `(s8)a0 + (a0 << 24)` 5 words an extra `sll`
|
|
693
|
+
* gcc2.7.2kmc raw 4 words / folded 4 words, BYTE-IDENTICAL
|
|
694
|
+
* gcc2.7.2 raw 4 words / folded 4 words, BYTE-IDENTICAL
|
|
695
|
+
* agbcc raw and folded identical (`lsl;asr;add`) — one `lsl` either way
|
|
696
|
+
*
|
|
697
|
+
* So only IDO recomputes, and `recomputesSharedInterior` carries that list. On the other three the
|
|
698
|
+
* fold stays byte-neutral in this shape and refusing it would COST: agbcc's `s16 i; i++` is one
|
|
699
|
+
* `lsl #16` read by both the write-back's `lsr` and the comparison's `asr`, and a blanket refusal
|
|
700
|
+
* measured `synthetic:{membnarrow,sibwalk}:agbcc` out of MATCH and `kleod:sub_0800A5B8:agbcc` from
|
|
701
|
+
* 173 to 179.
|
|
702
|
+
*
|
|
703
|
+
* The ROOT is exempt and must be: `replaceAllUsesWith` brings its readers along, which is what
|
|
704
|
+
* makes a fold a re-spelling at all. It is the interior — the operands the match walked THROUGH —
|
|
705
|
+
* that this asks about. CONSTANTS REACH IT ASYMMETRICALLY, and both answers are the wanted ones:
|
|
706
|
+
* a `constImm` node binds a literal and returns before the record, so a shared constant the
|
|
707
|
+
* replacement re-spells as a literal never refuses a fold — nothing keeps a register alive for a
|
|
708
|
+
* literal. An `{ op: 'const' }` node is walked like any other op and IS interior, so one the
|
|
709
|
+
* replacement drops while another op still reads it counts as a survivor and refuses, which is the
|
|
710
|
+
* conservative direction for a materialization the compiler may or may not rematerialize.
|
|
711
|
+
*
|
|
712
|
+
* Refusing leaves the raw ops standing, which is the spelling that was byte-exact before any fold
|
|
713
|
+
* existed — a worse-READING answer, never a worse-scoring one. */
|
|
714
|
+
function sharesInterior(fn: Fn, root: Op, interior: Set<Value>, defs: Map<Value, Op>): boolean {
|
|
715
|
+
const matched = new Set([...interior].map((v) => defs.get(v)));
|
|
716
|
+
for (const b of fn.blocks) {
|
|
717
|
+
for (const o of b.ops) {
|
|
718
|
+
if (matched.has(o)) {
|
|
719
|
+
continue; // a read from INSIDE the idiom is one this rewrite is replacing
|
|
720
|
+
}
|
|
721
|
+
for (const v of [...o.operands, ...o.successors.flatMap((x) => x.args)]) {
|
|
722
|
+
if (v !== root.results[0] && interior.has(v)) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Apply one pattern greedily to a fixed point. Returns the number of rewrites.
|
|
732
|
+
*
|
|
733
|
+
* `target` is the same one `patternApplies` filtered with, REQUIRED because exactly one refusal
|
|
734
|
+
* reads it (`recomputesSharedInterior`, whose answer is per-compiler). Both towers that fold —
|
|
735
|
+
* `pipeline.ts` and `trace.ts` — must hand it over or the traced tower shows a fold `decompile()`
|
|
736
|
+
* refuses; required, a caller that omits it is a type error rather than a wrong answer. */
|
|
737
|
+
export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget): number {
|
|
626
738
|
validatePattern(pat);
|
|
627
739
|
let count = 0,
|
|
628
740
|
changed = true;
|
|
@@ -635,10 +747,13 @@ export function applyPattern(fn: Fn, pat: RewritePattern): number {
|
|
|
635
747
|
if (op.results.length !== 1) {
|
|
636
748
|
continue;
|
|
637
749
|
}
|
|
638
|
-
const binds: Binds = { values: new Map(), imms: new Map() };
|
|
750
|
+
const binds: Binds = { values: new Map(), imms: new Map(), interior: new Set() };
|
|
639
751
|
if (!tryMatch(pat.match, op.results[0], defs, binds)) {
|
|
640
752
|
continue;
|
|
641
753
|
}
|
|
754
|
+
if (pat.recomputesSharedInterior?.includes(target.compiler) && sharesInterior(fn, op, binds.interior, defs)) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
642
757
|
if (pat.unsequencedRightFirst && reordersUnsequenced(fn, op, pat.unsequencedRightFirst, binds, defs, pat.id)) {
|
|
643
758
|
continue;
|
|
644
759
|
}
|
package/src/pipeline.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { LanguageBackend, SFn, gapReasonFor, walkExprs } from './l3/ast';
|
|
|
21
21
|
import { BASECSE_GATES, hoistBaseLocals } from './l3/basecse';
|
|
22
22
|
import { eliminateDeadStores } from './l3/dce';
|
|
23
23
|
import { mergeCommonTails } from './l3/tailmerge';
|
|
24
|
+
import { dropUnspelledReturns } from './l3/tailret';
|
|
24
25
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
25
26
|
import { type FnProto, type Prototypes, prototypesFromSymbols } from './proto';
|
|
26
27
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
@@ -212,7 +213,7 @@ export function applyIdiomPatterns(fn: Fn, target: TargetDescription, patterns?:
|
|
|
212
213
|
const active = (patterns ?? DEFAULT_IDIOM_PATTERNS).filter((p) => patternApplies(p, target));
|
|
213
214
|
let hits = 0;
|
|
214
215
|
for (const p of active) {
|
|
215
|
-
hits += applyPattern(fn, p);
|
|
216
|
+
hits += applyPattern(fn, p, target);
|
|
216
217
|
}
|
|
217
218
|
if (active.length) {
|
|
218
219
|
dce(fn);
|
|
@@ -261,7 +262,7 @@ export function raiseRecovered(
|
|
|
261
262
|
self?: FnProto,
|
|
262
263
|
pre: PreRecoveryOptions = {},
|
|
263
264
|
): void {
|
|
264
|
-
runPreRecovery(
|
|
265
|
+
const lifted = runPreRecovery(
|
|
265
266
|
fn,
|
|
266
267
|
target,
|
|
267
268
|
(pass, result) => {
|
|
@@ -276,7 +277,15 @@ export function raiseRecovered(
|
|
|
276
277
|
verify(fn);
|
|
277
278
|
assertTypesRecovered(fn);
|
|
278
279
|
hooks.afterRecover?.();
|
|
279
|
-
|
|
280
|
+
// `lifted.mergeShapes` is the CFG as it ENTERED pre-recovery, and retsink's `pre-diamond` needs
|
|
281
|
+
// exactly that: `raise/shortcircuit.ts` manufactures two-armed diamonds out of condition trees the
|
|
282
|
+
// ROM never merged, and a diamond this pass reads at its own turn may be one of those.
|
|
283
|
+
if (
|
|
284
|
+
sinkReturns(fn, {
|
|
285
|
+
hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm,
|
|
286
|
+
mergeShapes: lifted.mergeShapes,
|
|
287
|
+
})
|
|
288
|
+
) {
|
|
280
289
|
verify(fn);
|
|
281
290
|
hooks.afterRetsink?.();
|
|
282
291
|
}
|
|
@@ -333,16 +342,37 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
333
342
|
if (!names.size || /unmodelled instruction/.test(e.message)) {
|
|
334
343
|
throw e;
|
|
335
344
|
}
|
|
336
|
-
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written
|
|
345
|
+
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written spelling
|
|
337
346
|
// misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
|
|
338
347
|
const list = [...names].sort().map(gapReasonFor).join(', ');
|
|
339
348
|
throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
|
|
340
349
|
}
|
|
341
350
|
}
|
|
342
351
|
|
|
352
|
+
/** The committed readability/quality rewrites, in the order they are committed in: merge a
|
|
353
|
+
* statement common to every arm of an if, drop the void returns the assembly did not spell, drop
|
|
354
|
+
* dead stores (whose empty-then peephole flips the arm the merge empties), then hoist each leaf
|
|
355
|
+
* base the DEFAULT gate table admits into a typed local pointer.
|
|
356
|
+
*
|
|
357
|
+
* THE ORDER IS OBSERVABLE, not a preference: drop the returns before the merge and the same arms
|
|
358
|
+
* become peelable, emptying both of them. `l3/tailret.ts` states the mechanism, and
|
|
359
|
+
* `test/tailret.test.ts` pins the difference and which side of it this function is on.
|
|
360
|
+
*
|
|
361
|
+
* `hoistBaseLocals`' two arguments are SPELLED, defaults or not: this is the one call to that pass
|
|
362
|
+
* that is committed rather than offered, so it is the one whose gate table and whose placement can
|
|
363
|
+
* cost a MATCH instead of a candidate (docs/level-tower.md). A committed policy that reads as
|
|
364
|
+
* "whatever the default is" is the policy nobody reviews. */
|
|
365
|
+
export function readabilityRewrites(raw: SFn): SFn {
|
|
366
|
+
return hoistBaseLocals(eliminateDeadStores(dropUnspelledReturns(mergeCommonTails(raw))), BASECSE_GATES, 'head');
|
|
367
|
+
}
|
|
368
|
+
|
|
343
369
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
344
|
-
export function structureChecked(
|
|
345
|
-
|
|
370
|
+
export function structureChecked(
|
|
371
|
+
fn: Fn,
|
|
372
|
+
opts: Parameters<typeof structure>[1],
|
|
373
|
+
hooks?: Parameters<typeof structure>[2],
|
|
374
|
+
): SFn {
|
|
375
|
+
const raw = attributeOpaques(fn, () => structure(fn, opts, hooks));
|
|
346
376
|
// The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
347
377
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
348
378
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
|
@@ -353,16 +383,9 @@ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]):
|
|
|
353
383
|
assertDerefsTyped(raw);
|
|
354
384
|
assertLocalsWritten(raw);
|
|
355
385
|
assertEffectsPreserved(fn, raw);
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
// cast from each `index` node onto the local's initializer, so re-validate deref typing on the
|
|
360
|
-
// rewritten tree.
|
|
361
|
-
// Both arguments SPELLED, defaults or not: this is the one call to this pass that is committed
|
|
362
|
-
// rather than offered, so it is the one whose gate table and whose placement can cost a MATCH
|
|
363
|
-
// instead of a candidate (docs/level-tower.md). A committed policy that reads as "whatever the
|
|
364
|
-
// default is" is the policy nobody reviews.
|
|
365
|
-
const sfn = hoistBaseLocals(eliminateDeadStores(mergeCommonTails(raw)), BASECSE_GATES, 'head');
|
|
386
|
+
// The hoist moves the deref cast from each `index` node onto the local's initializer, so
|
|
387
|
+
// re-validate deref typing on the rewritten tree.
|
|
388
|
+
const sfn = readabilityRewrites(raw);
|
|
366
389
|
assertDerefsTyped(sfn);
|
|
367
390
|
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
368
391
|
// merges arms or drops statements must not be able to lose or duplicate a call.
|
package/src/proto.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { SymbolMap, SymbolTypeFacts } from './symbols';
|
|
|
16
16
|
* the extension is an inference off an encoding two different C sources produce. Where the asm
|
|
17
17
|
* carries no extension, this list is NOT consulted: pinning there would type every parameter of
|
|
18
18
|
* every row from the declaration, and a declared `u32` kills rank.ts's signed arm before the
|
|
19
|
-
* differ ever sees it. That half is
|
|
19
|
+
* differ ever sees it. That half is a variation question and is not answered here. */
|
|
20
20
|
export type ParamType = string;
|
|
21
21
|
|
|
22
22
|
/** What the headers know about one function. All fields optional: a partial table (only
|
|
@@ -143,7 +143,7 @@ export function validatePrototypes(value: unknown): string[] {
|
|
|
143
143
|
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
144
144
|
* makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
|
|
145
145
|
* would also be INERT: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
|
|
146
|
-
* are read for the list's length alone (test/param-pointee-
|
|
146
|
+
* are read for the list's length alone (test/param-pointee-variation.test.ts). */
|
|
147
147
|
function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
148
148
|
if (t.pointer) {
|
|
149
149
|
return 'void *';
|
package/src/raise/const.ts
CHANGED
|
@@ -3,17 +3,33 @@
|
|
|
3
3
|
// A RISC target builds a 32-bit literal in two halves: a high-half load (MIPS `lui`, PPC `lis`) then a
|
|
4
4
|
// low-half `ori`/`addiu`. The frontends lift that pair faithfully as `or(const(hi<<16), const(lo))` /
|
|
5
5
|
// `add(const(hi<<16), const(lo))` — a live binary op over two `const` ops — because neither frontend can
|
|
6
|
-
// see across the two instructions. This pass folds
|
|
6
|
+
// see across the two instructions. This pass folds such a const/const `or`/`add` into a single `const`,
|
|
7
7
|
// which is the form that (a) type-recovers as one 32-bit literal and (b) recompiles to the exact
|
|
8
8
|
// `lui;ori` / `lis;ori` pair. Without it a magic-division reciprocal or an address literal is never a
|
|
9
9
|
// single value the later passes can reason about.
|
|
10
10
|
//
|
|
11
|
+
// THE CLIENTELE IS "A VALUE THE TARGET MATERIALISES IN TWO INSTRUCTIONS", and it is wider than the
|
|
12
|
+
// RISC pair above in both directions, and both halves are measured:
|
|
13
|
+
// - It is NOT ARM-free. On Thumb a literal is a pool word plus an immediate `add`, which is exactly a
|
|
14
|
+
// two-instruction materialisation and lifts as the same const/const pair. Ablating the whole pass
|
|
15
|
+
// costs three agbcc BYTE-MATCHES — `synthetic:dmafield` MATCH -> diff:29, `synthetic:fieldbase`
|
|
16
|
+
// MATCH -> diff:22, `synthetic:bgfixed` MATCH -> diff:2 (measured, whole pass off). Do not gate
|
|
17
|
+
// this pass on a RISC target.
|
|
18
|
+
// - The pair is NOT confined to ONE block. `synthetic:mergepool:gcc2.7.2kmc` lifts a genuine
|
|
19
|
+
// `lui;ori` as `or(const 65536, const 9029)` whose two halves are defined in DIFFERENT blocks
|
|
20
|
+
// (instrumented: `b0=0 b1=1`), because gcc hoisted the high half above the branch. A guard
|
|
21
|
+
// tightened to a literal same-block test loses folds the corpus depends on.
|
|
22
|
+
// What the refusal below actually excludes is narrower than either: an operand a terminator hands to a
|
|
23
|
+
// successor's block-parameter — a REGISTER the compiler held live across a branch — where the pair is
|
|
24
|
+
// ALSO not recognisable as a hi/lo pair and the result is not an address. See its site for the cost.
|
|
25
|
+
//
|
|
11
26
|
// This cannot be a data-`RewritePattern`: the fold's result is COMPUTED from the two operands' values,
|
|
12
27
|
// which the pattern engine's numeric-exact `attrEquals` cannot express. So it lives here as an always-on
|
|
13
28
|
// recognizer, run before type recovery. Value-preserving and local; a single left-to-right pass suffices
|
|
14
29
|
// (SSA guarantees each const is defined before the op that consumes it, and a folded result feeds
|
|
15
30
|
// forward for any chained materialisation).
|
|
16
|
-
import { Fn, Op, defOpMap, mkOp } from '../ir/core';
|
|
31
|
+
import { Fn, Op, Value, defOpMap, mkOp } from '../ir/core';
|
|
32
|
+
import { MEM_BASE_OPS } from '../ir/opcodes';
|
|
17
33
|
|
|
18
34
|
// The binary opcodes whose const/const form is a constant. `>> 0` normalises to a signed 32-bit result
|
|
19
35
|
// (hardware wraparound): `|` already yields int32, `+` may exceed it and is truncated to match `addu`/`add`.
|
|
@@ -22,11 +38,123 @@ const FOLD: Record<string, (a: number, b: number) => number> = {
|
|
|
22
38
|
add: (a, b) => (a + b) >> 0,
|
|
23
39
|
};
|
|
24
40
|
|
|
41
|
+
/** Is `v` a RISC HIGH HALF — what one `lui`/`lis`/`addis` puts in a register? `hi << 16`, for a
|
|
42
|
+
* NON-ZERO `hi`. Zero is excluded deliberately, and that exclusion is what makes this test
|
|
43
|
+
* trustworthy as a positive: `lui rD, 0` is a no-op no compiler emits, so a `const 0` is never a
|
|
44
|
+
* half being materialised — it is an initialised register, exactly the shape the refusal below
|
|
45
|
+
* exists to protect.
|
|
46
|
+
*
|
|
47
|
+
* IT COVERS THE RISC HALF OF THE CLIENTELE ONLY, because the 16/16 split is an ISA fact: censused
|
|
48
|
+
* over the corpus's const/const fold sites, all 19 RISC ones (gcc2.7.2kmc / mwcc_242_81 / ido7.1)
|
|
49
|
+
* are recognisable to it and 0 of the 20 agbcc ones are — an ARM pool word is an arbitrary 32-bit
|
|
50
|
+
* value (`0x03001C00`, low half `0x1C00`) and can never pass. The ARM half of the clientele is
|
|
51
|
+
* protected by `memBases` and by simply not being edge-carried; four agbcc sites on three rows
|
|
52
|
+
* (`dmascope` ×2, `dmascope2`, `dmafield`'s `add(…,112)`) sit behind neither carve-out. If a row
|
|
53
|
+
* ever needs the buy-back on ARM, the half-width belongs on the target description —
|
|
54
|
+
* `PreRecoveryPass` already threads `target`, and `softdiv` gates on `capabilities.hwDivide` —
|
|
55
|
+
* rather than as a second constant here. */
|
|
56
|
+
const isHighHalf = (v: number): boolean => (v & 0xffff) === 0 && v !== 0;
|
|
57
|
+
|
|
58
|
+
/** Is `v` a LOW HALF — what one `ori`/`addi`/`addiu` can supply? `ori` takes an UNSIGNED 16-bit
|
|
59
|
+
* immediate and `addi`/`addiu` a SIGNED one, so the admissible range is the union: mwcc completes an
|
|
60
|
+
* `addis` with a NEGATIVE `addi` whenever bit 15 of the low half is set (`0x12350000 + -25924` is
|
|
61
|
+
* `0x12345ABC`). Deliberately NOT split per-opcode (`or` unsigned, `add` signed): that narrowing is
|
|
62
|
+
* true of the ISA, reaches 0 rows of the corpus, and no fixture reddens when it is removed, so it
|
|
63
|
+
* would be an unpinned clause. The half-width belongs on the target description if a row needs it. */
|
|
64
|
+
const isLowHalf = (v: number): boolean => v >= -0x8000 && v <= 0xffff;
|
|
65
|
+
|
|
66
|
+
/** The constant a foldable const/const pair denotes, or `null` when `opcode` is not one this pass
|
|
67
|
+
* folds. Exported because `structure.ts` repairs the refusal's residue at RENDER time and must
|
|
68
|
+
* print exactly what an unrefused fold here would have produced — the opcode set and the int32
|
|
69
|
+
* normalisation are one decision, so they live in one place. */
|
|
70
|
+
export function foldConstPair(opcode: string, a: number, b: number): number | null {
|
|
71
|
+
const f = FOLD[opcode];
|
|
72
|
+
return f ? f(a, b) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Does this opcode's const/const form denote a constant? The membership half of `foldConstPair`,
|
|
76
|
+
* for callers that must classify an op before they have its operands' values. */
|
|
77
|
+
export const isConstFoldOpcode = (opcode: string): boolean => opcode in FOLD;
|
|
78
|
+
|
|
79
|
+
/** THE EVIDENCE THE FOLD WOULD OTHERWISE DESTROY, stamped on the literal it produces.
|
|
80
|
+
*
|
|
81
|
+
* Three Thumb shapes put two accesses a constant distance apart, and the assembly tells them
|
|
82
|
+
* apart: two pool words (two independent `const` ops), one pool word plus memory-operand
|
|
83
|
+
* displacements (`l3/ast.ts`'s `operandOff`), and one pool word plus an `add` to the register
|
|
84
|
+
* that already held the first address (`ldr r3,=X; strh [r3]; adds r3,#2; strh [r3]`). The third
|
|
85
|
+
* lifts as `add(const X, const 2)` — indistinguishable, once folded, from a literal `X + 2` the
|
|
86
|
+
* compiler materialised in two instructions, which is this pass's whole clientele. So the fold
|
|
87
|
+
* still happens and the distinction is recorded, exactly as `structure.ts` records `operandOff`
|
|
88
|
+
* before its own fold (see the `operandOff` note in l3/ast.ts).
|
|
89
|
+
*
|
|
90
|
+
* WHAT IT ASSERTS is narrow and is a fact about REGISTERS, not about C: at this instruction the
|
|
91
|
+
* machine held the base address in a register, used it as an address, and advanced it by `step`
|
|
92
|
+
* bytes to reach another address it also used. THE READER decides what to spell — `l3/advance.ts`
|
|
93
|
+
* offers a pointer local advanced in place, which is the only C spelling that reproduces the
|
|
94
|
+
* `add` on a compiler that folds a constant subscript into the memory operand, and which is
|
|
95
|
+
* INERT (same bytes as the subscript) wherever the pointee is not `volatile`.
|
|
96
|
+
*
|
|
97
|
+
* THE THREE REFUSALS, each of which makes the stamp mean something else:
|
|
98
|
+
* • `or`, not `add` — a hi/lo `or` is a literal being assembled, never a pointer being moved.
|
|
99
|
+
* • the ADDEND is itself a memory base. Then both halves are addresses and neither is the step;
|
|
100
|
+
* the shape is not an advance and the stamp would name an arbitrary one of them.
|
|
101
|
+
* • either the base or the result is never used AS AN ADDRESS. A register that only feeds
|
|
102
|
+
* arithmetic is a value, and `X + 2` over two values is a literal by every reading.
|
|
103
|
+
* A zero step is dropped too: it names no advance, and `l3/ast.ts`'s readers all test
|
|
104
|
+
* `!== undefined` rather than truthiness, so a stamped 0 would read as a real one. */
|
|
105
|
+
function advanceEvidence(op: Op, a: number, c: number, memBases: ReadonlySet<Value>): { advancedBy?: number } {
|
|
106
|
+
if (op.opcode !== 'add' || !memBases.has(op.results[0])) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
const baseIdx = memBases.has(op.operands[0]) ? 0 : memBases.has(op.operands[1]) ? 1 : -1;
|
|
110
|
+
if (baseIdx < 0 || memBases.has(op.operands[1 - baseIdx])) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
const step = baseIdx === 0 ? c : a;
|
|
114
|
+
return step === 0 ? {} : { advancedBy: step };
|
|
115
|
+
}
|
|
116
|
+
|
|
25
117
|
/** Fold each const/const `or`/`add` into one `const`, in place. Returns whether anything changed. The
|
|
26
118
|
* now-dead source consts are left for DCE (they may still have other uses; liveness is not our concern). */
|
|
27
119
|
export function recognizeConsts(fn: Fn): boolean {
|
|
28
120
|
let changed = false;
|
|
29
121
|
const defs = defOpMap(fn);
|
|
122
|
+
// The two facts the CLIENTELE REFUSAL below reads, both collected in one walk.
|
|
123
|
+
// `edgeSlots` — for every value a terminator hands to a successor, WHICH block-parameters it
|
|
124
|
+
// feeds. Being in the map at all is "a register the compiler held live across a branch": in
|
|
125
|
+
// functional-form SSA the machine had this value in a register at the branch and the join
|
|
126
|
+
// reads it back. WHICH parameter is the second question, and the buy-back below needs it.
|
|
127
|
+
// `memBases` — every value used as a memory base, i.e. the values that ARE addresses.
|
|
128
|
+
const edgeSlots = new Map<Value, Set<Value>>();
|
|
129
|
+
const memBases = new Set<Value>();
|
|
130
|
+
for (const b of fn.blocks) {
|
|
131
|
+
for (const op of b.ops) {
|
|
132
|
+
for (const sc of op.successors) {
|
|
133
|
+
sc.args.forEach((v, i) => {
|
|
134
|
+
const param = sc.block.params[i];
|
|
135
|
+
if (param === undefined) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const slots = edgeSlots.get(v);
|
|
139
|
+
if (slots) {
|
|
140
|
+
slots.add(param);
|
|
141
|
+
} else {
|
|
142
|
+
edgeSlots.set(v, new Set([param]));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (MEM_BASE_OPS.has(op.opcode) && op.operands.length > 0) {
|
|
147
|
+
memBases.add(op.operands[0]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Do these two values reach the SAME block-parameter — i.e. are they two arms' feeds of one
|
|
152
|
+
* merge? See the refusal for why that is what tells an accumulator from a shared high half. */
|
|
153
|
+
const meetAtSameParam = (x: Value, y: Value): boolean => {
|
|
154
|
+
const sx = edgeSlots.get(x);
|
|
155
|
+
const sy = edgeSlots.get(y);
|
|
156
|
+
return !!sx && !!sy && [...sx].some((k) => sy.has(k));
|
|
157
|
+
};
|
|
30
158
|
const constOf = (op: Op | undefined): number | null =>
|
|
31
159
|
op && op.opcode === 'const' ? (op.attrs.value as number) : null;
|
|
32
160
|
for (const b of fn.blocks) {
|
|
@@ -41,8 +169,80 @@ export function recognizeConsts(fn: Fn): boolean {
|
|
|
41
169
|
if (a === null || c === null) {
|
|
42
170
|
continue;
|
|
43
171
|
}
|
|
172
|
+
// ── THE REFUSAL: this shape is not a literal being materialised ───────────────────────────
|
|
173
|
+
// An operand a terminator ALSO hands to a successor's block-parameter is a register the compiler
|
|
174
|
+
// held live across the branch, whose value on this path happens to be a constant. agbcc's
|
|
175
|
+
// `s = 0; ... if (c) s += 1;` lifts as `add(%s = const 0, const 1)` in the taken arm, where
|
|
176
|
+
// `%s` is also the value bb0 hands the join. Folding it to `const 1` deletes the accumulator's
|
|
177
|
+
// last reference, so every later level sees an arm that materialises a literal and spells it
|
|
178
|
+
// as one (`v = 1;` with an `else v = 0;`) instead of the `s += 1` the target records — and the
|
|
179
|
+
// enumeration gate for the shipped `/merge-home` variation, which is what would have spelled the
|
|
180
|
+
// hoisted init, reads FALSE because the merge feed it looks for is gone.
|
|
181
|
+
//
|
|
182
|
+
// The mapping is a FUNCTION, not an open question, so this is a default and not a variation: a register
|
|
183
|
+
// carried across a branch is not a literal being materialised, whichever compiler produced it.
|
|
184
|
+
//
|
|
185
|
+
// IT IS A PROXY, and the two carve-outs are where it is bought back. Edge-carrying is evidence
|
|
186
|
+
// of a register, not proof, and the same `add(const 0, const K)` still folds wherever nothing
|
|
187
|
+
// carries the zero (6 sites on 5 marioparty3/snowboardkids2 rows) — defensible, since with no
|
|
188
|
+
// merge there is no home to hoist and the incident cannot occur, but the rule is narrower than
|
|
189
|
+
// "never fold a const/const pair over a branch".
|
|
190
|
+
// - `hiLoPair` — the pass's OWN clientele beats the proxy. mwcc materialises `0x12345678` as
|
|
191
|
+
// `lis; addi` and shares the `lis` across a branch whenever the high half is live at the
|
|
192
|
+
// join, so the genuine pair IS edge-carried and the proxy refuses it: measured, a
|
|
193
|
+
// `base = 0x12340000; if (c) q[0] = base|0x5678; else q[1] = base|0x9ABC; *p = base;` row
|
|
194
|
+
// emitted `*a2 = 305397760 + 22136;` on mwcc_242_81 where every other toolchain emitted the
|
|
195
|
+
// folded literal. The literal is then no longer ONE value for `recognizeMagicDivision`, type
|
|
196
|
+
// recovery or the symbol map — the same never-enumerated failure this refusal exists to fix,
|
|
197
|
+
// one level down.
|
|
198
|
+
//
|
|
199
|
+
// `feedsSameMerge` IS A CONDITION ON THAT BUY-BACK: read as a bare "is this a hi/lo pair"
|
|
200
|
+
// it re-opens the very incident this refusal exists for. An accumulator's `const 0` init
|
|
201
|
+
// passes `isLowHalf`, so `s = 0; if (c) s += 0x10000;` — one 16.16 fixed-point step — is a
|
|
202
|
+
// hi/lo pair by the letter of the test, and its MIRROR `s = 0x10000; if (c) s += 1;` is
|
|
203
|
+
// one whichever operand is read as the half. Folded, a two-arm `s += 0x10000` row scores
|
|
204
|
+
// **diff:1** on mwcc_242_81 with `hasMergeFeedHome` FALSE; refused, **MATCH** with the
|
|
205
|
+
// gate TRUE. What separates the two shapes is WHERE THE VALUES GO rather than their bit
|
|
206
|
+
// patterns: an accumulator's init and its updated copy are two arms' feeds of ONE block
|
|
207
|
+
// parameter — exactly the merge `/merge-home` exists to home — whereas a shared high half
|
|
208
|
+
// reaches the join while the COMPLETED literal is stored or returned, never merged with
|
|
209
|
+
// the half it was built from. Measured FREE: with it in, 770 synthetic + 252 real rows are
|
|
210
|
+
// byte-identical in post-recovery IR, in emitted source and in gap list to the branch
|
|
211
|
+
// without it.
|
|
212
|
+
// - `memBases` — an address literal, `0x03001C00 + 1206` reached through one arm's base
|
|
213
|
+
// register. It decides 0 folds over the corpus's 806 lifted rows and is here as a
|
|
214
|
+
// statement of scope; `hiLoPair` is the clause that carries real traffic. Deliberately NOT
|
|
215
|
+
// transitive and NOT extended to call arguments: an address literal escaping as a call
|
|
216
|
+
// argument is a shape the refusal HELPS (a probe row scored diff:7 -> MATCH with it
|
|
217
|
+
// firing), so widening this test would give that back.
|
|
218
|
+
//
|
|
219
|
+
// NOT CONVERTIBLE TO A `Gate` TABLE, and the reason is the type rather than the minutes.
|
|
220
|
+
// `firstRejection` reads a table as a DISJUNCTION of independent refusals; the test below is
|
|
221
|
+
// a CONJUNCTION with two buy-backs, and "refuse unless exempted" decomposes only by making
|
|
222
|
+
// every term re-carry the whole conjunction. The census would then report one opaque id and
|
|
223
|
+
// stay silent about WHICH exemption fired — the only question the paragraphs above ask. Said
|
|
224
|
+
// here because a selector that ranks passes by refusal count or by instrument minutes points
|
|
225
|
+
// at this file (`grep -n "raise/const.ts" docs/level-tower.md`), and both readings are wrong
|
|
226
|
+
// about it for different reasons.
|
|
227
|
+
//
|
|
228
|
+
// A REFUSAL IS ALSO A SCHEDULING DECISION, and that coupling is invisible at this site:
|
|
229
|
+
// `pre-recovery.ts` registers this pass `dce: true` and runs `dce(fn)` only when the pass
|
|
230
|
+
// returns TRUTHY, so a function whose ONLY const/const pair is refused gets no DCE here at
|
|
231
|
+
// all — and `addrnum` above is `dce: false`, so this is the first pass that can schedule one.
|
|
232
|
+
// Measured inert: forcing the cancelled `dce(fn)` removes 0 ops on every invocation of every
|
|
233
|
+
// reachable row (`sinkacc`, 3 invocations). Worth knowing before this refusal is widened.
|
|
234
|
+
const edgeCarried = edgeSlots.has(op.operands[0]) || edgeSlots.has(op.operands[1]);
|
|
235
|
+
const feedsSameMerge =
|
|
236
|
+
meetAtSameParam(op.results[0], op.operands[0]) || meetAtSameParam(op.results[0], op.operands[1]);
|
|
237
|
+
const hiLoPair = ((isHighHalf(a) && isLowHalf(c)) || (isHighHalf(c) && isLowHalf(a))) && !feedsSameMerge;
|
|
238
|
+
if (edgeCarried && !hiLoPair && !memBases.has(op.results[0])) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
44
241
|
// Reuse the SAME result Value → every existing use already points at it (no RAUW needed).
|
|
45
|
-
const folded = mkOp('const', {
|
|
242
|
+
const folded = mkOp('const', {
|
|
243
|
+
results: [op.results[0]],
|
|
244
|
+
attrs: { value: fold(a, c), ...advanceEvidence(op, a, c, memBases) },
|
|
245
|
+
});
|
|
46
246
|
b.ops.splice(i, 1, folded);
|
|
47
247
|
defs.set(op.results[0], folded); // keep the def map current so a chained fold sees this const
|
|
48
248
|
changed = true;
|
package/src/raise/divpow2.ts
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
// SELF-VERIFYING. asmlift emits a plain `x / 2^k` and the target compiler regenerates ITS own
|
|
32
32
|
// lowering; a wrong divisor recompiles to different bytes and shows up as a nonmatch, never as a
|
|
33
33
|
// false match. The residual exposure is a lost match, not a miscompile — on a compiler that lowers
|
|
34
|
-
// `/2^k` branchlessly, a diamond of this shape came from hand-written biasing, and
|
|
34
|
+
// `/2^k` branchlessly, a diamond of this shape came from hand-written biasing, and rewriting it
|
|
35
35
|
// costs a match that used to land. Measured positive on ido7.1 (two flips), agbcc and gcc2.7.2kmc
|
|
36
|
-
// (modpow2 stays byte-exact through the
|
|
36
|
+
// (modpow2 stays byte-exact through the rewrite); mwcc_242_81 and gcc2.7.2 have no inhabitant, so
|
|
37
37
|
// they are unmeasured rather than clean.
|
|
38
38
|
//
|
|
39
39
|
// It is deliberately IDENTITY-OR-DECLINE about the shape (the bias constant must be exactly
|