@asmlift/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// A NARROWING EXTENSION FUSED WITH THE LEFT SHIFT THAT SCALES IT.
|
|
2
|
+
//
|
|
3
|
+
// agbcc lowers `(u8)x` to a shift pair, `lsl #24; lsr #24` (the CAST_PATTERNS idiom,
|
|
4
|
+
// pattern/engine.ts). Scale the narrowed value and gcc's combiner merges the right half of the pair
|
|
5
|
+
// with the scale, so `(u8)x << 3` is TWO instructions, not three:
|
|
6
|
+
//
|
|
7
|
+
// lsl r0, r0, #0x18 %1 = shl %x {imm=24}
|
|
8
|
+
// lsr r0, r0, #0x15 %2 = shr_u %1 {imm=21} == zext8(x) << 3
|
|
9
|
+
//
|
|
10
|
+
// Exactly: `(x << L) >>u R` for `0 < R < L` keeps x's low `32 - L` bits and lands them at bit
|
|
11
|
+
// `L - R`, which is `zext(x, 32 - L) << (L - R)`; `>>s` is the same with `sext`. What the fold buys
|
|
12
|
+
// is the two facts the raw pair hides from every pass below — and ONLY those: the spelling it
|
|
13
|
+
// prints when neither is claimed is not byte-neutral, so that spelling never ships (WHAT NOBODY
|
|
14
|
+
// CLAIMED, below):
|
|
15
|
+
//
|
|
16
|
+
// • THE WIDTH. A declared `u8 idx` is extended in the prologue, and that extension is what
|
|
17
|
+
// raise/paramwidth.ts reads to type the parameter. When the extension is fused, the prologue
|
|
18
|
+
// holds a bare `shl` instead: paramwidth sees no extension of this parameter, AND its prologue
|
|
19
|
+
// scan stops at that `shl`, so every LATER parameter's extension reads as body code too. Typed
|
|
20
|
+
// `u32`, the function recompiles with both shifts at the use and the whole schedule moves.
|
|
21
|
+
// • THE SCALE. `shl(ext(x), k)` is the element-scaled index raise/arrays.ts and
|
|
22
|
+
// raise/struct-arrays.ts legalize; `shr_u(shl(x, 24), 21)` is an opaque byte offset to both.
|
|
23
|
+
//
|
|
24
|
+
// WHERE THE EXTENSION GOES is the one real decision here, and it is read off the machine, not
|
|
25
|
+
// chosen. The two halves of the pair are not scheduled together. Compiled with this benchmark's
|
|
26
|
+
// agbcc, the same body over a narrow and a wide parameter:
|
|
27
|
+
//
|
|
28
|
+
// void pa(u8 a, u8 b) { gA |= 4; gB = (u32)&gT + a * 4; gC = b; }
|
|
29
|
+
// lsl r0,#24 / lsl r1,#24 / lsr r1,#24 / …body… / lsr r0,#22 / add …
|
|
30
|
+
// void pb(u32 a, u8 b) { gA |= 4; gB = (u32)&gT + (u8)a * 4; gC = b; }
|
|
31
|
+
// lsl r1,#24 / lsr r1,#24 / …body… / lsl r0,#24 / lsr r0,#22 / add …
|
|
32
|
+
//
|
|
33
|
+
// The declaration's extension starts in the prologue and only its SECOND half is merged into the
|
|
34
|
+
// use; a cast in the body is lowered at the use, both halves together. So the extension is placed
|
|
35
|
+
// where its `shl` stood and the scale where the right shift stood: a prologue `lsl` yields a
|
|
36
|
+
// prologue extension, which paramwidth's own gates then judge. Those gates keep a body cast wide
|
|
37
|
+
// only when body code that READS a value comes first — `not-prologue` steps over materializations,
|
|
38
|
+
// a pool-loaded address among them — which is why the fold records a pair it finds behind one
|
|
39
|
+
// (THE BODY CAST BEHIND A POOL LOAD, below). Placing both at the right shift instead throws `pa`'s
|
|
40
|
+
// prologue evidence away, so `pa` reads as `pb` and stays wide — measured on the benchmark's sa3
|
|
41
|
+
// rows whose first parameter is a declared `u8` scaled this way:
|
|
42
|
+
// `sa2__sub_8007858` 39/60 anchored against 44/61 at the right shift, and `sa2__sub_8007958` 64/87
|
|
43
|
+
// against 66/88.
|
|
44
|
+
//
|
|
45
|
+
// The SIGNED form does not carry that evidence on agbcc: `void pd(s16 a, …) { … a * 2 … }` and its
|
|
46
|
+
// wide twin `(s16)a * 2` compile to the SAME object, both halves at the use — the ambiguity of
|
|
47
|
+
// paramwidth's `pc` pair — so the placement rule is the same either way, and where the pair sits the
|
|
48
|
+
// judgement is paramwidth's.
|
|
49
|
+
//
|
|
50
|
+
// REFUSES — the pair is left as it is — when:
|
|
51
|
+
// • `R >= L`. `R == L` is the plain cast, which CAST_PATTERNS already folded; `R > L` keeps
|
|
52
|
+
// `32 - R` bits of `x << L` and scales nothing — a bitfield EXTRACT, structure/bitfields.ts's
|
|
53
|
+
// shape — so the two readings are disjoint by the shift amounts alone;
|
|
54
|
+
// • `32 - L` is not a C cast width (8 or 16) — `lsl #28; lsr #26` is `(x & 15) << 2`, which no
|
|
55
|
+
// cast spells;
|
|
56
|
+
// • either shift is the two-register form (no `imm`) or `R` is 0;
|
|
57
|
+
// • the target is not one CAST_PATTERNS applies to. Same predicate, because the fused pair is
|
|
58
|
+
// that cast's lowering with a scale merged in: where `(u8)x` is not a shift pair (IDO and GCC
|
|
59
|
+
// `andi`), `(u8)x << k` is not one either, and a `sll; srl` there is some other arithmetic.
|
|
60
|
+
//
|
|
61
|
+
// • another narrowing of the same value, at the same width and with the same SIGN, sits in the
|
|
62
|
+
// pair's block — a second right shift of a `shl(x, L)`, fused or plain, or the extension
|
|
63
|
+
// CAST_PATTERNS made of a plain one. agbcc extends a value once per block per signedness:
|
|
64
|
+
// `p[(u8)a * 4] = 1; return (u8)a;` is one `lsl #24; lsr #24` and a `lsl #4` at the use, and no
|
|
65
|
+
// fused pair. So a same-sign sibling means the source wrote the shift itself —
|
|
66
|
+
//
|
|
67
|
+
// u32 r6(u32 a, u32 *p) { u32 t = a << 24; p[t >> 22] = 1; return t >> 24; }
|
|
68
|
+
// lsl r0,#24 / lsr r2,r0,#20 / … / lsr r0,r0,#24
|
|
69
|
+
//
|
|
70
|
+
// which folded, struct-arrays spelled `a1[(u8)a0].field_0`: objdiff 3 where the raw pair is
|
|
71
|
+
// byte-exact. Opposite signs share only the `lsl` — `p[(u8)a] = (s8)a` is `lsl #24; lsr #22 …
|
|
72
|
+
// asr #24` — and both of those ARE casts, so they fold. The test is per BLOCK because the
|
|
73
|
+
// sharing is: the same two casts in two blocks lower twice, each with its own `lsl`
|
|
74
|
+
// (`if (c) p[1] = (u8)a; return p[(u8)a];`), and the fused one folds; and a `shl` shared
|
|
75
|
+
// ACROSS blocks is a declaration too — sa3's `sub_8010184` indexes `s->hitboxes[hbIndex]`
|
|
76
|
+
// twice over an `s16 hbIndex`: one `lsl #16`, read by an `asr #13` in each of two blocks. The
|
|
77
|
+
// idiom patterns turn a plain pair into its extension in place, so the test reads the same
|
|
78
|
+
// before them as after, and raise/globalshape.ts, which reads the lift, agrees with the fold
|
|
79
|
+
// about every pair.
|
|
80
|
+
//
|
|
81
|
+
// THE BODY CAST BEHIND A POOL LOAD folds, and the fold records it: the pair's `shl` reads an entry
|
|
82
|
+
// parameter and the machine ran it after a pool-loaded address. Compiled with this benchmark's
|
|
83
|
+
// agbcc, a body cast whose only predecessors are pool loads puts nothing between them and the
|
|
84
|
+
// extension that paramwidth's scan stops at, so the extension reads as a declaration:
|
|
85
|
+
//
|
|
86
|
+
// void pc1(u32 a) { gB = (u32)&gT + (u8)a * 4; } ldr r2,=gB / lsl r0,#24 / lsr r0,#22
|
|
87
|
+
//
|
|
88
|
+
// narrowed to `u8 a0` (objdiff 2) where the raw pair is MATCH. An UNSIGNED declared parameter's
|
|
89
|
+
// `lsl` comes before any pool load, symbol or numeric: all 44 over the benchmark's agbcc
|
|
90
|
+
// references. A signed one need not (27 before, 2 after): the signed form carries no placement
|
|
91
|
+
// evidence (above), and its narrow and wide spellings compile to the same object. So only the
|
|
92
|
+
// WIDTH is refused, by raise/paramwidth.ts's `fused-behind-pool` reading the record; the SCALE stays
|
|
93
|
+
// for the array passes: `gB = gW[(u8)a]` is objdiff 2 as the raw pair and byte-exact as
|
|
94
|
+
// `gW[(u8)a0]`, and sa3's `DemoPlayAlloc` (`gDemoRecordings[demoIndex]` over an `s16`) is spelled
|
|
95
|
+
// as the subscript it is.
|
|
96
|
+
// The order is read off the LIFTED function (`poolOrderOf`, which says why). NOT caught: a NUMERIC
|
|
97
|
+
// pool word lifts to `const`, the same op a `movs` does, and paramwidth's own header says why a
|
|
98
|
+
// `movs` ahead of the pair decides nothing — so a body cast behind only a numeric pool load
|
|
99
|
+
// (`& 0xfff` over a wide parameter in a loop) still narrows.
|
|
100
|
+
//
|
|
101
|
+
// One extension per (`shl`, signedness): two folded scales of one sign off one `shl` — in two
|
|
102
|
+
// blocks, `sub_8010184`'s shape — share it, and opposite signs get one each. The `shl` dies when
|
|
103
|
+
// every reader was folded; one that keeps a raw reader (`p[t >> 22] = 1; return t;`) stays beside
|
|
104
|
+
// the extension.
|
|
105
|
+
//
|
|
106
|
+
// WHAT NOBODY CLAIMED goes back (`restoreUnclaimedScales`, the last pre-recovery pass). An
|
|
107
|
+
// extension no width pass took and a scale no array pass took print as `(u8)a1 << 12`, and that is
|
|
108
|
+
// NOT the same object as the lifted `a1 << 24 >> 12`: straight-line they compile alike (the `pb`
|
|
109
|
+
// pair above), but kleod's `SetWorldMapTilePalette` — two `u8` parameters, the first used in agbcc's
|
|
110
|
+
// shifted domain so paramwidth's scan stops at its `shl` and refuses the second's extension — scores
|
|
111
|
+
// 54/93 with the raw pair and 59/91 with the cast, the loop's r4/r6 allocation swapped. So every
|
|
112
|
+
// scale the fold made that still reads its extension is rewritten in place to the pair it replaced;
|
|
113
|
+
// what survives the pass is exactly what a consumer claimed, and everywhere else the output is the
|
|
114
|
+
// one the lift alone produces.
|
|
115
|
+
//
|
|
116
|
+
// That is a decision about the DEFAULT, not a finding that the raw spelling is better: the unclaimed cast
|
|
117
|
+
// has both signs. Over the 55 corpus functions the fold fires on, scored on the benchmark's own
|
|
118
|
+
// path, restoring moves four scores — `SetWorldMapTilePalette` 59/91 → 54/93 and sa3
|
|
119
|
+
// `UnpackSaveSector` 347 → 346 better, sa3 `ClearSave` 189 → 191 and `CompleteSave` 185 → 187
|
|
120
|
+
// worse, all four to what the lift alone scores (and sa3 `ValidateSave`'s denominator, 213/378 →
|
|
121
|
+
// 213/379). A spelling with both signs is the differ's to referee (a ranked variation), and none is
|
|
122
|
+
// built; the default is the one that asserts nothing the lift did not.
|
|
123
|
+
import { type Block, type Fn, type Op, type Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
124
|
+
import { CAST_WIDTHS } from '../ir/opcodes';
|
|
125
|
+
import { CAST_PATTERNS, patternApplies } from '../pattern/engine';
|
|
126
|
+
import type { TargetDescription } from '../target';
|
|
127
|
+
|
|
128
|
+
/** `shr_{u,s}(shl(src, L), R)` read as `ext(src, 32 - L) << (L - R)`. */
|
|
129
|
+
export interface ScaledExtension {
|
|
130
|
+
/** the value being narrowed — the `shl`'s operand */
|
|
131
|
+
src: Value;
|
|
132
|
+
/** the cast width, `32 - L` */
|
|
133
|
+
width: number;
|
|
134
|
+
/** `shr_s` (sign-extend) rather than `shr_u` */
|
|
135
|
+
signed: boolean;
|
|
136
|
+
/** the scale's shift amount, `L - R` */
|
|
137
|
+
shift: number;
|
|
138
|
+
/** the `shl` — where the extension begins */
|
|
139
|
+
inner: Op;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The fused pair's SHAPE rooted at `op`, or null — the header's refusals on the amounts and the
|
|
143
|
+
* two-register form. The sibling refusal is {@link foldablePairs}', the target's the caller's. */
|
|
144
|
+
export function scaledExtensionOf(op: Op | undefined, defs: Map<Value, Op>): ScaledExtension | null {
|
|
145
|
+
if (!op || (op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const inner = defs.get(op.operands[0]);
|
|
149
|
+
const r = op.attrs.imm;
|
|
150
|
+
const l = inner?.attrs.imm;
|
|
151
|
+
if (inner?.opcode !== 'shl' || inner.operands.length !== 1 || typeof r !== 'number' || typeof l !== 'number') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!CAST_WIDTHS.has(32 - l) || r <= 0 || r >= l) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return { src: inner.operands[0], width: 32 - l, signed: op.opcode === 'shr_s', shift: l - r, inner };
|
|
158
|
+
}
|
|
159
|
+
|
|
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. */
|
|
165
|
+
export const foldsShiftPairCasts = (target: TargetDescription): boolean =>
|
|
166
|
+
CAST_PATTERNS.every((p) => patternApplies(p, target));
|
|
167
|
+
|
|
168
|
+
/** Where the MACHINE put each entry-block op relative to its first pool-loaded address, read off a
|
|
169
|
+
* function whose entry block is still in lifted order. See THE BODY CAST BEHIND A POOL LOAD. */
|
|
170
|
+
export interface PoolOrder {
|
|
171
|
+
/** the function's parameters */
|
|
172
|
+
entryParams: ReadonlySet<Value>;
|
|
173
|
+
/** the entry block's ops that come after a `gaddr` */
|
|
174
|
+
afterPoolLoad: ReadonlySet<Op>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Read {@link PoolOrder} off `fn`. Valid only BEFORE raise/gvn.ts's `addrnum` runs: that pass
|
|
178
|
+
* hoists a duplicated address to the head of the entry block, after which a `gaddr`'s position no
|
|
179
|
+
* longer says where the machine loaded it. The idiom patterns before it leave both a `gaddr` and a
|
|
180
|
+
* fused pair's `shl` where they were — a plain cast's pair is folded at its right half, which is
|
|
181
|
+
* why this fact is read for the FUSED form only. */
|
|
182
|
+
export function poolOrderOf(fn: Fn): PoolOrder {
|
|
183
|
+
const entry = fn.blocks[0];
|
|
184
|
+
const afterPoolLoad = new Set<Op>();
|
|
185
|
+
let seen = false;
|
|
186
|
+
for (const op of entry?.ops ?? []) {
|
|
187
|
+
if (seen) {
|
|
188
|
+
afterPoolLoad.add(op);
|
|
189
|
+
}
|
|
190
|
+
seen ||= op.opcode === 'gaddr';
|
|
191
|
+
}
|
|
192
|
+
return { entryParams: new Set(entry?.params ?? []), afterPoolLoad };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Is `o` a narrowing of `m.src` at `m.width` with `m`'s signedness — a right shift of a
|
|
196
|
+
* `shl(src, 32 - width)`, fused or plain, or the extension CAST_PATTERNS made of a plain one? The
|
|
197
|
+
* answer is the same before the idiom patterns and after them: they turn a plain pair into its
|
|
198
|
+
* extension at the pair's own position. */
|
|
199
|
+
function narrowsLike(o: Op, m: ScaledExtension, defs: Map<Value, Op>): boolean {
|
|
200
|
+
if (o.opcode === (m.signed ? 'sext' : 'zext')) {
|
|
201
|
+
return o.operands[0] === m.src && o.attrs.width === m.width;
|
|
202
|
+
}
|
|
203
|
+
if (o.opcode !== (m.signed ? 'shr_s' : 'shr_u') || o.operands.length !== 1) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const l = 32 - m.width;
|
|
207
|
+
const r = o.attrs.imm;
|
|
208
|
+
const d = defs.get(o.operands[0]);
|
|
209
|
+
return (
|
|
210
|
+
d?.opcode === 'shl' &&
|
|
211
|
+
d.operands.length === 1 &&
|
|
212
|
+
d.operands[0] === m.src &&
|
|
213
|
+
d.attrs.imm === l &&
|
|
214
|
+
typeof r === 'number' &&
|
|
215
|
+
r > 0 &&
|
|
216
|
+
r <= l
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Every fused pair in `fn` AS THE FOLD TAKES IT, keyed by its right shift: the shape, minus a pair
|
|
221
|
+
* with a same-sign sibling in its block. THE ONE READING — the fold and raise/globalshape.ts's
|
|
222
|
+
* stride reader both ask here, and both before any pair is rewritten; a reader that took a pair the
|
|
223
|
+
* fold leaves raw would license an element scale that no pass below legalizes. The TARGET half of
|
|
224
|
+
* the gate is the caller's: the pass list gates the fold, and globalshape asks
|
|
225
|
+
* {@link foldsShiftPairCasts} before it reads. */
|
|
226
|
+
export function foldablePairs(fn: Fn, defs: Map<Value, Op> = defOpMap(fn)): Map<Op, ScaledExtension> {
|
|
227
|
+
const out = new Map<Op, ScaledExtension>();
|
|
228
|
+
for (const b of fn.blocks) {
|
|
229
|
+
for (const op of b.ops) {
|
|
230
|
+
const m = scaledExtensionOf(op, defs);
|
|
231
|
+
if (m !== null && !b.ops.some((o) => o !== op && narrowsLike(o, m, defs))) {
|
|
232
|
+
out.set(op, m);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What the fold made, for the two passes that read it later: raise/paramwidth.ts's
|
|
240
|
+
* `fused-behind-pool` gate and {@link restoreUnclaimedScales}. One per pre-recovery run
|
|
241
|
+
* (`PreRecoveryFacts.scales`), keyed by op identity. */
|
|
242
|
+
export interface ScaleRecord {
|
|
243
|
+
/** each scale the fold made → the extension it reads and the pair it replaced */
|
|
244
|
+
folded: Map<Op, { ext: Op; l: number; r: number }>;
|
|
245
|
+
/** the extensions over an entry parameter whose `shl` the machine ran behind a pool load */
|
|
246
|
+
behindPool: Set<Op>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const emptyScaleRecord = (): ScaleRecord => ({ folded: new Map(), behindPool: new Set() });
|
|
250
|
+
|
|
251
|
+
/** Rewrite every fused pair to `shl(ext(src), shift)`, the extension spliced in at the `shl`'s
|
|
252
|
+
* position and the scale at the right shift's, and write what it made into `record`. Returns the
|
|
253
|
+
* number of pairs folded; the `shl`s left without a reader are the pass driver's DCE. `order` is
|
|
254
|
+
* the lifted-order fact the driver read before `addrnum` (`PreRecoveryFacts.poolOrder`); the
|
|
255
|
+
* default reads it off `fn` itself, which is right only while `fn`'s entry block is still in lifted
|
|
256
|
+
* order — a test's parsed IR. */
|
|
257
|
+
export function foldScaledExtensions(
|
|
258
|
+
fn: Fn,
|
|
259
|
+
order: PoolOrder = poolOrderOf(fn),
|
|
260
|
+
record: ScaleRecord = emptyScaleRecord(),
|
|
261
|
+
): number {
|
|
262
|
+
const defs = defOpMap(fn);
|
|
263
|
+
const blockOf = new Map<Op, Block>();
|
|
264
|
+
for (const b of fn.blocks) {
|
|
265
|
+
for (const op of b.ops) {
|
|
266
|
+
blockOf.set(op, b);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const exts = new Map<Op, Map<boolean, Op>>();
|
|
270
|
+
let folded = 0;
|
|
271
|
+
for (const [op, m] of foldablePairs(fn, defs)) {
|
|
272
|
+
const bySign = exts.get(m.inner) ?? exts.set(m.inner, new Map()).get(m.inner)!;
|
|
273
|
+
let ext = bySign.get(m.signed);
|
|
274
|
+
if (ext === undefined) {
|
|
275
|
+
ext = mkOp(m.signed ? 'sext' : 'zext', {
|
|
276
|
+
operands: [m.src],
|
|
277
|
+
results: [mkValue(m.inner.results[0].type)],
|
|
278
|
+
attrs: { width: m.width },
|
|
279
|
+
});
|
|
280
|
+
const home = blockOf.get(m.inner)!;
|
|
281
|
+
home.ops.splice(home.ops.indexOf(m.inner), 0, ext);
|
|
282
|
+
if (order.entryParams.has(m.src) && order.afterPoolLoad.has(m.inner)) {
|
|
283
|
+
record.behindPool.add(ext);
|
|
284
|
+
}
|
|
285
|
+
bySign.set(m.signed, ext);
|
|
286
|
+
}
|
|
287
|
+
const scaled = mkOp('shl', {
|
|
288
|
+
operands: [ext.results[0]],
|
|
289
|
+
results: [mkValue(op.results[0].type)],
|
|
290
|
+
attrs: { imm: m.shift },
|
|
291
|
+
});
|
|
292
|
+
const b = blockOf.get(op)!;
|
|
293
|
+
b.ops.splice(b.ops.indexOf(op), 1, scaled);
|
|
294
|
+
replaceAllUsesWith(fn, op.results[0], scaled.results[0]);
|
|
295
|
+
record.folded.set(scaled, { ext, l: 32 - m.width, r: 32 - m.width - m.shift });
|
|
296
|
+
folded++;
|
|
297
|
+
}
|
|
298
|
+
return folded;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Put back the machine's own pair wherever no pass below claimed what the fold exposed. Returns
|
|
302
|
+
* the number of scales restored; an extension left without a reader is the driver's DCE.
|
|
303
|
+
*
|
|
304
|
+
* A scale is CLAIMED when it no longer reads the fold's extension — raise/paramwidth.ts or
|
|
305
|
+
* raise/narrowlocal.ts retyped the value and dropped the extension, so the scale now reads that
|
|
306
|
+
* value — or when it is gone, legalized into an element index by raise/arrays.ts or
|
|
307
|
+
* raise/struct-arrays.ts. Anything else still reads the extension, and is rewritten in place (same
|
|
308
|
+
* result value, same position) to `shr(shl(src, L), R)`, with the `shl` where the extension stood:
|
|
309
|
+
* the ops the frontend lifted, in the order it lifted them. */
|
|
310
|
+
export function restoreUnclaimedScales(fn: Fn, record: ScaleRecord): number {
|
|
311
|
+
const shls = new Map<Op, Op>();
|
|
312
|
+
let restored = 0;
|
|
313
|
+
for (const b of fn.blocks) {
|
|
314
|
+
for (const op of [...b.ops]) {
|
|
315
|
+
const f = record.folded.get(op);
|
|
316
|
+
if (f === undefined || op.opcode !== 'shl' || op.operands[0] !== f.ext.results[0]) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// The pair is rebuilt from the amounts RECORDED at the fold, so they must still be the ones
|
|
320
|
+
// the two ops carry: a pass that re-scaled the `shl` or re-widened the extension in place
|
|
321
|
+
// would make the recorded pair compute a different value. None does today; refuse, not guess.
|
|
322
|
+
const extended = f.ext.opcode === 'zext' || f.ext.opcode === 'sext';
|
|
323
|
+
if (!extended || f.ext.attrs.width !== 32 - f.l || op.attrs.imm !== f.l - f.r) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
let shl = shls.get(f.ext);
|
|
327
|
+
if (shl === undefined) {
|
|
328
|
+
const home = fn.blocks.find((x) => x.ops.includes(f.ext))!;
|
|
329
|
+
shl = mkOp('shl', {
|
|
330
|
+
operands: [f.ext.operands[0]],
|
|
331
|
+
results: [mkValue(f.ext.results[0].type)],
|
|
332
|
+
attrs: { imm: f.l },
|
|
333
|
+
});
|
|
334
|
+
home.ops.splice(home.ops.indexOf(f.ext), 0, shl);
|
|
335
|
+
shls.set(f.ext, shl);
|
|
336
|
+
}
|
|
337
|
+
op.opcode = f.ext.opcode === 'sext' ? 'shr_s' : 'shr_u';
|
|
338
|
+
op.operands = [shl.results[0]];
|
|
339
|
+
op.attrs = { imm: f.r };
|
|
340
|
+
record.folded.delete(op);
|
|
341
|
+
restored++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return restored;
|
|
345
|
+
}
|
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
|
);
|