kopscript 0.3.0 → 0.4.1

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
@@ -56,11 +56,14 @@ below). No circular `using` (compile error).
56
56
  | `task` / `task<T>` | see Async below |
57
57
  | `state<T>` | see Reactive state below |
58
58
  | `T?` | nullable — see Nullable types below |
59
+ | `Box<T>` | a user-declared generic class/interface — see Generics below |
59
60
 
60
- **Does not exist**: generics (no `class Foo<T>`, no `T Resolve<T>()`), `any`/`unknown` as
61
- a writable annotation, a `let`/`var` keyword (see Declarations below — there isn't one),
62
- type inference for declarations (every local/`const`/param/field/return type is written
63
- out explicitly), union types, tuples, structural/duck typing (all typing is nominal).
61
+ **Does not exist**: `any`/`unknown` as a writable annotation, a `let`/`var` keyword (see
62
+ Declarations below — there isn't one), type inference for declarations (every
63
+ local/`const`/param/field/return type is written out explicitly), union types, tuples,
64
+ structural/duck typing (all typing is nominal). Generics exist but are deliberately
65
+ scoped down — see the Generics section for exactly what's NOT supported there (multiple
66
+ type parameters, constraints, generic functions, generic inheritance).
64
67
 
65
68
  ## Declarations
66
69
 
@@ -116,6 +119,8 @@ Dog d = new Dog("Rex");
116
119
  the interface in the base list and provide matching public methods, checked at compile time.
117
120
  - No nested classes/functions.
118
121
  - No multiple inheritance (at most one class in the base list; the rest must be interfaces).
122
+ - `class Box<T> { ... }` — a single type parameter is allowed; see Generics below for the
123
+ full (deliberately scoped-down) rules.
119
124
 
120
125
  ### Interfaces
121
126
 
@@ -126,6 +131,9 @@ interface IShape {
126
131
  interface INamedShape : IShape { // interfaces can extend other interfaces (not classes)
127
132
  string GetName();
128
133
  }
134
+ interface IContainer<T> { // interfaces take a single type parameter too — see Generics
135
+ T Get();
136
+ }
129
137
  ```
130
138
 
131
139
  ### Enums
@@ -193,6 +201,11 @@ $"Hello {name}, you are {age} years old" // interpolated string
193
201
  r"^[a-z]+$" // regex literal (only meaningful as a match pattern)
194
202
  ```
195
203
 
204
+ A regex literal's contents pass to the real `RegExp` unprocessed — `\s`, `\d`, `\w`, `\.`,
205
+ etc. all mean real regex syntax, not string escapes (only `\"` is special, letting a
206
+ literal `"` appear before the closing quote). This differs from every other string-like
207
+ literal in the language, which do interpret `\n`/`\t`/`\\`/etc.
208
+
196
209
  ### Lambdas
197
210
 
198
211
  ```ks
@@ -250,8 +263,54 @@ count.Value = count.Value + 1;
250
263
  count.Subscribe((number v) => { ... }); // called on every future .Value assignment
251
264
  ```
252
265
 
253
- Not a generic type parameter — `state<T>` is one of exactly two hardcoded parametrized
254
- forms in the type system (the other is `task<T>`).
266
+ Not a use of user-declared generics (see below) — `state<T>` is one of exactly two
267
+ hardcoded parametrized forms in the type system (the other is `task<T>`), each with only
268
+ ever one built-in meaning, unlike a real generic class/interface.
269
+
270
+ ## Generics — a single type parameter on classes/interfaces
271
+
272
+ ```ks
273
+ class Box<T> {
274
+ public T Value;
275
+ constructor(T v) { this.Value = v; }
276
+ public T Get() { return this.Value; }
277
+ }
278
+
279
+ Box<number> nb = new Box<number>(5); // type argument required on both the type AND `new`
280
+ Box<string> sb = new Box<string>("hi");
281
+ print(nb.Get()); // 5
282
+ ```
283
+
284
+ One unconstrained, invariant type parameter per class/interface — the whole feature.
285
+ Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
286
+ compile to the exact same plain `class Box`), so there's no runtime cost and no way to
287
+ inspect `T` at runtime.
288
+
289
+ **Rules:**
290
+ - The type argument is **required everywhere** a generic type is named — on the variable
291
+ type (`Box<number>`) and separately on `new` (`new Box<number>(...)`); a bare `Box` (no
292
+ argument) is a compile error, not "any"/inferred.
293
+ - **Invariant**: `Box<Dog>` is not assignable to `Box<Animal>` even if `Dog : Animal` —
294
+ type arguments must match exactly, not just be compatible.
295
+ - Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`.
296
+ - Inside the class/interface's own body, `T` is fully abstract — you can store it, return
297
+ it, pass it around, but (unconstrained) you **cannot call any member on a bare `T`
298
+ value** (`this.value.Foo()` is a compile error: "Cannot access member 'Foo' on type
299
+ 'T'"). This is correct, not a bug — same as an unconstrained type parameter in
300
+ C#/Java/TypeScript.
301
+
302
+ **Does not exist (v1 scope cuts, each deliberate)**:
303
+ - **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
304
+ take a type parameter, not free functions/methods themselves (a method *inside* a
305
+ generic class can use that class's own `T` freely, same as any other member).
306
+ - **Multiple type parameters.** No `Map<K, V>` — exactly one `<T>` or none.
307
+ - **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
308
+ which is why you can't call members on a bare `T` (see above).
309
+ - **Generic inheritance.** A class/interface's base list can only name a *non-generic*
310
+ type — `class Foo : Box<number>` and even `class Foo<T> : SomeGenericBase<T>` are both
311
+ compile errors ("cannot extend/implement generic type '...' — not supported in v1"). A
312
+ generic class/interface can still extend/implement ordinary non-generic bases normally.
313
+ - **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
255
314
 
256
315
  ## Nullable types — `T?`
257
316
 
@@ -328,6 +387,27 @@ KopScript-declared name exactly. Extern class members use real JS member names v
328
387
  (camelCase, no rename mechanism). No inheritance modeling between two `extern class`
329
388
  declarations — each stands alone.
330
389
 
390
+ **Never write `async` on an extern function/method signature** — declare its return type
391
+ as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
392
+ `async` only means something for a *body* the checker validates (legalizing `await`
393
+ inside it, checking `return` against the unwrapped type); an extern signature has no
394
+ body, so the keyword is simply invalid there — a parse error, not a no-op. This applies
395
+ equally to a top-level `extern` function and an `extern class` method signature.
396
+
397
+ `jsName` (the `as "..."` string) can be a dotted path, not just a bare identifier — e.g.
398
+ `as "JSON.parse"` — since it's spliced directly into `globalThis.<jsName>` for an
399
+ ambient binding. This is the supported way to get a *typed* (unchecked, trust-based)
400
+ JSON value: describe the shape as its own `extern class`, then declare a parse function
401
+ for it: `extern MyShape ParseIt(string json) as "JSON.parse";` — calling `ParseIt(text)`
402
+ returns whatever `JSON.parse` actually parsed, typed as `MyShape` with zero runtime
403
+ verification (the same trust model as every other `extern`).
404
+
405
+ An `extern class` reachable from a real npm package — not same-project `using`, which
406
+ only resolves relative `.ks` paths — is how KopScript consumes *any* JS dependency,
407
+ including another KopScript-authored package published as JS (e.g. Kopular): describe
408
+ exactly the members used, from that package's real module specifier. See Kopular's own
409
+ `LLM.md`/README for a live example (`Component`, `Router`, `Http`, ...).
410
+
331
411
  ## `raw string` — compile-time file embedding
332
412
 
333
413
  ```ks
