kopscript 0.13.0 → 0.15.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
@@ -75,9 +75,10 @@ below). No circular `using` (compile error).
75
75
  **Does not exist**: `any`/`unknown` as a writable annotation, a `let`/`var` keyword (see
76
76
  Declarations below — there isn't one), type inference for declarations (every
77
77
  local/`const`/param/field/return type is written out explicitly), union types, tuples,
78
- structural/duck typing (all typing is nominal). Generics exist but are deliberately
79
- scoped down see the Generics section for exactly what's NOT supported there (multiple
80
- type parameters, constraints, generic functions, generic inheritance).
78
+ structural/duck typing (all typing is nominal). Generics exist and cover multiple type
79
+ parameters, single-interface constraints, generic inheritance, and free generic functions
80
+ see the Generics section for exactly what's still NOT supported there (variance,
81
+ multiple constraints per parameter, generic methods).
81
82
 
82
83
  ## Declarations
83
84
 
@@ -303,9 +304,10 @@ class Pair<K, V> {
303
304
  Pair<number, string> p = new Pair<number, string>(1, "a");
304
305
  ```
305
306
 
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.
307
+ One or more invariant type parameters per class/interface (real *or* `extern` — see the
308
+ `extern` section below), comma-separated: `<T>`, `<K, V>`, `<A, B, C>`, however many the
309
+ declaration needs each optionally constrained to a single interface: `<T : IComparable>`,
310
+ `<K : IHashable, V>` (mixing constrained and unconstrained params is fine).
309
311
  Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
310
312
  compile to the exact same plain `class Box`), so there's no runtime cost and no way to
311
313
  inspect a type parameter at runtime.
@@ -322,10 +324,30 @@ inspect a type parameter at runtime.
322
324
  - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`,
323
325
  `Pair<number, Pair<string, bool>>`.
324
326
  - 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.
327
+ store it, return it, pass it around, but an **unconstrained** parameter (no `: IFoo`)
328
+ means you **cannot call any member on a bare type-parameter value** (`this.value.Foo()`
329
+ where `value: T` is a compile error: "Cannot access member 'Foo' on type 'T'"). This is
330
+ correct, not a bug — same as an unconstrained type parameter in C#/Java/TypeScript.
331
+
332
+ **Constraints**: `<T : IComparable>` lifts the "no member calls" restriction — a member
333
+ call on a constrained `T` resolves against the constraint interface's own signature:
334
+ ```ks
335
+ interface IComparable { number CompareTo(); }
336
+ class Box<T : IComparable> {
337
+ public T Value;
338
+ constructor(T v) { this.Value = v; }
339
+ public number Compare() { return this.Value.CompareTo(); } // legal — T : IComparable
340
+ }
341
+ Box<Money> b = new Box<Money>(new Money(5)); // Money must implement IComparable
342
+ ```
343
+ A type argument satisfies a constraint by implementing the interface directly, through an
344
+ ancestor (`classImplementsInterface` walks the whole superclass chain), or — inside another
345
+ generic body — by already being an equally- or more-constrained type parameter of its own
346
+ (`class Wrapper<T : IComparable> { Box<T> MakeBox(T v) { return new Box<T>(v); } }` is
347
+ legal without ever naming a concrete type). Checked everywhere a type argument is bound: on
348
+ `new`, on a bare generic type reference, and on a generic base in a base list.
349
+ **v1 limit**: at most one constraint per parameter (`T : IFoo, IBar` isn't supported), and
350
+ the constraint must be an interface, never a class.
329
351
 
330
352
  **Generic inheritance**: a class/interface base list CAN name a generic base, with its own
331
353
  type arguments, in either direction:
@@ -345,12 +367,30 @@ implemented/extended interface — never more than one) — `class Foo<T> : Box<
345
367
  IContainer<T>` is a compile error ("has more than one generic entry in its base list") even
346
368
  though either half alone would work.
347
369
 
370
+ **Generic functions**: only free/top-level functions, never a method inside a class (a
371
+ method still only ever uses its enclosing class's own type parameters):
372
+ ```ks
373
+ T Identity<T>(T x) { return x; }
374
+ number n = Identity(5); // T inferred as number, from the argument
375
+ string s = Identity("hi"); // T inferred as string, independently
376
+ ```
377
+ **No explicit type argument at a call site** — `Identity<number>(5)` isn't valid syntax;
378
+ `T` is always inferred from the actual argument types by structurally unifying each
379
+ declared (possibly abstract) parameter type against its argument's real type — including
380
+ through a nested generic type (`T Unwrap<T>(Box<T> b)` infers `T` from `Box<number>`) or a
381
+ lambda argument's own explicit parameter type (`void UseCallback<T>((T) => void cb)` infers
382
+ `T` from `(number n) => ...`). This is a real grammar constraint, not a missing feature:
383
+ `new Box<number>(...)` disambiguates `<` from a comparison only because `new` is a distinct
384
+ keyword context — a bare call has no such anchor, so `f<T>(x)` would be genuinely ambiguous
385
+ with a chained `<`/`>` comparison. If no argument determines a declared type parameter, or
386
+ two arguments would bind it to conflicting types, that's a compile error naming the
387
+ parameter, not a syntax feature to reach for. A constrained type parameter (`T :
388
+ IComparable`) works exactly like it does on a class — the inferred type must satisfy it,
389
+ checked after inference succeeds.
390
+
348
391
  **Does not exist (v1 scope cuts, each deliberate)**:
349
- - **Generic functions.** `T Identity<T>(T x)` doesn't parse only classes and interfaces
350
- take type parameters, not free functions/methods themselves (a method *inside* a
351
- generic class can use that class's own type parameters freely, same as any other member).
352
- - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
353
- which is why you can't call members on a bare one (see above).
392
+ - **Generic methods.** A class method can't introduce its own new type parameter beyond
393
+ its enclosing class's (generic functions are free-function-only see above).
354
394
  - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
355
395
 
356
396
  ## Nullable types — `T?`
@@ -428,13 +468,14 @@ KopScript-declared name exactly. Extern class members use real JS member names v
428
468
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
429
469
  declarations — each stands alone.
430
470
 
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.
471
+ `extern class` can carry type parameters, optionally constrained (`extern class Box<T :
472
+ IComparable> { constructor(T v); T Value { get; } } from "some-package";`) — identical
473
+ rules to a real generic class (see "Generics" above: invariant, erased, generic
474
+ inheritance and constraints both included), so a generic type from another package
475
+ instantiates and type-checks exactly like a local one (`Box<number>`, arity/invariance/
476
+ constraint errors included) — and a real KopScript class can extend a generic `extern
477
+ class` with a concrete or threaded-through type argument, same as extending a real generic
478
+ base.
438
479
 
439
480
  **Never write `async` on an extern function/method signature** — declare its return type
440
481
  as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
@@ -521,10 +562,11 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
521
562
 
522
563
  ## Does not exist (don't reach for these)
523
564
 
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 ·
565
+ Generics beyond one or more type parameters (each with at most one interface constraint),
566
+ single-generic-base inheritance, and free (never method-level) generic functions with
567
+ inference-only call sites (no `T : IFoo, IBar` multi-constraints, no explicit
568
+ `Identity<number>(5)` type arguments, no variance, no more than one generic entry per base
569
+ list — see Generics above for what *is* supported) · `any`/`unknown` annotations ·
528
570
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
529
571
  reflection · type inference on declarations · ternary expression · union/tuple types ·
530
572
  **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
@@ -559,9 +601,12 @@ Real, observed cases where a plausible-looking guess was wrong — not hypotheti
559
601
  dedicated rewriter, `qualifyThis`, specifically because this isn't automatic.)
560
602
  - **No `let`/`var`, ever.** Every local is `Type name = value;` — writing `let x = 5;` or a
561
603
  bare `const x = 5;` (missing the type) is a parse error, not a lenient inferred form.
562
- - **No generic functions.** `T Identity<T>(T x) { return x; }` doesn't parse — only classes
563
- and interfaces take `<T>`. Don't reach for this even though every mainstream generic
564
- language supports it.
604
+ - **No explicit type argument at a generic function call site.** `Identity<number>(5)`
605
+ doesn't parse a bare call has no keyword like `new` to disambiguate `<` from a
606
+ comparison, so type arguments are always inferred from the actual arguments instead.
607
+ Just call `Identity(5)`; if inference can't determine a type parameter, restructure the
608
+ call (e.g. an argument that actually mentions the type) rather than reaching for explicit
609
+ syntax that isn't there.
565
610
  - **Early-return doesn't narrow a nullable type.** `if (x == null) { return; } print(x.Length);`
566
611
  still errors on `x.Length` — narrowing is scope-based (an `if`/`else` block), not
567
612
  control-flow/reachability-based. Wrap the rest of the logic in the `if (x != null) { ... }`
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, interfaces, and free
26
+ functions (`class Box<T> { public T Value; }`, `T Identity<T>(T x) { return x; }`), each
27
+ optionally constrained to an interface (`class Box<T : IComparable> { ... }`), plus
28
+ generic inheritance (`class IntBox : Box<number> { }`) — erased at codegen with zero
29
+ runtime 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,16 +255,29 @@ 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.
263
- - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
264
- interfaces take type parameters. A method *inside* a generic class can still use that
265
- class's own type parameters freely; it's just not introducing one of its own.
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.
261
+
262
+ By default a type parameter is fully unconstrained, so you can't call any member on a bare
263
+ `T`/`K`/`V` value inside the generic class's own bodythe same restriction C#/Java/
264
+ TypeScript put on an unconstrained parameter, not a bug. A single optional `: IFoo`
265
+ constraint per parameter lifts that:
266
+
267
+ ```ks
268
+ interface IComparable { number CompareTo(); }
269
+ class Box<T : IComparable> {
270
+ public T Value;
271
+ constructor(T v) { this.Value = v; }
272
+ public number Compare() { return this.Value.CompareTo(); } // legal — T is constrained
273
+ }
274
+ Box<Money> b = new Box<Money>(new Money(5)); // Money must implement IComparable
275
+ ```
276
+
277
+ The constraint must be an interface (never a class), and a type argument satisfies it by
278
+ implementing it directly, through an ancestor, or (inside another generic body) by already
279
+ being an equally-or-more-constrained type parameter of its own. **v1 limit**: at most one
280
+ constraint per parameter — `T : IFoo, IBar` isn't supported.
266
281
 
267
282
  A class or interface *can* extend/implement a generic base, type arguments and all:
268
283
 
@@ -281,6 +296,24 @@ down. **v1 limit**: at most one generic entry across a whole base list (the supe
281
296
  one implemented interface — not several at once) — `class Foo<T> : Box<T>, IContainer<T>`
282
297
  is a compile error even though each half would work alone.
283
298
 
299
+ **Free functions can be generic too** — only free/top-level functions, not class methods
300
+ (a method inside a class still only uses its enclosing class's own type parameters, never
301
+ introduces a new one):
302
+
303
+ ```ks
304
+ T Identity<T>(T x) { return x; }
305
+ number n = Identity(5); // T = number, inferred from the argument
306
+ string s = Identity("hi"); // T = string, independently
307
+ ```
308
+
309
+ **No explicit type argument at the call site** — `Identity<number>(5)` isn't valid syntax;
310
+ `T` is always inferred from the actual argument types. This is a real grammar constraint,
311
+ not a missing feature: `new Box<number>(...)` can disambiguate `<` from a comparison only
312
+ because `new` is a distinct keyword context — a bare call has no such anchor, so
313
+ `f<T>(x)` would be genuinely ambiguous with a chained `<`/`>` comparison. If inference
314
+ can't pin down a type parameter from the arguments given, that's a compile error naming the
315
+ unresolved parameter, not a syntax feature to reach for.
316
+
284
317
  See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
285
318
  against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`, `Pair<number,
286
319
  Pair<string, bool>>`) and generic interfaces used as standalone parameter types both work
@@ -383,11 +416,12 @@ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...
383
416
  proving `extern` works as a real cross-*package* boundary, not just for describing DOM
384
417
  globals within a single project.
385
418
 
386
- An `extern class` can carry its own type parameter(s), `extern class Box<T> { ... }` or
387
- `extern class Pair<K, V> { ... }` exactly the same rules as a real generic class (see
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:
419
+ An `extern class` can carry its own type parameter(s), optionally constrained
420
+ `extern class Box<T> { ... }`, `extern class Pair<K, V> { ... }`, `extern class Box<T :
421
+ IComparable> { ... }` exactly the same rules as a real generic class (see "Generics"
422
+ above: invariant, erased, and a real class can extend it as a generic base), so a generic
423
+ type from another package (e.g. Kopular's `FormField<T>`) can be described and
424
+ instantiated generically, not just per concrete type:
391
425
 
392
426
  ```ks
393
427
  extern class Box<T> {
@@ -873,13 +907,13 @@ into your extensions folder).
873
907
 
874
908
  ## Status
875
909
 
876
- This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (a single
877
- unconstrained, invariant type parameter on classes/interfaces) have both shipped — see
878
- above for both. What generics deliberately doesn't cover: multiple type parameters
879
- (`Map<K, V>`), constraints (`T : IFoo`), generic functions, generic inheritance, and
880
- variance each a real, separable extension rather than a v1 oversight. Also not yet
881
- supported: static auto-properties, interface properties (methods only), and nested
882
- functions/classes.
910
+ This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (multiple type
911
+ parameters, single-interface constraints, generic inheritance, and free generic functions)
912
+ have all shipped — see above for the full "Generics" section. What generics still
913
+ deliberately doesn't cover: variance, multiple constraints per parameter, generic methods
914
+ (only free functions), and more than one generic entry per base list each a real,
915
+ separable extension rather than a v1 oversight. Also not yet supported: static
916
+ auto-properties, interface properties (methods only), and nested functions/classes.
883
917
 
884
918
  `async`/`await`, `task<T>`, and `try`/`catch`/`finally`/`throw` are now in place (see the
885
919
  language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
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)
@@ -168,7 +194,12 @@ export class Checker {
168
194
  this.checkClassBody(c);
169
195
  }
170
196
  registerExternFunction(decl) {
197
+ // extern functions never take a type parameter of their own in v1 —
198
+ // generic functions (see registerFunction) are a real-function-only
199
+ // feature; an extern binding always describes a concrete signature.
171
200
  this.functions.set(decl.name, {
201
+ typeParams: [],
202
+ typeParamConstraints: [],
172
203
  params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
173
204
  returnType: this.resolveType(decl.returnType, decl.line, decl.col),
174
205
  });
@@ -203,6 +234,7 @@ export class Checker {
203
234
  this.classes.set(decl.name, {
204
235
  name: decl.name,
205
236
  typeParams: decl.typeParams.map((p) => p.name),
237
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
206
238
  superclass: null,
207
239
  superclassTypeArgs: null,
208
240
  interfaces: [],
@@ -394,6 +426,14 @@ export class Checker {
394
426
  else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
395
427
  this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
396
428
  }
429
+ else if (declaredParams && suppliedCount === declaredParams.length) {
430
+ const constraints = this.genericTypeParamConstraints.get(type.name);
431
+ type.typeArgs.forEach((arg, i) => {
432
+ const constraint = constraints?.[i];
433
+ if (constraint)
434
+ this.checkConstraintSatisfied(arg, constraint, declaredParams[i], line, col);
435
+ });
436
+ }
397
437
  type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
398
438
  return;
399
439
  }
@@ -415,8 +455,11 @@ export class Checker {
415
455
  if (typeParams.length === 0)
416
456
  return fn();
417
457
  const previous = typeParams.map((p) => this.namedTypes.get(p.name));
418
- for (const p of typeParams)
458
+ const previousConstraints = typeParams.map((p) => this.activeConstraints.get(p.name));
459
+ for (const p of typeParams) {
419
460
  this.namedTypes.set(p.name, "typeParam");
461
+ this.activeConstraints.set(p.name, p.constraint);
462
+ }
420
463
  try {
421
464
  return fn();
422
465
  }
@@ -427,6 +470,11 @@ export class Checker {
427
470
  this.namedTypes.delete(p.name);
428
471
  else
429
472
  this.namedTypes.set(p.name, prev);
473
+ const prevConstraint = previousConstraints[i];
474
+ if (prevConstraint === undefined)
475
+ this.activeConstraints.delete(p.name);
476
+ else
477
+ this.activeConstraints.set(p.name, prevConstraint);
430
478
  });
431
479
  }
432
480
  }
@@ -461,6 +509,68 @@ export class Checker {
461
509
  return type;
462
510
  }
463
511
  }
512
+ // The reverse of substituteTypeParams: derives bindings *from* matching a
513
+ // generic function's declared (abstract) parameter type against one real
514
+ // argument's actual (concrete) type, instead of substituting bindings
515
+ // *into* a type. Structural unification, same recursive shape as
516
+ // substituteTypeParams/typesEqual — whenever `declared` is a TypeParamType
517
+ // for one of `typeParamNames`, records `bindings.set(name, actual)` the
518
+ // first time it's seen; a later argument that would bind the same name to
519
+ // a genuinely different type is recorded in `conflicts` instead of
520
+ // silently overwriting (see checkGenericFunctionCall's own error for
521
+ // that). A structural mismatch elsewhere (e.g. declared is `T[]` but
522
+ // actual isn't an array at all) simply infers nothing from that
523
+ // position — the normal post-substitution assignability check catches
524
+ // the real type error afterward, with a clearer message than unification
525
+ // failing silently here would give.
526
+ inferTypeParamBindings(declared, actual, typeParamNames, bindings, conflicts) {
527
+ if (declared.kind === "typeParam" && typeParamNames.has(declared.name)) {
528
+ const existing = bindings.get(declared.name);
529
+ if (existing) {
530
+ if (!T.typesEqual(existing, actual))
531
+ conflicts.set(declared.name, actual);
532
+ }
533
+ else {
534
+ bindings.set(declared.name, actual);
535
+ }
536
+ return;
537
+ }
538
+ if (declared.kind === "array" && actual.kind === "array") {
539
+ this.inferTypeParamBindings(declared.element, actual.element, typeParamNames, bindings, conflicts);
540
+ return;
541
+ }
542
+ if (declared.kind === "nullable") {
543
+ const actualInner = actual.kind === "nullable" ? actual.inner : actual;
544
+ this.inferTypeParamBindings(declared.inner, actualInner, typeParamNames, bindings, conflicts);
545
+ return;
546
+ }
547
+ if (declared.kind === "task" && actual.kind === "task") {
548
+ this.inferTypeParamBindings(declared.resultType, actual.resultType, typeParamNames, bindings, conflicts);
549
+ return;
550
+ }
551
+ if (declared.kind === "state" && actual.kind === "state") {
552
+ this.inferTypeParamBindings(declared.valueType, actual.valueType, typeParamNames, bindings, conflicts);
553
+ return;
554
+ }
555
+ if ((declared.kind === "class" || declared.kind === "interface") && declared.kind === actual.kind && declared.name === actual.name) {
556
+ const declaredArgs = declared.typeArgs ?? [];
557
+ const actualArgs = actual.typeArgs ?? [];
558
+ declaredArgs.forEach((d, i) => {
559
+ if (actualArgs[i])
560
+ this.inferTypeParamBindings(d, actualArgs[i], typeParamNames, bindings, conflicts);
561
+ });
562
+ return;
563
+ }
564
+ if (declared.kind === "function" && actual.kind === "function") {
565
+ declared.params.forEach((d, i) => {
566
+ if (actual.params[i])
567
+ this.inferTypeParamBindings(d, actual.params[i], typeParamNames, bindings, conflicts);
568
+ });
569
+ this.inferTypeParamBindings(declared.returnType, actual.returnType, typeParamNames, bindings, conflicts);
570
+ }
571
+ // Anything else (primitive/enum/void/unknown, or a structural
572
+ // mismatch) has nothing to unify — no-op.
573
+ }
464
574
  // Builds the bindings map for a generic reference (a `ClassType`/
465
575
  // `InterfaceType`'s own `typeArgs`, zipped against its declared
466
576
  // `typeParams`) — the "own bindings" every member-lookup/conformance
@@ -551,7 +661,14 @@ export class Checker {
551
661
  }
552
662
  return { methods, bases, baseTypeArgs };
553
663
  });
