kopscript 0.11.1 → 0.13.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 +63 -34
- package/README.md +55 -31
- package/dist/checker.js +325 -161
- package/dist/codegen.js +1 -1
- package/dist/parser.js +51 -24
- package/dist/printer.js +8 -5
- package/dist/types.js +29 -24
- package/package.json +1 -1
package/dist/checker.js
CHANGED
|
@@ -56,11 +56,13 @@ export class Checker {
|
|
|
56
56
|
this.functions = new Map();
|
|
57
57
|
this.externValues = new Map();
|
|
58
58
|
this.namedTypes = new Map();
|
|
59
|
-
// className/interfaceName -> its type
|
|
60
|
-
// for every generic class/interface —
|
|
61
|
-
// resolution runs, specifically so a
|
|
62
|
-
// referencing generic class B, declared
|
|
63
|
-
// resolves B's genericity correctly.
|
|
59
|
+
// className/interfaceName -> its type parameters' names, in declared order
|
|
60
|
+
// (e.g. "Pair" -> ["K", "V"]), for every generic class/interface —
|
|
61
|
+
// populated up front, before any type resolution runs, specifically so a
|
|
62
|
+
// forward reference (class A's field referencing generic class B, declared
|
|
63
|
+
// later in the same file) still resolves B's genericity correctly. Also
|
|
64
|
+
// doubles as the arity table (its length is how many type arguments a
|
|
65
|
+
// reference to that name must supply). See resolveType/validateGenericArity.
|
|
64
66
|
this.genericTypeParams = new Map();
|
|
65
67
|
this.importedNames = new Set();
|
|
66
68
|
this.rawContents = new Map();
|
|
@@ -73,13 +75,13 @@ export class Checker {
|
|
|
73
75
|
}
|
|
74
76
|
for (const [name, info] of this.imports.classes) {
|
|
75
77
|
this.classes.set(name, info);
|
|
76
|
-
if (info.
|
|
77
|
-
this.genericTypeParams.set(name, info.
|
|
78
|
+
if (info.typeParams.length > 0)
|
|
79
|
+
this.genericTypeParams.set(name, info.typeParams);
|
|
78
80
|
}
|
|
79
81
|
for (const [name, info] of this.imports.interfaces) {
|
|
80
82
|
this.interfaces.set(name, info);
|
|
81
|
-
if (info.
|
|
82
|
-
this.genericTypeParams.set(name, info.
|
|
83
|
+
if (info.typeParams.length > 0)
|
|
84
|
+
this.genericTypeParams.set(name, info.typeParams);
|
|
83
85
|
}
|
|
84
86
|
for (const [name, info] of this.imports.enums)
|
|
85
87
|
this.enums.set(name, info);
|
|
@@ -109,14 +111,14 @@ export class Checker {
|
|
|
109
111
|
for (const c of externClassDecls)
|
|
110
112
|
this.namedTypes.set(c.name, "class");
|
|
111
113
|
for (const c of classDecls)
|
|
112
|
-
if (c.
|
|
113
|
-
this.genericTypeParams.set(c.name, c.
|
|
114
|
+
if (c.typeParams.length > 0)
|
|
115
|
+
this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
|
|
114
116
|
for (const i of interfaceDecls)
|
|
115
|
-
if (i.
|
|
116
|
-
this.genericTypeParams.set(i.name, i.
|
|
117
|
+
if (i.typeParams.length > 0)
|
|
118
|
+
this.genericTypeParams.set(i.name, i.typeParams.map((p) => p.name));
|
|
117
119
|
for (const c of externClassDecls)
|
|
118
|
-
if (c.
|
|
119
|
-
this.genericTypeParams.set(c.name, c.
|
|
120
|
+
if (c.typeParams.length > 0)
|
|
121
|
+
this.genericTypeParams.set(c.name, c.typeParams.map((p) => p.name));
|
|
120
122
|
for (const e of enumDecls)
|
|
121
123
|
this.registerEnum(e);
|
|
122
124
|
for (const i of interfaceDecls)
|
|
@@ -172,11 +174,11 @@ export class Checker {
|
|
|
172
174
|
});
|
|
173
175
|
}
|
|
174
176
|
registerExternClass(decl) {
|
|
175
|
-
// Same
|
|
176
|
-
// signature (fields/methods/ctor params) — an extern class's own
|
|
177
|
-
//
|
|
178
|
-
// resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
|
|
179
|
-
const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.
|
|
177
|
+
// Same withTypeParamsInScope treatment registerClass gives a real class's
|
|
178
|
+
// signature (fields/methods/ctor params) — an extern class's own type
|
|
179
|
+
// params need to resolve the same way while its declared members are
|
|
180
|
+
// being resolved, e.g. `state<T> Value;` in an `extern class FormField<T>`.
|
|
181
|
+
const { fields, staticFields, methods, staticMethods, ownCtorParams } = this.withTypeParamsInScope(decl.typeParams, () => {
|
|
180
182
|
const fields = new Map();
|
|
181
183
|
const staticFields = new Map();
|
|
182
184
|
for (const p of decl.properties) {
|
|
@@ -200,9 +202,11 @@ export class Checker {
|
|
|
200
202
|
});
|
|
201
203
|
this.classes.set(decl.name, {
|
|
202
204
|
name: decl.name,
|
|
203
|
-
|
|
205
|
+
typeParams: decl.typeParams.map((p) => p.name),
|
|
204
206
|
superclass: null,
|
|
207
|
+
superclassTypeArgs: null,
|
|
205
208
|
interfaces: [],
|
|
209
|
+
interfaceTypeArgs: new Map(),
|
|
206
210
|
fields,
|
|
207
211
|
methods,
|
|
208
212
|
staticFields,
|
|
@@ -341,8 +345,9 @@ export class Checker {
|
|
|
341
345
|
});
|
|
342
346
|
if (!resolved) {
|
|
343
347
|
const name = node.kind === "NamedType" ? node.name : "[]";
|
|
344
|
-
|
|
345
|
-
|
|
348
|
+
const declaredParams = this.genericTypeParams.get(name);
|
|
349
|
+
if (name !== "[]" && declaredParams && !node.typeArgs) {
|
|
350
|
+
this.diagnostics.error("KS4007", `Generic type '${name}' requires ${this.describeArity(declaredParams)} (e.g. '${name}<${declaredParams.join(", ")}>')`, line, col);
|
|
346
351
|
}
|
|
347
352
|
else {
|
|
348
353
|
this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col);
|
|
@@ -378,74 +383,137 @@ export class Checker {
|
|
|
378
383
|
return;
|
|
379
384
|
case "class":
|
|
380
385
|
case "interface": {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
const declaredParams = this.genericTypeParams.get(type.name);
|
|
387
|
+
const suppliedCount = type.typeArgs?.length ?? 0;
|
|
388
|
+
if (declaredParams && suppliedCount === 0) {
|
|
389
|
+
this.diagnostics.error("KS4009", `Generic type '${type.name}' requires ${this.describeArity(declaredParams)} (e.g. '${type.name}<${declaredParams.join(", ")}>')`, line, col);
|
|
384
390
|
}
|
|
385
|
-
else if (!
|
|
391
|
+
else if (!declaredParams && suppliedCount > 0) {
|
|
386
392
|
this.diagnostics.error("KS4010", `Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
|
|
387
393
|
}
|
|
388
|
-
if (
|
|
389
|
-
this.
|
|
394
|
+
else if (declaredParams && suppliedCount > 0 && suppliedCount !== declaredParams.length) {
|
|
395
|
+
this.diagnostics.error("KS4090", `Type '${type.name}' expects ${this.describeArity(declaredParams)}, got ${suppliedCount}`, line, col);
|
|
396
|
+
}
|
|
397
|
+
type.typeArgs?.forEach((a) => this.validateGenericArity(a, line, col));
|
|
390
398
|
return;
|
|
391
399
|
}
|
|
392
400
|
default:
|
|
393
401
|
return;
|
|
394
402
|
}
|
|
395
403
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
describeArity(params) {
|
|
405
|
+
return params.length === 1 ? "a type argument" : `${params.length} type arguments`;
|
|
406
|
+
}
|
|
407
|
+
// Temporarily makes each of `typeParams` (a class/interface/function's own
|
|
408
|
+
// type parameters, e.g. ["K", "V"]) resolve as a TypeParamType, for the
|
|
409
|
+
// duration of `fn` — used only while registering that declaration's own
|
|
410
|
+
// signature (fields, methods, ctor/function params), so `T Value;`/`K
|
|
411
|
+
// key;` resolve correctly. Scoped this narrowly (set, run, delete/
|
|
412
|
+
// restore) rather than left in `namedTypes` permanently, since it's only
|
|
413
|
+
// ever meaningful inside that one declaration's own body.
|
|
414
|
+
withTypeParamsInScope(typeParams, fn) {
|
|
415
|
+
if (typeParams.length === 0)
|
|
405
416
|
return fn();
|
|
406
|
-
const previous = this.namedTypes.get(
|
|
407
|
-
|
|
417
|
+
const previous = typeParams.map((p) => this.namedTypes.get(p.name));
|
|
418
|
+
for (const p of typeParams)
|
|
419
|
+
this.namedTypes.set(p.name, "typeParam");
|
|
408
420
|
try {
|
|
409
421
|
return fn();
|
|
410
422
|
}
|
|
411
423
|
finally {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
424
|
+
typeParams.forEach((p, i) => {
|
|
425
|
+
const prev = previous[i];
|
|
426
|
+
if (prev === undefined)
|
|
427
|
+
this.namedTypes.delete(p.name);
|
|
428
|
+
else
|
|
429
|
+
this.namedTypes.set(p.name, prev);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Replaces every occurrence of a type parameter named in `bindings`
|
|
434
|
+
// (mapping name -> concrete Type) inside `type` — the substitution step
|
|
435
|
+
// that turns Pair<K, V>'s abstractly-stored field type `K` into `number`
|
|
436
|
+
// when someone actually asks about `Pair<number, string>.First`. A type
|
|
437
|
+
// with none of `bindings`' names occurring anywhere in it is returned
|
|
438
|
+
// unchanged (including every non-generic type, the overwhelming majority).
|
|
439
|
+
substituteTypeParams(type, bindings) {
|
|
425
440
|
switch (type.kind) {
|
|
426
441
|
case "typeParam":
|
|
427
|
-
return type.name
|
|
442
|
+
return bindings.get(type.name) ?? type;
|
|
428
443
|
case "array":
|
|
429
|
-
return T.arrayOf(this.
|
|
444
|
+
return T.arrayOf(this.substituteTypeParams(type.element, bindings));
|
|
430
445
|
case "nullable":
|
|
431
|
-
return T.nullableOf(this.
|
|
446
|
+
return T.nullableOf(this.substituteTypeParams(type.inner, bindings));
|
|
432
447
|
case "task":
|
|
433
|
-
return T.taskType(this.
|
|
448
|
+
return T.taskType(this.substituteTypeParams(type.resultType, bindings));
|
|
434
449
|
case "state":
|
|
435
|
-
return T.stateType(this.
|
|
450
|
+
return T.stateType(this.substituteTypeParams(type.valueType, bindings));
|
|
436
451
|
case "function":
|
|
437
|
-
return T.functionType(type.params.map((p) => this.
|
|
438
|
-
// A nested generic's own type
|
|
439
|
-
//
|
|
440
|
-
// to handle correctly): substitute inside
|
|
452
|
+
return T.functionType(type.params.map((p) => this.substituteTypeParams(p, bindings)), this.substituteTypeParams(type.returnType, bindings));
|
|
453
|
+
// A nested generic's own type arguments can themselves mention an
|
|
454
|
+
// outer type param (`Pair<K, V>`'s field being List<K> — a v1 corner
|
|
455
|
+
// case, but cheap to handle correctly): substitute inside each.
|
|
441
456
|
case "class":
|
|
442
|
-
return type.
|
|
457
|
+
return type.typeArgs ? T.classType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
|
|
443
458
|
case "interface":
|
|
444
|
-
return type.
|
|
459
|
+
return type.typeArgs ? T.interfaceType(type.name, type.typeArgs.map((a) => this.substituteTypeParams(a, bindings))) : type;
|
|
445
460
|
default:
|
|
446
461
|
return type;
|
|
447
462
|
}
|
|
448
463
|
}
|
|
464
|
+
// Builds the bindings map for a generic reference (a `ClassType`/
|
|
465
|
+
// `InterfaceType`'s own `typeArgs`, zipped against its declared
|
|
466
|
+
// `typeParams`) — the "own bindings" every member-lookup/conformance
|
|
467
|
+
// check starts from. Null when the reference isn't actually generic
|
|
468
|
+
// (no declared params) or carries no type arguments (an arity error
|
|
469
|
+
// already diagnosed elsewhere — see validateGenericArity).
|
|
470
|
+
ownBindings(typeParams, typeArgs) {
|
|
471
|
+
if (typeParams.length === 0 || !typeArgs)
|
|
472
|
+
return null;
|
|
473
|
+
return new Map(typeParams.map((name, i) => [name, typeArgs[i]]));
|
|
474
|
+
}
|
|
475
|
+
// Maps `typeParams` to themselves, as abstract TypeParamTypes — used when
|
|
476
|
+
// resolving a base-list/interface type argument that may reference the
|
|
477
|
+
// *declaring* class/interface's own (still-abstract) type parameter,
|
|
478
|
+
// rather than a concrete instantiation (see checkInterfaceConformance and
|
|
479
|
+
// the base(...) call check in checkClassBodyInner).
|
|
480
|
+
identityBindings(typeParams) {
|
|
481
|
+
return new Map(typeParams.map((name) => [name, T.typeParamType(name)]));
|
|
482
|
+
}
|
|
483
|
+
// One step of substitution composition, walking from a class/interface
|
|
484
|
+
// ("owner") to one of its own generic base-list entries. `baseArgs` are
|
|
485
|
+
// the type arguments `owner` supplied to `baseName` in its own base list
|
|
486
|
+
// — still in `owner`'s own type-param-name space (may reference `owner`'s
|
|
487
|
+
// own type parameters, not yet concrete). `ownerBindings` is `owner`'s
|
|
488
|
+
// current bindings at the point of this lookup. Returns `baseName`'s own
|
|
489
|
+
// bindings map (its declared type-param names -> resolved types), ready
|
|
490
|
+
// for the next composition step or a final substituteTypeParams call.
|
|
491
|
+
composeBaseBindings(baseName, baseArgs, ownerBindings) {
|
|
492
|
+
const baseParams = this.genericTypeParams.get(baseName) ?? [];
|
|
493
|
+
const resolvedArgs = baseArgs.map((t) => this.substituteTypeParams(t, ownerBindings));
|
|
494
|
+
return new Map(baseParams.map((name, i) => [name, resolvedArgs[i]]));
|
|
495
|
+
}
|
|
496
|
+
// Starting from `className` with its own bindings (from ownBindings,
|
|
497
|
+
// above — empty if non-generic or no type arguments), walks up the
|
|
498
|
+
// superclass chain composing each generic link's stored
|
|
499
|
+
// superclassTypeArgs, until it reaches `targetOwner` (the ancestor that
|
|
500
|
+
// actually declared the member being resolved — see lookupField/
|
|
501
|
+
// lookupMethod/etc.'s `owner` result). Returns the bindings in
|
|
502
|
+
// `targetOwner`'s own type-param-name space. A non-generic link along the
|
|
503
|
+
// way (superclassTypeArgs null) resets to empty bindings, same as today's
|
|
504
|
+
// behavior of never substituting through a non-generic ancestor.
|
|
505
|
+
bindingsAtAncestor(className, ownBindings, targetOwner) {
|
|
506
|
+
let current = className;
|
|
507
|
+
let bindings = ownBindings;
|
|
508
|
+
while (current !== targetOwner) {
|
|
509
|
+
const info = this.classes.get(current);
|
|
510
|
+
if (!info || !info.superclass)
|
|
511
|
+
return bindings;
|
|
512
|
+
bindings = info.superclassTypeArgs ? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, bindings) : new Map();
|
|
513
|
+
current = info.superclass;
|
|
514
|
+
}
|
|
515
|
+
return bindings;
|
|
516
|
+
}
|
|
449
517
|
registerEnum(decl) {
|
|
450
518
|
const members = new Map();
|
|
451
519
|
decl.members.forEach((name, index) => {
|
|
@@ -459,29 +527,32 @@ export class Checker {
|
|
|
459
527
|
this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
|
|
460
528
|
}
|
|
461
529
|
registerInterface(decl) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
|
|
530
|
+
const genericBaseCount = { count: 0 };
|
|
531
|
+
const { methods, bases, baseTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
|
|
532
|
+
const methods = decl.methods.map((m) => {
|
|
533
|
+
const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
|
|
534
|
+
const returnType = this.resolveType(m.returnType, m.line, m.col);
|
|
535
|
+
this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
536
|
+
return { name: m.name, params, returnType };
|
|
537
|
+
});
|
|
538
|
+
const bases = [];
|
|
539
|
+
const baseTypeArgs = new Map();
|
|
540
|
+
for (const entry of decl.baseList) {
|
|
541
|
+
if (this.namedTypes.get(entry.name) !== "interface") {
|
|
542
|
+
this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${entry.name}')`, decl.line, decl.col);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
bases.push(entry.name);
|
|
546
|
+
if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
|
|
547
|
+
const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
|
|
548
|
+
if (resolved)
|
|
549
|
+
baseTypeArgs.set(entry.name, resolved);
|
|
550
|
+
}
|
|
480
551
|
}
|
|
481
|
-
bases
|
|
482
|
-
}
|
|
483
|
-
this.interfaces.set(decl.name, { name: decl.name,
|
|
484
|
-
this.recordHover(decl.line, decl.col, decl.
|
|
552
|
+
return { methods, bases, baseTypeArgs };
|
|
553
|
+
});
|
|
554
|
+
this.interfaces.set(decl.name, { name: decl.name, typeParams: decl.typeParams.map((p) => p.name), bases, baseTypeArgs, methods });
|
|
555
|
+
this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `interface ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `interface ${decl.name}`);
|
|
485
556
|
}
|
|
486
557
|
checkInterfaceHierarchy(decl) {
|
|
487
558
|
const info = this.interfaces.get(decl.name);
|
|
@@ -504,16 +575,31 @@ export class Checker {
|
|
|
504
575
|
}
|
|
505
576
|
}
|
|
506
577
|
// All methods required to satisfy an interface: its own plus every
|
|
507
|
-
// transitively-inherited parent interface's (cycle-safe).
|
|
508
|
-
|
|
578
|
+
// transitively-inherited parent interface's (cycle-safe). `bindings` maps
|
|
579
|
+
// `interfaceName`'s own type-param names to resolved types (from
|
|
580
|
+
// ownBindings, at the root call — see its two call sites) — every
|
|
581
|
+
// returned signature is substituted through it, and a generic base link
|
|
582
|
+
// along the way (`interface IDerived<T> : IBase<T>`) composes its own
|
|
583
|
+
// stored baseTypeArgs through `bindings` before recursing, so a signature
|
|
584
|
+
// inherited from several generic-base links up still resolves correctly.
|
|
585
|
+
collectInterfaceMethods(interfaceName, bindings = new Map(), seen = new Set()) {
|
|
509
586
|
if (seen.has(interfaceName))
|
|
510
587
|
return [];
|
|
511
588
|
seen.add(interfaceName);
|
|
512
589
|
const info = this.interfaces.get(interfaceName);
|
|
513
590
|
if (!info)
|
|
514
591
|
return [];
|
|
515
|
-
const inherited = info.bases.flatMap((b) =>
|
|
516
|
-
|
|
592
|
+
const inherited = info.bases.flatMap((b) => {
|
|
593
|
+
const baseArgs = info.baseTypeArgs.get(b);
|
|
594
|
+
const baseBindings = baseArgs ? this.composeBaseBindings(b, baseArgs, bindings) : new Map();
|
|
595
|
+
return this.collectInterfaceMethods(b, baseBindings, seen);
|
|
596
|
+
});
|
|
597
|
+
const ownMethods = info.methods.map((m) => ({
|
|
598
|
+
name: m.name,
|
|
599
|
+
params: m.params.map((p) => this.substituteTypeParams(p, bindings)),
|
|
600
|
+
returnType: this.substituteTypeParams(m.returnType, bindings),
|
|
601
|
+
}));
|
|
602
|
+
return [...inherited, ...ownMethods];
|
|
517
603
|
}
|
|
518
604
|
interfaceExtends(sub, sup) {
|
|
519
605
|
if (sub === sup)
|
|
@@ -524,8 +610,9 @@ export class Checker {
|
|
|
524
610
|
return info.bases.some((b) => this.interfaceExtends(b, sup));
|
|
525
611
|
}
|
|
526
612
|
registerClass(decl) {
|
|
527
|
-
this.recordHover(decl.line, decl.col, decl.
|
|
528
|
-
const
|
|
613
|
+
this.recordHover(decl.line, decl.col, decl.typeParams.length > 0 ? `class ${decl.name}<${decl.typeParams.map((p) => p.name).join(", ")}>` : `class ${decl.name}`);
|
|
614
|
+
const genericBaseCount = { count: 0 };
|
|
615
|
+
const { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs } = this.withTypeParamsInScope(decl.typeParams, () => {
|
|
529
616
|
const fields = new Map();
|
|
530
617
|
const staticFields = new Map();
|
|
531
618
|
for (const f of decl.fields) {
|
|
@@ -554,39 +641,46 @@ export class Checker {
|
|
|
554
641
|
const ownCtorParams = decl.constructor
|
|
555
642
|
? decl.constructor.params.map((p) => this.resolveType(p.type, decl.constructor.line, decl.constructor.col))
|
|
556
643
|
: null;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
this.
|
|
644
|
+
// Base-list resolution runs in this same scope — so `Box<T>` in
|
|
645
|
+
// `class Container<T> : Box<T>` resolves its inner `T` as
|
|
646
|
+
// Container's own type parameter, exactly like a field type would.
|
|
647
|
+
let superclass = null;
|
|
648
|
+
let superclassTypeArgs = null;
|
|
649
|
+
const interfaces = [];
|
|
650
|
+
const interfaceTypeArgs = new Map();
|
|
651
|
+
for (const entry of decl.baseList) {
|
|
652
|
+
const kind = this.namedTypes.get(entry.name);
|
|
653
|
+
if (kind === "class") {
|
|
654
|
+
if (superclass !== null) {
|
|
655
|
+
this.diagnostics.error("KS4016", `Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${entry.name}')`, decl.line, decl.col);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
superclass = entry.name;
|
|
659
|
+
if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
|
|
660
|
+
superclassTypeArgs = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (kind === "interface") {
|
|
664
|
+
interfaces.push(entry.name);
|
|
665
|
+
if (entry.typeArgs || this.genericTypeParams.has(entry.name)) {
|
|
666
|
+
const resolved = this.resolveBaseTypeArgs(decl.name, entry.name, entry.typeArgs, decl.line, decl.col, genericBaseCount);
|
|
667
|
+
if (resolved)
|
|
668
|
+
interfaceTypeArgs.set(entry.name, resolved);
|
|
669
|
+
}
|
|
573
670
|
}
|
|
574
671
|
else {
|
|
575
|
-
|
|
672
|
+
this.diagnostics.error("KS4017", `Unknown base class or interface '${entry.name}'`, decl.line, decl.col);
|
|
576
673
|
}
|
|
577
674
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
this.diagnostics.error("KS4017", `Unknown base class or interface '${baseName}'`, decl.line, decl.col);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
675
|
+
return { fields, staticFields, methods, staticMethods, ownCtorParams, superclass, superclassTypeArgs, interfaces, interfaceTypeArgs };
|
|
676
|
+
});
|
|
585
677
|
this.classes.set(decl.name, {
|
|
586
678
|
name: decl.name,
|
|
587
|
-
|
|
679
|
+
typeParams: decl.typeParams.map((p) => p.name),
|
|
588
680
|
superclass,
|
|
681
|
+
superclassTypeArgs,
|
|
589
682
|
interfaces,
|
|
683
|
+
interfaceTypeArgs,
|
|
590
684
|
fields,
|
|
591
685
|
methods,
|
|
592
686
|
staticFields,
|
|
@@ -594,6 +688,35 @@ export class Checker {
|
|
|
594
688
|
ownCtorParams,
|
|
595
689
|
});
|
|
596
690
|
}
|
|
691
|
+
// Resolves a base-list entry's own type arguments against `baseName`'s
|
|
692
|
+
// declared arity — `typeArgNodes` is null when the entry was written bare
|
|
693
|
+
// (`: Box`, not `: Box<number>`). Must be called from inside the
|
|
694
|
+
// declaring class/interface's own withTypeParamsInScope, so a type
|
|
695
|
+
// argument that references the declaring type's own parameter (`Box<T>`
|
|
696
|
+
// in `class Container<T> : Box<T>`) resolves correctly. `genericBaseCount`
|
|
697
|
+
// is a shared mutable counter across one declaration's whole base list,
|
|
698
|
+
// enforcing v1's "at most one generic entry per base list" cut.
|
|
699
|
+
resolveBaseTypeArgs(declName, baseName, typeArgNodes, declLine, declCol, genericBaseCount) {
|
|
700
|
+
const declaredParams = this.genericTypeParams.get(baseName);
|
|
701
|
+
if (!declaredParams) {
|
|
702
|
+
if (typeArgNodes)
|
|
703
|
+
this.diagnostics.error("KS4092", `'${baseName}' is not generic — it doesn't take a type argument`, declLine, declCol);
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
if (!typeArgNodes) {
|
|
707
|
+
this.diagnostics.error("KS4093", `Generic base '${baseName}' requires ${this.describeArity(declaredParams)} (e.g. '${baseName}<${declaredParams.join(", ")}>')`, declLine, declCol);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
if (typeArgNodes.length !== declaredParams.length) {
|
|
711
|
+
this.diagnostics.error("KS4094", `Generic base '${baseName}' expects ${this.describeArity(declaredParams)}, got ${typeArgNodes.length}`, declLine, declCol);
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
genericBaseCount.count++;
|
|
715
|
+
if (genericBaseCount.count > 1) {
|
|
716
|
+
this.diagnostics.error("KS4095", `'${declName}' has more than one generic entry in its base list — v1 supports at most one`, declLine, declCol);
|
|
717
|
+
}
|
|
718
|
+
return typeArgNodes.map((a) => this.resolveType(a, declLine, declCol));
|
|
719
|
+
}
|
|
597
720
|
checkClassHierarchy(decl) {
|
|
598
721
|
const info = this.classes.get(decl.name);
|
|
599
722
|
this.checkBaseCall(decl, info);
|
|
@@ -671,10 +794,19 @@ export class Checker {
|
|
|
671
794
|
}
|
|
672
795
|
checkInterfaceConformance(decl) {
|
|
673
796
|
const classInfo = this.classes.get(decl.name);
|
|
797
|
+
// This is a conformance check against the class's own *declared*
|
|
798
|
+
// (still-abstract) signatures, not an instantiated use site, so a
|
|
799
|
+
// generic interface arg that references the class's own type param
|
|
800
|
+
// (`IContainer<T>` in `class Container<T> : IContainer<T>`) should
|
|
801
|
+
// compose down to that same abstract TypeParamType, matching how the
|
|
802
|
+
// class's own methods are stored — see identityBindings.
|
|
803
|
+
const selfBindings = this.identityBindings(classInfo.typeParams);
|
|
674
804
|
for (const ifaceName of classInfo.interfaces) {
|
|
675
805
|
if (!this.interfaces.has(ifaceName))
|
|
676
806
|
continue; // already reported as an unknown base type
|
|
677
|
-
|
|
807
|
+
const ifaceArgs = classInfo.interfaceTypeArgs.get(ifaceName);
|
|
808
|
+
const bindings = ifaceArgs ? this.composeBaseBindings(ifaceName, ifaceArgs, selfBindings) : new Map();
|
|
809
|
+
for (const sig of this.collectInterfaceMethods(ifaceName, bindings)) {
|
|
678
810
|
const found = this.lookupMethod(decl.name, sig.name);
|
|
679
811
|
if (!found) {
|
|
680
812
|
this.diagnostics.error("KS4026", `Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
|
|
@@ -825,11 +957,11 @@ export class Checker {
|
|
|
825
957
|
// only ever arises from an arity error already diagnosed at the
|
|
826
958
|
// reference site — see validateGenericArity).
|
|
827
959
|
typeArgsMatch(a, b) {
|
|
828
|
-
if (!a.
|
|
960
|
+
if (!a.typeArgs && !b.typeArgs)
|
|
829
961
|
return true;
|
|
830
|
-
if (!a.
|
|
962
|
+
if (!a.typeArgs || !b.typeArgs || a.typeArgs.length !== b.typeArgs.length)
|
|
831
963
|
return false;
|
|
832
|
-
return T.typesEqual(
|
|
964
|
+
return a.typeArgs.every((t, i) => T.typesEqual(t, b.typeArgs[i]));
|
|
833
965
|
}
|
|
834
966
|
// ---------- top-level ----------
|
|
835
967
|
// Validates that `isAsync` and the declared return type agree — `async`
|
|
@@ -867,12 +999,12 @@ export class Checker {
|
|
|
867
999
|
}
|
|
868
1000
|
checkClassBody(decl) {
|
|
869
1001
|
// Constructor/method *bodies* run in this same scope registerClass used
|
|
870
|
-
// for the *signatures* (see
|
|
1002
|
+
// for the *signatures* (see withTypeParamsInScope) — without it, `T`
|
|
871
1003
|
// resolves everywhere in a generic class's declared field/param/return
|
|
872
1004
|
// types but not inside a method body itself (a local `T x = ...;`, or a
|
|
873
1005
|
// lambda parameter typed `T`), which would make the type parameter
|
|
874
1006
|
// usable only at the class's boundary and not inside its own logic.
|
|
875
|
-
this.
|
|
1007
|
+
this.withTypeParamsInScope(decl.typeParams, () => this.checkClassBodyInner(decl));
|
|
876
1008
|
}
|
|
877
1009
|
checkClassBodyInner(decl) {
|
|
878
1010
|
const info = this.classes.get(decl.name);
|
|
@@ -883,7 +1015,18 @@ export class Checker {
|
|
|
883
1015
|
// params but no `this` — matching real base()/super() semantics,
|
|
884
1016
|
// which must run before `this` becomes available.
|
|
885
1017
|
if (decl.constructor.baseArgs && info.superclass) {
|
|
886
|
-
|
|
1018
|
+
// One step from `decl` to its immediate superclass (Container's own
|
|
1019
|
+
// type params, still abstract, composed through whatever it
|
|
1020
|
+
// supplied Box in its base list — see identityBindings), then
|
|
1021
|
+
// however many further steps lookupCtorParams' own `owner` implies
|
|
1022
|
+
// (Box itself has no constructor of its own, inherits its
|
|
1023
|
+
// superclass's) via bindingsAtAncestor.
|
|
1024
|
+
const superBindings = info.superclassTypeArgs
|
|
1025
|
+
? this.composeBaseBindings(info.superclass, info.superclassTypeArgs, this.identityBindings(info.typeParams))
|
|
1026
|
+
: new Map();
|
|
1027
|
+
const { params: rawBaseCtorParams, owner: baseCtorOwner } = this.lookupCtorParams(info.superclass);
|
|
1028
|
+
const baseCtorBindings = this.bindingsAtAncestor(info.superclass, superBindings, baseCtorOwner);
|
|
1029
|
+
const baseCtorParams = rawBaseCtorParams.map((p) => this.substituteTypeParams(p, baseCtorBindings));
|
|
887
1030
|
const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
|
|
888
1031
|
if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
|
|
889
1032
|
this.diagnostics.error("KS4032", `Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
|
|
@@ -1461,31 +1604,40 @@ export class Checker {
|
|
|
1461
1604
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1462
1605
|
return T.UNKNOWN;
|
|
1463
1606
|
}
|
|
1464
|
-
let
|
|
1465
|
-
if (info.
|
|
1466
|
-
this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires
|
|
1607
|
+
let typeArgs;
|
|
1608
|
+
if (info.typeParams.length > 0 && !expr.typeArgs) {
|
|
1609
|
+
this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires ${this.describeArity(info.typeParams)} (e.g. 'new ${expr.className}<${info.typeParams.join(", ")}>(...)')`, expr.line, expr.col);
|
|
1467
1610
|
// Abstractly-typed (T-containing) ctor params, un-substitutable
|
|
1468
|
-
// without
|
|
1611
|
+
// without real type arguments, would otherwise cascade into a
|
|
1469
1612
|
// confusing "expected 'T'" error on every argument — one clear error
|
|
1470
1613
|
// beats that pile-on.
|
|
1471
1614
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1472
1615
|
return T.UNKNOWN;
|
|
1473
1616
|
}
|
|
1474
|
-
if (
|
|
1617
|
+
if (info.typeParams.length === 0 && expr.typeArgs) {
|
|
1475
1618
|
this.diagnostics.error("KS4070", `Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
|
|
1476
1619
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1477
1620
|
return T.UNKNOWN;
|
|
1478
1621
|
}
|
|
1479
|
-
if (info.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1622
|
+
if (info.typeParams.length > 0 && expr.typeArgs) {
|
|
1623
|
+
if (expr.typeArgs.length !== info.typeParams.length) {
|
|
1624
|
+
this.diagnostics.error("KS4091", `Generic class '${expr.className}' expects ${this.describeArity(info.typeParams)}, got ${expr.typeArgs.length}`, expr.line, expr.col);
|
|
1625
|
+
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1626
|
+
return T.UNKNOWN;
|
|
1627
|
+
}
|
|
1628
|
+
typeArgs = expr.typeArgs.map((a) => this.resolveType(a, expr.line, expr.col));
|
|
1629
|
+
}
|
|
1630
|
+
this.recordHover(expr.line, expr.col, typeArgs ? `class ${expr.className}<${typeArgs.map(T.typeToString).join(", ")}>` : `class ${expr.className}`);
|
|
1631
|
+
// The constructor being called may be inherited from a generic
|
|
1632
|
+
// ancestor several base-list links up (e.g. `new NumberBox(5)` where
|
|
1633
|
+
// `NumberBox : Box<number>` has no constructor of its own) — compose
|
|
1634
|
+
// bindings all the way to whichever class's own constructor
|
|
1635
|
+
// lookupCtorParams actually found (bindingsAtAncestor is a no-op single
|
|
1636
|
+
// step when it's expr.className's own, the common case).
|
|
1637
|
+
const { params: rawCtorParams, owner: ctorOwner } = this.lookupCtorParams(expr.className);
|
|
1638
|
+
const ownCtorBindings = this.ownBindings(info.typeParams, typeArgs) ?? new Map();
|
|
1639
|
+
const ctorBindings = this.bindingsAtAncestor(expr.className, ownCtorBindings, ctorOwner);
|
|
1640
|
+
const ctorParams = rawCtorParams.map((p) => this.substituteTypeParams(p, ctorBindings));
|
|
1489
1641
|
if (expr.args.length !== ctorParams.length) {
|
|
1490
1642
|
this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1491
1643
|
}
|
|
@@ -1496,19 +1648,25 @@ export class Checker {
|
|
|
1496
1648
|
this.diagnostics.error("KS4072", `Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1497
1649
|
}
|
|
1498
1650
|
});
|
|
1499
|
-
return T.classType(expr.className,
|
|
1500
|
-
}
|
|
1651
|
+
return T.classType(expr.className, typeArgs);
|
|
1652
|
+
}
|
|
1653
|
+
// `owner` is which class up the chain actually declared the constructor
|
|
1654
|
+
// being inherited (itself, or the nearest ancestor with one) — needed so
|
|
1655
|
+
// a caller can compose the right type-parameter bindings when that
|
|
1656
|
+
// ancestor is a generic base (see bindingsAtAncestor). `params` is stored
|
|
1657
|
+
// abstractly in `owner`'s own type-param-name space, same as a field or
|
|
1658
|
+
// method's type would be.
|
|
1501
1659
|
lookupCtorParams(className) {
|
|
1502
1660
|
let current = className;
|
|
1503
1661
|
while (current) {
|
|
1504
1662
|
const info = this.classes.get(current);
|
|
1505
1663
|
if (!info)
|
|
1506
|
-
return [];
|
|
1664
|
+
return { params: [], owner: className };
|
|
1507
1665
|
if (info.ownCtorParams !== null)
|
|
1508
|
-
return info.ownCtorParams;
|
|
1666
|
+
return { params: info.ownCtorParams, owner: current };
|
|
1509
1667
|
current = info.superclass;
|
|
1510
1668
|
}
|
|
1511
|
-
return [];
|
|
1669
|
+
return { params: [], owner: className };
|
|
1512
1670
|
}
|
|
1513
1671
|
// Array stdlib with a fixed (non-polymorphic) signature, given the
|
|
1514
1672
|
// array's own element type — everything except Map, which checkCall
|
|
@@ -1646,12 +1804,14 @@ export class Checker {
|
|
|
1646
1804
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1647
1805
|
}
|
|
1648
1806
|
if (objectType.kind === "class") {
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
|
|
1654
|
-
|
|
1807
|
+
// A member found on an *ancestor* (owner !== objectType.name — v1 now
|
|
1808
|
+
// supports a generic superclass, see registerClass/
|
|
1809
|
+
// bindingsAtAncestor) needs bindings composed through however many
|
|
1810
|
+
// generic base links separate objectType from that ancestor, not just
|
|
1811
|
+
// objectType's own type arguments — bindingsAtAncestor does that walk
|
|
1812
|
+
// (a no-op single step when owner IS objectType.name, the common
|
|
1813
|
+
// case, same result as before this existed).
|
|
1814
|
+
const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
|
|
1655
1815
|
const field = this.lookupField(objectType.name, expr.property);
|
|
1656
1816
|
if (field) {
|
|
1657
1817
|
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
@@ -1661,28 +1821,32 @@ export class Checker {
|
|
|
1661
1821
|
this.diagnostics.error("KS4082", `'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
|
|
1662
1822
|
}
|
|
1663
1823
|
}
|
|
1664
|
-
|
|
1824
|
+
const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, field.owner);
|
|
1825
|
+
return { type: this.substituteTypeParams(field.info.type, bindings), methodInfo: null };
|
|
1665
1826
|
}
|
|
1666
1827
|
const method = this.lookupMethod(objectType.name, expr.property);
|
|
1667
1828
|
if (method) {
|
|
1668
1829
|
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
1669
|
-
const
|
|
1830
|
+
const bindings = this.bindingsAtAncestor(objectType.name, ownBindings, method.owner);
|
|
1831
|
+
const info = {
|
|
1832
|
+
...method.info,
|
|
1833
|
+
params: method.info.params.map((p) => this.substituteTypeParams(p, bindings)),
|
|
1834
|
+
returnType: this.substituteTypeParams(method.info.returnType, bindings),
|
|
1835
|
+
};
|
|
1670
1836
|
return { type: info.returnType, methodInfo: info };
|
|
1671
1837
|
}
|
|
1672
1838
|
this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1673
1839
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1674
1840
|
}
|
|
1675
1841
|
if (objectType.kind === "interface") {
|
|
1676
|
-
|
|
1842
|
+
// collectInterfaceMethods itself now substitutes through `bindings` —
|
|
1843
|
+
// including across a generic base-interface link — so passing
|
|
1844
|
+
// objectType's own bindings once at the root is enough; no separate
|
|
1845
|
+
// re-substitution needed here (see its own header comment).
|
|
1846
|
+
const ownBindings = this.ownBindings(this.genericTypeParams.get(objectType.name) ?? [], objectType.typeArgs) ?? new Map();
|
|
1847
|
+
const sig = this.collectInterfaceMethods(objectType.name, ownBindings).find((m) => m.name === expr.property);
|
|
1677
1848
|
if (sig) {
|
|
1678
|
-
|
|
1679
|
-
// as classes — see registerInterface), so a signature found here
|
|
1680
|
-
// was always declared directly on this same interface.
|
|
1681
|
-
const paramName = objectType.typeArg ? this.genericTypeParams.get(objectType.name) : undefined;
|
|
1682
|
-
const substitute = (type) => (paramName && objectType.typeArg ? this.substituteTypeParam(type, paramName, objectType.typeArg) : type);
|
|
1683
|
-
const params = sig.params.map(substitute);
|
|
1684
|
-
const returnType = substitute(sig.returnType);
|
|
1685
|
-
return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
|
|
1849
|
+
return { type: sig.returnType, methodInfo: { params: sig.params, returnType: sig.returnType, visibility: "public", isVirtual: false, isOverride: false } };
|
|
1686
1850
|
}
|
|
1687
1851
|
this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1688
1852
|
return { type: T.UNKNOWN, methodInfo: null };
|