@velarscript/compiler 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advisory-suppression.d.ts +62 -0
- package/dist/advisory-suppression.d.ts.map +1 -0
- package/dist/advisory-suppression.js +179 -0
- package/dist/advisory-suppression.js.map +1 -0
- package/dist/analyzer.d.ts +405 -17
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +1755 -211
- package/dist/analyzer.js.map +1 -1
- package/dist/ast.d.ts +6 -3
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/binding-stability.d.ts +22 -0
- package/dist/binding-stability.d.ts.map +1 -0
- package/dist/binding-stability.js +63 -0
- package/dist/binding-stability.js.map +1 -0
- package/dist/collection-lowering-runtime.d.ts +1 -1
- package/dist/collection-lowering-runtime.d.ts.map +1 -1
- package/dist/collection-lowering-runtime.js +183 -56
- package/dist/collection-lowering-runtime.js.map +1 -1
- package/dist/diagnostic.d.ts +34 -0
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +11 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/embedded-javascript.d.ts +12 -1
- package/dist/embedded-javascript.d.ts.map +1 -1
- package/dist/embedded-javascript.js +111 -2
- package/dist/embedded-javascript.js.map +1 -1
- package/dist/embedded-module.d.ts.map +1 -1
- package/dist/embedded-module.js +22 -0
- package/dist/embedded-module.js.map +1 -1
- package/dist/emitter.d.ts +83 -1
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +518 -41
- package/dist/emitter.js.map +1 -1
- package/dist/error-runtime.d.ts +9 -0
- package/dist/error-runtime.d.ts.map +1 -1
- package/dist/error-runtime.js +21 -0
- package/dist/error-runtime.js.map +1 -1
- package/dist/extension.d.ts +5 -2
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +2 -1
- package/dist/extension.js.map +1 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +222 -91
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -5
- package/dist/index.js.map +1 -1
- package/dist/interpolated-string.d.ts.map +1 -1
- package/dist/interpolated-string.js +29 -2
- package/dist/interpolated-string.js.map +1 -1
- package/dist/json-runtime.d.ts.map +1 -1
- package/dist/json-runtime.js +27 -3
- package/dist/json-runtime.js.map +1 -1
- package/dist/lexer.d.ts +217 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +799 -37
- package/dist/lexer.js.map +1 -1
- package/dist/limits.d.ts +9 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +9 -0
- package/dist/limits.js.map +1 -1
- package/dist/mechanical-fix.d.ts.map +1 -1
- package/dist/mechanical-fix.js +38 -0
- package/dist/mechanical-fix.js.map +1 -1
- package/dist/parser.d.ts +30 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +137 -21
- package/dist/parser.js.map +1 -1
- package/dist/promise-runtime.d.ts +20 -1
- package/dist/promise-runtime.d.ts.map +1 -1
- package/dist/promise-runtime.js +38 -3
- package/dist/promise-runtime.js.map +1 -1
- package/dist/reactive-bridge-runtime.d.ts.map +1 -1
- package/dist/reactive-bridge-runtime.js +7 -1
- package/dist/reactive-bridge-runtime.js.map +1 -1
- package/dist/runtime-abi.d.ts +21 -1
- package/dist/runtime-abi.d.ts.map +1 -1
- package/dist/runtime-abi.js +23 -3
- package/dist/runtime-abi.js.map +1 -1
- package/dist/source-names.d.ts +26 -0
- package/dist/source-names.d.ts.map +1 -1
- package/dist/source-names.js +49 -5
- package/dist/source-names.js.map +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +13 -0
- package/dist/source.js.map +1 -1
- package/dist/stable-order.d.ts +16 -0
- package/dist/stable-order.d.ts.map +1 -0
- package/dist/stable-order.js +18 -0
- package/dist/stable-order.js.map +1 -0
- package/dist/text-runtime.d.ts.map +1 -1
- package/dist/text-runtime.js +182 -41
- package/dist/text-runtime.js.map +1 -1
- package/dist/token.d.ts +11 -0
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js.map +1 -1
- package/dist/type-registry-runtime.d.ts.map +1 -1
- package/dist/type-registry-runtime.js +8 -1
- package/dist/type-registry-runtime.js.map +1 -1
- package/dist/type-validation-runtime.d.ts.map +1 -1
- package/dist/type-validation-runtime.js +129 -1
- package/dist/type-validation-runtime.js.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +161 -17
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/analyzer.js
CHANGED
|
@@ -1,11 +1,90 @@
|
|
|
1
1
|
import { blockContainsDirectAwait } from "./ast.js";
|
|
2
2
|
import { isPermanentNamespaceName } from "./core-vocabulary.js";
|
|
3
|
-
import { diagnostic, mechanicalEdits, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
|
|
3
|
+
import { advisory, diagnostic, mechanicalEdits, mechanicalFix, recoveredDiagnostic } from "./diagnostic.js";
|
|
4
4
|
import { VELAR_HOST_ERROR_NAMES, VELAR_HOST_ERROR_PATH_NAMES } from "./error-runtime.js";
|
|
5
5
|
import { collectionMemberGuidance, removedGlobalFunctionGuidance, stringMemberGuidance, REST_PARAMETER_ELEMENT_TYPE_MESSAGE } from "./language-guidance.js";
|
|
6
6
|
import { bindingNameRestriction } from "./source-names.js";
|
|
7
7
|
import { span, spanIdentity } from "./source.js";
|
|
8
|
-
import { analysisTypeIdentity, anyType, binaryStorageKind, boolType, boundGrants, collectGenericBoundViolations, collectTypeArgumentBoundViolations, describeType, genericApplicationType, instantiateGenericCallable, invalidType, isInvalidType, isAssignable, isReadonlyView, isTextConvertibleType, isTypeParameterBound, typeParameterBoundNames, mergeTypes, mutableViewOf, nullType, nonOptional, numberType, optionalOf, resolveTypeReference, readonlyViewOf, resolvedAsyncType, semanticTypeIdentity, sameType, sameTypeIgnoringCallableParameterNames, stringType, substituteTypeParameters, textConvertibleType, typeContainsParameter, typeContainsRuntimeTypeCheck, unifyTypeParameters, unionOf, unknownType, } from "./types.js";
|
|
8
|
+
import { analysisTypeIdentity, anyType, binaryStorageKind, boolType, boundGrants, boundaryUnknownType, collectGenericBoundViolations, collectTypeArgumentBoundViolations, describeType, genericApplicationType, instantiateGenericCallable, invalidType, isInvalidType, isAssignable, isReadonlyView, isTextConvertibleType, isTypeParameterBound, typeParameterBoundNames, mergeTypes, mutableViewOf, nullType, nonOptional, numberType, optionalOf, resolveTypeReference, readonlyViewOf, resolvedAsyncType, semanticTypeIdentity, sameType, sameTypeIgnoringCallableParameterNames, stringType, substituteTypeParameters, textConvertibleType, typeContainsAnyOutput, typeContainsParameter, typeContainsRuntimeTypeCheck, unifyTypeParameters, unionOf, unknownType, } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* How many back-edge passes may be running at once. One is what a single-level
|
|
11
|
+
* loop needs, and three covers a loop nest three deep — the deepest anyone
|
|
12
|
+
* writes on purpose — with every level analyzed exactly as before. Past that
|
|
13
|
+
* the count stops doubling per level: fourteen levels went from 3.2 seconds to
|
|
14
|
+
* 0.2, and seventeen from 30 seconds to 0.4.
|
|
15
|
+
*/
|
|
16
|
+
const maximumLoopReanalysisDepth = 3;
|
|
17
|
+
/**
|
|
18
|
+
* D90 R12: the class and record names a consumer can read a value *out of*
|
|
19
|
+
* this type, collected into two frontiers so `exportReachableClasses` can walk
|
|
20
|
+
* a record's fields without recursing through a cyclic record here.
|
|
21
|
+
*
|
|
22
|
+
* It visits output positions for the reason `typeContainsAnyOutput` (types.ts)
|
|
23
|
+
* does: an input position accepts a value *from* the consumer, so a class
|
|
24
|
+
* named there is one the consumer already had. The one deliberate difference
|
|
25
|
+
* is an extension type's `properties`, documented at that case.
|
|
26
|
+
*/
|
|
27
|
+
function collectOutputTypeNames(type, classes, records) {
|
|
28
|
+
switch (type.kind) {
|
|
29
|
+
case "class":
|
|
30
|
+
case "classConstructor":
|
|
31
|
+
classes.push(type.name);
|
|
32
|
+
return;
|
|
33
|
+
case "optional":
|
|
34
|
+
collectOutputTypeNames(type.inner, classes, records);
|
|
35
|
+
return;
|
|
36
|
+
case "list":
|
|
37
|
+
case "set":
|
|
38
|
+
collectOutputTypeNames(type.element, classes, records);
|
|
39
|
+
return;
|
|
40
|
+
case "map":
|
|
41
|
+
collectOutputTypeNames(type.key, classes, records);
|
|
42
|
+
collectOutputTypeNames(type.value, classes, records);
|
|
43
|
+
return;
|
|
44
|
+
case "record":
|
|
45
|
+
case "promise":
|
|
46
|
+
case "runtimeType":
|
|
47
|
+
collectOutputTypeNames(type.value, classes, records);
|
|
48
|
+
return;
|
|
49
|
+
case "object":
|
|
50
|
+
for (const field of type.fields.values())
|
|
51
|
+
collectOutputTypeNames(field, classes, records);
|
|
52
|
+
return;
|
|
53
|
+
case "named":
|
|
54
|
+
case "typeObject":
|
|
55
|
+
// A `named` may still denote a class before resolveNamedClasses runs, so
|
|
56
|
+
// the name joins both frontiers; the one that does not match a
|
|
57
|
+
// declaration simply finds nothing.
|
|
58
|
+
classes.push(type.name);
|
|
59
|
+
records.push(type.name);
|
|
60
|
+
for (const argument of type.kind === "named" ? type.application?.arguments ?? [] : []) {
|
|
61
|
+
collectOutputTypeNames(argument, classes, records);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
case "extension":
|
|
65
|
+
// An extension family's `properties` are its *named parameters* — a Web
|
|
66
|
+
// component's props are supplied by whoever renders it — so they are the
|
|
67
|
+
// `function` case's `parameters` under another spelling, and are skipped
|
|
68
|
+
// for the same reason. Walking them made `export component Panel(inner:
|
|
69
|
+
// Inner)` report `Inner`'s inferred member while `export def take(box:
|
|
70
|
+
// Inner)` stayed silent: one question, two answers. `arguments` carry
|
|
71
|
+
// the family's payload — a component's exposed Handle, a route input's
|
|
72
|
+
// validated value, a provider's result — which a consumer does read out.
|
|
73
|
+
for (const argument of type.arguments)
|
|
74
|
+
collectOutputTypeNames(argument, classes, records);
|
|
75
|
+
return;
|
|
76
|
+
case "function":
|
|
77
|
+
case "action":
|
|
78
|
+
case "intrinsic":
|
|
79
|
+
collectOutputTypeNames(type.result, classes, records);
|
|
80
|
+
return;
|
|
81
|
+
case "union":
|
|
82
|
+
for (const member of type.members)
|
|
83
|
+
collectOutputTypeNames(member, classes, records);
|
|
84
|
+
return;
|
|
85
|
+
default:
|
|
86
|
+
}
|
|
87
|
+
}
|
|
9
88
|
function continuesOptionalChain(expression) {
|
|
10
89
|
if (expression.kind === "MemberExpression") {
|
|
11
90
|
return expression.optional || continuesOptionalChain(expression.object);
|
|
@@ -99,26 +178,53 @@ function sameInferredResult(left, right) {
|
|
|
99
178
|
return false;
|
|
100
179
|
return sameType(left, right);
|
|
101
180
|
}
|
|
181
|
+
/** The furthest a suggestion may be from what was written. */
|
|
182
|
+
const nearestNameLimit = 2;
|
|
183
|
+
/**
|
|
184
|
+
* Edit distance, abandoned as soon as it is known to exceed `nearestNameLimit`.
|
|
185
|
+
* Only cells within that many steps of the diagonal can hold a value inside the
|
|
186
|
+
* limit, so each row is a fixed-width band rather than the whole right operand,
|
|
187
|
+
* and a row whose every cell is already over the limit ends the walk.
|
|
188
|
+
*/
|
|
189
|
+
function boundedEditDistance(left, right) {
|
|
190
|
+
const over = nearestNameLimit + 1;
|
|
191
|
+
if (Math.abs(left.length - right.length) > nearestNameLimit)
|
|
192
|
+
return over;
|
|
193
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
194
|
+
let current = new Array(right.length + 1).fill(over);
|
|
195
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
196
|
+
const from = Math.max(1, leftIndex - nearestNameLimit);
|
|
197
|
+
const to = Math.min(right.length, leftIndex + nearestNameLimit);
|
|
198
|
+
current[from - 1] = from === 1 ? leftIndex : over;
|
|
199
|
+
let rowBest = current[from - 1];
|
|
200
|
+
for (let rightIndex = from; rightIndex <= to; rightIndex += 1) {
|
|
201
|
+
const cell = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
202
|
+
current[rightIndex] = cell;
|
|
203
|
+
if (cell < rowBest)
|
|
204
|
+
rowBest = cell;
|
|
205
|
+
}
|
|
206
|
+
if (to < right.length)
|
|
207
|
+
current[to + 1] = over;
|
|
208
|
+
if (rowBest > nearestNameLimit)
|
|
209
|
+
return over;
|
|
210
|
+
const swap = previous;
|
|
211
|
+
previous = current;
|
|
212
|
+
current = swap;
|
|
213
|
+
}
|
|
214
|
+
return previous[right.length];
|
|
215
|
+
}
|
|
102
216
|
/** Returns the sole nearest spelling within two edits, never an ambiguous guess. */
|
|
103
217
|
function uniqueNearestName(requested, candidates) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const current = [leftIndex];
|
|
108
|
-
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
109
|
-
current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
110
|
-
}
|
|
111
|
-
previous = current;
|
|
112
|
-
}
|
|
113
|
-
return previous[right.length];
|
|
114
|
-
};
|
|
218
|
+
// A Set argument is already deduplicated; copying it again cost one full
|
|
219
|
+
// rebuild per unresolved name for nothing.
|
|
220
|
+
const unique = candidates instanceof Set ? candidates : new Set(candidates);
|
|
115
221
|
let best = null;
|
|
116
|
-
let bestDistance =
|
|
222
|
+
let bestDistance = nearestNameLimit + 1;
|
|
117
223
|
let tied = false;
|
|
118
|
-
for (const candidate of
|
|
119
|
-
if (candidate === requested || Math.abs(candidate.length - requested.length) >
|
|
224
|
+
for (const candidate of unique) {
|
|
225
|
+
if (candidate === requested || Math.abs(candidate.length - requested.length) > nearestNameLimit)
|
|
120
226
|
continue;
|
|
121
|
-
const candidateDistance =
|
|
227
|
+
const candidateDistance = boundedEditDistance(requested, candidate);
|
|
122
228
|
if (candidateDistance < bestDistance) {
|
|
123
229
|
best = candidate;
|
|
124
230
|
bestDistance = candidateDistance;
|
|
@@ -128,7 +234,83 @@ function uniqueNearestName(requested, candidates) {
|
|
|
128
234
|
tied = true;
|
|
129
235
|
}
|
|
130
236
|
}
|
|
131
|
-
return bestDistance <=
|
|
237
|
+
return bestDistance <= nearestNameLimit && !tied ? best : null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The roster a "did you mean" reads. It used to be rebuilt — core vocabulary,
|
|
241
|
+
* extension globals, imports, and every name in every live scope — and then run
|
|
242
|
+
* through a full edit-distance pass, once per unresolved name, which made a
|
|
243
|
+
* module of typos quadratic in its own size. The roster is now maintained as
|
|
244
|
+
* scopes come and go, and each name is filed under the strings left by deleting
|
|
245
|
+
* up to two of its characters: two spellings within two edits always share one
|
|
246
|
+
* of those, so a query reads a few buckets instead of the whole roster.
|
|
247
|
+
*/
|
|
248
|
+
class NearestNameRoster {
|
|
249
|
+
counts = new Map();
|
|
250
|
+
/** Filled on the first question asked of the roster; a module with no typo never pays for it. */
|
|
251
|
+
buckets = null;
|
|
252
|
+
add(name) {
|
|
253
|
+
const seen = this.counts.get(name) ?? 0;
|
|
254
|
+
this.counts.set(name, seen + 1);
|
|
255
|
+
if (seen === 0)
|
|
256
|
+
this.file(name);
|
|
257
|
+
}
|
|
258
|
+
remove(name) {
|
|
259
|
+
const seen = this.counts.get(name) ?? 0;
|
|
260
|
+
if (seen === 0)
|
|
261
|
+
return;
|
|
262
|
+
if (seen > 1) {
|
|
263
|
+
this.counts.set(name, seen - 1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
this.counts.delete(name);
|
|
267
|
+
if (!this.buckets)
|
|
268
|
+
return;
|
|
269
|
+
for (const key of deletionKeys(name)) {
|
|
270
|
+
const bucket = this.buckets.get(key);
|
|
271
|
+
if (!bucket)
|
|
272
|
+
continue;
|
|
273
|
+
bucket.delete(name);
|
|
274
|
+
if (bucket.size === 0)
|
|
275
|
+
this.buckets.delete(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
nearest(requested) {
|
|
279
|
+
if (!this.buckets) {
|
|
280
|
+
this.buckets = new Map();
|
|
281
|
+
for (const name of this.counts.keys())
|
|
282
|
+
this.file(name);
|
|
283
|
+
}
|
|
284
|
+
const candidates = new Set();
|
|
285
|
+
for (const key of deletionKeys(requested)) {
|
|
286
|
+
for (const candidate of this.buckets.get(key) ?? [])
|
|
287
|
+
candidates.add(candidate);
|
|
288
|
+
}
|
|
289
|
+
return uniqueNearestName(requested, candidates);
|
|
290
|
+
}
|
|
291
|
+
file(name) {
|
|
292
|
+
if (!this.buckets)
|
|
293
|
+
return;
|
|
294
|
+
for (const key of deletionKeys(name)) {
|
|
295
|
+
const bucket = this.buckets.get(key);
|
|
296
|
+
if (bucket)
|
|
297
|
+
bucket.add(name);
|
|
298
|
+
else
|
|
299
|
+
this.buckets.set(key, new Set([name]));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** `name` with up to `nearestNameLimit` characters deleted, the shared key of any two near spellings. */
|
|
304
|
+
function deletionKeys(name) {
|
|
305
|
+
const keys = [name];
|
|
306
|
+
for (let first = 0; first < name.length; first += 1) {
|
|
307
|
+
const once = name.slice(0, first) + name.slice(first + 1);
|
|
308
|
+
keys.push(once);
|
|
309
|
+
for (let second = first; second < once.length; second += 1) {
|
|
310
|
+
keys.push(once.slice(0, second) + once.slice(second + 1));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return keys;
|
|
132
314
|
}
|
|
133
315
|
const corePrimitiveNames = new Set(["string", "number", "bool", "null", "unknown", "Duration"]);
|
|
134
316
|
const builtinTypeNames = new Set(["string", "number", "bool", "null", "unknown", "any", "List", "Set", "Map", "Record", "Promise", "Function", "Type", "Duration"]);
|
|
@@ -156,6 +338,14 @@ export const disposeMemberKey = "__velar:dispose";
|
|
|
156
338
|
* member can answer it by accident and no author call can reach it.
|
|
157
339
|
*/
|
|
158
340
|
export const iterateMemberKey = "__velar:iterate";
|
|
341
|
+
/**
|
|
342
|
+
* The emitted member behind the asynchronous `@iterate:` form (D90 R18). It is
|
|
343
|
+
* a separate key because the two forms answer different questions — the
|
|
344
|
+
* synchronous member returns the finished collection once, this one is an
|
|
345
|
+
* async method `async for` pulls once per element — so no lowering can confuse
|
|
346
|
+
* one for the other.
|
|
347
|
+
*/
|
|
348
|
+
export const iterateAsyncMemberKey = "__velar:iterateAsync";
|
|
159
349
|
/** What each bound admits, written the way a rejected call needs to hear it. */
|
|
160
350
|
const boundVocabularyGuidance = {
|
|
161
351
|
Text: "a Text parameter accepts the types with a hook-free text form — strings, numbers, bools, enums, and null",
|
|
@@ -190,23 +380,111 @@ const coreGlobalGuidance = new Map([
|
|
|
190
380
|
["len", "Use 'value.size'; strings and collections measure with the size member"],
|
|
191
381
|
["parseInt", "Use 'number(text)', then '.floor()' or '.round()' for an integer; VelarScript has one text-to-number conversion"],
|
|
192
382
|
["parseFloat", "Use 'number(text)'; VelarScript has one text-to-number conversion"],
|
|
383
|
+
// D89 (message correction): `enumerate` and `zip` are the two Python loop
|
|
384
|
+
// reflexes that reached an unadorned "Unknown name" with no successor at all
|
|
385
|
+
// — `zip` even earned a "did you mean 'Map'?". `enumerate` has a spelling
|
|
386
|
+
// that needs no import, so the loop is named first; `zip` has none, so its
|
|
387
|
+
// guidance is the import that makes it exist.
|
|
388
|
+
["enumerate", "Use the two-slot loop — 'for value, index in values:' — which binds the value first; 'enumerate' is an import from \"velar/collections\" when you want the '{index, value}' List itself"],
|
|
389
|
+
["zip", "Import the builder — 'import {zip} from \"velar/collections\"' — it pairs two Lists as '{first, second}' up to the shorter length"],
|
|
193
390
|
["stringify", "Use Json.stringify(value) directly; VelarScript's pure namespaces need no import"],
|
|
194
391
|
["parse", "Use Json.parse(text) directly; VelarScript's pure namespaces need no import"],
|
|
392
|
+
// D90 (coherence): the rest of the Python builtin surface a model reaches
|
|
393
|
+
// for. Every one of these had an answer sitting in a roster the compiler
|
|
394
|
+
// already owns, and reached the author either as a bare "Unknown name" or —
|
|
395
|
+
// worse — as a confident edit-distance guess at an unrelated name (`sum` ->
|
|
396
|
+
// `str`, `max` -> `Map`, `map` -> `Map`). Naming the successor also
|
|
397
|
+
// suppresses the guess, because guidance is consulted first.
|
|
398
|
+
["sum", "Use 'values.sum()'; totalling is a List member"],
|
|
399
|
+
["min", "Use 'Math.min(a, b)' for two numbers, or 'values.min()' for a List"],
|
|
400
|
+
["max", "Use 'Math.max(a, b)' for two numbers, or 'values.max()' for a List"],
|
|
401
|
+
["sorted", "Use 'values.sorted()'; it returns a new List and never mutates the receiver"],
|
|
402
|
+
["reversed", "Use 'values.reversed()'; it returns a new List and never mutates the receiver"],
|
|
403
|
+
["any", "Use 'values.some(test)'; the collection members carry the quantifiers"],
|
|
404
|
+
["all", "Use 'values.every(test)'; the collection members carry the quantifiers"],
|
|
405
|
+
["filter", "Use 'values.filter(test)'; the collection members carry the transforms"],
|
|
406
|
+
["map", "Use 'values.map(transform)'; 'Map' with a capital M is the key-value collection, not the transform"],
|
|
407
|
+
["isinstance", "Use the 'is' operator — 'value is Type' — which also narrows the binding inside the branch"],
|
|
408
|
+
["pow", "Use 'Math.pow(base, exponent)'"],
|
|
409
|
+
["divmod", "Use '(a / b).floor()' for the quotient and 'a % b' for the remainder; VelarScript returns one value per operation"],
|
|
410
|
+
["repr", "Use 'print(value)' to inspect a value, 'str(value)' for its text form, or 'Json.stringify(value)' for data text"],
|
|
411
|
+
["format", "Use an f-string — 'f\"{value}\"' — and format the value first: 'value.toFixed(2)' for fixed decimals, 'str(value).padStart(size)' for width"],
|
|
412
|
+
["type", "Use 'value is Type' to test a value's type, and a 'type' declaration to name one; VelarScript has no runtime type-of function"],
|
|
413
|
+
["iter", "Use a 'for' loop over the collection — 'for value in values:' — VelarScript has no 'iter'/'next' iterator pair"],
|
|
414
|
+
["next", "Use a 'for' loop over the collection — 'for value in values:' — VelarScript has no 'iter'/'next' iterator pair"],
|
|
415
|
+
["tuple", "Use a List — '[a, b]' — for a positional sequence, or a record — '{first: a, second: b}' — for named parts; VelarScript has no tuple type"],
|
|
416
|
+
["bytes", "Import the Bytes type — 'import {Bytes} from \"velar/binary\"' — which is VelarScript's immutable byte snapshot"],
|
|
417
|
+
// The two capability answers. A terminal and a filesystem are target
|
|
418
|
+
// capabilities rather than prelude names, so the message names the module
|
|
419
|
+
// and says which extension carries it instead of implying a bare Core
|
|
420
|
+
// module can import it.
|
|
421
|
+
["input", "Use velar/terminal — 'terminal.readLine(prompt)' returns the next line — a terminal is a target capability, so it arrives with the @velarscript/node extension rather than the Core prelude"],
|
|
422
|
+
["open", "Use velar/fs to read or write a file, and 'using name = ...' to own a handle that must be released; a filesystem is a target capability, so it arrives with the @velarscript/node extension rather than the Core prelude"],
|
|
423
|
+
// D90 (coherence): the target-neutral host globals. Each of these is
|
|
424
|
+
// answered by a name a plain Core module can already reach, so the answer
|
|
425
|
+
// belongs here rather than in a target extension. `process`, `Buffer`,
|
|
426
|
+
// `require`, `localStorage` and the rest of the target-specific roster stay
|
|
427
|
+
// with the extension that owns their successor.
|
|
428
|
+
["setTimeout", "Use 'await Promise.sleep(250ms)' and then run the work; VelarScript waits with a Duration rather than a callback and a millisecond number"],
|
|
429
|
+
["setInterval", "Use a loop with 'await Promise.sleep(1s)' in it, or velar/task's 'task(work)' when the repetition must be cancellable; VelarScript has no callback scheduler"],
|
|
430
|
+
...["clearTimeout", "clearInterval"].map((name) => [
|
|
431
|
+
name,
|
|
432
|
+
"There is no callback scheduler to clear; 'await Promise.sleep(250ms)' waits inline, and velar/task's 'task(work)' is the schedule a Cancellation can stop",
|
|
433
|
+
]),
|
|
434
|
+
["structuredClone", "Use 'Json.clone(value, Target)'; it validates against the runtime type as it copies"],
|
|
435
|
+
["RegExp", "Use the Text pattern members — 'Text.matches', 'Text.findMatch', 'Text.findMatches', 'Text.replaceMatches' — which take the pattern as text"],
|
|
436
|
+
["TextEncoder", "Use 'Text.utf8Size(value)' for the byte count, and \"velar/binary\" for the byte vocabulary itself; VelarScript does not expose the TextEncoder global"],
|
|
437
|
+
["TextDecoder", "Use \"velar/binary\" for the byte vocabulary; VelarScript does not expose the TextDecoder global"],
|
|
438
|
+
["URL", "Import from \"velar/url\" — 'parse', 'join', 'query', 'withQuery', 'encode' — instead of the URL global"],
|
|
439
|
+
["AbortController", "Use the Cancellation that velar/task's 'task(work)' passes into its work; VelarScript cancels through that value rather than a signal object"],
|
|
440
|
+
["Symbol", "VelarScript has no symbol type; use an enum for a closed set of names, or a plain string constant for a unique key"],
|
|
441
|
+
// `velar/worker` is a Core module, so the ambient `Worker` a host offers is
|
|
442
|
+
// answered once here rather than twice in the two extensions that also carry
|
|
443
|
+
// a worker surface.
|
|
444
|
+
["Worker", "Import the builder — 'import {worker} from \"velar/worker\"' — it starts a typed worker from an entry declared in velar.json, and 'workerPool' runs several of them"],
|
|
195
445
|
...["length", "char", "slice", "trim", "lower", "upper", "startsWith", "endsWith", "includes", "split", "replace", "replaceAll", "repeat", "padStart", "padEnd", "abs", "round", "floor", "ceil", "isFinite", "isInteger"]
|
|
196
446
|
.map((name) => [name, removedGlobalFunctionGuidance(name)]),
|
|
197
447
|
]);
|
|
448
|
+
/**
|
|
449
|
+
* D90 (coherence): the foreign builtins — Python's and the host's — that a
|
|
450
|
+
* model writes from prior knowledge. A "did you mean" is an edit-distance
|
|
451
|
+
* guess over names this module can actually see, and on a foreign builtin it
|
|
452
|
+
* is confidently wrong: `sum` earned `str`, `max` and `map` earned `Map`,
|
|
453
|
+
* `dir` earned `str`. docs/ai-skill.md tells the model to do exactly what the
|
|
454
|
+
* diagnostic says, so a wrong successor is worse than none.
|
|
455
|
+
*
|
|
456
|
+
* Every name that has a Vel answer is in `coreGlobalGuidance` above, which is
|
|
457
|
+
* consulted first and short-circuits the guess on its own. This roster is the
|
|
458
|
+
* floor under the rest: a foreign builtin with no successor reads as a bare
|
|
459
|
+
* unknown name and stops there. It is deliberately a suppression rather than a
|
|
460
|
+
* change to `uniqueNearestName`, whose threshold and roster serve the field-
|
|
461
|
+
* name hints too.
|
|
462
|
+
*/
|
|
463
|
+
const foreignBuiltinNames = new Set([
|
|
464
|
+
// Python
|
|
465
|
+
"sum", "min", "max", "sorted", "reversed", "any", "all", "isinstance", "input", "open",
|
|
466
|
+
"filter", "map", "repr", "dir", "type", "id", "next", "iter", "format", "divmod", "pow",
|
|
467
|
+
"bytes", "tuple", "frozenset", "globals", "locals", "vars", "hasattr", "getattr", "setattr",
|
|
468
|
+
"callable", "issubclass", "abs", "round", "ord", "chr", "hex", "oct", "bin", "hash",
|
|
469
|
+
// Node and browser hosts
|
|
470
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval", "structuredClone", "queueMicrotask",
|
|
471
|
+
"URL", "URLSearchParams", "RegExp", "TextEncoder", "TextDecoder", "AbortController", "AbortSignal",
|
|
472
|
+
"Symbol", "Proxy", "Reflect", "WeakMap", "WeakSet", "BigInt", "Intl", "globalThis",
|
|
473
|
+
"process", "Buffer", "require", "__dirname", "__filename", "module", "exports", "global",
|
|
474
|
+
"localStorage", "sessionStorage", "fetch", "document", "window", "navigator", "alert",
|
|
475
|
+
]);
|
|
198
476
|
const durationType = { kind: "named", name: "Duration" };
|
|
199
477
|
const namespaceFunction = (name, parameterNames, parameters, result, requiredParameters = parameters.length) => ({ kind: "intrinsic", name, parameterNames, parameters, requiredParameters, result });
|
|
200
478
|
const promiseOf = (value) => ({ kind: "promise", value });
|
|
201
479
|
const jsonNamespaceType = {
|
|
202
480
|
kind: "object",
|
|
203
481
|
fields: new Map([
|
|
204
|
-
["parse", namespaceFunction("json.parse", ["text", "target"], [stringType,
|
|
205
|
-
["tryParse", namespaceFunction("json.tryParse", ["text", "target", "fallback"], [stringType,
|
|
206
|
-
["stringify", namespaceFunction("json.stringify", ["value", "pretty"], [
|
|
207
|
-
["stableStringify", namespaceFunction("json.stableStringify", ["value", "pretty"], [
|
|
208
|
-
["clone", namespaceFunction("json.clone", ["value", "target"], [
|
|
209
|
-
["isSerializable", { kind: "function", parameterNames: ["value"], parameters: [
|
|
482
|
+
["parse", namespaceFunction("json.parse", ["text", "target"], [stringType, unknownType], unknownType, 1)],
|
|
483
|
+
["tryParse", namespaceFunction("json.tryParse", ["text", "target", "fallback"], [stringType, unknownType, unknownType], unknownType, 1)],
|
|
484
|
+
["stringify", namespaceFunction("json.stringify", ["value", "pretty"], [unknownType, { kind: "union", members: [boolType, numberType] }], stringType, 1)],
|
|
485
|
+
["stableStringify", namespaceFunction("json.stableStringify", ["value", "pretty"], [unknownType, { kind: "union", members: [boolType, numberType] }], stringType, 1)],
|
|
486
|
+
["clone", namespaceFunction("json.clone", ["value", "target"], [unknownType, unknownType], unknownType, 1)],
|
|
487
|
+
["isSerializable", { kind: "function", parameterNames: ["value"], parameters: [unknownType], requiredParameters: 1, result: boolType }],
|
|
210
488
|
]),
|
|
211
489
|
readonlyFields: new Set(["parse", "tryParse", "stringify", "stableStringify", "clone", "isSerializable"]),
|
|
212
490
|
};
|
|
@@ -273,13 +551,13 @@ const textNamespaceType = {
|
|
|
273
551
|
const promiseNamespaceType = {
|
|
274
552
|
kind: "object",
|
|
275
553
|
fields: new Map([
|
|
276
|
-
["all", namespaceFunction("async.all", ["values"], [
|
|
277
|
-
["race", namespaceFunction("async.race", ["values"], [{ kind: "list", element:
|
|
554
|
+
["all", namespaceFunction("async.all", ["values"], [unknownType], promiseOf(unknownType))],
|
|
555
|
+
["race", namespaceFunction("async.race", ["values"], [{ kind: "list", element: unknownType }], promiseOf(unknownType))],
|
|
278
556
|
["sleep", { kind: "function", parameterNames: ["duration"], parameters: [durationType], requiredParameters: 1, result: promiseOf(nullType) }],
|
|
279
|
-
["timeout", namespaceFunction("async.timeout", ["value", "duration", "message"], [promiseOf(
|
|
280
|
-
["retry", namespaceFunction("async.retry", ["task", "attempts", "delay"], [
|
|
281
|
-
["map", namespaceFunction("async.map", ["values", "worker", "concurrency"], [{ kind: "list", element:
|
|
282
|
-
["series", namespaceFunction("async.series", ["tasks"], [{ kind: "list", element:
|
|
557
|
+
["timeout", namespaceFunction("async.timeout", ["value", "duration", "message"], [promiseOf(unknownType), durationType, stringType], promiseOf(unknownType), 2)],
|
|
558
|
+
["retry", namespaceFunction("async.retry", ["task", "attempts", "delay"], [unknownType, numberType, durationType], promiseOf(unknownType), 1)],
|
|
559
|
+
["map", namespaceFunction("async.map", ["values", "worker", "concurrency"], [{ kind: "list", element: unknownType }, unknownType, numberType], promiseOf({ kind: "list", element: unknownType }), 2)],
|
|
560
|
+
["series", namespaceFunction("async.series", ["tasks"], [{ kind: "list", element: unknownType }], promiseOf({ kind: "list", element: unknownType }))],
|
|
283
561
|
]),
|
|
284
562
|
readonlyFields: new Set(["all", "race", "sleep", "timeout", "retry", "map", "series"]),
|
|
285
563
|
};
|
|
@@ -344,8 +622,9 @@ const coreVocabularyTypes = {
|
|
|
344
622
|
// `values.map(str)` — is checked against the same whitelist the direct
|
|
345
623
|
// call form uses instead of executing a 'toString' hook.
|
|
346
624
|
str: { kind: "function", parameterNames: ["value"], parameters: [textConvertibleType], requiredParameters: 1, result: stringType },
|
|
347
|
-
// `print` inspects any value by contract
|
|
348
|
-
|
|
625
|
+
// `print` inspects any value by contract; its domain is the top type for
|
|
626
|
+
// assignment targets (D90 R17 keeps `any` for boundary declarations only).
|
|
627
|
+
print: { kind: "function", parameterNames: ["value"], parameters: [unknownType], requiredParameters: 1, result: nullType },
|
|
349
628
|
// D47 rule 81: equals(a, b) — deep structural comparison over data.
|
|
350
629
|
// Pure computation, so it lives in the prelude beside str/print; the
|
|
351
630
|
// call site owns the domain checks (inferEqualsCall).
|
|
@@ -455,10 +734,120 @@ function externClassContract(info) {
|
|
|
455
734
|
methodEntries(info.staticMethods),
|
|
456
735
|
]);
|
|
457
736
|
}
|
|
737
|
+
// D89 A2's two rosters. They are deliberately short: every name here is one a
|
|
738
|
+
// Python author reaches for without thinking, and a name that has to be argued
|
|
739
|
+
// for is a name the advisory would be guessing about.
|
|
740
|
+
const loopIndexSlotNames = new Set(["i", "idx", "index", "pos", "position"]);
|
|
741
|
+
const loopValueSlotNames = new Set(["v", "value", "item", "el", "element"]);
|
|
742
|
+
/**
|
|
743
|
+
* The singular of the iterated collection's own name, so `for i, user in
|
|
744
|
+
* users` reads as the same swap as `for i, v in users`. Only a plain name is
|
|
745
|
+
* read; an arbitrary expression has no name to make singular.
|
|
746
|
+
*/
|
|
747
|
+
function singularIterableName(iterable) {
|
|
748
|
+
const name = iterable.kind === "IdentifierExpression" ? iterable.name
|
|
749
|
+
: iterable.kind === "MemberExpression" ? iterable.property
|
|
750
|
+
: null;
|
|
751
|
+
if (name === null || !name.endsWith("s") || name.endsWith("ss"))
|
|
752
|
+
return null;
|
|
753
|
+
if (name.endsWith("ies"))
|
|
754
|
+
return `${name.slice(0, -3)}y`;
|
|
755
|
+
if (/(?:ch|sh|[sxz])es$/u.test(name))
|
|
756
|
+
return name.slice(0, -2);
|
|
757
|
+
return name.slice(0, -1);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* D85 rule 207: does this type still carry an unsettled collection — a List or
|
|
761
|
+
* Set element, or a Map's key and value together, that stayed `unknown`?
|
|
762
|
+
*
|
|
763
|
+
* This is the gate on the walk below, and it is what makes rule 208's boundary
|
|
764
|
+
* fall out of the semantics instead of out of a syntax list. `Set().size` is a
|
|
765
|
+
* number: nothing the empty `Set()` failed to say reaches the name, so the walk
|
|
766
|
+
* never descends to it. `[["a"], []]` is a `List<List<string>>`: the sibling
|
|
767
|
+
* element already said what the empty one holds, so there is nothing left to
|
|
768
|
+
* report. Only a hole that survives all the way out to the binding is a hole
|
|
769
|
+
* the author has to close.
|
|
770
|
+
*/
|
|
771
|
+
function carriesUnsettledCollection(type, seen = new Set()) {
|
|
772
|
+
if (seen.has(type))
|
|
773
|
+
return false;
|
|
774
|
+
seen.add(type);
|
|
775
|
+
switch (type.kind) {
|
|
776
|
+
case "list":
|
|
777
|
+
case "set":
|
|
778
|
+
return type.element.kind === "unknown" || carriesUnsettledCollection(type.element, seen);
|
|
779
|
+
case "map":
|
|
780
|
+
// A Map settles when either half does: `Map<string, unknown>` says its
|
|
781
|
+
// keys are strings, and only `Map()` with nothing at all is unsettled.
|
|
782
|
+
return (type.key.kind === "unknown" && type.value.kind === "unknown")
|
|
783
|
+
|| carriesUnsettledCollection(type.key, seen)
|
|
784
|
+
|| carriesUnsettledCollection(type.value, seen);
|
|
785
|
+
case "optional":
|
|
786
|
+
return carriesUnsettledCollection(type.inner, seen);
|
|
787
|
+
case "record":
|
|
788
|
+
case "promise":
|
|
789
|
+
return carriesUnsettledCollection(type.value, seen);
|
|
790
|
+
case "object":
|
|
791
|
+
return [...type.fields.values()].some((field) => carriesUnsettledCollection(field, seen));
|
|
792
|
+
case "union":
|
|
793
|
+
return type.members.some((member) => carriesUnsettledCollection(member, seen));
|
|
794
|
+
default:
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* D85 rule 207: the sub-expressions whose value becomes part of this
|
|
800
|
+
* expression's value, so each of them is a position that has to say what an
|
|
801
|
+
* empty collection written there holds.
|
|
802
|
+
*
|
|
803
|
+
* A receiver and an argument are here, but the caller only asks for them once
|
|
804
|
+
* `carriesUnsettledCollection` has said the enclosing value still has the hole
|
|
805
|
+
* in it — so `const a = [].copy()` and `const a = id([])` report at the `[]`
|
|
806
|
+
* that made the hole, while `print(Set().size)` and `const n = Set().size`
|
|
807
|
+
* never reach the receiver at all. That is rule 208 stated as a property of the
|
|
808
|
+
* value rather than as a list of node kinds.
|
|
809
|
+
*/
|
|
810
|
+
function settlingValuePositions(expression) {
|
|
811
|
+
switch (expression.kind) {
|
|
812
|
+
case "ConditionalExpression":
|
|
813
|
+
return [expression.thenValue, expression.elseValue];
|
|
814
|
+
case "ListExpression":
|
|
815
|
+
return expression.elements.map((element) => element.kind === "SpreadExpression" ? element.value : element);
|
|
816
|
+
case "ObjectExpression":
|
|
817
|
+
return expression.properties.map((entry) => entry.value);
|
|
818
|
+
case "BinaryExpression":
|
|
819
|
+
return expression.operator === "??" ? [expression.left, expression.right] : [];
|
|
820
|
+
case "CallExpression":
|
|
821
|
+
return [
|
|
822
|
+
...(expression.callee.kind === "MemberExpression" ? [expression.callee.object] : []),
|
|
823
|
+
...expression.arguments.map((argument) => argument.kind === "SpreadExpression" ? argument.value : argument),
|
|
824
|
+
];
|
|
825
|
+
case "MemberExpression":
|
|
826
|
+
return [expression.object];
|
|
827
|
+
case "IndexExpression":
|
|
828
|
+
return [expression.object];
|
|
829
|
+
default:
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
}
|
|
458
833
|
export class Analyzer {
|
|
459
834
|
diagnostics = [];
|
|
835
|
+
advisories = [];
|
|
836
|
+
advisedIdentities = new Set();
|
|
460
837
|
scopes = [new Map()];
|
|
461
838
|
memberNarrowings = [new Map()];
|
|
839
|
+
/** Per scope depth, the names a narrowing has written there; see `narrowingsForVisibleBindings`. */
|
|
840
|
+
narrowedNames = [new Set()];
|
|
841
|
+
/** How many loop back-edge passes are running; see `reanalyzeLoopBackEdge`. */
|
|
842
|
+
loopReanalysisDepth = 0;
|
|
843
|
+
/** The "did you mean" roster, and the names each scope depth contributed to it. */
|
|
844
|
+
nearestNames = new NearestNameRoster();
|
|
845
|
+
scopedNames = [[]];
|
|
846
|
+
nearestNamesSeeded = false;
|
|
847
|
+
/** Per scope depth, the bindings flow analysis has written; see `snapshotFlowFacts`. */
|
|
848
|
+
flowTouched = [new Set()];
|
|
849
|
+
/** What each of those held before its first write, or null for a shadow born mid-flow. */
|
|
850
|
+
flowOrigins = new Map();
|
|
462
851
|
namedTypes = new Map();
|
|
463
852
|
namedTypeReadonlyFields = new Map();
|
|
464
853
|
namedTypeIdentities = new Map();
|
|
@@ -509,12 +898,10 @@ export class Analyzer {
|
|
|
509
898
|
classDisposeChains = new Map();
|
|
510
899
|
/** D68 rule 177: expression spans a consumer iterates through `@iterate:`. */
|
|
511
900
|
iterationContracts = new Set();
|
|
512
|
-
/**
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
*/
|
|
517
|
-
iterateContractDepths = [];
|
|
901
|
+
/** D90 R18: `async for` statements pulling a declared asynchronous `@iterate:`. */
|
|
902
|
+
asyncIterationStatements = new Set();
|
|
903
|
+
/** D90 R18: `@iterate:` blocks that are the asynchronous pull form, by keyword span. */
|
|
904
|
+
asyncIterateBlocks = new Set();
|
|
518
905
|
/** D51 rule 101: arrows that read a `using`-owned binding, by arrow span. */
|
|
519
906
|
arrowOwnedCaptures = new Map();
|
|
520
907
|
arrowCaptureFrames = [];
|
|
@@ -591,6 +978,22 @@ export class Analyzer {
|
|
|
591
978
|
inferredFunctionResultSeeds;
|
|
592
979
|
inferredFunctionResultTypes = new Map();
|
|
593
980
|
finalizeFunctionResultInference;
|
|
981
|
+
/**
|
|
982
|
+
* D85 rule 209: one mistake is reported once. A reported VEL4039 hands its
|
|
983
|
+
* position `invalidType`, and an invalid body-inferred result is otherwise a
|
|
984
|
+
* convergence failure — so the four collections below carry the *reported*
|
|
985
|
+
* hole forward, and only that hole, to the place VEL4025 is decided.
|
|
986
|
+
* `reportedCollectionHoles` are the names bound to one, `reportedResultHoles`
|
|
987
|
+
* the local function results that are one, `functionResultKeys` maps a
|
|
988
|
+
* callable name to the result a call to it reaches, and
|
|
989
|
+
* `deferredConvergenceReports` holds the reports whose answer needs a callee
|
|
990
|
+
* that may be declared further down the module.
|
|
991
|
+
*/
|
|
992
|
+
reportedCollectionHoles = new Set();
|
|
993
|
+
bindingHoleCauses = new Map();
|
|
994
|
+
reportedResultHoles = new Set();
|
|
995
|
+
functionResultKeys = new Map();
|
|
996
|
+
deferredConvergenceReports = [];
|
|
594
997
|
logicalConditionNarrowings = new Map();
|
|
595
998
|
privateFields = new Map();
|
|
596
999
|
privateGetters = new Map();
|
|
@@ -624,8 +1027,27 @@ export class Analyzer {
|
|
|
624
1027
|
/** Namespace import locals by name, known before signature validation runs (ENM-I9 teaching). */
|
|
625
1028
|
namespaceImportLocals = new Map();
|
|
626
1029
|
initializationImportReadSites = new Map();
|
|
1030
|
+
/**
|
|
1031
|
+
* D31 item 23's recorded residual: a top-level call of a module-local
|
|
1032
|
+
* function runs that body during module evaluation, so a read of an imported
|
|
1033
|
+
* binding inside it is an initialization-position read reached through one
|
|
1034
|
+
* hop. The stack is the function frame a deferred read belongs to; the map
|
|
1035
|
+
* is that frame by the name it was bound to, so a call resolves to it after
|
|
1036
|
+
* the whole module is analyzed and hoisting order stops mattering.
|
|
1037
|
+
*/
|
|
1038
|
+
deferredReadFrames = [];
|
|
1039
|
+
localFunctionFrames = new Map();
|
|
1040
|
+
arrowDeferredFrames = new Map();
|
|
1041
|
+
initializationLocalCalls = [];
|
|
627
1042
|
/** Local class bindings mapped to the source offset where their `class` statement evaluates (CLS-D8). */
|
|
628
1043
|
hoistedClassDeclarations = new Map();
|
|
1044
|
+
/**
|
|
1045
|
+
* D90 R12: public class members whose omitted annotation inferred an output
|
|
1046
|
+
* `any`. Whether the member is at an export position depends on whether a
|
|
1047
|
+
* consumer can reach its class, which is not settled until the whole module
|
|
1048
|
+
* is analyzed, so the report waits for reportExportPositionAny.
|
|
1049
|
+
*/
|
|
1050
|
+
exportPositionCandidates = [];
|
|
629
1051
|
/** Module-scope names bound to runtime Type objects (local and imported); see LoweringHints.runtimeTypeObjectNames. */
|
|
630
1052
|
runtimeTypeObjectNames = new Set();
|
|
631
1053
|
staticFieldInitialization = null;
|
|
@@ -690,16 +1112,20 @@ export class Analyzer {
|
|
|
690
1112
|
staticGetters: new Set(),
|
|
691
1113
|
staticMethods: new Map(),
|
|
692
1114
|
});
|
|
693
|
-
// ENM-U4 + COL-U5: the
|
|
1115
|
+
// ENM-U4 + COL-U5: the compiler-raised error types are nameable —
|
|
694
1116
|
// catchable, `is`-narrowable, and constructible — wired exactly like
|
|
695
1117
|
// Error. ValidationError additionally carries the failure detail its
|
|
696
|
-
// parse sites report (path, field, reason).
|
|
1118
|
+
// parse sites report (path, field, reason). AssertionError joins the
|
|
1119
|
+
// roster because the charter already promises it does: "A `catch` block
|
|
1120
|
+
// still receives all three, because a `catch` is explicit: the author
|
|
1121
|
+
// wrote code to handle it, and `is` names which one it was."
|
|
697
1122
|
const builtinErrorDetails = [
|
|
698
1123
|
["ValidationError", [
|
|
699
1124
|
["path", { mutable: false, type: optionalOf(stringType) }],
|
|
700
1125
|
["field", { mutable: false, type: optionalOf(stringType) }],
|
|
701
1126
|
["reason", { mutable: false, type: optionalOf(stringType) }],
|
|
702
1127
|
]],
|
|
1128
|
+
["AssertionError", []],
|
|
703
1129
|
["NarrowingError", []],
|
|
704
1130
|
["IndexError", []],
|
|
705
1131
|
// D50 rule 89: the capability failures a caller recovers from
|
|
@@ -826,18 +1252,38 @@ export class Analyzer {
|
|
|
826
1252
|
}
|
|
827
1253
|
return this.globalGuidance.get(name);
|
|
828
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* D90 (coherence): the one report an unresolved name earns, wherever it was
|
|
1257
|
+
* written. A reserved global names the module that replaced it, a foreign
|
|
1258
|
+
* builtin with no successor stops at the bare message rather than guessing,
|
|
1259
|
+
* and everything else may carry the nearest visible name. Both unresolved-
|
|
1260
|
+
* name sites reach this, because `exports = {run: run}` is the same mistake
|
|
1261
|
+
* as `const value = exports` and used to earn a strictly worse answer for
|
|
1262
|
+
* standing on the left of the `=`.
|
|
1263
|
+
*/
|
|
1264
|
+
reportUnresolvedName(name, span) {
|
|
1265
|
+
const guidance = this.guidanceForGlobal(name);
|
|
1266
|
+
const nearest = guidance || foreignBuiltinNames.has(name) ? null : this.nearestVisibleBindingName(name);
|
|
1267
|
+
this.diagnostics.push(diagnostic(guidance ? "VEL3008" : "VEL3001", guidance ?? `Unknown name '${name}'${nearest ? `; did you mean '${nearest}'?` : ""}`, span));
|
|
1268
|
+
}
|
|
829
1269
|
nearestVisibleBindingName(name) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1270
|
+
if (!this.nearestNamesSeeded) {
|
|
1271
|
+
this.nearestNamesSeeded = true;
|
|
1272
|
+
for (const candidate of [
|
|
1273
|
+
...Object.keys(coreVocabularyTypes),
|
|
1274
|
+
...this.extensionGlobals.keys(),
|
|
1275
|
+
...this.importBindings.keys(),
|
|
1276
|
+
"Map", "Set", "Error", "ValidationError", "AssertionError", "NarrowingError", "IndexError",
|
|
1277
|
+
...VELAR_HOST_ERROR_NAMES,
|
|
1278
|
+
])
|
|
1279
|
+
this.nearestNames.add(candidate);
|
|
1280
|
+
}
|
|
1281
|
+
return this.nearestNames.nearest(name);
|
|
1282
|
+
}
|
|
1283
|
+
/** Files a scope's name in the "did you mean" roster and takes it back out when the scope exits. */
|
|
1284
|
+
recordScopedName(name) {
|
|
1285
|
+
this.nearestNames.add(name);
|
|
1286
|
+
this.scopedNames.at(-1).push(name);
|
|
841
1287
|
}
|
|
842
1288
|
modulePath;
|
|
843
1289
|
importBindings;
|
|
@@ -873,14 +1319,31 @@ export class Analyzer {
|
|
|
873
1319
|
for (const statement of program.body) {
|
|
874
1320
|
this.analyzeStatement(statement);
|
|
875
1321
|
}
|
|
1322
|
+
// D90 R12 reports last for the same reason: whether a class member is at
|
|
1323
|
+
// an export position is a question about the module's whole export
|
|
1324
|
+
// surface, which no single declaration can answer as it is analyzed.
|
|
1325
|
+
this.reportExportPositionAny(program);
|
|
876
1326
|
// D52 rules 114/116: both namespace migrations report last, because both
|
|
877
1327
|
// rewrites need the whole module before they can be written down — one has
|
|
878
1328
|
// to know every name the new import would have to clear, and the other has
|
|
879
1329
|
// to know every read the retiring import leaves behind.
|
|
880
1330
|
this.reportRetiredNamespaceUses(program);
|
|
881
1331
|
this.reportPermanentNamespaceImports(program);
|
|
1332
|
+
this.reportPermanentNamespaceReExports(program);
|
|
1333
|
+
// D85 rule 209 reports last for the same reason: a hole reaches a caller
|
|
1334
|
+
// through a callee the module may not declare until later, so the second
|
|
1335
|
+
// report is deleted once every hole in the module is on record.
|
|
1336
|
+
this.resolveDeferredConvergenceReports();
|
|
882
1337
|
return this.diagnostics;
|
|
883
1338
|
}
|
|
1339
|
+
/**
|
|
1340
|
+
* D89: the advisories this analysis raised. `analyze` keeps returning the
|
|
1341
|
+
* diagnostics alone, so the caller reads the two channels separately and the
|
|
1342
|
+
* cursor arithmetic over `this.diagnostics` stays exact.
|
|
1343
|
+
*/
|
|
1344
|
+
analyzedAdvisories() {
|
|
1345
|
+
return this.advisories;
|
|
1346
|
+
}
|
|
884
1347
|
registerExternTypeImports(program) {
|
|
885
1348
|
const classesBySource = new Map();
|
|
886
1349
|
for (const statement of program.body) {
|
|
@@ -1113,6 +1576,12 @@ export class Analyzer {
|
|
|
1113
1576
|
}
|
|
1114
1577
|
else if (statement.kind === "FunctionDeclaration") {
|
|
1115
1578
|
this.declareBinding(statement.name, false, this.functionType(statement), statement.span);
|
|
1579
|
+
// D85 rule 209: the result a call to this name reaches, recorded before
|
|
1580
|
+
// any body is analyzed so a call to a function declared further down
|
|
1581
|
+
// the module still resolves to it.
|
|
1582
|
+
const callable = this.scopes.at(-1)?.get(statement.name);
|
|
1583
|
+
if (callable)
|
|
1584
|
+
this.functionResultKeys.set(callable, this.functionResultKey(statement));
|
|
1116
1585
|
this.predeclared.add(statement);
|
|
1117
1586
|
}
|
|
1118
1587
|
else if (this.predeclareExtensionStatement(statement)) {
|
|
@@ -1357,6 +1826,8 @@ export class Analyzer {
|
|
|
1357
1826
|
usingDisposals: this.usingDisposals,
|
|
1358
1827
|
classDisposeChains: this.classDisposeChains,
|
|
1359
1828
|
iterationContracts: this.iterationContracts,
|
|
1829
|
+
asyncIterationStatements: this.asyncIterationStatements,
|
|
1830
|
+
asyncIterateBlocks: this.asyncIterateBlocks,
|
|
1360
1831
|
moduleTopLevelHostCalls: this.moduleTopLevelHostCalls,
|
|
1361
1832
|
};
|
|
1362
1833
|
}
|
|
@@ -2310,8 +2781,10 @@ export class Analyzer {
|
|
|
2310
2781
|
// from the body — the same shape as an omitted function result, and it
|
|
2311
2782
|
// rides the same seeded convergence passes. The shape pre-pass seeds
|
|
2312
2783
|
// what the previous pass learned so a use written above the class sees
|
|
2313
|
-
// the real collection instead of the placeholder.
|
|
2314
|
-
|
|
2784
|
+
// the real collection instead of the placeholder. D90 R18: an optional
|
|
2785
|
+
// seed is the asynchronous pull form — a collection can never validate
|
|
2786
|
+
// to `T?` — so the seed's shape says which field it belongs in.
|
|
2787
|
+
...(statement.iterate ? this.seededIterationInfo(statement.iterate) : {}),
|
|
2315
2788
|
parameters: statement.parameters.map((parameter) => this.resolveValidatedAnnotation(parameter.type)),
|
|
2316
2789
|
parameterNames: statement.parameters.map((parameter) => parameter.name),
|
|
2317
2790
|
requiredParameters: statement.parameters.filter((parameter) => !parameter.defaultValue).length,
|
|
@@ -2596,8 +3069,26 @@ export class Analyzer {
|
|
|
2596
3069
|
const contract = annotationValid ? annotated ?? inferredStorage : invalidType;
|
|
2597
3070
|
if (annotationValid)
|
|
2598
3071
|
this.requireAssignable(actual, declared, statement.initializer.span);
|
|
2599
|
-
|
|
2600
|
-
|
|
3072
|
+
// D85 rule 209: the construction that just reported is invalid from
|
|
3073
|
+
// here on. Binding the name to the hole instead would reproduce
|
|
3074
|
+
// `Cannot assign List<unknown> to ...` on a later line that has no
|
|
3075
|
+
// `[]` in it — the second, contradicting report the ruling deletes.
|
|
3076
|
+
// D90 R12: `any` may not cross a module boundary. The written spelling
|
|
3077
|
+
// is already refused by validateTypeReference above, so only the
|
|
3078
|
+
// inferred one reaches here — and that asymmetry was the defect, since
|
|
3079
|
+
// the spelling that got refused is the honest one. Checking the
|
|
3080
|
+
// settled type before declarePattern covers every pattern shape at
|
|
3081
|
+
// once, including `export const {a, b} = thing`.
|
|
3082
|
+
if (statement.exported && annotationValid && typeContainsAnyOutput(declared)) {
|
|
3083
|
+
const exported = [];
|
|
3084
|
+
this.collectPatternNames(statement.pattern, (name) => exported.push(name));
|
|
3085
|
+
this.reportExportedAny(exported, statement.span);
|
|
3086
|
+
}
|
|
3087
|
+
const unsettled = this.requireSettledCollectionElement(statement.initializer, declared, annotated !== null);
|
|
3088
|
+
this.declarePattern(statement.pattern, statement.binding === "let", unsettled ? invalidType : declared, unsettled ? invalidType : contract);
|
|
3089
|
+
if (annotated === null)
|
|
3090
|
+
this.recordBindingHoleSource(statement.pattern, statement.initializer, unsettled);
|
|
3091
|
+
this.claimArrowDeferredFrame(statement.pattern, statement.initializer);
|
|
2601
3092
|
// D51 rule 101: an alias of an owned handle — or a closure over one —
|
|
2602
3093
|
// is the same resource under a second name, so it inherits the
|
|
2603
3094
|
// ownership and the escape check follows it.
|
|
@@ -2669,10 +3160,35 @@ export class Analyzer {
|
|
|
2669
3160
|
this.asyncResolvedValues.add(spanIdentity(statement.value.span));
|
|
2670
3161
|
}
|
|
2671
3162
|
}
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
3163
|
+
if (inferredReturns) {
|
|
3164
|
+
// D85 rule 207: a body-inferred result has no annotation to settle
|
|
3165
|
+
// an empty collection, and the name that reads it can be in another
|
|
3166
|
+
// module — `export def make(): return []` publishes `List<unknown>`
|
|
3167
|
+
// across the interface. An annotated result is a contextual type and
|
|
3168
|
+
// settles the construction before it ever gets here. Rule 209: a
|
|
3169
|
+
// reported hole contributes `invalidType`, so the caller's
|
|
3170
|
+
// `const values: List<string> = make()` does not report the same
|
|
3171
|
+
// mistake a second time in a line that has no `[]` in it.
|
|
3172
|
+
const unsettled = statement.value !== null && this.requireSettledCollectionElement(statement.value, actual, false);
|
|
3173
|
+
if (returnContext) {
|
|
3174
|
+
// The hole reaches the result through whatever the author wrote
|
|
3175
|
+
// between it and the `return` — a name, a chain of them, or a call
|
|
3176
|
+
// to a local function whose own VEL4039 already reported. Each of
|
|
3177
|
+
// those is the same one mistake, so the convergence failure it
|
|
3178
|
+
// produces below is not a second problem to report.
|
|
3179
|
+
const causes = returnContext.resultHoleCauses ?? new Set();
|
|
3180
|
+
const carried = statement.value !== null && this.collectResultHoleSources(statement.value, causes);
|
|
3181
|
+
if (causes.size > 0)
|
|
3182
|
+
returnContext.resultHoleCauses = causes;
|
|
3183
|
+
if (unsettled || carried)
|
|
3184
|
+
returnContext.unsettledResult = true;
|
|
3185
|
+
}
|
|
3186
|
+
if (this.unreachableDiagnosticDepth === 0)
|
|
3187
|
+
inferredReturns.push(unsettled ? invalidType : returned);
|
|
2675
3188
|
break;
|
|
3189
|
+
}
|
|
3190
|
+
if (this.unreachableDiagnosticDepth === 0)
|
|
3191
|
+
returnContext?.observedReturns?.push(returned);
|
|
2676
3192
|
this.requireAssignable(returned, expected, statement.value?.span ?? statement.span);
|
|
2677
3193
|
break;
|
|
2678
3194
|
}
|
|
@@ -2976,11 +3492,10 @@ export class Analyzer {
|
|
|
2976
3492
|
&& this.builtinValueReferences.get(spanIdentity(statement.iterable.callee.span)) === "range") {
|
|
2977
3493
|
this.nativeRangeForStatements.add(statement.span.start);
|
|
2978
3494
|
}
|
|
2979
|
-
// D68 rule 177
|
|
2980
|
-
//
|
|
2981
|
-
//
|
|
2982
|
-
//
|
|
2983
|
-
// synchronous side only and `async for` keeps refusing user types.
|
|
3495
|
+
// D68 rule 177 + D90 R18: the eight plain consumers project through
|
|
3496
|
+
// the synchronous `@iterate:` answer; `async for` reads the
|
|
3497
|
+
// asynchronous form's declaration inside asyncPullElementType, so the
|
|
3498
|
+
// asynchronous side takes the operand unprojected.
|
|
2984
3499
|
const iterable = statement.asynchronous ? inferredIterable : this.iterationSource(statement.iterable, inferredIterable);
|
|
2985
3500
|
if (!statement.asynchronous
|
|
2986
3501
|
&& (iterable.kind === "list" || iterable.kind === "map" || iterable.kind === "set" || iterable.kind === "record" || iterable.kind === "string")) {
|
|
@@ -3001,7 +3516,7 @@ export class Analyzer {
|
|
|
3001
3516
|
? this.invalidExtensionAwaitMessage() ?? "'async for' is not valid in this synchronous extension context"
|
|
3002
3517
|
: "'async for' can only be used in an async function or at module scope", statement.span));
|
|
3003
3518
|
}
|
|
3004
|
-
first = this.asyncPullElementType(iterable, statement.iterable.span);
|
|
3519
|
+
first = this.asyncPullElementType(iterable, statement.iterable.span, statement.span.start);
|
|
3005
3520
|
second = numberType;
|
|
3006
3521
|
this.asyncForStatements.add(statement.span.start);
|
|
3007
3522
|
}
|
|
@@ -3020,6 +3535,8 @@ export class Analyzer {
|
|
|
3020
3535
|
: `Cannot iterate over ${describeType(iterable)}${this.iterationGuidance(iterable)}`, statement.iterable.span);
|
|
3021
3536
|
}
|
|
3022
3537
|
}
|
|
3538
|
+
if (!statement.asynchronous)
|
|
3539
|
+
this.adviseSwappedLoopSlots(statement, iterable);
|
|
3023
3540
|
const baseline = this.snapshotFlowFacts();
|
|
3024
3541
|
this.loopFlowContexts.push({ baseline, visible: this.visibleBindings(), carried: [], backEdges: [], breakFacts: [], sawBreak: false });
|
|
3025
3542
|
const diagnosticStart = this.diagnostics.length;
|
|
@@ -3095,7 +3612,7 @@ export class Analyzer {
|
|
|
3095
3612
|
// FLW-S1: a loop the body can re-enter tests its condition again in
|
|
3096
3613
|
// the back-edge state, so the exit fact is what both tests agree on.
|
|
3097
3614
|
let repeatedFalsy = null;
|
|
3098
|
-
const
|
|
3615
|
+
const backEdgePass = this.reanalyzeLoopBackEdge(baseline, loopFlow.visible, backEdges, statement.body, diagnosticStart, () => {
|
|
3099
3616
|
this.clearCachedFlowTypesInSpan(statement.condition.span);
|
|
3100
3617
|
const repeatedCondition = this.inferExpression(statement.condition);
|
|
3101
3618
|
this.requireCondition(repeatedCondition, statement.condition);
|
|
@@ -3124,15 +3641,19 @@ export class Analyzer {
|
|
|
3124
3641
|
// that always returns. A break can leave while the condition still
|
|
3125
3642
|
// holds, so one break drops the fact entirely.
|
|
3126
3643
|
if (!loopFlow.sawBreak) {
|
|
3127
|
-
|
|
3644
|
+
// A widened exit confirmed nothing about the back edge, so it keeps
|
|
3645
|
+
// nothing: the condition's fact holds only if the second test agrees.
|
|
3646
|
+
this.persistNarrowings(backEdgePass.widened
|
|
3647
|
+
? new Map()
|
|
3648
|
+
: repeatedFalsy === null ? falsy : this.joinedNarrowings(falsy, repeatedFalsy));
|
|
3128
3649
|
}
|
|
3129
3650
|
else if (statement.condition.kind === "LiteralExpression" && statement.condition.value === true) {
|
|
3130
3651
|
// FLW-N6: `while true:` has no failing condition, so its breaks are
|
|
3131
3652
|
// its only exits, and what every one of them proves holds after the
|
|
3132
3653
|
// loop. A loop whose condition can also fail keeps nothing: that
|
|
3133
3654
|
// exit proves none of it.
|
|
3134
|
-
const breakFacts = [...loopFlow.breakFacts, ...(
|
|
3135
|
-
if (breakFacts.length > 0)
|
|
3655
|
+
const breakFacts = [...loopFlow.breakFacts, ...(backEdgePass.repeated?.breakFacts ?? [])];
|
|
3656
|
+
if (breakFacts.length > 0 && !backEdgePass.widened)
|
|
3136
3657
|
this.persistNarrowings(this.commonNarrowings(breakFacts));
|
|
3137
3658
|
}
|
|
3138
3659
|
break;
|
|
@@ -3254,6 +3775,33 @@ export class Analyzer {
|
|
|
3254
3775
|
break;
|
|
3255
3776
|
}
|
|
3256
3777
|
}
|
|
3778
|
+
/**
|
|
3779
|
+
* D89 A2: the two-slot `for` over a List, Set, or string binds
|
|
3780
|
+
* `value, index`, which matches JavaScript's `forEach((v, i) => …)` and
|
|
3781
|
+
* inverts Python's `enumerate`. Python's own spelling is already a loud
|
|
3782
|
+
* error, so nothing silent comes from it; the silence happens when a model
|
|
3783
|
+
* writes `for i, v in nums`, a hybrid neither language has, and both names
|
|
3784
|
+
* quietly hold the other one's value.
|
|
3785
|
+
*
|
|
3786
|
+
* Both rosters must hit. One name alone proves nothing — `for index, total
|
|
3787
|
+
* in scores` may be counting exactly what it says — and a wrong guess here
|
|
3788
|
+
* would tell a correct author to break working code. The value slot also
|
|
3789
|
+
* accepts the singular of the collection's own name, because `for i, user
|
|
3790
|
+
* in users` is the same reflex spelled from the data instead of a letter.
|
|
3791
|
+
*/
|
|
3792
|
+
adviseSwappedLoopSlots(statement, iterable) {
|
|
3793
|
+
if (iterable.kind !== "list" && iterable.kind !== "set" && iterable.kind !== "string")
|
|
3794
|
+
return;
|
|
3795
|
+
const indexSlot = statement.pattern;
|
|
3796
|
+
const valueSlot = statement.secondPattern;
|
|
3797
|
+
if (indexSlot.kind !== "NameBindingPattern" || valueSlot?.kind !== "NameBindingPattern")
|
|
3798
|
+
return;
|
|
3799
|
+
if (!loopIndexSlotNames.has(indexSlot.name))
|
|
3800
|
+
return;
|
|
3801
|
+
if (!loopValueSlotNames.has(valueSlot.name) && valueSlot.name !== singularIterableName(statement.iterable))
|
|
3802
|
+
return;
|
|
3803
|
+
this.advise("A2", `A two-slot 'for' binds 'value, index', so '${indexSlot.name}' receives the element and '${valueSlot.name}' receives the position; write 'for ${valueSlot.name}, ${indexSlot.name} in ...' to bind them the way the names read`, span(indexSlot.span.start, valueSlot.span.end), mechanicalEdits([{ span: indexSlot.span, text: valueSlot.name }, { span: valueSlot.span, text: indexSlot.name }], `Swap '${indexSlot.name}' and '${valueSlot.name}'`));
|
|
3804
|
+
}
|
|
3257
3805
|
// D32 item 30: a Promise-typed expression statement is a floating promise —
|
|
3258
3806
|
// nothing waits for it and nothing owns its failure. The diagnostic teaches
|
|
3259
3807
|
// both current spellings: 'await' waits, the 'async' statement detaches.
|
|
@@ -3318,7 +3866,19 @@ export class Analyzer {
|
|
|
3318
3866
|
// The operation tables already prove which method the call lowered to, so
|
|
3319
3867
|
// the check needs no user-function purity analysis.
|
|
3320
3868
|
checkDiscardedPureResult(expression) {
|
|
3321
|
-
if (expression.kind !== "CallExpression"
|
|
3869
|
+
if (expression.kind !== "CallExpression")
|
|
3870
|
+
return;
|
|
3871
|
+
// D29 item 14's own rationale reaches `expect(...)` with no matcher:
|
|
3872
|
+
// building an expectation object and never asking it anything throws the
|
|
3873
|
+
// only product away, and the statement reads as an assertion that passes.
|
|
3874
|
+
// D30 item 17's general CallExpression exemption is untouched — a bare
|
|
3875
|
+
// call may perform an effect — because `testExpectOperands` already
|
|
3876
|
+
// proves this one call lowered to `test.expect`, which performs none.
|
|
3877
|
+
if (this.testExpectOperands.has(spanIdentity(expression.span))) {
|
|
3878
|
+
this.diagnostics.push(diagnostic("VEL4030", "'expect(...)' builds an expectation and asserts nothing on its own; add a matcher such as '.toBe(expected)'", expression.span));
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (expression.callee.kind !== "MemberExpression")
|
|
3322
3882
|
return;
|
|
3323
3883
|
const collectionOperation = this.collectionCalls.get(expression.callee.span.end);
|
|
3324
3884
|
const primitiveOperation = this.primitiveCalls.get(expression.callee.span.end);
|
|
@@ -3403,11 +3963,13 @@ export class Analyzer {
|
|
|
3403
3963
|
const baseName = statement.base?.name ?? null;
|
|
3404
3964
|
if (baseName) {
|
|
3405
3965
|
const baseBinding = this.lookup(baseName) ?? this.builtin(baseName);
|
|
3406
|
-
if (baseName === "ValidationError" || baseName === "
|
|
3966
|
+
if (baseName === "ValidationError" || baseName === "AssertionError" || baseName === "NarrowingError"
|
|
3967
|
+
|| baseName === "IndexError"
|
|
3407
3968
|
|| VELAR_HOST_ERROR_NAMES.includes(baseName)) {
|
|
3408
3969
|
// The compiler-raised error types are leaf contracts: user subclasses
|
|
3409
|
-
// would dilute what a caught ValidationError/
|
|
3410
|
-
// proves. Extend Error for custom
|
|
3970
|
+
// would dilute what a caught ValidationError/AssertionError/
|
|
3971
|
+
// NarrowingError/IndexError proves. Extend Error for custom
|
|
3972
|
+
// hierarchies.
|
|
3411
3973
|
this.typeError(`The builtin error type '${baseName}' cannot be extended; extend Error and declare your own fields`, statement.base.span);
|
|
3412
3974
|
}
|
|
3413
3975
|
else if (baseBinding?.type.kind === "classConstructor" && !this.classes.has(baseName)
|
|
@@ -3437,7 +3999,18 @@ export class Analyzer {
|
|
|
3437
3999
|
this.enterScope();
|
|
3438
4000
|
this.flowFrameDepth += 1;
|
|
3439
4001
|
this.superMemberContext = "instance";
|
|
3440
|
-
for (const parameter of statement.parameters) {
|
|
4002
|
+
for (const [index, parameter] of statement.parameters.entries()) {
|
|
4003
|
+
// D89 (message correction): `constructor(self, ...)` is the same Python
|
|
4004
|
+
// receiver reflex a method's `self` parameter is, and it used to land on
|
|
4005
|
+
// the bare reserved-binding refusal, which names no fix. A field-binding
|
|
4006
|
+
// spelling (`const self`) is excluded because its `const`/`private`
|
|
4007
|
+
// prefix sits outside the parameter span, so the deletion this report
|
|
4008
|
+
// carries would leave the prefix stranded — and D38 §48 admits only
|
|
4009
|
+
// rewrites that land on working source.
|
|
4010
|
+
if (parameter.name === "self" && !parameter.rest && parameter.binding === null && statement.initialization !== null) {
|
|
4011
|
+
this.reportImplicitSelfParameter(statement.parameters, index);
|
|
4012
|
+
continue;
|
|
4013
|
+
}
|
|
3441
4014
|
const type = this.resolveAnnotation(parameter.type);
|
|
3442
4015
|
const valid = parameter.type ? this.validateTypeReference(parameter.type) : true;
|
|
3443
4016
|
if (parameter.defaultValue && valid) {
|
|
@@ -4058,14 +4631,30 @@ export class Analyzer {
|
|
|
4058
4631
|
seededIterationSource(block) {
|
|
4059
4632
|
return this.inferredFunctionResultSeeds.get(this.iterationResultKey(block)) ?? inferredResultPlaceholderType;
|
|
4060
4633
|
}
|
|
4634
|
+
/**
|
|
4635
|
+
* D90 R18: the seed routed to the field its form owns. An optional seed can
|
|
4636
|
+
* only have come from the asynchronous pull form — the synchronous form
|
|
4637
|
+
* never validates to `T?` — so the shape pre-pass reads the form off the
|
|
4638
|
+
* seed the previous convergence pass learned.
|
|
4639
|
+
*/
|
|
4640
|
+
seededIterationInfo(block) {
|
|
4641
|
+
const seed = this.seededIterationSource(block);
|
|
4642
|
+
const expanded = this.expandAliases(seed);
|
|
4643
|
+
return expanded.kind === "optional" ? { iterateAsync: expanded.inner } : { iterate: seed };
|
|
4644
|
+
}
|
|
4061
4645
|
/**
|
|
4062
4646
|
* `@iterate:` answers the compiler's question "what does
|
|
4063
|
-
* iterating you mean?"
|
|
4064
|
-
*
|
|
4065
|
-
*
|
|
4066
|
-
*
|
|
4647
|
+
* iterating you mean?". It shares `@dispose:`'s compiler-name path, then
|
|
4648
|
+
* supplies its own role: it is a contract, not a method, and it produces a
|
|
4649
|
+
* value. D90 R18 gives it two forms, told apart by the answer's shape the
|
|
4650
|
+
* same way `@dispose:`'s async-ness is read off its own body: the
|
|
4651
|
+
* synchronous form answers a collection the language already iterates and
|
|
4652
|
+
* the eight plain consumers read it once; the asynchronous pull form
|
|
4653
|
+
* answers `T?` — `async for` drives it once per element, it may await, and
|
|
4654
|
+
* null is exhaustion.
|
|
4067
4655
|
*/
|
|
4068
4656
|
analyzeClassIterate(statement, block, baseName) {
|
|
4657
|
+
const awaits = blockContainsDirectAwait(block.body, (expression, contains) => this.extensionExpressionContainsDirectAwait(expression, contains), (owned, containsExpression, containsBlock) => this.extensionStatementContainsDirectAwait(owned, containsExpression, containsBlock));
|
|
4069
4658
|
this.enterScope();
|
|
4070
4659
|
this.flowFrameDepth += 1;
|
|
4071
4660
|
this.functionDepth += 1;
|
|
@@ -4077,13 +4666,14 @@ export class Analyzer {
|
|
|
4077
4666
|
const previousSuperMemberContext = this.superMemberContext;
|
|
4078
4667
|
this.currentClass = statement.name;
|
|
4079
4668
|
this.superMemberContext = "instance";
|
|
4080
|
-
|
|
4081
|
-
|
|
4669
|
+
// A block that awaits is the asynchronous form (the same reading
|
|
4670
|
+
// `@dispose:` gets), so its awaits are legal; a block without one has
|
|
4671
|
+
// nothing for the flag to allow.
|
|
4672
|
+
this.asynchronousFunctions.push(awaits);
|
|
4082
4673
|
const inferredReturns = [];
|
|
4083
4674
|
this.returnContexts.push({ expected: unknownType, inferredReturns, observedReturns: null, declarationKind: "Iteration contract" });
|
|
4084
4675
|
this.declareBinding("self", false, { kind: "class", name: statement.name }, block.span, true);
|
|
4085
4676
|
this.analyzeStatements(block.body);
|
|
4086
|
-
this.iterateContractDepths.pop();
|
|
4087
4677
|
this.returnContexts.pop();
|
|
4088
4678
|
this.asynchronousFunctions.pop();
|
|
4089
4679
|
this.currentClass = previousClass;
|
|
@@ -4094,37 +4684,80 @@ export class Analyzer {
|
|
|
4094
4684
|
this.flowFrameDepth -= 1;
|
|
4095
4685
|
this.exitScope();
|
|
4096
4686
|
const answered = this.inferCollectedFunctionResult(inferredReturns, !this.blockAlwaysReturns(block.body));
|
|
4097
|
-
const
|
|
4098
|
-
|
|
4687
|
+
const validated = this.validatedIterationSource(statement, block, answered, baseName, awaits);
|
|
4688
|
+
// D90 R12: `@iterate:` is the class's other inferred public contract. A
|
|
4689
|
+
// consumer writing `for item in box` reads the element straight out of
|
|
4690
|
+
// this block, so an element the compiler makes no promise about crosses
|
|
4691
|
+
// the boundary exactly as a method result does. The block has no
|
|
4692
|
+
// annotation to refuse and no `private` spelling, so the class's own
|
|
4693
|
+
// reachability is the whole question.
|
|
4694
|
+
if (typeContainsAnyOutput(validated.source)) {
|
|
4695
|
+
this.exportPositionCandidates.push({ className: statement.name, member: "@iterate", span: block.span });
|
|
4696
|
+
}
|
|
4697
|
+
// The stored result keeps the optional wrapper for the asynchronous form
|
|
4698
|
+
// so the convergence seed round-trips carrying the form (see
|
|
4699
|
+
// seededIterationInfo).
|
|
4700
|
+
this.inferredFunctionResultTypes.set(this.iterationResultKey(block), validated.form === "async" && !isInvalidType(validated.source) ? optionalOf(validated.source) : validated.source);
|
|
4701
|
+
if (validated.form === "async")
|
|
4702
|
+
this.asyncIterateBlocks.add(spanIdentity(block.keywordSpan));
|
|
4099
4703
|
const info = this.classes.get(statement.name);
|
|
4100
|
-
if (info)
|
|
4101
|
-
|
|
4704
|
+
if (info) {
|
|
4705
|
+
// Drop the other form's field: an earlier pass may have seeded it before
|
|
4706
|
+
// this pass's answer settled which form the block is.
|
|
4707
|
+
const { iterate: _sync, iterateAsync: _async, ...rest } = info;
|
|
4708
|
+
this.classes.set(statement.name, validated.form === "async"
|
|
4709
|
+
? { ...rest, iterateAsync: validated.source }
|
|
4710
|
+
: { ...rest, iterate: validated.source });
|
|
4711
|
+
}
|
|
4102
4712
|
}
|
|
4103
4713
|
/**
|
|
4104
|
-
* The
|
|
4105
|
-
* me is iterating this", and the language
|
|
4106
|
-
* List, Set, Map, or Record means
|
|
4107
|
-
*
|
|
4714
|
+
* The answer space is the four collections plus `T?` (D90 R18): the
|
|
4715
|
+
* synchronous form says "iterating me is iterating this", and the language
|
|
4716
|
+
* already fixed what iterating a List, Set, Map, or Record means; the
|
|
4717
|
+
* asynchronous pull form answers one element per pull, null for exhaustion.
|
|
4718
|
+
* Anything else would be a second iteration semantics, which is the thing
|
|
4719
|
+
* charter section 19 keeps out.
|
|
4108
4720
|
*/
|
|
4109
|
-
validatedIterationSource(statement, block, answered, baseName) {
|
|
4721
|
+
validatedIterationSource(statement, block, answered, baseName, awaits) {
|
|
4110
4722
|
if (isInvalidType(answered) || containsInferredResultPlaceholder(answered))
|
|
4111
|
-
return invalidType;
|
|
4723
|
+
return { form: "sync", source: invalidType };
|
|
4112
4724
|
const expanded = this.expandAliases(answered);
|
|
4113
|
-
if (expanded.kind !== "list" && expanded.kind !== "set" && expanded.kind !== "map" && expanded.kind !== "record") {
|
|
4114
|
-
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' says which collection iterating '${statement.name}' means, so it returns a List, Set, Map, or Record — those are the shapes the language already knows how to iterate; this block returns ${describeType(answered)}`, block.keywordSpan));
|
|
4115
|
-
return invalidType;
|
|
4116
|
-
}
|
|
4117
4725
|
// The override rule every other member already carries (a getter or method
|
|
4118
4726
|
// override keeps the base result). `@iterate:` replaces rather than chains,
|
|
4119
4727
|
// but the answer still has to be the one a base-typed binding was promised:
|
|
4120
4728
|
// `for item in bag` inside a function taking the base would otherwise walk
|
|
4121
|
-
// a different element type at runtime.
|
|
4729
|
+
// a different element type — or a different form — at runtime.
|
|
4122
4730
|
const inherited = baseName ? this.inheritedIterationSource(baseName) : null;
|
|
4731
|
+
const inheritedAsync = baseName ? this.inheritedAsyncIterationSource(baseName) : null;
|
|
4732
|
+
if (expanded.kind === "optional") {
|
|
4733
|
+
const element = expanded.inner;
|
|
4734
|
+
if (inherited && !isInvalidType(inherited)) {
|
|
4735
|
+
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' override in '${statement.name}' must keep the base form; '${baseName}' answers ${describeType(inherited)} to the plain 'for', and this block answers ${describeType(answered)} — the asynchronous pull form — so a base-typed binding would stream where it was promised a collection`, block.keywordSpan));
|
|
4736
|
+
return { form: "sync", source: inherited };
|
|
4737
|
+
}
|
|
4738
|
+
if (inheritedAsync && !isInvalidType(inheritedAsync) && !sameType(this.expandAliases(element), this.expandAliases(inheritedAsync))) {
|
|
4739
|
+
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' override in '${statement.name}' must keep the base answer ${describeType(inheritedAsync)}?; '${baseName}' already promised every caller that pulling one of these yields ${describeType(inheritedAsync)}, and a derived value is still one of those`, block.keywordSpan));
|
|
4740
|
+
return { form: "async", source: inheritedAsync };
|
|
4741
|
+
}
|
|
4742
|
+
return { form: "async", source: element };
|
|
4743
|
+
}
|
|
4744
|
+
if (expanded.kind !== "list" && expanded.kind !== "set" && expanded.kind !== "map" && expanded.kind !== "record") {
|
|
4745
|
+
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' says what iterating '${statement.name}' means: the synchronous form returns a List, Set, Map, or Record — the shapes the language already knows how to iterate — and the asynchronous pull form answers 'T?', one element per pull with null as exhaustion; this block returns ${describeType(answered)}`, block.keywordSpan));
|
|
4746
|
+
return { form: "sync", source: invalidType };
|
|
4747
|
+
}
|
|
4748
|
+
if (awaits) {
|
|
4749
|
+
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' in '${statement.name}' awaits but answers ${describeType(answered)}; the synchronous form is read whole by the plain consumers, so await the work before construction and hold the finished collection — or answer 'T?' to be the asynchronous pull form 'async for' drives once per element`, block.keywordSpan));
|
|
4750
|
+
return { form: "sync", source: invalidType };
|
|
4751
|
+
}
|
|
4752
|
+
if (inheritedAsync && !isInvalidType(inheritedAsync)) {
|
|
4753
|
+
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' override in '${statement.name}' must keep the base form; '${baseName}' answers ${describeType(inheritedAsync)}? — the asynchronous pull form — and this block answers ${describeType(answered)}, so a base-typed binding would read a collection where it was promised a stream`, block.keywordSpan));
|
|
4754
|
+
return { form: "async", source: inheritedAsync };
|
|
4755
|
+
}
|
|
4123
4756
|
if (inherited && !isInvalidType(inherited) && !sameType(expanded, this.expandAliases(inherited))) {
|
|
4124
4757
|
this.diagnostics.push(diagnostic("VEL4038", `'@iterate' override in '${statement.name}' must keep the base answer ${describeType(inherited)}; '${baseName}' already promised every caller that iterating one of these walks ${describeType(inherited)}, and a derived value is still one of those`, block.keywordSpan));
|
|
4125
|
-
return inherited;
|
|
4758
|
+
return { form: "sync", source: inherited };
|
|
4126
4759
|
}
|
|
4127
|
-
return expanded;
|
|
4760
|
+
return { form: "sync", source: expanded };
|
|
4128
4761
|
}
|
|
4129
4762
|
/** The `@iterate:` answer a class inherits, most derived ancestor first. */
|
|
4130
4763
|
inheritedIterationSource(className) {
|
|
@@ -4139,6 +4772,30 @@ export class Analyzer {
|
|
|
4139
4772
|
}
|
|
4140
4773
|
return null;
|
|
4141
4774
|
}
|
|
4775
|
+
/** D90 R18: the asynchronous `@iterate:` element a class inherits, most derived ancestor first. */
|
|
4776
|
+
inheritedAsyncIterationSource(className) {
|
|
4777
|
+
let current = className;
|
|
4778
|
+
const visited = new Set();
|
|
4779
|
+
while (current && !visited.has(current)) {
|
|
4780
|
+
visited.add(current);
|
|
4781
|
+
const info = this.classes.get(current);
|
|
4782
|
+
if (info?.iterateAsync)
|
|
4783
|
+
return info.iterateAsync;
|
|
4784
|
+
current = info?.base ?? null;
|
|
4785
|
+
}
|
|
4786
|
+
return null;
|
|
4787
|
+
}
|
|
4788
|
+
/**
|
|
4789
|
+
* D90 R18: what pulling this value under `async for` means. A class answers
|
|
4790
|
+
* through the asynchronous `@iterate:` form — its own, or the one it
|
|
4791
|
+
* inherits, mirroring the synchronous contract exactly.
|
|
4792
|
+
*/
|
|
4793
|
+
asyncIterationContract(type) {
|
|
4794
|
+
const resolved = this.resolveNamedClasses(this.expandAliases(type));
|
|
4795
|
+
if (resolved.kind !== "class")
|
|
4796
|
+
return null;
|
|
4797
|
+
return this.inheritedAsyncIterationSource(resolved.identity ?? resolved.name);
|
|
4798
|
+
}
|
|
4142
4799
|
/**
|
|
4143
4800
|
* D68 rule 177: what iterating this value means. A class answers through
|
|
4144
4801
|
* `@iterate:` — its own, or the one it inherits, because overriding replaces
|
|
@@ -4178,6 +4835,12 @@ export class Analyzer {
|
|
|
4178
4835
|
}
|
|
4179
4836
|
if (this.iterationContract(resolved) !== null)
|
|
4180
4837
|
return "";
|
|
4838
|
+
// D90 R18: the refusal is symmetric with `async for` refusing the
|
|
4839
|
+
// synchronous form — each names the other, so the author is one message
|
|
4840
|
+
// away from the loop that fits the declaration.
|
|
4841
|
+
if (this.asyncIterationContract(resolved) !== null) {
|
|
4842
|
+
return "; '@iterate' on this class is the asynchronous pull form, which 'async for' drives — use 'async for', or answer a List, Set, Map, or Record to iterate here";
|
|
4843
|
+
}
|
|
4181
4844
|
return "; declare an '@iterate:' block on the class to say which List, Set, Map, or Record iterating it means";
|
|
4182
4845
|
}
|
|
4183
4846
|
validateConstructorShape(statement) {
|
|
@@ -4290,12 +4953,37 @@ export class Analyzer {
|
|
|
4290
4953
|
}
|
|
4291
4954
|
return null;
|
|
4292
4955
|
}
|
|
4293
|
-
asyncPullElementType(source, sourceSpan) {
|
|
4956
|
+
asyncPullElementType(source, sourceSpan, statementStart) {
|
|
4294
4957
|
const expanded = this.resolveNamedClasses(this.expandAliases(source));
|
|
4295
4958
|
if (expanded.kind === "any")
|
|
4296
4959
|
return anyType;
|
|
4297
4960
|
if (isInvalidType(expanded))
|
|
4298
4961
|
return invalidType;
|
|
4962
|
+
// D90 R18: a VelarScript class declares itself an asynchronous stream
|
|
4963
|
+
// through the asynchronous `@iterate:` form, exactly as it declares the
|
|
4964
|
+
// synchronous one — `async for` reads the declaration, never a structural
|
|
4965
|
+
// resemblance. The structural `next() -> Promise<T?>` pull below stays the
|
|
4966
|
+
// contract of the declared foreign shapes: capability handles (a reply
|
|
4967
|
+
// stream, a child process, a watcher) and extern classes whose own
|
|
4968
|
+
// contract declares the pull as a function-valued field.
|
|
4969
|
+
if (expanded.kind === "class" && !isExternClassIdentity(expanded.identity ?? null)) {
|
|
4970
|
+
const declared = this.asyncIterationContract(expanded);
|
|
4971
|
+
if (declared !== null) {
|
|
4972
|
+
if (isInvalidType(declared))
|
|
4973
|
+
return invalidType;
|
|
4974
|
+
this.asyncIterationStatements.add(statementStart);
|
|
4975
|
+
return declared;
|
|
4976
|
+
}
|
|
4977
|
+
const identity = expanded.identity ?? expanded.name;
|
|
4978
|
+
const synchronous = this.iterationContract(expanded);
|
|
4979
|
+
if (synchronous !== null) {
|
|
4980
|
+
this.typeError(`async for pulls a declared asynchronous '@iterate:'; '@iterate' on ${describeType(source)} ${isInvalidType(synchronous) ? "answers the plain 'for'" : `answers ${describeType(synchronous)} to the plain 'for'`} — declare the asynchronous form instead: a block that answers 'T?', one element per pull, null as exhaustion`, sourceSpan);
|
|
4981
|
+
return unknownType;
|
|
4982
|
+
}
|
|
4983
|
+
const structuralNext = this.findMethod(identity, "next")?.type ?? this.findMethod(expanded.name, "next")?.type ?? null;
|
|
4984
|
+
this.typeError(`async for pulls a declared asynchronous '@iterate:'; ${describeType(source)} does not declare one — a block that answers 'T?' (it may await; one element per pull, null is exhaustion)${structuralNext ? "; 'next()' is a method of the author's namespace, not the contract — move its body into the '@iterate:' block" : ""}`, sourceSpan);
|
|
4985
|
+
return unknownType;
|
|
4986
|
+
}
|
|
4299
4987
|
let next = null;
|
|
4300
4988
|
if (expanded.kind === "object") {
|
|
4301
4989
|
next = expanded.optionalFields?.has("next") ? null : expanded.fields.get("next") ?? null;
|
|
@@ -4307,23 +4995,17 @@ export class Analyzer {
|
|
|
4307
4995
|
?? null;
|
|
4308
4996
|
}
|
|
4309
4997
|
else if (expanded.kind === "class") {
|
|
4310
|
-
|
|
4311
|
-
|
|
4998
|
+
// An extern class: its own contract may declare the pull as a
|
|
4999
|
+
// function-valued field; an extern method is never captured (charter
|
|
5000
|
+
// section 12 trusts a checked declaration's member kinds, and only a
|
|
5001
|
+
// field promises a function standing on the value).
|
|
5002
|
+
next = this.findField(expanded.identity ?? expanded.name, "next")?.type
|
|
4312
5003
|
?? this.findField(expanded.name, "next")?.type
|
|
4313
|
-
?? (!identity.startsWith("js:") ? this.findMethod(identity, "next")?.type : null)
|
|
4314
|
-
?? (!identity.startsWith("js:") ? this.findMethod(expanded.name, "next")?.type : null)
|
|
4315
5004
|
?? null;
|
|
4316
5005
|
}
|
|
4317
5006
|
const callable = next ? this.expandAliases(next) : null;
|
|
4318
5007
|
if (!callable || callable.kind !== "function" || callable.requiredParameters > 0 || (callable.typeParameterNames?.length ?? 0) > 0) {
|
|
4319
|
-
|
|
4320
|
-
// synchronous question. An asynchronous stream is a resource — it has a
|
|
4321
|
-
// lifetime, it fails, it needs releasing — so letting an ordinary class
|
|
4322
|
-
// pose as one would make "does this need `using`?" undecidable.
|
|
4323
|
-
const contractNote = this.iterationContract(source) === null
|
|
4324
|
-
? ""
|
|
4325
|
-
: "; '@iterate' answers the plain 'for', not 'async for' — an async stream is a resource, so pull it from the capability handle that owns the lifetime";
|
|
4326
|
-
this.typeError(`async for requires next() -> Promise<T?>; ${describeType(source)} does not expose that pull contract${contractNote}`, sourceSpan);
|
|
5008
|
+
this.typeError(`async for requires next() -> Promise<T?>; ${describeType(source)} does not expose that pull contract`, sourceSpan);
|
|
4327
5009
|
return unknownType;
|
|
4328
5010
|
}
|
|
4329
5011
|
const result = this.expandAliases(callable.result);
|
|
@@ -4478,6 +5160,25 @@ export class Analyzer {
|
|
|
4478
5160
|
const deletion = statement.resultAnnotationSpan;
|
|
4479
5161
|
this.diagnostics.push(diagnostic("VEL4037", `${declarationKind} '${statement.name}' infers '-> null' from its body; delete the annotation, and write it only where 'extern', 'abstract', or a function type leaves no body to infer`, reference.span, deletion ? mechanicalFix(deletion, "", "Delete the inferred '-> null'") : undefined));
|
|
4480
5162
|
}
|
|
5163
|
+
/**
|
|
5164
|
+
* D89 (message correction): the one report a `self` parameter earns, and the
|
|
5165
|
+
* deletion it names. The removed range reaches to the next parameter's start
|
|
5166
|
+
* (or back to the previous one's end), so the separating comma and its
|
|
5167
|
+
* whitespace come with it without reading the source text — the rewrite is a
|
|
5168
|
+
* spelling change with no judgment in it, which is what D38 §48 requires of
|
|
5169
|
+
* a registered fix.
|
|
5170
|
+
*/
|
|
5171
|
+
reportImplicitSelfParameter(parameters, index) {
|
|
5172
|
+
const parameter = parameters[index];
|
|
5173
|
+
const next = parameters[index + 1];
|
|
5174
|
+
const previous = parameters[index - 1];
|
|
5175
|
+
const removal = next
|
|
5176
|
+
? span(parameter.span.start, next.span.start)
|
|
5177
|
+
: previous
|
|
5178
|
+
? span(previous.span.end, parameter.span.end)
|
|
5179
|
+
: parameter.span;
|
|
5180
|
+
this.diagnostics.push(diagnostic("VEL3007", "'self' is the receiver a method body already has, not a parameter; delete it from the parameter list", parameter.span, mechanicalFix(removal, "", "Delete the implicit 'self' parameter")));
|
|
5181
|
+
}
|
|
4481
5182
|
analyzeFunctionDeclaration(statement, className, method = false, declareSelf = Boolean(className), forceAsynchronous = false, declarationKind = "accessor" in statement ? "Getter" : method ? "Method" : "Function") {
|
|
4482
5183
|
const outerConstructorDepth = this.constructorDepth;
|
|
4483
5184
|
if (!method && !className && !this.predeclared.has(statement)) {
|
|
@@ -4485,11 +5186,23 @@ export class Analyzer {
|
|
|
4485
5186
|
}
|
|
4486
5187
|
const candidateBinding = className === null ? this.lookup(statement.name) : null;
|
|
4487
5188
|
const callableBinding = candidateBinding?.span.start === statement.span.start ? candidateBinding : null;
|
|
5189
|
+
// D85 rule 209: the same registration the top-level predeclaration makes,
|
|
5190
|
+
// for a `def` nested in a body, which nothing predeclares.
|
|
5191
|
+
if (callableBinding && !this.functionResultKeys.has(callableBinding)) {
|
|
5192
|
+
this.functionResultKeys.set(callableBinding, this.functionResultKey(statement));
|
|
5193
|
+
}
|
|
4488
5194
|
this.checkTypeParameterDeclarations(statement.typeParameters);
|
|
4489
5195
|
this.typeParameterFrames.push(this.typeParameterFrame(statement.typeParameters));
|
|
4490
5196
|
this.enterScope();
|
|
4491
5197
|
this.flowFrameDepth += 1;
|
|
4492
5198
|
this.functionDepth += 1;
|
|
5199
|
+
// D31 item 23: this body is deferred, so its reads of imported bindings
|
|
5200
|
+
// become initialization-position reads only when something runs it during
|
|
5201
|
+
// module evaluation. Collect them here and let a top-level call decide.
|
|
5202
|
+
const deferredFrame = { reads: [], calls: [] };
|
|
5203
|
+
this.deferredReadFrames.push(deferredFrame);
|
|
5204
|
+
if (callableBinding)
|
|
5205
|
+
this.localFunctionFrames.set(callableBinding, deferredFrame);
|
|
4493
5206
|
const previousLoopDepth = this.loopDepth;
|
|
4494
5207
|
this.loopDepth = 0;
|
|
4495
5208
|
const previousFinallyLoopDepths = this.finallyLoopDepths;
|
|
@@ -4533,7 +5246,19 @@ export class Analyzer {
|
|
|
4533
5246
|
const selfType = { kind: "class", name: className };
|
|
4534
5247
|
this.declareBinding("self", false, selfType, statement.span, true);
|
|
4535
5248
|
}
|
|
4536
|
-
for (const parameter of statement.parameters) {
|
|
5249
|
+
for (const [index, parameter] of statement.parameters.entries()) {
|
|
5250
|
+
// D89 (message correction): a method body already has `self`, so writing
|
|
5251
|
+
// it as a parameter is Python's explicit receiver. It used to earn two
|
|
5252
|
+
// reports — "already declared in this scope" and "reserved Core binding"
|
|
5253
|
+
// — neither of which named the fix. Only a declaration that really has
|
|
5254
|
+
// an implicit receiver takes this branch; a plain or static function's
|
|
5255
|
+
// `self` keeps the reserved-binding refusal, which is the truth there.
|
|
5256
|
+
// A rest spelling is not the receiver reflex, and its '...' sits outside
|
|
5257
|
+
// the parameter span, so deleting the name alone would leave a stray one.
|
|
5258
|
+
if (parameter.name === "self" && !parameter.rest && className !== null && declareSelf) {
|
|
5259
|
+
this.reportImplicitSelfParameter(statement.parameters, index);
|
|
5260
|
+
continue;
|
|
5261
|
+
}
|
|
4537
5262
|
const contextualType = !parameter.type && parameter.defaultValue
|
|
4538
5263
|
? this.contextualFunctionParameterDefault(statement, parameter)
|
|
4539
5264
|
: null;
|
|
@@ -4556,10 +5281,29 @@ export class Analyzer {
|
|
|
4556
5281
|
const inferred = this.inferCollectedFunctionResult(inferredReturns, !this.blockAlwaysReturns(statement.body));
|
|
4557
5282
|
this.inferredFunctionResultTypes.set(resultKey, inferred);
|
|
4558
5283
|
const seeded = this.inferredFunctionResultSeeds.get(resultKey) ?? inferredResultPlaceholderType;
|
|
4559
|
-
if (
|
|
4560
|
-
|
|
4561
|
-
this.diagnostics.push(diagnostic("VEL4025", `${declarationKind} '${statement.name}' result inference did not converge; add an explicit result annotation to this recursive contract`, statement.signatureSpan));
|
|
5284
|
+
if (returnContext.unsettledResult === true) {
|
|
5285
|
+
this.reportedResultHoles.add(resultKey);
|
|
4562
5286
|
}
|
|
5287
|
+
else if (this.finalizeFunctionResultInference
|
|
5288
|
+
&& (containsInferredResultPlaceholder(inferred) || isInvalidType(inferred) || !sameInferredResult(seeded, inferred))) {
|
|
5289
|
+
const report = diagnostic("VEL4025", `${declarationKind} '${statement.name}' result inference did not converge; add an explicit result annotation to this recursive contract`, statement.signatureSpan);
|
|
5290
|
+
this.diagnostics.push(report);
|
|
5291
|
+
// D85 rule 209: a callee whose hole is reported after this caller is
|
|
5292
|
+
// analyzed is a hole nobody can know about yet, so the report waits
|
|
5293
|
+
// for the whole module before it is kept or deleted as the second
|
|
5294
|
+
// half of one mistake.
|
|
5295
|
+
const causes = returnContext.resultHoleCauses;
|
|
5296
|
+
if (causes && causes.size > 0)
|
|
5297
|
+
this.deferredConvergenceReports.push({ report, resultKey, causes });
|
|
5298
|
+
}
|
|
5299
|
+
// D90 R12: an omitted result annotation publishes whatever the body
|
|
5300
|
+
// inferred, so an exported `def` leaks `any` exactly as an exported
|
|
5301
|
+
// `const` does. Deliberately not gated on finalizeFunctionResultInference:
|
|
5302
|
+
// a probe pass is discarded whole — the driver keeps the first pass's
|
|
5303
|
+
// diagnostics only when nothing was left to converge — which is why the
|
|
5304
|
+
// VEL4006 below is ungated too.
|
|
5305
|
+
if (typeContainsAnyOutput(inferred))
|
|
5306
|
+
this.recordExportedAny(statement, className, statement.signatureSpan);
|
|
4563
5307
|
this.updateInferredCallableResult(statement, className, callableBinding, inferred, asynchronous);
|
|
4564
5308
|
}
|
|
4565
5309
|
else {
|
|
@@ -4576,6 +5320,7 @@ export class Analyzer {
|
|
|
4576
5320
|
this.superMemberContext = previousSuperMemberContext;
|
|
4577
5321
|
this.loopDepth = previousLoopDepth;
|
|
4578
5322
|
this.finallyLoopDepths = previousFinallyLoopDepths;
|
|
5323
|
+
this.deferredReadFrames.pop();
|
|
4579
5324
|
this.functionDepth -= 1;
|
|
4580
5325
|
this.flowFrameDepth -= 1;
|
|
4581
5326
|
this.exitScope();
|
|
@@ -4655,7 +5400,7 @@ export class Analyzer {
|
|
|
4655
5400
|
if (statement.target.kind === "IdentifierExpression") {
|
|
4656
5401
|
const binding = this.lookup(statement.target.name);
|
|
4657
5402
|
if (!binding) {
|
|
4658
|
-
this.
|
|
5403
|
+
this.reportUnresolvedName(statement.target.name, statement.target.span);
|
|
4659
5404
|
return;
|
|
4660
5405
|
}
|
|
4661
5406
|
this.checkShadowedRead(statement.target.name, statement.target.span);
|
|
@@ -4829,6 +5574,8 @@ export class Analyzer {
|
|
|
4829
5574
|
if (targetBinding?.mutable) {
|
|
4830
5575
|
const storageBinding = targetBinding.storageBinding ?? targetBinding;
|
|
4831
5576
|
const rebound = storageBinding.declaredType.kind !== "unknown" ? storageBinding.declaredType : valueType;
|
|
5577
|
+
this.recordFlowFactOrigin(storageBinding);
|
|
5578
|
+
this.recordFlowFactOrigin(targetBinding);
|
|
4832
5579
|
storageBinding.storageType = rebound;
|
|
4833
5580
|
if (storageBinding.narrowingFrame === null)
|
|
4834
5581
|
storageBinding.type = rebound;
|
|
@@ -5020,9 +5767,7 @@ export class Analyzer {
|
|
|
5020
5767
|
return unknownType;
|
|
5021
5768
|
}
|
|
5022
5769
|
}
|
|
5023
|
-
|
|
5024
|
-
const nearest = guidance ? null : this.nearestVisibleBindingName(expression.name);
|
|
5025
|
-
this.diagnostics.push(diagnostic(guidance ? "VEL3008" : "VEL3001", guidance ?? `Unknown name '${expression.name}'${nearest ? `; did you mean '${nearest}'?` : ""}`, expression.span));
|
|
5770
|
+
this.reportUnresolvedName(expression.name, expression.span);
|
|
5026
5771
|
return unknownType;
|
|
5027
5772
|
}
|
|
5028
5773
|
if (lexical) {
|
|
@@ -5160,6 +5905,20 @@ export class Analyzer {
|
|
|
5160
5905
|
if (objectContext?.kind === "named" && expectedFields?.has(property.name)) {
|
|
5161
5906
|
this.semanticObjectPropertyOwners.set(`${property.span.start}:${property.name}`, objectContext);
|
|
5162
5907
|
}
|
|
5908
|
+
// D90 R11: a literal written at a type-annotated position is
|
|
5909
|
+
// closed. Every one of its keys is in front of the compiler here,
|
|
5910
|
+
// so an unrecognised one is a misspelling rather than a value that
|
|
5911
|
+
// happens to be wider — which is why the openness a non-literal
|
|
5912
|
+
// keeps is untouched. Only written keys are checked, so a spread's
|
|
5913
|
+
// surplus fields stay legal and this sits outside the
|
|
5914
|
+
// missing-field guard below rather than inside it. A `Record<T>`
|
|
5915
|
+
// context declares every string key, and leaves `expectedFields`
|
|
5916
|
+
// null, so no key of one is ever unrecognised.
|
|
5917
|
+
if (objectContext && expectedFields && !expectedFields.has(property.name)) {
|
|
5918
|
+
const nearest = uniqueNearestName(property.name, expectedFields.keys());
|
|
5919
|
+
const owner = objectContext.kind === "named" ? `Type '${objectContext.name}'` : "Object";
|
|
5920
|
+
this.typeError(`${owner} has no field '${property.name}'${nearest ? `; did you mean '${nearest}'?` : ""}`, property.span);
|
|
5921
|
+
}
|
|
5163
5922
|
const expected = expectedFields?.get(property.name) ?? expectedRecordValue ?? unknownType;
|
|
5164
5923
|
const actual = this.inferExpression(property.value, expected.kind === "optional" ? expected.inner : expected);
|
|
5165
5924
|
fields.set(property.name, expected.kind === "unknown" ? this.widenAggregateSingleton(actual) : actual);
|
|
@@ -5235,15 +5994,12 @@ export class Analyzer {
|
|
|
5235
5994
|
const invalidExtensionAwait = this.functionDepth === 0 && this.invalidExtensionAwaitContext();
|
|
5236
5995
|
if (this.parameterDefaultDepth === 0 && this.constructorDepth === 0 && (invalidFunctionAwait || invalidExtensionAwait)) {
|
|
5237
5996
|
this.diagnostics.push(diagnostic("VEL4007",
|
|
5238
|
-
//
|
|
5239
|
-
//
|
|
5240
|
-
//
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
: invalidExtensionAwait
|
|
5245
|
-
? this.invalidExtensionAwaitMessage() ?? "'await' is not valid in this synchronous extension context"
|
|
5246
|
-
: "'await' can only be used in an async function or at module scope", expression.span));
|
|
5997
|
+
// D90 R18: an `@iterate:` block that awaits is the asynchronous
|
|
5998
|
+
// pull form, so awaiting inside one is never refused here — the
|
|
5999
|
+
// form's own validation owns the answer-shape question.
|
|
6000
|
+
invalidExtensionAwait
|
|
6001
|
+
? this.invalidExtensionAwaitMessage() ?? "'await' is not valid in this synchronous extension context"
|
|
6002
|
+
: "'await' can only be used in an async function or at module scope", expression.span));
|
|
5247
6003
|
}
|
|
5248
6004
|
const awaited = this.expandAliases(operand);
|
|
5249
6005
|
if (isInvalidType(awaited))
|
|
@@ -5256,11 +6012,12 @@ export class Analyzer {
|
|
|
5256
6012
|
}
|
|
5257
6013
|
return result;
|
|
5258
6014
|
}
|
|
5259
|
-
// ASY-U2
|
|
5260
|
-
// here and a raw undefined result
|
|
5261
|
-
//
|
|
5262
|
-
|
|
5263
|
-
|
|
6015
|
+
// ASY-U2 + D90 R17: awaiting an unchecked boundary value adopts a
|
|
6016
|
+
// foreign thenable — its hooks run here and a raw undefined result
|
|
6017
|
+
// skips null normalization — so `any` and `unknown` share one
|
|
6018
|
+
// refusal, and it teaches the way in: a declared contract.
|
|
6019
|
+
this.typeError(awaited.kind === "any" || awaited.kind === "unknown"
|
|
6020
|
+
? `Cannot await ${describeType(operand)}; an unchecked thenable runs foreign hooks and can leak raw undefined — declare the source in an extern contract so the result is a checked Promise, or validate the resolved data at the edge with 'Type.parse'`
|
|
5264
6021
|
: `Cannot await ${describeType(operand)}`, expression.span);
|
|
5265
6022
|
return unknownType;
|
|
5266
6023
|
}
|
|
@@ -5415,6 +6172,7 @@ export class Analyzer {
|
|
|
5415
6172
|
case "ArrowFunctionExpression":
|
|
5416
6173
|
return this.inferArrow(expression, contextualType);
|
|
5417
6174
|
case "CallExpression": {
|
|
6175
|
+
this.recordDeferredCallEdge(expression.callee, expression.span);
|
|
5418
6176
|
const result = this.inferCall(expression.callee, expression.arguments, expression.argumentNames, expression.span, contextualType, expression.optional);
|
|
5419
6177
|
if (this.expandAliases(result).kind === "null")
|
|
5420
6178
|
this.normalizedNullResults.add(spanIdentity(expression.span));
|
|
@@ -5482,7 +6240,9 @@ export class Analyzer {
|
|
|
5482
6240
|
return unknownType;
|
|
5483
6241
|
}
|
|
5484
6242
|
if (object.kind !== "any") {
|
|
5485
|
-
|
|
6243
|
+
// D90 R17: an unknown is a boundary value, so the refusal teaches
|
|
6244
|
+
// the validation ritual instead of restating the kind.
|
|
6245
|
+
this.typeError(`Cannot index ${describeType(object)}${object.kind === "unknown" && !isInvalidType(object) ? this.boundaryValidationGuidance(expression.object, null) : ""}`, expression.span);
|
|
5486
6246
|
}
|
|
5487
6247
|
return object.kind === "any" ? anyType : unknownType;
|
|
5488
6248
|
}
|
|
@@ -5577,10 +6337,19 @@ export class Analyzer {
|
|
|
5577
6337
|
this.collectionMemberships.set(spanIdentity(operationSpan), container.kind);
|
|
5578
6338
|
}
|
|
5579
6339
|
if (container.kind === "list" || container.kind === "set") {
|
|
5580
|
-
|
|
6340
|
+
// COL-I3 second half: `in` is the thirteenth membership probe and the
|
|
6341
|
+
// one that does not route through `checkProbeArgument`, so it carries
|
|
6342
|
+
// the fresh-literal rejection itself — against the probe only, never
|
|
6343
|
+
// the container, because the fresh List in `x in [1, 2, 3]` is the
|
|
6344
|
+
// domain being searched rather than the question being asked.
|
|
6345
|
+
if (!this.requireMembershipIntersection(left, this.readonlyDataViewOf(container.element), leftExpression.span, operator)) {
|
|
6346
|
+
this.rejectFreshCollectionProbe(leftExpression, operator, "element");
|
|
6347
|
+
}
|
|
5581
6348
|
}
|
|
5582
6349
|
else if (container.kind === "map") {
|
|
5583
|
-
this.requireMembershipIntersection(left, this.readonlyDataViewOf(container.key), leftExpression.span, operator)
|
|
6350
|
+
if (!this.requireMembershipIntersection(left, this.readonlyDataViewOf(container.key), leftExpression.span, operator)) {
|
|
6351
|
+
this.rejectFreshCollectionProbe(leftExpression, operator, "key");
|
|
6352
|
+
}
|
|
5584
6353
|
}
|
|
5585
6354
|
else if (container.kind === "record") {
|
|
5586
6355
|
this.requireMembershipIntersection(left, stringType, leftExpression.span, operator);
|
|
@@ -5612,10 +6381,63 @@ export class Analyzer {
|
|
|
5612
6381
|
this.typeError(`String concatenation requires two strings; use an f-string or str(value), received ${describeType(left)} and ${describeType(right)}`, operationSpan);
|
|
5613
6382
|
return stringType;
|
|
5614
6383
|
}
|
|
6384
|
+
if (operator === "%")
|
|
6385
|
+
this.adviseNegativeLiteralModulo(leftExpression, rightExpression, operationSpan);
|
|
5615
6386
|
this.requireAssignable(left, numberType, leftExpression.span);
|
|
5616
6387
|
this.requireAssignable(right, numberType, rightExpression.span);
|
|
5617
6388
|
return numberType;
|
|
5618
6389
|
}
|
|
6390
|
+
/**
|
|
6391
|
+
* D89 A3: `%` follows JavaScript and keeps the dividend's sign, so `-7 % 3`
|
|
6392
|
+
* is `-1` where Python answers `2`. Nothing here reports an error — both
|
|
6393
|
+
* languages accept the spelling, they just disagree about the result.
|
|
6394
|
+
*
|
|
6395
|
+
* Only a literal negative dividend triggers. A variable's sign is not
|
|
6396
|
+
* knowable, and advising every `%` whose left side might go negative would
|
|
6397
|
+
* be the noise the tier exists to avoid. The shape matched is a unary minus
|
|
6398
|
+
* wrapping a numeric literal, because that is what `-7` parses as; there is
|
|
6399
|
+
* no negative-valued literal for a value test to find.
|
|
6400
|
+
*
|
|
6401
|
+
* The admission bar is "Vel accepts the spelling as a different meaning", so
|
|
6402
|
+
* every shape whose two answers are the same is silent rather than advised:
|
|
6403
|
+
* a remainder of zero (`-6 % 3`) agrees, `% 0` answers NaN here and raises
|
|
6404
|
+
* in Python so there is no Python answer to name, and a non-finite dividend
|
|
6405
|
+
* answers NaN on both sides. A message that states a disagreement and then
|
|
6406
|
+
* prints the same number twice is a new defect, not a weaker advisory.
|
|
6407
|
+
*/
|
|
6408
|
+
adviseNegativeLiteralModulo(leftExpression, rightExpression, operationSpan) {
|
|
6409
|
+
if (leftExpression.kind !== "UnaryExpression" || leftExpression.operator !== "-")
|
|
6410
|
+
return;
|
|
6411
|
+
const dividend = leftExpression.operand;
|
|
6412
|
+
if (dividend.kind !== "LiteralExpression" || typeof dividend.value !== "number")
|
|
6413
|
+
return;
|
|
6414
|
+
const divisor = rightExpression.kind === "LiteralExpression" && typeof rightExpression.value === "number"
|
|
6415
|
+
? rightExpression
|
|
6416
|
+
: null;
|
|
6417
|
+
if (divisor !== null) {
|
|
6418
|
+
const divisorValue = Number(divisor.value);
|
|
6419
|
+
if (divisorValue === 0)
|
|
6420
|
+
return;
|
|
6421
|
+
// `-0` renders as `0`, so a zero remainder would print one number on
|
|
6422
|
+
// both sides of a sentence claiming they differ.
|
|
6423
|
+
const remainder = -Number(dividend.value) % divisorValue;
|
|
6424
|
+
if (!Number.isFinite(remainder) || remainder === 0)
|
|
6425
|
+
return;
|
|
6426
|
+
// A literal divisor is always positive — a negative one parses as a
|
|
6427
|
+
// unary minus, not a literal — so Python's answer, which takes the
|
|
6428
|
+
// divisor's sign, is this remainder lifted by one divisor.
|
|
6429
|
+
const python = remainder + divisorValue;
|
|
6430
|
+
// The rewrite the message advertises is its own remedy, so quoting an
|
|
6431
|
+
// answer that rewrite does not produce would be false. The two part ways
|
|
6432
|
+
// only when the lift rounds back onto the divisor; the general sentence
|
|
6433
|
+
// below covers that without naming a number.
|
|
6434
|
+
if ((remainder + divisorValue) % divisorValue === python) {
|
|
6435
|
+
this.advise("A3", `VelarScript's '%' follows JavaScript and keeps the dividend's sign, so '-${dividend.raw} % ${divisor.raw}' is ${remainder} where Python answers ${python}; write '((a % b) + b) % b' for the Python answer`, operationSpan);
|
|
6436
|
+
return;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
this.advise("A3", "VelarScript's '%' follows JavaScript and keeps the dividend's sign, so a negative dividend leaves a remainder that is negative or zero, where Python's takes the divisor's sign; write '((a % b) + b) % b' for the Python answer", operationSpan);
|
|
6440
|
+
}
|
|
5619
6441
|
// D42 item 64: `==`/`!=` require the operand types to intersect. Strict
|
|
5620
6442
|
// equality between two types that no single value inhabits is constant, so
|
|
5621
6443
|
// the tightening converts a silent logic bug into a compile error. Runtime
|
|
@@ -5681,12 +6503,36 @@ export class Analyzer {
|
|
|
5681
6503
|
// the same enum/string boundary as D42 item 64.
|
|
5682
6504
|
requireMembershipIntersection(probe, domain, span, operation) {
|
|
5683
6505
|
if (isInvalidType(probe) || isInvalidType(domain))
|
|
5684
|
-
return;
|
|
6506
|
+
return false;
|
|
5685
6507
|
if (this.equalityTypesIntersect(probe, domain))
|
|
5686
|
-
return;
|
|
6508
|
+
return false;
|
|
5687
6509
|
this.typeError(this.typesIntersect(probe, domain, false)
|
|
5688
6510
|
? `${describeType(probe)} can match ${describeType(domain)} only as an enum member against a raw string, and the enum and string domains never meet in '${operation}'${this.equalityGuidance(probe, domain)}`
|
|
5689
6511
|
: `${describeType(probe)} and ${describeType(domain)} have no values in common, so '${operation}' can never match${this.equalityGuidance(probe, domain)}`, span);
|
|
6512
|
+
return true;
|
|
6513
|
+
}
|
|
6514
|
+
// COL-I3 second half: the same ruling that rejects a freshly built literal
|
|
6515
|
+
// as an `==` operand governs the membership vocabulary, because a membership
|
|
6516
|
+
// test asks the `==` question one element at a time. A literal written
|
|
6517
|
+
// inside the probe is a new object no element can be identical to, so the
|
|
6518
|
+
// answer is provable from the literal alone.
|
|
6519
|
+
//
|
|
6520
|
+
// Only the probe side is closed, deliberately. The container side is an
|
|
6521
|
+
// ordinary spelling — `x in [1, 2, 3]` builds the fresh List as the domain,
|
|
6522
|
+
// not as the question — and `Set.add`, `Set<Record>` and `Map<Record, V>`
|
|
6523
|
+
// are left alone for the same reason: an identity-keyed container of records
|
|
6524
|
+
// is a legitimate program (adding the same object twice, holding a record as
|
|
6525
|
+
// an identity token), so a diagnostic there would refuse correct code. A
|
|
6526
|
+
// false positive on a correct program is worse than silence; the probe is
|
|
6527
|
+
// the one position where the always-false answer is provable.
|
|
6528
|
+
rejectFreshCollectionProbe(probe, operation, probes) {
|
|
6529
|
+
const fresh = this.freshCollectionOperand(probe);
|
|
6530
|
+
if (!fresh)
|
|
6531
|
+
return false;
|
|
6532
|
+
this.typeError(`A ${fresh.description} built inside the probe is a new object, and '${operation}' compares ${probes} identity, so it can never match; ${probes === "key"
|
|
6533
|
+
? "hold the key in a binding and probe with that binding, or compare contents with equals(a, b)"
|
|
6534
|
+
: "compare contents with equals(a, b) — 'values.some(item => equals(item, probe))' asks the same question one element at a time"}`, fresh.span);
|
|
6535
|
+
return true;
|
|
5690
6536
|
}
|
|
5691
6537
|
// ENM-I1: `is` / `is not` between statically disjoint enum domains is the
|
|
5692
6538
|
// last equality surface that could launder one enum's member into another
|
|
@@ -5742,6 +6588,79 @@ export class Analyzer {
|
|
|
5742
6588
|
return;
|
|
5743
6589
|
this.rejectDisjointEnumTest(probe, target, "is", argument.span);
|
|
5744
6590
|
}
|
|
6591
|
+
/**
|
|
6592
|
+
* D59 rule 141 settled that `toBe` *is* `==` ("toBe 必须用语言自己的 `==`")
|
|
6593
|
+
* and rule 141.1 settled that `toContain` *is* `values.has(item)`. The
|
|
6594
|
+
* runtime half of both landed; the compile-time half did not travel with
|
|
6595
|
+
* them, so `expect([1]).toBe([1])` compiled and failed at run time with
|
|
6596
|
+
* both operands rendering byte-identically, while `[1] == [1]` is refused
|
|
6597
|
+
* where it is written. This runs the operator's own two gates on the
|
|
6598
|
+
* matcher: D42 item 64's intersection requirement, and COL-I3's rejection
|
|
6599
|
+
* of a freshly built literal in an identity comparison.
|
|
6600
|
+
*
|
|
6601
|
+
* `toBe` and `toEqual` deliberately part company on the fresh-literal gate.
|
|
6602
|
+
* `toBe` asks the `==` question, where a new object can never be identical
|
|
6603
|
+
* to anything, so the literal proves the answer. `toEqual` asks the
|
|
6604
|
+
* `equals(a, b)` question, where a fresh literal is the normal and correct
|
|
6605
|
+
* spelling of the expected value — rejecting it there would refuse the very
|
|
6606
|
+
* repair the `toBe` message teaches. The intersection gate has no such
|
|
6607
|
+
* split: two types with no values in common never deeply equal either.
|
|
6608
|
+
*
|
|
6609
|
+
* `toHaveLength` and `toMatch` are left alone. Neither takes a comparand:
|
|
6610
|
+
* `toHaveLength` takes a count, and `toMatch` takes a regular-expression
|
|
6611
|
+
* pattern whose relation to the subject is matching, not equality.
|
|
6612
|
+
*/
|
|
6613
|
+
checkTestMatcherComparand(calleeExpression, arguments_) {
|
|
6614
|
+
if (calleeExpression.kind !== "MemberExpression" || arguments_.length !== 1)
|
|
6615
|
+
return;
|
|
6616
|
+
const matcher = calleeExpression.property;
|
|
6617
|
+
if (matcher !== "toBe" && matcher !== "toEqual" && matcher !== "toContain")
|
|
6618
|
+
return;
|
|
6619
|
+
const receiver = calleeExpression.object;
|
|
6620
|
+
if (receiver.kind !== "CallExpression")
|
|
6621
|
+
return;
|
|
6622
|
+
const operand = this.testExpectOperands.get(spanIdentity(receiver.span));
|
|
6623
|
+
if (operand === undefined)
|
|
6624
|
+
return;
|
|
6625
|
+
const argument = arguments_[0];
|
|
6626
|
+
if (argument.kind === "SpreadExpression")
|
|
6627
|
+
return;
|
|
6628
|
+
const probe = this.inferredExpressionTypes.get(spanIdentity(argument.span));
|
|
6629
|
+
if (!probe)
|
|
6630
|
+
return;
|
|
6631
|
+
// `==` leaves through `inferBinary`'s invalid-type exit before either of
|
|
6632
|
+
// these gates runs, so the matcher that inherits the gates leaves there
|
|
6633
|
+
// too: an operand the compiler already refused has been named once, and
|
|
6634
|
+
// the always-false reading of a program that does not yet type-check is
|
|
6635
|
+
// not a second mistake to report.
|
|
6636
|
+
if (isInvalidType(operand) || isInvalidType(probe))
|
|
6637
|
+
return;
|
|
6638
|
+
if (matcher === "toContain") {
|
|
6639
|
+
// The membership vocabulary's own pair (ENM-I3 and COL-I3's second
|
|
6640
|
+
// half), asked one element at a time. Only a List receiver compares
|
|
6641
|
+
// element identity; text containment is code-point containment, and a
|
|
6642
|
+
// dynamic receiver proves nothing about which of the two it will be.
|
|
6643
|
+
if (operand.kind !== "list")
|
|
6644
|
+
return;
|
|
6645
|
+
const contained = this.readonlyDataViewOf(operand.element);
|
|
6646
|
+
if (!this.requireMembershipIntersection(probe, contained, argument.span, matcher)) {
|
|
6647
|
+
this.rejectFreshCollectionProbe(argument, matcher, "element");
|
|
6648
|
+
}
|
|
6649
|
+
return;
|
|
6650
|
+
}
|
|
6651
|
+
if (this.requireMembershipIntersection(probe, operand, argument.span, matcher))
|
|
6652
|
+
return;
|
|
6653
|
+
if (matcher !== "toBe")
|
|
6654
|
+
return;
|
|
6655
|
+
// Either side settles it: `expect([1]).toBe(list)` is as constant as
|
|
6656
|
+
// `expect(list).toBe([1])`, exactly as `==` treats its two operands.
|
|
6657
|
+
const actualExpression = receiver.arguments[0];
|
|
6658
|
+
const fresh = this.freshCollectionOperand(argument)
|
|
6659
|
+
?? (actualExpression && actualExpression.kind !== "SpreadExpression" ? this.freshCollectionOperand(actualExpression) : null);
|
|
6660
|
+
if (!fresh)
|
|
6661
|
+
return;
|
|
6662
|
+
this.typeError(`A ${fresh.description} built inside the expectation is a new object, and 'toBe' compares collection identity, so it can never match; compare contents with 'toEqual(expected)'`, fresh.span);
|
|
6663
|
+
}
|
|
5745
6664
|
enumTargetOfValidatorObject(object) {
|
|
5746
6665
|
if (object.kind !== "IdentifierExpression")
|
|
5747
6666
|
return null;
|
|
@@ -6131,6 +7050,13 @@ export class Analyzer {
|
|
|
6131
7050
|
this.enterScope();
|
|
6132
7051
|
this.flowFrameDepth += 1;
|
|
6133
7052
|
this.functionDepth += 1;
|
|
7053
|
+
// D31 item 23: an arrow bound to a module-local name is the other deferred
|
|
7054
|
+
// body a top-level call can run, and the binding does not exist until the
|
|
7055
|
+
// declaration finishes, so the frame is filed by the arrow's own span and
|
|
7056
|
+
// the declaration claims it afterwards.
|
|
7057
|
+
const deferredFrame = { reads: [], calls: [] };
|
|
7058
|
+
this.deferredReadFrames.push(deferredFrame);
|
|
7059
|
+
this.arrowDeferredFrames.set(spanIdentity(expression.span), deferredFrame);
|
|
6134
7060
|
const previousFinallyLoopDepths = this.finallyLoopDepths;
|
|
6135
7061
|
this.finallyLoopDepths = [];
|
|
6136
7062
|
this.asynchronousFunctions.push(expression.asynchronous);
|
|
@@ -6179,12 +7105,21 @@ export class Analyzer {
|
|
|
6179
7105
|
this.arrowOwnedCaptures.set(spanIdentity(expression.span), captured);
|
|
6180
7106
|
this.parameterDefaultDepth = outerParameterDefaultDepth;
|
|
6181
7107
|
this.constructorDepth = outerConstructorDepth;
|
|
6182
|
-
|
|
7108
|
+
let checkedBodyResult = expected
|
|
6183
7109
|
&& expandedExpectedResult.kind !== "unknown"
|
|
6184
7110
|
&& expandedExpectedResult.kind !== "any"
|
|
6185
7111
|
&& this.contextuallyAssignable(bodyResult, contextualResult, expression.body.span)
|
|
6186
7112
|
? contextualResult
|
|
6187
7113
|
: bodyResult;
|
|
7114
|
+
// D85 rule 207: with no contextual result the arrow's body is the only
|
|
7115
|
+
// thing that says what it returns, so an empty collection written there
|
|
7116
|
+
// has nothing settling it — the same position a body-inferred `return`
|
|
7117
|
+
// occupies, reported the same way. Rule 209: once reported, the arrow's
|
|
7118
|
+
// result is invalid rather than a `List<unknown>` a caller reports again.
|
|
7119
|
+
if (expandedExpectedResult.kind === "unknown"
|
|
7120
|
+
&& this.requireSettledCollectionElement(expression.body, checkedBodyResult, false)) {
|
|
7121
|
+
checkedBodyResult = invalidType;
|
|
7122
|
+
}
|
|
6188
7123
|
const result = expression.asynchronous
|
|
6189
7124
|
? { kind: "promise", value: this.resolvedAsyncResult(checkedBodyResult) }
|
|
6190
7125
|
: checkedBodyResult;
|
|
@@ -6200,6 +7135,7 @@ export class Analyzer {
|
|
|
6200
7135
|
}
|
|
6201
7136
|
this.asynchronousFunctions.pop();
|
|
6202
7137
|
this.finallyLoopDepths = previousFinallyLoopDepths;
|
|
7138
|
+
this.deferredReadFrames.pop();
|
|
6203
7139
|
this.functionDepth -= 1;
|
|
6204
7140
|
this.flowFrameDepth -= 1;
|
|
6205
7141
|
this.exitScope();
|
|
@@ -6351,6 +7287,18 @@ export class Analyzer {
|
|
|
6351
7287
|
}
|
|
6352
7288
|
return source.fields.size === 0 && expectedMap ? expectedMap : { kind: "map", key: stringType, value };
|
|
6353
7289
|
}
|
|
7290
|
+
// A `Record<V>` is the dynamic-key record, and `__velarCreateMap` has
|
|
7291
|
+
// always read it. Only the structural `object` shape was accepted here,
|
|
7292
|
+
// so the diagnostic below listed "a record" among the forms it takes and
|
|
7293
|
+
// then refused one. Keys of a record are strings by construction.
|
|
7294
|
+
if (source.kind === "record") {
|
|
7295
|
+
const value = source.readonlyView ? this.readonlyDataViewOf(source.value) : source.value;
|
|
7296
|
+
if (expectedMap) {
|
|
7297
|
+
this.requireAssignable(stringType, expectedMap.key, argument.span);
|
|
7298
|
+
this.requireAssignable(value, expectedMap.value, argument.span);
|
|
7299
|
+
}
|
|
7300
|
+
return { kind: "map", key: stringType, value };
|
|
7301
|
+
}
|
|
6354
7302
|
if (source.kind === "any")
|
|
6355
7303
|
return { kind: "map", key: anyType, value: anyType };
|
|
6356
7304
|
this.typeError(`Map construction requires a Map, a List of [key, value] Lists, or a record, received ${describeType(source)}${this.iterationGuidance(source)}`, argument.span);
|
|
@@ -6467,8 +7415,15 @@ export class Analyzer {
|
|
|
6467
7415
|
&& arguments_[0]?.kind === "ObjectExpression" && callee.result.kind === "named") {
|
|
6468
7416
|
this.recordRuntimeObjectShape(arguments_[0], callee.result);
|
|
6469
7417
|
}
|
|
7418
|
+
const diagnosticsBeforeArguments = this.diagnostics.length;
|
|
6470
7419
|
this.checkArguments(arguments_, callee.parameters, callSpan, callee.requiredParameters, callee.rest, argumentNames, callee.parameterNames);
|
|
6471
7420
|
this.rejectDisjointEnumValidatorProbe(calleeExpression, arguments_);
|
|
7421
|
+
// One mistake, one diagnostic: the matcher gate speaks only when the
|
|
7422
|
+
// argument itself checked out, because an unassignable comparand has
|
|
7423
|
+
// already been named in the words the author needs.
|
|
7424
|
+
if (this.diagnostics.length === diagnosticsBeforeArguments) {
|
|
7425
|
+
this.checkTestMatcherComparand(calleeExpression, arguments_);
|
|
7426
|
+
}
|
|
6472
7427
|
this.reportPromiseCarrierHazard(callee.result, callSpan);
|
|
6473
7428
|
if (callee.result.kind === "optional")
|
|
6474
7429
|
this.optionalCalls.add(spanIdentity(callSpan));
|
|
@@ -6507,7 +7462,10 @@ export class Analyzer {
|
|
|
6507
7462
|
for (const argument of arguments_) {
|
|
6508
7463
|
this.inferExpression(argument);
|
|
6509
7464
|
}
|
|
6510
|
-
|
|
7465
|
+
// D90 R17: a call needs a declared signature — `Type.parse` validates
|
|
7466
|
+
// data, so the way in for a callable is the extern contract.
|
|
7467
|
+
const receiver = calleeExpression ? this.boundaryReceiverText(calleeExpression) : null;
|
|
7468
|
+
this.typeError(`Cannot call an unknown JavaScript value without a declaration or validation; declare the signature — an 'extern module' contract or a contracted 'extern js' block gives ${receiver ? `'${receiver}'` : "the value"} a checked type — or validate the data it came from with 'Type.parse' first`, callSpan);
|
|
6511
7469
|
}
|
|
6512
7470
|
return unknownType;
|
|
6513
7471
|
}
|
|
@@ -6741,8 +7699,17 @@ export class Analyzer {
|
|
|
6741
7699
|
const value = argument.kind === "SpreadExpression" ? argument.value : argument;
|
|
6742
7700
|
if (value.kind === "ArrowFunctionExpression")
|
|
6743
7701
|
deferredNamedArrows.add(value);
|
|
6744
|
-
else
|
|
6745
|
-
|
|
7702
|
+
else {
|
|
7703
|
+
const declared = target === null ? unknownType : intrinsic.parameters[target] ?? intrinsic.rest ?? unknownType;
|
|
7704
|
+
// D90 R17: an accept-anything parameter is spelled `List<unknown>`
|
|
7705
|
+
// in the vocabulary tables, and that spelling carries no element
|
|
7706
|
+
// information — preanalyzing a literal against it would launder
|
|
7707
|
+
// `[1, 2]` into a list the handler can read no numbers from, so the
|
|
7708
|
+
// literal keeps its own inferred element and the handler's own
|
|
7709
|
+
// expected type does the checking.
|
|
7710
|
+
const context = declared.kind === "list" && declared.element.kind === "unknown" ? unknownType : declared;
|
|
7711
|
+
this.inferExpression(value, context);
|
|
7712
|
+
}
|
|
6746
7713
|
}
|
|
6747
7714
|
if (!named.valid) {
|
|
6748
7715
|
for (const argument of deferredNamedArrows)
|
|
@@ -7142,7 +8109,9 @@ export class Analyzer {
|
|
|
7142
8109
|
fields.set("toBeFalsy", { kind: "function", parameters: [], requiredParameters: 0, result: nullType });
|
|
7143
8110
|
}
|
|
7144
8111
|
if (matched.kind === "list" || matched.kind === "string" || dynamic) {
|
|
7145
|
-
|
|
8112
|
+
// D90 R17: an accept-anything parameter position is `unknown`, the
|
|
8113
|
+
// top type for assignment targets; `any` stays a value kind only.
|
|
8114
|
+
const contained = matched.kind === "list" ? matched.element : matched.kind === "string" ? stringType : unknownType;
|
|
7146
8115
|
fields.set("toContain", { kind: "function", parameterNames: ["expected"], parameters: [contained], requiredParameters: 1, result: nullType });
|
|
7147
8116
|
fields.set("toHaveLength", { kind: "function", parameterNames: ["length"], parameters: [numberType], requiredParameters: 1, result: nullType });
|
|
7148
8117
|
}
|
|
@@ -7432,7 +8401,7 @@ export class Analyzer {
|
|
|
7432
8401
|
// Map/Record key of `get`) is judged by intersection with the element or
|
|
7433
8402
|
// key domain — the per-element `==` question — rather than assignability,
|
|
7434
8403
|
// whose enum -> string one-way exit would launder a bare-string match.
|
|
7435
|
-
const checkProbeArgument = (domain, operation) => {
|
|
8404
|
+
const checkProbeArgument = (domain, operation, probes = "element") => {
|
|
7436
8405
|
requireCount(1);
|
|
7437
8406
|
const argument = argumentAt(0);
|
|
7438
8407
|
if (!argument)
|
|
@@ -7442,7 +8411,12 @@ export class Analyzer {
|
|
|
7442
8411
|
return;
|
|
7443
8412
|
}
|
|
7444
8413
|
const probe = namedPreanalyzed ? this.inferredExpressionType(argument) : this.inferExpression(argument, domain);
|
|
7445
|
-
|
|
8414
|
+
// The domain mismatch is the more precise answer where both apply, so
|
|
8415
|
+
// the fresh-literal rejection speaks only when the probe's type is right
|
|
8416
|
+
// and identity is the sole reason it can never match.
|
|
8417
|
+
if (!this.requireMembershipIntersection(probe, domain, argument.span, operation)) {
|
|
8418
|
+
this.rejectFreshCollectionProbe(argument, operation, probes);
|
|
8419
|
+
}
|
|
7446
8420
|
if (!namedPreanalyzed) {
|
|
7447
8421
|
for (const extra of arguments_.slice(1)) {
|
|
7448
8422
|
if (!omitted(extra))
|
|
@@ -7736,7 +8710,7 @@ export class Analyzer {
|
|
|
7736
8710
|
this.typeError("Use 'get(key) ?? fallback'; Map.get has one optional-result contract", callSpan);
|
|
7737
8711
|
return optionalOf(readonlyValue);
|
|
7738
8712
|
}
|
|
7739
|
-
checkProbeArgument(comparisonKey, "Map.get");
|
|
8713
|
+
checkProbeArgument(comparisonKey, "Map.get", "key");
|
|
7740
8714
|
return optionalOf(readonlyValue);
|
|
7741
8715
|
}
|
|
7742
8716
|
if (member.property === "keys") {
|
|
@@ -7756,12 +8730,12 @@ export class Analyzer {
|
|
|
7756
8730
|
}
|
|
7757
8731
|
if (member.property === "has") {
|
|
7758
8732
|
this.collectionCalls.set(member.span.end, "mapHas");
|
|
7759
|
-
checkProbeArgument(comparisonKey, "Map.has");
|
|
8733
|
+
checkProbeArgument(comparisonKey, "Map.has", "key");
|
|
7760
8734
|
return boolType;
|
|
7761
8735
|
}
|
|
7762
8736
|
if (member.property === "remove") {
|
|
7763
8737
|
this.collectionCalls.set(member.span.end, "mapRemove");
|
|
7764
|
-
checkProbeArgument(comparisonKey, "Map.remove");
|
|
8738
|
+
checkProbeArgument(comparisonKey, "Map.remove", "key");
|
|
7765
8739
|
return boolType;
|
|
7766
8740
|
}
|
|
7767
8741
|
if (member.property === "clear") {
|
|
@@ -7783,7 +8757,7 @@ export class Analyzer {
|
|
|
7783
8757
|
}
|
|
7784
8758
|
if (member.property === "get") {
|
|
7785
8759
|
this.collectionCalls.set(member.span.end, "recordGet");
|
|
7786
|
-
checkProbeArgument(stringType, "Record.get");
|
|
8760
|
+
checkProbeArgument(stringType, "Record.get", "key");
|
|
7787
8761
|
return optionalOf(readonlyValue);
|
|
7788
8762
|
}
|
|
7789
8763
|
if (member.property === "keys") {
|
|
@@ -7803,12 +8777,12 @@ export class Analyzer {
|
|
|
7803
8777
|
}
|
|
7804
8778
|
if (member.property === "has") {
|
|
7805
8779
|
this.collectionCalls.set(member.span.end, "recordHas");
|
|
7806
|
-
checkProbeArgument(stringType, "Record.has");
|
|
8780
|
+
checkProbeArgument(stringType, "Record.has", "key");
|
|
7807
8781
|
return boolType;
|
|
7808
8782
|
}
|
|
7809
8783
|
if (member.property === "remove") {
|
|
7810
8784
|
this.collectionCalls.set(member.span.end, "recordRemove");
|
|
7811
|
-
checkProbeArgument(stringType, "Record.remove");
|
|
8785
|
+
checkProbeArgument(stringType, "Record.remove", "key");
|
|
7812
8786
|
return boolType;
|
|
7813
8787
|
}
|
|
7814
8788
|
if (member.property === "clear") {
|
|
@@ -8048,7 +9022,7 @@ export class Analyzer {
|
|
|
8048
9022
|
if (isInvalidType(object))
|
|
8049
9023
|
result = invalidType;
|
|
8050
9024
|
else
|
|
8051
|
-
this.typeError(`Cannot access '${property}' on unknown without validation`, memberSpan);
|
|
9025
|
+
this.typeError(`Cannot access '${property}' on unknown without validation${this.boundaryValidationGuidance(objectExpression, property)}`, memberSpan);
|
|
8052
9026
|
}
|
|
8053
9027
|
else if (object.kind === "string") {
|
|
8054
9028
|
result = this.stringMember(property) ?? unknownType;
|
|
@@ -8741,6 +9715,7 @@ export class Analyzer {
|
|
|
8741
9715
|
updateInferredCallableResult(statement, className, binding, result, asynchronous) {
|
|
8742
9716
|
if (binding) {
|
|
8743
9717
|
const type = this.callableWithInferredResult(binding.declaredType, result, asynchronous);
|
|
9718
|
+
this.recordFlowFactOrigin(binding);
|
|
8744
9719
|
binding.type = type;
|
|
8745
9720
|
binding.declaredType = type;
|
|
8746
9721
|
binding.storageType = type;
|
|
@@ -8919,8 +9894,18 @@ export class Analyzer {
|
|
|
8919
9894
|
this.classDisplayNames.set(type.identity, local);
|
|
8920
9895
|
return type;
|
|
8921
9896
|
}
|
|
9897
|
+
// D90 R17: an undeclared foreign value arrives as unknown — R12 refused
|
|
9898
|
+
// `any` at export positions, and this closes the entry. The value must be
|
|
9899
|
+
// validated into a concrete type (`Type.parse`) before members, calls, or
|
|
9900
|
+
// operators touch it; `unsafe` names the missing declaration, not a
|
|
9901
|
+
// license to chain through the boundary. A host-injected binding is a
|
|
9902
|
+
// declaration — the host answered for the name — so it still wins. The
|
|
9903
|
+
// boundary marker matters: a bare `unknown` is the inference seed a merge
|
|
9904
|
+
// absorbs, while this value is *known to be unchecked*, so `[mystery, 5]`
|
|
9905
|
+
// must stay `List<unknown | number>` instead of laundering into
|
|
9906
|
+
// `List<number>`.
|
|
8922
9907
|
if (statement.unsafe)
|
|
8923
|
-
return
|
|
9908
|
+
return this.importBindings.get(local) ?? boundaryUnknownType;
|
|
8924
9909
|
const declarations = this.externModules.get(statement.source);
|
|
8925
9910
|
if (namespace)
|
|
8926
9911
|
return declarations
|
|
@@ -9278,9 +10263,10 @@ export class Analyzer {
|
|
|
9278
10263
|
// whether the value is true, so 'false' and null take the same else path and
|
|
9279
10264
|
// 'if flag:' stays the spelling for both. Any other optional has to say
|
|
9280
10265
|
// which question it asks, because "holds a value" and "is true" are
|
|
9281
|
-
// different tests. BRG-N4:
|
|
9282
|
-
//
|
|
9283
|
-
//
|
|
10266
|
+
// different tests. BRG-N4 + D90 R17: an unchecked boundary value — `any` or
|
|
10267
|
+
// `unknown` — is rejected with one message: raw JavaScript truthiness would
|
|
10268
|
+
// judge 0 and "" false, which breaks the owner's ruling that a condition
|
|
10269
|
+
// judges only bool, so the boundary value is validated first.
|
|
9284
10270
|
requireCondition(type, condition) {
|
|
9285
10271
|
this.checkGetterNarrowingTest(condition);
|
|
9286
10272
|
if (isInvalidType(type))
|
|
@@ -9288,8 +10274,8 @@ export class Analyzer {
|
|
|
9288
10274
|
const expanded = this.expandAliases(type);
|
|
9289
10275
|
if (expanded.kind === "bool")
|
|
9290
10276
|
return;
|
|
9291
|
-
if (expanded.kind === "any") {
|
|
9292
|
-
this.typeError(
|
|
10277
|
+
if (expanded.kind === "any" || expanded.kind === "unknown") {
|
|
10278
|
+
this.typeError(`A condition judges only bool, and an unchecked ${describeType(type)} would ride JavaScript truthiness (0 and "" become false); validate the value at the edge — 'Type.parse' — and judge the checked result, or compare it explicitly`, condition.span);
|
|
9293
10279
|
return;
|
|
9294
10280
|
}
|
|
9295
10281
|
if (expanded.kind === "optional") {
|
|
@@ -9377,6 +10363,45 @@ export class Analyzer {
|
|
|
9377
10363
|
}
|
|
9378
10364
|
return null;
|
|
9379
10365
|
}
|
|
10366
|
+
/**
|
|
10367
|
+
* D90 R17: the author's own spelling of a boundary value, for the
|
|
10368
|
+
* diagnostics that teach `Type.parse`. Identifier and member paths render
|
|
10369
|
+
* exactly, a simple call renders as `name(...)`, and anything else answers
|
|
10370
|
+
* null so the caller falls back to the word `value`.
|
|
10371
|
+
*/
|
|
10372
|
+
boundaryReceiverText(expression) {
|
|
10373
|
+
if (expression.kind === "IdentifierExpression")
|
|
10374
|
+
return expression.name;
|
|
10375
|
+
if (expression.kind === "MemberExpression" && !expression.optional) {
|
|
10376
|
+
const owner = this.boundaryReceiverText(expression.object);
|
|
10377
|
+
return owner === null ? null : `${owner}.${expression.property}`;
|
|
10378
|
+
}
|
|
10379
|
+
if (expression.kind === "CallExpression") {
|
|
10380
|
+
const callee = this.boundaryReceiverText(expression.callee);
|
|
10381
|
+
return callee === null ? null : `${callee}(...)`;
|
|
10382
|
+
}
|
|
10383
|
+
return null;
|
|
10384
|
+
}
|
|
10385
|
+
/** A type name suggested from the receiver's last name segment, or 'X' when none reads naturally. */
|
|
10386
|
+
boundaryTypeNameSuggestion(receiver) {
|
|
10387
|
+
const segment = receiver?.replace(/\(\.\.\.\)$/u, "").split(".").at(-1) ?? "";
|
|
10388
|
+
return /^[a-zA-Z]/u.test(segment) ? segment[0].toUpperCase() + segment.slice(1) : "X";
|
|
10389
|
+
}
|
|
10390
|
+
/**
|
|
10391
|
+
* D90 R17: an undeclared foreign value arrives as unknown, and the way into
|
|
10392
|
+
* the typed world is `Type.parse` at the edge. Every refusal on an unknown
|
|
10393
|
+
* teaches that ritual with the author's own expression spelled into it.
|
|
10394
|
+
*/
|
|
10395
|
+
boundaryValidationGuidance(expression, property) {
|
|
10396
|
+
const receiver = expression ? this.boundaryReceiverText(expression) : null;
|
|
10397
|
+
const name = this.boundaryTypeNameSuggestion(receiver);
|
|
10398
|
+
const spelled = receiver ?? "value";
|
|
10399
|
+
const declared = property === null
|
|
10400
|
+
? `declare a type naming the shape you rely on — 'type ${name}:'`
|
|
10401
|
+
: `declare a type naming the fields you rely on — 'type ${name}:' with the '${property}' field`;
|
|
10402
|
+
const read = property === null ? "use 'checked' from there" : `read 'checked.${property}'`;
|
|
10403
|
+
return `; ${declared} — then validate first: 'const checked = ${name}.parse(${spelled})' and ${read}`;
|
|
10404
|
+
}
|
|
9380
10405
|
// Presence guidance names the exact spelling to write whenever the condition
|
|
9381
10406
|
// is a plain name or a plain member path; anything else is taught the
|
|
9382
10407
|
// operator without inventing source text for it.
|
|
@@ -9434,6 +10459,17 @@ export class Analyzer {
|
|
|
9434
10459
|
this.typeError(`Cannot assign ${actualDescription} to ${expectedDescription}; ${asyncResult}`, valueSpan);
|
|
9435
10460
|
return;
|
|
9436
10461
|
}
|
|
10462
|
+
// D90 R17: an undeclared foreign value is unknown until validated, so
|
|
10463
|
+
// the mismatch teaches the entry ritual instead of restating the kinds.
|
|
10464
|
+
if (expandedActual.kind === "unknown" && !isInvalidType(expandedActual)) {
|
|
10465
|
+
const named = expectedCore.kind === "named" || expectedCore.kind === "enum"
|
|
10466
|
+
? `'const checked = ${describeType(expectedCore)}.parse(value)'`
|
|
10467
|
+
: expectedCore.kind === "string" || expectedCore.kind === "number" || expectedCore.kind === "bool"
|
|
10468
|
+
? `narrow it with 'value is ${describeType(expectedCore)}', or parse a declared shape`
|
|
10469
|
+
: "declare a type naming the shape you rely on and call 'Type.parse' on the value";
|
|
10470
|
+
this.typeError(`Cannot assign ${actualDescription} to ${expectedDescription}; a boundary value stays unknown until validated at the edge — ${named}`, valueSpan);
|
|
10471
|
+
return;
|
|
10472
|
+
}
|
|
9437
10473
|
// COL-U10: a value of one collection family in another family's
|
|
9438
10474
|
// position gets the bridge spelling, not a bare mismatch.
|
|
9439
10475
|
const bridge = this.collectionBridgeGuidance(expandedActual, expectedCore);
|
|
@@ -9979,8 +11015,12 @@ export class Analyzer {
|
|
|
9979
11015
|
return extensionResult;
|
|
9980
11016
|
switch (syntax.kind) {
|
|
9981
11017
|
case "NamedTypeSyntax": {
|
|
11018
|
+
// D90 R17 removed the boundary that used to produce `any`, so the
|
|
11019
|
+
// old reason clause ("reserved for explicit unsafe JavaScript
|
|
11020
|
+
// boundaries") named a producer that no longer exists. The refusal
|
|
11021
|
+
// now teaches the same entrance every other unknown refusal teaches.
|
|
9982
11022
|
if (syntax.name === "any") {
|
|
9983
|
-
this.typeError(
|
|
11023
|
+
this.typeError(`'any' is not a VelarScript type; a foreign value arrives as 'unknown', which is what you annotate${this.boundaryValidationGuidance(null, null)}`, syntax.span);
|
|
9984
11024
|
return false;
|
|
9985
11025
|
}
|
|
9986
11026
|
if (this.invalidDeclaredTypes.has(syntax.name))
|
|
@@ -10387,6 +11427,30 @@ export class Analyzer {
|
|
|
10387
11427
|
}
|
|
10388
11428
|
}
|
|
10389
11429
|
}
|
|
11430
|
+
/**
|
|
11431
|
+
* D50 rule 97.3: a retirement that leaves one surviving spelling did not
|
|
11432
|
+
* happen. `export {stringify} from "velar/json"` is an import spelling with
|
|
11433
|
+
* an export in front of it — the barrel republishes the retired bare name
|
|
11434
|
+
* and every downstream `import {stringify} from "./barrel.vel"` is clean
|
|
11435
|
+
* forever after. No mechanical fix: which reads in which other modules
|
|
11436
|
+
* wanted the name is not a rewrite this module can make.
|
|
11437
|
+
*/
|
|
11438
|
+
reportPermanentNamespaceReExports(program) {
|
|
11439
|
+
for (const statement of program.body) {
|
|
11440
|
+
if (statement.kind !== "ReExportDeclaration")
|
|
11441
|
+
continue;
|
|
11442
|
+
const roster = permanentNamespaceImportRoster(statement.source);
|
|
11443
|
+
if (!roster)
|
|
11444
|
+
continue;
|
|
11445
|
+
for (const specifier of statement.specifiers) {
|
|
11446
|
+
if (!roster.members.has(specifier.imported))
|
|
11447
|
+
continue;
|
|
11448
|
+
this.diagnostics.push(diagnostic("VEL3008", roster.namespace === null
|
|
11449
|
+
? `Use ${specifier.imported}(...) directly; a re-export cannot restore a retired import spelling, and the Core prelude needs none`
|
|
11450
|
+
: `Use ${roster.namespace}.${specifier.imported} directly; a re-export cannot restore a retired import spelling`, specifier.span));
|
|
11451
|
+
}
|
|
11452
|
+
}
|
|
11453
|
+
}
|
|
10390
11454
|
/** A member name to show in the rule 106 guidance, so the fix is concrete. */
|
|
10391
11455
|
firstNamespaceMember(namespace) {
|
|
10392
11456
|
const binding = this.builtin(namespace);
|
|
@@ -10403,6 +11467,25 @@ export class Analyzer {
|
|
|
10403
11467
|
recoveredTypeError(message, errorSpan, fix) {
|
|
10404
11468
|
this.diagnostics.push(recoveredDiagnostic("VEL4001", message, errorSpan, fix));
|
|
10405
11469
|
}
|
|
11470
|
+
/**
|
|
11471
|
+
* D89: raises a roster advisory. It cannot reach `this.diagnostics`, so it
|
|
11472
|
+
* cannot fail a build and cannot shift the diagnostic cursors this analyzer
|
|
11473
|
+
* reads as array lengths.
|
|
11474
|
+
*
|
|
11475
|
+
* One report per code and span. `reanalyzeLoopBackEdge` runs a loop body a
|
|
11476
|
+
* second time whenever the back edge invalidates a fact, and its diagnostic
|
|
11477
|
+
* answer is `deduplicateDiagnostics`, which only ever touches
|
|
11478
|
+
* `this.diagnostics`. Deduplicating where the advisory is raised covers that
|
|
11479
|
+
* pass and every other re-analysis without a second pair of cursors, which
|
|
11480
|
+
* is the whole reason the two channels are separate arrays.
|
|
11481
|
+
*/
|
|
11482
|
+
advise(code, message, adviceSpan, fix) {
|
|
11483
|
+
const identity = `${code}\u0000${adviceSpan.start}\u0000${adviceSpan.end}`;
|
|
11484
|
+
if (this.advisedIdentities.has(identity))
|
|
11485
|
+
return;
|
|
11486
|
+
this.advisedIdentities.add(identity);
|
|
11487
|
+
this.advisories.push(advisory(code, message, adviceSpan, fix));
|
|
11488
|
+
}
|
|
10406
11489
|
analyzeMatchPattern(pattern, input, bindings) {
|
|
10407
11490
|
switch (pattern.kind) {
|
|
10408
11491
|
case "MatchAsPattern": {
|
|
@@ -10884,11 +11967,15 @@ export class Analyzer {
|
|
|
10884
11967
|
}
|
|
10885
11968
|
builtin(name) {
|
|
10886
11969
|
const type = this.extensionGlobals.get(name) ?? coreVocabularyType(name)
|
|
10887
|
-
?? (name === "Error" || name === "ValidationError" || name === "
|
|
11970
|
+
?? (name === "Error" || name === "ValidationError" || name === "AssertionError"
|
|
11971
|
+
|| name === "NarrowingError" || name === "IndexError"
|
|
10888
11972
|
|| VELAR_HOST_ERROR_NAMES.includes(name)
|
|
10889
11973
|
? { kind: "classConstructor", name }
|
|
10890
11974
|
: null)
|
|
10891
|
-
|
|
11975
|
+
// D90 R17: `Map`/`Set` as bare values are collection constructors the
|
|
11976
|
+
// call path special-cases; the bare binding itself carries no members,
|
|
11977
|
+
// so it is unknown, never a silent `any`.
|
|
11978
|
+
?? (name === "Map" || name === "Set" ? unknownType : null);
|
|
10892
11979
|
return type ? {
|
|
10893
11980
|
mutable: false,
|
|
10894
11981
|
type,
|
|
@@ -10904,19 +11991,144 @@ export class Analyzer {
|
|
|
10904
11991
|
* constructor's own arguments. Nothing infers it from a later mutation, so a
|
|
10905
11992
|
* binding left with no source is reported at the construction rather than
|
|
10906
11993
|
* kept as `unknown` for a following line to fill in.
|
|
11994
|
+
*
|
|
11995
|
+
* The value written at this position is not always the construction itself.
|
|
11996
|
+
* A ternary arm, a list element or its spread, a record-literal field, a
|
|
11997
|
+
* `??` fallback, a receiver and an argument all become part of the value the
|
|
11998
|
+
* name holds, so each is its own settling position and each reports at its
|
|
11999
|
+
* own `[]`. What stops the walk is the value, not the syntax: it descends
|
|
12000
|
+
* only while `carriesUnsettledCollection` still sees the hole in the type
|
|
12001
|
+
* arriving here, so `print(Set().size)` and `const n = Set().size` stay
|
|
12002
|
+
* legal per rule 208 — neither of those names holds a collection — while a
|
|
12003
|
+
* spread whose `unknown` the merge absorbs (`["x", ...[]]`) leaves nothing
|
|
12004
|
+
* to report. A sibling settles nothing for its neighbour: `[["a"], []]`
|
|
12005
|
+
* merges through `unionOf`, so the union still carries the hole and the
|
|
12006
|
+
* empty `[]` reports on its own.
|
|
12007
|
+
*
|
|
12008
|
+
* Returns whether it reported, so the caller can hand the name `invalidType`
|
|
12009
|
+
* instead of the hole. Rule 209 requires one mistake to be reported once,
|
|
12010
|
+
* and `List<unknown>` reaching a later line is what produces the second,
|
|
12011
|
+
* contradicting report the ruling exists to delete.
|
|
10907
12012
|
*/
|
|
10908
12013
|
requireSettledCollectionElement(initializer, declared, annotated) {
|
|
10909
12014
|
if (annotated)
|
|
12015
|
+
return false;
|
|
12016
|
+
return this.reportUnsettledCollection(initializer, this.expandAliases(declared));
|
|
12017
|
+
}
|
|
12018
|
+
/**
|
|
12019
|
+
* D85 rule 209: where the value at this position came from, when it came
|
|
12020
|
+
* from a hole VEL4039 already reported. The answer is two-part because a
|
|
12021
|
+
* callee can be declared after its caller: `true` is a hole already on
|
|
12022
|
+
* record, and `causes` are the local results that make this position a hole
|
|
12023
|
+
* too if theirs turn out to be one.
|
|
12024
|
+
*
|
|
12025
|
+
* Only a name and a call to a local name are modelled — the two shapes an
|
|
12026
|
+
* author writes between an empty collection and the `return` that publishes
|
|
12027
|
+
* it. Anything else contributes nothing, so an unmodelled position keeps the
|
|
12028
|
+
* report it has today rather than losing one.
|
|
12029
|
+
*/
|
|
12030
|
+
collectResultHoleSources(expression, causes) {
|
|
12031
|
+
if (expression.kind === "IdentifierExpression") {
|
|
12032
|
+
const binding = this.lookup(expression.name);
|
|
12033
|
+
if (!binding)
|
|
12034
|
+
return false;
|
|
12035
|
+
for (const cause of this.bindingHoleCauses.get(binding) ?? [])
|
|
12036
|
+
causes.add(cause);
|
|
12037
|
+
return this.reportedCollectionHoles.has(binding);
|
|
12038
|
+
}
|
|
12039
|
+
if (expression.kind === "CallExpression" && expression.callee.kind === "IdentifierExpression") {
|
|
12040
|
+
const binding = this.lookup(expression.callee.name);
|
|
12041
|
+
const resultKey = binding ? this.functionResultKeys.get(binding) : undefined;
|
|
12042
|
+
// An imported, dynamically dispatched, or method call resolves to no
|
|
12043
|
+
// local result. Its hole — if it has one — was reported in the module
|
|
12044
|
+
// that owns it, and nothing here can say so, so the call is not a cause.
|
|
12045
|
+
if (resultKey === undefined)
|
|
12046
|
+
return false;
|
|
12047
|
+
if (this.reportedResultHoles.has(resultKey))
|
|
12048
|
+
return true;
|
|
12049
|
+
causes.add(resultKey);
|
|
12050
|
+
return false;
|
|
12051
|
+
}
|
|
12052
|
+
return false;
|
|
12053
|
+
}
|
|
12054
|
+
/**
|
|
12055
|
+
* D85 rule 209: a name bound to a reported hole carries it, so `const a = []`
|
|
12056
|
+
* followed by `return a` is the same one mistake `return []` is. Only an
|
|
12057
|
+
* unannotated `const`/`let` of a single name carries anything: an annotation
|
|
12058
|
+
* settles the construction, and a destructuring pattern takes the hole apart
|
|
12059
|
+
* rather than passing it on.
|
|
12060
|
+
*/
|
|
12061
|
+
recordBindingHoleSource(pattern, initializer, reported) {
|
|
12062
|
+
if (pattern.kind !== "NameBindingPattern")
|
|
12063
|
+
return;
|
|
12064
|
+
const binding = this.scopes.at(-1)?.get(pattern.name);
|
|
12065
|
+
if (!binding)
|
|
10910
12066
|
return;
|
|
10911
|
-
const
|
|
10912
|
-
if (
|
|
12067
|
+
const causes = new Set();
|
|
12068
|
+
if (reported || this.collectResultHoleSources(initializer, causes)) {
|
|
12069
|
+
this.reportedCollectionHoles.add(binding);
|
|
12070
|
+
return;
|
|
12071
|
+
}
|
|
12072
|
+
if (causes.size > 0)
|
|
12073
|
+
this.bindingHoleCauses.set(binding, causes);
|
|
12074
|
+
}
|
|
12075
|
+
/**
|
|
12076
|
+
* D85 rule 209: delete the convergence report of every function whose result
|
|
12077
|
+
* is invalid only because a hole VEL4039 already explained reached it through
|
|
12078
|
+
* a local call. The set grows until it stops growing, because a chain of
|
|
12079
|
+
* forwarding functions is still one mistake however long it is — and a cycle
|
|
12080
|
+
* with no empty collection anywhere in it never enters the set, so a genuine
|
|
12081
|
+
* convergence failure still reports on both of its halves.
|
|
12082
|
+
*/
|
|
12083
|
+
resolveDeferredConvergenceReports() {
|
|
12084
|
+
if (this.deferredConvergenceReports.length === 0)
|
|
10913
12085
|
return;
|
|
10914
|
-
const
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
12086
|
+
const suppressed = new Set();
|
|
12087
|
+
for (let growing = true; growing;) {
|
|
12088
|
+
growing = false;
|
|
12089
|
+
for (const entry of this.deferredConvergenceReports) {
|
|
12090
|
+
if (suppressed.has(entry.report))
|
|
12091
|
+
continue;
|
|
12092
|
+
if (![...entry.causes].some((cause) => this.reportedResultHoles.has(cause)))
|
|
12093
|
+
continue;
|
|
12094
|
+
suppressed.add(entry.report);
|
|
12095
|
+
this.reportedResultHoles.add(entry.resultKey);
|
|
12096
|
+
growing = true;
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
for (let index = this.diagnostics.length - 1; index >= 0; index -= 1) {
|
|
12100
|
+
const report = this.diagnostics[index];
|
|
12101
|
+
if (report && suppressed.has(report))
|
|
12102
|
+
this.diagnostics.splice(index, 1);
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
reportUnsettledCollection(expression, type) {
|
|
12106
|
+
if (type !== null) {
|
|
12107
|
+
if (this.isFreshUnresolvedCollection(expression, type)) {
|
|
12108
|
+
const [spelling, holds, example] = type.kind === "list"
|
|
12109
|
+
? ["[]", "what the List holds", "let items: List<string> = []"]
|
|
12110
|
+
: type.kind === "set"
|
|
12111
|
+
? ["Set()", "what the Set holds", "const tags: Set<string> = Set()"]
|
|
12112
|
+
: ["Map()", "what the Map holds", "const users: Map<string, User> = Map()"];
|
|
12113
|
+
this.diagnostics.push(diagnostic("VEL4039", `Empty '${spelling}' requires an explicit type; nothing at this position says ${holds} — write '${example}'`, expression.span));
|
|
12114
|
+
return true;
|
|
12115
|
+
}
|
|
12116
|
+
if (!carriesUnsettledCollection(type))
|
|
12117
|
+
return false;
|
|
12118
|
+
}
|
|
12119
|
+
let reported = false;
|
|
12120
|
+
for (const part of settlingValuePositions(expression)) {
|
|
12121
|
+
// A part analyzed under a contextual type that settled it never reaches
|
|
12122
|
+
// here as `unknown`; one that was analyzed at all has its answer on
|
|
12123
|
+
// record. A part with no answer on record was never inferred as a whole
|
|
12124
|
+
// — `Map([[key, value]])` reads the entry's two leaves and never the
|
|
12125
|
+
// entry list itself — so the walk carries on through the gap rather
|
|
12126
|
+
// than stopping at one it did not make.
|
|
12127
|
+
const partType = this.inferredExpressionTypes.get(spanIdentity(part.span));
|
|
12128
|
+
if (this.reportUnsettledCollection(part, partType ? this.expandAliases(partType) : null))
|
|
12129
|
+
reported = true;
|
|
12130
|
+
}
|
|
12131
|
+
return reported;
|
|
10920
12132
|
}
|
|
10921
12133
|
isFreshUnresolvedCollection(expression, type) {
|
|
10922
12134
|
const unresolved = type.kind === "list" ? type.element.kind === "unknown"
|
|
@@ -10998,7 +12210,9 @@ export class Analyzer {
|
|
|
10998
12210
|
storageType: type,
|
|
10999
12211
|
span: declarationSpan,
|
|
11000
12212
|
narrowingFrame: null,
|
|
12213
|
+
flowScope: this.scopes.length - 1,
|
|
11001
12214
|
};
|
|
12215
|
+
this.recordScopedName(name);
|
|
11002
12216
|
scope.set(name, binding);
|
|
11003
12217
|
if (this.scopes.length === 1 && type.kind === "typeObject")
|
|
11004
12218
|
this.runtimeTypeObjectNames.add(name);
|
|
@@ -11024,12 +12238,54 @@ export class Analyzer {
|
|
|
11024
12238
|
}
|
|
11025
12239
|
recordInitializationImportRead(binding, local, span) {
|
|
11026
12240
|
const origin = this.importedBindingSources.get(binding);
|
|
11027
|
-
if (origin === undefined
|
|
12241
|
+
if (origin === undefined)
|
|
12242
|
+
return;
|
|
12243
|
+
const read = { local, source: origin.source, imported: origin.imported, span };
|
|
12244
|
+
// D31 item 23: a read inside a deferred body belongs to that body, not to
|
|
12245
|
+
// the module. Whether it runs during module evaluation is decided by
|
|
12246
|
+
// `moduleInitializationImportReads`, once the top-level calls are known.
|
|
12247
|
+
const frame = this.deferredReadFrames.at(-1);
|
|
12248
|
+
if (frame) {
|
|
12249
|
+
frame.reads.push(read);
|
|
11028
12250
|
return;
|
|
11029
|
-
const key = spanIdentity(span);
|
|
11030
|
-
if (!this.initializationImportReadSites.has(key)) {
|
|
11031
|
-
this.initializationImportReadSites.set(key, { local, source: origin.source, imported: origin.imported, span });
|
|
11032
12251
|
}
|
|
12252
|
+
if (!this.inModuleInitializationPosition())
|
|
12253
|
+
return;
|
|
12254
|
+
const key = spanIdentity(span);
|
|
12255
|
+
if (!this.initializationImportReadSites.has(key))
|
|
12256
|
+
this.initializationImportReadSites.set(key, read);
|
|
12257
|
+
}
|
|
12258
|
+
/**
|
|
12259
|
+
* D31 item 23: the call edge. Inside a deferred body it is an edge of the
|
|
12260
|
+
* reachability graph; at module top level it is a root, because that call
|
|
12261
|
+
* runs the callee while the module itself evaluates. The callee is held as
|
|
12262
|
+
* a binding, not as a frame — a `def` is hoisted, so `const x = pull()` can
|
|
12263
|
+
* be analyzed before `def pull()` is.
|
|
12264
|
+
*/
|
|
12265
|
+
recordDeferredCallEdge(callee, span) {
|
|
12266
|
+
if (callee.kind !== "IdentifierExpression")
|
|
12267
|
+
return;
|
|
12268
|
+
// The two cheap questions first: every other call would pay for a scope
|
|
12269
|
+
// lookup whose answer nothing reads.
|
|
12270
|
+
const frame = this.deferredReadFrames.at(-1);
|
|
12271
|
+
if (!frame && !this.inModuleInitializationPosition())
|
|
12272
|
+
return;
|
|
12273
|
+
const binding = this.lookup(callee.name);
|
|
12274
|
+
if (!binding)
|
|
12275
|
+
return;
|
|
12276
|
+
if (frame)
|
|
12277
|
+
frame.calls.push(binding);
|
|
12278
|
+
else
|
|
12279
|
+
this.initializationLocalCalls.push({ binding, span });
|
|
12280
|
+
}
|
|
12281
|
+
/** Files an arrow's deferred frame under the module-local name it was bound to. */
|
|
12282
|
+
claimArrowDeferredFrame(pattern, initializer) {
|
|
12283
|
+
if (initializer.kind !== "ArrowFunctionExpression" || pattern.kind !== "NameBindingPattern")
|
|
12284
|
+
return;
|
|
12285
|
+
const frame = this.arrowDeferredFrames.get(spanIdentity(initializer.span));
|
|
12286
|
+
const binding = this.scopes.at(-1)?.get(pattern.name);
|
|
12287
|
+
if (frame && binding)
|
|
12288
|
+
this.localFunctionFrames.set(binding, frame);
|
|
11033
12289
|
}
|
|
11034
12290
|
// True while code at this point runs during module evaluation itself:
|
|
11035
12291
|
// top-level initializers and expression statements (including nested
|
|
@@ -11047,9 +12303,52 @@ export class Analyzer {
|
|
|
11047
12303
|
&& this.instanceFieldInitializerDepth === 0
|
|
11048
12304
|
&& this.deferredExecutionDepth === 0;
|
|
11049
12305
|
}
|
|
11050
|
-
/**
|
|
12306
|
+
/**
|
|
12307
|
+
* Initialization-position reads of imported bindings, for the project
|
|
12308
|
+
* module-cycle check.
|
|
12309
|
+
*
|
|
12310
|
+
* D31 item 23 recorded the indirect shape as a v1 residual: a top-level call
|
|
12311
|
+
* of a module-local function runs that body while the module evaluates, so
|
|
12312
|
+
* an imported binding read inside it is an initialization-position read too
|
|
12313
|
+
* — and following VEL3019's own remediation ("Move this read into a
|
|
12314
|
+
* function") and then calling that function at top level re-created the bare
|
|
12315
|
+
* `ReferenceError` the check exists to delete. The closure below is the
|
|
12316
|
+
* intra-module reachability pass that closes it: one module, one walk over
|
|
12317
|
+
* the call edges already collected, no cross-module analysis.
|
|
12318
|
+
*
|
|
12319
|
+
* An indirect read is reported at the *call*, not at the read. The call is
|
|
12320
|
+
* the line that runs during module evaluation and the line an author can
|
|
12321
|
+
* move; the read inside the body is already in a function, which is what the
|
|
12322
|
+
* remediation asks for.
|
|
12323
|
+
*/
|
|
11051
12324
|
moduleInitializationImportReads() {
|
|
11052
|
-
|
|
12325
|
+
const sites = new Map(this.initializationImportReadSites);
|
|
12326
|
+
const visited = new Set();
|
|
12327
|
+
const collect = (frame, callSpan) => {
|
|
12328
|
+
if (visited.has(frame))
|
|
12329
|
+
return;
|
|
12330
|
+
visited.add(frame);
|
|
12331
|
+
for (const read of frame.reads) {
|
|
12332
|
+
const key = `${spanIdentity(callSpan)}\0${read.local}\0${read.source}`;
|
|
12333
|
+
if (!sites.has(key))
|
|
12334
|
+
sites.set(key, { ...read, span: callSpan });
|
|
12335
|
+
}
|
|
12336
|
+
for (const called of frame.calls) {
|
|
12337
|
+
const next = this.localFunctionFrames.get(called);
|
|
12338
|
+
if (next)
|
|
12339
|
+
collect(next, callSpan);
|
|
12340
|
+
}
|
|
12341
|
+
};
|
|
12342
|
+
for (const call of this.initializationLocalCalls) {
|
|
12343
|
+
const frame = this.localFunctionFrames.get(call.binding);
|
|
12344
|
+
// One root at a time: two roots reaching the same body must each report,
|
|
12345
|
+
// so the visited set is per root rather than per module.
|
|
12346
|
+
if (frame) {
|
|
12347
|
+
visited.clear();
|
|
12348
|
+
collect(frame, call.span);
|
|
12349
|
+
}
|
|
12350
|
+
}
|
|
12351
|
+
return [...sites.values()];
|
|
11053
12352
|
}
|
|
11054
12353
|
// D32 item 29: the language-wide text-conversion contract (charter
|
|
11055
12354
|
// section 14) shared by f-strings, str(), and target-owned render sites.
|
|
@@ -11330,6 +12629,128 @@ export class Analyzer {
|
|
|
11330
12629
|
}
|
|
11331
12630
|
}
|
|
11332
12631
|
}
|
|
12632
|
+
/**
|
|
12633
|
+
* D90 R12: "exported" is a property of the declaration a consumer can reach,
|
|
12634
|
+
* not of the `def` keyword. A module-level declaration carries the flag
|
|
12635
|
+
* itself and is judged here and now. A class member carries none — a public
|
|
12636
|
+
* member of a class this module publishes is read by a consumer exactly as
|
|
12637
|
+
* an exported `const` is — but whether the class is published is a question
|
|
12638
|
+
* about the whole module, so the member waits for reportExportPositionAny. A
|
|
12639
|
+
* `private` member is never reachable, and R12's boundary does not move:
|
|
12640
|
+
* module-internal `any` stays legal.
|
|
12641
|
+
*/
|
|
12642
|
+
recordExportedAny(statement, className, span) {
|
|
12643
|
+
if (statement.exported === true) {
|
|
12644
|
+
this.reportExportedAny([statement.name], span);
|
|
12645
|
+
return;
|
|
12646
|
+
}
|
|
12647
|
+
if (className === null || statement.private === true)
|
|
12648
|
+
return;
|
|
12649
|
+
this.exportPositionCandidates.push({ className, member: statement.name, span });
|
|
12650
|
+
}
|
|
12651
|
+
/**
|
|
12652
|
+
* D90 R12: the class members that turned out to be at an export position.
|
|
12653
|
+
* Reported once the module is analyzed, because the answer is reachability
|
|
12654
|
+
* and reachability is a property of the module, not of the declaration.
|
|
12655
|
+
*/
|
|
12656
|
+
reportExportPositionAny(program) {
|
|
12657
|
+
if (this.exportPositionCandidates.length === 0)
|
|
12658
|
+
return;
|
|
12659
|
+
const reachable = this.exportReachableClasses(program);
|
|
12660
|
+
for (const candidate of this.exportPositionCandidates) {
|
|
12661
|
+
if (reachable.has(candidate.className)) {
|
|
12662
|
+
this.reportExportedAny([`${candidate.className}.${candidate.member}`], candidate.span);
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
/**
|
|
12667
|
+
* D90 R12: which class declarations a consuming module can reach. Exported
|
|
12668
|
+
* classes seed the set; from there it follows every position a consumer can
|
|
12669
|
+
* read a value *out of* — the type of anything else this module exports, the
|
|
12670
|
+
* base a reachable class names, and the public surface of a class already
|
|
12671
|
+
* reachable. `export class Box extends Base:` publishes `Base`'s members,
|
|
12672
|
+
* and `def make() -> Inner` publishes `Inner`'s, whether or not either name
|
|
12673
|
+
* is exported.
|
|
12674
|
+
*
|
|
12675
|
+
* Input positions are deliberately absent, for the same reason
|
|
12676
|
+
* `typeContainsAnyOutput` omits them: a consumer that has to *supply* an
|
|
12677
|
+
* instance obtained it from an output position first, and that position is
|
|
12678
|
+
* what makes the class reachable.
|
|
12679
|
+
*/
|
|
12680
|
+
exportReachableClasses(program) {
|
|
12681
|
+
const classes = [];
|
|
12682
|
+
const records = [];
|
|
12683
|
+
const reach = (type) => {
|
|
12684
|
+
if (type)
|
|
12685
|
+
collectOutputTypeNames(type, classes, records);
|
|
12686
|
+
};
|
|
12687
|
+
// The same walk validateReExports makes over the module's export surface,
|
|
12688
|
+
// so the two cannot disagree about what "this module exports" means.
|
|
12689
|
+
const publish = (name) => {
|
|
12690
|
+
reach(this.scopes[0].get(name)?.type);
|
|
12691
|
+
records.push(name);
|
|
12692
|
+
};
|
|
12693
|
+
for (const statement of program.body) {
|
|
12694
|
+
if (statement.kind === "ReExportDeclaration" || !("exported" in statement) || !statement.exported)
|
|
12695
|
+
continue;
|
|
12696
|
+
if (statement.kind === "ClassDeclaration")
|
|
12697
|
+
classes.push(statement.name);
|
|
12698
|
+
else if (statement.kind === "VariableDeclaration")
|
|
12699
|
+
this.collectPatternNames(statement.pattern, publish);
|
|
12700
|
+
else if ("name" in statement && typeof statement.name === "string")
|
|
12701
|
+
publish(statement.name);
|
|
12702
|
+
}
|
|
12703
|
+
const reachable = new Set();
|
|
12704
|
+
const visitedRecords = new Set();
|
|
12705
|
+
while (classes.length > 0 || records.length > 0) {
|
|
12706
|
+
if (records.length > 0) {
|
|
12707
|
+
const name = records.pop();
|
|
12708
|
+
if (visitedRecords.has(name))
|
|
12709
|
+
continue;
|
|
12710
|
+
visitedRecords.add(name);
|
|
12711
|
+
// A record a consumer holds is read field by field, so a class in a
|
|
12712
|
+
// field is reachable even when the record type itself is not exported.
|
|
12713
|
+
for (const field of this.namedTypes.get(name)?.values() ?? [])
|
|
12714
|
+
reach(field);
|
|
12715
|
+
reach(this.typeAliases.get(name));
|
|
12716
|
+
continue;
|
|
12717
|
+
}
|
|
12718
|
+
const name = classes.pop();
|
|
12719
|
+
if (reachable.has(name))
|
|
12720
|
+
continue;
|
|
12721
|
+
reachable.add(name);
|
|
12722
|
+
const info = this.classes.get(name);
|
|
12723
|
+
if (!info)
|
|
12724
|
+
continue;
|
|
12725
|
+
if (info.base)
|
|
12726
|
+
classes.push(info.base);
|
|
12727
|
+
reach(info.iterate);
|
|
12728
|
+
// ClassInfo is exactly the public surface — private members live in
|
|
12729
|
+
// their own tables — and `fields` carries the getters' result types.
|
|
12730
|
+
// The constructor's parameters are inputs, so they are not followed.
|
|
12731
|
+
for (const field of info.fields.values())
|
|
12732
|
+
reach(field.type);
|
|
12733
|
+
for (const field of info.staticFields.values())
|
|
12734
|
+
reach(field.type);
|
|
12735
|
+
for (const method of info.methods.values())
|
|
12736
|
+
reach(method);
|
|
12737
|
+
for (const method of info.staticMethods.values())
|
|
12738
|
+
reach(method);
|
|
12739
|
+
}
|
|
12740
|
+
return reachable;
|
|
12741
|
+
}
|
|
12742
|
+
/**
|
|
12743
|
+
* D90 R12: the diagnostic has to teach the way out, not only refuse. A
|
|
12744
|
+
* consuming module never writes `unsafe`, so an exported `any` hands it a
|
|
12745
|
+
* value carrying no guarantee at all; the escape is to validate the value
|
|
12746
|
+
* into a declared type in the module that owns the boundary, which is what
|
|
12747
|
+
* `Type.parse` exists for. No new diagnostic code and no unsafe marker: this
|
|
12748
|
+
* is the rule at validateTypeReference finished, not a second rule.
|
|
12749
|
+
*/
|
|
12750
|
+
reportExportedAny(exported, span) {
|
|
12751
|
+
const names = exported.map((name) => `'${name}'`).join(", ");
|
|
12752
|
+
this.typeError(`${exported.length === 1 ? "Export" : "Exports"} ${names} ${exported.length === 1 ? "is" : "are"} 'any', which cannot cross a module boundary; validate the value into a declared type in this module first — 'const settled = Config.parse(candidate)' — and export that`, span);
|
|
12753
|
+
}
|
|
11333
12754
|
collectPatternNames(pattern, add) {
|
|
11334
12755
|
if (pattern.kind === "NameBindingPattern") {
|
|
11335
12756
|
add(pattern.name);
|
|
@@ -11391,7 +12812,10 @@ export class Analyzer {
|
|
|
11391
12812
|
: type.kind === "any" ? anyType : unknownType;
|
|
11392
12813
|
const declaredElement = declaredType.kind === "list" ? declaredType.readonlyView ? this.readonlyDataViewOf(declaredType.element) : declaredType.element
|
|
11393
12814
|
: declaredType.kind === "any" ? anyType : unknownType;
|
|
11394
|
-
|
|
12815
|
+
// An invalid source has already been reported where it went wrong —
|
|
12816
|
+
// D85 rule 209's "one mistake, one report" — and `describeType` would
|
|
12817
|
+
// render it as the bare `unknown` nobody wrote.
|
|
12818
|
+
if (type.kind !== "list" && type.kind !== "any" && !isInvalidType(type)) {
|
|
11395
12819
|
this.typeError(`Cannot list-destructure ${describeType(type)}`, pattern.span);
|
|
11396
12820
|
}
|
|
11397
12821
|
for (const child of pattern.elements)
|
|
@@ -11520,7 +12944,7 @@ export class Analyzer {
|
|
|
11520
12944
|
}
|
|
11521
12945
|
else {
|
|
11522
12946
|
const binding = this.lookup(key);
|
|
11523
|
-
|
|
12947
|
+
const shadow = {
|
|
11524
12948
|
mutable: binding?.mutable ?? false,
|
|
11525
12949
|
type,
|
|
11526
12950
|
declaredType: binding?.declaredType ?? type,
|
|
@@ -11528,8 +12952,14 @@ export class Analyzer {
|
|
|
11528
12952
|
...(binding ? { storageBinding: binding.storageBinding ?? binding } : {}),
|
|
11529
12953
|
span: binding?.span ?? narrowingSpan,
|
|
11530
12954
|
narrowingFrame: this.flowFrameDepth,
|
|
12955
|
+
flowScope: this.scopes.length - 1,
|
|
11531
12956
|
...(binding?.reactiveKind ? { reactiveKind: binding.reactiveKind } : {}),
|
|
11532
|
-
}
|
|
12957
|
+
};
|
|
12958
|
+
this.trackNarrowingShadow(shadow);
|
|
12959
|
+
this.narrowedNames.at(-1).add(key);
|
|
12960
|
+
if (!this.scopes.at(-1).has(key))
|
|
12961
|
+
this.recordScopedName(key);
|
|
12962
|
+
this.scopes.at(-1).set(key, shadow);
|
|
11533
12963
|
}
|
|
11534
12964
|
}
|
|
11535
12965
|
}
|
|
@@ -11545,14 +12975,16 @@ export class Analyzer {
|
|
|
11545
12975
|
if (!binding)
|
|
11546
12976
|
continue;
|
|
11547
12977
|
const local = scope.get(key);
|
|
12978
|
+
this.narrowedNames.at(-1).add(key);
|
|
11548
12979
|
if (local) {
|
|
12980
|
+
this.recordFlowFactOrigin(local);
|
|
11549
12981
|
local.type = type;
|
|
11550
12982
|
local.narrowingFrame = this.flowFrameDepth;
|
|
11551
12983
|
// A persisted (checked or merged) fact is not assignment-established.
|
|
11552
12984
|
local.assignedFact = false;
|
|
11553
12985
|
}
|
|
11554
12986
|
else {
|
|
11555
|
-
|
|
12987
|
+
const shadow = {
|
|
11556
12988
|
mutable: binding.mutable,
|
|
11557
12989
|
type,
|
|
11558
12990
|
declaredType: binding.declaredType,
|
|
@@ -11560,8 +12992,12 @@ export class Analyzer {
|
|
|
11560
12992
|
storageBinding: binding.storageBinding ?? binding,
|
|
11561
12993
|
span: binding.span,
|
|
11562
12994
|
narrowingFrame: this.flowFrameDepth,
|
|
12995
|
+
flowScope: this.scopes.length - 1,
|
|
11563
12996
|
...(binding.reactiveKind ? { reactiveKind: binding.reactiveKind } : {}),
|
|
11564
|
-
}
|
|
12997
|
+
};
|
|
12998
|
+
this.trackNarrowingShadow(shadow);
|
|
12999
|
+
this.recordScopedName(key);
|
|
13000
|
+
scope.set(key, shadow);
|
|
11565
13001
|
}
|
|
11566
13002
|
}
|
|
11567
13003
|
}
|
|
@@ -11613,6 +13049,7 @@ export class Analyzer {
|
|
|
11613
13049
|
const shadow = scope.get(name);
|
|
11614
13050
|
if (!shadow || shadow === target || (shadow.storageBinding ?? shadow) !== storage)
|
|
11615
13051
|
continue;
|
|
13052
|
+
this.recordFlowFactOrigin(shadow);
|
|
11616
13053
|
shadow.storageType = storage.storageType;
|
|
11617
13054
|
shadow.type = storage.storageType;
|
|
11618
13055
|
shadow.narrowingFrame = null;
|
|
@@ -11628,13 +13065,15 @@ export class Analyzer {
|
|
|
11628
13065
|
return;
|
|
11629
13066
|
const scope = this.scopes.at(-1);
|
|
11630
13067
|
const local = scope.get(name);
|
|
13068
|
+
this.narrowedNames.at(-1).add(name);
|
|
11631
13069
|
if (local) {
|
|
13070
|
+
this.recordFlowFactOrigin(local);
|
|
11632
13071
|
local.type = fact;
|
|
11633
13072
|
local.narrowingFrame = this.flowFrameDepth;
|
|
11634
13073
|
local.assignedFact = true;
|
|
11635
13074
|
}
|
|
11636
13075
|
else {
|
|
11637
|
-
|
|
13076
|
+
const shadow = {
|
|
11638
13077
|
mutable: binding.mutable,
|
|
11639
13078
|
type: fact,
|
|
11640
13079
|
declaredType: binding.declaredType,
|
|
@@ -11643,8 +13082,12 @@ export class Analyzer {
|
|
|
11643
13082
|
span: binding.span,
|
|
11644
13083
|
narrowingFrame: this.flowFrameDepth,
|
|
11645
13084
|
assignedFact: true,
|
|
13085
|
+
flowScope: this.scopes.length - 1,
|
|
11646
13086
|
...(binding.reactiveKind ? { reactiveKind: binding.reactiveKind } : {}),
|
|
11647
|
-
}
|
|
13087
|
+
};
|
|
13088
|
+
this.trackNarrowingShadow(shadow);
|
|
13089
|
+
this.recordScopedName(name);
|
|
13090
|
+
scope.set(name, shadow);
|
|
11648
13091
|
}
|
|
11649
13092
|
}
|
|
11650
13093
|
/** Rule 71 for member targets: establish after invalidation so the new fact survives its own write. */
|
|
@@ -11786,6 +13229,7 @@ export class Analyzer {
|
|
|
11786
13229
|
invalidateAssignmentNarrowings(target, binding) {
|
|
11787
13230
|
if (target.kind === "IdentifierExpression") {
|
|
11788
13231
|
if (binding && binding.narrowingFrame !== null) {
|
|
13232
|
+
this.recordFlowFactOrigin(binding);
|
|
11789
13233
|
binding.type = binding.storageType;
|
|
11790
13234
|
binding.narrowingFrame = null;
|
|
11791
13235
|
binding.assignedFact = false;
|
|
@@ -11935,25 +13379,66 @@ export class Analyzer {
|
|
|
11935
13379
|
: null;
|
|
11936
13380
|
return pattern.entries.some((entry) => this.matchPatternReflectionMayExecute(entry.pattern, fields?.get(entry.property) ?? unknownType));
|
|
11937
13381
|
}
|
|
13382
|
+
/**
|
|
13383
|
+
* A snapshot used to copy every binding of every live scope, which made a
|
|
13384
|
+
* branch cost O(names in the module) and whole-module analysis quadratic in
|
|
13385
|
+
* module size. A binding nothing ever narrows cannot differ between two
|
|
13386
|
+
* moments, so only the bindings flow analysis has actually written are
|
|
13387
|
+
* visited — `flowTouched`, kept per scope depth so an exiting scope drops
|
|
13388
|
+
* its own, and `flowOrigins`, which remembers what each one held before its
|
|
13389
|
+
* first write. `flowOrigins` answers for a binding a *later* write touched
|
|
13390
|
+
* than the snapshot being restored: the snapshot has no entry, and its
|
|
13391
|
+
* pre-write state is exactly the state that snapshot recorded. A narrowing
|
|
13392
|
+
* shadow born after the snapshot stores `null` instead, because a full-scope
|
|
13393
|
+
* snapshot had nothing to restore it to either.
|
|
13394
|
+
*/
|
|
13395
|
+
flowFactState(binding) {
|
|
13396
|
+
return {
|
|
13397
|
+
type: binding.type,
|
|
13398
|
+
storageType: binding.storageType,
|
|
13399
|
+
frame: binding.narrowingFrame,
|
|
13400
|
+
assigned: binding.assignedFact === true,
|
|
13401
|
+
};
|
|
13402
|
+
}
|
|
13403
|
+
/** Called immediately before flow analysis writes a binding, so the recorded state is the pre-write one. */
|
|
13404
|
+
recordFlowFactOrigin(binding) {
|
|
13405
|
+
if (this.flowOrigins.has(binding))
|
|
13406
|
+
return;
|
|
13407
|
+
this.flowOrigins.set(binding, this.flowFactState(binding));
|
|
13408
|
+
this.trackFlowBinding(binding);
|
|
13409
|
+
}
|
|
13410
|
+
/** A narrowing shadow created mid-flow: no older snapshot has a state for it. */
|
|
13411
|
+
trackNarrowingShadow(shadow) {
|
|
13412
|
+
this.flowOrigins.set(shadow, null);
|
|
13413
|
+
this.trackFlowBinding(shadow);
|
|
13414
|
+
}
|
|
13415
|
+
trackFlowBinding(binding) {
|
|
13416
|
+
const depth = Math.min(binding.flowScope ?? 0, this.flowTouched.length - 1);
|
|
13417
|
+
this.flowTouched[depth].add(binding);
|
|
13418
|
+
}
|
|
13419
|
+
/** Every binding whose flow facts may differ from another moment's, outermost scope first. */
|
|
13420
|
+
*touchedFlowBindings() {
|
|
13421
|
+
for (const level of this.flowTouched)
|
|
13422
|
+
yield* level;
|
|
13423
|
+
}
|
|
13424
|
+
/** The state `snapshot` recorded for `binding`, or null when it did not exist yet. */
|
|
13425
|
+
flowStateIn(snapshot, binding) {
|
|
13426
|
+
return snapshot.bindings.get(binding) ?? this.flowOrigins.get(binding) ?? null;
|
|
13427
|
+
}
|
|
11938
13428
|
snapshotFlowFacts() {
|
|
11939
13429
|
const bindings = new Map();
|
|
11940
|
-
for (const
|
|
11941
|
-
|
|
11942
|
-
bindings.set(binding, {
|
|
11943
|
-
type: binding.type,
|
|
11944
|
-
storageType: binding.storageType,
|
|
11945
|
-
frame: binding.narrowingFrame,
|
|
11946
|
-
assigned: binding.assignedFact === true,
|
|
11947
|
-
});
|
|
11948
|
-
}
|
|
11949
|
-
}
|
|
13430
|
+
for (const binding of this.touchedFlowBindings())
|
|
13431
|
+
bindings.set(binding, this.flowFactState(binding));
|
|
11950
13432
|
return {
|
|
11951
13433
|
bindings,
|
|
11952
13434
|
members: this.memberNarrowings.map((scope) => new Map(scope)),
|
|
11953
13435
|
};
|
|
11954
13436
|
}
|
|
11955
13437
|
restoreFlowFacts(snapshot) {
|
|
11956
|
-
for (const
|
|
13438
|
+
for (const binding of this.touchedFlowBindings()) {
|
|
13439
|
+
const state = this.flowStateIn(snapshot, binding);
|
|
13440
|
+
if (!state)
|
|
13441
|
+
continue;
|
|
11957
13442
|
binding.type = state.type;
|
|
11958
13443
|
binding.storageType = state.storageType;
|
|
11959
13444
|
binding.narrowingFrame = state.frame;
|
|
@@ -11978,7 +13463,13 @@ export class Analyzer {
|
|
|
11978
13463
|
flowInvalidationsSince(snapshot) {
|
|
11979
13464
|
const bindings = new Set();
|
|
11980
13465
|
const storageTypes = new Map();
|
|
11981
|
-
|
|
13466
|
+
// Only a written binding can carry a storage type this branch moved. One
|
|
13467
|
+
// nothing wrote merges its own storage type with itself, which is the
|
|
13468
|
+
// identity, so leaving it out of the set is what the merge already did.
|
|
13469
|
+
for (const binding of this.touchedFlowBindings()) {
|
|
13470
|
+
const state = this.flowStateIn(snapshot, binding);
|
|
13471
|
+
if (!state)
|
|
13472
|
+
continue;
|
|
11982
13473
|
if (state.frame !== null
|
|
11983
13474
|
&& (binding.narrowingFrame !== state.frame || !sameType(binding.type, state.type)))
|
|
11984
13475
|
bindings.add(binding);
|
|
@@ -11998,23 +13489,38 @@ export class Analyzer {
|
|
|
11998
13489
|
});
|
|
11999
13490
|
return { bindings, members, storageTypes };
|
|
12000
13491
|
}
|
|
13492
|
+
/**
|
|
13493
|
+
* A loop's back-edge pass re-runs the whole body, and a nested loop inside
|
|
13494
|
+
* that pass runs its own, so the work doubled with every level of loop
|
|
13495
|
+
* nesting: fourteen levels of `while` in a 91-line file took 2.4 seconds and
|
|
13496
|
+
* seventeen took 35. The passes are budgeted by how many back-edge passes
|
|
13497
|
+
* are already running. Past the budget a loop analyzes its body once and its
|
|
13498
|
+
* exit keeps nothing — `widened` — which is what the loop would answer if
|
|
13499
|
+
* its back edge had falsified every fact, so the degradation only ever
|
|
13500
|
+
* removes a fact, never invents one. Real code does not nest loops four
|
|
13501
|
+
* deep, so nothing reachable by hand reaches the budget.
|
|
13502
|
+
*/
|
|
12001
13503
|
reanalyzeLoopBackEdge(baseline, visible, backEdges, body, diagnosticStart, analyze) {
|
|
12002
13504
|
if (!this.flowInvalidationsAffectFacts(backEdges))
|
|
12003
|
-
return null;
|
|
13505
|
+
return { repeated: null, widened: false };
|
|
13506
|
+
if (this.loopReanalysisDepth >= maximumLoopReanalysisDepth)
|
|
13507
|
+
return { repeated: null, widened: true };
|
|
12004
13508
|
const loopHead = this.flowSnapshotAfterInvalidations(baseline, backEdges);
|
|
12005
13509
|
this.loopFlowContexts.push({ baseline: loopHead, visible, carried: [], backEdges: [], breakFacts: [], sawBreak: false });
|
|
12006
13510
|
const secondDiagnosticStart = this.diagnostics.length;
|
|
12007
13511
|
this.clearCachedFlowTypes(body);
|
|
12008
13512
|
let repeated = null;
|
|
13513
|
+
this.loopReanalysisDepth += 1;
|
|
12009
13514
|
try {
|
|
12010
13515
|
this.analyzeIsolatedFlow(loopHead, analyze);
|
|
12011
13516
|
this.deduplicateDiagnostics(diagnosticStart, secondDiagnosticStart);
|
|
12012
13517
|
}
|
|
12013
13518
|
finally {
|
|
13519
|
+
this.loopReanalysisDepth -= 1;
|
|
12014
13520
|
repeated = this.loopFlowContexts.pop() ?? null;
|
|
12015
13521
|
this.restoreFlowFacts(baseline);
|
|
12016
13522
|
}
|
|
12017
|
-
return repeated;
|
|
13523
|
+
return { repeated, widened: false };
|
|
12018
13524
|
}
|
|
12019
13525
|
flowInvalidationsAffectFacts(invalidations) {
|
|
12020
13526
|
return invalidations.some((item) => item.bindings.size > 0
|
|
@@ -12073,37 +13579,63 @@ export class Analyzer {
|
|
|
12073
13579
|
return result;
|
|
12074
13580
|
}
|
|
12075
13581
|
visibleBindings() {
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
13582
|
+
return this.scopes.length;
|
|
13583
|
+
}
|
|
13584
|
+
/** The binding a name resolved to when `visible` was captured. */
|
|
13585
|
+
visibleBinding(visible, name) {
|
|
13586
|
+
for (let index = Math.min(visible, this.scopes.length) - 1; index >= 0; index -= 1) {
|
|
13587
|
+
const binding = this.scopes[index]?.get(name);
|
|
13588
|
+
if (binding)
|
|
13589
|
+
return binding;
|
|
12082
13590
|
}
|
|
12083
|
-
return
|
|
13591
|
+
return null;
|
|
12084
13592
|
}
|
|
13593
|
+
/**
|
|
13594
|
+
* Only a name a narrowing has written can carry a fact, and `narrowedNames`
|
|
13595
|
+
* is the roster of those per scope — so this walks the narrowings rather
|
|
13596
|
+
* than every name in scope. The member half matches a dotted path against
|
|
13597
|
+
* the binding its root names instead of spreading the whole root set per
|
|
13598
|
+
* path, which is O(one lookup) rather than O(names in the module).
|
|
13599
|
+
*/
|
|
12085
13600
|
narrowingsForVisibleBindings(visible) {
|
|
12086
13601
|
const narrowed = new Map();
|
|
12087
|
-
const
|
|
12088
|
-
for (
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
13602
|
+
const seen = new Set();
|
|
13603
|
+
for (let index = this.scopes.length - 1; index >= 0; index -= 1) {
|
|
13604
|
+
for (const name of this.narrowedNames[index]) {
|
|
13605
|
+
if (seen.has(name))
|
|
13606
|
+
continue;
|
|
13607
|
+
seen.add(name);
|
|
13608
|
+
const original = this.visibleBinding(visible, name);
|
|
13609
|
+
if (!original)
|
|
13610
|
+
continue;
|
|
13611
|
+
const current = this.lookup(name);
|
|
13612
|
+
if (current?.narrowingFrame === this.flowFrameDepth
|
|
13613
|
+
&& current.span.start === original.span.start
|
|
13614
|
+
&& current.span.end === original.span.end)
|
|
13615
|
+
narrowed.set(name, current.type);
|
|
13616
|
+
}
|
|
12095
13617
|
}
|
|
12096
13618
|
for (let index = this.memberNarrowings.length - 1; index >= 0; index -= 1) {
|
|
12097
13619
|
for (const [path, fact] of this.memberNarrowings[index]) {
|
|
12098
13620
|
if (fact.frame !== this.flowFrameDepth || narrowed.has(`${memberNarrowingPrefix}${path}`))
|
|
12099
13621
|
continue;
|
|
12100
|
-
if (
|
|
13622
|
+
if (this.memberNarrowingRootIsVisible(visible, path)) {
|
|
12101
13623
|
narrowed.set(`${memberNarrowingPrefix}${path}`, fact.type);
|
|
12102
13624
|
}
|
|
12103
13625
|
}
|
|
12104
13626
|
}
|
|
12105
13627
|
return narrowed;
|
|
12106
13628
|
}
|
|
13629
|
+
/** Whether a member path's root — `<declaration offset>:<name>` — names a binding visible then. */
|
|
13630
|
+
memberNarrowingRootIsVisible(visible, path) {
|
|
13631
|
+
const separator = path.indexOf(":");
|
|
13632
|
+
if (separator < 0)
|
|
13633
|
+
return false;
|
|
13634
|
+
const dot = path.indexOf(".");
|
|
13635
|
+
const start = Number(path.slice(0, separator));
|
|
13636
|
+
const name = path.slice(separator + 1, dot < 0 ? path.length : dot);
|
|
13637
|
+
return this.visibleBinding(visible, name)?.span.start === start;
|
|
13638
|
+
}
|
|
12107
13639
|
narrowingsInSnapshot(snapshot, visible, restore) {
|
|
12108
13640
|
this.restoreFlowFacts(snapshot);
|
|
12109
13641
|
const narrowed = this.narrowingsForVisibleBindings(visible);
|
|
@@ -12146,6 +13678,7 @@ export class Analyzer {
|
|
|
12146
13678
|
const candidates = branches.map((branch) => branch.storageTypes.get(binding) ?? binding.storageType);
|
|
12147
13679
|
if (includeBaseline)
|
|
12148
13680
|
candidates.unshift(binding.storageType);
|
|
13681
|
+
this.recordFlowFactOrigin(binding);
|
|
12149
13682
|
binding.storageType = candidates.reduce((merged, candidate) => mergeTypes(merged, candidate));
|
|
12150
13683
|
if (binding.narrowingFrame === null)
|
|
12151
13684
|
binding.type = binding.storageType;
|
|
@@ -12153,6 +13686,7 @@ export class Analyzer {
|
|
|
12153
13686
|
}
|
|
12154
13687
|
for (const branch of branches) {
|
|
12155
13688
|
for (const binding of branch.bindings) {
|
|
13689
|
+
this.recordFlowFactOrigin(binding);
|
|
12156
13690
|
binding.type = binding.storageType;
|
|
12157
13691
|
binding.narrowingFrame = null;
|
|
12158
13692
|
binding.assignedFact = false;
|
|
@@ -12170,11 +13704,21 @@ export class Analyzer {
|
|
|
12170
13704
|
this.scopes.push(new Map());
|
|
12171
13705
|
this.memberNarrowings.push(new Map());
|
|
12172
13706
|
this.pendingScopeDeclarations.push(new Map());
|
|
13707
|
+
this.narrowedNames.push(new Set());
|
|
13708
|
+
this.scopedNames.push([]);
|
|
13709
|
+
this.flowTouched.push(new Set());
|
|
12173
13710
|
}
|
|
12174
13711
|
exitScope() {
|
|
12175
13712
|
this.scopes.pop();
|
|
12176
13713
|
this.memberNarrowings.pop();
|
|
12177
13714
|
this.pendingScopeDeclarations.pop();
|
|
13715
|
+
this.narrowedNames.pop();
|
|
13716
|
+
for (const name of this.scopedNames.pop() ?? [])
|
|
13717
|
+
this.nearestNames.remove(name);
|
|
13718
|
+
// The bindings this scope created are unreachable now, so the flow-fact
|
|
13719
|
+
// working set shrinks with it rather than growing across the module.
|
|
13720
|
+
for (const binding of this.flowTouched.pop() ?? [])
|
|
13721
|
+
this.flowOrigins.delete(binding);
|
|
12178
13722
|
}
|
|
12179
13723
|
}
|
|
12180
13724
|
//# sourceMappingURL=analyzer.js.map
|