kopscript 0.7.1 → 0.8.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 CHANGED
@@ -21,7 +21,7 @@ ks build|check <file.ks> --json # single JSON object on stdout instead of huma
21
21
  `--json` output shape (also what a tool/agent should parse instead of scraping text):
22
22
 
23
23
  ```json
24
- { "success": false, "diagnostics": [{ "severity": "error", "message": "...", "line": 2, "col": 14, "file": "/abs/path.ks" }], "written": [] }
24
+ { "success": false, "diagnostics": [{ "code": "KS4065", "severity": "error", "message": "...", "line": 2, "col": 14, "file": "/abs/path.ks" }], "written": [] }
25
25
  ```
26
26
 
27
27
  `written` is the absolute paths actually written (`build` only, and only on success — a
@@ -504,9 +504,48 @@ signature, use different names, not overloading. (Nested function/class/interfac
504
504
  declarations, by contrast, *are* a clean compile error: "Nested ... declarations are not
505
505
  supported".)
506
506
 
507
+ ## Common mistakes (seeded from real generation failures)
508
+
509
+ Real, observed cases where a plausible-looking guess was wrong — not hypothetical gotchas:
510
+
511
+ - **No implicit `this`.** Every reference to a member of the enclosing class — a field, a
512
+ property, a method call — must be `this.X`, always, everywhere, including inside a lambda.
513
+ `Increment()` inside a method body does **not** resolve to `this.Increment()` the way it
514
+ would in Java/C#/Python; it's an undefined-identifier error unless `Increment` is a real
515
+ free function. (Discovered building the template compiler above — templates need a
516
+ dedicated rewriter, `qualifyThis`, specifically because this isn't automatic.)
517
+ - **No `let`/`var`, ever.** Every local is `Type name = value;` — writing `let x = 5;` or a
518
+ bare `const x = 5;` (missing the type) is a parse error, not a lenient inferred form.
519
+ - **No generic functions.** `T Identity<T>(T x) { return x; }` doesn't parse — only classes
520
+ and interfaces take `<T>`. Don't reach for this even though every mainstream generic
521
+ language supports it.
522
+ - **Early-return doesn't narrow a nullable type.** `if (x == null) { return; } print(x.Length);`
523
+ still errors on `x.Length` — narrowing is scope-based (an `if`/`else` block), not
524
+ control-flow/reachability-based. Wrap the rest of the logic in the `if (x != null) { ... }`
525
+ block instead of guard-clause-and-continue.
526
+ - **Never write `async` on an `extern` signature.** `async task<string> text();` inside an
527
+ `extern class` is a parse error — declare the return type as `task<string> text();`
528
+ directly; `async` only means something for a body the checker validates, and an `extern`
529
+ signature has no body.
530
+ - **No object-literal syntax, anywhere, ever.** Reaching for `{ method: "POST", body: x }` to
531
+ call a JS API that expects one (a `fetch`-style options argument) doesn't compile — there's
532
+ no way to construct that value in KopScript at all. See the `extern`/`raw string` sections
533
+ above and Kopular's `http_runtime.js` for the actual workaround (a small hand-written JS
534
+ shim), not a language feature to reach for.
535
+ - **No ternary expression.** `cond ? a : b` doesn't parse. Use `match` on a `bool`, or an
536
+ `if`/`else` assigning to a local declared above it.
537
+ - **Two methods with the same name in one class don't error** — the second silently replaces
538
+ the first (see "Sharp edge" above). If a generated program seems to call the wrong
539
+ implementation of something, check for an accidental duplicate name before assuming a
540
+ compiler bug.
541
+
507
542
  ## Diagnostics
508
543
 
509
- Every compiler error/warning is `{ severity, message, line, col }` (1-based). CLI output
510
- format: `` file:line:col - severity: message `` plus a source line and a `^` pointer.
511
- `DiagnosticBag.hasErrors` gates whether codegen runs at all a program with any error
512
- produces no output.
544
+ Every compiler error/warning is `{ code, severity, message, line, col }` (1-based).
545
+ `code` is `KS` + a number, stable across compiler versions even when `message`'s wording
546
+ changes — match on `code` in tooling, not on `message` text. Ranges by pipeline stage,
547
+ never reused once assigned: `KS1xxx` lexer, `KS2xxx` parser, `KS3xxx` module/`using`
548
+ resolution, `KS4xxx` checker (the large majority of real errors), `KS5xxx` templates.
549
+ CLI output format: `` file:line:col - severity code: message `` plus a source line and a
550
+ `^` pointer. `DiagnosticBag.hasErrors` gates whether codegen runs at all — a program with
551
+ any error produces no output.
package/README.md CHANGED
@@ -755,7 +755,7 @@ programmatically instead of scraping formatted text:
755
755
  {
756
756
  "success": false,
757
757
  "diagnostics": [
758
- { "severity": "error", "message": "Argument 2 has type 'string', expected 'number'", "line": 2, "col": 14, "file": "/abs/path/to/file.ks" }
758
+ { "code": "KS4065", "severity": "error", "message": "Argument 2 has type 'string', expected 'number'", "line": 2, "col": 14, "file": "/abs/path/to/file.ks" }
759
759
  ],
760
760
  "written": []
761
761
  }
@@ -767,6 +767,15 @@ error-free). A missing entry file reports `{ "success": false, "diagnostics": []
767
767
  "written": [], "error": "cannot find file '...'" }` instead of throwing. Exit code is 0
768
768
  exactly when `success` is `true`, both with and without `--json`.
769
769
 
770
+ Every diagnostic carries a stable `code` (`KS` + a number) alongside its human-readable
771
+ `message` — meant for a tool/agent to pattern-match reliably (`code === "KS4065"`) instead
772
+ of parsing prose that can be reworded between versions. Codes are grouped by pipeline stage
773
+ and never reused once assigned: `KS1xxx` lexer, `KS2xxx` parser, `KS3xxx` module/`using`
774
+ resolution, `KS4xxx` the checker (by far the largest category — most real type errors live
775
+ here), `KS5xxx` templates. There's no generated reference doc mapping every code to an
776
+ explanation yet — for now, `message` is still the primary explanation; `code` is for
777
+ matching, not (yet) for looking up docs.
778
+
770
779
  During development, use `npm run ks -- <build|run|watch|check> <file.ks>` (backed by
771
780
  `tsx`), or run `npm run build` to compile the TypeScript compiler itself to `dist/` and
772
781
  use `node dist/cli.js` directly.
package/dist/checker.js CHANGED
@@ -221,7 +221,7 @@ export class Checker {
221
221
  this.externValues.set(decl.name, T.STRING);
222
222
  const resolvedPath = resolve(dirname(this.currentFilePath), decl.path);
223
223
  if (!existsSync(resolvedPath)) {
224
- this.diagnostics.error(`Cannot find file '${decl.path}' referenced by 'raw string ${decl.name}' (looked for '${resolvedPath}')`, decl.line, decl.col);
224
+ this.diagnostics.error("KS4001", `Cannot find file '${decl.path}' referenced by 'raw string ${decl.name}' (looked for '${resolvedPath}')`, decl.line, decl.col);
225
225
  return;
226
226
  }
227
227
  this.rawContents.set(decl.name, readFileSync(resolvedPath, "utf-8"));
@@ -272,10 +272,10 @@ export class Checker {
272
272
  const seen = new Map();
273
273
  const declare = (name, line, col) => {
274
274
  if (this.importedNames.has(name)) {
275
- this.diagnostics.error(`Declaration '${name}' conflicts with a name brought in by 'using'`, line, col);
275
+ this.diagnostics.error("KS4002", `Declaration '${name}' conflicts with a name brought in by 'using'`, line, col);
276
276
  }
277
277
  else if (seen.has(name)) {
278
- this.diagnostics.error(`Duplicate top-level declaration '${name}'`, line, col);
278
+ this.diagnostics.error("KS4003", `Duplicate top-level declaration '${name}'`, line, col);
279
279
  }
280
280
  seen.set(name, true);
281
281
  };
@@ -305,11 +305,11 @@ export class Checker {
305
305
  return;
306
306
  const info = this.classes.get(decl.name);
307
307
  if (info.superclass && !this.isExportedName(info.superclass)) {
308
- this.diagnostics.error(`Exported class '${decl.name}' has a base class '${info.superclass}' that isn't exported`, decl.line, decl.col);
308
+ this.diagnostics.error("KS4004", `Exported class '${decl.name}' has a base class '${info.superclass}' that isn't exported`, decl.line, decl.col);
309
309
  }
310
310
  for (const ifaceName of info.interfaces) {
311
311
  if (!this.isExportedName(ifaceName)) {
312
- this.diagnostics.error(`Exported class '${decl.name}' implements interface '${ifaceName}', which isn't exported`, decl.line, decl.col);
312
+ this.diagnostics.error("KS4005", `Exported class '${decl.name}' implements interface '${ifaceName}', which isn't exported`, decl.line, decl.col);
313
313
  }
314
314
  }
315
315
  }
@@ -321,7 +321,7 @@ export class Checker {
321
321
  return;
322
322
  for (const baseName of info.bases) {
323
323
  if (!this.isExportedName(baseName)) {
324
- this.diagnostics.error(`Exported interface '${decl.name}' extends interface '${baseName}', which isn't exported`, decl.line, decl.col);
324
+ this.diagnostics.error("KS4006", `Exported interface '${decl.name}' extends interface '${baseName}', which isn't exported`, decl.line, decl.col);
325
325
  }
326
326
  }
327
327
  }
@@ -342,10 +342,10 @@ export class Checker {
342
342
  if (!resolved) {
343
343
  const name = node.kind === "NamedType" ? node.name : "[]";
344
344
  if (name !== "[]" && this.genericTypeParams.has(name) && !node.typeArgs) {
345
- this.diagnostics.error(`Generic type '${name}' requires a type argument (e.g. '${name}<T>')`, line, col);
345
+ this.diagnostics.error("KS4007", `Generic type '${name}' requires a type argument (e.g. '${name}<T>')`, line, col);
346
346
  }
347
347
  else {
348
- this.diagnostics.error(`Unknown type '${name}'`, line, col);
348
+ this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col);
349
349
  }
350
350
  return T.UNKNOWN;
351
351
  }
@@ -380,10 +380,10 @@ export class Checker {
380
380
  case "interface": {
381
381
  const isGeneric = this.genericTypeParams.has(type.name);
382
382
  if (isGeneric && !type.typeArg) {
383
- this.diagnostics.error(`Generic type '${type.name}' requires a type argument (e.g. '${type.name}<T>')`, line, col);
383
+ this.diagnostics.error("KS4009", `Generic type '${type.name}' requires a type argument (e.g. '${type.name}<T>')`, line, col);
384
384
  }
385
385
  else if (!isGeneric && type.typeArg) {
386
- this.diagnostics.error(`Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
386
+ this.diagnostics.error("KS4010", `Type '${type.name}' is not generic — it doesn't take a type argument`, line, col);
387
387
  }
388
388
  if (type.typeArg)
389
389
  this.validateGenericArity(type.typeArg, line, col);
@@ -450,7 +450,7 @@ export class Checker {
450
450
  const members = new Map();
451
451
  decl.members.forEach((name, index) => {
452
452
  if (members.has(name)) {
453
- this.diagnostics.error(`Duplicate enum member '${name}'`, decl.line, decl.col);
453
+ this.diagnostics.error("KS4011", `Duplicate enum member '${name}'`, decl.line, decl.col);
454
454
  return;
455
455
  }
456
456
  members.set(name, index);
@@ -468,14 +468,14 @@ export class Checker {
468
468
  const bases = [];
469
469
  for (const baseName of decl.baseList) {
470
470
  if (this.namedTypes.get(baseName) !== "interface") {
471
- this.diagnostics.error(`Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
471
+ this.diagnostics.error("KS4012", `Interface '${decl.name}' can only extend other interfaces (unknown interface '${baseName}')`, decl.line, decl.col);
472
472
  continue;
473
473
  }
474
474
  // v1 generics can't appear in a base list at all — only as a
475
475
  // standalone type (field/param/return/local, `new Box<T>()`). A
476
476
  // class/interface always implements/extends a *bare* name.
477
477
  if (this.genericTypeParams.has(baseName)) {
478
- this.diagnostics.error(`Interface '${decl.name}' cannot extend generic interface '${baseName}' — not supported in v1`, decl.line, decl.col);
478
+ this.diagnostics.error("KS4013", `Interface '${decl.name}' cannot extend generic interface '${baseName}' — not supported in v1`, decl.line, decl.col);
479
479
  continue;
480
480
  }
481
481
  bases.push(baseName);
@@ -492,7 +492,7 @@ export class Checker {
492
492
  while (stack.length > 0) {
493
493
  const current = stack.pop();
494
494
  if (current === decl.name) {
495
- this.diagnostics.error(`Circular interface inheritance detected involving '${decl.name}'`, decl.line, decl.col);
495
+ this.diagnostics.error("KS4014", `Circular interface inheritance detected involving '${decl.name}'`, decl.line, decl.col);
496
496
  return;
497
497
  }
498
498
  if (seen.has(current))
@@ -563,13 +563,13 @@ export class Checker {
563
563
  // standalone type (field/param/return/local, `new Box<T>()`). A
564
564
  // class always extends/implements a *bare* name.
565
565
  if (this.genericTypeParams.has(baseName)) {
566
- this.diagnostics.error(`Class '${decl.name}' cannot extend/implement generic type '${baseName}' — not supported in v1`, decl.line, decl.col);
566
+ this.diagnostics.error("KS4015", `Class '${decl.name}' cannot extend/implement generic type '${baseName}' — not supported in v1`, decl.line, decl.col);
567
567
  continue;
568
568
  }
569
569
  const kind = this.namedTypes.get(baseName);
570
570
  if (kind === "class") {
571
571
  if (superclass !== null) {
572
- this.diagnostics.error(`Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${baseName}')`, decl.line, decl.col);
572
+ this.diagnostics.error("KS4016", `Class '${decl.name}' cannot extend multiple classes ('${superclass}' and '${baseName}')`, decl.line, decl.col);
573
573
  }
574
574
  else {
575
575
  superclass = baseName;
@@ -579,7 +579,7 @@ export class Checker {
579
579
  interfaces.push(baseName);
580
580
  }
581
581
  else {
582
- this.diagnostics.error(`Unknown base class or interface '${baseName}'`, decl.line, decl.col);
582
+ this.diagnostics.error("KS4017", `Unknown base class or interface '${baseName}'`, decl.line, decl.col);
583
583
  }
584
584
  }
585
585
  this.classes.set(decl.name, {
@@ -602,7 +602,7 @@ export class Checker {
602
602
  let current = info.superclass;
603
603
  while (current) {
604
604
  if (seen.has(current)) {
605
- this.diagnostics.error(`Circular inheritance detected involving '${decl.name}'`, decl.line, decl.col);
605
+ this.diagnostics.error("KS4018", `Circular inheritance detected involving '${decl.name}'`, decl.line, decl.col);
606
606
  return;
607
607
  }
608
608
  seen.add(current);
@@ -617,11 +617,11 @@ export class Checker {
617
617
  return;
618
618
  if (info.superclass) {
619
619
  if (!decl.constructor.baseArgs) {
620
- this.diagnostics.error(`Class '${decl.name}' extends '${info.superclass}' and must call the base constructor via ': base(...)'`, decl.constructor.line, decl.constructor.col);
620
+ this.diagnostics.error("KS4019", `Class '${decl.name}' extends '${info.superclass}' and must call the base constructor via ': base(...)'`, decl.constructor.line, decl.constructor.col);
621
621
  }
622
622
  }
623
623
  else if (decl.constructor.baseArgs) {
624
- this.diagnostics.error(`Class '${decl.name}' has no base class; ': base(...)' is not valid here`, decl.constructor.line, decl.constructor.col);
624
+ this.diagnostics.error("KS4020", `Class '${decl.name}' has no base class; ': base(...)' is not valid here`, decl.constructor.line, decl.constructor.col);
625
625
  }
626
626
  }
627
627
  // Walks up the inheritance chain to find the nearest ancestor that already
@@ -644,28 +644,28 @@ export class Checker {
644
644
  continue; // static methods don't participate in virtual dispatch
645
645
  const methodInfo = classInfo.methods.get(method.name);
646
646
  if (methodInfo.isVirtual && methodInfo.isOverride) {
647
- this.diagnostics.error(`Method '${method.name}' cannot be both 'virtual' and 'override'`, method.line, method.col);
647
+ this.diagnostics.error("KS4021", `Method '${method.name}' cannot be both 'virtual' and 'override'`, method.line, method.col);
648
648
  }
649
649
  const ancestor = this.findNearestMethodInChain(decl.name, method.name);
650
650
  if (ancestor) {
651
651
  const overridable = ancestor.info.isVirtual || ancestor.info.isOverride;
652
652
  if (!methodInfo.isOverride) {
653
- this.diagnostics.error(`Method '${method.name}' hides inherited member '${ancestor.owner}.${method.name}'; add 'override' (and mark the base member 'virtual')`, method.line, method.col);
653
+ this.diagnostics.error("KS4022", `Method '${method.name}' hides inherited member '${ancestor.owner}.${method.name}'; add 'override' (and mark the base member 'virtual')`, method.line, method.col);
654
654
  }
655
655
  else if (!overridable) {
656
- this.diagnostics.error(`Cannot override non-virtual method '${ancestor.owner}.${method.name}'; mark it 'virtual' in '${ancestor.owner}'`, method.line, method.col);
656
+ this.diagnostics.error("KS4023", `Cannot override non-virtual method '${ancestor.owner}.${method.name}'; mark it 'virtual' in '${ancestor.owner}'`, method.line, method.col);
657
657
  }
658
658
  else {
659
659
  const paramsMatch = methodInfo.params.length === ancestor.info.params.length &&
660
660
  methodInfo.params.every((p, i) => T.typesEqual(p, ancestor.info.params[i]));
661
661
  const returnMatches = T.typesEqual(methodInfo.returnType, ancestor.info.returnType);
662
662
  if (!paramsMatch || !returnMatches) {
663
- this.diagnostics.error(`Method '${method.name}' does not match the signature of overridden method '${ancestor.owner}.${method.name}'`, method.line, method.col);
663
+ this.diagnostics.error("KS4024", `Method '${method.name}' does not match the signature of overridden method '${ancestor.owner}.${method.name}'`, method.line, method.col);
664
664
  }
665
665
  }
666
666
  }
667
667
  else if (methodInfo.isOverride) {
668
- this.diagnostics.error(`Method '${method.name}' marked 'override' but no matching method was found in a base class`, method.line, method.col);
668
+ this.diagnostics.error("KS4025", `Method '${method.name}' marked 'override' but no matching method was found in a base class`, method.line, method.col);
669
669
  }
670
670
  }
671
671
  }
@@ -677,12 +677,12 @@ export class Checker {
677
677
  for (const sig of this.collectInterfaceMethods(ifaceName)) {
678
678
  const found = this.lookupMethod(decl.name, sig.name);
679
679
  if (!found) {
680
- this.diagnostics.error(`Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
680
+ this.diagnostics.error("KS4026", `Class '${decl.name}' does not implement method '${sig.name}' required by interface '${ifaceName}'`, decl.line, decl.col);
681
681
  continue;
682
682
  }
683
683
  const paramsMatch = found.info.params.length === sig.params.length && found.info.params.every((p, i) => T.typesEqual(p, sig.params[i]));
684
684
  if (!paramsMatch || !T.typesEqual(found.info.returnType, sig.returnType)) {
685
- this.diagnostics.error(`Class '${decl.name}' member '${sig.name}' does not match the signature required by interface '${ifaceName}'`, decl.line, decl.col);
685
+ this.diagnostics.error("KS4027", `Class '${decl.name}' member '${sig.name}' does not match the signature required by interface '${ifaceName}'`, decl.line, decl.col);
686
686
  }
687
687
  }
688
688
  }
@@ -772,13 +772,13 @@ export class Checker {
772
772
  return;
773
773
  if (visibility === "private") {
774
774
  if (ctx.currentClass?.name !== owner) {
775
- this.diagnostics.error(`'${memberName}' is private and not accessible outside class '${owner}'`, line, col);
775
+ this.diagnostics.error("KS4028", `'${memberName}' is private and not accessible outside class '${owner}'`, line, col);
776
776
  }
777
777
  return;
778
778
  }
779
779
  // protected
780
780
  if (!ctx.currentClass || !(ctx.currentClass.name === owner || this.isSubclass(ctx.currentClass.name, owner))) {
781
- this.diagnostics.error(`'${memberName}' is protected and only accessible within '${owner}' or its subclasses`, line, col);
781
+ this.diagnostics.error("KS4029", `'${memberName}' is protected and only accessible within '${owner}' or its subclasses`, line, col);
782
782
  }
783
783
  }
784
784
  // Assignability: is a value of type `from` usable where type `to` is expected?
@@ -840,13 +840,13 @@ export class Checker {
840
840
  resolveBodyReturnType(declaredReturnType, isAsync, line, col) {
841
841
  if (isAsync) {
842
842
  if (declaredReturnType.kind !== "task") {
843
- this.diagnostics.error(`'async' functions/methods must return 'task' or 'task<T>', got '${T.typeToString(declaredReturnType)}'`, line, col);
843
+ this.diagnostics.error("KS4030", `'async' functions/methods must return 'task' or 'task<T>', got '${T.typeToString(declaredReturnType)}'`, line, col);
844
844
  return T.UNKNOWN;
845
845
  }
846
846
  return declaredReturnType.resultType;
847
847
  }
848
848
  if (declaredReturnType.kind === "task") {
849
- this.diagnostics.error(`A function/method returning 'task'/'task<T>' must be marked 'async'`, line, col);
849
+ this.diagnostics.error("KS4031", `A function/method returning 'task'/'task<T>' must be marked 'async'`, line, col);
850
850
  }
851
851
  return declaredReturnType;
852
852
  }
@@ -886,13 +886,13 @@ export class Checker {
886
886
  const baseCtorParams = this.lookupCtorParams(info.superclass);
887
887
  const baseCtx = { returnType: T.VOID, currentClass: info, inConstructor: false, loopDepth: 0, isAsync: false };
888
888
  if (decl.constructor.baseArgs.length !== baseCtorParams.length) {
889
- this.diagnostics.error(`Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
889
+ this.diagnostics.error("KS4032", `Expected ${baseCtorParams.length} base constructor argument(s), got ${decl.constructor.baseArgs.length}`, decl.constructor.line, decl.constructor.col);
890
890
  }
891
891
  decl.constructor.baseArgs.forEach((arg, i) => {
892
892
  const expected = baseCtorParams[i] ?? T.UNKNOWN;
893
893
  const argType = this.checkExpressionExpecting(arg, expected, paramScope, baseCtx);
894
894
  if (!this.isAssignableType(argType, expected)) {
895
- this.diagnostics.error(`Base constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
895
+ this.diagnostics.error("KS4033", `Base constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
896
896
  }
897
897
  });
898
898
  }
@@ -922,7 +922,7 @@ export class Checker {
922
922
  isAsync: false,
923
923
  });
924
924
  if (!this.isAssignableType(initType, declaredType)) {
925
- this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to static field of type '${T.typeToString(declaredType)}'`, field.line, field.col);
925
+ this.diagnostics.error("KS4034", `Cannot assign value of type '${T.typeToString(initType)}' to static field of type '${T.typeToString(declaredType)}'`, field.line, field.col);
926
926
  }
927
927
  }
928
928
  }
@@ -939,7 +939,7 @@ export class Checker {
939
939
  this.recordHover(stmt.nameLine, stmt.nameCol, `${stmt.isConst ? "const" : "let"} ${stmt.name}: ${T.typeToString(declaredType)}`);
940
940
  const initType = this.checkExpressionExpecting(stmt.init, declaredType, scope, ctx);
941
941
  if (!this.isAssignableType(initType, declaredType)) {
942
- this.diagnostics.error(`Cannot assign value of type '${T.typeToString(initType)}' to variable of type '${T.typeToString(declaredType)}'`, stmt.line, stmt.col);
942
+ this.diagnostics.error("KS4035", `Cannot assign value of type '${T.typeToString(initType)}' to variable of type '${T.typeToString(declaredType)}'`, stmt.line, stmt.col);
943
943
  }
944
944
  scope.declare(stmt.name, declaredType, stmt.isConst);
945
945
  return;
@@ -994,11 +994,11 @@ export class Checker {
994
994
  const declaredVarType = this.resolveType(stmt.varType, stmt.line, stmt.col);
995
995
  if (iterableType.kind === "array") {
996
996
  if (!this.isAssignableType(iterableType.element, declaredVarType)) {
997
- this.diagnostics.error(`Cannot use loop variable of type '${T.typeToString(declaredVarType)}' for array of '${T.typeToString(iterableType.element)}'`, stmt.line, stmt.col);
997
+ this.diagnostics.error("KS4036", `Cannot use loop variable of type '${T.typeToString(declaredVarType)}' for array of '${T.typeToString(iterableType.element)}'`, stmt.line, stmt.col);
998
998
  }
999
999
  }
1000
1000
  else if (iterableType.kind !== "unknown") {
1001
- this.diagnostics.error(`Cannot iterate over non-array type '${T.typeToString(iterableType)}'`, stmt.line, stmt.col);
1001
+ this.diagnostics.error("KS4037", `Cannot iterate over non-array type '${T.typeToString(iterableType)}'`, stmt.line, stmt.col);
1002
1002
  }
1003
1003
  const forScope = scope.child();
1004
1004
  forScope.declare(stmt.varName, declaredVarType, false);
@@ -1008,14 +1008,14 @@ export class Checker {
1008
1008
  case "ReturnStatement": {
1009
1009
  const actual = stmt.value ? this.checkExpressionExpecting(stmt.value, ctx.returnType, scope, ctx) : T.VOID;
1010
1010
  if (!this.isAssignableType(actual, ctx.returnType)) {
1011
- this.diagnostics.error(`Return type '${T.typeToString(actual)}' does not match declared return type '${T.typeToString(ctx.returnType)}'`, stmt.line, stmt.col);
1011
+ this.diagnostics.error("KS4038", `Return type '${T.typeToString(actual)}' does not match declared return type '${T.typeToString(ctx.returnType)}'`, stmt.line, stmt.col);
1012
1012
  }
1013
1013
  return;
1014
1014
  }
1015
1015
  case "BreakStatement":
1016
1016
  case "ContinueStatement":
1017
1017
  if (ctx.loopDepth === 0) {
1018
- this.diagnostics.error(`'${stmt.kind === "BreakStatement" ? "break" : "continue"}' used outside of a loop`, stmt.line, stmt.col);
1018
+ this.diagnostics.error("KS4039", `'${stmt.kind === "BreakStatement" ? "break" : "continue"}' used outside of a loop`, stmt.line, stmt.col);
1019
1019
  }
1020
1020
  return;
1021
1021
  case "ExpressionStatement":
@@ -1039,30 +1039,30 @@ export class Checker {
1039
1039
  this.checkExpression(stmt.expression, scope, ctx);
1040
1040
  return;
1041
1041
  case "FunctionDecl":
1042
- this.diagnostics.error(`Nested function declarations are not supported`, stmt.line, stmt.col);
1042
+ this.diagnostics.error("KS4040", `Nested function declarations are not supported`, stmt.line, stmt.col);
1043
1043
  return;
1044
1044
  case "ClassDecl":
1045
- this.diagnostics.error(`Nested class declarations are not supported`, stmt.line, stmt.col);
1045
+ this.diagnostics.error("KS4041", `Nested class declarations are not supported`, stmt.line, stmt.col);
1046
1046
  return;
1047
1047
  case "InterfaceDecl":
1048
- this.diagnostics.error(`Nested interface declarations are not supported`, stmt.line, stmt.col);
1048
+ this.diagnostics.error("KS4042", `Nested interface declarations are not supported`, stmt.line, stmt.col);
1049
1049
  return;
1050
1050
  case "EnumDecl":
1051
- this.diagnostics.error(`Nested enum declarations are not supported`, stmt.line, stmt.col);
1051
+ this.diagnostics.error("KS4043", `Nested enum declarations are not supported`, stmt.line, stmt.col);
1052
1052
  return;
1053
1053
  case "ExternFunctionDecl":
1054
1054
  case "ExternClassDecl":
1055
1055
  case "ExternValueDecl":
1056
- this.diagnostics.error(`'extern' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
1056
+ this.diagnostics.error("KS4044", `'extern' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
1057
1057
  return;
1058
1058
  case "RawStringDecl":
1059
- this.diagnostics.error(`'raw' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
1059
+ this.diagnostics.error("KS4045", `'raw' declarations are only allowed at the top level of a file`, stmt.line, stmt.col);
1060
1060
  return;
1061
1061
  }
1062
1062
  }
1063
1063
  expectType(actual, expected, line, col, context) {
1064
1064
  if (!T.typesEqual(actual, expected)) {
1065
- this.diagnostics.error(`Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
1065
+ this.diagnostics.error("KS4046", `Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
1066
1066
  }
1067
1067
  }
1068
1068
  // Recognizes `name != null` / `name == null` (either operand order) as a
@@ -1129,7 +1129,7 @@ export class Checker {
1129
1129
  const first = elementTypes[0];
1130
1130
  for (let i = 1; i < elementTypes.length; i++) {
1131
1131
  if (!T.typesEqual(elementTypes[i], first)) {
1132
- this.diagnostics.error(`Array elements must all have the same type`, expr.line, expr.col);
1132
+ this.diagnostics.error("KS4047", `Array elements must all have the same type`, expr.line, expr.col);
1133
1133
  break;
1134
1134
  }
1135
1135
  }
@@ -1156,14 +1156,14 @@ export class Checker {
1156
1156
  const fnInfo = this.functions.get(expr.name);
1157
1157
  if (fnInfo)
1158
1158
  return T.functionType(fnInfo.params, fnInfo.returnType);
1159
- this.diagnostics.error(`Undefined identifier '${expr.name}'`, expr.line, expr.col);
1159
+ this.diagnostics.error("KS4048", `Undefined identifier '${expr.name}'`, expr.line, expr.col);
1160
1160
  return T.UNKNOWN;
1161
1161
  }
1162
1162
  case "ThisExpr": {
1163
1163
  const found = scope.resolve("this");
1164
1164
  if (found)
1165
1165
  return found.type;
1166
- this.diagnostics.error(`'this' used outside of a class method`, expr.line, expr.col);
1166
+ this.diagnostics.error("KS4049", `'this' used outside of a class method`, expr.line, expr.col);
1167
1167
  return T.UNKNOWN;
1168
1168
  }
1169
1169
  case "UnaryExpr": {
@@ -1203,13 +1203,13 @@ export class Checker {
1203
1203
  if (expr.target.kind === "Identifier") {
1204
1204
  const found = scope.resolve(expr.target.name);
1205
1205
  if (found?.isConst) {
1206
- this.diagnostics.error(`Cannot assign to const variable '${expr.target.name}'`, expr.line, expr.col);
1206
+ this.diagnostics.error("KS4050", `Cannot assign to const variable '${expr.target.name}'`, expr.line, expr.col);
1207
1207
  }
1208
1208
  }
1209
1209
  }
1210
1210
  const valueType = this.checkExpressionExpecting(expr.value, targetType, scope, ctx);
1211
1211
  if (!this.isAssignableType(valueType, targetType)) {
1212
- this.diagnostics.error(`Cannot assign value of type '${T.typeToString(valueType)}' to target of type '${T.typeToString(targetType)}'`, expr.line, expr.col);
1212
+ this.diagnostics.error("KS4051", `Cannot assign value of type '${T.typeToString(valueType)}' to target of type '${T.typeToString(targetType)}'`, expr.line, expr.col);
1213
1213
  }
1214
1214
  return targetType;
1215
1215
  }
@@ -1226,7 +1226,7 @@ export class Checker {
1226
1226
  if (objType.kind === "array")
1227
1227
  return objType.element;
1228
1228
  if (objType.kind !== "unknown") {
1229
- this.diagnostics.error(`Cannot index into non-array type '${T.typeToString(objType)}'`, expr.line, expr.col);
1229
+ this.diagnostics.error("KS4052", `Cannot index into non-array type '${T.typeToString(objType)}'`, expr.line, expr.col);
1230
1230
  }
1231
1231
  return T.UNKNOWN;
1232
1232
  }
@@ -1240,13 +1240,13 @@ export class Checker {
1240
1240
  return this.checkLambda(expr, T.UNKNOWN, scope, ctx);
1241
1241
  case "AwaitExpr": {
1242
1242
  if (!ctx.isAsync) {
1243
- this.diagnostics.error(`'await' can only be used inside an 'async' function or method`, expr.line, expr.col);
1243
+ this.diagnostics.error("KS4053", `'await' can only be used inside an 'async' function or method`, expr.line, expr.col);
1244
1244
  }
1245
1245
  const operandType = this.checkExpression(expr.operand, scope, ctx);
1246
1246
  if (operandType.kind === "task")
1247
1247
  return operandType.resultType;
1248
1248
  if (operandType.kind !== "unknown") {
1249
- this.diagnostics.error(`Cannot 'await' a value of type '${T.typeToString(operandType)}'`, expr.line, expr.col);
1249
+ this.diagnostics.error("KS4054", `Cannot 'await' a value of type '${T.typeToString(operandType)}'`, expr.line, expr.col);
1250
1250
  }
1251
1251
  return T.UNKNOWN;
1252
1252
  }
@@ -1268,7 +1268,7 @@ export class Checker {
1268
1268
  return this.checkLambda(expr, expected, scope, ctx);
1269
1269
  if (expr.kind === "NullLiteral") {
1270
1270
  if (expected.kind !== "nullable" && expected.kind !== "unknown") {
1271
- this.diagnostics.error(`Cannot assign 'null' to non-nullable type '${T.typeToString(expected)}'`, expr.line, expr.col);
1271
+ this.diagnostics.error("KS4055", `Cannot assign 'null' to non-nullable type '${T.typeToString(expected)}'`, expr.line, expr.col);
1272
1272
  return T.UNKNOWN;
1273
1273
  }
1274
1274
  return expected;
@@ -1278,7 +1278,7 @@ export class Checker {
1278
1278
  checkLambda(expr, expected, scope, ctx) {
1279
1279
  if (expected.kind !== "function") {
1280
1280
  if (expected.kind !== "unknown") {
1281
- this.diagnostics.error(`Lambda expression is not valid where type '${T.typeToString(expected)}' is expected`, expr.line, expr.col);
1281
+ this.diagnostics.error("KS4056", `Lambda expression is not valid where type '${T.typeToString(expected)}' is expected`, expr.line, expr.col);
1282
1282
  }
1283
1283
  const paramTypes = expr.params.map((p) => this.resolveType(p.type, expr.line, expr.col));
1284
1284
  const lambdaScope = scope.child();
@@ -1289,13 +1289,13 @@ export class Checker {
1289
1289
  return T.functionType(paramTypes, actualReturnType);
1290
1290
  }
1291
1291
  if (expr.params.length !== expected.params.length) {
1292
- this.diagnostics.error(`Lambda has ${expr.params.length} parameter(s), but ${expected.params.length} were expected`, expr.line, expr.col);
1292
+ this.diagnostics.error("KS4057", `Lambda has ${expr.params.length} parameter(s), but ${expected.params.length} were expected`, expr.line, expr.col);
1293
1293
  }
1294
1294
  const paramTypes = expr.params.map((p, i) => {
1295
1295
  const declared = this.resolveType(p.type, expr.line, expr.col);
1296
1296
  const expectedParamType = expected.params[i];
1297
1297
  if (expectedParamType && !T.typesEqual(declared, expectedParamType)) {
1298
- this.diagnostics.error(`Lambda parameter '${p.name}' has type '${T.typeToString(declared)}', expected '${T.typeToString(expectedParamType)}'`, expr.line, expr.col);
1298
+ this.diagnostics.error("KS4058", `Lambda parameter '${p.name}' has type '${T.typeToString(declared)}', expected '${T.typeToString(expectedParamType)}'`, expr.line, expr.col);
1299
1299
  }
1300
1300
  return declared;
1301
1301
  });
@@ -1330,7 +1330,7 @@ export class Checker {
1330
1330
  }
1331
1331
  const actual = this.checkExpressionExpecting(expr.body, expectedReturnType, scope, lambdaCtx);
1332
1332
  if (expectedReturnType.kind !== "unknown" && !this.isAssignableType(actual, expectedReturnType)) {
1333
- this.diagnostics.error(`Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col);
1333
+ this.diagnostics.error("KS4059", `Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col);
1334
1334
  }
1335
1335
  return actual;
1336
1336
  }
@@ -1361,12 +1361,12 @@ export class Checker {
1361
1361
  if (nullSide) {
1362
1362
  const otherType = nullSide === "left" ? rightType : leftType;
1363
1363
  if (otherType.kind !== "nullable" && otherType.kind !== "unknown") {
1364
- 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);
1364
+ this.diagnostics.error("KS4060", `Type '${T.typeToString(otherType)}' can never be null — only a nullable type (e.g. '${T.typeToString(otherType)}?') can be compared to 'null'`, line, col);
1365
1365
  }
1366
1366
  return T.BOOL;
1367
1367
  }
1368
1368
  if (!T.typesEqual(leftType, rightType)) {
1369
- this.diagnostics.error(`Cannot compare '${T.typeToString(leftType)}' with '${T.typeToString(rightType)}'`, line, col);
1369
+ this.diagnostics.error("KS4061", `Cannot compare '${T.typeToString(leftType)}' with '${T.typeToString(rightType)}'`, line, col);
1370
1370
  }
1371
1371
  return T.BOOL;
1372
1372
  }
@@ -1387,7 +1387,7 @@ export class Checker {
1387
1387
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1388
1388
  return T.VOID;
1389
1389
  }
1390
- this.diagnostics.error(`Undefined function '${expr.callee.name}'`, expr.line, expr.col);
1390
+ this.diagnostics.error("KS4062", `Undefined function '${expr.callee.name}'`, expr.line, expr.col);
1391
1391
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1392
1392
  return T.UNKNOWN;
1393
1393
  }
@@ -1427,43 +1427,43 @@ export class Checker {
1427
1427
  return calleeType.returnType;
1428
1428
  }
1429
1429
  if (calleeType.kind !== "unknown") {
1430
- this.diagnostics.error(`Cannot call a value of type '${T.typeToString(calleeType)}'`, expr.line, expr.col);
1430
+ this.diagnostics.error("KS4063", `Cannot call a value of type '${T.typeToString(calleeType)}'`, expr.line, expr.col);
1431
1431
  }
1432
1432
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1433
1433
  return T.UNKNOWN;
1434
1434
  }
1435
1435
  checkArgs(expr, paramTypes, scope, ctx) {
1436
1436
  if (expr.args.length !== paramTypes.length) {
1437
- this.diagnostics.error(`Expected ${paramTypes.length} argument(s), got ${expr.args.length}`, expr.line, expr.col);
1437
+ this.diagnostics.error("KS4064", `Expected ${paramTypes.length} argument(s), got ${expr.args.length}`, expr.line, expr.col);
1438
1438
  }
1439
1439
  expr.args.forEach((arg, i) => {
1440
1440
  const expected = paramTypes[i] ?? T.UNKNOWN;
1441
1441
  const argType = this.checkExpressionExpecting(arg, expected, scope, ctx);
1442
1442
  if (!this.isAssignableType(argType, expected)) {
1443
- this.diagnostics.error(`Argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1443
+ this.diagnostics.error("KS4065", `Argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1444
1444
  }
1445
1445
  });
1446
1446
  }
1447
1447
  checkNew(expr, scope, ctx) {
1448
1448
  if (this.interfaces.has(expr.className)) {
1449
- this.diagnostics.error(`Cannot instantiate interface '${expr.className}'`, expr.line, expr.col);
1449
+ this.diagnostics.error("KS4066", `Cannot instantiate interface '${expr.className}'`, expr.line, expr.col);
1450
1450
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1451
1451
  return T.UNKNOWN;
1452
1452
  }
1453
1453
  if (this.enums.has(expr.className)) {
1454
- this.diagnostics.error(`Cannot instantiate enum '${expr.className}'`, expr.line, expr.col);
1454
+ this.diagnostics.error("KS4067", `Cannot instantiate enum '${expr.className}'`, expr.line, expr.col);
1455
1455
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1456
1456
  return T.UNKNOWN;
1457
1457
  }
1458
1458
  const info = this.classes.get(expr.className);
1459
1459
  if (!info) {
1460
- this.diagnostics.error(`Unknown class '${expr.className}'`, expr.line, expr.col);
1460
+ this.diagnostics.error("KS4068", `Unknown class '${expr.className}'`, expr.line, expr.col);
1461
1461
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1462
1462
  return T.UNKNOWN;
1463
1463
  }
1464
1464
  let typeArg;
1465
1465
  if (info.typeParam && !expr.typeArgs) {
1466
- this.diagnostics.error(`Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
1466
+ this.diagnostics.error("KS4069", `Generic class '${expr.className}' requires a type argument (e.g. 'new ${expr.className}<T>(...)')`, expr.line, expr.col);
1467
1467
  // Abstractly-typed (T-containing) ctor params, un-substitutable
1468
1468
  // without a real type argument, would otherwise cascade into a
1469
1469
  // confusing "expected 'T'" error on every argument — one clear error
@@ -1472,7 +1472,7 @@ export class Checker {
1472
1472
  return T.UNKNOWN;
1473
1473
  }
1474
1474
  if (!info.typeParam && expr.typeArgs) {
1475
- this.diagnostics.error(`Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
1475
+ this.diagnostics.error("KS4070", `Class '${expr.className}' is not generic — it doesn't take a type argument`, expr.line, expr.col);
1476
1476
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1477
1477
  return T.UNKNOWN;
1478
1478
  }
@@ -1487,13 +1487,13 @@ export class Checker {
1487
1487
  ctorParams = ctorParams.map((p) => this.substituteTypeParam(p, paramName, arg));
1488
1488
  }
1489
1489
  if (expr.args.length !== ctorParams.length) {
1490
- this.diagnostics.error(`Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1490
+ this.diagnostics.error("KS4071", `Expected ${ctorParams.length} constructor argument(s), got ${expr.args.length}`, expr.line, expr.col);
1491
1491
  }
1492
1492
  expr.args.forEach((arg, i) => {
1493
1493
  const expected = ctorParams[i] ?? T.UNKNOWN;
1494
1494
  const argType = this.checkExpressionExpecting(arg, expected, scope, ctx);
1495
1495
  if (!this.isAssignableType(argType, expected)) {
1496
- this.diagnostics.error(`Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1496
+ this.diagnostics.error("KS4072", `Constructor argument ${i + 1} has type '${T.typeToString(argType)}', expected '${T.typeToString(expected)}'`, arg.line, arg.col);
1497
1497
  }
1498
1498
  });
1499
1499
  return T.classType(expr.className, typeArg);
@@ -1538,7 +1538,7 @@ export class Checker {
1538
1538
  // inference is needed there at all.
1539
1539
  checkArrayMap(expr, elementType, scope, ctx) {
1540
1540
  if (expr.args.length !== 1) {
1541
- this.diagnostics.error(`Map expects exactly 1 argument, got ${expr.args.length}`, expr.line, expr.col);
1541
+ this.diagnostics.error("KS4073", `Map expects exactly 1 argument, got ${expr.args.length}`, expr.line, expr.col);
1542
1542
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1543
1543
  return T.arrayOf(T.UNKNOWN);
1544
1544
  }
@@ -1547,12 +1547,12 @@ export class Checker {
1547
1547
  const argType = this.checkExpressionExpecting(arg, expectedCallbackType, scope, ctx);
1548
1548
  if (argType.kind !== "function") {
1549
1549
  if (argType.kind !== "unknown") {
1550
- this.diagnostics.error(`Map expects a function as its argument, got '${T.typeToString(argType)}'`, arg.line, arg.col);
1550
+ this.diagnostics.error("KS4074", `Map expects a function as its argument, got '${T.typeToString(argType)}'`, arg.line, arg.col);
1551
1551
  }
1552
1552
  return T.arrayOf(T.UNKNOWN);
1553
1553
  }
1554
1554
  if (argType.params.length !== 1 || !this.isAssignableType(elementType, argType.params[0])) {
1555
- this.diagnostics.error(`Map callback must take a single '${T.typeToString(elementType)}' parameter, got '${T.typeToString(argType)}'`, arg.line, arg.col);
1555
+ this.diagnostics.error("KS4075", `Map callback must take a single '${T.typeToString(elementType)}' parameter, got '${T.typeToString(argType)}'`, arg.line, arg.col);
1556
1556
  }
1557
1557
  return T.arrayOf(argType.returnType);
1558
1558
  }
@@ -1573,7 +1573,7 @@ export class Checker {
1573
1573
  this.recordHover(expr.object.line, expr.object.col, `enum ${objName}`);
1574
1574
  const enumInfo = this.enums.get(objName);
1575
1575
  if (!enumInfo.members.has(expr.property)) {
1576
- this.diagnostics.error(`Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
1576
+ this.diagnostics.error("KS4076", `Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
1577
1577
  return { type: T.UNKNOWN, methodInfo: null };
1578
1578
  }
1579
1579
  return { type: T.enumType(objName), methodInfo: null };
@@ -1590,13 +1590,13 @@ export class Checker {
1590
1590
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
1591
1591
  return { type: method.info.returnType, methodInfo: method.info };
1592
1592
  }
1593
- this.diagnostics.error(`Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col);
1593
+ this.diagnostics.error("KS4077", `Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col);
1594
1594
  return { type: T.UNKNOWN, methodInfo: null };
1595
1595
  }
1596
1596
  }
1597
1597
  const objectType = this.checkExpression(expr.object, scope, ctx);
1598
1598
  if (objectType.kind === "nullable") {
1599
- 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);
1599
+ this.diagnostics.error("KS4078", `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);
1600
1600
  return { type: T.UNKNOWN, methodInfo: null };
1601
1601
  }
1602
1602
  if (objectType.kind === "string") {
@@ -1610,7 +1610,7 @@ export class Checker {
1610
1610
  const method = STRING_METHODS[expr.property];
1611
1611
  if (method)
1612
1612
  return { type: method.returnType, methodInfo: method };
1613
- this.diagnostics.error(`Unknown string member '${expr.property}'`, expr.line, expr.col);
1613
+ this.diagnostics.error("KS4079", `Unknown string member '${expr.property}'`, expr.line, expr.col);
1614
1614
  return { type: T.UNKNOWN, methodInfo: null };
1615
1615
  }
1616
1616
  if (objectType.kind === "array") {
@@ -1626,7 +1626,7 @@ export class Checker {
1626
1626
  // ever reaching checkMember, so `arr.Map(f)` works; `arr.Map` used as
1627
1627
  // a bare value (not called) falls through to this error, same as an
1628
1628
  // unknown member — a known v1 restriction.
1629
- this.diagnostics.error(`Unknown array member '${expr.property}'`, expr.line, expr.col);
1629
+ this.diagnostics.error("KS4080", `Unknown array member '${expr.property}'`, expr.line, expr.col);
1630
1630
  return { type: T.UNKNOWN, methodInfo: null };
1631
1631
  }
1632
1632
  if (objectType.kind === "state") {
@@ -1642,7 +1642,7 @@ export class Checker {
1642
1642
  };
1643
1643
  return { type: method.returnType, methodInfo: method };
1644
1644
  }
1645
- this.diagnostics.error(`Unknown state member '${expr.property}'`, expr.line, expr.col);
1645
+ this.diagnostics.error("KS4081", `Unknown state member '${expr.property}'`, expr.line, expr.col);
1646
1646
  return { type: T.UNKNOWN, methodInfo: null };
1647
1647
  }
1648
1648
  if (objectType.kind === "class") {
@@ -1658,7 +1658,7 @@ export class Checker {
1658
1658
  if (isAssignTarget && !field.info.hasSetter) {
1659
1659
  const inOwnCtor = ctx.inConstructor && ctx.currentClass?.name === field.owner;
1660
1660
  if (!inOwnCtor) {
1661
- this.diagnostics.error(`'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1661
+ this.diagnostics.error("KS4082", `'${expr.property}' has no setter and can only be assigned within ${field.owner}'s constructor`, expr.line, expr.col);
1662
1662
  }
1663
1663
  }
1664
1664
  return { type: substitute(field.info.type), methodInfo: null };
@@ -1669,7 +1669,7 @@ export class Checker {
1669
1669
  const info = paramName ? { ...method.info, params: method.info.params.map(substitute), returnType: substitute(method.info.returnType) } : method.info;
1670
1670
  return { type: info.returnType, methodInfo: info };
1671
1671
  }
1672
- this.diagnostics.error(`Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1672
+ this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1673
1673
  return { type: T.UNKNOWN, methodInfo: null };
1674
1674
  }
1675
1675
  if (objectType.kind === "interface") {
@@ -1684,11 +1684,11 @@ export class Checker {
1684
1684
  const returnType = substitute(sig.returnType);
1685
1685
  return { type: returnType, methodInfo: { params, returnType, visibility: "public", isVirtual: false, isOverride: false } };
1686
1686
  }
1687
- this.diagnostics.error(`Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1687
+ this.diagnostics.error("KS4084", `Interface '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
1688
1688
  return { type: T.UNKNOWN, methodInfo: null };
1689
1689
  }
1690
1690
  if (objectType.kind !== "unknown") {
1691
- this.diagnostics.error(`Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
1691
+ this.diagnostics.error("KS4085", `Cannot access member '${expr.property}' on type '${T.typeToString(objectType)}'`, expr.line, expr.col);
1692
1692
  }
1693
1693
  return { type: T.UNKNOWN, methodInfo: null };
1694
1694
  }
@@ -1696,19 +1696,19 @@ export class Checker {
1696
1696
  const subjectType = this.checkExpression(expr.subject, scope, ctx);
1697
1697
  this.expectType(subjectType, T.STRING, expr.line, expr.col, "match subject");
1698
1698
  if (expr.arms.length === 0) {
1699
- this.diagnostics.error(`match expression must have at least one arm`, expr.line, expr.col);
1699
+ this.diagnostics.error("KS4086", `match expression must have at least one arm`, expr.line, expr.col);
1700
1700
  return T.UNKNOWN;
1701
1701
  }
1702
1702
  const lastArm = expr.arms[expr.arms.length - 1];
1703
1703
  if (lastArm.pattern.kind !== "WildcardPattern") {
1704
- this.diagnostics.error(`match expression must end with a wildcard '_' arm`, lastArm.line, lastArm.col);
1704
+ this.diagnostics.error("KS4087", `match expression must end with a wildcard '_' arm`, lastArm.line, lastArm.col);
1705
1705
  }
1706
1706
  let resultType = null;
1707
1707
  for (const arm of expr.arms) {
1708
1708
  if (arm.pattern.kind === "LiteralPattern") {
1709
1709
  for (const value of arm.pattern.values) {
1710
1710
  if (value.kind !== "StringLiteral") {
1711
- this.diagnostics.error(`match patterns must be string literals`, value.line, value.col);
1711
+ this.diagnostics.error("KS4088", `match patterns must be string literals`, value.line, value.col);
1712
1712
  }
1713
1713
  else {
1714
1714
  this.checkExpression(value, scope, ctx);
@@ -1721,7 +1721,7 @@ export class Checker {
1721
1721
  resultType = armResultType;
1722
1722
  }
1723
1723
  else if (!this.isAssignableType(armResultType, resultType)) {
1724
- this.diagnostics.error(`match arm result type '${T.typeToString(armResultType)}' does not match preceding arms' type '${T.typeToString(resultType)}'`, arm.line, arm.col);
1724
+ this.diagnostics.error("KS4089", `match arm result type '${T.typeToString(armResultType)}' does not match preceding arms' type '${T.typeToString(resultType)}'`, arm.line, arm.col);
1725
1725
  }
1726
1726
  }
1727
1727
  return resultType ?? T.UNKNOWN;
@@ -2,11 +2,11 @@ export class DiagnosticBag {
2
2
  constructor() {
3
3
  this.diagnostics = [];
4
4
  }
5
- error(message, line, col) {
6
- this.diagnostics.push({ severity: "error", message, line, col });
5
+ error(code, message, line, col) {
6
+ this.diagnostics.push({ code, severity: "error", message, line, col });
7
7
  }
8
- warning(message, line, col) {
9
- this.diagnostics.push({ severity: "warning", message, line, col });
8
+ warning(code, message, line, col) {
9
+ this.diagnostics.push({ code, severity: "warning", message, line, col });
10
10
  }
11
11
  get hasErrors() {
12
12
  return this.diagnostics.some((d) => d.severity === "error");
@@ -17,7 +17,7 @@ export class DiagnosticBag {
17
17
  .map((d) => {
18
18
  const sourceLine = lines[d.line - 1] ?? "";
19
19
  const pointer = " ".repeat(Math.max(0, d.col - 1)) + "^";
20
- return (`${fileName}:${d.line}:${d.col} - ${d.severity}: ${d.message}\n` +
20
+ return (`${fileName}:${d.line}:${d.col} - ${d.severity} ${d.code}: ${d.message}\n` +
21
21
  ` ${sourceLine}\n` +
22
22
  ` ${pointer}`);
23
23
  })
package/dist/lexer.js CHANGED
@@ -107,7 +107,7 @@ export class Lexer {
107
107
  value += this.readStringChar();
108
108
  }
109
109
  if (this.isAtEnd()) {
110
- this.diagnostics.error("Unterminated string literal", line, col);
110
+ this.diagnostics.error("KS1001", "Unterminated string literal", line, col);
111
111
  }
112
112
  else {
113
113
  this.advance(); // closing quote
@@ -127,7 +127,7 @@ export class Lexer {
127
127
  raw += this.advance();
128
128
  }
129
129
  if (this.isAtEnd()) {
130
- this.diagnostics.error("Unterminated interpolated string literal", line, col);
130
+ this.diagnostics.error("KS1002", "Unterminated interpolated string literal", line, col);
131
131
  }
132
132
  else {
133
133
  this.advance(); // closing quote
@@ -157,7 +157,7 @@ export class Lexer {
157
157
  }
158
158
  }
159
159
  if (this.isAtEnd()) {
160
- this.diagnostics.error("Unterminated regex literal", line, col);
160
+ this.diagnostics.error("KS1003", "Unterminated regex literal", line, col);
161
161
  }
162
162
  else {
163
163
  this.advance(); // closing quote
@@ -284,7 +284,7 @@ export class Lexer {
284
284
  }
285
285
  break;
286
286
  }
287
- this.diagnostics.error(`Unexpected character '${c}'`, line, col);
287
+ this.diagnostics.error("KS1004", `Unexpected character '${c}'`, line, col);
288
288
  return this.next();
289
289
  }
290
290
  make(kind, lexeme, line, col) {
package/dist/modules.js CHANGED
@@ -25,21 +25,21 @@ function expandTemplates(program, absPath, diagnostics, fileOverrides) {
25
25
  continue;
26
26
  const templateRef = stmt.template;
27
27
  if (stmt.methods.some((m) => m.name === "Render" && !m.isStatic)) {
28
- diagnostics.error(`Class '${stmt.name}' has both a 'template' declaration and a hand-written 'Render()' method — remove one`, templateRef.line, templateRef.col);
28
+ diagnostics.error("KS3001", `Class '${stmt.name}' has both a 'template' declaration and a hand-written 'Render()' method — remove one`, templateRef.line, templateRef.col);
29
29
  continue;
30
30
  }
31
31
  const templatePath = resolve(dirname(absPath), templateRef.path);
32
32
  watchFiles.push(templatePath);
33
33
  const exists = fileOverrides?.has(templatePath) || existsSync(templatePath);
34
34
  if (!exists) {
35
- diagnostics.error(`Cannot find template file '${templateRef.path}' (looked for '${displayPath(templatePath)}')`, templateRef.line, templateRef.col);
35
+ diagnostics.error("KS3002", `Cannot find template file '${templateRef.path}' (looked for '${displayPath(templatePath)}')`, templateRef.line, templateRef.col);
36
36
  continue;
37
37
  }
38
38
  const templateSource = fileOverrides?.get(templatePath) ?? readFileSync(templatePath, "utf-8");
39
39
  const templateDiagnostics = new DiagnosticBag();
40
40
  const root = new TemplateParser(templateSource, templateDiagnostics).parseDocument();
41
41
  for (const d of templateDiagnostics.diagnostics) {
42
- diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
42
+ diagnostics.error("KS3003", `[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
43
43
  }
44
44
  if (!root)
45
45
  continue;
@@ -48,12 +48,12 @@ function expandTemplates(program, absPath, diagnostics, fileOverrides) {
48
48
  const compileDiagnostics = new DiagnosticBag();
49
49
  const { renderMethod, autoSubscribeFields } = new TemplateCompiler(memberNames, stateFieldNames, compileDiagnostics).compile(root, templateRef);
50
50
  for (const d of compileDiagnostics.diagnostics) {
51
- diagnostics.error(`[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
51
+ diagnostics.error("KS3004", `[${templateRef.path}:${d.line}:${d.col}] ${d.message}`, templateRef.line, templateRef.col);
52
52
  }
53
53
  stmt.methods.push(renderMethod);
54
54
  if (autoSubscribeFields.length > 0) {
55
55
  if (!stmt.constructor) {
56
- diagnostics.error(`Class '${stmt.name}' has a template referencing state<T> field(s) (${autoSubscribeFields.join(", ")}) but no constructor to subscribe from`, templateRef.line, templateRef.col);
56
+ diagnostics.error("KS3005", `Class '${stmt.name}' has a template referencing state<T> field(s) (${autoSubscribeFields.join(", ")}) but no constructor to subscribe from`, templateRef.line, templateRef.col);
57
57
  continue;
58
58
  }
59
59
  for (const fieldName of autoSubscribeFields) {
@@ -128,16 +128,16 @@ export function loadModuleGraph(entryAbsPath, fileOverrides) {
128
128
  stack.push(absPath);
129
129
  for (const u of program.usings) {
130
130
  if (!u.path.startsWith("./") && !u.path.startsWith("../")) {
131
- diagnostics.error(`'using' path '${u.path}' must be relative (start with './' or '../')`, u.line, u.col);
131
+ diagnostics.error("KS3006", `'using' path '${u.path}' must be relative (start with './' or '../')`, u.line, u.col);
132
132
  continue;
133
133
  }
134
134
  const depPath = resolve(dirname(absPath), u.path) + ".ks";
135
135
  if (!exists(depPath)) {
136
- diagnostics.error(`Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
136
+ diagnostics.error("KS3007", `Cannot find module '${u.path}' (looked for '${displayPath(depPath)}')`, u.line, u.col);
137
137
  continue;
138
138
  }
139
139
  if (stack.includes(depPath)) {
140
- diagnostics.error(`Circular 'using' dependency: ${[...stack, depPath].map(displayPath).join(" -> ")}`, u.line, u.col);
140
+ diagnostics.error("KS3008", `Circular 'using' dependency: ${[...stack, depPath].map(displayPath).join(" -> ")}`, u.line, u.col);
141
141
  continue;
142
142
  }
143
143
  record.dependencies.push(depPath);
@@ -180,7 +180,7 @@ export function compileGraph(entryAbsPath, fileOverrides) {
180
180
  const mergeOne = (name, apply) => {
181
181
  const existingFrom = importedFrom.get(name);
182
182
  if (existingFrom && existingFrom !== depPath) {
183
- mod.diagnostics.error(`'${name}' is exported by both '${displayPath(existingFrom)}' and '${displayPath(depPath)}' — ambiguous 'using'`, u.line, u.col);
183
+ mod.diagnostics.error("KS3009", `'${name}' is exported by both '${displayPath(existingFrom)}' and '${displayPath(depPath)}' — ambiguous 'using'`, u.line, u.col);
184
184
  return;
185
185
  }
186
186
  importedFrom.set(name, depPath);
package/dist/parser.js CHANGED
@@ -16,7 +16,7 @@ export class Parser {
16
16
  while (!this.check(TokenKind.EOF)) {
17
17
  if (this.check(TokenKind.Using)) {
18
18
  const t = this.peek();
19
- this.diagnostics.error("'using' directives must appear at the top of the file, before any other declaration", t.line, t.col);
19
+ this.diagnostics.error("KS2001", "'using' directives must appear at the top of the file, before any other declaration", t.line, t.col);
20
20
  this.parseUsing(); // consume and discard so parsing can continue
21
21
  continue;
22
22
  }
@@ -44,7 +44,7 @@ export class Parser {
44
44
  const expr = this.parseExpression();
45
45
  if (!this.check(TokenKind.EOF)) {
46
46
  const t = this.peek();
47
- this.diagnostics.error(`Unexpected token '${t.lexeme}' after expression`, t.line, t.col);
47
+ this.diagnostics.error("KS2002", `Unexpected token '${t.lexeme}' after expression`, t.line, t.col);
48
48
  }
49
49
  return expr;
50
50
  }
@@ -60,7 +60,7 @@ export class Parser {
60
60
  const type = this.parseType();
61
61
  if (!this.check(TokenKind.EOF)) {
62
62
  const t = this.peek();
63
- this.diagnostics.error(`Unexpected token '${t.lexeme}' after type`, t.line, t.col);
63
+ this.diagnostics.error("KS2003", `Unexpected token '${t.lexeme}' after type`, t.line, t.col);
64
64
  }
65
65
  return type;
66
66
  }
@@ -152,11 +152,11 @@ export class Parser {
152
152
  if (this.isDeclStart()) {
153
153
  const decl = this.parseDeclaration(isExported);
154
154
  if (decl.kind === "VarDecl") {
155
- this.diagnostics.error("'public'/'private' cannot modify a variable declaration", modifierTok.line, modifierTok.col);
155
+ this.diagnostics.error("KS2004", "'public'/'private' cannot modify a variable declaration", modifierTok.line, modifierTok.col);
156
156
  }
157
157
  return decl;
158
158
  }
159
- this.diagnostics.error("Expected a class, interface, enum, or function declaration after 'public'/'private'", modifierTok.line, modifierTok.col);
159
+ this.diagnostics.error("KS2005", "Expected a class, interface, enum, or function declaration after 'public'/'private'", modifierTok.line, modifierTok.col);
160
160
  throw new ParseError();
161
161
  }
162
162
  // Lookahead for the C#-style `Type name` declaration shape (locals and
@@ -204,7 +204,7 @@ export class Parser {
204
204
  return { kind: "FunctionDecl", isExported, isAsync, name, params, returnType: type, body, line: start.line, col: start.col };
205
205
  }
206
206
  if (isAsync) {
207
- this.diagnostics.error("'async' cannot modify a variable declaration", start.line, start.col);
207
+ this.diagnostics.error("KS2006", "'async' cannot modify a variable declaration", start.line, start.col);
208
208
  }
209
209
  this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
210
210
  const init = this.parseExpression();
@@ -273,7 +273,7 @@ export class Parser {
273
273
  if (this.check(TokenKind.Template)) {
274
274
  const templateStart = this.advance();
275
275
  if (template) {
276
- this.diagnostics.error(`Class '${name}' already has a 'template' declaration`, templateStart.line, templateStart.col);
276
+ this.diagnostics.error("KS2007", `Class '${name}' already has a 'template' declaration`, templateStart.line, templateStart.col);
277
277
  }
278
278
  this.consume(TokenKind.From, "Expected 'from \"<path>\"' after 'template'");
279
279
  const pathTok = this.consume(TokenKind.String, "Expected a file path string after 'from'");
@@ -333,7 +333,7 @@ export class Parser {
333
333
  const params = this.parseParamList();
334
334
  const body = this.parseBlock();
335
335
  if (isStatic && (isVirtual || isOverride)) {
336
- this.diagnostics.error("'static' methods cannot be 'virtual' or 'override'", memberStart.line, memberStart.col);
336
+ this.diagnostics.error("KS2008", "'static' methods cannot be 'virtual' or 'override'", memberStart.line, memberStart.col);
337
337
  }
338
338
  methods.push({
339
339
  kind: "MethodDecl",
@@ -354,13 +354,13 @@ export class Parser {
354
354
  }
355
355
  else if (this.check(TokenKind.LBrace)) {
356
356
  if (isVirtual || isOverride) {
357
- this.diagnostics.error("'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
357
+ this.diagnostics.error("KS2009", "'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
358
358
  }
359
359
  if (isAsync) {
360
- this.diagnostics.error("'async' is only valid on methods", memberStart.line, memberStart.col);
360
+ this.diagnostics.error("KS2010", "'async' is only valid on methods", memberStart.line, memberStart.col);
361
361
  }
362
362
  if (isStatic) {
363
- this.diagnostics.error("'static' properties are not supported in v1 (use a static field instead)", memberStart.line, memberStart.col);
363
+ this.diagnostics.error("KS2011", "'static' properties are not supported in v1 (use a static field instead)", memberStart.line, memberStart.col);
364
364
  }
365
365
  this.advance(); // '{'
366
366
  this.consume(TokenKind.Get, "Expected 'get' in property accessor list");
@@ -386,10 +386,10 @@ export class Parser {
386
386
  }
387
387
  else {
388
388
  if (isVirtual || isOverride) {
389
- this.diagnostics.error("'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
389
+ this.diagnostics.error("KS2012", "'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
390
390
  }
391
391
  if (isAsync) {
392
- this.diagnostics.error("'async' is only valid on methods", memberStart.line, memberStart.col);
392
+ this.diagnostics.error("KS2013", "'async' is only valid on methods", memberStart.line, memberStart.col);
393
393
  }
394
394
  let initializer = null;
395
395
  if (isStatic) {
@@ -500,7 +500,7 @@ export class Parser {
500
500
  const start = this.advance(); // 'raw'
501
501
  const typeTok = this.consume(TokenKind.Identifier, "Expected 'string' after 'raw'");
502
502
  if (typeTok.lexeme !== "string") {
503
- this.diagnostics.error(`'raw' declarations must be of type 'string', got '${typeTok.lexeme}'`, typeTok.line, typeTok.col);
503
+ this.diagnostics.error("KS2014", `'raw' declarations must be of type 'string', got '${typeTok.lexeme}'`, typeTok.line, typeTok.col);
504
504
  }
505
505
  const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
506
506
  this.consume(TokenKind.From, "Expected 'from \"<path>\"' after 'raw string <Name>'");
@@ -531,10 +531,10 @@ export class Parser {
531
531
  if (this.check(TokenKind.Constructor)) {
532
532
  const ctorTok = this.advance();
533
533
  if (virtualTok) {
534
- this.diagnostics.error(`'virtual' is not valid on an extern constructor`, virtualTok.line, virtualTok.col);
534
+ this.diagnostics.error("KS2015", `'virtual' is not valid on an extern constructor`, virtualTok.line, virtualTok.col);
535
535
  }
536
536
  if (hasConstructor) {
537
- this.diagnostics.error(`Extern class '${name}' already has a constructor signature`, ctorTok.line, ctorTok.col);
537
+ this.diagnostics.error("KS2016", `Extern class '${name}' already has a constructor signature`, ctorTok.line, ctorTok.col);
538
538
  }
539
539
  ctorParams = this.parseParamList();
540
540
  hasConstructor = true;
@@ -550,7 +550,7 @@ export class Parser {
550
550
  }
551
551
  else if (this.check(TokenKind.LBrace)) {
552
552
  if (virtualTok) {
553
- this.diagnostics.error(`'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
553
+ this.diagnostics.error("KS2017", `'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
554
554
  }
555
555
  this.advance();
556
556
  this.consume(TokenKind.Get, "Expected 'get' in extern property accessor list");
@@ -566,7 +566,7 @@ export class Parser {
566
566
  }
567
567
  else {
568
568
  if (virtualTok) {
569
- this.diagnostics.error(`'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
569
+ this.diagnostics.error("KS2018", `'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
570
570
  }
571
571
  this.consume(TokenKind.Semicolon, "Expected ';' after extern property declaration");
572
572
  properties.push({ isStatic, name: memberName, type, hasSetter: true });
@@ -671,7 +671,7 @@ export class Parser {
671
671
  finallyBlock = this.parseBlock();
672
672
  }
673
673
  if (!catchBlock && !finallyBlock) {
674
- this.diagnostics.error("'try' must be followed by 'catch' and/or 'finally'", start.line, start.col);
674
+ this.diagnostics.error("KS2019", "'try' must be followed by 'catch' and/or 'finally'", start.line, start.col);
675
675
  }
676
676
  return { kind: "TryStatement", tryBlock, catchParam, catchBlock, finallyBlock, line: start.line, col: start.col };
677
677
  }
@@ -945,7 +945,7 @@ export class Parser {
945
945
  this.consume(TokenKind.RParen, "Expected ')' after expression");
946
946
  return expr;
947
947
  }
948
- this.diagnostics.error(`Unexpected token '${t.lexeme || t.kind}'`, t.line, t.col);
948
+ this.diagnostics.error("KS2020", `Unexpected token '${t.lexeme || t.kind}'`, t.line, t.col);
949
949
  throw new ParseError();
950
950
  }
951
951
  // Distinguishes a lambda's parameter list from a parenthesized expression.
@@ -1049,7 +1049,7 @@ export class Parser {
1049
1049
  const subParser = new Parser(subTokens, subDiagnostics);
1050
1050
  const expr = subParser.parseExpression();
1051
1051
  for (const d of subDiagnostics.diagnostics) {
1052
- this.diagnostics.error(d.message, token.line, token.col);
1052
+ this.diagnostics.error("KS2021", d.message, token.line, token.col);
1053
1053
  }
1054
1054
  return expr;
1055
1055
  }
@@ -1109,7 +1109,7 @@ export class Parser {
1109
1109
  if (this.check(kind))
1110
1110
  return this.advance();
1111
1111
  const t = this.peek();
1112
- this.diagnostics.error(message, t.line, t.col);
1112
+ this.diagnostics.error("KS2022", message, t.line, t.col);
1113
1113
  throw new ParseError();
1114
1114
  }
1115
1115
  synchronize() {
@@ -222,7 +222,7 @@ export class TemplateCompiler {
222
222
  const elementChildren = node.children.filter((c) => c.kind === "element");
223
223
  const textChildren = node.children.filter((c) => c.kind === "text");
224
224
  if (textChildren.length > 0 && elementChildren.length > 0) {
225
- this.diagnostics.error(`<${node.tag}> cannot mix text and element children — Kopular's DOM bindings have no text-node API, so text can only be a whole element's content`, node.line, node.col);
225
+ this.diagnostics.error("KS5015", `<${node.tag}> cannot mix text and element children — Kopular's DOM bindings have no text-node API, so text can only be a whole element's content`, node.line, node.col);
226
226
  }
227
227
  else if (textChildren.length > 0) {
228
228
  const parts = textChildren.flatMap((t) => t.parts).map((p) => (p.kind === "Expr" ? { kind: "Expr", expression: this.resolve(p.expression, localScope) } : p));
@@ -130,7 +130,7 @@ export class TemplateLexer {
130
130
  while (!this.isAtEnd() && this.peek() !== '"')
131
131
  value += this.advance();
132
132
  if (this.isAtEnd()) {
133
- this.diagnostics.error("Unterminated attribute value", line, col);
133
+ this.diagnostics.error("KS5001", "Unterminated attribute value", line, col);
134
134
  }
135
135
  else {
136
136
  this.advance(); // closing '"'
@@ -143,7 +143,7 @@ export class TemplateLexer {
143
143
  while (!this.isAtEnd() && !/[\s=>/]/.test(this.peek()))
144
144
  name += this.advance();
145
145
  if (name.length === 0) {
146
- this.diagnostics.error(`Unexpected character '${this.peek()}' in tag`, line, col);
146
+ this.diagnostics.error("KS5002", `Unexpected character '${this.peek()}' in tag`, line, col);
147
147
  this.advance(); // avoid an infinite loop on a genuinely unexpected character
148
148
  return this.nextTagToken();
149
149
  }
@@ -44,7 +44,7 @@ export class TemplateParser {
44
44
  const expr = new Parser(tokens, localDiagnostics).parseStandaloneExpression();
45
45
  for (const d of localDiagnostics.diagnostics) {
46
46
  const { line, col } = remapPosition(fragmentLine, fragmentCol, d.line, d.col);
47
- this.diagnostics.error(d.message, line, col);
47
+ this.diagnostics.error("KS5003", d.message, line, col);
48
48
  }
49
49
  return expr;
50
50
  }
@@ -57,7 +57,7 @@ export class TemplateParser {
57
57
  const tokens = new Lexer(source, localDiagnostics).tokenize();
58
58
  const ofIndex = tokens.findIndex((t) => t.kind === TokenKind.Identifier && t.lexeme === "of");
59
59
  if (ofIndex < 1 || tokens[ofIndex - 1].kind !== TokenKind.Identifier) {
60
- this.diagnostics.error(`Expected '*for="Type varName of iterable"', got '${source}'`, fragmentLine, fragmentCol);
60
+ this.diagnostics.error("KS5004", `Expected '*for="Type varName of iterable"', got '${source}'`, fragmentLine, fragmentCol);
61
61
  return null;
62
62
  }
63
63
  const varNameToken = tokens[ofIndex - 1];
@@ -67,7 +67,7 @@ export class TemplateParser {
67
67
  const iterable = new Parser(iterableTokens, localDiagnostics).parseStandaloneExpression();
68
68
  for (const d of localDiagnostics.diagnostics) {
69
69
  const { line, col } = remapPosition(fragmentLine, fragmentCol, d.line, d.col);
70
- this.diagnostics.error(d.message, line, col);
70
+ this.diagnostics.error("KS5005", d.message, line, col);
71
71
  }
72
72
  return { varType, varName: varNameToken.lexeme, iterable, line: fragmentLine, col: fragmentCol };
73
73
  }
@@ -104,7 +104,7 @@ export class TemplateParser {
104
104
  const exprCol = col;
105
105
  advancePos(text.slice(i, exprStartOffset));
106
106
  if (end === -1) {
107
- this.diagnostics.error("Unterminated '{{' interpolation", exprLine, exprCol);
107
+ this.diagnostics.error("KS5006", "Unterminated '{{' interpolation", exprLine, exprCol);
108
108
  break;
109
109
  }
110
110
  const exprSource = text.slice(exprStartOffset, end);
@@ -139,7 +139,7 @@ export class TemplateParser {
139
139
  const elementRoots = roots.filter((r) => r.kind === "element");
140
140
  if (elementRoots.length !== 1) {
141
141
  const at = roots[0] ?? { line: 1, col: 1 };
142
- this.diagnostics.error(`A template must have exactly one top-level element, found ${elementRoots.length}`, at.line, at.col);
142
+ this.diagnostics.error("KS5007", `A template must have exactly one top-level element, found ${elementRoots.length}`, at.line, at.col);
143
143
  return null;
144
144
  }
145
145
  return elementRoots[0];
@@ -153,7 +153,7 @@ export class TemplateParser {
153
153
  return this.parseElement();
154
154
  }
155
155
  const t = this.advance();
156
- this.diagnostics.error(`Unexpected token in template ('${t.lexeme}')`, t.line, t.col);
156
+ this.diagnostics.error("KS5008", `Unexpected token in template ('${t.lexeme}')`, t.line, t.col);
157
157
  return null;
158
158
  }
159
159
  parseElement() {
@@ -183,7 +183,7 @@ export class TemplateParser {
183
183
  }
184
184
  else if (name === "*if") {
185
185
  if (ifCondition || forBinding) {
186
- this.diagnostics.error(`Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
186
+ this.diagnostics.error("KS5009", `Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
187
187
  }
188
188
  else {
189
189
  ifCondition = this.parseEmbeddedExpression(valueTok.lexeme, valueTok.line, valueTok.col);
@@ -191,7 +191,7 @@ export class TemplateParser {
191
191
  }
192
192
  else if (name === "*for") {
193
193
  if (ifCondition || forBinding) {
194
- this.diagnostics.error(`Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
194
+ this.diagnostics.error("KS5010", `Only one structural directive ('*if'/'*for') is allowed per element`, nameTok.line, nameTok.col);
195
195
  }
196
196
  else {
197
197
  forBinding = this.parseForBinding(valueTok.lexeme, valueTok.line, valueTok.col);
@@ -219,11 +219,11 @@ export class TemplateParser {
219
219
  if (this.check(TemplateTokenKind.TagClose)) {
220
220
  const closeTok = this.advance();
221
221
  if (closeTok.lexeme !== tag) {
222
- this.diagnostics.error(`Mismatched closing tag: expected '</${tag}>', got '</${closeTok.lexeme}>'`, closeTok.line, closeTok.col);
222
+ this.diagnostics.error("KS5011", `Mismatched closing tag: expected '</${tag}>', got '</${closeTok.lexeme}>'`, closeTok.line, closeTok.col);
223
223
  }
224
224
  }
225
225
  else {
226
- this.diagnostics.error(`Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
226
+ this.diagnostics.error("KS5012", `Expected closing tag '</${tag}>'`, tagTok.line, tagTok.col);
227
227
  }
228
228
  }
229
229
  return { kind: "element", tag, staticAttrs, propBindings, eventBindings, ifCondition, forBinding, children, line: tagTok.line, col: tagTok.col };
@@ -233,7 +233,7 @@ export class TemplateParser {
233
233
  }
234
234
  consumeAttrValue(attrNameTok) {
235
235
  if (!this.check(TemplateTokenKind.AttrValue)) {
236
- this.diagnostics.error(`Expected a quoted value after '${attrNameTok.lexeme}='`, attrNameTok.line, attrNameTok.col);
236
+ this.diagnostics.error("KS5013", `Expected a quoted value after '${attrNameTok.lexeme}='`, attrNameTok.line, attrNameTok.col);
237
237
  return null;
238
238
  }
239
239
  return this.advance();
@@ -243,7 +243,7 @@ export class TemplateParser {
243
243
  this.advance();
244
244
  }
245
245
  else {
246
- this.diagnostics.error(message, at.line, at.col);
246
+ this.diagnostics.error("KS5014", message, at.line, at.col);
247
247
  }
248
248
  }
249
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "KopScript: a small OOP, strongly-typed language that transpiles to JavaScript, with generics and nullable types",
5
5
  "type": "module",
6
6
  "license": "MIT",