kopscript 0.12.0 → 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/LLM.md CHANGED
@@ -327,16 +327,30 @@ inspect a type parameter at runtime.
327
327
  "Cannot access member 'Foo' on type 'T'"). This is correct, not a bug — same as an
328
328
  unconstrained type parameter in C#/Java/TypeScript.
329
329
 
330
+ **Generic inheritance**: a class/interface base list CAN name a generic base, with its own
331
+ type arguments, in either direction:
332
+ ```ks
333
+ class IntBox : Box<number> { } // concrete — threads a fixed type through
334
+ class Container<T> : Box<T> { // generic — threads its OWN T through
335
+ public T GetAgain() { return this.Get(); }
336
+ }
337
+ interface INumberContainer : IContainer<number> { } // interfaces work the same way
338
+ ```
339
+ Member resolution (fields, methods, an inherited constructor via `base(...)` or an implicit
340
+ default) composes substitutions correctly however many generic-base links up the chain a
341
+ member was actually declared — `Container<number>.Get()` (declared on `Box<T>`, two links
342
+ away in a longer chain) resolves to `number`, not the abstract `T`. **v1 limit**: at most
343
+ one generic entry across a class/interface's *whole* base list (its superclass, or one
344
+ implemented/extended interface — never more than one) — `class Foo<T> : Box<T>,
345
+ IContainer<T>` is a compile error ("has more than one generic entry in its base list") even
346
+ though either half alone would work.
347
+
330
348
  **Does not exist (v1 scope cuts, each deliberate)**:
331
349
  - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
