@asmlift/core 0.9.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/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- 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 +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -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 +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- 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 +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- 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/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- 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 +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
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
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// asmlift — the soft-float boundary (L1 recognition): where integer words become floats.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Four things cross it. A runtime helper whose op computes on floats (`isFloatHelper`) — the peer of
|
|
4
4
|
// `raise/widehelpers.ts`, gated by the target's runtime table rather than a hardware capability.
|
|
5
|
-
// A parameter an ordinary callee declares `double` (the frontend's `doubles` on the call).
|
|
5
|
+
// A parameter an ordinary callee declares `double` (the frontend's `doubles` on the call). The
|
|
6
|
+
// result of an ordinary callee declared to return a floating type (its `floatReturn`). And a
|
|
6
7
|
// double literal, two constant words, where the target states their order
|
|
7
8
|
// (`TargetDescription.doubleArgWords`) and its compiler reads a literal back
|
|
8
|
-
// (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all
|
|
9
|
+
// (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all four apart from the integer
|
|
9
10
|
// helpers is the value's KIND: the words are bits in integer registers, and the pass re-types them
|
|
10
11
|
// as floats, which only data flow may justify — see the function below.
|
|
12
|
+
import { constWord } from '../ir/bits';
|
|
11
13
|
import { Fn, Op, Value, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
12
14
|
import { doubleBits, doubleOf } from '../ir/float-bits';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
+
import { type Opcode, fcmpOfTruth } from '../ir/opcodes';
|
|
16
|
+
import { T, intWidth } from '../ir/types';
|
|
17
|
+
import { arrivesAsDeclared, floatSidesOf, helperOp, isFloatHelper, lookupHelper } from '../runtime-helpers';
|
|
15
18
|
import type { TargetDescription } from '../target';
|
|
16
19
|
import { RaiseUnsupportedError } from './errors';
|
|
17
20
|
import { isArgumentPair } from './pairparams';
|
|
@@ -34,17 +37,36 @@ import { isArgumentPair } from './pairparams';
|
|
|
34
37
|
* target's word order into an `fconst` (`a + 1.5` stages `0x3ff80000` in r2, its high word);
|
|
35
38
|
* - an argument the function also reads on its own, as a word or as one half of a pair;
|
|
36
39
|
* - a result read as a word or a half (`*(int *)&c` is a double's HIGH word, in r0), passed to
|
|
37
|
-
* any other call, or carried across an edge
|
|
40
|
+
* any other call, or carried across an edge;
|
|
41
|
+
* - a result NOTHING reads. Folded, it would be a dead float op, and `dce` would delete the
|
|
42
|
+
* arithmetic. Optimising (-O1, -O2), agbcc deletes a helper call whose result is dead
|
|
43
|
+
* (`s = a * b; g(); return 0;` calls no `__mulsf3`), so the result's one reader was a later call
|
|
44
|
+
* that took it in r0, and a guessed arity does not count a register a call wrote (frontend/ssa.ts
|
|
45
|
+
* `fallbackArgc`, `trimClobberedCallArgs`): `g1(a * b)` with `g1` undeclared reads as
|
|
46
|
+
* `__mulsf3(); return g1();`, and folded would print `return g1();`. A double's pair refuses
|
|
47
|
+
* that shape at the frontend, naming the half the next call reads; a single is one word, so it
|
|
48
|
+
* reaches here. At -O0 the rule is not asked: agbcc stores the result to the frame, which reads
|
|
49
|
+
* it, and nothing folds there, its arguments being read from frame homes. A frontend that reads
|
|
50
|
+
* those homes must re-measure this. A result some other op ALSO reads is not refused:
|
|
51
|
+
* `s = a * b; g(s); return s;` and `s = a * b; g(); return s;` are the same bytes on agbcc, and
|
|
52
|
+
* a declaration of `g` closes the trim's `g()` reading (`trimClobberedCallArgs`).
|
|
38
53
|
* A call refused takes every call its result feeds or is fed by with it, so what folds is a closed
|
|
39
|
-
* set of floats and verify's float rule holds by construction.
|
|
54
|
+
* set of floats and verify's float rule holds by construction.
|
|
55
|
+
*
|
|
56
|
+
* A CONVERSION HAS AN INTEGER ON ONE SIDE (`floatSidesOf`), and only its float side is held to the
|
|
57
|
+
* rule above: `(float)i` converts whatever integer `i` is, a parameter, a load or a sum, and
|
|
58
|
+
* `(int)x` hands an integer to anything. Its result must still be read, by the dead-result rule,
|
|
59
|
+
* and a float the function also reads as a word is no float source for it either. */
|
|
40
60
|
export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
41
61
|
const table = target.runtimeHelpers ?? {};
|
|
42
62
|
let sites: Op[] = [];
|
|
43
63
|
const consumers = new Map<Op, readonly number[]>();
|
|
44
64
|
const def = new Map<Value, Op>();
|
|
45
65
|
const users = new Map<Value, Array<Op | null>>(); // null: an edge argument
|
|
66
|
+
const returners = new Map<Op, string>(); // ordinary calls declared to return a floating type
|
|
46
67
|
const use = (v: Value, by: Op | null) => users.set(v, [...(users.get(v) ?? []), by]);
|
|
47
68
|
const helperOf = (op: Op) => (op.opcode === 'call' ? lookupHelper(table, String(op.attrs.target)) : undefined);
|
|
69
|
+
const sidesOf = (op: Op) => floatSidesOf(helperOf(op)!);
|
|
48
70
|
for (const block of fn.blocks) {
|
|
49
71
|
for (const op of block.ops) {
|
|
50
72
|
op.results.forEach((r) => def.set(r, op));
|
|
@@ -64,9 +86,12 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
64
86
|
} else if (!helper && op.opcode === 'call' && Array.isArray(op.attrs.doubles)) {
|
|
65
87
|
consumers.set(op, op.attrs.doubles as number[]);
|
|
66
88
|
}
|
|
89
|
+
if (!helper && op.opcode === 'call' && typeof op.attrs.floatReturn === 'string') {
|
|
90
|
+
returners.set(op, op.attrs.floatReturn);
|
|
91
|
+
}
|
|
67
92
|
}
|
|
68
93
|
}
|
|
69
|
-
if (!sites.length && !consumers.size) {
|
|
94
|
+
if (!sites.length && !consumers.size && !returners.size) {
|
|
70
95
|
return false;
|
|
71
96
|
}
|
|
72
97
|
// A projection nothing reads is not a read of a half: `dce` runs ahead of this pass only after a
|
|
@@ -78,7 +103,8 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
78
103
|
// same call as a word is a word read.
|
|
79
104
|
const floatUse = (u: Op | null, v: Value, calls: ReadonlySet<Op>): boolean =>
|
|
80
105
|
u !== null &&
|
|
81
|
-
(calls.has(u)
|
|
106
|
+
((calls.has(u) && sidesOf(u).operands) ||
|
|
107
|
+
(consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
|
|
82
108
|
const floatsOnly = (v: Value, calls: ReadonlySet<Op>) => (users.get(v) ?? []).every((u) => floatUse(u, v, calls));
|
|
83
109
|
const entry = fn.blocks[0].params;
|
|
84
110
|
// A `concat` of the same two slots in the same order as `d`: the frontend builds one afresh at each
|
|
@@ -98,24 +124,20 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
98
124
|
users.get(d.operands[0])!.every((u) => samePair(u, d) && floatsOnly(u.results[0], calls))
|
|
99
125
|
);
|
|
100
126
|
};
|
|
127
|
+
// A SINGLE AN ORDINARY CALLEE IS DECLARED TO RETURN is one whole word in the return register, so
|
|
128
|
+
// it is a float source as a helper's result is. A double is not: the frontend reads one word of
|
|
129
|
+
// it (`declaredReturnWidth` sizes no floating type), and that word is not the value.
|
|
130
|
+
const declaredSingle = (d: Op) => returners.get(d) === 'float' && intWidth(d.results[0].type) === 32;
|
|
101
131
|
const fromCall = (v: Value, calls: ReadonlySet<Op>) => {
|
|
102
132
|
const d = def.get(v);
|
|
103
|
-
return d !== undefined && calls.has(d);
|
|
133
|
+
return d !== undefined && ((calls.has(d) && sidesOf(d).result) || declaredSingle(d));
|
|
104
134
|
};
|
|
105
135
|
// A CONSTANT WORD, as a compiler may build one in two instructions: agbcc moves a word that is a
|
|
106
136
|
// shifted byte as `mov`+`lsl` (thumb.md:66, `thumb_shiftable_const` at thumb.c:61) and one in
|
|
107
137
|
// -255..-1 as `mov`+`neg` (thumb.md:88), so the high word of 2.0 reaches r3 as `0x80 << 23`. Its
|
|
108
|
-
// value, or undefined where `v` is no constant.
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (d?.opcode === 'const') {
|
|
112
|
-
return Number(d.attrs.value);
|
|
113
|
-
}
|
|
114
|
-
const shift = d?.opcode === 'shl' ? d.attrs.imm : undefined;
|
|
115
|
-
const w =
|
|
116
|
-
d?.operands.length === 1 && (d.opcode === 'neg' || typeof shift === 'number') ? wordOf(d.operands[0]) : undefined;
|
|
117
|
-
return w === undefined ? undefined : d!.opcode === 'neg' ? -w | 0 : (w << (shift as number)) | 0;
|
|
118
|
-
};
|
|
138
|
+
// value, or undefined where `v` is no constant — the shared word evaluator's answer (ir/bits.ts).
|
|
139
|
+
const words = { defs: def };
|
|
140
|
+
const wordOf = (v: Value): number | undefined => constWord(words, v) ?? undefined;
|
|
119
141
|
// A LITERAL IS TWO CONSTANT WORDS, HIGH WORD FIRST on a target whose double crosses a call in
|
|
120
142
|
// general words (`TargetDescription.doubleArgWords`) — the order that makes the long long naming
|
|
121
143
|
// of the same pair another number. Its bits, or undefined where the pair is no literal, the target
|
|
@@ -138,21 +160,78 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
138
160
|
};
|
|
139
161
|
const literal = (v: Value, calls: ReadonlySet<Op>) => literalBits(v) !== undefined && floatsOnly(v, calls);
|
|
140
162
|
const source = (v: Value, calls: ReadonlySet<Op>) => argument(v, calls) || fromCall(v, calls) || literal(v, calls);
|
|
163
|
+
// A COMPARE FOLDS THROUGH THE TESTS OF ITS RESULT, which is libgcc's three-way answer: each reader
|
|
164
|
+
// must be a test of that integer against 0, and the compare the test then computes is read off
|
|
165
|
+
// the helper's contract — the sign that orders the operands, and its value on a NaN (`unordered`)
|
|
166
|
+
// — against `FCMP_TRUTH`. A test whose answer is no compare of the operands (always true, or true
|
|
167
|
+
// on both orders but not on equality), or a reader that is no test, refuses the helper.
|
|
168
|
+
const tests = new Map<Op, CompareTest[]>();
|
|
169
|
+
sites = sites.filter((op) => {
|
|
170
|
+
const unordered = helperOf(op)!.unordered;
|
|
171
|
+
if (unordered === undefined) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const found = compareTests(op.results[0], users, def, wordOf, unordered);
|
|
175
|
+
if (found !== null) {
|
|
176
|
+
tests.set(op, found);
|
|
177
|
+
}
|
|
178
|
+
return found !== null;
|
|
179
|
+
});
|
|
141
180
|
for (;;) {
|
|
142
181
|
const calls = new Set(sites);
|
|
143
|
-
const kept = sites.filter(
|
|
144
|
-
(op)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
const kept = sites.filter((op) => {
|
|
183
|
+
const sides = sidesOf(op);
|
|
184
|
+
const read = (users.get(op.results[0]) ?? []).filter((u) => !deadHalf(u));
|
|
185
|
+
return (
|
|
186
|
+
(!sides.operands || op.operands.every((o) => source(o, calls))) &&
|
|
187
|
+
read.length > 0 &&
|
|
188
|
+
(!sides.result || read.every((u) => floatUse(u, op.results[0], calls) || u?.opcode === 'ret'))
|
|
189
|
+
);
|
|
190
|
+
});
|
|
150
191
|
if (kept.length === sites.length) {
|
|
151
192
|
break;
|
|
152
193
|
}
|
|
153
194
|
sites = kept;
|
|
154
195
|
}
|
|
155
196
|
const calls = new Set(sites);
|
|
197
|
+
// A DECLARED FLOATING RETURN HAS NO FALLBACK EITHER. The candidate prints the call against its
|
|
198
|
+
// declaration, so its result stored, compared, narrowed, carried across an edge in an integer
|
|
199
|
+
// variable or handed to any other call is CONVERTED there — `u.f = f(); g = u.b[0];` lifted as
|
|
200
|
+
// `g = f();` compiles to `bl f; bl __fixsfsi` — where the bytes moved the bits. So a single may
|
|
201
|
+
// go only where a helper's result may (a helper the fold keeps, a `double` parameter, the
|
|
202
|
+
// return), and is retyped a float; a double read off its one word goes nowhere. A result nothing
|
|
203
|
+
// reads is a call statement, and stays one.
|
|
204
|
+
const returned = new Map<Op, Value>();
|
|
205
|
+
for (const [op, spelled] of returners) {
|
|
206
|
+
const r = op.results[0];
|
|
207
|
+
const read = (users.get(r) ?? []).filter((u) => !deadHalf(u));
|
|
208
|
+
if (read.length === 0) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!declaredSingle(op) || !read.every((u) => floatUse(u, r, calls) || u?.opcode === 'ret')) {
|
|
212
|
+
throw new RaiseUnsupportedError(
|
|
213
|
+
`cannot lift '${fn.name}': '${String(op.attrs.target)}' is declared to return ${spelled}, which this ` +
|
|
214
|
+
'target hands back as bits in a general register, and its result is read here as something ' +
|
|
215
|
+
'other than that float — a conversion the bytes do not make',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
returned.set(op, mkValue(T.f32()));
|
|
219
|
+
}
|
|
220
|
+
// A FLOAT RETURNED MAKES THE FUNCTION'S RETURN A FLOAT, so every other `ret` converts what it
|
|
221
|
+
// hands back: `return 5;` beside `return fget();` compiles to the bits of 5.0f where the asm
|
|
222
|
+
// hands back the word 5. Each value a `ret` reads must then be a float this pass makes — a kept
|
|
223
|
+
// helper's result or a declared single's.
|
|
224
|
+
const madeFloat = (v: Value) => {
|
|
225
|
+
const d = def.get(v);
|
|
226
|
+
return d !== undefined && (returned.has(d) || (calls.has(d) && sidesOf(d).result));
|
|
227
|
+
};
|
|
228
|
+
const rets = fn.blocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'ret' && op.operands.length > 0));
|
|
229
|
+
if (rets.some((r) => r.operands.some(madeFloat)) && !rets.every((r) => r.operands.every(madeFloat))) {
|
|
230
|
+
throw new RaiseUnsupportedError(
|
|
231
|
+
`cannot lift '${fn.name}': it returns a float on one path, and another return of this function hands ` +
|
|
232
|
+
'back an integer word, which C would convert to that float',
|
|
233
|
+
);
|
|
234
|
+
}
|
|
156
235
|
// A DECLARED DOUBLE HAS NO FALLBACK. A helper the fold refuses stays a call for
|
|
157
236
|
// `refuseUnmodelledHelpers` to gap, but an ordinary callee is no helper, and its two words handed
|
|
158
237
|
// on as integers are the wrong number — so a double parameter nothing here can hand a double
|
|
@@ -170,7 +249,7 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
170
249
|
}
|
|
171
250
|
}
|
|
172
251
|
}
|
|
173
|
-
if (!sites.length && !consumers.size) {
|
|
252
|
+
if (!sites.length && !consumers.size && !returned.size) {
|
|
174
253
|
return false;
|
|
175
254
|
}
|
|
176
255
|
const floatOf = new Map<Value, Value>();
|
|
@@ -209,23 +288,60 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
209
288
|
}
|
|
210
289
|
floatOf.set(o, whole);
|
|
211
290
|
};
|
|
291
|
+
for (const [op, now] of returned) {
|
|
292
|
+
floatOf.set(op.results[0], now);
|
|
293
|
+
}
|
|
212
294
|
for (const op of sites) {
|
|
213
295
|
const helper = helperOf(op)!;
|
|
214
|
-
|
|
215
|
-
|
|
296
|
+
const sides = floatSidesOf(helper);
|
|
297
|
+
if (sides.operands) {
|
|
298
|
+
op.operands.forEach((o, i) => retype(o, helper.params[i]));
|
|
299
|
+
}
|
|
300
|
+
if (sides.result) {
|
|
301
|
+
floatOf.set(op.results[0], float(helper.returns));
|
|
302
|
+
}
|
|
216
303
|
}
|
|
217
304
|
for (const [op, at] of consumers) {
|
|
218
305
|
at.forEach((i) => retype(op.operands[i], 64));
|
|
219
306
|
}
|
|
307
|
+
const folded = new Map<Op, Op>();
|
|
308
|
+
for (const [op, found] of tests) {
|
|
309
|
+
if (calls.has(op)) {
|
|
310
|
+
for (const t of found) {
|
|
311
|
+
const operands = op.operands.map((o) => floatOf.get(o)!);
|
|
312
|
+
const fcmp = helperOp(t.opcode, String(op.attrs.target), operands, t.at.results[0]);
|
|
313
|
+
// The relation the source spelled, where the test leaves it open: `__eqsf2` and `__nesf2`
|
|
314
|
+
// each fold to `fcmp_oeq` or to `fcmp_une`, by the sense of the branch that tests them
|
|
315
|
+
// (runtime-helpers.ts `RuntimeHelper.relation`).
|
|
316
|
+
const relation = helperOf(op)!.relation;
|
|
317
|
+
if (relation !== undefined) {
|
|
318
|
+
fcmp.attrs.relation = relation;
|
|
319
|
+
}
|
|
320
|
+
folded.set(t.at, fcmp);
|
|
321
|
+
t.spent.forEach((x) => spent.add(x));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
220
325
|
for (const block of fn.blocks) {
|
|
221
326
|
block.ops = block.ops.flatMap((op) => {
|
|
327
|
+
// the call stays, its result now the float it was declared
|
|
328
|
+
if (returned.has(op)) {
|
|
329
|
+
return [mkOp('call', { operands: op.operands, results: [returned.get(op)!], attrs: op.attrs })];
|
|
330
|
+
}
|
|
331
|
+
if (folded.has(op)) {
|
|
332
|
+
return [folded.get(op)!];
|
|
333
|
+
}
|
|
334
|
+
if (calls.has(op) && tests.has(op)) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
222
337
|
if (calls.has(op)) {
|
|
338
|
+
const sides = sidesOf(op);
|
|
223
339
|
return [
|
|
224
340
|
helperOp(
|
|
225
341
|
helperOf(op)!.op!,
|
|
226
342
|
String(op.attrs.target),
|
|
227
|
-
op.operands.map((o) => floatOf.get(o)!),
|
|
228
|
-
floatOf.get(op.results[0])
|
|
343
|
+
op.operands.map((o) => (sides.operands ? floatOf.get(o)! : o)),
|
|
344
|
+
sides.result ? floatOf.get(op.results[0])! : op.results[0],
|
|
229
345
|
),
|
|
230
346
|
];
|
|
231
347
|
}
|
|
@@ -241,3 +357,108 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
|
|
|
241
357
|
}
|
|
242
358
|
return true;
|
|
243
359
|
}
|
|
360
|
+
|
|
361
|
+
/** One test of a compare helper's result: the op whose 0/1 value is the test, the float compare
|
|
362
|
+
* it computes, and the ops between the result and it, which only it reads. */
|
|
363
|
+
interface CompareTest {
|
|
364
|
+
at: Op;
|
|
365
|
+
opcode: Opcode;
|
|
366
|
+
spent: Op[];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The integer compares a test may apply to the helper's result, each against the constant 0. */
|
|
370
|
+
const INT_TEST: Readonly<Record<string, (x: number, y: number) => boolean>> = {
|
|
371
|
+
icmp_eq: (x, y) => x === y,
|
|
372
|
+
icmp_ne: (x, y) => x !== y,
|
|
373
|
+
icmp_slt: (x, y) => x < y,
|
|
374
|
+
icmp_sle: (x, y) => x <= y,
|
|
375
|
+
icmp_sgt: (x, y) => x > y,
|
|
376
|
+
icmp_sge: (x, y) => x >= y,
|
|
377
|
+
icmp_ult: (x, y) => x >>> 0 < y >>> 0,
|
|
378
|
+
icmp_ule: (x, y) => x >>> 0 <= y >>> 0,
|
|
379
|
+
icmp_ugt: (x, y) => x >>> 0 > y >>> 0,
|
|
380
|
+
icmp_uge: (x, y) => x >>> 0 >= y >>> 0,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/** Every test of `r`, a compare helper's result, or null where some reader of it is none. The tests
|
|
384
|
+
* are the spellings agbcc gives a result's relation to 0: an integer compare with the constant 0
|
|
385
|
+
* on either side (`cmp r0, #0` before a branch or a 0/1 select), and the three sign-bit forms of a
|
|
386
|
+
* value — `r >> 31` (`< 0`), `~r >> 31` (`>= 0`) and `(-r | r) >> 31` (`!= 0`), each a logical
|
|
387
|
+
* shift by 31. Each answers by the SIGN of `r` alone, so the helper's three-way result is read at
|
|
388
|
+
* -1, 0 and 1 and at its NaN value, and the four answers name the compare in `FCMP_TRUTH`. */
|
|
389
|
+
function compareTests(
|
|
390
|
+
r: Value,
|
|
391
|
+
users: ReadonlyMap<Value, ReadonlyArray<Op | null>>,
|
|
392
|
+
def: ReadonlyMap<Value, Op>,
|
|
393
|
+
wordOf: (v: Value) => number | undefined,
|
|
394
|
+
unordered: 1 | -1,
|
|
395
|
+
): CompareTest[] | null {
|
|
396
|
+
const readers = (v: Value) => users.get(v) ?? [];
|
|
397
|
+
const only = (v: Value): Op | null => (readers(v).length === 1 ? readers(v)[0] : null);
|
|
398
|
+
const signBit = (u: Op | null, of: Value): boolean =>
|
|
399
|
+
u?.opcode === 'shr_u' && u.operands.length === 1 && u.operands[0] === of && u.attrs.imm === 31;
|
|
400
|
+
const found: CompareTest[] = [];
|
|
401
|
+
const answer = (at: Op, spent: Op[], test: (x: number) => boolean): boolean => {
|
|
402
|
+
const truth = [test(-1), test(0), test(1), test(unordered)];
|
|
403
|
+
const opcode = fcmpOfTruth(truth);
|
|
404
|
+
if (opcode === undefined) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
found.push({ at, opcode, spent });
|
|
408
|
+
return true;
|
|
409
|
+
};
|
|
410
|
+
for (const u of readers(r)) {
|
|
411
|
+
if (u === null) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const cmp = INT_TEST[u.opcode];
|
|
415
|
+
if (cmp !== undefined && u.operands.length === 2) {
|
|
416
|
+
const [x, y] = u.operands;
|
|
417
|
+
const ok =
|
|
418
|
+
x === r && y !== r && wordOf(y) === 0
|
|
419
|
+
? answer(u, [], (v) => cmp(v, 0))
|
|
420
|
+
: y === r && x !== r && wordOf(x) === 0 && answer(u, [], (v) => cmp(0, v));
|
|
421
|
+
if (!ok) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (signBit(u, r)) {
|
|
427
|
+
if (!answer(u, [], (v) => v < 0)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (u.opcode === 'not' && u.operands.length === 1) {
|
|
433
|
+
const shift = only(u.results[0]);
|
|
434
|
+
if (!signBit(shift, u.results[0]) || !answer(shift!, [u], (v) => v >= 0)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
// `(-r | r) >> 31`: `r` has two readers, the `neg` and the `or`, and the test is taken at the
|
|
440
|
+
// `neg`; the `or` is that same test, read when it is reached.
|
|
441
|
+
if (u.opcode === 'neg' && u.operands.length === 1) {
|
|
442
|
+
const or = only(u.results[0]);
|
|
443
|
+
const shift = or ? only(or.results[0]) : null;
|
|
444
|
+
const pairs =
|
|
445
|
+
or?.opcode === 'or' &&
|
|
446
|
+
or.operands.length === 2 &&
|
|
447
|
+
or.operands.includes(r) &&
|
|
448
|
+
or.operands.includes(u.results[0]) &&
|
|
449
|
+
readers(r).includes(or);
|
|
450
|
+
if (!pairs || !signBit(shift, or!.results[0]) || !answer(shift!, [u, or!], (v) => v !== 0)) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (
|
|
456
|
+
u.opcode === 'or' &&
|
|
457
|
+
u.operands.some((o) => o !== r && def.get(o)?.opcode === 'neg' && def.get(o)!.operands[0] === r)
|
|
458
|
+
) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
return found.length > 0 ? found : null;
|
|
464
|
+
}
|
package/src/raise/gvn.ts
CHANGED
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
// materializes a multi-use `const` that is live across a call into a named local, and
|
|
33
33
|
// its own comment records that this exact widening ("the small-constant regression")
|
|
34
34
|
// already cost matches once.
|
|
35
|
-
// `undef` — numbering it would be
|
|
36
|
-
// `
|
|
37
|
-
//
|
|
35
|
+
// `undef` — numbering it would be HARMFUL: two `undef`s of one `key` are a storage's content
|
|
36
|
+
// and a `copied` move of it into another storage's join (ir/opcodes.ts says so at the
|
|
37
|
+
// opcode), and one number would erase the move the structurer spells.
|
|
38
38
|
//
|
|
39
39
|
// The gate is a MATCHING policy, not a property of the opcode — which is why it is not a flag on
|
|
40
40
|
// the opcode table, where `const` would satisfy it.
|