gofront 1.9.0 → 1.9.2

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/backend/js/expressions.js +329 -58
  4. package/src/backend/js/index.js +90 -10
  5. package/src/backend/js/runtime.js +51 -9
  6. package/src/backend/js/statements.js +193 -84
  7. package/src/backend/js/type-info.js +123 -8
  8. package/src/backend/shared/fmt-types.js +30 -1
  9. package/src/backend/wasm/emit-builtins.js +250 -9
  10. package/src/backend/wasm/emit-exprs.js +63 -22
  11. package/src/backend/wasm/emit-shared.js +4 -1
  12. package/src/backend/wasm/emit-stmts.js +239 -205
  13. package/src/backend/wasm/emit.js +73 -26
  14. package/src/backend/wasm/globals.js +1 -0
  15. package/src/backend/wasm/index.js +8 -0
  16. package/src/backend/wasm/module-emitter.js +211 -57
  17. package/src/backend/wasm/monomorph.js +36 -2
  18. package/src/backend/wasm/natives.js +4 -0
  19. package/src/backend/wasm/types.js +32 -3
  20. package/src/backend/wasm/variadic.js +6 -1
  21. package/src/cli/minifier.js +20 -6
  22. package/src/compiler.js +6 -1
  23. package/src/lexer/lexer.js +1 -0
  24. package/src/link/stdlib.js +1 -0
  25. package/src/link/type-names.js +72 -16
  26. package/src/lower/embedding.js +43 -15
  27. package/src/lower/fmt-formatters.js +38 -14
  28. package/src/lower/functions.js +223 -7
  29. package/src/lower/gen-types.js +21 -2
  30. package/src/lower/index.js +7 -1
  31. package/src/lower/json-codecs.js +114 -6
  32. package/src/parser/declarations.js +4 -2
  33. package/src/parser/expressions.js +25 -41
  34. package/src/parser/statements.js +107 -0
  35. package/src/parser/types.js +11 -0
  36. package/src/typechecker/assignability.js +94 -12
  37. package/src/typechecker/constants.js +124 -2
  38. package/src/typechecker/expressions.js +171 -17
  39. package/src/typechecker/index.js +158 -29
  40. package/src/typechecker/operand-text.js +1 -0
  41. package/src/typechecker/resolve.js +308 -39
  42. package/src/typechecker/statements.js +171 -21
  43. package/src/typechecker/types.js +84 -2
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { parseJsonTag } from "../parser/struct-tag.js";
5
5
  import { foldConstExpr } from "./const-fold.js";
6
+ import { exprText, operandKind } from "./operand-text.js";
6
7
  import { EXCLUDED_STDLIB_MEMBERS } from "./stdlib/excluded.js";
