@chenglou/freerange 0.0.1
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import type {ValueID} from '../ir/ids.ts'
|
|
3
|
+
import type {ComparisonOperator, InstructionIR} from '../ir/instructions.ts'
|
|
4
|
+
import type {StaticAssertionProblem} from '../ir/program.ts'
|
|
5
|
+
import {declaredOnlyInDeclarationFiles, platformFact} from './platform.ts'
|
|
6
|
+
import {
|
|
7
|
+
addInstruction,
|
|
8
|
+
addInstructionAtSite,
|
|
9
|
+
addSite,
|
|
10
|
+
createBlock,
|
|
11
|
+
requiredSymbol,
|
|
12
|
+
terminate,
|
|
13
|
+
unsupported,
|
|
14
|
+
type FunctionContext,
|
|
15
|
+
} from './context.ts'
|
|
16
|
+
import type {StaticAnnotation} from './static-intrinsics.ts'
|
|
17
|
+
import {isUndefinedGlobal, numericLiteralValue, parameterDefaultLiteral, type ParameterDefaultLiteral} from './literals.ts'
|
|
18
|
+
|
|
19
|
+
// The only entry point through which assignments lower. Statement positions (expression
|
|
20
|
+
// statements, for-loop incrementors) call this; everything else goes through
|
|
21
|
+
// lowerExpression, which rejects assignment forms — so an assignment used as a value
|
|
22
|
+
// inside a larger expression cannot lower by construction, and ternary/logical arms are
|
|
23
|
+
// provably assignment-free (their join carries exactly one parameter, the result).
|
|
24
|
+
export function lowerStatementExpression(expression: ts.Expression, context: FunctionContext): void {
|
|
25
|
+
const current = unwrap(expression, context.checker)
|
|
26
|
+
const assignment = identifierAssignment(current)
|
|
27
|
+
if (assignment != null) {
|
|
28
|
+
const symbol = requiredSymbol(assignment.target, context.checker)
|
|
29
|
+
switch (assignment.form) {
|
|
30
|
+
case 'assign': {
|
|
31
|
+
const moduleBinding = context.moduleBindingsBySymbol.get(symbol)
|
|
32
|
+
if (!context.bindings.has(symbol) && moduleBinding == null) {
|
|
33
|
+
throw unsupported(assignment.target, {kind: 'unknownIdentifier', name: assignment.target.text})
|
|
34
|
+
}
|
|
35
|
+
// Rebinding is only sound when the target's declared type holds a single value
|
|
36
|
+
// kind — otherwise branches could bind different kinds that meet at a block join.
|
|
37
|
+
// Function locals with mixed-kind declared types already stop at their declaration;
|
|
38
|
+
// a module binding can still hold one (a top-level `let config: unknown`
|
|
39
|
+
// initializes through the initializer's own declarator path), so for those the
|
|
40
|
+
// write itself stops here. The checker returns the declared type at an assignment
|
|
41
|
+
// target, not a narrowed one: narrowing does not apply to write positions.
|
|
42
|
+
const targetType = context.checker.getTypeAtLocation(assignment.target)
|
|
43
|
+
const targetKind = valueKind(targetType, context.checker)
|
|
44
|
+
if (targetKind == null) {
|
|
45
|
+
throw unsupported(assignment.target, {kind: 'valueType', typeText: context.checker.typeToString(targetType)})
|
|
46
|
+
}
|
|
47
|
+
const value = lowerExpression(assignment.node.right, context)
|
|
48
|
+
// A binding declared opaque (unknown, a function type) admits writes of any kind;
|
|
49
|
+
// the stored value erases to opaque so a number written on one branch and a
|
|
50
|
+
// boolean on another meet as opaque ⊔ opaque instead of crashing the join. The
|
|
51
|
+
// right side still lowered above, so its constructs stay vetted.
|
|
52
|
+
const stored = targetKind === 'opaque'
|
|
53
|
+
? addInstruction(context, current, {kind: 'opaqueConstant'})
|
|
54
|
+
: value
|
|
55
|
+
assignIdentifier(symbol, assignment.target, stored, current, context)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
case 'logical': {
|
|
59
|
+
// `x ??= v` / `x ||= v` / `x &&= v` in statement position: the target rebinds to
|
|
60
|
+
// the same value branch the expression spellings lower to — ?? through the
|
|
61
|
+
// missing-value machinery for any carried kind, || and && over booleans.
|
|
62
|
+
const currentValue = identifierValue(symbol, assignment.target, context)
|
|
63
|
+
const targetType = context.checker.getTypeAtLocation(assignment.target)
|
|
64
|
+
let condition: ValueID
|
|
65
|
+
if (assignment.logical === 'nullish') {
|
|
66
|
+
condition = addInstruction(context, current, {kind: 'nullishCheck', value: currentValue, sentinel: 'nullish', negated: true})
|
|
67
|
+
} else {
|
|
68
|
+
const targetKind = valueKind(targetType, context.checker)
|
|
69
|
+
if (targetKind !== 'boolean') {
|
|
70
|
+
throw unsupported(assignment.target, {
|
|
71
|
+
kind: 'nonBooleanCondition',
|
|
72
|
+
conditionKind: targetKind === 'number' ? 'number' : 'other',
|
|
73
|
+
typeText: context.checker.typeToString(targetType),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
condition = currentValue
|
|
77
|
+
}
|
|
78
|
+
// For ??= and ||= the kept arm is the current value; for &&= the kept arm is the
|
|
79
|
+
// false side. lowerValueBranch orders (whenTrue, whenFalse).
|
|
80
|
+
const keepOnTrue = assignment.logical !== 'and'
|
|
81
|
+
const rebound = lowerValueBranch(
|
|
82
|
+
current,
|
|
83
|
+
condition,
|
|
84
|
+
keepOnTrue ? () => currentValue : () => lowerExpression(assignment.node.right, context),
|
|
85
|
+
keepOnTrue ? () => lowerExpression(assignment.node.right, context) : () => currentValue,
|
|
86
|
+
context,
|
|
87
|
+
)
|
|
88
|
+
assignIdentifier(symbol, assignment.target, rebound, current, context)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
case 'compound': {
|
|
92
|
+
// `message += suffix` is string concatenation when the checker types the result
|
|
93
|
+
// as a string — the target rebinds to an opaque value, like `width + 'px'` in
|
|
94
|
+
// value position. Any other non-number operand rejects here exactly as the
|
|
95
|
+
// value-position binary arm does, instead of slipping an untyped add through to
|
|
96
|
+
// the engine's kind-mismatch backstop.
|
|
97
|
+
if (assignment.operator === 'add'
|
|
98
|
+
&& (context.checker.getTypeAtLocation(current).flags & ts.TypeFlags.StringLike) !== 0) {
|
|
99
|
+
lowerExpression(assignment.node.right, context)
|
|
100
|
+
const concatenated = addInstruction(context, current, {kind: 'opaqueConstant'})
|
|
101
|
+
assignIdentifier(symbol, assignment.target, concatenated, current, context)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
requireNumberType(assignment.target, context.checker)
|
|
105
|
+
requireNumberType(assignment.node.right, context.checker)
|
|
106
|
+
const left = identifierValue(symbol, assignment.target, context)
|
|
107
|
+
const right = lowerExpression(assignment.node.right, context)
|
|
108
|
+
const value = addInstruction(context, current, {kind: 'binary', operator: assignment.operator, left, right})
|
|
109
|
+
assignIdentifier(symbol, assignment.target, value, current, context)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
case 'update': {
|
|
113
|
+
// In statement position the expression's own value is discarded, so the prefix
|
|
114
|
+
// versus postfix result distinction does not exist here.
|
|
115
|
+
const previous = identifierValue(symbol, assignment.target, context)
|
|
116
|
+
const one = addInstruction(context, current, {kind: 'constant', value: 1})
|
|
117
|
+
const value = addInstruction(context, current, {
|
|
118
|
+
kind: 'binary',
|
|
119
|
+
operator: assignment.node.operator === ts.SyntaxKind.PlusPlusToken ? 'add' : 'subtract',
|
|
120
|
+
left: previous,
|
|
121
|
+
right: one,
|
|
122
|
+
})
|
|
123
|
+
assignIdentifier(symbol, assignment.target, value, current, context)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lowerExpression(expression, context)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function lowerExpression(expression: ts.Expression, context: FunctionContext): ValueID {
|
|
132
|
+
const current = unwrap(expression, context.checker)
|
|
133
|
+
if (ts.isNumericLiteral(current)) {
|
|
134
|
+
return addInstruction(context, current, {kind: 'constant', value: Number(current.text)})
|
|
135
|
+
}
|
|
136
|
+
if (current.kind === ts.SyntaxKind.TrueKeyword || current.kind === ts.SyntaxKind.FalseKeyword) {
|
|
137
|
+
return addInstruction(context, current, {kind: 'booleanConstant', value: current.kind === ts.SyntaxKind.TrueKeyword})
|
|
138
|
+
}
|
|
139
|
+
if (ts.isPrefixUnaryExpression(current) && current.operator === ts.SyntaxKind.PlusToken) {
|
|
140
|
+
const positive = unwrap(current.operand, context.checker)
|
|
141
|
+
if (ts.isNumericLiteral(positive)) {
|
|
142
|
+
return addInstruction(context, current, {kind: 'constant', value: Number(positive.text)})
|
|
143
|
+
}
|
|
144
|
+
throw unsupported(current, {kind: 'expressionForm', syntax: ts.SyntaxKind[current.kind]})
|
|
145
|
+
}
|
|
146
|
+
if (ts.isPrefixUnaryExpression(current) && current.operator === ts.SyntaxKind.MinusToken) {
|
|
147
|
+
// A negated literal folds into one constant instead of lowering as `0 - operand`.
|
|
148
|
+
// For finite literals both are exact; for `-Infinity` the fold is the difference
|
|
149
|
+
// between an exact constant and a collapse to unknown, because interval arithmetic
|
|
150
|
+
// deliberately gives up on non-finite operands (Infinity - Infinity is NaN).
|
|
151
|
+
const negated = unwrap(current.operand, context.checker)
|
|
152
|
+
if (ts.isNumericLiteral(negated)) {
|
|
153
|
+
return addInstruction(context, current, {kind: 'constant', value: -Number(negated.text)})
|
|
154
|
+
}
|
|
155
|
+
if (isGlobalInfinity(negated, context.checker)) {
|
|
156
|
+
return addInstruction(context, current, {kind: 'constant', value: Number.NEGATIVE_INFINITY})
|
|
157
|
+
}
|
|
158
|
+
const zero = addInstruction(context, current, {kind: 'constant', value: 0})
|
|
159
|
+
const value = lowerExpression(current.operand, context)
|
|
160
|
+
return addInstruction(context, current, {kind: 'binary', operator: 'subtract', left: zero, right: value})
|
|
161
|
+
}
|
|
162
|
+
if (ts.isPrefixUnaryExpression(current) && current.operator === ts.SyntaxKind.ExclamationToken) {
|
|
163
|
+
requireBooleanCondition(current.operand, context.checker)
|
|
164
|
+
const value = lowerExpression(current.operand, context)
|
|
165
|
+
return addInstruction(context, current, {kind: 'not', value})
|
|
166
|
+
}
|
|
167
|
+
if (ts.isConditionalExpression(current)) {
|
|
168
|
+
return lowerConditionalExpression(current, context)
|
|
169
|
+
}
|
|
170
|
+
if (ts.isIdentifier(current)) {
|
|
171
|
+
return identifierValue(requiredSymbol(current, context.checker), current, context)
|
|
172
|
+
}
|
|
173
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
174
|
+
const literalType = context.checker.getTypeAtLocation(current)
|
|
175
|
+
const literalKind = valueKind(literalType, context.checker)
|
|
176
|
+
// A literal whose own type does not classify — `[1, true]` types as
|
|
177
|
+
// (number | boolean)[], whose element hull no read gate could ever describe — rejects
|
|
178
|
+
// here, covering every position a literal can appear in (declarators have their own
|
|
179
|
+
// gate, but object property values and call arguments do not).
|
|
180
|
+
// The empty literal is exempt: its never[] element type classifies as nothing, but
|
|
181
|
+
// there are no elements to mix.
|
|
182
|
+
if (literalKind !== 'array' && literalKind !== 'tuple' && current.elements.length > 0) {
|
|
183
|
+
throw unsupported(current, {kind: 'valueType', typeText: context.checker.typeToString(literalType)})
|
|
184
|
+
}
|
|
185
|
+
const elements: ValueID[] = []
|
|
186
|
+
for (const element of current.elements) {
|
|
187
|
+
if (ts.isSpreadElement(element) || ts.isOmittedExpression(element)) {
|
|
188
|
+
throw unsupported(element, {kind: 'expressionForm', syntax: ts.SyntaxKind[element.kind]})
|
|
189
|
+
}
|
|
190
|
+
elements.push(lowerExpression(element, context))
|
|
191
|
+
}
|
|
192
|
+
// The literal's static type decides the form: `[4, 8, 24] as const` is a tuple and
|
|
193
|
+
// stays exact per position; a plain literal is an array and joins its elements.
|
|
194
|
+
return addInstruction(context, current, {kind: 'arrayLiteral', elements, form: literalKind === 'tuple' ? 'tuple' : 'array'})
|
|
195
|
+
}
|
|
196
|
+
if (ts.isNonNullExpression(current) && ts.isElementAccessExpression(current.expression)) {
|
|
197
|
+
// `arr[i]!` — asserts presence; an unproven read becomes an in-bounds assumption line.
|
|
198
|
+
return lowerElementAccess(current.expression, true, context)
|
|
199
|
+
}
|
|
200
|
+
if (ts.isElementAccessExpression(current)) {
|
|
201
|
+
// Bare arr[i] types T | undefined; the result honestly carries the possible miss.
|
|
202
|
+
return lowerElementAccess(current, false, context)
|
|
203
|
+
}
|
|
204
|
+
if (ts.isObjectLiteralExpression(current)) {
|
|
205
|
+
// Map insertion order keeps the first position while set keeps the last value, which
|
|
206
|
+
// matches object-literal evaluation and overwrite order.
|
|
207
|
+
const properties = new Map<string, {name: string; value: ValueID}>()
|
|
208
|
+
for (const property of current.properties) {
|
|
209
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
210
|
+
const symbol = context.checker.getShorthandAssignmentValueSymbol(property)
|
|
211
|
+
if (symbol == null) throw unsupported(property, {kind: 'missingSymbol'})
|
|
212
|
+
properties.set(property.name.text, {
|
|
213
|
+
name: property.name.text,
|
|
214
|
+
value: identifierValue(symbol, property.name, context),
|
|
215
|
+
})
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
if (ts.isPropertyAssignment(property)) {
|
|
219
|
+
const name = propertyName(property.name)
|
|
220
|
+
// `__proto__: value` in a literal is prototype-setting syntax at runtime — no own
|
|
221
|
+
// property is created — while the checker types it as a plain property.
|
|
222
|
+
if (name === '__proto__') throw unsupported(property, {kind: 'protoProperty'})
|
|
223
|
+
properties.set(name, {name, value: lowerExpression(property.initializer, context)})
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
if (ts.isSpreadAssignment(property)) throw unsupported(property, {kind: 'objectSpread'})
|
|
227
|
+
throw unsupported(property, {kind: 'objectPropertyForm'})
|
|
228
|
+
}
|
|
229
|
+
// A literal written where a tagged union is expected ({type: 'sidebar', width: 240}
|
|
230
|
+
// returned as Frame) records which variant it builds, so branches building different
|
|
231
|
+
// variants join per tag instead of dropping every mismatched property. The tag VALUE
|
|
232
|
+
// comes from the literal's own checked type, not its syntax, so the rebuild idiom
|
|
233
|
+
// {type: frame.type, width: frame.width + 40} — where the tag arrives via a property
|
|
234
|
+
// read of the narrowed union — is recognized too.
|
|
235
|
+
const contextual = context.checker.getContextualType(current)
|
|
236
|
+
// Omitted optionals become explicit undefined values, keeping the invariant that a
|
|
237
|
+
// record value carries every property its static type declares — a join between a
|
|
238
|
+
// branch that set the property and one that omitted it must not drop it, and reads
|
|
239
|
+
// must find the honest maybe-missing value rather than crash. (A literal with no
|
|
240
|
+
// contextual record type has no optionals to fill.)
|
|
241
|
+
const fillOptionalsFrom = (recordType: ts.Type): void => {
|
|
242
|
+
for (const member of context.checker.getPropertiesOfType(recordType)) {
|
|
243
|
+
if ((member.flags & ts.SymbolFlags.Optional) === 0 || properties.has(member.name)) continue
|
|
244
|
+
const absent = addInstruction(context, current, {kind: 'nullishConstant', sentinel: 'undefined'})
|
|
245
|
+
properties.set(member.name, {name: member.name, value: absent})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// The contextual type may sit behind a nullable wrapper (`const config: Config |
|
|
249
|
+
// null = flag ? {...} : null`): the literal builds the non-missing part, so the
|
|
250
|
+
// filling and tag detection look through the wrapper at those members.
|
|
251
|
+
const contextMembers: readonly ts.Type[] = contextual == null
|
|
252
|
+
? []
|
|
253
|
+
: contextual.isUnion()
|
|
254
|
+
? nonMissingUnionMembers(contextual)
|
|
255
|
+
: [contextual]
|
|
256
|
+
if (contextMembers.length === 1 && valueKind(contextMembers[0]!, context.checker) === 'object') {
|
|
257
|
+
fillOptionalsFrom(contextMembers[0]!)
|
|
258
|
+
}
|
|
259
|
+
let tag: {property: string} | null = null
|
|
260
|
+
if (contextMembers.length > 1) {
|
|
261
|
+
const tagProperty = taggedUnionProperty(contextMembers, context.checker)
|
|
262
|
+
if (tagProperty != null) {
|
|
263
|
+
// WHICH property is the tag comes from the type (a property name carries no
|
|
264
|
+
// claim); WHICH VARIANT the literal builds is decided by the engine from the tag
|
|
265
|
+
// property's runtime-tracked value, never from the checker's type of the tag —
|
|
266
|
+
// the type channel is assertion-taintable at any distance (a review round chained
|
|
267
|
+
// `{kind: raw as 'lightbox'}`, its quoted-key spelling, and a spread of a
|
|
268
|
+
// cast-tagged template), while value-carried content only ever originates from
|
|
269
|
+
// written literals and declared-variant seeding.
|
|
270
|
+
tag = {property: tagProperty}
|
|
271
|
+
// Optional filling still needs the tag value at lowering; a literal WRITTEN in
|
|
272
|
+
// the tag position provides it (the same trust rule tag-check comparisons and
|
|
273
|
+
// switch labels follow). The variant's own optionals fill from every contextual
|
|
274
|
+
// member whose tag values include the written one, so a duplicate-tag literal
|
|
275
|
+
// covers both shapes' optionals. An optional property reads as possibly undefined
|
|
276
|
+
// either way.
|
|
277
|
+
const ownLiteral = writtenTagLiteral(current, tagProperty, context)
|
|
278
|
+
if (ownLiteral != null) {
|
|
279
|
+
for (const member of contextMembers) {
|
|
280
|
+
const memberTag = context.checker.getPropertyOfType(member, tagProperty)
|
|
281
|
+
const memberTagType = memberTag == null ? null : context.checker.getTypeOfSymbol(memberTag)
|
|
282
|
+
const memberLiterals = memberTagType == null ? null : tagLiteralValues(memberTagType)
|
|
283
|
+
if (memberLiterals != null && memberLiterals.includes(ownLiteral)) {
|
|
284
|
+
fillOptionalsFrom(member)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return addInstruction(context, current, {kind: 'object', properties: [...properties.values()], ...(tag == null ? {} : {tag})})
|
|
291
|
+
}
|
|
292
|
+
if (identifierAssignment(current) != null) {
|
|
293
|
+
throw unsupported(current, {kind: 'assignmentInValuePosition'})
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
ts.isBinaryExpression(current)
|
|
297
|
+
&& (current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
298
|
+
|| current.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
299
|
+
) {
|
|
300
|
+
return lowerLogicalExpression(current, context)
|
|
301
|
+
}
|
|
302
|
+
if (current.kind === ts.SyntaxKind.NullKeyword) {
|
|
303
|
+
return addInstruction(context, current, {kind: 'nullishConstant', sentinel: 'null'})
|
|
304
|
+
}
|
|
305
|
+
if (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) {
|
|
306
|
+
// The exact text rides along: its one consumer is the tagged-union variant pin, which
|
|
307
|
+
// trusts only value-carried content (see the engine's object arm).
|
|
308
|
+
return addInstruction(context, current, {kind: 'opaqueConstant', content: current.text})
|
|
309
|
+
}
|
|
310
|
+
// Any assertion except `as const` (which unwrap peels). The operand still lowers (an
|
|
311
|
+
// unsupported construct inside it rejects as usual), but its claims are erased:
|
|
312
|
+
// downstream sees a claim-free value whose uses stop at the gates and whose joins
|
|
313
|
+
// absorb, which is what keeps `true as unknown as number` — and every comparability
|
|
314
|
+
// spelling of the same launder — from carrying a boolean into number positions.
|
|
315
|
+
if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) {
|
|
316
|
+
lowerExpression(current.expression, context)
|
|
317
|
+
return addInstruction(context, current, {kind: 'opaqueConstant'})
|
|
318
|
+
}
|
|
319
|
+
if (ts.isTemplateExpression(current)) {
|
|
320
|
+
// `${width}px` — the interpolated expressions lower (they must be representable), the
|
|
321
|
+
// result is carried without claims.
|
|
322
|
+
for (const span of current.templateSpans) lowerExpression(span.expression, context)
|
|
323
|
+
return addInstruction(context, current, {kind: 'opaqueConstant'})
|
|
324
|
+
}
|
|
325
|
+
if (ts.isBinaryExpression(current) && current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
326
|
+
// `a ?? b` is `a` when not missing, else `b`. The whole expression's type must be a
|
|
327
|
+
// representable kind — `(record | null) ?? 0` mixes record and number arms.
|
|
328
|
+
const resultType = context.checker.getTypeAtLocation(current)
|
|
329
|
+
if (valueKind(resultType, context.checker) == null) {
|
|
330
|
+
throw unsupported(current, {kind: 'valueType', typeText: context.checker.typeToString(resultType)})
|
|
331
|
+
}
|
|
332
|
+
const left = lowerExpression(current.left, context)
|
|
333
|
+
const notMissing = addInstruction(context, current, {kind: 'nullishCheck', value: left, sentinel: 'nullish', negated: true})
|
|
334
|
+
// The true arm uses the left value refined by the nullish check.
|
|
335
|
+
return lowerValueBranch(
|
|
336
|
+
current,
|
|
337
|
+
notMissing,
|
|
338
|
+
() => left,
|
|
339
|
+
() => lowerExpression(current.right, context),
|
|
340
|
+
context,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
if (ts.isBinaryExpression(current)) {
|
|
344
|
+
const missingCheck = missingSentinelCheck(current, context)
|
|
345
|
+
if (missingCheck != null) return missingCheck
|
|
346
|
+
// `el instanceof HTMLDivElement` on a carried value: no narrowing (the analyzer does
|
|
347
|
+
// not model classes), but the check itself is an effect-free operator, so it answers
|
|
348
|
+
// unknown and both branches analyze — the function's other paths survive. The
|
|
349
|
+
// declaration-file check is the same shadowing defense Math uses.
|
|
350
|
+
if (current.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword
|
|
351
|
+
&& ts.isIdentifier(current.right)
|
|
352
|
+
&& declaredOnlyInDeclarationFiles(context.checker.getSymbolAtLocation(current.right))) {
|
|
353
|
+
lowerExpression(current.left, context)
|
|
354
|
+
return addInstruction(context, current, {kind: 'unknownBoolean'})
|
|
355
|
+
}
|
|
356
|
+
const tagComparison = tagCheckComparison(current, context)
|
|
357
|
+
if (tagComparison != null) return tagComparison
|
|
358
|
+
const opaqueComparison = opaqueEqualityCheck(current, context)
|
|
359
|
+
if (opaqueComparison != null) return opaqueComparison
|
|
360
|
+
// `width + 'px'`: string building with + is everywhere in UI code, and the template
|
|
361
|
+
// spelling `${width}px` is already carried — when the checker types the result as a
|
|
362
|
+
// string, the result is an opaque value. Both operands still lower, so an unsupported
|
|
363
|
+
// construct inside one rejects as usual.
|
|
364
|
+
if (current.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
365
|
+
&& (context.checker.getTypeAtLocation(current).flags & ts.TypeFlags.StringLike) !== 0) {
|
|
366
|
+
lowerExpression(current.left, context)
|
|
367
|
+
lowerExpression(current.right, context)
|
|
368
|
+
return addInstruction(context, current, {kind: 'opaqueConstant'})
|
|
369
|
+
}
|
|
370
|
+
const arithmetic = arithmeticOperator(current.operatorToken.kind)
|
|
371
|
+
const comparison = comparisonOperator(current.operatorToken.kind)
|
|
372
|
+
if (arithmetic == null && comparison == null) {
|
|
373
|
+
throw unsupported(current, {kind: 'binaryOperator', operator: current.operatorToken.getText(context.sourceFile)})
|
|
374
|
+
}
|
|
375
|
+
// flag === true and flag !== other: booleans are modeled exactly, so equality over
|
|
376
|
+
// them answers exactly too (the engine's compare arm dispatches on the operand kind).
|
|
377
|
+
if ((comparison === 'equal' || comparison === 'notEqual')
|
|
378
|
+
&& valueKind(context.checker.getTypeAtLocation(current.left), context.checker) === 'boolean'
|
|
379
|
+
&& valueKind(context.checker.getTypeAtLocation(current.right), context.checker) === 'boolean') {
|
|
380
|
+
const left = lowerExpression(current.left, context)
|
|
381
|
+
const right = lowerExpression(current.right, context)
|
|
382
|
+
return addInstruction(context, current, {kind: 'compare', operator: comparison, left, right})
|
|
383
|
+
}
|
|
384
|
+
requireNumberType(current.left, context.checker)
|
|
385
|
+
requireNumberType(current.right, context.checker)
|
|
386
|
+
const left = lowerExpression(current.left, context)
|
|
387
|
+
const right = lowerExpression(current.right, context)
|
|
388
|
+
return arithmetic != null
|
|
389
|
+
? addInstruction(context, current, {kind: 'binary', operator: arithmetic, left, right})
|
|
390
|
+
: addInstruction(context, current, {kind: 'compare', operator: comparison!, left, right})
|
|
391
|
+
}
|
|
392
|
+
if (ts.isCallExpression(current)) {
|
|
393
|
+
const staticAnnotation = context.staticAnnotations.get(current)
|
|
394
|
+
if (staticAnnotation != null) return lowerStaticAnnotation(staticAnnotation, context)
|
|
395
|
+
if (ts.isIdentifier(current.expression)) {
|
|
396
|
+
// Global parseFloat / parseInt / Number(x): honest NaN-carrying results, like their
|
|
397
|
+
// Number.* spellings below (the declaration-file check defends against shadowing).
|
|
398
|
+
const globalName = current.expression.text
|
|
399
|
+
if ((globalName === 'parseFloat' || globalName === 'parseInt' || globalName === 'Number')
|
|
400
|
+
&& declaredOnlyInDeclarationFiles(context.checker.getSymbolAtLocation(current.expression))) {
|
|
401
|
+
for (const argument of current.arguments) lowerExpression(argument, context)
|
|
402
|
+
return addInstruction(context, current, {kind: 'parsedNumber', integer: globalName === 'parseInt'})
|
|
403
|
+
}
|
|
404
|
+
const symbol = resolvedSymbol(context.checker.getSymbolAtLocation(current.expression), context.checker)
|
|
405
|
+
const callee = symbol == null ? undefined : context.functionsBySymbol.get(symbol)
|
|
406
|
+
if (callee == null) throw unsupported(current, {kind: 'call', callee: current.expression.text})
|
|
407
|
+
if (current.arguments.length > callee.declaration.parameters.length) {
|
|
408
|
+
throw unsupported(current, {kind: 'callWithMoreArguments', callee: current.expression.text})
|
|
409
|
+
}
|
|
410
|
+
const arguments_: ValueID[] = []
|
|
411
|
+
for (let index = 0; index < callee.declaration.parameters.length; index++) {
|
|
412
|
+
const parameter = callee.declaration.parameters[index]!
|
|
413
|
+
const argument = current.arguments[index]
|
|
414
|
+
if (argument == null) {
|
|
415
|
+
if (parameter.initializer != null) {
|
|
416
|
+
const default_ = parameterDefaultLiteral(parameter.initializer, context.checker)
|
|
417
|
+
if (default_ == null) {
|
|
418
|
+
throw unsupported(current, {kind: 'callWithFewerArguments', callee: current.expression.text})
|
|
419
|
+
}
|
|
420
|
+
arguments_.push(lowerParameterDefault(default_, parameter.initializer, context))
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (parameter.questionToken != null) {
|
|
424
|
+
arguments_.push(addInstruction(context, parameter, {kind: 'nullishConstant', sentinel: 'undefined'}))
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
throw unsupported(current, {kind: 'callWithFewerArguments', callee: current.expression.text})
|
|
428
|
+
}
|
|
429
|
+
const value = lowerExpression(argument, context)
|
|
430
|
+
const default_ = parameter.initializer == null
|
|
431
|
+
? null
|
|
432
|
+
: parameterDefaultLiteral(parameter.initializer, context.checker)
|
|
433
|
+
if (default_ == null
|
|
434
|
+
|| !typeCanIncludeUndefined(context.checker.getTypeAtLocation(argument))) {
|
|
435
|
+
arguments_.push(value)
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
// JavaScript applies a parameter default when the supplied value is undefined,
|
|
439
|
+
// not only when the argument is omitted. The normal value-producing branch keeps
|
|
440
|
+
// that rule exact for `number | undefined` arguments without a new IR operation.
|
|
441
|
+
const supplied = addInstruction(context, argument, {
|
|
442
|
+
kind: 'nullishCheck',
|
|
443
|
+
value,
|
|
444
|
+
sentinel: 'undefined',
|
|
445
|
+
negated: true,
|
|
446
|
+
})
|
|
447
|
+
arguments_.push(lowerValueBranch(
|
|
448
|
+
argument,
|
|
449
|
+
supplied,
|
|
450
|
+
() => value,
|
|
451
|
+
() => lowerParameterDefault(default_, parameter.initializer!, context),
|
|
452
|
+
context,
|
|
453
|
+
))
|
|
454
|
+
}
|
|
455
|
+
return addInstruction(context, current, {kind: 'call', function: callee.id, arguments: arguments_})
|
|
456
|
+
}
|
|
457
|
+
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
458
|
+
const platformCall = current.arguments.length === 0 ? platformFact(current.expression, true, context.checker) : null
|
|
459
|
+
if (platformCall != null) {
|
|
460
|
+
return addInstruction(context, current, {kind: 'platformValue', ...platformCall})
|
|
461
|
+
}
|
|
462
|
+
const method = current.expression.name.text
|
|
463
|
+
const standardMath = isStandardMathObject(current.expression.expression, context.checker)
|
|
464
|
+
if (standardMath && method === 'floor' && current.arguments.length === 1) {
|
|
465
|
+
requireNumberType(current.arguments[0]!, context.checker)
|
|
466
|
+
const value = lowerExpression(current.arguments[0]!, context)
|
|
467
|
+
return addInstruction(context, current, {kind: 'floor', value})
|
|
468
|
+
}
|
|
469
|
+
if (standardMath && method === 'abs' && current.arguments.length === 1) {
|
|
470
|
+
requireNumberType(current.arguments[0]!, context.checker)
|
|
471
|
+
const value = lowerExpression(current.arguments[0]!, context)
|
|
472
|
+
return addInstruction(context, current, {kind: 'absolute', value})
|
|
473
|
+
}
|
|
474
|
+
if (standardMath && (method === 'ceil' || method === 'round' || method === 'trunc' || method === 'sqrt')
|
|
475
|
+
&& current.arguments.length === 1) {
|
|
476
|
+
requireNumberType(current.arguments[0]!, context.checker)
|
|
477
|
+
const value = lowerExpression(current.arguments[0]!, context)
|
|
478
|
+
return addInstruction(context, current, {kind: 'mathUnary', operator: method, value})
|
|
479
|
+
}
|
|
480
|
+
if (standardMath && (method === 'min' || method === 'max') && current.arguments.length > 0) {
|
|
481
|
+
for (const argument of current.arguments) requireNumberType(argument, context.checker)
|
|
482
|
+
const values = current.arguments.map(argument => lowerExpression(argument, context))
|
|
483
|
+
return addInstruction(context, current, {kind: method === 'min' ? 'minimum' : 'maximum', values})
|
|
484
|
+
}
|
|
485
|
+
// Number.isInteger / Number.isFinite: predicate checks whose branches narrow — the
|
|
486
|
+
// missing halves of the bounds-check idiom (`i >= 0 && i < arr.length` proves the
|
|
487
|
+
// range; Number.isInteger(i) proves the read hits an element rather than arr[1.5]).
|
|
488
|
+
const standardNumber = isStandardNumberObject(current.expression.expression, context.checker)
|
|
489
|
+
// Number.parseFloat / Number.parseInt: honest NaN sources — the result is any
|
|
490
|
+
// number including NaN, and the isFinite/isNaN/isInteger narrowing downstream is
|
|
491
|
+
// exactly what launders it. Arguments still lower (opaque strings carry).
|
|
492
|
+
if (standardNumber && (method === 'parseFloat' || method === 'parseInt') && current.arguments.length >= 1) {
|
|
493
|
+
for (const argument of current.arguments) lowerExpression(argument, context)
|
|
494
|
+
return addInstruction(context, current, {kind: 'parsedNumber', integer: method === 'parseInt'})
|
|
495
|
+
}
|
|
496
|
+
if (standardNumber && (method === 'isInteger' || method === 'isFinite' || method === 'isNaN') && current.arguments.length === 1) {
|
|
497
|
+
requireNumberType(current.arguments[0]!, context.checker)
|
|
498
|
+
const value = lowerExpression(current.arguments[0]!, context)
|
|
499
|
+
return addInstruction(context, current, {
|
|
500
|
+
kind: 'numberCheck',
|
|
501
|
+
predicate: method === 'isInteger' ? 'integer' : method === 'isFinite' ? 'finite' : 'nan',
|
|
502
|
+
value,
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
const arrayMethod = ts.isPropertyAccessExpression(current.expression)
|
|
506
|
+
&& context.checker.isArrayType(context.checker.getTypeAtLocation(current.expression.expression))
|
|
507
|
+
? current.expression.name.text === 'reduce' ? 'reduce' : 'other'
|
|
508
|
+
: null
|
|
509
|
+
throw unsupported(current, {
|
|
510
|
+
kind: 'call',
|
|
511
|
+
callee: calleeDisplayName(current.expression, context.sourceFile),
|
|
512
|
+
...(arrayMethod == null ? {} : {arrayMethod}),
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
517
|
+
const platform = platformFact(current, false, context.checker)
|
|
518
|
+
if (platform != null) {
|
|
519
|
+
return addInstruction(context, current, {kind: 'platformValue', ...platform})
|
|
520
|
+
}
|
|
521
|
+
// config?.volume: read when the receiver is present, undefined when missing — the
|
|
522
|
+
// nullish machinery's value branch, with the true arm reading through the narrowed
|
|
523
|
+
// receiver. Each ?. link carries its own check, so config?.inner?.volume works
|
|
524
|
+
// link by link; the mixed spelling a?.b.c keeps rejecting at the .c receiver gate.
|
|
525
|
+
if (current.questionDotToken != null) {
|
|
526
|
+
const receiver = lowerExpression(current.expression, context)
|
|
527
|
+
const present = addInstruction(context, current, {kind: 'nullishCheck', value: receiver, sentinel: 'nullish', negated: true})
|
|
528
|
+
return lowerValueBranch(
|
|
529
|
+
current,
|
|
530
|
+
present,
|
|
531
|
+
() => {
|
|
532
|
+
// The branch refinement unwrapped the receiver's slot; the read must still
|
|
533
|
+
// pass the same gates a plain read does, against the non-missing part.
|
|
534
|
+
requireAccessedPropertyKind(current, context.checker)
|
|
535
|
+
return addInstruction(context, current, {kind: 'property', object: receiver, property: current.name.text})
|
|
536
|
+
},
|
|
537
|
+
() => addInstruction(context, current, {kind: 'nullishConstant', sentinel: 'undefined'}),
|
|
538
|
+
context,
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
const objectType = context.checker.getTypeAtLocation(current.expression)
|
|
542
|
+
const receiverKind = valueKind(objectType, context.checker)
|
|
543
|
+
if ((receiverKind === 'array' || receiverKind === 'tuple') && current.name.text === 'length') {
|
|
544
|
+
const array = lowerExpression(current.expression, context)
|
|
545
|
+
return addInstruction(context, current, {kind: 'arrayLength', array})
|
|
546
|
+
}
|
|
547
|
+
// A string's length is the one modeled read on an opaque string. Carry the immutable
|
|
548
|
+
// receiver so repeated reads can be recognized as the same number.
|
|
549
|
+
if (receiverKind === 'opaque' && current.name.text === 'length'
|
|
550
|
+
&& (objectType.flags & ts.TypeFlags.StringLike) !== 0) {
|
|
551
|
+
const value = lowerExpression(current.expression, context)
|
|
552
|
+
return addInstruction(context, current, {kind: 'stringLength', value})
|
|
553
|
+
}
|
|
554
|
+
// An enum member read gets its own name and rewrite; the generic receiver prose
|
|
555
|
+
// ("property read from typeof Direction") names the checker's type, not the construct.
|
|
556
|
+
const receiverSymbol = ts.isIdentifier(current.expression)
|
|
557
|
+
? context.checker.getSymbolAtLocation(current.expression)
|
|
558
|
+
: undefined
|
|
559
|
+
if (receiverSymbol != null && (receiverSymbol.flags & (ts.SymbolFlags.RegularEnum | ts.SymbolFlags.ConstEnum)) !== 0) {
|
|
560
|
+
throw unsupported(current, {kind: 'enumMemberRead'})
|
|
561
|
+
}
|
|
562
|
+
// Through valueKind: single record types and unions of one recursive shape both read
|
|
563
|
+
// fine (an admitted union joins losslessly, so every member's property is present),
|
|
564
|
+
// while index signatures, callables, and mixed shapes reject. A tagged-union receiver
|
|
565
|
+
// reads too: the engine answers reads of the tag and of properties every variant
|
|
566
|
+
// carries, and stops honestly on a partial property no check narrowed first.
|
|
567
|
+
if (receiverKind !== 'object' && receiverKind !== 'taggedUnion') {
|
|
568
|
+
throw unsupported(current.expression, {kind: 'propertyReadOnNonObject', typeText: context.checker.typeToString(objectType)})
|
|
569
|
+
}
|
|
570
|
+
requireAccessedPropertyKind(current, context.checker)
|
|
571
|
+
const object = lowerExpression(current.expression, context)
|
|
572
|
+
return addInstruction(context, current, {kind: 'property', object, property: current.name.text})
|
|
573
|
+
}
|
|
574
|
+
throw unsupported(current, {kind: 'expressionForm', syntax: ts.SyntaxKind[current.kind]})
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function lowerStaticAnnotation(annotation: StaticAnnotation, context: FunctionContext): ValueID {
|
|
578
|
+
if (annotation.kind === 'invalid') {
|
|
579
|
+
throw unsupported(annotation.node, {kind: 'staticAssertionForm', problem: annotation.problem})
|
|
580
|
+
}
|
|
581
|
+
const condition = annotation.condition
|
|
582
|
+
requireBooleanCondition(condition, context.checker)
|
|
583
|
+
const originalBlock = context.currentBlock
|
|
584
|
+
const originalBlockCount = context.blocks.length
|
|
585
|
+
const originalInstructionCount = originalBlock.instructions.length
|
|
586
|
+
let value: ValueID
|
|
587
|
+
if (annotation.role === 'requirement') {
|
|
588
|
+
const requirement = writtenRequirement(condition, context)
|
|
589
|
+
if (requirement == null) {
|
|
590
|
+
throw unsupported(condition, {
|
|
591
|
+
kind: 'staticAssertionForm',
|
|
592
|
+
problem: staticAssertionProblem(condition, context.checker, 'callerRequirement'),
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
value = lowerWrittenRequirement(requirement, condition, context)
|
|
596
|
+
} else {
|
|
597
|
+
if (!supportedWrittenAssertion(condition, context.checker)) {
|
|
598
|
+
throw unsupported(condition, {
|
|
599
|
+
kind: 'staticAssertionForm',
|
|
600
|
+
problem: staticAssertionProblem(condition, context.checker, 'directCheck'),
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
value = lowerExpression(condition, context)
|
|
604
|
+
}
|
|
605
|
+
if (context.currentBlock !== originalBlock || context.blocks.length !== originalBlockCount) {
|
|
606
|
+
throw unsupported(condition, {kind: 'staticAssertionForm', problem: 'bindValueFirst'})
|
|
607
|
+
}
|
|
608
|
+
const conditionInstructions = originalBlock.instructions.slice(originalInstructionCount)
|
|
609
|
+
if (conditionInstructions.some(instruction => !removableStaticConditionInstruction(instruction))) {
|
|
610
|
+
throw unsupported(condition, {kind: 'staticAssertionForm', problem: 'bindValueFirst'})
|
|
611
|
+
}
|
|
612
|
+
const site = addSite(context, annotation.call)
|
|
613
|
+
if (annotation.role === 'requirement') {
|
|
614
|
+
return addInstructionAtSite(context, site, {kind: 'staticRequire', value})
|
|
615
|
+
}
|
|
616
|
+
const assertion = context.assertions.length
|
|
617
|
+
context.assertions.push({site, text: condition.getText(context.sourceFile)})
|
|
618
|
+
return addInstructionAtSite(context, site, {kind: 'staticAssert', value, assertion})
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
type WrittenRequirementOperand = {kind: 'parameter'; value: ValueID} | {kind: 'constant'; value: number}
|
|
622
|
+
|
|
623
|
+
type WrittenRequirement =
|
|
624
|
+
| {kind: 'numberCheck'; predicate: 'integer' | 'finite'; value: ValueID}
|
|
625
|
+
| {
|
|
626
|
+
kind: 'comparison'
|
|
627
|
+
left: WrittenRequirementOperand
|
|
628
|
+
right: WrittenRequirementOperand
|
|
629
|
+
operator: ComparisonOperator
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function writtenRequirement(condition: ts.Expression, context: FunctionContext): WrittenRequirement | null {
|
|
633
|
+
const current = unwrapParentheses(condition)
|
|
634
|
+
if (ts.isCallExpression(current) && current.questionDotToken == null
|
|
635
|
+
&& current.arguments.length === 1 && ts.isPropertyAccessExpression(current.expression)
|
|
636
|
+
&& current.expression.questionDotToken == null
|
|
637
|
+
&& isStandardNumberObject(current.expression.expression, context.checker)
|
|
638
|
+
&& (current.expression.name.text === 'isInteger' || current.expression.name.text === 'isFinite')) {
|
|
639
|
+
const argument = current.arguments[0]!
|
|
640
|
+
const value = staticRequirementParameterPathValue(argument, context)
|
|
641
|
+
return value == null ? null : {
|
|
642
|
+
kind: 'numberCheck',
|
|
643
|
+
predicate: current.expression.name.text === 'isInteger' ? 'integer' : 'finite',
|
|
644
|
+
value,
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!ts.isBinaryExpression(current) || !staticAssertionComparison(current.operatorToken.kind)) return null
|
|
648
|
+
const leftParameter = staticRequirementParameterPathValue(current.left, context)
|
|
649
|
+
const rightParameter = staticRequirementParameterPathValue(current.right, context)
|
|
650
|
+
const leftConstant = leftParameter == null ? staticFiniteValue(current.left, context) : null
|
|
651
|
+
const rightConstant = rightParameter == null ? staticFiniteValue(current.right, context) : null
|
|
652
|
+
const left = leftParameter != null
|
|
653
|
+
? {kind: 'parameter' as const, value: leftParameter}
|
|
654
|
+
: leftConstant != null ? {kind: 'constant' as const, value: leftConstant} : null
|
|
655
|
+
const right = rightParameter != null
|
|
656
|
+
? {kind: 'parameter' as const, value: rightParameter}
|
|
657
|
+
: rightConstant != null ? {kind: 'constant' as const, value: rightConstant} : null
|
|
658
|
+
const operator = comparisonOperator(current.operatorToken.kind)
|
|
659
|
+
if (left == null || right == null || operator == null
|
|
660
|
+
|| (leftParameter != null && rightParameter != null)) return null
|
|
661
|
+
return {kind: 'comparison', left, right, operator}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function staticRequirementParameterPathValue(expression: ts.Expression, context: FunctionContext): ValueID | null {
|
|
665
|
+
requireNumberType(expression, context.checker)
|
|
666
|
+
let root = unwrapParentheses(expression)
|
|
667
|
+
while (ts.isPropertyAccessExpression(root) && root.questionDotToken == null) {
|
|
668
|
+
root = unwrapParentheses(root.expression)
|
|
669
|
+
}
|
|
670
|
+
if (!ts.isIdentifier(root)) return null
|
|
671
|
+
const symbol = context.checker.getSymbolAtLocation(root)
|
|
672
|
+
const rootValue = symbol == null ? null : context.bindings.get(symbol)
|
|
673
|
+
if (rootValue == null || !valueComesFromParameter(rootValue, context)) return null
|
|
674
|
+
return lowerExpression(expression, context)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function valueComesFromParameter(value: ValueID, context: FunctionContext): boolean {
|
|
678
|
+
if (context.parameters.some(parameter => parameter.value === value)) return true
|
|
679
|
+
for (const block of context.blocks) {
|
|
680
|
+
const producer = block.instructions.find(instruction => instruction.result === value)
|
|
681
|
+
if (producer != null) {
|
|
682
|
+
return producer.kind === 'property' && valueComesFromParameter(producer.object, context)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return false
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function staticFiniteValue(
|
|
689
|
+
expression: ts.Expression,
|
|
690
|
+
context: FunctionContext,
|
|
691
|
+
): number | null {
|
|
692
|
+
const seen = new Set<ts.Symbol>()
|
|
693
|
+
let current = expression
|
|
694
|
+
while (true) {
|
|
695
|
+
const literal = numericLiteralValue(current)
|
|
696
|
+
if (literal != null) return Number.isFinite(literal) ? literal : null
|
|
697
|
+
const unwrapped = unwrapParentheses(current)
|
|
698
|
+
if (!ts.isIdentifier(unwrapped)) return null
|
|
699
|
+
const symbol = resolvedSymbol(context.checker.getSymbolAtLocation(unwrapped), context.checker)
|
|
700
|
+
if (symbol == null || seen.has(symbol)) return null
|
|
701
|
+
seen.add(symbol)
|
|
702
|
+
const declaration = symbol.valueDeclaration
|
|
703
|
+
if (declaration == null || !ts.isVariableDeclaration(declaration)
|
|
704
|
+
|| (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0
|
|
705
|
+
|| declaration.getSourceFile().isDeclarationFile
|
|
706
|
+
|| declaration.initializer == null) return null
|
|
707
|
+
current = declaration.initializer
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function lowerWrittenRequirement(
|
|
712
|
+
requirement: WrittenRequirement,
|
|
713
|
+
condition: ts.Expression,
|
|
714
|
+
context: FunctionContext,
|
|
715
|
+
): ValueID {
|
|
716
|
+
if (requirement.kind === 'numberCheck') {
|
|
717
|
+
return addInstruction(context, condition, {
|
|
718
|
+
kind: 'numberCheck',
|
|
719
|
+
predicate: requirement.predicate,
|
|
720
|
+
value: requirement.value,
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
const lowerOperand = (operand: WrittenRequirementOperand): ValueID => {
|
|
724
|
+
if (operand.kind === 'parameter') return operand.value
|
|
725
|
+
return addInstruction(context, condition, {kind: 'constant', value: operand.value})
|
|
726
|
+
}
|
|
727
|
+
return addInstruction(context, condition, {
|
|
728
|
+
kind: 'compare',
|
|
729
|
+
operator: requirement.operator,
|
|
730
|
+
left: lowerOperand(requirement.left),
|
|
731
|
+
right: lowerOperand(requirement.right),
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// The static assertion language is deliberately smaller than ordinary expressions. A
|
|
736
|
+
// calculation is written and checked as normal code, then the assertion names its result.
|
|
737
|
+
// This gives agents one syntax boundary instead of exposing whichever instructions happen
|
|
738
|
+
// to be removable after general expression lowering.
|
|
739
|
+
function supportedWrittenAssertion(condition: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
740
|
+
const current = unwrapParentheses(condition)
|
|
741
|
+
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
742
|
+
return staticAssertionNumericAtom(current.left, checker) && staticAssertionNumericAtom(current.right, checker)
|
|
743
|
+
}
|
|
744
|
+
const operand = staticNumberCheckOperand(current, checker)
|
|
745
|
+
return operand != null && staticAssertionNumericAtom(operand, checker)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function staticAssertionNumericAtom(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
749
|
+
return staticAssertionAtom(expression) && valueKind(checker.getTypeAtLocation(expression), checker) === 'number'
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function staticAssertionComparison(kind: ts.SyntaxKind): boolean {
|
|
753
|
+
switch (kind) {
|
|
754
|
+
case ts.SyntaxKind.LessThanToken:
|
|
755
|
+
case ts.SyntaxKind.LessThanEqualsToken:
|
|
756
|
+
case ts.SyntaxKind.GreaterThanToken:
|
|
757
|
+
case ts.SyntaxKind.GreaterThanEqualsToken:
|
|
758
|
+
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
|
759
|
+
case ts.SyntaxKind.ExclamationEqualsEqualsToken: return true
|
|
760
|
+
default: return false
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function staticAssertionAtomProblem(expression: ts.Expression): StaticAssertionProblem | null {
|
|
765
|
+
const current = unwrapParentheses(expression)
|
|
766
|
+
if (ts.isIdentifier(current) || ts.isNumericLiteral(current)) return null
|
|
767
|
+
if (ts.isPrefixUnaryExpression(current)
|
|
768
|
+
&& (current.operator === ts.SyntaxKind.PlusToken || current.operator === ts.SyntaxKind.MinusToken)) {
|
|
769
|
+
return ts.isNumericLiteral(unwrapParentheses(current.operand)) ? null : 'bindValueFirst'
|
|
770
|
+
}
|
|
771
|
+
if (ts.isElementAccessExpression(current)) return 'bindValueFirst'
|
|
772
|
+
if (ts.isNonNullExpression(current)) return staticAssertionAtomProblem(current.expression)
|
|
773
|
+
if (ts.isCallExpression(current)) return 'functionCall'
|
|
774
|
+
if (ts.isBinaryExpression(current)) return 'bindValueFirst'
|
|
775
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
776
|
+
return current.questionDotToken == null
|
|
777
|
+
? staticAssertionAtomProblem(current.expression)
|
|
778
|
+
: 'directCheck'
|
|
779
|
+
}
|
|
780
|
+
return 'directCheck'
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function staticAssertionAtom(expression: ts.Expression): boolean {
|
|
784
|
+
const current = unwrapParentheses(expression)
|
|
785
|
+
if (ts.isIdentifier(current) || ts.isNumericLiteral(current)) return true
|
|
786
|
+
if (ts.isPrefixUnaryExpression(current)
|
|
787
|
+
&& (current.operator === ts.SyntaxKind.PlusToken || current.operator === ts.SyntaxKind.MinusToken)) {
|
|
788
|
+
return ts.isNumericLiteral(unwrapParentheses(current.operand))
|
|
789
|
+
}
|
|
790
|
+
return ts.isPropertyAccessExpression(current)
|
|
791
|
+
&& current.questionDotToken == null
|
|
792
|
+
&& staticAssertionAtom(current.expression)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function staticAssertionProblem(
|
|
796
|
+
condition: ts.Expression,
|
|
797
|
+
checker: ts.TypeChecker,
|
|
798
|
+
fallback: 'directCheck' | 'callerRequirement',
|
|
799
|
+
): StaticAssertionProblem {
|
|
800
|
+
const current = unwrapParentheses(condition)
|
|
801
|
+
if (ts.isBinaryExpression(current) && staticAssertionComparison(current.operatorToken.kind)) {
|
|
802
|
+
return staticAssertionAtomProblem(current.left) ?? staticAssertionAtomProblem(current.right) ?? fallback
|
|
803
|
+
}
|
|
804
|
+
const operand = staticNumberCheckOperand(current, checker)
|
|
805
|
+
if (operand != null) return staticAssertionAtomProblem(operand) ?? fallback
|
|
806
|
+
if (ts.isCallExpression(current)) return 'functionCall'
|
|
807
|
+
return 'directCheck'
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function staticNumberCheckOperand(expression: ts.Expression, checker: ts.TypeChecker): ts.Expression | null {
|
|
811
|
+
if (!ts.isCallExpression(expression) || expression.questionDotToken != null
|
|
812
|
+
|| expression.arguments.length !== 1 || !ts.isPropertyAccessExpression(expression.expression)
|
|
813
|
+
|| expression.expression.questionDotToken != null) return null
|
|
814
|
+
const callee = expression.expression
|
|
815
|
+
return isStandardNumberObject(callee.expression, checker)
|
|
816
|
+
&& (callee.name.text === 'isInteger' || callee.name.text === 'isFinite' || callee.name.text === 'isNaN')
|
|
817
|
+
? expression.arguments[0]!
|
|
818
|
+
: null
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function unwrapParentheses(expression: ts.Expression): ts.Expression {
|
|
822
|
+
let current = expression
|
|
823
|
+
while (ts.isParenthesizedExpression(current)) current = current.expression
|
|
824
|
+
return current
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// An application may erase static conditions from a production build. Normal expression
|
|
828
|
+
// lowering owns their JavaScript evaluation order; this list only accepts instructions
|
|
829
|
+
// whose removal cannot change program state. Compound control flow was rejected above.
|
|
830
|
+
function removableStaticConditionInstruction(instruction: InstructionIR): boolean {
|
|
831
|
+
switch (instruction.kind) {
|
|
832
|
+
case 'constant':
|
|
833
|
+
case 'arrayLength':
|
|
834
|
+
case 'moduleRead':
|
|
835
|
+
case 'compare':
|
|
836
|
+
case 'platformValue':
|
|
837
|
+
case 'numberCheck':
|
|
838
|
+
case 'property': return true
|
|
839
|
+
default: return false
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// The single recognizer for the three forms that assign through a plain identifier. The
|
|
844
|
+
// lowering arms and the loop-carry detection in statements.ts both dispatch on this, so a
|
|
845
|
+
// new assigning form cannot lower without also being carried across loop back edges (a
|
|
846
|
+
// binding rebound in a loop body but not carried would silently analyze later iterations
|
|
847
|
+
// with the stale pre-loop value).
|
|
848
|
+
export type IdentifierAssignment =
|
|
849
|
+
| {form: 'assign'; target: ts.Identifier; node: ts.BinaryExpression}
|
|
850
|
+
| {form: 'compound'; target: ts.Identifier; node: ts.BinaryExpression; operator: Extract<InstructionIR, {kind: 'binary'}>['operator']}
|
|
851
|
+
| {form: 'logical'; target: ts.Identifier; node: ts.BinaryExpression; logical: 'nullish' | 'or' | 'and'}
|
|
852
|
+
| {form: 'update'; target: ts.Identifier; node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression}
|
|
853
|
+
|
|
854
|
+
export function identifierAssignment(node: ts.Node): IdentifierAssignment | null {
|
|
855
|
+
if (ts.isBinaryExpression(node) && ts.isIdentifier(node.left)) {
|
|
856
|
+
if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken) return {form: 'assign', target: node.left, node}
|
|
857
|
+
const operator = compoundAssignmentOperator(node.operatorToken.kind)
|
|
858
|
+
if (operator != null) return {form: 'compound', target: node.left, node, operator}
|
|
859
|
+
const logical = node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken ? 'nullish'
|
|
860
|
+
: node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ? 'or'
|
|
861
|
+
: node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ? 'and'
|
|
862
|
+
: null
|
|
863
|
+
if (logical != null) return {form: 'logical', target: node.left, node, logical}
|
|
864
|
+
}
|
|
865
|
+
if (
|
|
866
|
+
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node))
|
|
867
|
+
&& (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
868
|
+
&& ts.isIdentifier(node.operand)
|
|
869
|
+
) {
|
|
870
|
+
return {form: 'update', target: node.operand, node}
|
|
871
|
+
}
|
|
872
|
+
return null
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function compoundAssignmentOperator(kind: ts.SyntaxKind): Extract<InstructionIR, {kind: 'binary'}>['operator'] | null {
|
|
876
|
+
switch (kind) {
|
|
877
|
+
case ts.SyntaxKind.PlusEqualsToken: return 'add'
|
|
878
|
+
case ts.SyntaxKind.MinusEqualsToken: return 'subtract'
|
|
879
|
+
case ts.SyntaxKind.AsteriskEqualsToken: return 'multiply'
|
|
880
|
+
case ts.SyntaxKind.SlashEqualsToken: return 'divide'
|
|
881
|
+
default: return null
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// The shared value-producing branch shape: branch on the condition, lower each arm in its
|
|
886
|
+
// own block, and join at a continuation whose single parameter carries the result. Arms are
|
|
887
|
+
// provably assignment-free — assignments lower only through lowerStatementExpression — so
|
|
888
|
+
// no bindings can change across the arms and the join needs no binding merge. Ternaries and
|
|
889
|
+
// the logical operators are the two consumers; lowerIfStatement stays separate (no result
|
|
890
|
+
// value, arms may terminate, and assignments are allowed there).
|
|
891
|
+
function lowerValueBranch(
|
|
892
|
+
node: ts.Expression,
|
|
893
|
+
condition: ValueID,
|
|
894
|
+
lowerTrueArm: () => ValueID,
|
|
895
|
+
lowerFalseArm: () => ValueID,
|
|
896
|
+
context: FunctionContext,
|
|
897
|
+
): ValueID {
|
|
898
|
+
const whenTrue = createBlock(context)
|
|
899
|
+
const whenFalse = createBlock(context)
|
|
900
|
+
terminate(context.currentBlock, {
|
|
901
|
+
kind: 'branch',
|
|
902
|
+
condition,
|
|
903
|
+
whenTrue: {block: whenTrue, arguments: []},
|
|
904
|
+
whenFalse: {block: whenFalse, arguments: []},
|
|
905
|
+
site: addSite(context, node),
|
|
906
|
+
})
|
|
907
|
+
context.currentBlock = context.blocks[whenTrue]!
|
|
908
|
+
const trueValue = lowerTrueArm()
|
|
909
|
+
const trueBlock = context.currentBlock
|
|
910
|
+
context.currentBlock = context.blocks[whenFalse]!
|
|
911
|
+
const falseValue = lowerFalseArm()
|
|
912
|
+
const falseBlock = context.currentBlock
|
|
913
|
+
const continuation = createBlock(context, 1)
|
|
914
|
+
terminate(trueBlock, {
|
|
915
|
+
kind: 'jump',
|
|
916
|
+
target: {block: continuation, arguments: [trueValue]},
|
|
917
|
+
site: addSite(context, node),
|
|
918
|
+
})
|
|
919
|
+
terminate(falseBlock, {
|
|
920
|
+
kind: 'jump',
|
|
921
|
+
target: {block: continuation, arguments: [falseValue]},
|
|
922
|
+
site: addSite(context, node),
|
|
923
|
+
})
|
|
924
|
+
context.currentBlock = context.blocks[continuation]!
|
|
925
|
+
return context.currentBlock.parameters[0]!
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function lowerConditionalExpression(expression: ts.ConditionalExpression, context: FunctionContext): ValueID {
|
|
929
|
+
requireBooleanCondition(expression.condition, context.checker)
|
|
930
|
+
const resultType = context.checker.getTypeAtLocation(expression)
|
|
931
|
+
if (valueKind(resultType, context.checker) == null) {
|
|
932
|
+
throw unsupported(expression, {kind: 'valueType', typeText: context.checker.typeToString(resultType)})
|
|
933
|
+
}
|
|
934
|
+
// Through the same short-circuit branching statement ifs use, so an `&&`-joined
|
|
935
|
+
// condition refines each conjunct in the arms — `a > 0 && b > 0 ? a / b : 0`
|
|
936
|
+
// discharges b's nonzero exactly like the if-statement spelling does. Lowering the
|
|
937
|
+
// condition as one boolean expression (the previous shape) hid the conjuncts behind a
|
|
938
|
+
// joined block parameter no branch refinement could see through; a conversion pass on
|
|
939
|
+
// the owner's repo caught the asymmetry.
|
|
940
|
+
const whenTrue = createBlock(context)
|
|
941
|
+
const whenFalse = createBlock(context)
|
|
942
|
+
lowerBranchingCondition(expression.condition, whenTrue, whenFalse, context)
|
|
943
|
+
context.currentBlock = context.blocks[whenTrue]!
|
|
944
|
+
const trueValue = lowerExpression(expression.whenTrue, context)
|
|
945
|
+
const trueBlock = context.currentBlock
|
|
946
|
+
context.currentBlock = context.blocks[whenFalse]!
|
|
947
|
+
const falseValue = lowerExpression(expression.whenFalse, context)
|
|
948
|
+
const falseBlock = context.currentBlock
|
|
949
|
+
const continuation = createBlock(context, 1)
|
|
950
|
+
terminate(trueBlock, {
|
|
951
|
+
kind: 'jump',
|
|
952
|
+
target: {block: continuation, arguments: [trueValue]},
|
|
953
|
+
site: addSite(context, expression),
|
|
954
|
+
})
|
|
955
|
+
terminate(falseBlock, {
|
|
956
|
+
kind: 'jump',
|
|
957
|
+
target: {block: continuation, arguments: [falseValue]},
|
|
958
|
+
site: addSite(context, expression),
|
|
959
|
+
})
|
|
960
|
+
context.currentBlock = context.blocks[continuation]!
|
|
961
|
+
return context.currentBlock.parameters[0]!
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// `a && b` evaluates b only when a is true and yields false otherwise; `a || b` mirrors it —
|
|
965
|
+
// the shared value-branch shape with one arm being a boolean constant.
|
|
966
|
+
// Lowers a statement-position condition into branch terminators with short-circuit CFG:
|
|
967
|
+
// `if (a && b)` becomes two chained branches sharing the false target, so each simple
|
|
968
|
+
// condition is its own branch producer and narrows on its own — nested guards and inline
|
|
969
|
+
// && guards refine identically, by construction. Conditions are assignment-free (see
|
|
970
|
+
// lowerStatementExpression), so the intermediate blocks carry no parameters and bindings
|
|
971
|
+
// never change inside.
|
|
972
|
+
export function lowerBranchingCondition(
|
|
973
|
+
expression: ts.Expression,
|
|
974
|
+
whenTrue: number,
|
|
975
|
+
whenFalse: number,
|
|
976
|
+
context: FunctionContext,
|
|
977
|
+
): void {
|
|
978
|
+
const current = unwrap(expression, context.checker)
|
|
979
|
+
if (ts.isBinaryExpression(current)
|
|
980
|
+
&& (current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
981
|
+
|| current.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
|
|
982
|
+
const isAnd = current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
983
|
+
const middle = createBlock(context)
|
|
984
|
+
if (isAnd) {
|
|
985
|
+
lowerBranchingCondition(current.left, middle, whenFalse, context)
|
|
986
|
+
} else {
|
|
987
|
+
lowerBranchingCondition(current.left, whenTrue, middle, context)
|
|
988
|
+
}
|
|
989
|
+
context.currentBlock = context.blocks[middle]!
|
|
990
|
+
lowerBranchingCondition(current.right, whenTrue, whenFalse, context)
|
|
991
|
+
return
|
|
992
|
+
}
|
|
993
|
+
if (ts.isPrefixUnaryExpression(current) && current.operator === ts.SyntaxKind.ExclamationToken) {
|
|
994
|
+
lowerBranchingCondition(current.operand, whenFalse, whenTrue, context)
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
requireBooleanCondition(current, context.checker)
|
|
998
|
+
// `if (result.ok)` where ok is a boolean-valued tag: truthiness of the tag IS the
|
|
999
|
+
// tag check against true, so the branches narrow the variant list exactly like the
|
|
1000
|
+
// `result.ok === true` spelling. Only boolean tags take this route — a string tag's
|
|
1001
|
+
// truthiness would additionally hinge on the empty string, which requireBooleanCondition
|
|
1002
|
+
// rejects anyway.
|
|
1003
|
+
const tagUnion = taggedUnionTagRead(current, context)
|
|
1004
|
+
const condition = tagUnion != null
|
|
1005
|
+
? addInstruction(context, current, {kind: 'tagCheck', union: lowerExpression(tagUnion, context), tagValue: true, negated: false})
|
|
1006
|
+
: lowerExpression(current, context)
|
|
1007
|
+
terminate(context.currentBlock, {
|
|
1008
|
+
kind: 'branch',
|
|
1009
|
+
condition,
|
|
1010
|
+
whenTrue: {block: whenTrue, arguments: []},
|
|
1011
|
+
whenFalse: {block: whenFalse, arguments: []},
|
|
1012
|
+
site: addSite(context, current),
|
|
1013
|
+
})
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function lowerLogicalExpression(expression: ts.BinaryExpression, context: FunctionContext): ValueID {
|
|
1017
|
+
requireBooleanCondition(expression.left, context.checker)
|
|
1018
|
+
requireBooleanCondition(expression.right, context.checker)
|
|
1019
|
+
const isAnd = expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
1020
|
+
const condition = lowerExpression(expression.left, context)
|
|
1021
|
+
return lowerValueBranch(
|
|
1022
|
+
expression,
|
|
1023
|
+
condition,
|
|
1024
|
+
() => isAnd
|
|
1025
|
+
? lowerExpression(expression.right, context)
|
|
1026
|
+
: addInstruction(context, expression, {kind: 'booleanConstant', value: true}),
|
|
1027
|
+
() => isAnd
|
|
1028
|
+
? addInstruction(context, expression, {kind: 'booleanConstant', value: false})
|
|
1029
|
+
: lowerExpression(expression.right, context),
|
|
1030
|
+
context,
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function arithmeticOperator(kind: ts.SyntaxKind): Extract<InstructionIR, {kind: 'binary'}>['operator'] | null {
|
|
1035
|
+
switch (kind) {
|
|
1036
|
+
case ts.SyntaxKind.PlusToken: return 'add'
|
|
1037
|
+
case ts.SyntaxKind.MinusToken: return 'subtract'
|
|
1038
|
+
case ts.SyntaxKind.AsteriskToken: return 'multiply'
|
|
1039
|
+
case ts.SyntaxKind.SlashToken: return 'divide'
|
|
1040
|
+
case ts.SyntaxKind.PercentToken: return 'remainder'
|
|
1041
|
+
default: return null
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function comparisonOperator(kind: ts.SyntaxKind): ComparisonOperator | null {
|
|
1046
|
+
switch (kind) {
|
|
1047
|
+
case ts.SyntaxKind.LessThanToken: return 'lessThan'
|
|
1048
|
+
case ts.SyntaxKind.LessThanEqualsToken: return 'lessThanOrEqual'
|
|
1049
|
+
case ts.SyntaxKind.GreaterThanToken: return 'greaterThan'
|
|
1050
|
+
case ts.SyntaxKind.GreaterThanEqualsToken: return 'greaterThanOrEqual'
|
|
1051
|
+
case ts.SyntaxKind.EqualsEqualsToken:
|
|
1052
|
+
case ts.SyntaxKind.EqualsEqualsEqualsToken: return 'equal'
|
|
1053
|
+
// Loose != on two numbers is exactly strict !== (no coercion between numbers); the
|
|
1054
|
+
// nullish and opaque spellings of both tokens are claimed by their own handlers
|
|
1055
|
+
// before the operator classification runs.
|
|
1056
|
+
case ts.SyntaxKind.ExclamationEqualsToken:
|
|
1057
|
+
case ts.SyntaxKind.ExclamationEqualsEqualsToken: return 'notEqual'
|
|
1058
|
+
default: return null
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function requireNumberType(node: ts.Node, checker: ts.TypeChecker): void {
|
|
1063
|
+
const type = checker.getTypeAtLocation(node)
|
|
1064
|
+
// Through valueKind, not a raw flag test, so there is one definition of "number":
|
|
1065
|
+
// a literal union like `1 | 2` — the numeric discriminant of a tagged record — is a
|
|
1066
|
+
// number here exactly as it is at the declarator and destructuring gates.
|
|
1067
|
+
// TypeScript permits `any` in a numeric operation, but its static permission proves
|
|
1068
|
+
// nothing about the runtime value. Let the claim-free value reach the evaluator, whose
|
|
1069
|
+
// numeric gate stops only the path that executes the operation. Other non-number types
|
|
1070
|
+
// remain lowering rejections because diagnostic-clean TypeScript does not license them.
|
|
1071
|
+
if (valueKind(type, checker) !== 'number' && (type.flags & ts.TypeFlags.Any) === 0) {
|
|
1072
|
+
throw unsupported(node, {kind: 'nonNumberOperand', typeText: checker.typeToString(type)})
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function typeCanIncludeUndefined(type: ts.Type): boolean {
|
|
1077
|
+
if ((type.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true
|
|
1078
|
+
return type.isUnion() && type.types.some(typeCanIncludeUndefined)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function lowerParameterDefault(
|
|
1082
|
+
default_: ParameterDefaultLiteral,
|
|
1083
|
+
node: ts.Expression,
|
|
1084
|
+
context: FunctionContext,
|
|
1085
|
+
): ValueID {
|
|
1086
|
+
switch (default_.kind) {
|
|
1087
|
+
case 'number': return addInstruction(context, node, {kind: 'constant', value: default_.value})
|
|
1088
|
+
case 'boolean': return addInstruction(context, node, {kind: 'booleanConstant', value: default_.value})
|
|
1089
|
+
case 'opaque': return addInstruction(context, node, {kind: 'opaqueConstant', content: default_.content})
|
|
1090
|
+
case 'nullish': return addInstruction(context, node, {kind: 'nullishConstant', sentinel: default_.sentinel})
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// The single value kind a type describes, or null when the type mixes kinds (a union like
|
|
1095
|
+
// number | boolean), mixes object shapes without a supported tag (a union like {x} |
|
|
1096
|
+
// {x, y}), or falls outside the accepted kinds entirely (e.g. bigint or symbol).
|
|
1097
|
+
type ValueKindResult = 'number' | 'boolean' | 'object' | 'nullable' | 'array' | 'tuple' | 'opaque' | 'taggedUnion' | null
|
|
1098
|
+
|
|
1099
|
+
// Exact memoization keyed on (interned type, remaining depth budget): the walk is pure
|
|
1100
|
+
// over both, so the cache cannot change any answer — it only stops the same type being
|
|
1101
|
+
// re-walked from every expression node that mentions it. A profiling pass measured one
|
|
1102
|
+
// context-bag file issuing 757 million checker queries over ~216 distinct types, ~93% of
|
|
1103
|
+
// the whole survey's wall time, precisely because these walks recompute per call site.
|
|
1104
|
+
// (declaredKind has had the same cache since the tagged-union milestone; valueKind and
|
|
1105
|
+
// taggedUnionProperty gain theirs here.)
|
|
1106
|
+
const valueKindCache = new WeakMap<ts.Type, ValueKindResult[]>()
|
|
1107
|
+
|
|
1108
|
+
export function valueKind(type: ts.Type, checker: ts.TypeChecker, depth = 0): ValueKindResult {
|
|
1109
|
+
// The depth guard bounds recursion into element types (a recursive `type T = T[]` would
|
|
1110
|
+
// otherwise loop); past it, nothing classifies.
|
|
1111
|
+
if (depth > 8) return null
|
|
1112
|
+
let byDepth = valueKindCache.get(type)
|
|
1113
|
+
if (byDepth == null) {
|
|
1114
|
+
byDepth = []
|
|
1115
|
+
valueKindCache.set(type, byDepth)
|
|
1116
|
+
}
|
|
1117
|
+
const cached = byDepth[depth]
|
|
1118
|
+
if (cached !== undefined) return cached
|
|
1119
|
+
const result = valueKindUncached(type, checker, depth)
|
|
1120
|
+
byDepth[depth] = result
|
|
1121
|
+
return result
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function valueKindUncached(type: ts.Type, checker: ts.TypeChecker, depth: number): ValueKindResult {
|
|
1125
|
+
if ((type.flags & ts.TypeFlags.NumberLike) !== 0) return 'number'
|
|
1126
|
+
if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) return 'boolean'
|
|
1127
|
+
// Strings are carried without claims: a label or id must not reject the numeric
|
|
1128
|
+
// contract of the function around it.
|
|
1129
|
+
if ((type.flags & ts.TypeFlags.StringLike) !== 0) return 'opaque'
|
|
1130
|
+
// The type system's own split, mirrored: tuple types are positional and exact, array
|
|
1131
|
+
// types are homogeneous. Checked before the general object arm (both carry the Object
|
|
1132
|
+
// flag and index signatures). An array classifies only when its ELEMENT does — a
|
|
1133
|
+
// (number | boolean)[] value's element hull is nothing any read gate could describe.
|
|
1134
|
+
if (checker.isTupleType(type)) return 'tuple'
|
|
1135
|
+
if (checker.isArrayType(type)) {
|
|
1136
|
+
const element = checker.getIndexTypeOfType(type, ts.IndexKind.Number)
|
|
1137
|
+
return element != null && valueKind(element, checker, depth + 1) != null ? 'array' : null
|
|
1138
|
+
}
|
|
1139
|
+
// Object types and intersections of object types (`Base & {subPage: 'select'}` — the
|
|
1140
|
+
// extends idiom for route variants) run the same classification: the checker's property
|
|
1141
|
+
// and signature queries answer for an intersection's merged view, so one body serves
|
|
1142
|
+
// both. A member outside the object kind keeps the whole intersection out.
|
|
1143
|
+
const objectLike = (type.flags & ts.TypeFlags.Object) !== 0
|
|
1144
|
+
|| (type.isIntersection() && type.types.every(member => valueKind(member, checker, depth + 1) === 'object'))
|
|
1145
|
+
if (objectLike) {
|
|
1146
|
+
// An index signature, e.g. Record<string, number>, admits properties the type never
|
|
1147
|
+
// names: a value typed with one can carry any key set at runtime, so the abstract
|
|
1148
|
+
// record — built from a specific literal — cannot honor reads or spreads the signature
|
|
1149
|
+
// licenses. `stats.misses` type-checks against Record<string, number> while the value
|
|
1150
|
+
// is `{clicks: 1}`, and `{...defaults, ...overrides}` would copy nothing from an
|
|
1151
|
+
// override map whose type names no properties. A callable or constructable type is
|
|
1152
|
+
// not a record either: `point.toString` type-checks on every object literal, but the
|
|
1153
|
+
// record value built from the literal carries no such property, and a class's static
|
|
1154
|
+
// side is a constructor, not plain data. Finally, the type must have at least one
|
|
1155
|
+
// required non-callable property, or primitives inhabit it — every non-null value
|
|
1156
|
+
// satisfies `{}`, and a number satisfies `{toString(): string}` — letting a number
|
|
1157
|
+
// and a record meet at a join.
|
|
1158
|
+
if (checker.getIndexInfosOfType(type).length > 0) return null
|
|
1159
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
|
1160
|
+
// A pure function type — call signatures and nothing else — is carried opaquely,
|
|
1161
|
+
// like a callback stored in a record already is: calls to it reject at the call
|
|
1162
|
+
// gate (the callee must be a top-level function), so carrying makes callback
|
|
1163
|
+
// PARAMETERS as cheap as callback properties. Hybrid callable-objects keep out:
|
|
1164
|
+
// their data properties would invite reads the carried value cannot answer.
|
|
1165
|
+
const dataProperties = checker.getPropertiesOfType(type).some(property =>
|
|
1166
|
+
checker.getTypeOfSymbol(property).getCallSignatures().length === 0)
|
|
1167
|
+
return dataProperties ? null : 'opaque'
|
|
1168
|
+
}
|
|
1169
|
+
// Optional properties anchor too, now that they model as maybe-undefined values: an
|
|
1170
|
+
// all-optional config record ({volume?: number}) is a weak type TypeScript refuses to
|
|
1171
|
+
// assign primitives to, so the primitive-inhabitation worry that bars `{}` does not
|
|
1172
|
+
// apply — only a type with NO data properties at all stays out.
|
|
1173
|
+
const anchored = checker.getPropertiesOfType(type).some(property =>
|
|
1174
|
+
checker.getTypeOfSymbol(property).getCallSignatures().length === 0)
|
|
1175
|
+
return anchored ? 'object' : null
|
|
1176
|
+
}
|
|
1177
|
+
// `unknown` and `any` both carry without claims. For unknown the checker forces
|
|
1178
|
+
// narrowing before any use, so its word stays intact; for any the checker's word is
|
|
1179
|
+
// void, and claim-free is the one honest reading — nothing numeric is ever said about
|
|
1180
|
+
// the value, operations that require a concrete kind stop at their gates, and a write
|
|
1181
|
+
// into a typed binding leaves the binding opaque instead of re-minting claims.
|
|
1182
|
+
if ((type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) !== 0) return 'opaque'
|
|
1183
|
+
if (type.isUnion()) {
|
|
1184
|
+
// `T | null`, `T | undefined`, and `T | null | undefined` classify as nullable when T
|
|
1185
|
+
// itself classifies to one kind. Gates that cannot carry a missing value keep
|
|
1186
|
+
// rejecting ('nullable' matches neither 'number' nor 'object'); kind-agnostic gates
|
|
1187
|
+
// (declarators, ternary results, destructure elements, returns) accept.
|
|
1188
|
+
const missingFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1189
|
+
if (type.types.some(member => (member.flags & missingFlags) !== 0)) {
|
|
1190
|
+
const rest = nonMissingUnionMembers(type)
|
|
1191
|
+
// The non-missing rest classifies as a group, so `4 | 8 | 24 | undefined` — an
|
|
1192
|
+
// as-const table's bare dynamic read — is nullable like `number | undefined`. A
|
|
1193
|
+
// rest that is itself a tagged union (`null | LightboxOwnerRoute`) is nullable too.
|
|
1194
|
+
const restKind = classifyUnionMembers(rest, checker, depth + 1)
|
|
1195
|
+
if (restKind != null) return 'nullable'
|
|
1196
|
+
return taggedUnionProperty(rest, checker, depth) == null ? null : 'nullable'
|
|
1197
|
+
}
|
|
1198
|
+
// A shared string- or boolean-literal property makes a record union tagged. Other
|
|
1199
|
+
// structural unions reject; scalar literal unions still collapse to one shared kind.
|
|
1200
|
+
if (taggedUnionProperty(type.types, checker, depth) != null) return 'taggedUnion'
|
|
1201
|
+
return classifyUnionMembers(type.types, checker, depth + 1)
|
|
1202
|
+
}
|
|
1203
|
+
return null
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Filtering a nullable union creates a new array. That array is also the exact cache key
|
|
1207
|
+
// for taggedUnionProperty, so rebuilding it during every recursive declared-kind walk
|
|
1208
|
+
// defeats that cache. TypeScript interns union types, which makes their filtered member
|
|
1209
|
+
// lists safe to reuse as well.
|
|
1210
|
+
const nonMissingUnionMembersCache = new WeakMap<ts.UnionType, readonly ts.Type[]>()
|
|
1211
|
+
|
|
1212
|
+
export function nonMissingUnionMembers(type: ts.UnionType): readonly ts.Type[] {
|
|
1213
|
+
const cached = nonMissingUnionMembersCache.get(type)
|
|
1214
|
+
if (cached != null) return cached
|
|
1215
|
+
const missingFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1216
|
+
const members = type.types.filter(member => (member.flags & missingFlags) === 0)
|
|
1217
|
+
nonMissingUnionMembersCache.set(type, members)
|
|
1218
|
+
return members
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// The property that tells a union of record shapes apart: present and required in every
|
|
1222
|
+
// member, typed as a single string literal in each. The first property (in the first
|
|
1223
|
+
// member's declaration order) that qualifies wins — by convention the tag comes first
|
|
1224
|
+
// (`type: 'lightbox'`). Two members MAY share a tag value (`{type: 'updates'; tab} |
|
|
1225
|
+
// {type: 'updates'; article}`): a tag check then keeps both. Code that must tell them apart
|
|
1226
|
+
// needs a distinct tag value; `in` checks are outside the subset because width subtyping
|
|
1227
|
+
// permits undeclared extra properties. Null when no property qualifies.
|
|
1228
|
+
// Keyed on the members ARRAY: a union type's .types array is interned by the checker, so
|
|
1229
|
+
// the reference identifies the member set exactly. Nullable unions use the stable array
|
|
1230
|
+
// from nonMissingUnionMembers; any other freshly built array simply misses.
|
|
1231
|
+
const taggedUnionPropertyCache = new WeakMap<readonly ts.Type[], Array<string | null>>()
|
|
1232
|
+
|
|
1233
|
+
export function taggedUnionProperty(members: readonly ts.Type[], checker: ts.TypeChecker, depth = 0): string | null {
|
|
1234
|
+
if (members.length < 2) return null
|
|
1235
|
+
let byDepth = taggedUnionPropertyCache.get(members)
|
|
1236
|
+
if (byDepth == null) {
|
|
1237
|
+
byDepth = []
|
|
1238
|
+
taggedUnionPropertyCache.set(members, byDepth)
|
|
1239
|
+
}
|
|
1240
|
+
const cached = byDepth[depth]
|
|
1241
|
+
if (cached !== undefined) return cached
|
|
1242
|
+
const result = taggedUnionPropertyUncached(members, checker, depth)
|
|
1243
|
+
byDepth[depth] = result
|
|
1244
|
+
return result
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function taggedUnionPropertyUncached(members: readonly ts.Type[], checker: ts.TypeChecker, depth: number): string | null {
|
|
1248
|
+
for (const member of members) {
|
|
1249
|
+
if (valueKind(member, checker, depth + 1) !== 'object') return null
|
|
1250
|
+
}
|
|
1251
|
+
// Two passes: a property whose tag is a SINGLE literal per member (`ok: true` /
|
|
1252
|
+
// `ok: false`, `type: 'lightbox'`) is a real discriminant and wins first. Only then do
|
|
1253
|
+
// multi-literal tags qualify (`type: 'desktopCollapsedNav' | 'desktopExpandedNav'` in
|
|
1254
|
+
// one variant, or a plain boolean property every member carries) — otherwise a
|
|
1255
|
+
// non-discriminating `enabled: boolean` shared by all members could shadow the actual
|
|
1256
|
+
// tag declared after it.
|
|
1257
|
+
const first = members[0]!
|
|
1258
|
+
const qualifies = (candidateName: string, singleLiteralOnly: boolean): boolean => {
|
|
1259
|
+
for (const member of members) {
|
|
1260
|
+
const property = checker.getPropertyOfType(member, candidateName)
|
|
1261
|
+
if (property == null || (property.flags & ts.SymbolFlags.Optional) !== 0) return false
|
|
1262
|
+
const literals = tagLiteralValues(checker.getTypeOfSymbol(property))
|
|
1263
|
+
if (literals == null || (singleLiteralOnly && literals.length !== 1)) return false
|
|
1264
|
+
}
|
|
1265
|
+
return true
|
|
1266
|
+
}
|
|
1267
|
+
for (const singleLiteralOnly of [true, false]) {
|
|
1268
|
+
for (const candidate of checker.getPropertiesOfType(first)) {
|
|
1269
|
+
if ((candidate.flags & ts.SymbolFlags.Optional) !== 0) continue
|
|
1270
|
+
if (qualifies(candidate.name, singleLiteralOnly)) return candidate.name
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return null
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// The callee as a short display name for the call rejection. A method on a simple
|
|
1277
|
+
// receiver reads naturally (localStorage.getItem, Math.max — one or two identifiers); a
|
|
1278
|
+
// method on a computed receiver — a call result, a regex literal, a chained pipeline —
|
|
1279
|
+
// collapses to (…).method. Raw source text carried newlines into the report (breaking
|
|
1280
|
+
// the one-fact-per-line format) and made the survey tally fragment into one bucket per
|
|
1281
|
+
// call site; the collapsed form keeps lines whole and groups the tally by method.
|
|
1282
|
+
function calleeDisplayName(expression: ts.Expression, sourceFile: ts.SourceFile): string {
|
|
1283
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1284
|
+
const receiver = expression.expression
|
|
1285
|
+
const receiverName = ts.isIdentifier(receiver)
|
|
1286
|
+
? receiver.text
|
|
1287
|
+
: ts.isPropertyAccessExpression(receiver) && ts.isIdentifier(receiver.expression)
|
|
1288
|
+
? `${receiver.expression.text}.${receiver.name.text}`
|
|
1289
|
+
: '(…)'
|
|
1290
|
+
return `${receiverName}.${expression.name.text}`
|
|
1291
|
+
}
|
|
1292
|
+
return expression.getText(sourceFile).replace(/\s+/g, ' ').slice(0, 60)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// The literal written in an object literal's tag position, or null: a string literal, a
|
|
1296
|
+
// no-substitution template, or the true/false keywords, seen through parens, satisfies,
|
|
1297
|
+
// and as-const (unwrap peels exactly those). Quoted property names count — the rule is
|
|
1298
|
+
// about the VALUE being a written literal, not about how the key is spelled.
|
|
1299
|
+
function writtenTagLiteral(
|
|
1300
|
+
literal: ts.Expression,
|
|
1301
|
+
tagProperty: string,
|
|
1302
|
+
context: FunctionContext,
|
|
1303
|
+
): string | boolean | null {
|
|
1304
|
+
if (!ts.isObjectLiteralExpression(literal)) return null
|
|
1305
|
+
for (const property of literal.properties) {
|
|
1306
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
1307
|
+
const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name) ? property.name.text : null
|
|
1308
|
+
if (name !== tagProperty) continue
|
|
1309
|
+
const initializer = unwrap(property.initializer, context.checker)
|
|
1310
|
+
if (ts.isStringLiteral(initializer) || ts.isNoSubstitutionTemplateLiteral(initializer)) return initializer.text
|
|
1311
|
+
if (initializer.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
1312
|
+
if (initializer.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
1313
|
+
return null
|
|
1314
|
+
}
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// The literal tag values a tag property's type covers: a string or boolean literal gives
|
|
1319
|
+
// one, a union of such literals gives one per member — and `ok: boolean` arrives here as
|
|
1320
|
+
// the checker's `true | false` union, so it gives both. Null when any member is not such
|
|
1321
|
+
// a literal (a number tag, a full string). The list is bounded by what the author wrote
|
|
1322
|
+
// in the type.
|
|
1323
|
+
export function tagLiteralValues(type: ts.Type): Array<string | boolean> | null {
|
|
1324
|
+
const single = (member: ts.Type): string | boolean | null => {
|
|
1325
|
+
if (member.isStringLiteral()) return member.value
|
|
1326
|
+
if ((member.flags & ts.TypeFlags.BooleanLiteral) !== 0) {
|
|
1327
|
+
return (member as unknown as {intrinsicName: string}).intrinsicName === 'true'
|
|
1328
|
+
}
|
|
1329
|
+
return null
|
|
1330
|
+
}
|
|
1331
|
+
const members = type.isUnion() ? type.types : [type]
|
|
1332
|
+
const literals: Array<string | boolean> = []
|
|
1333
|
+
for (const member of members) {
|
|
1334
|
+
const literal = single(member)
|
|
1335
|
+
if (literal == null) return null
|
|
1336
|
+
literals.push(literal)
|
|
1337
|
+
}
|
|
1338
|
+
return literals
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// One shared kind for a group of scalar union members, or null. Multiple object, array,
|
|
1342
|
+
// or tuple members need a string or boolean tag and are handled before this function.
|
|
1343
|
+
// A one-member list can occur after removing null and undefined; retain the ordinary
|
|
1344
|
+
// depth and cycle check for that structural member without comparing it to another shape.
|
|
1345
|
+
function classifyUnionMembers(
|
|
1346
|
+
members: readonly ts.Type[],
|
|
1347
|
+
checker: ts.TypeChecker,
|
|
1348
|
+
depth: number,
|
|
1349
|
+
): 'number' | 'boolean' | 'object' | 'array' | 'tuple' | 'opaque' | null {
|
|
1350
|
+
if (depth > 8) return null
|
|
1351
|
+
let shared: 'number' | 'boolean' | 'object' | 'array' | 'tuple' | 'opaque' | null = null
|
|
1352
|
+
for (const member of members) {
|
|
1353
|
+
const kind = valueKind(member, checker, depth)
|
|
1354
|
+
// A nullable or tagged-union member cannot arise here (TypeScript flattens nested
|
|
1355
|
+
// unions), but the type system cannot see that; both fail the shared-kind rule.
|
|
1356
|
+
if (kind == null || kind === 'nullable' || kind === 'taggedUnion' || (shared != null && kind !== shared)) return null
|
|
1357
|
+
if (kind === 'object' || kind === 'array' || kind === 'tuple') {
|
|
1358
|
+
if (members.length > 1 || !structuralTypeWalkCompletes(member, checker, [])) return null
|
|
1359
|
+
}
|
|
1360
|
+
shared = kind
|
|
1361
|
+
}
|
|
1362
|
+
return shared
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Nullable records still need the same bounded walk as other declared structures. A
|
|
1366
|
+
// recursive or excessively deep member rejects instead of producing an enormous partial
|
|
1367
|
+
// record. This walk only checks that traversal completes; it does not compare shapes.
|
|
1368
|
+
function structuralTypeWalkCompletes(type: ts.Type, checker: ts.TypeChecker, seen: ts.Type[]): boolean {
|
|
1369
|
+
if (type.isIntersection() && valueKind(type, checker) === 'object') {
|
|
1370
|
+
if (seen.length >= 8 || seen.includes(type)) return false
|
|
1371
|
+
return structuralPropertiesComplete(type, checker, seen)
|
|
1372
|
+
}
|
|
1373
|
+
if (type.isUnion()) return type.types.every(member => structuralTypeWalkCompletes(member, checker, seen))
|
|
1374
|
+
if (checker.isTupleType(type)) {
|
|
1375
|
+
if (seen.length >= 8 || seen.includes(type)) return false
|
|
1376
|
+
return checker.getTypeArguments(type as ts.TypeReference)
|
|
1377
|
+
.every(member => structuralTypeWalkCompletes(member, checker, [...seen, type]))
|
|
1378
|
+
}
|
|
1379
|
+
if (checker.isArrayType(type)) {
|
|
1380
|
+
if (seen.length >= 8 || seen.includes(type)) return false
|
|
1381
|
+
const element = checker.getIndexTypeOfType(type, ts.IndexKind.Number)
|
|
1382
|
+
return element == null || structuralTypeWalkCompletes(element, checker, [...seen, type])
|
|
1383
|
+
}
|
|
1384
|
+
if ((type.flags & ts.TypeFlags.Object) === 0) return true
|
|
1385
|
+
if (seen.length >= 8 || seen.includes(type)) return false
|
|
1386
|
+
if (declaredOnlyInDeclarationFiles(type.getSymbol() ?? type.aliasSymbol)) return true
|
|
1387
|
+
if (checker.getIndexInfosOfType(type).length > 0) return true
|
|
1388
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) return true
|
|
1389
|
+
return structuralPropertiesComplete(type, checker, seen)
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function structuralPropertiesComplete(type: ts.Type, checker: ts.TypeChecker, seen: ts.Type[]): boolean {
|
|
1393
|
+
const nextSeen = [...seen, type]
|
|
1394
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
1395
|
+
if ((property.flags & ts.SymbolFlags.Optional) !== 0) continue
|
|
1396
|
+
if (!structuralTypeWalkCompletes(checker.getTypeOfSymbol(property), checker, nextSeen)) return false
|
|
1397
|
+
}
|
|
1398
|
+
return true
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Truthiness conditions like `if (width)` on a number are legal TypeScript but outside the
|
|
1402
|
+
// accepted subset; the engine represents conditions as booleans only.
|
|
1403
|
+
export function requireBooleanCondition(node: ts.Node, checker: ts.TypeChecker): void {
|
|
1404
|
+
const type = checker.getTypeAtLocation(node)
|
|
1405
|
+
const kind = valueKind(type, checker)
|
|
1406
|
+
if (kind === 'boolean') return
|
|
1407
|
+
throw unsupported(node, {
|
|
1408
|
+
kind: 'nonBooleanCondition',
|
|
1409
|
+
conditionKind: kind === 'number' ? 'number' : 'other',
|
|
1410
|
+
typeText: checker.typeToString(type),
|
|
1411
|
+
})
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function requireAccessedPropertyKind(access: ts.PropertyAccessExpression, checker: ts.TypeChecker): void {
|
|
1415
|
+
// An optional property reads as its maybe-undefined value: declared kinds wrap it in
|
|
1416
|
+
// the undefined sentinel and object literals fill omitted ones explicitly, so a record
|
|
1417
|
+
// value always carries every property its static type declares — there is always
|
|
1418
|
+
// something honest to read.
|
|
1419
|
+
const receiverType = checker.getTypeAtLocation(access.expression)
|
|
1420
|
+
// For an optional read the receiver includes the missing sentinels; the property lives
|
|
1421
|
+
// on the non-missing part, which getNonNullableType strips to.
|
|
1422
|
+
const presentType = access.questionDotToken != null ? checker.getNonNullableType(receiverType) : receiverType
|
|
1423
|
+
const property = checker.getPropertyOfType(presentType, access.name.text)
|
|
1424
|
+
// point.toString type-checks on every object literal, but the record value carries only
|
|
1425
|
+
// its own properties — an inherited prototype member has no honest answer. The
|
|
1426
|
+
// ownership test is the .d.ts rule: a property symbol declared only in declaration
|
|
1427
|
+
// files was not written by the project, and on a project record that means prototype.
|
|
1428
|
+
if (valueKind(presentType, checker) === 'object' && property != null && declaredOnlyInDeclarationFiles(property)) {
|
|
1429
|
+
throw unsupported(access, {kind: 'prototypeMemberRead', property: access.name.text})
|
|
1430
|
+
}
|
|
1431
|
+
const type = checker.getTypeAtLocation(access)
|
|
1432
|
+
if (valueKind(type, checker) != null) return
|
|
1433
|
+
throw unsupported(access, {kind: 'valueType', typeText: checker.typeToString(type)})
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function resolvedSymbol(symbol: ts.Symbol | undefined, checker: ts.TypeChecker): ts.Symbol | null {
|
|
1437
|
+
if (symbol == null) return null
|
|
1438
|
+
return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol)
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function isStandardMathObject(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
1442
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'Math') return false
|
|
1443
|
+
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function isStandardNumberObject(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
1447
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'Number') return false
|
|
1448
|
+
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function lowerElementAccess(access: ts.ElementAccessExpression, asserted: boolean, context: FunctionContext): ValueID {
|
|
1452
|
+
const receiverType = context.checker.getTypeAtLocation(access.expression)
|
|
1453
|
+
const receiverKind = valueKind(receiverType, context.checker)
|
|
1454
|
+
if (receiverKind !== 'array' && receiverKind !== 'tuple') {
|
|
1455
|
+
throw unsupported(access.expression, {kind: 'propertyReadOnNonObject', typeText: context.checker.typeToString(receiverType)})
|
|
1456
|
+
}
|
|
1457
|
+
const resultType = context.checker.getTypeAtLocation(access)
|
|
1458
|
+
if (valueKind(resultType, context.checker) == null) {
|
|
1459
|
+
throw unsupported(access, {kind: 'valueType', typeText: context.checker.typeToString(resultType)})
|
|
1460
|
+
}
|
|
1461
|
+
requireNumberType(access.argumentExpression, context.checker)
|
|
1462
|
+
const array = lowerExpression(access.expression, context)
|
|
1463
|
+
const index = lowerExpression(access.argumentExpression, context)
|
|
1464
|
+
const missingFlag = ts.TypeFlags.Undefined
|
|
1465
|
+
const staticTypeAllowsUndefined = (resultType.flags & missingFlag) !== 0
|
|
1466
|
+
|| (resultType.isUnion() && resultType.types.some(member => (member.flags & missingFlag) !== 0))
|
|
1467
|
+
return addInstruction(context, access, {
|
|
1468
|
+
kind: 'arrayIndex',
|
|
1469
|
+
array,
|
|
1470
|
+
index,
|
|
1471
|
+
mode: asserted ? 'asserted' : staticTypeAllowsUndefined ? 'bare' : 'bareUnchecked',
|
|
1472
|
+
})
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// A read of the union's tag property (`route.type` where route is one of several
|
|
1476
|
+
// shapes): the recognizer both the === form and the switch subject share. Returns the
|
|
1477
|
+
// union expression, or null when the expression is not a tag read.
|
|
1478
|
+
export function taggedUnionTagRead(expression: ts.Expression, context: FunctionContext): ts.Expression | null {
|
|
1479
|
+
const unwrapped = unwrap(expression, context.checker)
|
|
1480
|
+
if (!ts.isPropertyAccessExpression(unwrapped)) return null
|
|
1481
|
+
const objectType = context.checker.getTypeAtLocation(unwrapped.expression)
|
|
1482
|
+
if (valueKind(objectType, context.checker) !== 'taggedUnion' || !objectType.isUnion()) return null
|
|
1483
|
+
const tagProperty = taggedUnionProperty(objectType.types, context.checker)
|
|
1484
|
+
return tagProperty === unwrapped.name.text ? unwrapped.expression : null
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// route.type === 'lightbox' (and !==, the loose spellings, and result.ok === true): the
|
|
1488
|
+
// check consumes the union value directly and the branches narrow its variant list — the
|
|
1489
|
+
// same move the null checks make, pointed at the tag. The compared side must be a string
|
|
1490
|
+
// or boolean literal; comparing two tag reads to each other stays an unknown boolean
|
|
1491
|
+
// through the opaque path.
|
|
1492
|
+
function tagCheckComparison(expression: ts.BinaryExpression, context: FunctionContext): ValueID | null {
|
|
1493
|
+
const operator = expression.operatorToken.kind
|
|
1494
|
+
const equals = operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsToken
|
|
1495
|
+
const notEquals = operator === ts.SyntaxKind.ExclamationEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken
|
|
1496
|
+
if (!equals && !notEquals) return null
|
|
1497
|
+
const literalOf = (side: ts.Expression): string | boolean | null => {
|
|
1498
|
+
const unwrapped = unwrap(side, context.checker)
|
|
1499
|
+
if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) return unwrapped.text
|
|
1500
|
+
if (unwrapped.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
1501
|
+
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
1502
|
+
return null
|
|
1503
|
+
}
|
|
1504
|
+
const sides = [
|
|
1505
|
+
{union: taggedUnionTagRead(expression.left, context), literal: literalOf(expression.right)},
|
|
1506
|
+
{union: taggedUnionTagRead(expression.right, context), literal: literalOf(expression.left)},
|
|
1507
|
+
]
|
|
1508
|
+
for (const side of sides) {
|
|
1509
|
+
if (side.union != null && side.literal != null) {
|
|
1510
|
+
const union = lowerExpression(side.union, context)
|
|
1511
|
+
return addInstruction(context, expression, {kind: 'tagCheck', union, tagValue: side.literal, negated: notEquals})
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return null
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// `mode === 'compact'`: comparing two carried-without-claims values yields a boolean the
|
|
1518
|
+
// analysis knows nothing about — both branches stay analyzed, which is sound and keeps
|
|
1519
|
+
// string-keyed control flow from rejecting the function. A possibly-missing string
|
|
1520
|
+
// qualifies too (`mode === 'wide'` where mode is string | undefined): a missing value
|
|
1521
|
+
// simply compares unequal, so the unknown-boolean result stays sound without a null guard
|
|
1522
|
+
// first. The operands still lower, so an unsupported construct inside one rejects as
|
|
1523
|
+
// usual. This check runs AFTER missingSentinelCheck, so `mode === null` is already claimed
|
|
1524
|
+
// by the sentinel narrowing before either side is classified here.
|
|
1525
|
+
function opaqueEqualityCheck(expression: ts.BinaryExpression, context: FunctionContext): ValueID | null {
|
|
1526
|
+
const operator = expression.operatorToken.kind
|
|
1527
|
+
const isEquality = operator === ts.SyntaxKind.EqualsEqualsEqualsToken
|
|
1528
|
+
|| operator === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
|
1529
|
+
|| operator === ts.SyntaxKind.EqualsEqualsToken
|
|
1530
|
+
|| operator === ts.SyntaxKind.ExclamationEqualsToken
|
|
1531
|
+
if (!isEquality) return null
|
|
1532
|
+
const opaqueOrMissingOpaque = (side: ts.Expression): boolean => {
|
|
1533
|
+
const type = context.checker.getTypeAtLocation(side)
|
|
1534
|
+
const kind = valueKind(type, context.checker)
|
|
1535
|
+
if (kind === 'opaque') return true
|
|
1536
|
+
if (kind === 'nullable' && type.isUnion()) {
|
|
1537
|
+
const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1538
|
+
const rest = type.types.filter(member => (member.flags & missing) === 0)
|
|
1539
|
+
return rest.length >= 1 && rest.every(member => valueKind(member, context.checker) === 'opaque')
|
|
1540
|
+
}
|
|
1541
|
+
return false
|
|
1542
|
+
}
|
|
1543
|
+
if (!opaqueOrMissingOpaque(expression.left) || !opaqueOrMissingOpaque(expression.right)) return null
|
|
1544
|
+
lowerExpression(expression.left, context)
|
|
1545
|
+
lowerExpression(expression.right, context)
|
|
1546
|
+
return addInstruction(context, expression, {kind: 'unknownBoolean'})
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Recognizes `x === null`, `x !== undefined`, `x == null`, and friends. The loose forms
|
|
1550
|
+
// test both sentinels at once; the strict forms test one, and the refinement consults the
|
|
1551
|
+
// VALUE's own possible sentinels, so `x !== null` on a possibly-undefined value narrows
|
|
1552
|
+
// null away while undefined honestly survives.
|
|
1553
|
+
function missingSentinelCheck(expression: ts.BinaryExpression, context: FunctionContext): ValueID | null {
|
|
1554
|
+
const operator = expression.operatorToken.kind
|
|
1555
|
+
const strict = operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
|
1556
|
+
const loose = operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken
|
|
1557
|
+
if (!strict && !loose) return null
|
|
1558
|
+
const negated = operator === ts.SyntaxKind.ExclamationEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken
|
|
1559
|
+
const sentinelOf = (side: ts.Expression): 'null' | 'undefined' | null => {
|
|
1560
|
+
const unwrapped = unwrap(side, context.checker)
|
|
1561
|
+
if (unwrapped.kind === ts.SyntaxKind.NullKeyword) return 'null'
|
|
1562
|
+
if (isUndefinedGlobal(unwrapped, context.checker)) return 'undefined'
|
|
1563
|
+
return null
|
|
1564
|
+
}
|
|
1565
|
+
// `typeof x === 'undefined'` is the classic guard spelling; it is the undefined
|
|
1566
|
+
// sentinel check with the checked side inside the typeof.
|
|
1567
|
+
const isUndefinedString = (side: ts.Expression): boolean => {
|
|
1568
|
+
const unwrapped = unwrap(side, context.checker)
|
|
1569
|
+
return ts.isStringLiteral(unwrapped) && unwrapped.text === 'undefined'
|
|
1570
|
+
}
|
|
1571
|
+
if (ts.isTypeOfExpression(expression.left) && isUndefinedString(expression.right)) {
|
|
1572
|
+
const value = lowerExpression(expression.left.expression, context)
|
|
1573
|
+
return addInstruction(context, expression, {kind: 'nullishCheck', value, sentinel: 'undefined', negated})
|
|
1574
|
+
}
|
|
1575
|
+
if (ts.isTypeOfExpression(expression.right) && isUndefinedString(expression.left)) {
|
|
1576
|
+
const value = lowerExpression(expression.right.expression, context)
|
|
1577
|
+
return addInstruction(context, expression, {kind: 'nullishCheck', value, sentinel: 'undefined', negated})
|
|
1578
|
+
}
|
|
1579
|
+
// `typeof x === 'number'` (or 'string', or 'boolean') on a value whose only
|
|
1580
|
+
// non-missing kind matches the literal equals "x is not missing" — the common
|
|
1581
|
+
// narrowing spelling for number | undefined and friends.
|
|
1582
|
+
const primitiveTypeofFlags = (side: ts.Expression): ts.TypeFlags | null => {
|
|
1583
|
+
const unwrapped = unwrap(side, context.checker)
|
|
1584
|
+
if (!ts.isStringLiteral(unwrapped)) return null
|
|
1585
|
+
switch (unwrapped.text) {
|
|
1586
|
+
case 'number': return ts.TypeFlags.NumberLike
|
|
1587
|
+
case 'string': return ts.TypeFlags.StringLike
|
|
1588
|
+
case 'boolean': return ts.TypeFlags.BooleanLike
|
|
1589
|
+
default: return null
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const rightFlags = primitiveTypeofFlags(expression.right)
|
|
1593
|
+
const leftFlags = primitiveTypeofFlags(expression.left)
|
|
1594
|
+
const typeofSide = ts.isTypeOfExpression(expression.left) && rightFlags != null
|
|
1595
|
+
? {operand: expression.left, flags: rightFlags}
|
|
1596
|
+
: ts.isTypeOfExpression(expression.right) && leftFlags != null
|
|
1597
|
+
? {operand: expression.right, flags: leftFlags}
|
|
1598
|
+
: null
|
|
1599
|
+
if (typeofSide != null) {
|
|
1600
|
+
const operandType = context.checker.getTypeAtLocation(typeofSide.operand.expression)
|
|
1601
|
+
// The TYPE FLAGS decide, not the analyzer kind: unknown classifies opaque like
|
|
1602
|
+
// strings do, but typeof unknown === 'string' is genuinely unknown — treating the
|
|
1603
|
+
// kinds as equivalent would answer it definitely-true. Only when every non-missing
|
|
1604
|
+
// member's flags name the checked primitive does the check translate to
|
|
1605
|
+
// "not missing"; everything else (unknown operands, mixed unions) answers an
|
|
1606
|
+
// unknown boolean — typeof is an effect-free operator, so both branches analyzing
|
|
1607
|
+
// is always sound.
|
|
1608
|
+
const missing = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1609
|
+
const members = operandType.isUnion() ? operandType.types : [operandType]
|
|
1610
|
+
const restMatches = members.every(member =>
|
|
1611
|
+
(member.flags & missing) !== 0 || (member.flags & typeofSide.flags) !== 0)
|
|
1612
|
+
&& members.some(member => (member.flags & missing) === 0)
|
|
1613
|
+
const value = lowerExpression(typeofSide.operand.expression, context)
|
|
1614
|
+
if (restMatches) {
|
|
1615
|
+
return addInstruction(context, expression, {kind: 'nullishCheck', value, sentinel: 'nullish', negated: !negated})
|
|
1616
|
+
}
|
|
1617
|
+
return addInstruction(context, expression, {kind: 'unknownBoolean'})
|
|
1618
|
+
}
|
|
1619
|
+
const leftSentinel = sentinelOf(expression.left)
|
|
1620
|
+
const rightSentinel = sentinelOf(expression.right)
|
|
1621
|
+
const sentinel = leftSentinel ?? rightSentinel
|
|
1622
|
+
if (sentinel == null || (leftSentinel != null && rightSentinel != null)) return null
|
|
1623
|
+
const checked = leftSentinel == null ? expression.left : expression.right
|
|
1624
|
+
// The checked side must be a kind the analysis represents: `voidCall() == null` is TRUE
|
|
1625
|
+
// at runtime (a void function returns undefined), but the void abstract value carries no
|
|
1626
|
+
// sentinel, so admitting it would prune the wrong branch. A pure-sentinel type
|
|
1627
|
+
// (`null | undefined`, after an outer `== null` narrowed everything else away) is fine:
|
|
1628
|
+
// the abstract value carries exactly those sentinels.
|
|
1629
|
+
const checkedType = context.checker.getTypeAtLocation(checked)
|
|
1630
|
+
const missingFlags = ts.TypeFlags.Null | ts.TypeFlags.Undefined
|
|
1631
|
+
const pureSentinel = (checkedType.flags & missingFlags) !== 0
|
|
1632
|
+
|| (checkedType.isUnion() && checkedType.types.every(member => (member.flags & missingFlags) !== 0))
|
|
1633
|
+
if (!pureSentinel && valueKind(checkedType, context.checker) == null) {
|
|
1634
|
+
throw unsupported(checked, {kind: 'valueType', typeText: context.checker.typeToString(checkedType)})
|
|
1635
|
+
}
|
|
1636
|
+
const value = lowerExpression(checked, context)
|
|
1637
|
+
return addInstruction(context, expression, {
|
|
1638
|
+
kind: 'nullishCheck',
|
|
1639
|
+
value,
|
|
1640
|
+
sentinel: loose ? 'nullish' : sentinel,
|
|
1641
|
+
negated,
|
|
1642
|
+
})
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function isGlobalInfinity(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
1646
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'Infinity') return false
|
|
1647
|
+
return declaredOnlyInDeclarationFiles(checker.getSymbolAtLocation(expression))
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// Resolves an identifier read: a function-local binding first, then a module binding
|
|
1651
|
+
// (which reads the binding's slot), then the global `Infinity` as an exact constant,
|
|
1652
|
+
// else the identifier is unknown. A local or module binding named Infinity has a
|
|
1653
|
+
// different symbol and wins above; the global check is the same declaration-file
|
|
1654
|
+
// defense the Math and platform dispatches use.
|
|
1655
|
+
function identifierValue(symbol: ts.Symbol, node: ts.Identifier, context: FunctionContext): ValueID {
|
|
1656
|
+
const local = context.bindings.get(symbol)
|
|
1657
|
+
if (local != null) return local
|
|
1658
|
+
const binding = context.moduleBindingsBySymbol.get(symbol)
|
|
1659
|
+
if (binding != null) return addInstruction(context, node, {kind: 'moduleRead', binding})
|
|
1660
|
+
if (isGlobalInfinity(node, context.checker)) {
|
|
1661
|
+
return addInstruction(context, node, {kind: 'constant', value: Number.POSITIVE_INFINITY})
|
|
1662
|
+
}
|
|
1663
|
+
if (isUndefinedGlobal(node, context.checker)) {
|
|
1664
|
+
return addInstruction(context, node, {kind: 'nullishConstant', sentinel: 'undefined'})
|
|
1665
|
+
}
|
|
1666
|
+
throw unsupported(node, {kind: 'unknownIdentifier', name: node.text})
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Assigns an identifier: rebinding for a local, a slot write for a module binding.
|
|
1670
|
+
function assignIdentifier(
|
|
1671
|
+
symbol: ts.Symbol,
|
|
1672
|
+
node: ts.Identifier,
|
|
1673
|
+
value: ValueID,
|
|
1674
|
+
wholeExpression: ts.Expression,
|
|
1675
|
+
context: FunctionContext,
|
|
1676
|
+
): ValueID {
|
|
1677
|
+
if (context.bindings.has(symbol)) {
|
|
1678
|
+
context.bindings.set(symbol, value)
|
|
1679
|
+
return value
|
|
1680
|
+
}
|
|
1681
|
+
const binding = context.moduleBindingsBySymbol.get(symbol)
|
|
1682
|
+
if (binding != null) return addInstruction(context, wholeExpression, {kind: 'moduleWrite', binding, value})
|
|
1683
|
+
throw unsupported(node, {kind: 'unknownIdentifier', name: node.text})
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function propertyName(name: ts.PropertyName): string {
|
|
1687
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text
|
|
1688
|
+
throw unsupported(name, {kind: 'computedPropertyName'})
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function unwrap(expression: ts.Expression, checker: ts.TypeChecker): ts.Expression {
|
|
1692
|
+
let current = expression
|
|
1693
|
+
while (true) {
|
|
1694
|
+
if (ts.isParenthesizedExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
1695
|
+
// Neither changes the expression's type.
|
|
1696
|
+
current = current.expression
|
|
1697
|
+
continue
|
|
1698
|
+
}
|
|
1699
|
+
// Only `as const` peels: TypeScript permits it solely on literals and it narrows the
|
|
1700
|
+
// literal to its own literal type, so the value kind provably cannot change. Every
|
|
1701
|
+
// other as/angle assertion is an erasure point (see the as/angle arm in
|
|
1702
|
+
// lowerExpression) — an assertion is exactly where the checker's word and the runtime
|
|
1703
|
+
// value may diverge, and claim-free is the one honest reading. Three review rounds
|
|
1704
|
+
// settled this: each attempt to LICENSE carrying (asserted kind matches operand kind;
|
|
1705
|
+
// then recursive type-shape comparisons) was defeated by another diagnostic-clean aliasing
|
|
1706
|
+
// route (`true as {} as number` via comparability, `flags as unknown[] as number[]`
|
|
1707
|
+
// at the element level, optional-property and heterogeneous-union comparison
|
|
1708
|
+
// collisions). No type-level test can be finer than TypeScript's own cast
|
|
1709
|
+
// permissiveness, so the license is gone rather than repaired again.
|
|
1710
|
+
if ((ts.isAsExpression(current) || ts.isTypeAssertionExpression(current))
|
|
1711
|
+
&& ts.isConstTypeReference(current.type)) {
|
|
1712
|
+
current = current.expression
|
|
1713
|
+
continue
|
|
1714
|
+
}
|
|
1715
|
+
// The non-null assertion `x!` peels only while the value kind is unchanged underneath —
|
|
1716
|
+
// on a nullable type, e.g. `x!` with `x: number | null`, the static type stops
|
|
1717
|
+
// describing the value the analysis models, so stop. The one blessed kind-changing
|
|
1718
|
+
// form is `arr[i]!`: the syntax itself requests asserted-read treatment — an in-bounds
|
|
1719
|
+
// assumption line, or a bounds proof when the loop supplies one. Bare reads carry
|
|
1720
|
+
// possible undefined in the engine regardless of the project's TypeScript options.
|
|
1721
|
+
if (ts.isNonNullExpression(current)) {
|
|
1722
|
+
const assertedType = checker.getTypeAtLocation(current)
|
|
1723
|
+
const operandType = checker.getTypeAtLocation(current.expression)
|
|
1724
|
+
// The one blessed kind-changing assertion is `arr[i]!`: the asserted read gets its
|
|
1725
|
+
// explicit treatment in lowering — an in-bounds assumption line, or a bounds proof
|
|
1726
|
+
// when a loop supplies one. It stays wrapped so lowering can see the assertion.
|
|
1727
|
+
if (ts.isElementAccessExpression(current.expression) && valueKind(assertedType, checker) != null) {
|
|
1728
|
+
return current
|
|
1729
|
+
}
|
|
1730
|
+
if (valueKind(assertedType, checker) !== valueKind(operandType, checker)) {
|
|
1731
|
+
throw unsupported(current, {
|
|
1732
|
+
kind: 'kindChangingAssertion',
|
|
1733
|
+
fromText: checker.typeToString(operandType),
|
|
1734
|
+
toText: checker.typeToString(assertedType),
|
|
1735
|
+
})
|
|
1736
|
+
}
|
|
1737
|
+
current = current.expression
|
|
1738
|
+
continue
|
|
1739
|
+
}
|
|
1740
|
+
return current
|
|
1741
|
+
}
|
|
1742
|
+
}
|