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/LLM.md +457 -0
- package/README.md +132 -22
- package/dist/checker.js +389 -48
- package/dist/cli.js +85 -16
- package/dist/codegen.js +15 -2
- package/dist/lexer.js +3 -0
- package/dist/modules.js +2 -1
- package/dist/parser.js +104 -31
- package/dist/tokens.js +2 -0
- package/dist/types.js +81 -23
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -7,31 +7,89 @@ function outputPathFor(filePath) {
|
|
|
7
7
|
const name = basename(filePath, ".ks");
|
|
8
8
|
return join(resolve(filePath, ".."), `${name}.js`);
|
|
9
9
|
}
|
|
10
|
+
function collectDiagnostics(result) {
|
|
11
|
+
const all = [];
|
|
12
|
+
for (const absPath of result.order) {
|
|
13
|
+
const mod = result.modules.get(absPath);
|
|
14
|
+
for (const d of mod.diagnostics.diagnostics)
|
|
15
|
+
all.push({ ...d, file: absPath });
|
|
16
|
+
}
|
|
17
|
+
return all;
|
|
18
|
+
}
|
|
19
|
+
// One JSON object per invocation, always to stdout (never split across
|
|
20
|
+
// lines, never mixed with human-readable output) — so a caller can pipe
|
|
21
|
+
// this straight into `JSON.parse` without worrying about interleaving.
|
|
22
|
+
function printJson(result, written) {
|
|
23
|
+
const diagnostics = collectDiagnostics(result);
|
|
24
|
+
console.log(JSON.stringify({ success: result.success, diagnostics, written }, null, 2));
|
|
25
|
+
}
|
|
26
|
+
function printJsonEntryMissing(filePath) {
|
|
27
|
+
console.log(JSON.stringify({ success: false, diagnostics: [], written: [], error: `cannot find file '${filePath}'` }, null, 2));
|
|
28
|
+
}
|
|
29
|
+
function printHumanDiagnostics(result) {
|
|
30
|
+
for (const absPath of result.order) {
|
|
31
|
+
const mod = result.modules.get(absPath);
|
|
32
|
+
if (mod.diagnostics.hasErrors) {
|
|
33
|
+
console.error(mod.diagnostics.format(mod.source, absPath));
|
|
34
|
+
console.error("");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
10
38
|
// Compiles the whole module graph reachable from `filePath`, writing one
|
|
11
|
-
// .js file next to each .ks source.
|
|
12
|
-
|
|
39
|
+
// .js file next to each .ks source. With `jsonMode`, nothing but a single
|
|
40
|
+
// JSON object goes to stdout (no "Wrote ..." line, no human diagnostics) —
|
|
41
|
+
// see `printJson` for the shape.
|
|
42
|
+
function build(filePath, jsonMode = false) {
|
|
13
43
|
const result = compileGraph(filePath);
|
|
14
44
|
if (result.entryMissing) {
|
|
15
|
-
|
|
45
|
+
if (jsonMode)
|
|
46
|
+
printJsonEntryMissing(filePath);
|
|
47
|
+
else
|
|
48
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
16
49
|
process.exitCode = 1;
|
|
17
50
|
return { outPath: null, watchFiles: [] };
|
|
18
51
|
}
|
|
19
52
|
if (!result.success) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.error("");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
53
|
+
if (jsonMode)
|
|
54
|
+
printJson(result, []);
|
|
55
|
+
else
|
|
56
|
+
printHumanDiagnostics(result);
|
|
27
57
|
process.exitCode = 1;
|
|
28
58
|
return { outPath: null, watchFiles: result.order };
|
|
29
59
|
}
|
|
30
60
|
for (const absPath of result.order) {
|
|
31
61
|
writeFileSync(outputPathFor(absPath), result.outputs.get(absPath), "utf-8");
|
|
32
62
|
}
|
|
63
|
+
if (jsonMode)
|
|
64
|
+
printJson(result, result.order.map(outputPathFor));
|
|
33
65
|
return { outPath: outputPathFor(filePath), watchFiles: result.order };
|
|
34
66
|
}
|
|
67
|
+
// Type-checks the graph without writing any output — for CI or an editor/
|
|
68
|
+
// agent that wants pass/fail plus diagnostics without touching the
|
|
69
|
+
// filesystem. Same exit-code convention as `build` (0 clean, 1 on any
|
|
70
|
+
// error or a missing entry file).
|
|
71
|
+
function checkCommand(filePath, jsonMode) {
|
|
72
|
+
const result = compileGraph(filePath);
|
|
73
|
+
if (result.entryMissing) {
|
|
74
|
+
if (jsonMode)
|
|
75
|
+
printJsonEntryMissing(filePath);
|
|
76
|
+
else
|
|
77
|
+
console.error(`ks: cannot find file '${filePath}'`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (jsonMode) {
|
|
82
|
+
printJson(result, []);
|
|
83
|
+
}
|
|
84
|
+
else if (result.success) {
|
|
85
|
+
console.log("No errors.");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
printHumanDiagnostics(result);
|
|
89
|
+
}
|
|
90
|
+
if (!result.success)
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
35
93
|
function run(filePath) {
|
|
36
94
|
const { outPath } = build(filePath);
|
|
37
95
|
if (!outPath)
|
|
@@ -77,17 +135,28 @@ function watchCommand(filePath) {
|
|
|
77
135
|
console.log(`[watch] ${filePath} — watching for changes. Press Ctrl+C to stop.`);
|
|
78
136
|
rebuild();
|
|
79
137
|
}
|
|
138
|
+
const COMMANDS = ["build", "run", "watch", "check"];
|
|
80
139
|
function main() {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
140
|
+
const args = process.argv.slice(2);
|
|
141
|
+
const jsonMode = args.includes("--json");
|
|
142
|
+
const [command, file] = args.filter((a) => a !== "--json");
|
|
143
|
+
if (!command || !file || !COMMANDS.includes(command)) {
|
|
144
|
+
console.error("Usage: ks <build|run|watch|check> <file.ks> [--json]");
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (jsonMode && command !== "build" && command !== "check") {
|
|
149
|
+
console.error("ks: --json is only supported with 'build' and 'check'");
|
|
84
150
|
process.exitCode = 1;
|
|
85
151
|
return;
|
|
86
152
|
}
|
|
87
153
|
const filePath = resolve(file);
|
|
88
|
-
if (command === "
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
if (command === "check") {
|
|
155
|
+
checkCommand(filePath, jsonMode);
|
|
156
|
+
}
|
|
157
|
+
else if (command === "build") {
|
|
158
|
+
const { outPath } = build(filePath, jsonMode);
|
|
159
|
+
if (outPath && !jsonMode)
|
|
91
160
|
console.log(`Wrote ${outPath}`);
|
|
92
161
|
}
|
|
93
162
|
else if (command === "watch") {
|
package/dist/codegen.js
CHANGED
|
@@ -253,6 +253,8 @@ export class CodeGenerator {
|
|
|
253
253
|
return JSON.stringify(expr.value);
|
|
254
254
|
case "BoolLiteral":
|
|
255
255
|
return String(expr.value);
|
|
256
|
+
case "NullLiteral":
|
|
257
|
+
return "null";
|
|
256
258
|
case "InterpolatedStringLiteral":
|
|
257
259
|
return this.genInterpolatedString(expr);
|
|
258
260
|
case "ArrayLiteral":
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
//
|
|
836
|
-
// parameter
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 (
|
|
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
|
}
|