@asmlift/core 0.5.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// A NARROW DECLARED PARAMETER, extended once in the prologue.
|
|
2
|
+
//
|
|
3
|
+
// agbcc has no byte/half register move, so a callee whose parameter is declared `u8`/`s16` widens
|
|
4
|
+
// it itself, with a shift pair at the very top of the function — the caller passes a full register
|
|
5
|
+
// and the extension is part of the prologue. Both spellings below compiled with this benchmark's
|
|
6
|
+
// own agbcc, and where each one puts the pair is the whole rule:
|
|
7
|
+
//
|
|
8
|
+
// void pa(s32 *out, u8 a) { out[0]=1; out[1]=2; out[2]=a; } lsl/lsr mov str mov str str
|
|
9
|
+
// void pb(s32 *out, s32 a) { out[0]=1; out[1]=2; out[2]=(u8)a; } mov str mov str lsl/lsr str
|
|
10
|
+
//
|
|
11
|
+
// The idiom patterns fold that pair to one `zext`/`sext` op, so `pa`'s parameter reaches this pass
|
|
12
|
+
// as a value whose SOLE use is its own extension — nothing can read the raw register, which is
|
|
13
|
+
// exactly what a narrow declaration means. Recovered as a wide parameter instead, the same function
|
|
14
|
+
// has to re-spell `(u8)a` at every use; agbcc then elides an extension no use needs, gives the
|
|
15
|
+
// extended value no register of its own, and the whole allocation moves.
|
|
16
|
+
//
|
|
17
|
+
// WIDTH AND SIGNEDNESS ARE READ OFF, NOT GUESSED: the extension states both (agbcc's shift pair by
|
|
18
|
+
// its amount and its `asr`/`lsr`, PPC's by the opcode), so no width is ever enumerated here.
|
|
19
|
+
//
|
|
20
|
+
// NOT agbcc-GATED, because the shape is not agbcc's alone: mwcc's PPC prologue widens a declared
|
|
21
|
+
// narrow parameter with the `extsb`/`extsh` the frontend lifts to the same op, and the synthetic
|
|
22
|
+
// `sextb`/`tos8` rows keep matching on that toolchain through this pass.
|
|
23
|
+
//
|
|
24
|
+
// WHAT THE PROLOGUE TEST CANNOT SEE, and why the declaration settles it. The scan steps over the
|
|
25
|
+
// pure materializations agbcc interleaves among the extensions, so a constant the scheduler HOISTED
|
|
26
|
+
// above a mid-body cast leaves `pb` looking like `pa`:
|
|
27
|
+
//
|
|
28
|
+
// void pc(s32 a, s32 *out) { s32 t = 7; out[0] = (u8)a; out[1] = t; out[2] = t; }
|
|
29
|
+
// movs r2,#7 / lsls r0,#24 / lsrs r0,#24 / str / str / str
|
|
30
|
+
// void pc(u8 a, s32 *out) { s32 t = 7; out[0] = a; out[1] = t; out[2] = t; }
|
|
31
|
+
// lsls r0,#24 / lsrs r0,#24 / movs r2,#7 / str / str / str
|
|
32
|
+
//
|
|
33
|
+
// Those two ROM sources are DIFFERENT BYTES — the const moves across the shift pair — so the width
|
|
34
|
+
// is a fact here and not a spelling, while no reader of the raw register and no body code is
|
|
35
|
+
// present to make the gates below refuse. Nor does the ORDER decide it: sa3's `sub_802DFC8` really
|
|
36
|
+
// is declared `s16 direction` and agbcc emits its `movs r5, #0` before the `lsl/asr` too, so the
|
|
37
|
+
// hoisted-const shape arrives from both source spellings and reaches this pass as the same IR.
|
|
38
|
+
//
|
|
39
|
+
// The tiebreak is therefore not in the asm, and the SCORE cannot supply it either: asmlift
|
|
40
|
+
// re-materializes a small constant at each use instead of binding it to a local, so its own two
|
|
41
|
+
// spellings of `pc` emit the same instruction order and score alike. `proto-width` takes the
|
|
42
|
+
// tiebreak from the caller's declaration instead, and where none was supplied the extension stands.
|
|
43
|
+
// What that refusal protects is a function this pass never compiles: agbcc truncates at every
|
|
44
|
+
// PROTOTYPED CALL SITE of a narrow-declared callee — `lsl/asr` ahead of the `bl`, two Thumb
|
|
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.
|
|
55
|
+
import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
|
|
56
|
+
import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
|
|
57
|
+
import { T } from '../ir/types';
|
|
58
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
59
|
+
import { type FnProto, declaredWidth } from '../proto';
|
|
60
|
+
|
|
61
|
+
/** What the gates below judge: one entry parameter and the extension that reads it. */
|
|
62
|
+
export interface NarrowParamCandidate {
|
|
63
|
+
/** the parameter the extension reads */
|
|
64
|
+
param: Value;
|
|
65
|
+
/** the extension's `width` attribute */
|
|
66
|
+
width: number;
|
|
67
|
+
/** the entry block has predecessors */
|
|
68
|
+
entryIsJoin: boolean;
|
|
69
|
+
/** the extension is in the entry block's PROLOGUE — see the scan in `narrowEntryParams` */
|
|
70
|
+
inPrologue: boolean;
|
|
71
|
+
/** reads of the RAW parameter anywhere in the function */
|
|
72
|
+
uses: number;
|
|
73
|
+
/** the width the caller's own prototype declares for this parameter, if it declares one */
|
|
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;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
|
|
81
|
+
{
|
|
82
|
+
id: 'entry-is-join',
|
|
83
|
+
why: "a joined entry's params are merge values, not the function's arguments",
|
|
84
|
+
sound: true,
|
|
85
|
+
guardedBy: 'param-width.test.ts: an entry block with a predecessor carries merge values, not arguments',
|
|
86
|
+
rejects: (c) => c.entryIsJoin,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'param-typed',
|
|
90
|
+
why: 'the pointer/aggregate recovery already decided this parameter',
|
|
91
|
+
sound: true,
|
|
92
|
+
guardedBy: 'param-width.test.ts: a parameter the pointer recovery already typed is left alone',
|
|
93
|
+
rejects: (c) => c.param.type.kind !== 'unknown',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'cast-width',
|
|
97
|
+
why: 'only 8 and 16 are widths a `zext`/`sext` — and so a C declaration — carries',
|
|
98
|
+
sound: true,
|
|
99
|
+
guardedBy: 'param-width.test.ts: a width no C type spells is refused',
|
|
100
|
+
rejects: (c) => !CAST_WIDTHS.has(c.width),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'raw-reader',
|
|
104
|
+
why: 'a reader of the un-extended register proves the declaration was wide',
|
|
105
|
+
sound: true,
|
|
106
|
+
guardedBy: 'param-width.test.ts: a second reader of the raw parameter proves the declaration was wide',
|
|
107
|
+
rejects: (c) => c.uses !== 1,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'proto-width',
|
|
111
|
+
why: "the caller's headers declare this parameter, and a declaration outranks an inference",
|
|
112
|
+
sound: true,
|
|
113
|
+
guardedBy: 'param-width.test.ts: a declared width the extension contradicts refuses the narrowing',
|
|
114
|
+
rejects: (c) => c.declared !== undefined && c.declared !== c.width,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'not-prologue',
|
|
118
|
+
why: 'an extension behind body code is where the SOURCE wrote the cast',
|
|
119
|
+
sound: true,
|
|
120
|
+
guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
|
|
121
|
+
rejects: (c) => !c.inPrologue,
|
|
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
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
|
|
133
|
+
function useCount(fn: Fn, v: Value): number {
|
|
134
|
+
let n = 0;
|
|
135
|
+
for (const b of fn.blocks) {
|
|
136
|
+
for (const op of b.ops) {
|
|
137
|
+
n += op.operands.filter((o) => o === v).length;
|
|
138
|
+
for (const s of op.successors) {
|
|
139
|
+
n += s.args.filter((a) => a === v).length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Type an entry parameter at the width its prologue extension proves, and drop the extension.
|
|
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. */
|
|
150
|
+
export function narrowEntryParams(
|
|
151
|
+
fn: Fn,
|
|
152
|
+
self?: FnProto,
|
|
153
|
+
gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
|
|
154
|
+
fusedBehindPool: ReadonlySet<Op> = new Set(),
|
|
155
|
+
): number {
|
|
156
|
+
const entry = fn.blocks[0];
|
|
157
|
+
const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
|
|
158
|
+
const entryIsJoin = fn.blocks.some((b) => successorsOf(b).includes(entry));
|
|
159
|
+
const params = new Set(entry.params);
|
|
160
|
+
// The prologue: the entry block's leading parameter extensions, plus the `MATERIALIZING_OPS`
|
|
161
|
+
// agbcc interleaves among them. Scanning stops at the first op that READS a value — body code
|
|
162
|
+
// has run by then, and an extension behind body code is where the SOURCE wrote it.
|
|
163
|
+
const prologue = new Set<Op>();
|
|
164
|
+
for (const op of entry.ops) {
|
|
165
|
+
if (MATERIALIZING_OPS.has(op.opcode)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if ((op.opcode !== 'sext' && op.opcode !== 'zext') || !params.has(op.operands[0])) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
prologue.add(op);
|
|
172
|
+
}
|
|
173
|
+
let narrowed = 0;
|
|
174
|
+
for (const op of [...entry.ops]) {
|
|
175
|
+
if (op.opcode !== 'sext' && op.opcode !== 'zext') {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const p = op.operands[0];
|
|
179
|
+
if (!params.has(p)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const width = op.attrs.width as number;
|
|
183
|
+
const c: NarrowParamCandidate = {
|
|
184
|
+
param: p,
|
|
185
|
+
width,
|
|
186
|
+
entryIsJoin,
|
|
187
|
+
inPrologue: prologue.has(op),
|
|
188
|
+
uses: useCount(fn, p),
|
|
189
|
+
declared: declared[entry.params.indexOf(p)],
|
|
190
|
+
fusedBehindPool: fusedBehindPool.has(op),
|
|
191
|
+
};
|
|
192
|
+
if (firstRejection(gates, c) !== null) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
p.type = T.int(width, op.opcode === 'sext');
|
|
196
|
+
replaceAllUsesWith(fn, op.results[0], p);
|
|
197
|
+
entry.ops.splice(entry.ops.indexOf(op), 1);
|
|
198
|
+
narrowed++;
|
|
199
|
+
}
|
|
200
|
+
return narrowed;
|
|
201
|
+
}
|
|
@@ -10,25 +10,79 @@
|
|
|
10
10
|
// rank's score-probe both verify; the report's trace entries ride pipeline's hook). The `dce` after a
|
|
11
11
|
// pass that changed the IR is INTRINSIC to the pass (it declares whether it leaves dead ops) and lives
|
|
12
12
|
// in the driver.
|
|
13
|
-
import { Fn } from '../ir/core';
|
|
13
|
+
import { Block, Fn } from '../ir/core';
|
|
14
14
|
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
15
15
|
import { dce } from '../pattern/engine';
|
|
16
|
+
import type { FnProto } from '../proto';
|
|
16
17
|
import type { TargetDescription } from '../target';
|
|
17
18
|
import { recognizeArrays } from './arrays';
|
|
18
19
|
import { recognizeConsts } from './const';
|
|
19
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';
|
|
20
30
|
import { numberPureValues } from './gvn';
|
|
21
31
|
import { recognizeMagicDivision } from './magicdiv';
|
|
22
|
-
import {
|
|
32
|
+
import { recognizeMemberArrays } from './memberarrays';
|
|
33
|
+
import { rerootNarrowReads } from './narrow';
|
|
34
|
+
import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
|
|
35
|
+
import { PARAM_WIDTH_GATES, narrowEntryParams } from './paramwidth';
|
|
36
|
+
import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
|
|
23
37
|
import { recognizeSoftDiv } from './softdiv';
|
|
24
38
|
import { recognizeStructArrays } from './struct-arrays';
|
|
25
39
|
import { recognizeStructs } from './structs';
|
|
26
40
|
|
|
41
|
+
/** Per-call options for the pass list — ONE field per pass that takes any, named for the pass, so
|
|
42
|
+
* a caller reads which recognizer it is steering and a pass that takes none says so by absence.
|
|
43
|
+
* A field here selects between spellings its pass can already produce; none of them relaxes a
|
|
44
|
+
* soundness refusal, and each recognizer documents its own. */
|
|
45
|
+
export interface PreRecoveryOptions {
|
|
46
|
+
/** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree question. */
|
|
47
|
+
shortCircuit?: BranchShortCircuitOptions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** CFG facts read off the function ONCE, before the first pass below rewrites it.
|
|
51
|
+
*
|
|
52
|
+
* A pass that judges the SHAPE agbcc emitted cannot read that shape off `fn` when its turn comes:
|
|
53
|
+
* by then divpow2 has deleted a block, both short-circuit folds have rewritten edges, and the
|
|
54
|
+
* eight `dce: true` passes have changed op counts. So the driver reads it here and hands it down. */
|
|
55
|
+
export interface PreRecoveryFacts {
|
|
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. */
|
|
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;
|
|
68
|
+
}
|
|
69
|
+
|
|
27
70
|
export interface PreRecoveryPass {
|
|
28
71
|
/** stable id — also the report's trace-stage key. */
|
|
29
72
|
id: string;
|
|
30
|
-
/** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR.
|
|
31
|
-
|
|
73
|
+
/** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR.
|
|
74
|
+
* `self` is the prototype the caller supplied for the function being raised — read only by
|
|
75
|
+
* parameter-width, which checks its inference against a declared width. `lifted` is the
|
|
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. */
|
|
79
|
+
run: (
|
|
80
|
+
fn: Fn,
|
|
81
|
+
self: FnProto | undefined,
|
|
82
|
+
opts: PreRecoveryOptions,
|
|
83
|
+
target: TargetDescription,
|
|
84
|
+
lifted: PreRecoveryFacts,
|
|
85
|
+
) => number | boolean;
|
|
32
86
|
/** run `dce` after this pass changes the IR (the pass declares it leaves dead ops behind). */
|
|
33
87
|
dce: boolean;
|
|
34
88
|
/** optional target gate (soft-div only fires on a no-hardware-divide target — see raise/softdiv.ts). */
|
|
@@ -36,9 +90,10 @@ export interface PreRecoveryPass {
|
|
|
36
90
|
}
|
|
37
91
|
|
|
38
92
|
/** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
|
|
39
|
-
* address-numbering → const-materialize → magic-division → pow2-division → soft-division →
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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. */
|
|
42
97
|
export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
43
98
|
// FIRST: collapsing duplicate address definitions removes block params every later recognizer
|
|
44
99
|
// would otherwise have to reason around, and it can only shrink the value graph.
|
|
@@ -47,10 +102,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
47
102
|
// Numbering alone is not enough and not safe to ship alone: collapsing the duplicates leaves a
|
|
48
103
|
// block param whose edges now all carry one value, and the structurer still destroys THAT into
|
|
49
104
|
// a local (it only reuses a name a carrier already has, and an inlined `gaddr` has none).
|
|
50
|
-
// Numbering alone
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
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.
|
|
54
110
|
run: (fn) => {
|
|
55
111
|
const n = numberPureValues(fn);
|
|
56
112
|
return n + simplifyTrivialPhis(fn);
|
|
@@ -66,37 +122,128 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
66
122
|
// beside magicdiv so that a reader looking for division recovery finds both together.
|
|
67
123
|
{ id: 'divpow2', run: recognizeDivPow2, dce: true },
|
|
68
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
|
+
},
|
|
69
136
|
{ id: 'arrays', run: recognizeArrays, dce: true },
|
|
70
137
|
// struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
|
|
71
138
|
// discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
|
|
72
139
|
// accesses must not be re-derived as constant-offset struct-pointer accesses).
|
|
73
140
|
{ id: 'struct-arrays', run: recognizeStructArrays, dce: true },
|
|
141
|
+
// member-arrays AFTER struct-arrays, and that order IS load-bearing — but the order alone does
|
|
142
|
+
// not deconflict them, because the two passes claim different VALUES: struct-arrays types the
|
|
143
|
+
// materialized `add(P, K)` and member-arrays groups on `P`, so `P->tbl[i].f` reaches both. What
|
|
144
|
+
// deconflicts them is member-arrays' `claimed-access` gate, reading the marks struct-arrays
|
|
145
|
+
// leaves. Its position before `structs` is NOT load-bearing — a base carrying both of THOSE
|
|
146
|
+
// shapes is refused on either side (`direct-access` here, the `unknown` check there) — and it
|
|
147
|
+
// sits there so the three struct synthesizers read in the order of their evidence. `narrowlocal`
|
|
148
|
+
// reads the carrier's extension chain rather than any address, so nothing here orders against it.
|
|
149
|
+
{ id: 'member-arrays', run: (fn) => recognizeMemberArrays(fn), dce: true },
|
|
74
150
|
{ id: 'structs', run: recognizeStructs, dce: false },
|
|
75
151
|
{ id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
|
|
76
|
-
// The control-flow sibling,
|
|
152
|
+
// The control-flow sibling, value form FIRST.
|
|
77
153
|
//
|
|
78
154
|
// Their input SHAPES are disjoint (the value form's second block ends in `br` carrying a phi
|
|
79
|
-
// argument, this one's ends in `cond_br`), so neither can eat the other's literal pattern.
|
|
80
|
-
// this pass REWRITES its head's condition into a
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
155
|
+
// argument, this one's ends in `cond_br`), so neither can eat the other's literal pattern. The
|
|
156
|
+
// order is not otherwise load-bearing: this pass REWRITES its head's condition into a
|
|
157
|
+
// `logic_or`/`logic_and`, and the value form takes a fused head — both siblings negate a
|
|
158
|
+
// connective by De Morgan (`negateCondOps`, raise/shortcircuit.ts). Running this one first can
|
|
159
|
+
// still cost a value fold wherever that helper refuses the fused cone (a non-negatable leaf, or a
|
|
160
|
+
// cone over its budget), but instrumenting the value form's head gate over the benchmark corpus
|
|
161
|
+
// counts 0 fused heads across the 782 rows that lift map-lessly, so the hazard has no producer
|
|
162
|
+
// there. The reverse direction never could: the value form replaces its head's `cond_br` with a
|
|
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`).
|
|
167
|
+
{
|
|
168
|
+
id: 'branch-shortcircuit',
|
|
169
|
+
run: (fn, _self, opts, target) =>
|
|
170
|
+
recognizeBranchShortCircuit(fn, {
|
|
171
|
+
...opts.shortCircuit,
|
|
172
|
+
reloadsLocalReread: target.compilerBehaviors.reloadsLocalReread,
|
|
173
|
+
}),
|
|
174
|
+
dce: true,
|
|
175
|
+
},
|
|
176
|
+
// LAST, and the position IS load-bearing: this pass reads the CFG's edge arguments to find a
|
|
177
|
+
// loop variable's next value, and both short-circuit folds above rewrite the very edges it reads.
|
|
178
|
+
// `dce: false` — the rewrite orphans nothing, since the operand it drops keeps its other use.
|
|
179
|
+
{ id: 'narrow', run: rerootNarrowReads, dce: false },
|
|
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.
|
|
186
|
+
// `dce: false` on both — the extension each drops is spliced out in place, and its result has no
|
|
187
|
+
// other reader.
|
|
188
|
+
// The `target` argument is read by ONE conjunct of ONE gate — see raise/narrowlocal.ts's
|
|
189
|
+
// `NarrowLocalOptions`. A whole-pass `gate` would be wrong: the pass's SOUND rules are claims
|
|
190
|
+
// about C and run everywhere; only the join-shape evidence is a claim about gcc 2.x's optimizer.
|
|
191
|
+
// That same conjunct is why it reads `lifted.mergeShapes`: every pass above it can rewrite the
|
|
192
|
+
// CFG whose shape it judges.
|
|
193
|
+
{
|
|
194
|
+
id: 'narrowlocal',
|
|
195
|
+
run: (fn, _self, _opts, target, lifted) =>
|
|
196
|
+
narrowBlockLocals(
|
|
197
|
+
fn,
|
|
198
|
+
undefined,
|
|
199
|
+
{ hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm },
|
|
200
|
+
lifted.mergeShapes,
|
|
201
|
+
),
|
|
202
|
+
dce: false,
|
|
203
|
+
},
|
|
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
|
+
},
|
|
85
219
|
];
|
|
86
220
|
|
|
87
221
|
/** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
|
|
88
222
|
* `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
|
|
89
|
-
* 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. */
|
|
90
230
|
export function runPreRecovery(
|
|
91
231
|
fn: Fn,
|
|
92
232
|
target: TargetDescription,
|
|
93
233
|
afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
|
|
94
|
-
|
|
234
|
+
self?: FnProto,
|
|
235
|
+
opts: PreRecoveryOptions = {},
|
|
236
|
+
): PreRecoveryFacts {
|
|
237
|
+
const lifted: PreRecoveryFacts = {
|
|
238
|
+
mergeShapes: mergeShapes(fn),
|
|
239
|
+
poolOrder: poolOrderOf(fn),
|
|
240
|
+
scales: emptyScaleRecord(),
|
|
241
|
+
};
|
|
95
242
|
for (const pass of PRE_RECOVERY_PASSES) {
|
|
96
243
|
if (pass.gate && !pass.gate(target)) {
|
|
97
244
|
continue;
|
|
98
245
|
}
|
|
99
|
-
const result = pass.run(fn);
|
|
246
|
+
const result = pass.run(fn, self, opts, target, lifted);
|
|
100
247
|
if (result) {
|
|
101
248
|
if (pass.dce) {
|
|
102
249
|
dce(fn);
|
|
@@ -104,4 +251,5 @@ export function runPreRecovery(
|
|
|
104
251
|
afterPass?.(pass, result);
|
|
105
252
|
}
|
|
106
253
|
}
|
|
254
|
+
return lifted;
|
|
107
255
|
}
|
package/src/raise/recover.ts
CHANGED
|
@@ -12,13 +12,30 @@ const UNSIGNED_CMP = new Set(['icmp_ult', 'icmp_ule', 'icmp_ugt', 'icmp_uge']);
|
|
|
12
12
|
const SIGNED_DIV = new Set(['sdiv', 'smod']);
|
|
13
13
|
const UNSIGNED_DIV = new Set(['udiv', 'umod']);
|
|
14
14
|
|
|
15
|
+
/** Type an as-yet-untyped value as an integer of its own width. Only `unknown`s: a value some
|
|
16
|
+
* earlier rule already typed keeps that answer, so the phases below compose without an order
|
|
17
|
+
* between the rules INSIDE one of them. */
|
|
18
|
+
function setInt(v: Value, signed: boolean): void {
|
|
19
|
+
if (v.type.kind === 'unknown') {
|
|
20
|
+
v.type = T.int(v.type.width, signed);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The four phases, in the one order they are sound in: seed signedness from the opcodes that carry
|
|
25
|
+
* it, type the bases of memory accesses as pointers, propagate that pointer-ness across the SSA,
|
|
26
|
+
* then default whatever is left. Each is in place over `fn`, and each depends on the previous one
|
|
27
|
+
* having already refused to overwrite a type — the pointer phases must run BEFORE the s32 default
|
|
28
|
+
* or a loop-carried pointer is flattened to an integer. */
|
|
15
29
|
export function recoverTypes(fn: Fn): void {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
seedSignednessFromOpcodes(fn);
|
|
31
|
+
typeDerefBases(fn);
|
|
32
|
+
propagatePointers(fn);
|
|
33
|
+
defaultUnknownsToS32(fn);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** PHASE 1 — signedness from op semantics: the operands of a signed comparison are signed integers,
|
|
37
|
+
* a comparison's result is a bool, and a division carries its signedness in the OPCODE. */
|
|
38
|
+
function seedSignednessFromOpcodes(fn: Fn): void {
|
|
22
39
|
for (const b of fn.blocks) {
|
|
23
40
|
for (const op of b.ops) {
|
|
24
41
|
if (SIGNED_CMP.has(op.opcode)) {
|
|
@@ -52,12 +69,14 @@ export function recoverTypes(fn: Fn): void {
|
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
|
-
|
|
56
|
-
// access width (and, for loads, signedness). This must run before the s32 default so the
|
|
57
|
-
// base is typed `T *` rather than being flattened to a plain integer. Both the constant-offset
|
|
58
|
-
// forms (load/store, width) and the variable-index forms (aload/astore, elemSize) type their
|
|
59
|
-
// base operand[0]; only the scale attribute differs.
|
|
72
|
+
}
|
|
60
73
|
|
|
74
|
+
/** PHASE 2 — a value used as the base of a memory access is a pointer; its pointee type comes from
|
|
75
|
+
* the access width (and, for loads, signedness). This must run before the s32 default so the base
|
|
76
|
+
* is typed `T *` rather than being flattened to a plain integer. Both the constant-offset forms
|
|
77
|
+
* (load/store, width) and the variable-index forms (aload/astore, elemSize) type their base
|
|
78
|
+
* operand[0]; only the scale attribute differs. */
|
|
79
|
+
function typeDerefBases(fn: Fn): void {
|
|
61
80
|
for (const b of fn.blocks) {
|
|
62
81
|
for (const op of b.ops) {
|
|
63
82
|
let width: number, signed: boolean;
|
|
@@ -76,7 +95,9 @@ export function recoverTypes(fn: Fn): void {
|
|
|
76
95
|
break;
|
|
77
96
|
case 'astore':
|
|
78
97
|
width = op.attrs.elemSize as number;
|
|
79
|
-
|
|
98
|
+
// an astore carries no signedness of its own unless a recovery recorded the member's;
|
|
99
|
+
// structure.ts reads it by the same rule
|
|
100
|
+
signed = (op.attrs.signed as boolean | undefined) ?? width === 4;
|
|
80
101
|
break;
|
|
81
102
|
default:
|
|
82
103
|
continue;
|
|
@@ -87,16 +108,16 @@ export function recoverTypes(fn: Fn): void {
|
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// PHASE 3 is `propagatePointers` (below, with its own note). Phase 2 types only the DIRECT base of
|
|
114
|
+
// a dereference; a loop-carried pointer reaches its dereference through a block-arg phi (its
|
|
115
|
+
// incoming `a0`) and a `p = p + stride` walk, so those values stay `unknown` and phase 4 would
|
|
116
|
+
// spell them `int` — an `int→int*` assignment mwcc/agbcc REJECT (gcc warns).
|
|
117
|
+
|
|
118
|
+
/** PHASE 4 — default every still-unknown value to s32. This is a COMPILER default (agbcc/IDO/GCC
|
|
119
|
+
* all take plain `int` as the integer default), not a hardware fact — applied uniformly. */
|
|
120
|
+
function defaultUnknownsToS32(fn: Fn): void {
|
|
100
121
|
for (const b of fn.blocks) {
|
|
101
122
|
for (const p of b.params) {
|
|
102
123
|
if (p.type.kind === 'unknown') {
|
|
@@ -225,7 +246,19 @@ export function returnType(fn: Fn): IrType {
|
|
|
225
246
|
const term = b.ops[b.ops.length - 1];
|
|
226
247
|
if (term?.opcode === 'ret' && term.operands.length > 0) {
|
|
227
248
|
const v = term.operands[0];
|
|
228
|
-
return
|
|
249
|
+
// A NARROW value in the return register does not make the DECLARED return type narrow: the
|
|
250
|
+
// register is a word, and the value's width says how it was COMPUTED, not what the header
|
|
251
|
+
// spelled. The two readings compile alike through agbcc and do NOT through mwcc —
|
|
252
|
+
// `s8 f(s8 x) { return x; }` drops the `extsb` that `s32 f(s8 x) { return x; }` keeps and the
|
|
253
|
+
// target has (synthetic `sextb`/`tos8`).
|
|
254
|
+
//
|
|
255
|
+
// The widening lands on `s32`, the same fallback an untyped return takes, rather than on the
|
|
256
|
+
// value's own signedness: a `zext` states what the ARGUMENT was declared as, which is no
|
|
257
|
+
// evidence about the header's return type — and `u32 f(u8 x){return x;}` and its `s32` twin
|
|
258
|
+
// are one `lsl/lsr/bx lr` through agbcc either way. A narrowed PARAMETER
|
|
259
|
+
// (raise/paramwidth.ts) is the only value that reaches here narrow: no frontend types a
|
|
260
|
+
// load's result below a word.
|
|
261
|
+
return v.type.kind === 'unknown' || (v.type.kind === 'int' && v.type.width < 32) ? T.s(32) : v.type;
|
|
229
262
|
}
|
|
230
263
|
}
|
|
231
264
|
return T.s(32);
|