kopscript 0.13.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,10 +323,30 @@ 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.
329
350
 
330
351
  **Generic inheritance**: a class/interface base list CAN name a generic base, with its own
331
352
  type arguments, in either direction:
@@ -349,8 +370,6 @@ though either half alone would work.
349
370
  - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
350
371
  take type parameters, not free functions/methods themselves (a method *inside* a
351
372
  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).
354
373
  - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
355
374
 
356
375
  ## Nullable types — `T?`
@@ -428,13 +447,14 @@ KopScript-declared name exactly. Extern class members use real JS member names v
428
447
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
429
448
  declarations — each stands alone.
430
449
 
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.
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.
438
458
 
439
459
  **Never write `async` on an extern function/method signature** — declare its return type
440
460
  as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
@@ -521,10 +541,10 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
521
541
 
522
542
  ## Does not exist (don't reach for these)
523
543
 
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 ·
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 ·
528
548
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
529
549
  reflection · type inference on declarations · ternary expression · union/tuple types ·
530
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,17 +255,33 @@ 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
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
+
267
285
  A class or interface *can* extend/implement a generic base, type arguments and all:
268
286
 
269
287
  ```ks
@@ -383,11 +401,12 @@ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...
383
401
  proving `extern` works as a real cross-*package* boundary, not just for describing DOM
384
402
  globals within a single project.
385
403
 
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:
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
409
+ instantiated generically, not just per concrete type:
391
410
 
392
411
  ```ks
393
412
  extern class Box<T> {
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,6 +229,7 @@ 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,
207
234
  superclassTypeArgs: null,
208
235
  interfaces: [],
@@ -394,6 +421,14 @@ export class Checker {
394
421
  else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
395
422
  this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
396
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
+ }
397
432
  type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
398
433
  return;
399
434
  }
@@ -415,8 +450,11 @@ export class Checker {
415
450
  if (typeParams.length === 0)
416
451
  return fn();
417
452
  const previous = typeParams.map((p) => this.namedTypes.get(p.name));
418
- for (const p of typeParams)
453
+ const previousConstraints = typeParams.map((p) => this.activeConstraints.get(p.name));
454
+ for (const p of typeParams) {
419
455
  this.namedTypes.set(p.name, "typeParam");
456
+ this.activeConstraints.set(p.name, p.constraint);
457
+ }
420
458
  try {
421
459
  return fn();
422
460
  }
@@ -427,6 +465,11 @@ export class Checker {
427
465
  this.namedTypes.delete(p.name);
428
466
  else
429
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);
430
473
  });
431
474
  }
432
475
  }
@@ -551,7 +594,14 @@ export class Checker {
551
594
  }
552
595
  return { methods, bases, baseTypeArgs };
553
596
  });
554
- this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, baseTypeArgs, methods });
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
+ });
555
605
  this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
556
606
  }
557
607
  checkInterfaceHierarchy(decl) {
@@ -677,6 +727,7 @@ export class Checker {
677
727
  this.classes.set(decl.name, {
678
728
  name: decl.name,
679
729
  typeParams: decl.typeParams.map((p) => p.name),
730
+ typeParamConstraints: decl.typeParams.map((p) => p.constraint),
680
731
  superclass,
681
732
  superclassTypeArgs,
682
733
  interfaces,
@@ -715,7 +766,15 @@ export class Checker {
715
766
  if (genericBaseCount.count > 1) {
716
767
  this.diagnostics.error("KS4095", `'${declName}' has more than one generic entry in its base list — v1 supports at most one`, declLine, declCol);
717
768
  }
718
- return typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
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;
719
778
  }
720
779
  checkClassHierarchy(decl) {
721
780
  const info = this.classes.get(decl.name);
@@ -846,6 +905,38 @@ export class Checker {
846
905
  }
847
906
  return false;
848
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
+ }
849
940
  lookupField(className, fieldName) {
850
941
  let current = className;
851
942
  while (current) {
@@ -1626,6 +1717,12 @@ export class Checker {
1626
1717
  return T.UNKNOWN;
1627
1718
  }
1628
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
+ });
1629
1726
  }
1630
1727
  this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
1631
1728
  // The constructor being called may be inherited from a generic
@@ -1851,6 +1948,22 @@ export class Checker {
1851
1948
  this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1852
1949
  return { type: T.UNKNOWN, methodInfo: null };
1853
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
+ }
1854
1967
  if (objectType.kind !== "unknown") {
1855
1968
  this.diagnostics.error("KS4085", `Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
1856
1969
  }
package/dist/parser.js CHANGED
@@ -243,17 +243,21 @@ 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;
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);
@@ -215,7 +221,7 @@ export class Printer {
215
221
  printClass(decl, indent) {
216
222
  const pad = indentStr(indent);
217
223
  const memberPad = indentStr(indent + 1);
218
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
224
+ const nameWithTypeParam = this.printTypeParamName(decl);
219
225
  const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `class ${nameWithTypeParam} {`;
220
226
  const prefix = decl.isExported ? "" : "private ";
221
227
  const memberParts = [];
@@ -253,7 +259,7 @@ export class Printer {
253
259
  printInterface(decl, indent) {
254
260
  const pad = indentStr(indent);
255
261
  const memberPad = indentStr(indent + 1);
256
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
262
+ const nameWithTypeParam = this.printTypeParamName(decl);
257
263
  const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.map((b) => this.printBaseListEntry(b)).join(", ")} {` : `interface ${nameWithTypeParam} {`;
258
264
  const prefix = decl.isExported ? "" : "private ";
259
265
  if (decl.methods.length === 0)
@@ -282,7 +288,7 @@ export class Printer {
282
288
  const pad = indentStr(indent);
283
289
  const memberPad = indentStr(indent + 1);
284
290
  const prefix = decl.isExported ? "" : "private ";
285
- const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
291
+ const nameWithTypeParam = this.printTypeParamName(decl);
286
292
  const lines = [];
287
293
  if (decl.hasConstructor)
288
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.13.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",