kopscript 0.12.0 → 0.14.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
@@ -303,9 +303,10 @@ class Pair<K, V> {
303
303
  Pair<number, string> p = new Pair<number, string>(1, "a");
304
304
  ```
305
305
 
306
- One or more unconstrained, invariant type parameters per class/interface (real *or*
307
- `extern` — see the `extern` section below), comma-separated: `<T>`, `<K, V>`, `<A, B, C>`,
308
- however many the declaration needs.
306
+ One or more invariant type parameters per class/interface (real *or* `extern` — see the
307
+ `extern` section below), comma-separated: `<T>`, `<K, V>`, `<A, B, C>`, however many the
308
+ declaration needs each optionally constrained to a single interface: `<T : IComparable>`,
309
+ `<K : IHashable, V>` (mixing constrained and unconstrained params is fine).
309
310
  Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
310
311
  compile to the exact same plain `class Box`), so there's no runtime cost and no way to
311
312
  inspect a type parameter at runtime.
@@ -322,21 +323,53 @@ inspect a type parameter at runtime.
322
323
  - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`,
323
324
  `Pair<number, Pair<string, bool>>`.
324
325
  - Inside the class/interface's own body, each type parameter is fully abstract — you can
325
- store it, return it, pass it around, but (unconstrained) you **cannot call any member on
326
- a bare type-parameter value** (`this.value.Foo()` where `value: T` is a compile error:
327
- "Cannot access member 'Foo' on type 'T'"). This is correct, not a bug — same as an
328
- unconstrained type parameter in C#/Java/TypeScript.
326
+ store it, return it, pass it around, but an **unconstrained** parameter (no `: IFoo`)
327
+ means you **cannot call any member on a bare type-parameter value** (`this.value.Foo()`
328
+ where `value: T` is a compile error: "Cannot access member 'Foo' on type 'T'"). This is
329
+ correct, not a bug — same as an unconstrained type parameter in C#/Java/TypeScript.
330
+
331
+ **Constraints**: `<T : IComparable>` lifts the "no member calls" restriction — a member
332
+ call on a constrained `T` resolves against the constraint interface's own signature:
333
+ ```ks
334
+ interface IComparable { number CompareTo(); }
335
+ class Box<T : IComparable> {
336
+ public T Value;
337
+ constructor(T v) { this.Value = v; }
338
+ public number Compare() { return this.Value.CompareTo(); } // legal — T : IComparable
339
+ }
340
+ Box<Money> b = new Box<Money>(new Money(5)); // Money must implement IComparable
341
+ ```
342
+ A type argument satisfies a constraint by implementing the interface directly, through an
343
+ ancestor (`classImplementsInterface` walks the whole superclass chain), or — inside another
344
+ generic body — by already being an equally- or more-constrained type parameter of its own
345
+ (`class Wrapper<T : IComparable> { Box<T> MakeBox(T v) { return new Box<T>(v); } }` is
346
+ legal without ever naming a concrete type). Checked everywhere a type argument is bound: on
347
+ `new`, on a bare generic type reference, and on a generic base in a base list.
348
+ **v1 limit**: at most one constraint per parameter (`T : IFoo, IBar` isn't supported), and
349
+ the constraint must be an interface, never a class.
350
+
351
+ **Generic inheritance**: a class/interface base list CAN name a generic base, with its own
352
+ type arguments, in either direction:
353
+ ```ks
354
+ class IntBox : Box<number> { } // concrete — threads a fixed type through
355
+ class Container<T> : Box<T> { // generic — threads its OWN T through
356
+ public T GetAgain() { return this.Get(); }
357
+ }
358
+ interface INumberContainer : IContainer<number> { } // interfaces work the same way
359
+ ```
360
+ Member resolution (fields, methods, an inherited constructor via `base(...)` or an implicit
361
+ default) composes substitutions correctly however many generic-base links up the chain a
362
+ member was actually declared — `Container<number>.Get()` (declared on `Box<T>`, two links
363
+ away in a longer chain) resolves to `number`, not the abstract `T`. **v1 limit**: at most
364
+ one generic entry across a class/interface's *whole* base list (its superclass, or one
365
+ implemented/extended interface — never more than one) — `class Foo<T> : Box<T>,
366
+ IContainer<T>` is a compile error ("has more than one generic entry in its base list") even
367
+ though either half alone would work.
329
368
 
330
369
  **Does not exist (v1 scope cuts, each deliberate)**:
331
370
  - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
332
371
  take type parameters, not free functions/methods themselves (a method *inside* a
333
372
  generic class can use that class's own type parameters freely, same as any other member).
334
- - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
335
- 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
373
  - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
341
374
 
342
375
  ## Nullable types — `T?`
@@ -414,11 +447,14 @@ KopScript-declared name exactly. Extern class members use real JS member names v
414
447
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
415
448
  declarations — each stands alone.
416
449
 
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).
450
+ `extern class` can carry type parameters, optionally constrained (`extern class Box<T :
451
+ IComparable> { constructor(T v); T Value { get; } } from "some-package";`) — identical
452
+ rules to a real generic class (see "Generics" above: invariant, erased, generic
453
+ inheritance and constraints both included), so a generic type from another package
454
+ instantiates and type-checks exactly like a local one (`Box<number>`, arity/invariance/
455
+ constraint errors included) — and a real KopScript class can extend a generic `extern
456
+ class` with a concrete or threaded-through type argument, same as extending a real generic
457
+ base.
422
458
 
423
459
  **Never write `async` on an extern function/method signature** — declare its return type
424
460
  as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
@@ -505,9 +541,10 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
505
541
 
506
542
  ## Does not exist (don't reach for these)
507
543
 
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 ·
544
+ Generics beyond one or more type parameters (each with at most one interface constraint)
545
+ and single-generic-base inheritance (no `T : IFoo, IBar` multi-constraints, no generic
546
+ functions, no variance, no more than one generic entry per base list — see Generics above
547
+ for what *is* supported) · `any`/`unknown` annotations ·
511
548
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
512
549
  reflection · type inference on declarations · ternary expression · union/tuple types ·
513
550
  **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
package/README.md CHANGED
@@ -22,9 +22,11 @@ LLM's context, as opposed to this README's narrative explanation.
22
22
  explicit `virtual`/`override` dispatch (methods are sealed unless marked `virtual`), and a full
23
23
  `public`/`protected`/`private` access model enforced at compile time.
24
24
  - **Strongly typed**: every declaration is explicitly typed and checked at compile time.
25
- - **Generics**: one or more unconstrained, invariant type parameters on classes and
26
- interfaces (`class Box<T> { public T Value; }`, `class Pair<K, V> { ... }`) erased at
27
- codegen with zero runtime cost, the same way `task<T>`/`state<T>` already are.
25
+ - **Generics**: one or more invariant type parameters on classes and interfaces
26
+ (`class Box<T> { public T Value; }`, `class Pair<K, V> { ... }`), each optionally
27
+ constrained to an interface (`class Box<T : IComparable> { ... }`), plus generic
28
+ inheritance (`class IntBox : Box<number> { }`) — erased at codegen with zero runtime
29
+ cost, the same way `task<T>`/`state<T>` already are.
28
30
  - **Nullable types with compiler-enforced null-checking**: `T?` (`string?`, `Dog?`,
29
31
  `number[]?`) — a `T?` can't be used where a `T` is expected without an `if (x != null)`
30
32
  check first (checked statically, not just at runtime), and comparing a value that can
@@ -253,20 +255,49 @@ compile to the identical plain `class Box`, so there's no runtime cost.
253
255
  Still a deliberately small slice of generics, not a scaled-down promise of more later
254
256
  inside v1:
255
257
 
256
- - **Invariant, unconstrained.** No `T : ISomething` (constraints) and because a type
257
- parameter is fully unconstrained, you can't call any member on a bare `T`/`K`/`V` value
258
- inside the generic class's own body (that's correct behavior, the same restriction
259
- C#/Java/TypeScript put on an unconstrained parameter, not a bug). Invariant means
260
- `Box<Dog>` is **not** assignable to `Box<Animal>` even though `Dog : Animal`, and
261
- `Pair<number, string>` is not assignable to `Pair<string, number>` — every slot must
262
- match exactly, in order.
258
+ - **Invariant.** `Box<Dog>` is **not** assignable to `Box<Animal>` even though `Dog :
259
+ Animal`, and `Pair<number, string>` is not assignable to `Pair<string, number>` every
260
+ slot must match exactly, in order.
263
261
  - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
264
262
  interfaces take type parameters. A method *inside* a generic class can still use that
265
263
  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.
264
+
265
+ By default a type parameter is fully unconstrained, so you can't call any member on a bare
266
+ `T`/`K`/`V` value inside the generic class's own body the same restriction C#/Java/
267
+ TypeScript put on an unconstrained parameter, not a bug. A single optional `: IFoo`
268
+ constraint per parameter lifts that:
269
+
270
+ ```ks
271
+ interface IComparable { number CompareTo(); }
272
+ class Box<T : IComparable> {
273
+ public T Value;
274
+ constructor(T v) { this.Value = v; }
275
+ public number Compare() { return this.Value.CompareTo(); } // legal — T is constrained
276
+ }
277
+ Box<Money> b = new Box<Money>(new Money(5)); // Money must implement IComparable
278
+ ```
279
+
280
+ The constraint must be an interface (never a class), and a type argument satisfies it by
281
+ implementing it directly, through an ancestor, or (inside another generic body) by already
282
+ being an equally-or-more-constrained type parameter of its own. **v1 limit**: at most one
283
+ constraint per parameter — `T : IFoo, IBar` isn't supported.
284
+
285
+ A class or interface *can* extend/implement a generic base, type arguments and all:
286
+
287
+ ```ks
288
+ class IntBox : Box<number> { } // concrete: threads a fixed type through
289
+ class Container<T> : Box<T> { // generic: threads its OWN T through
290
+ public T GetAgain() { return this.Get(); }
291
+ }
292
+ Container<number> c = new Container<number>(5);
293
+ print(c.GetAgain()); // 5, and c.Value/c.Get() work too
294
+ ```
295
+
296
+ Member lookup (fields, methods, an inherited constructor) resolves correctly however many
297
+ generic-base links up the chain a member was actually declared, substituting all the way
298
+ down. **v1 limit**: at most one generic entry across a whole base list (the superclass, or
299
+ one implemented interface — not several at once) — `class Foo<T> : Box<T>, IContainer<T>`
300
+ is a compile error even though each half would work alone.
270
301
 
271
302
  See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
272
303
  against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`, `Pair<number,
@@ -370,10 +401,11 @@ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...
370
401
  proving `extern` works as a real cross-*package* boundary, not just for describing DOM
371
402
  globals within a single project.
372
403
 
373
- An `extern class` can carry its own type parameter(s), `extern class Box<T> { ... }` or
374
- `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
404
+ An `extern class` can carry its own type parameter(s), optionally constrained
405
+ `extern class Box<T> { ... }`, `extern class Pair<K, V> { ... }`, `extern class Box<T :
406
+ IComparable> { ... }` exactly the same rules as a real generic class (see "Generics"
407
+ above: invariant, erased, and a real class can extend it as a generic base), so a generic
408
+ type from another package (e.g. Kopular's `FormField<T>`) can be described and
377
409
  instantiated generically, not just per concrete type:
378
410
 
379
411
  ```ks
package/dist/checker.js CHANGED
@@ -64,6 +64,19 @@ export class Checker {
64
64
  // doubles as the arity table (its length is how many type arguments a
65
65
  // reference to that name must supply). See resolveType/validateGenericArity.
66
66
  this.genericTypeParams = new Map();
67
+ // Same shape/population sites as genericTypeParams, but each entry is a
68
+ // parallel array of that generic's per-parameter constraint interface
69
+ // name (or null if unconstrained) — e.g. "Pair" -> [null, "IHashable"]
70
+ // for `class Pair<K, V : IHashable>`. See resolveBaseTypeArgs/checkNew/
71
+ // checkConstraintSatisfied.
72
+ this.genericTypeParamConstraints = new Map();
73
+ // The constraint interface (or null) for every type parameter *currently
74
+ // in scope* while checking a generic declaration's own body — set/
75
+ // restored by withTypeParamsInScope alongside namedTypes, so a bare `T`
76
+ // value's member access (`this.value.Foo()`) can resolve Foo against the
77
+ // constraint's own signature instead of always erroring (see
78
+ // checkMemberInner's "typeParam" branch).
79
+ this.activeConstraints = new Map();
67
80
  this.importedNames = new Set();
68
81
  this.rawContents = new Map();
69
82
  this.hoverEntries = [];
@@ -75,13 +88,17 @@ export class Checker {
75
88
  }
76
89
  for (const [name, info] of this.imports.classes) {
77
90
  this.classes.set(name, info);
78
- if (info.typeParams.length > 0)
91
+ if (info.typeParams.length > 0) {
79
92
  this.genericTypeParams.set(name, info.typeParams);
93
+ this.genericTypeParamConstraints.set(name, info.typeParamConstraints);
94
+ }
80
95
  }
81
96
  for (const [name, info] of this.imports.interfaces) {
82
97
  this.interfaces.set(name, info);
83
- if (info.typeParams.length > 0)
98
+ if (info.typeParams.length > 0) {
84
99
  this.genericTypeParams.set(name, info.typeParams);
100
+ this.genericTypeParamConstraints.set(name, info.typeParamConstraints);
101
+ }
85
102
  }
86
103
  for (const [name, info] of this.imports.enums)
87
104
  this.enums.set(name, info);
@@ -110,15 +127,24 @@ export class Checker {
110
127
  this.namedTypes.set(e.name, "enum");
111
128
  for (const c of externClassDecls)
112
129
  this.namedTypes.set(c.name, "class");
113
- for (const c of classDecls)
114
- if (c.typeParams.length > 0)
115
- this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
116
- for (const i of interfaceDecls)
117
- if (i.typeParams.length > 0)
118
- this.genericTypeParams.set(i.name, i.typeParams.map((p) => p.name));
119
- for (const c of externClassDecls)
120
- if (c.typeParams.length > 0)
121
- this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
130
+ for (const c of classDecls) {
131
+ if (c.typeParams.length === 0)
132
+ continue;
133
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
134
+ this.genericTypeParamConstraints.set(c.name, c.typeParams.map((p) => p.constraint));
135
+ }
136
+ for (const i of interfaceDecls) {
137
+ if (i.typeParams.length === 0)
138
+ continue;
139
+ this.genericTypeParams.set(i.name, i.typeParams.map((p) => p.name));
140
+ this.genericTypeParamConstraints.set(i.name, i.typeParams.map((p) => p.constraint));
141
+ }
142
+ for (const c of externClassDecls) {
143
+ if (c.typeParams.length === 0)
144
+ continue;
145
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
146
+ this.genericTypeParamConstraints.set(c.name, c.typeParams.map((p) => p.constraint));
147
+ }
122
148
  for (const e of enumDecls)
123
149
  this.registerEnum(e);
124
150
  for (const i of interfaceDecls)
@@ -203,8 +229,11 @@ export class Checker {
203
229
  this.classes.set(decl.name, {
204
230
  name: decl.name,
205
231
  typeParams: decl.typeParams.map((p) => p.name),
232
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
206
233
  superclass: null,
234
+ superclassTypeArgs: null,
207
235
  interfaces: [],
236
+ interfaceTypeArgs: new Map(),
208
237
  fields,
209
238
  methods,
210
239
  staticFields,
@@ -392,6 +421,14 @@ export class Checker {
392
421
  else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
393
422
  this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
394
423
  }
424
+ else if (declaredParams && suppliedCount === declaredParams.length) {
425
+ const constraints = this.genericTypeParamConstraints.get(type.name);
426
+ type.typeArgs.forEach((arg, i) => {
427
+ const constraint = constraints?.[i];
428
+ if (constraint)
429
+ this.checkConstraintSatisfied(arg, constraint, declaredParams[i], line, col);
430
+ });
431
+ }
395
432
  type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
396
433
  return;
397
434
  }
@@ -413,8 +450,11 @@ export class Checker {
413
450
  if (typeParams.length === 0)
414
451
  return fn();
415
452
  const previous = typeParams.map((p) => this.namedTypes.get(p.name));
416
- for (const p of typeParams)
453
+ const previousConstraints = typeParams.map((p) => this.activeConstraints.get(p.name));
454
+ for (const p of typeParams) {
417
455
  this.namedTypes.set(p.name, "typeParam");
456
+ this.activeConstraints.set(p.name, p.constraint);
457
+ }
418
458
  try {
419
459
  return fn();
420
460
  }
@@ -425,6 +465,11 @@ export class Checker {
425
465
  this.namedTypes.delete(p.name);
426
466
  else
427
467
  this.namedTypes.set(p.name, prev);
468
+ const prevConstraint = previousConstraints[i];
469
+ if (prevConstraint === undefined)
470
+ this.activeConstraints.delete(p.name);
471
+ else
472
+ this.activeConstraints.set(p.name, prevConstraint);
428
473
  });
429
474
  }
430
475
  }
@@ -459,6 +504,59 @@ export class Checker {
459
504
  return type;
460
505
  }
461
506
  }
507
+ // Builds the bindings map for a generic reference (a `ClassType`/
508
+ // `InterfaceType`'s own `typeArgs`, zipped against its declared
509
+ // `typeParams`) — the "own bindings" every member-lookup/conformance
510
+ // check starts from. Null when the reference isn't actually generic
511
+ // (no declared params) or carries no type arguments (an arity error
512
+ // already diagnosed elsewhere — see validateGenericArity).
513
+ ownBindings(typeParams, typeArgs) {
514
+ if (typeParams.length === 0 || !typeArgs)
515
+ return null;
516
+ return new Map(typeParams.map((name, i) => [name, typeArgs[i]]));
517
+ }
518
+ // Maps `typeParams` to themselves, as abstract TypeParamTypes — used when
519
+ // resolving a base-list/interface type argument that may reference the
520
+ // *declaring* class/interface's own (still-abstract) type parameter,
521
+ // rather than a concrete instantiation (see checkInterfaceConformance and
522
+ // the base(...) call check in checkClassBodyInner).
523
+ identityBindings(typeParams) {
524
+ return new Map(typeParams.map((name) => [name, T.typeParamType(name)]));
525
+ }
526
+ // One step of substitution composition, walking from a class/interface
527
+ // ("owner") to one of its own generic base-list entries. `baseArgs` are
528
+ // the type arguments `owner` supplied to `baseName` in its own base list
529
+ // — still in `owner`'s own type-param-name space (may reference `owner`'s
530
+ // own type parameters, not yet concrete). `ownerBindings` is `owner`'s
531
+ // current bindings at the point of this lookup. Returns `baseName`'s own
532
+ // bindings map (its declared type-param names -> resolved types), ready
533
+ // for the next composition step or a final substituteTypeParams call.
534
+ composeBaseBindings(baseName, baseArgs, ownerBindings) {
535
+ const baseParams = this.genericTypeParams.get(baseName) ?? [];
536
+ const resolvedArgs = baseArgs.map((t) => this.substituteTypeParams(t, ownerBindings));
537
+ return new Map(baseParams.map((name, i) => [name, resolvedArgs[i]]));
538
+ }
539
+ // Starting from `className` with its own bindings (from ownBindings,
540
+ // above — empty if non-generic or no type arguments), walks up the
541
+ // superclass chain composing each generic link's stored
542
+ // superclassTypeArgs, until it reaches `targetOwner` (the ancestor that
543
+ // actually declared the member being resolved — see lookupField/
544
+ // lookupMethod/etc.'s `owner` result). Returns the bindings in
545
+ // `targetOwner`'s own type-param-name space. A non-generic link along the
546
+ // way (superclassTypeArgs null) resets to empty bindings, same as today's
547
+ // behavior of never substituting through a non-generic ancestor.
548
+ bindingsAtAncestor(className, ownBindings, targetOwner) {
549
+ let current = className;
550
+ let bindings = ownBindings;
551
+ while (current !== targetOwner) {
552
+ const info = this.classes.get(current);
553
+ if (!info || !info.superclass)
554
+ return bindings;
555
+ bindings = info.superclassTypeArgs ? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, bindings) : new Map();
556
+ current = info.superclass;
557
+ }
558
+ return bindings;
559
+ }
462
560
  registerEnum(decl) {
463
561
  const members = new Map();
464
562
  decl.members.forEach((name, index) => {
@@ -472,28 +570,38 @@ export class Checker {
472
570
  this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
473
571
  }
474
572
  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;
573
+ const genericBaseCount = { count: 0 };
574
+ const { methods, bases, baseTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
575
+ const methods = decl.methods.map((m) => {
576
+ const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
577
+ const returnType = this.resolveType(m.returnType, m.line, m.col);
578
+ this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
579
+ return { name: m.name, params, returnType };
580
+ });
581
+ const bases = [];
582
+ const baseTypeArgs = new Map();
583
+ for (const entry of decl.baseList) {
584
+ if (this.namedTypes.get(entry.name) !== "interface") {
585
+ this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${entry.name}')`, decl.line, decl.col);
586
+ continue;
587
+ }
588
+ bases.push(entry.name);
589
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
590
+ const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
591
+ if (resolved)
592
+ baseTypeArgs.set(entry.name, resolved);
593
+ }
493
594
  }
494
- bases.push(baseName);
495
- }
496
- this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, methods });
595
+ return { methods, bases, baseTypeArgs };
596
+ });
597
+ this.interfaces.set(decl.name, {
598
+ name: decl.name,
599
+ typeParams: decl.typeParams.map((p) => p.name),
600
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
601
+ bases,
602
+ baseTypeArgs,
603
+ methods,
604
+ });
497
605
  this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
