kopscript 0.11.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/checker.js CHANGED
@@ -56,11 +56,13 @@ export class Checker {
56
56
  this.functions = new Map();
57
57
  this.externValues = new Map();
58
58
  this.namedTypes = new Map();
59
- // className/interfaceName -> its type parameter's name (e.g. "Box" -> "T"),
60
- // for every generic class/interface — populated up front, before any type
61
- // resolution runs, specifically so a forward reference (class A's field
62
- // referencing generic class B, declared later in the same file) still
63
- // resolves B's genericity correctly. See resolveType/validateGenericArity.
59
+ // className/interfaceName -> its type parameters' names, in declared order
60
+ // (e.g. "Pair" -> ["K", "V"]), for every generic class/interface —
61
+ // populated up front, before any type resolution runs, specifically so a
62
+ // forward reference (class A's field referencing generic class B, declared
63
+ // later in the same file) still resolves B's genericity correctly. Also
64
+ // doubles as the arity table (its length is how many type arguments a
65
+ // reference to that name must supply). See resolveType/validateGenericArity.
64
66
  this.genericTypeParams = new Map();
65
67
  this.importedNames = new Set();
66
68
  this.rawContents = new Map();
@@ -73,13 +75,13 @@ export class Checker {
73
75
  }
74
76
  for (const [name, info] of this.imports.classes) {
75
77
  this.classes.set(name, info);
76
- if (info.typeParam)
77
- this.genericTypeParams.set(name, info.typeParam);
78
+ if (info.typeParams.length > 0)
79
+ this.genericTypeParams.set(name, info.typeParams);
78
80
  }
79
81
  for (const [name, info] of this.imports.interfaces) {
80
82
  this.interfaces.set(name, info);
81
- if (info.typeParam)
82
- this.genericTypeParams.set(name, info.typeParam);
83
+ if (info.typeParams.length > 0)
84
+ this.genericTypeParams.set(name, info.typeParams);
83
85
  }
84
86
  for (const [name, info] of this.imports.enums)
85
87
  this.enums.set(name, info);
@@ -109,14 +111,14 @@ export class Checker {
109
111
  for (const c of externClassDecls)
110
112
  this.namedTypes.set(c.name, "class");
111
113
  for (const c of classDecls)
112
- if (c.typeParam)
113
- this.genericTypeParams.set(c.name, c.typeParam);
114
+ if (c.typeParams.length > 0)
115
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
114
116
  for (const i of interfaceDecls)
115
- if (i.typeParam)
116
- this.genericTypeParams.set(i.name, i.typeParam);
117
+ if (i.typeParams.length > 0)
118
+ this.genericTypeParams.set(i.name, i.typeParams.map((p) => p.name));
117
119
  for (const c of externClassDecls)
118
- if (c.typeParam)
119
- this.genericTypeParams.set(c.name, c.typeParam);
120
+ if (c.typeParams.length > 0)
121
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
120
122
  for (const e of enumDecls)
121
123
  this.registerEnum(e);
122
124
  for (const i of interfaceDecls)
@@ -172,11 +174,11 @@ export class Checker {
172
174
  });
173
175
  }
