kopscript 0.2.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 +457 -0
- package/README.md +132 -22
- package/dist/checker.js +389 -48
- package/dist/cli.js +85 -16
- package/dist/codegen.js +15 -2
- package/dist/lexer.js +3 -0
- package/dist/modules.js +2 -1
- package/dist/parser.js +104 -31
- package/dist/tokens.js +2 -0
- package/dist/types.js +81 -23
- package/package.json +4 -3
package/dist/checker.js
CHANGED
|
@@ -18,6 +18,12 @@ class Scope {
|
|
|
18
18
|
constructor(parent = null) {
|
|
19
19
|
this.parent = parent;
|
|
20
20
|
this.vars = new Map();
|
|
21
|
+
// Names known non-null *in this scope specifically* — an overlay checked
|
|
22
|
+
// independently of where a name was actually declared (which can be many
|
|
23
|
+
// scopes up), set only on the narrow child scope an `if (x != null)`
|
|
24
|
+
// branch (or `&&`/`||` short-circuit) is checked in. See
|
|
25
|
+
// Checker.detectNullCheck/narrowedChild.
|
|
26
|
+
this.narrowed = new Set();
|
|
21
27
|
}
|
|
22
28
|
declare(name, type, isConst) {
|
|
23
29
|
this.vars.set(name, { type, isConst });
|
|
@@ -25,11 +31,20 @@ class Scope {
|
|
|
25
31
|
resolve(name) {
|
|
26
32
|
return this.vars.get(name) ?? this.parent?.resolve(name) ?? null;
|
|
27
33
|
}
|
|
34
|
+
narrowNonNull(name) {
|
|
35
|
+
this.narrowed.add(name);
|
|
36
|
+
}
|
|
37
|
+
isNarrowedNonNull(name) {
|
|
38
|
+
return this.narrowed.has(name) || (this.parent?.isNarrowedNonNull(name) ?? false);
|
|
39
|
+
}
|
|
28
40
|
child() {
|
|
29
41
|
return new Scope(this);
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
export class Checker {
|
|
45
|
+
recordHover(line, col, text) {
|
|
46
|
+
this.hoverEntries.push({ line, col, text });
|
|
47
|
+
}
|
|
33
48
|
constructor(program, diagnostics, imports = emptyModuleExports(), currentFilePath = "test.ks") {
|
|
34
49
|
this.program = program;
|
|
35
50
|
this.diagnostics = diagnostics;
|
|
@@ -41,18 +56,31 @@ export class Checker {
|
|
|
41
56
|
this.functions = new Map();
|
|
42
57
|
this.externValues = new Map();
|
|
43
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();
|
|
44
65
|
this.importedNames = new Set();
|
|
45
66
|
this.rawContents = new Map();
|
|
67
|
+
this.hoverEntries = [];
|
|
46
68
|
}
|
|
47
69
|
check() {
|
|
48
70
|
for (const [name, kind] of this.imports.namedTypes) {
|
|
49
71
|
this.namedTypes.set(name, kind);
|
|
50
72
|
this.importedNames.add(name);
|
|
51
73
|
}
|
|
52
|
-
for (const [name, info] of this.imports.classes)
|
|
74
|
+
for (const [name, info] of this.imports.classes) {
|
|
53
75
|
this.classes.set(name, info);
|
|
54
|
-
|
|
76
|
+
if (info.typeParam)
|
|
77
|
+
this.genericTypeParams.set(name, info.typeParam);
|
|
78
|
+
}
|
|
79
|
+
for (const [name, info] of this.imports.interfaces) {
|
|
55
80
|
this.interfaces.set(name, info);
|
|
81
|
+
if (info.typeParam)
|
|
82
|
+
this.genericTypeParams.set(name, info.typeParam);
|
|
83
|
+
}
|
|
56
84
|
for (const [name, info] of this.imports.enums)
|
|
57
85
|
this.enums.set(name, info);
|
|
58
86
|
for (const [name, info] of this.imports.functions) {
|
|
@@ -80,6 +108,12 @@ export class Checker {
|
|
|
80
108
|
this.namedTypes.set(e.name, "enum");
|
|
81
109
|
for (const c of externClassDecls)
|
|
82
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);
|
|
83
117
|
for (const e of enumDecls)
|
|
84
118
|
this.registerEnum(e);
|
|
85
119
|
for (const i of interfaceDecls)
|
|
@@ -156,6 +190,7 @@ export class Checker {
|
|
|
156
190
|
const ownCtorParams = decl.hasConstructor ? decl.ctorParams.map((p) => this.resolveType(p.type, decl.line, decl.col)) : null;
|
|
157
191
|
this.classes.set(decl.name, {
|
|
158
192
|
name: decl.name,
|
|
193
|
+
typeParam: null,
|
|
159
194
|
superclass: null,
|
|
160
195
|
interfaces: [],
|
|
161
196
|
fields,
|
|
@@ -291,14 +326,116 @@ export class Checker {
|
|
|
291
326
|
}
|
|
292
327
|
// ---------- registration ----------
|
|
293
328
|
resolveType(node, line, col) {
|
|
294
|
-
const resolved = T.resolveTypeNode(node, this.namedTypes)
|
|
329
|
+
const resolved = T.resolveTypeNode(node, this.namedTypes, (namedNode, type) => {
|
|
330
|
+
this.recordHover(namedNode.line, namedNode.col, T.typeToString(type));
|
|
331
|
+
});
|
|
295
332
|
if (!resolved) {
|
|
296
333
|
const name = node.kind === "NamedType" ? node.name : "[]";
|
|
297
|
-
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
|
+
}
|
|
298
340
|
return T.UNKNOWN;
|
|
299
341
|
}
|
|
342
|
+
this.validateGenericArity(resolved, line, col);
|
|
300
343
|
return resolved;
|
|
301
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
|
+
}
|
|
302
439
|
registerEnum(decl) {
|
|
303
440
|
const members = new Map();
|
|
304
441
|
decl.members.forEach((name, index) => {
|
|
@@ -309,12 +446,14 @@ export class Checker {
|
|
|
309
446
|
members.set(name, index);
|
|
310
447
|
});
|
|
311
448
|
this.enums.set(decl.name, { name: decl.name, members });
|
|
449
|
+
this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
|
|
312
450
|
}
|
|
313
451
|
registerInterface(decl) {
|
|
314
|
-
const methods = decl.methods.map((m) =>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
452
|
+
const methods = this.withTypeParamInScope(decl.typeParam, () => decl.methods.map((m) => {
|
|
453
|
+
const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
|
|
454
|
+
const returnType = this.resolveType(m.returnType, m.line, m.col);
|
|
455
|
+
this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
456
|
+
return { name: m.name, params, returnType };
|
|
318
457
|
}));
|
|
319
458
|
const bases = [];
|
|
320
459
|
for (const baseName of decl.baseList) {
|
|
@@ -322,9 +461,17 @@ export class Checker {
|
|
|
322
461
|
this.diagnostics.error(`Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
|
|
323
462
|
continue;
|
|
324
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
|
+
}
|
|
325
471
|
bases.push(baseName);
|
|
326
472
|
}
|
|
327
|
-
this.interfaces.set(decl.name, { name: decl.name, bases, methods });
|
|
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}`);
|
|
328
475
|
}
|
|
329
476
|
checkInterfaceHierarchy(decl) {
|
|
330
477
|
const info = this.interfaces.get(decl.name);
|
|
@@ -367,33 +514,48 @@ export class Checker {
|
|
|
367
514
|
return info.bases.some((b) => this.interfaceExtends(b, sup));
|
|
368
515
|
}
|
|
369
516
|
registerClass(decl) {
|
|
370
|
-
|
|
371
|
-
const staticFields =
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
(f
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
});
|
|
394
549
|
let superclass = null;
|
|
395
550
|
const interfaces = [];
|
|
396
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
|
+
}
|
|
397
559
|
const kind = this.namedTypes.get(baseName);
|
|
398
560
|
if (kind === "class") {
|
|
399
561
|
if (superclass !== null) {
|
|
@@ -412,6 +574,7 @@ export class Checker {
|
|
|
412
574
|
}
|
|
413
575
|
this.classes.set(decl.name, {
|
|
414
576
|
name: decl.name,
|
|
577
|
+
typeParam: decl.typeParam,
|
|
415
578
|
superclass,
|
|
416
579
|
interfaces,
|
|
417
580
|
fields,
|
|
@@ -515,10 +678,10 @@ export class Checker {
|
|
|
515
678
|
}
|
|
516
679
|
}
|
|
517
680
|
registerFunction(decl) {
|
|
518
|
-
this.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
681
|
+
const params = decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col));
|
|
682
|
+
const returnType = this.resolveType(decl.returnType, decl.line, decl.col);
|
|
683
|
+
this.functions.set(decl.name, { params, returnType });
|
|
684
|
+
this.recordHover(decl.line, decl.col, `function ${decl.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
522
685
|
}
|
|
523
686
|
isSubclass(sub, sup) {
|
|
524
687
|
let current = sub;
|
|
@@ -612,7 +775,25 @@ export class Checker {
|
|
|
612
775
|
isAssignableType(from, to) {
|
|
613
776
|
if (from.kind === "unknown" || to.kind === "unknown")
|
|
614
777
|
return true;
|
|
778
|
+
// Widening: a non-null T (or another T?) is always fine where T? is
|
|
779
|
+
// expected. The reverse — a possibly-null value where a non-null type
|
|
780
|
+
// is expected — is never allowed here; that's exactly what narrowing
|
|
781
|
+
// (see detectNullCheck) exists to get past by changing `from` itself
|
|
782
|
+
// at the reference site, not by relaxing this rule.
|
|
783
|
+
if (to.kind === "nullable") {
|
|
784
|
+
return from.kind === "nullable" ? this.isAssignableType(from.inner, to.inner) : this.isAssignableType(from, to.inner);
|
|
785
|
+
}
|
|
786
|
+
if (from.kind === "nullable")
|
|
787
|
+
return false;
|
|
615
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);
|
|
616
797
|
if (from.kind === "class")
|
|
617
798
|
return this.classImplementsInterface(from.name, to.name);
|
|
618
799
|
if (from.kind === "interface")
|
|
@@ -620,13 +801,26 @@ export class Checker {
|
|
|
620
801
|
return false;
|
|
621
802
|
}
|
|
622
803
|
if (from.kind === "class" && to.kind === "class") {
|
|
623
|
-
|
|
804
|
+
if (from.name === to.name)
|
|
805
|
+
return this.typeArgsMatch(from, to);
|
|
806
|
+
return this.isSubclass(from.name, to.name);
|
|
624
807
|
}
|
|
625
808
|
if (from.kind === "array" && to.kind === "array") {
|
|
626
809
|
return this.isAssignableType(from.element, to.element);
|
|
627
810
|
}
|
|
628
811
|
return T.typesEqual(from, to);
|
|
629
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
|
+
}
|
|
630
824
|
// ---------- top-level ----------
|
|
631
825
|
// Validates that `isAsync` and the declared return type agree — `async`
|
|
632
826
|
// requires `task`/`task<T>`, and `task`/`task<T>` requires `async` (there's
|
|
@@ -723,6 +917,7 @@ export class Checker {
|
|
|
723
917
|
switch (stmt.kind) {
|
|
724
918
|
case "VarDecl": {
|
|
725
919
|
const declaredType = this.resolveType(stmt.type, stmt.line, stmt.col);
|
|
920
|
+
this.recordHover(stmt.nameLine, stmt.nameCol, `${stmt.isConst ? "const" : "let"} ${stmt.name}: ${T.typeToString(declaredType)}`);
|
|
726
921
|
const initType = this.checkExpressionExpecting(stmt.init, declaredType, scope, ctx);
|
|
727
922
|
if (!this.isAssignableType(initType, declaredType)) {
|
|
728
923
|
this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to variable of type '${T.typeToString(declaredType)}'`, stmt.line, stmt.col);
|
|
@@ -736,12 +931,19 @@ export class Checker {
|
|
|
736
931
|
case "IfStatement": {
|
|
737
932
|
const condType = this.checkExpression(stmt.condition, scope, ctx);
|
|
738
933
|
this.expectType(condType, T.BOOL, stmt.line, stmt.col, "if condition");
|
|
739
|
-
|
|
934
|
+
// `if (x != null)` narrows x to non-null in the then-branch;
|
|
935
|
+
// `if (x == null) ... else ...` narrows it in the else-branch —
|
|
936
|
+
// see detectNullCheck for exactly what's recognized (bare
|
|
937
|
+
// identifier vs. `null`, either operand order, `==`/`!=` only).
|
|
938
|
+
const nullCheck = this.detectNullCheck(stmt.condition);
|
|
939
|
+
const thenScope = nullCheck?.positiveWhenTrue ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
940
|
+
const elseScope = nullCheck && !nullCheck.positiveWhenTrue ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
941
|
+
this.checkBlock(stmt.thenBranch, thenScope, ctx);
|
|
740
942
|
if (stmt.elseBranch) {
|
|
741
943
|
if (stmt.elseBranch.kind === "IfStatement")
|
|
742
|
-
this.checkStatement(stmt.elseBranch,
|
|
944
|
+
this.checkStatement(stmt.elseBranch, elseScope, ctx);
|
|
743
945
|
else
|
|
744
|
-
this.checkBlock(stmt.elseBranch,
|
|
946
|
+
this.checkBlock(stmt.elseBranch, elseScope, ctx);
|
|
745
947
|
}
|
|
746
948
|
return;
|
|
747
949
|
}
|
|
@@ -844,8 +1046,41 @@ export class Checker {
|
|
|
844
1046
|
this.diagnostics.error(`Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
|
|
845
1047
|
}
|
|
846
1048
|
}
|
|
1049
|
+
// Recognizes `name != null` / `name == null` (either operand order) as a
|
|
1050
|
+
// null-check on a bare identifier — the only shape narrowing understands.
|
|
1051
|
+
// `positiveWhenTrue: true` means the condition being true implies `name`
|
|
1052
|
+
// is non-null (`!=`); `false` means the condition being *false* implies
|
|
1053
|
+
// that (`==`). Deliberately narrow: no `this.Field != null`, no `&&`-
|
|
1054
|
+
// chains inside the checked expression itself, no reachability analysis
|
|
1055
|
+
// for an early-return guard clause (`if (x == null) { return; }` doesn't
|
|
1056
|
+
// narrow `x` afterward) — each would need tracking narrowing by path or
|
|
1057
|
+
// by control-flow reachability rather than by scope, real additional
|
|
1058
|
+
// machinery this v1 doesn't take on.
|
|
1059
|
+
detectNullCheck(condition) {
|
|
1060
|
+
if (condition.kind !== "BinaryExpr" || (condition.op !== "==" && condition.op !== "!="))
|
|
1061
|
+
return null;
|
|
1062
|
+
const { left, right } = condition;
|
|
1063
|
+
const ident = left.kind === "Identifier" ? left : right.kind === "Identifier" ? right : null;
|
|
1064
|
+
const other = ident === left ? right : left;
|
|
1065
|
+
if (!ident || other.kind !== "NullLiteral")
|
|
1066
|
+
return null;
|
|
1067
|
+
return { name: ident.name, positiveWhenTrue: condition.op === "!=" };
|
|
1068
|
+
}
|
|
1069
|
+
narrowedChild(scope, name) {
|
|
1070
|
+
const child = scope.child();
|
|
1071
|
+
child.narrowNonNull(name);
|
|
1072
|
+
return child;
|
|
1073
|
+
}
|
|
847
1074
|
// ---------- expressions ----------
|
|
848
1075
|
checkExpression(expr, scope, ctx) {
|
|
1076
|
+
const type = this.checkExpressionInner(expr, scope, ctx);
|
|
1077
|
+
if (expr.kind === "Identifier")
|
|
1078
|
+
this.recordHover(expr.line, expr.col, `${expr.name}: ${T.typeToString(type)}`);
|
|
1079
|
+
if (expr.kind === "ThisExpr")
|
|
1080
|
+
this.recordHover(expr.line, expr.col, `this: ${T.typeToString(type)}`);
|
|
1081
|
+
return type;
|
|
1082
|
+
}
|
|
1083
|
+
checkExpressionInner(expr, scope, ctx) {
|
|
849
1084
|
switch (expr.kind) {
|
|
850
1085
|
case "NumberLiteral":
|
|
851
1086
|
return T.NUMBER;
|
|
@@ -853,6 +1088,15 @@ export class Checker {
|
|
|
853
1088
|
return T.STRING;
|
|
854
1089
|
case "BoolLiteral":
|
|
855
1090
|
return T.BOOL;
|
|
1091
|
+
// Modeled as UNKNOWN rather than a dedicated Type — typesEqual/
|
|
1092
|
+
// isAssignableType already treat UNKNOWN as compatible with anything,
|
|
1093
|
+
// which is exactly right for a bare `null` with no expected type
|
|
1094
|
+
// (e.g. `x == null`). The real enforcement (null only assignable
|
|
1095
|
+
// where a nullable type is actually expected) lives in
|
|
1096
|
+
// checkExpressionExpecting below, the same place LambdaExpr gets its
|
|
1097
|
+
// expected-type-aware handling.
|
|
1098
|
+
case "NullLiteral":
|
|
1099
|
+
return T.UNKNOWN;
|
|
856
1100
|
case "InterpolatedStringLiteral":
|
|
857
1101
|
for (const part of expr.parts) {
|
|
858
1102
|
if (part.kind === "Expr")
|
|
@@ -874,8 +1118,16 @@ export class Checker {
|
|
|
874
1118
|
}
|
|
875
1119
|
case "Identifier": {
|
|
876
1120
|
const found = scope.resolve(expr.name);
|
|
877
|
-
if (found)
|
|
1121
|
+
if (found) {
|
|
1122
|
+
// A nullable local/param narrowed by an enclosing `if (x != null)`
|
|
1123
|
+
// (or equivalent — see detectNullCheck) type-checks as its
|
|
1124
|
+
// non-null inner type at this specific reference, without
|
|
1125
|
+
// changing what's actually declared. Narrowing only applies to
|
|
1126
|
+
// bare names, not member-access paths like `this.Field`.
|
|
1127
|
+
if (found.type.kind === "nullable" && scope.isNarrowedNonNull(expr.name))
|
|
1128
|
+
return found.type.inner;
|
|
878
1129
|
return found.type;
|
|
1130
|
+
}
|
|
879
1131
|
const externValue = this.externValues.get(expr.name);
|
|
880
1132
|
if (externValue)
|
|
881
1133
|
return externValue;
|
|
@@ -910,7 +1162,14 @@ export class Checker {
|
|
|
910
1162
|
}
|
|
911
1163
|
case "LogicalExpr": {
|
|
912
1164
|
const leftType = this.checkExpression(expr.left, scope, ctx);
|
|
913
|
-
|
|
1165
|
+
// `x != null && x.Foo` narrows x for the right operand (only
|
|
1166
|
+
// reached once the left side is true, i.e. x is non-null); by De
|
|
1167
|
+
// Morgan's, `x == null || x.Foo` narrows it the same way (the
|
|
1168
|
+
// right side only runs once the left is false, i.e. x is non-null).
|
|
1169
|
+
const nullCheck = this.detectNullCheck(expr.left);
|
|
1170
|
+
const narrowRight = nullCheck && ((expr.op === "&&" && nullCheck.positiveWhenTrue) || (expr.op === "||" && !nullCheck.positiveWhenTrue));
|
|
1171
|
+
const rightScope = narrowRight ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
1172
|
+
const rightType = this.checkExpression(expr.right, rightScope, ctx);
|
|
914
1173
|
this.expectType(leftType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
915
1174
|
this.expectType(rightType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
916
1175
|
return T.BOOL;
|
|
@@ -988,6 +1247,13 @@ export class Checker {
|
|
|
988
1247
|
checkExpressionExpecting(expr, expected, scope, ctx) {
|
|
989
1248
|
if (expr.kind === "LambdaExpr")
|
|
990
1249
|
return this.checkLambda(expr, expected, scope, ctx);
|
|
1250
|
+
if (expr.kind === "NullLiteral") {
|
|
1251
|
+
if (expected.kind !== "nullable" && expected.kind !== "unknown") {
|
|
1252
|
+
this.diagnostics.error(`Cannot assign 'null' to non-nullable type '${T.typeToString(expected)}'`, expr.line, expr.col);
|
|
1253
|
+
return T.UNKNOWN;
|
|
1254
|
+
}
|
|
1255
|
+
return expected;
|
|
1256
|
+
}
|
|
991
1257
|
return this.checkExpression(expr, scope, ctx);
|
|
992
1258
|
}
|
|
993
1259
|
checkLambda(expr, expected, scope, ctx) {
|
|
@@ -1065,6 +1331,21 @@ export class Checker {
|
|
|
1065
1331
|
return T.NUMBER;
|
|
1066
1332
|
}
|
|
1067
1333
|
if (op === "==" || op === "!=") {
|
|
1334
|
+
// A literal `null` type-checks as UNKNOWN with no expected type (see
|
|
1335
|
+
// checkExpressionInner), which typesEqual would happily accept
|
|
1336
|
+
// against anything — including a type that can never actually be
|
|
1337
|
+
// null. Checked here, against the AST node itself, specifically so
|
|
1338
|
+
// `nonNullableThing == null` (almost always a leftover from before a
|
|
1339
|
+
// type was made non-nullable, or a copy-pasted guard that can't
|
|
1340
|
+
// trigger) is a compile error instead of a silently-always-false comparison.
|
|
1341
|
+
const nullSide = expr.left.kind === "NullLiteral" ? "left" : expr.right.kind === "NullLiteral" ? "right" : null;
|
|
1342
|
+
if (nullSide) {
|
|
1343
|
+
const otherType = nullSide === "left" ? rightType : leftType;
|
|
1344
|
+
if (otherType.kind !== "nullable" && otherType.kind !== "unknown") {
|
|
1345
|
+
this.diagnostics.error(`Type '${T.typeToString(otherType)}' can never be null — only a nullable type (e.g. '${T.typeToString(otherType)}?') can be compared to 'null'`, line, col);
|
|
1346
|
+
}
|
|
1347
|
+
return T.BOOL;
|
|
1348
|
+
}
|
|
1068
1349
|
if (!T.typesEqual(leftType, rightType)) {
|
|
1069
1350
|
this.diagnostics.error(`Cannot compare '${T.typeToString(leftType)}' with '${T.typeToString(rightType)}'`, line, col);
|
|
1070
1351
|
}
|
|
@@ -1091,6 +1372,7 @@ export class Checker {
|
|
|
1091
1372
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1092
1373
|
return T.UNKNOWN;
|
|
1093
1374
|
}
|
|
1375
|
+
this.recordHover(expr.callee.line, expr.callee.col, `function ${expr.callee.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
|
|
1094
1376
|
this.checkArgs(expr, info.params, scope, ctx);
|
|
1095
1377
|
return info.returnType;
|
|
1096
1378
|
}
|
|
@@ -1101,6 +1383,7 @@ export class Checker {
|
|
|
1101
1383
|
if (expr.callee.property === "Map") {
|
|
1102
1384
|
const objectType = this.checkExpression(expr.callee.object, scope, ctx);
|
|
1103
1385
|
if (objectType.kind === "array") {
|
|
1386
|
+
expr.callee.isBuiltin = true; // see checkMemberInner's string/array branches
|
|
1104
1387
|
return this.checkArrayMap(expr, objectType.element, scope, ctx);
|
|
1105
1388
|
}
|
|
1106
1389
|
}
|
|
@@ -1159,7 +1442,31 @@ export class Checker {
|
|
|
1159
1442
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1160
1443
|
return T.UNKNOWN;
|
|
1161
1444
|
}
|
|
1162
|
-
|
|
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
|
+
}
|
|
1163
1470
|
if (expr.args.length !== ctorParams.length) {
|
|
1164
1471
|
this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
1165
1472
|
}
|
|
@@ -1170,7 +1477,7 @@ export class Checker {
|
|
|
1170
1477
|
this.diagnostics.error(`Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
|
|
1171
1478
|
}
|
|
1172
1479
|
});
|
|
1173
|
-
return T.classType(expr.className);
|
|
1480
|
+
return T.classType(expr.className, typeArg);
|
|
1174
1481
|
}
|
|
1175
1482
|
lookupCtorParams(className) {
|
|
1176
1483
|
let current = className;
|
|
@@ -1231,11 +1538,20 @@ export class Checker {
|
|
|
1231
1538
|
return T.arrayOf(argType.returnType);
|
|
1232
1539
|
}
|
|
1233
1540
|
checkMember(expr, scope, ctx, isAssignTarget = false) {
|
|
1541
|
+
const result = this.checkMemberInner(expr, scope, ctx, isAssignTarget);
|
|
1542
|
+
const text = result.methodInfo
|
|
1543
|
+
? `${expr.property}(${result.methodInfo.params.map(T.typeToString).join(", ")}): ${T.typeToString(result.methodInfo.returnType)}`
|
|
1544
|
+
: `${expr.property}: ${T.typeToString(result.type)}`;
|
|
1545
|
+
this.recordHover(expr.line, expr.col, text);
|
|
1546
|
+
return result;
|
|
1547
|
+
}
|
|
1548
|
+
checkMemberInner(expr, scope, ctx, isAssignTarget) {
|
|
1234
1549
|
// A bare type name as the "object" — Color.Red (enum) or Dog.Count (static) —
|
|
1235
1550
|
// is a type reference, not a value, so it's handled before the general expression check.
|
|
1236
1551
|
if (expr.object.kind === "Identifier" && !scope.resolve(expr.object.name)) {
|
|
1237
1552
|
const objName = expr.object.name;
|
|
1238
1553
|
if (this.enums.has(objName)) {
|
|
1554
|
+
this.recordHover(expr.object.line, expr.object.col, `enum ${objName}`);
|
|
1239
1555
|
const enumInfo = this.enums.get(objName);
|
|
1240
1556
|
if (!enumInfo.members.has(expr.property)) {
|
|
1241
1557
|
this.diagnostics.error(`Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
@@ -1244,6 +1560,7 @@ export class Checker {
|
|
|
1244
1560
|
return { type: T.enumType(objName), methodInfo: null };
|
|
1245
1561
|
}
|
|
1246
1562
|
if (this.classes.has(objName)) {
|
|
1563
|
+
this.recordHover(expr.object.line, expr.object.col, `class ${objName}`);
|
|
1247
1564
|
const field = this.lookupStaticField(objName, expr.property);
|
|
1248
1565
|
if (field) {
|
|
1249
1566
|
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
@@ -1259,7 +1576,16 @@ export class Checker {
|
|
|
1259
1576
|
}
|
|
1260
1577
|
}
|
|
1261
1578
|
const objectType = this.checkExpression(expr.object, scope, ctx);
|
|
1579
|
+
if (objectType.kind === "nullable") {
|
|
1580
|
+
this.diagnostics.error(`Cannot access member '${expr.property}' on possibly-null type '${T.typeToString(objectType)}' — check for null first (e.g. 'if (x != null) { ... }')`, expr.line, expr.col);
|
|
1581
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1582
|
+
}
|
|
1262
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;
|
|
1263
1589
|
if (expr.property === "Length")
|
|
1264
1590
|
return { type: T.NUMBER, methodInfo: null };
|
|
1265
1591
|
const method = STRING_METHODS[expr.property];
|
|
@@ -1269,6 +1595,7 @@ export class Checker {
|
|
|
1269
1595
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1270
1596
|
}
|
|
1271
1597
|
if (objectType.kind === "array") {
|
|
1598
|
+
expr.isBuiltin = true; // see the string branch above
|
|
1272
1599
|
if (expr.property === "Length")
|
|
1273
1600
|
return { type: T.NUMBER, methodInfo: null };
|
|
1274
1601
|
const method = this.arrayMethod(objectType.element, expr.property);
|
|
@@ -1300,6 +1627,12 @@ export class Checker {
|
|
|
1300
1627
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
1301
1628
|
}
|
|
1302
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);
|
|
1303
1636
|
const field = this.lookupField(objectType.name, expr.property);
|
|
1304
1637
|
if (field) {
|
|
1305
1638
|
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
@@ -1309,12 +1642,13 @@ export class Checker {
|
|
|
1309
1642
|
this.diagnostics.error(`'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
|
|
1310
1643
|
}
|
|
1311
1644
|
}
|
|
1312
|
-
return { type: field.info.type, methodInfo: null };
|
|
1645
|
+
return { type: substitute(field.info.type), methodInfo: null };
|
|
1313
1646
|
}
|
|
1314
1647
|
const method = this.lookupMethod(objectType.name, expr.property);
|
|
1315
1648
|
if (method) {
|
|
1316
1649
|
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
1317
|
-
|
|
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 };
|
|
1318
1652
|
}
|
|
1319
1653
|
this.diagnostics.error(`Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1320
1654
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
@@ -1322,7 +1656,14 @@ export class Checker {
|
|
|
1322
1656
|
if (objectType.kind === "interface") {
|
|
1323
1657
|
const sig = this.collectInterfaceMethods(objectType.name).find((m) => m.name === expr.property);
|
|
1324
1658
|
if (sig) {
|
|
1325
|
-
|
|
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 } };
|
|
1326
1667
|
}
|
|
1327
1668
|
this.diagnostics.error(`Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
1328
1669
|
return { type: T.UNKNOWN, methodInfo: null };
|