lt-script 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 +33 -0
- package/dist/cli/ltc.d.ts +2 -0
- package/dist/cli/ltc.js +134 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +26 -0
- package/dist/cli/watch.d.ts +1 -0
- package/dist/cli/watch.js +42 -0
- package/dist/compiler/codegen/LuaEmitter.d.ts +59 -0
- package/dist/compiler/codegen/LuaEmitter.js +748 -0
- package/dist/compiler/lexer/Lexer.d.ts +31 -0
- package/dist/compiler/lexer/Lexer.js +322 -0
- package/dist/compiler/lexer/Token.d.ts +109 -0
- package/dist/compiler/lexer/Token.js +169 -0
- package/dist/compiler/parser/AST.d.ts +314 -0
- package/dist/compiler/parser/AST.js +60 -0
- package/dist/compiler/parser/Parser.d.ts +68 -0
- package/dist/compiler/parser/Parser.js +873 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -0
- package/package.json +33 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { TokenType } from '../lexer/Token.js';
|
|
2
|
+
import * as AST from './AST.js';
|
|
3
|
+
/**
|
|
4
|
+
* LT Language Parser
|
|
5
|
+
* Recursive descent parser for LT → AST
|
|
6
|
+
*/
|
|
7
|
+
export class Parser {
|
|
8
|
+
tokens;
|
|
9
|
+
pos = 0;
|
|
10
|
+
constructor(tokens) {
|
|
11
|
+
this.tokens = tokens;
|
|
12
|
+
}
|
|
13
|
+
parse() {
|
|
14
|
+
const body = [];
|
|
15
|
+
while (!this.isEOF()) {
|
|
16
|
+
body.push(this.parseStatement());
|
|
17
|
+
}
|
|
18
|
+
return { kind: AST.NodeType.Program, body };
|
|
19
|
+
}
|
|
20
|
+
// ============ Statement Parsing ============
|
|
21
|
+
parseStatement() {
|
|
22
|
+
switch (this.at().type) {
|
|
23
|
+
case TokenType.VAR:
|
|
24
|
+
case TokenType.LET:
|
|
25
|
+
case TokenType.CONST:
|
|
26
|
+
case TokenType.LOCAL:
|
|
27
|
+
return this.parseVariableDecl();
|
|
28
|
+
case TokenType.IF:
|
|
29
|
+
return this.parseIfStmt();
|
|
30
|
+
case TokenType.FOR:
|
|
31
|
+
return this.parseForStmt();
|
|
32
|
+
case TokenType.WHILE:
|
|
33
|
+
return this.parseWhileStmt();
|
|
34
|
+
case TokenType.RETURN:
|
|
35
|
+
return this.parseReturnStmt();
|
|
36
|
+
case TokenType.BREAK:
|
|
37
|
+
this.eat();
|
|
38
|
+
return { kind: AST.NodeType.BreakStmt };
|
|
39
|
+
case TokenType.CONTINUE:
|
|
40
|
+
this.eat();
|
|
41
|
+
return { kind: AST.NodeType.ContinueStmt };
|
|
42
|
+
case TokenType.GUARD:
|
|
43
|
+
return this.parseGuardStmt();
|
|
44
|
+
case TokenType.SAFE:
|
|
45
|
+
return this.parseSafeCall();
|
|
46
|
+
case TokenType.THREAD:
|
|
47
|
+
return this.parseThreadStmt();
|
|
48
|
+
case TokenType.LOOP:
|
|
49
|
+
return this.parseLoopStmt();
|
|
50
|
+
case TokenType.WAIT:
|
|
51
|
+
return this.parseWaitStmt();
|
|
52
|
+
case TokenType.TIMEOUT:
|
|
53
|
+
return this.parseTimeoutStmt();
|
|
54
|
+
case TokenType.INTERVAL:
|
|
55
|
+
return this.parseIntervalStmt();
|
|
56
|
+
case TokenType.TRY:
|
|
57
|
+
return this.parseTryCatch();
|
|
58
|
+
case TokenType.EMIT:
|
|
59
|
+
case TokenType.EMIT_CLIENT:
|
|
60
|
+
case TokenType.EMIT_SERVER:
|
|
61
|
+
return this.parseEmitStmt();
|
|
62
|
+
case TokenType.EVENT:
|
|
63
|
+
case TokenType.NETEVENT:
|
|
64
|
+
return this.parseEventHandler();
|
|
65
|
+
case TokenType.FUNCTION:
|
|
66
|
+
case TokenType.FUNC:
|
|
67
|
+
return this.parseFunctionDecl();
|
|
68
|
+
case TokenType.SWITCH:
|
|
69
|
+
return this.parseSwitchStmt();
|
|
70
|
+
case TokenType.EXPORT:
|
|
71
|
+
return this.parseExportDecl();
|
|
72
|
+
case TokenType.COMMAND:
|
|
73
|
+
return this.parseCommandStmt();
|
|
74
|
+
default:
|
|
75
|
+
return this.parseExpressionStatement();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
parseVariableDecl() {
|
|
79
|
+
const scopeToken = this.eat();
|
|
80
|
+
const scope = scopeToken.value;
|
|
81
|
+
// Handle 'local function' syntax (Lua compatibility)
|
|
82
|
+
if (scope === "local" && (this.at().type === TokenType.FUNCTION || this.at().type === TokenType.FUNC)) {
|
|
83
|
+
// Treat 'local function x()' as FunctionDecl with isLocal=true
|
|
84
|
+
const funcDecl = this.parseFunctionDecl(true);
|
|
85
|
+
return funcDecl;
|
|
86
|
+
}
|
|
87
|
+
const names = [];
|
|
88
|
+
const typeAnnotations = [];
|
|
89
|
+
const parseName = () => {
|
|
90
|
+
if (this.at().type === TokenType.LBRACE)
|
|
91
|
+
return this.parseObjectDestructure();
|
|
92
|
+
if (this.at().type === TokenType.LBRACKET)
|
|
93
|
+
return this.parseArrayDestructure();
|
|
94
|
+
const id = this.parseIdentifier();
|
|
95
|
+
// Lua 5.4 Attributes: <const>, <close>
|
|
96
|
+
if (this.match(TokenType.LT)) {
|
|
97
|
+
// Consume the attribute name (usually 'const' or 'close')
|
|
98
|
+
const attr = this.eat().value;
|
|
99
|
+
id.attribute = attr;
|
|
100
|
+
this.expect(TokenType.GT);
|
|
101
|
+
}
|
|
102
|
+
return id;
|
|
103
|
+
};
|
|
104
|
+
names.push(parseName());
|
|
105
|
+
if (this.match(TokenType.COLON)) {
|
|
106
|
+
typeAnnotations[0] = this.eat().value;
|
|
107
|
+
}
|
|
108
|
+
while (this.match(TokenType.COMMA)) {
|
|
109
|
+
names.push(parseName());
|
|
110
|
+
if (this.match(TokenType.COLON)) {
|
|
111
|
+
typeAnnotations[names.length - 1] = this.eat().value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let values;
|
|
115
|
+
if (this.match(TokenType.EQUALS)) {
|
|
116
|
+
values = [];
|
|
117
|
+
values.push(this.parseExpression(true));
|
|
118
|
+
while (this.match(TokenType.COMMA)) {
|
|
119
|
+
values.push(this.parseExpression(true));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { kind: AST.NodeType.VariableDecl, scope, names, typeAnnotations, values };
|
|
123
|
+
}
|
|
124
|
+
parseIfStmt() {
|
|
125
|
+
this.eat(); // if
|
|
126
|
+
const condition = this.parseExpression();
|
|
127
|
+
this.expect(TokenType.THEN);
|
|
128
|
+
const thenBody = this.parseBlockUntil(TokenType.END, TokenType.ELSE, TokenType.ELSEIF);
|
|
129
|
+
const elseIfClauses = [];
|
|
130
|
+
while (this.match(TokenType.ELSEIF)) {
|
|
131
|
+
const cond = this.parseExpression();
|
|
132
|
+
this.expect(TokenType.THEN);
|
|
133
|
+
const body = this.parseBlockUntil(TokenType.END, TokenType.ELSE, TokenType.ELSEIF);
|
|
134
|
+
elseIfClauses.push({ condition: cond, body });
|
|
135
|
+
}
|
|
136
|
+
let elseBody;
|
|
137
|
+
if (this.match(TokenType.ELSE)) {
|
|
138
|
+
elseBody = this.parseBlockUntil(TokenType.END);
|
|
139
|
+
}
|
|
140
|
+
this.expect(TokenType.END);
|
|
141
|
+
return { kind: AST.NodeType.IfStmt, condition, thenBody, elseIfClauses, elseBody };
|
|
142
|
+
}
|
|
143
|
+
parseForStmt() {
|
|
144
|
+
this.eat(); // for
|
|
145
|
+
const iterators = [];
|
|
146
|
+
iterators.push(this.parseIdentifier());
|
|
147
|
+
while (this.match(TokenType.COMMA)) {
|
|
148
|
+
iterators.push(this.parseIdentifier());
|
|
149
|
+
}
|
|
150
|
+
if (this.match(TokenType.IN) || (iterators.length > 1 && this.at().type !== TokenType.EQUALS)) {
|
|
151
|
+
if (this.at().type !== TokenType.IN && this.at().type !== TokenType.DO) {
|
|
152
|
+
this.match(TokenType.IN); // Optional IN in some LT contexts
|
|
153
|
+
}
|
|
154
|
+
// Check for Range For: i in 1..10
|
|
155
|
+
const checkpoint = this.pos;
|
|
156
|
+
if (iterators.length === 1) {
|
|
157
|
+
try {
|
|
158
|
+
const start = this.parseAdditive();
|
|
159
|
+
if (this.at().type === TokenType.CONCAT) {
|
|
160
|
+
this.eat(); // ..
|
|
161
|
+
const end = this.parseAdditive();
|
|
162
|
+
let step;
|
|
163
|
+
if (this.match(TokenType.BY)) {
|
|
164
|
+
step = this.parseAdditive();
|
|
165
|
+
}
|
|
166
|
+
this.expect(TokenType.DO);
|
|
167
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
168
|
+
this.expect(TokenType.END);
|
|
169
|
+
return { kind: AST.NodeType.RangeForStmt, counter: iterators[0], start, end, step, body };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
// Not a range, fallback
|
|
174
|
+
}
|
|
175
|
+
this.pos = checkpoint;
|
|
176
|
+
}
|
|
177
|
+
// Generic For
|
|
178
|
+
const iterable = this.parseExpression();
|
|
179
|
+
this.expect(TokenType.DO);
|
|
180
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
181
|
+
this.expect(TokenType.END);
|
|
182
|
+
return { kind: AST.NodeType.ForStmt, iterators, iterable, body };
|
|
183
|
+
}
|
|
184
|
+
// Numeric For: for i = 1, 10, 1 do
|
|
185
|
+
if (iterators.length > 1) {
|
|
186
|
+
const tk = this.at();
|
|
187
|
+
throw new Error(`Numeric for loops (e.g., 'for i = 1, 10') only support a single iterator at ${tk.line}:${tk.column}.`);
|
|
188
|
+
}
|
|
189
|
+
const iterator = iterators[0];
|
|
190
|
+
this.expect(TokenType.EQUALS);
|
|
191
|
+
const start = this.parseExpression();
|
|
192
|
+
this.expect(TokenType.COMMA);
|
|
193
|
+
const end = this.parseExpression();
|
|
194
|
+
let step;
|
|
195
|
+
if (this.match(TokenType.COMMA)) {
|
|
196
|
+
step = this.parseExpression();
|
|
197
|
+
}
|
|
198
|
+
this.expect(TokenType.DO);
|
|
199
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
200
|
+
this.expect(TokenType.END);
|
|
201
|
+
return { kind: AST.NodeType.RangeForStmt, counter: iterator, start, end, step, body };
|
|
202
|
+
}
|
|
203
|
+
parseWhileStmt() {
|
|
204
|
+
this.eat(); // while
|
|
205
|
+
const condition = this.parseExpression();
|
|
206
|
+
this.expect(TokenType.DO);
|
|
207
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
208
|
+
this.expect(TokenType.END);
|
|
209
|
+
return { kind: AST.NodeType.WhileStmt, condition, body };
|
|
210
|
+
}
|
|
211
|
+
parseReturnStmt() {
|
|
212
|
+
this.eat(); // return
|
|
213
|
+
let values;
|
|
214
|
+
if (!this.isStatementEnd()) {
|
|
215
|
+
values = [];
|
|
216
|
+
values.push(this.parseExpression());
|
|
217
|
+
while (this.match(TokenType.COMMA)) {
|
|
218
|
+
values.push(this.parseExpression());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { kind: AST.NodeType.ReturnStmt, values };
|
|
222
|
+
}
|
|
223
|
+
parseGuardStmt() {
|
|
224
|
+
this.eat(); // guard
|
|
225
|
+
const condition = this.parseExpression();
|
|
226
|
+
let elseBody;
|
|
227
|
+
if (this.match(TokenType.ELSE)) {
|
|
228
|
+
elseBody = [];
|
|
229
|
+
while (!this.check(TokenType.RETURN) && !this.isEOF()) {
|
|
230
|
+
elseBody.push(this.parseStatement());
|
|
231
|
+
}
|
|
232
|
+
this.expect(TokenType.RETURN);
|
|
233
|
+
this.match(TokenType.END); // Consume optional END
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.expect(TokenType.RETURN);
|
|
237
|
+
}
|
|
238
|
+
return { kind: AST.NodeType.GuardStmt, condition, elseBody };
|
|
239
|
+
}
|
|
240
|
+
parseSafeCall() {
|
|
241
|
+
this.eat(); // safe
|
|
242
|
+
const call = this.parseCallExpr();
|
|
243
|
+
return { kind: AST.NodeType.SafeCallStmt, call };
|
|
244
|
+
}
|
|
245
|
+
parseThreadStmt() {
|
|
246
|
+
this.eat(); // thread
|
|
247
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
248
|
+
this.expect(TokenType.END);
|
|
249
|
+
return { kind: AST.NodeType.ThreadStmt, body };
|
|
250
|
+
}
|
|
251
|
+
parseLoopStmt() {
|
|
252
|
+
this.eat(); // loop
|
|
253
|
+
this.expect(TokenType.LPAREN);
|
|
254
|
+
const conditions = [];
|
|
255
|
+
conditions.push(this.parseExpression());
|
|
256
|
+
while (this.match(TokenType.COMMA)) {
|
|
257
|
+
conditions.push(this.parseExpression());
|
|
258
|
+
}
|
|
259
|
+
this.expect(TokenType.RPAREN);
|
|
260
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
261
|
+
this.expect(TokenType.END);
|
|
262
|
+
return { kind: AST.NodeType.LoopStmt, conditions, body };
|
|
263
|
+
}
|
|
264
|
+
parseWaitStmt() {
|
|
265
|
+
this.eat(); // wait
|
|
266
|
+
const time = this.parseExpression();
|
|
267
|
+
return { kind: AST.NodeType.WaitStmt, time };
|
|
268
|
+
}
|
|
269
|
+
parseTimeoutStmt() {
|
|
270
|
+
this.eat(); // timeout
|
|
271
|
+
const delay = this.parseExpression();
|
|
272
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
273
|
+
this.expect(TokenType.END);
|
|
274
|
+
return { kind: AST.NodeType.TimeoutStmt, delay, body };
|
|
275
|
+
}
|
|
276
|
+
parseIntervalStmt() {
|
|
277
|
+
this.eat(); // interval
|
|
278
|
+
const interval = this.parseExpression();
|
|
279
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
280
|
+
this.expect(TokenType.END);
|
|
281
|
+
return { kind: AST.NodeType.IntervalStmt, interval, body };
|
|
282
|
+
}
|
|
283
|
+
parseTryCatch() {
|
|
284
|
+
this.eat(); // try
|
|
285
|
+
const tryBody = this.parseBlockUntil(TokenType.CATCH);
|
|
286
|
+
this.expect(TokenType.CATCH);
|
|
287
|
+
const catchParam = this.parseIdentifier();
|
|
288
|
+
const catchBody = this.parseBlockUntil(TokenType.END);
|
|
289
|
+
this.expect(TokenType.END);
|
|
290
|
+
return { kind: AST.NodeType.TryCatchStmt, tryBody, catchParam, catchBody };
|
|
291
|
+
}
|
|
292
|
+
parseEmitStmt() {
|
|
293
|
+
const token = this.eat();
|
|
294
|
+
const emitType = token.type === TokenType.EMIT ? "emit" :
|
|
295
|
+
token.type === TokenType.EMIT_CLIENT ? "emitClient" : "emitServer";
|
|
296
|
+
// Only parse primary to avoid consuming parens as a call
|
|
297
|
+
const eventName = this.parsePrimary();
|
|
298
|
+
const args = [];
|
|
299
|
+
if (this.match(TokenType.LPAREN)) {
|
|
300
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
301
|
+
args.push(this.parseExpression());
|
|
302
|
+
while (this.match(TokenType.COMMA)) {
|
|
303
|
+
args.push(this.parseExpression());
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.expect(TokenType.RPAREN);
|
|
307
|
+
}
|
|
308
|
+
return { kind: AST.NodeType.EmitStmt, emitType, eventName, args };
|
|
309
|
+
}
|
|
310
|
+
parseEventHandler() {
|
|
311
|
+
const isNet = this.eat().type === TokenType.NETEVENT;
|
|
312
|
+
// Only parse primary to avoid consuming parens as a call
|
|
313
|
+
const eventName = this.parsePrimary();
|
|
314
|
+
this.expect(TokenType.LPAREN);
|
|
315
|
+
const params = [];
|
|
316
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
317
|
+
params.push(this.parseParameter());
|
|
318
|
+
while (this.match(TokenType.COMMA)) {
|
|
319
|
+
params.push(this.parseParameter());
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
this.expect(TokenType.RPAREN);
|
|
323
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
324
|
+
this.expect(TokenType.END);
|
|
325
|
+
return { kind: AST.NodeType.EventHandler, isNet, eventName, params, body };
|
|
326
|
+
}
|
|
327
|
+
parseFunctionDecl(isLocal = false) {
|
|
328
|
+
this.eat(); // function or func
|
|
329
|
+
let name;
|
|
330
|
+
let current = this.parseIdentifier();
|
|
331
|
+
while (this.match(TokenType.DOT) || this.match(TokenType.COLON)) {
|
|
332
|
+
const isMethod = this.tokens[this.pos - 1].type === TokenType.COLON;
|
|
333
|
+
const property = this.parseIdentifier();
|
|
334
|
+
current = {
|
|
335
|
+
kind: AST.NodeType.MemberExpr,
|
|
336
|
+
object: current,
|
|
337
|
+
property,
|
|
338
|
+
computed: false,
|
|
339
|
+
isMethod
|
|
340
|
+
};
|
|
341
|
+
if (isMethod)
|
|
342
|
+
break; // Colon must be the last part
|
|
343
|
+
}
|
|
344
|
+
name = current;
|
|
345
|
+
this.expect(TokenType.LPAREN);
|
|
346
|
+
const params = [];
|
|
347
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
348
|
+
params.push(this.parseParameter());
|
|
349
|
+
while (this.match(TokenType.COMMA)) {
|
|
350
|
+
params.push(this.parseParameter());
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
this.expect(TokenType.RPAREN);
|
|
354
|
+
let returnType;
|
|
355
|
+
if (this.match(TokenType.COLON)) {
|
|
356
|
+
returnType = this.eat().value;
|
|
357
|
+
}
|
|
358
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
359
|
+
this.expect(TokenType.END);
|
|
360
|
+
return { kind: AST.NodeType.FunctionDecl, name, params, returnType, body, isLocal };
|
|
361
|
+
}
|
|
362
|
+
parseFunctionExpr() {
|
|
363
|
+
this.eat(); // function or func
|
|
364
|
+
let name;
|
|
365
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
366
|
+
name = this.parseIdentifier();
|
|
367
|
+
}
|
|
368
|
+
this.expect(TokenType.LPAREN);
|
|
369
|
+
const params = [];
|
|
370
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
371
|
+
params.push(this.parseParameter());
|
|
372
|
+
while (this.match(TokenType.COMMA)) {
|
|
373
|
+
params.push(this.parseParameter());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
this.expect(TokenType.RPAREN);
|
|
377
|
+
let returnType;
|
|
378
|
+
if (this.match(TokenType.COLON)) {
|
|
379
|
+
returnType = this.eat().value;
|
|
380
|
+
}
|
|
381
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
382
|
+
this.expect(TokenType.END);
|
|
383
|
+
// Function expressions are anonymous or named local-ish values, usually without 'local' keyword inside the expr itself
|
|
384
|
+
// but the AST node structure reuses FunctionDecl.
|
|
385
|
+
return { kind: AST.NodeType.FunctionDecl, name, params, returnType, body };
|
|
386
|
+
}
|
|
387
|
+
parseSwitchStmt() {
|
|
388
|
+
this.eat(); // switch
|
|
389
|
+
const discriminant = this.parseExpression();
|
|
390
|
+
const cases = [];
|
|
391
|
+
while (this.match(TokenType.CASE)) {
|
|
392
|
+
const values = [];
|
|
393
|
+
values.push(this.parseExpression());
|
|
394
|
+
while (this.match(TokenType.COMMA)) {
|
|
395
|
+
values.push(this.parseExpression());
|
|
396
|
+
}
|
|
397
|
+
const body = this.parseBlockUntil(TokenType.CASE, TokenType.DEFAULT, TokenType.END);
|
|
398
|
+
cases.push({ values, body });
|
|
399
|
+
}
|
|
400
|
+
let defaultCase;
|
|
401
|
+
if (this.match(TokenType.DEFAULT)) {
|
|
402
|
+
defaultCase = this.parseBlockUntil(TokenType.END);
|
|
403
|
+
}
|
|
404
|
+
this.expect(TokenType.END);
|
|
405
|
+
return { kind: AST.NodeType.SwitchStmt, discriminant, cases, defaultCase };
|
|
406
|
+
}
|
|
407
|
+
parseCommandStmt() {
|
|
408
|
+
this.eat(); // command
|
|
409
|
+
let nameVal;
|
|
410
|
+
if (this.at().type === TokenType.STRING) {
|
|
411
|
+
nameVal = this.eat().value;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
nameVal = this.parseIdentifier().name;
|
|
415
|
+
}
|
|
416
|
+
this.expect(TokenType.LPAREN);
|
|
417
|
+
const params = [];
|
|
418
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
419
|
+
params.push(this.parseParameter());
|
|
420
|
+
while (this.match(TokenType.COMMA)) {
|
|
421
|
+
params.push(this.parseParameter());
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.expect(TokenType.RPAREN);
|
|
425
|
+
const body = this.parseBlockUntil(TokenType.END);
|
|
426
|
+
this.expect(TokenType.END);
|
|
427
|
+
return { kind: AST.NodeType.CommandStmt, commandName: nameVal, params, body };
|
|
428
|
+
}
|
|
429
|
+
parseExportDecl() {
|
|
430
|
+
this.eat(); // export
|
|
431
|
+
if (this.at().type === TokenType.FUNCTION || this.at().type === TokenType.FUNC) {
|
|
432
|
+
const decl = this.parseFunctionDecl();
|
|
433
|
+
return { kind: AST.NodeType.ExportDecl, declaration: decl };
|
|
434
|
+
}
|
|
435
|
+
throw new Error(`Expected function declaration after export at ${this.at().line}:${this.at().column}`);
|
|
436
|
+
}
|
|
437
|
+
parseExpressionStatement() {
|
|
438
|
+
const expr = this.parseExpression();
|
|
439
|
+
// Compound assignment
|
|
440
|
+
if (this.match(TokenType.PLUS_EQ)) {
|
|
441
|
+
const value = this.parseExpression();
|
|
442
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "+=", value };
|
|
443
|
+
}
|
|
444
|
+
if (this.match(TokenType.MINUS_EQ)) {
|
|
445
|
+
const value = this.parseExpression();
|
|
446
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "-=", value };
|
|
447
|
+
}
|
|
448
|
+
if (this.match(TokenType.STAR_EQ)) {
|
|
449
|
+
const value = this.parseExpression();
|
|
450
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "*=", value };
|
|
451
|
+
}
|
|
452
|
+
if (this.match(TokenType.SLASH_EQ)) {
|
|
453
|
+
const value = this.parseExpression();
|
|
454
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "/=", value };
|
|
455
|
+
}
|
|
456
|
+
if (this.match(TokenType.PERCENT_EQ)) {
|
|
457
|
+
const value = this.parseExpression();
|
|
458
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "%=", value };
|
|
459
|
+
}
|
|
460
|
+
if (this.match(TokenType.CONCAT_EQ)) {
|
|
461
|
+
const value = this.parseExpression();
|
|
462
|
+
return { kind: AST.NodeType.CompoundAssignment, target: expr, operator: "..=", value };
|
|
463
|
+
}
|
|
464
|
+
// Multiple targets: a, b = 1, 2
|
|
465
|
+
if (this.at().type === TokenType.COMMA) {
|
|
466
|
+
const targets = [expr];
|
|
467
|
+
while (this.match(TokenType.COMMA)) {
|
|
468
|
+
targets.push(this.parseExpression());
|
|
469
|
+
}
|
|
470
|
+
this.expect(TokenType.EQUALS);
|
|
471
|
+
const values = [];
|
|
472
|
+
values.push(this.parseExpression());
|
|
473
|
+
while (this.match(TokenType.COMMA)) {
|
|
474
|
+
values.push(this.parseExpression());
|
|
475
|
+
}
|
|
476
|
+
return { kind: AST.NodeType.AssignmentStmt, targets, values };
|
|
477
|
+
}
|
|
478
|
+
// Regular assignment
|
|
479
|
+
if (this.match(TokenType.EQUALS)) {
|
|
480
|
+
const value = this.parseExpression();
|
|
481
|
+
return { kind: AST.NodeType.AssignmentStmt, targets: [expr], values: [value] };
|
|
482
|
+
}
|
|
483
|
+
return expr;
|
|
484
|
+
}
|
|
485
|
+
// ============ Expression Parsing ============
|
|
486
|
+
parseExpression(allowColon = true) {
|
|
487
|
+
return this.parseAssignment(allowColon);
|
|
488
|
+
}
|
|
489
|
+
parseAssignment(allowColon = true) {
|
|
490
|
+
let left = this.parseTernaryExpr(allowColon);
|
|
491
|
+
return left;
|
|
492
|
+
}
|
|
493
|
+
parseTernaryExpr(allowColon = true) {
|
|
494
|
+
let left = this.parseNullCoalesce(allowColon);
|
|
495
|
+
if (allowColon && this.match(TokenType.QUESTION)) {
|
|
496
|
+
const consequent = this.parseExpression(false);
|
|
497
|
+
this.expect(TokenType.COLON);
|
|
498
|
+
const alternate = this.parseExpression(allowColon);
|
|
499
|
+
return {
|
|
500
|
+
kind: AST.NodeType.ConditionalExpr,
|
|
501
|
+
test: left,
|
|
502
|
+
consequent,
|
|
503
|
+
alternate
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return left;
|
|
507
|
+
}
|
|
508
|
+
parseNullCoalesce(allowColon = true) {
|
|
509
|
+
let left = this.parseOr(allowColon);
|
|
510
|
+
while (this.match(TokenType.NULL_COALESCE)) {
|
|
511
|
+
const right = this.parseOr(allowColon);
|
|
512
|
+
left = { kind: AST.NodeType.NullCoalesceExpr, left, right };
|
|
513
|
+
}
|
|
514
|
+
return left;
|
|
515
|
+
}
|
|
516
|
+
parseOr(allowColon = true) {
|
|
517
|
+
let left = this.parseAnd(allowColon);
|
|
518
|
+
while (this.match(TokenType.OR)) {
|
|
519
|
+
const right = this.parseAnd(allowColon);
|
|
520
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: "or", right };
|
|
521
|
+
}
|
|
522
|
+
return left;
|
|
523
|
+
}
|
|
524
|
+
parseAnd(allowColon = true) {
|
|
525
|
+
let left = this.parseEquality(allowColon);
|
|
526
|
+
while (this.match(TokenType.AND)) {
|
|
527
|
+
const right = this.parseEquality(allowColon);
|
|
528
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: "and", right };
|
|
529
|
+
}
|
|
530
|
+
return left;
|
|
531
|
+
}
|
|
532
|
+
parseEquality(allowColon = true) {
|
|
533
|
+
let left = this.parseComparison(allowColon);
|
|
534
|
+
while (this.check(TokenType.EQ) || this.check(TokenType.NEQ)) {
|
|
535
|
+
const op = this.eat().value;
|
|
536
|
+
const right = this.parseComparison(allowColon);
|
|
537
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: op, right };
|
|
538
|
+
}
|
|
539
|
+
return left;
|
|
540
|
+
}
|
|
541
|
+
parseComparison(allowColon = true) {
|
|
542
|
+
let left = this.parseConcat(allowColon);
|
|
543
|
+
while (this.check(TokenType.LT) || this.check(TokenType.GT) ||
|
|
544
|
+
this.check(TokenType.LTE) || this.check(TokenType.GTE)) {
|
|
545
|
+
const op = this.eat().value;
|
|
546
|
+
const right = this.parseConcat(allowColon);
|
|
547
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: op, right };
|
|
548
|
+
}
|
|
549
|
+
return left;
|
|
550
|
+
}
|
|
551
|
+
parseConcat(allowColon = true) {
|
|
552
|
+
let left = this.parseAdditive(allowColon);
|
|
553
|
+
while (this.check(TokenType.CONCAT)) {
|
|
554
|
+
this.eat();
|
|
555
|
+
const right = this.parseAdditive(allowColon);
|
|
556
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: "..", right };
|
|
557
|
+
}
|
|
558
|
+
return left;
|
|
559
|
+
}
|
|
560
|
+
parseAdditive(allowColon = true) {
|
|
561
|
+
let left = this.parseMultiplicative(allowColon);
|
|
562
|
+
while (this.check(TokenType.PLUS) || this.check(TokenType.MINUS)) {
|
|
563
|
+
const op = this.eat().value;
|
|
564
|
+
const right = this.parseMultiplicative(allowColon);
|
|
565
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: op, right };
|
|
566
|
+
}
|
|
567
|
+
return left;
|
|
568
|
+
}
|
|
569
|
+
parseMultiplicative(allowColon = true) {
|
|
570
|
+
let left = this.parseUnary(allowColon);
|
|
571
|
+
while (this.check(TokenType.STAR) || this.check(TokenType.SLASH) || this.check(TokenType.PERCENT)) {
|
|
572
|
+
const op = this.eat().value;
|
|
573
|
+
const right = this.parseUnary(allowColon);
|
|
574
|
+
left = { kind: AST.NodeType.BinaryExpr, left, operator: op, right };
|
|
575
|
+
}
|
|
576
|
+
return left;
|
|
577
|
+
}
|
|
578
|
+
parseUnary(allowColon = true) {
|
|
579
|
+
if (this.check(TokenType.NOT) || this.check(TokenType.MINUS) || this.check(TokenType.HASH)) {
|
|
580
|
+
const op = this.eat().value;
|
|
581
|
+
const operand = this.parseUnary(allowColon);
|
|
582
|
+
return { kind: AST.NodeType.UnaryExpr, operator: op, operand };
|
|
583
|
+
}
|
|
584
|
+
return this.parseCallMember(allowColon);
|
|
585
|
+
}
|
|
586
|
+
parseCallMember(allowColon = true) {
|
|
587
|
+
let expr = this.parsePrimary();
|
|
588
|
+
while (true) {
|
|
589
|
+
if (this.match(TokenType.OPT_DOT)) {
|
|
590
|
+
const property = this.parseIdentifier();
|
|
591
|
+
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: false };
|
|
592
|
+
}
|
|
593
|
+
else if (this.match(TokenType.OPT_BRACKET)) {
|
|
594
|
+
const property = this.parseExpression(true);
|
|
595
|
+
this.expect(TokenType.RBRACKET);
|
|
596
|
+
expr = { kind: AST.NodeType.OptionalChainExpr, object: expr, property, computed: true };
|
|
597
|
+
}
|
|
598
|
+
else if (this.match(TokenType.DOT)) {
|
|
599
|
+
const property = this.parseIdentifier();
|
|
600
|
+
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false };
|
|
601
|
+
}
|
|
602
|
+
else if (allowColon && this.match(TokenType.COLON)) {
|
|
603
|
+
const property = this.parseIdentifier();
|
|
604
|
+
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: false, isMethod: true };
|
|
605
|
+
}
|
|
606
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
607
|
+
const property = this.parseExpression(true);
|
|
608
|
+
this.expect(TokenType.RBRACKET);
|
|
609
|
+
expr = { kind: AST.NodeType.MemberExpr, object: expr, property, computed: true };
|
|
610
|
+
}
|
|
611
|
+
else if (this.match(TokenType.LPAREN)) {
|
|
612
|
+
const args = [];
|
|
613
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
614
|
+
args.push(this.parseExpression(true));
|
|
615
|
+
while (this.match(TokenType.COMMA)) {
|
|
616
|
+
args.push(this.parseExpression(true));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
this.expect(TokenType.RPAREN);
|
|
620
|
+
expr = { kind: AST.NodeType.CallExpr, callee: expr, args };
|
|
621
|
+
}
|
|
622
|
+
else if (this.match(TokenType.PLUS_PLUS)) {
|
|
623
|
+
expr = { kind: AST.NodeType.UpdateExpr, operator: "++", argument: expr, prefix: false };
|
|
624
|
+
}
|
|
625
|
+
else if (this.match(TokenType.MINUS_MINUS)) {
|
|
626
|
+
expr = { kind: AST.NodeType.UpdateExpr, operator: "--", argument: expr, prefix: false };
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return expr;
|
|
633
|
+
}
|
|
634
|
+
parseCallExpr() {
|
|
635
|
+
return this.parseCallMember();
|
|
636
|
+
}
|
|
637
|
+
parsePrimary() {
|
|
638
|
+
const tk = this.at();
|
|
639
|
+
switch (tk.type) {
|
|
640
|
+
case TokenType.FUNCTION:
|
|
641
|
+
case TokenType.FUNC:
|
|
642
|
+
return this.parseFunctionExpr();
|
|
643
|
+
case TokenType.NUMBER:
|
|
644
|
+
return { kind: AST.NodeType.NumberLiteral, value: parseFloat(this.eat().value) };
|
|
645
|
+
case TokenType.STRING:
|
|
646
|
+
case TokenType.LONG_STRING:
|
|
647
|
+
return this.parseStringLiteral();
|
|
648
|
+
case TokenType.BOOLEAN:
|
|
649
|
+
return { kind: AST.NodeType.BooleanLiteral, value: this.eat().value === "true" };
|
|
650
|
+
case TokenType.NIL:
|
|
651
|
+
this.eat();
|
|
652
|
+
return { kind: AST.NodeType.NilLiteral };
|
|
653
|
+
case TokenType.IDENTIFIER:
|
|
654
|
+
return this.parseIdentifier();
|
|
655
|
+
case TokenType.LPAREN:
|
|
656
|
+
return this.parseParenOrArrow();
|
|
657
|
+
case TokenType.LBRACE:
|
|
658
|
+
return this.parseTableLiteral();
|
|
659
|
+
case TokenType.LBRACKET:
|
|
660
|
+
return this.parseArrayLiteral();
|
|
661
|
+
case TokenType.LT:
|
|
662
|
+
return this.parseVectorLiteral();
|
|
663
|
+
default:
|
|
664
|
+
throw new Error(`Unexpected token: ${tk.type} at ${tk.line}:${tk.column}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
parseStringLiteral() {
|
|
668
|
+
const tk = this.eat();
|
|
669
|
+
const value = tk.value;
|
|
670
|
+
const isLong = tk.type === TokenType.LONG_STRING;
|
|
671
|
+
const quote = tk.quote || '"'; // Default to double quote for long strings
|
|
672
|
+
// Check for interpolation only if $ is followed by a valid identifier start (letter or _)
|
|
673
|
+
// This prevents SQL JSON paths like "$.bank" from being treated as interpolation
|
|
674
|
+
const hasInterpolation = /\$[a-zA-Z_]/.test(value);
|
|
675
|
+
if (hasInterpolation) {
|
|
676
|
+
return this.parseInterpolatedString(value, quote);
|
|
677
|
+
}
|
|
678
|
+
return { kind: AST.NodeType.StringLiteral, value, isLong, quote };
|
|
679
|
+
}
|
|
680
|
+
parseInterpolatedString(str, quote = '"') {
|
|
681
|
+
const parts = [];
|
|
682
|
+
const regex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
683
|
+
let lastIndex = 0;
|
|
684
|
+
let match;
|
|
685
|
+
while ((match = regex.exec(str)) !== null) {
|
|
686
|
+
if (match.index > lastIndex) {
|
|
687
|
+
parts.push(str.slice(lastIndex, match.index));
|
|
688
|
+
}
|
|
689
|
+
parts.push({ kind: AST.NodeType.Identifier, name: match[1] });
|
|
690
|
+
lastIndex = regex.lastIndex;
|
|
691
|
+
}
|
|
692
|
+
if (lastIndex < str.length) {
|
|
693
|
+
parts.push(str.slice(lastIndex));
|
|
694
|
+
}
|
|
695
|
+
return { kind: AST.NodeType.InterpolatedString, parts, quote };
|
|
696
|
+
}
|
|
697
|
+
parseParenOrArrow() {
|
|
698
|
+
const checkpoint = this.pos;
|
|
699
|
+
this.eat(); // (
|
|
700
|
+
// Try to parse as arrow function
|
|
701
|
+
try {
|
|
702
|
+
const params = [];
|
|
703
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
704
|
+
params.push(this.parseParameter());
|
|
705
|
+
while (this.match(TokenType.COMMA)) {
|
|
706
|
+
params.push(this.parseParameter());
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
this.expect(TokenType.RPAREN);
|
|
710
|
+
if (this.match(TokenType.ARROW)) {
|
|
711
|
+
let body;
|
|
712
|
+
if (this.check(TokenType.LBRACE)) {
|
|
713
|
+
this.eat();
|
|
714
|
+
body = this.parseBlockUntil(TokenType.RBRACE);
|
|
715
|
+
this.expect(TokenType.RBRACE);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
body = this.parseExpression();
|
|
719
|
+
}
|
|
720
|
+
return { kind: AST.NodeType.ArrowFunc, params, body };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// Not an arrow function, backtrack
|
|
725
|
+
}
|
|
726
|
+
// Fallback: parenthesized expression
|
|
727
|
+
this.pos = checkpoint;
|
|
728
|
+
this.eat(); // (
|
|
729
|
+
const expr = this.parseExpression();
|
|
730
|
+
this.expect(TokenType.RPAREN);
|
|
731
|
+
return expr;
|
|
732
|
+
}
|
|
733
|
+
parseArrayLiteral() {
|
|
734
|
+
this.eat(); // [
|
|
735
|
+
const fields = [];
|
|
736
|
+
while (!this.check(TokenType.RBRACKET) && !this.isEOF()) {
|
|
737
|
+
fields.push({ value: this.parseExpression() });
|
|
738
|
+
if (!this.match(TokenType.COMMA))
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
this.expect(TokenType.RBRACKET);
|
|
742
|
+
return { kind: AST.NodeType.TableLiteral, fields };
|
|
743
|
+
}
|
|
744
|
+
parseTableLiteral() {
|
|
745
|
+
this.eat(); // {
|
|
746
|
+
const fields = [];
|
|
747
|
+
while (!this.check(TokenType.RBRACE) && !this.isEOF()) {
|
|
748
|
+
if (this.match(TokenType.SPREAD)) {
|
|
749
|
+
const argument = this.parseExpression();
|
|
750
|
+
fields.push({ value: { kind: AST.NodeType.SpreadExpr, argument } });
|
|
751
|
+
}
|
|
752
|
+
else if (this.check(TokenType.LBRACKET)) {
|
|
753
|
+
// [key] = value
|
|
754
|
+
this.eat();
|
|
755
|
+
const key = this.parseExpression();
|
|
756
|
+
this.expect(TokenType.RBRACKET);
|
|
757
|
+
this.expect(TokenType.EQUALS);
|
|
758
|
+
const value = this.parseExpression();
|
|
759
|
+
fields.push({ key, value });
|
|
760
|
+
}
|
|
761
|
+
else if (this.check(TokenType.IDENTIFIER) && (this.peek().type === TokenType.COLON || this.peek().type === TokenType.EQUALS)) {
|
|
762
|
+
const id = this.parseIdentifier();
|
|
763
|
+
this.eat(); // : or =
|
|
764
|
+
const value = this.parseExpression();
|
|
765
|
+
fields.push({ key: id, value });
|
|
766
|
+
}
|
|
767
|
+
else if (this.check(TokenType.IDENTIFIER) && (this.peek().type === TokenType.COMMA || this.peek().type === TokenType.RBRACE)) {
|
|
768
|
+
// Shorthand: { x } => { x = x }
|
|
769
|
+
const id = this.parseIdentifier();
|
|
770
|
+
fields.push({ key: id, value: id, shorthand: true });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
const value = this.parseExpression();
|
|
774
|
+
fields.push({ value });
|
|
775
|
+
}
|
|
776
|
+
if (!this.match(TokenType.COMMA))
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
this.expect(TokenType.RBRACE);
|
|
780
|
+
return { kind: AST.NodeType.TableLiteral, fields };
|
|
781
|
+
}
|
|
782
|
+
parseVectorLiteral() {
|
|
783
|
+
this.eat(); // <
|
|
784
|
+
const components = [];
|
|
785
|
+
// Use parseConcat to avoid consuming > as comparison operator
|
|
786
|
+
components.push(this.parseConcat());
|
|
787
|
+
while (this.match(TokenType.COMMA)) {
|
|
788
|
+
components.push(this.parseConcat());
|
|
789
|
+
}
|
|
790
|
+
this.expect(TokenType.GT);
|
|
791
|
+
return { kind: AST.NodeType.VectorLiteral, components };
|
|
792
|
+
}
|
|
793
|
+
parseIdentifier() {
|
|
794
|
+
const token = this.expect(TokenType.IDENTIFIER);
|
|
795
|
+
return { kind: AST.NodeType.Identifier, name: token.value };
|
|
796
|
+
}
|
|
797
|
+
parseParameter() {
|
|
798
|
+
const name = this.parseIdentifier();
|
|
799
|
+
let typeAnnotation;
|
|
800
|
+
if (this.match(TokenType.COLON)) {
|
|
801
|
+
typeAnnotation = this.eat().value;
|
|
802
|
+
}
|
|
803
|
+
let defaultValue;
|
|
804
|
+
if (this.match(TokenType.EQUALS)) {
|
|
805
|
+
defaultValue = this.parseExpression();
|
|
806
|
+
}
|
|
807
|
+
return { name, typeAnnotation, defaultValue };
|
|
808
|
+
}
|
|
809
|
+
parseObjectDestructure() {
|
|
810
|
+
this.eat(); // {
|
|
811
|
+
const properties = [];
|
|
812
|
+
while (!this.check(TokenType.RBRACE)) {
|
|
813
|
+
properties.push(this.parseIdentifier());
|
|
814
|
+
if (!this.match(TokenType.COMMA))
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
this.expect(TokenType.RBRACE);
|
|
818
|
+
return { kind: AST.NodeType.ObjectDestructure, properties };
|
|
819
|
+
}
|
|
820
|
+
parseArrayDestructure() {
|
|
821
|
+
this.eat(); // [
|
|
822
|
+
const elements = [];
|
|
823
|
+
while (!this.check(TokenType.RBRACKET)) {
|
|
824
|
+
elements.push(this.parseIdentifier());
|
|
825
|
+
if (!this.match(TokenType.COMMA))
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
this.expect(TokenType.RBRACKET);
|
|
829
|
+
return { kind: AST.NodeType.ArrayDestructure, elements };
|
|
830
|
+
}
|
|
831
|
+
parseBlockUntil(...stopTokens) {
|
|
832
|
+
const statements = [];
|
|
833
|
+
while (!this.isEOF() && !stopTokens.some(t => this.check(t))) {
|
|
834
|
+
statements.push(this.parseStatement());
|
|
835
|
+
}
|
|
836
|
+
return { kind: AST.NodeType.Block, statements };
|
|
837
|
+
}
|
|
838
|
+
// ============ Helpers ============
|
|
839
|
+
at() {
|
|
840
|
+
return this.tokens[this.pos] ?? { type: TokenType.EOF, value: "", line: -1, column: -1 };
|
|
841
|
+
}
|
|
842
|
+
peek() {
|
|
843
|
+
return this.tokens[this.pos + 1] ?? { type: TokenType.EOF, value: "", line: -1, column: -1 };
|
|
844
|
+
}
|
|
845
|
+
eat() {
|
|
846
|
+
return this.tokens[this.pos++] ?? { type: TokenType.EOF, value: "", line: -1, column: -1 };
|
|
847
|
+
}
|
|
848
|
+
match(type) {
|
|
849
|
+
if (this.at().type === type) {
|
|
850
|
+
this.eat();
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
check(type) {
|
|
856
|
+
return this.at().type === type;
|
|
857
|
+
}
|
|
858
|
+
expect(type) {
|
|
859
|
+
const token = this.at();
|
|
860
|
+
if (token.type !== type) {
|
|
861
|
+
throw new Error(`Expected ${type} but got ${token.type} at ${token.line}:${token.column}`);
|
|
862
|
+
}
|
|
863
|
+
return this.eat();
|
|
864
|
+
}
|
|
865
|
+
isEOF() {
|
|
866
|
+
return this.at().type === TokenType.EOF;
|
|
867
|
+
}
|
|
868
|
+
isStatementEnd() {
|
|
869
|
+
const t = this.at().type;
|
|
870
|
+
return t === TokenType.END || t === TokenType.ELSE || t === TokenType.ELSEIF ||
|
|
871
|
+
t === TokenType.CATCH || t === TokenType.EOF;
|
|
872
|
+
}
|
|
873
|
+
}
|