kopscript 0.3.0 → 0.4.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 +86 -6
- package/README.md +56 -11
- package/dist/checker.js +245 -43
- package/dist/codegen.js +13 -2
- package/dist/parser.js +61 -27
- package/dist/types.js +52 -9
- package/package.json +2 -2
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**:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
@@ -250,8 +258,54 @@ count.Value = count.Value + 1;
|
|
|
250
258
|
count.Subscribe((number v) => { ... }); // called on every future .Value assignment
|
|
251
259
|
```
|
|
252
260
|
|
|
253
|
-
Not a
|
|
254
|
-
forms in the type system (the other is `task<T>`)
|
|
261
|
+
Not a use of user-declared generics (see below) — `state<T>` is one of exactly two
|
|
262
|
+
hardcoded parametrized forms in the type system (the other is `task<T>`), each with only
|
|
263
|
+
ever one built-in meaning, unlike a real generic class/interface.
|
|
264
|
+
|
|
265
|
+
## Generics — a single type parameter on classes/interfaces
|
|
266
|
+
|
|
267
|
+
```ks
|
|
268
|
+
class Box<T> {
|
|
269
|
+
public T Value;
|
|
270
|
+
constructor(T v) { this.Value = v; }
|
|
271
|
+
public T Get() { return this.Value; }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Box<number> nb = new Box<number>(5); // type argument required on both the type AND `new`
|
|
275
|
+
Box<string> sb = new Box<string>("hi");
|
|
276
|
+
print(nb.Get()); // 5
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
One unconstrained, invariant type parameter per class/interface — the whole feature.
|
|
280
|
+
Erased at codegen (JS has no generics either; `Box<number>` and `Box<string>` both
|
|
281
|
+
compile to the exact same plain `class Box`), so there's no runtime cost and no way to
|
|
282
|
+
inspect `T` at runtime.
|
|
283
|
+
|
|
284
|
+
**Rules:**
|
|
285
|
+
- The type argument is **required everywhere** a generic type is named — on the variable
|
|
286
|
+
type (`Box<number>`) and separately on `new` (`new Box<number>(...)`); a bare `Box` (no
|
|
287
|
+
argument) is a compile error, not "any"/inferred.
|
|
288
|
+
- **Invariant**: `Box<Dog>` is not assignable to `Box<Animal>` even if `Dog : Animal` —
|
|
289
|
+
type arguments must match exactly, not just be compatible.
|
|
290
|
+
- Nesting works: `Box<Box<number>>`, `Box<number[]>`, `Box<number>?`, `Box<number>?[]`.
|
|
291
|
+
- Inside the class/interface's own body, `T` is fully abstract — you can store it, return
|
|
292
|
+
it, pass it around, but (unconstrained) you **cannot call any member on a bare `T`
|
|
293
|
+
value** (`this.value.Foo()` is a compile error: "Cannot access member 'Foo' on type
|
|
294
|
+
'T'"). This is correct, not a bug — same as an unconstrained type parameter in
|
|
295
|
+
C#/Java/TypeScript.
|
|
296
|
+
|
|
297
|
+
**Does not exist (v1 scope cuts, each deliberate)**:
|
|
298
|
+
- **Generic functions.** `T Identity<T>(T x)` doesn't parse — only classes and interfaces
|
|
299
|
+
take a type parameter, not free functions/methods themselves (a method *inside* a
|
|
300
|
+
generic class can use that class's own `T` freely, same as any other member).
|
|
301
|
+
- **Multiple type parameters.** No `Map<K, V>` — exactly one `<T>` or none.
|
|
302
|
+
- **Constraints.** No `T : IComparable` — a type parameter is always fully unconstrained,
|
|
303
|
+
which is why you can't call members on a bare `T` (see above).
|
|
304
|
+
- **Generic inheritance.** A class/interface's base list can only name a *non-generic*
|
|
305
|
+
type — `class Foo : Box<number>` and even `class Foo<T> : SomeGenericBase<T>` are both
|
|
306
|
+
compile errors ("cannot extend/implement generic type '...' — not supported in v1"). A
|
|
307
|
+
generic class/interface can still extend/implement ordinary non-generic bases normally.
|
|
308
|
+
- **Variance.** No `out`/`in`/covariance/contravariance — see invariance above.
|
|
255
309
|
|
|
256
310
|
## Nullable types — `T?`
|
|
257
311
|
|
|
@@ -328,6 +382,27 @@ KopScript-declared name exactly. Extern class members use real JS member names v
|
|
|
328
382
|
(camelCase, no rename mechanism). No inheritance modeling between two `extern class`
|
|
329
383
|
declarations — each stands alone.
|
|
330
384
|
|
|
385
|
+
**Never write `async` on an extern function/method signature** — declare its return type
|
|
386
|
+
as `task`/`task<T>` directly (`task<string> text();`, not `async task<string> text();`).
|
|
387
|
+
`async` only means something for a *body* the checker validates (legalizing `await`
|
|
388
|
+
inside it, checking `return` against the unwrapped type); an extern signature has no
|
|
389
|
+
body, so the keyword is simply invalid there — a parse error, not a no-op. This applies
|
|
390
|
+
equally to a top-level `extern` function and an `extern class` method signature.
|
|
391
|
+
|
|
392
|
+
`jsName` (the `as "..."` string) can be a dotted path, not just a bare identifier — e.g.
|
|
393
|
+
`as "JSON.parse"` — since it's spliced directly into `globalThis.<jsName>` for an
|
|
394
|
+
ambient binding. This is the supported way to get a *typed* (unchecked, trust-based)
|
|
395
|
+
JSON value: describe the shape as its own `extern class`, then declare a parse function
|
|
396
|
+
for it: `extern MyShape ParseIt(string json) as "JSON.parse";` — calling `ParseIt(text)`
|
|
397
|
+
returns whatever `JSON.parse` actually parsed, typed as `MyShape` with zero runtime
|
|
398
|
+
verification (the same trust model as every other `extern`).
|
|
399
|
+
|
|
400
|
+
An `extern class` reachable from a real npm package — not same-project `using`, which
|
|
401
|
+
only resolves relative `.ks` paths — is how KopScript consumes *any* JS dependency,
|
|
402
|
+
including another KopScript-authored package published as JS (e.g. Kopular): describe
|
|
403
|
+
exactly the members used, from that package's real module specifier. See Kopular's own
|
|
404
|
+
`LLM.md`/README for a live example (`Component`, `Router`, `Http`, ...).
|
|
405
|
+
|
|
331
406
|
## `raw string` — compile-time file embedding
|
|
332
407
|
|
|
333
408
|
```ks
|
|
@@ -354,6 +429,11 @@ Locals/params: **camelCase**. This is convention, not enforced by the compiler.
|
|
|
354
429
|
Generics · `any`/`unknown` annotations ·
|
|
355
430
|
a `let`/`var` keyword (locals are just `Type name = value;`) · decorators/annotations ·
|
|
356
431
|
reflection · type inference on declarations · ternary expression · union/tuple types ·
|
|
432
|
+
**object-literal syntax** (`{ key: value }` as a value — this doesn't exist *anywhere* in
|
|
433
|
+
the grammar, not just as a type; a JS API whose signature needs one — `fetch`'s options
|
|
434
|
+
argument, `addEventListener`'s options object, etc. — can't be called directly from
|
|
435
|
+
KopScript at all; see Kopular's `http.ks`/`http_runtime.js` for the one established
|
|
436
|
+
workaround, a small hand-written JS shim, not a language feature) ·
|
|
357
437
|
structural typing · multiple class inheritance ·
|
|
358
438
|
nested classes/functions · method overloading · default/optional parameters · varargs ·
|
|
359
439
|
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`),
|
|
525
|
-
compiler-enforced null-checking — see "Nullable types" below)
|
|
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?`)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
variance,
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1225,6 +1383,7 @@ export class Checker {
|
|
|
1225
1383
|
if (expr.callee.property === "Map") {
|
|
1226
1384
|
const objectType = this.checkExpression(expr.callee.object, scope, ctx);
|
|
1227
1385
|
if (objectType.kind === "array") {
|
|
1386
|
+
expr.callee.isBuiltin = true; // see checkMemberInner's string/array branches
|
|
1228
1387
|
return this.checkArrayMap(expr, objectType.element, scope, ctx);
|
|
1229
1388
|
}
|
|
1230
1389
|
}
|
|
@@ -1283,8 +1442,31 @@ export class Checker {
|
|
|
1283
1442
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1284
1443
|
return T.UNKNOWN;
|
|
1285
1444
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1445
|
+
let typeArg;
|
|
1446
|
+
if (info.typeParam && !expr.typeArgs) {
|
|
1447
|
+
this.diagnostics.error(`Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
|
|
1448
|
+
// Abstractly-typed (T-containing) ctor params, un-substitutable
|
|
1449
|
+
// without a real type argument, would otherwise cascade into a
|
|
1450
|
+
// confusing "expected 'T'" error on every argument — one clear error
|
|
1451
|
+
// beats that pile-on.
|
|
1452
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1453
|
+
return T.UNKNOWN;
|
|
1454
|
+
}
|
|
1455
|
+
if (!info.typeParam && expr.typeArgs) {
|
|
1456
|
+
this.diagnostics.error(`Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
|
|
1457
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1458
|
+
return T.UNKNOWN;
|
|
1459
|
+
}
|
|
1460
|
+
if (info.typeParam && expr.typeArgs) {
|
|
1461
|
+
typeArg = this.resolveType(expr.typeArgs[0], expr.line, expr.col);
|
|
1462
|
+
}
|
|
1463
|
+
this.recordHover(expr.line, expr.col, typeArg ? `class ${expr.className}<${T.typeToString(typeArg)}>` : `class ${expr.className}`);
|
|
1464
|
+
let ctorParams = this.lookupCtorParams(expr.className);
|
|
1465
|
+
if (info.typeParam && typeArg) {
|
|
1466
|
+
const paramName = info.typeParam;
|
|
1467
|
+
const arg = typeArg;
|
|
1468
|
+
ctorParams = ctorParams.map((p) => this.substituteTypeParam(p, paramName, arg));
|
|
1469
|
+
}
|
|
1288
1470
|
if (expr.args.length !== ctorParams.length) {
|
|
1289
1471
|
this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1290
1472
|
}
|
|
@@ -1295,7 +1477,7 @@ export class Checker {
|
|
|
1295
1477
|
this.diagnostics.error(`Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1296
1478
|
}
|
|
1297
1479
|
});
|
|
1298
|
-
return T.classType(expr.className);
|
|
1480
|
+
return T.classType(expr.className, typeArg);
|
|
1299
1481
|
}
|
|
1300
1482
|
lookupCtorParams(className) {
|
|
1301
1483
|
let current = className;
|
|
@@ -1399,6 +1581,11 @@ export class Checker {
|
|
|
1399
1581
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1400
1582
|
}
|
|
1401
1583
|
if (objectType.kind === "string") {
|
|
1584
|
+
// Marks this specific access as resolved against string's own
|
|
1585
|
+
// built-ins, not some user member that merely shares the name —
|
|
1586
|
+
// codegen's PascalCase -> camelCase renames rely on this, since they
|
|
1587
|
+
// have no type information of their own to tell the two apart.
|
|
1588
|
+
expr.isBuiltin = true;
|
|
1402
1589
|
if (expr.property === "Length")
|
|
1403
1590
|
return { type: T.NUMBER, methodInfo: null };
|
|
1404
1591
|
const method = STRING_METHODS[expr.property];
|
|
@@ -1408,6 +1595,7 @@ export class Checker {
|
|
|
1408
1595
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1409
1596
|
}
|
|
1410
1597
|
if (objectType.kind === "array") {
|
|
1598
|
+
expr.isBuiltin = true; // see the string branch above
|
|
1411
1599
|
if (expr.property === "Length")
|
|
1412
1600
|
return { type: T.NUMBER, methodInfo: null };
|
|
1413
1601
|
const method = this.arrayMethod(objectType.element, expr.property);
|
|
@@ -1439,6 +1627,12 @@ export class Checker {
|
|
|
1439
1627
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1440
1628
|
}
|
|
1441
1629
|
if (objectType.kind === "class") {
|
|
1630
|
+
// v1 has no generic inheritance (a generic class's base list can only
|
|
1631
|
+
// name non-generic types), so a member found on a generic instance
|
|
1632
|
+
// was always declared directly on that same class — substituting by
|
|
1633
|
+
// its own type parameter, not some ancestor's, is always correct.
|
|
1634
|
+
const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
|
|
1635
|
+
const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
|
|
1442
1636
|
const field = this.lookupField(objectType.name, expr.property);
|
|
1443
1637
|
if (field) {
|
|
1444
1638
|
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
@@ -1448,12 +1642,13 @@ export class Checker {
|
|
|
1448
1642
|
this.diagnostics.error(`'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
|
|
1449
1643
|
}
|
|
1450
1644
|
}
|
|
1451
|
-
return { type: field.info.type, methodInfo: null };
|
|
1645
|
+
return { type: substitute(field.info.type), methodInfo: null };
|
|
1452
1646
|
}
|
|
1453
1647
|
const method = this.lookupMethod(objectType.name, expr.property);
|
|
1454
1648
|
if (method) {
|
|
1455
1649
|
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
1456
|
-
|
|
1650
|
+
const info = paramName ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
|
|
1651
|
+
return { type: info.returnType, methodInfo: info };
|
|
1457
1652
|
}
|
|
1458
1653
|
this.diagnostics.error(`Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1459
1654
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
@@ -1461,7 +1656,14 @@ export class Checker {
|
|
|
1461
1656
|
if (objectType.kind === "interface") {
|
|
1462
1657
|
const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
|
|
1463
1658
|
if (sig) {
|
|
1464
|
-
|
|
1659
|
+
// v1 has no generic interface inheritance either (same restriction
|
|
1660
|
+
// as classes — see registerInterface), so a signature found here
|
|
1661
|
+
// was always declared directly on this same interface.
|
|
1662
|
+
const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
|
|
1663
|
+
const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
|
|
1664
|
+
const params = sig.params.map(substitute);
|
|
1665
|
+
const returnType = substitute(sig.returnType);
|
|
1666
|
+
return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
|
|
1465
1667
|
}
|
|
1466
1668
|
this.diagnostics.error(`Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1467
1669
|
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
|
-
|
|
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/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
|
-
|
|
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
|
-
//
|
|
865
|
-
// parameter
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
4
|
-
"description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript",
|
|
3
|
+
"version": "0.4.0",
|
|
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>",
|