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.
@@ -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
+ }