@@ -354,6 +434,11 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
354
434
  Generics · `any`/`unknown` annotations ·
355
435
  a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
356
436
  reflection · type inference on declarations · ternary expression · union/tuple types ·
437
+ **object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
438
+ the grammar, not just as a type; a JS API whose signature needs one — `fetch`'s options
439
+ argument, `addEventListener`'s options object, etc. — can't be called directly from
440
+ KopScript at all; see Kopular's `http.ks`/`http_runtime.js` for the one established
441
+ workaround, a small hand-written JS shim, not a language feature) ·
357
442
  structural typing · multiple class inheritance ·
358
443
  nested classes/functions · method overloading · default/optional parameters · varargs ·
359
444
  static properties with accessors (static fields only) · interface properties (interface
package/README.md CHANGED
@@ -22,6 +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
28
  - **Nullable types with compiler-enforced null-checking**: `T?` (`string?`, `Dog?`,
26
29
  `number[]?`) — a `T?` can't be used where a `T` is expected without an `if (x != null)`
27
30
  check first (checked statically, not just at runtime), and comparing a value that can
@@ -204,6 +207,49 @@ void Announce(ISpeaker s) {
204
207
  }
205
208
  ```
206
209
 
210
+ ### Generics
211
+
212
+ Classes and interfaces can take a single type parameter:
213
+
214
+ ```ks
215
+ class Box<T> {
216
+ public T Value;
217
+ constructor(T v) { this.Value = v; }
218
+ public T Get() { return this.Value; }
219
+ }
220
+
221
+ Box<number> nb = new Box<number>(5);
222
+ Box<string> sb = new Box<string>("hi");
223
+ print(nb.Get()); // 5
224
+ ```
225
+
226
+ The type argument is required everywhere the generic type is named — on the variable's
227
+ declared type *and* separately on `new` (`Box<number> nb = new Box<number>(5);`, not
228
+ just one or the other). Generics are erased at codegen exactly like `task<T>`/`state<T>`
229
+ already are — `Box<number>` and `Box<string>` compile to the identical plain `class Box`,
230
+ so there's no runtime cost.
231
+
232
+ This is deliberately the smallest useful slice of generics, not a scaled-down promise of
233
+ more later inside v1:
234
+
235
+ - **One type parameter, invariant, unconstrained.** No `Map<K, V>` (multiple parameters),
236
+ no `T : ISomething` (constraints) — and because `T` is fully unconstrained, you can't
237
+ call any member on a bare `T` value inside the generic class's own body (that's correct
238
+ behavior, the same restriction C#/Java/TypeScript put on an unconstrained parameter, not
239
+ a bug). Invariant means `Box<Dog>` is **not** assignable to `Box<Animal>` even though
240
+ `Dog : Animal` — type arguments must match exactly.
241
+ - **No generic functions.** `T Identity<T>(T x)` isn't supported — only classes and
242
+ interfaces take a type parameter. A method *inside* a generic class can still use that
243
+ class's own `T` freely; it's just not introducing a type parameter of its own.
244
+ - **No generic inheritance.** A class or interface's base list can only name a
245
+ *non-generic* type. `class Foo : Box<number>` and `class Foo<T> : SomeBase<T>` are both
246
+ compile errors — a generic class can still extend/implement ordinary non-generic bases
247
+ normally, it just can't be the one on either side of a generic base relationship.
248
+
249
+ See `LLM.md`'s "Generics" section for the exhaustive rules if you're generating code
250
+ against this — nested generics (`Box<Box<number>>`, `Box<number>?[]`) and generic
251
+ interfaces used as standalone parameter types both work and are covered there.
252
+
207
253
  ### Enums
208
254
 
209
255
  ```ks
@@ -521,8 +567,9 @@ foreach (number item in items) {
521
567
  ### Types (v1 scope)
522
568
 
523
569
  `number`, `string`, `bool`, `void`, `T[]` (arrays), class types, interface types, enum
524
- types, function types (`(T, ...) => R`), and nullable types (`T?`, with `null` and
525
- compiler-enforced null-checking — see "Nullable types" below). No generics yet.
570
+ types, function types (`(T, ...) => R`), nullable types (`T?`, with `null` and
571
+ compiler-enforced null-checking — see "Nullable types" below), and a single type
572
+ parameter on classes/interfaces (`Box<T>` — see "Generics" above).
526
573
 
527
574
  ### Nullable types — `T?`
528
575
 
@@ -649,15 +696,13 @@ into your extensions folder).
649
696
 
650
697
  ## Status
651
698
 
652
- This is a v1 / hobby-project scope. Nullable types (`T?`) shipped see above. Not yet
653
- supported: generics, static auto-properties, interface properties (methods only), and
654
- nested functions/classes. Generics in particular is a substantially bigger undertaking
655
- than everything else here it touches the type system's core (type parameters,
656
- variance, constraint checking) rather than being an additive feature, so it's
657
- deliberately left for a dedicated future pass rather than bolted on. Nullable types
658
- turned out to fit that "additive feature" shape after all: no variance or constraint
659
- solving involved, so it landed as a normal-sized change — a lesson for scoping generics
660
- itself, not evidence generics will be similarly sized.
699
+ This is a v1 / hobby-project scope. Nullable types (`T?`) and generics (a single
700
+ unconstrained, invariant type parameter on classes/interfaces) have both shipped — see
701
+ above for both. What generics deliberately doesn't cover: multiple type parameters
702
+ (`Map<K, V>`), constraints (`T : IFoo`), generic functions, generic inheritance, and
703
+ variance — each a real, separable extension rather than a v1 oversight. Also not yet
704
+ supported: static auto-properties, interface properties (methods only), and nested
705
+ functions/classes.
661
706
 
