naidejs 1.0.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 +171 -0
- package/SPEC-X.nx +144 -0
- package/SPEC.naide +182 -0
- package/bin/naide.js +124 -0
- package/examples/async.naide +50 -0
- package/examples/async.nx +40 -0
- package/examples/crud.naide +79 -0
- package/examples/crud.nx +64 -0
- package/examples/hello.naide +33 -0
- package/examples/hello.nx +29 -0
- package/examples/model.naide +44 -0
- package/examples/server.naide +28 -0
- package/examples/server.nx +27 -0
- package/package.json +46 -0
- package/src/generator.js +573 -0
- package/src/index.js +23 -0
- package/src/lexer.js +389 -0
- package/src/parser.js +1079 -0
- package/src/preprocess.js +286 -0
- package/src/tokens.js +162 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import { T, TYPE_TOKENS } from './tokens.js';
|
|
2
|
+
|
|
3
|
+
class ASTNode {
|
|
4
|
+
constructor(type, props = {}) {
|
|
5
|
+
this.type = type;
|
|
6
|
+
Object.assign(this, props);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Parser {
|
|
11
|
+
constructor(tokens) {
|
|
12
|
+
this.tokens = this.preprocessTokens(tokens.filter(t => t.type !== T.COMMENT));
|
|
13
|
+
this.pos = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
preprocessTokens(tokens) {
|
|
17
|
+
// Remove INDENT/DEDENT pairs around pipe continuation lines
|
|
18
|
+
// Pattern: NEWLINE INDENT PIPE -> NEWLINE PIPE (and remove matching DEDENT)
|
|
19
|
+
const result = [...tokens];
|
|
20
|
+
const indentsToRemove = new Set();
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < result.length; i++) {
|
|
23
|
+
if (result[i].type !== T.INDENT) continue;
|
|
24
|
+
// Look forward past any newlines for PIPE
|
|
25
|
+
let j = i + 1;
|
|
26
|
+
while (j < result.length && result[j].type === T.NEWLINE) j++;
|
|
27
|
+
if (j < result.length && result[j].type === T.PIPE) {
|
|
28
|
+
indentsToRemove.add(i);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (indentsToRemove.size === 0) return result;
|
|
33
|
+
|
|
34
|
+
// For each INDENT to remove, find matching DEDENT
|
|
35
|
+
const dedentsToRemove = new Set();
|
|
36
|
+
for (const idx of indentsToRemove) {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
for (let i = idx; i < result.length; i++) {
|
|
39
|
+
if (result[i].type === T.INDENT) depth++;
|
|
40
|
+
if (result[i].type === T.DEDENT) {
|
|
41
|
+
depth--;
|
|
42
|
+
if (depth === 0) {
|
|
43
|
+
dedentsToRemove.add(i);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const allRemove = new Set([...indentsToRemove, ...dedentsToRemove]);
|
|
51
|
+
return result.filter((_, i) => !allRemove.has(i));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
peek(offset = 0) {
|
|
55
|
+
const idx = this.pos + offset;
|
|
56
|
+
return idx < this.tokens.length ? this.tokens[idx] : { type: T.EOF, value: '' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
advance() {
|
|
60
|
+
const tok = this.tokens[this.pos];
|
|
61
|
+
this.pos++;
|
|
62
|
+
return tok;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(type) {
|
|
66
|
+
const tok = this.advance();
|
|
67
|
+
if (tok.type !== type) {
|
|
68
|
+
throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')`, tok);
|
|
69
|
+
}
|
|
70
|
+
return tok;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
match(type) {
|
|
74
|
+
if (this.peek().type === type) {
|
|
75
|
+
return this.advance();
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
at(type) {
|
|
81
|
+
return this.peek().type === type;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
atAny(...types) {
|
|
85
|
+
return types.includes(this.peek().type);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expectPropertyName() {
|
|
89
|
+
const tok = this.advance();
|
|
90
|
+
if (tok.type === T.IDENT || TYPE_TOKENS.has(tok.type) ||
|
|
91
|
+
tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
|
|
92
|
+
tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
|
|
93
|
+
tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
|
|
94
|
+
tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF) {
|
|
95
|
+
return tok.value;
|
|
96
|
+
}
|
|
97
|
+
throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
error(msg, tok) {
|
|
101
|
+
const t = tok || this.peek();
|
|
102
|
+
return new Error(`[NAIDE Parse Error] ${msg} at line ${t.line}:${t.col}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
skipNewlines() {
|
|
106
|
+
while (this.at(T.NEWLINE)) this.advance();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
skipWhitespace() {
|
|
110
|
+
while (this.atAny(T.NEWLINE, T.INDENT, T.DEDENT)) this.advance();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
parse() {
|
|
114
|
+
const body = [];
|
|
115
|
+
this.skipNewlines();
|
|
116
|
+
while (!this.at(T.EOF)) {
|
|
117
|
+
const stmt = this.parseStatement();
|
|
118
|
+
if (stmt) body.push(stmt);
|
|
119
|
+
this.skipNewlines();
|
|
120
|
+
}
|
|
121
|
+
return new ASTNode('Program', { body });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
parseStatement() {
|
|
125
|
+
this.skipNewlines();
|
|
126
|
+
const tok = this.peek();
|
|
127
|
+
|
|
128
|
+
switch (tok.type) {
|
|
129
|
+
case T.USE: return this.parseUse();
|
|
130
|
+
case T.FN: return this.parseFunction(false, false);
|
|
131
|
+
case T.FN_ASYNC: return this.parseFunction(true, false);
|
|
132
|
+
case T.PUB: return this.parsePub();
|
|
133
|
+
case T.RET: return this.parseReturn();
|
|
134
|
+
case T.IF: return this.parseIf();
|
|
135
|
+
case T.EACH: return this.parseEach();
|
|
136
|
+
case T.FOR: return this.parseFor();
|
|
137
|
+
case T.WHILE: return this.parseWhile();
|
|
138
|
+
case T.MATCH: return this.parseMatch();
|
|
139
|
+
case T.TRY: return this.parseTry();
|
|
140
|
+
case T.SERVER: return this.parseServer();
|
|
141
|
+
case T.MODEL: return this.parseModel();
|
|
142
|
+
case T.ON: return this.parseOn();
|
|
143
|
+
case T.LOG: return this.parseLog();
|
|
144
|
+
case T.THROW: return this.parseThrow();
|
|
145
|
+
case T.BREAK: this.advance(); return new ASTNode('Break');
|
|
146
|
+
case T.CONTINUE: this.advance(); return new ASTNode('Continue');
|
|
147
|
+
case T.MUT: return this.parseMutVariable();
|
|
148
|
+
case T.DB: return this.parseDbStatement();
|
|
149
|
+
case T.AWAIT: return this.parseAwaitStatement();
|
|
150
|
+
case T.AWAIT_ALL: return this.parseAwaitAll();
|
|
151
|
+
default:
|
|
152
|
+
if (TYPE_TOKENS.has(tok.type)) {
|
|
153
|
+
return this.parseTypedVariable();
|
|
154
|
+
}
|
|
155
|
+
return this.parseExpressionStatement();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// use express
|
|
160
|
+
// use express from "express"
|
|
161
|
+
// use {readFile, writeFile} from "fs/promises"
|
|
162
|
+
parseUse() {
|
|
163
|
+
this.advance(); // use
|
|
164
|
+
|
|
165
|
+
if (this.at(T.LBRACE)) {
|
|
166
|
+
// use {a, b} from "module"
|
|
167
|
+
this.advance();
|
|
168
|
+
const names = [];
|
|
169
|
+
while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
|
|
170
|
+
const name = this.expect(T.IDENT).value;
|
|
171
|
+
let alias = null;
|
|
172
|
+
if (this.match(T.AS)) {
|
|
173
|
+
alias = this.expect(T.IDENT).value;
|
|
174
|
+
}
|
|
175
|
+
names.push({ name, alias });
|
|
176
|
+
this.match(T.COMMA);
|
|
177
|
+
}
|
|
178
|
+
this.expect(T.RBRACE);
|
|
179
|
+
this.expect(T.FROM);
|
|
180
|
+
const source = this.parseString();
|
|
181
|
+
return new ASTNode('UseDestructured', { names, source });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const name = this.expect(T.IDENT).value;
|
|
185
|
+
let source = null;
|
|
186
|
+
let alias = null;
|
|
187
|
+
|
|
188
|
+
if (this.match(T.AS)) {
|
|
189
|
+
alias = this.expect(T.IDENT).value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.match(T.FROM)) {
|
|
193
|
+
source = this.parseString();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return new ASTNode('Use', { name, source, alias });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
parseString() {
|
|
200
|
+
const tok = this.expect(T.STRING);
|
|
201
|
+
return tok.value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// fn name(params) -> returnType:
|
|
205
|
+
// body
|
|
206
|
+
parseFunction(isAsync, isPublic) {
|
|
207
|
+
this.advance(); // fn or fn.async
|
|
208
|
+
const name = this.expect(T.IDENT).value;
|
|
209
|
+
this.expect(T.LPAREN);
|
|
210
|
+
const params = this.parseFnParams();
|
|
211
|
+
this.expect(T.RPAREN);
|
|
212
|
+
|
|
213
|
+
let returnType = null;
|
|
214
|
+
if (this.match(T.ARROW)) {
|
|
215
|
+
returnType = this.parseTypeAnnotation();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.expect(T.COLON);
|
|
219
|
+
const body = this.parseBlock();
|
|
220
|
+
|
|
221
|
+
return new ASTNode('Function', { name, params, returnType, body, isAsync, isPublic });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
parseFnParams() {
|
|
225
|
+
const params = [];
|
|
226
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
227
|
+
let type = null;
|
|
228
|
+
let spread = false;
|
|
229
|
+
|
|
230
|
+
if (this.match(T.SPREAD)) {
|
|
231
|
+
spread = true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
235
|
+
type = this.advance().value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const name = this.expect(T.IDENT).value;
|
|
239
|
+
|
|
240
|
+
let defaultValue = null;
|
|
241
|
+
if (this.match(T.ASSIGN)) {
|
|
242
|
+
defaultValue = this.parseExpression();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
params.push({ name, type, defaultValue, spread });
|
|
246
|
+
this.match(T.COMMA);
|
|
247
|
+
}
|
|
248
|
+
return params;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
parseTypeAnnotation() {
|
|
252
|
+
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
253
|
+
return this.advance().value;
|
|
254
|
+
}
|
|
255
|
+
return this.expect(T.IDENT).value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
parseBlock() {
|
|
259
|
+
this.skipNewlines();
|
|
260
|
+
this.expect(T.INDENT);
|
|
261
|
+
const body = [];
|
|
262
|
+
this.skipNewlines();
|
|
263
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
264
|
+
const stmt = this.parseStatement();
|
|
265
|
+
if (stmt) body.push(stmt);
|
|
266
|
+
this.skipNewlines();
|
|
267
|
+
}
|
|
268
|
+
this.match(T.DEDENT);
|
|
269
|
+
return body;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parsePub() {
|
|
273
|
+
this.advance(); // pub
|
|
274
|
+
if (this.at(T.FN)) return this.parseFunction(false, true);
|
|
275
|
+
if (this.at(T.FN_ASYNC)) return this.parseFunction(true, true);
|
|
276
|
+
if (this.at(T.MODEL)) return this.parseModel(true);
|
|
277
|
+
|
|
278
|
+
// pub variable
|
|
279
|
+
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
280
|
+
const node = this.parseTypedVariable();
|
|
281
|
+
node.isPublic = true;
|
|
282
|
+
return node;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw this.error('Expected fn, model, or type after pub');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ret expression
|
|
289
|
+
parseReturn() {
|
|
290
|
+
this.advance(); // ret
|
|
291
|
+
if (this.at(T.NEWLINE) || this.at(T.EOF) || this.at(T.DEDENT)) {
|
|
292
|
+
return new ASTNode('Return', { value: null });
|
|
293
|
+
}
|
|
294
|
+
// ret.status 200 {...}
|
|
295
|
+
if (this.match(T.DOT)) {
|
|
296
|
+
const method = this.expect(T.IDENT).value;
|
|
297
|
+
const statusCode = this.parseExpression();
|
|
298
|
+
const body = this.parseExpression();
|
|
299
|
+
return new ASTNode('ReturnStatus', { method, statusCode, body });
|
|
300
|
+
}
|
|
301
|
+
const value = this.parseExpression();
|
|
302
|
+
return new ASTNode('Return', { value });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// str name = "hello"
|
|
306
|
+
parseTypedVariable() {
|
|
307
|
+
const varType = this.advance().value;
|
|
308
|
+
const name = this.expect(T.IDENT).value;
|
|
309
|
+
this.expect(T.ASSIGN);
|
|
310
|
+
const value = this.parseExpression();
|
|
311
|
+
return new ASTNode('TypedVar', { varType, name, value, isMut: false, isPublic: false });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// mut int counter = 0
|
|
315
|
+
parseMutVariable() {
|
|
316
|
+
this.advance(); // mut
|
|
317
|
+
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
318
|
+
const node = this.parseTypedVariable();
|
|
319
|
+
node.isMut = true;
|
|
320
|
+
return node;
|
|
321
|
+
}
|
|
322
|
+
// mut name = value (no type)
|
|
323
|
+
const name = this.expect(T.IDENT).value;
|
|
324
|
+
this.expect(T.ASSIGN);
|
|
325
|
+
const value = this.parseExpression();
|
|
326
|
+
return new ASTNode('TypedVar', { varType: null, name, value, isMut: true });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// if condition:
|
|
330
|
+
// body
|
|
331
|
+
// elif condition:
|
|
332
|
+
// body
|
|
333
|
+
// else:
|
|
334
|
+
// body
|
|
335
|
+
parseIf() {
|
|
336
|
+
this.advance(); // if
|
|
337
|
+
const condition = this.parseExpression();
|
|
338
|
+
this.expect(T.COLON);
|
|
339
|
+
const body = this.parseBlock();
|
|
340
|
+
|
|
341
|
+
const elifs = [];
|
|
342
|
+
this.skipNewlines();
|
|
343
|
+
while (this.at(T.ELIF)) {
|
|
344
|
+
this.advance();
|
|
345
|
+
const elifCond = this.parseExpression();
|
|
346
|
+
this.expect(T.COLON);
|
|
347
|
+
const elifBody = this.parseBlock();
|
|
348
|
+
elifs.push({ condition: elifCond, body: elifBody });
|
|
349
|
+
this.skipNewlines();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let elseBody = null;
|
|
353
|
+
if (this.at(T.ELSE)) {
|
|
354
|
+
this.advance();
|
|
355
|
+
this.expect(T.COLON);
|
|
356
|
+
elseBody = this.parseBlock();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return new ASTNode('If', { condition, body, elifs, elseBody });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// each item in collection:
|
|
363
|
+
// body
|
|
364
|
+
parseEach() {
|
|
365
|
+
this.advance(); // each
|
|
366
|
+
let key = null;
|
|
367
|
+
const valueName = this.expect(T.IDENT).value;
|
|
368
|
+
if (this.match(T.COMMA)) {
|
|
369
|
+
key = valueName;
|
|
370
|
+
// The next ident is actually the value
|
|
371
|
+
const val = this.expect(T.IDENT).value;
|
|
372
|
+
this.expect(T.IN);
|
|
373
|
+
const collection = this.parseExpression();
|
|
374
|
+
this.expect(T.COLON);
|
|
375
|
+
const body = this.parseBlock();
|
|
376
|
+
return new ASTNode('Each', { key, value: val, collection, body });
|
|
377
|
+
}
|
|
378
|
+
this.expect(T.IN);
|
|
379
|
+
const collection = this.parseExpression();
|
|
380
|
+
this.expect(T.COLON);
|
|
381
|
+
const body = this.parseBlock();
|
|
382
|
+
return new ASTNode('Each', { key: null, value: valueName, collection, body });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// for i in 0..10:
|
|
386
|
+
// body
|
|
387
|
+
parseFor() {
|
|
388
|
+
this.advance(); // for
|
|
389
|
+
const varName = this.expect(T.IDENT).value;
|
|
390
|
+
this.expect(T.IN);
|
|
391
|
+
const start = this.parseExpression();
|
|
392
|
+
this.expect(T.RANGE);
|
|
393
|
+
const end = this.parseExpression();
|
|
394
|
+
this.expect(T.COLON);
|
|
395
|
+
const body = this.parseBlock();
|
|
396
|
+
return new ASTNode('For', { varName, start, end, body });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// while condition:
|
|
400
|
+
// body
|
|
401
|
+
parseWhile() {
|
|
402
|
+
this.advance(); // while
|
|
403
|
+
const condition = this.parseExpression();
|
|
404
|
+
this.expect(T.COLON);
|
|
405
|
+
const body = this.parseBlock();
|
|
406
|
+
return new ASTNode('While', { condition, body });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// match value:
|
|
410
|
+
// pattern: body
|
|
411
|
+
// _: default
|
|
412
|
+
parseMatch() {
|
|
413
|
+
this.advance(); // match
|
|
414
|
+
const value = this.parseExpression();
|
|
415
|
+
this.expect(T.COLON);
|
|
416
|
+
this.skipNewlines();
|
|
417
|
+
this.expect(T.INDENT);
|
|
418
|
+
|
|
419
|
+
const cases = [];
|
|
420
|
+
this.skipNewlines();
|
|
421
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
422
|
+
let pattern;
|
|
423
|
+
if (this.at(T.IDENT) && this.peek().value === '_') {
|
|
424
|
+
this.advance();
|
|
425
|
+
pattern = new ASTNode('DefaultPattern');
|
|
426
|
+
} else {
|
|
427
|
+
pattern = this.parseExpression();
|
|
428
|
+
}
|
|
429
|
+
this.expect(T.COLON);
|
|
430
|
+
|
|
431
|
+
let body;
|
|
432
|
+
if (this.at(T.NEWLINE) || this.at(T.INDENT)) {
|
|
433
|
+
this.skipNewlines();
|
|
434
|
+
if (this.at(T.INDENT)) {
|
|
435
|
+
body = this.parseBlock();
|
|
436
|
+
} else {
|
|
437
|
+
body = [this.parseStatement()];
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
body = [this.parseStatement()];
|
|
441
|
+
}
|
|
442
|
+
cases.push({ pattern, body });
|
|
443
|
+
this.skipNewlines();
|
|
444
|
+
}
|
|
445
|
+
this.match(T.DEDENT);
|
|
446
|
+
return new ASTNode('Match', { value, cases });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// try:
|
|
450
|
+
// body
|
|
451
|
+
// fail e:
|
|
452
|
+
// handler
|
|
453
|
+
parseTry() {
|
|
454
|
+
this.advance(); // try
|
|
455
|
+
this.expect(T.COLON);
|
|
456
|
+
const body = this.parseBlock();
|
|
457
|
+
|
|
458
|
+
this.skipNewlines();
|
|
459
|
+
let catchVar = null;
|
|
460
|
+
let catchBody = null;
|
|
461
|
+
if (this.at(T.FAIL)) {
|
|
462
|
+
this.advance();
|
|
463
|
+
if (!this.at(T.COLON)) {
|
|
464
|
+
catchVar = this.expect(T.IDENT).value;
|
|
465
|
+
}
|
|
466
|
+
this.expect(T.COLON);
|
|
467
|
+
catchBody = this.parseBlock();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return new ASTNode('Try', { body, catchVar, catchBody });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// server app port 3000:
|
|
474
|
+
// get "/path" -> type:
|
|
475
|
+
// body
|
|
476
|
+
parseServer() {
|
|
477
|
+
this.advance(); // server
|
|
478
|
+
const name = this.expect(T.IDENT).value;
|
|
479
|
+
|
|
480
|
+
let portKw = null;
|
|
481
|
+
let port = null;
|
|
482
|
+
if (this.at(T.IDENT) && this.peek().value === 'port') {
|
|
483
|
+
this.advance();
|
|
484
|
+
port = this.parseExpression();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.expect(T.COLON);
|
|
488
|
+
this.skipNewlines();
|
|
489
|
+
this.expect(T.INDENT);
|
|
490
|
+
|
|
491
|
+
const routes = [];
|
|
492
|
+
const middleware = [];
|
|
493
|
+
this.skipNewlines();
|
|
494
|
+
|
|
495
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
496
|
+
if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
|
|
497
|
+
routes.push(this.parseRoute());
|
|
498
|
+
} else if (this.at(T.MID)) {
|
|
499
|
+
middleware.push(this.parseMiddleware());
|
|
500
|
+
} else {
|
|
501
|
+
// generic statement in server block
|
|
502
|
+
routes.push(this.parseStatement());
|
|
503
|
+
}
|
|
504
|
+
this.skipNewlines();
|
|
505
|
+
}
|
|
506
|
+
this.match(T.DEDENT);
|
|
507
|
+
|
|
508
|
+
return new ASTNode('Server', { name, port, routes, middleware });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
parseRoute() {
|
|
512
|
+
const method = this.advance().value; // get/post/put/del
|
|
513
|
+
const path = this.parseString();
|
|
514
|
+
|
|
515
|
+
let params = [];
|
|
516
|
+
if (this.match(T.LPAREN)) {
|
|
517
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
518
|
+
params.push(this.expect(T.IDENT).value);
|
|
519
|
+
this.match(T.COMMA);
|
|
520
|
+
}
|
|
521
|
+
this.expect(T.RPAREN);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let returnType = null;
|
|
525
|
+
if (this.match(T.ARROW)) {
|
|
526
|
+
returnType = this.parseTypeAnnotation();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.expect(T.COLON);
|
|
530
|
+
const body = this.parseBlock();
|
|
531
|
+
|
|
532
|
+
return new ASTNode('Route', { method, path, params, returnType, body });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
parseMiddleware() {
|
|
536
|
+
this.advance(); // mid
|
|
537
|
+
const name = this.expect(T.IDENT).value;
|
|
538
|
+
this.expect(T.LPAREN);
|
|
539
|
+
const params = [];
|
|
540
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
541
|
+
params.push(this.expect(T.IDENT).value);
|
|
542
|
+
this.match(T.COMMA);
|
|
543
|
+
}
|
|
544
|
+
this.expect(T.RPAREN);
|
|
545
|
+
this.expect(T.COLON);
|
|
546
|
+
const body = this.parseBlock();
|
|
547
|
+
return new ASTNode('Middleware', { name, params, body });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// model User:
|
|
551
|
+
// str name
|
|
552
|
+
// int age
|
|
553
|
+
// fn greet() -> str:
|
|
554
|
+
// ret "hi"
|
|
555
|
+
parseModel(isPublic = false) {
|
|
556
|
+
this.advance(); // model
|
|
557
|
+
const name = this.expect(T.IDENT).value;
|
|
558
|
+
|
|
559
|
+
let parent = null;
|
|
560
|
+
if (this.at(T.IDENT) && this.peek().value === 'extends') {
|
|
561
|
+
this.advance();
|
|
562
|
+
parent = this.expect(T.IDENT).value;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.expect(T.COLON);
|
|
566
|
+
this.skipNewlines();
|
|
567
|
+
this.expect(T.INDENT);
|
|
568
|
+
|
|
569
|
+
const fields = [];
|
|
570
|
+
const methods = [];
|
|
571
|
+
this.skipNewlines();
|
|
572
|
+
|
|
573
|
+
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
574
|
+
if (this.at(T.FN) || this.at(T.FN_ASYNC)) {
|
|
575
|
+
const isAsync = this.at(T.FN_ASYNC);
|
|
576
|
+
methods.push(this.parseFunction(isAsync, false));
|
|
577
|
+
} else if (TYPE_TOKENS.has(this.peek().type)) {
|
|
578
|
+
const fieldType = this.advance().value;
|
|
579
|
+
const fieldName = this.expect(T.IDENT).value;
|
|
580
|
+
let defaultValue = null;
|
|
581
|
+
if (this.match(T.ASSIGN)) {
|
|
582
|
+
defaultValue = this.parseExpression();
|
|
583
|
+
}
|
|
584
|
+
fields.push({ name: fieldName, type: fieldType, defaultValue });
|
|
585
|
+
} else {
|
|
586
|
+
// skip unknown
|
|
587
|
+
this.advance();
|
|
588
|
+
}
|
|
589
|
+
this.skipNewlines();
|
|
590
|
+
}
|
|
591
|
+
this.match(T.DEDENT);
|
|
592
|
+
|
|
593
|
+
return new ASTNode('Model', { name, parent, fields, methods, isPublic });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// on event:
|
|
597
|
+
// body
|
|
598
|
+
parseOn() {
|
|
599
|
+
this.advance(); // on
|
|
600
|
+
const event = this.parseExpression();
|
|
601
|
+
this.expect(T.COLON);
|
|
602
|
+
const body = this.parseBlock();
|
|
603
|
+
return new ASTNode('On', { event, body });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// log "message"
|
|
607
|
+
// log.error "message"
|
|
608
|
+
parseLog() {
|
|
609
|
+
this.advance(); // log
|
|
610
|
+
let level = 'log';
|
|
611
|
+
if (this.match(T.DOT)) {
|
|
612
|
+
level = this.expect(T.IDENT).value;
|
|
613
|
+
}
|
|
614
|
+
const args = [this.parseExpression()];
|
|
615
|
+
while (this.match(T.COMMA)) {
|
|
616
|
+
args.push(this.parseExpression());
|
|
617
|
+
}
|
|
618
|
+
return new ASTNode('Log', { level, args });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
parseThrow() {
|
|
622
|
+
this.advance(); // throw
|
|
623
|
+
const value = this.parseExpression();
|
|
624
|
+
return new ASTNode('Throw', { value });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
parseDbStatement() {
|
|
628
|
+
this.advance(); // db
|
|
629
|
+
this.expect(T.DOT);
|
|
630
|
+
const method = this.expect(T.IDENT).value;
|
|
631
|
+
if (method === 'connect') {
|
|
632
|
+
const connectionString = this.parseExpression();
|
|
633
|
+
return new ASTNode('DbConnect', { connectionString });
|
|
634
|
+
}
|
|
635
|
+
// db.query, db.find, etc -> treat as expression
|
|
636
|
+
this.pos -= 3; // rewind to parse as expression
|
|
637
|
+
return this.parseExpressionStatement();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
parseAwaitStatement() {
|
|
641
|
+
// could be await expression or standalone await call
|
|
642
|
+
return this.parseExpressionStatement();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// [a, b] = await.all [expr1, expr2]
|
|
646
|
+
parseAwaitAll() {
|
|
647
|
+
this.advance(); // await.all
|
|
648
|
+
const exprs = [];
|
|
649
|
+
if (this.match(T.LBRACKET)) {
|
|
650
|
+
while (!this.at(T.RBRACKET) && !this.at(T.EOF)) {
|
|
651
|
+
this.skipWhitespace();
|
|
652
|
+
if (this.at(T.RBRACKET)) break;
|
|
653
|
+
exprs.push(this.parseExpression());
|
|
654
|
+
this.match(T.COMMA);
|
|
655
|
+
this.skipWhitespace();
|
|
656
|
+
}
|
|
657
|
+
this.expect(T.RBRACKET);
|
|
658
|
+
} else {
|
|
659
|
+
exprs.push(this.parseExpression());
|
|
660
|
+
}
|
|
661
|
+
return new ASTNode('AwaitAll', { expressions: exprs });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
parseExpressionStatement() {
|
|
665
|
+
const expr = this.parseExpression();
|
|
666
|
+
|
|
667
|
+
// Check for assignment: expr = value
|
|
668
|
+
if (this.match(T.ASSIGN)) {
|
|
669
|
+
const value = this.parseExpression();
|
|
670
|
+
return new ASTNode('Assignment', { target: expr, value });
|
|
671
|
+
}
|
|
672
|
+
if (this.match(T.PLUS_ASSIGN)) {
|
|
673
|
+
const value = this.parseExpression();
|
|
674
|
+
return new ASTNode('CompoundAssign', { target: expr, op: '+=', value });
|
|
675
|
+
}
|
|
676
|
+
if (this.match(T.MINUS_ASSIGN)) {
|
|
677
|
+
const value = this.parseExpression();
|
|
678
|
+
return new ASTNode('CompoundAssign', { target: expr, op: '-=', value });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return new ASTNode('ExprStatement', { expression: expr });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Expression parsing with precedence climbing
|
|
685
|
+
parseExpression() {
|
|
686
|
+
return this.parsePipe();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
parsePipe() {
|
|
690
|
+
let left = this.parseTernary();
|
|
691
|
+
while (this.matchPipeAcrossLines()) {
|
|
692
|
+
this.skipWhitespace();
|
|
693
|
+
const right = this.parseTernary();
|
|
694
|
+
left = new ASTNode('Pipe', { left, right });
|
|
695
|
+
}
|
|
696
|
+
return left;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
matchPipeAcrossLines() {
|
|
700
|
+
if (this.match(T.PIPE)) return true;
|
|
701
|
+
// Lookahead across newlines for pipe (INDENT/DEDENT already preprocessed)
|
|
702
|
+
let scanPos = this.pos;
|
|
703
|
+
while (scanPos < this.tokens.length && this.tokens[scanPos].type === T.NEWLINE) {
|
|
704
|
+
scanPos++;
|
|
705
|
+
}
|
|
706
|
+
if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.PIPE) {
|
|
707
|
+
while (this.at(T.NEWLINE)) this.advance();
|
|
708
|
+
this.advance(); // consume PIPE
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
parseTernary() {
|
|
715
|
+
let expr = this.parseNullish();
|
|
716
|
+
// inline if: value = if cond then a else b
|
|
717
|
+
if (this.at(T.IF)) {
|
|
718
|
+
// Only treat as ternary if we're in an expression context
|
|
719
|
+
// Lookahead: if ... then ... else
|
|
720
|
+
const savedPos = this.pos;
|
|
721
|
+
this.advance(); // if
|
|
722
|
+
const condition = this.parseNullish();
|
|
723
|
+
if (this.match(T.THEN)) {
|
|
724
|
+
const consequent = this.parseNullish();
|
|
725
|
+
this.expect(T.ELSE);
|
|
726
|
+
const alternate = this.parseNullish();
|
|
727
|
+
return new ASTNode('Ternary', { condition, consequent, alternate });
|
|
728
|
+
}
|
|
729
|
+
// Not a ternary, restore
|
|
730
|
+
this.pos = savedPos;
|
|
731
|
+
}
|
|
732
|
+
return expr;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
parseNullish() {
|
|
736
|
+
let left = this.parseOr();
|
|
737
|
+
while (this.match(T.NULLISH)) {
|
|
738
|
+
const right = this.parseOr();
|
|
739
|
+
left = new ASTNode('Binary', { op: '??', left, right });
|
|
740
|
+
}
|
|
741
|
+
return left;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
parseOr() {
|
|
745
|
+
let left = this.parseAnd();
|
|
746
|
+
while (this.match(T.OR)) {
|
|
747
|
+
const right = this.parseAnd();
|
|
748
|
+
left = new ASTNode('Binary', { op: '||', left, right });
|
|
749
|
+
}
|
|
750
|
+
return left;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
parseAnd() {
|
|
754
|
+
let left = this.parseComparison();
|
|
755
|
+
while (this.match(T.AND)) {
|
|
756
|
+
const right = this.parseComparison();
|
|
757
|
+
left = new ASTNode('Binary', { op: '&&', left, right });
|
|
758
|
+
}
|
|
759
|
+
return left;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
parseComparison() {
|
|
763
|
+
let left = this.parseAddition();
|
|
764
|
+
while (this.atAny(T.EQ, T.NEQ, T.GT, T.LT, T.GTE, T.LTE)) {
|
|
765
|
+
const op = this.advance().value;
|
|
766
|
+
const right = this.parseAddition();
|
|
767
|
+
left = new ASTNode('Binary', { op: op === '==' ? '===' : op === '!=' ? '!==' : op, left, right });
|
|
768
|
+
}
|
|
769
|
+
return left;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
parseAddition() {
|
|
773
|
+
let left = this.parseMultiplication();
|
|
774
|
+
while (this.atAny(T.PLUS, T.MINUS)) {
|
|
775
|
+
const op = this.advance().value;
|
|
776
|
+
const right = this.parseMultiplication();
|
|
777
|
+
left = new ASTNode('Binary', { op, left, right });
|
|
778
|
+
}
|
|
779
|
+
return left;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
parseMultiplication() {
|
|
783
|
+
let left = this.parseUnary();
|
|
784
|
+
while (this.atAny(T.STAR, T.SLASH, T.PERCENT)) {
|
|
785
|
+
const op = this.advance().value;
|
|
786
|
+
const right = this.parseUnary();
|
|
787
|
+
left = new ASTNode('Binary', { op, left, right });
|
|
788
|
+
}
|
|
789
|
+
return left;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
parseUnary() {
|
|
793
|
+
if (this.match(T.NOT)) {
|
|
794
|
+
const expr = this.parseUnary();
|
|
795
|
+
return new ASTNode('Unary', { op: '!', expr });
|
|
796
|
+
}
|
|
797
|
+
if (this.at(T.MINUS)) {
|
|
798
|
+
this.advance();
|
|
799
|
+
const expr = this.parseUnary();
|
|
800
|
+
return new ASTNode('Unary', { op: '-', expr });
|
|
801
|
+
}
|
|
802
|
+
if (this.match(T.AWAIT)) {
|
|
803
|
+
const expr = this.parseUnary();
|
|
804
|
+
return new ASTNode('Await', { expr });
|
|
805
|
+
}
|
|
806
|
+
if (this.at(T.AWAIT_ALL)) {
|
|
807
|
+
this.advance();
|
|
808
|
+
const expr = this.parsePostfix();
|
|
809
|
+
return new ASTNode('AwaitAllExpr', { expr });
|
|
810
|
+
}
|
|
811
|
+
if (this.match(T.SPREAD)) {
|
|
812
|
+
const expr = this.parseUnary();
|
|
813
|
+
return new ASTNode('Spread', { expr });
|
|
814
|
+
}
|
|
815
|
+
if (this.match(T.NEW)) {
|
|
816
|
+
const expr = this.parsePostfix();
|
|
817
|
+
return new ASTNode('New', { expr });
|
|
818
|
+
}
|
|
819
|
+
return this.parsePostfix();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
parsePostfix() {
|
|
823
|
+
let expr = this.parsePrimary();
|
|
824
|
+
|
|
825
|
+
while (true) {
|
|
826
|
+
if (this.match(T.DOT)) {
|
|
827
|
+
const prop = this.expectPropertyName();
|
|
828
|
+
expr = new ASTNode('MemberAccess', { object: expr, property: prop });
|
|
829
|
+
} else if (this.match(T.OPTIONAL)) {
|
|
830
|
+
const prop = this.expectPropertyName();
|
|
831
|
+
expr = new ASTNode('OptionalAccess', { object: expr, property: prop });
|
|
832
|
+
} else if (this.at(T.LPAREN)) {
|
|
833
|
+
this.advance();
|
|
834
|
+
const args = this.parseArgList();
|
|
835
|
+
this.expect(T.RPAREN);
|
|
836
|
+
expr = new ASTNode('Call', { callee: expr, args });
|
|
837
|
+
} else if (this.at(T.LBRACKET)) {
|
|
838
|
+
this.advance();
|
|
839
|
+
const index = this.parseExpression();
|
|
840
|
+
this.expect(T.RBRACKET);
|
|
841
|
+
expr = new ASTNode('IndexAccess', { object: expr, index });
|
|
842
|
+
} else {
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return expr;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
parseArgList() {
|
|
851
|
+
const args = [];
|
|
852
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
853
|
+
this.skipWhitespace();
|
|
854
|
+
if (this.at(T.RPAREN)) break;
|
|
855
|
+
args.push(this.parseExpression());
|
|
856
|
+
this.match(T.COMMA);
|
|
857
|
+
this.skipWhitespace();
|
|
858
|
+
}
|
|
859
|
+
return args;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
parsePrimary() {
|
|
863
|
+
const tok = this.peek();
|
|
864
|
+
|
|
865
|
+
switch (tok.type) {
|
|
866
|
+
case T.NUMBER:
|
|
867
|
+
this.advance();
|
|
868
|
+
return new ASTNode('Number', { value: tok.value });
|
|
869
|
+
|
|
870
|
+
case T.STRING:
|
|
871
|
+
this.advance();
|
|
872
|
+
return new ASTNode('String', { value: tok.value });
|
|
873
|
+
|
|
874
|
+
case T.BOOL:
|
|
875
|
+
this.advance();
|
|
876
|
+
return new ASTNode('Bool', { value: tok.value === 'true' });
|
|
877
|
+
|
|
878
|
+
case T.NULL:
|
|
879
|
+
this.advance();
|
|
880
|
+
return new ASTNode('Null');
|
|
881
|
+
|
|
882
|
+
case T.SELF:
|
|
883
|
+
this.advance();
|
|
884
|
+
return new ASTNode('Self');
|
|
885
|
+
|
|
886
|
+
case T.IDENT:
|
|
887
|
+
this.advance();
|
|
888
|
+
return new ASTNode('Identifier', { name: tok.value });
|
|
889
|
+
|
|
890
|
+
case T.TYPE_STR: case T.TYPE_INT: case T.TYPE_NUM:
|
|
891
|
+
case T.TYPE_BOOL: case T.TYPE_LIST: case T.TYPE_MAP:
|
|
892
|
+
case T.TYPE_ANY: case T.TYPE_JSON: case T.TYPE_VOID:
|
|
893
|
+
// In expression context, type keywords act as identifiers (e.g., arr.map(), JSON.parse())
|
|
894
|
+
this.advance();
|
|
895
|
+
return new ASTNode('Identifier', { name: tok.value });
|
|
896
|
+
|
|
897
|
+
case T.DB:
|
|
898
|
+
return this.parseDbExpression();
|
|
899
|
+
|
|
900
|
+
case T.LOG:
|
|
901
|
+
return this.parseLogExpression();
|
|
902
|
+
|
|
903
|
+
case T.LPAREN:
|
|
904
|
+
return this.parseGroupOrArrow();
|
|
905
|
+
|
|
906
|
+
case T.LBRACKET:
|
|
907
|
+
return this.parseArray();
|
|
908
|
+
|
|
909
|
+
case T.LBRACE:
|
|
910
|
+
return this.parseObject();
|
|
911
|
+
|
|
912
|
+
case T.FN:
|
|
913
|
+
case T.FN_ASYNC:
|
|
914
|
+
return this.parseLambda();
|
|
915
|
+
|
|
916
|
+
default:
|
|
917
|
+
throw this.error(`Unexpected token ${tok.type} ('${tok.value}')`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
parseDbExpression() {
|
|
922
|
+
this.advance(); // db
|
|
923
|
+
this.expect(T.DOT);
|
|
924
|
+
const method = this.expect(T.IDENT).value;
|
|
925
|
+
if (this.at(T.LPAREN)) {
|
|
926
|
+
this.advance();
|
|
927
|
+
const args = this.parseArgList();
|
|
928
|
+
this.expect(T.RPAREN);
|
|
929
|
+
return new ASTNode('Call', {
|
|
930
|
+
callee: new ASTNode('MemberAccess', {
|
|
931
|
+
object: new ASTNode('Identifier', { name: 'db' }),
|
|
932
|
+
property: method
|
|
933
|
+
}),
|
|
934
|
+
args
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return new ASTNode('MemberAccess', {
|
|
938
|
+
object: new ASTNode('Identifier', { name: 'db' }),
|
|
939
|
+
property: method
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
parseLogExpression() {
|
|
944
|
+
this.advance(); // log
|
|
945
|
+
let level = 'log';
|
|
946
|
+
if (this.match(T.DOT)) {
|
|
947
|
+
level = this.expect(T.IDENT).value;
|
|
948
|
+
}
|
|
949
|
+
if (this.at(T.LPAREN)) {
|
|
950
|
+
this.advance();
|
|
951
|
+
const args = this.parseArgList();
|
|
952
|
+
this.expect(T.RPAREN);
|
|
953
|
+
return new ASTNode('Call', {
|
|
954
|
+
callee: new ASTNode('MemberAccess', {
|
|
955
|
+
object: new ASTNode('Identifier', { name: 'console' }),
|
|
956
|
+
property: level
|
|
957
|
+
}),
|
|
958
|
+
args
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return new ASTNode('MemberAccess', {
|
|
962
|
+
object: new ASTNode('Identifier', { name: 'console' }),
|
|
963
|
+
property: level
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
parseGroupOrArrow() {
|
|
968
|
+
// Check if this is an arrow function: (params) => body
|
|
969
|
+
const savedPos = this.pos;
|
|
970
|
+
this.advance(); // (
|
|
971
|
+
|
|
972
|
+
// Try to parse as arrow function params
|
|
973
|
+
let isArrow = false;
|
|
974
|
+
let depth = 1;
|
|
975
|
+
let scanPos = this.pos;
|
|
976
|
+
while (scanPos < this.tokens.length && depth > 0) {
|
|
977
|
+
if (this.tokens[scanPos].type === T.LPAREN) depth++;
|
|
978
|
+
if (this.tokens[scanPos].type === T.RPAREN) depth--;
|
|
979
|
+
scanPos++;
|
|
980
|
+
}
|
|
981
|
+
if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.FAT_ARROW) {
|
|
982
|
+
isArrow = true;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (isArrow) {
|
|
986
|
+
const params = [];
|
|
987
|
+
while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
|
|
988
|
+
let type = null;
|
|
989
|
+
if (TYPE_TOKENS.has(this.peek().type)) {
|
|
990
|
+
type = this.advance().value;
|
|
991
|
+
}
|
|
992
|
+
const name = this.expect(T.IDENT).value;
|
|
993
|
+
params.push({ name, type });
|
|
994
|
+
this.match(T.COMMA);
|
|
995
|
+
}
|
|
996
|
+
this.expect(T.RPAREN);
|
|
997
|
+
this.expect(T.FAT_ARROW);
|
|
998
|
+
const body = this.parseExpression();
|
|
999
|
+
return new ASTNode('ArrowFn', { params, body });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Regular grouping
|
|
1003
|
+
this.pos = savedPos;
|
|
1004
|
+
this.advance(); // (
|
|
1005
|
+
const expr = this.parseExpression();
|
|
1006
|
+
this.expect(T.RPAREN);
|
|
1007
|
+
return expr;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
parseArray() {
|
|
1011
|
+
this.advance(); // [
|
|
1012
|
+
const elements = [];
|
|
1013
|
+
while (!this.at(T.RBRACKET) && !this.at(T.EOF)) {
|
|
1014
|
+
this.skipWhitespace();
|
|
1015
|
+
if (this.at(T.RBRACKET)) break;
|
|
1016
|
+
elements.push(this.parseExpression());
|
|
1017
|
+
this.match(T.COMMA);
|
|
1018
|
+
this.skipWhitespace();
|
|
1019
|
+
}
|
|
1020
|
+
this.expect(T.RBRACKET);
|
|
1021
|
+
return new ASTNode('Array', { elements });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
parseObject() {
|
|
1025
|
+
this.advance(); // {
|
|
1026
|
+
const properties = [];
|
|
1027
|
+
while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
|
|
1028
|
+
this.skipWhitespace();
|
|
1029
|
+
if (this.at(T.RBRACE)) break;
|
|
1030
|
+
|
|
1031
|
+
if (this.match(T.SPREAD)) {
|
|
1032
|
+
const expr = this.parseExpression();
|
|
1033
|
+
properties.push({ type: 'spread', value: expr });
|
|
1034
|
+
this.match(T.COMMA);
|
|
1035
|
+
this.skipWhitespace();
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
let key;
|
|
1040
|
+
if (this.at(T.STRING)) {
|
|
1041
|
+
key = this.parseString();
|
|
1042
|
+
key = new ASTNode('String', { value: key });
|
|
1043
|
+
} else if (this.at(T.LBRACKET)) {
|
|
1044
|
+
this.advance();
|
|
1045
|
+
key = this.parseExpression();
|
|
1046
|
+
this.expect(T.RBRACKET);
|
|
1047
|
+
key = new ASTNode('Computed', { expr: key });
|
|
1048
|
+
} else {
|
|
1049
|
+
key = this.expect(T.IDENT).value;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (this.match(T.COLON)) {
|
|
1053
|
+
const value = this.parseExpression();
|
|
1054
|
+
properties.push({ type: 'property', key, value });
|
|
1055
|
+
} else {
|
|
1056
|
+
properties.push({ type: 'shorthand', key });
|
|
1057
|
+
}
|
|
1058
|
+
this.match(T.COMMA);
|
|
1059
|
+
this.skipWhitespace();
|
|
1060
|
+
}
|
|
1061
|
+
this.expect(T.RBRACE);
|
|
1062
|
+
return new ASTNode('Object', { properties });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
parseLambda() {
|
|
1066
|
+
const isAsync = this.at(T.FN_ASYNC);
|
|
1067
|
+
this.advance(); // fn or fn.async
|
|
1068
|
+
this.expect(T.LPAREN);
|
|
1069
|
+
const params = this.parseFnParams();
|
|
1070
|
+
this.expect(T.RPAREN);
|
|
1071
|
+
let returnType = null;
|
|
1072
|
+
if (this.match(T.ARROW)) {
|
|
1073
|
+
returnType = this.parseTypeAnnotation();
|
|
1074
|
+
}
|
|
1075
|
+
this.expect(T.COLON);
|
|
1076
|
+
const body = this.parseBlock();
|
|
1077
|
+
return new ASTNode('Lambda', { params, returnType, body, isAsync });
|
|
1078
|
+
}
|
|
1079
|
+
}
|