498
606
  }
499
607
  checkInterfaceHierarchy(decl) {
@@ -517,16 +625,31 @@ export class Checker {
517
625
  }
518
626
  }
519
627
  // 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()) {
628
+ // transitively-inherited parent interface's (cycle-safe). `bindings` maps
629
+ // `interfaceName`'s own type-param names to resolved types (from
630
+ // ownBindings, at the root call — see its two call sites) — every
631
+ // returned signature is substituted through it, and a generic base link
632
+ // along the way (`interface IDerived<T> : IBase<T>`) composes its own
633
+ // stored baseTypeArgs through `bindings` before recursing, so a signature
634
+ // inherited from several generic-base links up still resolves correctly.
635
+ collectInterfaceMethods(interfaceName, bindings = new Map(), seen = new Set()) {
522
636
  if (seen.has(interfaceName))
523
637
  return [];
524
638
  seen.add(interfaceName);
525
639
  const info = this.interfaces.get(interfaceName);
526
640
  if (!info)
527
641
  return [];
528
- const inherited = info.bases.flatMap((b) => this.collectInterfaceMethods(b, seen));
529
- return [...inherited, ...info.methods];
642
+ const inherited = info.bases.flatMap((b) => {
643
+ const baseArgs = info.baseTypeArgs.get(b);
644
+ const baseBindings = baseArgs ? this.composeBaseBindings(b, baseArgs, bindings) : new Map();
645
+ return this.collectInterfaceMethods(b, baseBindings, seen);
646
+ });
647
+ const ownMethods = info.methods.map((m) => ({
648
+ name: m.name,
649
+ params: m.params.map((p) => this.substituteTypeParams(p, bindings)),
650
+ returnType: this.substituteTypeParams(m.returnType, bindings),
651
+ }));
652
+ return [...inherited, ...ownMethods];
530
653
  }
