kopscript 0.19.0 → 0.20.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
@@ -23,9 +23,18 @@ ks build|check <file.ks> --json # single JSON object on stdout instead of huma
23
23
  `--json` output shape (also what a tool/agent should parse instead of scraping text):
24
24
 
25
25
  ```json
26
- { "success": false, "diagnostics": [{ "code": "KS4065", "severity": "error", "message": "...", "line": 2, "col": 14, "file": "/abs/path.ks" }], "written": [] }
26
+ { "success": false, "diagnostics": [{ "code": "KS4083", "severity": "error", "message": "Class 'Widget' has no member 'Vlaue'", "line": 3, "col": 13, "fix": "Did you mean 'Value'?", "file": "/abs/path.ks" }], "written": [] }
27
27
  ```
28
28
 
29
+ `fix` is present only when the checker/parser found something concrete enough to say beyond
30
+ the message itself — a real "did you mean 'X'?" against an actually-declared name close
31
+ enough to be a plausible typo (unknown member/function/identifier/type — `KS4083`/`KS4062`/
32
+ `KS4048`/`KS4008`/`KS4076`/`KS4077`), or a precise, always-accurate pointer for a handful of
33
+ other high-frequency errors (a missing `await` on `KS4059`, `match` also accepting an enum
34
+ subject on `KS4046`, a stray type-parameter list on `KS2023`, an invalid `match` pattern on
35
+ `KS4088`). Absent entirely (no empty string) when nothing that specific applies — most
36
+ diagnostics still don't have one, and that's expected, not a gap to fill in for every code.
37
+
29
38
  `written` is the absolute paths actually written (`build` only, and only on success — a
30
39
  failed `build` writes nothing; `.js.map` paths aren't listed separately, one is written next
31
40
  to each `.js` path in `written`). Exit code is `0` iff `success` is `true`. A missing entry
package/README.md CHANGED
@@ -873,12 +873,20 @@ programmatically instead of scraping formatted text:
873
873
  {
874
874
  "success": false,
875
875
  "diagnostics": [
876
- { "code": "KS4065", "severity": "error", "message": "Argument 2 has type 'string', expected 'number'", "line": 2, "col": 14, "file": "/abs/path/to/file.ks" }
876
+ { "code": "KS4083", "severity": "error", "message": "Class 'Widget' has no member 'Vlaue'", "line": 3, "col": 13, "fix": "Did you mean 'Value'?", "file": "/abs/path/to/file.ks" }
877
877
  ],
878
878
  "written": []
879
879
  }
880
880
  ```
881
881
 
882
+ Some diagnostics also carry a `fix` field — a real "did you mean 'X'?" against something
883
+ actually declared, for the unknown-member/function/identifier/type family of errors, or a
884
+ precise next step for a handful of other common ones (a missing `await`, `match` also
885
+ accepting an enum subject, a stray type-parameter list). Not every diagnostic has one —
886
+ absent entirely, not an empty string, when nothing that specific applies — but where it's
887
+ present, it's meant to be actionable directly, by a human or an AI agent, without needing to
888
+ re-derive it from the message alone.
889
+
882
890
  `written` lists the absolute paths of every `.js` file actually written (always empty for
883
891
  `check`, and for `build` on failure — nothing is written unless the whole graph is
884
892
  error-free). A missing entry file reports `{ "success": false, "diagnostics": [],
@@ -890,9 +898,9 @@ Every diagnostic carries a stable `code` (`KS` + a number) alongside its human-r
890
898
  of parsing prose that can be reworded between versions. Codes are grouped by pipeline stage
891
899
  and never reused once assigned: `KS1xxx` lexer, `KS2xxx` parser, `KS3xxx` module/`using`
892
900
  resolution, `KS4xxx` the checker (by far the largest category — most real type errors live
893
- here), `KS5xxx` templates. There's no generated reference doc mapping every code to an
894
- explanation yet for now, `message` is still the primary explanation; `code` is for
895
- matching, not (yet) for looking up docs.
901
+ here), `KS5xxx` templates. There's still no generated reference doc mapping every code to an
902
+ explanation — `message` (plus `fix`, when present — see above) is the explanation; `code` is
903
+ for matching reliably across wording changes, not for looking up a separate page.
896
904
 
