@asmlift/core 0.9.0 → 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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/pattern/engine.ts
CHANGED
|
@@ -6,8 +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 {
|
|
10
|
-
import { NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
9
|
+
import { deadOps, effectful, orderSensitive } from '../ir/discipline';
|
|
10
|
+
import { NEGATED_FCMP, NEGATED_ICMP, type Opcode } from '../ir/opcodes';
|
|
11
11
|
import type { IrType } from '../ir/types';
|
|
12
12
|
import { T } from '../ir/types';
|
|
13
13
|
|
|
@@ -23,8 +23,11 @@ export type MatchNode =
|
|
|
23
23
|
args: MatchNode[];
|
|
24
24
|
}
|
|
25
25
|
| { bind: string } // bind this operand's VALUE to a name
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
28
31
|
|
|
29
32
|
// A tiny declarative arithmetic over bound immediate names. It keeps a *computed* replacement
|
|
30
33
|
// attribute DATA rather than a JS closure, so a strength-reduced idiom whose replacement is a
|
|
@@ -61,7 +64,16 @@ function evalImm(e: ImmExpr, imms: Map<string, number>): number {
|
|
|
61
64
|
// A replacement operand is either a previously-BOUND value (by name) or a SYNTHESIZED constant
|
|
62
65
|
// computed from the bound immediates (e.g. the derived multiplier `2^k+1`). A synthesized const
|
|
63
66
|
// is materialized as its own `const` op spliced in before the rewrite site.
|
|
64
|
-
|
|
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
|
+
}
|
|
65
77
|
|
|
66
78
|
export interface RewritePattern {
|
|
67
79
|
id: string;
|
|
@@ -76,7 +88,7 @@ export interface RewritePattern {
|
|
|
76
88
|
// a computed divisor proof and lives as the bespoke raise/magicdiv.ts pass instead. The
|
|
77
89
|
// COMPUTED-attr half of the envelope (ImmExpr, above) IS built — earned by the
|
|
78
90
|
// multiply-by-constant idioms.
|
|
79
|
-
replaceWith:
|
|
91
|
+
replaceWith: Replacement;
|
|
80
92
|
/** Two REPLACEMENT operand names, in the order the replacement RENDERS them, whose defs C leaves
|
|
81
93
|
* UNSEQUENCED against each other. A fold that collapses a multi-op idiom into one op drops its
|
|
82
94
|
* operands from several uses to one, which lets the structurer inline both at that one use — and
|
|
@@ -93,12 +105,18 @@ export interface RewritePattern {
|
|
|
93
105
|
* declaring pattern's compiler gate to that set; widening it fails loud. */
|
|
94
106
|
unsequencedRightFirst?: [string, string];
|
|
95
107
|
/** The compilers measured to RECOMPUTE this idiom instead of CSEing it back, when an interior op
|
|
96
|
-
* 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
|
|
97
110
|
* fold refuses that shape (`sharesInterior`, which carries the disassemblies); everywhere else
|
|
98
111
|
* it is byte-neutral and refusing it costs matches, so the list is opt-in and per-compiler. A
|
|
99
112
|
* compiler absent from it was either measured neutral or not measured — `validatePattern`
|
|
100
113
|
* refuses a name that is not in `applies.compilers`, where the fold cannot fire anyway. */
|
|
101
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;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
/** What the pattern layer reads off a target. STRUCTURAL on purpose: the pattern set is
|
|
@@ -145,6 +163,242 @@ export const CNTLZW_EQ0: RewritePattern = {
|
|
|
145
163
|
replaceWith: { op: 'icmp_eq', args: ['X', { constImm: 0 }], resultType: T.u(32) },
|
|
146
164
|
};
|
|
147
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
|
+
|
|
148
402
|
// The PPC rotate mirror, as pure data: hardware only rotates LEFT, so mwcc spells `rotr(x, n)`
|
|
149
403
|
// as `rotlw(x, 32 - n)`. Lowering that literally emits `x << (32 - n) | x >> (32 - (32 - n))`,
|
|
150
404
|
// whose inner subtraction recompiles to an extra instruction; folding to `rotr(x, n)` lets the
|
|
@@ -359,11 +613,18 @@ export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16),
|
|
|
359
613
|
// Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
|
|
360
614
|
// with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
|
|
361
615
|
//
|
|
362
|
-
// The negation comes from THE shared
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
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));
|
|
367
628
|
const notCmpPat = (cmp: string): RewritePattern => ({
|
|
368
629
|
id: `not-${cmp}`,
|
|
369
630
|
applies: {},
|
|
@@ -378,7 +639,7 @@ const notCmpPat = (cmp: string): RewritePattern => ({
|
|
|
378
639
|
// It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
|
|
379
640
|
// the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
|
|
380
641
|
// patterns on one discipline instead of leaving a reader to infer which is canonical.
|
|
381
|
-
replaceWith: { op:
|
|
642
|
+
replaceWith: { op: NEGATED_CMP[cmp], args: ['A', 'B'], resultType: cmpResultType(cmp) },
|
|
382
643
|
});
|
|
383
644
|
|
|
384
645
|
// The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
|
|
@@ -401,15 +662,35 @@ const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern =>
|
|
|
401
662
|
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
402
663
|
],
|
|
403
664
|
},
|
|
404
|
-
replaceWith: { op: test === 'icmp_eq' ?
|
|
665
|
+
replaceWith: { op: test === 'icmp_eq' ? NEGATED_CMP[cmp] : cmp, args: ['A', 'B'], resultType: cmpResultType(cmp) },
|
|
405
666
|
});
|
|
406
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']);
|
|
407
687
|
/** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
|
|
408
|
-
* entry is one comparison of `NEGATED_ICMP
|
|
409
|
-
*
|
|
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. */
|
|
410
691
|
export const NOT_CMP_PATTERNS: RewritePattern[] = [
|
|
411
|
-
...
|
|
412
|
-
...
|
|
692
|
+
...notCmpPatterns(NEGATED_ICMP, ['icmp_eq', 'icmp_ne']),
|
|
693
|
+
...NOT_FCMP_PATTERNS,
|
|
413
694
|
];
|
|
414
695
|
|
|
415
696
|
// The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
|
|
@@ -425,6 +706,7 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
|
425
706
|
SDIV_POW2_2,
|
|
426
707
|
...HWMOD_PATTERNS,
|
|
427
708
|
CNTLZW_EQ0,
|
|
709
|
+
...MWCC_COMPARE_PATTERNS,
|
|
428
710
|
// AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
|
|
429
711
|
// negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
|
|
430
712
|
...NOT_CMP_PATTERNS,
|
|
@@ -450,13 +732,27 @@ interface Binds {
|
|
|
450
732
|
interior: Set<Value>;
|
|
451
733
|
}
|
|
452
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
|
+
|
|
453
740
|
function tryMatch(node: MatchNode, v: Value, defs: Map<Value, Op>, b: Binds): boolean {
|
|
454
741
|
if ('bind' in node) {
|
|
455
742
|
b.values.set(node.bind, v);
|
|
456
743
|
return true;
|
|
457
744
|
}
|
|
458
745
|
if ('same' in node) {
|
|
459
|
-
|
|
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));
|
|
460
756
|
}
|
|
461
757
|
if ('constImm' in node) {
|
|
462
758
|
const d = defs.get(v);
|
|
@@ -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;
|
|
773
1074
|
}
|
|
774
|
-
|
|
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;
|
|
1079
|
+
}
|
|
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,34 +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
|
-
*
|
|
798
|
-
*
|
|
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. */
|
|
799
1106
|
export function dce(fn: Fn): void {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
for (const o of op.operands) {
|
|
807
|
-
used.add(o);
|
|
808
|
-
}
|
|
809
|
-
for (const s of op.successors) {
|
|
810
|
-
for (const a of s.args) {
|
|
811
|
-
used.add(a);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
for (const b of fn.blocks) {
|
|
817
|
-
const kept = b.ops.filter(
|
|
818
|
-
(op) => !(deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])),
|
|
819
|
-
);
|
|
820
|
-
if (kept.length !== b.ops.length) {
|
|
821
|
-
b.ops = kept;
|
|
822
|
-
changed = true;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
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));
|
|
825
1113
|
}
|
|
826
1114
|
}
|