kopscript 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,31 +7,89 @@ function outputPathFor(filePath) {
7
7
  const name = basename(filePath, ".ks");
8
8
  return join(resolve(filePath, ".."), `${name}.js`);
9
9
  }
10
+ function collectDiagnostics(result) {
11
+ const all = [];
12
+ for (const absPath of result.order) {
13
+ const mod = result.modules.get(absPath);
14
+ for (const d of mod.diagnostics.diagnostics)
15
+ all.push({ ...d, file: absPath });
16
+ }
17
+ return all;
18
+ }
19
+ // One JSON object per invocation, always to stdout (never split across
20
+ // lines, never mixed with human-readable output) — so a caller can pipe
21
+ // this straight into `JSON.parse` without worrying about interleaving.
22
+ function printJson(result, written) {
23
+ const diagnostics = collectDiagnostics(result);
24
+ console.log(JSON.stringify({ success: result.success, diagnostics, written }, null, 2));
25
+ }
26
+ function printJsonEntryMissing(filePath) {
27
+ console.log(JSON.stringify({ success: false, diagnostics: [], written: [], error: `cannot find file '${filePath}'` }, null, 2));
28
+ }
29
+ function printHumanDiagnostics(result) {
30
+ for (const absPath of result.order) {
31
+ const mod = result.modules.get(absPath);
32
+ if (mod.diagnostics.hasErrors) {
33
+ console.error(mod.diagnostics.format(mod.source, absPath));
34
+ console.error("");
35
+ }
36
+ }
37
+ }
10
38
  // Compiles the whole module graph reachable from `filePath`, writing one