662
707
  `async`/`await`, `task<T>`, and `try`/`catch`/`finally`/`throw` are now in place (see the
663
708
  language tour above) — the ceiling that's left is what's *inside* those: no async lambdas,
package/dist/checker.js CHANGED
@@ -56,6 +56,12 @@ 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.
64
+ this.genericTypeParams = new Map();
59
65
  this.importedNames = new Set();
60
66
  this.rawContents = new Map();
61
67
  this.hoverEntries = [];
@@ -65,10 +71,16 @@ export class Checker {
65
71
  this.namedTypes.set(name, kind);
66
72
  this.importedNames.add(name);
67
73
  }
68
- for (const [name, info] of this.imports.classes)
74
+ for (const [name, info] of this.imports.classes) {
69
75
  this.classes.set(name, info);
70
- for (const [name, info] of this.imports.interfaces)
76
+ if (info.typeParam)
77
+ this.genericTypeParams.set(name, info.typeParam);
78
+ }
79
+ for (const [name, info] of this.imports.interfaces) {
71
80
  this.interfaces.set(name, info);
81
+ if (info.typeParam)
82
+ this.genericTypeParams.set(name, info.typeParam);
83
+ }
72
84
  for (const [name, info] of this.imports.enums)
73
85
  this.enums.set(name, info);
