@asmlift/core 0.8.0 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/raise/arrays.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// byte form (`add(base, index)`, elemSize 1) is deferred: without types, base and index are
|
|
21
21
|
// indistinguishable there.
|
|
22
22
|
import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
23
|
+
import { carryDiscipline } from '../ir/discipline';
|
|
23
24
|
|
|
24
25
|
// If `addr` is `add(base, shl(index, k))` with `1 << k === width`, return {base, index}.
|
|
25
26
|
// The `add` is commutative, so the scaled side may be either operand.
|
|
@@ -62,11 +63,11 @@ export function recognizeArrays(fn: Fn): number {
|
|
|
62
63
|
operands: [m.base, m.index],
|
|
63
64
|
results: [res],
|
|
64
65
|
// listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
|
|
65
|
-
attrs: {
|
|
66
|
+
attrs: carryDiscipline(op, {
|
|
66
67
|
elemSize: op.attrs.width as number,
|
|
67
68
|
signed: op.attrs.signed as boolean,
|
|
68
69
|
...(op.attrs.listOrder === true && { listOrder: true }),
|
|
69
|
-
},
|
|
70
|
+
}),
|
|
70
71
|
});
|
|
71
72
|
replaceAllUsesWith(fn, op.results[0], res);
|
|
72
73
|
count++;
|
|
@@ -77,7 +78,7 @@ export function recognizeArrays(fn: Fn): number {
|
|
|
77
78
|
}
|
|
78
79
|
b.ops[i] = mkOp('astore', {
|
|
79
80
|
operands: [m.base, m.index, op.operands[1]],
|
|
80
|
-
attrs: { elemSize: op.attrs.width as number },
|
|
81
|
+
attrs: carryDiscipline(op, { elemSize: op.attrs.width as number }),
|
|
81
82
|
});
|
|
82
83
|
count++;
|
|
83
84
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// asmlift — the decider for ir/discipline.ts's `declared` placement: a memory access of an object
|
|
2
|
+
// the project's symbol map declares volatile.
|
|
3
|
+
//
|
|
4
|
+
// The stamp is a function of the IR, the map the function is lifted and structured under, and the
|
|
5
|
+
// compiler, and nothing else: an access whose base reaches a global the map qualifies, at the byte
|
|
6
|
+
// the access names (`declaresVolatile`) — and, for a read, one the compiler could have made of a
|
|
7
|
+
// `volatile` (target.ts `readCouldBeVolatile`, which the device pin asks too). It is put as the
|
|
8
|
+
// lift is made (pipeline.ts `liftStamped`), before the first pass that asks whether a read may be
|
|
9
|
+
// deleted, folded or moved, and re-derived by every pass that changes which object an access names
|
|
10
|
+
// and by structuring. Structuring the same function under another map — the `/raw-globals` setting
|
|
11
|
+
// has none — re-runs it, and an op the new map does not qualify loses the stamp, so a structuring
|
|
12
|
+
// never answers for a map it was not given. The base is read as the structurer will read it: a join
|
|
13
|
+
// whose every edge passes one address is that address (`addressThroughJoins`). A base that reaches
|
|
14
|
+
// no name is a pointer parameter, a raw address or a genuine join: no declaration is in evidence,
|
|
15
|
+
// and the access stays unplaced unless the lift pinned it (`device`).
|
|
16
|
+
//
|
|
17
|
+
// A read structuring stamps that the earlier stamps left plain was plain to every raising pass, and a
|
|
18
|
+
// short-circuit fold may have lifted it out of the arm it ran in. structure.ts declines on such a
|
|
19
|
+
// read in a `&&`/`||`'s guarded operand rather than place it.
|
|
20
|
+
import { globalBaseOf, globalCellOf } from '../ir/alias';
|
|
21
|
+
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
22
|
+
import { DECLARED_VOLATILE } from '../ir/discipline';
|
|
23
|
+
import { MEM_BASE_OPS } from '../ir/opcodes';
|
|
24
|
+
import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
|
|
25
|
+
import { type TargetDescription, readCouldBeVolatile } from '../target';
|
|
26
|
+
import { valueNumberOf } from './gvn';
|
|
27
|
+
|
|
28
|
+
/** The compiler fact the stamp reads. */
|
|
29
|
+
export type StampBehaviors = Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'>;
|
|
30
|
+
|
|
31
|
+
/** May this memory access be stamped: a store, or a read the compiler could have made of a
|
|
32
|
+
* `volatile`? */
|
|
33
|
+
const stampable = (op: Op, behaviors: StampBehaviors): boolean =>
|
|
34
|
+
MEM_BASE_OPS.has(op.opcode) &&
|
|
35
|
+
(op.opcode === 'store' || op.opcode === 'astore' || readCouldBeVolatile(behaviors, op));
|
|
36
|
+
|
|
37
|
+
/** The address a value stands for once `addrnum` (raise/pre-recovery.ts) has run: a block parameter
|
|
38
|
+
* every in-edge of which, a back edge's self-reference aside, passes one value after numbering
|
|
39
|
+
* (raise/gvn.ts `valueNumberOf`) is that value, as `simplifyTrivialPhis` collapses it; any other
|
|
40
|
+
* value stands for itself. Each arm of an `if` loading `&gVolReg` into the register the join reads
|
|
41
|
+
* is such a parameter, and the stamp is put before that pass, so it reads the base the structurer
|
|
42
|
+
* will. */
|
|
43
|
+
function addressThroughJoins(fn: Fn): (v: Value) => Value {
|
|
44
|
+
const incoming = new Map<Value, Value[]>();
|
|
45
|
+
for (const b of fn.blocks) {
|
|
46
|
+
for (const op of b.ops) {
|
|
47
|
+
for (const s of op.successors) {
|
|
48
|
+
if (s.block === fn.blocks[0]) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
s.block.params.forEach((param, i) => {
|
|
52
|
+
if (i < s.args.length) {
|
|
53
|
+
(incoming.get(param) ?? incoming.set(param, []).get(param)!).push(s.args[i]);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const numberOf = valueNumberOf(fn);
|
|
60
|
+
const through = (v: Value, onWalk: Set<Value>): Value | null => {
|
|
61
|
+
const ins = incoming.get(v);
|
|
62
|
+
if (ins === undefined) {
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
if (onWalk.has(v)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
onWalk.add(v);
|
|
69
|
+
const each = ins.filter((a) => a !== v).map((a) => through(a, onWalk));
|
|
70
|
+
onWalk.delete(v);
|
|
71
|
+
const first = each[0];
|
|
72
|
+
return first !== undefined && first !== null && each.every((a) => a !== null && numberOf(a) === numberOf(first))
|
|
73
|
+
? first
|
|
74
|
+
: null;
|
|
75
|
+
};
|
|
76
|
+
return (v) => through(v, new Set()) ?? v;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stamp every memory access of `fn` to an object `symbols` declares volatile — a read only where a
|
|
80
|
+
* compiler behaving as `behaviors` says could have made it of one — and clear the stamp from every
|
|
81
|
+
* other op. Returns the ops it stamped that were not stamped before. */
|
|
82
|
+
export function stampDeclaredVolatile(
|
|
83
|
+
fn: Fn,
|
|
84
|
+
symbols: ReadonlyMap<string, SymbolInfo> | undefined,
|
|
85
|
+
behaviors: StampBehaviors,
|
|
86
|
+
): Op[] {
|
|
87
|
+
const added: Op[] = [];
|
|
88
|
+
const defs = symbols ? defOpMap(fn) : undefined;
|
|
89
|
+
const addressOf = symbols ? addressThroughJoins(fn) : undefined;
|
|
90
|
+
for (const b of fn.blocks) {
|
|
91
|
+
for (const op of b.ops) {
|
|
92
|
+
const addr = defs && stampable(op, behaviors) ? addressOf!(op.operands[0]) : undefined;
|
|
93
|
+
const base = addr !== undefined ? globalBaseOf(defs!, addr) : null;
|
|
94
|
+
// A subscript reaches the object while naming no cell, so only a `load`/`store` asks by byte,
|
|
95
|
+
// and an access that names no byte is placed only where every byte it could reach is qualified.
|
|
96
|
+
const cell =
|
|
97
|
+
base !== null && (op.opcode === 'load' || op.opcode === 'store')
|
|
98
|
+
? globalCellOf(defs!, addr!, op.attrs.off as number)
|
|
99
|
+
: null;
|
|
100
|
+
const declared = base !== null && declaresVolatile(symbols!.get(base), cell === null ? null : cell.byte);
|
|
101
|
+
// A fresh record either way: a rebuilder may hand two ops one `attrs` object.
|
|
102
|
+
if (declared && op.attrs[DECLARED_VOLATILE] !== true) {
|
|
103
|
+
op.attrs = { ...op.attrs, [DECLARED_VOLATILE]: true };
|
|
104
|
+
added.push(op);
|
|
105
|
+
} else if (!declared && DECLARED_VOLATILE in op.attrs) {
|
|
106
|
+
const { [DECLARED_VOLATILE]: _, ...rest } = op.attrs;
|
|
107
|
+
op.attrs = rest;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return added;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** {@link stampDeclaredVolatile} under an address-keyed project map, through the name-keyed view
|
|
115
|
+
* structuring is handed for the same map (symbols.ts `symbolsByName`), narrowed to the names the
|
|
116
|
+
* function's own `gaddr`s carry. */
|
|
117
|
+
export function stampDeclaredVolatileUnder(fn: Fn, map: SymbolMap | undefined, behaviors: StampBehaviors): void {
|
|
118
|
+
if (map === undefined) {
|
|
119
|
+
stampDeclaredVolatile(fn, undefined, behaviors);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const names = new Set<string>();
|
|
123
|
+
for (const b of fn.blocks) {
|
|
124
|
+
for (const op of b.ops) {
|
|
125
|
+
if (op.opcode === 'gaddr') {
|
|
126
|
+
names.add(op.attrs.sym as string);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
stampDeclaredVolatile(fn, symbolsByName(map, names), behaviors);
|
|
131
|
+
}
|
package/src/raise/divpow2.ts
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
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
43
|
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
44
|
-
import {
|
|
44
|
+
import { effectful } from '../ir/discipline';
|
|
45
45
|
import { T } from '../ir/types';
|
|
46
46
|
|
|
47
47
|
/** `shr_s v {imm=k}` → k, else null. */
|
|
@@ -110,7 +110,7 @@ export function recognizeDivPow2(fn: Fn): boolean {
|
|
|
110
110
|
continue;
|
|
111
111
|
}
|
|
112
112
|
const bp = preds.get(bias) ?? [];
|
|
113
|
-
if (bp.length !== 1 || bias.ops.some(
|
|
113
|
+
if (bp.length !== 1 || bias.ops.some(effectful)) {
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
116
|
const h = bp[0];
|
package/src/raise/errors.ts
CHANGED
|
@@ -8,3 +8,20 @@ export class RaiseUnsupportedError extends Error {
|
|
|
8
8
|
this.name = 'RaiseUnsupportedError';
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/** The one raise refusal a caller ACTS ON rather than only reporting: raise/structs.ts cannot
|
|
13
|
+
* reproduce a layout with a plain struct because two accesses' byte ranges COLLIDE. For a base
|
|
14
|
+
* whose address is declared outside the function that is not a reason to decline, since each
|
|
15
|
+
* access is spellable on its own; for any other base the overlap is declared as a union member
|
|
16
|
+
* where the union can be laid out. A CLASS rather than a substring test on the message, because
|
|
17
|
+
* the two facts a caller needs (which refusal this is, and what to tell the reader) would
|
|
18
|
+
* otherwise be the same string, and the guard would then fall open the day the message is
|
|
19
|
+
* reworded. Every other raise/structs.ts refusal (a packed layout, a union it cannot lay out)
|
|
20
|
+
* stays a plain RaiseUnsupportedError and declines: a subclass is minted for a caller that acts
|
|
21
|
+
* on it, not for every refusal that wants a name. */
|
|
22
|
+
export class StructOverlapError extends RaiseUnsupportedError {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'StructOverlapError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// asmlift — the soft-float boundary (L1 recognition): where integer words become floats.
|
|
2
|
+
//
|
|
3
|
+
// Three things cross it. A runtime helper whose op is a float op (`isFloatHelper`) — the peer of
|
|
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). And a
|
|
6
|
+
// double literal, two constant words, where the target states their order
|
|
7
|
+
// (`TargetDescription.doubleArgWords`) and its compiler reads a literal back
|
|
8
|
+
// (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all three apart from the integer
|
|
9
|
+
// helpers is the value's KIND: the words are bits in integer registers, and the pass re-types them
|
|
10
|
+
// as floats, which only data flow may justify — see the function below.
|
|
11
|
+
import { Fn, Op, Value, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
12
|
+
import { doubleBits, doubleOf } from '../ir/float-bits';
|
|
13
|
+
import { T } from '../ir/types';
|
|
14
|
+
import { arrivesAsDeclared, helperOp, isFloatHelper, lookupHelper } from '../runtime-helpers';
|
|
15
|
+
import type { TargetDescription } from '../target';
|
|
16
|
+
import { RaiseUnsupportedError } from './errors';
|
|
17
|
+
import { isArgumentPair } from './pairparams';
|
|
18
|
+
|
|
19
|
+
/** Rewrite the float-arithmetic helper calls to the float ops, in place, each value at the width its
|
|
20
|
+
* helper's signature states, and hand a parameter the callee declares `double` (the frontend's
|
|
21
|
+
* `doubles` on the call) a double. Returns whether anything changed.
|
|
22
|
+
*
|
|
23
|
+
* A SOFT-FLOAT VALUE IS BITS IN INTEGER REGISTERS, AND ONLY DATA FLOW MAY READ THEM AS A FLOAT. A
|
|
24
|
+
* single travels in one register; a double in the pair a long long travels in, which the frontend
|
|
25
|
+
* builds the way it builds a long long's, `concat(lo = r0, hi = r1)` — but agbcc stores a double
|
|
26
|
+
* high word first (thumb.h:335 `FLOAT_WORDS_BIG_ENDIAN`): r0 holds the sign and exponent. Moving
|
|
27
|
+
* the value whole keeps it right, so a float may come only from where the ABI hands one over
|
|
28
|
+
* whole — this function's argument slot, or two consecutive ones for a double, or another of these
|
|
29
|
+
* helpers' results — and may go only into another of them, the return, or a parameter a callee
|
|
30
|
+
* declares `double`. Anything else refuses, and the call stays for `refuseUnmodelledHelpers` to
|
|
31
|
+
* gap:
|
|
32
|
+
* - an operand built from a load or two unrelated words, whose halves the int64 naming would put
|
|
33
|
+
* in the wrong order. Two CONSTANT words are the exception: they are a literal, read in the
|
|
34
|
+
* target's word order into an `fconst` (`a + 1.5` stages `0x3ff80000` in r2, its high word);
|
|
35
|
+
* - an argument the function also reads on its own, as a word or as one half of a pair;
|
|
36
|
+
* - 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.
|
|
38
|
+
* 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. */
|
|
40
|
+
export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
41
|
+
const table = target.runtimeHelpers ?? {};
|
|
42
|
+
let sites: Op[] = [];
|
|
43
|
+
const consumers = new Map<Op, readonly number[]>();
|
|
44
|
+
const def = new Map<Value, Op>();
|
|
45
|
+
const users = new Map<Value, Array<Op | null>>(); // null: an edge argument
|
|
46
|
+
const use = (v: Value, by: Op | null) => users.set(v, [...(users.get(v) ?? []), by]);
|
|
47
|
+
const helperOf = (op: Op) => (op.opcode === 'call' ? lookupHelper(table, String(op.attrs.target)) : undefined);
|
|
48
|
+
for (const block of fn.blocks) {
|
|
49
|
+
for (const op of block.ops) {
|
|
50
|
+
op.results.forEach((r) => def.set(r, op));
|
|
51
|
+
op.operands.forEach((o) => use(o, op));
|
|
52
|
+
op.successors.forEach((s) => s.args.forEach((a) => use(a, null)));
|
|
53
|
+
const helper = helperOf(op);
|
|
54
|
+
if (
|
|
55
|
+
helper &&
|
|
56
|
+
isFloatHelper(helper) &&
|
|
57
|
+
arrivesAsDeclared(
|
|
58
|
+
helper,
|
|
59
|
+
op.operands.map((o) => o.type),
|
|
60
|
+
op.results.map((r) => r.type),
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
63
|
+
sites.push(op);
|
|
64
|
+
} else if (!helper && op.opcode === 'call' && Array.isArray(op.attrs.doubles)) {
|
|
65
|
+
consumers.set(op, op.attrs.doubles as number[]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!sites.length && !consumers.size) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// A projection nothing reads is not a read of a half: `dce` runs ahead of this pass only after a
|
|
73
|
+
// fold that changed the IR, so the frontend's split of the result back into its registers may stand.
|
|
74
|
+
const deadHalf = (u: Op | null) =>
|
|
75
|
+
u !== null && (u.opcode === 'lo32' || u.opcode === 'hi32') && !users.has(u.results[0]);
|
|
76
|
+
// A USE THAT TAKES THE VALUE AS A FLOAT: a helper the fold keeps, or a parameter the callee declares
|
|
77
|
+
// `double` (the frontend's `doubles`) — and only that parameter, so the same value handed to the
|
|
78
|
+
// same call as a word is a word read.
|
|
79
|
+
const floatUse = (u: Op | null, v: Value, calls: ReadonlySet<Op>): boolean =>
|
|
80
|
+
u !== null &&
|
|
81
|
+
(calls.has(u) || (consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
|
|
82
|
+
const floatsOnly = (v: Value, calls: ReadonlySet<Op>) => (users.get(v) ?? []).every((u) => floatUse(u, v, calls));
|
|
83
|
+
const entry = fn.blocks[0].params;
|
|
84
|
+
// A `concat` of the same two slots in the same order as `d`: the frontend builds one afresh at each
|
|
85
|
+
// call it hands the pair to, so `g(x); g(x)` reads one parameter through two of them.
|
|
86
|
+
const samePair = (u: Op | null, d: Op): u is Op =>
|
|
87
|
+
u?.opcode === 'concat' && u.operands[0] === d.operands[0] && u.operands[1] === d.operands[1];
|
|
88
|
+
// The argument slot itself, or the pair of them `isArgumentPair` names, read by nothing but
|
|
89
|
+
// `concat`s of that pair, each taken as a float; `arrivesAsDeclared` has already held each operand
|
|
90
|
+
// to its parameter's width.
|
|
91
|
+
const argument = (v: Value, calls: ReadonlySet<Op>): boolean => {
|
|
92
|
+
const d = def.get(v);
|
|
93
|
+
if (d === undefined) {
|
|
94
|
+
return entry.includes(v) && floatsOnly(v, calls);
|
|
95
|
+
}
|
|
96
|
+
return (
|
|
97
|
+
isArgumentPair(entry, d, (p) => users.get(p)!.filter((u) => !samePair(u, d)).length + 1) &&
|
|
98
|
+
users.get(d.operands[0])!.every((u) => samePair(u, d) && floatsOnly(u.results[0], calls))
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
const fromCall = (v: Value, calls: ReadonlySet<Op>) => {
|
|
102
|
+
const d = def.get(v);
|
|
103
|
+
return d !== undefined && calls.has(d);
|
|
104
|
+
};
|
|
105
|
+
// A CONSTANT WORD, as a compiler may build one in two instructions: agbcc moves a word that is a
|
|
106
|
+
// shifted byte as `mov`+`lsl` (thumb.md:66, `thumb_shiftable_const` at thumb.c:61) and one in
|
|
107
|
+
// -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 wordOf = (v: Value): number | undefined => {
|
|
110
|
+
const d = def.get(v);
|
|
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
|
+
};
|
|
119
|
+
// A LITERAL IS TWO CONSTANT WORDS, HIGH WORD FIRST on a target whose double crosses a call in
|
|
120
|
+
// general words (`TargetDescription.doubleArgWords`) — the order that makes the long long naming
|
|
121
|
+
// of the same pair another number. Its bits, or undefined where the pair is no literal, the target
|
|
122
|
+
// states no such double, its compiler is not known to read the literal back
|
|
123
|
+
// (`compilerBehaviors.roundTripsDoubleLiterals`), or the double is not finite — which no C literal
|
|
124
|
+
// spells.
|
|
125
|
+
const literalBits = (v: Value): string | undefined => {
|
|
126
|
+
const d = def.get(v);
|
|
127
|
+
const [first, second] = (d?.opcode === 'concat' ? d.operands : []).map(wordOf);
|
|
128
|
+
if (
|
|
129
|
+
target.doubleArgWords === undefined ||
|
|
130
|
+
target.compilerBehaviors.roundTripsDoubleLiterals !== true ||
|
|
131
|
+
first === undefined ||
|
|
132
|
+
second === undefined
|
|
133
|
+
) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const bits = doubleBits(first, second);
|
|
137
|
+
return Number.isFinite(doubleOf(bits)) ? bits : undefined;
|
|
138
|
+
};
|
|
139
|
+
const literal = (v: Value, calls: ReadonlySet<Op>) => literalBits(v) !== undefined && floatsOnly(v, calls);
|
|
140
|
+
const source = (v: Value, calls: ReadonlySet<Op>) => argument(v, calls) || fromCall(v, calls) || literal(v, calls);
|
|
141
|
+
for (;;) {
|
|
142
|
+
const calls = new Set(sites);
|
|
143
|
+
const kept = sites.filter(
|
|
144
|
+
(op) =>
|
|
145
|
+
op.operands.every((o) => source(o, calls)) &&
|
|
146
|
+
(users.get(op.results[0]) ?? []).every(
|
|
147
|
+
(u) => deadHalf(u) || floatUse(u, op.results[0], calls) || u?.opcode === 'ret',
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
if (kept.length === sites.length) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
sites = kept;
|
|
154
|
+
}
|
|
155
|
+
const calls = new Set(sites);
|
|
156
|
+
// A DECLARED DOUBLE HAS NO FALLBACK. A helper the fold refuses stays a call for
|
|
157
|
+
// `refuseUnmodelledHelpers` to gap, but an ordinary callee is no helper, and its two words handed
|
|
158
|
+
// on as integers are the wrong number — so a double parameter nothing here can hand a double
|
|
159
|
+
// refuses the function. It counts arguments as the source does: an `sret` call's operand 0, the
|
|
160
|
+
// hidden pointer, is not one.
|
|
161
|
+
for (const [op, at] of consumers) {
|
|
162
|
+
for (const i of at) {
|
|
163
|
+
if (!source(op.operands[i], calls)) {
|
|
164
|
+
const arg = i + (op.attrs.sret === true ? 0 : 1);
|
|
165
|
+
throw new RaiseUnsupportedError(
|
|
166
|
+
`cannot lift '${fn.name}': argument ${arg} of the call to '${String(op.attrs.target)}' ` +
|
|
167
|
+
'is a `double` its callee declares, and its two words are not a double this function was ' +
|
|
168
|
+
'handed, a runtime helper returned or a finite literal staged, moved whole',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!sites.length && !consumers.size) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const floatOf = new Map<Value, Value>();
|
|
177
|
+
const float = (bits: number) => mkValue(bits > 32 ? T.f64() : T.f32());
|
|
178
|
+
// the `fconst` each literal's `concat` becomes, and the ops computing the constant words only it read
|
|
179
|
+
const literals = new Map<Op, Op>();
|
|
180
|
+
const spent = new Set<Op>();
|
|
181
|
+
const spend = (w: Value, by: Op) => {
|
|
182
|
+
const d = def.get(w)!;
|
|
183
|
+
if (users.get(w)!.every((u) => u === by)) {
|
|
184
|
+
spent.add(d);
|
|
185
|
+
d.operands.forEach((o) => spend(o, d));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
// One parameter in its first slot's place, which is where the ABI put the value.
|
|
189
|
+
const retype = (o: Value, bits: number) => {
|
|
190
|
+
if (floatOf.has(o) || fromCall(o, calls)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const d = def.get(o);
|
|
194
|
+
const pattern = literalBits(o);
|
|
195
|
+
if (pattern !== undefined) {
|
|
196
|
+
const whole = float(64);
|
|
197
|
+
literals.set(d!, mkOp('fconst', { results: [whole], attrs: { bits: pattern } }));
|
|
198
|
+
d!.operands.forEach((w) => spend(w, d!));
|
|
199
|
+
floatOf.set(o, whole);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const slots = d === undefined ? [o] : d.operands;
|
|
203
|
+
const whole = float(bits);
|
|
204
|
+
entry.splice(entry.indexOf(slots[0]), slots.length, whole);
|
|
205
|
+
for (const u of d === undefined ? [] : users.get(slots[0])!) {
|
|
206
|
+
if (samePair(u, d!)) {
|
|
207
|
+
floatOf.set(u.results[0], whole);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
floatOf.set(o, whole);
|
|
211
|
+
};
|
|
212
|
+
for (const op of sites) {
|
|
213
|
+
const helper = helperOf(op)!;
|
|
214
|
+
op.operands.forEach((o, i) => retype(o, helper.params[i]));
|
|
215
|
+
floatOf.set(op.results[0], float(helper.returns));
|
|
216
|
+
}
|
|
217
|
+
for (const [op, at] of consumers) {
|
|
218
|
+
at.forEach((i) => retype(op.operands[i], 64));
|
|
219
|
+
}
|
|
220
|
+
for (const block of fn.blocks) {
|
|
221
|
+
block.ops = block.ops.flatMap((op) => {
|
|
222
|
+
if (calls.has(op)) {
|
|
223
|
+
return [
|
|
224
|
+
helperOp(
|
|
225
|
+
helperOf(op)!.op!,
|
|
226
|
+
String(op.attrs.target),
|
|
227
|
+
op.operands.map((o) => floatOf.get(o)!),
|
|
228
|
+
floatOf.get(op.results[0])!,
|
|
229
|
+
),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
if (literals.has(op)) {
|
|
233
|
+
return [literals.get(op)!];
|
|
234
|
+
}
|
|
235
|
+
const retired = op.results.length === 1 && floatOf.has(op.results[0]);
|
|
236
|
+
return retired || spent.has(op) || (deadHalf(op) && floatOf.has(op.operands[0])) ? [] : [op];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const [was, now] of floatOf) {
|
|
240
|
+
replaceAllUsesWith(fn, was, now);
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
package/src/raise/globalshape.ts
CHANGED
|
@@ -505,11 +505,12 @@ export const SHAPE_GATES: readonly Gate<ShapeEvidence>[] = [
|
|
|
505
505
|
id: 'index-materialized-first',
|
|
506
506
|
why: 'a scaling of the index precedes the pool load IN ITS OWN BLOCK — the pointer path, which is the cast spelling',
|
|
507
507
|
sound: true,
|
|
508
|
-
// Its SECOND reaching fixture is the compiler-shaped one:
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
508
|
+
// Its SECOND reaching fixture is the compiler-shaped one: the real agbcc output for a function
|
|
509
|
+
// that subscripts the name once outside a loop and once inside it.
|
|
510
|
+
guardedBy: [
|
|
511
|
+
'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
|
|
512
|
+
'global-array-shape.test.ts: …and the same function cast-spelled still refuses, on the access that CAN be compared',
|
|
513
|
+
],
|
|
513
514
|
rejects: anIndexFirstAccess,
|
|
514
515
|
},
|
|
515
516
|
{
|
|
@@ -955,6 +956,11 @@ export function inferGlobalArrays(
|
|
|
955
956
|
}
|
|
956
957
|
const pos = positions(fn);
|
|
957
958
|
for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
|
|
959
|
+
// a function-scope static's shape is its definition's (structure/local-statics.ts), not an
|
|
960
|
+
// inference, and it is declared in the body rather than assumed
|
|
961
|
+
if (fn.localObjects?.has(sym)) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
958
964
|
const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
|
|
959
965
|
if (si !== null) {
|
|
960
966
|
out.set(sym, si);
|
package/src/raise/gvn.ts
CHANGED
|
@@ -93,11 +93,8 @@ function keyOf(op: Op): string {
|
|
|
93
93
|
return `${op.opcode}(${attrs})`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
* block. Returns the number of definitions removed (0 when nothing changed).
|
|
99
|
-
*/
|
|
100
|
-
export function numberPureValues(fn: Fn): number {
|
|
96
|
+
/** The blocks numbering looks in: those the entry reaches through any op's successors. */
|
|
97
|
+
function numberedBlocks(fn: Fn): Set<Block> {
|
|
101
98
|
// REACHABLE blocks only. The entry dominates everything REACHABLE — it does not dominate an
|
|
102
99
|
// unreachable block, and verify()'s dominator fixpoint models that faithfully (a block with no
|
|
103
100
|
// predecessors converges to dom = {itself}). Numbering a group with a member in such a block
|
|
@@ -120,6 +117,15 @@ export function numberPureValues(fn: Fn): number {
|
|
|
120
117
|
if (fn.blocks[0]) {
|
|
121
118
|
walk(fn.blocks[0]);
|
|
122
119
|
}
|
|
120
|
+
return reachable;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
|
|
125
|
+
* block. Returns the number of definitions removed (0 when nothing changed).
|
|
126
|
+
*/
|
|
127
|
+
export function numberPureValues(fn: Fn): number {
|
|
128
|
+
const reachable = numberedBlocks(fn);
|
|
123
129
|
const groups = new Map<string, { op: Op; block: number }[]>();
|
|
124
130
|
fn.blocks.forEach((b, bi) => {
|
|
125
131
|
if (!reachable.has(b)) {
|
|
@@ -164,3 +170,18 @@ export function numberPureValues(fn: Fn): number {
|
|
|
164
170
|
entry.ops.unshift(...hoisted);
|
|
165
171
|
return removed;
|
|
166
172
|
}
|
|
173
|
+
|
|
174
|
+
/** The class {@link numberPureValues} puts a value in: the key of a numberable definition in a block
|
|
175
|
+
* it numbers, or the value itself. Two values of one class are one value once it has run. */
|
|
176
|
+
export function valueNumberOf(fn: Fn): (v: Value) => Value | string {
|
|
177
|
+
const reachable = numberedBlocks(fn);
|
|
178
|
+
const key = new Map<Value, string>();
|
|
179
|
+
for (const b of reachable) {
|
|
180
|
+
for (const op of b.ops) {
|
|
181
|
+
if (isNumberable(op.opcode) && op.results.length === 1) {
|
|
182
|
+
key.set(op.results[0], keyOf(op));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return (v) => key.get(v) ?? v;
|
|
187
|
+
}
|
package/src/raise/narrow.ts
CHANGED
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
// A signed and an unsigned right shift of the same value by the same amount differ only in the bits
|
|
24
24
|
// the shift brings in, so `%134` is `sext(%133, 32 - 16)` — the same rewrite, and the reason this
|
|
25
25
|
// pass matches the pair rather than either extension on its own.
|
|
26
|
+
//
|
|
27
|
+
// And a third, where the variable is carried SHIFTED and never extended at all: an `s8` walked down
|
|
28
|
+
// a list, `i = buf[i].next`, is kept as `next << 24` and the test reads it back with `asr #24`.
|
|
29
|
+
// The cast idiom folds that `asr` of the `lsl` into a `sext` of the load, which is the same bits
|
|
30
|
+
// and no longer reads the carried value:
|
|
31
|
+
//
|
|
32
|
+
// %41 = shl %40 {imm=24} what `i` holds next iteration, shifted
|
|
33
|
+
// %42 = sext %40 {8} the loop test
|
|
34
|
+
//
|
|
35
|
+
// `sext(v, 32 - k) === shr_s(shl(v, k), k)`, so `%42` is re-rooted as `shr_s %41 {k}`.
|
|
26
36
|
import { Fn, Op, Value, defOpMap } from '../ir/core';
|
|
27
37
|
import { CAST_WIDTHS, Opcode } from '../ir/opcodes';
|
|
28
38
|
|
|
@@ -41,8 +51,8 @@ const domainOf = (op: Op, low: Opcode, high: Opcode): { key: string; width: numb
|
|
|
41
51
|
return null;
|
|
42
52
|
};
|
|
43
53
|
|
|
44
|
-
/** Re-root a sign extension on the co-existing zero extension of the same bits
|
|
45
|
-
* of rewrites. */
|
|
54
|
+
/** Re-root a sign extension on the co-existing zero extension of the same bits, or on the carried
|
|
55
|
+
* left shift that holds them. Returns the number of rewrites. */
|
|
46
56
|
export function rerootNarrowReads(fn: Fn): number {
|
|
47
57
|
// LOOP-CARRIED edge arguments only. The rewrite is sound for ANY co-existing pair — it only
|
|
48
58
|
// re-associates two extensions of the same bits — so this gate is about what it BUYS, not about
|
|
@@ -87,6 +97,8 @@ export function rerootNarrowReads(fn: Fn): number {
|
|
|
87
97
|
// else this could be a use before a def. That is the shape the idiom emits — one increment,
|
|
88
98
|
// both extensions of it, in the block that computes it.
|
|
89
99
|
const unsigned = new Map<Value, Map<string, Value>>();
|
|
100
|
+
// …and the carried left shifts, by operand and then by amount, under the same two rules
|
|
101
|
+
const shifted = new Map<Value, Map<number, Value>>();
|
|
90
102
|
for (const op of b.ops) {
|
|
91
103
|
const u = domainOf(op, 'zext', 'shr_u');
|
|
92
104
|
if (u !== null && carried.has(op.results[0])) {
|
|
@@ -95,6 +107,21 @@ export function rerootNarrowReads(fn: Fn): number {
|
|
|
95
107
|
unsigned.set(op.operands[0], byDomain);
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
110
|
+
if (op.opcode === 'shl' && typeof op.attrs.imm === 'number' && carried.has(op.results[0])) {
|
|
111
|
+
const byAmount = shifted.get(op.operands[0]) ?? new Map<number, Value>();
|
|
112
|
+
byAmount.set(op.attrs.imm, op.results[0]);
|
|
113
|
+
shifted.set(op.operands[0], byAmount);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const k = 32 - (op.attrs.width as number);
|
|
117
|
+
const sh = op.opcode === 'sext' && CAST_WIDTHS.has(32 - k) ? shifted.get(op.operands[0])?.get(k) : undefined;
|
|
118
|
+
if (sh !== undefined) {
|
|
119
|
+
op.opcode = 'shr_s';
|
|
120
|
+
op.operands = [sh];
|
|
121
|
+
op.attrs = { imm: k };
|
|
122
|
+
rewritten++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
98
125
|
const sgn = domainOf(op, 'sext', 'shr_s');
|
|
99
126
|
if (sgn === null) {
|
|
100
127
|
continue;
|
package/src/raise/narrowlocal.ts
CHANGED
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
// u16 v; … *out = v; zext16 4 0 MATCH
|
|
59
59
|
// s32 v; … *out = (u16)v; zext16 0 MATCH 4
|
|
60
60
|
//
|
|
61
|
+
// A THIRD SPELLING of rows three and four is the `/escape-home` variation's
|
|
62
|
+
// (structure/analysis.ts `escapingExtensions`): keep `s32 v` and give the truncation a NAME where
|
|
63
|
+
// it happened, `t = (u16)v`. It is not an alternative to this pass — it judges an extension's
|
|
64
|
+
// result where this one judges a block PARAMETER — and it is a ranked candidate where this is a
|
|
65
|
+
// decision, so neither can take the other's input.
|
|
66
|
+
//
|
|
61
67
|
// Rows one and two are DECIDABLE by the carrier's own readers: a `zext_w` read by a `sext_w` is the
|
|
62
68
|
// write-back truncation followed by the declaration's own sign extension, and no cast on a wide
|
|
63
69
|
// local writes that pair. Rows three and four are the same IR in this pass's READER vocabulary —
|
|
@@ -131,7 +137,8 @@
|
|
|
131
137
|
// admits), and `runPreRecovery` reads the shape ahead of the pass that manufactures it —
|
|
132
138
|
// `mergeShapes`.
|
|
133
139
|
import { type Block, type Fn, type Op, type Value, defOpMap, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
134
|
-
import {
|
|
140
|
+
import { reevalUnsafe } from '../ir/discipline';
|
|
141
|
+
import { CAST_WIDTHS } from '../ir/opcodes';
|
|
135
142
|
import { T } from '../ir/types';
|
|
136
143
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
137
144
|
|
|
@@ -350,8 +357,8 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
|
|
|
350
357
|
* • a MEMORY READ is one op and is NOT one speculatable SET. `gcc/rtlanal.c:1770-1771` sends a
|
|
351
358
|
* MEM to `rtx_addr_can_trap_p` and `:144-147` says an address held in a plain pseudo CAN trap,
|
|
352
359
|
* so `jump.c` never hoists a load and the diamond survives under BOTH spellings, carrying no
|
|
353
|
-
* information. `
|
|
354
|
-
* refuses the trapping divides too, at `rtlanal.c:1774-1784`); `
|
|
360
|
+
* information. `reevalUnsafe` answers this (effects, reads, or traps — `may_trap_p`
|
|
361
|
+
* refuses the trapping divides too, at `rtlanal.c:1774-1784`); `speculationUnsafe` does NOT,
|
|
355
362
|
* because it omits reads for `raise/shortcircuit.ts`, whose arm C's own `&&` re-guards, and
|
|
356
363
|
* that exemption does not transfer to speculation above a compare.
|
|
357
364
|
* • a CONSTANT is not free. `v = a + 0x12345` is a literal-pool `ldr` plus the `add` — two
|
|
@@ -382,9 +389,7 @@ function mergeArms(preds: Map<Block, Block[]>, fn: Fn, blk: Block): [Block, Bloc
|
|
|
382
389
|
* clause in that table is `sound: false`), so the shared conservative predicate is the right one
|
|
383
390
|
* until a row asks for the cost model neither pass has. */
|
|
384
391
|
export function armIsOneSet(b: Block): boolean {
|
|
385
|
-
return (
|
|
386
|
-
!b.ops.some((op) => REEVAL_UNSAFE_OPS.has(op.opcode)) && b.ops.filter((op) => op.results.length > 0).length === 1
|
|
387
|
-
);
|
|
392
|
+
return !b.ops.some(reevalUnsafe) && b.ops.filter((op) => op.results.length > 0).length === 1;
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
/** The join shape of every block, read ONCE off the IR it is handed.
|