@velarscript/compiler 0.11.1 → 0.12.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/analyzer.js CHANGED
@@ -145,16 +145,15 @@ const builtinTypeNames = new Set(["string", "number", "bool", "null", "unknown",
145
145
  const asyncResultAnnotationMessage = "An async result annotation in a declaration names the resolved value; write '-> T', not '-> Promise<T>'";
146
146
  const memberNarrowingPrefix = "\u0000member:";
147
147
  /**
148
- * D43 item 69: the emitted member behind a class's `@dispose:` block. The key
149
- * is not a source-shaped identifier, so no author member can collide with it
150
- * `@dispose` is the language's name, not a name in the author's namespace.
148
+ * The emitted member behind a class's `@dispose:` block. The key is not a
149
+ * source-shaped identifier, so no author member can collide with it:
150
+ * `@dispose` is compiler-owned, not part of the author's namespace.
151
151
  */
152
152
  export const disposeMemberKey = "__velar:dispose";
153
153
  /**
154
- * D68 rule 177: the emitted member behind a class's `@iterate:` block, under
155
- * the same kind of key and for the same reason `@iterate` is the language's
156
- * name for the question, so no author member can answer it by accident and no
157
- * author call can reach it.
154
+ * The emitted member behind a class's `@iterate:` block, under the same kind of
155
+ * key and for the same reason: `@iterate` is compiler-owned, so no author
156
+ * member can answer it by accident and no author call can reach it.
158
157
  */
159
158
  export const iterateMemberKey = "__velar:iterate";
160
159
  /** What each bound admits, written the way a rejected call needs to hear it. */
@@ -593,8 +592,6 @@ export class Analyzer {
593
592
  inferredFunctionResultTypes = new Map();
594
593
  finalizeFunctionResultInference;
595
594
  logicalConditionNarrowings = new Map();
596
- collectionInferenceGroups = new WeakMap();
597
- collectionInferenceTypes = new WeakMap();
598
595
  privateFields = new Map();
599
596
  privateGetters = new Map();
600
597
  privateMethods = new Map();
@@ -1418,6 +1415,10 @@ export class Analyzer {
1418
1415
  return instantiated;
1419
1416
  return this.extensionFieldsOf(identity);
1420
1417
  }
1418
+ enumValuesOf(identity) {
1419
+ const values = this.enums.get(identity)?.members;
1420
+ return values ? [...values] : null;
1421
+ }
1421
1422
  /**
1422
1423
  * D55 rule 121: an instantiation's field table is the declaration's fields
1423
1424
  * with the arguments substituted, registered under the instantiation's own
@@ -1534,11 +1535,10 @@ export class Analyzer {
1534
1535
  if (!info)
1535
1536
  return null;
1536
1537
  const built = genericApplicationType(info.identity, info.name, application.arguments.map(resolveArgument), type.readonlyView === true);
1537
- // One object per instantiation, not one per resolution. Several traversals
1538
- // cut their cycles by object identity (`freezeEscapedCollectionInference`
1539
- // keys a WeakMap by the expanded type), and a recursive generic re-enters
1540
- // them; handing back a fresh equal object each time would defeat the guard
1541
- // and recur without end.
1538
+ // One object per instantiation, not one per resolution. A recursive
1539
+ // generic re-enters resolution, so handing back a fresh equal object each
1540
+ // time would defeat any traversal that memoizes by type identity and recur
1541
+ // without end.
1542
1542
  const key = `${built.identity}${built.readonlyView ? "\u0000readonly" : ""}`;
1543
1543
  const cached = this.canonicalGenericApplications.get(key);
1544
1544
  if (cached)
@@ -2583,9 +2583,6 @@ export class Analyzer {
2583
2583
  }
2584
2584
  const annotated = statement.type ? this.resolveAnnotation(statement.type) : null;
2585
2585
  const annotationValid = statement.type ? this.validateTypeReference(statement.type) : true;
2586
- const aliasedBinding = !annotated && statement.initializer.kind === "IdentifierExpression"
2587
- ? this.lookup(statement.initializer.name)
2588
- : null;
2589
2586
  const actual = this.inferExpression(statement.initializer, annotationValid ? annotated ?? unknownType : invalidType);
2590
2587
  // D44 rule 71: an unannotated alias of an assignment-established fact
2591
2588
  // declares the source's domain and re-establishes the fact below, so
@@ -2599,6 +2596,7 @@ export class Analyzer {
2599
2596
  const contract = annotationValid ? annotated ?? inferredStorage : invalidType;
2600
2597
  if (annotationValid)
2601
2598
  this.requireAssignable(actual, declared, statement.initializer.span);
2599
+ this.requireSettledCollectionElement(statement.initializer, declared, annotated !== null);
2602
2600
  this.declarePattern(statement.pattern, statement.binding === "let", declared, contract);
2603
2601
  // D51 rule 101: an alias of an owned handle — or a closure over one —
2604
2602
  // is the same resource under a second name, so it inherits the
@@ -2620,14 +2618,6 @@ export class Analyzer {
2620
2618
  if (binding?.span.start === statement.pattern.span.start && binding.span.end === statement.pattern.span.end) {
2621
2619
  if (this.expandAliases(actual).kind === "promise")
2622
2620
  this.promiseInitializerBindings.add(binding);
2623
- if (!annotated) {
2624
- const aliasedGroup = aliasedBinding ? this.collectionInferenceGroups.get(aliasedBinding) : null;
2625
- if (aliasedGroup)
2626
- this.joinCollectionInference(statement.pattern.name, binding, aliasedGroup);
2627
- else if (this.isFreshUnresolvedCollection(statement.initializer, declared)) {
2628
- this.joinCollectionInference(statement.pattern.name, binding, this.createCollectionInference(declared));
2629
- }
2630
- }
2631
2621
  }
2632
2622
  }
2633
2623
  break;
@@ -4069,12 +4059,11 @@ export class Analyzer {
4069
4059
  return this.inferredFunctionResultSeeds.get(this.iterationResultKey(block)) ?? inferredResultPlaceholderType;
4070
4060
  }
4071
4061
  /**
4072
- * D68 rule 177: `@iterate:` answers the language's question "what does
4062
+ * `@iterate:` answers the compiler's question "what does
4073
4063
  * iterating you mean?" with a collection the language already iterates. It
4074
- * shares `@dispose:`'s path because it is the same kind of member — a
4075
- * contract, not a method and it differs in exactly two ways, both forced:
4076
- * it produces a value, and it may not `await`, because every one of the eight
4077
- * consumers reads it synchronously.
4064
+ * shares `@dispose:`'s compiler-name path, then supplies its own role: it is
4065
+ * a contract, not a method, it produces a value, and it may not `await`,
4066
+ * because every one of the eight consumers reads it synchronously.
4078
4067
  */
4079
4068
  analyzeClassIterate(statement, block, baseName) {
4080
4069
  this.enterScope();
@@ -4545,9 +4534,12 @@ export class Analyzer {
4545
4534
  this.declareBinding("self", false, selfType, statement.span, true);
4546
4535
  }
4547
4536
  for (const parameter of statement.parameters) {
4548
- const type = this.resolveAnnotation(parameter.type);
4549
- const valid = parameter.type ? this.validateTypeReference(parameter.type) : true;
4550
- if (parameter.defaultValue && valid) {
4537
+ const contextualType = !parameter.type && parameter.defaultValue
4538
+ ? this.contextualFunctionParameterDefault(statement, parameter)
4539
+ : null;
4540
+ const type = contextualType ?? this.resolveAnnotation(parameter.type);
4541
+ const valid = contextualType !== null || (parameter.type ? this.validateTypeReference(parameter.type) : true);
4542
+ if (parameter.defaultValue && valid && contextualType === null) {
4551
4543
  this.requireAssignable(this.inferParameterDefault(parameter.defaultValue, type), type, parameter.defaultValue.span);
4552
4544
  }
4553
4545
  const declared = valid ? type : invalidType;
@@ -4590,6 +4582,15 @@ export class Analyzer {
4590
4582
  this.typeParameterFrames.pop();
4591
4583
  this.constructorDepth = outerConstructorDepth;
4592
4584
  }
4585
+ /**
4586
+ * A target-owned declaration may use an ordinary default expression as a
4587
+ * typed input descriptor. The target analyzes that expression and returns
4588
+ * the value type visible inside the body; Core owns only the function-scope
4589
+ * plumbing and never learns the descriptor vocabulary.
4590
+ */
4591
+ contextualFunctionParameterDefault(_statement, _parameter) {
4592
+ return null;
4593
+ }
4593
4594
  analyzeBlock(statements, narrowed = new Map()) {
4594
4595
  const visible = this.visibleBindings();
4595
4596
  this.enterScope();
@@ -4833,7 +4834,6 @@ export class Analyzer {
4833
4834
  storageBinding.type = rebound;
4834
4835
  targetBinding.storageType = rebound;
4835
4836
  targetBinding.type = rebound;
4836
- this.rebindCollectionInference(statement.target.kind === "IdentifierExpression" ? statement.target.name : "", targetBinding, statement.value, valueType);
4837
4837
  }
4838
4838
  // D44 rule 71: the assignment establishes the right-hand side's type
4839
4839
  // as the location's fact (`x = maybeNull()` establishes nothing —
@@ -5273,6 +5273,31 @@ export class Analyzer {
5273
5273
  this.requireAssignable(operand, numberType, expression.operand.span);
5274
5274
  return numberType;
5275
5275
  }
5276
+ case "RequiredExpression": {
5277
+ // D86 rule 212: `value!` answers "absent here is a bug", so it takes
5278
+ // `T?` to `T` and has nothing to say about a value that already holds
5279
+ // one. The contextual type is the optional of what the consumer wants,
5280
+ // since the unwrap is what removes the question.
5281
+ const value = this.inferExpression(expression.value, optionalOf(this.expandAliases(contextualType)));
5282
+ if (isInvalidType(value))
5283
+ return invalidType;
5284
+ const resolved = this.expandAliases(value);
5285
+ if (resolved.kind === "any")
5286
+ return value;
5287
+ if (resolved.kind === "optional")
5288
+ return resolved.inner;
5289
+ const message = resolved.kind === "unknown"
5290
+ ? `'!' unwraps an optional, and ${describeType(value)} is not one; validate it with 'is' or 'parse' before reading it`
5291
+ : resolved.kind === "promise"
5292
+ // The same mistake `try` guards against: the unwrap reached the
5293
+ // Promise rather than the value it resolves to.
5294
+ ? `'!' unwraps an optional, and this expression is ${describeType(value)}; write '(await ...)!' so the unwrap reaches the resolved value`
5295
+ : `'!' unwraps an optional, and this value is already ${describeType(value)}; remove the '!'`;
5296
+ this.diagnostics.push(diagnostic("VEL4040", message, expression.span, ...(resolved.kind === "unknown" || resolved.kind === "promise"
5297
+ ? []
5298
+ : [mechanicalFix({ start: expression.span.end - 1, end: expression.span.end }, "", "Remove the redundant '!'")])));
5299
+ return value;
5300
+ }
5276
5301
  case "TryExpression": {
5277
5302
  // D39 item 51: an expected failure is an optional. The inner
5278
5303
  // expression is checked against the non-optional shape of whatever the
@@ -7507,11 +7532,7 @@ export class Analyzer {
7507
7532
  this.collectionCalls.set(member.span.end, "listAppend");
7508
7533
  const argument = argumentAt(0);
7509
7534
  const value = inferArgument(0, object.element);
7510
- if (argument && object.element.kind === "unknown") {
7511
- if (!this.refineCollectionInference(object, { kind: "list", element: value }))
7512
- this.requireAssignable(value, object.element, argument.span);
7513
- }
7514
- else if (argument)
7535
+ if (argument)
7515
7536
  this.requireAssignable(value, object.element, argument.span);
7516
7537
  requireCount(1);
7517
7538
  return nullType;
@@ -7520,11 +7541,7 @@ export class Analyzer {
7520
7541
  this.collectionCalls.set(member.span.end, "listExtend");
7521
7542
  const argument = argumentAt(0);
7522
7543
  const source = argument ? this.expandAliases(inferArgument(0)) : unknownType;
7523
- let inferred = false;
7524
- if (argument && source.kind === "list" && object.element.kind === "unknown") {
7525
- inferred = this.refineCollectionInference(object, source);
7526
- }
7527
- if (argument && !inferred)
7544
+ if (argument)
7528
7545
  this.requireAssignable(source, object, argument.span);
7529
7546
  requireCount(1);
7530
7547
  return nullType;
@@ -7536,11 +7553,7 @@ export class Analyzer {
7536
7553
  this.requireAssignable(inferArgument(0, numberType), numberType, indexArgument.span);
7537
7554
  const argument = argumentAt(1);
7538
7555
  const value = inferArgument(1, object.element);
7539
- if (argument && object.element.kind === "unknown") {
7540
- if (!this.refineCollectionInference(object, { kind: "list", element: value }))
7541
- this.requireAssignable(value, object.element, argument.span);
7542
- }
7543
- else if (argument)
7556
+ if (argument)
7544
7557
  this.requireAssignable(value, object.element, argument.span);
7545
7558
  requireCount(2);
7546
7559
  return nullType;
@@ -7690,11 +7703,12 @@ export class Analyzer {
7690
7703
  this.collectionCalls.set(member.span.end, "mapSet");
7691
7704
  const keyArgument = argumentAt(0);
7692
7705
  const valueArgument = argumentAt(1);
7693
- const key = inferArgument(0);
7694
- const value = inferArgument(1);
7695
- if (object.key.kind === "unknown" && object.value.kind === "unknown") {
7696
- this.refineCollectionInference(object, { kind: "map", key, value });
7697
- }
7706
+ // D85 rule 211: the receiver's declared key and value types are the
7707
+ // contextual types of this call, exactly as `List.append` passes its
7708
+ // element type. Without them an empty `[]`, an arrow, or any other
7709
+ // value that reads its shape from context arrives as `unknown`.
7710
+ const key = inferArgument(0, object.key);
7711
+ const value = inferArgument(1, object.value);
7698
7712
  if (keyArgument)
7699
7713
  this.requireAssignable(key, object.key, keyArgument.span);
7700
7714
  if (valueArgument)
@@ -7706,11 +7720,7 @@ export class Analyzer {
7706
7720
  this.collectionCalls.set(member.span.end, "mapUpdate");
7707
7721
  const argument = argumentAt(0);
7708
7722
  const source = argument ? this.expandAliases(inferArgument(0)) : unknownType;
7709
- let inferred = false;
7710
- if (argument && source.kind === "map" && object.key.kind === "unknown" && object.value.kind === "unknown") {
7711
- inferred = this.refineCollectionInference(object, source);
7712
- }
7713
- if (argument && !inferred)
7723
+ if (argument)
7714
7724
  this.requireAssignable(source, object, argument.span);
7715
7725
  requireCount(1);
7716
7726
  return nullType;
@@ -7818,25 +7828,17 @@ export class Analyzer {
7818
7828
  const argument = argumentAt(0);
7819
7829
  const value = inferArgument(0, object.element);
7820
7830
  requireCount(1);
7821
- if (argument && object.element.kind === "unknown") {
7822
- if (!this.refineCollectionInference(object, { kind: "set", element: value }))
7823
- this.requireAssignable(value, object.element, argument.span);
7824
- }
7825
- else if (argument)
7831
+ if (argument)
7826
7832
  this.requireAssignable(value, object.element, argument.span);
7827
7833
  return nullType;
7828
7834
  }
7829
7835
  if (member.property === "update") {
7830
7836
  this.collectionCalls.set(member.span.end, "setUpdate");
7831
7837
  const argument = argumentAt(0);
7838
+ const accepted = { kind: "union", members: [object, { kind: "list", element: object.element }] };
7832
7839
  const source = argument ? this.expandAliases(inferArgument(0)) : unknownType;
7833
- let inferred = false;
7834
- if (argument && (source.kind === "set" || source.kind === "list") && object.element.kind === "unknown") {
7835
- inferred = this.refineCollectionInference(object, { kind: "set", element: source.element });
7836
- }
7837
- if (argument && !inferred) {
7838
- this.requireAssignable(source, { kind: "union", members: [object, { kind: "list", element: object.element }] }, argument.span);
7839
- }
7840
+ if (argument)
7841
+ this.requireAssignable(source, accepted, argument.span);
7840
7842
  requireCount(1);
7841
7843
  return nullType;
7842
7844
  }
@@ -9388,10 +9390,8 @@ export class Analyzer {
9388
9390
  return null;
9389
9391
  }
9390
9392
  requireAssignable(actual, expected, valueSpan) {
9391
- if (this.contextuallyAssignable(actual, expected, valueSpan)) {
9392
- this.freezeEscapedCollectionInference(actual, expected);
9393
+ if (this.contextuallyAssignable(actual, expected, valueSpan))
9393
9394
  return;
9394
- }
9395
9395
  const expandedActual = this.expandAliases(actual);
9396
9396
  const expandedExpected = this.expandAliases(expected);
9397
9397
  const expectedCore = expandedExpected.kind === "optional" ? this.expandAliases(expandedExpected.inner) : expandedExpected;
@@ -9526,64 +9526,6 @@ export class Analyzer {
9526
9526
  }
9527
9527
  return null;
9528
9528
  }
9529
- freezeEscapedCollectionInference(actual, expected, seen = new WeakMap()) {
9530
- const inference = this.collectionInferenceTypes.get(actual);
9531
- const expectedInference = this.collectionInferenceTypes.get(expected);
9532
- if (inference && actual !== expected && !expectedInference)
9533
- inference.open = false;
9534
- const expandedActual = this.expandAliases(actual);
9535
- const expandedExpected = this.expandAliases(expected);
9536
- const expectedSeen = seen.get(expandedActual) ?? new WeakSet();
9537
- if (expectedSeen.has(expandedExpected))
9538
- return;
9539
- expectedSeen.add(expandedExpected);
9540
- seen.set(expandedActual, expectedSeen);
9541
- if (expandedExpected.kind === "optional") {
9542
- if (expandedActual.kind === "optional")
9543
- this.freezeEscapedCollectionInference(expandedActual.inner, expandedExpected.inner, seen);
9544
- else if (expandedActual.kind !== "null")
9545
- this.freezeEscapedCollectionInference(expandedActual, expandedExpected.inner, seen);
9546
- return;
9547
- }
9548
- if (expandedExpected.kind === "union") {
9549
- const target = expandedExpected.members.find((member) => isAssignable(expandedActual, member, this));
9550
- if (target)
9551
- this.freezeEscapedCollectionInference(expandedActual, target, seen);
9552
- return;
9553
- }
9554
- if (expandedActual.kind === "union") {
9555
- for (const member of expandedActual.members)
9556
- this.freezeEscapedCollectionInference(member, expandedExpected, seen);
9557
- return;
9558
- }
9559
- if ((expandedActual.kind === "list" && expandedExpected.kind === "list")
9560
- || (expandedActual.kind === "set" && expandedExpected.kind === "set")) {
9561
- this.freezeEscapedCollectionInference(expandedActual.element, expandedExpected.element, seen);
9562
- return;
9563
- }
9564
- if (expandedActual.kind === "map" && expandedExpected.kind === "map") {
9565
- this.freezeEscapedCollectionInference(expandedActual.key, expandedExpected.key, seen);
9566
- this.freezeEscapedCollectionInference(expandedActual.value, expandedExpected.value, seen);
9567
- return;
9568
- }
9569
- if (expandedActual.kind === "record" && expandedExpected.kind === "record") {
9570
- this.freezeEscapedCollectionInference(expandedActual.value, expandedExpected.value, seen);
9571
- return;
9572
- }
9573
- const actualFields = expandedActual.kind === "object" ? expandedActual.fields
9574
- : expandedActual.kind === "named" ? this.fieldsOf(expandedActual.identity ?? expandedActual.name)
9575
- : null;
9576
- const expectedFields = expandedExpected.kind === "object" ? expandedExpected.fields
9577
- : expandedExpected.kind === "named" ? this.fieldsOf(expandedExpected.identity ?? expandedExpected.name)
9578
- : null;
9579
- if (actualFields && expectedFields) {
9580
- for (const [name, expectedField] of expectedFields) {
9581
- const actualField = actualFields.get(name);
9582
- if (actualField)
9583
- this.freezeEscapedCollectionInference(actualField, expectedField, seen);
9584
- }
9585
- }
9586
- }
9587
9529
  contextuallyAssignable(actual, expected, valueSpan) {
9588
9530
  if (isAssignable(actual, expected, this))
9589
9531
  return true;
@@ -10956,6 +10898,26 @@ export class Analyzer {
10956
10898
  narrowingFrame: null,
10957
10899
  } : null;
10958
10900
  }
10901
+ /**
10902
+ * D85 rule 207: an empty collection's element type must be settled where the
10903
+ * collection is written — by an annotation, a contextual type, or the
10904
+ * constructor's own arguments. Nothing infers it from a later mutation, so a
10905
+ * binding left with no source is reported at the construction rather than
10906
+ * kept as `unknown` for a following line to fill in.
10907
+ */
10908
+ requireSettledCollectionElement(initializer, declared, annotated) {
10909
+ if (annotated)
10910
+ return;
10911
+ const type = this.expandAliases(declared);
10912
+ if (!this.isFreshUnresolvedCollection(initializer, type))
10913
+ return;
10914
+ const [spelling, holds, example] = type.kind === "list"
10915
+ ? ["[]", "what the List holds", "let items: List<string> = []"]
10916
+ : type.kind === "set"
10917
+ ? ["Set()", "what the Set holds", "const tags: Set<string> = Set()"]
10918
+ : ["Map()", "what the Map holds", "const users: Map<string, User> = Map()"];
10919
+ this.diagnostics.push(diagnostic("VEL4039", `Empty '${spelling}' requires an explicit type; nothing at this position says ${holds} — write '${example}'`, initializer.span));
10920
+ }
10959
10921
  isFreshUnresolvedCollection(expression, type) {
10960
10922
  const unresolved = type.kind === "list" ? type.element.kind === "unknown"
10961
10923
  : type.kind === "set" ? type.element.kind === "unknown"
@@ -10963,71 +10925,20 @@ export class Analyzer {
10963
10925
  : false;
10964
10926
  if (!unresolved)
10965
10927
  return false;
10928
+ // Only a genuinely empty construction is unsettled. A populated one whose
10929
+ // items happen to be `unknown` (`[value]` over an unchecked boundary
10930
+ // value) says what it holds; the element type is simply that.
10966
10931
  if (expression.kind === "ListExpression")
10967
- return true;
10932
+ return expression.elements.length === 0;
10968
10933
  return expression.kind === "CallExpression"
10934
+ && expression.arguments.length === 0
10935
+ // `Set<string>()` already told the author where the element type goes
10936
+ // (VEL2031); reporting the missing one here would name the same mistake
10937
+ // twice and contradict the fix the first report offers.
10938
+ && expression.typeArgumentsRemoved !== true
10969
10939
  && expression.callee.kind === "IdentifierExpression"
10970
10940
  && (expression.callee.name === "Map" || expression.callee.name === "Set");
10971
10941
  }
10972
- joinCollectionInference(name, binding, group) {
10973
- group.bindings.set(binding, name);
10974
- this.collectionInferenceGroups.set(binding, group);
10975
- }
10976
- createCollectionInference(type) {
10977
- const group = { type, bindings: new Map(), open: true };
10978
- this.collectionInferenceTypes.set(type, group);
10979
- return group;
10980
- }
10981
- refineCollectionInference(current, next) {
10982
- const group = this.collectionInferenceTypes.get(current);
10983
- if (!group?.open)
10984
- return false;
10985
- if (group.type.kind === "list" && next.kind === "list") {
10986
- group.type.element = next.element;
10987
- }
10988
- else if (group.type.kind === "set" && next.kind === "set") {
10989
- group.type.element = next.element;
10990
- }
10991
- else if (group.type.kind === "map" && next.kind === "map") {
10992
- group.type.key = next.key;
10993
- group.type.value = next.value;
10994
- }
10995
- else
10996
- return false;
10997
- for (const [member, name] of group.bindings) {
10998
- this.recordSemanticBinding(`${member.span.start}:${name}`, group.type);
10999
- }
11000
- return true;
11001
- }
11002
- rebindCollectionInference(name, binding, value, valueType) {
11003
- const previous = this.collectionInferenceGroups.get(binding);
11004
- if (!previous)
11005
- return;
11006
- const aliasedBinding = value.kind === "IdentifierExpression" ? this.lookup(value.name) : null;
11007
- const aliasedGroup = aliasedBinding ? this.collectionInferenceGroups.get(aliasedBinding) : null;
11008
- if (previous === aliasedGroup && aliasedGroup)
11009
- return;
11010
- previous?.bindings.delete(binding);
11011
- this.collectionInferenceGroups.delete(binding);
11012
- if (aliasedGroup) {
11013
- binding.type = aliasedGroup.type;
11014
- binding.declaredType = aliasedGroup.type;
11015
- binding.storageType = aliasedGroup.type;
11016
- this.joinCollectionInference(name, binding, aliasedGroup);
11017
- }
11018
- else if (this.isFreshUnresolvedCollection(value, valueType)) {
11019
- binding.type = valueType;
11020
- binding.declaredType = valueType;
11021
- binding.storageType = valueType;
11022
- this.joinCollectionInference(name, binding, this.createCollectionInference(valueType));
11023
- }
11024
- else {
11025
- binding.type = valueType;
11026
- binding.declaredType = valueType;
11027
- binding.storageType = valueType;
11028
- }
11029
- this.recordSemanticBinding(`${binding.span.start}:${name}`, binding.type);
11030
- }
11031
10942
  declareBinding(name, mutable, type, declarationSpan, internal = false, declaredType = type, importSource) {
11032
10943
  this.pendingScopeDeclarations.at(-1)?.delete(name);
11033
10944
  if (!internal) {
@@ -11981,6 +11892,13 @@ export class Analyzer {
11981
11892
  }
11982
11893
  runtimeCheckedType(input, checked) {
11983
11894
  const source = this.expandAliases(input);
11895
+ // D85 rule 210: `unknown` is the one checked domain that proves nothing,
11896
+ // so a check against it leaves the subject's own type alone. Without this
11897
+ // a membership probe against a container whose element or key type is
11898
+ // `unknown` replaced a `string` subject with `unknown`, which is a
11899
+ // widening — every later read of it then failed for the wrong reason.
11900
+ if (this.expandAliases(checked).kind === "unknown")
11901
+ return source;
11984
11902
  const candidates = source.kind === "union" ? source.members
11985
11903
  : source.kind === "optional" ? [source.inner, nullType]
11986
11904
  : [source];