@asmlift/core 0.7.0 → 0.8.1
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/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- 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 +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +54 -20
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// L3 spelling pass: drop a void `return;` the assembly says the source never wrote.
|
|
2
|
+
//
|
|
3
|
+
// `structure/retspell.ts` marks a return `unspelled` when the machine reached that epilogue without
|
|
4
|
+
// the `b <epilogue>` a source `return;` compiles to — the reading, and what makes it decidable, is
|
|
5
|
+
// stated there. This pass is the other half: a mark alone is not a licence to delete, because a
|
|
6
|
+
// `return` is still a control transfer in the STATEMENT tree. Deleting one that something follows
|
|
7
|
+
// lets control run on into it, and that is a semantic change, not a spelling one.
|
|
8
|
+
//
|
|
9
|
+
// So the licence is TAIL POSITION and nothing weaker: nothing executes after the statement on any
|
|
10
|
+
// path, all the way up to the end of the function. A return ending a loop body, a `switch` arm, or
|
|
11
|
+
// an `if` arm the function continues past is left exactly where it is, whatever the mark says.
|
|
12
|
+
//
|
|
13
|
+
// TWO SHAPES REFUSE EVEN IN TAIL POSITION, both because removing the statement would leave a
|
|
14
|
+
// statement list that has to be re-spelled rather than shortened:
|
|
15
|
+
//
|
|
16
|
+
// - the sole statement of a `then` arm. `if (c) { }` is not the answer — `if (!c) { … }` is, and
|
|
17
|
+
// which sense a compiler emits is a per-SITE question this pass holds nothing to decide.
|
|
18
|
+
// - the whole function body. A body is not a place a statement can vanish from.
|
|
19
|
+
//
|
|
20
|
+
// An `else` arm IS allowed to empty: an `if` with no else is the same statement, and both the
|
|
21
|
+
// printer (`backend/cfamily.ts`) and `l3/dce.ts` already spell `else: []` that way.
|
|
22
|
+
//
|
|
23
|
+
// THE REFUSALS ARE PROSE, NOT A `Gate` TABLE, and that is the second of the three answers
|
|
24
|
+
// `docs/level-tower.md` sanctions rather than an omission. A table buys the ablation — drop this
|
|
25
|
+
// rule and something breaks — as a test instead of a claim, and it is worth building for a refusal a
|
|
26
|
+
// round has HAD to instrument. None of these was: each is a property of one candidate's own
|
|
27
|
+
// position, and `test/tailret.test.ts` holds a test per refusal, which is the ablation the table
|
|
28
|
+
// would have bought. Convert them when a round has to argue about one.
|
|
29
|
+
//
|
|
30
|
+
// Ordering: after `l3/tailmerge.ts`, whose peel moves `assign`/`store`/`exprstmt` only, so a
|
|
31
|
+
// `return` ending an arm blocks it — run first and this pass hands tailmerge arms it could not
|
|
32
|
+
// otherwise peel, changing rows that have nothing to do with returns. Before `l3/dce.ts`, so its
|
|
33
|
+
// branch peephole sees the `else` this pass empties. `pipeline.ts` commits that order.
|
|
34
|
+
import type { SFn, Stmt } from './ast';
|
|
35
|
+
|
|
36
|
+
/** Can this statement be deleted outright — a void return the asm did not spell? */
|
|
37
|
+
const isDroppable = (s: Stmt): boolean => s.k === 'return' && s.value === undefined && s.unspelled === true;
|
|
38
|
+
|
|
39
|
+
/** `stmts` rewritten. `isTail` — control falls off the END of the function after this list, so its
|
|
40
|
+
* last statement is in tail position. `mayEmpty` — the list is allowed to come back empty. */
|
|
41
|
+
function walk(stmts: Stmt[], isTail: boolean, mayEmpty: boolean): Stmt[] {
|
|
42
|
+
const out = stmts.map((s, i) => {
|
|
43
|
+
const tail = isTail && i === stmts.length - 1;
|
|
44
|
+
switch (s.k) {
|
|
45
|
+
case 'if':
|
|
46
|
+
return { ...s, then: walk(s.then, tail, false), else: walk(s.else, tail, true) };
|
|
47
|
+
case 'while':
|
|
48
|
+
case 'dowhile':
|
|
49
|
+
case 'for':
|
|
50
|
+
// A return inside a loop is never in tail position: the statement after it is the next
|
|
51
|
+
// iteration.
|
|
52
|
+
return { ...s, body: walk(s.body, false, false) };
|
|
53
|
+
case 'switch':
|
|
54
|
+
// Nor inside a `switch`: dropping an arm's return diverts it into the arm below.
|
|
55
|
+
return {
|
|
56
|
+
...s,
|
|
57
|
+
cases: s.cases.map((c) => ({ ...c, body: walk(c.body, false, false) })),
|
|
58
|
+
...(s.default ? { default: walk(s.default, false, false) } : {}),
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const last = out[out.length - 1];
|
|
65
|
+
return isTail && last !== undefined && isDroppable(last) && (out.length > 1 || mayEmpty) ? out.slice(0, -1) : out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function dropUnspelledReturns(sfn: SFn): SFn {
|
|
69
|
+
return { ...sfn, body: walk(sfn.body, true, false) };
|
|
70
|
+
}
|
package/src/l3/unmerge.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// own definitions into the join statement and duplicating it back recovers that spelling.
|
|
14
14
|
//
|
|
15
15
|
// A VARIATION, NOT A DEFAULT, and the reason is the tower's: the LIFTED TREE underdetermines the
|
|
16
|
-
// source. This is a compiler claim, so it was compiled — agbcc `gcc 2.9-arm-000512` at
|
|
17
|
-
//
|
|
16
|
+
// source. This is a compiler claim, so it was compiled — agbcc `gcc 2.9-arm-000512` at its
|
|
17
|
+
// canonical flags, both spellings, `diff` on the `.s`:
|
|
18
18
|
//
|
|
19
19
|
// - the example above (an ADDRESS temp and a VALUE temp, against `*gA1 = gB1;` in one arm and
|
|
20
20
|
// `*gA2 = gB2;` in the other, `v17` typed as the store's own type) is BYTE-IDENTICAL. There
|
package/src/l3/unreduce.ts
CHANGED
|
@@ -196,6 +196,7 @@ import {
|
|
|
196
196
|
exprEquals,
|
|
197
197
|
exprHasEffect,
|
|
198
198
|
exprReadsVolatile,
|
|
199
|
+
isLoop,
|
|
199
200
|
mapExprChildren,
|
|
200
201
|
stmtChildren,
|
|
201
202
|
stmtExprs,
|
|
@@ -813,7 +814,7 @@ export function unreduceAccumulators(
|
|
|
813
814
|
|
|
814
815
|
for (let li = 0; li < body.length; li++) {
|
|
815
816
|
const loop = body[li];
|
|
816
|
-
if (loop
|
|
817
|
+
if (!isLoop(loop)) {
|
|
817
818
|
continue;
|
|
818
819
|
}
|
|
819
820
|
// the counter: one name stepped by a constant, whose start stands above the loop
|
package/src/mangle.ts
CHANGED
|
@@ -129,6 +129,55 @@ export function demangle(sym: string): CppSig | null {
|
|
|
129
129
|
return { name, cls, params };
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** A class qualifier off the front of `s` — `12RefCountable`, or `Q23zen17particleGenerator` for a
|
|
133
|
+
* nested one — as its scope names and the remaining string; null when `s` does not open with one. */
|
|
134
|
+
function parseQualifier(s: string): { scopes: string[]; rest: string } | null {
|
|
135
|
+
const nested = /^Q(\d)/.exec(s);
|
|
136
|
+
let count = nested === null ? 1 : Number(nested[1]);
|
|
137
|
+
let rest = nested === null ? s : s.slice(2);
|
|
138
|
+
const scopes: string[] = [];
|
|
139
|
+
for (; count > 0; count--) {
|
|
140
|
+
const digits = /^(\d+)/.exec(rest);
|
|
141
|
+
if (digits === null) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const len = Number(digits[1]);
|
|
145
|
+
const id = rest.slice(digits[1].length, digits[1].length + len);
|
|
146
|
+
if (len === 0 || id.length !== len) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
scopes.push(id);
|
|
150
|
+
rest = rest.slice(digits[1].length + len);
|
|
151
|
+
}
|
|
152
|
+
return { scopes, rest };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The name a CodeWarrior function symbol has in its SOURCE — `PlayerState::getStartHour` for
|
|
156
|
+
* `getStartHour__11PlayerStateFv`, `zen::particleGenerator::RotAxisX` for a nested class, and the
|
|
157
|
+
* constructor and destructor spelled as they are declared (`RefCountable::RefCountable`,
|
|
158
|
+
* `System::~System`). Only the name: unlike `demangle` it reads no parameter code, so it answers
|
|
159
|
+
* for the references, arrays and const qualifiers that one refuses. Null where `sym` is not a
|
|
160
|
+
* mangled function, and for an operator, whose source name is not an identifier. */
|
|
161
|
+
export function demangledName(sym: string): string | null {
|
|
162
|
+
for (let at = sym.indexOf('__', 1); at > 0; at = sym.indexOf('__', at + 1)) {
|
|
163
|
+
const name = sym.slice(0, at);
|
|
164
|
+
const rest = sym.slice(at + 2);
|
|
165
|
+
const qualified = /^[\dQ]/.test(rest) ? parseQualifier(rest) : { scopes: [], rest };
|
|
166
|
+
if (qualified === null || !/^C?F/.test(qualified.rest)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const { scopes } = qualified;
|
|
170
|
+
const cls = scopes.at(-1);
|
|
171
|
+
// every other special name — `__as`, `__nw`, `__pl` — is an operator
|
|
172
|
+
const member = name === '__ct' ? cls : name === '__dt' && cls !== undefined ? `~${cls}` : name;
|
|
173
|
+
if (member === undefined || member.startsWith('__')) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return [...scopes, member].join('::');
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
132
181
|
/** Spell a CppType as C++ source (`Vec *`, `unsigned int`). */
|
|
133
182
|
export function spellType(t: CppType): string {
|
|
134
183
|
return t.base + (t.ptr ? ' ' + '*'.repeat(t.ptr) : '');
|
package/src/pattern/engine.ts
CHANGED
|
@@ -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[];
|
|
101
|
+
}
|
|
102
|
+
|
|
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 };
|
|
94
111
|
}
|
|
95
112
|
|
|
96
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
116
|
* declared capability. An omitted field is unconstrained. */
|
|
100
|
-
export function patternApplies(
|
|
101
|
-
p: RewritePattern,
|
|
102
|
-
target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
|
|
103
|
-
): boolean {
|
|
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
|
}
|
|
@@ -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);
|
|
@@ -348,6 +349,23 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
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
|
+
|
|
351
369
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
352
370
|
export function structureChecked(
|
|
353
371
|
fn: Fn,
|
|
@@ -365,16 +383,9 @@ export function structureChecked(
|
|
|
365
383
|
assertDerefsTyped(raw);
|
|
366
384
|
assertLocalsWritten(raw);
|
|
367
385
|
assertEffectsPreserved(fn, raw);
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
// cast from each `index` node onto the local's initializer, so re-validate deref typing on the
|
|
372
|
-
// rewritten tree.
|
|
373
|
-
// Both arguments SPELLED, defaults or not: this is the one call to this pass that is committed
|
|
374
|
-
// rather than offered, so it is the one whose gate table and whose placement can cost a MATCH
|
|
375
|
-
// instead of a candidate (docs/level-tower.md). A committed policy that reads as "whatever the
|
|
376
|
-
// default is" is the policy nobody reviews.
|
|
377
|
-
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);
|
|
378
389
|
assertDerefsTyped(sfn);
|
|
379
390
|
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
380
391
|
// merges arms or drops statements must not be able to lose or duplicate a call.
|
package/src/raise/extscale.ts
CHANGED
|
@@ -157,8 +157,11 @@ export function scaledExtensionOf(op: Op | undefined, defs: Map<Value, Op>): Sca
|
|
|
157
157
|
return { src: inner.operands[0], width: 32 - l, signed: op.opcode === 'shr_s', shift: l - r, inner };
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/** Does `target` lower
|
|
161
|
-
*
|
|
160
|
+
/** Does `target` lower BOTH narrowing casts to a shift pair? The conjunction is what the two
|
|
161
|
+
* consumers need: a fused pair carries either signedness and this fold re-splits it without
|
|
162
|
+
* knowing which. It is a stricter question than either cast pattern's own gate, because
|
|
163
|
+
* `pattern/engine.ts` measures the halves separately — MIPS spells `(s8)x` as a shift pair and
|
|
164
|
+
* `(u8)x` as `andi`, so every MIPS compiler answers `false` here while folding the signed half. */
|
|
162
165
|
export const foldsShiftPairCasts = (target: TargetDescription): boolean =>
|
|
163
166
|
CAST_PATTERNS.every((p) => patternApplies(p, target));
|
|
164
167
|
|
package/src/raise/paramwidth.ts
CHANGED
|
@@ -21,6 +21,76 @@
|
|
|
21
21
|
// narrow parameter with the `extsb`/`extsh` the frontend lifts to the same op, and the synthetic
|
|
22
22
|
// `sextb`/`tos8` rows keep matching on that toolchain through this pass.
|
|
23
23
|
//
|
|
24
|
+
// BUT THE PROLOGUE TEST IS NOT UNIVERSAL EVIDENCE, AND ON MIPS IT IS NO EVIDENCE AT ALL. Everything
|
|
25
|
+
// above reads the extension's POSITION, which works only where the compiler puts a declaration's
|
|
26
|
+
// extension somewhere a body cast's never goes. IDO 7.1 at -O2 does not: it leads the function with
|
|
27
|
+
// the `sll` for BOTH spellings. Two OTHER facts separate them there, and it takes both — each one
|
|
28
|
+
// alone is refuted by a compiled counterexample, at the row's own flags:
|
|
29
|
+
//
|
|
30
|
+
// home store widened in place
|
|
31
|
+
// int f(s8 x){ return x; } sw a0 sll a0,a0,0x18 the declaration
|
|
32
|
+
// int f(s32 x){ return (s8)x; } — sll v0,a0,0x18 a body cast
|
|
33
|
+
// int f(int x){ x = (signed char)x; return x; } — sll a0,a0,0x18 NOT homed
|
|
34
|
+
// int f(long long x){ return (signed char)x; } sw a1 sll v0,a1,0x18 NOT in place
|
|
35
|
+
//
|
|
36
|
+
// The last two lines are why the conjunction is the claim. A DEAD ABI ARGUMENT HOME STORE on its
|
|
37
|
+
// own is not "declared narrow" — IDO emits one wherever the incoming register value goes unused,
|
|
38
|
+
// whatever that value's type:
|
|
39
|
+
//
|
|
40
|
+
// int unused1(int x){ return 7; } sw a0,0(sp) / jr ra / li v0,7
|
|
41
|
+
// int ptr1(int *p, int y){ return y; } sw a0,0(sp) / jr ra / move v0,a1
|
|
42
|
+
// int ll2i(long long x){ return (int)x; } sw a0,0(sp) / sw a1,4(sp) / jr ra / move v0,a1
|
|
43
|
+
// int used2(int x, int y){ return x+y; } jr ra / addu v0,a0,a1 (no store at all)
|
|
44
|
+
//
|
|
45
|
+
// — so the store says the incoming register was not consumed, not that anything was declared
|
|
46
|
+
// narrow, and on the `long long` line that is a 64-bit parameter whose low half the extension would
|
|
47
|
+
// narrow to `s8`; `widened-elsewhere` is what refuses it. WIDENING IN PLACE on its own is not
|
|
48
|
+
// "declared narrow" either: the third line is a plain `int` the source re-assigns, and only the
|
|
49
|
+
// absent home store tells it from the first.
|
|
50
|
+
//
|
|
51
|
+
// The conjunction is NECESSARY-and-measured, not necessary-and-sufficient, and it errs toward
|
|
52
|
+
// refusing: `int m2(signed char a){ int i,s=0; for(i=0;i<10;i++) s+=arr[i]+a; return s; }` is a
|
|
53
|
+
// narrow declaration IDO widens into a SCRATCH register under register pressure (`sll a1,a0,0x18`),
|
|
54
|
+
// so this pass leaves its parameter wide and re-spells the cast. That is a worse-reading answer,
|
|
55
|
+
// not a wrong one.
|
|
56
|
+
//
|
|
57
|
+
// THE PAIR SAYS A NARROW DECLARATION EXISTS, NOT WHICH ONE. The WIDTH comes from the extension,
|
|
58
|
+
// and where a wider mask sits on that extension's result the declaration could have been the wider
|
|
59
|
+
// type — `int f(u16 x){ x = (signed char)x; return x; }` and `int f(s8 x){ return x & 0xffff; }`
|
|
60
|
+
// are ONE object at the row's own flags (`sw a0,0(sp) / sll a0,a0,0x18 / sra a0,a0,0x18 / andi
|
|
61
|
+
// v0,a0,0xffff`), so the asm decides nothing between them and this pass answers `s8`. Refusing on
|
|
62
|
+
// that disagreement is not the repair, because the spelling a refusal falls back to is a DIFFERENT
|
|
63
|
+
// object: `int f(int a){ return ((a << 24) >> 24) & 0xffff; }` is four words with no home store and
|
|
64
|
+
// both shifts in `v0`, so the refusal would cost the byte match under BOTH readings. Where the
|
|
65
|
+
// caller declares the parameter `proto-width` takes the tiebreak; where nobody does, the two
|
|
66
|
+
// readings recompile alike and only a PROTOTYPED CALL SITE of this function could tell them apart.
|
|
67
|
+
//
|
|
68
|
+
// Ungated, this pass narrows the body cast too and loses `synthetic:tos8:ido7.1`, a MATCH. The two
|
|
69
|
+
// MIPS GCCs are a third case again: their two spellings are one BYTE-IDENTICAL object, so nothing
|
|
70
|
+
// in the asm decides the width and the honest answer is to leave the extension standing.
|
|
71
|
+
//
|
|
72
|
+
// So which fact settles the width is a per-COMPILER question, asked as
|
|
73
|
+
// `compilerBehaviors.narrowParamWitness` and answered by `no-declaration-witness`, `unhomed-param`
|
|
74
|
+
// and `widened-elsewhere` below. Both facts it reads are destroyed at lift and survive as the
|
|
75
|
+
// frontend's `Fn.paramEvidence` (ir/core.ts).
|
|
76
|
+
//
|
|
77
|
+
// `not-prologue` STILL FIRES ON SUCH A TARGET, and it costs rather than protects there. IDO's
|
|
78
|
+
// scheduler interleaves the widenings of several narrow parameters with the arithmetic that
|
|
79
|
+
// consumes the earlier ones, and the scan below stops at the first value-reading op, so only the
|
|
80
|
+
// LEADING parameters are seen as prologue:
|
|
81
|
+
//
|
|
82
|
+
// int d4(s8 a, s8 b, s8 c){ return a+b+c; } sll a1 / sll a0 / sll a2 / sra a0 / sra a1 /
|
|
83
|
+
// addu t6,a0,a1 / sra a2 / addu v0,t6,a2
|
|
84
|
+
// recovered s32 d4(s8 a0, s8 a1, s32 a2) { return a0 + a1 + (s8)a2; }
|
|
85
|
+
// int m3(s16 a, s16 b, s16 c, s16 d){ return a*b+c*d; } all four widenings precede the first
|
|
86
|
+
// recovered s32 m3(s16 a0, s16 a1, s16 a2, s16 a3) multiply — all four narrow
|
|
87
|
+
//
|
|
88
|
+
// So the limit is the SCHEDULE's, not the declaration's, and the gate answers in the refusing
|
|
89
|
+
// direction on the parameters it cuts off. Kept because it is the sound direction and because it is
|
|
90
|
+
// the only gate that keeps a body cast behind real body code from being read as a prologue widening
|
|
91
|
+
// on the position-witness targets; NO row on either tier is known to turn on the IDO half of it —
|
|
92
|
+
// `d4` above is a probe, not a row, and lifting the scan is a change with its own measurement to make.
|
|
93
|
+
//
|
|
24
94
|
// WHAT THE PROLOGUE TEST CANNOT SEE, and why the declaration settles it. The scan steps over the
|
|
25
95
|
// pure materializations agbcc interleaves among the extensions, so a constant the scheduler HOISTED
|
|
26
96
|
// above a mid-body cast leaves `pb` looking like `pa`:
|
|
@@ -57,6 +127,7 @@ import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
|
|
|
57
127
|
import { T } from '../ir/types';
|
|
58
128
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
59
129
|
import { type FnProto, declaredWidth } from '../proto';
|
|
130
|
+
import type { NarrowParamWitness } from '../target';
|
|
60
131
|
|
|
61
132
|
/** What the gates below judge: one entry parameter and the extension that reads it. */
|
|
62
133
|
export interface NarrowParamCandidate {
|
|
@@ -75,6 +146,12 @@ export interface NarrowParamCandidate {
|
|
|
75
146
|
/** the extension is one raise/extscale.ts re-split from a fused pair whose `shl` the machine ran
|
|
76
147
|
* behind a pool load — see FUSED BEHIND A POOL LOAD */
|
|
77
148
|
fusedBehindPool: boolean;
|
|
149
|
+
/** what this compiler's object shows for a narrow declaration (target.ts `narrowParamWitness`) */
|
|
150
|
+
witness: NarrowParamWitness;
|
|
151
|
+
/** the machine stored this parameter to a stack slot nothing reads back (`ParamObservation`) */
|
|
152
|
+
homed: boolean;
|
|
153
|
+
/** the machine put the parameter's own widened value back in its argument register (`ParamObservation`) */
|
|
154
|
+
selfRedefined: boolean;
|
|
78
155
|
}
|
|
79
156
|
|
|
80
157
|
export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
|
|
@@ -115,11 +192,34 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
|
|
|
115
192
|
},
|
|
116
193
|
{
|
|
117
194
|
id: 'not-prologue',
|
|
118
|
-
why: 'an extension behind body code is where the SOURCE wrote the cast',
|
|
195
|
+
why: 'an extension behind body code is where the SOURCE wrote the cast — and on a schedule that interleaves them, the refusing answer',
|
|
119
196
|
sound: true,
|
|
120
197
|
guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
|
|
121
198
|
rejects: (c) => !c.inPrologue,
|
|
122
199
|
},
|
|
200
|
+
{
|
|
201
|
+
id: 'no-declaration-witness',
|
|
202
|
+
why: 'this compiler spells a narrow declaration and a body cast the same way, so the asm decides nothing',
|
|
203
|
+
sound: true,
|
|
204
|
+
guardedBy:
|
|
205
|
+
'param-width.test.ts: a compiler whose two spellings are one object refuses the narrowing, measured or not',
|
|
206
|
+
rejects: (c) => c.witness === 'none',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'unhomed-param',
|
|
210
|
+
why: 'this compiler homes a narrow DECLARED parameter, so the absent home store proves the declaration was wide',
|
|
211
|
+
sound: true,
|
|
212
|
+
guardedBy: 'param-width.test.ts: a homing compiler refuses the parameter it did not home',
|
|
213
|
+
rejects: (c) => c.witness === 'home-store-and-in-place' && !c.homed,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'widened-elsewhere',
|
|
217
|
+
why: 'this compiler widens a narrow DECLARED parameter in the argument register itself, so a widening that lands in a scratch register is not a declaration',
|
|
218
|
+
sound: true,
|
|
219
|
+
guardedBy:
|
|
220
|
+
'param-width.test.ts: \u2026and refuses one it homed but widened SOMEWHERE ELSE \u2014 that is a 64-bit half',
|
|
221
|
+
rejects: (c) => c.witness === 'home-store-and-in-place' && !c.selfRedefined,
|
|
222
|
+
},
|
|
123
223
|
{
|
|
124
224
|
id: 'fused-behind-pool',
|
|
125
225
|
why: 'a fused cast behind a pool load is body code if unsigned, and either width is the same object if signed',
|
|
@@ -144,15 +244,20 @@ function useCount(fn: Fn, v: Value): number {
|
|
|
144
244
|
}
|
|
145
245
|
|
|
146
246
|
/** Type an entry parameter at the width its prologue extension proves, and drop the extension.
|
|
147
|
-
* `
|
|
148
|
-
*
|
|
247
|
+
* `witness` is what this compiler's object shows for a narrow declaration (target.ts
|
|
248
|
+
* `narrowParamWitness`); `self` is the prototype the caller supplied for THIS function, if any;
|
|
249
|
+
* `fusedBehindPool` is raise/extscale.ts's record of the extensions it re-split behind a pool load
|
|
149
250
|
* (`ScaleRecord.behindPool`). Returns the number of parameters narrowed. */
|
|
150
251
|
export function narrowEntryParams(
|
|
151
252
|
fn: Fn,
|
|
253
|
+
witness: NarrowParamWitness,
|
|
152
254
|
self?: FnProto,
|
|
153
255
|
gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
|
|
154
256
|
fusedBehindPool: ReadonlySet<Op> = new Set(),
|
|
155
257
|
): number {
|
|
258
|
+
// ABSENT ⇒ NEITHER OBSERVATION, which is the refusing direction on a target that reads them: a
|
|
259
|
+
// function nobody measured (parsed IR, a hand-built fn) is never narrowed on evidence never taken.
|
|
260
|
+
const evidence = fn.paramEvidence;
|
|
156
261
|
const entry = fn.blocks[0];
|
|
157
262
|
const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
|
|
158
263
|
const entryIsJoin = fn.blocks.some((b) => successorsOf(b).includes(entry));
|
|
@@ -188,6 +293,9 @@ export function narrowEntryParams(
|
|
|
188
293
|
uses: useCount(fn, p),
|
|
189
294
|
declared: declared[entry.params.indexOf(p)],
|
|
190
295
|
fusedBehindPool: fusedBehindPool.has(op),
|
|
296
|
+
witness,
|
|
297
|
+
homed: evidence?.get(p)?.deadHome ?? false,
|
|
298
|
+
selfRedefined: evidence?.get(p)?.selfRedefined ?? false,
|
|
191
299
|
};
|
|
192
300
|
if (firstRejection(gates, c) !== null) {
|
|
193
301
|
continue;
|
|
@@ -201,9 +201,19 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
|
|
|
201
201
|
),
|
|
202
202
|
dce: false,
|
|
203
203
|
},
|
|
204
|
+
// The `target` argument is read by TWO of this pass's gates, and unlike `narrowlocal`'s single
|
|
205
|
+
// conjunct it is not a tuning knob: `narrowParamWitness` says WHICH fact in the object settles a
|
|
206
|
+
// parameter's declared width on this compiler, and a target that names none refuses outright.
|
|
204
207
|
{
|
|
205
208
|
id: 'paramwidth',
|
|
206
|
-
run: (fn, self, _opts,
|
|
209
|
+
run: (fn, self, _opts, target, lifted) =>
|
|
210
|
+
narrowEntryParams(
|
|
211
|
+
fn,
|
|
212
|
+
target.compilerBehaviors.narrowParamWitness ?? 'none',
|
|
213
|
+
self,
|
|
214
|
+
PARAM_WIDTH_GATES,
|
|
215
|
+
lifted.scales.behindPool,
|
|
216
|
+
),
|
|
207
217
|
dce: false,
|
|
208
218
|
},
|
|
209
219
|
// LAST, after every pass that can CLAIM what `extscale` exposed — the two width passes above take
|