174
176
  registerExternClass(decl) {
175
- // Same withTypeParamInScope treatment registerClass gives a real class's
176
- // signature (fields/methods/ctor params) — an extern class's own `T`
177
- // needs to resolve the same way while its declared members are being
178
- // resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
179
- const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
177
+ // Same withTypeParamsInScope treatment registerClass gives a real class's
178
+ // signature (fields/methods/ctor params) — an extern class's own type
179
+ // params need to resolve the same way while its declared members are
180
+ // being resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
181
+ const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamsInScope(decl.typeParams, () => {
180
182
  const fields = new Map();
181
183
  const staticFields = new Map();
182
184
  for (const p of decl.properties) {
@@ -200,9 +202,11 @@ export class Checker {
200
202
  });
201
203
  this.classes.set(decl.name, {
202
204
  name: decl.name,
203
- typeParam: decl.typeParam,
205
+ typeParams: decl.typeParams.map((p) => p.name),
204
206
  superclass: null,
207
+ superclassTypeArgs: null,
205
208
  interfaces: [],
209
+ interfaceTypeArgs: new Map(),
206
210
  fields,
207
211
  methods,
208
212
  staticFields,
@@ -341,8 +345,9 @@ export class Checker {
341
345
  });
342
346
  if (!resolved) {
343
347
  const name = node.kind === "NamedType" ? node.name : "[]";
344
- if (name !== "[]" && this.genericTypeParams.has(name) && !node.typeArgs) {
345
- this.diagnostics.error("KS4007", `Generic type '${name}' requires a type argument (e.g. '${name}<T>')`, line, col);
348
+ const declaredParams = this.genericTypeParams.get(name);
349
+ if (name !== "[]" && declaredParams && !node.typeArgs) {
350
+ this.diagnostics.error("KS4007", `Generic type '${name}' requires ${this.describeArity(declaredParams)} (e.g. '${name}<${declaredParams.join(", ")}>')`, line, col);
346
351
  }
347
352
  else {
348
353
  this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col);
@@ -378,74 +383,137 @@ export class Checker {
378
383
  return;
379
384
  case "class":
380
385
  case "interface": {
381
- const isGeneric = this.genericTypeParams.has(type.name);
382
- if (isGeneric && !type.typeArg) {
383
- this.diagnostics.error("KS4009", `Generic type '${type.name}' requires a type argument (e.g. '${type.name}<T>')`, line, col);
386
+ const declaredParams = this.genericTypeParams.get(type.name);
387
+ const suppliedCount = type.typeArgs?.length ?? 0;
388
+ if (declaredParams && suppliedCount === 0) {
389
+ this.diagnostics.error("KS4009", `Generic type '${type.name}' requires ${this.describeArity(declaredParams)} (e.g. '${type.name}<${declaredParams.join(", ")}>')`, line, col);
384
390
  }
385
- else if (!isGeneric && type.typeArg) {
391
+ else if (!declaredParams && suppliedCount > 0) {
386
392
  this.diagnostics.error("KS4010", `Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
387
393
  }
388
- if (type.typeArg)
389
- this.validateGenericArity(type.typeArg, line, col);
394
+ else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
395
+ this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
396
+ }
397
+ type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
390
398
  return;
391
399
  }
392
400
  default:
393
401
  return;
394
402
  }
395
403
  }
396
- // Temporarily makes `name` (a class/interface's own type parameter, e.g.
397
- // "T") resolve as a TypeParamType, for the duration of `fn` — used only
398
- // while registering that class/interface's own declaration (fields,
399
- // methods, ctor params), so `T Value;` resolves correctly. Scoped this
400
- // narrowly (set, run, delete/restore) rather than left in `namedTypes`
401
- // permanently, since it's only ever meaningful inside that one
402
- // declaration's own body.
403
- withTypeParamInScope(typeParam, fn) {
404
- if (!typeParam)
404
+ describeArity(params) {
405
+ return params.length === 1 ? "a type argument" : `${params.length} type arguments`;
406
+ }
407
+ // Temporarily makes each of `typeParams` (a class/interface/function's own
408
+ // type parameters, e.g. ["K", "V"]) resolve as a TypeParamType, for the
409
+ // duration of `fn` — used only while registering that declaration's own
410
+ // signature (fields, methods, ctor/function params), so `T Value;`/`K
411
+ // key;` resolve correctly. Scoped this narrowly (set, run, delete/
412
+ // restore) rather than left in `namedTypes` permanently, since it's only
413
+ // ever meaningful inside that one declaration's own body.
414
+ withTypeParamsInScope(typeParams, fn) {
415
+ if (typeParams.length === 0)
405
416
  return fn();
406
- const previous = this.namedTypes.get(typeParam);
407
- this.namedTypes.set(typeParam, "typeParam");
417
+ const previous = typeParams.map((p) => this.namedTypes.get(p.name));
418
+ for (const p of typeParams)
419
+ this.namedTypes.set(p.name, "typeParam");
408
420
  try {
409
421
  return fn();
410
422
  }
411
423
  finally {
412
- if (previous === undefined)
413
- this.namedTypes.delete(typeParam);
414
- else
415
- this.namedTypes.set(typeParam, previous);
416
- }
417
- }
418
- // Replaces every occurrence of the class/interface's own type parameter
419
- // (by name) inside `type` with `arg` — the substitution step that turns
420
- // Box<T>'s abstractly-stored field type `T` into `number` when someone
421
- // actually asks about `Box<number>.Value`. A type with no occurrence of
422
- // `paramName` anywhere in it is returned unchanged (including every
423
- // non-generic type, the overwhelming majority).
424
- substituteTypeParam(type, paramName, arg) {
424
+ typeParams.forEach((p, i) => {
425
+ const prev = previous[i];
426
+ if (prev === undefined)
427
+ this.namedTypes.delete(p.name);
428
+ else
429
+ this.namedTypes.set(p.name, prev);
430
+ });
431
+ }
432
+ }
433
+ // Replaces every occurrence of a type parameter named in `bindings`
434
+ // (mapping name -> concrete Type) inside `type` the substitution step
435
+ // that turns Pair<K, V>'s abstractly-stored field type `K` into `number`
436
+ // when someone actually asks about `Pair<number, string>.First`. A type
437
+ // with none of `bindings`' names occurring anywhere in it is returned
438
+ // unchanged (including every non-generic type, the overwhelming majority).
439
+ substituteTypeParams(type, bindings) {
425
440
  switch (type.kind) {
426
441
  case "typeParam":
427
- return type.name === paramName ? arg : type;
442
+ return bindings.get(type.name) ?? type;
428
443
  case "array":
429
- return T.arrayOf(this.substituteTypeParam(type.element, paramName, arg));
444
+ return T.arrayOf(this.substituteTypeParams(type.element, bindings));
430
445
  case "nullable":
431
- return T.nullableOf(this.substituteTypeParam(type.inner, paramName, arg));
446
+ return T.nullableOf(this.substituteTypeParams(type.inner, bindings));
432
447
  case "task":
433
- return T.taskType(this.substituteTypeParam(type.resultType, paramName, arg));
448
+ return T.taskType(this.substituteTypeParams(type.resultType, bindings));
434
449
  case "state":
435
- return T.stateType(this.substituteTypeParam(type.valueType, paramName, arg));
450
+ return T.stateType(this.substituteTypeParams(type.valueType, bindings));
436
451
  case "function":
437
- return T.functionType(type.params.map((p) => this.substituteTypeParam(p, paramName, arg)), this.substituteTypeParam(type.returnType, paramName, arg));
438
- // A nested generic's own type argument can itself mention the outer
439
- // T (`Box<T[]>`'s field being List<T> — a v1 corner case, but cheap
440
- // to handle correctly): substitute inside it too.
452
+ return T.functionType(type.params.map((p) => this.substituteTypeParams(p, bindings)), this.substituteTypeParams(type.returnType, bindings));
453
+ // A nested generic's own type arguments can themselves mention an
454
+ // outer type param (`Pair<K, V>`'s field being List<K> — a v1 corner
455
+ // case, but cheap to handle correctly): substitute inside each.
441
456
  case "class":
442
- return type.typeArg ? T.classType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
457
+ return type.typeArgs ? T.classType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
443
458
  case "interface":
444
- return type.typeArg ? T.interfaceType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
459
+ return type.typeArgs ? T.interfaceType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
445
460
  default:
446
461
  return type;
447
462
  }
448
463
  }
464
+ // Builds the bindings map for a generic reference (a `ClassType`/
465
+ // `InterfaceType`'s own `typeArgs`, zipped against its declared
466
+ // `typeParams`) — the "own bindings" every member-lookup/conformance
467
+ // check starts from. Null when the reference isn't actually generic
468
+ // (no declared params) or carries no type arguments (an arity error
469
+ // already diagnosed elsewhere — see validateGenericArity).
470
+ ownBindings(typeParams, typeArgs) {
471
+ if (typeParams.length === 0 || !typeArgs)
472
+ return null;
473
+ return new Map(typeParams.map((name, i) => [name, typeArgs[i]]));
474
+ }
475
+ // Maps `typeParams` to themselves, as abstract TypeParamTypes — used when
476
+ // resolving a base-list/interface type argument that may reference the
477
+ // *declaring* class/interface's own (still-abstract) type parameter,
478
+ // rather than a concrete instantiation (see checkInterfaceConformance and
479
+ // the base(...) call check in checkClassBodyInner).
480
+ identityBindings(typeParams) {
481
+ return new Map(typeParams.map((name) => [name, T.typeParamType(name)]));
482
+ }
483
+ // One step of substitution composition, walking from a class/interface
484
+ // ("owner") to one of its own generic base-list entries. `baseArgs` are
485
+ // the type arguments `owner` supplied to `baseName` in its own base list
486
+ // — still in `owner`'s own type-param-name space (may reference `owner`'s
487
+ // own type parameters, not yet concrete). `ownerBindings` is `owner`'s
488
+ // current bindings at the point of this lookup. Returns `baseName`'s own
489
+ // bindings map (its declared type-param names -> resolved types), ready
490
+ // for the next composition step or a final substituteTypeParams call.
491
+ composeBaseBindings(baseName, baseArgs, ownerBindings) {
492
+ const baseParams = this.genericTypeParams.get(baseName) ?? [];
493
+ const resolvedArgs = baseArgs.map((t) => this.substituteTypeParams(t, ownerBindings));
494
+ return new Map(baseParams.map((name, i) => [name, resolvedArgs[i]]));
495
+ }
496
+ // Starting from `className` with its own bindings (from ownBindings,
497
+ // above — empty if non-generic or no type arguments), walks up the
498
+ // superclass chain composing each generic link's stored
499
+ // superclassTypeArgs, until it reaches `targetOwner` (the ancestor that
500
+ // actually declared the member being resolved — see lookupField/
501
+ // lookupMethod/etc.'s `owner` result). Returns the bindings in
502
+ // `targetOwner`'s own type-param-name space. A non-generic link along the
503
+ // way (superclassTypeArgs null) resets to empty bindings, same as today's
504
+ // behavior of never substituting through a non-generic ancestor.
505
+ bindingsAtAncestor(className, ownBindings, targetOwner) {
506
+ let current = className;
507
+ let bindings = ownBindings;
508
+ while (current !== targetOwner) {
509
+ const info = this.classes.get(current);
510
+ if (!info || !info.superclass)
511
+ return bindings;
512
+ bindings = info.superclassTypeArgs ? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, bindings) : new Map();
513
+ current = info.superclass;
514
+ }
515
+ return bindings;
516
+ }
449
517
  registerEnum(decl) {
450
518
  const members = new Map();
451
519
  decl.members.forEach((name, index) => {
@@ -459,29 +527,32 @@ export class Checker {
459
527
  this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
460
528
  }
461
529
  registerInterface(decl) {
462
- const methods = this.withTypeParamInScope(decl.typeParam, () => decl.methods.map((m) => {
463
- const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
464
- const returnType = this.resolveType(m.returnType, m.line, m.col);
465
- this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
466
- return { name: m.name, params, returnType };
467
- }));
468
- const bases = [];
469
- for (const baseName of decl.baseList) {
470
- if (this.namedTypes.get(baseName) !== "interface") {
471
- this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
472
- continue;
473
- }
474
- // v1 generics can't appear in a base list at all only as a
475
- // standalone type (field/param/return/local, `new Box<T>()`). A
476
- // class/interface always implements/extends a *bare* name.
477
- if (this.genericTypeParams.has(baseName)) {
478
- this.diagnostics.error("KS4013", `Interface '${decl.name}' cannot extend generic interface '${baseName}' — not supported in v1`, decl.line, decl.col);
479
- continue;
530
+ const genericBaseCount = { count: 0 };
531
+ const { methods, bases, baseTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
532
+ const methods = decl.methods.map((m) => {
533
+ const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
534
+ const returnType = this.resolveType(m.returnType, m.line, m.col);
535
+ this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
536
+ return { name: m.name, params, returnType };
537
+ });
538
+ const bases = [];
539
+ const baseTypeArgs = new Map();
540
+ for (const entry of decl.baseList) {
541
+ if (this.namedTypes.get(entry.name) !== "interface") {
542
+ this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${entry.name}')`, decl.line, decl.col);
543
+ continue;
544
+ }
545
+ bases.push(entry.name);
546
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
547
+ const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
548
+ if (resolved)
549
+ baseTypeArgs.set(entry.name, resolved);
550
+ }
480
551
  }
481
- bases.push(baseName);
482
- }
483
- this.interfaces.set(decl.name, { name: decl.name, typeParam: decl.typeParam, bases, methods });
484
- this.recordHover(decl.line, decl.col, decl.typeParam ? `interface ${decl.name}<${decl.typeParam}>` : `interface ${decl.name}`);
552
+ return { methods, bases, baseTypeArgs };
553
+ });
554
+ this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, baseTypeArgs, methods });
555
+ this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
485
556
  }
486
557
  checkInterfaceHierarchy(decl) {
487
558
  const info = this.interfaces.get(decl.name);
@@ -504,16 +575,31 @@ export class Checker {
504
575
  }
505
576
  }
506
577
  // All methods required to satisfy an interface: its own plus every
507
- // transitively-inherited parent interface's (cycle-safe).
508
- collectInterfaceMethods(interfaceName, seen = new Set()) {
578
+ // transitively-inherited parent interface's (cycle-safe). `bindings` maps
579
+ // `interfaceName`'s own type-param names to resolved types (from
580
+ // ownBindings, at the root call — see its two call sites) — every
581
+ // returned signature is substituted through it, and a generic base link
582
+ // along the way (`interface IDerived<T> : IBase<T>`) composes its own
583
+ // stored baseTypeArgs through `bindings` before recursing, so a signature
584
+ // inherited from several generic-base links up still resolves correctly.
585
+ collectInterfaceMethods(interfaceName, bindings = new Map(), seen = new Set()) {
509
586
  if (seen.has(interfaceName))
510
587
  return [];
511
588
  seen.add(interfaceName);
512
589
  const info = this.interfaces.get(interfaceName);
513
590
  if (!info)
514
591
  return [];
515
- const inherited = info.bases.flatMap((b) => this.collectInterfaceMethods(b, seen));
516
- return [...inherited, ...info.methods];
592
+ const inherited = info.bases.flatMap((b) => {
593
+ const baseArgs = info.baseTypeArgs.get(b);
594
+ const baseBindings = baseArgs ? this.composeBaseBindings(b, baseArgs, bindings) : new Map();
595
+ return this.collectInterfaceMethods(b, baseBindings, seen);
596
+ });
597
+ const ownMethods = info.methods.map((m) => ({
598
+ name: m.name,
599
+ params: m.params.map((p) => this.substituteTypeParams(p, bindings)),
600
+ returnType: this.substituteTypeParams(m.returnType, bindings),
601
+ }));
602
+ return [...inherited, ...ownMethods];
517
603
  }
518
604
  interfaceExtends(sub, sup) {
519
605
  if (sub === sup)
@@ -524,8 +610,9 @@ export class Checker {
524
610
  return info.bases.some((b) => this.interfaceExtends(b, sup));
525
611
  }
526
612
  registerClass(decl) {
527
- this.recordHover(decl.line, decl.col, decl.typeParam ? `class ${decl.name}<${decl.typeParam}>` : `class ${decl.name}`);
528
- const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
613
+ this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `class ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `class ${decl.name}`);
614
+ const genericBaseCount = { count: 0 };
615
+ const { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
529
616
  const fields = new Map();
530
617
  const staticFields = new Map();
531
618
  for (const f of decl.fields) {
@@ -554,39 +641,46 @@ export class Checker {
554
641
  const ownCtorParams = decl.constructor
555
642
  ? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
556
643
  : null;
557
- return { fields, staticFields, methods, staticMethods, ownCtorParams };
558
- });
559
- let superclass = null;
560
- const interfaces = [];
561
- for (const baseName of decl.baseList) {
562
- // v1 generics can't appear in a base list at all — only as a
563
- // standalone type (field/param/return/local, `new Box<T>()`). A
564
- // class always extends/implements a *bare* name.
565
- if (this.genericTypeParams.has(baseName)) {
566
- this.diagnostics.error("KS4015", `Class '${decl.name}' cannot extend/implement generic type '${baseName}' — not supported in v1`, decl.line, decl.col);
567
- continue;
568
- }
569
- const kind = this.namedTypes.get(baseName);
570
- if (kind === "class") {
571
- if (superclass !== null) {
572
- this.diagnostics.error("KS4016", `Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${baseName}')`, decl.line, decl.col);
644
+ // Base-list resolution runs in this same scope — so `Box<T>` in
645
+ // `class Container<T> : Box<T>` resolves its inner `T` as
646
+ // Container's own type parameter, exactly like a field type would.
647
+ let superclass = null;
648
+ let superclassTypeArgs = null;
649
+ const interfaces = [];
650
+ const interfaceTypeArgs = new Map();
651
+ for (const entry of decl.baseList) {
652
+ const kind = this.namedTypes.get(entry.name);
653
+ if (kind === "class") {
654
+ if (superclass !== null) {
655
+ this.diagnostics.error("KS4016", `Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${entry.name}')`, decl.line, decl.col);
656
+ continue;
657
+ }
658
+ superclass = entry.name;
659
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
660
+ superclassTypeArgs = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
661
+ }
662
+ }
663
+ else if (kind === "interface") {
664
+ interfaces.push(entry.name);
665
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
666
+ const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
667
+ if (resolved)
668
+ interfaceTypeArgs.set(entry.name, resolved);
669
+ }
573
670
  }
574
671
  else {
575
- superclass = baseName;
672
+ this.diagnostics.error("KS4017", `Unknown base class or interface '${entry.name}'`, decl.line, decl.col);
576
673
  }
577
674
  }
578
- else if (kind === "interface") {
579
- interfaces.push(baseName);
580
- }
581
- else {
582
- this.diagnostics.error("KS4017", `Unknown base class or interface '${baseName}'`, decl.line, decl.col);
583
- }
584
- }
675
+ return { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs };
676
+ });
585
677
  this.classes.set(decl.name, {
586
678
  name: decl.name,
587
- typeParam: decl.typeParam,
679
+ typeParams: decl.typeParams.map((p) => p.name),
588
680
  superclass,
681
+ superclassTypeArgs,
589
682
  interfaces,
683
+ interfaceTypeArgs,
590
684
  fields,
591
685
  methods,
592
686
  staticFields,
@@ -594,6 +688,35 @@ export class Checker {
594
688
  ownCtorParams,
595
689
  });
596
690
  }
691
+ // Resolves a base-list entry's own type arguments against `baseName`'s
692
+ // declared arity — `typeArgNodes` is null when the entry was written bare
693
+ // (`: Box`, not `: Box<number>`). Must be called from inside the
694
+ // declaring class/interface's own withTypeParamsInScope, so a type
695
+ // argument that references the declaring type's own parameter (`Box<T>`
696
+ // in `class Container<T> : Box<T>`) resolves correctly. `genericBaseCount`
697
+ // is a shared mutable counter across one declaration's whole base list,
698
+ // enforcing v1's "at most one generic entry per base list" cut.
699
+ resolveBaseTypeArgs(declName, baseName, typeArgNodes, declLine, declCol, genericBaseCount) {
700
+ const declaredParams = this.genericTypeParams.get(baseName);
701
+ if (!declaredParams) {
702
+ if (typeArgNodes)
703
+ this.diagnostics.error("KS4092", `'${baseName}' is not generic — it doesn't take a type argument`, declLine, declCol);
704
+ return null;
705
+ }
706
+ if (!typeArgNodes) {
707
+ this.diagnostics.error("KS4093", `Generic base '${baseName}' requires ${this.describeArity(declaredParams)} (e.g. '${baseName}<${declaredParams.join(", ")}>')`, declLine, declCol);
708
+ return null;
709
+ }
710
+ if (typeArgNodes.length !== declaredParams.length) {
711
+ this.diagnostics.error("KS4094", `Generic base '${baseName}' expects ${this.describeArity(declaredParams)}, got ${typeArgNodes.length}`, declLine, declCol);
712
+ return null;
713
+ }
714
+ genericBaseCount.count++;
715
+ if (genericBaseCount.count > 1) {
716
+ this.diagnostics.error("KS4095", `'${declName}' has more than one generic entry in its base list — v1 supports at most one`, declLine, declCol);
717
+ }
718
+ return typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
719
+ }
597
720
  checkClassHierarchy(decl) {
598
721
  const info = this.classes.get(decl.name);
599
722
  this.checkBaseCall(decl, info);
@@ -671,10 +794,19 @@ export class Checker {
671
794
  }
672
795
  checkInterfaceConformance(decl) {
673
796
  const classInfo = this.classes.get(decl.name);
797
+ // This is a conformance check against the class's own *declared*
798
+ // (still-abstract) signatures, not an instantiated use site, so a
799
+ // generic interface arg that references the class's own type param
800
+ // (`IContainer<T>` in `class Container<T> : IContainer<T>`) should
801
+ // compose down to that same abstract TypeParamType, matching how the
802
+ // class's own methods are stored — see identityBindings.
803
+ const selfBindings = this.identityBindings(classInfo.typeParams);
674
804
  for (const ifaceName of classInfo.interfaces) {
675
805
  if (!this.interfaces.has(ifaceName))
676
806
  continue; // already reported as an unknown base type
677
- for (const sig of this.collectInterfaceMethods(ifaceName)) {
807
+ const ifaceArgs = classInfo.interfaceTypeArgs.get(ifaceName);
808
+ const bindings = ifaceArgs ? this.composeBaseBindings(ifaceName, ifaceArgs, selfBindings) : new Map();
809
+ for (const sig of this.collectInterfaceMethods(ifaceName, bindings)) {
678
810
  const found = this.lookupMethod(decl.name, sig.name);
679
811
  if (!found) {
680
812
  this.diagnostics.error("KS4026", `Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
@@ -825,11 +957,11 @@ export class Checker {
825
957
  // only ever arises from an arity error already diagnosed at the
826
958
  // reference site — see validateGenericArity).
827
959
  typeArgsMatch(a, b) {
828
- if (!a.typeArg && !b.typeArg)
960
+ if (!a.typeArgs && !b.typeArgs)
829
961
  return true;
830
- if (!a.typeArg || !b.typeArg)
962
+ if (!a.typeArgs || !b.typeArgs || a.typeArgs.length !== b.typeArgs.length)
831
963
  return false;
832
- return T.typesEqual(a.typeArg, b.typeArg);
964
+ return a.typeArgs.every((t, i) => T.typesEqual(t, b.typeArgs[i]));
833
965
  }
834
966
  // ---------- top-level ----------
835
967
  // Validates that `isAsync` and the declared return type agree — `async`
@@ -867,12 +999,12 @@ export class Checker {
867
999
  }
868
1000
  checkClassBody(decl) {
869
1001
  // Constructor/method *bodies* run in this same scope registerClass used
870
- // for the *signatures* (see withTypeParamInScope) — without it, `T`
1002
+ // for the *signatures* (see withTypeParamsInScope) — without it, `T`
871
1003
  // resolves everywhere in a generic class's declared field/param/return
872
1004
  // types but not inside a method body itself (a local `T x = ...;`, or a
873
1005
  // lambda parameter typed `T`), which would make the type parameter
874
1006
  // usable only at the class's boundary and not inside its own logic.
875
- this.withTypeParamInScope(decl.typeParam, () => this.checkClassBodyInner(decl));
1007
+ this.withTypeParamsInScope(decl.typeParams, () => this.checkClassBodyInner(decl));
876
1008
  }
877
1009
  checkClassBodyInner(decl) {
878
1010
  const info = this.classes.get(decl.name);
@@ -883,7 +1015,18 @@ export class Checker {
883
1015
  // params but no `this` — matching real base()/super() semantics,
884
1016
  // which must run before `this` becomes available.
885
1017
  if (decl.constructor.baseArgs && info.superclass) {
886
- const baseCtorParams = this.lookupCtorParams(info.superclass);
1018
+ // One step from `decl` to its immediate superclass (Container's own
1019
+ // type params, still abstract, composed through whatever it
1020
+ // supplied Box in its base list — see identityBindings), then
1021
+ // however many further steps lookupCtorParams' own `owner` implies
1022
+ // (Box itself has no constructor of its own, inherits its
1023
+ // superclass's) via bindingsAtAncestor.
1024
+ const superBindings = info.superclassTypeArgs
1025
+ ? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, this.identityBindings(info.typeParams))
1026
+ : new Map();
1027
+ const { params: rawBaseCtorParams, owner: baseCtorOwner } = this.lookupCtorParams(info.superclass);
1028
+ const baseCtorBindings = this.bindingsAtAncestor(info.superclass, superBindings, baseCtorOwner);
1029
+ const baseCtorParams = rawBaseCtorParams.map((p) => this.substituteTypeParams(p, baseCtorBindings));
887
1030
  const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
888
1031
  if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
889
1032
  this.diagnostics.error("KS4032", `Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
@@ -1461,31 +1604,40 @@ export class Checker {
1461
1604
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1462
1605
  return T.UNKNOWN;
1463
1606
  }
1464
- let typeArg;
1465
- if (info.typeParam && !expr.typeArgs) {
1466
- this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
1607
+ let typeArgs;
1608
+ if (info.typeParams.length > 0 && !expr.typeArgs) {
1609
+ this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires ${this.describeArity(info.typeParams)} (e.g. 'new ${expr.className}<${info.typeParams.join(", ")}>(...)')`, expr.line, expr.col);
1467
1610
  // Abstractly-typed (T-containing) ctor params, un-substitutable
1468
- // without a real type argument, would otherwise cascade into a
1611
+ // without real type arguments, would otherwise cascade into a
1469
1612
  // confusing "expected 'T'" error on every argument — one clear error
1470
1613
  // beats that pile-on.
1471
1614
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1472
1615
  return T.UNKNOWN;
1473
1616
  }
1474
- if (!info.typeParam && expr.typeArgs) {
1617
+ if (info.typeParams.length === 0 && expr.typeArgs) {
1475
1618
  this.diagnostics.error("KS4070", `Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
1476
1619
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1477
1620
  return T.UNKNOWN;
1478
1621
  }
1479
- if (info.typeParam && expr.typeArgs) {
1480
- typeArg = this.resolveType(expr.typeArgs[0], expr.line, expr.col);
1481
- }
1482
- this.recordHover(expr.line, expr.col, typeArg ? `class ${expr.className}<${T.typeToString(typeArg)}>` : `class ${expr.className}`);
1483
- let ctorParams = this.lookupCtorParams(expr.className);
1484
- if (info.typeParam && typeArg) {
1485
- const paramName = info.typeParam;
1486
- const arg = typeArg;
1487
- ctorParams = ctorParams.map((p) => this.substituteTypeParam(p, paramName, arg));
1488
- }
1622
+ if (info.typeParams.length > 0 && expr.typeArgs) {
1623
+ if (expr.typeArgs.length !== info.typeParams.length) {
1624
+ this.diagnostics.error("KS4091", `Generic class '${expr.className}' expects ${this.describeArity(info.typeParams)}, got ${expr.typeArgs.length}`, expr.line, expr.col);
1625
+ expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1626
+ return T.UNKNOWN;
1627
+ }
1628
+ typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
1629
+ }
1630
+ this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
1631
+ // The constructor being called may be inherited from a generic
1632
+ // ancestor several base-list links up (e.g. `new NumberBox(5)` where
1633
+ // `NumberBox : Box<number>` has no constructor of its own) — compose
1634
+ // bindings all the way to whichever class's own constructor
1635
+ // lookupCtorParams actually found (bindingsAtAncestor is a no-op single
1636
+ // step when it's expr.className's own, the common case).
1637
+ const { params: rawCtorParams, owner: ctorOwner } = this.lookupCtorParams(expr.className);
1638
+ const ownCtorBindings = this.ownBindings(info.typeParams, typeArgs) ?? new Map();
1639
+ const ctorBindings = this.bindingsAtAncestor(expr.className, ownCtorBindings, ctorOwner);
1640
+ const ctorParams = rawCtorParams.map((p) => this.substituteTypeParams(p, ctorBindings));
1489
1641
  if (expr.args.length !== ctorParams.length) {
1490
1642
  this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1491
1643
  }
@@ -1496,19 +1648,25 @@ export class Checker {
1496
1648
  this.diagnostics.error("KS4072", `Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1497
1649
  }
1498
1650
  });
1499
- return T.classType(expr.className, typeArg);
1500
- }
1651
+ return T.classType(expr.className, typeArgs);
1652
+ }
1653
+ // `owner` is which class up the chain actually declared the constructor
1654
+ // being inherited (itself, or the nearest ancestor with one) — needed so
1655
+ // a caller can compose the right type-parameter bindings when that
1656
+ // ancestor is a generic base (see bindingsAtAncestor). `params` is stored
1657
+ // abstractly in `owner`'s own type-param-name space, same as a field or
1658
+ // method's type would be.
1501
1659
  lookupCtorParams(className) {
1502
1660
  let current = className;
1503
1661
  while (current) {
1504
1662
  const info = this.classes.get(current);
1505
1663
  if (!info)
1506
- return [];
1664
+ return { params: [], owner: className };
1507
1665
  if (info.ownCtorParams !== null)
1508
- return info.ownCtorParams;
1666
+ return { params: info.ownCtorParams, owner: current };
1509
1667
  current = info.superclass;
1510
1668
  }
1511
- return [];
1669
+ return { params: [], owner: className };
1512
1670
  }
1513
1671
  // Array stdlib with a fixed (non-polymorphic) signature, given the
1514
1672
  // array's own element type — everything except Map, which checkCall
@@ -1646,12 +1804,14 @@ export class Checker {
1646
1804
  return { type: T.UNKNOWN, methodInfo: null };
1647
1805
  }
1648
1806
  if (objectType.kind === "class") {
1649
- // v1 has no generic inheritance (a generic class's base list can only
1650
- // name non-generic types), so a member found on a generic instance
1651
- // was always declared directly on that same class — substituting by
1652
- // its own type parameter, not some ancestor's, is always correct.
1653
- const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1654
- const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
1807
+ // A member found on an *ancestor* (owner !== objectType.name v1 now
1808
+ // supports a generic superclass, see registerClass/
1809
+ // bindingsAtAncestor) needs bindings composed through however many
1810
+ // generic base links separate objectType from that ancestor, not just
1811
+ // objectType's own type arguments bindingsAtAncestor does that walk
1812
+ // (a no-op single step when owner IS objectType.name, the common
1813
+ // case, same result as before this existed).
1814
+ const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
1655
1815
  const field = this.lookupField(objectType.name, expr.property);
1656
1816
  if (field) {
1657
1817
  this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
@@ -1661,28 +1821,32 @@ export class Checker {
1661
1821
  this.diagnostics.error("KS4082", `'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1662
1822
  }
1663
1823
  }
1664
- return { type: substitute(field.info.type), methodInfo: null };
1824
+ const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, field.owner);
1825
+ return { type: this.substituteTypeParams(field.info.type, bindings), methodInfo: null };
1665
1826
  }
1666
1827
  const method = this.lookupMethod(objectType.name, expr.property);
1667
1828
  if (method) {
1668
1829
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
1669
- const info = paramName ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
1830
+ const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, method.owner);
1831
+ const info = {
1832
+ ...method.info,
1833
+ params: method.info.params.map((p) => this.substituteTypeParams(p, bindings)),
1834
+ returnType: this.substituteTypeParams(method.info.returnType, bindings),
1835
+ };
1670
1836
  return { type: info.returnType, methodInfo: info };
1671
1837
  }
1672
1838
  this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1673
1839
  return { type: T.UNKNOWN, methodInfo: null };
1674
1840
  }
1675
1841
  if (objectType.kind === "interface") {
1676
- const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
1842
+ // collectInterfaceMethods itself now substitutes through `bindings`
1843
+ // including across a generic base-interface link — so passing
1844
+ // objectType's own bindings once at the root is enough; no separate
1845
+ // re-substitution needed here (see its own header comment).
1846
+ const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
1847
+ const sig = this.collectInterfaceMethods(objectType.name, ownBindings).find((m) => m.name === expr.property);
1677
1848
  if (sig) {
1678
- // v1 has no generic interface inheritance either (same restriction
1679
- // as classes — see registerInterface), so a signature found here
1680
- // was always declared directly on this same interface.
1681
- const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1682
- const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
1683
- const params = sig.params.map(substitute);
1684
- const returnType = substitute(sig.returnType);
1685
- return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
1849
+ return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
1686
1850
  }
1687
1851
  this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1688
1852
  return { type: T.UNKNOWN, methodInfo: null };