531
654
  interfaceExtends(sub, sup) {
532
655
  if (sub === sup)
@@ -538,7 +661,8 @@ export class Checker {
538
661
  }
539
662
  registerClass(decl) {
540
663
  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, () => {
664
+ const genericBaseCount = { count: 0 };
665
+ const { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
542
666
  const fields = new Map();
543
667
  const staticFields = new Map();
544
668
  for (const f of decl.fields) {
@@ -567,39 +691,47 @@ export class Checker {
567
691
  const ownCtorParams = decl.constructor
568
692
  ? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
569
693
  : 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);
694
+ // Base-list resolution runs in this same scope — so `Box<T>` in
695
+ // `class Container<T> : Box<T>` resolves its inner `T` as
696
+ // Container's own type parameter, exactly like a field type would.
697
+ let superclass = null;
698
+ let superclassTypeArgs = null;
699
+ const interfaces = [];
700
+ const interfaceTypeArgs = new Map();
701
+ for (const entry of decl.baseList) {
702
+ const kind = this.namedTypes.get(entry.name);
703
+ if (kind === "class") {
704
+ if (superclass !== null) {
705
+ this.diagnostics.error("KS4016", `Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${entry.name}')`, decl.line, decl.col);
706
+ continue;
707
+ }
708
+ superclass = entry.name;
709
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
710
+ superclassTypeArgs = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
711
+ }
712
+ }
713
+ else if (kind === "interface") {
714
+ interfaces.push(entry.name);
715
+ if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
716
+ const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
717
+ if (resolved)
718
+ interfaceTypeArgs.set(entry.name, resolved);
719
+ }
586
720
  }
587
721
  else {
588
- superclass = baseName;
722
+ this.diagnostics.error("KS4017", `Unknown base class or interface '${entry.name}'`, decl.line, decl.col);
589
723
  }
590
724
  }
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
- }
725
+ return { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs };
726
+ });
598
727
  this.classes.set(decl.name, {
599
728
  name: decl.name,
600
729
  typeParams: decl.typeParams.map((p) => p.name),
730
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
601
731
  superclass,
732
+ superclassTypeArgs,
602
733
  interfaces,
734
+ interfaceTypeArgs,
603
735
  fields,
604
736
  methods,
605
737
  staticFields,
@@ -607,6 +739,43 @@ export class Checker {
607
739
  ownCtorParams,
608
740
  });
609
741
  }
