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