@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/pattern/engine.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
|
|
7
7
|
// mutation of a live value.
|
|
8
8
|
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
9
|
-
import {
|
|
9
|
+
import { deadOps, effectful, orderSensitive } from '../ir/discipline';
|
|
10
|
+
import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
10
11
|
import type { IrType } from '../ir/types';
|
|
11
12
|
import { T } from '../ir/types';
|
|
12
13
|
|
|
@@ -22,8 +23,11 @@ export type MatchNode =
|
|
|
22
23
|
args: MatchNode[];
|
|
23
24
|
}
|
|
24
25
|
| { bind: string } // bind this operand's VALUE to a name
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// this operand must equal a previously-bound value — or, when both are `const`, have its value:
|
|
27
|
+
// two `li rN,10` are one operand to C, as they are to the compiler that materialised both
|
|
28
|
+
| { same: string }
|
|
29
|
+
| { constImm: string } // this operand must be a `const`; bind its numeric VALUE to an imm name
|
|
30
|
+
| { constEq: ImmExpr }; // this operand must be a `const` whose value is this, modulo 2^32
|
|
27
31
|
|
|
28
32
|
// A tiny declarative arithmetic over bound immediate names. It keeps a *computed* replacement
|
|
29
33
|
// attribute DATA rather than a JS closure, so a strength-reduced idiom whose replacement is a
|
|
@@ -60,7 +64,16 @@ function evalImm(e: ImmExpr, imms: Map<string, number>): number {
|
|
|
60
64
|
// A replacement operand is either a previously-BOUND value (by name) or a SYNTHESIZED constant
|
|
61
65
|
// computed from the bound immediates (e.g. the derived multiplier `2^k+1`). A synthesized const
|
|
62
66
|
// is materialized as its own `const` op spliced in before the rewrite site.
|
|
63
|
-
|
|
67
|
+
// A NESTED replacement — an op of its own, spliced in before the root — lets an idiom fold to a
|
|
68
|
+
// two-op expression (`-(a < b)`) without a pattern per intermediate.
|
|
69
|
+
export type ReplaceArg = string | { constImm: ImmExpr } | Replacement;
|
|
70
|
+
|
|
71
|
+
export interface Replacement {
|
|
72
|
+
op: string;
|
|
73
|
+
args: ReplaceArg[];
|
|
74
|
+
attrs?: Record<string, number | ImmExpr>;
|
|
75
|
+
resultType?: IrType;
|
|
76
|
+
}
|
|
64
77
|
|
|
65
78
|
export interface RewritePattern {
|
|
66
79
|
id: string;
|
|
@@ -75,7 +88,7 @@ export interface RewritePattern {
|
|
|
75
88
|
// a computed divisor proof and lives as the bespoke raise/magicdiv.ts pass instead. The
|
|
76
89
|
// COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
|
|
77
90
|
// multiply-by-constant idioms.
|
|
78
|
-
replaceWith:
|
|
91
|
+
replaceWith: Replacement;
|
|
79
92
|
/** Two REPLACEMENT operand names, in the order the replacement RENDERS them, whose defs C leaves
|
|
80
93
|
* UNSEQUENCED against each other. A fold that collapses a multi-op idiom into one op drops its
|
|
81
94
|
* operands from several uses to one, which lets the structurer inline both at that one use — and
|
|
@@ -92,12 +105,18 @@ export interface RewritePattern {
|
|
|
92
105
|
* declaring pattern's compiler gate to that set; widening it fails loud. */
|
|
93
106
|
unsequencedRightFirst?: [string, string];
|
|
94
107
|
/** 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
|
|
108
|
+
* of the match survives the fold because something outside still reads it (a read by an op `dce`
|
|
109
|
+
* will delete does not count, as for `foldsOnlyDyingInterior`). On one of these the
|
|
96
110
|
* fold refuses that shape (`sharesInterior`, which carries the disassemblies); everywhere else
|
|
97
111
|
* it is byte-neutral and refusing it costs matches, so the list is opt-in and per-compiler. A
|
|
98
112
|
* compiler absent from it was either measured neutral or not measured — `validatePattern`
|
|
99
113
|
* refuses a name that is not in `applies.compilers`, where the fold cannot fire anyway. */
|
|
100
114
|
recomputesSharedInterior?: string[];
|
|
115
|
+
/** The fold fires only where its interior DIES with it, on every compiler: an interior op some
|
|
116
|
+
* live op outside the match still reads refuses it (a read by an op `dce` will delete does not
|
|
117
|
+
* count). For an idiom whose interior means something of its own to a later pass — a `bdnz`'s
|
|
118
|
+
* decremented counter is the loop's, not a subtraction to fold away. */
|
|
119
|
+
foldsOnlyDyingInterior?: true;
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
/** What the pattern layer reads off a target. STRUCTURAL on purpose: the pattern set is
|
|
@@ -144,6 +163,242 @@ export const CNTLZW_EQ0: RewritePattern = {
|
|
|
144
163
|
replaceWith: { op: 'icmp_eq', args: ['X', { constImm: 0 }], resultType: T.u(32) },
|
|
145
164
|
};
|
|
146
165
|
|
|
166
|
+
// ── mwcc's branch-free compares ─────────────────────────────────────────────────────────────
|
|
167
|
+
// CodeWarrior materialises a 0/1 compare with no branch and no set-on-compare instruction: either
|
|
168
|
+
// through the carry bit (the frontend reads each carry as the compare it is — frontend/ppc.ts
|
|
169
|
+
// `readCarry`), or with a bit trick on the operands. Each fold is an IDENTITY over every pair of
|
|
170
|
+
// 32-bit words (PPC `slw` by 32 is 0; mwcc-compare.test.ts and ppc-carry.test.ts evaluate both
|
|
171
|
+
// sides over the edge words).
|
|
172
|
+
// The compiled forms, at -O4,p (2.4.2 and 2.4.7 identical), a and b in r3 and r4:
|
|
173
|
+
//
|
|
174
|
+
// a <u b 2.4.x xor r0,b,a; cntlzw r0,r0; slw r0,b,r0; srwi r3,r0,31
|
|
175
|
+
// 2.3.3 subfc r3,b,a; subfe r0,r0,r0; neg r3,r0 -(CA - 1), CA = a >=u b
|
|
176
|
+
// a >=u b 2.4.x subf r0,b,a; orc r3,a,b; srwi r0,r0,1; subf r0,r0,r3; srwi r3,r0,31
|
|
177
|
+
// 2.3.3 subfc r0,b,a; li r0,-1; subfze r3,r0 CA itself
|
|
178
|
+
// a <s b 2.4.x xor r0,b,a; srawi r3,r0,1; and r0,r0,b; subf r0,r0,r3; srwi r3,r0,31
|
|
179
|
+
// 2.3.3 eqv r0,b,a; subfc r3,b,a; srwi r0,r0,31; addze r3,r0; clrlwi r3,r3,31
|
|
180
|
+
// a >=s b all srawi r6,a,31; srwi r5,b,31; subfc r0,b,a; adde r3,r6,r5
|
|
181
|
+
// -(a <u b) 2.4.x subfc r0,b,a; subfe r3,r0,r0 CA - 1
|
|
182
|
+
//
|
|
183
|
+
// `>` and `<=` are the same code with the operands swapped, and fold to `b < a` / `b >= a`. A
|
|
184
|
+
// constant operand is materialised once, into a register the trick reads twice — except in the
|
|
185
|
+
// `orc` form, which subtracts it as `addi rD,a,-K`, hence that form's second pattern. The signed
|
|
186
|
+
// and unsigned forms share no shape, so no fold reads one as the other.
|
|
187
|
+
const mwccOnly = { compilers: ['mwcc'] };
|
|
188
|
+
const C31 = { imm: 31 };
|
|
189
|
+
|
|
190
|
+
/** `shr_u(shl(b, clz(a ^ b)), 31)`: b's bit at the highest bit where a and b differ, set exactly
|
|
191
|
+
* when b is the larger. */
|
|
192
|
+
export const MWCC_CLZ_ULT: RewritePattern = {
|
|
193
|
+
id: 'mwcc-clz-ult',
|
|
194
|
+
applies: mwccOnly,
|
|
195
|
+
match: {
|
|
196
|
+
op: 'shr_u',
|
|
197
|
+
attrEquals: C31,
|
|
198
|
+
args: [
|
|
199
|
+
{
|
|
200
|
+
op: 'shl',
|
|
201
|
+
args: [{ bind: 'B' }, { op: 'clz', args: [{ op: 'xor', args: [{ bind: 'A' }, { same: 'B' }] }] }],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
replaceWith: { op: 'icmp_ult', args: ['A', 'B'], resultType: T.u(32) },
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** `shr_u((a | ~b) - ((a - b) >>u 1), 31)`, and its constant-b twin over `a + -K`. */
|
|
209
|
+
export const MWCC_ORC_UGE: RewritePattern[] = [
|
|
210
|
+
{
|
|
211
|
+
id: 'mwcc-orc-uge',
|
|
212
|
+
applies: mwccOnly,
|
|
213
|
+
match: {
|
|
214
|
+
op: 'shr_u',
|
|
215
|
+
attrEquals: C31,
|
|
216
|
+
args: [
|
|
217
|
+
{
|
|
218
|
+
op: 'sub',
|
|
219
|
+
args: [
|
|
220
|
+
{ op: 'or', args: [{ bind: 'A' }, { op: 'not', args: [{ bind: 'B' }] }] },
|
|
221
|
+
{ op: 'shr_u', attrEquals: { imm: 1 }, args: [{ op: 'sub', args: [{ same: 'A' }, { same: 'B' }] }] },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
replaceWith: { op: 'icmp_uge', args: ['A', 'B'], resultType: T.u(32) },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'mwcc-orc-uge-k',
|
|
230
|
+
applies: mwccOnly,
|
|
231
|
+
match: {
|
|
232
|
+
op: 'shr_u',
|
|
233
|
+
attrEquals: C31,
|
|
234
|
+
args: [
|
|
235
|
+
{
|
|
236
|
+
op: 'sub',
|
|
237
|
+
args: [
|
|
238
|
+
{ op: 'or', args: [{ bind: 'A' }, { op: 'not', args: [{ constImm: 'k' }] }] },
|
|
239
|
+
{
|
|
240
|
+
op: 'shr_u',
|
|
241
|
+
attrEquals: { imm: 1 },
|
|
242
|
+
args: [{ op: 'add', args: [{ same: 'A' }, { constEq: { op: '-', args: [0, { imm: 'k' }] } }] }],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
replaceWith: { op: 'icmp_uge', args: ['A', { constImm: { imm: 'k' } }], resultType: T.u(32) },
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
/** `shr_u(((a ^ b) >>s 1) - ((a ^ b) & b), 31)`. The `and` names b, so the two xor operand orders
|
|
253
|
+
* are two patterns: a commutative match commits to the first order that binds. */
|
|
254
|
+
const xorSlt = (first: string, second: string): RewritePattern => ({
|
|
255
|
+
id: `mwcc-xor-slt/${first}${second}`,
|
|
256
|
+
applies: mwccOnly,
|
|
257
|
+
match: {
|
|
258
|
+
op: 'shr_u',
|
|
259
|
+
attrEquals: C31,
|
|
260
|
+
args: [
|
|
261
|
+
{
|
|
262
|
+
op: 'sub',
|
|
263
|
+
args: [
|
|
264
|
+
{
|
|
265
|
+
op: 'shr_s',
|
|
266
|
+
attrEquals: { imm: 1 },
|
|
267
|
+
args: [{ op: 'xor', ordered: true, args: [{ bind: first }, { bind: second }] }],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
op: 'and',
|
|
271
|
+
args: [{ op: 'xor', ordered: true, args: [{ same: first }, { same: second }] }, { same: 'B' }],
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
replaceWith: { op: 'icmp_slt', args: ['A', 'B'], resultType: T.u(32) },
|
|
278
|
+
});
|
|
279
|
+
export const MWCC_XOR_SLT: RewritePattern[] = [xorSlt('A', 'B'), xorSlt('B', 'A')];
|
|
280
|
+
|
|
281
|
+
/** `(a >>s 31) + (b >>u 31) + (a >=u b)`, `adde` over the `subfc` carry: the sign words cancel
|
|
282
|
+
* when the signs agree, and decide the compare when they differ. */
|
|
283
|
+
export const MWCC_ADDE_SGE: RewritePattern = {
|
|
284
|
+
id: 'mwcc-adde-sge',
|
|
285
|
+
applies: mwccOnly,
|
|
286
|
+
match: {
|
|
287
|
+
op: 'add',
|
|
288
|
+
args: [
|
|
289
|
+
{ op: 'icmp_uge', args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
290
|
+
{
|
|
291
|
+
op: 'add',
|
|
292
|
+
args: [
|
|
293
|
+
{ op: 'shr_s', attrEquals: C31, args: [{ same: 'A' }] },
|
|
294
|
+
{ op: 'shr_u', attrEquals: C31, args: [{ same: 'B' }] },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
replaceWith: { op: 'icmp_sge', args: ['A', 'B'], resultType: T.u(32) },
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** 2.3.3's `((~(a ^ b) >>u 31) + (a >=u b)) & 1`, `addze` over the `subfc` carry: one plus the
|
|
303
|
+
* carry when the signs agree, the carry alone when they differ. */
|
|
304
|
+
export const MWCC_EQV_SLT: RewritePattern = {
|
|
305
|
+
id: 'mwcc-eqv-slt',
|
|
306
|
+
applies: mwccOnly,
|
|
307
|
+
match: {
|
|
308
|
+
op: 'and',
|
|
309
|
+
args: [
|
|
310
|
+
{
|
|
311
|
+
op: 'add',
|
|
312
|
+
args: [
|
|
313
|
+
{ op: 'icmp_uge', args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
314
|
+
{
|
|
315
|
+
op: 'shr_u',
|
|
316
|
+
attrEquals: C31,
|
|
317
|
+
args: [{ op: 'not', args: [{ op: 'xor', args: [{ same: 'A' }, { same: 'B' }] }] }],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
{ op: 'const', attrEquals: { value: 1 }, args: [] },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
replaceWith: { op: 'icmp_slt', args: ['A', 'B'], resultType: T.u(32) },
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** A carry less one (`subfe rD,rY,rY`): `cmp - 1` is `-(!cmp)`, and negated again (2.3.3's `neg`)
|
|
328
|
+
* `!cmp`. One of each per comparison of NEGATED_ICMP, since a carry is any of them (`addic -1`
|
|
329
|
+
* makes `!=`, `subfic 0` makes `==`); the negated form runs first, while the `- 1` is whole. */
|
|
330
|
+
const carryLessOne = (cmp: string): MatchNode => ({
|
|
331
|
+
op: 'add',
|
|
332
|
+
args: [
|
|
333
|
+
{ op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
334
|
+
{ op: 'const', attrEquals: { value: -1 }, args: [] },
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
const carryNot = (cmp: string): RewritePattern => ({
|
|
338
|
+
id: `mwcc-carry-not/${cmp}`,
|
|
339
|
+
applies: mwccOnly,
|
|
340
|
+
match: { op: 'neg', args: [carryLessOne(cmp)] },
|
|
341
|
+
replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
|
|
342
|
+
});
|
|
343
|
+
const carryMask = (cmp: string): RewritePattern => ({
|
|
344
|
+
id: `mwcc-carry-mask/${cmp}`,
|
|
345
|
+
applies: mwccOnly,
|
|
346
|
+
match: carryLessOne(cmp),
|
|
347
|
+
replaceWith: { op: 'neg', args: [{ op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) }] },
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
/** `(b - a) == 0` / `!= 0` is `a == b` / `a != b`: the form 2.3.3's `!=` takes through the carry
|
|
351
|
+
* (`subf r3,a,b; addic r0,r3,-1; subfe r3,r0,r3`), and every build's `==` (`subf; cntlzw; srwi`),
|
|
352
|
+
* measured byte-exact in this operand order for both on 2.3.3 and for `==` on 2.4.2. Only where
|
|
353
|
+
* the difference dies: a `bdnz` tests its decremented counter this way, and a record-form `subf.`
|
|
354
|
+
* its result. */
|
|
355
|
+
const subZeroTest = (test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
|
|
356
|
+
id: `mwcc-sub-zerotest/${test}`,
|
|
357
|
+
applies: mwccOnly,
|
|
358
|
+
foldsOnlyDyingInterior: true,
|
|
359
|
+
match: {
|
|
360
|
+
op: test,
|
|
361
|
+
args: [
|
|
362
|
+
{ op: 'sub', args: [{ bind: 'B' }, { bind: 'A' }] },
|
|
363
|
+
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
replaceWith: { op: test, args: ['A', 'B'], resultType: T.u(32) },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/** `-x == 0` / `-x != 0` is `x == 0` / `x != 0`: 2.3.3 materialises every `x != 0` as `neg; addic
|
|
370
|
+
* -1; subfe` (`a != 0`, `a > 0u`, `(a - b) != 0`), so the carry reads the negation. AFTER
|
|
371
|
+
* `subZeroTest`, which must not see the difference this exposes: 2.3.3's `a != b` is `subf; addic;
|
|
372
|
+
* subfe` with no `neg`, so `(a - b) != 0` keeps its subtraction. No dying gate: a `bdnz` counts down
|
|
373
|
+
* with a `sub`, never a `neg`. */
|
|
374
|
+
const negZeroTest = (test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
|
|
375
|
+
id: `mwcc-neg-zerotest/${test}`,
|
|
376
|
+
applies: mwccOnly,
|
|
377
|
+
match: {
|
|
378
|
+
op: test,
|
|
379
|
+
args: [
|
|
380
|
+
{ op: 'neg', args: [{ bind: 'X' }] },
|
|
381
|
+
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
replaceWith: { op: test, args: ['X', { constImm: 0 }], resultType: T.u(32) },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
/** The branch-free compare bundle. */
|
|
388
|
+
export const MWCC_COMPARE_PATTERNS: RewritePattern[] = [
|
|
389
|
+
MWCC_CLZ_ULT,
|
|
390
|
+
...MWCC_ORC_UGE,
|
|
391
|
+
...MWCC_XOR_SLT,
|
|
392
|
+
MWCC_ADDE_SGE,
|
|
393
|
+
MWCC_EQV_SLT,
|
|
394
|
+
...Object.keys(NEGATED_ICMP).map(carryNot),
|
|
395
|
+
...Object.keys(NEGATED_ICMP).map(carryMask),
|
|
396
|
+
subZeroTest('icmp_eq'),
|
|
397
|
+
subZeroTest('icmp_ne'),
|
|
398
|
+
negZeroTest('icmp_eq'),
|
|
399
|
+
negZeroTest('icmp_ne'),
|
|
400
|
+
];
|
|
401
|
+
|
|
147
402
|
// The PPC rotate mirror, as pure data: hardware only rotates LEFT, so mwcc spells `rotr(x, n)`
|
|
148
403
|
// as `rotlw(x, 32 - n)`. Lowering that literally emits `x << (32 - n) | x >> (32 - (32 - n))`,
|
|
149
404
|
// whose inner subtraction recompiles to an extra instruction; folding to `rotr(x, n)` lets the
|
|
@@ -358,11 +613,18 @@ export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16),
|
|
|
358
613
|
// Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
|
|
359
614
|
// with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
|
|
360
615
|
//
|
|
361
|
-
// The negation comes from THE shared
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
616
|
+
// The negation comes from THE shared tables (ir/opcodes.ts NEGATED_ICMP, and NEGATED_FCMP for a float
|
|
617
|
+
// compare, whose negation is the unordered twin: `!(a > b)` is true on a NaN, `a <= b` is not), so
|
|
618
|
+
// this fold, the MIPS `slt …; beqz` branch fold and the short-circuit diamond negation cannot
|
|
619
|
+
// disagree about what the opposite of a compare is. One pattern per comparison — a data-driven fold
|
|
620
|
+
// needs a fixed replacement opcode, so the tables are unrolled into one pattern per entry rather
|
|
621
|
+
// than expressed as a (nonexistent) computed-opcode replacement.
|
|
622
|
+
const NEGATED_CMP: Readonly<Record<string, Opcode>> = { ...NEGATED_ICMP, ...NEGATED_FCMP };
|
|
623
|
+
// An integer compare's result is pinned u32 (below). A float compare's is the untyped word both
|
|
624
|
+
// frontends give one (raise/recover.ts stamps no float compare), not the `xor`'s or the zero test's:
|
|
625
|
+
// mwcc's `!(a > b)` is a `cntlzw` test that CNTLZW_EQ0 has already typed u32, and a u32 result
|
|
626
|
+
// would declare an `int` function `u32`.
|
|
627
|
+
const cmpResultType = (cmp: string): IrType => (cmp in NEGATED_ICMP ? T.u(32) : T.unk(32));
|
|
366
628
|
const notCmpPat = (cmp: string): RewritePattern => ({
|
|
367
629
|
id: `not-${cmp}`,
|
|
368
630
|
applies: {},
|
|
@@ -377,7 +639,7 @@ const notCmpPat = (cmp: string): RewritePattern => ({
|
|
|
377
639
|
// It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
|
|
378
640
|
// the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
|
|
379
641
|
// patterns on one discipline instead of leaving a reader to infer which is canonical.
|
|
380
|
-
replaceWith: { op:
|
|
642
|
+
replaceWith: { op: NEGATED_CMP[cmp], args: ['A', 'B'], resultType: cmpResultType(cmp) },
|
|
381
643
|
});
|
|
382
644
|
|
|
383
645
|
// The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
|
|
@@ -400,15 +662,35 @@ const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern =>
|
|
|
400
662
|
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
401
663
|
],
|
|
402
664
|
},
|
|
403
|
-
replaceWith: { op: test === 'icmp_eq' ?
|
|
665
|
+
replaceWith: { op: test === 'icmp_eq' ? NEGATED_CMP[cmp] : cmp, args: ['A', 'B'], resultType: cmpResultType(cmp) },
|
|
404
666
|
});
|
|
405
667
|
|
|
668
|
+
// KNOWN GAP: `!(a == b)` folds to `fcmp_une`, which C spells `a != b` — one program, which mwcc
|
|
669
|
+
// compiles apart (`cntlzw` of the EQ bit for the first, `xori` for the second), so it recompiles to
|
|
670
|
+
// the other object. Unfolded it would be no better: the zero test is CNTLZW_EQ0's u32, and a u32
|
|
671
|
+
// result declares an `int` function `u32`.
|
|
672
|
+
const notCmpPatterns = (
|
|
673
|
+
table: Readonly<Record<string, Opcode>>,
|
|
674
|
+
tests: readonly ('icmp_eq' | 'icmp_ne')[],
|
|
675
|
+
): RewritePattern[] => [
|
|
676
|
+
...Object.keys(table).map(notCmpPat),
|
|
677
|
+
...Object.keys(table).flatMap((cmp) => tests.map((t) => cmpZeroPat(cmp, t))),
|
|
678
|
+
];
|
|
679
|
+
/** The float compares' share of `NOT_CMP_PATTERNS`, which raise/pre-recovery.ts runs again once
|
|
680
|
+
* agbcc's compare helpers have folded to float compares: the idioms run before that pass, when
|
|
681
|
+
* `__ltsf2(a, b) >> 31 ^ 1` has no compare to negate yet. WITHOUT `cmp != 0 → cmp`, which is how
|
|
682
|
+
* PowerPC's record-form extract of a compare's bit reaches a branch (`mfcr; srwi. r0,r0,31;
|
|
683
|
+
* bnelr`): mwcc 2.4.2 and 2.4.7 compile `if (!(a < b)) gI = 1;` to that. Folded through it, `if
|
|
684
|
+
* (!(a0 < a1)) { gI = 1; return; }` recompiles to a plain `bltlr`; unfolded, `if (a0 < a1 == 0) {
|
|
685
|
+
* …; return; }` recompiles to the extract (`synthetic:fcmpbrn`). */
|
|
686
|
+
export const NOT_FCMP_PATTERNS: RewritePattern[] = notCmpPatterns(NEGATED_FCMP, ['icmp_eq']);
|
|
406
687
|
/** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
|
|
407
|
-
* entry is one comparison of `NEGATED_ICMP
|
|
408
|
-
*
|
|
688
|
+
* entry is one comparison of `NEGATED_ICMP` or `NEGATED_FCMP` (a float compare's `!= 0` aside,
|
|
689
|
+
* `NOT_FCMP_PATTERNS`); the bundle is what the boolean-reasoning consumers downstream
|
|
690
|
+
* (short-circuit recovery, the structurer's condition spelling) actually match on. */
|
|
409
691
|
export const NOT_CMP_PATTERNS: RewritePattern[] = [
|
|
410
|
-
...
|
|
411
|
-
...
|
|
692
|
+
...notCmpPatterns(NEGATED_ICMP, ['icmp_eq', 'icmp_ne']),
|
|
693
|
+
...NOT_FCMP_PATTERNS,
|
|
412
694
|
];
|
|
413
695
|
|
|
414
696
|
// The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
|
|
@@ -424,6 +706,7 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
|
424
706
|
SDIV_POW2_2,
|
|
425
707
|
...HWMOD_PATTERNS,
|
|
426
708
|
CNTLZW_EQ0,
|
|
709
|
+
...MWCC_COMPARE_PATTERNS,
|
|
427
710
|
// AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
|
|
428
711
|
// negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
|
|
429
712
|
...NOT_CMP_PATTERNS,
|
|
@@ -449,13 +732,27 @@ interface Binds {
|
|
|
449
732
|
interior: Set<Value>;
|
|
450
733
|
}
|
|
451
734
|
|
|
735
|
+
/** Two constants that are the same 32-bit word (`-1` and `0xffffffff` are one register value). */
|
|
736
|
+
function sameWord(x: unknown, y: unknown): boolean {
|
|
737
|
+
return typeof x === 'number' && typeof y === 'number' && x >>> 0 === y >>> 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
452
740
|
function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
|
|
453
741
|
if ('bind' in node) {
|
|
454
742
|
b.values.set(node.bind, v);
|
|
455
743
|
return true;
|
|
456
744
|
}
|
|
457
745
|
if ('same' in node) {
|
|
458
|
-
|
|
746
|
+
const bound = b.values.get(node.same);
|
|
747
|
+
if (bound === v) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
const [x, y] = [bound && defs.get(bound), defs.get(v)];
|
|
751
|
+
return x?.opcode === 'const' && y?.opcode === 'const' && sameWord(x.attrs.value, y.attrs.value);
|
|
752
|
+
}
|
|
753
|
+
if ('constEq' in node) {
|
|
754
|
+
const d = defs.get(v);
|
|
755
|
+
return d?.opcode === 'const' && sameWord(d.attrs.value, evalImm(node.constEq, b.imms));
|
|
459
756
|
}
|
|
460
757
|
if ('constImm' in node) {
|
|
461
758
|
const d = defs.get(v);
|
|
@@ -589,17 +886,18 @@ function validatePattern(pat: RewritePattern): void {
|
|
|
589
886
|
* just its def, and the question has to be asked over the cones: one pure `+ 1` between an effect
|
|
590
887
|
* and the fold's operand is the difference between `f() % g()` and `(f() + 1) % g()`, and the
|
|
591
888
|
* second one reorders exactly as the first does. Both cones become operands of ONE expression,
|
|
592
|
-
* where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
889
|
+
* where C leaves their order unspecified. asmlift's inline-at-use model (structure/analysis.ts,
|
|
890
|
+
* the barrier scan) keeps two operands of one statement inline only in the order every corpus
|
|
891
|
+
* compiler gives back — a call before the reads beside it — but it judges the program the fold
|
|
892
|
+
* leaves, after the idiom's own ops are gone. The fold INVENTS the expression, so it checks the
|
|
893
|
+
* cones it joins itself.
|
|
597
894
|
*
|
|
598
|
-
* A cone's members are weighed by
|
|
895
|
+
* A cone's members are weighed by `orderSensitive`, not `effectful`: a memory read answers
|
|
599
896
|
* whichever stores ran before it, so hoisting a `load` over a `call` — one asmlift may itself be
|
|
600
897
|
* passing the loaded pointer to — changes the answer as surely as swapping two calls. Two READS
|
|
601
|
-
* are the exception and commute, the same fact the structurer states as "a load never bars
|
|
602
|
-
* load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted.
|
|
898
|
+
* are the exception and commute, the same fact the structurer states as "a plain load never bars
|
|
899
|
+
* a load", so a hazard needs an EFFECT on at least one side; `*p % *q` is admitted. Two `volatile`
|
|
900
|
+
* device reads do not commute, and the structurer names the first of them itself.
|
|
603
901
|
*
|
|
604
902
|
* Refuses only what it must — the alternative spelling (the idiom written out) names the operands
|
|
605
903
|
* and states the order, so a refusal is a loud, correct, slightly-worse-scoring answer. */
|
|
@@ -654,20 +952,18 @@ function reordersUnsequenced(
|
|
|
654
952
|
return cone;
|
|
655
953
|
};
|
|
656
954
|
const [lc, rc] = names.map(coneOf);
|
|
657
|
-
const sensitive = (c: Set<Op>) => [...c].filter(
|
|
955
|
+
const sensitive = (c: Set<Op>) => [...c].filter(orderSensitive);
|
|
658
956
|
for (const l of sensitive(lc)) {
|
|
659
957
|
for (const r of sensitive(rc)) {
|
|
660
958
|
// One op standing in BOTH cones is one evaluation and cannot be sequenced against itself; two
|
|
661
959
|
// READS commute; and a right-cone op the machine ALREADY runs first loses nothing.
|
|
662
|
-
if (l === r || at.get(l)! >= at.get(r)! || !(
|
|
960
|
+
if (l === r || at.get(l)! >= at.get(r)! || !(effectful(l) || effectful(r))) {
|
|
663
961
|
continue;
|
|
664
962
|
}
|
|
665
963
|
// The left one runs first, so an inlined `A op B` would swap them — UNLESS a sibling effect
|
|
666
964
|
// stands between, which the inline-at-use model refuses to cross, forcing a named temp at the
|
|
667
965
|
// def's own position. A cone member is no sibling: it is inlined into this very expression.
|
|
668
|
-
if (
|
|
669
|
-
!blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => EFFECTFUL_OPS.has(o.opcode) && !lc.has(o) && !rc.has(o))
|
|
670
|
-
) {
|
|
966
|
+
if (!blk.ops.slice(at.get(l)! + 1, at.get(r)!).some((o) => effectful(o) && !lc.has(o) && !rc.has(o))) {
|
|
671
967
|
return true;
|
|
672
968
|
}
|
|
673
969
|
}
|
|
@@ -711,11 +1007,11 @@ function reordersUnsequenced(
|
|
|
711
1007
|
*
|
|
712
1008
|
* Refusing leaves the raw ops standing, which is the spelling that was byte-exact before any fold
|
|
713
1009
|
* 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 {
|
|
1010
|
+
function sharesInterior(fn: Fn, root: Op, interior: Set<Value>, defs: Map<Value, Op>, dead?: Set<Op>): boolean {
|
|
715
1011
|
const matched = new Set([...interior].map((v) => defs.get(v)));
|
|
716
1012
|
for (const b of fn.blocks) {
|
|
717
1013
|
for (const o of b.ops) {
|
|
718
|
-
if (matched.has(o)) {
|
|
1014
|
+
if (matched.has(o) || dead?.has(o)) {
|
|
719
1015
|
continue; // a read from INSIDE the idiom is one this rewrite is replacing
|
|
720
1016
|
}
|
|
721
1017
|
for (const v of [...o.operands, ...o.successors.flatMap((x) => x.args)]) {
|
|
@@ -751,38 +1047,49 @@ export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget)
|
|
|
751
1047
|
if (!tryMatch(pat.match, op.results[0], defs, binds)) {
|
|
752
1048
|
continue;
|
|
753
1049
|
}
|
|
754
|
-
|
|
1050
|
+
// Both interior gates ask one question — does an interior op outlive the fold? — and both
|
|
1051
|
+
// skip a reader `dce` will delete: the stage DCEs only after every pattern ran, so an op an
|
|
1052
|
+
// earlier fold orphaned still reads the interior here and keeps nothing alive.
|
|
1053
|
+
const askInterior = pat.foldsOnlyDyingInterior || pat.recomputesSharedInterior?.includes(target.compiler);
|
|
1054
|
+
if (askInterior && sharesInterior(fn, op, binds.interior, defs, deadOps(fn.blocks))) {
|
|
755
1055
|
continue;
|
|
756
1056
|
}
|
|
757
1057
|
if (pat.unsequencedRightFirst && reordersUnsequenced(fn, op, pat.unsequencedRightFirst, binds, defs, pat.id)) {
|
|
758
1058
|
continue;
|
|
759
1059
|
}
|
|
760
|
-
// Materialize
|
|
1060
|
+
// Materialize the replacement: synthesized constants and nested ops become their own ops,
|
|
761
1061
|
// spliced in before the rewrite; bound-value operands resolve from the value binds.
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1062
|
+
const spliced: Op[] = [];
|
|
1063
|
+
const build = (rw: Replacement, type: IrType): Op => {
|
|
1064
|
+
const operands: Value[] = rw.args.map((a) => {
|
|
1065
|
+
if (typeof a === 'string') {
|
|
1066
|
+
// Attribute a malformed pattern HERE with its id — an unbound value would otherwise
|
|
1067
|
+
// become `undefined` and detonate stages later; patterns are meant to become
|
|
1068
|
+
// AI-generated data, so diagnosability is first-class.
|
|
1069
|
+
const bound = binds.values.get(a);
|
|
1070
|
+
if (!bound) {
|
|
1071
|
+
throw new Error(`pattern '${pat.id}' replaceWith references unbound value '${a}'`);
|
|
1072
|
+
}
|
|
1073
|
+
return bound;
|
|
1074
|
+
}
|
|
1075
|
+
if ('constImm' in a) {
|
|
1076
|
+
const cv = mkValue(T.s());
|
|
1077
|
+
spliced.push(mkOp('const', { results: [cv], attrs: { value: evalImm(a.constImm, binds.imms) } }));
|
|
1078
|
+
return cv;
|
|
773
1079
|
}
|
|
774
|
-
|
|
1080
|
+
const inner = build(a, a.resultType ?? T.unk(32));
|
|
1081
|
+
spliced.push(inner);
|
|
1082
|
+
return inner.results[0];
|
|
1083
|
+
});
|
|
1084
|
+
const attrs: Record<string, number> = {};
|
|
1085
|
+
for (const [k, v] of Object.entries(rw.attrs ?? {})) {
|
|
1086
|
+
attrs[k] = typeof v === 'number' ? v : evalImm(v, binds.imms);
|
|
775
1087
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
for (const [k, v] of Object.entries(rw.attrs ?? {})) {
|
|
782
|
-
attrs[k] = typeof v === 'number' ? v : evalImm(v, binds.imms);
|
|
783
|
-
}
|
|
784
|
-
const newOp = mkOp(rw.op as Opcode, { operands, results: [newRes], attrs }); // pattern data boundary: verify() rejects unknowns
|
|
785
|
-
b.ops.splice(i, 1, ...consts, newOp);
|
|
1088
|
+
return mkOp(rw.op as Opcode, { operands, results: [mkValue(type)], attrs }); // pattern data boundary: verify() rejects unknowns
|
|
1089
|
+
};
|
|
1090
|
+
const newOp = build(pat.replaceWith, pat.replaceWith.resultType ?? op.results[0].type);
|
|
1091
|
+
const newRes = newOp.results[0];
|
|
1092
|
+
b.ops.splice(i, 1, ...spliced, newOp);
|
|
786
1093
|
replaceAllUsesWith(fn, op.results[0], newRes);
|
|
787
1094
|
count++;
|
|
788
1095
|
changed = true;
|
|
@@ -793,31 +1100,15 @@ export function applyPattern(fn: Fn, pat: RewritePattern, target: PatternTarget)
|
|
|
793
1100
|
return count;
|
|
794
1101
|
}
|
|
795
1102
|
|
|
796
|
-
/** Remove effect-free ops whose single result is unused, to a fixed point.
|
|
797
|
-
*
|
|
1103
|
+
/** Remove effect-free ops whose single result is unused, to a fixed point: ir/discipline.ts
|
|
1104
|
+
* `deadOps`, which keeps a read the lift pinned `volatile` (the machine made it, so the recompile
|
|
1105
|
+
* has to) and is the same set the dead-read pin (frontend/dead-reads.ts) calls dead. */
|
|
798
1106
|
export function dce(fn: Fn): void {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
for (const o of op.operands) {
|
|
806
|
-
used.add(o);
|
|
807
|
-
}
|
|
808
|
-
for (const s of op.successors) {
|
|
809
|
-
for (const a of s.args) {
|
|
810
|
-
used.add(a);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
for (const b of fn.blocks) {
|
|
816
|
-
const kept = b.ops.filter((op) => !(isDceSafe(op.opcode) && op.results.length === 1 && !used.has(op.results[0])));
|
|
817
|
-
if (kept.length !== b.ops.length) {
|
|
818
|
-
b.ops = kept;
|
|
819
|
-
changed = true;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
1107
|
+
const dead = deadOps(fn.blocks);
|
|
1108
|
+
if (dead.size === 0) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
for (const b of fn.blocks) {
|
|
1112
|
+
b.ops = b.ops.filter((op) => !dead.has(op));
|
|
822
1113
|
}
|
|
823
1114
|
}
|