@xnoxs/flux-lang 3.2.1 → 3.3.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/src/parser.js DELETED
@@ -1,1708 +0,0 @@
1
- 'use strict';
2
-
3
- const { T } = require('./lexer');
4
-
5
- // Human-readable labels for token types used in error messages
6
- const TOKEN_LABEL = {
7
- COLON: '":"', COMMA: '","', DOT: '"."',
8
- LPAREN: '"("', RPAREN: '")"',
9
- LBRACKET: '"["', RBRACKET: '"]"',
10
- LBRACE: '"{"', RBRACE: '"}"',
11
- ARROW: '"->"', FATARROW: '"=>"',
12
- EQ: '"="', EQEQ: '"=="', NEQ: '"!="',
13
- PLUS: '"+"', MINUS: '"-"', STAR: '"*"',
14
- SLASH: '"/"', PERCENT: '"%"', STARSTAR: '"**"',
15
- LT: '"<"', GT: '">"', LTE: '"<="', GTE: '">="',
16
- PIPE: '"|>"', PIPEB: '"|"', DOTDOT: '".."',
17
- DOTDOTDOT: '"..."', QUESTION: '"?"', BANG: '"!"',
18
- NEWLINE: 'end of line',
19
- INDENT: 'indented block',
20
- DEDENT: 'end of block',
21
- EOF: 'end of file',
22
- IDENT: 'identifier',
23
- NUMBER: 'number',
24
- STRING: 'string',
25
- BOOL: 'true/false',
26
- NULL: 'null',
27
- FN: '"fn"', VAL: '"val"', VAR: '"var"',
28
- IF: '"if"', ELSE: '"else"', FOR: '"for"',
29
- WHILE: '"while"', IN: '"in"', RETURN: '"return"',
30
- CLASS: '"class"', NEW: '"new"', SELF: '"self"',
31
- IMPORT: '"import"', FROM: '"from"', EXPORT: '"export"',
32
- MATCH: '"match"', WHEN: '"when"',
33
- AND: '"and"', OR: '"or"', NOT: '"not"',
34
- ASYNC: '"async"', AWAIT:'"await"',
35
- TRY: '"try"', CATCH:'"catch"', FINALLY:'"finally"', THROW:'"throw"',
36
- };
37
-
38
- function tokenLabel(type, value) {
39
- if (TOKEN_LABEL[type]) return TOKEN_LABEL[type];
40
- if (value != null && value !== type) return `"${value}"`;
41
- return `"${type.toLowerCase()}"`;
42
- }
43
-
44
- class ParseError extends Error {
45
- constructor(msg, tok) {
46
- super(msg);
47
- this.name = 'ParseError';
48
- this.tok = tok;
49
- this.line = tok ? tok.line : null;
50
- this.col = tok ? tok.col : null;
51
- }
52
- }
53
-
54
- // ── "Did you mean?" suggestion engine ────────────────────────────────────────
55
-
56
- // All Flux keywords a user might misspell
57
- const FLUX_KEYWORDS = [
58
- 'val', 'var', 'fn', 'return', 'if', 'else', 'for', 'in', 'while', 'do',
59
- 'break', 'continue', 'match', 'when', 'class', 'extends', 'self', 'new',
60
- 'interface', 'implements', 'import', 'export', 'from', 'as', 'default',
61
- 'and', 'or', 'not', 'async', 'await', 'try', 'catch', 'finally', 'throw',
62
- 'type', 'enum', 'static', 'abstract', 'override', 'readonly',
63
- 'private', 'public', 'protected', 'true', 'false', 'null',
64
- ];
65
-
66
- // Common language cross-contamination: JS / Python / other → Flux
67
- const COMMON_ALIASES = {
68
- // JavaScript habits
69
- 'function': { fix: 'fn', note: 'Flux uses "fn" instead of "function"' },
70
- 'func': { fix: 'fn', note: 'Flux uses "fn" for functions' },
71
- 'def': { fix: 'fn', note: 'Flux uses "fn" for functions (not "def" like Python)' },
72
- 'const': { fix: 'val', note: 'Flux uses "val" for immutable bindings (not "const")' },
73
- 'let': { fix: 'var', note: 'Flux uses "var" for mutable bindings (not "let")' },
74
- 'elif': { fix: 'else if', note: 'Flux uses "else if" (not "elif" like Python)' },
75
- 'elsif': { fix: 'else if', note: 'Flux uses "else if" (not "elsif")' },
76
- 'elseif': { fix: 'else if', note: 'Flux uses "else if" as two separate keywords' },
77
- 'switch': { fix: 'match', note: 'Flux uses "match/when" instead of "switch/case"' },
78
- 'case': { fix: 'when', note: 'Flux uses "when" inside a "match" block' },
79
- 'foreach': { fix: 'for ... in', note: 'Flux uses "for item in collection:" syntax' },
80
- 'forEach': { fix: 'for ... in', note: 'Flux uses "for item in collection:" syntax' },
81
- 'lambda': { fix: 'fn', note: 'Flux uses "fn" or the "->" arrow for inline functions' },
82
- 'struct': { fix: 'class', note: 'Flux uses "class" for data types (no structs)' },
83
- 'interface': null, // valid Flux keyword
84
- 'print': null, // valid Flux builtin
85
- // Capitalisation mistakes
86
- 'Fn': { fix: 'fn', note: 'Flux keywords are lowercase' },
87
- 'FN': { fix: 'fn', note: 'Flux keywords are lowercase' },
88
- 'Val': { fix: 'val', note: 'Flux keywords are lowercase' },
89
- 'Var': { fix: 'var', note: 'Flux keywords are lowercase' },
90
- 'If': { fix: 'if', note: 'Flux keywords are lowercase' },
91
- 'Else': { fix: 'else', note: 'Flux keywords are lowercase' },
92
- 'For': { fix: 'for', note: 'Flux keywords are lowercase' },
93
- 'While': { fix: 'while', note: 'Flux keywords are lowercase' },
94
- 'Return': { fix: 'return', note: 'Flux keywords are lowercase' },
95
- 'Class': { fix: 'class', note: 'Flux keywords are lowercase' },
96
- 'Import': { fix: 'import', note: 'Flux keywords are lowercase' },
97
- 'Export': { fix: 'export', note: 'Flux keywords are lowercase' },
98
- 'New': { fix: 'new', note: 'Flux keywords are lowercase' },
99
- 'True': { fix: 'true', note: 'Flux keywords are lowercase' },
100
- 'False': { fix: 'false', note: 'Flux keywords are lowercase' },
101
- 'Null': { fix: 'null', note: 'Flux keywords are lowercase' },
102
- 'Async': { fix: 'async', note: 'Flux keywords are lowercase' },
103
- 'Await': { fix: 'await', note: 'Flux keywords are lowercase' },
104
- 'Match': { fix: 'match', note: 'Flux keywords are lowercase' },
105
- 'Try': { fix: 'try', note: 'Flux keywords are lowercase' },
106
- 'Catch': { fix: 'catch', note: 'Flux keywords are lowercase' },
107
- };
108
-
109
- // Levenshtein distance (max 2 — bail out early for performance)
110
- function levenshtein(a, b, maxDist = 2) {
111
- if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
112
- const m = a.length, n = b.length;
113
- const prev = new Uint8Array(n + 1);
114
- const curr = new Uint8Array(n + 1);
115
- for (let j = 0; j <= n; j++) prev[j] = j;
116
- for (let i = 1; i <= m; i++) {
117
- curr[0] = i;
118
- let rowMin = i;
119
- for (let j = 1; j <= n; j++) {
120
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
121
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
122
- if (curr[j] < rowMin) rowMin = curr[j];
123
- }
124
- if (rowMin > maxDist) return maxDist + 1;
125
- curr.copyWithin(0, 0, n + 1);
126
- prev.set(curr);
127
- }
128
- return prev[n];
129
- }
130
-
131
- // Returns a hint string if `word` looks like a misspelled Flux keyword, else null
132
- function suggestKeyword(word) {
133
- if (!word || word.length < 2) return null;
134
-
135
- // 1. Exact alias match (cross-language / capitalisation)
136
- if (Object.prototype.hasOwnProperty.call(COMMON_ALIASES, word)) {
137
- const alias = COMMON_ALIASES[word];
138
- if (!alias) return null; // explicitly marked as valid
139
- return `Did you mean "${alias.fix}"? ${alias.note}`;
140
- }
141
-
142
- // 2. Fuzzy match against all keywords (distance ≤ 2, prefer closer matches)
143
- let bestDist = 3;
144
- let bestKw = null;
145
- for (const kw of FLUX_KEYWORDS) {
146
- if (Math.abs(word.length - kw.length) > 2) continue;
147
- const d = levenshtein(word.toLowerCase(), kw, 2);
148
- if (d < bestDist) { bestDist = d; bestKw = kw; }
149
- }
150
- if (bestKw && bestDist <= 2 && word !== bestKw) {
151
- return `Did you mean "${bestKw}"?`;
152
- }
153
-
154
- return null;
155
- }
156
-
157
- // ── Parser ────────────────────────────────────────────────────────────────────
158
- class Parser {
159
- constructor(tokens) {
160
- this.tokens = tokens;
161
- this.pos = 0;
162
- }
163
-
164
- peek(n = 0) { return this.tokens[this.pos + n] || { type: T.EOF, value: null, line: 0, col: 0 }; }
165
- check(type) { return this.peek().type === type; }
166
- at(...types) { return types.includes(this.peek().type); }
167
-
168
- eat(type) {
169
- const tok = this.peek();
170
- if (tok.type !== type) {
171
- const exp = tokenLabel(type, null);
172
- const got = tokenLabel(tok.type, tok.value);
173
- const err = new ParseError(`Expected ${exp} but got ${got}`, tok);
174
- // If we got an identifier where a keyword was expected, check for typo
175
- if (tok.type === T.IDENT) {
176
- const hint = suggestKeyword(tok.value);
177
- if (hint) err.hint = hint;
178
- }
179
- throw err;
180
- }
181
- this.pos++;
182
- return tok;
183
- }
184
-
185
- maybe(type) { if (this.check(type)) { this.pos++; return true; } return false; }
186
- skip() { return this.tokens[this.pos++]; }
187
- skipNewlines() { while (this.check(T.NEWLINE)) this.pos++; }
188
- err(msg) { throw new ParseError(msg, this.peek()); }
189
-
190
- parseBlock() {
191
- this.eat(T.COLON);
192
- if (this.check(T.NEWLINE)) {
193
- this.pos++;
194
- this.eat(T.INDENT);
195
- const body = this.parseStmtList(() =>
196
- this.check(T.DEDENT) || this.check(T.EOF));
197
- this.maybe(T.DEDENT);
198
- return body;
199
- }
200
- const stmt = this.parseOneStmt();
201
- return [stmt];
202
- }
203
-
204
- // Parse an indented block WITHOUT a leading colon (used by `->` multiline arrow).
205
- parseIndentedBlock() {
206
- this.eat(T.NEWLINE);
207
- this.eat(T.INDENT);
208
- const body = this.parseStmtList(() => this.check(T.DEDENT) || this.check(T.EOF));
209
- this.maybe(T.DEDENT);
210
- return body;
211
- }
212
-
213
- parse() {
214
- this.skipNewlines();
215
- const body = this.parseStmtList(() => this.check(T.EOF));
216
- return { type: 'Program', body };
217
- }
218
-
219
- parseStmtList(stopFn) {
220
- const stmts = [];
221
- while (!stopFn()) {
222
- this.skipNewlines();
223
- if (stopFn()) break;
224
- stmts.push(this.parseOneStmt());
225
- }
226
- return stmts;
227
- }
228
-
229
- // ── Decorator parsing: @name or @name(args) ───────────────────
230
- parseDecorators() {
231
- const decorators = [];
232
- while (this.check(T.AT)) {
233
- const loc = this.skip(); // consume @
234
- const name = this.eat(T.IDENT).value;
235
- let args = [];
236
- if (this.check(T.LPAREN)) {
237
- this.pos++; // consume (
238
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
239
- args.push(this.parseExpr());
240
- if (!this.maybe(T.COMMA)) break;
241
- }
242
- this.eat(T.RPAREN);
243
- }
244
- decorators.push({ name, args, loc });
245
- this.skipNewlines();
246
- }
247
- return decorators;
248
- }
249
-
250
- parseOneStmt() {
251
- // Collect decorators (@decorator) before any declaration
252
- const decorators = this.check(T.AT) ? this.parseDecorators() : [];
253
-
254
- const tok = this.peek();
255
- switch (tok.type) {
256
- case T.VAR:
257
- case T.VAL: return this.parseVarDecl();
258
- case T.FN: { const n = this.parseFnDecl(); if (decorators.length) n.decorators = decorators; return n; }
259
- case T.ASYNC: { const n = this.parseAsyncFn(); if (decorators.length) n.decorators = decorators; return n; }
260
- case T.CLASS: { const n = this.parseClassDecl(); if (decorators.length) n.decorators = decorators; return n; }
261
- case T.IF: return this.parseIf();
262
- case T.FOR: return this.parseFor();
263
- case T.WHILE: return this.parseWhile();
264
- case T.DO: return this.parseDoWhile();
265
- case T.MATCH: return this.parseMatch();
266
- case T.RETURN: return this.parseReturn();
267
- case T.BREAK: {
268
- this.skip();
269
- // break label — label is an IDENT on the same line (no newline between)
270
- const blabel = (!this.check(T.NEWLINE) && !this.check(T.EOF) && this.check(T.IDENT)) ? this.skip().value : null;
271
- this.skipNewlines();
272
- return { type:'BreakStmt', label:blabel, loc:tok };
273
- }
274
- case T.CONTINUE: {
275
- this.skip();
276
- const clabel = (!this.check(T.NEWLINE) && !this.check(T.EOF) && this.check(T.IDENT)) ? this.skip().value : null;
277
- this.skipNewlines();
278
- return { type:'ContinueStmt', label:clabel, loc:tok };
279
- }
280
- case T.IMPORT: return this.parseImport();
281
- case T.EXPORT: return this.parseExport();
282
- case T.TRY: return this.parseTryCatch();
283
- case T.THROW: return this.parseThrow();
284
- case T.TYPE: return this.parseTypeDecl();
285
- case T.INTERFACE: return this.parseInterfaceDecl();
286
- case T.ENUM: return this.parseEnumDecl();
287
- default: {
288
- // Keyword tokens that are in the lexer but not valid statement-starters
289
- const STMT_KW_HINTS = {
290
- [T.CONST]: { msg: '"const" is not a Flux keyword', hint: 'Did you mean "val"? Flux uses "val" for immutable bindings (not "const")' },
291
- [T.TYPEOF]: { msg: '"typeof" is an expression, not a statement' },
292
- [T.INSTANCEOF]: { msg: '"instanceof" is an expression, not a statement' },
293
- [T.SATISFIES]: { msg: '"satisfies" is a type expression, not a statement' },
294
- };
295
- if (Object.prototype.hasOwnProperty.call(STMT_KW_HINTS, tok.type)) {
296
- const info = STMT_KW_HINTS[tok.type];
297
- const err = new ParseError(info.msg, tok);
298
- if (info.hint) err.hint = info.hint;
299
- throw err;
300
- }
301
- // Labeled statement: identifier followed by COLON then a loop/while
302
- // e.g. `outer: for i in 0..5:` or `loop: while true:`
303
- if (tok.type === T.IDENT && this.peek(1).type === T.COLON) {
304
- const afterColon = this.peek(2).type;
305
- if (afterColon === T.FOR || afterColon === T.WHILE) {
306
- const label = this.skip().value; // consume label name
307
- this.pos++; // consume ':'
308
- const body = this.parseOneStmt();
309
- return { type:'LabeledStmt', label, body, loc:tok };
310
- }
311
- }
312
- // Check if IDENT looks like a misspelled statement-level keyword.
313
- // Only trigger when the next token cannot be an expression-continuation
314
- // (dot, call, index, assignment, arrow, etc.) — otherwise the identifier
315
- // is a valid expression statement start (e.g. `Fs.writeFileSync(…)`).
316
- if (tok.type === T.IDENT) {
317
- const next = this.peek(1).type;
318
- const isExprContinuation = (
319
- next === T.DOT || next === T.LPAREN || next === T.LBRACKET ||
320
- next === T.EQ || next === T.PLUSEQ || next === T.MINUSEQ ||
321
- next === T.STAREQ || next === T.SLASHEQ || next === T.PERCENTEQ ||
322
- next === T.ARROW || next === T.FATARROW || next === T.QUESTION ||
323
- next === T.EQEQ || next === T.NEQ || next === T.AND ||
324
- next === T.OR || next === T.PIPE
325
- );
326
- if (!isExprContinuation) {
327
- const hint = suggestKeyword(tok.value);
328
- if (hint) {
329
- const err = new ParseError(`Unexpected identifier "${tok.value}"`, tok);
330
- err.hint = hint;
331
- throw err;
332
- }
333
- }
334
- }
335
- return this.parseExprStmt();
336
- }
337
- }
338
- }
339
-
340
- // ── var / val ─────────────────────────────────────────────────
341
- parseVarDecl() {
342
- const kind = this.peek().type === T.VAR ? 'var' : 'val';
343
- const loc = this.skip();
344
-
345
- if (this.check(T.LBRACE)) {
346
- const pattern = this.parseObjectDestructurePattern();
347
- this.eat(T.EQ);
348
- const init = this.parseExpr();
349
- this.skipNewlines();
350
- return { type:'DestructureDecl', kind, patternType:'object', pattern, init, loc };
351
- }
352
-
353
- if (this.check(T.LBRACKET)) {
354
- const pattern = this.parseArrayDestructurePattern();
355
- this.eat(T.EQ);
356
- const init = this.parseExpr();
357
- this.skipNewlines();
358
- return { type:'DestructureDecl', kind, patternType:'array', pattern, init, loc };
359
- }
360
-
361
- const name = this.eat(T.IDENT).value;
362
- let typeAnn = null;
363
- if (this.maybe(T.COLON)) typeAnn = this.parseTypeAnn();
364
- let init = null;
365
- if (this.maybe(T.EQ)) init = this.parseExpr();
366
- this.skipNewlines();
367
- return { type:'VarDecl', kind, name, typeAnn, init, loc };
368
- }
369
-
370
- parseObjectDestructurePattern() {
371
- this.eat(T.LBRACE);
372
- const props = [];
373
- while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
374
- let rest = false;
375
- if (this.check(T.DOTDOTDOT)) { this.pos++; rest = true; }
376
- const key = this.eat(T.IDENT).value;
377
- if (rest) { props.push({ key, alias: key, rest: true }); break; }
378
- let alias = key;
379
- if (this.maybe(T.COLON)) alias = this.eat(T.IDENT).value;
380
- let defaultVal = null;
381
- if (this.maybe(T.EQ)) defaultVal = this.parseExpr();
382
- props.push({ key, alias, defaultVal, rest: false });
383
- if (!this.maybe(T.COMMA)) break;
384
- }
385
- this.eat(T.RBRACE);
386
- return props;
387
- }
388
-
389
- parseArrayDestructurePattern() {
390
- this.eat(T.LBRACKET);
391
- const items = [];
392
- while (!this.check(T.RBRACKET) && !this.check(T.EOF)) {
393
- if (this.check(T.COMMA)) { items.push(null); this.pos++; continue; }
394
- let rest = false;
395
- if (this.check(T.DOTDOTDOT)) { this.pos++; rest = true; }
396
- const name = this.eat(T.IDENT).value;
397
- let defaultVal = null;
398
- if (this.maybe(T.EQ)) defaultVal = this.parseExpr();
399
- items.push({ name, defaultVal, rest });
400
- if (rest) break;
401
- if (!this.maybe(T.COMMA)) break;
402
- }
403
- this.eat(T.RBRACKET);
404
- return items;
405
- }
406
-
407
- // ── Type annotation ────────────────────────────────────────────────────────
408
- // Grammar:
409
- // TypeAnn = IntersectionType (| IntersectionType)*
410
- // IntersectionType = SingleType (& SingleType)*
411
- // SingleType = IDENT[<...>][][][?]
412
- // | [T, T, ...] (tuple)
413
- // | { key: T, ... } (inline object type)
414
- // | keyof T
415
- // | typeof IDENT
416
- // | readonly T
417
- // | (TypeAnn) (parenthesised)
418
- parseTypeAnn() {
419
- let name = this._parseIntersectionType();
420
- // Union types: A | B | C
421
- while (this.check(T.PIPEB)) {
422
- this.pos++;
423
- name += ' | ' + this._parseIntersectionType();
424
- }
425
- return name;
426
- }
427
-
428
- _parseIntersectionType() {
429
- let name = this._parseSingleType();
430
- // Intersection types: A & B & C
431
- while (this.check(T.AMPERSAND)) {
432
- this.pos++;
433
- name += ' & ' + this._parseSingleType();
434
- }
435
- return name;
436
- }
437
-
438
- _parseSingleType() {
439
- const tok = this.peek();
440
-
441
- // Tuple type: [String, Int, Bool]
442
- if (tok.type === T.LBRACKET) {
443
- this.pos++;
444
- const parts = [];
445
- while (!this.check(T.RBRACKET) && !this.check(T.EOF)) {
446
- parts.push(this.parseTypeAnn());
447
- if (!this.maybe(T.COMMA)) break;
448
- }
449
- this.eat(T.RBRACKET);
450
- return '[' + parts.join(', ') + ']';
451
- }
452
-
453
- // Inline object type: { key: Type; key2?: Type }
454
- if (tok.type === T.LBRACE) {
455
- this.pos++;
456
- const pairs = [];
457
- while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
458
- let readonly_ = false;
459
- if (this.check(T.READONLY)) { this.pos++; readonly_ = true; }
460
- // [key: Type] index signature
461
- if (this.check(T.LBRACKET)) {
462
- this.pos++;
463
- const indexName = this.eat(T.IDENT).value;
464
- this.eat(T.COLON);
465
- const indexKeyType = this.parseTypeAnn();
466
- this.eat(T.RBRACKET);
467
- this.eat(T.COLON);
468
- const indexValType = this.parseTypeAnn();
469
- pairs.push(`[${indexName}: ${indexKeyType}]: ${indexValType}`);
470
- } else {
471
- const key = this.at(T.IDENT, T.STRING) ? this.skip().value : this.eat(T.IDENT).value;
472
- let optional = false;
473
- if (this.check(T.QUESTION)) { this.pos++; optional = true; }
474
- this.eat(T.COLON);
475
- const valType = this.parseTypeAnn();
476
- const prefix = readonly_ ? 'readonly ' : '';
477
- pairs.push(`${prefix}${key}${optional ? '?' : ''}: ${valType}`);
478
- }
479
- // Allow comma or semicolon between members
480
- this.maybe(T.COMMA);
481
- }
482
- this.eat(T.RBRACE);
483
- return '{ ' + pairs.join(', ') + ' }';
484
- }
485
-
486
- // Parenthesised type: (A | B)
487
- if (tok.type === T.LPAREN) {
488
- this.pos++;
489
- const inner = this.parseTypeAnn();
490
- this.eat(T.RPAREN);
491
- return '(' + inner + ')';
492
- }
493
-
494
- // keyof T
495
- if (tok.type === T.IDENT && tok.value === 'keyof') {
496
- this.pos++;
497
- return 'keyof ' + this._parseSingleType();
498
- }
499
-
500
- // typeof x
501
- if (tok.type === T.TYPEOF) {
502
- this.pos++;
503
- return 'typeof ' + this.eat(T.IDENT).value;
504
- }
505
-
506
- // readonly T
507
- if (tok.type === T.READONLY) {
508
- this.pos++;
509
- return 'readonly ' + this._parseSingleType();
510
- }
511
-
512
- // infer T (conditional type helper)
513
- if (tok.type === T.IDENT && tok.value === 'infer') {
514
- this.pos++;
515
- return 'infer ' + this.eat(T.IDENT).value;
516
- }
517
-
518
- // Function type: fn(T1, T2) -> RetType
519
- if (tok.type === T.FN) {
520
- this.pos++;
521
- const paramTypes = [];
522
- if (this.check(T.LPAREN)) {
523
- this.pos++;
524
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
525
- // named param: name: Type OR just: Type
526
- let pt;
527
- if (this.check(T.IDENT) && this.peek(1).type === T.COLON) {
528
- this.pos += 2; // skip name + colon
529
- pt = this.parseTypeAnn();
530
- } else {
531
- pt = this.parseTypeAnn();
532
- }
533
- paramTypes.push(pt);
534
- if (!this.maybe(T.COMMA)) break;
535
- }
536
- this.eat(T.RPAREN);
537
- }
538
- let retType = 'Void';
539
- if (this.check(T.ARROW)) {
540
- this.pos++;
541
- retType = this.parseTypeAnn();
542
- }
543
- return `fn(${paramTypes.join(', ')}) -> ${retType}`;
544
- }
545
-
546
- // Standard named/generic type: String, Array<T>, Map<K,V>
547
- let name;
548
- if (tok.type === T.IDENT || tok.type === T.CONST || tok.type === T.TYPE) {
549
- name = this.skip().value;
550
- } else {
551
- name = this.eat(T.IDENT).value;
552
- }
553
-
554
- // Conditional type: T extends U ? A : B
555
- if (this.check(T.EXTENDS)) {
556
- this.pos++;
557
- const constraint = this._parseSingleType();
558
- this.eat(T.QUESTION);
559
- const thenType = this.parseTypeAnn();
560
- this.eat(T.COLON);
561
- const elseType = this.parseTypeAnn();
562
- return `${name} extends ${constraint} ? ${thenType} : ${elseType}`;
563
- }
564
-
565
- // Generics: List<T>, Map<K, V>, Array<String>
566
- if (this.check(T.LT)) {
567
- this.pos++;
568
- const params = [this._parseGenericArg()];
569
- while (this.maybe(T.COMMA)) params.push(this._parseGenericArg());
570
- this.eat(T.GT);
571
- name += '<' + params.join(', ') + '>';
572
- }
573
-
574
- // Array suffix: String[]
575
- while (this.check(T.LBRACKET) && this.peek(1).type === T.RBRACKET) {
576
- this.pos += 2;
577
- name += '[]';
578
- }
579
-
580
- // Nullable shorthand: String?
581
- if (this.check(T.QUESTION) && this.peek(1).type !== T.DOT) {
582
- this.pos++;
583
- name += '?';
584
- }
585
-
586
- return name;
587
- }
588
-
589
- // Parse generic argument — supports full type ann + conditional types
590
- _parseGenericArg() {
591
- return this.parseTypeAnn();
592
- }
593
-
594
- // ── Look-ahead: is current position a type annotation followed by ':' ? ──
595
- // Used to disambiguate `fn f() -> RetType:` vs `fn f() -> expr`
596
- _isTypeAnnBeforeColon() {
597
- const peek = (n) => this.tokens[this.pos + n] || { type: T.EOF };
598
- let i = 0;
599
-
600
- // Tuple / inline-object / paren — complex: just check if LBRACKET then scan to RBRACKET
601
- if (peek(i).type === T.LBRACKET || peek(i).type === T.LBRACE || peek(i).type === T.LPAREN) {
602
- let depth = 0;
603
- const open = new Set([T.LBRACKET, T.LBRACE, T.LPAREN, T.LT]);
604
- const close = new Set([T.RBRACKET, T.RBRACE, T.RPAREN, T.GT]);
605
- while (peek(i).type !== T.EOF) {
606
- if (open.has(peek(i).type)) depth++;
607
- if (close.has(peek(i).type)) depth--;
608
- i++;
609
- if (depth === 0) break;
610
- }
611
- if (peek(i).type === T.QUESTION) i++;
612
- // & or | are ok too before :
613
- while (peek(i).type === T.PIPEB || peek(i).type === T.AMPERSAND) {
614
- i++;
615
- if (peek(i).type !== T.IDENT) return false;
616
- i++;
617
- }
618
- return peek(i).type === T.COLON;
619
- }
620
-
621
- // keyof / typeof / readonly
622
- if (peek(i).type === T.TYPEOF || peek(i).type === T.READONLY ||
623
- (peek(i).type === T.IDENT && (peek(i).value === 'keyof' || peek(i).value === 'readonly'))) {
624
- i++;
625
- }
626
-
627
- if (peek(i).type !== T.IDENT) return false;
628
- i++;
629
- // Generics <...>
630
- if (peek(i).type === T.LT) {
631
- i++; let depth = 1;
632
- while (peek(i).type !== T.EOF && depth > 0) {
633
- if (peek(i).type === T.LT) depth++;
634
- if (peek(i).type === T.GT) depth--;
635
- i++;
636
- }
637
- }
638
- // Array []
639
- while (peek(i).type === T.LBRACKET && peek(i+1).type === T.RBRACKET) i += 2;
640
- // Nullable ?
641
- if (peek(i).type === T.QUESTION) i++;
642
- // Union types
643
- while (peek(i).type === T.PIPEB) {
644
- i++;
645
- if (peek(i).type !== T.IDENT) return false;
646
- i++;
647
- if (peek(i).type === T.LT) {
648
- i++; let depth = 1;
649
- while (peek(i).type !== T.EOF && depth > 0) {
650
- if (peek(i).type === T.LT) depth++;
651
- if (peek(i).type === T.GT) depth--;
652
- i++;
653
- }
654
- }
655
- while (peek(i).type === T.LBRACKET && peek(i+1).type === T.RBRACKET) i += 2;
656
- if (peek(i).type === T.QUESTION) i++;
657
- }
658
- return peek(i).type === T.COLON;
659
- }
660
-
661
- // ── type Name<T, U> = Variant1(f1, f2) | Variant2 | ... ───────
662
- parseTypeDecl() {
663
- const loc = this.eat(T.TYPE);
664
- const name = this.eat(T.IDENT).value;
665
- // Optional generic type parameters: <T>, <T, U>, etc.
666
- const typeParams = [];
667
- if (this.check(T.LT)) {
668
- this.pos++;
669
- while (!this.check(T.GT) && !this.check(T.EOF)) {
670
- typeParams.push(this.eat(T.IDENT).value);
671
- if (!this.maybe(T.COMMA)) break;
672
- }
673
- this.eat(T.GT);
674
- }
675
- this.eat(T.EQ);
676
-
677
- const variants = [];
678
- while (true) {
679
- const vname = this.eat(T.IDENT).value;
680
- const fields = [];
681
- const fieldTypes = {};
682
- if (this.check(T.LPAREN)) {
683
- this.eat(T.LPAREN);
684
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
685
- const ftok = this.peek();
686
- let fname;
687
- if (ftok.type === T.IDENT || ftok.type in T) {
688
- fname = this.skip().value;
689
- } else {
690
- this.err(`Expected field name, got '${ftok.type}'`);
691
- }
692
- // Support named fields with type: Variant(fieldName: Type)
693
- if (this.maybe(T.COLON)) {
694
- fieldTypes[fname] = this.parseTypeAnn();
695
- }
696
- fields.push(fname);
697
- if (!this.maybe(T.COMMA)) break;
698
- }
699
- this.eat(T.RPAREN);
700
- }
701
- variants.push({ name: vname, fields, fieldTypes });
702
- if (!this.check(T.PIPEB)) break;
703
- this.pos++;
704
- }
705
- this.skipNewlines();
706
- return { type: 'TypeDecl', name, variants, loc };
707
- }
708
-
709
- // ── Lookahead: is COLON followed by a type annotation then NEWLINE/EOF?
710
- // Used to distinguish `fn f(): RetType\n body` from `fn f(): singleExpr`
711
- _isColonReturnType() {
712
- const saved = this.pos;
713
- try {
714
- this.pos++; // skip COLON
715
- if (!this.at(T.IDENT, T.LBRACKET, T.LBRACE, T.LPAREN, T.FN, T.READONLY, T.TYPEOF)) return false;
716
- this.parseTypeAnn();
717
- return this.check(T.NEWLINE) || this.check(T.EOF);
718
- } catch (_) {
719
- return false;
720
- } finally {
721
- this.pos = saved;
722
- }
723
- }
724
-
725
- // ── fn ─────────────────────────────────────────────────────────
726
- parseFnDecl(isAsync = false) {
727
- const loc = this.eat(T.FN);
728
- // Allow reserved keywords as method/function names (e.g. `fn new(...)`)
729
- const KEYWORD_AS_NAME = new Set([
730
- T.NEW, T.DELETE, T.FROM, T.AS, T.DEFAULT, T.IS, T.IN, T.TYPE
731
- ]);
732
- const name = (this.check(T.IDENT) || KEYWORD_AS_NAME.has(this.peek().type))
733
- ? this.skip().value : null;
734
- const params = this.parseParamList();
735
-
736
- let retType = null;
737
-
738
- if (this.check(T.ARROW)) {
739
- this.pos++; // consume ->
740
- if (this._isTypeAnnBeforeColon()) {
741
- // Return type annotation: fn foo(x) -> String:
742
- retType = this.parseTypeAnn();
743
- const body = this.parseBlock();
744
- return { type:'FnDecl', name, params, retType, body, inline:false, async:isAsync, loc };
745
- } else {
746
- // Inline body: fn foo(x) -> expr
747
- const body = this.parseExpr();
748
- this.skipNewlines();
749
- return { type:'FnDecl', name, params, retType:null, body, inline:true, async:isAsync, loc };
750
- }
751
- }
752
-
753
- if (this.check(T.COLON)) {
754
- // Check for `fn f(): RetType\n body` — colon-style return type annotation
755
- if (this._isColonReturnType()) {
756
- this.pos++; // eat ':'
757
- retType = this.parseTypeAnn();
758
- // Parse indented body block
759
- if (this.check(T.NEWLINE)) this.pos++;
760
- if (this.check(T.INDENT)) {
761
- this.pos++;
762
- const body = this.parseStmtList(() => this.check(T.DEDENT) || this.check(T.EOF));
763
- this.maybe(T.DEDENT);
764
- return { type:'FnDecl', name, params, retType, body, inline:false, async:isAsync, loc };
765
- }
766
- // Single-line body after return type (edge case)
767
- const stmt = this.parseOneStmt();
768
- return { type:'FnDecl', name, params, retType, body:[stmt], inline:false, async:isAsync, loc };
769
- }
770
- const body = this.parseBlock();
771
- return { type:'FnDecl', name, params, retType:null, body, inline:false, async:isAsync, loc };
772
- }
773
-
774
- this.err('Expected -> or : after function signature');
775
- }
776
-
777
- // ── async fn ──────────────────────────────────────────────────
778
- parseAsyncFn() {
779
- this.eat(T.ASYNC);
780
- if (!this.check(T.FN)) this.err('Expected fn after async');
781
- return this.parseFnDecl(true);
782
- }
783
-
784
- parseParamList() {
785
- this.eat(T.LPAREN);
786
- const params = [];
787
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
788
- let rest = false;
789
- if (this.check(T.DOTDOTDOT)) { this.pos++; rest = true; }
790
- const name = this.eat(T.IDENT).value;
791
- let optional = false;
792
- let typeAnn = null;
793
- if (!rest && this.check(T.QUESTION)) { this.pos++; optional = true; }
794
- if (!rest && this.maybe(T.COLON)) typeAnn = this.parseTypeAnn();
795
- let defaultVal = null;
796
- if (!rest && this.maybe(T.EQ)) defaultVal = this.parseExpr();
797
- params.push({ name, typeAnn, optional, defaultVal, rest });
798
- if (rest) break;
799
- if (!this.maybe(T.COMMA)) break;
800
- }
801
- this.eat(T.RPAREN);
802
- return params;
803
- }
804
-
805
- // ── access modifier helper ─────────────────────────────────────
806
- parseAccessModifiers() {
807
- const mods = new Set();
808
- const modTokens = new Set([
809
- T.PRIVATE, T.PUBLIC, T.PROTECTED, T.READONLY,
810
- T.STATIC, T.ABSTRACT, T.OVERRIDE
811
- ]);
812
- while (modTokens.has(this.peek().type)) {
813
- mods.add(this.skip().value);
814
- }
815
- return mods;
816
- }
817
-
818
- // ── class ─────────────────────────────────────────────────────
819
- parseClassDecl() {
820
- const loc = this.eat(T.CLASS);
821
- const name = this.eat(T.IDENT).value;
822
- let superClass = null;
823
- let interfaces = [];
824
- // Generic type params: class Foo<T>
825
- let typeParams = [];
826
- if (this.check(T.LT)) {
827
- this.pos++;
828
- typeParams.push(this.eat(T.IDENT).value);
829
- while (this.maybe(T.COMMA)) typeParams.push(this.eat(T.IDENT).value);
830
- this.eat(T.GT);
831
- }
832
- if (this.maybe(T.EXTENDS)) superClass = this.eat(T.IDENT).value;
833
- if (this.maybe(T.IMPLEMENTS)) {
834
- interfaces.push(this.eat(T.IDENT).value);
835
- while (this.maybe(T.COMMA)) interfaces.push(this.eat(T.IDENT).value);
836
- }
837
-
838
- this.eat(T.COLON);
839
- if (this.check(T.NEWLINE)) {
840
- this.pos++;
841
- this.eat(T.INDENT);
842
- }
843
-
844
- const fields = [];
845
- const methods = [];
846
-
847
- while (!this.check(T.DEDENT) && !this.check(T.EOF)) {
848
- this.skipNewlines();
849
- if (this.check(T.DEDENT) || this.check(T.EOF)) break;
850
-
851
- const mods = this.parseAccessModifiers();
852
-
853
- // getter/setter: `get propName():` or `set propName(v):` (no fn keyword)
854
- // Also: `fn get propName():` or `fn set propName(v):` (with fn keyword)
855
- const isGetSet = this.check(T.IDENT) && (this.peek().value === 'get' || this.peek().value === 'set') && (this.peek(1).type === T.IDENT || this.peek(1).type === T.FN);
856
- const isFnGetSet = this.check(T.FN) && (this.peek(1).type === T.IDENT) && (this.peek(1).value === 'get' || this.peek(1).value === 'set') && this.peek(2).type === T.IDENT;
857
- if (isGetSet && this.peek(1).type !== T.FN) {
858
- const loc = this.peek();
859
- const kind = this.skip().value; // 'get' or 'set'
860
- const name = this.eat(T.IDENT).value;
861
- const params = this.parseParamList();
862
- let retType = null;
863
- if (this.check(T.ARROW)) { this.pos++; if (this._isTypeAnnBeforeColon()) retType = this.parseTypeAnn(); }
864
- else if (this.check(T.COLON) && this._isColonReturnType()) { this.pos++; retType = this.parseTypeAnn(); }
865
- const body = this.parseBlock();
866
- const m = { type:'FnDecl', name, params, retType, body, inline:false, async:false, modifiers:mods, getset:kind, loc };
867
- methods.push(m);
868
- } else if (this.check(T.FN)) {
869
- const m = this.parseFnDecl();
870
- m.modifiers = mods;
871
- methods.push(m);
872
- } else if (this.check(T.ASYNC)) {
873
- const m = this.parseAsyncFn();
874
- m.modifiers = mods;
875
- methods.push(m);
876
- } else if (this.check(T.STATIC) && (this.peek(1).type === T.FN || this.peek(1).type === T.ASYNC)) {
877
- this.skip();
878
- const m = this.check(T.ASYNC) ? this.parseAsyncFn() : this.parseFnDecl();
879
- m.modifiers = mods; m.modifiers.add('static');
880
- methods.push(m);
881
- } else if (this.check(T.VAR) || this.check(T.VAL)) {
882
- // `var fieldName: Type = init` or `var fieldName = init` in class body
883
- const fieldKind = this.skip().type === T.VAR ? 'var' : 'val';
884
- const fname = this.eat(T.IDENT).value;
885
- let optional = false;
886
- let ftype = null;
887
- if (this.check(T.QUESTION)) { this.pos++; optional = true; }
888
- if (this.check(T.COLON)) { this.pos++; ftype = this.parseTypeAnn(); }
889
- let init = null;
890
- if (this.check(T.EQ)) { this.pos++; init = this.parseExpr(); }
891
- this.skipNewlines();
892
- fields.push({ name: fname, typeAnn: ftype, optional, modifiers: mods, init, fieldKind });
893
- } else if (this.check(T.IDENT)) {
894
- // Bare `fieldName: Type` without var/val keyword (interface-style field in class)
895
- const fname = this.eat(T.IDENT).value;
896
- let optional = false;
897
- if (this.check(T.QUESTION)) { this.pos++; optional = true; }
898
- this.eat(T.COLON);
899
- const ftype = this.parseTypeAnn();
900
- this.skipNewlines();
901
- fields.push({ name: fname, typeAnn: ftype, optional, modifiers: mods, init: null });
902
- } else {
903
- this.skip();
904
- }
905
- }
906
- this.maybe(T.DEDENT);
907
- return { type:'ClassDecl', name, typeParams, superClass, interfaces, fields, methods, loc };
908
- }
909
-
910
- // ── interface ──────────────────────────────────────────────────
911
- parseInterfaceDecl() {
912
- const loc = this.eat(T.INTERFACE);
913
- const name = this.eat(T.IDENT).value;
914
- // Generic type params: interface Container<T>
915
- let typeParams = [];
916
- if (this.check(T.LT)) {
917
- this.pos++;
918
- typeParams.push(this.eat(T.IDENT).value);
919
- while (this.maybe(T.COMMA)) typeParams.push(this.eat(T.IDENT).value);
920
- this.eat(T.GT);
921
- }
922
- let superInterfaces = [];
923
- if (this.maybe(T.EXTENDS)) {
924
- superInterfaces.push(this.eat(T.IDENT).value);
925
- while (this.maybe(T.COMMA)) superInterfaces.push(this.eat(T.IDENT).value);
926
- }
927
-
928
- this.eat(T.COLON);
929
- if (this.check(T.NEWLINE)) {
930
- this.pos++;
931
- this.eat(T.INDENT);
932
- }
933
-
934
- const members = [];
935
- while (!this.check(T.DEDENT) && !this.check(T.EOF)) {
936
- this.skipNewlines();
937
- if (this.check(T.DEDENT) || this.check(T.EOF)) break;
938
-
939
- const mods = this.parseAccessModifiers();
940
-
941
- if (this.check(T.ASYNC)) {
942
- this.pos++;
943
- this.eat(T.FN);
944
- const mname = this.eat(T.IDENT).value;
945
- const params = this.parseParamList();
946
- let retType = null;
947
- if (this.maybe(T.ARROW)) retType = this.parseTypeAnn();
948
- this.skipNewlines();
949
- members.push({ kind: 'method', name: mname, params, retType, modifiers: mods, isAsync: true });
950
- } else if (this.check(T.FN)) {
951
- this.eat(T.FN);
952
- const mname = this.eat(T.IDENT).value;
953
- const params = this.parseParamList();
954
- let retType = null;
955
- if (this.maybe(T.ARROW)) retType = this.parseTypeAnn();
956
- this.skipNewlines();
957
- members.push({ kind: 'method', name: mname, params, retType, modifiers: mods, isAsync: false });
958
- } else if (this.check(T.IDENT)) {
959
- const fname = this.eat(T.IDENT).value;
960
- let optional = false;
961
- if (this.check(T.QUESTION)) { this.pos++; optional = true; }
962
- this.eat(T.COLON);
963
- const ftype = this.parseTypeAnn();
964
- this.skipNewlines();
965
- members.push({ kind: 'field', name: fname, typeAnn: ftype, optional, modifiers: mods });
966
- } else {
967
- this.skip();
968
- }
969
- }
970
- this.maybe(T.DEDENT);
971
- return { type: 'InterfaceDecl', name, typeParams, superInterfaces, members, loc };
972
- }
973
-
974
- // ── enum ───────────────────────────────────────────────────────
975
- parseEnumDecl() {
976
- const loc = this.eat(T.ENUM);
977
- const name = this.eat(T.IDENT).value;
978
-
979
- this.eat(T.COLON);
980
- if (this.check(T.NEWLINE)) {
981
- this.pos++;
982
- this.eat(T.INDENT);
983
- }
984
-
985
- const members = [];
986
- let autoIndex = 0;
987
-
988
- while (!this.check(T.DEDENT) && !this.check(T.EOF)) {
989
- this.skipNewlines();
990
- if (this.check(T.DEDENT) || this.check(T.EOF)) break;
991
-
992
- const mname = this.eat(T.IDENT).value;
993
- let value = null;
994
- if (this.maybe(T.EQ)) {
995
- value = this.parseExpr();
996
- // FIX: after an explicit numeric value, continue auto-index from it
997
- // so subsequent auto-assigned members follow the explicit value.
998
- if (value.type === 'NumberLit') autoIndex = value.value;
999
- } else {
1000
- value = { type: 'NumberLit', value: autoIndex };
1001
- }
1002
- autoIndex++;
1003
- this.skipNewlines();
1004
- members.push({ name: mname, value });
1005
- }
1006
- this.maybe(T.DEDENT);
1007
- return { type: 'EnumDecl', name, members, loc };
1008
- }
1009
-
1010
- // ── if / else if / else ───────────────────────────────────────
1011
- parseIf() {
1012
- const loc = this.eat(T.IF);
1013
- const cond = this.parseExpr();
1014
- const then = this.parseBlock();
1015
- const elseifs = [];
1016
- let else_ = null;
1017
-
1018
- this.skipNewlines();
1019
- while (this.check(T.ELSE)) {
1020
- this.pos++;
1021
- if (this.check(T.IF)) {
1022
- this.pos++;
1023
- const eic = this.parseExpr();
1024
- const eib = this.parseBlock();
1025
- elseifs.push({ cond:eic, body:eib });
1026
- this.skipNewlines();
1027
- } else {
1028
- else_ = this.parseBlock();
1029
- break;
1030
- }
1031
- }
1032
- return { type:'IfStmt', cond, then, elseifs, else_, loc };
1033
- }
1034
-
1035
- // ── for x in iter / for await x in iter ──────────────────────
1036
- parseFor() {
1037
- const loc = this.eat(T.FOR);
1038
- let isAwait = false;
1039
- if (this.check(T.AWAIT)) { this.pos++; isAwait = true; }
1040
-
1041
- // Support destructuring in loop variable: for [a, b] in ..., for {x} in ...
1042
- let varName;
1043
- let varPattern = null;
1044
- if (this.check(T.LBRACKET)) {
1045
- // Array destructuring: for [a, b] in arr
1046
- this.pos++;
1047
- const names = [];
1048
- while (!this.check(T.RBRACKET) && !this.check(T.EOF)) {
1049
- if (this.check(T.DOTDOTDOT)) { this.pos++; names.push({ rest: true, name: this.eat(T.IDENT).value }); break; }
1050
- names.push({ name: this.eat(T.IDENT).value });
1051
- if (!this.maybe(T.COMMA)) break;
1052
- }
1053
- this.eat(T.RBRACKET);
1054
- varName = '__item__';
1055
- varPattern = { type: 'array', names };
1056
- } else {
1057
- varName = this.eat(T.IDENT).value;
1058
- }
1059
-
1060
- // Accept both `in` and `of` (of is an identifier in the lexer)
1061
- if (this.check(T.IN)) {
1062
- this.pos++;
1063
- } else if (this.check(T.IDENT) && this.peek().value === 'of') {
1064
- this.pos++;
1065
- isAwait = isAwait; // keep flag
1066
- } else {
1067
- this.eat(T.IN); // will throw with proper error
1068
- }
1069
-
1070
- const iter = this.parseExpr();
1071
- const body = this.parseBlock();
1072
- return { type:'ForInStmt', var:varName, varPattern, iter, body, isAwait, loc };
1073
- }
1074
-
1075
- // ── while ─────────────────────────────────────────────────────
1076
- parseWhile() {
1077
- const loc = this.eat(T.WHILE);
1078
- const cond = this.parseExpr();
1079
- const body = this.parseBlock();
1080
- return { type:'WhileStmt', cond, body, loc };
1081
- }
1082
-
1083
- // ── do...while ────────────────────────────────────────────────
1084
- parseDoWhile() {
1085
- const loc = this.eat(T.DO);
1086
- const body = this.parseBlock();
1087
- this.skipNewlines();
1088
- this.eat(T.WHILE);
1089
- const cond = this.parseExpr();
1090
- this.skipNewlines();
1091
- return { type:'DoWhileStmt', body, cond, loc };
1092
- }
1093
-
1094
- // ── match / when ──────────────────────────────────────────────
1095
- parseMatch() {
1096
- const loc = this.eat(T.MATCH);
1097
- const subject = this.parseExpr();
1098
- this.eat(T.COLON);
1099
- if (this.check(T.NEWLINE)) this.pos++;
1100
- this.eat(T.INDENT);
1101
-
1102
- const arms = [];
1103
- while (!this.check(T.DEDENT) && !this.check(T.EOF)) {
1104
- this.skipNewlines();
1105
- if (this.check(T.DEDENT) || this.check(T.EOF)) break;
1106
- this.eat(T.WHEN);
1107
- const pattern = this.parsePattern();
1108
-
1109
- let guard = null;
1110
- if (this.check(T.IF)) {
1111
- this.pos++;
1112
- guard = this.parseExpr();
1113
- }
1114
-
1115
- if (this.check(T.ARROW)) {
1116
- this.pos++;
1117
- const expr = this.parseExpr();
1118
- this.skipNewlines();
1119
- arms.push({ pattern, guard, body:[{ type:'ExprStmt', expr }], inline:true });
1120
- } else if (this.check(T.COLON)) {
1121
- // peek(1) is the token after ':'. If it's not a NEWLINE, the body is
1122
- // a single inline expression on the same line → treat as inline (return value).
1123
- const isInline = this.peek(1).type !== T.NEWLINE;
1124
- const body = this.parseBlock();
1125
- arms.push({ pattern, guard, body, inline: isInline && body.length === 1 && body[0].type === 'ExprStmt' });
1126
- }
1127
- }
1128
- this.maybe(T.DEDENT);
1129
- return { type:'MatchStmt', subject, arms, loc };
1130
- }
1131
-
1132
- parsePattern() {
1133
- if (this.check(T.WILDCARD)) { this.skip(); return { type:'WildcardPat' }; }
1134
-
1135
- if (this.check(T.IDENT) && this.peek(1).type === T.LPAREN) {
1136
- const vname = this.eat(T.IDENT).value;
1137
- this.eat(T.LPAREN);
1138
- const bindings = [];
1139
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
1140
- // Allow _ (wildcard) as a binding name inside variant patterns
1141
- if (this.check(T.WILDCARD)) {
1142
- bindings.push('_');
1143
- this.pos++;
1144
- } else {
1145
- bindings.push(this.eat(T.IDENT).value);
1146
- }
1147
- if (!this.maybe(T.COMMA)) break;
1148
- }
1149
- this.eat(T.RPAREN);
1150
- return { type:'VariantPat', variant:vname, bindings };
1151
- }
1152
-
1153
- let left = this.parsePrimary();
1154
-
1155
- while (this.check(T.DOT)) {
1156
- this.pos++;
1157
- const prop = this.skip().value;
1158
- left = { type:'MemberExpr', obj:left, prop };
1159
- }
1160
-
1161
- if (this.check(T.DOTDOT)) {
1162
- this.pos++;
1163
- const right = this.parsePrimary();
1164
- return { type:'RangePat', start:left, end:right };
1165
- }
1166
- return { type:'LiteralPat', value:left };
1167
- }
1168
-
1169
- // ── return ────────────────────────────────────────────────────
1170
- parseReturn() {
1171
- const loc = this.eat(T.RETURN);
1172
- let value = null;
1173
- if (!this.at(T.NEWLINE, T.EOF, T.DEDENT))
1174
- value = this.parseExpr();
1175
- this.skipNewlines();
1176
- return { type:'ReturnStmt', value, loc };
1177
- }
1178
-
1179
- // ── try / catch / finally ─────────────────────────────────────
1180
- parseTryCatch() {
1181
- const loc = this.eat(T.TRY);
1182
- const tryBody = this.parseBlock();
1183
-
1184
- let catchParam = null;
1185
- let catchBody = null;
1186
- let finallyBody = null;
1187
-
1188
- this.skipNewlines();
1189
- if (this.check(T.CATCH)) {
1190
- this.pos++;
1191
- if (this.check(T.LPAREN)) {
1192
- this.pos++;
1193
- catchParam = this.eat(T.IDENT).value;
1194
- // optional type annotation: catch(e: Error)
1195
- if (this.maybe(T.COLON)) this.parseTypeAnn(); // consume, discard
1196
- this.eat(T.RPAREN);
1197
- }
1198
- catchBody = this.parseBlock();
1199
- this.skipNewlines();
1200
- }
1201
- if (this.check(T.FINALLY)) {
1202
- this.pos++;
1203
- finallyBody = this.parseBlock();
1204
- }
1205
- return { type:'TryCatchStmt', tryBody, catchParam, catchBody, finallyBody, loc };
1206
- }
1207
-
1208
- // ── throw ─────────────────────────────────────────────────────
1209
- parseThrow() {
1210
- const loc = this.eat(T.THROW);
1211
- const value = this.parseExpr();
1212
- this.skipNewlines();
1213
- return { type:'ThrowStmt', value, loc };
1214
- }
1215
-
1216
- // ── import ────────────────────────────────────────────────────
1217
- parseImport() {
1218
- this.eat(T.IMPORT);
1219
- if (this.check(T.STAR)) {
1220
- this.pos++;
1221
- this.eat(T.AS);
1222
- const namespaceName = this.eat(T.IDENT).value;
1223
- this.eat(T.FROM);
1224
- const source = this.eat(T.STRING).value;
1225
- this.skipNewlines();
1226
- return { type:'ImportDecl', names:[], defaultName:null, namespaceName, source };
1227
- }
1228
- if (this.check(T.IDENT)) {
1229
- const defaultName = this.eat(T.IDENT).value;
1230
- this.eat(T.FROM);
1231
- const source = this.eat(T.STRING).value;
1232
- this.skipNewlines();
1233
- return { type:'ImportDecl', names:[], defaultName, source };
1234
- }
1235
- const names = [];
1236
- if (this.maybe(T.LBRACE)) {
1237
- while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
1238
- const name = this.eat(T.IDENT).value;
1239
- let alias = name;
1240
- if (this.check(T.AS)) { this.pos++; alias = this.eat(T.IDENT).value; }
1241
- names.push({ name, alias });
1242
- if (!this.maybe(T.COMMA)) break;
1243
- }
1244
- this.eat(T.RBRACE);
1245
- }
1246
- this.eat(T.FROM);
1247
- const source = this.eat(T.STRING).value;
1248
- this.skipNewlines();
1249
- return { type:'ImportDecl', names, defaultName:null, source };
1250
- }
1251
-
1252
- // ── export ────────────────────────────────────────────────────
1253
- parseExport() {
1254
- this.eat(T.EXPORT);
1255
- if (this.check(T.DEFAULT) && (this.peek(1).type === T.FN || this.peek(1).type === T.ASYNC)) {
1256
- this.pos++;
1257
- const decl = this.check(T.ASYNC) ? (this.pos++, this.parseFnDecl(true)) : this.parseFnDecl();
1258
- return { type:'ExportDecl', isDefault:true, decl };
1259
- }
1260
- if (this.check(T.DEFAULT)) {
1261
- this.pos++;
1262
- const value = this.parseExpr();
1263
- this.skipNewlines();
1264
- return { type:'ExportDecl', isDefault:true, decl:value };
1265
- }
1266
- if (this.check(T.ASYNC)) {
1267
- this.pos++;
1268
- if (!this.check(T.FN)) this.err('Expected fn after async');
1269
- const decl = this.parseFnDecl(true);
1270
- return { type:'ExportDecl', isDefault:false, decl };
1271
- }
1272
- const decl = this.parseOneStmt();
1273
- return { type:'ExportDecl', isDefault:false, decl };
1274
- }
1275
-
1276
- // ── expression statement ──────────────────────────────────────
1277
- parseExprStmt() {
1278
- const expr = this.parseExpr();
1279
- this.skipNewlines();
1280
- return { type:'ExprStmt', expr };
1281
- }
1282
-
1283
- // ── Expressions ───────────────────────────────────────────────
1284
- parseExpr() { return this.parsePipe(); }
1285
-
1286
- parsePipe() {
1287
- let left = this.parseAssign();
1288
- while (true) {
1289
- if (this.check(T.PIPE)) {
1290
- this.pos++;
1291
- const right = this.parseAssign();
1292
- left = { type:'PipeExpr', left, right };
1293
- } else if (this.check(T.NEWLINE)) {
1294
- // Multi-line pipe: look past NEWLINEs to see if next meaningful token is |>
1295
- let i = 1;
1296
- while (this.peek(i).type === T.NEWLINE) i++;
1297
- if (this.peek(i).type === T.PIPE) {
1298
- // Consume all pending NEWLINEs then the PIPE token
1299
- while (this.check(T.NEWLINE)) this.pos++;
1300
- this.pos++; // consume |>
1301
- const right = this.parseAssign();
1302
- left = { type:'PipeExpr', left, right };
1303
- } else {
1304
- break;
1305
- }
1306
- } else {
1307
- break;
1308
- }
1309
- }
1310
- return left;
1311
- }
1312
-
1313
- parseAssign() {
1314
- const left = this.parseTernary();
1315
- const ASSIGN = {
1316
- [T.EQ]:'=', [T.PLUSEQ]:'+=', [T.MINUSEQ]:'-=',
1317
- [T.STAREQ]:'*=', [T.SLASHEQ]:'/=', [T.PERCENTEQ]:'%='
1318
- };
1319
- const op = ASSIGN[this.peek().type];
1320
- if (op) { this.pos++; return { type:'AssignExpr', target:left, op, value:this.parseAssign() }; }
1321
- return left;
1322
- }
1323
-
1324
- parseTernary() {
1325
- const cond = this.parseNullish();
1326
- if (this.maybe(T.QUESTION)) {
1327
- const then = this.parseNullish();
1328
- this.eat(T.COLON);
1329
- const else_ = this.parseTernary();
1330
- return { type:'TernaryExpr', cond, then, else_ };
1331
- }
1332
- return cond;
1333
- }
1334
-
1335
- parseNullish() {
1336
- let l = this.parseOr();
1337
- while (this.check(T.NULLISH)) {
1338
- this.pos++;
1339
- const r = this.parseOr();
1340
- l = { type:'BinaryExpr', op:'??', left:l, right:r };
1341
- }
1342
- return l;
1343
- }
1344
-
1345
- parseOr() {
1346
- let l = this.parseAnd();
1347
- while (this.check(T.OR) || this.check(T.OROR)) { this.pos++; const r = this.parseAnd(); l = { type:'BinaryExpr', op:'||', left:l, right:r }; }
1348
- return l;
1349
- }
1350
-
1351
- parseAnd() {
1352
- let l = this.parseBitOr();
1353
- while (this.check(T.AND) || this.check(T.ANDAND)) { this.pos++; const r = this.parseBitOr(); l = { type:'BinaryExpr', op:'&&', left:l, right:r }; }
1354
- return l;
1355
- }
1356
-
1357
- parseBitOr() {
1358
- let l = this.parseBitXor();
1359
- while (this.check(T.PIPEB)) { this.pos++; const r = this.parseBitXor(); l = { type:'BinaryExpr', op:'|', left:l, right:r }; }
1360
- return l;
1361
- }
1362
-
1363
- parseBitXor() {
1364
- let l = this.parseBitAnd();
1365
- while (this.check(T.CARET)) { this.pos++; const r = this.parseBitAnd(); l = { type:'BinaryExpr', op:'^', left:l, right:r }; }
1366
- return l;
1367
- }
1368
-
1369
- parseBitAnd() {
1370
- let l = this.parseEq();
1371
- while (this.check(T.AMPERSAND)) { this.pos++; const r = this.parseEq(); l = { type:'BinaryExpr', op:'&', left:l, right:r }; }
1372
- return l;
1373
- }
1374
-
1375
- parseEq() {
1376
- let l = this.parseRel();
1377
- while (this.at(T.EQEQ, T.NEQ, T.EQEQEQ, T.NEQEQ)) { const op = this.skip().value; const r = this.parseRel(); l = { type:'BinaryExpr', op, left:l, right:r }; }
1378
- return l;
1379
- }
1380
-
1381
- parseRel() {
1382
- let l = this.parseShift();
1383
- while (this.at(T.LT, T.LTE, T.GT, T.GTE) || this.check(T.IN)) {
1384
- const op = this.skip().value;
1385
- const r = this.parseShift();
1386
- l = { type:'BinaryExpr', op, left:l, right:r };
1387
- }
1388
- return l;
1389
- }
1390
-
1391
- parseShift() {
1392
- let l = this.parseRange();
1393
- while (this.at(T.LSHIFT, T.RSHIFT)) { const op = this.skip().value; const r = this.parseRange(); l = { type:'BinaryExpr', op, left:l, right:r }; }
1394
- return l;
1395
- }
1396
-
1397
- parseRange() {
1398
- const l = this.parseAdd();
1399
- if (this.check(T.DOTDOT)) { this.pos++; const r = this.parseAdd(); return { type:'RangeExpr', start:l, end:r }; }
1400
- return l;
1401
- }
1402
-
1403
- parseAdd() {
1404
- let l = this.parseMul();
1405
- while (this.at(T.PLUS, T.MINUS)) { const op = this.skip().value; const r = this.parseMul(); l = { type:'BinaryExpr', op, left:l, right:r }; }
1406
- return l;
1407
- }
1408
-
1409
- parseMul() {
1410
- let l = this.parsePow();
1411
- while (this.at(T.STAR, T.SLASH, T.PERCENT)) { const op = this.skip().value; const r = this.parsePow(); l = { type:'BinaryExpr', op, left:l, right:r }; }
1412
- return l;
1413
- }
1414
-
1415
- parsePow() {
1416
- const l = this.parseUnary();
1417
- if (this.check(T.STARSTAR)) { this.pos++; return { type:'BinaryExpr', op:'**', left:l, right:this.parsePow() }; }
1418
- return l;
1419
- }
1420
-
1421
- parseUnary() {
1422
- if (this.check(T.MINUS)) { this.pos++; return { type:'UnaryExpr', op:'-', operand:this.parseUnary() }; }
1423
- if (this.check(T.NOT)) { this.pos++; return { type:'UnaryExpr', op:'!', operand:this.parseUnary() }; }
1424
- if (this.check(T.BANG)) { this.pos++; return { type:'UnaryExpr', op:'!', operand:this.parseUnary() }; }
1425
- if (this.check(T.TILDE)) { this.pos++; return { type:'UnaryExpr', op:'~', operand:this.parseUnary() }; }
1426
- if (this.check(T.PLUSPLUS)) { this.pos++; return { type:'UpdateExpr', op:'++', prefix:true, operand:this.parseUnary() }; }
1427
- if (this.check(T.MINUSMINUS)) { this.pos++; return { type:'UpdateExpr', op:'--', prefix:true, operand:this.parseUnary() }; }
1428
- if (this.check(T.AWAIT)) { this.pos++; return { type:'AwaitExpr', operand:this.parseUnary() }; }
1429
- if (this.check(T.TYPEOF)) { this.pos++; return { type:'TypeofExpr', operand:this.parseUnary() }; }
1430
- return this.parseLambdaOrPostfix();
1431
- }
1432
-
1433
- parseLambdaOrPostfix() {
1434
- const saved = this.pos;
1435
- // Single-param lambda: x -> expr OR x ->\n block OR _ -> expr
1436
- if (this.check(T.IDENT) || this.check(T.WILDCARD)) {
1437
- const name = this.skip().value ?? '_';
1438
- if (this.check(T.ARROW)) {
1439
- this.pos++;
1440
- if (this.check(T.NEWLINE)) {
1441
- // Multiline block body: x ->\n stmts
1442
- const body = this.parseIndentedBlock();
1443
- return { type:'LambdaExpr', params:[{ name }], body, block:true };
1444
- }
1445
- const body = this.parseExpr();
1446
- return { type:'LambdaExpr', params:[{ name }], body };
1447
- }
1448
- this.pos = saved;
1449
- }
1450
- return this.parsePostfix();
1451
- }
1452
-
1453
- parsePostfix() {
1454
- let expr = this.parsePrimary();
1455
- loop: while (true) {
1456
- if (this.check(T.PLUSPLUS)) {
1457
- this.pos++;
1458
- expr = { type:'UpdateExpr', op:'++', prefix:false, operand:expr };
1459
- } else if (this.check(T.MINUSMINUS)) {
1460
- this.pos++;
1461
- expr = { type:'UpdateExpr', op:'--', prefix:false, operand:expr };
1462
- } else if (this.check(T.DOT)) {
1463
- this.pos++;
1464
- const propTok = this.peek();
1465
- if (propTok.type !== T.IDENT && !(propTok.type in T)) this.err(`Expected property name, got '${propTok.type}'`);
1466
- const prop = this.skip().value;
1467
- expr = { type:'MemberExpr', obj:expr, prop };
1468
- } else if (this.check(T.QUESTIONDOT)) {
1469
- this.pos++;
1470
- if (this.check(T.LPAREN)) {
1471
- const args = this.parseArgList();
1472
- expr = { type:'OptCallExpr', callee:expr, args };
1473
- } else if (this.check(T.LBRACKET)) {
1474
- this.pos++;
1475
- const idx = this.parseExpr();
1476
- this.eat(T.RBRACKET);
1477
- expr = { type:'OptIndexExpr', obj:expr, idx };
1478
- } else {
1479
- const propTok = this.peek();
1480
- if (propTok.type !== T.IDENT && !(propTok.type in T)) this.err(`Expected property name, got '${propTok.type}'`);
1481
- const prop = this.skip().value;
1482
- expr = { type:'OptMemberExpr', obj:expr, prop };
1483
- }
1484
- } else if (this.check(T.INSTANCEOF)) {
1485
- this.pos++;
1486
- const right = this.eat(T.IDENT).value;
1487
- expr = { type:'BinaryExpr', op:'instanceof', left:expr, right:{ type:'Identifier', name:right } };
1488
- } else if (this.check(T.AS)) {
1489
- this.pos++;
1490
- if (this.check(T.CONST)) {
1491
- this.pos++;
1492
- expr = { type:'AsConstExpr', expr };
1493
- } else {
1494
- const castType = this.parseTypeAnn();
1495
- expr = { type:'CastExpr', expr, castType };
1496
- }
1497
- } else if (this.check(T.SATISFIES)) {
1498
- this.pos++;
1499
- const satType = this.parseTypeAnn();
1500
- expr = { type:'SatisfiesExpr', expr, satType };
1501
- } else if (this.check(T.IS)) {
1502
- this.pos++;
1503
- const isType = this.parseTypeAnn();
1504
- expr = { type:'IsExpr', expr, isType };
1505
- } else if (this.check(T.BANG)) {
1506
- this.pos++;
1507
- expr = { type:'NonNullExpr', expr };
1508
- } else if (this.check(T.LBRACKET)) {
1509
- this.pos++;
1510
- const idx = this.parseExpr();
1511
- this.eat(T.RBRACKET);
1512
- expr = { type:'IndexExpr', obj:expr, idx };
1513
- } else if (this.check(T.LPAREN)) {
1514
- const args = this.parseArgList();
1515
- if (this.check(T.ARROW)) {
1516
- this.pos++;
1517
- const body = this.parseExpr();
1518
- const params = args.map(a => ({ name: a.type === 'Identifier' ? a.name : '_' }));
1519
- return { type:'LambdaExpr', params, body };
1520
- }
1521
- expr = { type:'CallExpr', callee:expr, args };
1522
- } else {
1523
- break loop;
1524
- }
1525
- }
1526
- return expr;
1527
- }
1528
-
1529
- parseArgList() {
1530
- this.eat(T.LPAREN);
1531
- const args = [];
1532
- while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
1533
- if (this.check(T.DOTDOTDOT)) {
1534
- this.pos++;
1535
- args.push({ type:'SpreadExpr', expr:this.parseExpr() });
1536
- } else {
1537
- args.push(this.parseExpr());
1538
- }
1539
- if (!this.maybe(T.COMMA)) break;
1540
- }
1541
- this.eat(T.RPAREN);
1542
- return args;
1543
- }
1544
-
1545
- parsePrimary() {
1546
- const tok = this.peek();
1547
- switch (tok.type) {
1548
- case T.NUMBER: return (this.pos++, { type:'NumberLit', value:tok.value, loc:tok });
1549
- case T.BOOL: return (this.pos++, { type:'BoolLit', value:tok.value, loc:tok });
1550
- case T.NULL: return (this.pos++, { type:'NullLit', loc:tok });
1551
- case T.SELF: return (this.pos++, { type:'SelfExpr', loc:tok });
1552
- case T.WILDCARD: return (this.pos++, { type:'Identifier', name:'_', loc:tok });
1553
- case T.IDENT: return (this.pos++, { type:'Identifier', name:tok.value, loc:tok });
1554
-
1555
- case T.STRING:
1556
- this.pos++;
1557
- if (tok.value && tok.value.template)
1558
- return { type:'TemplateLit', parts:tok.value.parts, loc:tok };
1559
- return { type:'StringLit', value:tok.value, loc:tok };
1560
-
1561
- case T.REGEX:
1562
- return (this.pos++, { type:'RegexLit', value:tok.value, loc:tok });
1563
-
1564
- case T.NEW: {
1565
- this.pos++;
1566
- const callee = this.eat(T.IDENT).value;
1567
- const args = this.parseArgList();
1568
- return { type:'NewExpr', callee, args };
1569
- }
1570
-
1571
- case T.FN: {
1572
- this.pos++;
1573
- const params = this.parseParamList();
1574
- if (this.check(T.ARROW)) {
1575
- this.pos++;
1576
- // Check if it's a return type annotation or inline body
1577
- if (this._isTypeAnnBeforeColon()) {
1578
- const retType = this.parseTypeAnn();
1579
- const body = this.parseBlock();
1580
- return { type:'FnDecl', name:null, params, retType, body, inline:false, async:false, loc:tok };
1581
- }
1582
- const body = this.parseExpr();
1583
- return { type:'FnDecl', name:null, params, retType:null, body, inline:true, async:false, loc:tok };
1584
- }
1585
- if (this.check(T.COLON)) {
1586
- const body = this.parseBlock();
1587
- return { type:'FnDecl', name:null, params, retType:null, body, inline:false, async:false, loc:tok };
1588
- }
1589
- this.err('Expected -> or : after anonymous fn');
1590
- }
1591
-
1592
- // match used as an expression: val result = match x: when ...
1593
- // CodeGenerator wraps it in an IIFE so it yields a value.
1594
- case T.MATCH:
1595
- return this.parseMatch();
1596
-
1597
- case T.LPAREN: {
1598
- this.pos++;
1599
- if (this.check(T.RPAREN)) {
1600
- this.pos++;
1601
- if (this.check(T.ARROW)) {
1602
- this.pos++;
1603
- if (this.check(T.NEWLINE)) {
1604
- const body = this.parseIndentedBlock();
1605
- return { type:'LambdaExpr', params:[], body, block:true };
1606
- }
1607
- const body = this.parseExpr();
1608
- return { type:'LambdaExpr', params:[], body };
1609
- }
1610
- return { type:'NullLit' };
1611
- }
1612
- const first = this.parseExpr();
1613
- if (this.check(T.RPAREN)) {
1614
- this.pos++;
1615
- if (this.check(T.ARROW)) {
1616
- this.pos++;
1617
- if (this.check(T.NEWLINE)) {
1618
- const body = this.parseIndentedBlock();
1619
- const name = first.type === 'Identifier' ? first.name : '_';
1620
- return { type:'LambdaExpr', params:[{ name }], body, block:true };
1621
- }
1622
- const body = this.parseExpr();
1623
- const name = first.type === 'Identifier' ? first.name : '_';
1624
- return { type:'LambdaExpr', params:[{ name }], body };
1625
- }
1626
- return first;
1627
- }
1628
- const items = [first];
1629
- while (this.maybe(T.COMMA)) items.push(this.parseExpr());
1630
- this.eat(T.RPAREN);
1631
- if (this.check(T.ARROW)) {
1632
- this.pos++;
1633
- const params = items.map(i => ({ name: i.type === 'Identifier' ? i.name : '_' }));
1634
- if (this.check(T.NEWLINE)) {
1635
- const body = this.parseIndentedBlock();
1636
- return { type:'LambdaExpr', params, body, block:true };
1637
- }
1638
- const body = this.parseExpr();
1639
- return { type:'LambdaExpr', params, body };
1640
- }
1641
- // FIX: (a, b, c) without -> is not a lambda — if multiple items, raise a
1642
- // parse error instead of silently discarding items[1..n].
1643
- if (items.length > 1)
1644
- this.err(`Unexpected comma in expression — did you mean a lambda? Write (${items.map((_,k)=>'p'+k).join(', ')}) -> expr`);
1645
- return items[0];
1646
- }
1647
-
1648
- case T.LBRACKET: {
1649
- this.pos++;
1650
- const items = [];
1651
- while (!this.check(T.RBRACKET) && !this.check(T.EOF)) {
1652
- if (this.check(T.DOTDOTDOT)) {
1653
- this.pos++;
1654
- items.push({ type:'SpreadExpr', expr:this.parseExpr() });
1655
- } else {
1656
- items.push(this.parseExpr());
1657
- }
1658
- if (!this.maybe(T.COMMA)) break;
1659
- }
1660
- this.eat(T.RBRACKET);
1661
- return { type:'ArrayExpr', items };
1662
- }
1663
-
1664
- case T.LBRACE: {
1665
- this.pos++;
1666
- const pairs = [];
1667
- while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
1668
- if (this.check(T.DOTDOTDOT)) {
1669
- this.pos++;
1670
- pairs.push({ spread: true, value: this.parseExpr() });
1671
- if (!this.maybe(T.COMMA)) break;
1672
- continue;
1673
- }
1674
- // Computed property key: { [expr]: value }
1675
- if (this.check(T.LBRACKET)) {
1676
- this.pos++;
1677
- const keyExpr = this.parseExpr();
1678
- this.eat(T.RBRACKET);
1679
- this.eat(T.COLON);
1680
- const val = this.parseExpr();
1681
- pairs.push({ computed: true, keyExpr, value: val });
1682
- if (!this.maybe(T.COMMA)) break;
1683
- continue;
1684
- }
1685
- // Allow reserved words as property keys (valid JS — e.g. { type: x, var: y })
1686
- const key = this.check(T.STRING) ? this.eat(T.STRING).value
1687
- : this.check(T.IDENT) ? this.eat(T.IDENT).value
1688
- : this.skip().value;
1689
- if (!this.check(T.COLON)) {
1690
- pairs.push({ key, value:{ type:'Identifier', name:key } });
1691
- } else {
1692
- this.eat(T.COLON);
1693
- const val = this.parseExpr();
1694
- pairs.push({ key, value:val });
1695
- }
1696
- if (!this.maybe(T.COMMA)) break;
1697
- }
1698
- this.eat(T.RBRACE);
1699
- return { type:'ObjectExpr', pairs };
1700
- }
1701
-
1702
- default:
1703
- this.err(`Unexpected token: ${tok.type} (${JSON.stringify(tok.value)})`);
1704
- }
1705
- }
1706
- }
1707
-
1708
- module.exports = { Parser, ParseError };