742
+ // Resolves a base-list entry's own type arguments against `baseName`'s
743
+ // declared arity — `typeArgNodes` is null when the entry was written bare
744
+ // (`: Box`, not `: Box<number>`). Must be called from inside the
745
+ // declaring class/interface's own withTypeParamsInScope, so a type
746
+ // argument that references the declaring type's own parameter (`Box<T>`
747
+ // in `class Container<T> : Box<T>`) resolves correctly. `genericBaseCount`
748
+ // is a shared mutable counter across one declaration's whole base list,
749
+ // enforcing v1's "at most one generic entry per base list" cut.
750
+ resolveBaseTypeArgs(declName, baseName, typeArgNodes, declLine, declCol, genericBaseCount) {
751
+ const declaredParams = this.genericTypeParams.get(baseName);
752
+ if (!declaredParams) {
753
+ if (typeArgNodes)
754
+ this.diagnostics.error("KS4092", `'${baseName}' is not generic — it doesn't take a type argument`, declLine, declCol);
755
+ return null;
756
+ }
757
+ if (!typeArgNodes) {
758
+ this.diagnostics.error("KS4093", `Generic base '${baseName}' requires ${this.describeArity(declaredParams)} (e.g. '${baseName}<${declaredParams.join(", ")}>')`, declLine, declCol);
759
+ return null;
760
+ }
761
+ if (typeArgNodes.length !== declaredParams.length) {
762
+ this.diagnostics.error("KS4094", `Generic base '${baseName}' expects ${this.describeArity(declaredParams)}, got ${typeArgNodes.length}`, declLine, declCol);
763
+ return null;
764
+ }
765
+ genericBaseCount.count++;
766
+ if (genericBaseCount.count > 1) {
767
+ this.diagnostics.error("KS4095", `'${declName}' has more than one generic entry in its base list — v1 supports at most one`, declLine, declCol);
768
+ }
769
+ const resolvedArgs = typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
770
+ const constraints = this.genericTypeParamConstraints.get(baseName);
771
+ const declaredParamNames = this.genericTypeParams.get(baseName);
772
+ resolvedArgs.forEach((arg, i) => {
773
+ const constraint = constraints?.[i];
774
+ if (constraint)
775
+ this.checkConstraintSatisfied(arg, constraint, declaredParamNames[i], declLine, declCol);
776
+ });
777
+ return resolvedArgs;
778
+ }
610
779
  checkClassHierarchy(decl) {
611
780
  const info = this.classes.get(decl.name);
612
781
  this.checkBaseCall(decl, info);
@@ -684,10 +853,19 @@ export class Checker {
684
853
  }
685
854
  checkInterfaceConformance(decl) {
686
855
  const classInfo = this.classes.get(decl.name);
856
+ // This is a conformance check against the class's own *declared*
857
+ // (still-abstract) signatures, not an instantiated use site, so a
858
+ // generic interface arg that references the class's own type param
859
+ // (`IContainer<T>` in `class Container<T> : IContainer<T>`) should
860
+ // compose down to that same abstract TypeParamType, matching how the
861
+ // class's own methods are stored — see identityBindings.
862
+ const selfBindings = this.identityBindings(classInfo.typeParams);
687
863
  for (const ifaceName of classInfo.interfaces) {
688
864
  if (!this.interfaces.has(ifaceName))
689
865
  continue; // already reported as an unknown base type
690
- for (const sig of this.collectInterfaceMethods(ifaceName)) {
866
+ const ifaceArgs = classInfo.interfaceTypeArgs.get(ifaceName);
867
+ const bindings = ifaceArgs ? this.composeBaseBindings(ifaceName, ifaceArgs, selfBindings) : new Map();
868
+ for (const sig of this.collectInterfaceMethods(ifaceName, bindings)) {
691
869
  const found = this.lookupMethod(decl.name, sig.name);
692
870
  if (!found) {
693
871
  this.diagnostics.error("KS4026", `Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
@@ -727,6 +905,38 @@ export class Checker {
727
905
  }
728
906
  return false;
729
907
  }
908
+ // Checks that `argType` (bound to type parameter `paramName`, declared
909
+ // `paramName : constraintInterfaceName`) actually satisfies the
910
+ // constraint: a class implementing the interface (possibly via an
911
+ // ancestor — classImplementsInterface already walks the chain), the
912
+ // interface itself (or one extending it), or a currently-in-scope type
913
+ // parameter whose own constraint already extends this one (so a
914
+ // constrained T can be threaded into another equally-constrained slot,
915
+ // e.g. `class Wrapper<T : IComparable> { void F(Box<T> b) { ... } }`
916
+ // passing Wrapper's own T to a `Box<T : IComparable>`). Anything else —
917
+ // a primitive, an array, an unconstrained type parameter, an unrelated
918
+ // class — is a compile error.
919
+ checkConstraintSatisfied(argType, constraintInterfaceName, paramName, line, col) {
920
+ if (argType.kind === "unknown")
921
+ return; // an earlier error already reported; don't cascade
922
+ let satisfied;
923
+ if (argType.kind === "class") {
924
+ satisfied = this.classImplementsInterface(argType.name, constraintInterfaceName);
925
+ }
926
+ else if (argType.kind === "interface") {
927
+ satisfied = this.interfaceExtends(argType.name, constraintInterfaceName);
928
+ }
929
+ else if (argType.kind === "typeParam") {
930
+ const ownConstraint = this.activeConstraints.get(argType.name);
931
+ satisfied = !!ownConstraint && this.interfaceExtends(ownConstraint, constraintInterfaceName);
932
+ }
933
+ else {
934
+ satisfied = false;
935
+ }
936
+ if (!satisfied) {
937
+ this.diagnostics.error("KS4096", `Type argument '${T.typeToString(argType)}' does not satisfy constraint '${constraintInterfaceName}' for type parameter '${paramName}'`, line, col);
938
+ }
939
+ }
730
940
  lookupField(className, fieldName) {
731
941
  let current = className;
732
942
  while (current) {
@@ -896,7 +1106,18 @@ export class Checker {
896
1106
  // params but no `this` — matching real base()/super() semantics,
897
1107
  // which must run before `this` becomes available.
898
1108
  if (decl.constructor.baseArgs && info.superclass) {
899
- const baseCtorParams = this.lookupCtorParams(info.superclass);
1109
+ // One step from `decl` to its immediate superclass (Container's own
1110
+ // type params, still abstract, composed through whatever it
1111
+ // supplied Box in its base list — see identityBindings), then
1112
+ // however many further steps lookupCtorParams' own `owner` implies
1113
+ // (Box itself has no constructor of its own, inherits its
1114
+ // superclass's) via bindingsAtAncestor.
1115
+ const superBindings = info.superclassTypeArgs
1116
+ ? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, this.identityBindings(info.typeParams))
1117
+ : new Map();
1118
+ const { params: rawBaseCtorParams, owner: baseCtorOwner } = this.lookupCtorParams(info.superclass);
1119
+ const baseCtorBindings = this.bindingsAtAncestor(info.superclass, superBindings, baseCtorOwner);
1120
+ const baseCtorParams = rawBaseCtorParams.map((p) => this.substituteTypeParams(p, baseCtorBindings));
900
1121
  const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
901
1122
  if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
902
1123
  this.diagnostics.error("KS4032", `Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
@@ -1496,13 +1717,24 @@ export class Checker {
1496
1717
  return T.UNKNOWN;
1497
1718
  }
1498
1719
  typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
1720
+ const constraints = this.genericTypeParamConstraints.get(expr.className);
1721
+ typeArgs.forEach((arg, i) => {
1722
+ const constraint = constraints?.[i];
1723
+ if (constraint)
1724
+ this.checkConstraintSatisfied(arg, constraint, info.typeParams[i], expr.line, expr.col);
1725
+ });
1499
1726
  }
1500
1727
  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
- }
1728
+ // The constructor being called may be inherited from a generic
1729
+ // ancestor several base-list links up (e.g. `new NumberBox(5)` where
1730
+ // `NumberBox : Box<number>` has no constructor of its own) — compose
1731
+ // bindings all the way to whichever class's own constructor
1732
+ // lookupCtorParams actually found (bindingsAtAncestor is a no-op single
1733
+ // step when it's expr.className's own, the common case).
1734
+ const { params: rawCtorParams, owner: ctorOwner } = this.lookupCtorParams(expr.className);
1735
+ const ownCtorBindings = this.ownBindings(info.typeParams, typeArgs) ?? new Map();
1736
+ const ctorBindings = this.bindingsAtAncestor(expr.className, ownCtorBindings, ctorOwner);
1737
+ const ctorParams = rawCtorParams.map((p) => this.substituteTypeParams(p, ctorBindings));
1506
1738
  if (expr.args.length !== ctorParams.length) {
1507
1739
  this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1508
1740
  }
@@ -1515,17 +1747,23 @@ export class Checker {
1515
1747
  });
1516
1748
  return T.classType(expr.className, typeArgs);
1517
1749
  }
1750
+ // `owner` is which class up the chain actually declared the constructor
1751
+ // being inherited (itself, or the nearest ancestor with one) — needed so
1752
+ // a caller can compose the right type-parameter bindings when that
1753
+ // ancestor is a generic base (see bindingsAtAncestor). `params` is stored
1754
+ // abstractly in `owner`'s own type-param-name space, same as a field or
1755
+ // method's type would be.
1518
1756
  lookupCtorParams(className) {
1519
1757
  let current = className;
1520
1758
  while (current) {
1521
1759
  const info = this.classes.get(current);
1522
1760
  if (!info)
1523
- return [];
1761
+ return { params: [], owner: className };
1524
1762
  if (info.ownCtorParams !== null)
1525
- return info.ownCtorParams;
1763
+ return { params: info.ownCtorParams, owner: current };
1526
1764
  current = info.superclass;
1527
1765
  }
1528
- return [];
1766
+ return { params: [], owner: className };
1529
1767
  }
1530
1768
  // Array stdlib with a fixed (non-polymorphic) signature, given the
1531
1769
  // array's own element type — everything except Map, which checkCall
@@ -1663,13 +1901,14 @@ export class Checker {
1663
1901
  return { type: T.UNKNOWN, methodInfo: null };
1664
1902
  }
1665
1903
  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);
1904
+ // A member found on an *ancestor* (owner !== objectType.name v1 now
1905
+ // supports a generic superclass, see registerClass/
1906
+ // bindingsAtAncestor) needs bindings composed through however many
1907
+ // generic base links separate objectType from that ancestor, not just
1908
+ // objectType's own type arguments bindingsAtAncestor does that walk
1909
+ // (a no-op single step when owner IS objectType.name, the common
1910
+ // case, same result as before this existed).
1911
+ const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
1673
1912
  const field = this.lookupField(objectType.name, expr.property);
1674
1913
  if (field) {
1675
1914
  this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
@@ -1679,33 +1918,52 @@ export class Checker {
1679
1918
  this.diagnostics.error("KS4082", `'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1680
1919
  }
1681
1920
  }
1682
- return { type: substitute(field.info.type), methodInfo: null };
1921
+ const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, field.owner);
1922
+ return { type: this.substituteTypeParams(field.info.type, bindings), methodInfo: null };
1683
1923
  }
1684
1924
  const method = this.lookupMethod(objectType.name, expr.property);
1685
1925
  if (method) {
1686
1926
  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;
1927
+ const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, method.owner);
1928
+ const info = {
1929
+ ...method.info,
1930
+ params: method.info.params.map((p) => this.substituteTypeParams(p, bindings)),
1931
+ returnType: this.substituteTypeParams(method.info.returnType, bindings),
1932
+ };
1688
1933
  return { type: info.returnType, methodInfo: info };
1689
1934
  }
1690
1935
  this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1691
1936
  return { type: T.UNKNOWN, methodInfo: null };
1692
1937
  }
1693
1938
  if (objectType.kind === "interface") {
1694
- const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
1939
+ // collectInterfaceMethods itself now substitutes through `bindings`
1940
+ // including across a generic base-interface link — so passing
1941
+ // objectType's own bindings once at the root is enough; no separate
1942
+ // re-substitution needed here (see its own header comment).
1943
+ const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
1944
+ const sig = this.collectInterfaceMethods(objectType.name, ownBindings).find((m) => m.name === expr.property);
1695
1945
  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 } };
1946
+ return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
1705
1947
  }
1706
1948
  this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1707
1949
  return { type: T.UNKNOWN, methodInfo: null };
1708
1950
  }
1951
+ // A bare type-parameter receiver (`this.value.Foo()` where `value: T`)
1952
+ // — resolves against the constraint interface's own signature if `T`
1953
+ // is constrained; an unconstrained `T` falls through to the generic
1954
+ // "cannot access member" error below, unchanged from before constraints
1955
+ // existed (see README/LLM.md's own documented restriction).
1956
+ if (objectType.kind === "typeParam") {
1957
+ const constraintName = this.activeConstraints.get(objectType.name);
1958
+ if (constraintName) {
1959
+ const sig = this.collectInterfaceMethods(constraintName).find((m) => m.name === expr.property);
1960
+ if (sig) {
1961
+ return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
1962
+ }
1963
+ this.diagnostics.error("KS4097", `Interface '${constraintName}' (the constraint on type parameter '${objectType.name}') has no member '${expr.property}'`, expr.line, expr.col);
1964
+ return { type: T.UNKNOWN, methodInfo: null };
1965
+ }
1966
+ }
1709
1967
  if (objectType.kind !== "unknown") {
1710
1968
  this.diagnostics.error("KS4085", `Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
1711
1969
  }
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
@@ -243,21 +243,40 @@ export class Parser {
243
243
  this.consume(TokenKind.RParen, "Expected ')' after parameters");
244
244
  return params;
245
245
  }
246
- // `<K, V>` right after a class/interface name — one or more type
247
- // parameters, comma-separated. Empty array if absent, the overwhelmingly
248
- // common case. Constraint syntax (`<T : IFoo>`) isn't parsed yet — every
249
- // entry's `constraint` is always null for now (see AST.TypeParamDecl).
246
+ // `<K : IHashable, V>` right after a class/interface/function name — one
247
+ // or more type parameters, comma-separated, each with an optional
248
+ // `: InterfaceName` constraint. Empty array if absent, the overwhelmingly
249
+ // common case.
250
250
  parseTypeParamList() {
251
251
  if (!this.match(TokenKind.Lt))
252
252
  return [];
253
253
  const params = [];
254
254
  do {
255
255
  const nameTok = this.consume(TokenKind.Identifier, "Expected a type parameter name");
256
- params.push({ name: nameTok.lexeme, constraint: null, line: nameTok.line, col: nameTok.col });
256
+ let constraint = null;
257
+ if (this.match(TokenKind.Colon)) {
258
+ constraint = this.consume(TokenKind.Identifier, "Expected a constraint interface name after ':'").lexeme;
259
+ }
260
+ params.push({ name: nameTok.lexeme, constraint, line: nameTok.line, col: nameTok.col });
257
261
  } while (this.match(TokenKind.Comma));
258
262
  this.consume(TokenKind.Gt, "Expected '>' after type parameter list");
259
263
  return params;
260
264
  }
265
+ // `Bar<T>` or `IBaz` — one base-list entry, with or without its own type
266
+ // arguments. Used by both class and interface base lists.
267
+ parseBaseListEntry() {
268
+ const name = this.consume(TokenKind.Identifier, "Expected base class or interface name").lexeme;
269
+ let typeArgs = null;
270
+ if (this.check(TokenKind.Lt)) {
271
+ this.advance();
272
+ typeArgs = [this.parseType()];
273
+ while (this.match(TokenKind.Comma)) {
274
+ typeArgs.push(this.parseType());
275
+ }
276
+ this.consume(TokenKind.Gt, "Expected '>' after type argument list");
277
+ }
278
+ return { name, typeArgs };
279
+ }
261
280
  parseClassDecl(isExported) {
262
281
  const start = this.advance(); // 'class'
263
282
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
@@ -265,7 +284,7 @@ export class Parser {
265
284
  const baseList = [];
266
285
  if (this.match(TokenKind.Colon)) {
267
286
  do {
268
- baseList.push(this.consume(TokenKind.Identifier, "Expected base class or interface name").lexeme);
287
+ baseList.push(this.parseBaseListEntry());
269
288
  } while (this.match(TokenKind.Comma));
270
289
  }
271
290
  this.consume(TokenKind.LBrace, "Expected '{' before class body");
@@ -426,7 +445,7 @@ export class Parser {
426
445
  const baseList = [];
427
446
  if (this.match(TokenKind.Colon)) {
428
447
  do {
429
- baseList.push(this.consume(TokenKind.Identifier, "Expected base interface name").lexeme);
448
+ baseList.push(this.parseBaseListEntry());
430
449
  } while (this.match(TokenKind.Comma));
431
450
  }
432
451
  this.consume(TokenKind.LBrace, "Expected '{' before interface body");
package/dist/printer.js CHANGED
@@ -118,6 +118,15 @@ 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
+ }
124
+ printTypeParamName(decl) {
125
+ if (decl.typeParams.length === 0)
126
+ return decl.name;
127
+ const params = decl.typeParams.map((p) => (p.constraint ? `${p.name} : ${p.constraint}` : p.name)).join(", ");
128
+ return `${decl.name}<${params}>`;
129
+ }
121
130
  // ---------- top-level declarations ----------
122
131
  printStatement(stmt, indent) {
123
132
  const pad = indentStr(indent);
@@ -212,8 +221,8 @@ export class Printer {
212
221
  printClass(decl, indent) {
213
222
  const pad = indentStr(indent);
214
223
  const memberPad = indentStr(indent + 1);
215
- 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} {`;
224
+ const nameWithTypeParam = this.printTypeParamName(decl);
225
+ const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `class ${nameWithTypeParam} {`;
217
226
  const prefix = decl.isExported ? "" : "private ";
218
227
  const memberParts = [];
219
228
  const pushMember = (node, text) => {
@@ -250,8 +259,8 @@ export class Printer {
250
259
  printInterface(decl, indent) {
251
260
  const pad = indentStr(indent);
252
261
  const memberPad = indentStr(indent + 1);
253
- 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} {`;
262
+ const nameWithTypeParam = this.printTypeParamName(decl);
263
+ const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `interface ${nameWithTypeParam} {`;
255
264
  const prefix = decl.isExported ? "" : "private ";
256
265
  if (decl.methods.length === 0)
257
266
  return `${pad}${prefix}${header}\n${pad}}`;
@@ -279,7 +288,7 @@ export class Printer {
279
288
  const pad = indentStr(indent);
280
289
  const memberPad = indentStr(indent + 1);
281
290
  const prefix = decl.isExported ? "" : "private ";
282
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
291
+ const nameWithTypeParam = this.printTypeParamName(decl);
283
292
  const lines = [];
284
293
  if (decl.hasConstructor)
285
294
  lines.push(`${memberPad}constructor(${this.printParams(decl.ctorParams)});`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.12.0",
3
+ "version": "0.14.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",