starlight-cli 1.0.33 → 1.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +464 -337
- package/package.json +1 -1
- package/src/evaluator.js +47 -2
- package/src/parser.js +418 -336
package/src/parser.js
CHANGED
|
@@ -55,164 +55,231 @@ class Parser {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
varDeclaration() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
this.eat('LET');
|
|
59
|
+
const id = this.current.value;
|
|
60
|
+
this.eat('IDENTIFIER');
|
|
61
|
+
|
|
62
|
+
let expr = null;
|
|
63
|
+
if (this.current.type === 'EQUAL') {
|
|
61
64
|
this.eat('EQUAL');
|
|
62
|
-
|
|
63
|
-
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
64
|
-
return { type: 'VarDeclaration', id, expr };
|
|
65
|
+
expr = this.expression();
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const expr = this.expression();
|
|
70
|
-
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
71
|
-
return { type: 'SldeployStatement', expr };
|
|
72
|
-
}
|
|
68
|
+
// semicolon optional
|
|
69
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
return { type: 'VarDeclaration', id, expr };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sldeployStatement() {
|
|
75
|
+
this.eat('SLDEPLOY');
|
|
76
|
+
const expr = this.expression();
|
|
77
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
78
|
+
return { type: 'SldeployStatement', expr };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
defineStatement() {
|
|
82
|
+
this.eat('DEFINE');
|
|
83
|
+
const id = this.current.value;
|
|
84
|
+
this.eat('IDENTIFIER');
|
|
85
|
+
|
|
86
|
+
let expr = null;
|
|
87
|
+
if (this.current.type === 'EQUAL') {
|
|
88
|
+
this.eat('EQUAL');
|
|
89
|
+
expr = this.expression();
|
|
85
90
|
}
|
|
91
|
+
|
|
92
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
93
|
+
|
|
94
|
+
return { type: 'DefineStatement', id, expr };
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
asyncFuncDeclaration() {
|
|
87
98
|
const name = this.current.value;
|
|
88
99
|
this.eat('IDENTIFIER');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (this.current.type
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
this.eat('
|
|
100
|
+
|
|
101
|
+
let params = [];
|
|
102
|
+
if (this.current.type === 'LPAREN') {
|
|
103
|
+
this.eat('LPAREN');
|
|
104
|
+
if (this.current.type !== 'RPAREN') {
|
|
105
|
+
params.push(this.current.value);
|
|
106
|
+
this.eat('IDENTIFIER');
|
|
107
|
+
while (this.current.type === 'COMMA') {
|
|
108
|
+
this.eat('COMMA');
|
|
109
|
+
params.push(this.current.value);
|
|
110
|
+
this.eat('IDENTIFIER');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.eat('RPAREN');
|
|
114
|
+
} else {
|
|
115
|
+
// no parentheses: single param as Python style
|
|
116
|
+
if (this.current.type === 'IDENTIFIER') {
|
|
96
117
|
params.push(this.current.value);
|
|
97
118
|
this.eat('IDENTIFIER');
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
|
-
|
|
121
|
+
|
|
101
122
|
const body = this.block();
|
|
102
123
|
return { type: 'FunctionDeclaration', name, params, body, async: true };
|
|
103
124
|
}
|
|
104
125
|
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
ifStatement() {
|
|
127
|
+
this.eat('IF');
|
|
128
|
+
|
|
129
|
+
let test;
|
|
130
|
+
if (this.current.type === 'LPAREN') {
|
|
107
131
|
this.eat('LPAREN');
|
|
108
|
-
|
|
132
|
+
test = this.expression();
|
|
109
133
|
this.eat('RPAREN');
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.eat('ELSE');
|
|
114
|
-
if (this.current.type === 'IF') alternate = this.ifStatement();
|
|
115
|
-
else alternate = this.block();
|
|
116
|
-
}
|
|
117
|
-
return { type: 'IfStatement', test, consequent, alternate };
|
|
134
|
+
} else {
|
|
135
|
+
// python style: no parentheses
|
|
136
|
+
test = this.expression();
|
|
118
137
|
}
|
|
119
138
|
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
const consequent = this.block();
|
|
140
|
+
|
|
141
|
+
let alternate = null;
|
|
142
|
+
if (this.current.type === 'ELSE') {
|
|
143
|
+
this.eat('ELSE');
|
|
144
|
+
if (this.current.type === 'IF') alternate = this.ifStatement();
|
|
145
|
+
else alternate = this.block();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { type: 'IfStatement', test, consequent, alternate };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
whileStatement() {
|
|
152
|
+
this.eat('WHILE');
|
|
153
|
+
|
|
154
|
+
let test;
|
|
155
|
+
if (this.current.type === 'LPAREN') {
|
|
122
156
|
this.eat('LPAREN');
|
|
123
|
-
|
|
157
|
+
test = this.expression();
|
|
124
158
|
this.eat('RPAREN');
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
} else {
|
|
160
|
+
test = this.expression();
|
|
127
161
|
}
|
|
128
|
-
importStatement() {
|
|
129
|
-
this.eat('IMPORT');
|
|
130
162
|
|
|
131
|
-
|
|
163
|
+
const body = this.block();
|
|
164
|
+
return { type: 'WhileStatement', test, body };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
importStatement() {
|
|
168
|
+
this.eat('IMPORT');
|
|
132
169
|
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
let specifiers = [];
|
|
171
|
+
if (this.current.type === 'STAR') {
|
|
172
|
+
this.eat('STAR');
|
|
173
|
+
this.eat('AS');
|
|
174
|
+
const name = this.current.value;
|
|
175
|
+
this.eat('IDENTIFIER');
|
|
176
|
+
specifiers.push({ type: 'NamespaceImport', local: name });
|
|
177
|
+
} else if (this.current.type === 'LBRACE') {
|
|
178
|
+
this.eat('LBRACE');
|
|
179
|
+
while (this.current.type !== 'RBRACE') {
|
|
180
|
+
const importedName = this.current.value;
|
|
181
|
+
this.eat('IDENTIFIER');
|
|
182
|
+
|
|
183
|
+
let localName = importedName;
|
|
184
|
+
if (this.current.type === 'AS') {
|
|
135
185
|
this.eat('AS');
|
|
136
|
-
|
|
137
|
-
this.eat('IDENTIFIER');
|
|
138
|
-
specifiers.push({ type: 'NamespaceImport', local: name });
|
|
139
|
-
}
|
|
140
|
-
else if (this.current.type === 'LBRACE') {
|
|
141
|
-
this.eat('LBRACE');
|
|
142
|
-
while (this.current.type !== 'RBRACE') {
|
|
143
|
-
const importedName = this.current.value;
|
|
144
|
-
this.eat('IDENTIFIER');
|
|
145
|
-
let localName = importedName;
|
|
146
|
-
if (this.current.type === 'AS') {
|
|
147
|
-
this.eat('AS');
|
|
148
|
-
localName = this.current.value;
|
|
149
|
-
this.eat('IDENTIFIER');
|
|
150
|
-
}
|
|
151
|
-
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
152
|
-
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
153
|
-
}
|
|
154
|
-
this.eat('RBRACE');
|
|
155
|
-
}
|
|
156
|
-
else if (this.current.type === 'IDENTIFIER') {
|
|
157
|
-
const localName = this.current.value;
|
|
186
|
+
localName = this.current.value;
|
|
158
187
|
this.eat('IDENTIFIER');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
specifiers.push({ type: 'NamedImport', imported: importedName, local: localName });
|
|
191
|
+
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
162
192
|
}
|
|
193
|
+
this.eat('RBRACE');
|
|
194
|
+
} else if (this.current.type === 'IDENTIFIER') {
|
|
195
|
+
const localName = this.current.value;
|
|
196
|
+
this.eat('IDENTIFIER');
|
|
197
|
+
specifiers.push({ type: 'DefaultImport', local: localName });
|
|
198
|
+
} else {
|
|
199
|
+
throw new Error(`Unexpected token in import at line ${this.current.line}, column ${this.current.column}`);
|
|
200
|
+
}
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
this.eat('FROM');
|
|
203
|
+
const pathToken = this.current;
|
|
204
|
+
if (pathToken.type !== 'STRING') throw new Error(`Expected string after FROM at line ${this.current.line}, column ${this.current.column}`);
|
|
205
|
+
this.eat('STRING');
|
|
168
206
|
|
|
169
|
-
|
|
207
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
170
208
|
|
|
171
|
-
|
|
209
|
+
return { type: 'ImportStatement', path: pathToken.value, specifiers };
|
|
172
210
|
}
|
|
211
|
+
forStatement() {
|
|
212
|
+
this.eat('FOR');
|
|
173
213
|
|
|
214
|
+
// --- Python-style: for variable in iterable ---
|
|
215
|
+
if (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN') {
|
|
216
|
+
const variable = this.current.value; // loop variable
|
|
217
|
+
this.eat('IDENTIFIER');
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
this.
|
|
219
|
+
this.eat('IN');
|
|
220
|
+
const iterable = this.expression(); // the array/object to loop over
|
|
221
|
+
|
|
222
|
+
const body = this.block();
|
|
223
|
+
return { type: 'ForInStatement', variable, iterable, body };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- C-style: for(init; test; update) ---
|
|
227
|
+
let init = null;
|
|
228
|
+
let test = null;
|
|
229
|
+
let update = null;
|
|
230
|
+
|
|
231
|
+
if (this.current.type === 'LPAREN') {
|
|
177
232
|
this.eat('LPAREN');
|
|
178
233
|
|
|
179
|
-
|
|
234
|
+
// init
|
|
180
235
|
if (this.current.type !== 'SEMICOLON') {
|
|
181
236
|
init = this.current.type === 'LET' ? this.varDeclaration() : this.expressionStatement();
|
|
182
237
|
} else {
|
|
183
238
|
this.eat('SEMICOLON');
|
|
184
239
|
}
|
|
185
240
|
|
|
186
|
-
|
|
241
|
+
// test
|
|
187
242
|
if (this.current.type !== 'SEMICOLON') test = this.expression();
|
|
188
243
|
this.eat('SEMICOLON');
|
|
189
244
|
|
|
190
|
-
|
|
245
|
+
// update
|
|
191
246
|
if (this.current.type !== 'RPAREN') update = this.expression();
|
|
192
247
|
this.eat('RPAREN');
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
248
|
+
} else {
|
|
249
|
+
// fallback: single expression (could be used for Python-style with "in", already handled above)
|
|
250
|
+
init = this.expression();
|
|
251
|
+
if (this.current.type === 'IN') {
|
|
252
|
+
this.eat('IN');
|
|
253
|
+
test = this.expression(); // iterable
|
|
254
|
+
}
|
|
196
255
|
}
|
|
197
256
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return { type: 'BreakStatement' };
|
|
202
|
-
}
|
|
257
|
+
const body = this.block();
|
|
258
|
+
return { type: 'ForStatement', init, test, update, body };
|
|
259
|
+
}
|
|
203
260
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
261
|
+
breakStatement() {
|
|
262
|
+
this.eat('BREAK');
|
|
263
|
+
// Python-style: no semicolon needed, ignore if present
|
|
264
|
+
if (this.current.type === 'SEMICOLON') this.advance();
|
|
265
|
+
return { type: 'BreakStatement' };
|
|
266
|
+
}
|
|
209
267
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
268
|
+
continueStatement() {
|
|
269
|
+
this.eat('CONTINUE');
|
|
270
|
+
// Python-style: no semicolon needed, ignore if present
|
|
271
|
+
if (this.current.type === 'SEMICOLON') this.advance();
|
|
272
|
+
return { type: 'ContinueStatement' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
funcDeclaration() {
|
|
276
|
+
this.eat('FUNC');
|
|
277
|
+
const name = this.current.value;
|
|
278
|
+
this.eat('IDENTIFIER');
|
|
279
|
+
|
|
280
|
+
let params = [];
|
|
281
|
+
if (this.current.type === 'LPAREN') {
|
|
214
282
|
this.eat('LPAREN');
|
|
215
|
-
const params = [];
|
|
216
283
|
if (this.current.type !== 'RPAREN') {
|
|
217
284
|
params.push(this.current.value);
|
|
218
285
|
this.eat('IDENTIFIER');
|
|
@@ -223,176 +290,203 @@ asyncFuncDeclaration() {
|
|
|
223
290
|
}
|
|
224
291
|
}
|
|
225
292
|
this.eat('RPAREN');
|
|
226
|
-
|
|
227
|
-
|
|
293
|
+
} else {
|
|
294
|
+
// Python-style: single param without parentheses
|
|
295
|
+
if (this.current.type === 'IDENTIFIER') {
|
|
296
|
+
params.push(this.current.value);
|
|
297
|
+
this.eat('IDENTIFIER');
|
|
298
|
+
}
|
|
228
299
|
}
|
|
229
300
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
301
|
+
const body = this.block();
|
|
302
|
+
return { type: 'FunctionDeclaration', name, params, body };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
returnStatement() {
|
|
306
|
+
this.eat('RETURN');
|
|
307
|
+
|
|
308
|
+
let argument = null;
|
|
309
|
+
if (this.current.type !== 'SEMICOLON') {
|
|
310
|
+
argument = this.expression();
|
|
236
311
|
}
|
|
237
312
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
313
|
+
// semicolon optional
|
|
314
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
315
|
+
|
|
316
|
+
return { type: 'ReturnStatement', argument };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
block() {
|
|
320
|
+
this.eat('LBRACE');
|
|
321
|
+
const body = [];
|
|
322
|
+
while (this.current.type !== 'RBRACE') {
|
|
323
|
+
body.push(this.statement());
|
|
246
324
|
}
|
|
325
|
+
this.eat('RBRACE');
|
|
326
|
+
return { type: 'BlockStatement', body };
|
|
327
|
+
}
|
|
247
328
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
329
|
+
expressionStatement() {
|
|
330
|
+
const expr = this.expression();
|
|
331
|
+
// semicolon optional
|
|
332
|
+
if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
|
|
333
|
+
return { type: 'ExpressionStatement', expression: expr };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
expression() {
|
|
337
|
+
return this.assignment();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
assignment() {
|
|
341
|
+
const node = this.logicalOr();
|
|
342
|
+
const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
|
|
343
|
+
|
|
344
|
+
if (compoundOps.includes(this.current.type)) {
|
|
345
|
+
const op = this.current.type;
|
|
346
|
+
this.eat(op);
|
|
347
|
+
const right = this.assignment();
|
|
348
|
+
return { type: 'CompoundAssignment', operator: op, left: node, right };
|
|
252
349
|
}
|
|
253
350
|
|
|
254
|
-
|
|
255
|
-
|
|
351
|
+
if (this.current.type === 'EQUAL') {
|
|
352
|
+
this.eat('EQUAL');
|
|
353
|
+
const right = this.assignment();
|
|
354
|
+
return { type: 'AssignmentExpression', left: node, right };
|
|
256
355
|
}
|
|
257
356
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
|
|
261
|
-
if (compoundOps.includes(this.current.type)) {
|
|
262
|
-
const op = this.current.type;
|
|
263
|
-
this.eat(op);
|
|
264
|
-
const right = this.assignment();
|
|
265
|
-
return { type: 'CompoundAssignment', operator: op, left: node, right };
|
|
266
|
-
}
|
|
357
|
+
return node;
|
|
358
|
+
}
|
|
267
359
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
360
|
+
logicalOr() {
|
|
361
|
+
let node = this.logicalAnd();
|
|
362
|
+
while (this.current.type === 'OR') {
|
|
363
|
+
const op = this.current.type;
|
|
364
|
+
this.eat(op);
|
|
365
|
+
node = { type: 'LogicalExpression', operator: op, left: node, right: this.logicalAnd() };
|
|
366
|
+
}
|
|
367
|
+
return node;
|
|
368
|
+
}
|
|
273
369
|
|
|
274
|
-
|
|
370
|
+
logicalAnd() {
|
|
371
|
+
let node = this.equality();
|
|
372
|
+
while (this.current.type === 'AND') {
|
|
373
|
+
const op = this.current.type;
|
|
374
|
+
this.eat(op);
|
|
375
|
+
node = { type: 'LogicalExpression', operator: op, left: node, right: this.equality() };
|
|
275
376
|
}
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
equality() {
|
|
380
|
+
let node = this.comparison();
|
|
381
|
+
while (['EQEQ', 'NOTEQ'].includes(this.current.type)) {
|
|
382
|
+
const op = this.current.type;
|
|
383
|
+
this.eat(op);
|
|
384
|
+
node = { type: 'BinaryExpression', operator: op, left: node, right: this.comparison() };
|
|
385
|
+
}
|
|
386
|
+
return node;
|
|
387
|
+
}
|
|
276
388
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
return node;
|
|
389
|
+
comparison() {
|
|
390
|
+
let node = this.term();
|
|
391
|
+
while (['LT', 'LTE', 'GT', 'GTE'].includes(this.current.type)) {
|
|
392
|
+
const op = this.current.type;
|
|
393
|
+
this.eat(op);
|
|
394
|
+
node = { type: 'BinaryExpression', operator: op, left: node, right: this.term() };
|
|
285
395
|
}
|
|
396
|
+
return node;
|
|
397
|
+
}
|
|
286
398
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
return node;
|
|
399
|
+
term() {
|
|
400
|
+
let node = this.factor();
|
|
401
|
+
while (['PLUS', 'MINUS'].includes(this.current.type)) {
|
|
402
|
+
const op = this.current.type;
|
|
403
|
+
this.eat(op);
|
|
404
|
+
node = { type: 'BinaryExpression', operator: op, left: node, right: this.factor() };
|
|
295
405
|
}
|
|
406
|
+
return node;
|
|
407
|
+
}
|
|
296
408
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
return node;
|
|
409
|
+
factor() {
|
|
410
|
+
let node = this.unary();
|
|
411
|
+
while (['STAR', 'SLASH', 'MOD'].includes(this.current.type)) {
|
|
412
|
+
const op = this.current.type;
|
|
413
|
+
this.eat(op);
|
|
414
|
+
node = { type: 'BinaryExpression', operator: op, left: node, right: this.unary() };
|
|
305
415
|
}
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
306
418
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
node = { type: 'BinaryExpression', operator: op, left: node, right: this.term() };
|
|
313
|
-
}
|
|
314
|
-
return node;
|
|
419
|
+
unary() {
|
|
420
|
+
if (['NOT', 'MINUS', 'PLUS'].includes(this.current.type)) {
|
|
421
|
+
const op = this.current.type;
|
|
422
|
+
this.eat(op);
|
|
423
|
+
return { type: 'UnaryExpression', operator: op, argument: this.unary() };
|
|
315
424
|
}
|
|
316
425
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
return node;
|
|
426
|
+
// Python-like: ignore ++ and -- if not used
|
|
427
|
+
if (this.current.type === 'PLUSPLUS' || this.current.type === 'MINUSMINUS') {
|
|
428
|
+
const op = this.current.type;
|
|
429
|
+
this.eat(op);
|
|
430
|
+
const argument = this.unary();
|
|
431
|
+
return { type: 'UpdateExpression', operator: op, argument, prefix: true };
|
|
325
432
|
}
|
|
326
433
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
434
|
+
return this.postfix();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
postfix() {
|
|
438
|
+
let node = this.primary();
|
|
439
|
+
while (true) {
|
|
440
|
+
if (this.current.type === 'LBRACKET') {
|
|
441
|
+
this.eat('LBRACKET');
|
|
442
|
+
const index = this.expression();
|
|
443
|
+
if (this.current.type === 'RBRACKET') this.eat('RBRACKET');
|
|
444
|
+
node = { type: 'IndexExpression', object: node, indexer: index };
|
|
445
|
+
continue;
|
|
333
446
|
}
|
|
334
|
-
return node;
|
|
335
|
-
}
|
|
336
447
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
this.
|
|
341
|
-
|
|
448
|
+
if (this.current.type === 'LPAREN') {
|
|
449
|
+
this.eat('LPAREN');
|
|
450
|
+
const args = [];
|
|
451
|
+
while (this.current.type !== 'RPAREN' && this.current.type !== 'EOF') {
|
|
452
|
+
args.push(this.expression());
|
|
453
|
+
if (this.current.type === 'COMMA') this.eat('COMMA'); // optional comma
|
|
454
|
+
}
|
|
455
|
+
if (this.current.type === 'RPAREN') this.eat('RPAREN');
|
|
456
|
+
node = { type: 'CallExpression', callee: node, arguments: args };
|
|
457
|
+
continue;
|
|
342
458
|
}
|
|
459
|
+
|
|
460
|
+
if (this.current.type === 'DOT') {
|
|
461
|
+
this.eat('DOT');
|
|
462
|
+
const property = this.current.value || '';
|
|
463
|
+
if (this.current.type === 'IDENTIFIER') this.eat('IDENTIFIER');
|
|
464
|
+
node = { type: 'MemberExpression', object: node, property };
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
343
468
|
if (this.current.type === 'PLUSPLUS' || this.current.type === 'MINUSMINUS') {
|
|
344
469
|
const op = this.current.type;
|
|
345
470
|
this.eat(op);
|
|
346
|
-
|
|
347
|
-
|
|
471
|
+
node = { type: 'UpdateExpression', operator: op, argument: node, prefix: false };
|
|
472
|
+
continue;
|
|
348
473
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
this.eat('LBRACKET');
|
|
357
|
-
const index = this.expression();
|
|
358
|
-
this.eat('RBRACKET');
|
|
359
|
-
node = { type: 'IndexExpression', object: node, indexer: index };
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (this.current.type === 'LPAREN') {
|
|
363
|
-
this.eat('LPAREN');
|
|
364
|
-
const args = [];
|
|
365
|
-
if (this.current.type !== 'RPAREN') {
|
|
366
|
-
args.push(this.expression());
|
|
367
|
-
while (this.current.type === 'COMMA') {
|
|
368
|
-
this.eat('COMMA');
|
|
369
|
-
args.push(this.expression());
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
this.eat('RPAREN');
|
|
373
|
-
node = { type: 'CallExpression', callee: node, arguments: args };
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (this.current.type === 'DOT') {
|
|
377
|
-
this.eat('DOT');
|
|
378
|
-
if (this.current.type !== 'IDENTIFIER') throw new Error(`Expected property name after dot at line ${this.current.line}, column ${this.current.column}`);
|
|
379
|
-
const property = this.current.value;
|
|
380
|
-
this.eat('IDENTIFIER');
|
|
381
|
-
node = { type: 'MemberExpression', object: node, property };
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (this.current.type === 'PLUSPLUS' || this.current.type === 'MINUSMINUS') {
|
|
385
|
-
const op = this.current.type;
|
|
386
|
-
this.eat(op);
|
|
387
|
-
node = { type: 'UpdateExpression', operator: op, argument: node, prefix: false };
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
break;
|
|
474
|
+
|
|
475
|
+
// Python-style: implicit function calls (if identifier followed by identifier)
|
|
476
|
+
if (node.type === 'Identifier' && this.current.type === 'IDENTIFIER') {
|
|
477
|
+
const argNode = { type: 'Identifier', name: this.current.value };
|
|
478
|
+
this.eat('IDENTIFIER');
|
|
479
|
+
node = { type: 'CallExpression', callee: node, arguments: [argNode] };
|
|
480
|
+
continue;
|
|
391
481
|
}
|
|
392
|
-
|
|
482
|
+
|
|
483
|
+
break;
|
|
393
484
|
}
|
|
485
|
+
return node;
|
|
486
|
+
}
|
|
487
|
+
|
|
394
488
|
arrowFunction(params) {
|
|
395
|
-
this.eat('ARROW');
|
|
489
|
+
if (this.current.type === 'ARROW') this.eat('ARROW');
|
|
396
490
|
const body = this.expression();
|
|
397
491
|
return {
|
|
398
492
|
type: 'ArrowFunctionExpression',
|
|
@@ -401,126 +495,114 @@ arrowFunction(params) {
|
|
|
401
495
|
};
|
|
402
496
|
}
|
|
403
497
|
|
|
498
|
+
|
|
404
499
|
primary() {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
500
|
+
const t = this.current;
|
|
501
|
+
|
|
502
|
+
if (t.type === 'NUMBER') { this.eat('NUMBER'); return { type: 'Literal', value: t.value }; }
|
|
503
|
+
if (t.type === 'STRING') { this.eat('STRING'); return { type: 'Literal', value: t.value }; }
|
|
504
|
+
if (t.type === 'TRUE') { this.eat('TRUE'); return { type: 'Literal', value: true }; }
|
|
505
|
+
if (t.type === 'FALSE') { this.eat('FALSE'); return { type: 'Literal', value: false }; }
|
|
506
|
+
if (t.type === 'NULL') { this.eat('NULL'); return { type: 'Literal', value: null }; }
|
|
507
|
+
|
|
508
|
+
if (t.type === 'AWAIT') {
|
|
413
509
|
this.eat('AWAIT');
|
|
414
510
|
const argument = this.expression();
|
|
415
511
|
return { type: 'AwaitExpression', argument };
|
|
416
512
|
}
|
|
417
|
-
if (t.type === 'NEW') {
|
|
418
|
-
this.eat('NEW');
|
|
419
|
-
const callee = this.primary(); // the class or function being called
|
|
420
|
-
this.eat('LPAREN');
|
|
421
|
-
const args = [];
|
|
422
|
-
if (this.current.type !== 'RPAREN') {
|
|
423
|
-
args.push(this.expression());
|
|
424
|
-
while (this.current.type === 'COMMA') {
|
|
425
|
-
this.eat('COMMA');
|
|
426
|
-
args.push(this.expression());
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
this.eat('RPAREN');
|
|
430
|
-
return { type: 'NewExpression', callee, arguments: args };
|
|
431
|
-
}
|
|
432
513
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
514
|
+
if (t.type === 'NEW') {
|
|
515
|
+
this.eat('NEW');
|
|
516
|
+
const callee = this.primary();
|
|
517
|
+
this.eat('LPAREN');
|
|
518
|
+
const args = [];
|
|
519
|
+
if (this.current.type !== 'RPAREN') {
|
|
520
|
+
args.push(this.expression());
|
|
521
|
+
while (this.current.type === 'COMMA') {
|
|
522
|
+
this.eat('COMMA');
|
|
523
|
+
if (this.current.type !== 'RPAREN') args.push(this.expression());
|
|
443
524
|
}
|
|
444
|
-
this.eat('RPAREN');
|
|
445
|
-
return { type: 'CallExpression', callee: { type: 'Identifier', name: 'ask' }, arguments: args };
|
|
446
525
|
}
|
|
526
|
+
this.eat('RPAREN');
|
|
527
|
+
return { type: 'NewExpression', callee, arguments: args };
|
|
528
|
+
}
|
|
447
529
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
530
|
+
if (t.type === 'ASK') {
|
|
531
|
+
this.eat('ASK');
|
|
532
|
+
this.eat('LPAREN');
|
|
533
|
+
const args = [];
|
|
534
|
+
if (this.current.type !== 'RPAREN') {
|
|
535
|
+
args.push(this.expression());
|
|
536
|
+
while (this.current.type === 'COMMA') {
|
|
537
|
+
this.eat('COMMA');
|
|
538
|
+
if (this.current.type !== 'RPAREN') args.push(this.expression());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
this.eat('RPAREN');
|
|
542
|
+
return { type: 'CallExpression', callee: { type: 'Identifier', name: 'ask' }, arguments: args };
|
|
454
543
|
}
|
|
455
544
|
|
|
456
|
-
|
|
457
|
-
|
|
545
|
+
if (t.type === 'IDENTIFIER') {
|
|
546
|
+
const name = t.value;
|
|
547
|
+
this.eat('IDENTIFIER');
|
|
458
548
|
|
|
549
|
+
if (this.current.type === 'ARROW') {
|
|
550
|
+
return this.arrowFunction([name]);
|
|
551
|
+
}
|
|
459
552
|
|
|
460
|
-
|
|
461
|
-
|
|
553
|
+
return { type: 'Identifier', name };
|
|
554
|
+
}
|
|
462
555
|
|
|
463
|
-
|
|
556
|
+
if (t.type === 'LPAREN') {
|
|
557
|
+
this.eat('LPAREN');
|
|
558
|
+
const elements = [];
|
|
464
559
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
this.eat('IDENTIFIER');
|
|
560
|
+
if (this.current.type !== 'RPAREN') {
|
|
561
|
+
elements.push(this.expression());
|
|
562
|
+
while (this.current.type === 'COMMA') {
|
|
563
|
+
this.eat('COMMA');
|
|
564
|
+
if (this.current.type !== 'RPAREN') elements.push(this.expression());
|
|
565
|
+
}
|
|
472
566
|
}
|
|
473
|
-
}
|
|
474
567
|
|
|
475
|
-
|
|
568
|
+
this.eat('RPAREN');
|
|
476
569
|
|
|
477
|
-
|
|
478
|
-
return this.arrowFunction(params);
|
|
479
|
-
}
|
|
570
|
+
if (this.current.type === 'ARROW') return this.arrowFunction(elements);
|
|
480
571
|
|
|
481
|
-
|
|
482
|
-
return { type: 'Identifier', name: params[0] };
|
|
572
|
+
return elements.length === 1 ? elements[0] : { type: 'ArrayExpression', elements };
|
|
483
573
|
}
|
|
484
574
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const elements = [];
|
|
494
|
-
if (this.current.type !== 'RBRACKET') {
|
|
495
|
-
elements.push(this.expression());
|
|
496
|
-
while (this.current.type === 'COMMA') {
|
|
497
|
-
this.eat('COMMA');
|
|
498
|
-
elements.push(this.expression());
|
|
499
|
-
}
|
|
575
|
+
if (t.type === 'LBRACKET') {
|
|
576
|
+
this.eat('LBRACKET');
|
|
577
|
+
const elements = [];
|
|
578
|
+
if (this.current.type !== 'RBRACKET') {
|
|
579
|
+
elements.push(this.expression());
|
|
580
|
+
while (this.current.type === 'COMMA') {
|
|
581
|
+
this.eat('COMMA');
|
|
582
|
+
if (this.current.type !== 'RBRACKET') elements.push(this.expression());
|
|
500
583
|
}
|
|
501
|
-
this.eat('RBRACKET');
|
|
502
|
-
return { type: 'ArrayExpression', elements };
|
|
503
584
|
}
|
|
585
|
+
this.eat('RBRACKET');
|
|
586
|
+
return { type: 'ArrayExpression', elements };
|
|
587
|
+
}
|
|
504
588
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const value = this.expression();
|
|
515
|
-
props.push({ key, value });
|
|
516
|
-
if (this.current.type === 'COMMA') this.eat('COMMA');
|
|
517
|
-
}
|
|
518
|
-
this.eat('RBRACE');
|
|
519
|
-
return { type: 'ObjectExpression', props };
|
|
589
|
+
if (t.type === 'LBRACE') {
|
|
590
|
+
this.eat('LBRACE');
|
|
591
|
+
const props = [];
|
|
592
|
+
while (this.current.type !== 'RBRACE') {
|
|
593
|
+
const key = this.expression(); // Flexible key: can be any expression
|
|
594
|
+
this.eat('COLON');
|
|
595
|
+
const value = this.expression();
|
|
596
|
+
props.push({ key, value });
|
|
597
|
+
if (this.current.type === 'COMMA') this.eat('COMMA'); // optional trailing comma
|
|
520
598
|
}
|
|
521
|
-
|
|
522
|
-
|
|
599
|
+
this.eat('RBRACE');
|
|
600
|
+
return { type: 'ObjectExpression', props };
|
|
523
601
|
}
|
|
602
|
+
|
|
603
|
+
throw new Error(`Unexpected token in primary: ${t.type} at line ${this.current.line}, column ${this.current.column}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
524
606
|
}
|
|
525
607
|
|
|
526
608
|
module.exports = Parser;
|