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/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
- name: m.name,
316
- params: m.params.map((p) => this.resolveType(p.type, m.line, m.col)),
317
- returnType: this.resolveType(m.returnType, m.line, m.col),
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
- fields.set(p.name, { type: this.resolveType(p.type, p.line, p.col), visibility: p.visibility, hasSetter: p.hasSetter });
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.functions.set(decl.name, {
519
- params: decl.params.map((p) => this.resolveType(p.type, decl.line, decl.col)),
520
- returnType: this.resolveType(decl.returnType, decl.line, decl.col),
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
- this.checkBlock(stmt.thenBranch, scope, ctx);
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, scope, ctx);
786
+ this.checkStatement(stmt.elseBranch, elseScope, ctx);
743
787
  else
744
- this.checkBlock(stmt.elseBranch, scope, ctx);
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
- const rightType = this.checkExpression(expr.right, scope, ctx);
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
- function build(filePath) {
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
- console.error(`ks: cannot find file '${filePath}'`);
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
- for (const absPath of result.order) {
21
- const mod = result.modules.get(absPath);
22
- if (mod.diagnostics.hasErrors) {
23
- console.error(mod.diagnostics.format(mod.source, absPath));
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 [, , command, file] = process.argv;
82
- if (!command || !file || (command !== "build" && command !== "run" && command !== "watch")) {
83
- console.error("Usage: ks <build|run|watch> <file.ks>");
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 === "build") {
89
- const { outPath } = build(filePath);
90
- if (outPath)
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());