kopscript 0.1.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/LICENSE +21 -0
- package/README.md +588 -0
- package/bin/ks.js +17 -0
- package/dist/ast.js +2 -0
- package/dist/checker.js +1334 -0
- package/dist/cli.js +100 -0
- package/dist/codegen.js +339 -0
- package/dist/diagnostics.js +26 -0
- package/dist/lexer.js +301 -0
- package/dist/modules.js +133 -0
- package/dist/parser.js +995 -0
- package/dist/tokens.js +81 -0
- package/dist/types.js +108 -0
- package/package.json +43 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
import { TokenKind } from "./tokens.js";
|
|
2
|
+
import { Lexer } from "./lexer.js";
|
|
3
|
+
import { DiagnosticBag } from "./diagnostics.js";
|
|
4
|
+
export class Parser {
|
|
5
|
+
constructor(tokens, diagnostics) {
|
|
6
|
+
this.tokens = tokens;
|
|
7
|
+
this.diagnostics = diagnostics;
|
|
8
|
+
this.pos = 0;
|
|
9
|
+
}
|
|
10
|
+
parseProgram() {
|
|
11
|
+
const usings = [];
|
|
12
|
+
while (this.check(TokenKind.Using)) {
|
|
13
|
+
usings.push(this.parseUsing());
|
|
14
|
+
}
|
|
15
|
+
const statements = [];
|
|
16
|
+
while (!this.check(TokenKind.EOF)) {
|
|
17
|
+
if (this.check(TokenKind.Using)) {
|
|
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);
|
|
20
|
+
this.parseUsing(); // consume and discard so parsing can continue
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const stmt = this.parseStatement();
|
|
24
|
+
if (stmt)
|
|
25
|
+
statements.push(stmt);
|
|
26
|
+
}
|
|
27
|
+
return { kind: "Program", usings, statements };
|
|
28
|
+
}
|
|
29
|
+
parseUsing() {
|
|
30
|
+
const start = this.advance(); // 'using'
|
|
31
|
+
const pathTok = this.consume(TokenKind.String, "Expected a module path string after 'using'");
|
|
32
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after using directive");
|
|
33
|
+
return { kind: "UsingDecl", path: pathTok.lexeme, line: start.line, col: start.col };
|
|
34
|
+
}
|
|
35
|
+
// ---------- statements ----------
|
|
36
|
+
parseStatement() {
|
|
37
|
+
try {
|
|
38
|
+
if (this.check(TokenKind.Public) || this.check(TokenKind.Private))
|
|
39
|
+
return this.parseExportableDecl();
|
|
40
|
+
if (this.check(TokenKind.Const)) {
|
|
41
|
+
const decl = this.parseConstDecl();
|
|
42
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after constant declaration");
|
|
43
|
+
return decl;
|
|
44
|
+
}
|
|
45
|
+
if (this.check(TokenKind.Class))
|
|
46
|
+
return this.parseClassDecl(true);
|
|
47
|
+
if (this.check(TokenKind.Interface))
|
|
48
|
+
return this.parseInterfaceDecl(true);
|
|
49
|
+
if (this.check(TokenKind.Enum))
|
|
50
|
+
return this.parseEnumDecl(true);
|
|
51
|
+
if (this.check(TokenKind.Extern))
|
|
52
|
+
return this.parseExternDecl(true);
|
|
53
|
+
if (this.check(TokenKind.LBrace))
|
|
54
|
+
return this.parseBlock();
|
|
55
|
+
if (this.check(TokenKind.If))
|
|
56
|
+
return this.parseIf();
|
|
57
|
+
if (this.check(TokenKind.While))
|
|
58
|
+
return this.parseWhile();
|
|
59
|
+
if (this.check(TokenKind.For))
|
|
60
|
+
return this.parseFor();
|
|
61
|
+
if (this.check(TokenKind.Foreach))
|
|
62
|
+
return this.parseForeach();
|
|
63
|
+
if (this.check(TokenKind.Return))
|
|
64
|
+
return this.parseReturn();
|
|
65
|
+
if (this.check(TokenKind.Try))
|
|
66
|
+
return this.parseTry();
|
|
67
|
+
if (this.check(TokenKind.Throw))
|
|
68
|
+
return this.parseThrow();
|
|
69
|
+
if (this.check(TokenKind.Break)) {
|
|
70
|
+
const t = this.advance();
|
|
71
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'break'");
|
|
72
|
+
return { kind: "BreakStatement", line: t.line, col: t.col };
|
|
73
|
+
}
|
|
74
|
+
if (this.check(TokenKind.Continue)) {
|
|
75
|
+
const t = this.advance();
|
|
76
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'continue'");
|
|
77
|
+
return { kind: "ContinueStatement", line: t.line, col: t.col };
|
|
78
|
+
}
|
|
79
|
+
if (this.isDeclStart())
|
|
80
|
+
return this.parseDeclaration(true);
|
|
81
|
+
const expr = this.parseExpression();
|
|
82
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after expression");
|
|
83
|
+
return { kind: "ExpressionStatement", expression: expr, line: expr.line, col: expr.col };
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
this.synchronize();
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Parses a leading `public`/`private` modifier and applies it to whatever
|
|
91
|
+
// exportable declaration follows (class, interface, enum, or free
|
|
92
|
+
// function). Variable declarations aren't exportable — `using` only pulls
|
|
93
|
+
// in types and functions — so `public`/`private` on one is a parse error.
|
|
94
|
+
parseExportableDecl() {
|
|
95
|
+
const modifierTok = this.advance(); // 'public' or 'private'
|
|
96
|
+
const isExported = modifierTok.kind === TokenKind.Public;
|
|
97
|
+
if (this.check(TokenKind.Class))
|
|
98
|
+
return this.parseClassDecl(isExported);
|
|
99
|
+
if (this.check(TokenKind.Interface))
|
|
100
|
+
return this.parseInterfaceDecl(isExported);
|
|
101
|
+
if (this.check(TokenKind.Enum))
|
|
102
|
+
return this.parseEnumDecl(isExported);
|
|
103
|
+
if (this.check(TokenKind.Extern))
|
|
104
|
+
return this.parseExternDecl(isExported);
|
|
105
|
+
if (this.isDeclStart()) {
|
|
106
|
+
const decl = this.parseDeclaration(isExported);
|
|
107
|
+
if (decl.kind === "VarDecl") {
|
|
108
|
+
this.diagnostics.error("'public'/'private' cannot modify a variable declaration", modifierTok.line, modifierTok.col);
|
|
109
|
+
}
|
|
110
|
+
return decl;
|
|
111
|
+
}
|
|
112
|
+
this.diagnostics.error("Expected a class, interface, enum, or function declaration after 'public'/'private'", modifierTok.line, modifierTok.col);
|
|
113
|
+
throw new ParseError();
|
|
114
|
+
}
|
|
115
|
+
// Lookahead for the C#-style `Type name` declaration shape (locals and
|
|
116
|
+
// free functions alike): a type, then an identifier. Two bare identifiers
|
|
117
|
+
// in a row never occurs anywhere else in this grammar, so the heuristic is
|
|
118
|
+
// unambiguous — but now that types can start with `(` (function types),
|
|
119
|
+
// a simple fixed-width token peek can't reliably tell a function type
|
|
120
|
+
// apart from, say, a parenthesized call. So this does a real trial parse
|
|
121
|
+
// of a type via parseType() and rolls back position and any diagnostics
|
|
122
|
+
// it emitted along the way, regardless of outcome.
|
|
123
|
+
isDeclStart() {
|
|
124
|
+
const savedPos = this.pos;
|
|
125
|
+
const savedDiagnosticsLength = this.diagnostics.diagnostics.length;
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
if (this.check(TokenKind.Async))
|
|
129
|
+
this.advance(); // optional 'async' prefix on a free function
|
|
130
|
+
this.parseType();
|
|
131
|
+
result = this.check(TokenKind.Identifier);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
result = false;
|
|
135
|
+
}
|
|
136
|
+
this.pos = savedPos;
|
|
137
|
+
this.diagnostics.diagnostics.length = savedDiagnosticsLength;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
// Parses `Type name = init;` or `[async] Type name(params) { ... }` — a
|
|
141
|
+
// variable or a free function, disambiguated by what follows the name.
|
|
142
|
+
// `isExported` only applies to the function case (see parseExportableDecl);
|
|
143
|
+
// `async` only applies there too (checked and rejected on a var below).
|
|
144
|
+
parseDeclaration(isExported) {
|
|
145
|
+
const start = this.peek();
|
|
146
|
+
let isAsync = false;
|
|
147
|
+
if (this.check(TokenKind.Async)) {
|
|
148
|
+
isAsync = true;
|
|
149
|
+
this.advance();
|
|
150
|
+
}
|
|
151
|
+
const type = this.parseType();
|
|
152
|
+
const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
|
|
153
|
+
if (this.check(TokenKind.LParen)) {
|
|
154
|
+
const params = this.parseParamList();
|
|
155
|
+
const body = this.parseBlock();
|
|
156
|
+
return { kind: "FunctionDecl", isExported, isAsync, name, params, returnType: type, body, line: start.line, col: start.col };
|
|
157
|
+
}
|
|
158
|
+
if (isAsync) {
|
|
159
|
+
this.diagnostics.error("'async' cannot modify a variable declaration", start.line, start.col);
|
|
160
|
+
}
|
|
161
|
+
this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
|
|
162
|
+
const init = this.parseExpression();
|
|
163
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after variable declaration");
|
|
164
|
+
return { kind: "VarDecl", isConst: false, name, type, init, line: start.line, col: start.col };
|
|
165
|
+
}
|
|
166
|
+
// Parses `Type name = init` without consuming a trailing terminator — used
|
|
167
|
+
// both for top-level/block statements (which add the `;`) and for-loop
|
|
168
|
+
// initializers (which don't).
|
|
169
|
+
parseVarDeclHeader() {
|
|
170
|
+
const start = this.peek();
|
|
171
|
+
const type = this.parseType();
|
|
172
|
+
const name = this.consume(TokenKind.Identifier, "Expected variable name").lexeme;
|
|
173
|
+
this.consume(TokenKind.Assign, "Expected '=' in variable declaration");
|
|
174
|
+
const init = this.parseExpression();
|
|
175
|
+
return { kind: "VarDecl", isConst: false, name, type, init, line: start.line, col: start.col };
|
|
176
|
+
}
|
|
177
|
+
parseConstDecl() {
|
|
178
|
+
const start = this.advance(); // 'const'
|
|
179
|
+
const type = this.parseType();
|
|
180
|
+
const name = this.consume(TokenKind.Identifier, "Expected constant name").lexeme;
|
|
181
|
+
this.consume(TokenKind.Assign, "Expected '=' in constant declaration");
|
|
182
|
+
const init = this.parseExpression();
|
|
183
|
+
return { kind: "VarDecl", isConst: true, name, type, init, line: start.line, col: start.col };
|
|
184
|
+
}
|
|
185
|
+
parseParamList() {
|
|
186
|
+
this.consume(TokenKind.LParen, "Expected '('");
|
|
187
|
+
const params = [];
|
|
188
|
+
if (!this.check(TokenKind.RParen)) {
|
|
189
|
+
do {
|
|
190
|
+
const type = this.parseType();
|
|
191
|
+
const name = this.consume(TokenKind.Identifier, "Expected parameter name").lexeme;
|
|
192
|
+
params.push({ name, type });
|
|
193
|
+
} while (this.match(TokenKind.Comma));
|
|
194
|
+
}
|
|
195
|
+
this.consume(TokenKind.RParen, "Expected ')' after parameters");
|
|
196
|
+
return params;
|
|
197
|
+
}
|
|
198
|
+
parseClassDecl(isExported) {
|
|
199
|
+
const start = this.advance(); // 'class'
|
|
200
|
+
const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
|
|
201
|
+
const baseList = [];
|
|
202
|
+
if (this.match(TokenKind.Colon)) {
|
|
203
|
+
do {
|
|
204
|
+
baseList.push(this.consume(TokenKind.Identifier, "Expected base class or interface name").lexeme);
|
|
205
|
+
} while (this.match(TokenKind.Comma));
|
|
206
|
+
}
|
|
207
|
+
this.consume(TokenKind.LBrace, "Expected '{' before class body");
|
|
208
|
+
const fields = [];
|
|
209
|
+
const properties = [];
|
|
210
|
+
const methods = [];
|
|
211
|
+
let ctor = null;
|
|
212
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
213
|
+
let visibility = "public";
|
|
214
|
+
if (this.check(TokenKind.Public) || this.check(TokenKind.Private) || this.check(TokenKind.Protected)) {
|
|
215
|
+
const v = this.advance().kind;
|
|
216
|
+
visibility = v === TokenKind.Public ? "public" : v === TokenKind.Private ? "private" : "protected";
|
|
217
|
+
}
|
|
218
|
+
if (this.check(TokenKind.Constructor)) {
|
|
219
|
+
const ctorStart = this.advance();
|
|
220
|
+
const params = this.parseParamList();
|
|
221
|
+
let baseArgs = null;
|
|
222
|
+
if (this.match(TokenKind.Colon)) {
|
|
223
|
+
this.consume(TokenKind.Base, "Expected 'base' after ':'");
|
|
224
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'base'");
|
|
225
|
+
baseArgs = [];
|
|
226
|
+
if (!this.check(TokenKind.RParen)) {
|
|
227
|
+
do {
|
|
228
|
+
baseArgs.push(this.parseExpression());
|
|
229
|
+
} while (this.match(TokenKind.Comma));
|
|
230
|
+
}
|
|
231
|
+
this.consume(TokenKind.RParen, "Expected ')' after base constructor arguments");
|
|
232
|
+
}
|
|
233
|
+
const body = this.parseBlock();
|
|
234
|
+
ctor = { kind: "ConstructorDecl", params, baseArgs, body, line: ctorStart.line, col: ctorStart.col };
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let isStatic = false;
|
|
238
|
+
if (this.check(TokenKind.Static)) {
|
|
239
|
+
isStatic = true;
|
|
240
|
+
this.advance();
|
|
241
|
+
}
|
|
242
|
+
let isAsync = false;
|
|
243
|
+
if (this.check(TokenKind.Async)) {
|
|
244
|
+
isAsync = true;
|
|
245
|
+
this.advance();
|
|
246
|
+
}
|
|
247
|
+
let isVirtual = false;
|
|
248
|
+
let isOverride = false;
|
|
249
|
+
if (this.check(TokenKind.Virtual)) {
|
|
250
|
+
isVirtual = true;
|
|
251
|
+
this.advance();
|
|
252
|
+
}
|
|
253
|
+
else if (this.check(TokenKind.Override)) {
|
|
254
|
+
isOverride = true;
|
|
255
|
+
this.advance();
|
|
256
|
+
}
|
|
257
|
+
const memberStart = this.peek();
|
|
258
|
+
const type = this.parseType();
|
|
259
|
+
const memberName = this.consume(TokenKind.Identifier, "Expected field, property, or method name").lexeme;
|
|
260
|
+
if (this.check(TokenKind.LParen)) {
|
|
261
|
+
const params = this.parseParamList();
|
|
262
|
+
const body = this.parseBlock();
|
|
263
|
+
if (isStatic && (isVirtual || isOverride)) {
|
|
264
|
+
this.diagnostics.error("'static' methods cannot be 'virtual' or 'override'", memberStart.line, memberStart.col);
|
|
265
|
+
}
|
|
266
|
+
methods.push({
|
|
267
|
+
kind: "MethodDecl",
|
|
268
|
+
visibility,
|
|
269
|
+
isStatic,
|
|
270
|
+
isAsync,
|
|
271
|
+
isVirtual,
|
|
272
|
+
isOverride,
|
|
273
|
+
name: memberName,
|
|
274
|
+
params,
|
|
275
|
+
returnType: type,
|
|
276
|
+
body,
|
|
277
|
+
line: memberStart.line,
|
|
278
|
+
col: memberStart.col,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else if (this.check(TokenKind.LBrace)) {
|
|
282
|
+
if (isVirtual || isOverride) {
|
|
283
|
+
this.diagnostics.error("'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
|
|
284
|
+
}
|
|
285
|
+
if (isAsync) {
|
|
286
|
+
this.diagnostics.error("'async' is only valid on methods", memberStart.line, memberStart.col);
|
|
287
|
+
}
|
|
288
|
+
if (isStatic) {
|
|
289
|
+
this.diagnostics.error("'static' properties are not supported in v1 (use a static field instead)", memberStart.line, memberStart.col);
|
|
290
|
+
}
|
|
291
|
+
this.advance(); // '{'
|
|
292
|
+
this.consume(TokenKind.Get, "Expected 'get' in property accessor list");
|
|
293
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'get'");
|
|
294
|
+
let hasSetter = false;
|
|
295
|
+
if (this.check(TokenKind.Set)) {
|
|
296
|
+
hasSetter = true;
|
|
297
|
+
this.advance();
|
|
298
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'set'");
|
|
299
|
+
}
|
|
300
|
+
this.consume(TokenKind.RBrace, "Expected '}' after property accessors");
|
|
301
|
+
properties.push({
|
|
302
|
+
kind: "PropertyDecl",
|
|
303
|
+
visibility,
|
|
304
|
+
hasSetter,
|
|
305
|
+
name: memberName,
|
|
306
|
+
type,
|
|
307
|
+
line: memberStart.line,
|
|
308
|
+
col: memberStart.col,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
if (isVirtual || isOverride) {
|
|
313
|
+
this.diagnostics.error("'virtual'/'override' are only valid on methods", memberStart.line, memberStart.col);
|
|
314
|
+
}
|
|
315
|
+
if (isAsync) {
|
|
316
|
+
this.diagnostics.error("'async' is only valid on methods", memberStart.line, memberStart.col);
|
|
317
|
+
}
|
|
318
|
+
let initializer = null;
|
|
319
|
+
if (isStatic) {
|
|
320
|
+
this.consume(TokenKind.Assign, "Static fields require an initializer (there is no constructor to assign them in)");
|
|
321
|
+
initializer = this.parseExpression();
|
|
322
|
+
}
|
|
323
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after field declaration");
|
|
324
|
+
fields.push({
|
|
325
|
+
kind: "FieldDecl",
|
|
326
|
+
visibility,
|
|
327
|
+
isStatic,
|
|
328
|
+
initializer,
|
|
329
|
+
name: memberName,
|
|
330
|
+
type,
|
|
331
|
+
line: memberStart.line,
|
|
332
|
+
col: memberStart.col,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.consume(TokenKind.RBrace, "Expected '}' after class body");
|
|
337
|
+
return { kind: "ClassDecl", isExported, name, baseList, fields, properties, constructor: ctor, methods, line: start.line, col: start.col };
|
|
338
|
+
}
|
|
339
|
+
parseInterfaceDecl(isExported) {
|
|
340
|
+
const start = this.advance(); // 'interface'
|
|
341
|
+
const name = this.consume(TokenKind.Identifier, "Expected interface name").lexeme;
|
|
342
|
+
const baseList = [];
|
|
343
|
+
if (this.match(TokenKind.Colon)) {
|
|
344
|
+
do {
|
|
345
|
+
baseList.push(this.consume(TokenKind.Identifier, "Expected base interface name").lexeme);
|
|
346
|
+
} while (this.match(TokenKind.Comma));
|
|
347
|
+
}
|
|
348
|
+
this.consume(TokenKind.LBrace, "Expected '{' before interface body");
|
|
349
|
+
const methods = [];
|
|
350
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
351
|
+
const memberStart = this.peek();
|
|
352
|
+
const returnType = this.parseType();
|
|
353
|
+
const methodName = this.consume(TokenKind.Identifier, "Expected method name").lexeme;
|
|
354
|
+
const params = this.parseParamList();
|
|
355
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after interface method signature");
|
|
356
|
+
methods.push({ name: methodName, params, returnType, line: memberStart.line, col: memberStart.col });
|
|
357
|
+
}
|
|
358
|
+
this.consume(TokenKind.RBrace, "Expected '}' after interface body");
|
|
359
|
+
return { kind: "InterfaceDecl", isExported, name, baseList, methods, line: start.line, col: start.col };
|
|
360
|
+
}
|
|
361
|
+
parseEnumDecl(isExported) {
|
|
362
|
+
const start = this.advance(); // 'enum'
|
|
363
|
+
const name = this.consume(TokenKind.Identifier, "Expected enum name").lexeme;
|
|
364
|
+
this.consume(TokenKind.LBrace, "Expected '{' before enum body");
|
|
365
|
+
const members = [];
|
|
366
|
+
if (!this.check(TokenKind.RBrace)) {
|
|
367
|
+
do {
|
|
368
|
+
members.push(this.consume(TokenKind.Identifier, "Expected enum member name").lexeme);
|
|
369
|
+
} while (this.match(TokenKind.Comma) && !this.check(TokenKind.RBrace));
|
|
370
|
+
}
|
|
371
|
+
this.consume(TokenKind.RBrace, "Expected '}' after enum body");
|
|
372
|
+
return { kind: "EnumDecl", isExported, name, members, line: start.line, col: start.col };
|
|
373
|
+
}
|
|
374
|
+
// `extern <Type> <Name>(<params>) [from "<path>"] [as "<jsName>"];` (function)
|
|
375
|
+
// `extern <Type> <Name> [from "<path>"] [as "<jsName>"];` (value)
|
|
376
|
+
// `extern class <Name> { ... } [from "<path>"] [as "<jsName>"];` (class)
|
|
377
|
+
parseExternDecl(isExported) {
|
|
378
|
+
const start = this.advance(); // 'extern'
|
|
379
|
+
if (this.check(TokenKind.Class)) {
|
|
380
|
+
this.advance();
|
|
381
|
+
return this.parseExternClassBody(start, isExported);
|
|
382
|
+
}
|
|
383
|
+
const type = this.parseType();
|
|
384
|
+
const name = this.consume(TokenKind.Identifier, "Expected name").lexeme;
|
|
385
|
+
if (this.check(TokenKind.LParen)) {
|
|
386
|
+
const params = this.parseParamList();
|
|
387
|
+
const { modulePath, jsName } = this.parseExternTail(name);
|
|
388
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern function declaration");
|
|
389
|
+
return { kind: "ExternFunctionDecl", isExported, name, jsName, params, returnType: type, modulePath, line: start.line, col: start.col };
|
|
390
|
+
}
|
|
391
|
+
const { modulePath, jsName } = this.parseExternTail(name);
|
|
392
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern declaration");
|
|
393
|
+
return { kind: "ExternValueDecl", isExported, name, jsName, type, modulePath, line: start.line, col: start.col };
|
|
394
|
+
}
|
|
395
|
+
// Optional `from "<path>"` (omit for an ambient global) and optional
|
|
396
|
+
// `as "<jsName>"` (omit when the JS-side name matches the KopScript-declared one).
|
|
397
|
+
parseExternTail(defaultJsName) {
|
|
398
|
+
let modulePath = null;
|
|
399
|
+
if (this.match(TokenKind.From)) {
|
|
400
|
+
modulePath = this.consume(TokenKind.String, "Expected a module path string after 'from'").lexeme;
|
|
401
|
+
}
|
|
402
|
+
let jsName = defaultJsName;
|
|
403
|
+
if (this.match(TokenKind.As)) {
|
|
404
|
+
jsName = this.consume(TokenKind.String, "Expected the external name string after 'as'").lexeme;
|
|
405
|
+
}
|
|
406
|
+
return { modulePath, jsName };
|
|
407
|
+
}
|
|
408
|
+
parseExternClassBody(start, isExported) {
|
|
409
|
+
const name = this.consume(TokenKind.Identifier, "Expected class name").lexeme;
|
|
410
|
+
this.consume(TokenKind.LBrace, "Expected '{' before extern class body");
|
|
411
|
+
let hasConstructor = false;
|
|
412
|
+
let ctorParams = [];
|
|
413
|
+
const properties = [];
|
|
414
|
+
const methods = [];
|
|
415
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
416
|
+
let isStatic = false;
|
|
417
|
+
if (this.check(TokenKind.Static)) {
|
|
418
|
+
isStatic = true;
|
|
419
|
+
this.advance();
|
|
420
|
+
}
|
|
421
|
+
let isVirtual = false;
|
|
422
|
+
let virtualTok = null;
|
|
423
|
+
if (this.check(TokenKind.Virtual)) {
|
|
424
|
+
virtualTok = this.advance();
|
|
425
|
+
isVirtual = true;
|
|
426
|
+
}
|
|
427
|
+
if (this.check(TokenKind.Constructor)) {
|
|
428
|
+
const ctorTok = this.advance();
|
|
429
|
+
if (virtualTok) {
|
|
430
|
+
this.diagnostics.error(`'virtual' is not valid on an extern constructor`, virtualTok.line, virtualTok.col);
|
|
431
|
+
}
|
|
432
|
+
if (hasConstructor) {
|
|
433
|
+
this.diagnostics.error(`Extern class '${name}' already has a constructor signature`, ctorTok.line, ctorTok.col);
|
|
434
|
+
}
|
|
435
|
+
ctorParams = this.parseParamList();
|
|
436
|
+
hasConstructor = true;
|
|
437
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern constructor signature");
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const type = this.parseType();
|
|
441
|
+
const memberName = this.consume(TokenKind.Identifier, "Expected member name").lexeme;
|
|
442
|
+
if (this.check(TokenKind.LParen)) {
|
|
443
|
+
const params = this.parseParamList();
|
|
444
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern method signature");
|
|
445
|
+
methods.push({ isStatic, isVirtual, name: memberName, params, returnType: type });
|
|
446
|
+
}
|
|
447
|
+
else if (this.check(TokenKind.LBrace)) {
|
|
448
|
+
if (virtualTok) {
|
|
449
|
+
this.diagnostics.error(`'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
|
|
450
|
+
}
|
|
451
|
+
this.advance();
|
|
452
|
+
this.consume(TokenKind.Get, "Expected 'get' in extern property accessor list");
|
|
453
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'get'");
|
|
454
|
+
let hasSetter = false;
|
|
455
|
+
if (this.check(TokenKind.Set)) {
|
|
456
|
+
hasSetter = true;
|
|
457
|
+
this.advance();
|
|
458
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'set'");
|
|
459
|
+
}
|
|
460
|
+
this.consume(TokenKind.RBrace, "Expected '}' after extern property accessors");
|
|
461
|
+
properties.push({ isStatic, name: memberName, type, hasSetter });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
if (virtualTok) {
|
|
465
|
+
this.diagnostics.error(`'virtual' is not valid on an extern property`, virtualTok.line, virtualTok.col);
|
|
466
|
+
}
|
|
467
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern property declaration");
|
|
468
|
+
properties.push({ isStatic, name: memberName, type, hasSetter: true });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
this.consume(TokenKind.RBrace, "Expected '}' after extern class body");
|
|
472
|
+
const { modulePath, jsName } = this.parseExternTail(name);
|
|
473
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after extern class declaration");
|
|
474
|
+
return { kind: "ExternClassDecl", isExported, name, jsName, hasConstructor, ctorParams, properties, methods, modulePath, line: start.line, col: start.col };
|
|
475
|
+
}
|
|
476
|
+
parseBlock() {
|
|
477
|
+
const start = this.consume(TokenKind.LBrace, "Expected '{'");
|
|
478
|
+
const statements = [];
|
|
479
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
480
|
+
const stmt = this.parseStatement();
|
|
481
|
+
if (stmt)
|
|
482
|
+
statements.push(stmt);
|
|
483
|
+
}
|
|
484
|
+
this.consume(TokenKind.RBrace, "Expected '}'");
|
|
485
|
+
return { kind: "Block", statements, line: start.line, col: start.col };
|
|
486
|
+
}
|
|
487
|
+
parseIf() {
|
|
488
|
+
const start = this.advance(); // 'if'
|
|
489
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'if'");
|
|
490
|
+
const condition = this.parseExpression();
|
|
491
|
+
this.consume(TokenKind.RParen, "Expected ')' after condition");
|
|
492
|
+
const thenBranch = this.parseBlock();
|
|
493
|
+
let elseBranch = null;
|
|
494
|
+
if (this.match(TokenKind.Else)) {
|
|
495
|
+
elseBranch = this.check(TokenKind.If) ? this.parseIf() : this.parseBlock();
|
|
496
|
+
}
|
|
497
|
+
return { kind: "IfStatement", condition, thenBranch, elseBranch, line: start.line, col: start.col };
|
|
498
|
+
}
|
|
499
|
+
parseWhile() {
|
|
500
|
+
const start = this.advance(); // 'while'
|
|
501
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'while'");
|
|
502
|
+
const condition = this.parseExpression();
|
|
503
|
+
this.consume(TokenKind.RParen, "Expected ')' after condition");
|
|
504
|
+
const body = this.parseBlock();
|
|
505
|
+
return { kind: "WhileStatement", condition, body, line: start.line, col: start.col };
|
|
506
|
+
}
|
|
507
|
+
parseFor() {
|
|
508
|
+
const start = this.advance(); // 'for'
|
|
509
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'for'");
|
|
510
|
+
let init = null;
|
|
511
|
+
if (this.check(TokenKind.Const)) {
|
|
512
|
+
init = this.parseConstDecl();
|
|
513
|
+
}
|
|
514
|
+
else if (this.isDeclStart()) {
|
|
515
|
+
init = this.parseVarDeclHeader();
|
|
516
|
+
}
|
|
517
|
+
else if (!this.check(TokenKind.Semicolon)) {
|
|
518
|
+
const expr = this.parseExpression();
|
|
519
|
+
init = { kind: "ExpressionStatement", expression: expr, line: expr.line, col: expr.col };
|
|
520
|
+
}
|
|
521
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after for-loop initializer");
|
|
522
|
+
let condition = null;
|
|
523
|
+
if (!this.check(TokenKind.Semicolon))
|
|
524
|
+
condition = this.parseExpression();
|
|
525
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after for-loop condition");
|
|
526
|
+
let update = null;
|
|
527
|
+
if (!this.check(TokenKind.RParen))
|
|
528
|
+
update = this.parseExpression();
|
|
529
|
+
this.consume(TokenKind.RParen, "Expected ')' after for clauses");
|
|
530
|
+
const body = this.parseBlock();
|
|
531
|
+
return { kind: "ForStatement", init, condition, update, body, line: start.line, col: start.col };
|
|
532
|
+
}
|
|
533
|
+
parseForeach() {
|
|
534
|
+
const start = this.advance(); // 'foreach'
|
|
535
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'foreach'");
|
|
536
|
+
const varType = this.parseType();
|
|
537
|
+
const varName = this.consume(TokenKind.Identifier, "Expected loop variable name").lexeme;
|
|
538
|
+
this.consume(TokenKind.In, "Expected 'in' in foreach loop");
|
|
539
|
+
const iterable = this.parseExpression();
|
|
540
|
+
this.consume(TokenKind.RParen, "Expected ')' after foreach clause");
|
|
541
|
+
const body = this.parseBlock();
|
|
542
|
+
return { kind: "ForInStatement", varType, varName, iterable, body, line: start.line, col: start.col };
|
|
543
|
+
}
|
|
544
|
+
parseReturn() {
|
|
545
|
+
const start = this.advance(); // 'return'
|
|
546
|
+
let value = null;
|
|
547
|
+
if (!this.check(TokenKind.Semicolon))
|
|
548
|
+
value = this.parseExpression();
|
|
549
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after return statement");
|
|
550
|
+
return { kind: "ReturnStatement", value, line: start.line, col: start.col };
|
|
551
|
+
}
|
|
552
|
+
parseTry() {
|
|
553
|
+
const start = this.advance(); // 'try'
|
|
554
|
+
const tryBlock = this.parseBlock();
|
|
555
|
+
let catchParam = null;
|
|
556
|
+
let catchBlock = null;
|
|
557
|
+
if (this.match(TokenKind.Catch)) {
|
|
558
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'catch'");
|
|
559
|
+
const type = this.parseType();
|
|
560
|
+
const name = this.consume(TokenKind.Identifier, "Expected catch parameter name").lexeme;
|
|
561
|
+
this.consume(TokenKind.RParen, "Expected ')' after catch parameter");
|
|
562
|
+
catchParam = { type, name };
|
|
563
|
+
catchBlock = this.parseBlock();
|
|
564
|
+
}
|
|
565
|
+
let finallyBlock = null;
|
|
566
|
+
if (this.match(TokenKind.Finally)) {
|
|
567
|
+
finallyBlock = this.parseBlock();
|
|
568
|
+
}
|
|
569
|
+
if (!catchBlock && !finallyBlock) {
|
|
570
|
+
this.diagnostics.error("'try' must be followed by 'catch' and/or 'finally'", start.line, start.col);
|
|
571
|
+
}
|
|
572
|
+
return { kind: "TryStatement", tryBlock, catchParam, catchBlock, finallyBlock, line: start.line, col: start.col };
|
|
573
|
+
}
|
|
574
|
+
parseThrow() {
|
|
575
|
+
const start = this.advance(); // 'throw'
|
|
576
|
+
const expression = this.parseExpression();
|
|
577
|
+
this.consume(TokenKind.Semicolon, "Expected ';' after 'throw'");
|
|
578
|
+
return { kind: "ThrowStatement", expression, line: start.line, col: start.col };
|
|
579
|
+
}
|
|
580
|
+
// ---------- types ----------
|
|
581
|
+
parseType() {
|
|
582
|
+
let type;
|
|
583
|
+
if (this.check(TokenKind.LParen)) {
|
|
584
|
+
type = this.parseFunctionType();
|
|
585
|
+
}
|
|
586
|
+
else if (this.check(TokenKind.Task)) {
|
|
587
|
+
this.advance();
|
|
588
|
+
let resultType = { kind: "NamedType", name: "void" };
|
|
589
|
+
if (this.match(TokenKind.Lt)) {
|
|
590
|
+
resultType = this.parseType();
|
|
591
|
+
this.consume(TokenKind.Gt, "Expected '>' after task result type");
|
|
592
|
+
}
|
|
593
|
+
type = { kind: "TaskType", resultType };
|
|
594
|
+
}
|
|
595
|
+
else if (this.check(TokenKind.State)) {
|
|
596
|
+
this.advance();
|
|
597
|
+
this.consume(TokenKind.Lt, "Expected '<' after 'state'");
|
|
598
|
+
const valueType = this.parseType();
|
|
599
|
+
this.consume(TokenKind.Gt, "Expected '>' after state value type");
|
|
600
|
+
type = { kind: "StateType", valueType };
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
const nameToken = this.check(TokenKind.Void) ? this.advance() : this.consume(TokenKind.Identifier, "Expected type name");
|
|
604
|
+
type = { kind: "NamedType", name: nameToken.lexeme };
|
|
605
|
+
}
|
|
606
|
+
while (this.check(TokenKind.LBracket)) {
|
|
607
|
+
this.advance();
|
|
608
|
+
this.consume(TokenKind.RBracket, "Expected ']' after '[' in array type");
|
|
609
|
+
type = { kind: "ArrayType", element: type };
|
|
610
|
+
}
|
|
611
|
+
return type;
|
|
612
|
+
}
|
|
613
|
+
// `(number, string) => bool` — unnamed parameter types, unlike a lambda's
|
|
614
|
+
// parameter list.
|
|
615
|
+
parseFunctionType() {
|
|
616
|
+
this.consume(TokenKind.LParen, "Expected '('");
|
|
617
|
+
const params = [];
|
|
618
|
+
if (!this.check(TokenKind.RParen)) {
|
|
619
|
+
do {
|
|
620
|
+
params.push(this.parseType());
|
|
621
|
+
} while (this.match(TokenKind.Comma));
|
|
622
|
+
}
|
|
623
|
+
this.consume(TokenKind.RParen, "Expected ')' after function type parameters");
|
|
624
|
+
this.consume(TokenKind.Arrow, "Expected '=>' after function type parameters");
|
|
625
|
+
const returnType = this.parseType();
|
|
626
|
+
return { kind: "FunctionType", params, returnType };
|
|
627
|
+
}
|
|
628
|
+
// ---------- expressions ----------
|
|
629
|
+
parseExpression() {
|
|
630
|
+
return this.parseAssignment();
|
|
631
|
+
}
|
|
632
|
+
parseAssignment() {
|
|
633
|
+
const left = this.parseLogicalOr();
|
|
634
|
+
if (this.check(TokenKind.Assign)) {
|
|
635
|
+
const opTok = this.advance();
|
|
636
|
+
const value = this.parseAssignment();
|
|
637
|
+
return { kind: "AssignExpr", target: left, value, line: opTok.line, col: opTok.col };
|
|
638
|
+
}
|
|
639
|
+
return left;
|
|
640
|
+
}
|
|
641
|
+
parseLogicalOr() {
|
|
642
|
+
let left = this.parseLogicalAnd();
|
|
643
|
+
while (this.check(TokenKind.OrOr)) {
|
|
644
|
+
const op = this.advance();
|
|
645
|
+
const right = this.parseLogicalAnd();
|
|
646
|
+
left = { kind: "LogicalExpr", op: "||", left, right, line: op.line, col: op.col };
|
|
647
|
+
}
|
|
648
|
+
return left;
|
|
649
|
+
}
|
|
650
|
+
parseLogicalAnd() {
|
|
651
|
+
let left = this.parseEquality();
|
|
652
|
+
while (this.check(TokenKind.AndAnd)) {
|
|
653
|
+
const op = this.advance();
|
|
654
|
+
const right = this.parseEquality();
|
|
655
|
+
left = { kind: "LogicalExpr", op: "&&", left, right, line: op.line, col: op.col };
|
|
656
|
+
}
|
|
657
|
+
return left;
|
|
658
|
+
}
|
|
659
|
+
parseEquality() {
|
|
660
|
+
let left = this.parseComparison();
|
|
661
|
+
while (this.check(TokenKind.Eq) || this.check(TokenKind.NotEq)) {
|
|
662
|
+
const op = this.advance();
|
|
663
|
+
const right = this.parseComparison();
|
|
664
|
+
left = { kind: "BinaryExpr", op: op.kind === TokenKind.Eq ? "==" : "!=", left, right, line: op.line, col: op.col };
|
|
665
|
+
}
|
|
666
|
+
return left;
|
|
667
|
+
}
|
|
668
|
+
parseComparison() {
|
|
669
|
+
let left = this.parseAdditive();
|
|
670
|
+
while (this.check(TokenKind.Lt) ||
|
|
671
|
+
this.check(TokenKind.Gt) ||
|
|
672
|
+
this.check(TokenKind.LtEq) ||
|
|
673
|
+
this.check(TokenKind.GtEq)) {
|
|
674
|
+
const op = this.advance();
|
|
675
|
+
const right = this.parseAdditive();
|
|
676
|
+
const map = { [TokenKind.Lt]: "<", [TokenKind.Gt]: ">", [TokenKind.LtEq]: "<=", [TokenKind.GtEq]: ">=" };
|
|
677
|
+
left = { kind: "BinaryExpr", op: map[op.kind], left, right, line: op.line, col: op.col };
|
|
678
|
+
}
|
|
679
|
+
return left;
|
|
680
|
+
}
|
|
681
|
+
parseAdditive() {
|
|
682
|
+
let left = this.parseMultiplicative();
|
|
683
|
+
while (this.check(TokenKind.Plus) || this.check(TokenKind.Minus)) {
|
|
684
|
+
const op = this.advance();
|
|
685
|
+
const right = this.parseMultiplicative();
|
|
686
|
+
left = { kind: "BinaryExpr", op: op.kind === TokenKind.Plus ? "+" : "-", left, right, line: op.line, col: op.col };
|
|
687
|
+
}
|
|
688
|
+
return left;
|
|
689
|
+
}
|
|
690
|
+
parseMultiplicative() {
|
|
691
|
+
let left = this.parseUnary();
|
|
692
|
+
while (this.check(TokenKind.Star) || this.check(TokenKind.Slash) || this.check(TokenKind.Percent)) {
|
|
693
|
+
const op = this.advance();
|
|
694
|
+
const right = this.parseUnary();
|
|
695
|
+
const map = { [TokenKind.Star]: "*", [TokenKind.Slash]: "/", [TokenKind.Percent]: "%" };
|
|
696
|
+
left = { kind: "BinaryExpr", op: map[op.kind], left, right, line: op.line, col: op.col };
|
|
697
|
+
}
|
|
698
|
+
return left;
|
|
699
|
+
}
|
|
700
|
+
parseUnary() {
|
|
701
|
+
if (this.check(TokenKind.Minus) || this.check(TokenKind.Not)) {
|
|
702
|
+
const op = this.advance();
|
|
703
|
+
const operand = this.parseUnary();
|
|
704
|
+
return { kind: "UnaryExpr", op: op.kind === TokenKind.Minus ? "-" : "!", operand, line: op.line, col: op.col };
|
|
705
|
+
}
|
|
706
|
+
if (this.check(TokenKind.Await)) {
|
|
707
|
+
const op = this.advance();
|
|
708
|
+
const operand = this.parseUnary();
|
|
709
|
+
return { kind: "AwaitExpr", operand, line: op.line, col: op.col };
|
|
710
|
+
}
|
|
711
|
+
return this.parsePostfix();
|
|
712
|
+
}
|
|
713
|
+
parsePostfix() {
|
|
714
|
+
let expr = this.parsePrimary();
|
|
715
|
+
for (;;) {
|
|
716
|
+
if (this.check(TokenKind.Dot)) {
|
|
717
|
+
const dot = this.advance();
|
|
718
|
+
const name = this.consume(TokenKind.Identifier, "Expected property name after '.'").lexeme;
|
|
719
|
+
expr = { kind: "MemberExpr", object: expr, property: name, line: dot.line, col: dot.col };
|
|
720
|
+
}
|
|
721
|
+
else if (this.check(TokenKind.LParen)) {
|
|
722
|
+
const paren = this.advance();
|
|
723
|
+
const args = [];
|
|
724
|
+
if (!this.check(TokenKind.RParen)) {
|
|
725
|
+
do {
|
|
726
|
+
args.push(this.parseExpression());
|
|
727
|
+
} while (this.match(TokenKind.Comma));
|
|
728
|
+
}
|
|
729
|
+
this.consume(TokenKind.RParen, "Expected ')' after arguments");
|
|
730
|
+
expr = { kind: "CallExpr", callee: expr, args, line: paren.line, col: paren.col };
|
|
731
|
+
}
|
|
732
|
+
else if (this.check(TokenKind.LBracket)) {
|
|
733
|
+
const bracket = this.advance();
|
|
734
|
+
const index = this.parseExpression();
|
|
735
|
+
this.consume(TokenKind.RBracket, "Expected ']' after index");
|
|
736
|
+
expr = { kind: "IndexExpr", object: expr, index, line: bracket.line, col: bracket.col };
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return expr;
|
|
743
|
+
}
|
|
744
|
+
parsePrimary() {
|
|
745
|
+
const t = this.peek();
|
|
746
|
+
if (this.check(TokenKind.Number)) {
|
|
747
|
+
this.advance();
|
|
748
|
+
return { kind: "NumberLiteral", value: parseFloat(t.lexeme), line: t.line, col: t.col };
|
|
749
|
+
}
|
|
750
|
+
if (this.check(TokenKind.String)) {
|
|
751
|
+
this.advance();
|
|
752
|
+
return { kind: "StringLiteral", value: t.lexeme, line: t.line, col: t.col };
|
|
753
|
+
}
|
|
754
|
+
if (this.check(TokenKind.InterpolatedString)) {
|
|
755
|
+
this.advance();
|
|
756
|
+
return this.parseInterpolatedString(t);
|
|
757
|
+
}
|
|
758
|
+
if (this.check(TokenKind.True) || this.check(TokenKind.False)) {
|
|
759
|
+
this.advance();
|
|
760
|
+
return { kind: "BoolLiteral", value: t.kind === TokenKind.True, line: t.line, col: t.col };
|
|
761
|
+
}
|
|
762
|
+
if (this.check(TokenKind.This)) {
|
|
763
|
+
this.advance();
|
|
764
|
+
return { kind: "ThisExpr", line: t.line, col: t.col };
|
|
765
|
+
}
|
|
766
|
+
if (this.check(TokenKind.New)) {
|
|
767
|
+
this.advance();
|
|
768
|
+
const className = this.consume(TokenKind.Identifier, "Expected class name after 'new'").lexeme;
|
|
769
|
+
this.consume(TokenKind.LParen, "Expected '(' after class name");
|
|
770
|
+
const args = [];
|
|
771
|
+
if (!this.check(TokenKind.RParen)) {
|
|
772
|
+
do {
|
|
773
|
+
args.push(this.parseExpression());
|
|
774
|
+
} while (this.match(TokenKind.Comma));
|
|
775
|
+
}
|
|
776
|
+
this.consume(TokenKind.RParen, "Expected ')' after constructor arguments");
|
|
777
|
+
return { kind: "NewExpr", className, args, line: t.line, col: t.col };
|
|
778
|
+
}
|
|
779
|
+
if (this.check(TokenKind.LBracket)) {
|
|
780
|
+
this.advance();
|
|
781
|
+
const elements = [];
|
|
782
|
+
if (!this.check(TokenKind.RBracket)) {
|
|
783
|
+
do {
|
|
784
|
+
elements.push(this.parseExpression());
|
|
785
|
+
} while (this.match(TokenKind.Comma));
|
|
786
|
+
}
|
|
787
|
+
this.consume(TokenKind.RBracket, "Expected ']' after array literal");
|
|
788
|
+
return { kind: "ArrayLiteral", elements, line: t.line, col: t.col };
|
|
789
|
+
}
|
|
790
|
+
if (this.check(TokenKind.Match)) {
|
|
791
|
+
return this.parseMatchExpr();
|
|
792
|
+
}
|
|
793
|
+
if (this.check(TokenKind.State)) {
|
|
794
|
+
this.advance();
|
|
795
|
+
this.consume(TokenKind.LParen, "Expected '(' after 'state'");
|
|
796
|
+
const initializer = this.parseExpression();
|
|
797
|
+
this.consume(TokenKind.RParen, "Expected ')' after state initializer");
|
|
798
|
+
return { kind: "StateExpr", initializer, line: t.line, col: t.col };
|
|
799
|
+
}
|
|
800
|
+
if (this.check(TokenKind.Identifier)) {
|
|
801
|
+
this.advance();
|
|
802
|
+
return { kind: "Identifier", name: t.lexeme, line: t.line, col: t.col };
|
|
803
|
+
}
|
|
804
|
+
if (this.check(TokenKind.LParen)) {
|
|
805
|
+
if (this.looksLikeLambda())
|
|
806
|
+
return this.parseLambda();
|
|
807
|
+
this.advance();
|
|
808
|
+
const expr = this.parseExpression();
|
|
809
|
+
this.consume(TokenKind.RParen, "Expected ')' after expression");
|
|
810
|
+
return expr;
|
|
811
|
+
}
|
|
812
|
+
this.diagnostics.error(`Unexpected token '${t.lexeme || t.kind}'`, t.line, t.col);
|
|
813
|
+
throw new ParseError();
|
|
814
|
+
}
|
|
815
|
+
// Lightweight lookahead (not a trial parse) distinguishing a lambda's
|
|
816
|
+
// parameter list from a parenthesized expression. A lambda parameter is
|
|
817
|
+
// always `Type name` (types are required, no inference in v1), so seeing
|
|
818
|
+
// two consecutive identifiers — or an empty `()` followed by `=>` — is
|
|
819
|
+
// unambiguous. This doesn't handle a lambda whose own first parameter is
|
|
820
|
+
// itself a function type (e.g. a higher-order lambda); that's a known v1
|
|
821
|
+
// gap in favor of keeping this check cheap.
|
|
822
|
+
looksLikeLambda() {
|
|
823
|
+
let i = this.pos + 1; // just past '('
|
|
824
|
+
if (this.tokens[i]?.kind === TokenKind.RParen) {
|
|
825
|
+
return this.tokens[i + 1]?.kind === TokenKind.Arrow;
|
|
826
|
+
}
|
|
827
|
+
const startKind = this.tokens[i]?.kind;
|
|
828
|
+
if (startKind !== TokenKind.Identifier && startKind !== TokenKind.Void)
|
|
829
|
+
return false;
|
|
830
|
+
i++;
|
|
831
|
+
while (this.tokens[i]?.kind === TokenKind.LBracket && this.tokens[i + 1]?.kind === TokenKind.RBracket) {
|
|
832
|
+
i += 2;
|
|
833
|
+
}
|
|
834
|
+
return this.tokens[i]?.kind === TokenKind.Identifier;
|
|
835
|
+
}
|
|
836
|
+
parseLambda() {
|
|
837
|
+
const start = this.peek(); // '('
|
|
838
|
+
this.advance();
|
|
839
|
+
const params = [];
|
|
840
|
+
if (!this.check(TokenKind.RParen)) {
|
|
841
|
+
do {
|
|
842
|
+
const type = this.parseType();
|
|
843
|
+
const name = this.consume(TokenKind.Identifier, "Expected lambda parameter name").lexeme;
|
|
844
|
+
params.push({ name, type });
|
|
845
|
+
} while (this.match(TokenKind.Comma));
|
|
846
|
+
}
|
|
847
|
+
this.consume(TokenKind.RParen, "Expected ')' after lambda parameters");
|
|
848
|
+
this.consume(TokenKind.Arrow, "Expected '=>' after lambda parameters");
|
|
849
|
+
const body = this.check(TokenKind.LBrace) ? this.parseBlock() : this.parseExpression();
|
|
850
|
+
return { kind: "LambdaExpr", params, body, line: start.line, col: start.col };
|
|
851
|
+
}
|
|
852
|
+
parseInterpolatedString(token) {
|
|
853
|
+
const parts = [];
|
|
854
|
+
const raw = token.lexeme;
|
|
855
|
+
let text = "";
|
|
856
|
+
let i = 0;
|
|
857
|
+
const flushText = () => {
|
|
858
|
+
if (text.length > 0) {
|
|
859
|
+
parts.push({ kind: "Text", text });
|
|
860
|
+
text = "";
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
while (i < raw.length) {
|
|
864
|
+
const c = raw[i];
|
|
865
|
+
if (c === "\\" && i + 1 < raw.length) {
|
|
866
|
+
const esc = raw[i + 1];
|
|
867
|
+
const map = { n: "\n", t: "\t", r: "\r", "\\": "\\", '"': '"', "{": "{", "}": "}" };
|
|
868
|
+
text += map[esc] ?? esc;
|
|
869
|
+
i += 2;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (c === "{") {
|
|
873
|
+
flushText();
|
|
874
|
+
let depth = 1;
|
|
875
|
+
let j = i + 1;
|
|
876
|
+
while (j < raw.length && depth > 0) {
|
|
877
|
+
if (raw[j] === "{")
|
|
878
|
+
depth++;
|
|
879
|
+
else if (raw[j] === "}")
|
|
880
|
+
depth--;
|
|
881
|
+
if (depth > 0)
|
|
882
|
+
j++;
|
|
883
|
+
}
|
|
884
|
+
const exprSource = raw.slice(i + 1, j);
|
|
885
|
+
parts.push({ kind: "Expr", expression: this.parseSubExpression(exprSource, token) });
|
|
886
|
+
i = j + 1;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
text += c;
|
|
890
|
+
i++;
|
|
891
|
+
}
|
|
892
|
+
flushText();
|
|
893
|
+
if (parts.length === 0)
|
|
894
|
+
parts.push({ kind: "Text", text: "" });
|
|
895
|
+
return { kind: "InterpolatedStringLiteral", parts, line: token.line, col: token.col };
|
|
896
|
+
}
|
|
897
|
+
parseSubExpression(source, token) {
|
|
898
|
+
const subDiagnostics = new DiagnosticBag();
|
|
899
|
+
const subTokens = new Lexer(source, subDiagnostics).tokenize();
|
|
900
|
+
const subParser = new Parser(subTokens, subDiagnostics);
|
|
901
|
+
const expr = subParser.parseExpression();
|
|
902
|
+
for (const d of subDiagnostics.diagnostics) {
|
|
903
|
+
this.diagnostics.error(d.message, token.line, token.col);
|
|
904
|
+
}
|
|
905
|
+
return expr;
|
|
906
|
+
}
|
|
907
|
+
parseMatchExpr() {
|
|
908
|
+
const start = this.advance(); // 'match'
|
|
909
|
+
const subject = this.parseExpression();
|
|
910
|
+
this.consume(TokenKind.LBrace, "Expected '{' after match subject");
|
|
911
|
+
const arms = [];
|
|
912
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
913
|
+
const armStart = this.peek();
|
|
914
|
+
const pattern = this.parsePattern();
|
|
915
|
+
this.consume(TokenKind.Arrow, "Expected '=>' after match pattern");
|
|
916
|
+
const result = this.parseExpression();
|
|
917
|
+
arms.push({ pattern, result, line: armStart.line, col: armStart.col });
|
|
918
|
+
if (!this.match(TokenKind.Comma))
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
this.consume(TokenKind.RBrace, "Expected '}' after match arms");
|
|
922
|
+
return { kind: "MatchExpr", subject, arms, line: start.line, col: start.col };
|
|
923
|
+
}
|
|
924
|
+
parsePattern() {
|
|
925
|
+
if (this.check(TokenKind.Underscore)) {
|
|
926
|
+
this.advance();
|
|
927
|
+
return { kind: "WildcardPattern" };
|
|
928
|
+
}
|
|
929
|
+
if (this.check(TokenKind.RegexLiteral)) {
|
|
930
|
+
const t = this.advance();
|
|
931
|
+
return { kind: "RegexPattern", source: t.lexeme };
|
|
932
|
+
}
|
|
933
|
+
const values = [this.parseLogicalOr()];
|
|
934
|
+
while (this.match(TokenKind.Comma)) {
|
|
935
|
+
values.push(this.parseLogicalOr());
|
|
936
|
+
}
|
|
937
|
+
return { kind: "LiteralPattern", values };
|
|
938
|
+
}
|
|
939
|
+
// ---------- token helpers ----------
|
|
940
|
+
check(kind) {
|
|
941
|
+
return this.peek().kind === kind;
|
|
942
|
+
}
|
|
943
|
+
peek() {
|
|
944
|
+
return this.tokens[this.pos];
|
|
945
|
+
}
|
|
946
|
+
advance() {
|
|
947
|
+
const t = this.tokens[this.pos];
|
|
948
|
+
if (this.pos < this.tokens.length - 1)
|
|
949
|
+
this.pos++;
|
|
950
|
+
return t;
|
|
951
|
+
}
|
|
952
|
+
match(kind) {
|
|
953
|
+
if (this.check(kind)) {
|
|
954
|
+
this.advance();
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
consume(kind, message) {
|
|
960
|
+
if (this.check(kind))
|
|
961
|
+
return this.advance();
|
|
962
|
+
const t = this.peek();
|
|
963
|
+
this.diagnostics.error(message, t.line, t.col);
|
|
964
|
+
throw new ParseError();
|
|
965
|
+
}
|
|
966
|
+
synchronize() {
|
|
967
|
+
// Guarantee forward progress: always consume the offending token first,
|
|
968
|
+
// so a parse failure immediately after a semicolon can't leave `pos`
|
|
969
|
+
// unmoved and re-throw on the same token forever.
|
|
970
|
+
if (!this.check(TokenKind.EOF))
|
|
971
|
+
this.advance();
|
|
972
|
+
while (!this.check(TokenKind.EOF)) {
|
|
973
|
+
if (this.tokens[this.pos - 1]?.kind === TokenKind.Semicolon)
|
|
974
|
+
return;
|
|
975
|
+
if (this.check(TokenKind.Class) ||
|
|
976
|
+
this.check(TokenKind.Interface) ||
|
|
977
|
+
this.check(TokenKind.Enum) ||
|
|
978
|
+
this.check(TokenKind.Extern) ||
|
|
979
|
+
this.check(TokenKind.Const) ||
|
|
980
|
+
this.check(TokenKind.If) ||
|
|
981
|
+
this.check(TokenKind.While) ||
|
|
982
|
+
this.check(TokenKind.For) ||
|
|
983
|
+
this.check(TokenKind.Foreach) ||
|
|
984
|
+
this.check(TokenKind.Return) ||
|
|
985
|
+
this.check(TokenKind.Try) ||
|
|
986
|
+
this.check(TokenKind.Throw) ||
|
|
987
|
+
this.isDeclStart()) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
this.advance();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
class ParseError extends Error {
|
|
995
|
+
}
|