7
8
  import {
8
9
  ANY,
@@ -11,7 +12,10 @@ import {
11
12
  DECLARED_ANY,
12
13
  defaultType,
13
14
  displayTypeName,
15
+ isAmbiguousFieldSelector,
16
+ isAmbiguousPromotion,
14
17
  isAny,
18
+ isMethodPromotedOverField,
15
19
  isTypedArraySlice,
16
20
  Scope,
17
21
  TAINTED_ANY,
@@ -28,6 +32,11 @@ import {
28
32
  // instantiation is reported as a cycle.
29
33
  const MAX_INSTANTIATION_DEPTH = 32;
30
34
 
35
+ // Type nodes in a constraint position, where a constraint interface may
36
+ // appear. A property of the AST node, so it is shared by every checker: an
37
+ // importing package resolves the constraints of the imported generics.
38
+ const CONSTRAINT_POS = new WeakSet();
39
+
31
40
  // Two imported packages may both declare `Pt`, so the cache key of an
32
41
  // instance tells declarations apart by identity instead of by display name.
33
42
  const declIds = new WeakMap();
@@ -215,7 +224,8 @@ export const resolveMethods = {
215
224
  case "TypeName": {
216
225
  if (BASIC_TYPES[node.name])
217
226
  return node.name === "any" ? DECLARED_ANY : BASIC_TYPES[node.name];
218
- if (node.name === "comparable") return COMPARABLE;
227
+ if (node.name === "comparable")
228
+ return this._checkConstraintUse(COMPARABLE, node);
219
229
  if (scope) {
220
230
  const fromScope = scope.lookup(node.name);
221
231
  if (fromScope?.kind === "typeParam") return fromScope;
@@ -224,7 +234,7 @@ export const resolveMethods = {
224
234
  const named = this.types.get(node.name);
225
235
  if (named) {
226
236
  this._useQualifiedTypeName(node, named);
227
- return named;
237
+ return this._checkConstraintUse(named, node);
228
238
  }
229
239
  const jsOnly = this._jsOnlyMember(node.name);
230
240
  if (jsOnly) return this._errJsOnly(jsOnly, node);
@@ -241,6 +251,7 @@ export const resolveMethods = {
241
251
  case "IndexExpr":
242
252
  return this._resolveGenericTypeName(node, scope);
243
253
  case "TypeParam": {
254
+ this._markConstraintPos(node.constraint);
244
255
  const constraint = node.constraint
245
256
  ? this.resolveTypeNode(node.constraint, scope)
246
257
  : ANY;
@@ -264,7 +275,10 @@ export const resolveMethods = {
264
275
  case "StructType":
265
276
  return this._resolveStructType(node, scope);
266
277
  case "InterfaceType":
267
- return this._resolveInterfaceType(node, scope);
278
+ return this._checkConstraintUse(
279
+ this._resolveInterfaceType(node, scope),
280
+ node,
281
+ );
268
282
  case "UnaryExpr":
269
283
  // `new(*T)`: the parser reads the pointer type as a dereference.
270
284
  if (node.op !== "*") return ANY;
@@ -283,10 +297,9 @@ export const resolveMethods = {
283
297
  if (node.expr?.kind === "Ident" && typeof node.field === "string") {
284
298
  const qualified = `${node.expr.name}.${node.field}`;
285
299
  if (this.types.has(qualified)) {
286
- const resolved = this.resolveTypeNode(
287
- { kind: "TypeName", name: qualified },
288
- scope,
289
- );
300
+ const inner = { kind: "TypeName", name: qualified };
301
+ this._inheritConstraintPos(node, inner);
302
+ const resolved = this.resolveTypeNode(inner, scope);
290
303
  this._useQualifiedTypeName(node, resolved);
291
304
  return resolved;
292
305
  }
@@ -306,6 +319,95 @@ export const resolveMethods = {
306
319
  }
307
320
  },
308
321
 
322
+ // ── Constraint interfaces outside constraints ───────────────────
323
+
324
+ // Records a type node that sits in a constraint position (a type
325
+ // parameter's constraint, an interface's embedded element or union term,
326
+ // the right-hand side of a type declaration), where a constraint
327
+ // interface is allowed.
328
+ _markConstraintPos(node) {
329
+ if (!node || typeof node !== "object") return;
330
+ if (node.kind === "UnionConstraint") {
331
+ for (const term of node.terms) this._markConstraintPos(term.type);
332
+ return;
333
+ }
334
+ CONSTRAINT_POS.add(node);
335
+ },
336
+
337
+ // `inner` stands for `node` (a rewritten qualified name).
338
+ _inheritConstraintPos(node, inner) {
339
+ if (CONSTRAINT_POS.has(node)) CONSTRAINT_POS.add(inner);
340
+ },
341
+
342
+ // An interface's embedded elements and union terms are constraint positions.
343
+ _markInterfaceElements(node) {
344
+ for (const embed of node.embeds ?? []) this._markConstraintPos(embed);
345
+ this._markConstraintPos(node.unionConstraint);
346
+ },
347
+
348
+ // Marks the constraint positions of top-level declarations before any
349
+ // type resolves (constraints resolve lazily from several places).
350
+ _markConstraintPositions(programs) {
351
+ for (const p of programs) {
352
+ for (const d of p.decls) {
353
+ if (d.kind === "TypeDecl") this._markConstraintPos(d.type);
354
+ for (const tp of d.typeParams ?? [])
355
+ this._markConstraintPos(tp.constraint);
356
+ }
357
+ }
358
+ },
359
+
360
+ // A constraint interface (one with a type set, or comparable) resolved
361
+ // outside a constraint position is an error. A type whose declaration has
362
+ // not resolved yet is judged once every package-level type has.
363
+ _checkConstraintUse(type, node) {
364
+ if (CONSTRAINT_POS.has(node)) return type;
365
+ if (type?.kind === "named" && type.underlying == null) {
366
+ this._pendingConstraintUses.push({
367
+ type,
368
+ node,
369
+ file: this._currentFile,
370
+ source: this._currentSource,
371
+ });
372
+ return type;
373
+ }
374
+ this._reportConstraintUse(type, node);
375
+ return type;
376
+ },
377
+
378
+ _reportConstraintUse(type, node) {
379
+ const u = this._deepUnderlying(type);
380
+ if (u === COMPARABLE || (u?.kind === "interface" && u._comparable)) {
381
+ this.err(
382
+ `cannot use type ${typeStr(type)} outside a type constraint: interface is (or embeds) comparable`,
383
+ node,
384
+ );
385
+ } else if (
386
+ u?.kind === "interface" &&
387
+ (u.unionConstraint || u._typeSetEmbeds)
388
+ ) {
389
+ this.err(
390
+ `cannot use type ${typeStr(type)} outside a type constraint: interface contains type constraints`,
391
+ node,
392
+ );
393
+ }
394
+ },
395
+
396
+ _flushConstraintUses() {
397
+ const pending = this._pendingConstraintUses;
398
+ if (pending.length === 0) return;
399
+ this._pendingConstraintUses = [];
400
+ const file = this._currentFile;
401
+ const source = this._currentSource;
402
+ for (const u of pending) {
403
+ this._currentFile = u.file;
404
+ this._currentSource = u.source;
405
+ this._reportConstraintUse(u.type, u.node);
406
+ }
407
+ this._currentFile = file;
408
+ this._currentSource = source;
409
+ },
410
+
309
411
  // `pkg.Name` when `pkg` is a Go-source package whose `js` files declare
310
412
  // Name and this package is wasm or both (those files are not loaded).
311
413
  _jsOnlyMember(qualified) {
@@ -342,18 +444,50 @@ export const resolveMethods = {
342
444
  const e = depth < 16 ? scope.lookupConstExpr(name) : null;
343
445
  return e ? foldConstExpr(e, (n) => resolve(n, depth + 1)) : undefined;
344
446
  };
447
+ let n = Number.NaN;
345
448
  try {
346
449
  const lit = foldConstExpr(size, resolve);
347
- const n = lit ? Number(lit.value) : Number.NaN;
348
- if (Number.isInteger(n) && n >= 0) return n;
450
+ n = lit ? Number(lit.value) : Number.NaN;
349
451
  } catch {
350
452
  // fall through to the checker's evaluator
351
453
  }
352
454
  // `[math.MaxInt8]T`: a package constant the AST folder cannot see.
353
- const q = this._constIntValue(size, scope);
354
- if (q === null || !Number.isInteger(q) || q < 0) return size;
455
+ if (!Number.isInteger(n))
456
+ n = this._constIntValue(size, scope) ?? Number.NaN;
457
+ if (!Number.isInteger(n)) {
458
+ this._reportNonConstantArrayLen(size, scope);
459
+ return size;
460
+ }
355
461
  this._useQualifiedConsts(size);
356
- return q;
462
+ if (n >= 0) return n;
463
+ this.err(
464
+ `invalid array length ${exprText(size)} (untyped int constant ${n})`,
465
+ size,
466
+ );
467
+ return 0;
468
+ },
469
+
470
+ // A length that is not a constant at all (`[len(s)]T` with a slice, `[n]T`
471
+ // with a variable) cannot size an array. The operand's type is checked
472
+ // first, so it counts as used.
473
+ _reportNonConstantArrayLen(size, scope) {
474
+ if (size._type === undefined) this.checkExpr(size, scope);
475
+ this.err(
476
+ `invalid array length ${exprText(size)} (${operandKind(size, typeStr(size._type), false)}): must be constant`,
477
+ size,
478
+ );
479
+ },
480
+
481
+ // An array length must be an integer constant: `[1 == 1]T`, `["a"]T`,
482
+ // `[nil]T`, `[2.5]T`, `[2i]T` and a typed float constant are rejected.
483
+ _reportNonIntegerArrayLen(size, scope) {
484
+ const what = this._nonIntegerConst(size, scope);
485
+ if (!what) return false;
486
+ this.err(
487
+ `invalid array length ${exprText(size)} (${what}): must be integer`,
488
+ size,
489
+ );
490
+ return true;
357
491
  },
358
492
 
359
493
  // `pkg.K` inside a folded array length counts as using the import.
@@ -367,10 +501,24 @@ export const resolveMethods = {
367
501
  },
368
502
 
369
503
  _resolveArrayTypeNode(node, scope) {
504
+ if (
505
+ !node.inferLen &&
506
+ this._reportNonIntegerArrayLen(node._sizeExpr ?? node.size, scope)
507
+ )
508
+ return {
509
+ kind: "array",
510
+ size: 0,
511
+ elem: this.resolveTypeNode(node.elem, scope),
512
+ };
370
513
  // Fold in place so backends that read the type node see a literal length.
371
- if (!node.inferLen && node.size?.value === undefined) {
514
+ // The node may come from the parse cache: keep the original expression so
515
+ // a later compile of the same AST still counts `pkg.K` as using `pkg`
516
+ // and still reports an invalid length.
517
+ if (node._sizeExpr) this._foldArrayLen(node._sizeExpr, scope);
518
+ else if (!node.inferLen && node.size?.value === undefined) {
372
519
  const n = this._foldArrayLen(node.size, scope);
373
- if (typeof n === "number")
520
+ if (typeof n === "number") {
521
+ node._sizeExpr = node.size;
374
522
  node.size = {
375
523
  kind: "BasicLit",
376
524
  litKind: "INT",
@@ -378,6 +526,7 @@ export const resolveMethods = {
378
526
  _line: node.size._line,
379
527
  _col: node.size._col,
380
528
  };
529
+ }
381
530
  }
382
531
  return {
383
532
  kind: "array",
@@ -489,6 +638,7 @@ export const resolveMethods = {
489
638
  },
490
639
 
491
640
  _resolveInterfaceType(node, scope) {
641
+ this._markInterfaceElements(node);
492
642
  const methods = new Map();
493
643
  for (const m of node.methods) {
494
644
  const params = m.params.map((p) => this.resolveTypeNode(p.type, scope));
@@ -504,24 +654,12 @@ export const resolveMethods = {
504
654
  // Embedded interfaces that carry a type set (`interface{ Integer }`):
505
655
  // kept so type-parameter operator checks can follow them.
506
656
  const typeSetEmbeds = [];
507
- if (node.embeds) {
508
- for (const embed of node.embeds) {
509
- const resolved = this.resolveTypeNode(embed, scope);
510
- const base =
511
- resolved?.kind === "named" ? resolved.underlying : resolved;
512
- if (base?.kind === "interface") {
513
- for (const [mName, mType] of base.methods) {
514
- if (!methods.has(mName)) methods.set(mName, mType);
515
- }
516
- if (base.unionConstraint || base._typeSetEmbeds)
517
- typeSetEmbeds.push(base);
518
- } else if (!isAny(resolved)) {
519
- this.err(
520
- `cannot embed non-interface type ${typeStr(resolved)}`,
521
- embed,
522
- );
523
- }
524
- }
657
+ // `interface{ comparable; ... }`: the type set is restricted to
658
+ // comparable types (directly or through an embedded constraint).
659
+ let comparable = false;
660
+ for (const embed of node.embeds ?? []) {
661
+ if (this._mergeInterfaceEmbed(embed, scope, methods, typeSetEmbeds))
662
+ comparable = true;
525
663
  }
526
664
  const result = { kind: "interface", methods };
527
665
  if (node.unionConstraint) {
@@ -529,9 +667,28 @@ export const resolveMethods = {
529
667
  result._unionScope = scope;
530
668
  }
531
669
  if (typeSetEmbeds.length > 0) result._typeSetEmbeds = typeSetEmbeds;
670
+ if (comparable) result._comparable = true;
532
671
  return result;
533
672
  },
534
673
 
674
+ // Folds one embedded element of an interface into `methods` and
675
+ // `typeSetEmbeds`; returns true when it makes the interface comparable-only.
676
+ _mergeInterfaceEmbed(embed, scope, methods, typeSetEmbeds) {
677
+ const resolved = this.resolveTypeNode(embed, scope);
678
+ const base = resolved?.kind === "named" ? resolved.underlying : resolved;
679
+ if (base === COMPARABLE) return true;
680
+ if (base?.kind !== "interface") {
681
+ if (!isAny(resolved))
682
+ this.err(`cannot embed non-interface type ${typeStr(resolved)}`, embed);
683
+ return false;
684
+ }
685
+ for (const [mName, mType] of base.methods) {
686
+ if (!methods.has(mName)) methods.set(mName, mType);
687
+ }
688
+ if (base.unionConstraint || base._typeSetEmbeds) typeSetEmbeds.push(base);
689
+ return Boolean(base._comparable);
690
+ },
691
+
535
692
  // ── Generics — substitution, instantiation, inference ────────────
536
693
 
537
694
  substituteType(type, map) {
@@ -715,7 +872,11 @@ export const resolveMethods = {
715
872
  const before = map.size;
716
873
  for (const tp of typeParams) {
717
874
  const arg = map.get(tp.name);
718
- if (!arg || !tp.constraint) continue;
875
+ if (!tp.constraint) continue;
876
+ if (!arg) {
877
+ this._inferFromPointerCore(tp, typeParams, map, scope);
878
+ continue;
879
+ }
719
880
  const terms = this._constraintTerms(tp.constraint, new Set(), scope);
720
881
  if (terms?.length !== 1) continue;
721
882
  const argUnder =
@@ -727,6 +888,25 @@ export const resolveMethods = {
727
888
  }
728
889
  },
729
890
 
891
+ // An unbound type parameter whose constraint has the single exact term
892
+ // `*T` (`P interface{ *T; M() }`) is the pointer type, once `T` is known.
893
+ _inferFromPointerCore(tp, typeParams, map, scope) {
894
+ const terms = this._constraintTerms(tp.constraint, new Set(), scope);
895
+ if (terms?.length !== 1 || terms[0]?.kind !== "pointer") return;
896
+ const free = new Set(
897
+ typeParams.filter((p) => !map.has(p.name)).map((p) => p.name),
898
+ );
899
+ const inferred = this.substituteType(terms[0], map);
900
+ const mentionsFree = (t) =>
901
+ t?.kind === "typeParam"
902
+ ? free.has(t.name)
903
+ : t?.kind === "pointer"
904
+ ? mentionsFree(t.base)
905
+ : false;
906
+ if (!mentionsFree(inferred) && inferred.kind === "pointer")
907
+ map.set(tp.name, inferred);
908
+ },
909
+
730
910
  _inferFromFuncPair(paramType, argType, map) {
731
911
  for (
732
912
  let i = 0;
@@ -745,6 +925,8 @@ export const resolveMethods = {
745
925
  _inferFromStructuredPair(paramType, argType, map) {
746
926
  if (paramType.kind === "slice" && argType.kind === "slice")
747
927
  this._inferFromPair(paramType.elem, argType.elem, map);
928
+ if (paramType.kind === "array" && argType.kind === "array")
929
+ this._inferFromPair(paramType.elem, argType.elem, map);
748
930
  if (paramType.kind === "pointer" && argType.kind === "pointer")
749
931
  this._inferFromPair(paramType.base, argType.base, map);
750
932
  if (paramType.kind === "map" && argType.kind === "map") {
@@ -798,16 +980,91 @@ export const resolveMethods = {
798
980
  },
799
981
 
800
982
  _checkUnionConstraint(typeArg, constraint, node) {
801
- for (const term of constraint.terms) {
802
- if (term.approx) return;
803
- const termType =
804
- term.resolved ?? this.resolveTypeNode(term.type, this.globals);
805
- if (typeStr(typeArg) === typeStr(termType)) return;
806
- }
983
+ if (typeArg?._tainted || this._unionAccepts(typeArg, constraint, new Set()))
984
+ return;
807
985
  this.err(`type ${typeStr(typeArg)} does not satisfy constraint`, node);
808
986
  },
809
987
 
988
+ // Whether `typeArg` is in the type set of a union: some term holds it.
989
+ // `scope` resolves the names its terms mention.
990
+ _unionAccepts(typeArg, union, active, scope = this.globals) {
991
+ return union.terms.some((term) => {
992
+ // `~[]E` may mention a type parameter that is not in `scope`, so an
993
+ // approximation term is resolved (and compared) only when it names
994
+ // a type.
995
+ if (!term.resolved && term.approx && term.type?.kind !== "TypeName")
996
+ return true;
997
+ const t = term.resolved ?? this.resolveTypeNode(term.type, scope);
998
+ return this._termAccepts(typeArg, t, term.approx, active);
999
+ });
1000
+ },
1001
+
1002
+ // One union term: a named interface contributes its own type set
1003
+ // (`S | U` with `type S interface{ ~int }`), `~T` any type with the
1004
+ // underlying type of `T`, and `T` exactly `T`.
1005
+ _termAccepts(typeArg, t, approx, active) {
1006
+ const base =
1007
+ t?.kind === "named"
1008
+ ? (this.resolveType(t.underlying) ?? t.underlying)
1009
+ : t;
1010
+ if (base?.kind === "interface")
1011
+ return this._interfaceAccepts(typeArg, base, active);
1012
+ if (!approx) return typeStr(typeArg) === typeStr(t);
1013
+ const want = this._deepUnderlying(t);
1014
+ if (typeArg?.kind === "typeParam" || want?.kind !== "basic") return true;
1015
+ return typeStr(this._deepUnderlying(typeArg)) === typeStr(want);
1016
+ },
1017
+
1018
+ // An interface's type set is the intersection of its own union and the
1019
+ // type sets of the interfaces it embeds; one with neither holds every type.
1020
+ _interfaceAccepts(typeArg, iface, active) {
1021
+ if (active.has(iface)) return true;
1022
+ active.add(iface);
1023
+ const ok =
1024
+ (!iface.unionConstraint ||
1025
+ this._unionAccepts(
1026
+ typeArg,
1027
+ iface.unionConstraint,
1028
+ active,
1029
+ iface._unionScope ?? this.globals,
1030
+ )) &&
1031
+ (iface._typeSetEmbeds ?? []).every((e) =>
1032
+ this._interfaceAccepts(typeArg, e, active),
1033
+ );
1034
+ active.delete(iface);
1035
+ return ok;
1036
+ },
1037
+
1038
+ // Go's comparability: slices, maps and funcs are not comparable, nor are
1039
+ // arrays and structs that contain them.
1040
+ _isComparable(t, seen = new Set()) {
1041
+ const base = t?.kind === "named" ? t.underlying : t;
1042
+ if (!base || seen.has(base)) return true;
1043
+ seen.add(base);
1044
+ switch (base.kind) {
1045
+ case "slice":
1046
+ case "map":
1047
+ case "func":
1048
+ return false;
1049
+ case "array":
1050
+ return this._isComparable(base.elem, seen);
1051
+ case "struct":
1052
+ return [...(base.fields?.values() ?? [])].every((f) =>
1053
+ this._isComparable(f, seen),
1054
+ );
1055
+ default:
1056
+ return true;
1057
+ }
1058
+ },
1059
+
810
1060
  _checkInterfaceConstraint(typeArg, base, constraint, node) {
1061
+ if (base._comparable && !this._isComparable(typeArg)) {
1062
+ this.err(
1063
+ `type ${typeStr(typeArg)} does not satisfy constraint ${typeStr(constraint)}`,
1064
+ node,
1065
+ );
1066
+ return;
1067
+ }
811
1068
  if (base.methods.size === 0 && !base.unionConstraint) return;
812
1069
  if (base.unionConstraint) {
813
1070
  this.checkConstraint(typeArg, base.unionConstraint, node);
@@ -925,7 +1182,11 @@ export const resolveMethods = {
925
1182
  },
926
1183
 
927
1184
  _fieldTypeStruct(base, baseType, field, node) {
1185
+ if (base.fields?.has(field) && isMethodPromotedOverField(base, field))
1186
+ return base.methods.get(field);
928
1187
  if (base.fields?.has(field)) {
1188
+ if (isAmbiguousFieldSelector(base, field))
1189
+ return this._errAmbiguousSelector(baseType, field, node);
929
1190
  if (
930
1191
  field.startsWith("_") &&
931
1192
  (baseType?.name?.startsWith("strings.") ||
@@ -939,6 +1200,8 @@ export const resolveMethods = {
939
1200
  return base.fields.get(field);
940
1201
  }
941
1202
  if (base.methods?.has(field)) return base.methods.get(field);
1203
+ if (isAmbiguousPromotion(base, field))
1204
+ return this._errAmbiguousSelector(baseType, field, node);
942
1205
  const typeName = baseType?.name || base.name;
943
1206
  if (typeName) {
944
1207
  const dName = displayTypeName(typeName);
@@ -963,6 +1226,12 @@ export const resolveMethods = {
963
1226
  return this.err(`No field '${field}' on ${typeStr(baseType)}`, node);
964
1227
  },
965
1228
 
1229
+ _errAmbiguousSelector(baseType, field, node) {
1230
+ const text = exprText(node?.expr);
1231
+ const recv = text === "expression" ? typeStr(baseType) : text;
1232
+ return this.err(`ambiguous selector ${recv}.${field}`, node);
1233
+ },
1234
+
966
1235
  _fieldTypeInterface(base, baseType, field, node) {
967
1236
  if (base.methods?.has(field)) return base.methods.get(field);
968
1237
  if (base.fields?.has(field)) return base.fields.get(field);