74
86
  for (const [name, info] of this.imports.functions) {
@@ -96,6 +108,12 @@ export class Checker {
96
108
  this.namedTypes.set(e.name, "enum");
97
109
  for (const c of externClassDecls)
98
110
  this.namedTypes.set(c.name, "class");
111
+ for (const c of classDecls)
112
+ if (c.typeParam)
113
+ this.genericTypeParams.set(c.name, c.typeParam);
114
+ for (const i of interfaceDecls)
115
+ if (i.typeParam)
116
+ this.genericTypeParams.set(i.name, i.typeParam);
99
117
  for (const e of enumDecls)
100
118
  this.registerEnum(e);
101
119
  for (const i of interfaceDecls)
@@ -172,6 +190,7 @@ export class Checker {
172
190
  const ownCtorParams = decl.hasConstructor ? decl.ctorParams.map((p) => this.resolveType(p.type, decl.line, decl.col)) : null;
173
191
  this.classes.set(decl.name, {
174
192
  name: decl.name,
193
+ typeParam: null,
175
194
  superclass: null,
176
195
  interfaces: [],
177
196
  fields,
@@ -312,11 +331,111 @@ export class Checker {
312
331
  });
313
332
  if (!resolved) {
314
333
  const name = node.kind === "NamedType" ? node.name : "[]";
315
- this.diagnostics.error(`Unknown type '${name}'`, line, col);
334
+ if (name !== "[]" && this.genericTypeParams.has(name) && !node.typeArgs) {
335
+ this.diagnostics.error(`Generic type '${name}' requires a type argument (e.g. '${name}<T>')`, line, col);
336
+ }
337
+ else {
338
+ this.diagnostics.error(`Unknown type '${name}'`, line, col);
339
+ }
316
340
  return T.UNKNOWN;
317
341
  }
342
+ this.validateGenericArity(resolved, line, col);
318
343
  return resolved;
319
344
  }
345
+ // resolveTypeNode mechanically attaches whatever type argument a
346
+ // NamedType node happened to carry, without knowing which names are
347
+ // actually declared generic (that's checker state, not something the
348
+ // pure type-resolution function has access to) — this is the other half:
349
+ // walking the successfully-resolved type looking for a class/interface
350
+ // whose generic-ness doesn't match whether it got a type argument.
351
+ validateGenericArity(type, line, col) {
352
+ switch (type.kind) {
353
+ case "array":
354
+ this.validateGenericArity(type.element, line, col);
355
+ return;
356
+ case "function":
357
+ type.params.forEach((p) => this.validateGenericArity(p, line, col));
358
+ this.validateGenericArity(type.returnType, line, col);
359
+ return;
360
+ case "task":
361
+ this.validateGenericArity(type.resultType, line, col);
362
+ return;
363
+ case "state":
364
+ this.validateGenericArity(type.valueType, line, col);
365
+ return;
366
+ case "nullable":
367
+ this.validateGenericArity(type.inner, line, col);
368
+ return;
369
+ case "class":
370
+ case "interface": {
371
+ const isGeneric = this.genericTypeParams.has(type.name);
372
+ if (isGeneric && !type.typeArg) {
373
+ this.diagnostics.error(`Generic type '${type.name}' requires a type argument (e.g. '${type.name}<T>')`, line, col);
374
+ }
375
+ else if (!isGeneric && type.typeArg) {
376
+ this.diagnostics.error(`Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
377
+ }
378
+ if (type.typeArg)
379
+ this.validateGenericArity(type.typeArg, line, col);
380
+ return;
381
+ }
382
+ default:
383
+ return;
384
+ }
385
+ }
386
+ // Temporarily makes `name` (a class/interface's own type parameter, e.g.
387
+ // "T") resolve as a TypeParamType, for the duration of `fn` — used only
388
+ // while registering that class/interface's own declaration (fields,
389
+ // methods, ctor params), so `T Value;` resolves correctly. Scoped this
390
+ // narrowly (set, run, delete/restore) rather than left in `namedTypes`
391
+ // permanently, since it's only ever meaningful inside that one
392
+ // declaration's own body.
393
+ withTypeParamInScope(typeParam, fn) {
394
+ if (!typeParam)
395
+ return fn();
396
+ const previous = this.namedTypes.get(typeParam);
397
+ this.namedTypes.set(typeParam, "typeParam");
398
+ try {
399
+ return fn();
400
+ }
401
+ finally {
402
+ if (previous === undefined)
403
+ this.namedTypes.delete(typeParam);
404
+ else
405
+ this.namedTypes.set(typeParam, previous);
406
+ }
407
+ }
408
+ // Replaces every occurrence of the class/interface's own type parameter
409
+ // (by name) inside `type` with `arg` — the substitution step that turns
410
+ // Box<T>'s abstractly-stored field type `T` into `number` when someone
411
+ // actually asks about `Box<number>.Value`. A type with no occurrence of
412
+ // `paramName` anywhere in it is returned unchanged (including every
413
+ // non-generic type, the overwhelming majority).
414
+ substituteTypeParam(type, paramName, arg) {
415
+ switch (type.kind) {
416
+ case "typeParam":
417
+ return type.name === paramName ? arg : type;
418
+ case "array":
419
+ return T.arrayOf(this.substituteTypeParam(type.element, paramName, arg));
420
+ case "nullable":
421
+ return T.nullableOf(this.substituteTypeParam(type.inner, paramName, arg));
422
+ case "task":
423
+ return T.taskType(this.substituteTypeParam(type.resultType, paramName, arg));
424
+ case "state":
425
+ return T.stateType(this.substituteTypeParam(type.valueType, paramName, arg));
426
+ case "function":
427
+ return T.functionType(type.params.map((p) => this.substituteTypeParam(p, paramName, arg)), this.substituteTypeParam(type.returnType, paramName, arg));
428
+ // A nested generic's own type argument can itself mention the outer
429
+ // T (`Box<T[]>`'s field being List<T> — a v1 corner case, but cheap
430
+ // to handle correctly): substitute inside it too.
431
+ case "class":
432
+ return type.typeArg ? T.classType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
433
+ case "interface":
434
+ return type.typeArg ? T.interfaceType(type.name, this.substituteTypeParam(type.typeArg, paramName, arg)) : type;
435
+ default:
436
+ return type;
437
+ }
438
+ }
320
439
  registerEnum(decl) {
321
440
  const members = new Map();
322
441
  decl.members.forEach((name, index) => {
@@ -330,22 +449,29 @@ export class Checker {
330
449
  this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
331
450
  }
332
451
  registerInterface(decl) {
333
- const methods = decl.methods.map((m) => {
452
+ const methods = this.withTypeParamInScope(decl.typeParam, () => decl.methods.map((m) => {
334
453
  const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
335
454
  const returnType = this.resolveType(m.returnType, m.line, m.col);
336
455
  this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
337
456
  return { name: m.name, params, returnType };
338
- });
457
+ }));
339
458
  const bases = [];
340
459
  for (const baseName of decl.baseList) {
341
460
  if (this.namedTypes.get(baseName) !== "interface") {
342
461
  this.diagnostics.error(`Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
343
462
  continue;
344
463
  }
464
+ // v1 generics can't appear in a base list at all — only as a
465
+ // standalone type (field/param/return/local, `new Box<T>()`). A
466
+ // class/interface always implements/extends a *bare* name.
467
+ if (this.genericTypeParams.has(baseName)) {
468
+ this.diagnostics.error(`Interface '${decl.name}' cannot extend generic interface '${baseName}' — not supported in v1`, decl.line, decl.col);
469
+ continue;
470
+ }
345
471
  bases.push(baseName);
346
472
  }
347
- this.interfaces.set(decl.name, { name: decl.name, bases, methods });
348
- this.recordHover(decl.line, decl.col, `interface ${decl.name}`);
473
+ this.interfaces.set(decl.name, { name: decl.name, typeParam: decl.typeParam, bases, methods });
474
+ this.recordHover(decl.line, decl.col, decl.typeParam ? `interface ${decl.name}<${decl.typeParam}>` : `interface ${decl.name}`);
349
475
  }
350
476
  checkInterfaceHierarchy(decl) {
351
477
  const info = this.interfaces.get(decl.name);
@@ -388,38 +514,48 @@ export class Checker {
388
514
  return info.bases.some((b) => this.interfaceExtends(b, sup));
389
515
  }
390
516
  registerClass(decl) {
391
- this.recordHover(decl.line, decl.col, `class ${decl.name}`);
392
- const fields = new Map();
393
- const staticFields = new Map();
394
- for (const f of decl.fields) {
395
- const info = { type: this.resolveType(f.type, f.line, f.col), visibility: f.visibility, hasSetter: true };
396
- this.recordHover(f.nameLine, f.nameCol, `${f.name}: ${T.typeToString(info.type)}`);
397
- (f.isStatic ? staticFields : fields).set(f.name, info);
398
- }
399
- for (const p of decl.properties) {
400
- const type = this.resolveType(p.type, p.line, p.col);
401
- this.recordHover(p.nameLine, p.nameCol, `${p.name}: ${T.typeToString(type)}`);
402
- fields.set(p.name, { type, visibility: p.visibility, hasSetter: p.hasSetter });
403
- }
404
- const methods = new Map();
405
- const staticMethods = new Map();
406
- for (const m of decl.methods) {
407
- const info = {
408
- params: m.params.map((p) => this.resolveType(p.type, m.line, m.col)),
409
- returnType: this.resolveType(m.returnType, m.line, m.col),
410
- visibility: m.visibility,
411
- isVirtual: m.isVirtual,
412
- isOverride: m.isOverride,
413
- };
414
- this.recordHover(m.nameLine, m.nameCol, `${m.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
415
- (m.isStatic ? staticMethods : methods).set(m.name, info);
416
- }
417
- const ownCtorParams = decl.constructor
418
- ? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
419
- : null;
517
+ this.recordHover(decl.line, decl.col, decl.typeParam ? `class ${decl.name}<${decl.typeParam}>` : `class ${decl.name}`);
518
+ const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamInScope(decl.typeParam, () => {
519
+ const fields = new Map();
520
+ const staticFields = new Map();
521
+ for (const f of decl.fields) {
522
+ const info = { type: this.resolveType(f.type, f.line, f.col), visibility: f.visibility, hasSetter: true };
523
+ this.recordHover(f.nameLine, f.nameCol, `${f.name}: ${T.typeToString(info.type)}`);
524
+ (f.isStatic ? staticFields : fields).set(f.name, info);
525
+ }
526
+ for (const p of decl.properties) {
527
+ const type = this.resolveType(p.type, p.line, p.col);
528
+ this.recordHover(p.nameLine, p.nameCol, `${p.name}: ${T.typeToString(type)}`);
529
+ fields.set(p.name, { type, visibility: p.visibility, hasSetter: p.hasSetter });
530
+ }
531
+ const methods = new Map();
532
+ const staticMethods = new Map();
533
+ for (const m of decl.methods) {
534
+ const info = {
535
+ params: m.params.map((p) => this.resolveType(p.type, m.line, m.col)),
536
+ returnType: this.resolveType(m.returnType, m.line, m.col),
537
+ visibility: m.visibility,
538
+ isVirtual: m.isVirtual,
539
+ isOverride: m.isOverride,
540
+ };
541
+ this.recordHover(m.nameLine, m.nameCol, `${m.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
542
+ (m.isStatic ? staticMethods : methods).set(m.name, info);
543
+ }
544
+ const ownCtorParams = decl.constructor
545
+ ? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
546
+ : null;
547
+ return { fields, staticFields, methods, staticMethods, ownCtorParams };
548
+ });
420
549
  let superclass = null;
421
550
  const interfaces = [];
422
551
  for (const baseName of decl.baseList) {
552
+ // v1 generics can't appear in a base list at all — only as a
553
+ // standalone type (field/param/return/local, `new Box<T>()`). A
554
+ // class always extends/implements a *bare* name.
555
+ if (this.genericTypeParams.has(baseName)) {
556
+ this.diagnostics.error(`Class '${decl.name}' cannot extend/implement generic type '${baseName}' — not supported in v1`, decl.line, decl.col);
557
+ continue;
558
+ }
423
559
  const kind = this.namedTypes.get(baseName);
424
560
  if (kind === "class") {
425
561
  if (superclass !== null) {
@@ -438,6 +574,7 @@ export class Checker {
438
574
  }
439
575
  this.classes.set(decl.name, {
440
576
  name: decl.name,
577
+ typeParam: decl.typeParam,
441
578
  superclass,
442
579
  interfaces,
443
580
  fields,
@@ -649,6 +786,14 @@ export class Checker {
649
786
  if (from.kind === "nullable")
650
787
  return false;
651
788
  if (to.kind === "interface") {
789
+ // Invariant generics: v1 has no generic inheritance (a class/interface
790
+ // base list can only name a non-generic type — see registerClass/
791
+ // registerInterface), so the *only* way `from`'s interface name can
792
+ // ever match `to`'s is a literal same-interface reference (never a
793
+ // distinct generic instantiation implementing another) — still worth
794
+ // checking the type argument actually matches, same as the class case.
795
+ if (from.kind === "interface" && from.name === to.name)
796
+ return this.typeArgsMatch(from, to);
652
797
  if (from.kind === "class")
653
798
  return this.classImplementsInterface(from.name, to.name);
654
799
  if (from.kind === "interface")
@@ -656,13 +801,26 @@ export class Checker {
656
801
  return false;
657
802
  }
658
803
  if (from.kind === "class" && to.kind === "class") {
659
- return from.name === to.name || this.isSubclass(from.name, to.name);
804
+ if (from.name === to.name)
805
+ return this.typeArgsMatch(from, to);
806
+ return this.isSubclass(from.name, to.name);
660
807
  }
661
808
  if (from.kind === "array" && to.kind === "array") {
662
809
  return this.isAssignableType(from.element, to.element);
663
810
  }
664
811
  return T.typesEqual(from, to);
665
812
  }
813
+ // Invariant: Box<number> and Box<string> are unrelated, and so is a
814
+ // generic type referenced with vs. without its type argument (the latter
815
+ // only ever arises from an arity error already diagnosed at the
816
+ // reference site — see validateGenericArity).
817
+ typeArgsMatch(a, b) {
818
+ if (!a.typeArg && !b.typeArg)
819
+ return true;
820
+ if (!a.typeArg || !b.typeArg)
821
+ return false;
822
+ return T.typesEqual(a.typeArg, b.typeArg);
823
+ }
666
824
  // ---------- top-level ----------
667
825
  // Validates that `isAsync` and the declared return type agree — `async`
668
826
  // requires `task`/`task<T>`, and `task`/`task<T>` requires `async` (there's
@@ -698,6 +856,15 @@ export class Checker {
698
856
  this.checkBlock(decl.body, scope, { returnType, currentClass: null, inConstructor: false, loopDepth: 0, isAsync: decl.isAsync });
699
857
  }
700
858
  checkClassBody(decl) {
859
+ // Constructor/method *bodies* run in this same scope registerClass used
860
+ // for the *signatures* (see withTypeParamInScope) — without it, `T`
861
+ // resolves everywhere in a generic class's declared field/param/return
862
+ // types but not inside a method body itself (a local `T x = ...;`, or a
863
+ // lambda parameter typed `T`), which would make the type parameter
864
+ // usable only at the class's boundary and not inside its own logic.
865
+ this.withTypeParamInScope(decl.typeParam, () => this.checkClassBodyInner(decl));
866
+ }
867
+ checkClassBodyInner(decl) {
701
868
  const info = this.classes.get(decl.name);
702
869
  if (decl.constructor) {
703
870
  const paramScope = new Scope();
@@ -1225,6 +1392,7 @@ export class Checker {
1225
1392
  if (expr.callee.property === "Map") {
1226
1393
  const objectType = this.checkExpression(expr.callee.object, scope, ctx);
1227
1394
  if (objectType.kind === "array") {
1395
+ expr.callee.isBuiltin = true; // see checkMemberInner's string/array branches
1228
1396
  return this.checkArrayMap(expr, objectType.element, scope, ctx);
1229
1397
  }
1230
1398
  }
@@ -1283,8 +1451,31 @@ export class Checker {
1283
1451
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1284
1452
  return T.UNKNOWN;
1285
1453
  }
1286
- this.recordHover(expr.line, expr.col, `class ${expr.className}`);
1287
- const ctorParams = this.lookupCtorParams(expr.className);
1454
+ let typeArg;
1455
+ if (info.typeParam && !expr.typeArgs) {
1456
+ this.diagnostics.error(`Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
1457
+ // Abstractly-typed (T-containing) ctor params, un-substitutable
1458
+ // without a real type argument, would otherwise cascade into a
1459
+ // confusing "expected 'T'" error on every argument — one clear error
1460
+ // beats that pile-on.
1461
+ expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1462
+ return T.UNKNOWN;
1463
+ }
1464
+ if (!info.typeParam && expr.typeArgs) {
1465
+ this.diagnostics.error(`Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
1466
+ expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1467
+ return T.UNKNOWN;
1468
+ }
1469
+ if (info.typeParam && expr.typeArgs) {
1470
+ typeArg = this.resolveType(expr.typeArgs[0], expr.line, expr.col);
1471
+ }
1472
+ this.recordHover(expr.line, expr.col, typeArg ? `class ${expr.className}<${T.typeToString(typeArg)}>` : `class ${expr.className}`);
1473
+ let ctorParams = this.lookupCtorParams(expr.className);
1474
+ if (info.typeParam && typeArg) {
1475
+ const paramName = info.typeParam;
1476
+ const arg = typeArg;
1477
+ ctorParams = ctorParams.map((p) => this.substituteTypeParam(p, paramName, arg));
1478
+ }
1288
1479
  if (expr.args.length !== ctorParams.length) {
1289
1480
  this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1290
1481
  }
@@ -1295,7 +1486,7 @@ export class Checker {
1295
1486
  this.diagnostics.error(`Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1296
1487
  }
1297
1488
  });
1298
- return T.classType(expr.className);
1489
+ return T.classType(expr.className, typeArg);
1299
1490
  }
1300
1491
  lookupCtorParams(className) {
1301
1492
  let current = className;
@@ -1399,6 +1590,11 @@ export class Checker {
1399
1590
  return { type: T.UNKNOWN, methodInfo: null };
1400
1591
  }
1401
1592
  if (objectType.kind === "string") {
1593
+ // Marks this specific access as resolved against string's own
1594
+ // built-ins, not some user member that merely shares the name —
1595
+ // codegen's PascalCase -> camelCase renames rely on this, since they
1596
+ // have no type information of their own to tell the two apart.
1597
+ expr.isBuiltin = true;
1402
1598
  if (expr.property === "Length")
1403
1599
  return { type: T.NUMBER, methodInfo: null };
1404
1600
  const method = STRING_METHODS[expr.property];
@@ -1408,6 +1604,7 @@ export class Checker {
1408
1604
  return { type: T.UNKNOWN, methodInfo: null };
1409
1605
  }
1410
1606
  if (objectType.kind === "array") {
1607
+ expr.isBuiltin = true; // see the string branch above
1411
1608
  if (expr.property === "Length")
1412
1609
  return { type: T.NUMBER, methodInfo: null };
1413
1610
  const method = this.arrayMethod(objectType.element, expr.property);
@@ -1439,6 +1636,12 @@ export class Checker {
1439
1636
  return { type: T.UNKNOWN, methodInfo: null };
1440
1637
  }
1441
1638
  if (objectType.kind === "class") {
1639
+ // v1 has no generic inheritance (a generic class's base list can only
1640
+ // name non-generic types), so a member found on a generic instance
1641
+ // was always declared directly on that same class — substituting by
1642
+ // its own type parameter, not some ancestor's, is always correct.
1643
+ const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1644
+ const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
1442
1645
  const field = this.lookupField(objectType.name, expr.property);
1443
1646
  if (field) {
1444
1647
  this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
@@ -1448,12 +1651,13 @@ export class Checker {
1448
1651
  this.diagnostics.error(`'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1449
1652
  }
1450
1653
  }
1451
- return { type: field.info.type, methodInfo: null };
1654
+ return { type: substitute(field.info.type), methodInfo: null };
1452
1655
  }
1453
1656
  const method = this.lookupMethod(objectType.name, expr.property);
1454
1657
  if (method) {
1455
1658
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
1456
- return { type: method.info.returnType, methodInfo: method.info };
1659
+ const info = paramName ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
1660
+ return { type: info.returnType, methodInfo: info };
1457
1661
  }
1458
1662
  this.diagnostics.error(`Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1459
1663
  return { type: T.UNKNOWN, methodInfo: null };
@@ -1461,7 +1665,14 @@ export class Checker {
1461
1665
  if (objectType.kind === "interface") {
1462
1666
  const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
1463
1667
  if (sig) {
1464
- return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
1668
+ // v1 has no generic interface inheritance either (same restriction
1669
+ // as classes — see registerInterface), so a signature found here
1670
+ // was always declared directly on this same interface.
1671
+ const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
1672
+ const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
1673
+ const params = sig.params.map(substitute);
1674
+ const returnType = substitute(sig.returnType);
1675
+ return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
1465
1676
  }
1466
1677
  this.diagnostics.error(`Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1467
1678
  return { type: T.UNKNOWN, methodInfo: null };
package/dist/codegen.js CHANGED
@@ -311,13 +311,24 @@ export class CodeGenerator {
311
311
  // Array.Push is non-mutating in KopScript, unlike JS's own (mutating,
312
312
  // length-returning) Array.prototype.push — so it can't just be a member
313
313
  // rename like Map/Filter/ForEach; it needs an entirely different call
314
- // shape (a spread into a fresh array).
315
- if (expr.callee.kind === "MemberExpr" && expr.callee.property === "Push" && expr.args.length === 1) {
314
+ // shape (a spread into a fresh array). Gated on `isBuiltin` (set by the
315
+ // checker only when the receiver is actually an array) so a user class
316
+ // with its own same-named `Push(x)` method — a natural name for any
317
+ // Stack<T>/List<T>-style container, now that generics exist — still
318
+ // compiles to a normal method call instead of this rewrite.
319
+ if (expr.callee.kind === "MemberExpr" && expr.callee.isBuiltin && expr.callee.property === "Push" && expr.args.length === 1) {
316
320
  return `[...${this.genExpr(expr.callee.object)}, ${this.genExpr(expr.args[0])}]`;
317
321
  }
318
322
  return `${this.genExpr(expr.callee)}(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
319
323
  }
320
324
  genMember(expr) {
325
+ // PascalCase -> camelCase (and Length -> length) only applies to a
326
+ // genuine string/array built-in — see MemberExpr.isBuiltin's own
327
+ // comment for why this can't just be a name match. Anything else
328
+ // (including a user member that happens to share one of these names)
329
+ // passes through with its real, declared name untouched.
330
+ if (!expr.isBuiltin)
331
+ return `${this.genExpr(expr.object)}.${expr.property}`;
321
332
  const jsProperty = expr.property === "Length" ? "length" : MEMBER_METHOD_MAP[expr.property] ?? expr.property;
322
333
  return `${this.genExpr(expr.object)}.${jsProperty}`;
323
334
  }
package/dist/lexer.js CHANGED
@@ -133,12 +133,27 @@ export class Lexer {
133
133
  }
134
134
  return this.make(TokenKind.InterpolatedString, raw, line, col);
135
135
  }
136
+ // Deliberately its own reader, not `readStringChar` — a regex literal's
137
+ // backslash escapes (`\s`, `\d`, `\.`, ...) are regex syntax, meant to
138
+ // reach the real `RegExp` constructor unchanged, not string escapes to be
139
+ // interpreted here. `readStringChar` turns any unrecognized `\x` into a
140
+ // bare `x` (dropping the backslash), which silently corrupts almost every
141
+ // realistic pattern. Only `\"` gets special handling, so a literal quote
142
+ // can appear inside a pattern without ending the literal early; every
143
+ // other `\` + character (including `\\` itself) passes through verbatim.
136
144
  readRegexLiteral(line, col) {
137
145
  this.advance(); // 'r'
138
146
  this.advance(); // opening quote
139
147
  let value = "";
140
148
  while (!this.isAtEnd() && this.peek() !== '"') {
141
- value += this.readStringChar();
149
+ const c = this.advance();
150
+ if (c === "\\" && !this.isAtEnd()) {
151
+ const next = this.advance();
152
+ value += next === '"' ? '"' : "\\" + next;
153
+ }
154
+ else {
155
+ value += c;
156
+ }
142
157
  }
143
158
  if (this.isAtEnd()) {
144
159
  this.diagnostics.error("Unterminated regex literal", line, col);
package/dist/parser.js CHANGED
@@ -200,9 +200,20 @@ export class Parser {
200
200
  this.consume(TokenKind.RParen, "Expected ')' after parameters");
201
201
  return params;
202
202
  }
203
+ // `<T>` right after a class/interface name — a single, unconstrained type
204
+ // parameter (v1 has no `Map<K, V>`, no `T : IFoo` constraints). Null if
205
+ // absent, the overwhelmingly common case.
206
+ parseOptionalTypeParam() {
207
+ if (!this.match(TokenKind.Lt))
208
+ return null;
209
+ const name = this.consume(TokenKind.Identifier, "Expected a type parameter name").lexeme;
210
+ this.consume(TokenKind.Gt, "Expected '>' after type parameter");
211
+ return name;
212
+ }
203
213
  parseClassDecl(isExported) {
204
214
  const start = this.advance(); // 'class'
205
215
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
216
+ const typeParam = this.parseOptionalTypeParam();
206
217
  const baseList = [];
207
218
  if (this.match(TokenKind.Colon)) {
208
219
  do {
@@ -346,11 +357,12 @@ export class Parser {
346
357
  }
347
358
  }
348
359
  this.consume(TokenKind.RBrace, "Expected '}' after class body");
349
- return { kind: "ClassDecl", isExported, name, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
360
+ return { kind: "ClassDecl", isExported, name, typeParam, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
350
361
  }
351
362
  parseInterfaceDecl(isExported) {
352
363
  const start = this.advance(); // 'interface'
353
364
  const name = this.consume(TokenKind.Identifier, "Expected interface name").lexeme;
365
+ const typeParam = this.parseOptionalTypeParam();
354
366
  const baseList = [];
355
367
  if (this.match(TokenKind.Colon)) {
356
368
  do {
@@ -376,7 +388,7 @@ export class Parser {
376
388
  });
377
389
  }
378
390
  this.consume(TokenKind.RBrace, "Expected '}' after interface body");
379
- return { kind: "InterfaceDecl", isExported, name, baseList, methods, line: start.line, col: start.col };
391
+ return { kind: "InterfaceDecl", isExported, name, typeParam, baseList, methods, line: start.line, col: start.col };
380
392
  }
381
393
  parseEnumDecl(isExported) {
382
394
  const start = this.advance(); // 'enum'
@@ -621,7 +633,7 @@ export class Parser {
621
633
  }
622
634
  else if (this.check(TokenKind.Task)) {
623
635
  const taskTok = this.advance();
624
- let resultType = { kind: "NamedType", name: "void", line: taskTok.line, col: taskTok.col };
636
+ let resultType = { kind: "NamedType", name: "void", typeArgs: null, line: taskTok.line, col: taskTok.col };
625
637
  if (this.match(TokenKind.Lt)) {
626
638
  resultType = this.parseType();
627
639
  this.consume(TokenKind.Gt, "Expected '>' after task result type");
@@ -637,7 +649,17 @@ export class Parser {
637
649
  }
638
650
  else {
639
651
  const nameToken = this.check(TokenKind.Void) ? this.advance() : this.consume(TokenKind.Identifier, "Expected type name");
640
- type = { kind: "NamedType", name: nameToken.lexeme, line: nameToken.line, col: nameToken.col };
652
+ // `Box<number>` a generic type reference. At most one type argument
653
+ // in v1 (matching ClassDecl/InterfaceDecl's single type parameter);
654
+ // whether `name` actually refers to a declared generic type is a
655
+ // semantic question the checker answers, not the parser.
656
+ let typeArgs = null;
657
+ if (this.check(TokenKind.Lt)) {
658
+ this.advance();
659
+ typeArgs = [this.parseType()];
660
+ this.consume(TokenKind.Gt, "Expected '>' after type argument");
661
+ }
662
+ type = { kind: "NamedType", name: nameToken.lexeme, typeArgs, line: nameToken.line, col: nameToken.col };
641
663
  }
642
664
  // `[]` and `?` are both postfix and can alternate in either order —
643
665
  // `string?[]` (array of nullable strings) and `string[]?` (nullable
@@ -815,6 +837,15 @@ export class Parser {
815
837
  if (this.check(TokenKind.New)) {
816
838
  this.advance();
817
839
  const className = this.consume(TokenKind.Identifier, "Expected class name after 'new'").lexeme;
840
+ // `new Box<number>(...)` — unambiguous here: `new <Identifier>` is
841
+ // always followed by `(`, generic type args or not, so seeing `<`
842
+ // instead can only mean a type argument list, never a comparison.
843
+ let typeArgs = null;
844
+ if (this.check(TokenKind.Lt)) {
845
+ this.advance();
846
+ typeArgs = [this.parseType()];
847
+ this.consume(TokenKind.Gt, "Expected '>' after type argument");
848
+ }
818
849
  this.consume(TokenKind.LParen, "Expected '(' after class name");
819
850
  const args = [];
820
851
  if (!this.check(TokenKind.RParen)) {
@@ -823,7 +854,7 @@ export class Parser {
823
854
  } while (this.match(TokenKind.Comma));
824
855
  }
825
856
  this.consume(TokenKind.RParen, "Expected ')' after constructor arguments");
826
- return { kind: "NewExpr", className, args, line: t.line, col: t.col };
857
+ return { kind: "NewExpr", className, typeArgs, args, line: t.line, col: t.col };
827
858
  }
828
859
  if (this.check(TokenKind.LBracket)) {
829
860
  this.advance();
@@ -861,36 +892,39 @@ export class Parser {
861
892
  this.diagnostics.error(`Unexpected token '${t.lexeme || t.kind}'`, t.line, t.col);
862
893
  throw new ParseError();
863
894
  }
864
- // Lightweight lookahead (not a trial parse) distinguishing a lambda's
865
- // parameter list from a parenthesized expression. A lambda parameter is
866
- // always `Type name` (types are required, no inference in v1), so seeing
867
- // two consecutive identifiers — or an empty `()` followed by `=>` — is
868
- // unambiguous. This doesn't handle a lambda whose own first parameter is
869
- // itself a function type (e.g. a higher-order lambda); that's a known v1
870
- // gap in favor of keeping this check cheap.
895
+ // Distinguishes a lambda's parameter list from a parenthesized expression.
896
+ // A lambda parameter is always `Type name` (types are required, no
897
+ // inference in v1), so seeing a full type followed by an identifier — or
898
+ // an empty `()` followed by `=>` — is unambiguous. Uses a real speculative
899
+ // parse of the candidate type (same save-position/try/rollback shape as
900
+ // isDeclStart) rather than hand-rolled token lookahead, so it stays
901
+ // correct as parseType grows new postfix/prefix forms (`?`, `<T>`, ...)
902
+ // without needing a matching update here every time. This doesn't handle
903
+ // a lambda whose own first parameter is itself a function type (e.g. a
904
+ // higher-order lambda); that's a known v1 gap in favor of keeping this
905
+ // check simple.
871
906
  looksLikeLambda() {
872
- let i = this.pos + 1; // just past '('
907
+ const i = this.pos + 1; // just past '('
873
908
  if (this.tokens[i]?.kind === TokenKind.RParen) {
874
909
  return this.tokens[i + 1]?.kind === TokenKind.Arrow;
875
910
  }
876
911
  const startKind = this.tokens[i]?.kind;
877
912
  if (startKind !== TokenKind.Identifier && startKind !== TokenKind.Void)
878
913
  return false;
879
- i++;
880
- // Mirrors parseType's postfix loop: `[]` and `?` can alternate in
881
- // either order (`string?[]`, `string[]?`) after the base type name.
882
- while (true) {
883
- if (this.tokens[i]?.kind === TokenKind.LBracket && this.tokens[i + 1]?.kind === TokenKind.RBracket) {
884
- i += 2;
885
- }
886
- else if (this.tokens[i]?.kind === TokenKind.Question) {
887
- i += 1;
888
- }
889
- else {
890
- break;
891
- }
914
+ const savedPos = this.pos;
915
+ const savedDiagnosticsLength = this.diagnostics.diagnostics.length;
916
+ this.pos = i;
917
+ let result;
918
+ try {
919
+ this.parseType();
920
+ result = this.check(TokenKind.Identifier);
892
921
  }
893
- return this.tokens[i]?.kind === TokenKind.Identifier;
922
+ catch {
923
+ result = false;
924
+ }
925
+ this.pos = savedPos;
926
+ this.diagnostics.diagnostics.length = savedDiagnosticsLength;
927
+ return result;
894
928
  }
895
929
  parseLambda() {
896
930
  const start = this.peek(); // '('
package/dist/types.js CHANGED
@@ -6,11 +6,14 @@ export const UNKNOWN = { kind: "unknown" };
6
6
  export function arrayOf(element) {
7
7
  return { kind: "array", element };
8
8
  }
9
- export function classType(name) {
10
- return { kind: "class", name };
9
+ export function classType(name, typeArg) {
10
+ return typeArg ? { kind: "class", name, typeArg } : { kind: "class", name };
11
11
  }
12
- export function interfaceType(name) {
13
- return { kind: "interface", name };
12
+ export function interfaceType(name, typeArg) {
13
+ return typeArg ? { kind: "interface", name, typeArg } : { kind: "interface", name };
14
+ }
15
+ export function typeParamType(name) {
16
+ return { kind: "typeParam", name };
14
17
  }
15
18
  export function enumType(name) {
16
19
  return { kind: "enum", name };
@@ -37,10 +40,11 @@ export function typeToString(t) {
37
40
  return t.kind;
38
41
  case "array":
39
42
  return `${typeToString(t.element)}[]`;
40
- case "class":
41
- case "interface":
42
43
  case "enum":
43
44
  return t.name;
45
+ case "class":
46
+ case "interface":
47
+ return t.typeArg ? `${t.name}<${typeToString(t.typeArg)}>` : t.name;
44
48
  case "function":
45
49
  return `(${t.params.map(typeToString).join(", ")}) => ${typeToString(t.returnType)}`;
46
50
  case "task":
@@ -49,6 +53,8 @@ export function typeToString(t) {
49
53
  return `state<${typeToString(t.valueType)}>`;
50
54
  case "nullable":
51
55
  return `${typeToString(t.inner)}?`;
56
+ case "typeParam":
57
+ return t.name;
52
58
  }
53
59
  }
54
60
  const PRIMITIVE_NAMES = new Set(["number", "string", "bool", "void"]);
@@ -85,13 +91,33 @@ export function resolveTypeNode(node, namedTypes, onNamedType) {
85
91
  const inner = resolveTypeNode(node.inner, namedTypes, onNamedType);
86
92
  return inner ? nullableOf(inner) : null;
87
93
  }
94
+ // At most one type argument in v1 — resolve 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;
100
+ if (node.typeArgs) {
101
+ typeArg = resolveTypeNode(node.typeArgs[0], namedTypes, onNamedType);
102
+ if (!typeArg)
103
+ return null;
104
+ }
88
105
  let resolved;
89
106
  if (PRIMITIVE_NAMES.has(node.name)) {
90
- resolved = { kind: node.name };
107
+ resolved = typeArg ? null : { kind: node.name };
91
108
  }
92
109
  else {
93
110
  const kind = namedTypes.get(node.name);
94
- resolved = kind === "class" ? classType(node.name) : kind === "interface" ? interfaceType(node.name) : kind === "enum" ? enumType(node.name) : null;
111
+ if (kind === "class")
112
+ resolved = classType(node.name, typeArg ?? undefined);
113
+ else if (kind === "interface")
114
+ resolved = interfaceType(node.name, typeArg ?? undefined);
115
+ else if (kind === "enum")
116
+ resolved = typeArg ? null : enumType(node.name);
117
+ else if (kind === "typeParam")
118
+ resolved = typeArg ? null : typeParamType(node.name);
119
+ else
120
+ resolved = null;
95
121
  }
96
122
  if (resolved)
97
123
  onNamedType?.(node, resolved);
@@ -116,8 +142,25 @@ export function typesEqual(a, b) {
116
142
  if (a.kind === "nullable" && b.kind === "nullable") {
117
143
  return typesEqual(a.inner, b.inner);
118
144
  }
119
- if ((a.kind === "class" || a.kind === "interface" || a.kind === "enum") && "name" in b) {
145
+ if (a.kind === "typeParam" && b.kind === "typeParam") {
120
146
  return a.name === b.name;
121
147
  }
148
+ // Invariant generics: Box<number> and Box<string> are unrelated types,
149
+ // and so are Box<number> and bare (non-generic-reference) Box — the
150
+ // latter only arises from an arity error already diagnosed elsewhere, so
151
+ // treating it as unequal here (rather than papering over it) is right.
152
+ if ((a.kind === "class" || a.kind === "interface" || a.kind === "enum") && "name" in b) {
153
+ const other = b;
154
+ if (a.name !== other.name)
155
+ return false;
156
+ if ("typeArg" in a || "typeArg" in other) {
157
+ const aArg = a.typeArg;
158
+ const bArg = other.typeArg;
159
+ if (!aArg || !bArg)
160
+ return false;
161
+ return typesEqual(aArg, bArg);
162
+ }
163
+ return true;
164
+ }
122
165
  return true;
123
166
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.3.0",
4
- "description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript",
3
+ "version": "0.4.1",
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",
7
7
  "author": "Joe Koppin <koppinjo@gmail.com>",