kopscript 0.11.0 → 0.12.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
@@ -133,8 +133,8 @@ Dog d = new Dog("Rex");
133
133
  the interface in the base list and provide matching public methods, checked at compile time.
134
134
  - No nested classes/functions.
135
135
  - No multiple inheritance (at most one class in the base list; the rest must be interfaces).
136
- - `class Box<T> { ... }` a single type parameter is allowed; see Generics below for the
137
- full (deliberately scoped-down) rules.
136
+ - `class Box<T> { ... }` / `class Pair<K, V> { ... }` one or more type parameters are
137
+ allowed; see Generics below for the full (deliberately scoped-down) rules.
138
138
 
139
139
  ### Interfaces
140
140
 
@@ -145,7 +145,7 @@ interface IShape {
145
145
  interface INamedShape : IShape { // interfaces can extend other interfaces (not classes)
146
146
  string GetName();
147
147
  }
148
- interface IContainer<T> { // interfaces take a single type parameter too — see Generics
148
+ interface IContainer<T> { // interfaces take type parameters too — see Generics
149
149
  T Get();
150
150
  }
151
151
  ```
@@ -281,7 +281,7 @@ Not a use of user-declared generics (see below) — `state<T>` is one of exactly
281
281
  hardcoded parametrized forms in the type system (the other is `task<T>`), each with only
282
282
  ever one built-in meaning, unlike a real generic class/interface.
283
283
 
284
- ## Generics — a single type parameter on classes/interfaces
284
+ ## Generics — one or more type parameters on classes/interfaces
285
285
 
286
286
  ```ks
287
287
  class Box<T> {
@@ -293,34 +293,46 @@ class Box<T> {
293
293
  Box<number> nb = new Box<number>(5); // type argument required on both the type AND `new`
294
294
  Box<string> sb = new Box<string>("hi");
295
295
  print(nb.Get()); // 5
296
+
297
+ class Pair<K, V> {
298
+ public K First;
299
+ public V Second;
300
+ constructor(K k, V v) { this.First = k; this.Second = v; }
301
+ }
302
+
303
+ Pair<number, string> p = new Pair<number, string>(1, "a");
296
304
  ```
297
305
 
298
- One unconstrained, invariant type parameter per class/interface (real *or* `extern` — see
299
- the `extern` section below) the whole feature.
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.
300
309
  Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
301
310
  compile to the exact same plain `class Box`), so there's no runtime cost and no way to
302
- inspect `T` at runtime.
311
+ inspect a type parameter at runtime.
303
312
 
304
313
  **Rules:**
305
- - The type argument is **required everywhere** a generic type is named — on the variable
306
- type (`Box<number>`) and separately on `new` (`new Box<number>(...)`); a bare `Box` (no
307
- argument) is a compile error, not "any"/inferred.
308
- - **Invariant**: `Box<Dog>` is not assignable to `Box<Animal>` even if `Dog : Animal` —
309
- type arguments must match exactly, not just be compatible.
310
- - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`.
311
- - Inside the class/interface's own body, `T` is fully abstract you can store it, return
312
- it, pass it around, but (unconstrained) you **cannot call any member on a bare `T`
313
- value** (`this.value.Foo()` is a compile error: "Cannot access member 'Foo' on type
314
- 'T'"). This is correct, not a bug — same as an unconstrained type parameter in
315
- C#/Java/TypeScript.
314
+ - The type argument(s) are **required everywhere** a generic type is named — on the
315
+ variable type (`Box<number>`) and separately on `new` (`new Box<number>(...)`); a bare
316
+ `Box`/`Pair` (no arguments) is a compile error, not "any"/inferred. The count must match
317
+ the declaration exactly — `Pair<number>` (too few) and `Pair<number, string, bool>` (too
318
+ many) are both arity errors, not truncated/padded.
319
+ - **Invariant, per slot**: `Box<Dog>` is not assignable to `Box<Animal>` even if `Dog :
320
+ Animal`, and `Pair<number, string>` is not assignable to `Pair<string, number>` every
321
+ type argument must match exactly, in the same position, not just be compatible.
322
+ - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`,
323
+ `Pair<number, Pair<string, bool>>`.
324
+ - 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.
316
329
 
317
330
  **Does not exist (v1 scope cuts, each deliberate)**:
318
331
  - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
319
- take a type parameter, not free functions/methods themselves (a method *inside* a
320
- generic class can use that class's own `T` freely, same as any other member).
321
- - **Multiple type parameters.** No `Map<K, V>` — exactly one `<T>` or none.
332
+ take type parameters, not free functions/methods themselves (a method *inside* a
333
+ generic class can use that class's own type parameters freely, same as any other member).
322
334
  - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
323
- which is why you can't call members on a bare `T` (see above).
335
+ which is why you can't call members on a bare one (see above).
324
336
  - **Generic inheritance.** A class/interface's base list can only name a *non-generic*
325
337
  type — `class Foo : Box<number>` and even `class Foo<T> : SomeGenericBase<T>` are both
326
338
  compile errors ("cannot extend/implement generic type '...' — not supported in v1"). A
@@ -493,9 +505,9 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
493
505
 
494
506
  ## Does not exist (don't reach for these)
495
507
 
496
- Generics beyond a single unconstrained type parameter (no `Map<K, V>`, no `T : IFoo`
497
- constraints, no generic functions, no generic inheritance, no variance — see Generics
498
- above for what *is* supported) · `any`/`unknown` annotations ·
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 ·
499
511
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
500
512
  reflection · type inference on declarations · ternary expression · union/tuple types ·
501
513
  **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
package/README.md CHANGED
@@ -22,9 +22,9 @@ 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**: a single, unconstrained, invariant type parameter on classes and
26
- interfaces (`class Box<T> { public T Value; }`) — erased at codegen with zero runtime
27
- cost, the same way `task<T>`/`state<T>` already are.
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.
28
28
  - **Nullable types with compiler-enforced null-checking**: `T?` (`string?`, `Dog?`,
29
29
  `number[]?`) — a `T?` can't be used where a `T` is expected without an `if (x != null)`
30
30
  check first (checked statically, not just at runtime), and comparing a value that can
@@ -221,7 +221,7 @@ void Announce(ISpeaker s) {
221
221
 
222
222
  ### Generics
223
223
 
224
- Classes and interfaces can take a single type parameter:
224
+ Classes and interfaces can take one or more type parameters:
225
225
 
226
226
  ```ks
227
227
  class Box<T> {
@@ -233,34 +233,45 @@ class Box<T> {
233
233
  Box<number> nb = new Box<number>(5);
234
234
  Box<string> sb = new Box<string>("hi");
235
235
  print(nb.Get()); // 5
236
+
237
+ class Pair<K, V> {
238
+ public K First;
239
+ public V Second;
240
+ constructor(K k, V v) { this.First = k; this.Second = v; }
241
+ }
242
+
243
+ Pair<number, string> p = new Pair<number, string>(1, "a");
236
244
  ```
237
245
 
238
- The type argument is required everywhere the generic type is named — on the variable's
246
+ The type argument(s) are required everywhere the generic type is named — on the variable's
239
247
  declared type *and* separately on `new` (`Box<number> nb = new Box<number>(5);`, not
240
- just one or the other). Generics are erased at codegen exactly like `task<T>`/`state<T>`
241
- already are `Box<number>` and `Box<string>` compile to the identical plain `class Box`,
242
- so there's no runtime cost.
243
-
244
- This is deliberately the smallest useful slice of generics, not a scaled-down promise of
245
- more later inside v1:
246
-
247
- - **One type parameter, invariant, unconstrained.** No `Map<K, V>` (multiple parameters),
248
- no `T : ISomething` (constraints) — and because `T` is fully unconstrained, you can't
249
- call any member on a bare `T` value inside the generic class's own body (that's correct
250
- behavior, the same restriction C#/Java/TypeScript put on an unconstrained parameter, not
251
- a bug). Invariant means `Box<Dog>` is **not** assignable to `Box<Animal>` even though
252
- `Dog : Animal` type arguments must match exactly.
248
+ just one or the other) and the count must match the declaration exactly (`Pair<number>`
249
+ alone, or `Pair<number, string, bool>`, are both arity errors). Generics are erased at
250
+ codegen exactly like `task<T>`/`state<T>` already are — `Box<number>` and `Box<string>`
251
+ compile to the identical plain `class Box`, so there's no runtime cost.
252
+
253
+ Still a deliberately small slice of generics, not a scaled-down promise of more later
254
+ inside v1:
255
+
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.
253
263
  - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
254
- interfaces take a type parameter. A method *inside* a generic class can still use that
255
- class's own `T` freely; it's just not introducing a type parameter of its own.
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.
256
266
  - **No generic inheritance.** A class or interface's base list can only name a
257
267
  *non-generic* type. `class Foo : Box<number>` and `class Foo<T> : SomeBase<T>` are both
258
268
  compile errors — a generic class can still extend/implement ordinary non-generic bases
259
269
  normally, it just can't be the one on either side of a generic base relationship.
260
270
 
261
271
  See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
262
- against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`) and generic
263
- interfaces used as standalone parameter types both work and are covered there.
272
+ against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`, `Pair<number,
273
+ Pair<string, bool>>`) and generic interfaces used as standalone parameter types both work
274
+ and are covered there.
264
275
 
265
276
  ### Enums
266
277
 
@@ -359,11 +370,11 @@ consumes `Component`/`Router` themselves via `extern class ... from "kopular/...
359
370
  proving `extern` works as a real cross-*package* boundary, not just for describing DOM
360
371
  globals within a single project.
361
372
 
362
- An `extern class` can carry its own type parameter, `extern class Box<T> { ... }`
363
- exactly the same single-type-parameter rules as a real generic class (see "Generics"
364
- above: invariant, unconstrained, erased, can't appear in a base list), so a generic type
365
- from another package (e.g. Kopular's `FormField<T>`) can be described and instantiated
366
- generically, not just per concrete type:
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
377
+ instantiated generically, not just per concrete type:
367
378
 
368
379
  ```ks
369
380
  extern class Box<T> {
package/dist/checker.js CHANGED
@@ -56,11 +56,13 @@ export class Checker {
56
56
  this.functions = new Map();
57
57
  this.externValues = new Map();
58
58
  this.namedTypes = new Map();
59
- // className/interfaceName -> its type parameter's name (e.g. "Box" -> "T"),
60
- // for every generic class/interface — populated up front, before any type
61
- // resolution runs, specifically so a forward reference (class A's field
62
- // referencing generic class B, declared later in the same file) still
63
- // resolves B's genericity correctly. See resolveType/validateGenericArity.
59
+ // className/interfaceName -> its type parameters' names, in declared order
60
+ // (e.g. "Pair" -> ["K", "V"]), for every generic class/interface —
61
+ // populated up front, before any type resolution runs, specifically so a
62
+ // forward reference (class A's field referencing generic class B, declared
63
+ // later in the same file) still resolves B's genericity correctly. Also
64
+ // doubles as the arity table (its length is how many type arguments a
65
+ // reference to that name must supply). See resolveType/validateGenericArity.
64
66
  this.genericTypeParams = new Map();
65
67
  this.importedNames = new Set();
66
68
  this.rawContents = new Map();
@@ -73,13 +75,13 @@ export class Checker {
73
75
  }
74
76
  for (const [name, info] of this.imports.classes) {
75
77
  this.classes.set(name, info);
76
- if (info.typeParam)
77
- this.genericTypeParams.set(name, info.typeParam);
78
+ if (info.typeParams.length > 0)
79
+ this.genericTypeParams.set(name, info.typeParams);
78
80
  }
79
81
  for (const [name, info] of this.imports.interfaces) {
80
82
  this.interfaces.set(name, info);
81
- if (info.typeParam)
82
- this.genericTypeParams.set(name, info.typeParam);
83
+ if (info.typeParams.length > 0)
84
+ this.genericTypeParams.set(name, info.typeParams);
83
85
  }
84
86
  for (const [name, info] of this.imports.enums)
85
87
  this.enums.set(name, info);
@@ -109,14 +111,14 @@ export class Checker {
109
111
  for (const c of externClassDecls)
110
112
  this.namedTypes.set(c.name, "class");
111
113
  for (const c of classDecls)
112
- if (c.typeParam)
113
- this.genericTypeParams.set(c.name, c.typeParam);
114
+ if (c.typeParams.length > 0)
115
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
114
116
  for (const i of interfaceDecls)
115
- if (i.typeParam)
116
- this.genericTypeParams.set(i.name, i.typeParam);
117
+ if (i.typeParams.length > 0)
118
+ this.genericTypeParams.set(i.name, i.typeParams.map((p) => p.name));
117
119
  for (const c of externClassDecls)
118
- if (c.typeParam)
119
- this.genericTypeParams.set(c.name, c.typeParam);
120
+ if (c.typeParams.length > 0)
121
+ this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
120
122
  for (const e of enumDecls)
121
123
  this.registerEnum(e);
122
124
  for (const i of interfaceDecls)
@@ -172,11 +174,11 @@ export class Checker {
172
174
  });
173
175
  }
174
176
  registerExternClass(decl) {
175
- // Same withTypeParamInScope treatment registerClass gives a real class's
176
- // signature (fields/methods/ctor params) — an extern class's own `T`
177
- // needs to resolve the same way while its declared members are being
178
- // resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
179
- const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
177
+ // Same withTypeParamsInScope treatment registerClass gives a real class's
178
+ // signature (fields/methods/ctor params) — an extern class's own type
179
+ // params need to resolve the same way while its declared members are
180
+ // being resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
181
+ const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamsInScope(decl.typeParams, () => {
180
182
  const fields = new Map();
181
183
  const staticFields = new Map();
182
184
  for (const p of decl.properties) {
@@ -200,7 +202,7 @@ export class Checker {
200
202
  });
201
203
  this.classes.set(decl.name, {
202
204
  name: decl.name,
203
- typeParam: decl.typeParam,
205
+ typeParams: decl.typeParams.map((p) => p.name),
204
206
  superclass: null,
205
207
  interfaces: [],
206
208
  fields,
@@ -341,8 +343,9 @@ export class Checker {
341
343
  });
342
344
  if (!resolved) {
343
345
  const name = node.kind === "NamedType" ? node.name : "[]";
344
- if (name !== "[]" && this.genericTypeParams.has(name) && !node.typeArgs) {
345
- this.diagnostics.error("KS4007", `Generic type '${name}' requires a type argument (e.g. '${name}<T>')`, line, col);
346
+ const declaredParams = this.genericTypeParams.get(name);
347
+ if (name !== "[]" && declaredParams && !node.typeArgs) {
348
+ this.diagnostics.error("KS4007", `Generic type '${name}' requires ${this.describeArity(declaredParams)} (e.g. '${name}<${declaredParams.join(", ")}>')`, line, col);
346
349
  }
347
350
  else {
348
351
  this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col);
@@ -378,70 +381,80 @@ export class Checker {
378
381
  return;
379
382
  case "class":
380
383
  case "interface": {
381
- const isGeneric = this.genericTypeParams.has(type.name);
382
- if (isGeneric && !type.typeArg) {
383
- this.diagnostics.error("KS4009", `Generic type '${type.name}' requires a type argument (e.g. '${type.name}<T>')`, line, col);
384
+ const declaredParams = this.genericTypeParams.get(type.name);
385
+ const suppliedCount = type.typeArgs?.length ?? 0;
386
+ if (declaredParams && suppliedCount === 0) {
387
+ this.diagnostics.error("KS4009", `Generic type '${type.name}' requires ${this.describeArity(declaredParams)} (e.g. '${type.name}<${declaredParams.join(", ")}>')`, line, col);
384
388
  }
385
- else if (!isGeneric && type.typeArg) {
389
+ else if (!declaredParams && suppliedCount > 0) {
386
390
  this.diagnostics.error("KS4010", `Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
387
391
  }
388
- if (type.typeArg)
389
- this.validateGenericArity(type.typeArg, line, col);
392
+ else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
393
+ this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
394
+ }
395
+ type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
390
396
  return;
391
397
  }
392
398
  default:
393
399
  return;
394
400
  }
395
401
  }
396
- // Temporarily makes `name` (a class/interface's own type parameter, e.g.
397
- // "T") resolve as a TypeParamType, for the duration of `fn` — used only
398
- // while registering that class/interface's own declaration (fields,
399
- // methods, ctor params), so `T Value;` resolves correctly. Scoped this
400
- // narrowly (set, run, delete/restore) rather than left in `namedTypes`
401
- // permanently, since it's only ever meaningful inside that one
402
- // declaration's own body.
403
- withTypeParamInScope(typeParam, fn) {
404
- if (!typeParam)
402
+ describeArity(params) {
403
+ return params.length === 1 ? "a type argument" : `${params.length} type arguments`;
404
+ }
405
+ // Temporarily makes each of `typeParams` (a class/interface/function's own
406
+ // type parameters, e.g. ["K", "V"]) resolve as a TypeParamType, for the
407
+ // duration of `fn` — used only while registering that declaration's own
408
+ // signature (fields, methods, ctor/function params), so `T Value;`/`K
409
+ // key;` resolve correctly. Scoped this narrowly (set, run, delete/
410
+ // restore) rather than left in `namedTypes` permanently, since it's only
411
+ // ever meaningful inside that one declaration's own body.
412
+ withTypeParamsInScope(typeParams, fn) {
413
+ if (typeParams.length === 0)
405
414
  return fn();
406
- const previous = this.namedTypes.get(typeParam);
407
- this.namedTypes.set(typeParam, "typeParam");
415
+ const previous = typeParams.map((p) => this.namedTypes.get(p.name));
416
+ for (const p of typeParams)
417
+ this.namedTypes.set(p.name, "typeParam");
408
418
  try {
409
419
  return fn();
410
420
  }
411
421
  finally {
412
- if (previous === undefined)
413
- this.namedTypes.delete(typeParam);
414
- else
415
- this.namedTypes.set(typeParam, previous);
416
- }
417
- }
418
- // Replaces every occurrence of the class/interface's own type parameter
419
- // (by name) inside `type` with `arg` — the substitution step that turns
420
- // Box<T>'s abstractly-stored field type `T` into `number` when someone
421
- // actually asks about `Box<number>.Value`. A type with no occurrence of
422
- // `paramName` anywhere in it is returned unchanged (including every
423
- // non-generic type, the overwhelming majority).
424
- substituteTypeParam(type, paramName, arg) {
422
+ typeParams.forEach((p, i) => {
423
+ const prev = previous[i];
424
+ if (prev === undefined)
425
+ this.namedTypes.delete(p.name);
426
+ else
427
+ this.namedTypes.set(p.name, prev);
428
+ });
429
+ }
430
+ }
431
+ // Replaces every occurrence of a type parameter named in `bindings`
432
+ // (mapping name -> concrete Type) inside `type` the substitution step
433
+ // that turns Pair<K, V>'s abstractly-stored field type `K` into `number`
434
+ // when someone actually asks about `Pair<number, string>.First`. A type
435
+ // with none of `bindings`' names occurring anywhere in it is returned
436
+ // unchanged (including every non-generic type, the overwhelming majority).
437
+ substituteTypeParams(type, bindings) {
425
438
  switch (type.kind) {
426
439
  case "typeParam":
427
- return type.name === paramName ? arg : type;
440
+ return bindings.get(type.name) ?? type;
428
441
  case "array":
429
- return T.arrayOf(this.substituteTypeParam(type.element, paramName, arg));
442
+ return T.arrayOf(this.substituteTypeParams(type.element, bindings));
430
443
  case "nullable":
431
- return T.nullableOf(this.substituteTypeParam(type.inner, paramName, arg));
444
+ return T.nullableOf(this.substituteTypeParams(type.inner, bindings));
432
445
  case "task":
433
- return T.taskType(this.substituteTypeParam(type.resultType, paramName, arg));
446
+ return T.taskType(this.substituteTypeParams(type.resultType, bindings));
434
447
  case "state":
435
- return T.stateType(this.substituteTypeParam(type.valueType, paramName, arg));
448
+ return T.stateType(this.substituteTypeParams(type.valueType, bindings));
436
449
  case "function":
437
- return T.functionType(type.params.map((p) => this.substituteTypeParam(p, paramName, arg)), this.substituteTypeParam(type.returnType, paramName, arg));
438
- // A nested generic's own type argument can itself mention the outer
439
- // T (`Box<T[]>`'s field being List<T> — a v1 corner case, but cheap
440
- // to handle correctly): substitute inside it too.
450
+ return T.functionType(type.params.map((p) => this.substituteTypeParams(p, bindings)), this.substituteTypeParams(type.returnType, bindings));
451
+ // A nested generic's own type arguments can themselves mention an
452
+ // outer type param (`Pair<K, V>`'s field being List<K> — a v1 corner
453
+ // case, but cheap to handle correctly): substitute inside each.
441
454
  case "class":
442
- return type.typeArg ? T.classType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
455
+ return type.typeArgs ? T.classType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
443
456
  case "interface":
444
- return type.typeArg ? T.interfaceType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
457
+ return type.typeArgs ? T.interfaceType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
445
458
  default:
446
459
  return type;
447
460
  }
@@ -459,7 +472,7 @@ export class Checker {
459
472
  this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
460
473
  }
461
474
  registerInterface(decl) {
462
- const methods = this.withTypeParamInScope(decl.typeParam, () => decl.methods.map((m) => {
475
+ const methods = this.withTypeParamsInScope(decl.typeParams, () => decl.methods.map((m) => {
463
476
  const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
464
477
  const returnType = this.resolveType(m.returnType, m.line, m.col);
465
478
  this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
@@ -480,8 +493,8 @@ export class Checker {
480
493
  }
481
494
  bases.push(baseName);
482
495
  }
483
- this.interfaces.set(decl.name, { name: decl.name, typeParam: decl.typeParam, bases, methods });
484
- this.recordHover(decl.line, decl.col, decl.typeParam ? `interface ${decl.name}<${decl.typeParam}>` : `interface ${decl.name}`);
496
+ this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, methods });
497
+ this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
485
498
  }
486
499
  checkInterfaceHierarchy(decl) {
487
500
  const info = this.interfaces.get(decl.name);
@@ -524,8 +537,8 @@ export class Checker {
524
537
  return info.bases.some((b) => this.interfaceExtends(b, sup));
525
538
  }
526
539
  registerClass(decl) {
527
- this.recordHover(decl.line, decl.col, decl.typeParam ? `class ${decl.name}<${decl.typeParam}>` : `class ${decl.name}`);
528
- const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
540
+ 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, () => {
529
542
  const fields = new Map();
530
543
  const staticFields = new Map();
531
544
  for (const f of decl.fields) {
@@ -584,7 +597,7 @@ export class Checker {
584
597
  }
585
598
  this.classes.set(decl.name, {
586
599
  name: decl.name,
587
- typeParam: decl.typeParam,
600
+ typeParams: decl.typeParams.map((p) => p.name),
588
601
  superclass,
589
602
  interfaces,
590
603
  fields,
@@ -825,11 +838,11 @@ export class Checker {
825
838
  // only ever arises from an arity error already diagnosed at the
826
839
  // reference site — see validateGenericArity).
827
840
  typeArgsMatch(a, b) {
828
- if (!a.typeArg && !b.typeArg)
841
+ if (!a.typeArgs && !b.typeArgs)
829
842
  return true;
830
- if (!a.typeArg || !b.typeArg)
843
+ if (!a.typeArgs || !b.typeArgs || a.typeArgs.length !== b.typeArgs.length)
831
844
  return false;
832
- return T.typesEqual(a.typeArg, b.typeArg);
845
+ return a.typeArgs.every((t, i) => T.typesEqual(t, b.typeArgs[i]));
833
846
  }
834
847
  // ---------- top-level ----------
835
848
  // Validates that `isAsync` and the declared return type agree — `async`
@@ -867,12 +880,12 @@ export class Checker {
867
880
  }
868
881
  checkClassBody(decl) {
869
882
  // Constructor/method *bodies* run in this same scope registerClass used
870
- // for the *signatures* (see withTypeParamInScope) — without it, `T`
883
+ // for the *signatures* (see withTypeParamsInScope) — without it, `T`
871
884
  // resolves everywhere in a generic class's declared field/param/return
872
885
  // types but not inside a method body itself (a local `T x = ...;`, or a
873
886
  // lambda parameter typed `T`), which would make the type parameter
874
887
  // usable only at the class's boundary and not inside its own logic.
875
- this.withTypeParamInScope(decl.typeParam, () => this.checkClassBodyInner(decl));
888
+ this.withTypeParamsInScope(decl.typeParams, () => this.checkClassBodyInner(decl));
876
889
  }
877
890
  checkClassBodyInner(decl) {
878
891
  const info = this.classes.get(decl.name);
@@ -1461,30 +1474,34 @@ export class Checker {
1461
1474
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1462
1475
  return T.UNKNOWN;
1463
1476
  }
1464
- let typeArg;
1465
- if (info.typeParam && !expr.typeArgs) {
1466
- this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
1477
+ let typeArgs;
1478
+ if (info.typeParams.length > 0 && !expr.typeArgs) {
1479
+ this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires ${this.describeArity(info.typeParams)} (e.g. 'new ${expr.className}<${info.typeParams.join(", ")}>(...)')`, expr.line, expr.col);
1467
1480
  // Abstractly-typed (T-containing) ctor params, un-substitutable
1468
- // without a real type argument, would otherwise cascade into a
1481
+ // without real type arguments, would otherwise cascade into a
1469
1482
  // confusing "expected 'T'" error on every argument — one clear error
1470
1483
  // beats that pile-on.
1471
1484
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1472
1485
  return T.UNKNOWN;
1473
1486
  }
1474
- if (!info.typeParam && expr.typeArgs) {
1487
+ if (info.typeParams.length === 0 && expr.typeArgs) {
1475
1488
  this.diagnostics.error("KS4070", `Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
1476
1489
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1477
1490
  return T.UNKNOWN;
1478
1491
  }
1479
- if (info.typeParam && expr.typeArgs) {
1480
- typeArg = this.resolveType(expr.typeArgs[0], expr.line, expr.col);
1492
+ if (info.typeParams.length > 0 && expr.typeArgs) {
1493
+ if (expr.typeArgs.length !== info.typeParams.length) {
1494
+ this.diagnostics.error("KS4091", `Generic class '${expr.className}' expects ${this.describeArity(info.typeParams)}, got ${expr.typeArgs.length}`, expr.line, expr.col);
1495
+ expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1496
+ return T.UNKNOWN;
1497
+ }
1498
+ typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
1481
1499
  }
1482
- this.recordHover(expr.line, expr.col, typeArg ? `class ${expr.className}<${T.typeToString(typeArg)}>` : `class ${expr.className}`);
1500
+ this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
1483
1501
  let ctorParams = this.lookupCtorParams(expr.className);
1484
- if (info.typeParam && typeArg) {
1485
- const paramName = info.typeParam;
1486
- const arg = typeArg;
1487
- ctorParams = ctorParams.map((p) => this.substituteTypeParam(p, paramName, arg));
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));
1488
1505
  }
1489
1506
  if (expr.args.length !== ctorParams.length) {
1490
1507
  this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
@@ -1496,7 +1513,7 @@ export class Checker {
1496
1513
  this.diagnostics.error("KS4072", `Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1497
1514
  }
1498
1515
  });
1499
- return T.classType(expr.className, typeArg);
1516
+ return T.classType(expr.className, typeArgs);
1500
1517
  }
1501
1518
  lookupCtorParams(className) {
1502
1519
  let current = className;
@@ -1649,9 +1666,10 @@ export class Checker {
1649
1666
  // v1 has no generic inheritance (a generic class's base list can only
1650
1667
  // name non-generic types), so a member found on a generic instance
1651
1668
  // was always declared directly on that same class — substituting by
1652
- // its own type parameter, not some ancestor's, is always correct.
1653
- const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1654
- const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
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);
1655
1673
  const field = this.lookupField(objectType.name, expr.property);
1656
1674
  if (field) {
1657
1675
  this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
@@ -1666,7 +1684,7 @@ export class Checker {
1666
1684
  const method = this.lookupMethod(objectType.name, expr.property);
1667
1685
  if (method) {
1668
1686
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
1669
- const info = paramName ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
1687
+ const info = bindings ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
1670
1688
  return { type: info.returnType, methodInfo: info };
1671
1689
  }
1672
1690
  this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
@@ -1678,8 +1696,9 @@ export class Checker {
1678
1696
  // v1 has no generic interface inheritance either (same restriction
1679
1697
  // as classes — see registerInterface), so a signature found here
1680
1698
  // was always declared directly on this same interface.
1681
- const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1682
- const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
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);
1683
1702
  const params = sig.params.map(substitute);
1684
1703
  const returnType = substitute(sig.returnType);
1685
1704
  return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
package/dist/parser.js CHANGED
@@ -243,20 +243,25 @@ export class Parser {
243
243
  this.consume(TokenKind.RParen, "Expected ')' after parameters");
244
244
  return params;
245
245
  }
246
- // `<T>` right after a class/interface name — a single, unconstrained type
247
- // parameter (v1 has no `Map<K, V>`, no `T : IFoo` constraints). Null if
248
- // absent, the overwhelmingly common case.
249
- parseOptionalTypeParam() {
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).
250
+ parseTypeParamList() {
250
251
  if (!this.match(TokenKind.Lt))
251
- return null;
252
- const name = this.consume(TokenKind.Identifier, "Expected a type parameter name").lexeme;
253
- this.consume(TokenKind.Gt, "Expected '>' after type parameter");
254
- return name;
252
+ return [];
253
+ const params = [];
254
+ do {
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 });
257
+ } while (this.match(TokenKind.Comma));
258
+ this.consume(TokenKind.Gt, "Expected '>' after type parameter list");
259
+ return params;
255
260
  }
256
261
  parseClassDecl(isExported) {
257
262
  const start = this.advance(); // 'class'
258
263
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
259
- const typeParam = this.parseOptionalTypeParam();
264
+ const typeParams = this.parseTypeParamList();
260
265
  const baseList = [];
261
266
  if (this.match(TokenKind.Colon)) {
262
267
  do {
@@ -412,12 +417,12 @@ export class Parser {
412
417
  }
413
418
  }
414
419
  this.consume(TokenKind.RBrace, "Expected '}' after class body");
415
- return { kind: "ClassDecl", isExported, name, typeParam, baseList, fields, properties, constructor: ctor, methods, template, line: start.line, col: start.col };
420
+ return { kind: "ClassDecl", isExported, name, typeParams, baseList, fields, properties, constructor: ctor, methods, template, line: start.line, col: start.col };
416
421
  }
417
422
  parseInterfaceDecl(isExported) {
418
423
  const start = this.advance(); // 'interface'
419
424
  const name = this.consume(TokenKind.Identifier, "Expected interface name").lexeme;
420
- const typeParam = this.parseOptionalTypeParam();
425
+ const typeParams = this.parseTypeParamList();
421
426
  const baseList = [];
422
427
  if (this.match(TokenKind.Colon)) {
423
428
  do {
@@ -443,7 +448,7 @@ export class Parser {
443
448
  });
444
449
  }
445
450
  this.consume(TokenKind.RBrace, "Expected '}' after interface body");
446
- return { kind: "InterfaceDecl", isExported, name, typeParam, baseList, methods, line: start.line, col: start.col };
451
+ return { kind: "InterfaceDecl", isExported, name, typeParams, baseList, methods, line: start.line, col: start.col };
447
452
  }
448
453
  parseEnumDecl(isExported) {
449
454
  const start = this.advance(); // 'enum'
@@ -510,7 +515,7 @@ export class Parser {
510
515
  }
511
516
  parseExternClassBody(start, isExported) {
512
517
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
513
- const typeParam = this.parseOptionalTypeParam();
518
+ const typeParams = this.parseTypeParamList();
514
519
  this.consume(TokenKind.LBrace, "Expected '{' before extern class body");
515
520
  let hasConstructor = false;
516
521
  let ctorParams = [];
@@ -575,7 +580,7 @@ export class Parser {
575
580
  this.consume(TokenKind.RBrace, "Expected '}' after extern class body");
576
581
  const { modulePath, jsName } = this.parseExternTail(name);
577
582
  this.consume(TokenKind.Semicolon, "Expected ';' after extern class declaration");
578
- return { kind: "ExternClassDecl", isExported, name, typeParam, jsName, hasConstructor, ctorParams, properties, methods, modulePath, line: start.line, col: start.col };
583
+ return { kind: "ExternClassDecl", isExported, name, typeParams, jsName, hasConstructor, ctorParams, properties, methods, modulePath, line: start.line, col: start.col };
579
584
  }
580
585
  parseBlock() {
581
586
  const start = this.consume(TokenKind.LBrace, "Expected '{'");
@@ -705,15 +710,18 @@ export class Parser {
705
710
  }
706
711
  else {
707
712
  const nameToken = this.check(TokenKind.Void) ? this.advance() : this.consume(TokenKind.Identifier, "Expected type name");
708
- // `Box<number>` — a generic type reference. At most one type argument
709
- // in v1 (matching ClassDecl/InterfaceDecl's single type parameter);
710
- // whether `name` actually refers to a declared generic type is a
713
+ // `Pair<number, string>` — a generic type reference, comma-separated
714
+ // type arguments. Whether `name` actually refers to a declared
715
+ // generic type (and whether the count matches its arity) is a
711
716
  // semantic question the checker answers, not the parser.
712
717
  let typeArgs = null;
713
718
  if (this.check(TokenKind.Lt)) {
714
719
  this.advance();
715
720
  typeArgs = [this.parseType()];
716
- this.consume(TokenKind.Gt, "Expected '>' after type argument");
721
+ while (this.match(TokenKind.Comma)) {
722
+ typeArgs.push(this.parseType());
723
+ }
724
+ this.consume(TokenKind.Gt, "Expected '>' after type argument list");
717
725
  }
718
726
  type = { kind: "NamedType", name: nameToken.lexeme, typeArgs, line: nameToken.line, col: nameToken.col };
719
727
  }
@@ -893,14 +901,18 @@ export class Parser {
893
901
  if (this.check(TokenKind.New)) {
894
902
  this.advance();
895
903
  const className = this.consume(TokenKind.Identifier, "Expected class name after 'new'").lexeme;
896
- // `new Box<number>(...)` — unambiguous here: `new <Identifier>` is
897
- // always followed by `(`, generic type args or not, so seeing `<`
898
- // instead can only mean a type argument list, never a comparison.
904
+ // `new Pair<number, string>(...)` — unambiguous here: `new
905
+ // <Identifier>` is always followed by `(`, generic type args or not,
906
+ // so seeing `<` instead can only mean a type argument list, never a
907
+ // comparison.
899
908
  let typeArgs = null;
900
909
  if (this.check(TokenKind.Lt)) {
901
910
  this.advance();
902
911
  typeArgs = [this.parseType()];
903
- this.consume(TokenKind.Gt, "Expected '>' after type argument");
912
+ while (this.match(TokenKind.Comma)) {
913
+ typeArgs.push(this.parseType());
914
+ }
915
+ this.consume(TokenKind.Gt, "Expected '>' after type argument list");
904
916
  }
905
917
  this.consume(TokenKind.LParen, "Expected '(' after class name");
906
918
  const args = [];
package/dist/printer.js CHANGED
@@ -212,7 +212,7 @@ export class Printer {
212
212
  printClass(decl, indent) {
213
213
  const pad = indentStr(indent);
214
214
  const memberPad = indentStr(indent + 1);
215
- const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
215
+ const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
216
216
  const header = decl.baseList.length > 0 ? `class ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `class ${nameWithTypeParam} {`;
217
217
  const prefix = decl.isExported ? "" : "private ";
218
218
  const memberParts = [];
@@ -250,7 +250,7 @@ export class Printer {
250
250
  printInterface(decl, indent) {
251
251
  const pad = indentStr(indent);
252
252
  const memberPad = indentStr(indent + 1);
253
- const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
253
+ const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
254
254
  const header = decl.baseList.length > 0 ? `interface ${nameWithTypeParam} : ${decl.baseList.join(", ")} {` : `interface ${nameWithTypeParam} {`;
255
255
  const prefix = decl.isExported ? "" : "private ";
256
256
  if (decl.methods.length === 0)
@@ -279,7 +279,7 @@ export class Printer {
279
279
  const pad = indentStr(indent);
280
280
  const memberPad = indentStr(indent + 1);
281
281
  const prefix = decl.isExported ? "" : "private ";
282
- const nameWithTypeParam = decl.typeParam ? `${decl.name}<${decl.typeParam}>` : decl.name;
282
+ const nameWithTypeParam = decl.typeParams.length > 0 ? `${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : decl.name;
283
283
  const lines = [];
284
284
  if (decl.hasConstructor)
285
285
  lines.push(`${memberPad}constructor(${this.printParams(decl.ctorParams)});`);
package/dist/types.js CHANGED
@@ -6,11 +6,11 @@ export const UNKNOWN = { kind: "unknown" };
6
6
  export function arrayOf(element) {
7
7
  return { kind: "array", element };
8
8
  }
9
- export function classType(name, typeArg) {
10
- return typeArg ? { kind: "class", name, typeArg } : { kind: "class", name };
9
+ export function classType(name, typeArgs) {
10
+ return typeArgs && typeArgs.length > 0 ? { kind: "class", name, typeArgs } : { kind: "class", name };
11
11
  }
12
- export function interfaceType(name, typeArg) {
13
- return typeArg ? { kind: "interface", name, typeArg } : { kind: "interface", name };
12
+ export function interfaceType(name, typeArgs) {
13
+ return typeArgs && typeArgs.length > 0 ? { kind: "interface", name, typeArgs } : { kind: "interface", name };
14
14
  }
15
15
  export function typeParamType(name) {
16
16
  return { kind: "typeParam", name };
@@ -44,7 +44,7 @@ export function typeToString(t) {
44
44
  return t.name;
45
45
  case "class":
46
46
  case "interface":
47
- return t.typeArg ? `${t.name}<${typeToString(t.typeArg)}>` : t.name;
47
+ return t.typeArgs && t.typeArgs.length > 0 ? `${t.name}<${t.typeArgs.map(typeToString).join(", ")}>` : t.name;
48
48
  case "function":
49
49
  return `(${t.params.map(typeToString).join(", ")}) => ${typeToString(t.returnType)}`;
50
50
  case "task":
@@ -91,31 +91,36 @@ export function resolveTypeNode(node, namedTypes, onNamedType) {
91
91
  const inner = resolveTypeNode(node.inner, namedTypes, onNamedType);
92
92
  return inner ? nullableOf(inner) : null;
93
93
  }
94
- // At most one type argument in v1resolve it once, up front; a
95
- // primitive/enum/type-param name below never accepts one (arity error,
96
- // caught as a plain resolution failure here a class/interface's own
97
- // required-vs-supplied arity mismatch is a separate check the caller
98
- // makes, since only it knows which names are actually declared generic).
99
- let typeArg = null;
94
+ // Resolve every type argument given, in ordera primitive/enum/
95
+ // type-param name below never accepts any (arity error, caught as a plain
96
+ // resolution failure here). Arity *against a class/interface's own
97
+ // declared parameter count* is a separate check the caller makes, since
98
+ // only it knows which names are actually declared generic and with how
99
+ // many parameters (see Checker.validateGenericArity).
100
+ let typeArgs = null;
100
101
  if (node.typeArgs) {
101
- typeArg = resolveTypeNode(node.typeArgs[0], namedTypes, onNamedType);
102
- if (!typeArg)
103
- return null;
102
+ typeArgs = [];
103
+ for (const argNode of node.typeArgs) {
104
+ const resolvedArg = resolveTypeNode(argNode, namedTypes, onNamedType);
105
+ if (!resolvedArg)
106
+ return null;
107
+ typeArgs.push(resolvedArg);
108
+ }
104
109
  }
105
110
  let resolved;
106
111
  if (PRIMITIVE_NAMES.has(node.name)) {
107
- resolved = typeArg ? null : { kind: node.name };
112
+ resolved = typeArgs ? null : { kind: node.name };
108
113
  }
109
114
  else {
110
115
  const kind = namedTypes.get(node.name);
111
116
  if (kind === "class")
112
- resolved = classType(node.name, typeArg ?? undefined);
117
+ resolved = classType(node.name, typeArgs ?? undefined);
113
118
  else if (kind === "interface")
114
- resolved = interfaceType(node.name, typeArg ?? undefined);
119
+ resolved = interfaceType(node.name, typeArgs ?? undefined);
115
120
  else if (kind === "enum")
116
- resolved = typeArg ? null : enumType(node.name);
121
+ resolved = typeArgs ? null : enumType(node.name);
117
122
  else if (kind === "typeParam")
118
- resolved = typeArg ? null : typeParamType(node.name);
123
+ resolved = typeArgs ? null : typeParamType(node.name);
119
124
  else
120
125
  resolved = null;
121
126
  }
@@ -153,12 +158,12 @@ export function typesEqual(a, b) {
153
158
  const other = b;
154
159
  if (a.name !== other.name)
155
160
  return false;
156
- if ("typeArg" in a || "typeArg" in other) {
157
- const aArg = a.typeArg;
158
- const bArg = other.typeArg;
159
- if (!aArg || !bArg)
161
+ if ("typeArgs" in a || "typeArgs" in other) {
162
+ const aArgs = a.typeArgs;
163
+ const bArgs = other.typeArgs;
164
+ if (!aArgs || !bArgs || aArgs.length !== bArgs.length)
160
165
  return false;
161
- return typesEqual(aArg, bArg);
166
+ return aArgs.every((t, i) => typesEqual(t, bArgs[i]));
162
167
  }
163
168
  return true;
164
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",