11
- // .js file next to each .ks source.
12
- function build(filePath) {
39
+ // .js file next to each .ks source. With `jsonMode`, nothing but a single
40
+ // JSON object goes to stdout (no "Wrote ..." line, no human diagnostics)
41
+ // see `printJson` for the shape.
42
+ function build(filePath, jsonMode = false) {
13
43
  const result = compileGraph(filePath);
14
44
  if (result.entryMissing) {
15
- console.error(`ks: cannot find file '${filePath}'`);
45
+ if (jsonMode)
46
+ printJsonEntryMissing(filePath);
47
+ else
48
+ console.error(`ks: cannot find file '${filePath}'`);
16
49
  process.exitCode = 1;
17
50
  return { outPath: null, watchFiles: [] };
18
51
  }
19
52
  if (!result.success) {
20
- for (const absPath of result.order) {
21
- const mod = result.modules.get(absPath);
22
- if (mod.diagnostics.hasErrors) {
23
- console.error(mod.diagnostics.format(mod.source, absPath));
24
- console.error("");
25
- }
26
- }
53
+ if (jsonMode)
54
+ printJson(result, []);
55
+ else
56
+ printHumanDiagnostics(result);
27
57
  process.exitCode = 1;
28
58
  return { outPath: null, watchFiles: result.order };
29
59
  }
30
60
  for (const absPath of result.order) {
31
61
  writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
32
62
  }
63
+ if (jsonMode)
64
+ printJson(result, result.order.map(outputPathFor));
33
65
  return { outPath: outputPathFor(filePath), watchFiles: result.order };
34
66
  }
67
+ // Type-checks the graph without writing any output — for CI or an editor/
68
+ // agent that wants pass/fail plus diagnostics without touching the
69
+ // filesystem. Same exit-code convention as `build` (0 clean, 1 on any
70
+ // error or a missing entry file).
71
+ function checkCommand(filePath, jsonMode) {
72
+ const result = compileGraph(filePath);
73
+ if (result.entryMissing) {
74
+ if (jsonMode)
75
+ printJsonEntryMissing(filePath);
76
+ else
77
+ console.error(`ks: cannot find file '${filePath}'`);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ if (jsonMode) {
82
+ printJson(result, []);
83
+ }
84
+ else if (result.success) {
85
+ console.log("No errors.");
86
+ }
87
+ else {
88
+ printHumanDiagnostics(result);
89
+ }
90
+ if (!result.success)
91
+ process.exitCode = 1;
92
+ }
35
93
  function run(filePath) {
36
94
  const { outPath } = build(filePath);
37
95
  if (!outPath)
@@ -77,17 +135,28 @@ function watchCommand(filePath) {
77
135
  console.log(`[watch] ${filePath} — watching for changes. Press Ctrl+C to stop.`);
78
136
  rebuild();
79
137
  }
138
+ const COMMANDS = ["build", "run", "watch", "check"];
80
139
  function main() {
81
- const [, , command, file] = process.argv;
82
- if (!command || !file || (command !== "build" && command !== "run" && command !== "watch")) {
83
- console.error("Usage: ks <build|run|watch> <file.ks>");
140
+ const args = process.argv.slice(2);
141
+ const jsonMode = args.includes("--json");
142
+ const [command, file] = args.filter((a) => a !== "--json");
143
+ if (!command || !file || !COMMANDS.includes(command)) {
144
+ console.error("Usage: ks <build|run|watch|check> <file.ks> [--json]");
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+ if (jsonMode && command !== "build" && command !== "check") {
149
+ console.error("ks: --json is only supported with 'build' and 'check'");
84
150
  process.exitCode = 1;
85
151
  return;
86
152
  }
87
153
  const filePath = resolve(file);
88
- if (command === "build") {
89
- const { outPath } = build(filePath);
90
- if (outPath)
154
+ if (command === "check") {
155
+ checkCommand(filePath, jsonMode);
156
+ }
157
+ else if (command === "build") {
158
+ const { outPath } = build(filePath, jsonMode);
159
+ if (outPath && !jsonMode)
91
160
  console.log(`Wrote ${outPath}`);
92
161
  }
93
162
  else if (command === "watch") {
package/dist/codegen.js CHANGED
@@ -253,6 +253,8 @@ export class CodeGenerator {
253
253
  return JSON.stringify(expr.value);
254
254
  case "BoolLiteral":
255
255
  return String(expr.value);
256
+ case "NullLiteral":
257
+ return "null";
256
258
  case "InterpolatedStringLiteral":
257
259
  return this.genInterpolatedString(expr);
258
260
  case "ArrayLiteral":
@@ -309,13 +311,24 @@ export class CodeGenerator {
309
311
  // Array.Push is non-mutating in KopScript, unlike JS's own (mutating,
310
312
  // length-returning) Array.prototype.push — so it can't just be a member
311
313
  // rename like Map/Filter/ForEach; it needs an entirely different call
312
- // shape (a spread into a fresh array).
313
- if (expr.callee.kind === "MemberExpr" && expr.callee.property === "Push" && expr.args.length === 1) {
314
+ // shape (a spread into a fresh array). Gated on `isBuiltin` (set by the
315
+ // checker only when the receiver is actually an array) so a user class
316
+ // with its own same-named `Push(x)` method — a natural name for any
317
+ // Stack<T>/List<T>-style container, now that generics exist — still
318
+ // compiles to a normal method call instead of this rewrite.
319
+ if (expr.callee.kind === "MemberExpr" && expr.callee.isBuiltin && expr.callee.property === "Push" && expr.args.length === 1) {
314
320
  return `[...${this.genExpr(expr.callee.object)}, ${this.genExpr(expr.args[0])}]`;
315
321
  }
316
322
  return `${this.genExpr(expr.callee)}(${expr.args.map((a) => this.genExpr(a)).join(", ")})`;
317
323
  }
318
324
  genMember(expr) {
325
+ // PascalCase -> camelCase (and Length -> length) only applies to a
326
+ // genuine string/array built-in — see MemberExpr.isBuiltin's own
327
+ // comment for why this can't just be a name match. Anything else
328
+ // (including a user member that happens to share one of these names)
329
+ // passes through with its real, declared name untouched.
330
+ if (!expr.isBuiltin)
331
+ return `${this.genExpr(expr.object)}.${expr.property}`;
319
332
  const jsProperty = expr.property === "Length" ? "length" : MEMBER_METHOD_MAP[expr.property] ?? expr.property;
320
333
  return `${this.genExpr(expr.object)}.${jsProperty}`;
321
334
  }
package/dist/lexer.js CHANGED
@@ -34,6 +34,7 @@ const KEYWORDS = {
34
34
  void: TokenKind.Void,
35
35
  true: TokenKind.True,
36
36
  false: TokenKind.False,
37
+ null: TokenKind.Null,
37
38
  task: TokenKind.Task,
38
39
  state: TokenKind.State,
39
40
  async: TokenKind.Async,
@@ -214,6 +215,8 @@ export class Lexer {
214
215
  return this.make(TokenKind.Semicolon, c, line, col);
215
216
  case ".":
216
217
  return this.make(TokenKind.Dot, c, line, col);
218
+ case "?":
219
+ return this.make(TokenKind.Question, c, line, col);
217
220
  case "+":
218
221
  return this.make(TokenKind.Plus, c, line, col);
219
222
  case "-":
package/dist/modules.js CHANGED
@@ -29,7 +29,7 @@ export function loadModuleGraph(entryAbsPath) {
29
29
  const diagnostics = new DiagnosticBag();
30
30
  const tokens = new Lexer(source, diagnostics).tokenize();
31
31
  const program = new Parser(tokens, diagnostics).parseProgram();
32
- const record = { absPath, source, program, diagnostics, dependencies: [] };
32
+ const record = { absPath, source, program, diagnostics, dependencies: [], hoverEntries: [] };
33
33
  modules.set(absPath, record);
34
34
  stack.push(absPath);
35
35
  for (const u of program.usings) {
@@ -107,6 +107,7 @@ export function compileGraph(entryAbsPath) {
107
107
  }
108
108
  const checker = new Checker(mod.program, mod.diagnostics, merged, absPath);
109
109
  checker.check();
110
+ mod.hoverEntries = checker.hoverEntries;
110
111
  if (mod.diagnostics.hasErrors)
111
112
  hasErrors = true;
112
113
  exportsByModule.set(absPath, checker.getExports());
package/dist/parser.js CHANGED
@@ -153,7 +153,8 @@ export class Parser {
153
153
  this.advance();
154
154
  }
155
155
  const type = this.parseType();
156
- const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
156
+ const nameTok = this.consume(TokenKind.Identifier, "Expected name");
157
+ const name = nameTok.lexeme;
157
158
  if (this.check(TokenKind.LParen)) {
158
159
  const params = this.parseParamList();
159
160
  const body = this.parseBlock();
@@ -165,7 +166,7 @@ export class Parser {
165
166
  this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
166
167
  const init = this.parseExpression();
167
168
  this.consume(TokenKind.Semicolon, "Expected ';' after variable declaration");
168
- return { kind: "VarDecl", isConst: false, name, type, init, line: start.line, col: start.col };
169
+ return { kind: "VarDecl", isConst: false, name, nameLine: nameTok.line, nameCol: nameTok.col, type, init, line: start.line, col: start.col };
169
170
  }
170
171
  // Parses `Type name = init` without consuming a trailing terminator — used
171
172
  // both for top-level/block statements (which add the `;`) and for-loop
@@ -173,18 +174,18 @@ export class Parser {
173
174
  parseVarDeclHeader() {
174
175
  const start = this.peek();
175
176
  const type = this.parseType();
176
- const name = this.consume(TokenKind.Identifier, "Expected variable name").lexeme;
177
+ const nameTok = this.consume(TokenKind.Identifier, "Expected variable name");
177
178
  this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
178
179
  const init = this.parseExpression();
179
- return { kind: "VarDecl", isConst: false, name, type, init, line: start.line, col: start.col };
180
+ return { kind: "VarDecl", isConst: false, name: nameTok.lexeme, nameLine: nameTok.line, nameCol: nameTok.col, type, init, line: start.line, col: start.col };
180
181
  }
181
182
  parseConstDecl() {
182
183
  const start = this.advance(); // 'const'
183
184
  const type = this.parseType();
184
- const name = this.consume(TokenKind.Identifier, "Expected constant name").lexeme;
185
+ const nameTok = this.consume(TokenKind.Identifier, "Expected constant name");
185
186
  this.consume(TokenKind.Assign, "Expected '=' in constant declaration");
186
187
  const init = this.parseExpression();
187
- return { kind: "VarDecl", isConst: true, name, type, init, line: start.line, col: start.col };
188
+ return { kind: "VarDecl", isConst: true, name: nameTok.lexeme, nameLine: nameTok.line, nameCol: nameTok.col, type, init, line: start.line, col: start.col };
188
189
  }
189
190
  parseParamList() {
190
191
  this.consume(TokenKind.LParen, "Expected '('");
@@ -199,9 +200,20 @@ export class Parser {
199
200
  this.consume(TokenKind.RParen, "Expected ')' after parameters");
200
201
  return params;
201
202
  }
203
+ // `<T>` right after a class/interface name — a single, unconstrained type
204
+ // parameter (v1 has no `Map<K, V>`, no `T : IFoo` constraints). Null if
205
+ // absent, the overwhelmingly common case.
206
+ parseOptionalTypeParam() {
207
+ if (!this.match(TokenKind.Lt))
208
+ return null;
209
+ const name = this.consume(TokenKind.Identifier, "Expected a type parameter name").lexeme;
210
+ this.consume(TokenKind.Gt, "Expected '>' after type parameter");
211
+ return name;
212
+ }
202
213
  parseClassDecl(isExported) {
203
214
  const start = this.advance(); // 'class'
204
215
  const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
216
+ const typeParam = this.parseOptionalTypeParam();
205
217
  const baseList = [];
206
218
  if (this.match(TokenKind.Colon)) {
207
219
  do {
@@ -260,7 +272,8 @@ export class Parser {
260
272
  }
261
273
  const memberStart = this.peek();
262
274
  const type = this.parseType();
263
- const memberName = this.consume(TokenKind.Identifier, "Expected field, property, or method name").lexeme;
275
+ const memberNameTok = this.consume(TokenKind.Identifier, "Expected field, property, or method name");
276
+ const memberName = memberNameTok.lexeme;
264
277
  if (this.check(TokenKind.LParen)) {
265
278
  const params = this.parseParamList();
266
279
  const body = this.parseBlock();
@@ -275,6 +288,8 @@ export class Parser {
275
288
  isVirtual,
276
289
  isOverride,
277
290
  name: memberName,
291
+ nameLine: memberNameTok.line,
292
+ nameCol: memberNameTok.col,
278
293
  params,
279
294
  returnType: type,
280
295
  body,
@@ -307,6 +322,8 @@ export class Parser {
307
322
  visibility,
308
323
  hasSetter,
309
324
  name: memberName,
325
+ nameLine: memberNameTok.line,
326
+ nameCol: memberNameTok.col,
310
327
  type,
311
328
  line: memberStart.line,
312
329
  col: memberStart.col,
@@ -331,6 +348,8 @@ export class Parser {
331
348
  isStatic,
332
349
  initializer,
333
350
  name: memberName,
351
+ nameLine: memberNameTok.line,
352
+ nameCol: memberNameTok.col,
334
353
  type,
335
354
  line: memberStart.line,
336
355
  col: memberStart.col,
@@ -338,11 +357,12 @@ export class Parser {
338
357
  }
339
358
  }
340
359
  this.consume(TokenKind.RBrace, "Expected '}' after class body");
341
- return { kind: "ClassDecl", isExported, name, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
360
+ return { kind: "ClassDecl", isExported, name, typeParam, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
342
361
  }
343
362
  parseInterfaceDecl(isExported) {
344
363
  const start = this.advance(); // 'interface'
345
364
  const name = this.consume(TokenKind.Identifier, "Expected interface name").lexeme;
365
+ const typeParam = this.parseOptionalTypeParam();
346
366
  const baseList = [];
347
367
  if (this.match(TokenKind.Colon)) {
348
368
  do {
@@ -354,13 +374,21 @@ export class Parser {
354
374
  while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
355
375
  const memberStart = this.peek();
356
376
  const returnType = this.parseType();
357
- const methodName = this.consume(TokenKind.Identifier, "Expected method name").lexeme;
377
+ const methodNameTok = this.consume(TokenKind.Identifier, "Expected method name");
358
378
  const params = this.parseParamList();
359
379
  this.consume(TokenKind.Semicolon, "Expected ';' after interface method signature");
360
- methods.push({ name: methodName, params, returnType, line: memberStart.line, col: memberStart.col });
380
+ methods.push({
381
+ name: methodNameTok.lexeme,
382
+ nameLine: methodNameTok.line,
383
+ nameCol: methodNameTok.col,
384
+ params,
385
+ returnType,
386
+ line: memberStart.line,
387
+ col: memberStart.col,
388
+ });
361
389
  }
362
390
  this.consume(TokenKind.RBrace, "Expected '}' after interface body");
363
- return { kind: "InterfaceDecl", isExported, name, baseList, methods, line: start.line, col: start.col };
391
+ return { kind: "InterfaceDecl", isExported, name, typeParam, baseList, methods, line: start.line, col: start.col };
364
392
  }
365
393
  parseEnumDecl(isExported) {
366
394
  const start = this.advance(); // 'enum'
@@ -604,8 +632,8 @@ export class Parser {
604
632
  type = this.parseFunctionType();
605
633
  }
606
634
  else if (this.check(TokenKind.Task)) {
607
- this.advance();
608
- let resultType = { kind: "NamedType", name: "void" };
635
+ const taskTok = this.advance();
636
+ let resultType = { kind: "NamedType", name: "void", typeArgs: null, line: taskTok.line, col: taskTok.col };
609
637
  if (this.match(TokenKind.Lt)) {
610
638
  resultType = this.parseType();
611
639
  this.consume(TokenKind.Gt, "Expected '>' after task result type");
@@ -621,12 +649,31 @@ export class Parser {
621
649
  }
622
650
  else {
623
651
  const nameToken = this.check(TokenKind.Void) ? this.advance() : this.consume(TokenKind.Identifier, "Expected type name");
624
- type = { kind: "NamedType", name: nameToken.lexeme };
652
+ // `Box<number>` a generic type reference. At most one type argument
653
+ // in v1 (matching ClassDecl/InterfaceDecl's single type parameter);
654
+ // whether `name` actually refers to a declared generic type is a
655
+ // semantic question the checker answers, not the parser.
656
+ let typeArgs = null;
657
+ if (this.check(TokenKind.Lt)) {
658
+ this.advance();
659
+ typeArgs = [this.parseType()];
660
+ this.consume(TokenKind.Gt, "Expected '>' after type argument");
661
+ }
662
+ type = { kind: "NamedType", name: nameToken.lexeme, typeArgs, line: nameToken.line, col: nameToken.col };
625
663
  }
626
- while (this.check(TokenKind.LBracket)) {
627
- this.advance();
628
- this.consume(TokenKind.RBracket, "Expected ']' after '[' in array type");
629
- type = { kind: "ArrayType", element: type };
664
+ // `[]` and `?` are both postfix and can alternate in either order —
665
+ // `string?[]` (array of nullable strings) and `string[]?` (nullable
666
+ // array of strings) parse with the nesting their order implies.
667
+ while (this.check(TokenKind.LBracket) || this.check(TokenKind.Question)) {
668
+ if (this.check(TokenKind.LBracket)) {
669
+ this.advance();
670
+ this.consume(TokenKind.RBracket, "Expected ']' after '[' in array type");
671
+ type = { kind: "ArrayType", element: type };
672
+ }
673
+ else {
674
+ const q = this.advance();
675
+ type = { kind: "NullableType", inner: type, line: q.line, col: q.col };
676
+ }
630
677
  }
631
678
  return type;
632
679
  }
@@ -779,6 +826,10 @@ export class Parser {
779
826
  this.advance();
780
827
  return { kind: "BoolLiteral", value: t.kind === TokenKind.True, line: t.line, col: t.col };
781
828
  }
829
+ if (this.check(TokenKind.Null)) {
830
+ this.advance();
831
+ return { kind: "NullLiteral", line: t.line, col: t.col };
832
+ }
782
833
  if (this.check(TokenKind.This)) {
783
834
  this.advance();
784
835
  return { kind: "ThisExpr", line: t.line, col: t.col };
@@ -786,6 +837,15 @@ export class Parser {
786
837
  if (this.check(TokenKind.New)) {
787
838
  this.advance();
788
839
  const className = this.consume(TokenKind.Identifier, "Expected class name after 'new'").lexeme;
840
+ // `new Box<number>(...)` — unambiguous here: `new <Identifier>` is
841
+ // always followed by `(`, generic type args or not, so seeing `<`
842
+ // instead can only mean a type argument list, never a comparison.
843
+ let typeArgs = null;
844
+ if (this.check(TokenKind.Lt)) {
845
+ this.advance();
846
+ typeArgs = [this.parseType()];
847
+ this.consume(TokenKind.Gt, "Expected '>' after type argument");
848
+ }
789
849
  this.consume(TokenKind.LParen, "Expected '(' after class name");
790
850
  const args = [];
791
851
  if (!this.check(TokenKind.RParen)) {
@@ -794,7 +854,7 @@ export class Parser {
794
854
  } while (this.match(TokenKind.Comma));
795
855
  }
796
856
  this.consume(TokenKind.RParen, "Expected ')' after constructor arguments");
797
- return { kind: "NewExpr", className, args, line: t.line, col: t.col };
857
+ return { kind: "NewExpr", className, typeArgs, args, line: t.line, col: t.col };
798
858
  }
799
859
  if (this.check(TokenKind.LBracket)) {
800
860
  this.advance();
@@ -832,26 +892,39 @@ export class Parser {
832
892
  this.diagnostics.error(`Unexpected token '${t.lexeme || t.kind}'`, t.line, t.col);
833
893
  throw new ParseError();
834
894
  }
835
- // Lightweight lookahead (not a trial parse) distinguishing a lambda's
836
- // parameter list from a parenthesized expression. A lambda parameter is
837
- // always `Type name` (types are required, no inference in v1), so seeing
838
- // two consecutive identifiers — or an empty `()` followed by `=>` — is
839
- // unambiguous. This doesn't handle a lambda whose own first parameter is
840
- // itself a function type (e.g. a higher-order lambda); that's a known v1
841
- // gap in favor of keeping this check cheap.
895
+ // Distinguishes a lambda's parameter list from a parenthesized expression.
896
+ // A lambda parameter is always `Type name` (types are required, no
897
+ // inference in v1), so seeing a full type followed by an identifier — or
898
+ // an empty `()` followed by `=>` — is unambiguous. Uses a real speculative
899
+ // parse of the candidate type (same save-position/try/rollback shape as
900
+ // isDeclStart) rather than hand-rolled token lookahead, so it stays
901
+ // correct as parseType grows new postfix/prefix forms (`?`, `<T>`, ...)
902
+ // without needing a matching update here every time. This doesn't handle
903
+ // a lambda whose own first parameter is itself a function type (e.g. a
904
+ // higher-order lambda); that's a known v1 gap in favor of keeping this
905
+ // check simple.
842
906
  looksLikeLambda() {
843
- let i = this.pos + 1; // just past '('
907
+ const i = this.pos + 1; // just past '('
844
908
  if (this.tokens[i]?.kind === TokenKind.RParen) {
845
909
  return this.tokens[i + 1]?.kind === TokenKind.Arrow;
846
910
  }
847
911
  const startKind = this.tokens[i]?.kind;
848
912
  if (startKind !== TokenKind.Identifier && startKind !== TokenKind.Void)
849
913
  return false;
850
- i++;
851
- while (this.tokens[i]?.kind === TokenKind.LBracket && this.tokens[i + 1]?.kind === TokenKind.RBracket) {
852
- i += 2;
914
+ const savedPos = this.pos;
915
+ const savedDiagnosticsLength = this.diagnostics.diagnostics.length;
916
+ this.pos = i;
917
+ let result;
918
+ try {
919
+ this.parseType();
920
+ result = this.check(TokenKind.Identifier);
853
921
  }
854
- return this.tokens[i]?.kind === TokenKind.Identifier;
922
+ catch {
923
+ result = false;
924
+ }
925
+ this.pos = savedPos;
926
+ this.diagnostics.diagnostics.length = savedDiagnosticsLength;
927
+ return result;
855
928
  }
856
929
  parseLambda() {
857
930
  const start = this.peek(); // '('
package/dist/tokens.js CHANGED
@@ -8,6 +8,7 @@ export var TokenKind;
8
8
  TokenKind["Identifier"] = "Identifier";
9
9
  TokenKind["True"] = "True";
10
10
  TokenKind["False"] = "False";
11
+ TokenKind["Null"] = "Null";
11
12
  // Keywords
12
13
  TokenKind["Using"] = "Using";
13
14
  TokenKind["Extern"] = "Extern";
@@ -62,6 +63,7 @@ export var TokenKind;
62
63
  TokenKind["Dot"] = "Dot";
63
64
  TokenKind["Arrow"] = "Arrow";
64
65
  TokenKind["Underscore"] = "Underscore";
66
+ TokenKind["Question"] = "Question";
65
67
  // Operators
66
68
  TokenKind["Plus"] = "Plus";
67
69
  TokenKind["Minus"] = "Minus";
package/dist/types.js CHANGED
@@ -6,11 +6,14 @@ export const UNKNOWN = { kind: "unknown" };
6
6
  export function arrayOf(element) {
7
7
  return { kind: "array", element };
8
8
  }
9
- export function classType(name) {
10
- return { kind: "class", name };
9
+ export function classType(name, typeArg) {
10
+ return typeArg ? { kind: "class", name, typeArg } : { kind: "class", name };
11
11
  }
12
- export function interfaceType(name) {
13
- return { kind: "interface", name };
12
+ export function interfaceType(name, typeArg) {
13
+ return typeArg ? { kind: "interface", name, typeArg } : { kind: "interface", name };
14
+ }
15
+ export function typeParamType(name) {
16
+ return { kind: "typeParam", name };
14
17
  }
15
18
  export function enumType(name) {
16
19
  return { kind: "enum", name };
@@ -24,6 +27,9 @@ export function taskType(resultType) {
24
27
  export function stateType(valueType) {
25
28
  return { kind: "state", valueType };
26
29
  }
30
+ export function nullableOf(inner) {
31
+ return inner.kind === "nullable" ? inner : { kind: "nullable", inner };
32
+ }
27
33
  export function typeToString(t) {
28
34
  switch (t.kind) {
29
35
  case "number":
@@ -34,56 +40,88 @@ export function typeToString(t) {
34
40
  return t.kind;
35
41
  case "array":
36
42
  return `${typeToString(t.element)}[]`;
37
- case "class":
38
- case "interface":
39
43
  case "enum":
40
44
  return t.name;
45
+ case "class":
46
+ case "interface":
47
+ return t.typeArg ? `${t.name}<${typeToString(t.typeArg)}>` : t.name;
41
48
  case "function":
42
49
  return `(${t.params.map(typeToString).join(", ")}) => ${typeToString(t.returnType)}`;
43
50
  case "task":
44
51
  return t.resultType.kind === "void" ? "task" : `task<${typeToString(t.resultType)}>`;
45
52
  case "state":
46
53
  return `state<${typeToString(t.valueType)}>`;
54
+ case "nullable":
55
+ return `${typeToString(t.inner)}?`;
56
+ case "typeParam":
57
+ return t.name;
47
58
  }
48
59
  }
49
60
  const PRIMITIVE_NAMES = new Set(["number", "string", "bool", "void"]);
50
61
  // Resolves a syntactic type annotation to a Type, given the kind of every known
51
- // user-declared name (class, interface, or enum).
52
- export function resolveTypeNode(node, namedTypes) {
62
+ // user-declared name (class, interface, or enum). `onNamedType`, when given, is
63
+ // called with every `NamedType` leaf visited and the Type it resolved to — a
64
+ // hook for tools (e.g. hover) that want to know what a type annotation in
65
+ // source actually refers to, without duplicating this resolution logic.
66
+ export function resolveTypeNode(node, namedTypes, onNamedType) {
53
67
  if (node.kind === "ArrayType") {
54
- const element = resolveTypeNode(node.element, namedTypes);
68
+ const element = resolveTypeNode(node.element, namedTypes, onNamedType);
55
69
  return element ? arrayOf(element) : null;
56
70
  }
57
71
  if (node.kind === "FunctionType") {
58
72
  const params = [];
59
73
  for (const p of node.params) {
60
- const resolved = resolveTypeNode(p, namedTypes);
74
+ const resolved = resolveTypeNode(p, namedTypes, onNamedType);
61
75
  if (!resolved)
62
76
  return null;
63
77
  params.push(resolved);
64
78
  }
65
- const returnType = resolveTypeNode(node.returnType, namedTypes);
79
+ const returnType = resolveTypeNode(node.returnType, namedTypes, onNamedType);
66
80
  return returnType ? functionType(params, returnType) : null;
67
81
  }
68
82
  if (node.kind === "TaskType") {
69
- const resultType = resolveTypeNode(node.resultType, namedTypes);
83
+ const resultType = resolveTypeNode(node.resultType, namedTypes, onNamedType);
70
84
  return resultType ? taskType(resultType) : null;
71
85
  }
72
86
  if (node.kind === "StateType") {
73
- const valueType = resolveTypeNode(node.valueType, namedTypes);
87
+ const valueType = resolveTypeNode(node.valueType, namedTypes, onNamedType);
74
88
  return valueType ? stateType(valueType) : null;
75
89
  }
90
+ if (node.kind === "NullableType") {
91
+ const inner = resolveTypeNode(node.inner, namedTypes, onNamedType);
92
+ return inner ? nullableOf(inner) : null;
93
+ }
94
+ // At most one type argument in v1 — resolve it once, up front; a
95
+ // primitive/enum/type-param name below never accepts one (arity error,
96
+ // caught as a plain resolution failure here — a class/interface's own
97
+ // required-vs-supplied arity mismatch is a separate check the caller
98
+ // makes, since only it knows which names are actually declared generic).
99
+ let typeArg = null;
100
+ if (node.typeArgs) {
101
+ typeArg = resolveTypeNode(node.typeArgs[0], namedTypes, onNamedType);
102
+ if (!typeArg)
103
+ return null;
104
+ }
105
+ let resolved;
76
106
  if (PRIMITIVE_NAMES.has(node.name)) {
77
- return { kind: node.name };
107
+ resolved = typeArg ? null : { kind: node.name };
108
+ }
109
+ else {
110
+ const kind = namedTypes.get(node.name);
111
+ if (kind === "class")
112
+ resolved = classType(node.name, typeArg ?? undefined);
113
+ else if (kind === "interface")
114
+ resolved = interfaceType(node.name, typeArg ?? undefined);
115
+ else if (kind === "enum")
116
+ resolved = typeArg ? null : enumType(node.name);
117
+ else if (kind === "typeParam")
118
+ resolved = typeArg ? null : typeParamType(node.name);
119
+ else
120
+ resolved = null;
78
121
  }
79
- const kind = namedTypes.get(node.name);
80
- if (kind === "class")
81
- return classType(node.name);
82
- if (kind === "interface")
83
- return interfaceType(node.name);
84
- if (kind === "enum")
85
- return enumType(node.name);
86
- return null;
122
+ if (resolved)
123
+ onNamedType?.(node, resolved);
124
+ return resolved;
87
125
  }
88
126
  export function typesEqual(a, b) {
89
127
  if (a.kind === "unknown" || b.kind === "unknown")
@@ -101,8 +139,28 @@ export function typesEqual(a, b) {
101
139
  if (a.kind === "state" && b.kind === "state") {
102
140
  return typesEqual(a.valueType, b.valueType);
103
141
  }
104
- if ((a.kind === "class" || a.kind === "interface" || a.kind === "enum") && "name" in b) {
142
+ if (a.kind === "nullable" && b.kind === "nullable") {
143
+ return typesEqual(a.inner, b.inner);
144
+ }
145
+ if (a.kind === "typeParam" && b.kind === "typeParam") {
105
146
  return a.name === b.name;
106
147
  }
148
+ // Invariant generics: Box<number> and Box<string> are unrelated types,
149
+ // and so are Box<number> and bare (non-generic-reference) Box — the
150
+ // latter only arises from an arity error already diagnosed elsewhere, so
151
+ // treating it as unequal here (rather than papering over it) is right.
152
+ if ((a.kind === "class" || a.kind === "interface" || a.kind === "enum") && "name" in b) {
153
+ const other = b;
154
+ if (a.name !== other.name)
155
+ return false;
156
+ if ("typeArg" in a || "typeArg" in other) {
157
+ const aArg = a.typeArg;
158
+ const bArg = other.typeArg;
159
+ if (!aArg || !bArg)
160
+ return false;
161
+ return typesEqual(aArg, bArg);
162
+ }
163
+ return true;
164
+ }
107
165
  return true;
108
166
  }