554
- this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, baseTypeArgs, methods });
664
+ this.interfaces.set(decl.name, {
665
+ name: decl.name,
666
+ typeParams: decl.typeParams.map((p) => p.name),
667
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
668
+ bases,
669
+ baseTypeArgs,
670
+ methods,
671
+ });
555
672
  this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
556
673
  }
557
674
  checkInterfaceHierarchy(decl) {
@@ -677,6 +794,7 @@ export class Checker {
677
794
  this.classes.set(decl.name, {
678
795
  name: decl.name,
679
796
  typeParams: decl.typeParams.map((p) => p.name),
797
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
680
798
  superclass,
681
799
  superclassTypeArgs,
682
800
  interfaces,
@@ -715,7 +833,15 @@ export class Checker {
715
833
  if (genericBaseCount.count > 1) {
716
834
  this.diagnostics.error("KS4095", `'${declName}' has more than one generic entry in its base list — v1 supports at most one`, declLine, declCol);
717
835
  }
718
- return typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
836
+ const resolvedArgs = typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
837
+ const constraints = this.genericTypeParamConstraints.get(baseName);
838
+ const declaredParamNames = this.genericTypeParams.get(baseName);
839
+ resolvedArgs.forEach((arg, i) => {
840
+ const constraint = constraints?.[i];
841
+ if (constraint)
842
+ this.checkConstraintSatisfied(arg, constraint, declaredParamNames[i], declLine, declCol);
843
+ });
844
+ return resolvedArgs;
719
845
  }
720
846
  checkClassHierarchy(decl) {
721
847
  const info = this.classes.get(decl.name);
@@ -820,10 +946,18 @@ export class Checker {
820
946
  }
821
947
  }
822
948
  registerFunction(decl) {
823
- const params = decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col));
824
- const returnType = this.resolveType(decl.returnType, decl.line, decl.col);
825
- this.functions.set(decl.name, { params, returnType });
826
- this.recordHover(decl.line, decl.col, `function ${decl.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
949
+ const { params, returnType } = this.withTypeParamsInScope(decl.typeParams, () => ({
950
+ params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
951
+ returnType: this.resolveType(decl.returnType, decl.line, decl.col),
952
+ }));
953
+ this.functions.set(decl.name, {
954
+ typeParams: decl.typeParams.map((p) => p.name),
955
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
956
+ params,
957
+ returnType,
958
+ });
959
+ const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
960
+ this.recordHover(decl.line, decl.col, `function ${nameWithTypeParam}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
827
961
  }
828
962
  isSubclass(sub, sup) {
829
963
  let current = sub;
@@ -846,6 +980,38 @@ export class Checker {
846
980
  }
847
981
  return false;
848
982
  }
983
+ // Checks that `argType` (bound to type parameter `paramName`, declared
984
+ // `paramName : constraintInterfaceName`) actually satisfies the
985
+ // constraint: a class implementing the interface (possibly via an
986
+ // ancestor — classImplementsInterface already walks the chain), the
987
+ // interface itself (or one extending it), or a currently-in-scope type
988
+ // parameter whose own constraint already extends this one (so a
989
+ // constrained T can be threaded into another equally-constrained slot,
990
+ // e.g. `class Wrapper<T : IComparable> { void F(Box<T> b) { ... } }`
991
+ // passing Wrapper's own T to a `Box<T : IComparable>`). Anything else —
992
+ // a primitive, an array, an unconstrained type parameter, an unrelated
993
+ // class — is a compile error.
994
+ checkConstraintSatisfied(argType, constraintInterfaceName, paramName, line, col) {
995
+ if (argType.kind === "unknown")
996
+ return; // an earlier error already reported; don't cascade
997
+ let satisfied;
998
+ if (argType.kind === "class") {
999
+ satisfied = this.classImplementsInterface(argType.name, constraintInterfaceName);
1000
+ }
1001
+ else if (argType.kind === "interface") {
1002
+ satisfied = this.interfaceExtends(argType.name, constraintInterfaceName);
1003
+ }
1004
+ else if (argType.kind === "typeParam") {
1005
+ const ownConstraint = this.activeConstraints.get(argType.name);
1006
+ satisfied = !!ownConstraint && this.interfaceExtends(ownConstraint, constraintInterfaceName);
1007
+ }
1008
+ else {
1009
+ satisfied = false;
1010
+ }
1011
+ if (!satisfied) {
1012
+ this.diagnostics.error("KS4096", `Type argument '${T.typeToString(argType)}' does not satisfy constraint '${constraintInterfaceName}' for type parameter '${paramName}'`, line, col);
1013
+ }
1014
+ }
849
1015
  lookupField(className, fieldName) {
850
1016
  let current = className;
851
1017
  while (current) {
@@ -991,11 +1157,17 @@ export class Checker {
991
1157
  this.checkStatement(stmt, scope, { returnType: T.VOID, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: true });
992
1158
  }
993
1159
  checkFunctionBody(decl, parentScope) {
994
- const info = this.functions.get(decl.name);
995
- const scope = parentScope.child();
996
- decl.params.forEach((p, i) => scope.declare(p.name, info.params[i], false));
997
- const returnType = this.resolveBodyReturnType(info.returnType, decl.isAsync, decl.line, decl.col);
998
- this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
1160
+ // Same withTypeParamsInScope treatment checkClassBody gives a generic
1161
+ // class's own method bodies — without it, a generic function's type
1162
+ // parameter would resolve in its declared signature but not inside a
1163
+ // local declaration/lambda written in its own body.
1164
+ this.withTypeParamsInScope(decl.typeParams, () => {
1165
+ const info = this.functions.get(decl.name);
1166
+ const scope = parentScope.child();
1167
+ decl.params.forEach((p, i) => scope.declare(p.name, info.params[i], false));
1168
+ const returnType = this.resolveBodyReturnType(info.returnType, decl.isAsync, decl.line, decl.col);
1169
+ this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
1170
+ });
999
1171
  }
1000
1172
  checkClassBody(decl) {
1001
1173
  // Constructor/method *bodies* run in this same scope registerClass used
@@ -1535,6 +1707,9 @@ export class Checker {
1535
1707
  return T.UNKNOWN;
1536
1708
  }
1537
1709
  this.recordHover(expr.callee.line, expr.callee.col, `function ${expr.callee.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
1710
+ if (info.typeParams.length > 0) {
1711
+ return this.checkGenericFunctionCall(expr, expr.callee.name, info, scope, ctx);
1712
+ }
1538
1713
  this.checkArgs(expr, info.params, scope, ctx);
1539
1714
  return info.returnType;
1540
1715
  }
@@ -1587,6 +1762,50 @@ export class Checker {
1587
1762
  }
1588
1763
  });
1589
1764
  }
1765
+ // A generic function call has no explicit type-argument syntax (see
1766
+ // README's "Generics" — a real grammar-ambiguity reason, `f<T>(x)` is
1767
+ // indistinguishable from a chained comparison with no keyword like `new`
1768
+ // to disambiguate it), so every type parameter is inferred from the
1769
+ // actual argument types instead. Checks argument count first (an arity
1770
+ // mismatch would make inference itself meaningless), infers bindings by
1771
+ // unifying each declared (abstract) parameter type against its actual
1772
+ // argument's real type (see inferTypeParamBindings), then re-runs the
1773
+ // normal assignability check with the now-concrete substituted parameter
1774
+ // types — catching anything structural unification alone wouldn't (e.g.
1775
+ // an argument assignable to, but not identical to, its inferred slot).
1776
+ checkGenericFunctionCall(expr, functionName, info, scope, ctx) {
1777
+ if (expr.args.length !== info.params.length) {
1778
+ this.diagnostics.error("KS4098", `Expected ${info.params.length} argument(s), got ${expr.args.length}`, expr.line, expr.col);
1779
+ expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1780
+ return T.UNKNOWN;
1781
+ }
1782
+ const actualArgTypes = expr.args.map((a) => this.checkExpression(a, scope, ctx));
1783
+ const typeParamNames = new Set(info.typeParams);
1784
+ const bindings = new Map();
1785
+ const conflicts = new Map();
1786
+ info.params.forEach((declared, i) => this.inferTypeParamBindings(declared, actualArgTypes[i], typeParamNames, bindings, conflicts));
1787
+ for (const [name, conflictingType] of conflicts) {
1788
+ this.diagnostics.error("KS4099", `Type parameter '${name}' inferred as both '${T.typeToString(bindings.get(name))}' and '${T.typeToString(conflictingType)}' from different arguments — conflicting types`, expr.line, expr.col);
1789
+ }
1790
+ const unresolved = info.typeParams.filter((name) => !bindings.has(name));
1791
+ if (unresolved.length > 0) {
1792
+ this.diagnostics.error("KS4100", `Cannot infer type parameter${unresolved.length === 1 ? "" : "s"} '${unresolved.join("', '")}' for '${functionName}' — no argument determines ${unresolved.length === 1 ? "it" : "them"}`, expr.line, expr.col);
1793
+ return T.UNKNOWN;
1794
+ }
1795
+ info.typeParams.forEach((name, i) => {
1796
+ const constraint = info.typeParamConstraints[i];
1797
+ if (constraint)
1798
+ this.checkConstraintSatisfied(bindings.get(name), constraint, name, expr.line, expr.col);
1799
+ });
1800
+ const substitutedParams = info.params.map((p) => this.substituteTypeParams(p, bindings));
1801
+ expr.args.forEach((arg, i) => {
1802
+ const expected = substitutedParams[i];
1803
+ if (!this.isAssignableType(actualArgTypes[i], expected)) {
1804
+ this.diagnostics.error("KS4101", `Argument ${i + 1} has type '${T.typeToString(actualArgTypes[i])}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1805
+ }
1806
+ });
1807
+ return this.substituteTypeParams(info.returnType, bindings);
1808
+ }
1590
1809
  checkNew(expr, scope, ctx) {
1591
1810
  if (this.interfaces.has(expr.className)) {
1592
1811
  this.diagnostics.error("KS4066", `Cannot instantiate interface '${expr.className}'`, expr.line, expr.col);
@@ -1626,6 +1845,12 @@ export class Checker {
1626
1845
  return T.UNKNOWN;
1627
1846
  }
1628
1847
  typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
1848
+ const constraints = this.genericTypeParamConstraints.get(expr.className);
1849
+ typeArgs.forEach((arg, i) => {
1850
+ const constraint = constraints?.[i];
1851
+ if (constraint)
1852
+ this.checkConstraintSatisfied(arg, constraint, info.typeParams[i], expr.line, expr.col);
1853
+ });
1629
1854
  }
1630
1855
  this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
1631
1856
  // The constructor being called may be inherited from a generic
@@ -1851,6 +2076,22 @@ export class Checker {
1851
2076
  this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1852
2077
  return { type: T.UNKNOWN, methodInfo: null };
1853
2078
  }
2079
+ // A bare type-parameter receiver (`this.value.Foo()` where `value: T`)
2080
+ // — resolves against the constraint interface's own signature if `T`
2081
+ // is constrained; an unconstrained `T` falls through to the generic
2082
+ // "cannot access member" error below, unchanged from before constraints
2083
+ // existed (see README/LLM.md's own documented restriction).
2084
+ if (objectType.kind === "typeParam") {
2085
+ const constraintName = this.activeConstraints.get(objectType.name);
2086
+ if (constraintName) {
2087
+ const sig = this.collectInterfaceMethods(constraintName).find((m) => m.name === expr.property);
2088
+ if (sig) {
2089
+ return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
2090
+ }
2091
+ this.diagnostics.error("KS4097", `Interface '${constraintName}' (the constraint on type parameter '${objectType.name}') has no member '${expr.property}'`, expr.line, expr.col);
2092
+ return { type: T.UNKNOWN, methodInfo: null };
2093
+ }
2094
+ }
1854
2095
  if (objectType.kind !== "unknown") {
1855
2096
  this.diagnostics.error("KS4085", `Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
1856
2097
  }
package/dist/parser.js CHANGED
@@ -198,14 +198,24 @@ export class Parser {
198
198
  const type = this.parseType();
199
199
  const nameTok = this.consume(TokenKind.Identifier, "Expected name");
200
200
  const name = nameTok.lexeme;
201
+ // `T Identity<T>(T x) { ... }` — a free function's own type-param list,
202
+ // between its name and `(`. Only meaningful for the function branch
203
+ // below; a local/top-level variable declaration never has `<` right
204
+ // after its name, so trying this first costs nothing in that case
205
+ // (parseTypeParamList returns immediately without consuming anything
206
+ // when `<` isn't there).
207
+ const typeParams = this.parseTypeParamList();
201
208
  if (this.check(TokenKind.LParen)) {
202
209
  const params = this.parseParamList();
203
210
  const body = this.parseBlock();
204
- return { kind: "FunctionDecl", isExported, isAsync, name, params, returnType: type, body, line: start.line, col: start.col };
211
+ return { kind: "FunctionDecl", isExported, isAsync, name, typeParams, params, returnType: type, body, line: start.line, col: start.col };
205
212
  }
206
213
  if (isAsync) {
207
214
  this.diagnostics.error("KS2006", "'async' cannot modify a variable declaration", start.line, start.col);
208
215
  }
216
+ if (typeParams.length > 0) {
217
+ this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", start.line, start.col);
218
+ }
209
219
  this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
210
220
  const init = this.parseExpression();
211
221
  this.consume(TokenKind.Semicolon, "Expected ';' after variable declaration");
@@ -243,17 +253,21 @@ export class Parser {
243
253
  this.consume(TokenKind.RParen, "Expected ')' after parameters");
244
254
  return params;
245
255
  }
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).
256
+ // `<K : IHashable, V>` right after a class/interface/function name — one
257
+ // or more type parameters, comma-separated, each with an optional
258
+ // `: InterfaceName` constraint. Empty array if absent, the overwhelmingly
259
+ // common case.
250
260
  parseTypeParamList() {
251
261
  if (!this.match(TokenKind.Lt))
252
262
  return [];
253
263
  const params = [];
254
264
  do {
255
265
  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 });
266
+ let constraint = null;
267
+ if (this.match(TokenKind.Colon)) {
268
+ constraint = this.consume(TokenKind.Identifier, "Expected a constraint interface name after ':'").lexeme;
269
+ }
270
+ params.push({ name: nameTok.lexeme, constraint, line: nameTok.line, col: nameTok.col });
257
271
  } while (this.match(TokenKind.Comma));
258
272
  this.consume(TokenKind.Gt, "Expected '>' after type parameter list");
259
273
  return params;
package/dist/printer.js CHANGED
@@ -121,6 +121,12 @@ export class Printer {
121
121
  printBaseListEntry(entry) {
122
122
  return entry.typeArgs ? `${entry.name}<${entry.typeArgs.map((t) => this.printType(t)).join(", ")}>` : entry.name;
123
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
+ }
124
130
  // ---------- top-level declarations ----------
125
131
  printStatement(stmt, indent) {
126
132
  const pad = indentStr(indent);
@@ -202,7 +208,8 @@ export class Printer {
202
208
  printFunction(decl, indent) {
203
209
  const pad = indentStr(indent);
204
210
  const prefix = `${decl.isExported ? "" : "private "}${decl.isAsync ? "async " : ""}`;
205
- return `${pad}${prefix}${this.printType(decl.returnType)} ${decl.name}(${this.printParams(decl.params)}) ${this.printBlock(decl.body, indent).trimStart()}`;
211
+ const nameWithTypeParam = this.printTypeParamName(decl);
212
+ return `${pad}${prefix}${this.printType(decl.returnType)} ${nameWithTypeParam}(${this.printParams(decl.params)}) ${this.printBlock(decl.body, indent).trimStart()}`;
206
213
  }
207
214
  // Canonical member order: fields, then properties, then the constructor,
208
215
  // then methods — regardless of how the original source interleaved them.
@@ -215,7 +222,7 @@ export class Printer {
215
222
  printClass(decl, indent) {
216
223
  const pad = indentStr(indent);
217
224
  const memberPad = indentStr(indent + 1);
218
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
225
+ const nameWithTypeParam = this.printTypeParamName(decl);
219
226
  const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `class ${nameWithTypeParam} {`;
220
227
  const prefix = decl.isExported ? "" : "private ";
221
228
  const memberParts = [];
@@ -253,7 +260,7 @@ export class Printer {
253
260
  printInterface(decl, indent) {
254
261
  const pad = indentStr(indent);
255
262
  const memberPad = indentStr(indent + 1);
256
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
263
+ const nameWithTypeParam = this.printTypeParamName(decl);
257
264
  const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `interface ${nameWithTypeParam} {`;
258
265
  const prefix = decl.isExported ? "" : "private ";
259
266
  if (decl.methods.length === 0)
@@ -282,7 +289,7 @@ export class Printer {
282
289
  const pad = indentStr(indent);
283
290
  const memberPad = indentStr(indent + 1);
284
291
  const prefix = decl.isExported ? "" : "private ";
285
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
292
+ const nameWithTypeParam = this.printTypeParamName(decl);
286
293
  const lines = [];
287
294
  if (decl.hasConstructor)
288
295
  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.13.0",
3
+ "version": "0.15.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",