@formaui-net/nexo 0.0.0 → 1.0.0-preview.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +148 -6
- package/package.json +23 -3
- package/src/common/ArrayUtils.mjs +16 -0
- package/src/common/ObjectUtils.mjs +33 -0
- package/src/language/DefinitionLocations.mjs +41 -0
- package/src/language/DefinitionValidator.mjs +1265 -0
- package/src/language/DefinitionWriter.mjs +475 -0
- package/src/language/Dictionary.mjs +52 -0
- package/src/language/Evaluator.mjs +3480 -0
- package/src/language/ImportedVocabulary.mjs +137 -0
- package/src/language/Imports.mjs +141 -0
- package/src/language/Index.mjs +29 -0
- package/src/language/LanguageResolver.mjs +4068 -0
- package/src/language/Limits.mjs +78 -0
- package/src/language/LineStructure.mjs +385 -0
- package/src/language/Lowering.mjs +453 -0
- package/src/language/MemberSelector.mjs +89 -0
- package/src/language/Names.mjs +65 -0
- package/src/language/NexoError.mjs +145 -0
- package/src/language/Pair.mjs +21 -0
- package/src/language/ParameterBindings.mjs +361 -0
- package/src/language/ParsedTexts.mjs +62 -0
- package/src/language/Parser.mjs +1817 -0
- package/src/language/Range.mjs +51 -0
- package/src/language/RecentlyUsed.mjs +50 -0
- package/src/language/ResolvedDefinition.mjs +219 -0
- package/src/language/ResultPath.mjs +101 -0
- package/src/language/ScopeLocals.mjs +102 -0
- package/src/language/StructureTypes.mjs +48 -0
- package/src/language/SyntaxTree.mjs +203 -0
- package/src/language/TextLocations.mjs +185 -0
- package/src/language/TextPositions.mjs +23 -0
- package/src/language/Tokenizer.mjs +917 -0
- package/src/language/Type.mjs +401 -0
- package/src/language/TypeChecker.mjs +449 -0
- package/src/language/ValueCapture.mjs +315 -0
- package/src/language/Versions.mjs +14 -0
- package/src/language/vocabulary/Declarations.mjs +64 -0
- package/src/language/vocabulary/ItemPositions.mjs +48 -0
- package/src/language/vocabulary/ListOrdering.mjs +145 -0
- package/src/language/vocabulary/MemberFailure.mjs +21 -0
- package/src/language/vocabulary/NexoModule.mjs +1119 -0
- package/src/language/vocabulary/NexoRegistry.mjs +1540 -0
- package/src/language/vocabulary/TypeNameScope.mjs +63 -0
- package/src/language/vocabulary/Vocabulary.mjs +640 -0
- package/src/nexo.d.ts +743 -0
- package/src/nexo.js +338 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeChecker — every relation between two types: assignability, merging, the types an equality compares, and the receivers a member applies to, read from what declares the types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from './Type.mjs';
|
|
6
|
+
import { NexoModule } from './vocabulary/NexoModule.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The answers to whether a value of one type is assignable to another: assignable, assignable once its value is checked when it is evaluated, as a value of `Any` is, or refused.
|
|
10
|
+
* @typedef {'assignable' | 'checked' | 'refused'} Assignability
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class TypeChecker {
|
|
14
|
+
/**
|
|
15
|
+
* The registry that declares the types the checker relates, of which it reads these four methods: a value type's declaration, a generic one's variances among it, a reference type's supertype, whether a type is a
|
|
16
|
+
* reference type, and a member's receiver types.
|
|
17
|
+
* @type {{ getTypeDeclaration(type: Type): { kind: string, variances?: readonly ('covariant' | 'invariant')[] } | undefined, getSupertype(type: Type): Type | null, isReferenceType(type: Type): boolean, getReceiverTypes(declaration: object): Type[] }}
|
|
18
|
+
*/
|
|
19
|
+
#registry;
|
|
20
|
+
|
|
21
|
+
/** @param {object} registry The registry that declares the types the checker relates. */
|
|
22
|
+
constructor(registry) {
|
|
23
|
+
this.#registry = registry;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a value of the type is assignable to the declared type: `assignable`, `refused`, or `checked`, assignable once its value is checked against the declared
|
|
28
|
+
* type when it is evaluated, which a value of `Any` asks for, the check refusing null where the declared type isn't nullable.
|
|
29
|
+
* @param {Type} type
|
|
30
|
+
* @param {Type} declaredType
|
|
31
|
+
* @returns {Assignability}
|
|
32
|
+
*/
|
|
33
|
+
assignabilityOf(type, declaredType) {
|
|
34
|
+
if (type.kind === 'never') {
|
|
35
|
+
return 'assignable';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (type.kind === 'null') {
|
|
39
|
+
return declaredType.isNullable ? 'assignable' : 'refused';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A value of `Any` waits for its value, whatever is declared but `Any` itself, whose nullability the value's must fit; no value is a never, and only a nullable one is null.
|
|
43
|
+
if (type.kind === 'any') {
|
|
44
|
+
if (declaredType.kind === 'never' || (declaredType.kind === 'null' && !type.isNullable)) {
|
|
45
|
+
return 'refused';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return declaredType.kind === 'any' && (declaredType.isNullable || !type.isNullable) ? 'assignable' : 'checked';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (type.isNullable && !declaredType.isNullable) {
|
|
52
|
+
return 'refused';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (declaredType.kind === 'any') {
|
|
56
|
+
return 'assignable';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (type.kind !== declaredType.kind) {
|
|
60
|
+
return 'refused';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (type.kind) {
|
|
64
|
+
case 'named':
|
|
65
|
+
return this.#isSameOrSupertypeByName(declaredType, type) ? this.#getTypeArgumentsAssignability(type, declaredType) : 'refused';
|
|
66
|
+
case 'object':
|
|
67
|
+
return TypeChecker.#holdsFieldsOf(type, declaredType) ? TypeChecker.#combineAssignabilities([...type.fieldTypes].map(([name, fieldType]) => this.assignabilityOf(fieldType, declaredType.fieldTypes.get(name)))) : 'refused';
|
|
68
|
+
// An object root stands where one of its object expression reads the fields it does, and a function reference where one that may be any of its functions does.
|
|
69
|
+
case 'objectRoot':
|
|
70
|
+
return type.toNonNullable().equals(declaredType.toNonNullable()) ? 'assignable' : 'refused';
|
|
71
|
+
case 'function':
|
|
72
|
+
return type.functions.every(candidate => declaredType.functions.includes(candidate)) ? 'assignable' : 'refused';
|
|
73
|
+
default:
|
|
74
|
+
return type.name === declaredType.name ? 'assignable' : 'refused';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether a value of the type is proven assignable to the declared type; one that waits for a check is not, so a caller that records no check refuses it. */
|
|
79
|
+
isAssignable(type, declaredType) {
|
|
80
|
+
return this.assignabilityOf(type, declaredType) === 'assignable';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The least type holding a value of either type, nullable where either can be null; null where the two don't merge.
|
|
85
|
+
* @param {Type} firstType
|
|
86
|
+
* @param {Type} secondType
|
|
87
|
+
* @returns {Type | null}
|
|
88
|
+
*/
|
|
89
|
+
merge(firstType, secondType) {
|
|
90
|
+
if (firstType.kind === 'never') {
|
|
91
|
+
return secondType;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (secondType.kind === 'never') {
|
|
95
|
+
return firstType;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (firstType.kind === 'null') {
|
|
99
|
+
return secondType.toNullable();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (secondType.kind === 'null') {
|
|
103
|
+
return firstType.toNullable();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isNullable = firstType.isNullable || secondType.isNullable;
|
|
107
|
+
|
|
108
|
+
// Object roots merge where they identify one object expression and read the same fields, and with nothing else, `Any` included, which holds values alone.
|
|
109
|
+
if (firstType.kind === 'objectRoot' || secondType.kind === 'objectRoot') {
|
|
110
|
+
return firstType.toNonNullable().equals(secondType.toNonNullable()) ? (isNullable ? firstType.toNullable() : firstType.toNonNullable()) : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Function types merge into one that may be any of their functions, and with nothing else, `Any` included.
|
|
114
|
+
if (firstType.kind === 'function' || secondType.kind === 'function') {
|
|
115
|
+
return firstType.kind === secondType.kind ? Type.createFunctionType([...firstType.functions, ...secondType.functions], isNullable) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `Any` holds a value of any type.
|
|
119
|
+
if (firstType.kind === 'any' || secondType.kind === 'any') {
|
|
120
|
+
return isNullable ? Type.any.toNullable() : Type.any;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Two reference types of one hierarchy merge to the nearest supertype they share, and of two hierarchies to none.
|
|
124
|
+
if (firstType.kind === 'named' && secondType.kind === 'named' && firstType.identity !== secondType.identity && this.#registry.isReferenceType(firstType) && this.#registry.isReferenceType(secondType)) {
|
|
125
|
+
const shared = this.#lineageOf(firstType).find(ancestor => this.#lineageOf(secondType).some(other => other.identity === ancestor.identity));
|
|
126
|
+
|
|
127
|
+
return shared === undefined ? null : isNullable ? shared.toNullable() : shared.toNonNullable();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (firstType.kind !== secondType.kind || firstType.name !== secondType.name || firstType.moduleName !== secondType.moduleName) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (firstType.kind === 'object') {
|
|
135
|
+
if (!TypeChecker.#holdsFieldsOf(firstType, secondType)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fieldTypes = new Map();
|
|
140
|
+
|
|
141
|
+
for (const [name, fieldType] of firstType.fieldTypes) {
|
|
142
|
+
const merged = this.merge(fieldType, secondType.fieldTypes.get(name));
|
|
143
|
+
|
|
144
|
+
if (merged === null) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fieldTypes.set(name, merged);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Type.createObjectType(fieldTypes, isNullable);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (firstType.typeArguments.length > 0) {
|
|
155
|
+
const typeArguments = this.#mergeTypeArguments(firstType, secondType);
|
|
156
|
+
|
|
157
|
+
return typeArguments === null ? null : new Type(firstType.kind, { name: firstType.name, moduleName: firstType.moduleName, typeArguments, isNullable });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return isNullable ? firstType.toNullable() : firstType.toNonNullable();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether two values an equality compares are of types that fit one way or the other, each proven assignable to the other's. */
|
|
164
|
+
fitsEitherWay(firstType, secondType) {
|
|
165
|
+
return this.isAssignable(firstType, secondType) || this.isAssignable(secondType, firstType);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether values of the type are compared with each other by an equality, rather than with null alone: strings, numbers and Booleans by their values, and references by their identity. */
|
|
169
|
+
isComparable(type) {
|
|
170
|
+
return type.kind === 'named' && ((type.moduleName === null && NexoModule.scalarTypeNames.includes(type.name)) || this.#registry.isReferenceType(type));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Whether the member applies to the receiver, by one of its receiver types, as inferring its receiver's type arguments succeeds; it records nothing. */
|
|
174
|
+
matchesReceiver(declaration, receiverType) {
|
|
175
|
+
return this.inferReceiverTypeArguments(declaration, receiverType) !== null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The member's receiver type where the receiver applies to it by that type: a type, or a type parameter alone, which takes a receiver of any type a value has; undefined where it does not apply, a module member's
|
|
180
|
+
* among them, which takes no receiver.
|
|
181
|
+
* @returns {Type | undefined}
|
|
182
|
+
*/
|
|
183
|
+
resolveApplyingReceiverType(declaration, receiverType) {
|
|
184
|
+
const base = receiverType.toNonNullable();
|
|
185
|
+
const isValue = base.kind === 'named' || base.kind === 'object';
|
|
186
|
+
|
|
187
|
+
return this.#registry.getReceiverTypes(declaration).find(type => (type.kind !== 'typeParameter' || isValue) && this.matchesType(type, base));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The arguments of the type parameters the member's receiver type binds for the receiver: an empty map where it binds none, or null where the member does not apply to the receiver.
|
|
192
|
+
* @returns {Map<string, Type> | null}
|
|
193
|
+
*/
|
|
194
|
+
inferReceiverTypeArguments(declaration, receiverType) {
|
|
195
|
+
const type = this.resolveApplyingReceiverType(declaration, receiverType);
|
|
196
|
+
|
|
197
|
+
return type === undefined ? null : this.inferTypeArguments(type, receiverType.toNonNullable());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The applicable declaration of those given whose receiver type is more specific than every other applicable declaration's, alone, as `Light` is than `Device` and `List<Light>` than `List<Device>`, a type parameter
|
|
202
|
+
* that takes a receiver of any type the least specific: a type's member hides one of its name its supertype declares. Where no declaration is, it answers the competing ones, each applicable and none less specific than another, which make the access ambiguous;
|
|
203
|
+
* none where none applies.
|
|
204
|
+
*/
|
|
205
|
+
mostSpecificOf(declarations, receiverType) {
|
|
206
|
+
const applying = declarations.map(declaration => ({ declaration, type: this.resolveApplyingReceiverType(declaration, receiverType) })).filter(({ type }) => type !== undefined);
|
|
207
|
+
const selected = applying.find(candidate => applying.every(other => other === candidate || this.#isMoreSpecific(candidate.type, other.type)));
|
|
208
|
+
|
|
209
|
+
if (selected !== undefined) {
|
|
210
|
+
return [selected.declaration];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return applying.filter(({ type }) => !applying.some(other => this.#isMoreSpecific(other.type, type))).map(({ declaration }) => declaration);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Whether the one receiver type is more specific than the other: assignable to it by the type relationships, the other matching it and not it the other, so a type parameter, which matches every type, is the least specific. */
|
|
217
|
+
#isMoreSpecific(one, other) {
|
|
218
|
+
return this.matchesType(other, one) && !this.matchesType(one, other);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Whether the actual type matches the declared one, structure for structure, as inferring the declared type's parameters from it succeeds; it records nothing. */
|
|
222
|
+
matchesType(declaredType, actualType) {
|
|
223
|
+
return this.inferTypeArguments(declaredType, actualType) !== null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The arguments of the type parameters the declared type writes, inferred from the actual type, structure for structure, beside those bound already: an empty map where it writes none, or null where the actual type does not
|
|
228
|
+
* match. A type parameter matches any type the first time it is met, and binds to it; met again, or bound already, it matches the type it binds to, nullability aside where it stands alone, its caller's to apply.
|
|
229
|
+
* Within a type argument a type that can be null matches only a declared one that can be, as a `List<Number?>` matches no `List<Number>`, and a parameter met again there matches its binding null and all, so a
|
|
230
|
+
* `Pair<T, T>` matches no `Pair<Number, Number?>`; the actual type's own null is its caller's, which a receiver's `?.` handles.
|
|
231
|
+
* @param {Type} declaredType
|
|
232
|
+
* @param {Type} actualType
|
|
233
|
+
* @param {ReadonlyMap<string, Type>} [bound] The arguments bound already, by parameter name, which it never changes.
|
|
234
|
+
* @returns {Map<string, Type> | null}
|
|
235
|
+
*/
|
|
236
|
+
inferTypeArguments(declaredType, actualType, bound = new Map()) {
|
|
237
|
+
const typeArguments = new Map(bound);
|
|
238
|
+
|
|
239
|
+
return this.#inferInto(declaredType, actualType, typeArguments) ? typeArguments : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Adds to the evidence the actual type gives each type parameter the declared type writes, by exact extraction, structure for structure: a parameter written `T` is given the type as it is, and one written `T?` the
|
|
244
|
+
* type without null; a null is evidence that its occurrence accepts null, kept with whether that occurrence is written `T?`, and a never, the item of an empty list, is evidence of no type. A part of the actual type
|
|
245
|
+
* whose structure the declared one doesn't share gives nothing, since checking the argument against its binding refuses it apart.
|
|
246
|
+
* @param {Type} declaredType
|
|
247
|
+
* @param {Type} actualType
|
|
248
|
+
* @param {Map<string, ({ kind: 'type', type: Type } | { kind: 'null', isNullableOccurrence: boolean } | { kind: 'never' })[]>} evidence The evidence by parameter name, which it adds to.
|
|
249
|
+
*/
|
|
250
|
+
extractTypeArgumentEvidence(declaredType, actualType, evidence) {
|
|
251
|
+
const add = entry => evidence.set(declaredType.name, [...(evidence.get(declaredType.name) ?? []), entry]);
|
|
252
|
+
|
|
253
|
+
if (declaredType.kind === 'typeParameter') {
|
|
254
|
+
if (declaredType.name === '@selected') {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (actualType.kind === 'never') {
|
|
259
|
+
add({ kind: 'never' });
|
|
260
|
+
} else if (actualType.kind === 'null') {
|
|
261
|
+
add({ kind: 'null', isNullableOccurrence: declaredType.isNullable });
|
|
262
|
+
} else {
|
|
263
|
+
add({ kind: 'type', type: declaredType.isNullable ? actualType.toNonNullable() : actualType });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (declaredType.kind === 'object' && actualType.kind === 'object') {
|
|
270
|
+
for (const [name, fieldType] of declaredType.fieldTypes) {
|
|
271
|
+
const actualField = actualType.getObjectFieldType(name);
|
|
272
|
+
|
|
273
|
+
if (actualField !== null) {
|
|
274
|
+
this.extractTypeArgumentEvidence(fieldType, actualField, evidence);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (declaredType.kind === 'named' && actualType.kind === 'named' && declaredType.identity === actualType.identity && declaredType.typeArguments.length === actualType.typeArguments.length) {
|
|
282
|
+
declaredType.typeArguments.forEach((typeArgument, index) => this.extractTypeArgumentEvidence(typeArgument, actualType.typeArguments[index], evidence));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The type a type parameter binds to from the evidence its sources gave it: the one type its candidates are, made nullable where one is nullable or an occurrence written `T` was given null, so a `Device` and a
|
|
288
|
+
* `Device?` give `Device?`; `null` where an occurrence written `T` was given null alone; and `Never` where it was given only nevers, or only nulls at occurrences written `T?`, so that `T?` is `null`. It answers the
|
|
289
|
+
* candidates where they are of different types, which no binding reconciles, whichever came first, and null where there is no evidence.
|
|
290
|
+
* @returns {{ type: Type } | { candidates: Type[] } | null}
|
|
291
|
+
*/
|
|
292
|
+
resolveTypeArgument(evidence) {
|
|
293
|
+
const types = evidence.filter(entry => entry.kind === 'type').map(entry => entry.type);
|
|
294
|
+
const acceptsNull = evidence.some(entry => entry.kind === 'null' && !entry.isNullableOccurrence);
|
|
295
|
+
|
|
296
|
+
if (types.length > 0) {
|
|
297
|
+
const base = types[0].toNonNullable();
|
|
298
|
+
|
|
299
|
+
if (types.some(type => !type.toNonNullable().equals(base))) {
|
|
300
|
+
return { candidates: types.filter((type, index) => types.findIndex(other => other.equals(type)) === index) };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { type: acceptsNull || types.some(type => type.isNullable) ? base.toNullable() : base };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (acceptsNull) {
|
|
307
|
+
return { type: Type.null };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return evidence.length > 0 ? { type: Type.never } : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Matches the actual type against the declared one, adding each argument it binds to the map: the one relation matching and inferring share. Binding is exact: a parameter written `T` binds the type as it is, and one
|
|
315
|
+
* written `T?` the type without null, since the `?` it is written with gives it that.
|
|
316
|
+
*/
|
|
317
|
+
#inferInto(declaredType, actualType, typeArguments, isNested = false) {
|
|
318
|
+
if (declaredType.kind === 'typeParameter') {
|
|
319
|
+
const typeArgument = typeArguments.get(declaredType.name);
|
|
320
|
+
|
|
321
|
+
if (typeArgument === undefined) {
|
|
322
|
+
typeArguments.set(declaredType.name, declaredType.isNullable ? actualType.toNonNullable() : actualType);
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Within a type argument a parameter written alone holds what it bound, null and all; one written nullable, or one standing alone, takes its argument with null or without.
|
|
328
|
+
return isNested && !declaredType.isNullable ? typeArgument.equals(actualType) : typeArgument.toNonNullable().equals(actualType.toNonNullable());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// No value is a never, so a never matches whatever is declared, and binds to a never each type parameter written there that nothing bound yet, as `[].flatten` binds T.
|
|
332
|
+
if (actualType.kind === 'never') {
|
|
333
|
+
TypeChecker.#bindToNever(declaredType, typeArguments);
|
|
334
|
+
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (declaredType.kind !== actualType.kind || !this.#isSameOrSupertypeByName(declaredType, actualType)) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return declaredType.typeArguments.length === actualType.typeArguments.length && declaredType.typeArguments.every((typeArgument, index) => {
|
|
343
|
+
const actualArgument = actualType.typeArguments[index];
|
|
344
|
+
|
|
345
|
+
// A type argument that can be null fits only one declared nullable, or a type parameter, which binds the null with it.
|
|
346
|
+
if (actualArgument.isNullable && !typeArgument.isNullable && typeArgument.kind !== 'typeParameter') {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return this.#inferInto(typeArgument, actualArgument, typeArguments, true);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Binds to a never each type parameter the declared type writes, at any depth, that nothing bound yet. */
|
|
355
|
+
static #bindToNever(declaredType, typeArguments) {
|
|
356
|
+
if (declaredType.kind === 'typeParameter') {
|
|
357
|
+
if (!typeArguments.has(declaredType.name)) {
|
|
358
|
+
typeArguments.set(declaredType.name, Type.never);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
declaredType.typeArguments.forEach(typeArgument => TypeChecker.#bindToNever(typeArgument, typeArguments));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Whether the declared type is the actual type or one of its supertypes by name: the same named type, by its module-qualified identity, or a named type the actual type's declared supertype chain reaches. A local name that
|
|
369
|
+
* matches alone never makes two types compatible, and their type arguments are checked apart.
|
|
370
|
+
*/
|
|
371
|
+
#isSameOrSupertypeByName(declaredType, actualType) {
|
|
372
|
+
if (declaredType.name === actualType.name && declaredType.moduleName === actualType.moduleName) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return declaredType.kind === 'named' && actualType.kind === 'named' && this.#lineageOf(actualType).some(ancestor => ancestor.identity === declaredType.identity);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The reference type and each supertype above it, nearest first, each without null; the type alone for any other type. */
|
|
380
|
+
#lineageOf(type) {
|
|
381
|
+
const lineage = [];
|
|
382
|
+
|
|
383
|
+
for (let current = type.toNonNullable(); current !== null; current = this.#registry.getSupertype(current)) {
|
|
384
|
+
lineage.push(current.toNonNullable());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return lineage;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Gets the assignability of two types of one generic declaration, or of none, argument by argument, a covariant one by assignability and an invariant one by equality: refused where an argument is refused, checked where one
|
|
392
|
+
* waits for its value, and assignable where every one is. An invariant argument of a never fits any, since the one invariant parameter, a dictionary's key, is a never only for a dictionary of no entries.
|
|
393
|
+
*/
|
|
394
|
+
#getTypeArgumentsAssignability(type, declaredType) {
|
|
395
|
+
if (type.typeArguments.length === 0) {
|
|
396
|
+
return 'assignable';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const { variances } = this.#registry.getTypeDeclaration(declaredType);
|
|
400
|
+
|
|
401
|
+
return TypeChecker.#combineAssignabilities(type.typeArguments.map((typeArgument, index) => {
|
|
402
|
+
const declaredArgument = declaredType.typeArguments[index];
|
|
403
|
+
|
|
404
|
+
if (variances[index] === 'covariant') {
|
|
405
|
+
return this.assignabilityOf(typeArgument, declaredArgument);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return typeArgument.kind === 'never' || typeArgument.equals(declaredArgument) ? 'assignable' : 'refused';
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The invariant argument two arguments merge to: the one where they are equal, the other where one is a never, and none otherwise. */
|
|
413
|
+
static #mergeInvariant(one, other) {
|
|
414
|
+
if (one.kind === 'never') {
|
|
415
|
+
return other;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return other.kind === 'never' || one.equals(other) ? one : null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The type arguments two types of one generic declaration merge to, each a covariant one's merge and an invariant one's where they are equal or one is a never, the other; null where one doesn't merge. */
|
|
422
|
+
#mergeTypeArguments(firstType, secondType) {
|
|
423
|
+
const { variances } = this.#registry.getTypeDeclaration(firstType);
|
|
424
|
+
const typeArguments = [];
|
|
425
|
+
|
|
426
|
+
for (const [index, typeArgument] of firstType.typeArguments.entries()) {
|
|
427
|
+
const other = secondType.typeArguments[index];
|
|
428
|
+
const merged = variances[index] === 'covariant' ? this.merge(typeArgument, other) : TypeChecker.#mergeInvariant(typeArgument, other);
|
|
429
|
+
|
|
430
|
+
if (merged === null) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
typeArguments.push(merged);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return typeArguments;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Whether two object types hold the same field names, none more and none fewer. */
|
|
441
|
+
static #holdsFieldsOf(type, otherType) {
|
|
442
|
+
return type.fieldTypes.size === otherType.fieldTypes.size && [...type.fieldTypes.keys()].every(name => otherType.fieldTypes.has(name));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Combines the assignability its parts answer into a type's own, by precedence: refused where one is, then checked where one waits for evaluation, and assignable where every one is. */
|
|
446
|
+
static #combineAssignabilities(answers) {
|
|
447
|
+
return answers.includes('refused') ? 'refused' : answers.includes('checked') ? 'checked' : 'assignable';
|
|
448
|
+
}
|
|
449
|
+
}
|