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