@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- 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 +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- 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 +28 -25
- 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/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- 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/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- 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 +342 -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 +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- 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/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/raise/globalshape.ts
CHANGED
|
@@ -164,10 +164,12 @@ import type { SFn } from '../l3/ast';
|
|
|
164
164
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
165
165
|
import type { SymbolInfo } from '../symbols';
|
|
166
166
|
import type { TargetDescription } from '../target';
|
|
167
|
+
import { type ScaledExtension, foldablePairs, foldsShiftPairCasts } from './extscale';
|
|
167
168
|
|
|
168
169
|
/** One additive term of an address residual: `v` scaled by `scale`, or a pure constant.
|
|
169
|
-
* `scaleOp` is the op that DID the scaling (a `shl`/`mul
|
|
170
|
-
* the order licence reads; a term at scale 1
|
|
170
|
+
* `scaleOp` is the op that DID the scaling (a `shl`/`mul`, or the right shift of a fused cast —
|
|
171
|
+
* see `scaleOf`), which is what carries the position the order licence reads; a term at scale 1
|
|
172
|
+
* has none. */
|
|
171
173
|
interface Term {
|
|
172
174
|
scale: number;
|
|
173
175
|
/** null ⇒ a constant term, whose value is `konst` */
|
|
@@ -618,8 +620,18 @@ function useIndex(fn: Fn): Map<Value, Op[]> {
|
|
|
618
620
|
}
|
|
619
621
|
|
|
620
622
|
/** `x * K` / `x << k` read as a scale, or scale 1 for anything else. A CONSTANT operand makes the
|
|
621
|
-
* whole term constant instead (`const << 2` is a displacement, not a subscript).
|
|
622
|
-
|
|
623
|
+
* whole term constant instead (`const << 2` is a displacement, not a subscript).
|
|
624
|
+
*
|
|
625
|
+
* …and a narrowing cast FUSED with its scale, `shr(shl(x, 24), 21)` — `(u8)x << 3` after agbcc's
|
|
626
|
+
* combiner merged the pair's right half into the scale. Its RIGHT shift is the scaling op:
|
|
627
|
+
* compiled, `gTbl[i]` over a `u8 i` is `lsl` / `ldr` / `lsr` and `((u16 *)gTbl)[i]` is `lsl` /
|
|
628
|
+
* `lsr` / `ldr`, so the right half is where the order fork shows. A constant under the pair keeps
|
|
629
|
+
* the scale-1 reading — `const` folds that pair to its value before anything spells it.
|
|
630
|
+
*
|
|
631
|
+
* `fused` is raise/extscale.ts's own `foldablePairs`, or null where the target keeps the fold off,
|
|
632
|
+
* so a pair is read as a scale exactly when the fold will take it: a scale read anywhere else would
|
|
633
|
+
* license an element no pass legalizes. */
|
|
634
|
+
function scaleOf(v: Value, defs: Map<Value, Op>, fused: Map<Op, ScaledExtension> | null): Term {
|
|
623
635
|
const d = defs.get(v);
|
|
624
636
|
const constOf = (x: Value): number | null => {
|
|
625
637
|
const dx = defs.get(x);
|
|
@@ -638,6 +650,10 @@ function scaleOf(v: Value, defs: Map<Value, Op>): Term {
|
|
|
638
650
|
? { scale: 1 << k, v: d.operands[0], konst: 0, scaleOp: d }
|
|
639
651
|
: { scale: 1, v, konst: 0, scaleOp: null };
|
|
640
652
|
}
|
|
653
|
+
const pair = d === undefined ? undefined : fused?.get(d);
|
|
654
|
+
if (d !== undefined && pair !== undefined && constOf(pair.src) === null) {
|
|
655
|
+
return { scale: 1 << pair.shift, v: pair.src, konst: 0, scaleOp: d };
|
|
656
|
+
}
|
|
641
657
|
if (d?.opcode === 'mul') {
|
|
642
658
|
for (const [a, b] of [
|
|
643
659
|
[d.operands[0], d.operands[1]],
|
|
@@ -655,7 +671,7 @@ function scaleOf(v: Value, defs: Map<Value, Op>): Term {
|
|
|
655
671
|
/** The additive terms of a byte residual. Only `add` is opened: a `sub` at the top of the tree
|
|
656
672
|
* makes a term's sign depend on the walk, and a NEGATIVE stride is not an array subscript this
|
|
657
673
|
* spelling can express, so it refuses rather than dropping the sign. */
|
|
658
|
-
function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
|
|
674
|
+
function residualTerms(root: Value, defs: Map<Value, Op>, fused: Map<Op, ScaledExtension> | null): Term[] | null {
|
|
659
675
|
const out: Term[] = [];
|
|
660
676
|
const walk = (v: Value, depth: number): boolean => {
|
|
661
677
|
if (depth > 16) {
|
|
@@ -668,7 +684,7 @@ function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
|
|
|
668
684
|
if (d?.opcode === 'add') {
|
|
669
685
|
return walk(d.operands[0], depth + 1) && walk(d.operands[1], depth + 1);
|
|
670
686
|
}
|
|
671
|
-
out.push(scaleOf(v, defs));
|
|
687
|
+
out.push(scaleOf(v, defs, fused));
|
|
672
688
|
return true;
|
|
673
689
|
};
|
|
674
690
|
return walk(root, 0) ? out : null;
|
|
@@ -691,19 +707,23 @@ function residualTerms(root: Value, defs: Map<Value, Op>): Term[] | null {
|
|
|
691
707
|
* say in the type. */
|
|
692
708
|
function accessesBySymbol(
|
|
693
709
|
fn: Fn,
|
|
710
|
+
target: TargetDescription,
|
|
694
711
|
gates: readonly Gate<AddressUse>[],
|
|
695
712
|
): Map<string, ElementAccess[] | { refusedBy: string }>;
|
|
696
713
|
function accessesBySymbol(
|
|
697
714
|
fn: Fn,
|
|
715
|
+
target: TargetDescription,
|
|
698
716
|
gates: readonly Gate<AddressUse>[],
|
|
699
717
|
interiorIsEvidence: true,
|
|
700
718
|
): Map<string, Access[] | { refusedBy: string }>;
|
|
701
719
|
function accessesBySymbol(
|
|
702
720
|
fn: Fn,
|
|
721
|
+
target: TargetDescription,
|
|
703
722
|
gates: readonly Gate<AddressUse>[],
|
|
704
723
|
interiorIsEvidence = false,
|
|
705
724
|
): Map<string, Access[] | { refusedBy: string }> {
|
|
706
725
|
const defs = defOpMap(fn);
|
|
726
|
+
const fused = foldsShiftPairCasts(target) ? foldablePairs(fn, defs) : null;
|
|
707
727
|
const uses = useIndex(fn);
|
|
708
728
|
const out = new Map<string, Access[] | { refusedBy: string }>();
|
|
709
729
|
const refuse = (sym: string, id: string): void => void out.set(sym, { refusedBy: id });
|
|
@@ -728,7 +748,7 @@ function accessesBySymbol(
|
|
|
728
748
|
for (const u of gUses) {
|
|
729
749
|
const isAdd = u.opcode === 'add';
|
|
730
750
|
const other = isAdd ? (u.operands[0] === base ? u.operands[1] : u.operands[0]) : undefined;
|
|
731
|
-
const terms = other === undefined ? null : residualTerms(other, defs);
|
|
751
|
+
const terms = other === undefined ? null : residualTerms(other, defs, fused);
|
|
732
752
|
const consumers = (isAdd ? (uses.get(u.results[0]) ?? []) : []).map((m) => {
|
|
733
753
|
const isLoad = m.opcode === 'load' && m.operands[0] === u.results[0];
|
|
734
754
|
const isStore = m.opcode === 'store' && m.operands[0] === u.results[0];
|
|
@@ -903,8 +923,8 @@ function extentsOf(strides: number[]): number[] | null {
|
|
|
903
923
|
* same addresses whichever one a consumer picked up.
|
|
904
924
|
*
|
|
905
925
|
* This exists because the SPELLING and the DECLARATION are decided from different derivations —
|
|
906
|
-
* rank.ts derives the declaration dictionary once off the
|
|
907
|
-
*
|
|
926
|
+
* rank.ts derives the declaration dictionary once off the shared lift and the spelling per
|
|
927
|
+
* lift off that lift's own — and a candidate that spells from one and declares from
|
|
908
928
|
* the other addresses a different object than the assembly did, compiling either way. */
|
|
909
929
|
export function sameDerivedShape(a: SymbolInfo | undefined, b: SymbolInfo | undefined): boolean {
|
|
910
930
|
if (a === undefined || b === undefined) {
|
|
@@ -934,7 +954,7 @@ export function inferGlobalArrays(
|
|
|
934
954
|
return out;
|
|
935
955
|
}
|
|
936
956
|
const pos = positions(fn);
|
|
937
|
-
for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
|
|
957
|
+
for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
|
|
938
958
|
const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
|
|
939
959
|
if (si !== null) {
|
|
940
960
|
out.set(sym, si);
|
|
@@ -968,7 +988,7 @@ export function orderLicensedGlobals(
|
|
|
968
988
|
return out;
|
|
969
989
|
}
|
|
970
990
|
const pos = positions(fn);
|
|
971
|
-
for (const [sym, accs] of accessesBySymbol(fn, gates.address, true)) {
|
|
991
|
+
for (const [sym, accs] of accessesBySymbol(fn, target, gates.address, true)) {
|
|
972
992
|
if (Array.isArray(accs) && firstRejection(gates.shape, evidenceOf(accs, pos)) === null) {
|
|
973
993
|
out.add(sym);
|
|
974
994
|
}
|
|
@@ -991,7 +1011,7 @@ export function arrayShapeRefusals(
|
|
|
991
1011
|
return out;
|
|
992
1012
|
}
|
|
993
1013
|
const pos = positions(fn);
|
|
994
|
-
for (const [sym, accs] of accessesBySymbol(fn, gates.address)) {
|
|
1014
|
+
for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
|
|
995
1015
|
out.set(sym, Array.isArray(accs) ? firstRejection(gates.shape, evidenceOf(accs, pos)) : accs.refusedBy);
|
|
996
1016
|
}
|
|
997
1017
|
return out;
|
package/src/raise/gvn.ts
CHANGED
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
//
|
|
65
65
|
// Several modules answer "is this address a local?" with independent policies — here: never;
|
|
66
66
|
// basecse: at whichever of the two positions `l3/hoist.ts` is handed (the COMMITTED call states
|
|
67
|
-
// the function top, its roster
|
|
67
|
+
// the function top, its roster's hoists also offer each init's first use), when the gate table
|
|
68
68
|
// admits the base; l3/scopebase.ts: at the innermost scope holding the uses; l3/argbase.ts:
|
|
69
|
-
// immediately before a call whose arguments share it. The newer placement
|
|
69
|
+
// immediately before a call whose arguments share it. The newer placement variations — l3/nearbase.ts,
|
|
70
70
|
// l3/inlinebase.ts, l3/homesplit.ts — answer it too, each with its own.
|
|
71
71
|
// Reconciling them is recorded debt, and the same test pins the two places they actively disagree, because a
|
|
72
72
|
// consolidation has to PICK rather than discover them: a `for`'s init (basecse reads it at loop
|
package/src/raise/magicdiv.ts
CHANGED
|
@@ -56,7 +56,7 @@ function magicS(d: number): { M: number; s: number } {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Forward UNSIGNED magic generator (Hacker's Delight §10-8). Returns the multiplier `M`, shift `s`,
|
|
59
|
-
* and the `add` indicator (true ⇒ the "add-correction"
|
|
59
|
+
* and the `add` indicator (true ⇒ the "add-correction" form that needs an extra `+x` term —
|
|
60
60
|
* matched by matchUnsignedAddCorrection, not the simple `mulhu>>s` shape). */
|
|
61
61
|
function magicU(d: number): { M: number; s: number; add: boolean } {
|
|
62
62
|
const u = (x: number) => x >>> 0;
|
|
@@ -312,7 +312,7 @@ function matchSignedMagic(ctx: Ctx, mul: Op): Match | null {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/** Match the SIMPLE unsigned magic-division DAG `shr_u(mulhu(x, M), s)` (no correction) and
|
|
315
|
-
* reconstruct+verify the divisor. The add-correction
|
|
315
|
+
* reconstruct+verify the divisor. The add-correction form (`t + ((x−t)>>1)`) is not matched
|
|
316
316
|
* here — its `mulhu` result feeds a `sub`, not a direct `shr_u`, so this matcher naturally declines it. */
|
|
317
317
|
function matchUnsignedSimpleMagic(ctx: Ctx, mul: Op): Match | null {
|
|
318
318
|
const bound = bindMulOperands(ctx, mul);
|
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
// produces no site here. `synthetic:basefold` is that control — its reference IS the cast spelling
|
|
33
33
|
// — and it MATCHES today, untouched.
|
|
34
34
|
//
|
|
35
|
-
// A DEFAULT, NOT A RANKED
|
|
35
|
+
// A DEFAULT, NOT A RANKED VARIATION, and the reason is a measurement rather than a preference. A variation
|
|
36
36
|
// exists where two source spellings collapse onto one asm, so nothing but the differ can separate
|
|
37
37
|
// them. Here the asm separates them itself — line one folds `+K` into the memory operand and
|
|
38
38
|
// produces NO site — so `add(P, K)` feeding an off-0 scaled access is evidence AGAINST the spelling
|
|
39
|
-
// asmlift emits today, not a coin flip. And
|
|
39
|
+
// asmlift emits today, not a coin flip. And a variation at this level is a LIFT variation (`/setup-args`'s
|
|
40
40
|
// position in rank.ts), doubling the whole candidate product for every function with a site: swept
|
|
41
41
|
// over 2288 sa3 and 412 klonoa functions in both symbol-map configurations, the gates below admit
|
|
42
42
|
// two bases, in one function, which declines for an unrelated reason — so the price would be paid
|
|
43
43
|
// to referee a question the corpus never poses. What is NOT settled is line two, and that is a
|
|
44
|
-
// different question: where a base LOCAL goes is the L3 base-local
|
|
44
|
+
// different question: where a base LOCAL goes is the L3 base-local variations' business, taken over
|
|
45
45
|
// whatever type the recovery mints, not a second answer to the TYPE this pass decides.
|
|
46
46
|
//
|
|
47
47
|
// WHY THE TRAILING COUNT IS A GATE RATHER THAN A GUESS, and it is the struct's SIZE that makes it
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
// only once the object is big enough. So a count is not a fact to derive but a free parameter with
|
|
53
53
|
// a byte-observable LOWER BOUND, and inventing one invents a size. The count is therefore read off
|
|
54
54
|
// the loop that walks the member — `boundedCount` below — and the whole base DECLINES when no
|
|
55
|
-
// counted loop states one. An INTERIOR member's count is not
|
|
55
|
+
// counted loop states one. An INTERIOR member's count is not an open question at all: it is forced by the
|
|
56
56
|
// member that follows it.
|
|
57
57
|
//
|
|
58
58
|
// THE SIBLING PASSES, and why this is a third one rather than a case inside either.
|
package/src/raise/narrowlocal.ts
CHANGED
|
@@ -364,8 +364,24 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
|
|
|
364
364
|
* is its jump has none.
|
|
365
365
|
*
|
|
366
366
|
* Over-refusal here is free by construction: it returns the carrier to the wide-local-plus-cast
|
|
367
|
-
* spelling this pass emits without the clause.
|
|
368
|
-
|
|
367
|
+
* spelling this pass emits without the clause.
|
|
368
|
+
*
|
|
369
|
+
* A SECOND READER, AND WHY THE PREDICATE IS SHARED RATHER THAN RE-DERIVED. `raise/retsink.ts`'s
|
|
370
|
+
* `arms-are-one-set` asks this same question of the same optimizer for a different purpose: a
|
|
371
|
+
* merge-variable select whose arms this guard would have collapsed never comes back as a diamond,
|
|
372
|
+
* so a TARGET holding one was written with early returns. Compiled both ways with agbcc -O2
|
|
373
|
+
* -mthumb and committed (`test/corpus/agbcc-select-{merge,early}.s`), the three bullets above hold
|
|
374
|
+
* term for term in that direction too: `selcomp` (`v = a + b`, one SET) loses its diamond in the
|
|
375
|
+
* merge spelling and keeps it in the early one; `selload` (`v = *p`) keeps a diamond in BOTH,
|
|
376
|
+
* carrying no information; `selcomp3` (three ops) likewise. ONE DIFFERENCE IS WORTH NAMING: the
|
|
377
|
+
* constant bullet's "refusing the foldable case costs nothing" is an argument about THIS pass's
|
|
378
|
+
* fallback, and it does not transfer — `selk3` (`v = a + 3`) is one `adds`, so `jump.c`'s own guard
|
|
379
|
+
* counts one SET where this predicate counts two, and retsink's refusal of it costs a candidate
|
|
380
|
+
* there rather than nothing. That divergence is argued from the optimizer rather than compiled:
|
|
381
|
+
* `selk3` is deliberately outside the committed pair. It still never costs an ANSWER (every
|
|
382
|
+
* clause in that table is `sound: false`), so the shared conservative predicate is the right one
|
|
383
|
+
* until a row asks for the cost model neither pass has. */
|
|
384
|
+
export function armIsOneSet(b: Block): boolean {
|
|
369
385
|
return (
|
|
370
386
|
!b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
|
|
371
387
|
);
|
package/src/raise/paramwidth.ts
CHANGED
|
@@ -43,6 +43,15 @@
|
|
|
43
43
|
// What that refusal protects is a function this pass never compiles: agbcc truncates at every
|
|
44
44
|
// PROTOTYPED CALL SITE of a narrow-declared callee — `lsl/asr` ahead of the `bl`, two Thumb
|
|
45
45
|
// instructions per site — so a wrong width here costs bytes the per-function differ cannot see.
|
|
46
|
+
//
|
|
47
|
+
// FUSED BEHIND A POOL LOAD. The prologue scan steps over a pool-loaded address, and a body cast
|
|
48
|
+
// with nothing else ahead of it — `gB = gW[(u8)a]` is `ldr r2,=gB / ldr r1,=gW / lsl r0,#24 /
|
|
49
|
+
// lsr r0,#22` — reaches it looking like a declaration once raise/extscale.ts has re-split the fused
|
|
50
|
+
// pair. The fold knows where the machine put that `lsl` and records it (`ScaleRecord.behindPool`),
|
|
51
|
+
// and `fused-behind-pool` reads the record: an unsigned declared parameter's `lsl` precedes every
|
|
52
|
+
// pool load in the benchmark's agbcc references (extscale.ts's header has the census). Only the
|
|
53
|
+
// fold's own extensions are judged this way; a plain cast's extension sits at its right shift,
|
|
54
|
+
// where its position says nothing about its `lsl`'s.
|
|
46
55
|
import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
|
|
47
56
|
import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
|
|
48
57
|
import { T } from '../ir/types';
|
|
@@ -63,6 +72,9 @@ export interface NarrowParamCandidate {
|
|
|
63
72
|
uses: number;
|
|
64
73
|
/** the width the caller's own prototype declares for this parameter, if it declares one */
|
|
65
74
|
declared: number | undefined;
|
|
75
|
+
/** the extension is one raise/extscale.ts re-split from a fused pair whose `shl` the machine ran
|
|
76
|
+
* behind a pool load — see FUSED BEHIND A POOL LOAD */
|
|
77
|
+
fusedBehindPool: boolean;
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
|
|
@@ -108,6 +120,13 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
|
|
|
108
120
|
guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
|
|
109
121
|
rejects: (c) => !c.inPrologue,
|
|
110
122
|
},
|
|
123
|
+
{
|
|
124
|
+
id: 'fused-behind-pool',
|
|
125
|
+
why: 'a fused cast behind a pool load is body code if unsigned, and either width is the same object if signed',
|
|
126
|
+
sound: true,
|
|
127
|
+
guardedBy: 'extscale.test.ts: a body cast behind nothing but a pool load keeps its parameter wide',
|
|
128
|
+
rejects: (c) => c.fusedBehindPool,
|
|
129
|
+
},
|
|
111
130
|
];
|
|
112
131
|
|
|
113
132
|
/** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
|
|
@@ -125,12 +144,14 @@ function useCount(fn: Fn, v: Value): number {
|
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
/** Type an entry parameter at the width its prologue extension proves, and drop the extension.
|
|
128
|
-
* `self` is the prototype the caller supplied for THIS function, if any
|
|
129
|
-
*
|
|
147
|
+
* `self` is the prototype the caller supplied for THIS function, if any; `fusedBehindPool` is
|
|
148
|
+
* raise/extscale.ts's record of the extensions it re-split behind a pool load
|
|
149
|
+
* (`ScaleRecord.behindPool`). Returns the number of parameters narrowed. */
|
|
130
150
|
export function narrowEntryParams(
|
|
131
151
|
fn: Fn,
|
|
132
152
|
self?: FnProto,
|
|
133
153
|
gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
|
|
154
|
+
fusedBehindPool: ReadonlySet<Op> = new Set(),
|
|
134
155
|
): number {
|
|
135
156
|
const entry = fn.blocks[0];
|
|
136
157
|
const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
|
|
@@ -166,6 +187,7 @@ export function narrowEntryParams(
|
|
|
166
187
|
inPrologue: prologue.has(op),
|
|
167
188
|
uses: useCount(fn, p),
|
|
168
189
|
declared: declared[entry.params.indexOf(p)],
|
|
190
|
+
fusedBehindPool: fusedBehindPool.has(op),
|
|
169
191
|
};
|
|
170
192
|
if (firstRejection(gates, c) !== null) {
|
|
171
193
|
continue;
|
|
@@ -18,12 +18,21 @@ import type { TargetDescription } from '../target';
|
|
|
18
18
|
import { recognizeArrays } from './arrays';
|
|
19
19
|
import { recognizeConsts } from './const';
|
|
20
20
|
import { recognizeDivPow2 } from './divpow2';
|
|
21
|
+
import {
|
|
22
|
+
type PoolOrder,
|
|
23
|
+
type ScaleRecord,
|
|
24
|
+
emptyScaleRecord,
|
|
25
|
+
foldScaledExtensions,
|
|
26
|
+
foldsShiftPairCasts,
|
|
27
|
+
poolOrderOf,
|
|
28
|
+
restoreUnclaimedScales,
|
|
29
|
+
} from './extscale';
|
|
21
30
|
import { numberPureValues } from './gvn';
|
|
22
31
|
import { recognizeMagicDivision } from './magicdiv';
|
|
23
32
|
import { recognizeMemberArrays } from './memberarrays';
|
|
24
33
|
import { rerootNarrowReads } from './narrow';
|
|
25
34
|
import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
|
|
26
|
-
import { narrowEntryParams } from './paramwidth';
|
|
35
|
+
import { PARAM_WIDTH_GATES, narrowEntryParams } from './paramwidth';
|
|
27
36
|
import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
|
|
28
37
|
import { recognizeSoftDiv } from './softdiv';
|
|
29
38
|
import { recognizeStructArrays } from './struct-arrays';
|
|
@@ -34,7 +43,7 @@ import { recognizeStructs } from './structs';
|
|
|
34
43
|
* A field here selects between spellings its pass can already produce; none of them relaxes a
|
|
35
44
|
* soundness refusal, and each recognizer documents its own. */
|
|
36
45
|
export interface PreRecoveryOptions {
|
|
37
|
-
/** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree
|
|
46
|
+
/** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree question. */
|
|
38
47
|
shortCircuit?: BranchShortCircuitOptions;
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -44,9 +53,18 @@ export interface PreRecoveryOptions {
|
|
|
44
53
|
* by then divpow2 has deleted a block, both short-circuit folds have rewritten edges, and the
|
|
45
54
|
* eight `dce: true` passes have changed op counts. So the driver reads it here and hands it down. */
|
|
46
55
|
export interface PreRecoveryFacts {
|
|
47
|
-
/** the join shape of every block, for raise/narrowlocal.ts's `edge-extends
|
|
48
|
-
*
|
|
56
|
+
/** the join shape of every block, for raise/narrowlocal.ts's `edge-extends` and, downstream of
|
|
57
|
+
* pre-recovery entirely, raise/retsink.ts's `pre-diamond`. Blocks a later pass creates are
|
|
58
|
+
* absent, and absent reads as "no diamond" — the refusing direction, in both readers. */
|
|
49
59
|
mergeShapes: Map<Block, MergeShape>;
|
|
60
|
+
/** which entry-block ops the machine ran after its first pool-loaded address, for
|
|
61
|
+
* raise/extscale.ts's behind-a-pool-load record. Read HERE because `addrnum`, the first pass,
|
|
62
|
+
* hoists duplicated addresses to the head of the entry block and that order is gone after it. */
|
|
63
|
+
poolOrder: PoolOrder;
|
|
64
|
+
/** THE ONE FACT A PASS WRITES rather than the lift: what raise/extscale.ts's fold made, for the
|
|
65
|
+
* two passes after it that read it — paramwidth's `fused-behind-pool` gate and
|
|
66
|
+
* `extscale-restore`. Empty until the fold runs. */
|
|
67
|
+
scales: ScaleRecord;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
export interface PreRecoveryPass {
|
|
@@ -55,7 +73,9 @@ export interface PreRecoveryPass {
|
|
|
55
73
|
/** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR.
|
|
56
74
|
* `self` is the prototype the caller supplied for the function being raised — read only by
|
|
57
75
|
* parameter-width, which checks its inference against a declared width. `lifted` is the
|
|
58
|
-
* pre-pass
|
|
76
|
+
* pre-pass snapshot — read by narrow-local (its CFG) and scaled-extension (its entry order) —
|
|
77
|
+
* plus the fold's record, which scaled-extension writes and parameter-width and
|
|
78
|
+
* scaled-extension-restore read. */
|
|
59
79
|
run: (
|
|
60
80
|
fn: Fn,
|
|
61
81
|
self: FnProto | undefined,
|
|
@@ -70,9 +90,10 @@ export interface PreRecoveryPass {
|
|
|
70
90
|
}
|
|
71
91
|
|
|
72
92
|
/** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
|
|
73
|
-
* address-numbering → const-materialize → magic-division → pow2-division → soft-division →
|
|
74
|
-
*
|
|
75
|
-
* narrow-local → parameter-width
|
|
93
|
+
* address-numbering → const-materialize → magic-division → pow2-division → soft-division →
|
|
94
|
+
* scaled-extension → array-legalize → struct-array → member-array → struct-pointer → short-circuit →
|
|
95
|
+
* branch-short-circuit → narrow-reads → narrow-local → parameter-width → scaled-extension-restore.
|
|
96
|
+
* See each recognizer's file for the rationale. */
|
|
76
97
|
export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
77
98
|
// FIRST: collapsing duplicate address definitions removes block params every later recognizer
|
|
78
99
|
// would otherwise have to reason around, and it can only shrink the value graph.
|
|
@@ -81,10 +102,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
81
102
|
// Numbering alone is not enough and not safe to ship alone: collapsing the duplicates leaves a
|
|
82
103
|
// block param whose edges now all carry one value, and the structurer still destroys THAT into
|
|
83
104
|
// a local (it only reuses a name a carrier already has, and an inlined `gaddr` has none).
|
|
84
|
-
// Numbering alone
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
105
|
+
// Numbering alone cost kleod:UpdateHUDCounterDisplay its match (measured on kleod's
|
|
106
|
+
// kl-eod-decomp rows, retired 2026-09-13), so the pair is the atomic unit, expressed as a body
|
|
107
|
+
// rather than a sum of two unrelated counts. It was NOT monotone, which is worth knowing before
|
|
108
|
+
// tuning either half: dropping the cleanup IMPROVED kleod:ConfigureEntityBehavior and
|
|
109
|
+
// kleod:CountCollectedGems, neither of them near matching then.
|
|
88
110
|
run: (fn) => {
|
|
89
111
|
const n = numberPureValues(fn);
|
|
90
112
|
return n + simplifyTrivialPhis(fn);
|
|
@@ -100,6 +122,17 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
100
122
|
// beside magicdiv so that a reader looking for division recovery finds both together.
|
|
101
123
|
{ id: 'divpow2', run: recognizeDivPow2, dce: true },
|
|
102
124
|
{ id: 'softdiv', run: (fn) => recognizeSoftDiv(fn), dce: false, gate: (t) => !t.capabilities.hwDivide },
|
|
125
|
+
// AFTER `const`, which folds a shift pair over a constant to the constant it computes, and BEFORE
|
|
126
|
+
// the three array recognizers, whose input this pass produces: `shl(ext(x), k)` is an element
|
|
127
|
+
// scale they legalize and the fused pair is not. `dce: true` — the `shl` a fold leaves readerless.
|
|
128
|
+
// It reads `lifted.poolOrder`, the entry block's order before `addrnum` hoisted its addresses,
|
|
129
|
+
// and writes `lifted.scales`.
|
|
130
|
+
{
|
|
131
|
+
id: 'extscale',
|
|
132
|
+
run: (fn, _self, _opts, _target, lifted) => foldScaledExtensions(fn, lifted.poolOrder, lifted.scales),
|
|
133
|
+
dce: true,
|
|
134
|
+
gate: foldsShiftPairCasts,
|
|
135
|
+
},
|
|
103
136
|
{ id: 'arrays', run: recognizeArrays, dce: true },
|
|
104
137
|
// struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
|
|
105
138
|
// discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
|
|
@@ -128,28 +161,35 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
128
161
|
// counts 0 fused heads across the 782 rows that lift map-lessly, so the hazard has no producer
|
|
129
162
|
// there. The reverse direction never could: the value form replaces its head's `cond_br` with a
|
|
130
163
|
// `br`, which this pass never matches.
|
|
164
|
+
// The `target` argument is read by ONE conjunct of ONE gate, as at `narrowlocal` below:
|
|
165
|
+
// `read-behind-effect`'s "a local costs a second load", compiled on all five bench toolchains and
|
|
166
|
+
// true on agbcc alone (raise/shortcircuit.ts, target.ts `reloadsLocalReread`).
|
|
131
167
|
{
|
|
132
168
|
id: 'branch-shortcircuit',
|
|
133
|
-
run: (fn, _self, opts) =>
|
|
169
|
+
run: (fn, _self, opts, target) =>
|
|
170
|
+
recognizeBranchShortCircuit(fn, {
|
|
171
|
+
...opts.shortCircuit,
|
|
172
|
+
reloadsLocalReread: target.compilerBehaviors.reloadsLocalReread,
|
|
173
|
+
}),
|
|
134
174
|
dce: true,
|
|
135
175
|
},
|
|
136
176
|
// LAST, and the position IS load-bearing: this pass reads the CFG's edge arguments to find a
|
|
137
177
|
// loop variable's next value, and both short-circuit folds above rewrite the very edges it reads.
|
|
138
178
|
// `dce: false` — the rewrite orphans nothing, since the operand it drops keeps its other use.
|
|
139
179
|
{ id: 'narrow', run: rerootNarrowReads, dce: false },
|
|
140
|
-
// The two WIDTH passes, last and in either order relative to each other: each
|
|
141
|
-
// extension and retypes the parameter that fed it, so every recognizer above sees
|
|
142
|
-
// was written against and neither can match a shape the other creates. They are
|
|
143
|
-
// construction — `narrowlocal` refuses an entry parameter, `paramwidth` reads only
|
|
144
|
-
// parameters — and `narrowlocal` cannot take an extension `narrow` above wants either, since
|
|
145
|
-
// parameter carrying BOTH a `zext` and a `sext` has two readers and is refused.
|
|
180
|
+
// The two WIDTH passes, last of the recognizers and in either order relative to each other: each
|
|
181
|
+
// only DELETES an extension and retypes the parameter that fed it, so every recognizer above sees
|
|
182
|
+
// the shape it was written against and neither can match a shape the other creates. They are
|
|
183
|
+
// disjoint by construction — `narrowlocal` refuses an entry parameter, `paramwidth` reads only
|
|
184
|
+
// entry parameters — and `narrowlocal` cannot take an extension `narrow` above wants either, since
|
|
185
|
+
// a parameter carrying BOTH a `zext` and a `sext` has two readers and is refused.
|
|
146
186
|
// `dce: false` on both — the extension each drops is spliced out in place, and its result has no
|
|
147
187
|
// other reader.
|
|
148
188
|
// The `target` argument is read by ONE conjunct of ONE gate — see raise/narrowlocal.ts's
|
|
149
189
|
// `NarrowLocalOptions`. A whole-pass `gate` would be wrong: the pass's SOUND rules are claims
|
|
150
190
|
// about C and run everywhere; only the join-shape evidence is a claim about gcc 2.x's optimizer.
|
|
151
|
-
// That same conjunct is why
|
|
152
|
-
//
|
|
191
|
+
// That same conjunct is why it reads `lifted.mergeShapes`: every pass above it can rewrite the
|
|
192
|
+
// CFG whose shape it judges.
|
|
153
193
|
{
|
|
154
194
|
id: 'narrowlocal',
|
|
155
195
|
run: (fn, _self, _opts, target, lifted) =>
|
|
@@ -161,20 +201,44 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
161
201
|
),
|
|
162
202
|
dce: false,
|
|
163
203
|
},
|
|
164
|
-
{
|
|
204
|
+
{
|
|
205
|
+
id: 'paramwidth',
|
|
206
|
+
run: (fn, self, _opts, _target, lifted) => narrowEntryParams(fn, self, PARAM_WIDTH_GATES, lifted.scales.behindPool),
|
|
207
|
+
dce: false,
|
|
208
|
+
},
|
|
209
|
+
// LAST, after every pass that can CLAIM what `extscale` exposed — the two width passes above take
|
|
210
|
+
// an extension, the array recognizers a scale. What none of them took goes back to the pair the
|
|
211
|
+
// frontend lifted (raise/extscale.ts, WHAT NOBODY CLAIMED). `dce: true` — the extension it leaves
|
|
212
|
+
// readerless.
|
|
213
|
+
{
|
|
214
|
+
id: 'extscale-restore',
|
|
215
|
+
run: (fn, _self, _opts, _target, lifted) => restoreUnclaimedScales(fn, lifted.scales),
|
|
216
|
+
dce: true,
|
|
217
|
+
gate: foldsShiftPairCasts,
|
|
218
|
+
},
|
|
165
219
|
];
|
|
166
220
|
|
|
167
221
|
/** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
|
|
168
222
|
* `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
|
|
169
|
-
* hook), if given.
|
|
223
|
+
* hook), if given.
|
|
224
|
+
*
|
|
225
|
+
* RETURNS the facts it read, because a pass AFTER pre-recovery needs one of them too:
|
|
226
|
+
* `raise/retsink.ts`'s `pre-diamond` asks whether a return merge was a diamond in the ROM, and
|
|
227
|
+
* by its turn `raise/shortcircuit.ts` has manufactured diamonds that were not. The map is the
|
|
228
|
+
* driver's to compute — it is the only place that sees `fn` before the first pass — so handing it
|
|
229
|
+
* back is cheaper and truer than recomputing something later that cannot be recomputed. */
|
|
170
230
|
export function runPreRecovery(
|
|
171
231
|
fn: Fn,
|
|
172
232
|
target: TargetDescription,
|
|
173
233
|
afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
|
|
174
234
|
self?: FnProto,
|
|
175
235
|
opts: PreRecoveryOptions = {},
|
|
176
|
-
):
|
|
177
|
-
const lifted: PreRecoveryFacts = {
|
|
236
|
+
): PreRecoveryFacts {
|
|
237
|
+
const lifted: PreRecoveryFacts = {
|
|
238
|
+
mergeShapes: mergeShapes(fn),
|
|
239
|
+
poolOrder: poolOrderOf(fn),
|
|
240
|
+
scales: emptyScaleRecord(),
|
|
241
|
+
};
|
|
178
242
|
for (const pass of PRE_RECOVERY_PASSES) {
|
|
179
243
|
if (pass.gate && !pass.gate(target)) {
|
|
180
244
|
continue;
|
|
@@ -187,4 +251,5 @@ export function runPreRecovery(
|
|
|
187
251
|
afterPass?.(pass, result);
|
|
188
252
|
}
|
|
189
253
|
}
|
|
254
|
+
return lifted;
|
|
190
255
|
}
|