332
350
  take type parameters, not free functions/methods themselves (a method *inside* a
333
351
  generic class can use that class's own type parameters freely, same as any other member).
334
352
  - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
335
353
  which is why you can't call members on a bare one (see above).
336
- - **Generic inheritance.** A class/interface's base list can only name a *non-generic*
337
- type — `class Foo : Box<number>` and even `class Foo<T> : SomeGenericBase<T>` are both
338
- compile errors ("cannot extend/implement generic type '...' — not supported in v1"). A
339
- generic class/interface can still extend/implement ordinary non-generic bases normally.
340
354
  - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
341
355
 
342
356
  ## Nullable types — `T?`
@@ -414,11 +428,13 @@ KopScript-declared name exactly. Extern class members use real JS member names v
414
428
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
415
429
  declarations — each stands alone.
416
430
 
417
- `extern class` can carry `<T>` (`extern class Box<T> { constructor(T v); T Value { get; } }
418
- from "some-package";`) — identical rules to a real generic class (see "Generics" above:
419
- one unconstrained invariant parameter, erased, no base-list generics), so a generic type
420
- from another package instantiates and type-checks exactly like a local one
421
- (`Box<number>`, arity/invariance errors included).
431
+ `extern class` can carry type parameters (`extern class Box<T> { constructor(T v); T Value
432
+ { get; } } from "some-package";`) — identical rules to a real generic class (see "Generics"
433
+ above: one or more unconstrained invariant parameters, erased, generic inheritance
434
+ included), so a generic type from another package instantiates and type-checks exactly
435
+ like a local one (`Box<number>`, arity/invariance errors included) — and a real KopScript
436
+ class can extend a generic `extern class` with a concrete or threaded-through type
437
+ argument, same as extending a real generic base.
422
438
 
423
439
  **Never write `async` on an extern function/method signature** — declare its return type
424
440
  as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
@@ -505,9 +521,10 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
505
521
 
506
522
  ## Does not exist (don't reach for these)
507
523
 
508
- Generics beyond one or more unconstrained type parameters (no `T : IFoo` constraints, no
509
- generic functions, no generic inheritance, no variance see Generics above for what *is*
510
- supported) · `any`/`unknown` annotations ·
524
+ Generics beyond one or more unconstrained type parameters and single-generic-base
525
+ inheritance (no `T : IFoo` constraints, no generic functions, no variance, no more than one
526
+ generic entry per base list — see Generics above for what *is* supported) ·
527
+ `any`/`unknown` annotations ·
511
528
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
512
529
  reflection · type inference on declarations · ternary expression · union/tuple types ·
513
530
  **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
package/README.md CHANGED
@@ -263,10 +263,23 @@ inside v1:
263
263
  - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
264
264
  interfaces take type parameters. A method *inside* a generic class can still use that
265
265
  class's own type parameters freely; it's just not introducing one of its own.
266
- - **No generic inheritance.** A class or interface's base list can only name a
267
- *non-generic* type. `class Foo : Box<number>` and `class Foo<T> : SomeBase<T>` are both
268
- compile errors — a generic class can still extend/implement ordinary non-generic bases
269
- normally, it just can't be the one on either side of a generic base relationship.
266
+
267
+ A class or interface *can* extend/implement a generic base, type arguments and all:
268
+
269
+ ```ks
270
+ class IntBox : Box<number> { } // concrete: threads a fixed type through
271
+ class Container<T> : Box<T> { // generic: threads its OWN T through
272
+ public T GetAgain() { return this.Get(); }
273
+ }
274
+ Container<number> c = new Container<number>(5);
275
+ print(c.GetAgain()); // 5, and c.Value/c.Get() work too
276
+ ```
277
+
278
+ Member lookup (fields, methods, an inherited constructor) resolves correctly however many
279
+ generic-base links up the chain a member was actually declared, substituting all the way
280
+ down. **v1 limit**: at most one generic entry across a whole base list (the superclass, or
281
+ one implemented interface — not several at once) — `class Foo<T> : Box<T>, IContainer<T>`
282
+ is a compile error even though each half would work alone.
270
283
 
271
284
  See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
272
285
  against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`, `Pair<number,
@@ -372,9 +385,9 @@ globals within a single project.
372
385
 
373
386
  An `extern class` can carry its own type parameter(s), `extern class Box<T> { ... }` or
374
387
  `extern class Pair<K, V> { ... }` — exactly the same rules as a real generic class (see
375
- "Generics" above: invariant, unconstrained, erased, can't appear in a base list), so a
376
- generic type from another package (e.g. Kopular's `FormField<T>`) can be described and
377
- instantiated generically, not just per concrete type:
388
+ "Generics" above: invariant, unconstrained, erased, and a real class can extend it as a
389
+ generic base), so a generic type from another package (e.g. Kopular's `FormField<T>`) can
390
+ be described and instantiated generically, not just per concrete type:
378
391
 
379
392
  ```ks
380
393
  extern class Box<T> {
package/dist/checker.js CHANGED
@@ -204,7 +204,9 @@ export class Checker {
204
204
  name: decl.name,
205
205
  typeParams: decl.typeParams.map((p) => p.name),
206
206
  superclass: null,
207
+ superclassTypeArgs: null,
207
208
  interfaces: [],
209
+ interfaceTypeArgs: new Map(),
208
210
  fields,
209
211
  methods,
210
212
  staticFields,
@@ -459,6 +461,59 @@ export class Checker {
459
461
  return type;
460
462
  }
461
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
+ }
462
517
  registerEnum(decl) {
463
518
  const members = new Map();
464
519
  decl.members.forEach((name, index) => {
@@ -472,28 +527,31 @@ export class Checker {
472
527
  this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
473
528
  }
474
529
  registerInterface(decl) {
475
- const methods = this.withTypeParamsInScope(decl.typeParams, () => decl.methods.map((m) => {
476
- const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
477
- const returnType = this.resolveType(m.returnType, m.line, m.col);
478
- this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
479
- return { name: m.name, params, returnType };
480
- }));
481
- const bases = [];
482
- for (const baseName of decl.baseList) {
483
- if (this.namedTypes.get(baseName) !== "interface") {
484
- this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
485
- continue;
486
- }
487
- // v1 generics can't appear in a base list at all only as a
488
- // standalone type (field/param/return/local, `new Box<T>()`). A
489
- // class/interface always implements/extends a *bare* name.
490
- if (this.genericTypeParams.has(baseName)) {
491
- this.diagnostics.error("KS4013", `Interface '${decl.name}' cannot extend generic interface '${baseName}' — not supported in v1`, decl.line, decl.col);
492
- 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
+ }
493
551
  }
494
- bases.push(baseName);
495
- }
496
- this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, methods });
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 });
497
555
  this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
498
556
  }
499
557
  checkInterfaceHierarchy(decl) {
@@ -517,16 +575,31 @@ export class Checker {
517
575
  }
518
576
  }
519
577
  // All methods required to satisfy an interface: its own plus every
520
- // transitively-inherited parent interface's (cycle-safe).
521
- 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()) {
522
586
  if (seen.has(interfaceName))
523
587
  return [];
524
588
  seen.add(interfaceName);
525
589
  const info = this.interfaces.get(interfaceName);
526
590
  if (!info)
527
591
  return [];
528
- const inherited = info.bases.flatMap((b) => this.collectInterfaceMethods(b, seen));
529
- 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];
530
603
  }
531
604
  interfaceExtends(sub, sup) {
532
605
  if (sub === sup)
@@ -538,7 +611,8 @@ export class Checker {
538
611
  }
539
612
  registerClass(decl) {
540
613
  this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `class ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `class ${decl.name}`);
541
- const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamsInScope(decl.typeParams, () => {
614
+ const genericBaseCount = { count: 0 };
615
+ const { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
542
616
  const fields = new Map();
543
617
  const staticFields = new Map();
544
618
  for (const f of decl.fields) {
@@ -567,39 +641,46 @@ export class Checker {
567
641
  const ownCtorParams = decl.constructor
568
642
  ? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
569
643
  : null;
570
- return { fields, staticFields, methods, staticMethods, ownCtorParams };
571
- });
572
- let superclass = null;
573
- const interfaces = [];
574
- for (const baseName of decl.baseList) {
575
- // v1 generics can't appear in a base list at all — only as a
576
- // standalone type (field/param/return/local, `new Box<T>()`). A
577
- // class always extends/implements a *bare* name.
578
- if (this.genericTypeParams.has(baseName)) {
579
- this.diagnostics.error("KS4015", `Class '${decl.name}' cannot extend/implement generic type '${baseName}' — not supported in v1`, decl.line, decl.col);
580
- continue;
581
- }
582
- const kind = this.namedTypes.get(baseName);
583
- if (kind === "class") {
584
- if (superclass !== null) {
585
- 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
+ }
586
670
  }
587
671
  else {
588
- superclass = baseName;
672
+ this.diagnostics.error("KS4017", `Unknown base class or interface '${entry.name}'`, decl.line, decl.col);
589
673
  }
590
674
  }
591
- else if (kind === "interface") {
592
- interfaces.push(baseName);
593
- }
594
- else {
595
- this.diagnostics.error("KS4017", `Unknown base class or interface '${baseName}'`, decl.line, decl.col);
596
- }
597
- }
675
+ return { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs };
676
+ });
598
677
  this.classes.set(decl.name, {
599
678
  name: decl.name,
600
679
  typeParams: decl.typeParams.map((p) => p.name),
601
680
  superclass,
681
+ superclassTypeArgs,
602
682
  interfaces,
683
+ interfaceTypeArgs,
603
684
  fields,
604
685
  methods,
605
686
  staticFields,
@@ -607,6 +688,35 @@ export class Checker {
607
688
  ownCtorParams,
608
689
  });
609
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
+ }
610
720
  checkClassHierarchy(decl) {
611
721
  const info = this.classes.get(decl.name);
612
722
  this.checkBaseCall(decl, info);
@@ -684,10 +794,19 @@ export class Checker {
684
794
  }
685
795
  checkInterfaceConformance(decl) {
686
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);
687
804
  for (const ifaceName of classInfo.interfaces) {
688
805
  if (!this.interfaces.has(ifaceName))
689
806
  continue; // already reported as an unknown base type
690
- 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)) {
691
810
  const found = this.lookupMethod(decl.name, sig.name);
692
811
  if (!found) {
693
812
  this.diagnostics.error("KS4026", `Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
@@ -896,7 +1015,18 @@ export class Checker {
896
1015
  // params but no `this` — matching real base()/super() semantics,
897
1016
  // which must run before `this` becomes available.
898
1017
  if (decl.constructor.baseArgs && info.superclass) {
899
- 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));
900
1030
  const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
901
1031
  if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
902
1032
  this.diagnostics.error("KS4032", `Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
@@ -1498,11 +1628,16 @@ export class Checker {
1498
1628
  typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
1499
1629
  }
1500
1630
  this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
1501
- let ctorParams = this.lookupCtorParams(expr.className);
1502
- if (info.typeParams.length > 0 && typeArgs) {
1503
- const bindings = new Map(info.typeParams.map((name, i) => [name, typeArgs[i]]));
1504
- ctorParams = ctorParams.map((p) => this.substituteTypeParams(p, bindings));
1505
- }
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));
1506
1641
  if (expr.args.length !== ctorParams.length) {
1507
1642
  this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1508
1643
  }
@@ -1515,17 +1650,23 @@ export class Checker {
1515
1650
  });
1516
1651
  return T.classType(expr.className, typeArgs);
1517
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.
1518
1659
  lookupCtorParams(className) {
1519
1660
  let current = className;
1520
1661
  while (current) {
1521
1662
  const info = this.classes.get(current);
1522
1663
  if (!info)
1523
- return [];
1664
+ return { params: [], owner: className };
1524
1665
  if (info.ownCtorParams !== null)
1525
- return info.ownCtorParams;
1666
+ return { params: info.ownCtorParams, owner: current };
1526
1667
  current = info.superclass;
1527
1668
  }
1528
- return [];
1669
+ return { params: [], owner: className };
1529
1670
  }
1530
1671
  // Array stdlib with a fixed (non-polymorphic) signature, given the
1531
1672
  // array's own element type — everything except Map, which checkCall
@@ -1663,13 +1804,14 @@ export class Checker {
1663
1804
  return { type: T.UNKNOWN, methodInfo: null };
1664
1805
  }
1665
1806
  if (objectType.kind === "class") {
1666
- // v1 has no generic inheritance (a generic class's base list can only
1667
- // name non-generic types), so a member found on a generic instance
1668
- // was always declared directly on that same class — substituting by
1669
- // its own type parameters, not some ancestor's, is always correct.
1670
- const declaredParams = objectType.typeArgs ? this.genericTypeParams.get(objectType.name) : undefined;
1671
- const bindings = declaredParams && objectType.typeArgs ? new Map(declaredParams.map((name, i) => [name, objectType.typeArgs[i]])) : null;
1672
- const substitute = (type) => (bindings ? this.substituteTypeParams(type, bindings) : 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();
1673
1815
  const field = this.lookupField(objectType.name, expr.property);
1674
1816
  if (field) {
1675
1817
  this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
@@ -1679,29 +1821,32 @@ export class Checker {
1679
1821
  this.diagnostics.error("KS4082", `'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1680
1822
  }
1681
1823
  }
1682
- 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 };
1683
1826
  }
1684
1827
  const method = this.lookupMethod(objectType.name, expr.property);
1685
1828
  if (method) {
1686
1829
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
1687
- const info = bindings ? { ...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
+ };
1688
1836
  return { type: info.returnType, methodInfo: info };
1689
1837
  }
1690
1838
  this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1691
1839
  return { type: T.UNKNOWN, methodInfo: null };
1692
1840
  }
1693
1841
  if (objectType.kind === "interface") {
1694
- 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);
1695
1848
  if (sig) {
1696
- // v1 has no generic interface inheritance either (same restriction
1697
- // as classes — see registerInterface), so a signature found here
1698
- // was always declared directly on this same interface.
1699
- const declaredParams = objectType.typeArgs ? this.genericTypeParams.get(objectType.name) : undefined;
1700
- const bindings = declaredParams && objectType.typeArgs ? new Map(declaredParams.map((name, i) => [name, objectType.typeArgs[i]])) : null;
1701
- const substitute = (type) => (bindings ? this.substituteTypeParams(type, bindings) : type);
1702
- const params = sig.params.map(substitute);
1703
- const returnType = substitute(sig.returnType);
1704
- 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 } };
1705
1850
  }
1706
1851
  this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1707
1852
  return { type: T.UNKNOWN, methodInfo: null };
package/dist/codegen.js CHANGED
@@ -286,7 +286,7 @@ export class CodeGenerator {
286
286
  const pad = this.indentStr(indent);
287
287
  // The base list mixes an optional superclass with interface names (checker-validated);
288
288
  // only the non-interface entry, if any, becomes a JS `extends` clause.
289
- const superclass = decl.baseList.find((n) => !this.interfaceNames.has(n)) ?? null;
289
+ const superclass = decl.baseList.find((b) => !this.interfaceNames.has(b.name))?.name ?? null;
290
290
  const header = superclass ? `class ${decl.name} extends ${superclass} {` : `class ${decl.name} {`;
291
291
  const memberPad = this.indentStr(indent + 1);
292
292
  const memberCol = memberPad.length;
package/dist/parser.js CHANGED
@@ -258,6 +258,21 @@ export class Parser {
258
258
  this.consume(TokenKind.Gt, "Expected '>' after type parameter list");
259
259
  return params;
260
260
  }
261
+ // `Bar<T>` or `IBaz` — one base-list entry, with or without its own type
262
+ // arguments. Used by both class and interface base lists.
263
+ parseBaseListEntry() {
264
+ const name = this.consume(TokenKind.Identifier, "Expected base class or interface name").lexeme;
265
+ let typeArgs = null;
266
+ if (this.check(TokenKind.Lt)) {
267
+ this.advance();
268
+ typeArgs = [this.parseType()];
269
+ while (this.match(TokenKind.Comma)) {
270
+ typeArgs.push(this.parseType());
271
+ }
272
+ this.consume(TokenKind.Gt, "Expected '>' after type argument list");
273
+ }
274
+ return { name, typeArgs };
275
+ }
261
276
  parseClassDecl(isExported) {
262
277
  const start = this.advance(); // 'class'
263
278
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
@@ -265,7 +280,7 @@ export class Parser {
265
280
  const baseList = [];
266
281
  if (this.match(TokenKind.Colon)) {
267
282
  do {
268
- baseList.push(this.consume(TokenKind.Identifier, "Expected base class or interface name").lexeme);
283
+ baseList.push(this.parseBaseListEntry());
269
284
  } while (this.match(TokenKind.Comma));
270
285
  }
271
286
  this.consume(TokenKind.LBrace, "Expected '{' before class body");
@@ -426,7 +441,7 @@ export class Parser {
426
441
  const baseList = [];
427
442
  if (this.match(TokenKind.Colon)) {
428
443
  do {
429
- baseList.push(this.consume(TokenKind.Identifier, "Expected base interface name").lexeme);
444
+ baseList.push(this.parseBaseListEntry());
430
445
  } while (this.match(TokenKind.Comma));
431
446
  }
432
447
  this.consume(TokenKind.LBrace, "Expected '{' before interface body");
package/dist/printer.js CHANGED
@@ -118,6 +118,9 @@ export class Printer {
118
118
  printParams(params) {
119
119
  return params.map((p) => `${this.printType(p.type)} ${p.name}`).join(", ");
120
120
  }
121
+ printBaseListEntry(entry) {
122
+ return entry.typeArgs ? `${entry.name}<${entry.typeArgs.map((t) => this.printType(t)).join(", ")}>` : entry.name;
123
+ }
121
124
  // ---------- top-level declarations ----------
122
125
  printStatement(stmt, indent) {
123
126
  const pad = indentStr(indent);
@@ -213,7 +216,7 @@ export class Printer {
213
216
  const pad = indentStr(indent);
214
217
  const memberPad = indentStr(indent + 1);
215
218
  const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
216
- const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `class ${nameWithTypeParam} {`;
219
+ const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `class ${nameWithTypeParam} {`;
217
220
  const prefix = decl.isExported ? "" : "private ";
218
221
  const memberParts = [];
219
222
  const pushMember = (node, text) => {
@@ -251,7 +254,7 @@ export class Printer {
251
254
  const pad = indentStr(indent);
252
255
  const memberPad = indentStr(indent + 1);
253
256
  const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
254
- const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `interface ${nameWithTypeParam} {`;
257
+ const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `interface ${nameWithTypeParam} {`;
255
258
  const prefix = decl.isExported ? "" : "private ";
256
259
  if (decl.methods.length === 0)
257
260
  return `${pad}${prefix}${header}\n${pad}}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types",
5
5
  "type": "module",
6
6
  "license": "MIT",