kopscript 0.2.0 → 0.3.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 +377 -0
- package/README.md +87 -22
- package/dist/checker.js +155 -16
- package/dist/cli.js +85 -16
- package/dist/codegen.js +2 -0
- package/dist/lexer.js +3 -0
- package/dist/modules.js +2 -1
- package/dist/parser.js +57 -18
- package/dist/tokens.js +2 -0
- package/dist/types.js +31 -16
- package/package.json +3 -2
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;
|
|
@@ -43,6 +58,7 @@ export class Checker {
|
|
|
43
58
|
this.namedTypes = new Map();
|
|
44
59
|
this.importedNames = new Set();
|
|
45
60
|
this.rawContents = new Map();
|
|
61
|
+
this.hoverEntries = [];
|
|
46
62
|
}
|
|
47
63
|
check() {
|
|
48
64
|
for (const [name, kind] of this.imports.namedTypes) {
|
|
@@ -291,7 +307,9 @@ export class Checker {
|
|
|
291
307
|
}
|
|
292
308
|
// ---------- registration ----------
|
|
293
309
|
resolveType(node, line, col) {
|
|
294
|
-
const resolved = T.resolveTypeNode(node, this.namedTypes)
|
|
310
|
+
const resolved = T.resolveTypeNode(node, this.namedTypes, (namedNode, type) => {
|
|
311
|
+
this.recordHover(namedNode.line, namedNode.col, T.typeToString(type));
|
|
312
|
+
});
|
|
295
313
|
if (!resolved) {
|
|
296
314
|
const name = node.kind === "NamedType" ? node.name : "[]";
|
|
297
315
|
this.diagnostics.error(`Unknown type '${name}'`, line, col);
|
|
@@ -309,13 +327,15 @@ export class Checker {
|
|
|
309
327
|
members.set(name, index);
|
|
310
328
|
});
|
|
311
329
|
this.enums.set(decl.name, { name: decl.name, members });
|
|
330
|
+
this.recordHover(decl.line, decl.col, `enum ${decl.name}`);
|
|
312
331
|
}
|
|
313
332
|
registerInterface(decl) {
|
|
314
|
-
const methods = decl.methods.map((m) =>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
const methods = decl.methods.map((m) => {
|
|
334
|
+
const params = m.params.map((p) => this.resolveType(p.type, m.line, m.col));
|
|
335
|
+
const returnType = this.resolveType(m.returnType, m.line, m.col);
|
|
336
|
+
this.recordHover(m.nameLine, m.nameCol, `${m.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
337
|
+
return { name: m.name, params, returnType };
|
|
338
|
+
});
|
|
319
339
|
const bases = [];
|
|
320
340
|
for (const baseName of decl.baseList) {
|
|
321
341
|
if (this.namedTypes.get(baseName) !== "interface") {
|
|
@@ -325,6 +345,7 @@ export class Checker {
|
|
|
325
345
|
bases.push(baseName);
|
|
326
346
|
}
|
|
327
347
|
this.interfaces.set(decl.name, { name: decl.name, bases, methods });
|
|
348
|
+
this.recordHover(decl.line, decl.col, `interface ${decl.name}`);
|
|
328
349
|
}
|
|
329
350
|
checkInterfaceHierarchy(decl) {
|
|
330
351
|
const info = this.interfaces.get(decl.name);
|
|
@@ -367,14 +388,18 @@ export class Checker {
|
|
|
367
388
|
return info.bases.some((b) => this.interfaceExtends(b, sup));
|
|
368
389
|
}
|
|
369
390
|
registerClass(decl) {
|
|
391
|
+
this.recordHover(decl.line, decl.col, `class ${decl.name}`);
|
|
370
392
|
const fields = new Map();
|
|
371
393
|
const staticFields = new Map();
|
|
372
394
|
for (const f of decl.fields) {
|
|
373
395
|
const info = { type: this.resolveType(f.type, f.line, f.col), visibility: f.visibility, hasSetter: true };
|
|
396
|
+
this.recordHover(f.nameLine, f.nameCol, `${f.name}: ${T.typeToString(info.type)}`);
|
|
374
397
|
(f.isStatic ? staticFields : fields).set(f.name, info);
|
|
375
398
|
}
|
|
376
399
|
for (const p of decl.properties) {
|
|
377
|
-
|
|
400
|
+
const type = this.resolveType(p.type, p.line, p.col);
|
|
401
|
+
this.recordHover(p.nameLine, p.nameCol, `${p.name}: ${T.typeToString(type)}`);
|
|
402
|
+
fields.set(p.name, { type, visibility: p.visibility, hasSetter: p.hasSetter });
|
|
378
403
|
}
|
|
379
404
|
const methods = new Map();
|
|
380
405
|
const staticMethods = new Map();
|
|
@@ -386,6 +411,7 @@ export class Checker {
|
|
|
386
411
|
isVirtual: m.isVirtual,
|
|
387
412
|
isOverride: m.isOverride,
|
|
388
413
|
};
|
|
414
|
+
this.recordHover(m.nameLine, m.nameCol, `${m.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
|
|
389
415
|
(m.isStatic ? staticMethods : methods).set(m.name, info);
|
|
390
416
|
}
|
|
391
417
|
const ownCtorParams = decl.constructor
|
|
@@ -515,10 +541,10 @@ export class Checker {
|
|
|
515
541
|
}
|
|
516
542
|
}
|
|
517
543
|
registerFunction(decl) {
|
|
518
|
-
this.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
544
|
+
const params = decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col));
|
|
545
|
+
const returnType = this.resolveType(decl.returnType, decl.line, decl.col);
|
|
546
|
+
this.functions.set(decl.name, { params, returnType });
|
|
547
|
+
this.recordHover(decl.line, decl.col, `function ${decl.name}(${params.map(T.typeToString).join(", ")}): ${T.typeToString(returnType)}`);
|
|
522
548
|
}
|
|
523
549
|
isSubclass(sub, sup) {
|
|
524
550
|
let current = sub;
|
|
@@ -612,6 +638,16 @@ export class Checker {
|
|
|
612
638
|
isAssignableType(from, to) {
|
|
613
639
|
if (from.kind === "unknown" || to.kind === "unknown")
|
|
614
640
|
return true;
|
|
641
|
+
// Widening: a non-null T (or another T?) is always fine where T? is
|
|
642
|
+
// expected. The reverse — a possibly-null value where a non-null type
|
|
643
|
+
// is expected — is never allowed here; that's exactly what narrowing
|
|
644
|
+
// (see detectNullCheck) exists to get past by changing `from` itself
|
|
645
|
+
// at the reference site, not by relaxing this rule.
|
|
646
|
+
if (to.kind === "nullable") {
|
|
647
|
+
return from.kind === "nullable" ? this.isAssignableType(from.inner, to.inner) : this.isAssignableType(from, to.inner);
|
|
648
|
+
}
|
|
649
|
+
if (from.kind === "nullable")
|
|
650
|
+
return false;
|
|
615
651
|
if (to.kind === "interface") {
|
|
616
652
|
if (from.kind === "class")
|
|
617
653
|
return this.classImplementsInterface(from.name, to.name);
|
|
@@ -723,6 +759,7 @@ export class Checker {
|
|
|
723
759
|
switch (stmt.kind) {
|
|
724
760
|
case "VarDecl": {
|
|
725
761
|
const declaredType = this.resolveType(stmt.type, stmt.line, stmt.col);
|
|
762
|
+
this.recordHover(stmt.nameLine, stmt.nameCol, `${stmt.isConst ? "const" : "let"} ${stmt.name}: ${T.typeToString(declaredType)}`);
|
|
726
763
|
const initType = this.checkExpressionExpecting(stmt.init, declaredType, scope, ctx);
|
|
727
764
|
if (!this.isAssignableType(initType, declaredType)) {
|
|
728
765
|
this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to variable of type '${T.typeToString(declaredType)}'`, stmt.line, stmt.col);
|
|
@@ -736,12 +773,19 @@ export class Checker {
|
|
|
736
773
|
case "IfStatement": {
|
|
737
774
|
const condType = this.checkExpression(stmt.condition, scope, ctx);
|
|
738
775
|
this.expectType(condType, T.BOOL, stmt.line, stmt.col, "if condition");
|
|
739
|
-
|
|
776
|
+
// `if (x != null)` narrows x to non-null in the then-branch;
|
|
777
|
+
// `if (x == null) ... else ...` narrows it in the else-branch —
|
|
778
|
+
// see detectNullCheck for exactly what's recognized (bare
|
|
779
|
+
// identifier vs. `null`, either operand order, `==`/`!=` only).
|
|
780
|
+
const nullCheck = this.detectNullCheck(stmt.condition);
|
|
781
|
+
const thenScope = nullCheck?.positiveWhenTrue ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
782
|
+
const elseScope = nullCheck && !nullCheck.positiveWhenTrue ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
783
|
+
this.checkBlock(stmt.thenBranch, thenScope, ctx);
|
|
740
784
|
if (stmt.elseBranch) {
|
|
741
785
|
if (stmt.elseBranch.kind === "IfStatement")
|
|
742
|
-
this.checkStatement(stmt.elseBranch,
|
|
786
|
+
this.checkStatement(stmt.elseBranch, elseScope, ctx);
|
|
743
787
|
else
|
|
744
|
-
this.checkBlock(stmt.elseBranch,
|
|
788
|
+
this.checkBlock(stmt.elseBranch, elseScope, ctx);
|
|
745
789
|
}
|
|
746
790
|
return;
|
|
747
791
|
}
|
|
@@ -844,8 +888,41 @@ export class Checker {
|
|
|
844
888
|
this.diagnostics.error(`Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
|
|
845
889
|
}
|
|
846
890
|
}
|
|
891
|
+
// Recognizes `name != null` / `name == null` (either operand order) as a
|
|
892
|
+
// null-check on a bare identifier — the only shape narrowing understands.
|
|
893
|
+
// `positiveWhenTrue: true` means the condition being true implies `name`
|
|
894
|
+
// is non-null (`!=`); `false` means the condition being *false* implies
|
|
895
|
+
// that (`==`). Deliberately narrow: no `this.Field != null`, no `&&`-
|
|
896
|
+
// chains inside the checked expression itself, no reachability analysis
|
|
897
|
+
// for an early-return guard clause (`if (x == null) { return; }` doesn't
|
|
898
|
+
// narrow `x` afterward) — each would need tracking narrowing by path or
|
|
899
|
+
// by control-flow reachability rather than by scope, real additional
|
|
900
|
+
// machinery this v1 doesn't take on.
|
|
901
|
+
detectNullCheck(condition) {
|
|
902
|
+
if (condition.kind !== "BinaryExpr" || (condition.op !== "==" && condition.op !== "!="))
|
|
903
|
+
return null;
|
|
904
|
+
const { left, right } = condition;
|
|
905
|
+
const ident = left.kind === "Identifier" ? left : right.kind === "Identifier" ? right : null;
|
|
906
|
+
const other = ident === left ? right : left;
|
|
907
|
+
if (!ident || other.kind !== "NullLiteral")
|
|
908
|
+
return null;
|
|
909
|
+
return { name: ident.name, positiveWhenTrue: condition.op === "!=" };
|
|
910
|
+
}
|
|
911
|
+
narrowedChild(scope, name) {
|
|
912
|
+
const child = scope.child();
|
|
913
|
+
child.narrowNonNull(name);
|
|
914
|
+
return child;
|
|
915
|
+
}
|
|
847
916
|
// ---------- expressions ----------
|
|
848
917
|
checkExpression(expr, scope, ctx) {
|
|
918
|
+
const type = this.checkExpressionInner(expr, scope, ctx);
|
|
919
|
+
if (expr.kind === "Identifier")
|
|
920
|
+
this.recordHover(expr.line, expr.col, `${expr.name}: ${T.typeToString(type)}`);
|
|
921
|
+
if (expr.kind === "ThisExpr")
|
|
922
|
+
this.recordHover(expr.line, expr.col, `this: ${T.typeToString(type)}`);
|
|
923
|
+
return type;
|
|
924
|
+
}
|
|
925
|
+
checkExpressionInner(expr, scope, ctx) {
|
|
849
926
|
switch (expr.kind) {
|
|
850
927
|
case "NumberLiteral":
|
|
851
928
|
return T.NUMBER;
|
|
@@ -853,6 +930,15 @@ export class Checker {
|
|
|
853
930
|
return T.STRING;
|
|
854
931
|
case "BoolLiteral":
|
|
855
932
|
return T.BOOL;
|
|
933
|
+
// Modeled as UNKNOWN rather than a dedicated Type — typesEqual/
|
|
934
|
+
// isAssignableType already treat UNKNOWN as compatible with anything,
|
|
935
|
+
// which is exactly right for a bare `null` with no expected type
|
|
936
|
+
// (e.g. `x == null`). The real enforcement (null only assignable
|
|
937
|
+
// where a nullable type is actually expected) lives in
|
|
938
|
+
// checkExpressionExpecting below, the same place LambdaExpr gets its
|
|
939
|
+
// expected-type-aware handling.
|
|
940
|
+
case "NullLiteral":
|
|
941
|
+
return T.UNKNOWN;
|
|
856
942
|
case "InterpolatedStringLiteral":
|
|
857
943
|
for (const part of expr.parts) {
|
|
858
944
|
if (part.kind === "Expr")
|
|
@@ -874,8 +960,16 @@ export class Checker {
|
|
|
874
960
|
}
|
|
875
961
|
case "Identifier": {
|
|
876
962
|
const found = scope.resolve(expr.name);
|
|
877
|
-
if (found)
|
|
963
|
+
if (found) {
|
|
964
|
+
// A nullable local/param narrowed by an enclosing `if (x != null)`
|
|
965
|
+
// (or equivalent — see detectNullCheck) type-checks as its
|
|
966
|
+
// non-null inner type at this specific reference, without
|
|
967
|
+
// changing what's actually declared. Narrowing only applies to
|
|
968
|
+
// bare names, not member-access paths like `this.Field`.
|
|
969
|
+
if (found.type.kind === "nullable" && scope.isNarrowedNonNull(expr.name))
|
|
970
|
+
return found.type.inner;
|
|
878
971
|
return found.type;
|
|
972
|
+
}
|
|
879
973
|
const externValue = this.externValues.get(expr.name);
|
|
880
974
|
if (externValue)
|
|
881
975
|
return externValue;
|
|
@@ -910,7 +1004,14 @@ export class Checker {
|
|
|
910
1004
|
}
|
|
911
1005
|
case "LogicalExpr": {
|
|
912
1006
|
const leftType = this.checkExpression(expr.left, scope, ctx);
|
|
913
|
-
|
|
1007
|
+
// `x != null && x.Foo` narrows x for the right operand (only
|
|
1008
|
+
// reached once the left side is true, i.e. x is non-null); by De
|
|
1009
|
+
// Morgan's, `x == null || x.Foo` narrows it the same way (the
|
|
1010
|
+
// right side only runs once the left is false, i.e. x is non-null).
|
|
1011
|
+
const nullCheck = this.detectNullCheck(expr.left);
|
|
1012
|
+
const narrowRight = nullCheck && ((expr.op === "&&" && nullCheck.positiveWhenTrue) || (expr.op === "||" && !nullCheck.positiveWhenTrue));
|
|
1013
|
+
const rightScope = narrowRight ? this.narrowedChild(scope, nullCheck.name) : scope;
|
|
1014
|
+
const rightType = this.checkExpression(expr.right, rightScope, ctx);
|
|
914
1015
|
this.expectType(leftType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
915
1016
|
this.expectType(rightType, T.BOOL, expr.line, expr.col, `'${expr.op}' operand`);
|
|
916
1017
|
return T.BOOL;
|
|
@@ -988,6 +1089,13 @@ export class Checker {
|
|
|
988
1089
|
checkExpressionExpecting(expr, expected, scope, ctx) {
|
|
989
1090
|
if (expr.kind === "LambdaExpr")
|
|
990
1091
|
return this.checkLambda(expr, expected, scope, ctx);
|
|
1092
|
+
if (expr.kind === "NullLiteral") {
|
|
1093
|
+
if (expected.kind !== "nullable" && expected.kind !== "unknown") {
|
|
1094
|
+
this.diagnostics.error(`Cannot assign 'null' to non-nullable type '${T.typeToString(expected)}'`, expr.line, expr.col);
|
|
1095
|
+
return T.UNKNOWN;
|
|
1096
|
+
}
|
|
1097
|
+
return expected;
|
|
1098
|
+
}
|
|
991
1099
|
return this.checkExpression(expr, scope, ctx);
|
|
992
1100
|
}
|
|
993
1101
|
checkLambda(expr, expected, scope, ctx) {
|
|
@@ -1065,6 +1173,21 @@ export class Checker {
|
|
|
1065
1173
|
return T.NUMBER;
|
|
1066
1174
|
}
|
|
1067
1175
|
if (op === "==" || op === "!=") {
|
|
1176
|
+
// A literal `null` type-checks as UNKNOWN with no expected type (see
|
|
1177
|
+
// checkExpressionInner), which typesEqual would happily accept
|
|
1178
|
+
// against anything — including a type that can never actually be
|
|
1179
|
+
// null. Checked here, against the AST node itself, specifically so
|
|
1180
|
+
// `nonNullableThing == null` (almost always a leftover from before a
|
|
1181
|
+
// type was made non-nullable, or a copy-pasted guard that can't
|
|
1182
|
+
// trigger) is a compile error instead of a silently-always-false comparison.
|
|
1183
|
+
const nullSide = expr.left.kind === "NullLiteral" ? "left" : expr.right.kind === "NullLiteral" ? "right" : null;
|
|
1184
|
+
if (nullSide) {
|
|
1185
|
+
const otherType = nullSide === "left" ? rightType : leftType;
|
|
1186
|
+
if (otherType.kind !== "nullable" && otherType.kind !== "unknown") {
|
|
1187
|
+
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);
|
|
1188
|
+
}
|
|
1189
|
+
return T.BOOL;
|
|
1190
|
+
}
|
|
1068
1191
|
if (!T.typesEqual(leftType, rightType)) {
|
|
1069
1192
|
this.diagnostics.error(`Cannot compare '${T.typeToString(leftType)}' with '${T.typeToString(rightType)}'`, line, col);
|
|
1070
1193
|
}
|
|
@@ -1091,6 +1214,7 @@ export class Checker {
|
|
|
1091
1214
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1092
1215
|
return T.UNKNOWN;
|
|
1093
1216
|
}
|
|
1217
|
+
this.recordHover(expr.callee.line, expr.callee.col, `function ${expr.callee.name}(${info.params.map(T.typeToString).join(", ")}): ${T.typeToString(info.returnType)}`);
|
|
1094
1218
|
this.checkArgs(expr, info.params, scope, ctx);
|
|
1095
1219
|
return info.returnType;
|
|
1096
1220
|
}
|
|
@@ -1159,6 +1283,7 @@ export class Checker {
|
|
|
1159
1283
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1160
1284
|
return T.UNKNOWN;
|
|
1161
1285
|
}
|
|
1286
|
+
this.recordHover(expr.line, expr.col, `class ${expr.className}`);
|
|
1162
1287
|
const ctorParams = this.lookupCtorParams(expr.className);
|
|
1163
1288
|
if (expr.args.length !== ctorParams.length) {
|
|
1164
1289
|
this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
|
|
@@ -1231,11 +1356,20 @@ export class Checker {
|
|
|
1231
1356
|
return T.arrayOf(argType.returnType);
|
|
1232
1357
|
}
|
|
1233
1358
|
checkMember(expr, scope, ctx, isAssignTarget = false) {
|
|
1359
|
+
const result = this.checkMemberInner(expr, scope, ctx, isAssignTarget);
|
|
1360
|
+
const text = result.methodInfo
|
|
1361
|
+
? `${expr.property}(${result.methodInfo.params.map(T.typeToString).join(", ")}): ${T.typeToString(result.methodInfo.returnType)}`
|
|
1362
|
+
: `${expr.property}: ${T.typeToString(result.type)}`;
|
|
1363
|
+
this.recordHover(expr.line, expr.col, text);
|
|
1364
|
+
return result;
|
|
1365
|
+
}
|
|
1366
|
+
checkMemberInner(expr, scope, ctx, isAssignTarget) {
|
|
1234
1367
|
// A bare type name as the "object" — Color.Red (enum) or Dog.Count (static) —
|
|
1235
1368
|
// is a type reference, not a value, so it's handled before the general expression check.
|
|
1236
1369
|
if (expr.object.kind === "Identifier" && !scope.resolve(expr.object.name)) {
|
|
1237
1370
|
const objName = expr.object.name;
|
|
1238
1371
|
if (this.enums.has(objName)) {
|
|
1372
|
+
this.recordHover(expr.object.line, expr.object.col, `enum ${objName}`);
|
|
1239
1373
|
const enumInfo = this.enums.get(objName);
|
|
1240
1374
|
if (!enumInfo.members.has(expr.property)) {
|
|
1241
1375
|
this.diagnostics.error(`Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
|
|
@@ -1244,6 +1378,7 @@ export class Checker {
|
|
|
1244
1378
|
return { type: T.enumType(objName), methodInfo: null };
|
|
1245
1379
|
}
|
|
1246
1380
|
if (this.classes.has(objName)) {
|
|
1381
|
+
this.recordHover(expr.object.line, expr.object.col, `class ${objName}`);
|
|
1247
1382
|
const field = this.lookupStaticField(objName, expr.property);
|
|
1248
1383
|
if (field) {
|
|
1249
1384
|
this.checkAccessibility(field.info.visibility, field.owner, ctx, expr.property, expr.line, expr.col);
|
|
@@ -1259,6 +1394,10 @@ export class Checker {
|
|
|
1259
1394
|
}
|
|
1260
1395
|
}
|
|
1261
1396
|
const objectType = this.checkExpression(expr.object, scope, ctx);
|
|
1397
|
+
if (objectType.kind === "nullable") {
|
|
1398
|
+
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);
|
|
1399
|
+
return { type: T.UNKNOWN, methodInfo: null };
|
|
1400
|
+
}
|
|
1262
1401
|
if (objectType.kind === "string") {
|
|
1263
1402
|
if (expr.property === "Length")
|
|
1264
1403
|
return { type: T.NUMBER, methodInfo: null };
|
package/dist/cli.js
CHANGED
|
@@ -7,31 +7,89 @@ function outputPathFor(filePath) {
|
|
|
7
7
|
const name = basename(filePath, ".ks");
|
|
8
8
|
return join(resolve(filePath, ".."), `${name}.js`);
|
|
9
9
|
}
|
|
10
|
+
function collectDiagnostics(result) {
|
|
11
|
+
const all = [];
|
|
12
|
+
for (const absPath of result.order) {
|
|
13
|
+
const mod = result.modules.get(absPath);
|
|
14
|
+
for (const d of mod.diagnostics.diagnostics)
|
|
15
|
+
all.push({ ...d, file: absPath });
|
|
16
|
+
}
|
|
17
|
+
return all;
|
|
18
|
+
}
|
|
19
|
+
// One JSON object per invocation, always to stdout (never split across
|
|
20
|
+
// lines, never mixed with human-readable output) — so a caller can pipe
|
|
21
|
+
// this straight into `JSON.parse` without worrying about interleaving.
|
|
22
|
+
function printJson(result, written) {
|
|
23
|
+
const diagnostics = collectDiagnostics(result);
|
|
24
|
+
console.log(JSON.stringify({ success: result.success, diagnostics, written }, null, 2));
|
|
25
|
+
}
|
|
26
|
+
function printJsonEntryMissing(filePath) {
|
|
27
|
+
console.log(JSON.stringify({ success: false, diagnostics: [], written: [], error: `cannot find file '${filePath}'` }, null, 2));
|
|
28
|
+
}
|
|
29
|
+
function printHumanDiagnostics(result) {
|
|
30
|
+
for (const absPath of result.order) {
|
|
31
|
+
const mod = result.modules.get(absPath);
|
|
32
|
+
if (mod.diagnostics.hasErrors) {
|
|
33
|
+
console.error(mod.diagnostics.format(mod.source, absPath));
|
|
34
|
+
console.error("");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
10
38
|
// Compiles the whole module graph reachable from `filePath`, writing one
|
|
11
|
-
// .js file next to each .ks source.
|
|
12
|
-
|
|
39
|
+
// .js file next to each .ks source. With `jsonMode`, nothing but a single
|
|
40
|
+
// JSON object goes to stdout (no "Wrote ..." line, no human diagnostics) —
|
|
41
|
+
// see `printJson` for the shape.
|
|
42
|
+
function build(filePath, jsonMode = false) {
|
|
13
43
|
const result = compileGraph(filePath);
|
|
14
44
|
if (result.entryMissing) {
|
|
15
|
-
|
|
45
|
+
if (jsonMode)
|
|
46
|
+
printJsonEntryMissing(filePath);
|
|
47
|
+
else
|
|
48
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
16
49
|
process.exitCode = 1;
|
|
17
50
|
return { outPath: null, watchFiles: [] };
|
|
18
51
|
}
|
|
19
52
|
if (!result.success) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.error("");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
53
|
+
if (jsonMode)
|
|
54
|
+
printJson(result, []);
|
|
55
|
+
else
|
|
56
|
+
printHumanDiagnostics(result);
|
|
27
57
|
process.exitCode = 1;
|
|
28
58
|
return { outPath: null, watchFiles: result.order };
|
|
29
59
|
}
|
|
30
60
|
for (const absPath of result.order) {
|
|
31
61
|
writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
|
|
32
62
|
}
|
|
63
|
+
if (jsonMode)
|
|
64
|
+
printJson(result, result.order.map(outputPathFor));
|
|
33
65
|
return { outPath: outputPathFor(filePath), watchFiles: result.order };
|
|
34
66
|
}
|
|
67
|
+
// Type-checks the graph without writing any output — for CI or an editor/
|
|
68
|
+
// agent that wants pass/fail plus diagnostics without touching the
|
|
69
|
+
// filesystem. Same exit-code convention as `build` (0 clean, 1 on any
|
|
70
|
+
// error or a missing entry file).
|
|
71
|
+
function checkCommand(filePath, jsonMode) {
|
|
72
|
+
const result = compileGraph(filePath);
|
|
73
|
+
if (result.entryMissing) {
|
|
74
|
+
if (jsonMode)
|
|
75
|
+
printJsonEntryMissing(filePath);
|
|
76
|
+
else
|
|
77
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (jsonMode) {
|
|
82
|
+
printJson(result, []);
|
|
83
|
+
}
|
|
84
|
+
else if (result.success) {
|
|
85
|
+
console.log("No errors.");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
printHumanDiagnostics(result);
|
|
89
|
+
}
|
|
90
|
+
if (!result.success)
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
35
93
|
function run(filePath) {
|
|
36
94
|
const { outPath } = build(filePath);
|
|
37
95
|
if (!outPath)
|
|
@@ -77,17 +135,28 @@ function watchCommand(filePath) {
|
|
|
77
135
|
console.log(`[watch] ${filePath} — watching for changes. Press Ctrl+C to stop.`);
|
|
78
136
|
rebuild();
|
|
79
137
|
}
|
|
138
|
+
const COMMANDS = ["build", "run", "watch", "check"];
|
|
80
139
|
function main() {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
const jsonMode = args.includes("--json");
|
|
142
|
+
const [command, file] = args.filter((a) => a !== "--json");
|
|
143
|
+
if (!command || !file || !COMMANDS.includes(command)) {
|
|
144
|
+
console.error("Usage: ks <build|run|watch|check> <file.ks> [--json]");
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (jsonMode && command !== "build" && command !== "check") {
|
|
149
|
+
console.error("ks: --json is only supported with 'build' and 'check'");
|
|
84
150
|
process.exitCode = 1;
|
|
85
151
|
return;
|
|
86
152
|
}
|
|
87
153
|
const filePath = resolve(file);
|
|
88
|
-
if (command === "
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
if (command === "check") {
|
|
155
|
+
checkCommand(filePath, jsonMode);
|
|
156
|
+
}
|
|
157
|
+
else if (command === "build") {
|
|
158
|
+
const { outPath } = build(filePath, jsonMode);
|
|
159
|
+
if (outPath && !jsonMode)
|
|
91
160
|
console.log(`Wrote ${outPath}`);
|
|
92
161
|
}
|
|
93
162
|
else if (command === "watch") {
|
package/dist/codegen.js
CHANGED
|
@@ -253,6 +253,8 @@ export class CodeGenerator {
|
|
|
253
253
|
return JSON.stringify(expr.value);
|
|
254
254
|
case "BoolLiteral":
|
|
255
255
|
return String(expr.value);
|
|
256
|
+
case "NullLiteral":
|
|
257
|
+
return "null";
|
|
256
258
|
case "InterpolatedStringLiteral":
|
|
257
259
|
return this.genInterpolatedString(expr);
|
|
258
260
|
case "ArrayLiteral":
|
package/dist/lexer.js
CHANGED
|
@@ -34,6 +34,7 @@ const KEYWORDS = {
|
|
|
34
34
|
void: TokenKind.Void,
|
|
35
35
|
true: TokenKind.True,
|
|
36
36
|
false: TokenKind.False,
|
|
37
|
+
null: TokenKind.Null,
|
|
37
38
|
task: TokenKind.Task,
|
|
38
39
|
state: TokenKind.State,
|
|
39
40
|
async: TokenKind.Async,
|
|
@@ -214,6 +215,8 @@ export class Lexer {
|
|
|
214
215
|
return this.make(TokenKind.Semicolon, c, line, col);
|
|
215
216
|
case ".":
|
|
216
217
|
return this.make(TokenKind.Dot, c, line, col);
|
|
218
|
+
case "?":
|
|
219
|
+
return this.make(TokenKind.Question, c, line, col);
|
|
217
220
|
case "+":
|
|
218
221
|
return this.make(TokenKind.Plus, c, line, col);
|
|
219
222
|
case "-":
|
package/dist/modules.js
CHANGED
|
@@ -29,7 +29,7 @@ export function loadModuleGraph(entryAbsPath) {
|
|
|
29
29
|
const diagnostics = new DiagnosticBag();
|
|
30
30
|
const tokens = new Lexer(source, diagnostics).tokenize();
|
|
31
31
|
const program = new Parser(tokens, diagnostics).parseProgram();
|
|
32
|
-
const record = { absPath, source, program, diagnostics, dependencies: [] };
|
|
32
|
+
const record = { absPath, source, program, diagnostics, dependencies: [], hoverEntries: [] };
|
|
33
33
|
modules.set(absPath, record);
|
|
34
34
|
stack.push(absPath);
|
|
35
35
|
for (const u of program.usings) {
|
|
@@ -107,6 +107,7 @@ export function compileGraph(entryAbsPath) {
|
|
|
107
107
|
}
|
|
108
108
|
const checker = new Checker(mod.program, mod.diagnostics, merged, absPath);
|
|
109
109
|
checker.check();
|
|
110
|
+
mod.hoverEntries = checker.hoverEntries;
|
|
110
111
|
if (mod.diagnostics.hasErrors)
|
|
111
112
|
hasErrors = true;
|
|
112
113
|
exportsByModule.set(absPath, checker.getExports());
|