897
905
  **Source maps**: `build` writes a real source-map v3 `<file>.js.map` alongside every
898
906
  `<file>.js`, with the original `.ks` source embedded (`sourcesContent`) so a deployed app
package/dist/checker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
+ import { closestMatch } from "./suggest.js";
3
4
  import * as T from "./types.js";
4
5
  export function emptyModuleExports() {
5
6
  return { namedTypes: new Map(), classes: new Map(), interfaces: new Map(), enums: new Map(), functions: new Map(), externValues: new Map() };
@@ -40,6 +41,14 @@ class Scope {
40
41
  child() {
41
42
  return new Scope(this);
42
43
  }
44
+ // Every local/param name visible from here, walking up through every
45
+ // enclosing scope — used only to build "did you mean 'X'?" candidates for
46
+ // an undefined-identifier error (KS4048), never for real name resolution
47
+ // (resolve() above already does that correctly, including shadowing).
48
+ allNames() {
49
+ const own = [...this.vars.keys()];
50
+ return this.parent ? [...own, ...this.parent.allNames()] : own;
51
+ }
43
52
  }
44
53
  export class Checker {
45
54
  recordHover(line, col, text) {
@@ -389,7 +398,9 @@ export class Checker {
389
398
  this.diagnostics.error("KS4007", `Generic type '${name}' requires ${this.describeArity(declaredParams)} (e.g. '${name}<${declaredParams.join(", ")}>')`, line, col);
390
399
  }
391
400
  else {
392
- this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col);
401
+ const candidates = [...this.namedTypes.keys(), "number", "string", "bool", "void"];
402
+ const suggestion = closestMatch(name, candidates);
403
+ this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col, suggestion ? `Did you mean '${suggestion}'?` : undefined);
393
404
  }
394
405
  return T.UNKNOWN;
395
406
  }
@@ -1071,6 +1082,35 @@ export class Checker {
1071
1082
  }
1072
1083
  return null;
1073
1084
  }
1085
+ // Every instance field/method name declared anywhere in className's own
1086
+ // superclass chain — used only to build "did you mean 'X'?" candidates
1087
+ // for an unknown-member error (KS4083), never for real member resolution
1088
+ // (lookupField/lookupMethod above already do that correctly).
1089
+ allInstanceMemberNames(className) {
1090
+ const names = [];
1091
+ let current = className;
1092
+ while (current) {
1093
+ const info = this.classes.get(current);
1094
+ if (!info)
1095
+ break;
1096
+ names.push(...info.fields.keys(), ...info.methods.keys());
1097
+ current = info.superclass;
1098
+ }
1099
+ return names;
1100
+ }
1101
+ // Same, for static members (KS4077's own candidates).
1102
+ allStaticMemberNames(className) {
1103
+ const names = [];
1104
+ let current = className;
1105
+ while (current) {
1106
+ const info = this.classes.get(current);
1107
+ if (!info)
1108
+ break;
1109
+ names.push(...info.staticFields.keys(), ...info.staticMethods.keys());
1110
+ current = info.superclass;
1111
+ }
1112
+ return names;
1113
+ }
1074
1114
  // Enforces C#-style private/protected/public access from the current class context.
