@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/src/proto-context.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { demangledName, implicitArgumentWords } from './mangle';
|
|
|
20
20
|
import type { AggregateLayout, AggregateMember, FnProto, ParamType, Prototypes } from './proto';
|
|
21
21
|
import {
|
|
22
22
|
declaredCallArgs,
|
|
23
|
+
declaredSigned,
|
|
23
24
|
declaredWidth,
|
|
24
25
|
declaresAggregateReturn,
|
|
25
26
|
declaresVoidReturn,
|
|
@@ -115,8 +116,39 @@ export interface ContextGlobals {
|
|
|
115
116
|
* it. Read through `structNamed`, which is how two spellings of one struct are told to be one:
|
|
116
117
|
* `typedef struct Q {…} Q;` spells the struct `Q`, `struct Q` and every other alias of it. */
|
|
117
118
|
structTypes: ReadonlyMap<string, string>;
|
|
119
|
+
/** The storage each object of `declared` that is no aggregate and no function is declared with,
|
|
120
|
+
* its typedefs resolved (`declaredWidth`, `declaredSigned`): `extern s32 gN;` is a four-byte
|
|
121
|
+
* signed integer, `extern u8 *gP;` a four-byte one whose sign nothing reads, `extern float gF;`
|
|
122
|
+
* a four-byte float. A bare name reads and writes exactly this, so the structurer spells a
|
|
123
|
+
* global bare only where its one access agrees (structure.ts, the declaration-shape override): a
|
|
124
|
+
* byte read of `gN` is `*(u8 *)&gN`, never `gN`, and so is a halfword read of `gF`, which
|
|
125
|
+
* reinterprets the bytes where the bare name would convert them. A type this cannot read — a
|
|
126
|
+
* typedef carrying an attribute, an unexpanded macro, a name it never saw — is `unread`, which
|
|
127
|
+
* agrees with no access: the cast states the access whatever the declaration is. A pointer
|
|
128
|
+
* behind an attributed typedef is still read as one (`writtenDerivation`). A C++ reference is
|
|
129
|
+
* a `reference`, whose cell no spelling names, so the structurer declines a function that
|
|
130
|
+
* touches it. */
|
|
131
|
+
storage: ReadonlyMap<string, DeclaredStorage>;
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
/** What one declared object that is no aggregate occupies ({@link ContextGlobals.storage}). */
|
|
135
|
+
export type DeclaredStorage =
|
|
136
|
+
/** an integer or a pointer of `bytes` bytes; `signed` says whether a read sign-extends it, `char`
|
|
137
|
+
* for a plain `char`, whose sign is the compiler's (`compilerBehaviors.plainCharSigned`), and
|
|
138
|
+
* undefined where nothing reads it (a pointer). A declaration with no type specifier at all
|
|
139
|
+
* (`extern gN;`, C89's implicit `int`) is an `int`. */
|
|
140
|
+
| { kind: 'integer'; bytes: number; signed: boolean | 'char' | undefined }
|
|
141
|
+
/** an enum, whose size is the compiler's (`compilerBehaviors.enumBytes`), unless `unsized`: one
|
|
142
|
+
* `packed` or holding a value an int cannot (`TypeTable.unsizedEnums`) is no size this knows */
|
|
143
|
+
| { kind: 'enum'; unsized: boolean }
|
|
144
|
+
/** a `float` (4 bytes) or a `double` (8) */
|
|
145
|
+
| { kind: 'float'; bytes: number }
|
|
146
|
+
/** a C++ reference (`extern int &gRef;`): the symbol is a cell holding the referent's address,
|
|
147
|
+
* which no C++ spelling names — `gRef` and `&gRef` both name the referent */
|
|
148
|
+
| { kind: 'reference' }
|
|
149
|
+
/** a type this cannot read */
|
|
150
|
+
| { kind: 'unread' };
|
|
151
|
+
|
|
120
152
|
/** The struct or union `spelling` names, qualifiers aside, by the identity `structTypes` keys it
|
|
121
153
|
* under: `struct Tag` as written, or what a typedef name stands for. Undefined for any other
|
|
122
154
|
* spelling, and for a name the context does not resolve to one — which then names no struct this
|
|
@@ -142,10 +174,20 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
|
|
|
142
174
|
const pointeeVolatiles = new Set<string>();
|
|
143
175
|
const declared = new Set<string>();
|
|
144
176
|
const objectTypes = new Map<string, string>();
|
|
177
|
+
const storage = new Map<string, DeclaredStorage>();
|
|
145
178
|
for (const d of ctx.declarations) {
|
|
146
179
|
if (d.specifiers.typedef || d.body !== undefined) {
|
|
147
180
|
continue;
|
|
148
181
|
}
|
|
182
|
+
const implicit = language === 'c' ? implicitIntObject(d, table) : undefined;
|
|
183
|
+
if (implicit !== undefined) {
|
|
184
|
+
declared.add(implicit);
|
|
185
|
+
objectTypes.set(implicit, 'int');
|
|
186
|
+
storage.set(implicit, { kind: 'integer', bytes: 4, signed: true });
|
|
187
|
+
if (d.specifiers.qualifiers.includes('volatile')) {
|
|
188
|
+
volatiles.add(implicit);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
for (const x of d.declarators) {
|
|
150
192
|
if (x.name === undefined || !WORD.test(x.name)) {
|
|
151
193
|
continue;
|
|
@@ -159,6 +201,8 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
|
|
|
159
201
|
}
|
|
160
202
|
if (isAggregate(t, table, scope.typedefs)) {
|
|
161
203
|
aggregates.add(x.name);
|
|
204
|
+
} else if (t.derivations[0]?.kind !== 'function') {
|
|
205
|
+
storage.set(x.name, declaredStorage(t, table, scope.typedefs, ctx.tokens));
|
|
162
206
|
}
|
|
163
207
|
if (hasVolatileStorage(t, scope, 0)) {
|
|
164
208
|
volatiles.add(x.name);
|
|
@@ -179,7 +223,97 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
|
|
|
179
223
|
structTypes.set(name, named);
|
|
180
224
|
}
|
|
181
225
|
}
|
|
182
|
-
return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes };
|
|
226
|
+
return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes, storage };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The object a C declaration with a storage class and no type specifier declares, C89's implicit
|
|
230
|
+
* `int`: `extern gN;` reads as the type name `gN` with no declarator, since the parser cannot tell
|
|
231
|
+
* a name from an unknown type with nothing after it. Only a lone word that names no type the
|
|
232
|
+
* context declares and no base type is one. */
|
|
233
|
+
function implicitIntObject(d: Declaration, table: TypeTable): string | undefined {
|
|
234
|
+
const { storage, type, unknownWords } = d.specifiers;
|
|
235
|
+
if (d.declarators.length > 0 || unknownWords.length > 0 || type.kind !== 'words' || type.words.length !== 1) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const [w] = type.words;
|
|
239
|
+
const named =
|
|
240
|
+
!WORD.test(w) ||
|
|
241
|
+
table.typedefs.has(w) ||
|
|
242
|
+
table.enums.has(w) ||
|
|
243
|
+
table.named.has(w) ||
|
|
244
|
+
declaredWidth(w) !== undefined ||
|
|
245
|
+
['void', 'float', 'double', '_Bool'].includes(w);
|
|
246
|
+
return !named && (storage.includes('extern') || storage.includes('static')) ? w : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The storage an object of type `t`, no aggregate and no function, is declared with
|
|
250
|
+
* ({@link DeclaredStorage}). `written` is every typedef as written, an attributed one included
|
|
251
|
+
* (`typedefTypes`), which `writtenDerivation` reads past. */
|
|
252
|
+
function declaredStorage(
|
|
253
|
+
t: DeclaredType,
|
|
254
|
+
table: TypeTable,
|
|
255
|
+
written: ReadonlyMap<string, DeclaredType>,
|
|
256
|
+
tokens: Tokens,
|
|
257
|
+
): DeclaredStorage {
|
|
258
|
+
const [first] = t.derivations;
|
|
259
|
+
if (first?.kind === 'reference') {
|
|
260
|
+
return { kind: 'reference' };
|
|
261
|
+
}
|
|
262
|
+
if (first !== undefined && !isPointer(first)) {
|
|
263
|
+
return { kind: 'unread' };
|
|
264
|
+
}
|
|
265
|
+
const r = resolve(t, table.typedefs, tokens);
|
|
266
|
+
// the derivation the type reaches, through a typedef the table does not read too
|
|
267
|
+
const behind = r.derivations[0] ?? writtenDerivation(r, written);
|
|
268
|
+
if (behind?.kind === 'reference') {
|
|
269
|
+
return { kind: 'reference' };
|
|
270
|
+
}
|
|
271
|
+
const spelled = spellType(r, tokens);
|
|
272
|
+
const bare = spelled
|
|
273
|
+
.replace(/\b(?:const|volatile)\b/g, ' ')
|
|
274
|
+
.trim()
|
|
275
|
+
.replace(/\s+/g, ' ');
|
|
276
|
+
const bits = declaredWidth(bare === '' ? 'int' : spelled);
|
|
277
|
+
if (bits !== undefined) {
|
|
278
|
+
const signed = bare === 'char' ? 'char' : declaredSigned(bare === '' ? 'int' : spelled);
|
|
279
|
+
return { kind: 'integer', bytes: bits / 8, signed };
|
|
280
|
+
}
|
|
281
|
+
if (r.derivations.length > 0) {
|
|
282
|
+
return { kind: 'unread' };
|
|
283
|
+
}
|
|
284
|
+
if (bare === 'float' || bare === 'double') {
|
|
285
|
+
return { kind: 'float', bytes: bare === 'float' ? 4 : 8 };
|
|
286
|
+
}
|
|
287
|
+
if (table.enums.has(bare) || (r.type.kind === 'tag' && r.type.keyword === 'enum')) {
|
|
288
|
+
return { kind: 'enum', unsized: table.unsizedEnums.has(bare) };
|
|
289
|
+
}
|
|
290
|
+
// A pointer behind a typedef that carries an attribute (`typedef u8 *P8
|
|
291
|
+
// __attribute__((aligned(4)));`), which the table leaves unread because the attribute may retype
|
|
292
|
+
// it (`realigns`). No attribute changes a pointer's width on these 32-bit targets — `aligned` and
|
|
293
|
+
// `__declspec(align)` move its alignment alone, and `mode` takes no pointer mode but the 4-byte
|
|
294
|
+
// one — so it is a 4-byte pointer, read and written bare as a word. Read `unread`, that word is
|
|
295
|
+
// cast to an integer, which mwcc refuses wherever it flows into a pointer (`gQ = *(s32 *)&gP;`).
|
|
296
|
+
return behind !== undefined && isPointer(behind)
|
|
297
|
+
? { kind: 'integer', bytes: 4, signed: undefined }
|
|
298
|
+
: { kind: 'unread' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The first derivation a type with none of its own reaches through the typedefs as written
|
|
302
|
+
* (`typedefTypes`), an attributed one and a reference the type table does not read included:
|
|
303
|
+
* `P8` over `typedef u8 *P8 __attribute__((aligned(4)));` is a pointer, `IR` over
|
|
304
|
+
* `typedef int &IR;` a reference. Undefined where the chain ends at a base type or a name it
|
|
305
|
+
* never saw. */
|
|
306
|
+
function writtenDerivation(t: DeclaredType, written: ReadonlyMap<string, DeclaredType>): Derivation | undefined {
|
|
307
|
+
let cur = t;
|
|
308
|
+
for (let hops = 0; hops < 16 && cur.derivations.length === 0; hops++) {
|
|
309
|
+
const name = nameOf(cur);
|
|
310
|
+
const next = name === undefined ? undefined : written.get(name);
|
|
311
|
+
if (next === undefined || nameOf(next) === name) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
cur = next;
|
|
315
|
+
}
|
|
316
|
+
return cur.derivations[0];
|
|
183
317
|
}
|
|
184
318
|
|
|
185
319
|
/** The struct or union the typedef `name` stands for (`ContextGlobals.structTypes`): its tag where
|
package/src/raise/const.ts
CHANGED
|
@@ -72,6 +72,31 @@ export function foldConstPair(opcode: string, a: number, b: number): number | nu
|
|
|
72
72
|
return f ? f(a, b) : null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** Integer opcodes whose const/const form a test can be read on, as int32: `foldConstPair`'s `add`
|
|
76
|
+
* and `or`, the rest of the arithmetic and the comparisons. `null` for any other opcode. Shared by
|
|
77
|
+
* the reroll's folded top test (`restoreTopTest`) and the `for` whose first test the compiler
|
|
78
|
+
* folded away (structure.ts, `forPlanAt`): which opcodes a test folds through is one decision. */
|
|
79
|
+
const EVAL: Readonly<Record<string, (a: number, b: number) => number>> = {
|
|
80
|
+
add: (a, b) => foldConstPair('add', a, b)!,
|
|
81
|
+
or: (a, b) => foldConstPair('or', a, b)!,
|
|
82
|
+
sub: (a, b) => (a - b) | 0,
|
|
83
|
+
and: (a, b) => a & b,
|
|
84
|
+
xor: (a, b) => a ^ b,
|
|
85
|
+
icmp_eq: (a, b) => +(a === b),
|
|
86
|
+
icmp_ne: (a, b) => +(a !== b),
|
|
87
|
+
icmp_slt: (a, b) => +(a < b),
|
|
88
|
+
icmp_sle: (a, b) => +(a <= b),
|
|
89
|
+
icmp_sgt: (a, b) => +(a > b),
|
|
90
|
+
icmp_sge: (a, b) => +(a >= b),
|
|
91
|
+
icmp_ult: (a, b) => +(a >>> 0 < b >>> 0),
|
|
92
|
+
icmp_ule: (a, b) => +(a >>> 0 <= b >>> 0),
|
|
93
|
+
icmp_ugt: (a, b) => +(a >>> 0 > b >>> 0),
|
|
94
|
+
icmp_uge: (a, b) => +(a >>> 0 >= b >>> 0),
|
|
95
|
+
};
|
|
96
|
+
export const evalIntPair = (opcode: string, a: number, b: number): number | null =>
|
|
97
|
+
opcode in EVAL ? EVAL[opcode](a, b) : null;
|
|
98
|
+
export const isIntPairOpcode = (opcode: string): boolean => opcode in EVAL;
|
|
99
|
+
|
|
75
100
|
/** Does this opcode's const/const form denote a constant? The membership half of `foldConstPair`,
|
|
76
101
|
* for callers that must classify an op before they have its operands' values. */
|
|
77
102
|
export const isConstFoldOpcode = (opcode: string): boolean => opcode in FOLD;
|
|
@@ -84,7 +84,7 @@ function addressThroughJoins(fn: Fn): (v: Value) => Value {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/** Every block parameter's incoming values, one per in-edge (the entry block's excepted). */
|
|
87
|
-
function joinIncoming(fn: Fn): Map<Value, Value[]> {
|
|
87
|
+
export function joinIncoming(fn: Fn): Map<Value, Value[]> {
|
|
88
88
|
const incoming = new Map<Value, Value[]>();
|
|
89
89
|
for (const b of fn.blocks) {
|
|
90
90
|
for (const op of b.ops) {
|
package/src/raise/divpow2.ts
CHANGED
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
// `2^k - 1` for the SAME k the shift uses, the guard must test the SAME value the arms divide, and
|
|
41
41
|
// the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
|
|
42
42
|
// because every one of those is a way for a superficially similar diamond to mean something else.
|
|
43
|
+
//
|
|
44
|
+
// ir/bits.ts `integerRange` (its `remainderPair`) reads the same SUNK diamond under a remainder,
|
|
45
|
+
// `x - (phi >> k << k)`, as one bounded term. It cannot wait for this pass's `sdiv`: the frontends'
|
|
46
|
+
// frame audit asks those ranges before runPreRecovery runs; and as a bound, not a rewrite, it holds
|
|
47
|
+
// for any bias in [0, 2^k) on any edge, so it does not take this pass's gates.
|
|
43
48
|
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
44
49
|
import { effectful } from '../ir/discipline';
|
|
45
50
|
import { T } from '../ir/types';
|
|
@@ -225,3 +230,190 @@ export function recognizeDivPow2(fn: Fn): boolean {
|
|
|
225
230
|
}
|
|
226
231
|
return changed;
|
|
227
232
|
}
|
|
233
|
+
|
|
234
|
+
// ── the signed REMAINDER by a power of two, in IDO's branching form ─────────────────────────
|
|
235
|
+
//
|
|
236
|
+
// `x % 2^k` truncates toward zero, so a negative x with low bits set takes `2^k` off its masked
|
|
237
|
+
// value. IDO masks first and corrects on a two-test branch (modpow2, `a % 16`):
|
|
238
|
+
//
|
|
239
|
+
// bgez a0, .L ; x >= 0: the mask is the remainder
|
|
240
|
+
// andi v0, a0, 15 ; (delay slot: runs on both paths)
|
|
241
|
+
// beqz v0, .L ; low bits clear: so is it
|
|
242
|
+
// nop
|
|
243
|
+
// addiu v0, v0, -16 ; else take 2^k off
|
|
244
|
+
// .L:
|
|
245
|
+
//
|
|
246
|
+
// Two heads and a correction arm reach one merge, so — like the quotient above — it is a shape in
|
|
247
|
+
// the control flow, out of the idiom patterns' reach. Lifted as written it is spelled
|
|
248
|
+
// `if (x < 0 && (x & 15) != 0) v = (x & 15) + -16; else v = x & 15;`, which IDO compiles to other
|
|
249
|
+
// code; `x % 16` compiles back to this diamond. The identity is exact for every signed x — the
|
|
250
|
+
// diamond IS C's `%` — and the round trip is self-verifying for the reason the quotient's is.
|
|
251
|
+
//
|
|
252
|
+
// IDENTITY-OR-DECLINE again: the mask must be exactly `2^k - 1` of the dividend the first test
|
|
253
|
+
// compares against zero, the second test must compare THAT mask against zero, the correction must
|
|
254
|
+
// be exactly `mask - 2^k` (`correctedMask`), and the corrected value must reach the merge only from the arm
|
|
255
|
+
// both tests failed into. The two inner blocks are deleted, so neither may do anything else.
|
|
256
|
+
// agbcc, gcc and kmc COMPILE `%` as a biased floor (`x - (t >> k << k)`), which ir/bits.ts
|
|
257
|
+
// `remainderPair` bounds and this does not touch; mwcc's is branch-free.
|
|
258
|
+
//
|
|
259
|
+
// THE PASS IS NOT GATED BY TARGET: a HAND-WRITTEN diamond of this shape on any compiler is folded
|
|
260
|
+
// to `%` too (agbcc compiles `r = i & 15; if (i < 0 && r != 0) r -= 16;` to the same two-test
|
|
261
|
+
// diamond, which lifts `i % 16`). The identity holds whatever wrote it, so the C is the same value;
|
|
262
|
+
// whether `%` recompiles to the target's bytes there is the bench's question, not this pass's.
|
|
263
|
+
|
|
264
|
+
/** `and x, const(2^k - 1)` → { x, k }, else null. The and is commutative. */
|
|
265
|
+
function maskOf(defs: Map<Value, Op>, v: Value): { x: Value; k: number } | null {
|
|
266
|
+
const d = defs.get(v);
|
|
267
|
+
if (!d || d.opcode !== 'and' || d.operands.length !== 2) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
for (const [a, b] of [
|
|
271
|
+
[0, 1],
|
|
272
|
+
[1, 0],
|
|
273
|
+
] as const) {
|
|
274
|
+
const c = defs.get(d.operands[b]);
|
|
275
|
+
const m = c?.opcode === 'const' ? (c.attrs.value as number) : undefined;
|
|
276
|
+
if (m !== undefined && m > 0 && (m & (m + 1)) === 0) {
|
|
277
|
+
return { x: d.operands[a], k: Math.log2(m + 1) };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The correction arm's value, `mask - 2^k` for the mask's own k, in either spelling the IR carries
|
|
284
|
+
* it: an `add` of the constant `-2^k` (IDO's `addiu v0, v0, -2^k`, where it fits an immediate:
|
|
285
|
+
* k < 16), or a `sub` of the constant `2^k` (k >= 16, where IDO materialises `2^k` as the mask's
|
|
286
|
+
* register plus one in the second test's delay slot, `addiu at, at, 1; subu v0, v0, at`, which
|
|
287
|
+
* the constant fold has made `2^k` by now). The identity is the subtraction, not its encoding. */
|
|
288
|
+
function correctedMask(defs: Map<Value, Op>, v: Value): { t: Value; mk: { x: Value; k: number } } | null {
|
|
289
|
+
const u = defs.get(v);
|
|
290
|
+
if ((u?.opcode !== 'add' && u?.opcode !== 'sub') || u.operands.length !== 2) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
// a sub's minuend is its first operand; an add's mask may stand on either side
|
|
294
|
+
const orders =
|
|
295
|
+
u.opcode === 'sub'
|
|
296
|
+
? ([[0, 1]] as const)
|
|
297
|
+
: ([
|
|
298
|
+
[0, 1],
|
|
299
|
+
[1, 0],
|
|
300
|
+
] as const);
|
|
301
|
+
for (const [a, b] of orders) {
|
|
302
|
+
const c = defs.get(u.operands[b]);
|
|
303
|
+
const mk = maskOf(defs, u.operands[a]);
|
|
304
|
+
const want = u.opcode === 'sub' ? 2 ** (mk?.k ?? 0) : -(2 ** (mk?.k ?? 0));
|
|
305
|
+
if (c?.opcode === 'const' && mk !== null && c.attrs.value === want) {
|
|
306
|
+
return { t: u.operands[a], mk };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** `icmp <op> v, const 0` → the compare's opcode, else null. */
|
|
313
|
+
function zeroTest(defs: Map<Value, Op>, c: Value, v: Value): string | null {
|
|
314
|
+
const d = defs.get(c);
|
|
315
|
+
if (!d || d.operands.length !== 2 || d.operands[0] !== v) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const z = defs.get(d.operands[1]);
|
|
319
|
+
return z?.opcode === 'const' && z.attrs.value === 0 ? d.opcode : null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fold IDO's branching signed-remainder-by-2^k diamond into `smod x, 2^k`. Returns true if the IR
|
|
324
|
+
* changed. Leaves the mask, the compares and the correction behind for the driver's DCE.
|
|
325
|
+
*/
|
|
326
|
+
export function recognizeSremPow2(fn: Fn): boolean {
|
|
327
|
+
let changed = false;
|
|
328
|
+
let progress = true;
|
|
329
|
+
while (progress) {
|
|
330
|
+
progress = false;
|
|
331
|
+
const defs = defOpMap(fn);
|
|
332
|
+
const preds = predecessors(fn);
|
|
333
|
+
const term = (b: Block) => b.ops[b.ops.length - 1];
|
|
334
|
+
// a result of `blocks`' ops read anywhere outside them, which deleting them would orphan
|
|
335
|
+
const escapes = (blocks: Block[]): boolean => {
|
|
336
|
+
const inside = new Set(blocks.flatMap((b) => b.ops.flatMap((op) => op.results)));
|
|
337
|
+
return fn.blocks
|
|
338
|
+
.filter((b) => !blocks.includes(b))
|
|
339
|
+
.some((b) =>
|
|
340
|
+
b.ops.some(
|
|
341
|
+
(op) =>
|
|
342
|
+
op.operands.some((v) => inside.has(v)) || op.successors.some((s) => s.args.some((v) => inside.has(v))),
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
outer: for (const m of fn.blocks) {
|
|
347
|
+
// the merge holds the remainder alone and is reached by exactly the diamond's three edges
|
|
348
|
+
// (the entry block has an invisible edge; see recognizeDivPow2)
|
|
349
|
+
const mp = preds.get(m) ?? [];
|
|
350
|
+
if (m.params.length !== 1 || mp.length !== 3 || new Set(mp).size !== 3 || m === fn.blocks[0]) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const fix of mp) {
|
|
354
|
+
// the CORRECTION arm: `mask - 2^k` to the merge, from the second test alone, nothing else
|
|
355
|
+
const ft = term(fix);
|
|
356
|
+
if (ft.opcode !== 'br' || ft.successors[0]?.block !== m || fix === fn.blocks[0] || fix.ops.some(effectful)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const n = preds.get(fix) ?? [];
|
|
360
|
+
if (n.length !== 1 || !mp.includes(n[0]) || n[0] === fn.blocks[0]) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const inner = n[0];
|
|
364
|
+
const it = term(inner);
|
|
365
|
+
const hp = preds.get(inner) ?? [];
|
|
366
|
+
if (it.opcode !== 'cond_br' || hp.length !== 1 || !mp.includes(hp[0]) || inner.ops.some(effectful)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const head = hp[0];
|
|
370
|
+
const ht = term(head);
|
|
371
|
+
if (ht.opcode !== 'cond_br' || head === m || head === fix) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
// the corrected value: the mask less `2^k`
|
|
375
|
+
const corrected = correctedMask(defs, ft.successors[0].args[0]);
|
|
376
|
+
if (corrected === null || corrected.mk.k < 1 || corrected.mk.k > 30) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const { t, mk } = corrected;
|
|
380
|
+
// the first test: x >= 0 to the merge with the mask, else on to the second
|
|
381
|
+
const [hTaken, hFall] = ht.successors;
|
|
382
|
+
const hFirst = zeroTest(defs, ht.operands[0], mk.x);
|
|
383
|
+
const hm = hFirst === 'icmp_sge' ? hTaken : hFirst === 'icmp_slt' ? hFall : null;
|
|
384
|
+
const hn = hFirst === 'icmp_sge' ? hFall : hFirst === 'icmp_slt' ? hTaken : null;
|
|
385
|
+
if (hm?.block !== m || hn?.block !== inner || hm.args[0] !== t || hn.args.length !== 0) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// the second test: the mask == 0 to the merge with the mask, else to the correction
|
|
389
|
+
const [iTaken, iFall] = it.successors;
|
|
390
|
+
const iSecond = zeroTest(defs, it.operands[0], t);
|
|
391
|
+
const im = iSecond === 'icmp_eq' ? iTaken : iSecond === 'icmp_ne' ? iFall : null;
|
|
392
|
+
const ifx = iSecond === 'icmp_eq' ? iFall : iSecond === 'icmp_ne' ? iTaken : null;
|
|
393
|
+
if (im?.block !== m || ifx?.block !== fix || im.args[0] !== t || ifx.args.length !== 0) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (escapes([inner, fix])) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Rewrite: the head computes the remainder and goes straight to the merge.
|
|
400
|
+
const by = mkValue(T.s());
|
|
401
|
+
const r = mkValue(T.s());
|
|
402
|
+
head.ops.splice(
|
|
403
|
+
head.ops.length - 1,
|
|
404
|
+
0,
|
|
405
|
+
mkOp('const', { results: [by], attrs: { value: 2 ** mk.k } }),
|
|
406
|
+
mkOp('smod', { operands: [mk.x, by], results: [r] }),
|
|
407
|
+
);
|
|
408
|
+
head.ops[head.ops.length - 1] = mkOp('br', { successors: [{ block: m, args: [] }] });
|
|
409
|
+
replaceAllUsesWith(fn, m.params[0], r);
|
|
410
|
+
m.params = [];
|
|
411
|
+
fn.blocks = fn.blocks.filter((b) => b !== inner && b !== fix);
|
|
412
|
+
changed = true;
|
|
413
|
+
progress = true;
|
|
414
|
+
break outer;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return changed;
|
|
419
|
+
}
|