@scriptc/compiler 0.2.8 → 0.2.9
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/ambient/scriptc-node-fallback.d.ts +1 -0
- package/dist/backend/cycle-analysis.js +50 -17
- package/dist/backend/cycle-analysis.js.map +1 -1
- package/dist/backend/cycle-analysis.test.js +138 -14
- package/dist/backend/cycle-analysis.test.js.map +1 -1
- package/dist/backend/cycle-mutability.d.ts +31 -6
- package/dist/backend/cycle-mutability.js +120 -18
- package/dist/backend/cycle-mutability.js.map +1 -1
- package/dist/backend/llvm/alloc.d.ts +13 -3
- package/dist/backend/llvm/alloc.js +24 -10
- package/dist/backend/llvm/alloc.js.map +1 -1
- package/dist/backend/llvm/alloc.test.js +5 -5
- package/dist/backend/llvm/alloc.test.js.map +1 -1
- package/dist/backend/llvm/byte-numbers.js +2 -3
- package/dist/backend/llvm/byte-numbers.js.map +1 -1
- package/dist/backend/llvm/classes.js +7 -6
- package/dist/backend/llvm/classes.js.map +1 -1
- package/dist/backend/llvm/common.d.ts +17 -0
- package/dist/backend/llvm/common.js +28 -0
- package/dist/backend/llvm/common.js.map +1 -1
- package/dist/backend/llvm/dense-array-access.js +1 -1
- package/dist/backend/llvm/dense-array-access.test.js +1 -1
- package/dist/backend/llvm/dyn.d.ts +4 -4
- package/dist/backend/llvm/dyn.js +27 -11
- package/dist/backend/llvm/dyn.js.map +1 -1
- package/dist/backend/llvm/emitter.d.ts +45 -3
- package/dist/backend/llvm/emitter.js +333 -74
- package/dist/backend/llvm/emitter.js.map +1 -1
- package/dist/backend/llvm/emitter.test.js +36 -3
- package/dist/backend/llvm/emitter.test.js.map +1 -1
- package/dist/backend/llvm/expr-bytes.js +35 -28
- package/dist/backend/llvm/expr-bytes.js.map +1 -1
- package/dist/backend/llvm/expr-containers.js +1 -1
- package/dist/backend/llvm/expr-context.d.ts +2 -0
- package/dist/backend/llvm/expr-control.js +46 -5
- package/dist/backend/llvm/expr-control.js.map +1 -1
- package/dist/backend/llvm/expr-dynamic.js +3 -0
- package/dist/backend/llvm/expr-dynamic.js.map +1 -1
- package/dist/backend/llvm/expr-stream-bridges.d.ts +3 -0
- package/dist/backend/llvm/expr-stream-bridges.js +73 -11
- package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
- package/dist/backend/llvm/lib-filesystem.js +18 -2
- package/dist/backend/llvm/lib-filesystem.js.map +1 -1
- package/dist/backend/llvm/lib-shared.js +1 -0
- package/dist/backend/llvm/lib-shared.js.map +1 -1
- package/dist/backend/llvm/local-array-reads.d.ts +17 -6
- package/dist/backend/llvm/local-array-reads.js +155 -43
- package/dist/backend/llvm/local-array-reads.js.map +1 -1
- package/dist/backend/llvm/reference-effects.js +34 -1
- package/dist/backend/llvm/reference-effects.js.map +1 -1
- package/dist/backend/llvm/shapes.d.ts +14 -3
- package/dist/backend/llvm/shapes.js +43 -12
- package/dist/backend/llvm/shapes.js.map +1 -1
- package/dist/backend/llvm/shapes.test.js +5 -5
- package/dist/backend/llvm/shapes.test.js.map +1 -1
- package/dist/backend/llvm/string-reads.js +29 -26
- package/dist/backend/llvm/string-reads.js.map +1 -1
- package/dist/backend/llvm/string-slices.js +1 -1
- package/dist/backend/llvm/trunc.d.ts +8 -0
- package/dist/backend/llvm/trunc.js +28 -0
- package/dist/backend/llvm/trunc.js.map +1 -0
- package/dist/backend/loop-polls.d.ts +14 -0
- package/dist/backend/loop-polls.js +100 -0
- package/dist/backend/loop-polls.js.map +1 -0
- package/dist/backend/loop-polls.test.d.ts +1 -0
- package/dist/backend/loop-polls.test.js +86 -0
- package/dist/backend/loop-polls.test.js.map +1 -0
- package/dist/backend/may-throw.js +20 -1
- package/dist/backend/may-throw.js.map +1 -1
- package/dist/backend/may-throw.test.js +23 -0
- package/dist/backend/may-throw.test.js.map +1 -1
- package/dist/backend/native-codegen.d.ts +1 -3
- package/dist/backend/native-codegen.js +4 -32
- package/dist/backend/native-codegen.js.map +1 -1
- package/dist/backend/runtime-abi.d.ts +2 -2
- package/dist/backend/runtime-abi.js +2 -2
- package/dist/backend/runtime-abi.js.map +1 -1
- package/dist/backend/runtime-pack-targets.test.js +1 -1
- package/dist/backend/runtime-pack-targets.test.js.map +1 -1
- package/dist/backend/runtime-pack.test.js +1 -1
- package/dist/backend/runtime-pack.test.js.map +1 -1
- package/dist/cli/command.js +8 -1
- package/dist/cli/command.js.map +1 -1
- package/dist/cli/output-options.d.ts +1 -0
- package/dist/cli/output-options.js +3 -0
- package/dist/cli/output-options.js.map +1 -1
- package/dist/cli/usage.d.ts +5 -1
- package/dist/cli/usage.js +6 -0
- package/dist/cli/usage.js.map +1 -1
- package/dist/compile-types.d.ts +5 -0
- package/dist/executable/executable-cache.d.ts +2 -0
- package/dist/executable/executable-cache.js +3 -0
- package/dist/executable/executable-cache.js.map +1 -1
- package/dist/executable/executable-cache.test.js +1 -0
- package/dist/executable/executable-cache.test.js.map +1 -1
- package/dist/frontend/lowering/array-values.js +37 -1
- package/dist/frontend/lowering/array-values.js.map +1 -1
- package/dist/frontend/lowering/builtins/strings.js +4 -1
- package/dist/frontend/lowering/builtins/strings.js.map +1 -1
- package/dist/frontend/lowering/call-arguments.js +96 -4
- package/dist/frontend/lowering/call-arguments.js.map +1 -1
- package/dist/frontend/lowering/class-method-mutations.js +88 -0
- package/dist/frontend/lowering/class-method-mutations.js.map +1 -1
- package/dist/frontend/lowering/containers/bytes.js +15 -4
- package/dist/frontend/lowering/containers/bytes.js.map +1 -1
- package/dist/frontend/lowering/expressions/union-fields.d.ts +19 -0
- package/dist/frontend/lowering/expressions/union-fields.js +236 -0
- package/dist/frontend/lowering/expressions/union-fields.js.map +1 -0
- package/dist/frontend/lowering/lower-calls.js +12 -2
- package/dist/frontend/lowering/lower-calls.js.map +1 -1
- package/dist/frontend/lowering/lower-exprs.d.ts +6 -0
- package/dist/frontend/lowering/lower-exprs.js +162 -3
- package/dist/frontend/lowering/lower-exprs.js.map +1 -1
- package/dist/frontend/lowering/lower-island.js +5 -19
- package/dist/frontend/lowering/lower-island.js.map +1 -1
- package/dist/frontend/lowering/lower-stmts.js +11 -1
- package/dist/frontend/lowering/lower-stmts.js.map +1 -1
- package/dist/frontend/lowering/lowerer.d.ts +5 -0
- package/dist/frontend/lowering/lowerer.js +174 -8
- package/dist/frontend/lowering/lowerer.js.map +1 -1
- package/dist/frontend/lowering/nan-coded-reads.d.ts +161 -0
- package/dist/frontend/lowering/nan-coded-reads.js +1342 -0
- package/dist/frontend/lowering/nan-coded-reads.js.map +1 -0
- package/dist/frontend/lowering/runtime-optional-bounds.d.ts +14 -2
- package/dist/frontend/lowering/runtime-optional-bounds.js +17 -11
- package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -1
- package/dist/frontend/lowering/runtime-optional-bounds.test.js +28 -0
- package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -1
- package/dist/frontend/lowering/surfaces.js +8 -0
- package/dist/frontend/lowering/surfaces.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -8
- package/dist/index.js.map +1 -1
- package/dist/ir/builtin-signatures.d.ts +1 -1
- package/dist/ir/builtin-signatures.js +2 -0
- package/dist/ir/builtin-signatures.js.map +1 -1
- package/dist/ir/constant-globals.d.ts +14 -0
- package/dist/ir/constant-globals.js +81 -0
- package/dist/ir/constant-globals.js.map +1 -0
- package/dist/ir/constant-globals.test.d.ts +1 -0
- package/dist/ir/constant-globals.test.js +97 -0
- package/dist/ir/constant-globals.test.js.map +1 -0
- package/dist/ir/final-fields.d.ts +35 -0
- package/dist/ir/final-fields.js +187 -0
- package/dist/ir/final-fields.js.map +1 -0
- package/dist/ir/final-fields.test.d.ts +1 -0
- package/dist/ir/final-fields.test.js +106 -0
- package/dist/ir/final-fields.test.js.map +1 -0
- package/dist/ir/int32-slots.d.ts +9 -0
- package/dist/ir/int32-slots.js +70 -57
- package/dist/ir/int32-slots.js.map +1 -1
- package/dist/ir/int32-slots.test.js +27 -0
- package/dist/ir/int32-slots.test.js.map +1 -1
- package/dist/ir/integer-loops.d.ts +5 -0
- package/dist/ir/integer-loops.js +20 -4
- package/dist/ir/integer-loops.js.map +1 -1
- package/dist/ir/integer-loops.test.js +17 -0
- package/dist/ir/integer-loops.test.js.map +1 -1
- package/dist/ir/ir.d.ts +6 -1
- package/dist/ir/ir.js +4 -0
- package/dist/ir/ir.js.map +1 -1
- package/dist/ir/serialize.d.ts +12 -2
- package/dist/ir/serialize.js +190 -39
- package/dist/ir/serialize.js.map +1 -1
- package/dist/ir/serialize.test.js +65 -1
- package/dist/ir/serialize.test.js.map +1 -1
- package/dist/native/cache.d.ts +8 -1
- package/dist/native/cache.js +38 -1
- package/dist/native/cache.js.map +1 -1
- package/dist/native/cache.test.js +11 -0
- package/dist/native/cache.test.js.map +1 -1
- package/dist/native/compiler.js +4 -3
- package/dist/native/compiler.js.map +1 -1
- package/dist/native/prepare.d.ts +3 -1
- package/dist/native/prepare.js +63 -28
- package/dist/native/prepare.js.map +1 -1
- package/dist/native/prepare.test.js +23 -1
- package/dist/native/prepare.test.js.map +1 -1
- package/dist/native/text-file.d.ts +10 -0
- package/dist/native/text-file.js +41 -0
- package/dist/native/text-file.js.map +1 -0
- package/dist/text-parts.d.ts +5 -0
- package/dist/text-parts.js +36 -0
- package/dist/text-parts.js.map +1 -0
- package/package.json +20 -20
- package/surface-manifest.json +7 -1
- package/dist/frontend/lowering/expressions/union-field-write.d.ts +0 -8
- package/dist/frontend/lowering/expressions/union-field-write.js +0 -108
- package/dist/frontend/lowering/expressions/union-field-write.js.map +0 -1
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
import * as ts from "../ts7/adapter.js";
|
|
2
|
+
import { isJsSourceFile } from "../program.js";
|
|
3
|
+
import { isIntegerBytesElem } from "../../ir/ir.js";
|
|
4
|
+
import { indexReadInBounds } from "./runtime-optional-bounds.js";
|
|
5
|
+
function peelTransparent(node) {
|
|
6
|
+
let e = node;
|
|
7
|
+
while (ts.isParenthesizedExpression(e) ||
|
|
8
|
+
ts.isAsExpression(e) ||
|
|
9
|
+
ts.isTypeAssertion(e) ||
|
|
10
|
+
ts.isSatisfiesExpression(e) ||
|
|
11
|
+
ts.isNonNullExpression(e))
|
|
12
|
+
e = e.expression;
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
/** The node whose consumer decides how `node`'s value is used. */
|
|
16
|
+
function transparentParent(node) {
|
|
17
|
+
let e = node;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const p = e.parent;
|
|
20
|
+
if (p &&
|
|
21
|
+
(ts.isParenthesizedExpression(p) ||
|
|
22
|
+
ts.isAsExpression(p) ||
|
|
23
|
+
ts.isTypeAssertion(p) ||
|
|
24
|
+
ts.isSatisfiesExpression(p) ||
|
|
25
|
+
ts.isNonNullExpression(p)) &&
|
|
26
|
+
p.expression === e) {
|
|
27
|
+
e = p;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isAssignmentOperator(kind) {
|
|
34
|
+
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
|
35
|
+
}
|
|
36
|
+
const RELATIONAL = new Set([
|
|
37
|
+
ts.SyntaxKind.LessThanToken,
|
|
38
|
+
ts.SyntaxKind.GreaterThanToken,
|
|
39
|
+
ts.SyntaxKind.LessThanEqualsToken,
|
|
40
|
+
ts.SyntaxKind.GreaterThanEqualsToken,
|
|
41
|
+
]);
|
|
42
|
+
const EQUALITY = new Set([
|
|
43
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
44
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
45
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
46
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
47
|
+
]);
|
|
48
|
+
/** Results never NaN-free: ToNumber of each operand may be NaN. */
|
|
49
|
+
const ARITHMETIC = new Set([
|
|
50
|
+
ts.SyntaxKind.MinusToken,
|
|
51
|
+
ts.SyntaxKind.AsteriskToken,
|
|
52
|
+
ts.SyntaxKind.SlashToken,
|
|
53
|
+
ts.SyntaxKind.PercentToken,
|
|
54
|
+
ts.SyntaxKind.AsteriskAsteriskToken,
|
|
55
|
+
]);
|
|
56
|
+
/** ToInt32/ToUint32 map undefined and NaN to 0 alike; results are integers. */
|
|
57
|
+
const BITWISE = new Set([
|
|
58
|
+
ts.SyntaxKind.AmpersandToken,
|
|
59
|
+
ts.SyntaxKind.BarToken,
|
|
60
|
+
ts.SyntaxKind.CaretToken,
|
|
61
|
+
ts.SyntaxKind.LessThanLessThanToken,
|
|
62
|
+
ts.SyntaxKind.GreaterThanGreaterThanToken,
|
|
63
|
+
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken,
|
|
64
|
+
]);
|
|
65
|
+
/** Compound assignments whose operator converts undefined like NaN. */
|
|
66
|
+
const NUMERIC_COMPOUND = new Set([
|
|
67
|
+
ts.SyntaxKind.MinusEqualsToken,
|
|
68
|
+
ts.SyntaxKind.AsteriskEqualsToken,
|
|
69
|
+
ts.SyntaxKind.SlashEqualsToken,
|
|
70
|
+
ts.SyntaxKind.PercentEqualsToken,
|
|
71
|
+
ts.SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
72
|
+
ts.SyntaxKind.AmpersandEqualsToken,
|
|
73
|
+
ts.SyntaxKind.BarEqualsToken,
|
|
74
|
+
ts.SyntaxKind.CaretEqualsToken,
|
|
75
|
+
ts.SyntaxKind.LessThanLessThanEqualsToken,
|
|
76
|
+
ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
77
|
+
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
78
|
+
]);
|
|
79
|
+
/** `Math` functions convert every argument with ToNumber. */
|
|
80
|
+
const MATH_INTEGER_RESULTS = new Set(["imul", "clz32", "sign"]);
|
|
81
|
+
function isLoopOrIf(node) {
|
|
82
|
+
return (ts.isIfStatement(node) ||
|
|
83
|
+
ts.isWhileStatement(node) ||
|
|
84
|
+
ts.isDoStatement(node) ||
|
|
85
|
+
ts.isForStatement(node));
|
|
86
|
+
}
|
|
87
|
+
/** Constructors of fixed-length number typed arrays. */
|
|
88
|
+
const TYPED_ARRAYS = new Set([
|
|
89
|
+
"Int8Array",
|
|
90
|
+
"Uint8Array",
|
|
91
|
+
"Uint8ClampedArray",
|
|
92
|
+
"Int16Array",
|
|
93
|
+
"Uint16Array",
|
|
94
|
+
"Int32Array",
|
|
95
|
+
"Uint32Array",
|
|
96
|
+
"Float32Array",
|
|
97
|
+
"Float64Array",
|
|
98
|
+
]);
|
|
99
|
+
function nonNegativeIntegerLiteral(node) {
|
|
100
|
+
const e = peelTransparent(node);
|
|
101
|
+
if (!ts.isNumericLiteral(e))
|
|
102
|
+
return false;
|
|
103
|
+
const value = Number(e.text);
|
|
104
|
+
return Number.isInteger(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
|
|
105
|
+
}
|
|
106
|
+
/** `const a = ..., b = ...;` or two consecutive single declarations. */
|
|
107
|
+
function adjacentDeclarations(a, b) {
|
|
108
|
+
if (!ts.isVariableDeclaration(a) || !ts.isVariableDeclaration(b))
|
|
109
|
+
return false;
|
|
110
|
+
const la = a.parent;
|
|
111
|
+
const lb = b.parent;
|
|
112
|
+
if (!ts.isVariableDeclarationList(la) || !ts.isVariableDeclarationList(lb))
|
|
113
|
+
return false;
|
|
114
|
+
if (la === lb) {
|
|
115
|
+
const ia = la.declarations.indexOf(a);
|
|
116
|
+
const ib = la.declarations.indexOf(b);
|
|
117
|
+
return Math.abs(ia - ib) === 1;
|
|
118
|
+
}
|
|
119
|
+
const sa = la.parent;
|
|
120
|
+
const sb = lb.parent;
|
|
121
|
+
if (!ts.isVariableStatement(sa) || !ts.isVariableStatement(sb) || sa.parent !== sb.parent)
|
|
122
|
+
return false;
|
|
123
|
+
if (la.declarations.length !== 1 || lb.declarations.length !== 1)
|
|
124
|
+
return false;
|
|
125
|
+
const list = sa.parent;
|
|
126
|
+
if (!ts.isBlock(list) &&
|
|
127
|
+
!ts.isSourceFile(list) &&
|
|
128
|
+
!ts.isCaseClause(list) &&
|
|
129
|
+
!ts.isDefaultClause(list))
|
|
130
|
+
return false;
|
|
131
|
+
const statements = list.statements;
|
|
132
|
+
return Math.abs(statements.indexOf(sa) - statements.indexOf(sb)) === 1;
|
|
133
|
+
}
|
|
134
|
+
export class NanCodedReads {
|
|
135
|
+
host;
|
|
136
|
+
files;
|
|
137
|
+
/** Typed-array element reads that can lower as NaN-coded doubles. */
|
|
138
|
+
reads = new Map();
|
|
139
|
+
/** Typed-array element reads proven to name an existing element. */
|
|
140
|
+
presentReads = new Map();
|
|
141
|
+
counters = new Map();
|
|
142
|
+
fixedLengths = new Map();
|
|
143
|
+
bindings = new Map();
|
|
144
|
+
notSlots = new Set();
|
|
145
|
+
/** Every slot's value occurrences. */
|
|
146
|
+
useNodes = new Set();
|
|
147
|
+
returns = new Map();
|
|
148
|
+
/** Identifiers by text across the analyzed files. */
|
|
149
|
+
names = new Map();
|
|
150
|
+
symbolCache = new Map();
|
|
151
|
+
pneCache = new Map();
|
|
152
|
+
pneReturnCache = new Map();
|
|
153
|
+
fnEligibility = new Map();
|
|
154
|
+
callSites = new Map();
|
|
155
|
+
/** Slots that are NaN-coded after the fixed point. */
|
|
156
|
+
eligible = new Set();
|
|
157
|
+
genuineNaN = new Set();
|
|
158
|
+
numericReads = new Set();
|
|
159
|
+
analyzed = false;
|
|
160
|
+
constructor(host, files) {
|
|
161
|
+
this.host = host;
|
|
162
|
+
this.files = files;
|
|
163
|
+
}
|
|
164
|
+
/** The read lowers to a double holding NaN for an invalid index. */
|
|
165
|
+
isNumericRead(read) {
|
|
166
|
+
return this.numericReads.has(read) || this.presentReads.has(read);
|
|
167
|
+
}
|
|
168
|
+
/** Both operands of this equality are NaN-coded values that may stand
|
|
169
|
+
* for undefined: undefined equals undefined, so equal NaNs compare equal. */
|
|
170
|
+
needsUndefinedEquality(expr) {
|
|
171
|
+
if (!EQUALITY.has(expr.operatorToken.kind))
|
|
172
|
+
return false;
|
|
173
|
+
const left = this.loweredInfo(expr.left);
|
|
174
|
+
const right = this.loweredInfo(expr.right);
|
|
175
|
+
return left.nce && right.nce && !left.nan && !right.nan;
|
|
176
|
+
}
|
|
177
|
+
/** Bindings and returns that are NaN-coded (consistency checks). */
|
|
178
|
+
nanCodedBinding(symbol) {
|
|
179
|
+
const slot = this.bindings.get(symbol);
|
|
180
|
+
return slot !== undefined && this.eligible.has(slot);
|
|
181
|
+
}
|
|
182
|
+
nanCodedReturn(symbol) {
|
|
183
|
+
const slot = this.returns.get(symbol);
|
|
184
|
+
return slot !== undefined && this.eligible.has(slot);
|
|
185
|
+
}
|
|
186
|
+
get size() {
|
|
187
|
+
return {
|
|
188
|
+
reads: this.numericReads.size,
|
|
189
|
+
slots: this.eligible.size,
|
|
190
|
+
present: this.presentReads.size,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
analyze() {
|
|
194
|
+
if (this.analyzed)
|
|
195
|
+
return;
|
|
196
|
+
this.analyzed = true;
|
|
197
|
+
const candidates = new Map();
|
|
198
|
+
for (const sf of this.files) {
|
|
199
|
+
if (isJsSourceFile(sf) || sf.isDeclarationFile)
|
|
200
|
+
continue;
|
|
201
|
+
ts.walkPreorder(sf, (node) => {
|
|
202
|
+
if (ts.isIdentifier(node)) {
|
|
203
|
+
const list = this.names.get(node.text);
|
|
204
|
+
if (list)
|
|
205
|
+
list.push(node);
|
|
206
|
+
else
|
|
207
|
+
this.names.set(node.text, [node]);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (ts.isElementAccessExpression(node) && this.isReadCandidate(node)) {
|
|
211
|
+
const elem = this.host.bytesElemOf(node.expression);
|
|
212
|
+
if (elem)
|
|
213
|
+
candidates.set(node, elem);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// A proven-present read is a plain number; only the rest may be
|
|
218
|
+
// undefined. The proof needs every reference, so it runs after the walk.
|
|
219
|
+
for (const [read, elem] of candidates)
|
|
220
|
+
(this.presentRead(read) ? this.presentReads : this.reads).set(read, elem);
|
|
221
|
+
if (this.reads.size === 0)
|
|
222
|
+
return;
|
|
223
|
+
// Forward: the slots a read can reach.
|
|
224
|
+
const work = [...this.reads.keys()];
|
|
225
|
+
const reached = new Set();
|
|
226
|
+
while (work.length > 0) {
|
|
227
|
+
const value = work.pop();
|
|
228
|
+
const slot = this.sinkOf(value);
|
|
229
|
+
if (!slot || reached.has(slot))
|
|
230
|
+
continue;
|
|
231
|
+
reached.add(slot);
|
|
232
|
+
for (const use of slot.uses)
|
|
233
|
+
work.push(use);
|
|
234
|
+
}
|
|
235
|
+
for (const slot of reached)
|
|
236
|
+
this.eligible.add(slot);
|
|
237
|
+
// Backward: drop slots with an unsafe use or source until stable.
|
|
238
|
+
for (let changed = true; changed;) {
|
|
239
|
+
changed = false;
|
|
240
|
+
this.computeGenuineNaN();
|
|
241
|
+
for (const slot of [...this.eligible]) {
|
|
242
|
+
if (this.slotOk(slot))
|
|
243
|
+
continue;
|
|
244
|
+
if (this.removals)
|
|
245
|
+
this.removals.set(slot, this.removalReason(slot));
|
|
246
|
+
this.eligible.delete(slot);
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
this.computeGenuineNaN();
|
|
251
|
+
for (const read of this.reads.keys())
|
|
252
|
+
if (this.useSafe(read))
|
|
253
|
+
this.numericReads.add(read);
|
|
254
|
+
if (process.env["SCRIPTC_DEBUG_NAN_CODED"])
|
|
255
|
+
this.debugReport(reached);
|
|
256
|
+
}
|
|
257
|
+
removals = process.env["SCRIPTC_DEBUG_NAN_CODED"]
|
|
258
|
+
? new Map()
|
|
259
|
+
: null;
|
|
260
|
+
removalReason(slot) {
|
|
261
|
+
const badSource = slot.sources.find((s) => s === null || this.sourceInfo(s) === null);
|
|
262
|
+
if (badSource !== undefined)
|
|
263
|
+
return `source ${badSource ? badSource.getText().slice(0, 60) : "<unknown>"}`;
|
|
264
|
+
const nan = this.genuineNaN.has(slot);
|
|
265
|
+
const badUse = slot.uses.find((u) => !this.useSafe(u) && (nan || !this.reboxConsumer(u)));
|
|
266
|
+
if (badUse)
|
|
267
|
+
return `${nan ? "nan-use" : "use"} ${transparentParent(badUse).parent?.getText().slice(0, 80) ?? "?"}`;
|
|
268
|
+
return "?";
|
|
269
|
+
}
|
|
270
|
+
debugReport(reached) {
|
|
271
|
+
const name = (slot) => {
|
|
272
|
+
const decl = this.host.declarationOf(slot.symbol);
|
|
273
|
+
const sf = decl?.getSourceFile();
|
|
274
|
+
const where = sf && decl ? `${sf.fileName}:${decl.getStart()}` : "?";
|
|
275
|
+
return `${slot.kind} ${slot.symbol.name} @ ${where}`;
|
|
276
|
+
};
|
|
277
|
+
for (const slot of reached) {
|
|
278
|
+
const ok = this.eligible.has(slot);
|
|
279
|
+
const why = ok
|
|
280
|
+
? this.genuineNaN.has(slot)
|
|
281
|
+
? "(nan)"
|
|
282
|
+
: `(rebox ${slot.uses.filter((u) => !this.useSafe(u)).length})`
|
|
283
|
+
: (this.removals?.get(slot) ?? "?");
|
|
284
|
+
process.stderr.write(`nan-coded ${ok ? "yes" : "no "} ${name(slot)} ${why}\n`);
|
|
285
|
+
}
|
|
286
|
+
process.stderr.write(`nan-coded reads ${this.numericReads.size}/${this.reads.size}, present ${this.presentReads.size}, slots ${this.eligible.size}/${reached.size}\n`);
|
|
287
|
+
}
|
|
288
|
+
symbolOf(node) {
|
|
289
|
+
// A cached null is an answer: `!` would be a checked extraction in a
|
|
290
|
+
// native build of the compiler.
|
|
291
|
+
const cached = this.symbolCache.get(node);
|
|
292
|
+
if (cached !== undefined)
|
|
293
|
+
return cached;
|
|
294
|
+
const symbol = this.host.symbolOf(node);
|
|
295
|
+
this.symbolCache.set(node, symbol);
|
|
296
|
+
return symbol;
|
|
297
|
+
}
|
|
298
|
+
/** An element read (never a write target) of a TypeScript file. */
|
|
299
|
+
isReadCandidate(read) {
|
|
300
|
+
if (read.questionDotToken)
|
|
301
|
+
return false;
|
|
302
|
+
let n = read;
|
|
303
|
+
let p = n.parent;
|
|
304
|
+
while (p && ts.isParenthesizedExpression(p)) {
|
|
305
|
+
n = p;
|
|
306
|
+
p = n.parent;
|
|
307
|
+
}
|
|
308
|
+
// `t[i]!` asserts presence: it already lowers to the plain numeric read
|
|
309
|
+
// and is never optional.
|
|
310
|
+
if (!p || ts.isNonNullExpression(p))
|
|
311
|
+
return false;
|
|
312
|
+
if (ts.isBinaryExpression(p) && p.left === n && isAssignmentOperator(p.operatorToken.kind))
|
|
313
|
+
return false;
|
|
314
|
+
if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
|
|
315
|
+
(p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken))
|
|
316
|
+
return false;
|
|
317
|
+
if (ts.isDeleteExpression(p))
|
|
318
|
+
return false;
|
|
319
|
+
if (ts.isArrayLiteralExpression(p) ||
|
|
320
|
+
ts.isShorthandPropertyAssignment(p) ||
|
|
321
|
+
ts.isPropertyAssignment(p) ||
|
|
322
|
+
ts.isSpreadElement(p) ||
|
|
323
|
+
ts.isForOfStatement(p) ||
|
|
324
|
+
ts.isForInStatement(p))
|
|
325
|
+
return false;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
// ---- present reads ------------------------------------------------------
|
|
329
|
+
/** `t[i]` names an existing element: `i` is a non-negative integer
|
|
330
|
+
* counter and a dominating guard bounds it by `t.length` (or by the
|
|
331
|
+
* length of a typed array that always has the same length).
|
|
332
|
+
*
|
|
333
|
+
* The guard proof only sees code written between the guard and the read.
|
|
334
|
+
* Code it cannot see (a getter, a conversion hook) cannot reach either
|
|
335
|
+
* operand: `t` is a fixed-length typed array no other code can reach, and
|
|
336
|
+
* only the reading function itself writes `i`. */
|
|
337
|
+
presentRead(read) {
|
|
338
|
+
const array = peelTransparent(read.expression);
|
|
339
|
+
const index = peelTransparent(read.argumentExpression);
|
|
340
|
+
if (!ts.isIdentifier(array) || !ts.isIdentifier(index))
|
|
341
|
+
return false;
|
|
342
|
+
const arraySymbol = this.symbolOf(array);
|
|
343
|
+
if (!arraySymbol || !this.fixedLength(arraySymbol))
|
|
344
|
+
return false;
|
|
345
|
+
const symbol = this.symbolOf(index);
|
|
346
|
+
if (!symbol || !this.nonNegativeCounter(symbol))
|
|
347
|
+
return false;
|
|
348
|
+
const decl = this.host.declarationOf(symbol);
|
|
349
|
+
if (!decl || this.containingFunction(decl) !== this.containingFunction(read))
|
|
350
|
+
return false;
|
|
351
|
+
return indexReadInBounds(read, {
|
|
352
|
+
sameLength: (array, other) => this.sameFixedLength(array, other),
|
|
353
|
+
conditions: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/** A `let`/`const` binding that starts at a non-negative integer literal
|
|
357
|
+
* and only ever grows by non-negative integer literals (`i++`, `++i`,
|
|
358
|
+
* `i += 2`) or is reset to one (`i = 0`): it always holds a non-negative
|
|
359
|
+
* integer. Every write is in the declaring function (no closure writes). */
|
|
360
|
+
nonNegativeCounter(symbol) {
|
|
361
|
+
const known = this.counters.get(symbol);
|
|
362
|
+
if (known !== undefined)
|
|
363
|
+
return known;
|
|
364
|
+
this.counters.set(symbol, false);
|
|
365
|
+
const decl = this.host.declarationOf(symbol);
|
|
366
|
+
if (!decl ||
|
|
367
|
+
!ts.isVariableDeclaration(decl) ||
|
|
368
|
+
!ts.isIdentifier(decl.name) ||
|
|
369
|
+
!decl.initializer ||
|
|
370
|
+
!nonNegativeIntegerLiteral(decl.initializer) ||
|
|
371
|
+
!ts.isVariableDeclarationList(decl.parent) ||
|
|
372
|
+
!(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) ||
|
|
373
|
+
isJsSourceFile(decl.getSourceFile()))
|
|
374
|
+
return false;
|
|
375
|
+
const name = decl.name;
|
|
376
|
+
const owner = this.containingFunction(decl);
|
|
377
|
+
for (const id of this.names.get(name.text) ?? []) {
|
|
378
|
+
if (id === name || this.symbolOf(id) !== symbol)
|
|
379
|
+
continue;
|
|
380
|
+
let n = id;
|
|
381
|
+
let p = n.parent;
|
|
382
|
+
while (p && ts.isParenthesizedExpression(p)) {
|
|
383
|
+
n = p;
|
|
384
|
+
p = n.parent;
|
|
385
|
+
}
|
|
386
|
+
if (!p)
|
|
387
|
+
return false;
|
|
388
|
+
if (this.writeOf(id) !== "read" && this.containingFunction(id) !== owner)
|
|
389
|
+
return false;
|
|
390
|
+
if (ts.isBinaryExpression(p) && p.left === n && isAssignmentOperator(p.operatorToken.kind)) {
|
|
391
|
+
const k = p.operatorToken.kind;
|
|
392
|
+
if (k !== ts.SyntaxKind.EqualsToken && k !== ts.SyntaxKind.PlusEqualsToken)
|
|
393
|
+
return false;
|
|
394
|
+
if (!nonNegativeIntegerLiteral(p.right))
|
|
395
|
+
return false;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) {
|
|
399
|
+
if (p.operator === ts.SyntaxKind.MinusMinusToken)
|
|
400
|
+
return false;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Any other write position (destructuring, for-in/of) is a write of
|
|
404
|
+
// an unknown value.
|
|
405
|
+
if (this.writeOf(id) !== "read")
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
this.counters.set(symbol, true);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
/** Two typed arrays whose lengths are equal for their whole lifetime. */
|
|
412
|
+
sameFixedLength(array, other) {
|
|
413
|
+
const a = peelTransparent(array);
|
|
414
|
+
const b = peelTransparent(other);
|
|
415
|
+
if (!ts.isIdentifier(a) || !ts.isIdentifier(b))
|
|
416
|
+
return false;
|
|
417
|
+
const sa = this.symbolOf(a);
|
|
418
|
+
const sb = this.symbolOf(b);
|
|
419
|
+
if (!sa || !sb || sa === sb)
|
|
420
|
+
return false;
|
|
421
|
+
const na = this.fixedLength(sa);
|
|
422
|
+
const nb = this.fixedLength(sb);
|
|
423
|
+
if (!na || !nb)
|
|
424
|
+
return false;
|
|
425
|
+
const la = na.arguments[0];
|
|
426
|
+
const lb = nb.arguments[0];
|
|
427
|
+
// The same literal length.
|
|
428
|
+
const literal = (e) => {
|
|
429
|
+
const x = peelTransparent(e);
|
|
430
|
+
return ts.isNumericLiteral(x) ? Number(x.text) : null;
|
|
431
|
+
};
|
|
432
|
+
const literalA = literal(la);
|
|
433
|
+
if (literalA !== null)
|
|
434
|
+
return literalA === literal(lb);
|
|
435
|
+
// The same `const` binding.
|
|
436
|
+
const ia = peelTransparent(la);
|
|
437
|
+
const ib = peelTransparent(lb);
|
|
438
|
+
if (ts.isIdentifier(ia) && ts.isIdentifier(ib)) {
|
|
439
|
+
const symbol = this.symbolOf(ia);
|
|
440
|
+
if (!symbol || symbol !== this.symbolOf(ib))
|
|
441
|
+
return false;
|
|
442
|
+
const decl = this.host.declarationOf(symbol);
|
|
443
|
+
return (decl !== undefined &&
|
|
444
|
+
ts.isVariableDeclaration(decl) &&
|
|
445
|
+
ts.isVariableDeclarationList(decl.parent) &&
|
|
446
|
+
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
|
447
|
+
this.host.isNumber(ia));
|
|
448
|
+
}
|
|
449
|
+
// `xs.length` of the same `const` array or typed array, read by two
|
|
450
|
+
// adjacent declarations: nothing runs between the two reads that could
|
|
451
|
+
// change it (allocating a typed array runs no program code).
|
|
452
|
+
if (ts.isPropertyAccessExpression(ia) &&
|
|
453
|
+
ts.isPropertyAccessExpression(ib) &&
|
|
454
|
+
!ia.questionDotToken &&
|
|
455
|
+
!ib.questionDotToken &&
|
|
456
|
+
ia.name.text === "length" &&
|
|
457
|
+
ib.name.text === "length" &&
|
|
458
|
+
ts.isIdentifier(peelTransparent(ia.expression)) &&
|
|
459
|
+
ts.isIdentifier(peelTransparent(ib.expression)) &&
|
|
460
|
+
this.host.hasLength(ia.expression)) {
|
|
461
|
+
const owner = this.symbolOf(peelTransparent(ia.expression));
|
|
462
|
+
if (!owner || owner !== this.symbolOf(peelTransparent(ib.expression)))
|
|
463
|
+
return false;
|
|
464
|
+
const ownerDecl = this.host.declarationOf(owner);
|
|
465
|
+
if (!ownerDecl ||
|
|
466
|
+
!((ts.isVariableDeclaration(ownerDecl) &&
|
|
467
|
+
ts.isVariableDeclarationList(ownerDecl.parent) &&
|
|
468
|
+
ownerDecl.parent.flags & ts.NodeFlags.Const) ||
|
|
469
|
+
ts.isParameter(ownerDecl)))
|
|
470
|
+
return false;
|
|
471
|
+
return adjacentDeclarations(na.parent, nb.parent);
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/** The allocation `new Uint8Array(n)` of a `const` typed array whose every
|
|
476
|
+
* reference is an element access or `.length`: nothing can reach its
|
|
477
|
+
* buffer to detach it, so its length never changes. */
|
|
478
|
+
fixedLength(symbol) {
|
|
479
|
+
const known = this.fixedLengths.get(symbol);
|
|
480
|
+
if (known !== undefined)
|
|
481
|
+
return known;
|
|
482
|
+
this.fixedLengths.set(symbol, null);
|
|
483
|
+
const decl = this.host.declarationOf(symbol);
|
|
484
|
+
if (!decl ||
|
|
485
|
+
!ts.isVariableDeclaration(decl) ||
|
|
486
|
+
!ts.isIdentifier(decl.name) ||
|
|
487
|
+
!decl.initializer ||
|
|
488
|
+
!ts.isVariableDeclarationList(decl.parent) ||
|
|
489
|
+
!(decl.parent.flags & ts.NodeFlags.Const) ||
|
|
490
|
+
// A module binding can escape through an export.
|
|
491
|
+
(ts.isVariableStatement(decl.parent.parent) && ts.isSourceFile(decl.parent.parent.parent)) ||
|
|
492
|
+
isJsSourceFile(decl.getSourceFile()) ||
|
|
493
|
+
this.host.bytesElemOf(decl.name) === null)
|
|
494
|
+
return null;
|
|
495
|
+
const init = peelTransparent(decl.initializer);
|
|
496
|
+
if (!ts.isNewExpression(init) ||
|
|
497
|
+
!ts.isIdentifier(init.expression) ||
|
|
498
|
+
!TYPED_ARRAYS.has(init.expression.text) ||
|
|
499
|
+
!this.host.isStdlibGlobal(init.expression, init.expression.text) ||
|
|
500
|
+
init.typeArguments ||
|
|
501
|
+
init.arguments?.length !== 1 ||
|
|
502
|
+
ts.isSpreadElement(init.arguments[0]) ||
|
|
503
|
+
!this.host.isNumber(init.arguments[0]))
|
|
504
|
+
return null;
|
|
505
|
+
const name = decl.name;
|
|
506
|
+
for (const id of this.names.get(name.text) ?? []) {
|
|
507
|
+
if (id === name || this.symbolOf(id) !== symbol)
|
|
508
|
+
continue;
|
|
509
|
+
let n = id;
|
|
510
|
+
let p = n.parent;
|
|
511
|
+
while (p && ts.isParenthesizedExpression(p)) {
|
|
512
|
+
n = p;
|
|
513
|
+
p = n.parent;
|
|
514
|
+
}
|
|
515
|
+
if (!p)
|
|
516
|
+
return null;
|
|
517
|
+
if (ts.isElementAccessExpression(p) && p.expression === n)
|
|
518
|
+
continue;
|
|
519
|
+
if (ts.isPropertyAccessExpression(p) &&
|
|
520
|
+
p.expression === n &&
|
|
521
|
+
p.name.text === "length" &&
|
|
522
|
+
!(p.parent &&
|
|
523
|
+
ts.isBinaryExpression(p.parent) &&
|
|
524
|
+
p.parent.left === p &&
|
|
525
|
+
isAssignmentOperator(p.parent.operatorToken.kind)))
|
|
526
|
+
continue;
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
this.fixedLengths.set(symbol, init);
|
|
530
|
+
return init;
|
|
531
|
+
}
|
|
532
|
+
// ---- slots --------------------------------------------------------------
|
|
533
|
+
/** The function whose `return` statement contains `node`. */
|
|
534
|
+
containingFunction(node) {
|
|
535
|
+
let n = node.parent;
|
|
536
|
+
while (n &&
|
|
537
|
+
!ts.isFunctionLike(n) &&
|
|
538
|
+
!ts.isClassStaticBlockDeclaration(n) &&
|
|
539
|
+
!ts.isSourceFile(n))
|
|
540
|
+
n = n.parent;
|
|
541
|
+
return n;
|
|
542
|
+
}
|
|
543
|
+
/** The declaration of a directly called function or method, when every
|
|
544
|
+
* reference to it is a direct call (no value use, no overrides). */
|
|
545
|
+
eligibleFunction(symbol) {
|
|
546
|
+
const known = this.fnEligibility.get(symbol);
|
|
547
|
+
if (known !== undefined)
|
|
548
|
+
return known;
|
|
549
|
+
this.fnEligibility.set(symbol, null);
|
|
550
|
+
const decl = this.host.declarationOf(symbol);
|
|
551
|
+
if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl)))
|
|
552
|
+
return null;
|
|
553
|
+
if (!decl.body || decl.asteriskToken || decl.typeParameters || !decl.name)
|
|
554
|
+
return null;
|
|
555
|
+
if (!ts.isIdentifier(decl.name))
|
|
556
|
+
return null;
|
|
557
|
+
const sf = decl.getSourceFile();
|
|
558
|
+
if (isJsSourceFile(sf) || sf.isDeclarationFile)
|
|
559
|
+
return null;
|
|
560
|
+
const flags = ts.getCombinedModifierFlags(decl);
|
|
561
|
+
if (flags & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient))
|
|
562
|
+
return null;
|
|
563
|
+
if (ts.isMethodDeclaration(decl)) {
|
|
564
|
+
const cls = decl.parent;
|
|
565
|
+
if (!ts.isClassDeclaration(cls) || (cls.heritageClauses?.length ?? 0) > 0)
|
|
566
|
+
return null;
|
|
567
|
+
if (this.host.methodOverridden(symbol))
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
// Sloppy-mode `arguments` aliases parameters.
|
|
571
|
+
let usesArguments = false;
|
|
572
|
+
ts.walkPreorder(decl.body, (node) => {
|
|
573
|
+
if (ts.isIdentifier(node) && node.text === "arguments")
|
|
574
|
+
usesArguments = true;
|
|
575
|
+
});
|
|
576
|
+
if (usesArguments)
|
|
577
|
+
return null;
|
|
578
|
+
const calls = this.directCalls(symbol, decl.name);
|
|
579
|
+
if (!calls)
|
|
580
|
+
return null;
|
|
581
|
+
this.fnEligibility.set(symbol, decl);
|
|
582
|
+
return decl;
|
|
583
|
+
}
|
|
584
|
+
/** Every call of the function, or null when some reference is not a
|
|
585
|
+
* direct, non-optional call. */
|
|
586
|
+
directCalls(symbol, declName) {
|
|
587
|
+
const known = this.callSites.get(symbol);
|
|
588
|
+
if (known !== undefined)
|
|
589
|
+
return known;
|
|
590
|
+
const calls = [];
|
|
591
|
+
let ok = true;
|
|
592
|
+
for (const id of this.names.get(declName.text) ?? []) {
|
|
593
|
+
if (id === declName)
|
|
594
|
+
continue;
|
|
595
|
+
// Import and export specifiers only alias the function.
|
|
596
|
+
const parent = id.parent;
|
|
597
|
+
if (parent &&
|
|
598
|
+
(ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isImportClause(parent)))
|
|
599
|
+
continue;
|
|
600
|
+
if (this.symbolOf(id) !== symbol)
|
|
601
|
+
continue;
|
|
602
|
+
let callee = id;
|
|
603
|
+
const p = id.parent;
|
|
604
|
+
if (p && ts.isPropertyAccessExpression(p) && p.name === id) {
|
|
605
|
+
if (p.questionDotToken)
|
|
606
|
+
ok = false;
|
|
607
|
+
callee = p;
|
|
608
|
+
}
|
|
609
|
+
const call = callee.parent;
|
|
610
|
+
if (!call ||
|
|
611
|
+
!ts.isCallExpression(call) ||
|
|
612
|
+
call.expression !== callee ||
|
|
613
|
+
call.questionDotToken ||
|
|
614
|
+
call.arguments.some((a) => ts.isSpreadElement(a))) {
|
|
615
|
+
ok = false;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
calls.push(call);
|
|
619
|
+
}
|
|
620
|
+
const result = ok ? calls : null;
|
|
621
|
+
this.callSites.set(symbol, result);
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
bindingSlot(symbol) {
|
|
625
|
+
const existing = this.bindings.get(symbol);
|
|
626
|
+
if (existing)
|
|
627
|
+
return existing;
|
|
628
|
+
if (this.notSlots.has(symbol))
|
|
629
|
+
return null;
|
|
630
|
+
const slot = this.scanBindingSlot(symbol);
|
|
631
|
+
if (slot)
|
|
632
|
+
this.bindings.set(symbol, slot);
|
|
633
|
+
else
|
|
634
|
+
this.notSlots.add(symbol);
|
|
635
|
+
return slot;
|
|
636
|
+
}
|
|
637
|
+
scanBindingSlot(symbol) {
|
|
638
|
+
const decl = this.host.declarationOf(symbol);
|
|
639
|
+
if (!decl)
|
|
640
|
+
return null;
|
|
641
|
+
const sf = decl.getSourceFile();
|
|
642
|
+
if (isJsSourceFile(sf) || sf.isDeclarationFile)
|
|
643
|
+
return null;
|
|
644
|
+
let nameNode;
|
|
645
|
+
const slot = {
|
|
646
|
+
kind: "binding",
|
|
647
|
+
symbol,
|
|
648
|
+
sources: [],
|
|
649
|
+
uses: [],
|
|
650
|
+
arithmeticWrites: false,
|
|
651
|
+
reassigned: false,
|
|
652
|
+
};
|
|
653
|
+
if (ts.isVariableDeclaration(decl)) {
|
|
654
|
+
const list = decl.parent;
|
|
655
|
+
if (!ts.isIdentifier(decl.name) ||
|
|
656
|
+
!ts.isVariableDeclarationList(list) ||
|
|
657
|
+
!(list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) ||
|
|
658
|
+
ts.isForOfStatement(list.parent) ||
|
|
659
|
+
ts.isForInStatement(list.parent))
|
|
660
|
+
return null;
|
|
661
|
+
// Module bindings are globals with their own promotion rules.
|
|
662
|
+
if (ts.isVariableStatement(list.parent) && ts.isSourceFile(list.parent.parent))
|
|
663
|
+
return null;
|
|
664
|
+
if (!this.host.isNumber(decl.name))
|
|
665
|
+
return null;
|
|
666
|
+
nameNode = decl.name;
|
|
667
|
+
if (decl.initializer)
|
|
668
|
+
slot.sources.push(decl.initializer);
|
|
669
|
+
}
|
|
670
|
+
else if (ts.isParameter(decl)) {
|
|
671
|
+
if (!ts.isIdentifier(decl.name) || decl.dotDotDotToken || decl.questionToken)
|
|
672
|
+
return null;
|
|
673
|
+
const fn = decl.parent;
|
|
674
|
+
if (!ts.isFunctionDeclaration(fn) && !ts.isMethodDeclaration(fn))
|
|
675
|
+
return null;
|
|
676
|
+
const fnSymbol = fn.name ? this.symbolOf(fn.name) : null;
|
|
677
|
+
if (!fnSymbol || !this.eligibleFunction(fnSymbol))
|
|
678
|
+
return null;
|
|
679
|
+
const index = fn.parameters.indexOf(decl);
|
|
680
|
+
if (index < 0 || !this.host.paramIsNumber(fnSymbol, index))
|
|
681
|
+
return null;
|
|
682
|
+
nameNode = decl.name;
|
|
683
|
+
if (decl.initializer)
|
|
684
|
+
slot.sources.push(decl.initializer);
|
|
685
|
+
for (const call of this.callSites.get(fnSymbol) ?? []) {
|
|
686
|
+
const arg = call.arguments[index];
|
|
687
|
+
if (arg)
|
|
688
|
+
slot.sources.push(arg);
|
|
689
|
+
else if (!decl.initializer)
|
|
690
|
+
slot.sources.push(null);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else
|
|
694
|
+
return null;
|
|
695
|
+
for (const id of this.names.get(nameNode.text) ?? []) {
|
|
696
|
+
if (id === nameNode || this.symbolOf(id) !== symbol)
|
|
697
|
+
continue;
|
|
698
|
+
const write = this.writeOf(id);
|
|
699
|
+
if (write !== "read")
|
|
700
|
+
slot.reassigned = true;
|
|
701
|
+
if (write === "read")
|
|
702
|
+
slot.uses.push(id);
|
|
703
|
+
else if (write === "arith") {
|
|
704
|
+
slot.arithmeticWrites = true;
|
|
705
|
+
// The compound operator reads the binding as well.
|
|
706
|
+
slot.uses.push(id);
|
|
707
|
+
}
|
|
708
|
+
else if (write === "unknown")
|
|
709
|
+
slot.sources.push(null);
|
|
710
|
+
else
|
|
711
|
+
slot.sources.push(write);
|
|
712
|
+
}
|
|
713
|
+
for (const use of slot.uses)
|
|
714
|
+
this.useNodes.add(use);
|
|
715
|
+
return slot;
|
|
716
|
+
}
|
|
717
|
+
/** How an identifier reference touches its binding. */
|
|
718
|
+
writeOf(id) {
|
|
719
|
+
let n = id;
|
|
720
|
+
let p = n.parent;
|
|
721
|
+
while (p && ts.isParenthesizedExpression(p)) {
|
|
722
|
+
n = p;
|
|
723
|
+
p = n.parent;
|
|
724
|
+
}
|
|
725
|
+
if (!p)
|
|
726
|
+
return "read";
|
|
727
|
+
if (ts.isBinaryExpression(p) && p.left === n && isAssignmentOperator(p.operatorToken.kind)) {
|
|
728
|
+
const k = p.operatorToken.kind;
|
|
729
|
+
if (k === ts.SyntaxKind.EqualsToken)
|
|
730
|
+
return p.right;
|
|
731
|
+
if (k === ts.SyntaxKind.PlusEqualsToken)
|
|
732
|
+
return this.host.isNumber(p.right) ? "arith" : "unknown";
|
|
733
|
+
return NUMERIC_COMPOUND.has(k) ? "arith" : "unknown";
|
|
734
|
+
}
|
|
735
|
+
if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
|
|
736
|
+
(p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken))
|
|
737
|
+
return "arith";
|
|
738
|
+
if (ts.isArrayLiteralExpression(p) ||
|
|
739
|
+
ts.isShorthandPropertyAssignment(p) ||
|
|
740
|
+
ts.isSpreadElement(p) ||
|
|
741
|
+
(ts.isPropertyAssignment(p) && p.initializer === n) ||
|
|
742
|
+
((ts.isForOfStatement(p) || ts.isForInStatement(p)) && p.initializer === n)) {
|
|
743
|
+
// A destructuring target, or an object literal value (a read).
|
|
744
|
+
return ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)
|
|
745
|
+
? this.inDestructuringTarget(p)
|
|
746
|
+
? "unknown"
|
|
747
|
+
: "read"
|
|
748
|
+
: ts.isForOfStatement(p) || ts.isForInStatement(p)
|
|
749
|
+
? "unknown"
|
|
750
|
+
: this.inDestructuringTarget(p)
|
|
751
|
+
? "unknown"
|
|
752
|
+
: "read";
|
|
753
|
+
}
|
|
754
|
+
return "read";
|
|
755
|
+
}
|
|
756
|
+
inDestructuringTarget(node) {
|
|
757
|
+
let n = node;
|
|
758
|
+
let p = n.parent;
|
|
759
|
+
while (p &&
|
|
760
|
+
(ts.isArrayLiteralExpression(p) ||
|
|
761
|
+
ts.isObjectLiteralExpression(p) ||
|
|
762
|
+
ts.isPropertyAssignment(p) ||
|
|
763
|
+
ts.isShorthandPropertyAssignment(p) ||
|
|
764
|
+
ts.isSpreadElement(p) ||
|
|
765
|
+
ts.isSpreadAssignment(p) ||
|
|
766
|
+
ts.isParenthesizedExpression(p))) {
|
|
767
|
+
n = p;
|
|
768
|
+
p = n.parent;
|
|
769
|
+
}
|
|
770
|
+
if (!p)
|
|
771
|
+
return false;
|
|
772
|
+
if (ts.isBinaryExpression(p) &&
|
|
773
|
+
p.left === n &&
|
|
774
|
+
p.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
|
775
|
+
return true;
|
|
776
|
+
return (ts.isForOfStatement(p) || ts.isForInStatement(p)) && p.initializer === n;
|
|
777
|
+
}
|
|
778
|
+
returnSlot(symbol) {
|
|
779
|
+
const existing = this.returns.get(symbol);
|
|
780
|
+
if (existing)
|
|
781
|
+
return existing;
|
|
782
|
+
const decl = this.eligibleFunction(symbol);
|
|
783
|
+
if (!decl || !decl.body || !this.host.returnIsNumber(symbol))
|
|
784
|
+
return null;
|
|
785
|
+
const slot = {
|
|
786
|
+
kind: "return",
|
|
787
|
+
symbol,
|
|
788
|
+
sources: [],
|
|
789
|
+
uses: [...(this.callSites.get(symbol) ?? [])],
|
|
790
|
+
arithmeticWrites: false,
|
|
791
|
+
};
|
|
792
|
+
const body = decl.body;
|
|
793
|
+
ts.walkPreorder(body, (node) => {
|
|
794
|
+
if (!ts.isReturnStatement(node) || this.containingFunction(node) !== decl)
|
|
795
|
+
return;
|
|
796
|
+
slot.sources.push(node.expression ?? null);
|
|
797
|
+
});
|
|
798
|
+
for (const use of slot.uses)
|
|
799
|
+
this.useNodes.add(use);
|
|
800
|
+
this.returns.set(symbol, slot);
|
|
801
|
+
return slot;
|
|
802
|
+
}
|
|
803
|
+
/** The callee of a call: a function or method symbol. */
|
|
804
|
+
calleeSymbol(call) {
|
|
805
|
+
if (call.questionDotToken)
|
|
806
|
+
return null;
|
|
807
|
+
let callee = call.expression;
|
|
808
|
+
while (ts.isParenthesizedExpression(callee))
|
|
809
|
+
callee = callee.expression;
|
|
810
|
+
if (ts.isIdentifier(callee))
|
|
811
|
+
return this.symbolOf(callee);
|
|
812
|
+
if (ts.isPropertyAccessExpression(callee) && !callee.questionDotToken)
|
|
813
|
+
return this.symbolOf(callee.name);
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/** The slot a value flows into, through transparent wrappers and
|
|
817
|
+
* conditional branches; null for a direct consumer. */
|
|
818
|
+
sinkOf(value) {
|
|
819
|
+
const e = transparentParent(value);
|
|
820
|
+
const p = e.parent;
|
|
821
|
+
if (!p)
|
|
822
|
+
return null;
|
|
823
|
+
if (ts.isConditionalExpression(p) && p.condition !== e)
|
|
824
|
+
return this.sinkOf(p);
|
|
825
|
+
if (ts.isBinaryExpression(p)) {
|
|
826
|
+
const k = p.operatorToken.kind;
|
|
827
|
+
if (k === ts.SyntaxKind.CommaToken && p.right === e)
|
|
828
|
+
return this.sinkOf(p);
|
|
829
|
+
if (k === ts.SyntaxKind.EqualsToken && p.right === e) {
|
|
830
|
+
const target = peelTransparent(p.left);
|
|
831
|
+
if (!ts.isIdentifier(target))
|
|
832
|
+
return null;
|
|
833
|
+
const symbol = this.symbolOf(target);
|
|
834
|
+
return symbol ? this.bindingSlot(symbol) : null;
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
if (ts.isVariableDeclaration(p) && p.initializer === e && ts.isIdentifier(p.name)) {
|
|
839
|
+
const symbol = this.symbolOf(p.name);
|
|
840
|
+
return symbol ? this.bindingSlot(symbol) : null;
|
|
841
|
+
}
|
|
842
|
+
if (ts.isReturnStatement(p)) {
|
|
843
|
+
const fn = this.containingFunction(p);
|
|
844
|
+
if (!fn || (!ts.isFunctionDeclaration(fn) && !ts.isMethodDeclaration(fn)) || !fn.name)
|
|
845
|
+
return null;
|
|
846
|
+
const symbol = this.symbolOf(fn.name);
|
|
847
|
+
return symbol ? this.returnSlot(symbol) : null;
|
|
848
|
+
}
|
|
849
|
+
if (ts.isCallExpression(p) && p.expression !== e) {
|
|
850
|
+
const index = p.arguments.indexOf(e);
|
|
851
|
+
const fn = this.calleeSymbol(p);
|
|
852
|
+
if (index < 0 || !fn)
|
|
853
|
+
return null;
|
|
854
|
+
const decl = this.eligibleFunction(fn);
|
|
855
|
+
const param = decl?.parameters[index];
|
|
856
|
+
if (!param || !ts.isIdentifier(param.name))
|
|
857
|
+
return null;
|
|
858
|
+
const symbol = this.symbolOf(param.name);
|
|
859
|
+
return symbol ? this.bindingSlot(symbol) : null;
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
/** A slot that never holds a genuine NaN encodes undefined exactly, so
|
|
864
|
+
* any consumer can rebuild the union (see `reboxes`); a slot that may
|
|
865
|
+
* hold a genuine NaN needs NaN-insensitive consumers throughout. */
|
|
866
|
+
slotOk(slot) {
|
|
867
|
+
for (const source of slot.sources)
|
|
868
|
+
if (source === null || !this.sourceInfo(source))
|
|
869
|
+
return false;
|
|
870
|
+
const nan = this.genuineNaN.has(slot);
|
|
871
|
+
for (const use of slot.uses)
|
|
872
|
+
if (!this.useSafe(use) && (nan || !this.reboxConsumer(use)))
|
|
873
|
+
return false;
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
/** The eligible slot whose value this occurrence reads, if any. */
|
|
877
|
+
slotOfValue(node) {
|
|
878
|
+
if (ts.isIdentifier(node)) {
|
|
879
|
+
const symbol = this.symbolOf(node);
|
|
880
|
+
const slot = symbol ? this.bindings.get(symbol) : undefined;
|
|
881
|
+
return slot && this.eligible.has(slot) && this.useNodes.has(node) ? slot : null;
|
|
882
|
+
}
|
|
883
|
+
if (ts.isCallExpression(node)) {
|
|
884
|
+
const fn = this.calleeSymbol(node);
|
|
885
|
+
const slot = fn ? this.returns.get(fn) : undefined;
|
|
886
|
+
return slot && this.eligible.has(slot) && this.useNodes.has(node) ? slot : null;
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
/** This read of a NaN-coded slot reaches a consumer that could observe
|
|
891
|
+
* undefined: lowering rebuilds `number | undefined` from the double (NaN
|
|
892
|
+
* is undefined, exactly, since the slot never holds a genuine NaN), and
|
|
893
|
+
* the optional-read analysis treats the occurrence as optional, so the
|
|
894
|
+
* consumer lowers exactly as it does for an ordinary read. */
|
|
895
|
+
reboxes(node) {
|
|
896
|
+
if (!ts.isIdentifier(node) && !ts.isCallExpression(node))
|
|
897
|
+
return false;
|
|
898
|
+
const slot = this.slotOfValue(node);
|
|
899
|
+
return slot !== null && !this.genuineNaN.has(slot) && !this.useSafe(node);
|
|
900
|
+
}
|
|
901
|
+
/** A consumer that takes the value whole, so the rebuilt union lowers
|
|
902
|
+
* there exactly like an ordinary optional read: an argument, a stored
|
|
903
|
+
* or returned value, a literal element, or string concatenation. Any
|
|
904
|
+
* other consumer (a member access, `typeof`, `??`, ...) keeps the slot
|
|
905
|
+
* on the ordinary optional lowering instead. */
|
|
906
|
+
reboxConsumer(value) {
|
|
907
|
+
const e = transparentParent(value);
|
|
908
|
+
const p = e.parent;
|
|
909
|
+
if (!p)
|
|
910
|
+
return false;
|
|
911
|
+
if (ts.isConditionalExpression(p))
|
|
912
|
+
return p.condition !== e && this.reboxConsumer(p);
|
|
913
|
+
if (ts.isCallExpression(p) || ts.isNewExpression(p))
|
|
914
|
+
return p.expression !== e;
|
|
915
|
+
if (ts.isVariableDeclaration(p) || ts.isReturnStatement(p))
|
|
916
|
+
return true;
|
|
917
|
+
if (ts.isArrayLiteralExpression(p) || ts.isTemplateSpan(p))
|
|
918
|
+
return true;
|
|
919
|
+
if (ts.isPropertyAssignment(p))
|
|
920
|
+
return p.initializer === e;
|
|
921
|
+
if (ts.isBinaryExpression(p)) {
|
|
922
|
+
const k = p.operatorToken.kind;
|
|
923
|
+
if (k === ts.SyntaxKind.EqualsToken)
|
|
924
|
+
return p.right === e;
|
|
925
|
+
if (k === ts.SyntaxKind.PlusToken)
|
|
926
|
+
return true;
|
|
927
|
+
if (k === ts.SyntaxKind.CommaToken)
|
|
928
|
+
return p.right === e && this.reboxConsumer(p);
|
|
929
|
+
}
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
computeGenuineNaN() {
|
|
933
|
+
this.genuineNaN.clear();
|
|
934
|
+
for (let changed = true; changed;) {
|
|
935
|
+
changed = false;
|
|
936
|
+
for (const slot of this.eligible) {
|
|
937
|
+
if (this.genuineNaN.has(slot))
|
|
938
|
+
continue;
|
|
939
|
+
let nan = slot.arithmeticWrites;
|
|
940
|
+
for (const source of slot.sources) {
|
|
941
|
+
if (nan)
|
|
942
|
+
break;
|
|
943
|
+
if (source)
|
|
944
|
+
nan = this.sourceInfo(source)?.nan ?? true;
|
|
945
|
+
}
|
|
946
|
+
if (nan) {
|
|
947
|
+
this.genuineNaN.add(slot);
|
|
948
|
+
changed = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// ---- values -------------------------------------------------------------
|
|
954
|
+
/** A value allowed into a NaN-coded slot, or null. */
|
|
955
|
+
sourceInfo(node) {
|
|
956
|
+
const e = peelTransparent(node);
|
|
957
|
+
if (ts.isConditionalExpression(e)) {
|
|
958
|
+
const a = this.sourceInfo(e.whenTrue);
|
|
959
|
+
const b = a && this.sourceInfo(e.whenFalse);
|
|
960
|
+
return a && b ? { nce: a.nce || b.nce, nan: a.nan || b.nan } : null;
|
|
961
|
+
}
|
|
962
|
+
if (ts.isElementAccessExpression(e)) {
|
|
963
|
+
const elem = this.reads.get(e);
|
|
964
|
+
if (elem !== undefined)
|
|
965
|
+
return { nce: true, nan: !isIntegerBytesElem(elem) };
|
|
966
|
+
}
|
|
967
|
+
if (ts.isIdentifier(e)) {
|
|
968
|
+
if (this.guardedOccurrence(e))
|
|
969
|
+
return { nce: false, nan: false };
|
|
970
|
+
const symbol = this.symbolOf(e);
|
|
971
|
+
const slot = symbol ? this.bindings.get(symbol) : undefined;
|
|
972
|
+
if (slot && this.eligible.has(slot))
|
|
973
|
+
return { nce: true, nan: this.genuineNaN.has(slot) };
|
|
974
|
+
}
|
|
975
|
+
if (ts.isCallExpression(e)) {
|
|
976
|
+
const fn = this.calleeSymbol(e);
|
|
977
|
+
const slot = fn ? this.returns.get(fn) : undefined;
|
|
978
|
+
if (slot && this.eligible.has(slot) && this.useNodes.has(e))
|
|
979
|
+
return { nce: true, nan: this.genuineNaN.has(slot) };
|
|
980
|
+
}
|
|
981
|
+
if (ts.isBinaryExpression(e) && this.nanFreeArithmetic(e, 0))
|
|
982
|
+
return { nce: false, nan: false };
|
|
983
|
+
const plain = this.plainNumber(e, 0);
|
|
984
|
+
if (plain)
|
|
985
|
+
return plain;
|
|
986
|
+
// Any other number value may be undefined at run time (an optional
|
|
987
|
+
// read the default lowering widens): lowering converts it with
|
|
988
|
+
// ToNumber on entry (see `convertsSource`), so the slot may then hold
|
|
989
|
+
// a genuine NaN.
|
|
990
|
+
return this.host.isNumber(e) ? { nce: true, nan: true } : null;
|
|
991
|
+
}
|
|
992
|
+
/** The value flows into a NaN-coded slot: lowering converts an optional
|
|
993
|
+
* number to NaN-for-undefined on entry. */
|
|
994
|
+
convertsSource(node) {
|
|
995
|
+
const slot = this.sinkOf(node);
|
|
996
|
+
return slot !== null && this.eligible.has(slot);
|
|
997
|
+
}
|
|
998
|
+
/** The occurrence reads a NaN-coded slot. */
|
|
999
|
+
readsNanCodedSlot(node) {
|
|
1000
|
+
return this.slotOfValue(node) !== null;
|
|
1001
|
+
}
|
|
1002
|
+
/** `+`, `-` or `*` over finite literals and integer values proven present
|
|
1003
|
+
* where they are read: the result is never NaN. A NaN-coded slot that
|
|
1004
|
+
* never holds a genuine NaN holds integers, finite plain numbers or NaN
|
|
1005
|
+
* for undefined, and a comparison guarding the read excludes NaN. */
|
|
1006
|
+
nanFreeArithmetic(node, depth) {
|
|
1007
|
+
if (depth > 6)
|
|
1008
|
+
return false;
|
|
1009
|
+
const e = peelTransparent(node);
|
|
1010
|
+
if (ts.isNumericLiteral(e))
|
|
1011
|
+
return Number.isFinite(Number(e.text)) && Math.abs(Number(e.text)) < 2 ** 53;
|
|
1012
|
+
if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.MinusToken)
|
|
1013
|
+
return this.nanFreeArithmetic(e.operand, depth + 1);
|
|
1014
|
+
if (ts.isBinaryExpression(e)) {
|
|
1015
|
+
const k = e.operatorToken.kind;
|
|
1016
|
+
if (k === ts.SyntaxKind.PlusToken || k === ts.SyntaxKind.MinusToken) {
|
|
1017
|
+
if (!this.host.isNumber(e.left) || !this.host.isNumber(e.right))
|
|
1018
|
+
return false;
|
|
1019
|
+
return (this.nanFreeArithmetic(e.left, depth + 1) && this.nanFreeArithmetic(e.right, depth + 1));
|
|
1020
|
+
}
|
|
1021
|
+
if (k === ts.SyntaxKind.AsteriskToken)
|
|
1022
|
+
return ((ts.isNumericLiteral(peelTransparent(e.left)) ||
|
|
1023
|
+
ts.isNumericLiteral(peelTransparent(e.right))) &&
|
|
1024
|
+
this.nanFreeArithmetic(e.left, depth + 1) &&
|
|
1025
|
+
this.nanFreeArithmetic(e.right, depth + 1));
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (ts.isPropertyAccessExpression(e)) {
|
|
1029
|
+
const info = this.plainNumber(e, 0);
|
|
1030
|
+
return info !== null && !info.nan;
|
|
1031
|
+
}
|
|
1032
|
+
if (!ts.isIdentifier(e))
|
|
1033
|
+
return false;
|
|
1034
|
+
const symbol = this.symbolOf(e);
|
|
1035
|
+
if (!symbol)
|
|
1036
|
+
return false;
|
|
1037
|
+
const slot = this.bindings.get(symbol);
|
|
1038
|
+
if (slot && this.eligible.has(slot)) {
|
|
1039
|
+
return (!this.genuineNaN.has(slot) &&
|
|
1040
|
+
slot.sources.length <= 1 &&
|
|
1041
|
+
!slot.arithmeticWrites &&
|
|
1042
|
+
this.provenPresent(e, symbol));
|
|
1043
|
+
}
|
|
1044
|
+
const info = this.plainNumber(e, 0);
|
|
1045
|
+
return info !== null && !info.nan;
|
|
1046
|
+
}
|
|
1047
|
+
/** An occurrence of a never-reassigned number binding under a dominating
|
|
1048
|
+
* comparison with a number that is never NaN (`x >= 0 ? x : y`): the
|
|
1049
|
+
* comparison is false for undefined and for NaN, so here the binding
|
|
1050
|
+
* holds a plain number that is not NaN, even when the binding itself is
|
|
1051
|
+
* NaN-coded. */
|
|
1052
|
+
guardedOccurrence(use) {
|
|
1053
|
+
const symbol = this.symbolOf(use);
|
|
1054
|
+
if (!symbol)
|
|
1055
|
+
return false;
|
|
1056
|
+
const slot = this.bindingSlot(symbol);
|
|
1057
|
+
return (slot !== null &&
|
|
1058
|
+
!slot.reassigned &&
|
|
1059
|
+
!slot.arithmeticWrites &&
|
|
1060
|
+
this.useNodes.has(use) &&
|
|
1061
|
+
this.provenPresent(use, symbol));
|
|
1062
|
+
}
|
|
1063
|
+
/** A dominating condition compares the never-reassigned binding with a
|
|
1064
|
+
* number literal, which is false for undefined. */
|
|
1065
|
+
provenPresent(use, symbol) {
|
|
1066
|
+
let child = use;
|
|
1067
|
+
for (let n = use.parent; n && !ts.isFunctionLike(n); child = n, n = n.parent) {
|
|
1068
|
+
let condition;
|
|
1069
|
+
if (ts.isIfStatement(n) && n.thenStatement === child)
|
|
1070
|
+
condition = n.expression;
|
|
1071
|
+
else if (ts.isConditionalExpression(n) && n.whenTrue === child)
|
|
1072
|
+
condition = n.condition;
|
|
1073
|
+
else if (ts.isBinaryExpression(n) &&
|
|
1074
|
+
n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
1075
|
+
n.right === child)
|
|
1076
|
+
condition = n.left;
|
|
1077
|
+
if (condition && this.conditionExcludesUndefined(condition, symbol))
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
conditionExcludesUndefined(condition, symbol) {
|
|
1083
|
+
const e = peelTransparent(condition);
|
|
1084
|
+
if (!ts.isBinaryExpression(e))
|
|
1085
|
+
return false;
|
|
1086
|
+
const k = e.operatorToken.kind;
|
|
1087
|
+
if (k === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
1088
|
+
return (this.conditionExcludesUndefined(e.left, symbol) ||
|
|
1089
|
+
this.conditionExcludesUndefined(e.right, symbol));
|
|
1090
|
+
if (!RELATIONAL.has(k) && k !== ts.SyntaxKind.EqualsEqualsEqualsToken)
|
|
1091
|
+
return false;
|
|
1092
|
+
const left = peelTransparent(e.left);
|
|
1093
|
+
const right = peelTransparent(e.right);
|
|
1094
|
+
const names = (x) => ts.isIdentifier(x) && this.symbolOf(x) === symbol;
|
|
1095
|
+
const literal = (x) => {
|
|
1096
|
+
const info = this.plainNumber(x, 0);
|
|
1097
|
+
return info !== null && !info.nan;
|
|
1098
|
+
};
|
|
1099
|
+
return (names(left) && literal(right)) || (names(right) && literal(left));
|
|
1100
|
+
}
|
|
1101
|
+
/** How the value lowers once the analysis is final: NaN-coded (a numeric
|
|
1102
|
+
* read or a read of a NaN-coded slot) or not. */
|
|
1103
|
+
loweredInfo(node) {
|
|
1104
|
+
const e = peelTransparent(node);
|
|
1105
|
+
if (ts.isConditionalExpression(e)) {
|
|
1106
|
+
const a = this.loweredInfo(e.whenTrue);
|
|
1107
|
+
const b = this.loweredInfo(e.whenFalse);
|
|
1108
|
+
return { nce: a.nce || b.nce, nan: a.nan || b.nan };
|
|
1109
|
+
}
|
|
1110
|
+
if (ts.isElementAccessExpression(e)) {
|
|
1111
|
+
const elem = this.reads.get(e);
|
|
1112
|
+
if (elem !== undefined && this.numericReads.has(e))
|
|
1113
|
+
return { nce: true, nan: !isIntegerBytesElem(elem) };
|
|
1114
|
+
}
|
|
1115
|
+
if (ts.isIdentifier(e) && this.guardedOccurrence(e))
|
|
1116
|
+
return { nce: false, nan: false };
|
|
1117
|
+
const slot = this.slotOfValue(e);
|
|
1118
|
+
if (slot)
|
|
1119
|
+
return { nce: true, nan: this.genuineNaN.has(slot) };
|
|
1120
|
+
return { nce: false, nan: true };
|
|
1121
|
+
}
|
|
1122
|
+
/** A number expression that is never undefined in Node and always lowers
|
|
1123
|
+
* to a plain double, whatever the optional-read analysis decides. */
|
|
1124
|
+
plainNumber(node, depth) {
|
|
1125
|
+
if (depth > 8)
|
|
1126
|
+
return null;
|
|
1127
|
+
const cached = this.pneCache.get(node);
|
|
1128
|
+
if (cached !== undefined)
|
|
1129
|
+
return cached;
|
|
1130
|
+
const result = this.computePlainNumber(node, depth);
|
|
1131
|
+
this.pneCache.set(node, result);
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
computePlainNumber(node, depth) {
|
|
1135
|
+
let e = node;
|
|
1136
|
+
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e))
|
|
1137
|
+
e = e.expression;
|
|
1138
|
+
if (ts.isNumericLiteral(e))
|
|
1139
|
+
return { nce: false, nan: false };
|
|
1140
|
+
if (ts.isElementAccessExpression(e)) {
|
|
1141
|
+
const elem = this.presentReads.get(e);
|
|
1142
|
+
return elem === undefined ? null : { nce: false, nan: !isIntegerBytesElem(elem) };
|
|
1143
|
+
}
|
|
1144
|
+
if (ts.isPrefixUnaryExpression(e)) {
|
|
1145
|
+
if (e.operator === ts.SyntaxKind.TildeToken)
|
|
1146
|
+
return { nce: false, nan: false };
|
|
1147
|
+
if (e.operator === ts.SyntaxKind.MinusToken || e.operator === ts.SyntaxKind.PlusToken)
|
|
1148
|
+
return { nce: false, nan: !ts.isNumericLiteral(e.operand) };
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
if (ts.isBinaryExpression(e)) {
|
|
1152
|
+
const k = e.operatorToken.kind;
|
|
1153
|
+
if (BITWISE.has(k))
|
|
1154
|
+
return { nce: false, nan: false };
|
|
1155
|
+
if (ARITHMETIC.has(k))
|
|
1156
|
+
return { nce: false, nan: true };
|
|
1157
|
+
if (k === ts.SyntaxKind.PlusToken &&
|
|
1158
|
+
this.host.isNumber(e.left) &&
|
|
1159
|
+
this.host.isNumber(e.right))
|
|
1160
|
+
return { nce: false, nan: true };
|
|
1161
|
+
// A number fallback for a nullish (or falsy) left value: never
|
|
1162
|
+
// undefined. The left value is used as is, so it may be NaN.
|
|
1163
|
+
if ((k === ts.SyntaxKind.QuestionQuestionToken || k === ts.SyntaxKind.BarBarToken) &&
|
|
1164
|
+
this.host.isNumber(e) &&
|
|
1165
|
+
this.plainNumber(e.right, depth + 1) !== null)
|
|
1166
|
+
return { nce: false, nan: true };
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
|
1170
|
+
const symbol = this.symbolOf(e.name);
|
|
1171
|
+
if (symbol && symbol.flags & ts.SymbolFlags.EnumMember && this.host.isNumber(e))
|
|
1172
|
+
return { nce: false, nan: false };
|
|
1173
|
+
if (e.name.text === "length" &&
|
|
1174
|
+
(this.host.isString(e.expression) || this.host.hasLength(e.expression)))
|
|
1175
|
+
return { nce: false, nan: false };
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
if (ts.isCallExpression(e) && !e.questionDotToken) {
|
|
1179
|
+
const callee = e.expression;
|
|
1180
|
+
if (ts.isPropertyAccessExpression(callee) && !callee.questionDotToken) {
|
|
1181
|
+
if (this.host.isStdlibGlobal(callee.expression, "Math"))
|
|
1182
|
+
return { nce: false, nan: !MATH_INTEGER_RESULTS.has(callee.name.text) };
|
|
1183
|
+
if (callee.name.text === "charCodeAt" && this.host.isString(callee.expression))
|
|
1184
|
+
return { nce: false, nan: true };
|
|
1185
|
+
}
|
|
1186
|
+
const fn = this.calleeSymbol(e);
|
|
1187
|
+
return fn ? this.plainReturn(fn, depth) : null;
|
|
1188
|
+
}
|
|
1189
|
+
if (ts.isIdentifier(e)) {
|
|
1190
|
+
const symbol = this.symbolOf(e);
|
|
1191
|
+
const decl = symbol ? this.host.declarationOf(symbol) : undefined;
|
|
1192
|
+
if (decl &&
|
|
1193
|
+
ts.isVariableDeclaration(decl) &&
|
|
1194
|
+
decl.initializer &&
|
|
1195
|
+
ts.isVariableDeclarationList(decl.parent) &&
|
|
1196
|
+
decl.parent.flags & ts.NodeFlags.Const &&
|
|
1197
|
+
!isJsSourceFile(decl.getSourceFile()))
|
|
1198
|
+
return this.plainNumber(decl.initializer, depth + 1);
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
/** A function whose every return value is a plain number. */
|
|
1204
|
+
plainReturn(fn, depth) {
|
|
1205
|
+
const known = this.pneReturnCache.get(fn);
|
|
1206
|
+
if (known !== undefined)
|
|
1207
|
+
return known;
|
|
1208
|
+
this.pneReturnCache.set(fn, null);
|
|
1209
|
+
const decl = this.eligibleFunction(fn);
|
|
1210
|
+
if (!decl?.body || !this.host.returnIsNumber(fn))
|
|
1211
|
+
return null;
|
|
1212
|
+
let result = { nce: false, nan: false };
|
|
1213
|
+
ts.walkPreorder(decl.body, (node) => {
|
|
1214
|
+
if (!result || !ts.isReturnStatement(node) || this.containingFunction(node) !== decl)
|
|
1215
|
+
return;
|
|
1216
|
+
const info = node.expression ? this.plainNumber(node.expression, depth + 1) : null;
|
|
1217
|
+
result = info ? { nce: false, nan: result.nan || info.nan } : null;
|
|
1218
|
+
});
|
|
1219
|
+
this.pneReturnCache.set(fn, result);
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
// ---- uses ---------------------------------------------------------------
|
|
1223
|
+
/** Whether the value's consumer cannot tell undefined from NaN. */
|
|
1224
|
+
useSafe(value) {
|
|
1225
|
+
// A plain number here, whatever consumes it.
|
|
1226
|
+
if (ts.isIdentifier(value) && this.guardedOccurrence(value))
|
|
1227
|
+
return true;
|
|
1228
|
+
const e = transparentParent(value);
|
|
1229
|
+
const p = e.parent;
|
|
1230
|
+
if (!p)
|
|
1231
|
+
return false;
|
|
1232
|
+
if (ts.isConditionalExpression(p))
|
|
1233
|
+
return p.condition === e || this.useSafe(p);
|
|
1234
|
+
if (ts.isExpressionStatement(p) || ts.isVoidExpression(p))
|
|
1235
|
+
return true;
|
|
1236
|
+
if (isLoopOrIf(p)) {
|
|
1237
|
+
if (ts.isForStatement(p))
|
|
1238
|
+
return p.condition === e || p.incrementor === e;
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
if (ts.isPrefixUnaryExpression(p)) {
|
|
1242
|
+
return (p.operator === ts.SyntaxKind.MinusToken ||
|
|
1243
|
+
p.operator === ts.SyntaxKind.PlusToken ||
|
|
1244
|
+
p.operator === ts.SyntaxKind.TildeToken ||
|
|
1245
|
+
p.operator === ts.SyntaxKind.ExclamationToken);
|
|
1246
|
+
}
|
|
1247
|
+
if (ts.isBinaryExpression(p)) {
|
|
1248
|
+
const k = p.operatorToken.kind;
|
|
1249
|
+
const other = p.left === e ? p.right : p.left;
|
|
1250
|
+
if (RELATIONAL.has(k) || ARITHMETIC.has(k) || BITWISE.has(k))
|
|
1251
|
+
return true;
|
|
1252
|
+
if (k === ts.SyntaxKind.PlusToken)
|
|
1253
|
+
return this.host.isNumber(other);
|
|
1254
|
+
if (EQUALITY.has(k)) {
|
|
1255
|
+
const otherInfo = this.sourceInfo(other);
|
|
1256
|
+
if (!otherInfo)
|
|
1257
|
+
return false;
|
|
1258
|
+
if (!otherInfo.nce)
|
|
1259
|
+
return true;
|
|
1260
|
+
const self = this.sourceInfo(e);
|
|
1261
|
+
return self !== null && !self.nan && !otherInfo.nan;
|
|
1262
|
+
}
|
|
1263
|
+
if (k === ts.SyntaxKind.AmpersandAmpersandToken || k === ts.SyntaxKind.BarBarToken)
|
|
1264
|
+
return this.inConditionContext(p);
|
|
1265
|
+
if (k === ts.SyntaxKind.CommaToken)
|
|
1266
|
+
return p.left === e || this.useSafe(p);
|
|
1267
|
+
if (p.left === e) {
|
|
1268
|
+
// Compound assignment reads of a binding: arithmetic.
|
|
1269
|
+
return NUMERIC_COMPOUND.has(k) || k === ts.SyntaxKind.PlusEqualsToken;
|
|
1270
|
+
}
|
|
1271
|
+
if (k === ts.SyntaxKind.EqualsToken) {
|
|
1272
|
+
const slot = this.sinkOf(e);
|
|
1273
|
+
if (!slot || !this.eligible.has(slot))
|
|
1274
|
+
return false;
|
|
1275
|
+
// The assignment's own value flows on when it is used.
|
|
1276
|
+
const outer = transparentParent(p);
|
|
1277
|
+
const consumer = outer.parent;
|
|
1278
|
+
if (!consumer ||
|
|
1279
|
+
ts.isExpressionStatement(consumer) ||
|
|
1280
|
+
(ts.isForStatement(consumer) &&
|
|
1281
|
+
(consumer.incrementor === outer || consumer.initializer === outer)))
|
|
1282
|
+
return true;
|
|
1283
|
+
return this.useSafe(p);
|
|
1284
|
+
}
|
|
1285
|
+
return (NUMERIC_COMPOUND.has(k) ||
|
|
1286
|
+
(k === ts.SyntaxKind.PlusEqualsToken && this.host.isNumber(p.left)));
|
|
1287
|
+
}
|
|
1288
|
+
if (ts.isPostfixUnaryExpression(p))
|
|
1289
|
+
return true;
|
|
1290
|
+
if (ts.isVariableDeclaration(p) || ts.isReturnStatement(p)) {
|
|
1291
|
+
const slot = this.sinkOf(e);
|
|
1292
|
+
return slot !== null && this.eligible.has(slot);
|
|
1293
|
+
}
|
|
1294
|
+
if (ts.isCallExpression(p) && p.expression !== e) {
|
|
1295
|
+
const callee = p.expression;
|
|
1296
|
+
if (ts.isPropertyAccessExpression(callee) &&
|
|
1297
|
+
!callee.questionDotToken &&
|
|
1298
|
+
!p.questionDotToken &&
|
|
1299
|
+
(this.host.isStdlibGlobal(callee.expression, "Math") ||
|
|
1300
|
+
(callee.name.text === "fromCharCode" &&
|
|
1301
|
+
this.host.isStdlibGlobal(callee.expression, "String"))))
|
|
1302
|
+
return true;
|
|
1303
|
+
const slot = this.sinkOf(e);
|
|
1304
|
+
return slot !== null && this.eligible.has(slot);
|
|
1305
|
+
}
|
|
1306
|
+
// A typed-array element read answers undefined for both an undefined
|
|
1307
|
+
// and a NaN index (neither names an integer index).
|
|
1308
|
+
if (ts.isElementAccessExpression(p) &&
|
|
1309
|
+
p.argumentExpression === e &&
|
|
1310
|
+
this.isReadCandidate(p) &&
|
|
1311
|
+
this.host.bytesElemOf(p.expression) !== null)
|
|
1312
|
+
return true;
|
|
1313
|
+
if (ts.isSwitchStatement(p)) {
|
|
1314
|
+
return p.caseBlock.clauses.every((clause) => !ts.isCaseClause(clause) || this.plainNumber(clause.expression, 0) !== null);
|
|
1315
|
+
}
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
/** The value of a logical expression is only tested for truthiness. */
|
|
1319
|
+
inConditionContext(node) {
|
|
1320
|
+
const e = transparentParent(node);
|
|
1321
|
+
const p = e.parent;
|
|
1322
|
+
if (!p)
|
|
1323
|
+
return false;
|
|
1324
|
+
if (ts.isIfStatement(p) || ts.isWhileStatement(p) || ts.isDoStatement(p))
|
|
1325
|
+
return p.expression === e;
|
|
1326
|
+
if (ts.isForStatement(p))
|
|
1327
|
+
return p.condition === e;
|
|
1328
|
+
if (ts.isConditionalExpression(p))
|
|
1329
|
+
return p.condition === e;
|
|
1330
|
+
if (ts.isPrefixUnaryExpression(p))
|
|
1331
|
+
return p.operator === ts.SyntaxKind.ExclamationToken;
|
|
1332
|
+
if (ts.isExpressionStatement(p))
|
|
1333
|
+
return true;
|
|
1334
|
+
if (ts.isBinaryExpression(p)) {
|
|
1335
|
+
const k = p.operatorToken.kind;
|
|
1336
|
+
if (k === ts.SyntaxKind.AmpersandAmpersandToken || k === ts.SyntaxKind.BarBarToken)
|
|
1337
|
+
return this.inConditionContext(p);
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
//# sourceMappingURL=nan-coded-reads.js.map
|