@starpak/lang 0.0.0-stage → 0.1.2
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 +22 -0
- package/lib/ast.d.ts +334 -0
- package/lib/ast.d.ts.map +1 -0
- package/lib/ast.js +9 -0
- package/lib/ast.js.map +1 -0
- package/lib/checker.d.ts +17 -0
- package/lib/checker.d.ts.map +1 -0
- package/lib/checker.js +525 -0
- package/lib/checker.js.map +1 -0
- package/lib/diagnostics.d.ts +46 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +77 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/emit/lua.d.ts +19 -0
- package/lib/emit/lua.d.ts.map +1 -0
- package/lib/emit/lua.js +451 -0
- package/lib/emit/lua.js.map +1 -0
- package/lib/emit/ts.d.ts +28 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +803 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/index.js.map +1 -0
- package/lib/lexer.d.ts +10 -0
- package/lib/lexer.d.ts.map +1 -0
- package/lib/lexer.js +282 -0
- package/lib/lexer.js.map +1 -0
- package/lib/parser.d.ts +11 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +1133 -0
- package/lib/parser.js.map +1 -0
- package/lib/token.d.ts +32 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/lib/types.d.ts +63 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +177 -0
- package/lib/types.js.map +1 -0
- package/package.json +10 -3
- package/src/ast.ts +459 -0
- package/src/checker.ts +554 -0
- package/src/diagnostics.ts +100 -0
- package/src/emit/lua.ts +484 -0
- package/src/emit/ts.ts +861 -0
- package/src/index.ts +21 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1251 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/README.md +0 -3
package/lib/parser.js
ADDED
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Spak Lang - Recursive descent parser
|
|
4
|
+
*
|
|
5
|
+
* Produces an AST (`Program`) from Spak source. The grammar follows
|
|
6
|
+
* docs/syntax/spak-lang.md.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parse = parse;
|
|
10
|
+
exports.parseExpression = parseExpression;
|
|
11
|
+
const lexer_1 = require("./lexer");
|
|
12
|
+
const token_1 = require("./token");
|
|
13
|
+
/**
|
|
14
|
+
* Binary operator precedence, low to high. `^` is right-associative.
|
|
15
|
+
*/
|
|
16
|
+
const BINARY_PRECEDENCE = {
|
|
17
|
+
'or': 1,
|
|
18
|
+
'and': 2,
|
|
19
|
+
'==': 3,
|
|
20
|
+
'~=': 3,
|
|
21
|
+
'<': 3,
|
|
22
|
+
'<=': 3,
|
|
23
|
+
'>': 3,
|
|
24
|
+
'>=': 3,
|
|
25
|
+
'|>': 4,
|
|
26
|
+
'??': 5,
|
|
27
|
+
'..': 6,
|
|
28
|
+
'+': 7,
|
|
29
|
+
'-': 7,
|
|
30
|
+
'*': 8,
|
|
31
|
+
'/': 8,
|
|
32
|
+
'//': 8,
|
|
33
|
+
'%': 8,
|
|
34
|
+
'^': 10,
|
|
35
|
+
};
|
|
36
|
+
const UNARY_PRECEDENCE = 9;
|
|
37
|
+
const BLOCK_END_KEYWORDS = [
|
|
38
|
+
'end', 'else', 'elseif', 'when', 'until', 'catch', 'finally',
|
|
39
|
+
];
|
|
40
|
+
const PRIMITIVE_TYPE_NAMES = new Set([
|
|
41
|
+
'any', 'nil', 'number', 'string', 'boolean', 'table', 'function',
|
|
42
|
+
]);
|
|
43
|
+
/** Statement kinds that may carry a `---` doc comment. */
|
|
44
|
+
const DOC_STATEMENTS = new Set([
|
|
45
|
+
'FnDecl', 'ClassDecl', 'VarDecl', 'TypeAliasDecl', 'ExportDecl', 'ImportDecl',
|
|
46
|
+
]);
|
|
47
|
+
function isNameToken(token) {
|
|
48
|
+
return token.type === token_1.TokenType.Identifier || token.type === token_1.TokenType.Keyword;
|
|
49
|
+
}
|
|
50
|
+
class Parser {
|
|
51
|
+
tokens;
|
|
52
|
+
pos = 0;
|
|
53
|
+
constructor(source) {
|
|
54
|
+
this.tokens = (0, lexer_1.tokenize)(source);
|
|
55
|
+
}
|
|
56
|
+
/* ---------------------------------------------------------------- */
|
|
57
|
+
/* Token helpers */
|
|
58
|
+
/* ---------------------------------------------------------------- */
|
|
59
|
+
peek(offset = 0) {
|
|
60
|
+
const index = this.pos + offset;
|
|
61
|
+
return this.tokens[index < this.tokens.length ? index : this.tokens.length - 1];
|
|
62
|
+
}
|
|
63
|
+
next() {
|
|
64
|
+
const token = this.peek();
|
|
65
|
+
if (this.pos < this.tokens.length - 1)
|
|
66
|
+
this.pos++;
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
at(type, value) {
|
|
70
|
+
const token = this.peek();
|
|
71
|
+
return token.type === type && (value === undefined || token.value === value);
|
|
72
|
+
}
|
|
73
|
+
atKeyword(value) {
|
|
74
|
+
return this.at(token_1.TokenType.Keyword, value);
|
|
75
|
+
}
|
|
76
|
+
atOperator(value) {
|
|
77
|
+
return this.at(token_1.TokenType.Operator, value);
|
|
78
|
+
}
|
|
79
|
+
match(type, value) {
|
|
80
|
+
if (!this.at(type, value))
|
|
81
|
+
return false;
|
|
82
|
+
this.next();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
matchKeyword(value) {
|
|
86
|
+
return this.match(token_1.TokenType.Keyword, value);
|
|
87
|
+
}
|
|
88
|
+
matchOperator(value) {
|
|
89
|
+
return this.match(token_1.TokenType.Operator, value);
|
|
90
|
+
}
|
|
91
|
+
expect(type, value, message) {
|
|
92
|
+
if (this.at(type, value))
|
|
93
|
+
return this.next();
|
|
94
|
+
const token = this.peek();
|
|
95
|
+
throw this.error(message ?? `Expected ${value ?? type} but got "${token.value}"`, token);
|
|
96
|
+
}
|
|
97
|
+
expectKeyword(value) {
|
|
98
|
+
return this.expect(token_1.TokenType.Keyword, value, `Expected "${value}"`);
|
|
99
|
+
}
|
|
100
|
+
expectName(message) {
|
|
101
|
+
if (isNameToken(this.peek()))
|
|
102
|
+
return this.next();
|
|
103
|
+
throw this.error(message);
|
|
104
|
+
}
|
|
105
|
+
error(message, token = this.peek()) {
|
|
106
|
+
return new SyntaxError(`${message} at ${token.line}:${token.column}`);
|
|
107
|
+
}
|
|
108
|
+
loc(token) {
|
|
109
|
+
return { line: token.line, column: token.column };
|
|
110
|
+
}
|
|
111
|
+
skipNewlines() {
|
|
112
|
+
while (this.at(token_1.TokenType.Newline))
|
|
113
|
+
this.next();
|
|
114
|
+
}
|
|
115
|
+
skipSeparators() {
|
|
116
|
+
while (this.at(token_1.TokenType.Newline) || this.at(token_1.TokenType.Semicolon))
|
|
117
|
+
this.next();
|
|
118
|
+
}
|
|
119
|
+
isBlockEnd() {
|
|
120
|
+
const token = this.peek();
|
|
121
|
+
return token.type === token_1.TokenType.Keyword && BLOCK_END_KEYWORDS.includes(token.value);
|
|
122
|
+
}
|
|
123
|
+
isStatementEnd() {
|
|
124
|
+
const token = this.peek();
|
|
125
|
+
return token.type === token_1.TokenType.EOF
|
|
126
|
+
|| token.type === token_1.TokenType.Newline
|
|
127
|
+
|| token.type === token_1.TokenType.Semicolon
|
|
128
|
+
|| this.isBlockEnd();
|
|
129
|
+
}
|
|
130
|
+
/* ---------------------------------------------------------------- */
|
|
131
|
+
/* Program & statements */
|
|
132
|
+
/* ---------------------------------------------------------------- */
|
|
133
|
+
parseProgram() {
|
|
134
|
+
const start = this.peek();
|
|
135
|
+
const body = [];
|
|
136
|
+
this.skipSeparators();
|
|
137
|
+
while (!this.at(token_1.TokenType.EOF)) {
|
|
138
|
+
const before = this.pos;
|
|
139
|
+
body.push(this.parseStatement());
|
|
140
|
+
if (this.pos === before)
|
|
141
|
+
throw this.error('Unexpected token');
|
|
142
|
+
this.skipSeparators();
|
|
143
|
+
}
|
|
144
|
+
return { kind: 'Program', body, loc: this.loc(start) };
|
|
145
|
+
}
|
|
146
|
+
parseStatement() {
|
|
147
|
+
const doc = this.peek().doc;
|
|
148
|
+
const statement = this.parseStatementCore();
|
|
149
|
+
if (doc && DOC_STATEMENTS.has(statement.kind)) {
|
|
150
|
+
;
|
|
151
|
+
statement.doc = doc;
|
|
152
|
+
}
|
|
153
|
+
return statement;
|
|
154
|
+
}
|
|
155
|
+
parseStatementCore() {
|
|
156
|
+
const token = this.peek();
|
|
157
|
+
if (token.type === token_1.TokenType.Operator && token.value === '@') {
|
|
158
|
+
return this.parseDecoratedDeclaration();
|
|
159
|
+
}
|
|
160
|
+
if (token.type === token_1.TokenType.Keyword) {
|
|
161
|
+
switch (token.value) {
|
|
162
|
+
case 'global':
|
|
163
|
+
return this.parseVarDeclaration('global');
|
|
164
|
+
case 'const':
|
|
165
|
+
return this.parseVarDeclaration('const');
|
|
166
|
+
case 'import':
|
|
167
|
+
return this.parseImport();
|
|
168
|
+
case 'export':
|
|
169
|
+
return this.parseExport();
|
|
170
|
+
case 'type':
|
|
171
|
+
return this.parseTypeAlias();
|
|
172
|
+
case 'fn':
|
|
173
|
+
return this.parseFnDecl([], false, false);
|
|
174
|
+
case 'class':
|
|
175
|
+
return this.parseClassDecl([]);
|
|
176
|
+
case 'async':
|
|
177
|
+
if (this.peek(1).type === token_1.TokenType.Keyword && this.peek(1).value === 'fn') {
|
|
178
|
+
this.next();
|
|
179
|
+
return this.parseFnDecl([], true, false);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case 'throw':
|
|
183
|
+
return this.parseThrow();
|
|
184
|
+
case 'spawn':
|
|
185
|
+
return this.parseSpawn();
|
|
186
|
+
case 'if':
|
|
187
|
+
return this.parseIf();
|
|
188
|
+
case 'unless':
|
|
189
|
+
return this.parseUnless();
|
|
190
|
+
case 'while':
|
|
191
|
+
return this.parseWhile();
|
|
192
|
+
case 'repeat':
|
|
193
|
+
return this.parseRepeat();
|
|
194
|
+
case 'for':
|
|
195
|
+
return this.parseFor();
|
|
196
|
+
case 'return':
|
|
197
|
+
return this.parseReturn();
|
|
198
|
+
case 'break': {
|
|
199
|
+
const keyword = this.next();
|
|
200
|
+
return { kind: 'BreakStmt', loc: this.loc(keyword) };
|
|
201
|
+
}
|
|
202
|
+
case 'continue': {
|
|
203
|
+
const keyword = this.next();
|
|
204
|
+
return { kind: 'ContinueStmt', loc: this.loc(keyword) };
|
|
205
|
+
}
|
|
206
|
+
case 'match':
|
|
207
|
+
return this.parseMatch();
|
|
208
|
+
case 'try':
|
|
209
|
+
return this.parseTry();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return this.parseSimpleStatement();
|
|
213
|
+
}
|
|
214
|
+
parseBlock(terminators) {
|
|
215
|
+
const start = this.peek();
|
|
216
|
+
const body = [];
|
|
217
|
+
this.skipSeparators();
|
|
218
|
+
while (!this.at(token_1.TokenType.EOF)) {
|
|
219
|
+
const token = this.peek();
|
|
220
|
+
if (token.type === token_1.TokenType.Keyword && terminators.includes(token.value))
|
|
221
|
+
break;
|
|
222
|
+
const before = this.pos;
|
|
223
|
+
body.push(this.parseStatement());
|
|
224
|
+
if (this.pos === before)
|
|
225
|
+
throw this.error('Unexpected token');
|
|
226
|
+
this.skipSeparators();
|
|
227
|
+
}
|
|
228
|
+
return { kind: 'Block', body, loc: this.loc(start) };
|
|
229
|
+
}
|
|
230
|
+
parseVarDeclaration(scope) {
|
|
231
|
+
const keyword = this.next();
|
|
232
|
+
const names = [];
|
|
233
|
+
do {
|
|
234
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected variable name');
|
|
235
|
+
const entry = { name: name.value };
|
|
236
|
+
if (this.matchOperator(':'))
|
|
237
|
+
entry.type = this.parseType();
|
|
238
|
+
names.push(entry);
|
|
239
|
+
} while (this.matchOperator(','));
|
|
240
|
+
const values = this.matchOperator('=') ? this.parseExprList() : [];
|
|
241
|
+
return { kind: 'VarDecl', names, values, scope, loc: this.loc(keyword) };
|
|
242
|
+
}
|
|
243
|
+
parseSimpleStatement() {
|
|
244
|
+
const start = this.peek();
|
|
245
|
+
// Destructuring assignment: `{ x, y } = p` / `[a, b] = list`.
|
|
246
|
+
if (this.atOperator('{') || this.atOperator('[')) {
|
|
247
|
+
const saved = this.pos;
|
|
248
|
+
try {
|
|
249
|
+
const target = this.parsePattern();
|
|
250
|
+
if (this.matchOperator('=')) {
|
|
251
|
+
const values = this.parseExprList();
|
|
252
|
+
const node = { kind: 'Assign', targets: [target], values, loc: this.loc(start) };
|
|
253
|
+
return node;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Not a pattern; rewind and parse it as an expression instead.
|
|
258
|
+
}
|
|
259
|
+
this.pos = saved;
|
|
260
|
+
}
|
|
261
|
+
const first = this.parseExpression();
|
|
262
|
+
// Typed declaration without a keyword: x: number = 1
|
|
263
|
+
if (first.kind === 'Ident' && this.atOperator(':')) {
|
|
264
|
+
this.next();
|
|
265
|
+
const type = this.parseType();
|
|
266
|
+
const name = { name: first.name, type };
|
|
267
|
+
const values = this.matchOperator('=') ? this.parseExprList() : [];
|
|
268
|
+
return { kind: 'VarDecl', names: [name], values, scope: 'local', loc: first.loc };
|
|
269
|
+
}
|
|
270
|
+
const targets = [first];
|
|
271
|
+
while (this.matchOperator(','))
|
|
272
|
+
targets.push(this.parseExpression());
|
|
273
|
+
if (this.matchOperator('=')) {
|
|
274
|
+
const values = this.parseExprList();
|
|
275
|
+
// Assignment is declaration: plain identifiers create local bindings.
|
|
276
|
+
if (targets.every((target) => target.kind === 'Ident')) {
|
|
277
|
+
const names = targets.map((target) => ({ name: target.name }));
|
|
278
|
+
return { kind: 'VarDecl', names, values, scope: 'local', loc: this.loc(start) };
|
|
279
|
+
}
|
|
280
|
+
return { kind: 'Assign', targets, values, loc: this.loc(start) };
|
|
281
|
+
}
|
|
282
|
+
if (targets.length > 1)
|
|
283
|
+
throw this.error('Expected "=" in assignment');
|
|
284
|
+
return { kind: 'ExprStmt', expr: first, loc: this.loc(start) };
|
|
285
|
+
}
|
|
286
|
+
parsePattern() {
|
|
287
|
+
if (this.atOperator('{'))
|
|
288
|
+
return this.parseTablePattern();
|
|
289
|
+
if (this.atOperator('['))
|
|
290
|
+
return this.parseArrayPattern();
|
|
291
|
+
throw this.error('Expected destructuring pattern');
|
|
292
|
+
}
|
|
293
|
+
parseTablePattern() {
|
|
294
|
+
const start = this.expect(token_1.TokenType.Operator, '{');
|
|
295
|
+
const entries = [];
|
|
296
|
+
this.skipNewlines();
|
|
297
|
+
while (!this.atOperator('}')) {
|
|
298
|
+
const key = this.expectName('Expected field name in table pattern');
|
|
299
|
+
const entry = { key: key.value };
|
|
300
|
+
if (this.matchOperator('=')) {
|
|
301
|
+
const value = this.parseExpression();
|
|
302
|
+
// `{ a = first }` renames, any other value is a default.
|
|
303
|
+
if (value.kind === 'Ident')
|
|
304
|
+
entry.target = value.name;
|
|
305
|
+
else
|
|
306
|
+
entry.default = value;
|
|
307
|
+
}
|
|
308
|
+
entries.push(entry);
|
|
309
|
+
this.skipNewlines();
|
|
310
|
+
if (!this.matchOperator(','))
|
|
311
|
+
break;
|
|
312
|
+
this.skipNewlines();
|
|
313
|
+
}
|
|
314
|
+
this.expect(token_1.TokenType.Operator, '}', 'Expected "}"');
|
|
315
|
+
return { kind: 'TablePattern', entries, loc: this.loc(start) };
|
|
316
|
+
}
|
|
317
|
+
parseArrayPattern() {
|
|
318
|
+
const start = this.expect(token_1.TokenType.Operator, '[');
|
|
319
|
+
const elements = [];
|
|
320
|
+
this.skipNewlines();
|
|
321
|
+
while (!this.atOperator(']')) {
|
|
322
|
+
const name = this.expectName('Expected name in array pattern');
|
|
323
|
+
elements.push(name.value);
|
|
324
|
+
this.skipNewlines();
|
|
325
|
+
if (!this.matchOperator(','))
|
|
326
|
+
break;
|
|
327
|
+
this.skipNewlines();
|
|
328
|
+
}
|
|
329
|
+
this.expect(token_1.TokenType.Operator, ']', 'Expected "]"');
|
|
330
|
+
return { kind: 'ArrayPattern', elements, loc: this.loc(start) };
|
|
331
|
+
}
|
|
332
|
+
parseFnDecl(decorators, isAsync, isMethod) {
|
|
333
|
+
const start = this.expectKeyword('fn');
|
|
334
|
+
let name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected function name').value;
|
|
335
|
+
let receiver;
|
|
336
|
+
if (this.matchOperator(':')) {
|
|
337
|
+
receiver = name;
|
|
338
|
+
name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected method name').value;
|
|
339
|
+
}
|
|
340
|
+
const params = this.parseParams();
|
|
341
|
+
const returnType = this.matchOperator(':') ? this.parseType() : undefined;
|
|
342
|
+
let body;
|
|
343
|
+
let exprBody;
|
|
344
|
+
if (this.matchOperator('=')) {
|
|
345
|
+
exprBody = this.parseExpression();
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
body = this.parseBlock(['end']);
|
|
349
|
+
this.expectKeyword('end');
|
|
350
|
+
}
|
|
351
|
+
const decl = {
|
|
352
|
+
kind: 'FnDecl',
|
|
353
|
+
name,
|
|
354
|
+
params,
|
|
355
|
+
returnType,
|
|
356
|
+
body,
|
|
357
|
+
exprBody,
|
|
358
|
+
isMethod: isMethod || !!receiver,
|
|
359
|
+
decorators,
|
|
360
|
+
loc: this.loc(start),
|
|
361
|
+
};
|
|
362
|
+
if (receiver)
|
|
363
|
+
decl.receiver = receiver;
|
|
364
|
+
if (isAsync)
|
|
365
|
+
decl.async = true;
|
|
366
|
+
return decl;
|
|
367
|
+
}
|
|
368
|
+
parseParams() {
|
|
369
|
+
this.expect(token_1.TokenType.Operator, '(', 'Expected "("');
|
|
370
|
+
const params = [];
|
|
371
|
+
this.skipNewlines();
|
|
372
|
+
while (!this.atOperator(')')) {
|
|
373
|
+
if (this.matchOperator('...')) {
|
|
374
|
+
params.push({ name: '...', rest: true });
|
|
375
|
+
this.skipNewlines();
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected parameter name');
|
|
379
|
+
const param = { name: name.value };
|
|
380
|
+
if (this.matchOperator(':'))
|
|
381
|
+
param.type = this.parseType();
|
|
382
|
+
if (this.matchOperator('='))
|
|
383
|
+
param.default = this.parseExpression();
|
|
384
|
+
params.push(param);
|
|
385
|
+
this.skipNewlines();
|
|
386
|
+
if (!this.matchOperator(','))
|
|
387
|
+
break;
|
|
388
|
+
this.skipNewlines();
|
|
389
|
+
}
|
|
390
|
+
this.expect(token_1.TokenType.Operator, ')', 'Expected ")"');
|
|
391
|
+
return params;
|
|
392
|
+
}
|
|
393
|
+
parseDecorators() {
|
|
394
|
+
const decorators = [];
|
|
395
|
+
while (this.atOperator('@')) {
|
|
396
|
+
const at = this.next();
|
|
397
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected decorator name').value;
|
|
398
|
+
const args = this.atOperator('(') ? this.parseArguments() : [];
|
|
399
|
+
decorators.push({ kind: 'Decorator', name, args, loc: this.loc(at) });
|
|
400
|
+
this.skipNewlines();
|
|
401
|
+
}
|
|
402
|
+
return decorators;
|
|
403
|
+
}
|
|
404
|
+
parseDecoratedDeclaration() {
|
|
405
|
+
const decorators = this.parseDecorators();
|
|
406
|
+
if (this.atKeyword('class'))
|
|
407
|
+
return this.parseClassDecl(decorators);
|
|
408
|
+
if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
409
|
+
this.next();
|
|
410
|
+
return this.parseFnDecl(decorators, true, false);
|
|
411
|
+
}
|
|
412
|
+
if (this.atKeyword('fn'))
|
|
413
|
+
return this.parseFnDecl(decorators, false, false);
|
|
414
|
+
throw this.error('Expected "fn" or "class" after decorator');
|
|
415
|
+
}
|
|
416
|
+
parseClassDecl(decorators) {
|
|
417
|
+
const start = this.expectKeyword('class');
|
|
418
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected class name').value;
|
|
419
|
+
const superClass = this.matchKeyword('extends')
|
|
420
|
+
? this.expect(token_1.TokenType.Identifier, undefined, 'Expected super class name').value
|
|
421
|
+
: undefined;
|
|
422
|
+
const members = [];
|
|
423
|
+
this.skipSeparators();
|
|
424
|
+
while (!this.atKeyword('end') && !this.at(token_1.TokenType.EOF)) {
|
|
425
|
+
const doc = this.peek().doc;
|
|
426
|
+
let member;
|
|
427
|
+
if (this.atOperator('@')) {
|
|
428
|
+
member = this.parseClassMember(this.parseDecorators());
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
member = this.parseClassMember([]);
|
|
432
|
+
}
|
|
433
|
+
if (doc)
|
|
434
|
+
member.doc = doc;
|
|
435
|
+
members.push(member);
|
|
436
|
+
this.skipSeparators();
|
|
437
|
+
}
|
|
438
|
+
this.expectKeyword('end');
|
|
439
|
+
return { kind: 'ClassDecl', name, superClass, members, decorators, loc: this.loc(start) };
|
|
440
|
+
}
|
|
441
|
+
/** Parses one class body member, with decorators already consumed. */
|
|
442
|
+
parseClassMember(decorators) {
|
|
443
|
+
if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
444
|
+
this.next();
|
|
445
|
+
return this.parseFnDecl(decorators, true, true);
|
|
446
|
+
}
|
|
447
|
+
if (this.atKeyword('fn'))
|
|
448
|
+
return this.parseFnDecl(decorators, false, true);
|
|
449
|
+
return this.parseField(decorators);
|
|
450
|
+
}
|
|
451
|
+
parseField(decorators = []) {
|
|
452
|
+
const name = this.expectName('Expected field name');
|
|
453
|
+
const entry = { name: name.value };
|
|
454
|
+
if (this.matchOperator(':'))
|
|
455
|
+
entry.type = this.parseType();
|
|
456
|
+
const values = this.matchOperator('=') ? [this.parseExpression()] : [];
|
|
457
|
+
const decl = { kind: 'VarDecl', names: [entry], values, scope: 'local', loc: this.loc(name) };
|
|
458
|
+
if (decorators.length)
|
|
459
|
+
decl.decorators = decorators;
|
|
460
|
+
return decl;
|
|
461
|
+
}
|
|
462
|
+
parseIf() {
|
|
463
|
+
const start = this.expectKeyword('if');
|
|
464
|
+
const cond = this.parseExpression();
|
|
465
|
+
this.expectKeyword('then');
|
|
466
|
+
const then = this.parseBlock(['elseif', 'else', 'end']);
|
|
467
|
+
const elifs = [];
|
|
468
|
+
while (this.matchKeyword('elseif')) {
|
|
469
|
+
const elifCond = this.parseExpression();
|
|
470
|
+
this.expectKeyword('then');
|
|
471
|
+
const body = this.parseBlock(['elseif', 'else', 'end']);
|
|
472
|
+
elifs.push({ cond: elifCond, body });
|
|
473
|
+
}
|
|
474
|
+
const elseBody = this.matchKeyword('else') ? this.parseBlock(['end']) : undefined;
|
|
475
|
+
this.expectKeyword('end');
|
|
476
|
+
return { kind: 'IfStmt', cond, then, elifs, else: elseBody, loc: this.loc(start) };
|
|
477
|
+
}
|
|
478
|
+
parseUnless() {
|
|
479
|
+
const start = this.expectKeyword('unless');
|
|
480
|
+
const cond = this.parseExpression();
|
|
481
|
+
this.expectKeyword('do');
|
|
482
|
+
const body = this.parseBlock(['end']);
|
|
483
|
+
this.expectKeyword('end');
|
|
484
|
+
return { kind: 'UnlessStmt', cond, body, loc: this.loc(start) };
|
|
485
|
+
}
|
|
486
|
+
parseWhile() {
|
|
487
|
+
const start = this.expectKeyword('while');
|
|
488
|
+
const cond = this.parseExpression();
|
|
489
|
+
this.expectKeyword('do');
|
|
490
|
+
const body = this.parseBlock(['end']);
|
|
491
|
+
this.expectKeyword('end');
|
|
492
|
+
return { kind: 'WhileStmt', cond, body, loc: this.loc(start) };
|
|
493
|
+
}
|
|
494
|
+
parseRepeat() {
|
|
495
|
+
const start = this.expectKeyword('repeat');
|
|
496
|
+
const body = this.parseBlock(['until']);
|
|
497
|
+
this.expectKeyword('until');
|
|
498
|
+
const cond = this.parseExpression();
|
|
499
|
+
return { kind: 'RepeatStmt', body, cond, loc: this.loc(start) };
|
|
500
|
+
}
|
|
501
|
+
parseFor() {
|
|
502
|
+
const start = this.expectKeyword('for');
|
|
503
|
+
const first = this.expect(token_1.TokenType.Identifier, undefined, 'Expected loop variable');
|
|
504
|
+
const vars = [first.value];
|
|
505
|
+
while (this.atOperator(',')) {
|
|
506
|
+
this.next();
|
|
507
|
+
vars.push(this.expect(token_1.TokenType.Identifier, undefined, 'Expected loop variable').value);
|
|
508
|
+
}
|
|
509
|
+
if (this.matchOperator('=')) {
|
|
510
|
+
if (vars.length > 1)
|
|
511
|
+
throw this.error('Numeric for accepts a single variable');
|
|
512
|
+
const from = this.parseExpression();
|
|
513
|
+
this.expect(token_1.TokenType.Operator, ',', 'Expected "," in numeric for');
|
|
514
|
+
const to = this.parseExpression();
|
|
515
|
+
const step = this.matchOperator(',') ? this.parseExpression() : undefined;
|
|
516
|
+
this.expectKeyword('do');
|
|
517
|
+
const body = this.parseBlock(['end']);
|
|
518
|
+
this.expectKeyword('end');
|
|
519
|
+
const node = {
|
|
520
|
+
kind: 'ForNumeric',
|
|
521
|
+
varName: first.value,
|
|
522
|
+
from,
|
|
523
|
+
to,
|
|
524
|
+
step,
|
|
525
|
+
inclusive: true,
|
|
526
|
+
body,
|
|
527
|
+
loc: this.loc(start),
|
|
528
|
+
};
|
|
529
|
+
return node;
|
|
530
|
+
}
|
|
531
|
+
this.expectKeyword('in');
|
|
532
|
+
const iter = this.parseRangeExpr();
|
|
533
|
+
this.expectKeyword('do');
|
|
534
|
+
const body = this.parseBlock(['end']);
|
|
535
|
+
this.expectKeyword('end');
|
|
536
|
+
const node = { kind: 'ForIn', vars, iter, body, loc: this.loc(start) };
|
|
537
|
+
return node;
|
|
538
|
+
}
|
|
539
|
+
parseReturn() {
|
|
540
|
+
const start = this.expectKeyword('return');
|
|
541
|
+
const values = this.isStatementEnd() ? [] : this.parseExprList();
|
|
542
|
+
return { kind: 'ReturnStmt', values, loc: this.loc(start) };
|
|
543
|
+
}
|
|
544
|
+
parseThrow() {
|
|
545
|
+
const start = this.expectKeyword('throw');
|
|
546
|
+
if (this.isStatementEnd())
|
|
547
|
+
throw this.error('Expected expression after "throw"');
|
|
548
|
+
return { kind: 'ThrowStmt', value: this.parseExpression(), loc: this.loc(start) };
|
|
549
|
+
}
|
|
550
|
+
parseSpawn() {
|
|
551
|
+
const start = this.expectKeyword('spawn');
|
|
552
|
+
if (this.isStatementEnd())
|
|
553
|
+
throw this.error('Expected expression after "spawn"');
|
|
554
|
+
return { kind: 'SpawnStmt', value: this.parseExpression(), loc: this.loc(start) };
|
|
555
|
+
}
|
|
556
|
+
parseMatch() {
|
|
557
|
+
const start = this.expectKeyword('match');
|
|
558
|
+
const subject = this.parseExpression();
|
|
559
|
+
this.skipSeparators();
|
|
560
|
+
const arms = [];
|
|
561
|
+
while (this.atKeyword('when')) {
|
|
562
|
+
const when = this.next();
|
|
563
|
+
const patterns = this.parseExprList();
|
|
564
|
+
const guard = this.matchKeyword('if') ? this.parseExpression() : undefined;
|
|
565
|
+
this.expectKeyword('then');
|
|
566
|
+
const body = this.parseBlock(['when', 'else', 'end']);
|
|
567
|
+
arms.push({ patterns, guard, body, loc: this.loc(when) });
|
|
568
|
+
this.skipSeparators();
|
|
569
|
+
}
|
|
570
|
+
const elseBody = this.matchKeyword('else') ? this.parseBlock(['end']) : undefined;
|
|
571
|
+
this.expectKeyword('end');
|
|
572
|
+
return { kind: 'MatchStmt', subject, arms, elseBody, loc: this.loc(start) };
|
|
573
|
+
}
|
|
574
|
+
parseTry() {
|
|
575
|
+
const start = this.expectKeyword('try');
|
|
576
|
+
const body = this.parseBlock(['catch', 'finally', 'end']);
|
|
577
|
+
let catchVar;
|
|
578
|
+
let catchType;
|
|
579
|
+
let catchBody;
|
|
580
|
+
if (this.matchKeyword('catch')) {
|
|
581
|
+
if (this.at(token_1.TokenType.Identifier)) {
|
|
582
|
+
catchVar = this.next().value;
|
|
583
|
+
if (this.matchOperator(':'))
|
|
584
|
+
catchType = this.parseType();
|
|
585
|
+
}
|
|
586
|
+
catchBody = this.parseBlock(['finally', 'end']);
|
|
587
|
+
}
|
|
588
|
+
const finallyBody = this.matchKeyword('finally') ? this.parseBlock(['end']) : undefined;
|
|
589
|
+
this.expectKeyword('end');
|
|
590
|
+
return { kind: 'TryStmt', body, catchVar, catchType, catchBody, finallyBody, loc: this.loc(start) };
|
|
591
|
+
}
|
|
592
|
+
parseImport() {
|
|
593
|
+
const start = this.expectKeyword('import');
|
|
594
|
+
const clauses = [];
|
|
595
|
+
let source;
|
|
596
|
+
if (this.at(token_1.TokenType.String)) {
|
|
597
|
+
source = this.next().value;
|
|
598
|
+
}
|
|
599
|
+
else if (this.matchOperator('{')) {
|
|
600
|
+
this.skipNewlines();
|
|
601
|
+
while (!this.atOperator('}')) {
|
|
602
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected import name');
|
|
603
|
+
const clause = { name: name.value };
|
|
604
|
+
if (this.matchKeyword('as')) {
|
|
605
|
+
clause.alias = this.expect(token_1.TokenType.Identifier, undefined, 'Expected alias').value;
|
|
606
|
+
}
|
|
607
|
+
clauses.push(clause);
|
|
608
|
+
this.skipNewlines();
|
|
609
|
+
if (!this.matchOperator(','))
|
|
610
|
+
break;
|
|
611
|
+
this.skipNewlines();
|
|
612
|
+
}
|
|
613
|
+
this.expect(token_1.TokenType.Operator, '}', 'Expected "}"');
|
|
614
|
+
this.expectKeyword('from');
|
|
615
|
+
source = this.expect(token_1.TokenType.String, undefined, 'Expected module path').value;
|
|
616
|
+
}
|
|
617
|
+
else if (this.matchOperator('*')) {
|
|
618
|
+
this.expectKeyword('as');
|
|
619
|
+
const alias = this.expect(token_1.TokenType.Identifier, undefined, 'Expected namespace alias');
|
|
620
|
+
clauses.push({ name: '*', alias: alias.value });
|
|
621
|
+
this.expectKeyword('from');
|
|
622
|
+
source = this.expect(token_1.TokenType.String, undefined, 'Expected module path').value;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected import name');
|
|
626
|
+
const clause = { name: name.value };
|
|
627
|
+
if (this.matchKeyword('as')) {
|
|
628
|
+
clause.alias = this.expect(token_1.TokenType.Identifier, undefined, 'Expected alias').value;
|
|
629
|
+
}
|
|
630
|
+
clauses.push(clause);
|
|
631
|
+
this.expectKeyword('from');
|
|
632
|
+
source = this.expect(token_1.TokenType.String, undefined, 'Expected module path').value;
|
|
633
|
+
}
|
|
634
|
+
return { kind: 'ImportDecl', clauses, source, loc: this.loc(start) };
|
|
635
|
+
}
|
|
636
|
+
parseExport() {
|
|
637
|
+
const start = this.expectKeyword('export');
|
|
638
|
+
if (this.matchKeyword('default')) {
|
|
639
|
+
const expr = this.parseExpression();
|
|
640
|
+
const node = { kind: 'ExportDefault', expr, loc: this.loc(start) };
|
|
641
|
+
return node;
|
|
642
|
+
}
|
|
643
|
+
let declaration;
|
|
644
|
+
if (this.atKeyword('const'))
|
|
645
|
+
declaration = this.parseVarDeclaration('const');
|
|
646
|
+
else if (this.atKeyword('global'))
|
|
647
|
+
declaration = this.parseVarDeclaration('global');
|
|
648
|
+
else if (this.atKeyword('fn'))
|
|
649
|
+
declaration = this.parseFnDecl([], false, false);
|
|
650
|
+
else if (this.atKeyword('class'))
|
|
651
|
+
declaration = this.parseClassDecl([]);
|
|
652
|
+
else if (this.atKeyword('type'))
|
|
653
|
+
declaration = this.parseTypeAlias();
|
|
654
|
+
else if (this.atKeyword('async') && this.peek(1).value === 'fn') {
|
|
655
|
+
this.next();
|
|
656
|
+
declaration = this.parseFnDecl([], true, false);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
throw this.error('Expected declaration after "export"');
|
|
660
|
+
}
|
|
661
|
+
if (declaration.kind === 'VarDecl')
|
|
662
|
+
declaration.export = true;
|
|
663
|
+
const node = { kind: 'ExportDecl', declaration, loc: this.loc(start) };
|
|
664
|
+
return node;
|
|
665
|
+
}
|
|
666
|
+
parseTypeAlias() {
|
|
667
|
+
const start = this.expectKeyword('type');
|
|
668
|
+
const name = this.expect(token_1.TokenType.Identifier, undefined, 'Expected type name').value;
|
|
669
|
+
this.expect(token_1.TokenType.Operator, '=', 'Expected "=" in type alias');
|
|
670
|
+
const type = this.parseType();
|
|
671
|
+
return { kind: 'TypeAliasDecl', name, type, loc: this.loc(start) };
|
|
672
|
+
}
|
|
673
|
+
/* ---------------------------------------------------------------- */
|
|
674
|
+
/* Types */
|
|
675
|
+
/* ---------------------------------------------------------------- */
|
|
676
|
+
parseType() {
|
|
677
|
+
const start = this.peek();
|
|
678
|
+
const types = [this.parseTypePostfix()];
|
|
679
|
+
while (this.matchOperator('|'))
|
|
680
|
+
types.push(this.parseTypePostfix());
|
|
681
|
+
if (types.length === 1)
|
|
682
|
+
return types[0];
|
|
683
|
+
return { kind: 'union', types, loc: this.loc(start) };
|
|
684
|
+
}
|
|
685
|
+
parseTypePostfix() {
|
|
686
|
+
let type = this.parseTypePrimary();
|
|
687
|
+
while (this.matchOperator('?')) {
|
|
688
|
+
type = { kind: 'optional', type, inner: type, loc: type.loc };
|
|
689
|
+
}
|
|
690
|
+
return type;
|
|
691
|
+
}
|
|
692
|
+
parseTypePrimary() {
|
|
693
|
+
const token = this.peek();
|
|
694
|
+
if (this.matchOperator('(')) {
|
|
695
|
+
const params = [];
|
|
696
|
+
this.skipNewlines();
|
|
697
|
+
while (!this.atOperator(')')) {
|
|
698
|
+
params.push(this.parseType());
|
|
699
|
+
this.skipNewlines();
|
|
700
|
+
if (!this.matchOperator(','))
|
|
701
|
+
break;
|
|
702
|
+
this.skipNewlines();
|
|
703
|
+
}
|
|
704
|
+
this.expect(token_1.TokenType.Operator, ')', 'Expected ")"');
|
|
705
|
+
this.expect(token_1.TokenType.Operator, '->', 'Expected "->" in function type');
|
|
706
|
+
const returns = this.parseType();
|
|
707
|
+
return { kind: 'fn', params, returns, loc: this.loc(token) };
|
|
708
|
+
}
|
|
709
|
+
if (this.matchOperator('[')) {
|
|
710
|
+
const element = this.parseType();
|
|
711
|
+
this.expect(token_1.TokenType.Operator, ']', 'Expected "]"');
|
|
712
|
+
return { kind: 'array', element, loc: this.loc(token) };
|
|
713
|
+
}
|
|
714
|
+
if (this.matchOperator('{')) {
|
|
715
|
+
this.skipNewlines();
|
|
716
|
+
const entries = [];
|
|
717
|
+
while (!this.atOperator('}')) {
|
|
718
|
+
const member = this.expect(token_1.TokenType.Identifier, undefined, 'Expected type member name');
|
|
719
|
+
this.expect(token_1.TokenType.Operator, ':', 'Expected ":" in type');
|
|
720
|
+
const type = this.parseType();
|
|
721
|
+
entries.push({ name: member.value, type });
|
|
722
|
+
this.skipNewlines();
|
|
723
|
+
if (!this.matchOperator(','))
|
|
724
|
+
break;
|
|
725
|
+
this.skipNewlines();
|
|
726
|
+
}
|
|
727
|
+
this.expect(token_1.TokenType.Operator, '}', 'Expected "}"');
|
|
728
|
+
if (entries.length === 1 && PRIMITIVE_TYPE_NAMES.has(entries[0].name)) {
|
|
729
|
+
return {
|
|
730
|
+
kind: 'dict',
|
|
731
|
+
key: { kind: 'named', name: entries[0].name, loc: this.loc(token) },
|
|
732
|
+
value: entries[0].type,
|
|
733
|
+
loc: this.loc(token),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return { kind: 'record', fields: entries, loc: this.loc(token) };
|
|
737
|
+
}
|
|
738
|
+
if (token.type === token_1.TokenType.Identifier || this.atKeyword('nil')) {
|
|
739
|
+
this.next();
|
|
740
|
+
return { kind: 'named', name: token.value, loc: this.loc(token) };
|
|
741
|
+
}
|
|
742
|
+
throw this.error('Expected type annotation');
|
|
743
|
+
}
|
|
744
|
+
/* ---------------------------------------------------------------- */
|
|
745
|
+
/* Expressions */
|
|
746
|
+
/* ---------------------------------------------------------------- */
|
|
747
|
+
parseExprList() {
|
|
748
|
+
const list = [this.parseExpression()];
|
|
749
|
+
while (this.matchOperator(','))
|
|
750
|
+
list.push(this.parseExpression());
|
|
751
|
+
return list;
|
|
752
|
+
}
|
|
753
|
+
parseExpression(minPrecedence = 1) {
|
|
754
|
+
let left = this.parseUnary();
|
|
755
|
+
while (true) {
|
|
756
|
+
const token = this.peek();
|
|
757
|
+
const op = token.value;
|
|
758
|
+
const precedence = BINARY_PRECEDENCE[op];
|
|
759
|
+
if (precedence === undefined || precedence < minPrecedence)
|
|
760
|
+
break;
|
|
761
|
+
const isOperator = token.type === token_1.TokenType.Operator
|
|
762
|
+
|| (token.type === token_1.TokenType.Keyword && (op === 'and' || op === 'or'));
|
|
763
|
+
if (!isOperator)
|
|
764
|
+
break;
|
|
765
|
+
this.next();
|
|
766
|
+
const right = this.parseExpression(op === '^' ? precedence : precedence + 1);
|
|
767
|
+
const binary = { kind: 'Binary', op, left, right, loc: left.loc };
|
|
768
|
+
left = binary;
|
|
769
|
+
}
|
|
770
|
+
return left;
|
|
771
|
+
}
|
|
772
|
+
parseUnary() {
|
|
773
|
+
const token = this.peek();
|
|
774
|
+
if (token.type === token_1.TokenType.Keyword && token.value === 'not') {
|
|
775
|
+
this.next();
|
|
776
|
+
return {
|
|
777
|
+
kind: 'Unary',
|
|
778
|
+
op: 'not',
|
|
779
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
780
|
+
loc: this.loc(token),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (token.type === token_1.TokenType.Keyword && token.value === 'await') {
|
|
784
|
+
this.next();
|
|
785
|
+
const node = {
|
|
786
|
+
kind: 'AwaitExpr',
|
|
787
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
788
|
+
loc: this.loc(token),
|
|
789
|
+
};
|
|
790
|
+
return node;
|
|
791
|
+
}
|
|
792
|
+
if (token.type === token_1.TokenType.Operator && (token.value === '-' || token.value === '#')) {
|
|
793
|
+
this.next();
|
|
794
|
+
return {
|
|
795
|
+
kind: 'Unary',
|
|
796
|
+
op: token.value,
|
|
797
|
+
argument: this.parseExpression(UNARY_PRECEDENCE),
|
|
798
|
+
loc: this.loc(token),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
return this.parsePostfix();
|
|
802
|
+
}
|
|
803
|
+
parsePostfix() {
|
|
804
|
+
let expr = this.parsePrimary();
|
|
805
|
+
while (true) {
|
|
806
|
+
const token = this.peek();
|
|
807
|
+
if (token.type === token_1.TokenType.Operator && token.value === '(') {
|
|
808
|
+
const args = this.parseArguments();
|
|
809
|
+
const call = { kind: 'Call', callee: expr, args, loc: expr.loc };
|
|
810
|
+
expr = call;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (token.type === token_1.TokenType.Operator && (token.value === '.' || token.value === '?.')) {
|
|
814
|
+
this.next();
|
|
815
|
+
const property = this.expectName('Expected property name');
|
|
816
|
+
const member = {
|
|
817
|
+
kind: 'Member',
|
|
818
|
+
object: expr,
|
|
819
|
+
property: property.value,
|
|
820
|
+
loc: expr.loc,
|
|
821
|
+
};
|
|
822
|
+
if (token.value === '?.')
|
|
823
|
+
member.optional = true;
|
|
824
|
+
expr = member;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (token.type === token_1.TokenType.Operator && token.value === '[') {
|
|
828
|
+
this.next();
|
|
829
|
+
this.skipNewlines();
|
|
830
|
+
const index = this.parseRangeExpr();
|
|
831
|
+
this.skipNewlines();
|
|
832
|
+
this.expect(token_1.TokenType.Operator, ']', 'Expected "]"');
|
|
833
|
+
const node = { kind: 'Index', object: expr, index, loc: expr.loc };
|
|
834
|
+
if (index.kind === 'Range')
|
|
835
|
+
node.isRange = true;
|
|
836
|
+
expr = node;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
// Method call: a:b(args). A bare `:` is a type annotation instead.
|
|
840
|
+
if (token.type === token_1.TokenType.Operator && token.value === ':'
|
|
841
|
+
&& isNameToken(this.peek(1))
|
|
842
|
+
&& this.peek(2).type === token_1.TokenType.Operator && this.peek(2).value === '(') {
|
|
843
|
+
this.next();
|
|
844
|
+
const property = this.next();
|
|
845
|
+
const member = {
|
|
846
|
+
kind: 'Member',
|
|
847
|
+
object: expr,
|
|
848
|
+
property: property.value,
|
|
849
|
+
loc: expr.loc,
|
|
850
|
+
};
|
|
851
|
+
const args = this.parseArguments();
|
|
852
|
+
const call = { kind: 'Call', callee: member, args, isMethod: true, loc: expr.loc };
|
|
853
|
+
expr = call;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
return expr;
|
|
859
|
+
}
|
|
860
|
+
parseArguments() {
|
|
861
|
+
this.expect(token_1.TokenType.Operator, '(', 'Expected "("');
|
|
862
|
+
const args = [];
|
|
863
|
+
this.skipNewlines();
|
|
864
|
+
while (!this.atOperator(')')) {
|
|
865
|
+
if (this.atOperator('...')) {
|
|
866
|
+
const spreadToken = this.next();
|
|
867
|
+
const spread = { kind: 'Spread', loc: this.loc(spreadToken) };
|
|
868
|
+
if (!this.atOperator(')') && !this.atOperator(','))
|
|
869
|
+
spread.argument = this.parseExpression();
|
|
870
|
+
args.push(spread);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
args.push(this.parseExpression());
|
|
874
|
+
}
|
|
875
|
+
this.skipNewlines();
|
|
876
|
+
if (!this.matchOperator(','))
|
|
877
|
+
break;
|
|
878
|
+
this.skipNewlines();
|
|
879
|
+
}
|
|
880
|
+
this.expect(token_1.TokenType.Operator, ')', 'Expected ")"');
|
|
881
|
+
return args;
|
|
882
|
+
}
|
|
883
|
+
parsePrimary() {
|
|
884
|
+
const token = this.peek();
|
|
885
|
+
switch (token.type) {
|
|
886
|
+
case token_1.TokenType.Number: {
|
|
887
|
+
this.next();
|
|
888
|
+
return {
|
|
889
|
+
kind: 'NumberLit',
|
|
890
|
+
value: Number(token.value.replace(/_/g, '')),
|
|
891
|
+
loc: this.loc(token),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
case token_1.TokenType.String: {
|
|
895
|
+
this.next();
|
|
896
|
+
const literal = { kind: 'StringLit', value: token.value, loc: this.loc(token) };
|
|
897
|
+
if (token.value.includes('\n'))
|
|
898
|
+
literal.multiline = true;
|
|
899
|
+
return literal;
|
|
900
|
+
}
|
|
901
|
+
case token_1.TokenType.FString: {
|
|
902
|
+
this.next();
|
|
903
|
+
const literal = {
|
|
904
|
+
kind: 'StringLit',
|
|
905
|
+
value: token.value,
|
|
906
|
+
fstring: true,
|
|
907
|
+
parts: parseFStringParts(token),
|
|
908
|
+
loc: this.loc(token),
|
|
909
|
+
};
|
|
910
|
+
if (token.value.includes('\n'))
|
|
911
|
+
literal.multiline = true;
|
|
912
|
+
return literal;
|
|
913
|
+
}
|
|
914
|
+
case token_1.TokenType.Identifier: {
|
|
915
|
+
this.next();
|
|
916
|
+
const ident = { kind: 'Ident', name: token.value, loc: this.loc(token) };
|
|
917
|
+
return ident;
|
|
918
|
+
}
|
|
919
|
+
case token_1.TokenType.Keyword:
|
|
920
|
+
return this.parseKeywordPrimary(token);
|
|
921
|
+
case token_1.TokenType.Operator:
|
|
922
|
+
if (token.value === '{')
|
|
923
|
+
return this.parseTable();
|
|
924
|
+
if (token.value === '(') {
|
|
925
|
+
this.next();
|
|
926
|
+
const expr = this.parseExpression();
|
|
927
|
+
this.expect(token_1.TokenType.Operator, ')', 'Expected ")"');
|
|
928
|
+
return expr;
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
throw this.error(`Unexpected token "${token.value}"`);
|
|
933
|
+
}
|
|
934
|
+
parseKeywordPrimary(token) {
|
|
935
|
+
switch (token.value) {
|
|
936
|
+
case 'true':
|
|
937
|
+
case 'false':
|
|
938
|
+
this.next();
|
|
939
|
+
return { kind: 'BoolLit', value: token.value === 'true', loc: this.loc(token) };
|
|
940
|
+
case 'nil':
|
|
941
|
+
this.next();
|
|
942
|
+
return { kind: 'NilLit', loc: this.loc(token) };
|
|
943
|
+
case 'self':
|
|
944
|
+
this.next();
|
|
945
|
+
return { kind: 'SelfExpr', loc: this.loc(token) };
|
|
946
|
+
case 'fn':
|
|
947
|
+
return this.parseFunctionExpr(false);
|
|
948
|
+
case 'async':
|
|
949
|
+
if (this.peek(1).type === token_1.TokenType.Keyword && this.peek(1).value === 'fn') {
|
|
950
|
+
this.next();
|
|
951
|
+
return this.parseFunctionExpr(true);
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
throw this.error(`Unexpected keyword "${token.value}"`);
|
|
956
|
+
}
|
|
957
|
+
parseFunctionExpr(isAsync) {
|
|
958
|
+
const start = this.expectKeyword('fn');
|
|
959
|
+
const params = this.parseParams();
|
|
960
|
+
const returnType = this.matchOperator(':') ? this.parseType() : undefined;
|
|
961
|
+
let body;
|
|
962
|
+
let exprBody;
|
|
963
|
+
if (this.matchOperator('->')) {
|
|
964
|
+
exprBody = this.parseExpression();
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
body = this.parseBlock(['end']);
|
|
968
|
+
this.expectKeyword('end');
|
|
969
|
+
}
|
|
970
|
+
const node = {
|
|
971
|
+
kind: 'FunctionExpr',
|
|
972
|
+
params,
|
|
973
|
+
returnType,
|
|
974
|
+
body,
|
|
975
|
+
exprBody,
|
|
976
|
+
loc: this.loc(start),
|
|
977
|
+
};
|
|
978
|
+
if (isAsync)
|
|
979
|
+
node.async = true;
|
|
980
|
+
return node;
|
|
981
|
+
}
|
|
982
|
+
parseTable() {
|
|
983
|
+
const start = this.expect(token_1.TokenType.Operator, '{');
|
|
984
|
+
const fields = [];
|
|
985
|
+
while (true) {
|
|
986
|
+
this.skipNewlines();
|
|
987
|
+
if (this.atOperator('}'))
|
|
988
|
+
break;
|
|
989
|
+
if (this.matchOperator('[')) {
|
|
990
|
+
const key = this.parseExpression();
|
|
991
|
+
this.expect(token_1.TokenType.Operator, ']', 'Expected "]"');
|
|
992
|
+
this.expect(token_1.TokenType.Operator, '=', 'Expected "="');
|
|
993
|
+
const value = this.parseExpression();
|
|
994
|
+
fields.push({ key, value });
|
|
995
|
+
}
|
|
996
|
+
else if (isNameToken(this.peek())
|
|
997
|
+
&& this.peek(1).type === token_1.TokenType.Operator
|
|
998
|
+
&& this.peek(1).value === '=') {
|
|
999
|
+
const key = this.next();
|
|
1000
|
+
this.next();
|
|
1001
|
+
const value = this.parseExpression();
|
|
1002
|
+
const keyNode = { kind: 'Ident', name: key.value, loc: this.loc(key) };
|
|
1003
|
+
fields.push({ key: keyNode, value });
|
|
1004
|
+
}
|
|
1005
|
+
else if (this.peek().type === token_1.TokenType.Identifier && this.isTableFieldEnd(this.peek(1))) {
|
|
1006
|
+
const shorthand = this.next();
|
|
1007
|
+
const ident = { kind: 'Ident', name: shorthand.value, loc: this.loc(shorthand) };
|
|
1008
|
+
fields.push({ key: ident, value: ident });
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
fields.push({ value: this.parseExpression() });
|
|
1012
|
+
}
|
|
1013
|
+
this.skipNewlines();
|
|
1014
|
+
if (this.matchOperator(',') || this.match(token_1.TokenType.Semicolon))
|
|
1015
|
+
continue;
|
|
1016
|
+
if (this.atOperator('}'))
|
|
1017
|
+
break;
|
|
1018
|
+
}
|
|
1019
|
+
this.expect(token_1.TokenType.Operator, '}', 'Expected "}"');
|
|
1020
|
+
return { kind: 'Table', fields, loc: this.loc(start) };
|
|
1021
|
+
}
|
|
1022
|
+
isTableFieldEnd(token) {
|
|
1023
|
+
return token.type === token_1.TokenType.Newline
|
|
1024
|
+
|| token.type === token_1.TokenType.Semicolon
|
|
1025
|
+
|| token.type === token_1.TokenType.EOF
|
|
1026
|
+
|| (token.type === token_1.TokenType.Operator && (token.value === ',' || token.value === '}'));
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Range is only meaningful in `for ... in` heads and slices, where `..`
|
|
1030
|
+
* must not be treated as string concatenation.
|
|
1031
|
+
*/
|
|
1032
|
+
parseRangeExpr() {
|
|
1033
|
+
const start = this.peek();
|
|
1034
|
+
const expr = this.parseExpression();
|
|
1035
|
+
if (expr.kind === 'Binary' && expr.op === '..') {
|
|
1036
|
+
const range = {
|
|
1037
|
+
kind: 'Range',
|
|
1038
|
+
from: expr.left,
|
|
1039
|
+
to: expr.right,
|
|
1040
|
+
inclusive: true,
|
|
1041
|
+
loc: expr.loc,
|
|
1042
|
+
};
|
|
1043
|
+
return range;
|
|
1044
|
+
}
|
|
1045
|
+
if (this.atOperator('..') || this.atOperator('..<')) {
|
|
1046
|
+
const inclusive = this.next().value === '..';
|
|
1047
|
+
const to = this.parseExpression();
|
|
1048
|
+
const range = { kind: 'Range', from: expr, to, inclusive, loc: this.loc(start) };
|
|
1049
|
+
return range;
|
|
1050
|
+
}
|
|
1051
|
+
return expr;
|
|
1052
|
+
}
|
|
1053
|
+
/** Parses a complete standalone expression (used by f-string interpolation). */
|
|
1054
|
+
parseExpressionOnly() {
|
|
1055
|
+
const expr = this.parseExpression();
|
|
1056
|
+
if (!this.at(token_1.TokenType.EOF))
|
|
1057
|
+
throw this.error('Unexpected token in expression');
|
|
1058
|
+
return expr;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/** Splits an f-string body into literal text and parsed interpolation expressions. */
|
|
1062
|
+
function parseFStringParts(token) {
|
|
1063
|
+
const raw = token.value;
|
|
1064
|
+
const parts = [];
|
|
1065
|
+
let text = '';
|
|
1066
|
+
let index = 0;
|
|
1067
|
+
while (index < raw.length) {
|
|
1068
|
+
const ch = raw[index];
|
|
1069
|
+
if (ch === '{' && raw[index + 1] === '{') {
|
|
1070
|
+
text += '{';
|
|
1071
|
+
index += 2;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (ch === '}' && raw[index + 1] === '}') {
|
|
1075
|
+
text += '}';
|
|
1076
|
+
index += 2;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (ch === '{') {
|
|
1080
|
+
const close = findInterpolationEnd(raw, index + 1);
|
|
1081
|
+
if (close < 0) {
|
|
1082
|
+
throw new SyntaxError(`Unterminated f-string interpolation (line ${token.line}, column ${token.column})`);
|
|
1083
|
+
}
|
|
1084
|
+
if (text) {
|
|
1085
|
+
parts.push(text);
|
|
1086
|
+
text = '';
|
|
1087
|
+
}
|
|
1088
|
+
parts.push(new Parser(raw.slice(index + 1, close)).parseExpressionOnly());
|
|
1089
|
+
index = close + 1;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
text += ch;
|
|
1093
|
+
index++;
|
|
1094
|
+
}
|
|
1095
|
+
if (text)
|
|
1096
|
+
parts.push(text);
|
|
1097
|
+
return parts;
|
|
1098
|
+
}
|
|
1099
|
+
/** Finds the `}` closing an interpolation, skipping nested braces and strings. */
|
|
1100
|
+
function findInterpolationEnd(raw, from) {
|
|
1101
|
+
let depth = 1;
|
|
1102
|
+
let quote = '';
|
|
1103
|
+
for (let i = from; i < raw.length; i++) {
|
|
1104
|
+
const ch = raw[i];
|
|
1105
|
+
if (quote) {
|
|
1106
|
+
if (ch === '\\')
|
|
1107
|
+
i++;
|
|
1108
|
+
else if (ch === quote)
|
|
1109
|
+
quote = '';
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (ch === '"' || ch === "'") {
|
|
1113
|
+
quote = ch;
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (ch === '{')
|
|
1117
|
+
depth++;
|
|
1118
|
+
else if (ch === '}') {
|
|
1119
|
+
depth--;
|
|
1120
|
+
if (depth === 0)
|
|
1121
|
+
return i;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return -1;
|
|
1125
|
+
}
|
|
1126
|
+
function parse(source) {
|
|
1127
|
+
return new Parser(source).parseProgram();
|
|
1128
|
+
}
|
|
1129
|
+
/** Parses a single expression; mainly useful for tooling and f-string chunks. */
|
|
1130
|
+
function parseExpression(source) {
|
|
1131
|
+
return new Parser(source).parseExpressionOnly();
|
|
1132
|
+
}
|
|
1133
|
+
//# sourceMappingURL=parser.js.map
|