1075
1115
  checkAccessibility(visibility, owner, ctx, memberName, line, col) {
1076
1116
  if (visibility === "public")
@@ -1384,7 +1424,14 @@ export class Checker {
1384
1424
  }
1385
1425
  expectType(actual, expected, line, col, context) {
1386
1426
  if (!T.typesEqual(actual, expected)) {
1387
- this.diagnostics.error("KS4046", `Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col);
1427
+ // The one context-specific hint worth giving here: a match subject
1428
+ // that's neither string nor enum most likely means the author didn't
1429
+ // know match accepts an enum too (see LLM.md's "match expression").
1430
+ // Every OTHER expectType call site (if-condition, binary-op operand,
1431
+ // ...) doesn't have an equally specific, generically-true next step,
1432
+ // so it gets no fix rather than a vague one.
1433
+ const fix = context === "match subject" ? `match also accepts an enum subject — see LLM.md's "match expression" section.` : undefined;
1434
+ this.diagnostics.error("KS4046", `Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col, fix);
1388
1435
  }
1389
1436
  }
1390
1437
  // Recognizes `name != null` / `name == null` (either operand order) as a
@@ -1478,7 +1525,23 @@ export class Checker {
1478
1525
  const fnInfo = this.functions.get(expr.name);
1479
1526
  if (fnInfo)
1480
1527
  return T.functionType(fnInfo.params, fnInfo.returnType);
1481
- this.diagnostics.error("KS4048", `Undefined identifier '${expr.name}'`, expr.line, expr.col);
1528
+ {
1529
+ const candidates = [...scope.allNames(), ...this.functions.keys(), ...this.externValues.keys()];
1530
+ const suggestion = closestMatch(expr.name, candidates);
1531
+ // No implicit `this` (see LLM.md's "Common mistakes") is a real,
1532
+ // common, PRECISELY detectable case of this exact error: the name
1533
+ // is undefined as a bare identifier, but it IS a real member of
1534
+ // the enclosing class — so it's always accurate to point at
1535
+ // `this.${name}`, never a guess the way the Levenshtein
1536
+ // suggestion above already handles unrelated-typo cases.
1537
+ const isOwnMember = ctx.currentClass ? this.allInstanceMemberNames(ctx.currentClass.name).includes(expr.name) : false;
1538
+ const fix = suggestion
1539
+ ? `Did you mean '${suggestion}'?`
1540
+ : isOwnMember
1541
+ ? `No implicit 'this' — did you mean 'this.${expr.name}'?`
1542
+ : undefined;
1543
+ this.diagnostics.error("KS4048", `Undefined identifier '${expr.name}'`, expr.line, expr.col, fix);
1544
+ }
1482
1545
  return T.UNKNOWN;
1483
1546
  }
1484
1547
  case "ThisExpr": {
@@ -1665,7 +1728,12 @@ export class Checker {
1665
1728
  return T.VOID;
1666
1729
  }
1667
1730
  if (expectedReturnType.kind !== "unknown" && !this.isAssignableType(actual, expectedReturnType)) {
1668
- this.diagnostics.error("KS4059", `Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col);
1731
+ // A `task`-typed body where anything else was expected is very often
1732
+ // a missing `await`, not a genuinely wrong return type — precise and
1733
+ // always accurate to say so specifically, unlike a generic "check the
1734
+ // types" hint that wouldn't be worth giving.
1735
+ const fix = actual.kind === "task" ? "Did you forget 'await'?" : undefined;
1736
+ this.diagnostics.error("KS4059", `Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col, fix);
1669
1737
  }
1670
1738
  return actual;
1671
1739
  }
@@ -1722,7 +1790,20 @@ export class Checker {
1722
1790
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1723
1791
  return T.VOID;
1724
1792
  }
1725
- this.diagnostics.error("KS4062", `Undefined function '${expr.callee.name}'`, expr.line, expr.col);
1793
+ {
1794
+ // Same "no implicit this" mistake KS4048 checks for (see its own
1795
+ // comment) — `Increment()` inside a method never resolves to
1796
+ // `this.Increment()`, even when Increment really is a method on
1797
+ // the enclosing class.
1798
+ const isOwnMethod = ctx.currentClass ? this.allInstanceMemberNames(ctx.currentClass.name).includes(expr.callee.name) : false;
1799
+ const suggestion = isOwnMethod ? null : closestMatch(expr.callee.name, [...this.functions.keys()]);
1800
+ const fix = isOwnMethod
1801
+ ? `No implicit 'this' — did you mean 'this.${expr.callee.name}(...)'?`
1802
+ : suggestion
1803
+ ? `Did you mean '${suggestion}'?`
1804
+ : undefined;
1805
+ this.diagnostics.error("KS4062", `Undefined function '${expr.callee.name}'`, expr.line, expr.col, fix);
1806
+ }
1726
1807
  expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
1727
1808
  return T.UNKNOWN;
1728
1809
  }
@@ -2054,7 +2135,8 @@ export class Checker {
2054
2135
  this.recordHover(expr.object.line, expr.object.col, `enum ${objName}`);
2055
2136
  const enumInfo = this.enums.get(objName);
2056
2137
  if (!enumInfo.members.has(expr.property)) {
2057
- this.diagnostics.error("KS4076", `Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col);
2138
+ const suggestion = closestMatch(expr.property, [...enumInfo.members.keys()]);
2139
+ this.diagnostics.error("KS4076", `Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col, suggestion ? `Did you mean '${objName}.${suggestion}'?` : undefined);
2058
2140
  return { type: T.UNKNOWN, methodInfo: null };
2059
2141
  }
2060
2142
  return { type: T.enumType(objName), methodInfo: null };
@@ -2071,7 +2153,10 @@ export class Checker {
2071
2153
  this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
2072
2154
  return { type: method.info.returnType, methodInfo: method.info };
2073
2155
  }
2074
- this.diagnostics.error("KS4077", `Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col);
2156
+ {
2157
+ const suggestion = closestMatch(expr.property, this.allStaticMemberNames(objName));
2158
+ this.diagnostics.error("KS4077", `Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col, suggestion ? `Did you mean '${objName}.${suggestion}'?` : undefined);
2159
+ }
2075
2160
  return { type: T.UNKNOWN, methodInfo: null };
2076
2161
  }
2077
2162
  }
@@ -2159,7 +2244,12 @@ export class Checker {
2159
2244
  };
2160
2245
  return { type: info.returnType, methodInfo: info };
2161
2246
  }
2162
- this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col);
2247
+ {
2248
+ const suggestion = closestMatch(expr.property, this.allInstanceMemberNames(objectType.name));
2249
+ this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col, suggestion
2250
+ ? `Did you mean '${suggestion}'?`
2251
+ : `No similar member found on '${objectType.name}' — if this should exist, check its class (or extern binding, if '${objectType.name}' describes external JS) actually declares it.`);
2252
+ }
2163
2253
  return { type: T.UNKNOWN, methodInfo: null };
2164
2254
  }
2165
2255
  if (objectType.kind === "interface") {
@@ -2243,11 +2333,11 @@ export class Checker {
2243
2333
  // valueType === "unknown" means checkExpression already
2244
2334
  // reported its own error (e.g. "Enum has no member X") —
2245
2335
  // don't pile a second, less specific one on top of it.
2246
- this.reportInvalidMatchPattern(`match pattern must be a member of enum '${enumName}' (e.g. '${enumName}.SomeMember')`, value.line, value.col);
2336
+ this.reportInvalidMatchPattern(`match pattern must be a member of enum '${enumName}' (e.g. '${enumName}.SomeMember')`, value.line, value.col, `See LLM.md's "match expression" section for enum-pattern syntax.`);
2247
2337
  }
2248
2338
  }
2249
2339
  else if (value.kind !== "StringLiteral") {
2250
- this.reportInvalidMatchPattern(`match patterns must be string literals`, value.line, value.col);
2340
+ this.reportInvalidMatchPattern(`match patterns must be string literals`, value.line, value.col, `A non-string, non-enum match subject isn't supported at all — see LLM.md's "match expression" section.`);
2251
2341
  }
2252
2342
  else {
2253
2343
  this.checkExpression(value, scope, ctx);
@@ -2255,7 +2345,7 @@ export class Checker {
2255
2345
  }
2256
2346
  }
2257
2347
  else if (arm.pattern.kind === "RegexPattern" && isEnumSubject) {
2258
- this.reportInvalidMatchPattern(`a regex pattern is not valid for a match over enum '${enumName}' — enum patterns must name a member`, arm.line, arm.col);
2348
+ this.reportInvalidMatchPattern(`a regex pattern is not valid for a match over enum '${enumName}' — enum patterns must name a member`, arm.line, arm.col, `Use '${enumName}.SomeMember' patterns instead — see LLM.md's "match expression" section.`);
2259
2349
  }
2260
2350
  // WildcardPattern (and RegexPattern against a string subject) need no
2261
2351
  // further checking here.
@@ -2282,7 +2372,7 @@ export class Checker {
2282
2372
  // subject's kind (not a string literal against a string subject, not a
2283
2373
  // same-enum member reference against an enum subject, or a regex pattern
2284
2374
  // against an enum subject at all).
2285
- reportInvalidMatchPattern(message, line, col) {
2286
- this.diagnostics.error("KS4088", message, line, col);
2375
+ reportInvalidMatchPattern(message, line, col, fix) {
2376
+ this.diagnostics.error("KS4088", message, line, col, fix);
2287
2377
  }
2288
2378
  }
package/dist/codegen.js CHANGED
@@ -98,8 +98,27 @@ export class CodeGenerator {
98
98
  // `public` into scope without naming it explicitly. `sourceFileName`/
99
99
  // `sourceText` are for the emitted source map only (both optional so every
100
100
  // existing test call site keeps compiling unchanged).
101
- generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = "") {
102
- this.interfaceNames = new Set(program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name));
101
+ generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = "",
102
+ // Interface names visible via a `using` this module doesn't declare
103
+ // itself — separate from `usingExports` above, which deliberately
104
+ // excludes interfaces (they're compile-time only and never produce a
105
+ // JS import). Without this, a class implementing an interface declared
106
+ // in a DIFFERENT file (e.g. `class Component : Flushable` where
107
+ // `Flushable` lives in vdom.ks) has that interface name missing from
108
+ // `interfaceNames` entirely, so genClass's own "only a non-interface
109
+ // base list entry becomes `extends`" check can't tell it apart from a
110
+ // real (missing) superclass — it gets emitted as `extends Flushable`,
111
+ // a real runtime reference to a name nothing ever imports, throwing
112
+ // `ReferenceError: Flushable is not defined` the moment the class is
113
+ // ever loaded. Found for real building Kopular's own event-batching
114
+ // fix — the checker already resolves a cross-module interface's base
115
+ // list entry correctly (it type-checks fine); only codegen's own,
116
+ // separate, same-file-only interfaceNames set didn't know about it.
117
+ importedInterfaceNames = new Set()) {
118
+ this.interfaceNames = new Set([
119
+ ...program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name),
120
+ ...importedInterfaceNames,
121
+ ]);
103
122
  this.outputLine = 1;
104
123
  const outFileName = sourceFileName.replace(/\.ks$/, ".js");
105
124
  this.sourceMap = new SourceMapBuilder(outFileName, sourceFileName, sourceText);
@@ -2,11 +2,11 @@ export class DiagnosticBag {
2
2
  constructor() {
3
3
  this.diagnostics = [];
4
4
  }
5
- error(code, message, line, col) {
6
- this.diagnostics.push({ code, severity: "error", message, line, col });
5
+ error(code, message, line, col, fix) {
6
+ this.diagnostics.push({ code, severity: "error", message, line, col, ...(fix ? { fix } : {}) });
7
7
  }
8
- warning(code, message, line, col) {
9
- this.diagnostics.push({ code, severity: "warning", message, line, col });
8
+ warning(code, message, line, col, fix) {
9
+ this.diagnostics.push({ code, severity: "warning", message, line, col, ...(fix ? { fix } : {}) });
10
10
  }
11
11
  get hasErrors() {
12
12
  return this.diagnostics.some((d) => d.severity === "error");
@@ -17,10 +17,19 @@ 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
+ const fixLine = d.fix ? `\n fix: ${d.fix}` : "";
20
21
  return (`${fileName}:${d.line}:${d.col} - ${d.severity} ${d.code}: ${d.message}\n` +
21
22
  ` ${sourceLine}\n` +
22
- ` ${pointer}`);
23
+ ` ${pointer}${fixLine}`);
23
24
  })
24
25
  .join("\n\n");
25
26
  }
27
+ // Machine-readable form of the exact same diagnostics format() prints —
28
+ // for a caller that wants to act on a compile error programmatically
29
+ // (an editor extension, a CI check, an AI coding agent) instead of
30
+ // scraping formatted text. `fileName` matches format()'s own parameter so
31
+ // both outputs agree on what file each diagnostic is attributed to.
32
+ toJSON(fileName) {
33
+ return { file: fileName, diagnostics: this.diagnostics };
34
+ }
26
35
  }
package/dist/modules.js CHANGED
@@ -226,6 +226,13 @@ export function compileGraph(entryAbsPath, fileOverrides) {
226
226
  for (const absPath of order) {
227
227
  const mod = modules.get(absPath);
228
228
  const usingExports = new Map();
229
+ // Separate from usingExports (interfaces are excluded there — see its
230
+ // own comment below) — this is what tells codegen's own interfaceNames
231
+ // set about an interface declared in a DIFFERENT file this module
232
+ // `using`s, so a class implementing one doesn't get miscompiled into a
233
+ // real `extends <InterfaceName>` runtime reference (see
234
+ // CodeGenerator.generate's own comment on importedInterfaceNames).
235
+ const importedInterfaceNames = new Set();
229
236
  for (const u of mod.program.usings) {
230
237
  const depPath = resolve(dirname(absPath), u.path) + ".ks";
231
238
  const depExports = exportsByModule.get(depPath);
@@ -234,13 +241,17 @@ export function compileGraph(entryAbsPath, fileOverrides) {
234
241
  // they'd make an invalid import specifier if listed here.
235
242
  const importableTypeNames = [...depExports.namedTypes.entries()].filter(([, kind]) => kind !== "interface").map(([name]) => name);
236
243
  usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
244
+ for (const [name, kind] of depExports.namedTypes) {
245
+ if (kind === "interface")
246
+ importedInterfaceNames.add(name);
247
+ }
237
248
  }
238
249
  }
239
250
  // Just the basename, not a cwd-relative path — the .js.map file always
240
251
  // lands right next to its .ks/.js siblings (ks build writes output in
241
252
  // place), so "sources" has to resolve relative to *that* directory, not
242
253
  // wherever the compiler happened to be invoked from.
243
- const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source);
254
+ const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source, importedInterfaceNames);
244
255
  outputs.set(absPath, code);
245
256
  sourceMaps.set(absPath, map);
246
257
  }
package/dist/parser.js CHANGED
@@ -528,7 +528,7 @@ export class Parser {
528
528
  // site"): a type-parameter list appeared somewhere only a function
529
529
  // declaration (real or extern) may have one.
530
530
  reportStrayTypeParamList(line, col) {
531
- this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", line, col);
531
+ this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", line, col, "Remove the type parameter list here, or add '(...)' parameters if this was meant to be a function — a variable (real or extern) can never have its own type parameters, only a function (real or extern) or a class/interface can.");
532
532
  }
533
533
  // Optional `from "<path>"` (omit for an ambient global) and optional
534
534
  // `as "<jsName>"` (omit when the JS-side name matches the KopScript-declared one).
@@ -0,0 +1,44 @@
1
+ // Plain edit-distance "did you mean X?" suggestion — used by the checker
2
+ // wherever a name doesn't resolve (an unknown member, function, identifier,
3
+ // or type name) to point at the closest ACTUALLY-DECLARED name, when one is
4
+ // close enough to plausibly be a typo. Deliberately generic (no knowledge
5
+ // of what kind of name it's comparing — member, function, type, whatever
6
+ // the caller passes as `candidates`), so every "unknown X" diagnostic can
7
+ // reuse the exact same logic instead of each hand-rolling its own.
8
+ function levenshteinDistance(a, b) {
9
+ const rows = a.length + 1;
10
+ const cols = b.length + 1;
11
+ const dp = Array.from({ length: rows }, () => new Array(cols).fill(0));
12
+ for (let i = 0; i < rows; i++)
13
+ dp[i][0] = i;
14
+ for (let j = 0; j < cols; j++)
15
+ dp[0][j] = j;
16
+ for (let i = 1; i < rows; i++) {
17
+ for (let j = 1; j < cols; j++) {
18
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
19
+ }
20
+ }
21
+ return dp[a.length][b.length];
22
+ }
23
+ // Returns the closest of `candidates` to `target`, or null if nothing is
24
+ // close enough to be worth suggesting (a totally unrelated name is worse
25
+ // than no suggestion at all — e.g. suggesting 'value' isn't found via
26
+ // "close typo of an existing member" reasoning if the member genuinely
27
+ // doesn't exist anywhere close). The threshold scales with the target's own
28
+ // length: a short name needs a near-exact match, a long one tolerates
29
+ // proportionally more difference.
30
+ export function closestMatch(target, candidates) {
31
+ let best = null;
32
+ let bestDistance = Infinity;
33
+ for (const candidate of candidates) {
34
+ if (candidate === target)
35
+ continue; // never suggest the name itself
36
+ const distance = levenshteinDistance(target, candidate);
37
+ if (distance < bestDistance) {
38
+ bestDistance = distance;
39
+ best = candidate;
40
+ }
41
+ }
42
+ const threshold = Math.max(2, Math.ceil(target.length / 3));
43
+ return best !== null && bestDistance <= threshold ? best : null;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopscript",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",