tova 0.3.0 → 0.3.2
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/bin/tova.js +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +491 -1064
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
package/src/parser/parser.js
CHANGED
|
@@ -5,13 +5,23 @@ export class Parser {
|
|
|
5
5
|
static MAX_EXPRESSION_DEPTH = 200;
|
|
6
6
|
|
|
7
7
|
constructor(tokens, filename = '<stdin>') {
|
|
8
|
-
this.tokens = tokens
|
|
9
|
-
this.rawTokens = tokens;
|
|
8
|
+
this.tokens = tokens;
|
|
10
9
|
this.filename = filename;
|
|
11
10
|
this.pos = 0;
|
|
12
11
|
this.errors = [];
|
|
13
12
|
this._expressionDepth = 0;
|
|
14
13
|
this.docstrings = this.extractDocstrings(tokens);
|
|
14
|
+
this._skipInsignificant();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_isInsignificant(type) {
|
|
18
|
+
return type === TokenType.NEWLINE || type === TokenType.DOCSTRING || type === TokenType.SEMICOLON;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_skipInsignificant() {
|
|
22
|
+
while (this.pos < this.tokens.length && this._isInsignificant(this.tokens[this.pos].type)) {
|
|
23
|
+
this.pos++;
|
|
24
|
+
}
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
extractDocstrings(tokens) {
|
|
@@ -26,12 +36,13 @@ export class Parser {
|
|
|
26
36
|
|
|
27
37
|
// ─── Helpers ───────────────────────────────────────────────
|
|
28
38
|
|
|
29
|
-
error(message) {
|
|
39
|
+
error(message, code = null) {
|
|
30
40
|
const tok = this.current();
|
|
31
41
|
const err = new Error(
|
|
32
42
|
`${this.filename}:${tok.line}:${tok.column} — Parse error: ${message}\n Got: ${tok.type} (${JSON.stringify(tok.value)})`
|
|
33
43
|
);
|
|
34
44
|
err.loc = { line: tok.line, column: tok.column, file: this.filename };
|
|
45
|
+
if (code) err.code = code;
|
|
35
46
|
throw err;
|
|
36
47
|
}
|
|
37
48
|
|
|
@@ -40,13 +51,23 @@ export class Parser {
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
peek(offset = 0) {
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
// Fast path: offset 0 is just the current position (always significant after skip)
|
|
55
|
+
if (offset === 0) return this.tokens[this.pos] || this.tokens[this.tokens.length - 1];
|
|
56
|
+
// General path: skip over insignificant tokens
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (let idx = this.pos + 1; idx < this.tokens.length; idx++) {
|
|
59
|
+
if (!this._isInsignificant(this.tokens[idx].type)) {
|
|
60
|
+
count++;
|
|
61
|
+
if (count === offset) return this.tokens[idx];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return this.tokens[this.tokens.length - 1];
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
advance() {
|
|
48
68
|
const tok = this.current();
|
|
49
69
|
this.pos++;
|
|
70
|
+
this._skipInsignificant();
|
|
50
71
|
return tok;
|
|
51
72
|
}
|
|
52
73
|
|
|
@@ -85,6 +106,7 @@ export class Parser {
|
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
_synchronize() {
|
|
109
|
+
const startPos = this.pos;
|
|
88
110
|
this.advance(); // skip the problematic token
|
|
89
111
|
while (!this.isAtEnd()) {
|
|
90
112
|
const tok = this.current();
|
|
@@ -98,7 +120,8 @@ export class Parser {
|
|
|
98
120
|
tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
|
|
99
121
|
tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
|
|
100
122
|
tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
|
|
101
|
-
tok.type === TokenType.EXTERN
|
|
123
|
+
tok.type === TokenType.EXTERN ||
|
|
124
|
+
tok.type === TokenType.VAR || tok.type === TokenType.ASYNC) {
|
|
102
125
|
return;
|
|
103
126
|
}
|
|
104
127
|
if (tok.type === TokenType.RBRACE) {
|
|
@@ -107,6 +130,10 @@ export class Parser {
|
|
|
107
130
|
}
|
|
108
131
|
this.advance();
|
|
109
132
|
}
|
|
133
|
+
// Safety: if we didn't advance at all, force advance to avoid infinite loop
|
|
134
|
+
if (this.pos === startPos && !this.isAtEnd()) {
|
|
135
|
+
this.advance();
|
|
136
|
+
}
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
_synchronizeBlock() {
|
|
@@ -128,7 +155,7 @@ export class Parser {
|
|
|
128
155
|
tok.type === TokenType.GUARD || tok.type === TokenType.INTERFACE ||
|
|
129
156
|
tok.type === TokenType.IMPL || tok.type === TokenType.TRAIT ||
|
|
130
157
|
tok.type === TokenType.PUB || tok.type === TokenType.DEFER ||
|
|
131
|
-
tok.type === TokenType.EXTERN || tok.type === TokenType.VAR ||
|
|
158
|
+
tok.type === TokenType.EXTERN || tok.type === TokenType.VAR || tok.type === TokenType.MUT ||
|
|
132
159
|
tok.type === TokenType.STATE || tok.type === TokenType.ROUTE ||
|
|
133
160
|
tok.type === TokenType.IDENTIFIER) {
|
|
134
161
|
return;
|
|
@@ -141,10 +168,23 @@ export class Parser {
|
|
|
141
168
|
_looksLikeJSX() {
|
|
142
169
|
if (!this.check(TokenType.LESS)) return false;
|
|
143
170
|
const next = this.peek(1);
|
|
171
|
+
// Fragment: <>
|
|
172
|
+
if (next.type === TokenType.GREATER) return true;
|
|
144
173
|
if (next.type !== TokenType.IDENTIFIER) return false;
|
|
145
174
|
// Uppercase tag is always a component reference, never a comparison variable
|
|
146
175
|
if (/^[A-Z]/.test(next.value)) return true;
|
|
147
176
|
const afterIdent = this.peek(2);
|
|
177
|
+
// Negative check: if afterIdent is a comparison/logical operator, this is NOT JSX
|
|
178
|
+
// This catches `a < b && c > d` being misread as JSX
|
|
179
|
+
if (afterIdent.type === TokenType.LESS ||
|
|
180
|
+
afterIdent.type === TokenType.LESS_EQUAL ||
|
|
181
|
+
afterIdent.type === TokenType.GREATER_EQUAL ||
|
|
182
|
+
afterIdent.type === TokenType.AND_AND ||
|
|
183
|
+
afterIdent.type === TokenType.OR_OR ||
|
|
184
|
+
afterIdent.type === TokenType.EQUAL ||
|
|
185
|
+
afterIdent.type === TokenType.NOT_EQUAL) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
148
188
|
// JSX patterns: <div>, <div/>, <div attr=...>, <div on:click=...>
|
|
149
189
|
// After the tag name, we can see >, /, an attribute name (identifier or keyword), or :
|
|
150
190
|
return afterIdent.type === TokenType.GREATER ||
|
|
@@ -166,7 +206,9 @@ export class Parser {
|
|
|
166
206
|
|
|
167
207
|
parse() {
|
|
168
208
|
const body = [];
|
|
209
|
+
const maxErrors = 50; // Stop after 50 errors to avoid cascading noise
|
|
169
210
|
while (!this.isAtEnd()) {
|
|
211
|
+
if (this.errors.length >= maxErrors) break;
|
|
170
212
|
try {
|
|
171
213
|
const stmt = this.parseTopLevel();
|
|
172
214
|
if (stmt) body.push(stmt);
|
|
@@ -176,17 +218,81 @@ export class Parser {
|
|
|
176
218
|
}
|
|
177
219
|
}
|
|
178
220
|
if (this.errors.length > 0) {
|
|
221
|
+
const program = new AST.Program(body);
|
|
222
|
+
this._attachDocstrings(program);
|
|
179
223
|
const combined = new Error(this.errors.map(e => e.message).join('\n'));
|
|
180
224
|
combined.errors = this.errors;
|
|
181
|
-
combined.partialAST =
|
|
225
|
+
combined.partialAST = program;
|
|
226
|
+
if (this.errors.length >= maxErrors) {
|
|
227
|
+
combined.truncated = true;
|
|
228
|
+
}
|
|
182
229
|
throw combined;
|
|
183
230
|
}
|
|
184
|
-
|
|
231
|
+
const program = new AST.Program(body);
|
|
232
|
+
this._attachDocstrings(program);
|
|
233
|
+
return program;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_attachDocstrings(program) {
|
|
237
|
+
// Build a map of docstring line ranges from raw tokens
|
|
238
|
+
const docTokens = this.tokens.filter(t => t.type === TokenType.DOCSTRING);
|
|
239
|
+
if (docTokens.length === 0) return;
|
|
240
|
+
|
|
241
|
+
// Group consecutive docstring lines
|
|
242
|
+
const groups = [];
|
|
243
|
+
let current = [docTokens[0]];
|
|
244
|
+
for (let i = 1; i < docTokens.length; i++) {
|
|
245
|
+
if (docTokens[i].line === current[current.length - 1].line + 1) {
|
|
246
|
+
current.push(docTokens[i]);
|
|
247
|
+
} else {
|
|
248
|
+
groups.push(current);
|
|
249
|
+
current = [docTokens[i]];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
groups.push(current);
|
|
253
|
+
|
|
254
|
+
// Map: endLine → docstring text
|
|
255
|
+
const docsByEndLine = new Map();
|
|
256
|
+
for (const group of groups) {
|
|
257
|
+
const endLine = group[group.length - 1].line;
|
|
258
|
+
const text = group.map(t => t.value).join('\n');
|
|
259
|
+
docsByEndLine.set(endLine, text);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Walk top-level nodes and attach docstrings
|
|
263
|
+
const docTypes = new Set(['FunctionDeclaration', 'TypeDeclaration', 'InterfaceDeclaration', 'Assignment', 'TraitDeclaration']);
|
|
264
|
+
const walk = (nodes) => {
|
|
265
|
+
for (const node of nodes) {
|
|
266
|
+
if (!node || !node.loc) continue;
|
|
267
|
+
if (docTypes.has(node.type)) {
|
|
268
|
+
const doc = docsByEndLine.get(node.loc.line - 1);
|
|
269
|
+
if (doc) node.docstring = doc;
|
|
270
|
+
}
|
|
271
|
+
// Walk into blocks
|
|
272
|
+
if (node.body && Array.isArray(node.body)) walk(node.body);
|
|
273
|
+
if (node.type === 'ServerBlock' || node.type === 'ClientBlock' || node.type === 'SharedBlock') {
|
|
274
|
+
if (node.body) walk(node.body);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
walk(program.body);
|
|
185
279
|
}
|
|
186
280
|
|
|
187
281
|
parseTopLevel() {
|
|
188
|
-
if (this.check(TokenType.SERVER))
|
|
189
|
-
|
|
282
|
+
if (this.check(TokenType.SERVER)) {
|
|
283
|
+
if (!Parser.prototype._serverParserInstalled) {
|
|
284
|
+
const { installServerParser } = import.meta.require('./server-parser.js');
|
|
285
|
+
installServerParser(Parser);
|
|
286
|
+
}
|
|
287
|
+
return this.parseServerBlock();
|
|
288
|
+
}
|
|
289
|
+
if (this.check(TokenType.CLIENT)) {
|
|
290
|
+
if (!Parser.prototype._clientParserInstalled) {
|
|
291
|
+
const { installClientParser } = import.meta.require('./client-parser.js');
|
|
292
|
+
installClientParser(Parser);
|
|
293
|
+
}
|
|
294
|
+
return this.parseClientBlock();
|
|
295
|
+
}
|
|
190
296
|
if (this.check(TokenType.SHARED)) return this.parseSharedBlock();
|
|
191
297
|
if (this.check(TokenType.IMPORT)) return this.parseImport();
|
|
192
298
|
// data block: data { ... }
|
|
@@ -200,6 +306,13 @@ export class Parser {
|
|
|
200
306
|
return this.parseTestBlock();
|
|
201
307
|
}
|
|
202
308
|
}
|
|
309
|
+
// bench block: bench "name" { ... } or bench { ... }
|
|
310
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'bench') {
|
|
311
|
+
const next = this.peek(1);
|
|
312
|
+
if (next.type === TokenType.LBRACE || next.type === TokenType.STRING) {
|
|
313
|
+
return this.parseBenchBlock();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
203
316
|
return this.parseStatement();
|
|
204
317
|
}
|
|
205
318
|
|
|
@@ -210,10 +323,44 @@ export class Parser {
|
|
|
210
323
|
if (this.check(TokenType.STRING)) {
|
|
211
324
|
name = this.advance().value;
|
|
212
325
|
}
|
|
326
|
+
// Parse optional timeout=N
|
|
327
|
+
let timeout = null;
|
|
328
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'timeout' && this.peek(1).type === TokenType.ASSIGN) {
|
|
329
|
+
this.advance(); // consume 'timeout'
|
|
330
|
+
this.advance(); // consume '='
|
|
331
|
+
const tok = this.expect(TokenType.NUMBER, "Expected number after timeout=");
|
|
332
|
+
timeout = Number(tok.value);
|
|
333
|
+
}
|
|
213
334
|
this.expect(TokenType.LBRACE, "Expected '{' after test block name");
|
|
214
335
|
const body = [];
|
|
336
|
+
let beforeEach = null;
|
|
337
|
+
let afterEach = null;
|
|
215
338
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
216
339
|
try {
|
|
340
|
+
// Check for before_each { ... }
|
|
341
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'before_each' && this.peek(1).type === TokenType.LBRACE) {
|
|
342
|
+
this.advance(); // consume 'before_each'
|
|
343
|
+
this.expect(TokenType.LBRACE, "Expected '{' after before_each");
|
|
344
|
+
beforeEach = [];
|
|
345
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
346
|
+
const s = this.parseStatement();
|
|
347
|
+
if (s) beforeEach.push(s);
|
|
348
|
+
}
|
|
349
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close before_each");
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
// Check for after_each { ... }
|
|
353
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'after_each' && this.peek(1).type === TokenType.LBRACE) {
|
|
354
|
+
this.advance(); // consume 'after_each'
|
|
355
|
+
this.expect(TokenType.LBRACE, "Expected '{' after after_each");
|
|
356
|
+
afterEach = [];
|
|
357
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
358
|
+
const s = this.parseStatement();
|
|
359
|
+
if (s) afterEach.push(s);
|
|
360
|
+
}
|
|
361
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close after_each");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
217
364
|
const stmt = this.parseStatement();
|
|
218
365
|
if (stmt) body.push(stmt);
|
|
219
366
|
} catch (e) {
|
|
@@ -222,56 +369,33 @@ export class Parser {
|
|
|
222
369
|
}
|
|
223
370
|
}
|
|
224
371
|
this.expect(TokenType.RBRACE, "Expected '}' to close test block");
|
|
225
|
-
return new AST.TestBlock(name, body, l);
|
|
372
|
+
return new AST.TestBlock(name, body, l, { timeout, beforeEach, afterEach });
|
|
226
373
|
}
|
|
227
374
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
parseServerBlock() {
|
|
375
|
+
parseBenchBlock() {
|
|
231
376
|
const l = this.loc();
|
|
232
|
-
this.
|
|
233
|
-
// Optional block name: server "api" { }
|
|
377
|
+
this.advance(); // consume 'bench'
|
|
234
378
|
let name = null;
|
|
235
379
|
if (this.check(TokenType.STRING)) {
|
|
236
380
|
name = this.advance().value;
|
|
237
381
|
}
|
|
238
|
-
this.expect(TokenType.LBRACE, "Expected '{' after
|
|
382
|
+
this.expect(TokenType.LBRACE, "Expected '{' after bench block name");
|
|
239
383
|
const body = [];
|
|
240
384
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
241
385
|
try {
|
|
242
|
-
const stmt = this.
|
|
386
|
+
const stmt = this.parseStatement();
|
|
243
387
|
if (stmt) body.push(stmt);
|
|
244
388
|
} catch (e) {
|
|
245
389
|
this.errors.push(e);
|
|
246
390
|
this._synchronizeBlock();
|
|
247
391
|
}
|
|
248
392
|
}
|
|
249
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close
|
|
250
|
-
return new AST.
|
|
393
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close bench block");
|
|
394
|
+
return new AST.BenchBlock(name, body, l);
|
|
251
395
|
}
|
|
252
396
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.expect(TokenType.CLIENT);
|
|
256
|
-
// Optional block name: client "admin" { }
|
|
257
|
-
let name = null;
|
|
258
|
-
if (this.check(TokenType.STRING)) {
|
|
259
|
-
name = this.advance().value;
|
|
260
|
-
}
|
|
261
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'client'");
|
|
262
|
-
const body = [];
|
|
263
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
264
|
-
try {
|
|
265
|
-
const stmt = this.parseClientStatement();
|
|
266
|
-
if (stmt) body.push(stmt);
|
|
267
|
-
} catch (e) {
|
|
268
|
-
this.errors.push(e);
|
|
269
|
-
this._synchronizeBlock();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close client block");
|
|
273
|
-
return new AST.ClientBlock(body, l, name);
|
|
274
|
-
}
|
|
397
|
+
// ─── Full-stack blocks ────────────────────────────────────
|
|
398
|
+
// parseClientBlock() and client-specific methods are in client-parser.js (lazy-loaded)
|
|
275
399
|
|
|
276
400
|
parseSharedBlock() {
|
|
277
401
|
const l = this.loc();
|
|
@@ -292,1055 +416,149 @@ export class Parser {
|
|
|
292
416
|
this._synchronizeBlock();
|
|
293
417
|
}
|
|
294
418
|
}
|
|
295
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close shared block");
|
|
296
|
-
return new AST.SharedBlock(body, l, name);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ─── Data block ────────────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
parseDataBlock() {
|
|
302
|
-
const l = this.loc();
|
|
303
|
-
this.advance(); // consume 'data'
|
|
304
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'data'");
|
|
305
|
-
const body = [];
|
|
306
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
307
|
-
try {
|
|
308
|
-
const stmt = this.parseDataStatement();
|
|
309
|
-
if (stmt) body.push(stmt);
|
|
310
|
-
} catch (e) {
|
|
311
|
-
this.errors.push(e);
|
|
312
|
-
this._synchronizeBlock();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close data block");
|
|
316
|
-
return new AST.DataBlock(body, l);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
parseDataStatement() {
|
|
320
|
-
if (!this.check(TokenType.IDENTIFIER)) {
|
|
321
|
-
return this.parseStatement();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const val = this.current().value;
|
|
325
|
-
|
|
326
|
-
// source customers: Table<Customer> = read("customers.csv")
|
|
327
|
-
if (val === 'source') {
|
|
328
|
-
return this.parseSourceDeclaration();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// pipeline clean_customers = customers |> where(...)
|
|
332
|
-
if (val === 'pipeline') {
|
|
333
|
-
return this.parsePipelineDeclaration();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// validate Customer { .email |> contains("@"), ... }
|
|
337
|
-
if (val === 'validate') {
|
|
338
|
-
return this.parseValidateBlock();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// refresh customers every 15.minutes
|
|
342
|
-
// refresh orders on_demand
|
|
343
|
-
if (val === 'refresh') {
|
|
344
|
-
return this.parseRefreshPolicy();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return this.parseStatement();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
parseSourceDeclaration() {
|
|
351
|
-
const l = this.loc();
|
|
352
|
-
this.advance(); // consume 'source'
|
|
353
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected source name").value;
|
|
354
|
-
|
|
355
|
-
// Optional type annotation: source customers: Table<Customer>
|
|
356
|
-
let typeAnnotation = null;
|
|
357
|
-
if (this.match(TokenType.COLON)) {
|
|
358
|
-
typeAnnotation = this.parseTypeAnnotation();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
this.expect(TokenType.ASSIGN, "Expected '=' after source name");
|
|
362
|
-
const expression = this.parseExpression();
|
|
363
|
-
|
|
364
|
-
return new AST.SourceDeclaration(name, typeAnnotation, expression, l);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
parsePipelineDeclaration() {
|
|
368
|
-
const l = this.loc();
|
|
369
|
-
this.advance(); // consume 'pipeline'
|
|
370
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected pipeline name").value;
|
|
371
|
-
this.expect(TokenType.ASSIGN, "Expected '=' after pipeline name");
|
|
372
|
-
const expression = this.parseExpression();
|
|
373
|
-
return new AST.PipelineDeclaration(name, expression, l);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
parseValidateBlock() {
|
|
377
|
-
const l = this.loc();
|
|
378
|
-
this.advance(); // consume 'validate'
|
|
379
|
-
const typeName = this.expect(TokenType.IDENTIFIER, "Expected type name after 'validate'").value;
|
|
380
|
-
this.expect(TokenType.LBRACE, "Expected '{' after validate type name");
|
|
381
|
-
|
|
382
|
-
const rules = [];
|
|
383
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
384
|
-
const rule = this.parseExpression();
|
|
385
|
-
rules.push(rule);
|
|
386
|
-
this.match(TokenType.COMMA); // optional comma separator
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close validate block");
|
|
390
|
-
return new AST.ValidateBlock(typeName, rules, l);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
parseRefreshPolicy() {
|
|
394
|
-
const l = this.loc();
|
|
395
|
-
this.advance(); // consume 'refresh'
|
|
396
|
-
const sourceName = this.expect(TokenType.IDENTIFIER, "Expected source name after 'refresh'").value;
|
|
397
|
-
|
|
398
|
-
// refresh X every N.unit OR refresh X on_demand
|
|
399
|
-
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'on_demand') {
|
|
400
|
-
this.advance();
|
|
401
|
-
return new AST.RefreshPolicy(sourceName, 'on_demand', l);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// expect 'every'
|
|
405
|
-
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'every') {
|
|
406
|
-
this.advance(); // consume 'every'
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Parse interval: N.unit (e.g., 15.minutes, 1.hour)
|
|
410
|
-
const value = this.expect(TokenType.NUMBER, "Expected interval value").value;
|
|
411
|
-
this.expect(TokenType.DOT, "Expected '.' after interval value");
|
|
412
|
-
const unit = this.expect(TokenType.IDENTIFIER, "Expected time unit (minutes, hours, seconds)").value;
|
|
413
|
-
|
|
414
|
-
return new AST.RefreshPolicy(sourceName, { value, unit }, l);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ─── Server-specific statements ───────────────────────────
|
|
418
|
-
|
|
419
|
-
parseServerStatement() {
|
|
420
|
-
if (this.check(TokenType.ROUTE)) return this.parseRoute();
|
|
421
|
-
|
|
422
|
-
// Contextual keywords in server blocks
|
|
423
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
424
|
-
const val = this.current().value;
|
|
425
|
-
if (val === 'middleware' && this.peek(1).type === TokenType.FN) {
|
|
426
|
-
return this.parseMiddleware();
|
|
427
|
-
}
|
|
428
|
-
if (val === 'health') {
|
|
429
|
-
return this.parseHealthCheck();
|
|
430
|
-
}
|
|
431
|
-
if (val === 'cors' && this.peek(1).type === TokenType.LBRACE) {
|
|
432
|
-
return this.parseCorsConfig();
|
|
433
|
-
}
|
|
434
|
-
if (val === 'on_error' && this.peek(1).type === TokenType.FN) {
|
|
435
|
-
return this.parseErrorHandler();
|
|
436
|
-
}
|
|
437
|
-
if (val === 'ws' && this.peek(1).type === TokenType.LBRACE) {
|
|
438
|
-
return this.parseWebSocket();
|
|
439
|
-
}
|
|
440
|
-
if (val === 'static' && this.peek(1).type === TokenType.STRING) {
|
|
441
|
-
return this.parseStaticDeclaration();
|
|
442
|
-
}
|
|
443
|
-
if (val === 'discover' && this.peek(1).type === TokenType.STRING) {
|
|
444
|
-
return this.parseDiscover();
|
|
445
|
-
}
|
|
446
|
-
if (val === 'auth' && this.peek(1).type === TokenType.LBRACE) {
|
|
447
|
-
return this.parseAuthConfig();
|
|
448
|
-
}
|
|
449
|
-
if (val === 'max_body') {
|
|
450
|
-
return this.parseMaxBody();
|
|
451
|
-
}
|
|
452
|
-
if (val === 'routes' && this.peek(1).type === TokenType.STRING) {
|
|
453
|
-
return this.parseRouteGroup();
|
|
454
|
-
}
|
|
455
|
-
if (val === 'rate_limit' && this.peek(1).type === TokenType.LBRACE) {
|
|
456
|
-
return this.parseRateLimitConfig();
|
|
457
|
-
}
|
|
458
|
-
if (val === 'on_start' && this.peek(1).type === TokenType.FN) {
|
|
459
|
-
return this.parseLifecycleHook('start');
|
|
460
|
-
}
|
|
461
|
-
if (val === 'on_stop' && this.peek(1).type === TokenType.FN) {
|
|
462
|
-
return this.parseLifecycleHook('stop');
|
|
463
|
-
}
|
|
464
|
-
if (val === 'subscribe' && this.peek(1).type === TokenType.STRING) {
|
|
465
|
-
return this.parseSubscribe();
|
|
466
|
-
}
|
|
467
|
-
if (val === 'env' && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
468
|
-
return this.parseEnvDeclaration();
|
|
469
|
-
}
|
|
470
|
-
if (val === 'schedule' && this.peek(1).type === TokenType.STRING) {
|
|
471
|
-
return this.parseSchedule();
|
|
472
|
-
}
|
|
473
|
-
if (val === 'upload' && this.peek(1).type === TokenType.LBRACE) {
|
|
474
|
-
return this.parseUploadConfig();
|
|
475
|
-
}
|
|
476
|
-
if (val === 'session' && this.peek(1).type === TokenType.LBRACE) {
|
|
477
|
-
return this.parseSessionConfig();
|
|
478
|
-
}
|
|
479
|
-
if (val === 'db' && this.peek(1).type === TokenType.LBRACE) {
|
|
480
|
-
return this.parseDbConfig();
|
|
481
|
-
}
|
|
482
|
-
if (val === 'tls' && this.peek(1).type === TokenType.LBRACE) {
|
|
483
|
-
return this.parseTlsConfig();
|
|
484
|
-
}
|
|
485
|
-
if (val === 'compression' && this.peek(1).type === TokenType.LBRACE) {
|
|
486
|
-
return this.parseCompressionConfig();
|
|
487
|
-
}
|
|
488
|
-
if (val === 'background' && this.peek(1).type === TokenType.FN) {
|
|
489
|
-
return this.parseBackgroundJob();
|
|
490
|
-
}
|
|
491
|
-
if (val === 'cache' && this.peek(1).type === TokenType.LBRACE) {
|
|
492
|
-
return this.parseCacheConfig();
|
|
493
|
-
}
|
|
494
|
-
if (val === 'sse' && this.peek(1).type === TokenType.STRING) {
|
|
495
|
-
return this.parseSseDeclaration();
|
|
496
|
-
}
|
|
497
|
-
if (val === 'model' && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
498
|
-
return this.parseModelDeclaration();
|
|
499
|
-
}
|
|
500
|
-
// ai { ... } or ai "name" { ... }
|
|
501
|
-
if (val === 'ai' && (this.peek(1).type === TokenType.LBRACE || this.peek(1).type === TokenType.STRING)) {
|
|
502
|
-
return this.parseAiConfig();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
return this.parseStatement();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
parseMiddleware() {
|
|
510
|
-
const l = this.loc();
|
|
511
|
-
this.advance(); // consume 'middleware'
|
|
512
|
-
this.expect(TokenType.FN);
|
|
513
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected middleware name").value;
|
|
514
|
-
this.expect(TokenType.LPAREN, "Expected '(' after middleware name");
|
|
515
|
-
const params = this.parseParameterList();
|
|
516
|
-
this.expect(TokenType.RPAREN, "Expected ')' after middleware parameters");
|
|
517
|
-
const body = this.parseBlock();
|
|
518
|
-
return new AST.MiddlewareDeclaration(name, params, body, l);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
parseHealthCheck() {
|
|
522
|
-
const l = this.loc();
|
|
523
|
-
this.advance(); // consume 'health'
|
|
524
|
-
const path = this.expect(TokenType.STRING, "Expected health check path string");
|
|
525
|
-
return new AST.HealthCheckDeclaration(path.value, l);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
parseCorsConfig() {
|
|
529
|
-
const l = this.loc();
|
|
530
|
-
this.advance(); // consume 'cors'
|
|
531
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'cors'");
|
|
532
|
-
const config = {};
|
|
533
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
534
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected cors config key").value;
|
|
535
|
-
this.expect(TokenType.COLON, "Expected ':' after cors key");
|
|
536
|
-
const value = this.parseExpression();
|
|
537
|
-
config[key] = value;
|
|
538
|
-
this.match(TokenType.COMMA);
|
|
539
|
-
}
|
|
540
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close cors config");
|
|
541
|
-
return new AST.CorsDeclaration(config, l);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
parseErrorHandler() {
|
|
545
|
-
const l = this.loc();
|
|
546
|
-
this.advance(); // consume 'on_error'
|
|
547
|
-
this.expect(TokenType.FN);
|
|
548
|
-
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
549
|
-
const params = this.parseParameterList();
|
|
550
|
-
this.expect(TokenType.RPAREN, "Expected ')' after error handler parameters");
|
|
551
|
-
const body = this.parseBlock();
|
|
552
|
-
return new AST.ErrorHandlerDeclaration(params, body, l);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
parseWebSocket() {
|
|
556
|
-
const l = this.loc();
|
|
557
|
-
this.advance(); // consume 'ws'
|
|
558
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'ws'");
|
|
559
|
-
|
|
560
|
-
const handlers = {};
|
|
561
|
-
const config = {};
|
|
562
|
-
const validEvents = ['on_open', 'on_message', 'on_close', 'on_error'];
|
|
563
|
-
const validConfigKeys = ['auth'];
|
|
564
|
-
|
|
565
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
566
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected WebSocket event handler name or config key").value;
|
|
567
|
-
if (validConfigKeys.includes(name)) {
|
|
568
|
-
// Config key: auth: <expr>
|
|
569
|
-
this.expect(TokenType.COLON, `Expected ':' after '${name}'`);
|
|
570
|
-
config[name] = this.parseExpression();
|
|
571
|
-
this.match(TokenType.COMMA);
|
|
572
|
-
} else if (validEvents.includes(name)) {
|
|
573
|
-
this.expect(TokenType.FN, "Expected 'fn' after event name");
|
|
574
|
-
this.expect(TokenType.LPAREN);
|
|
575
|
-
const params = this.parseParameterList();
|
|
576
|
-
this.expect(TokenType.RPAREN);
|
|
577
|
-
const body = this.parseBlock();
|
|
578
|
-
handlers[name] = { params, body };
|
|
579
|
-
} else {
|
|
580
|
-
this.error(`Invalid WebSocket key '${name}'. Expected one of: ${[...validConfigKeys, ...validEvents].join(', ')}`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close ws block");
|
|
585
|
-
const wsConfig = Object.keys(config).length > 0 ? config : null;
|
|
586
|
-
return new AST.WebSocketDeclaration(handlers, l, wsConfig);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
parseStaticDeclaration() {
|
|
590
|
-
const l = this.loc();
|
|
591
|
-
this.advance(); // consume 'static'
|
|
592
|
-
const urlPath = this.expect(TokenType.STRING, "Expected URL path for static files").value;
|
|
593
|
-
this.expect(TokenType.ARROW, "Expected '=>' after static path");
|
|
594
|
-
const dir = this.expect(TokenType.STRING, "Expected directory path for static files").value;
|
|
595
|
-
let fallback = null;
|
|
596
|
-
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'fallback') {
|
|
597
|
-
this.advance(); // consume 'fallback'
|
|
598
|
-
fallback = this.expect(TokenType.STRING, "Expected fallback file path").value;
|
|
599
|
-
}
|
|
600
|
-
return new AST.StaticDeclaration(urlPath, dir, l, fallback);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
parseDiscover() {
|
|
604
|
-
const l = this.loc();
|
|
605
|
-
this.advance(); // consume 'discover'
|
|
606
|
-
const peerName = this.expect(TokenType.STRING, "Expected peer name string after 'discover'").value;
|
|
607
|
-
// Expect 'at' as contextual keyword
|
|
608
|
-
const atTok = this.expect(TokenType.IDENTIFIER, "Expected 'at' after peer name");
|
|
609
|
-
if (atTok.value !== 'at') {
|
|
610
|
-
this.error("Expected 'at' after peer name in discover declaration");
|
|
611
|
-
}
|
|
612
|
-
const urlExpression = this.parseExpression();
|
|
613
|
-
let config = null;
|
|
614
|
-
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'with') {
|
|
615
|
-
this.advance(); // consume 'with'
|
|
616
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'with'");
|
|
617
|
-
config = {};
|
|
618
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
619
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
|
|
620
|
-
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
621
|
-
const value = this.parseExpression();
|
|
622
|
-
config[key] = value;
|
|
623
|
-
this.match(TokenType.COMMA);
|
|
624
|
-
}
|
|
625
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close discover config");
|
|
626
|
-
}
|
|
627
|
-
return new AST.DiscoverDeclaration(peerName, urlExpression, l, config);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
parseAuthConfig() {
|
|
631
|
-
const l = this.loc();
|
|
632
|
-
this.advance(); // consume 'auth'
|
|
633
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'auth'");
|
|
634
|
-
const config = {};
|
|
635
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
636
|
-
// Accept keywords (like 'type') and identifiers as config keys
|
|
637
|
-
let key;
|
|
638
|
-
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE)) {
|
|
639
|
-
key = this.advance().value;
|
|
640
|
-
} else {
|
|
641
|
-
this.error("Expected auth config key");
|
|
642
|
-
}
|
|
643
|
-
this.expect(TokenType.COLON, "Expected ':' after auth key");
|
|
644
|
-
const value = this.parseExpression();
|
|
645
|
-
config[key] = value;
|
|
646
|
-
this.match(TokenType.COMMA);
|
|
647
|
-
}
|
|
648
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close auth config");
|
|
649
|
-
return new AST.AuthDeclaration(config, l);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
parseMaxBody() {
|
|
653
|
-
const l = this.loc();
|
|
654
|
-
this.advance(); // consume 'max_body'
|
|
655
|
-
const limit = this.parseExpression();
|
|
656
|
-
return new AST.MaxBodyDeclaration(limit, l);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
parseRouteGroup() {
|
|
660
|
-
const l = this.loc();
|
|
661
|
-
this.advance(); // consume 'routes'
|
|
662
|
-
const prefix = this.expect(TokenType.STRING, "Expected route group prefix string").value;
|
|
663
|
-
this.expect(TokenType.LBRACE, "Expected '{' after route group prefix");
|
|
664
|
-
const body = [];
|
|
665
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
666
|
-
try {
|
|
667
|
-
const stmt = this.parseServerStatement();
|
|
668
|
-
if (stmt) body.push(stmt);
|
|
669
|
-
} catch (e) {
|
|
670
|
-
this.errors.push(e);
|
|
671
|
-
this._synchronizeBlock();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close route group");
|
|
675
|
-
return new AST.RouteGroupDeclaration(prefix, body, l);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
parseRateLimitConfig() {
|
|
679
|
-
const l = this.loc();
|
|
680
|
-
this.advance(); // consume 'rate_limit'
|
|
681
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'rate_limit'");
|
|
682
|
-
const config = {};
|
|
683
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
684
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected rate_limit config key").value;
|
|
685
|
-
this.expect(TokenType.COLON, "Expected ':' after rate_limit key");
|
|
686
|
-
const value = this.parseExpression();
|
|
687
|
-
config[key] = value;
|
|
688
|
-
this.match(TokenType.COMMA);
|
|
689
|
-
}
|
|
690
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close rate_limit config");
|
|
691
|
-
return new AST.RateLimitDeclaration(config, l);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
parseLifecycleHook(hookName) {
|
|
695
|
-
const l = this.loc();
|
|
696
|
-
this.advance(); // consume 'on_start' or 'on_stop'
|
|
697
|
-
this.expect(TokenType.FN);
|
|
698
|
-
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
699
|
-
const params = this.parseParameterList();
|
|
700
|
-
this.expect(TokenType.RPAREN, "Expected ')' after lifecycle hook parameters");
|
|
701
|
-
const body = this.parseBlock();
|
|
702
|
-
return new AST.LifecycleHookDeclaration(hookName, params, body, l);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
parseSubscribe() {
|
|
706
|
-
const l = this.loc();
|
|
707
|
-
this.advance(); // consume 'subscribe'
|
|
708
|
-
const event = this.expect(TokenType.STRING, "Expected event name string").value;
|
|
709
|
-
this.expect(TokenType.FN, "Expected 'fn' after event name");
|
|
710
|
-
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
711
|
-
const params = this.parseParameterList();
|
|
712
|
-
this.expect(TokenType.RPAREN, "Expected ')' after subscribe parameters");
|
|
713
|
-
const body = this.parseBlock();
|
|
714
|
-
return new AST.SubscribeDeclaration(event, params, body, l);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
parseEnvDeclaration() {
|
|
718
|
-
const l = this.loc();
|
|
719
|
-
this.advance(); // consume 'env'
|
|
720
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected env variable name").value;
|
|
721
|
-
this.expect(TokenType.COLON, "Expected ':' after env variable name");
|
|
722
|
-
const typeAnnotation = this.parseTypeAnnotation();
|
|
723
|
-
let defaultValue = null;
|
|
724
|
-
if (this.match(TokenType.ASSIGN)) {
|
|
725
|
-
defaultValue = this.parseExpression();
|
|
726
|
-
}
|
|
727
|
-
return new AST.EnvDeclaration(name, typeAnnotation, defaultValue, l);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
parseSchedule() {
|
|
731
|
-
const l = this.loc();
|
|
732
|
-
this.advance(); // consume 'schedule'
|
|
733
|
-
const pattern = this.expect(TokenType.STRING, "Expected schedule pattern string").value;
|
|
734
|
-
this.expect(TokenType.FN, "Expected 'fn' after schedule pattern");
|
|
735
|
-
let name = null;
|
|
736
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
737
|
-
name = this.advance().value;
|
|
738
|
-
}
|
|
739
|
-
this.expect(TokenType.LPAREN, "Expected '(' after schedule fn");
|
|
740
|
-
const params = this.parseParameterList();
|
|
741
|
-
this.expect(TokenType.RPAREN, "Expected ')' after schedule parameters");
|
|
742
|
-
const body = this.parseBlock();
|
|
743
|
-
return new AST.ScheduleDeclaration(pattern, name, params, body, l);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
parseUploadConfig() {
|
|
747
|
-
const l = this.loc();
|
|
748
|
-
this.advance(); // consume 'upload'
|
|
749
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'upload'");
|
|
750
|
-
const config = {};
|
|
751
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
752
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected upload config key").value;
|
|
753
|
-
this.expect(TokenType.COLON, "Expected ':' after upload key");
|
|
754
|
-
const value = this.parseExpression();
|
|
755
|
-
config[key] = value;
|
|
756
|
-
this.match(TokenType.COMMA);
|
|
757
|
-
}
|
|
758
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close upload config");
|
|
759
|
-
return new AST.UploadDeclaration(config, l);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
parseSessionConfig() {
|
|
763
|
-
const l = this.loc();
|
|
764
|
-
this.advance(); // consume 'session'
|
|
765
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'session'");
|
|
766
|
-
const config = {};
|
|
767
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
768
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected session config key").value;
|
|
769
|
-
this.expect(TokenType.COLON, "Expected ':' after session key");
|
|
770
|
-
const value = this.parseExpression();
|
|
771
|
-
config[key] = value;
|
|
772
|
-
this.match(TokenType.COMMA);
|
|
773
|
-
}
|
|
774
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close session config");
|
|
775
|
-
return new AST.SessionDeclaration(config, l);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
parseAiConfig() {
|
|
779
|
-
const l = this.loc();
|
|
780
|
-
this.advance(); // consume 'ai'
|
|
781
|
-
|
|
782
|
-
// Optional name: ai "claude" { ... }
|
|
783
|
-
let name = null;
|
|
784
|
-
if (this.check(TokenType.STRING)) {
|
|
785
|
-
name = this.advance().value;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'ai'");
|
|
789
|
-
const config = {};
|
|
790
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
791
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected ai config key").value;
|
|
792
|
-
this.expect(TokenType.COLON, "Expected ':' after ai config key");
|
|
793
|
-
const value = this.parseExpression();
|
|
794
|
-
config[key] = value;
|
|
795
|
-
this.match(TokenType.COMMA);
|
|
796
|
-
}
|
|
797
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close ai config");
|
|
798
|
-
return new AST.AiConfigDeclaration(name, config, l);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
parseDbConfig() {
|
|
802
|
-
const l = this.loc();
|
|
803
|
-
this.advance(); // consume 'db'
|
|
804
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'db'");
|
|
805
|
-
const config = {};
|
|
806
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
807
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected db config key").value;
|
|
808
|
-
this.expect(TokenType.COLON, "Expected ':' after db key");
|
|
809
|
-
const value = this.parseExpression();
|
|
810
|
-
config[key] = value;
|
|
811
|
-
this.match(TokenType.COMMA);
|
|
812
|
-
}
|
|
813
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close db config");
|
|
814
|
-
return new AST.DbDeclaration(config, l);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
parseTlsConfig() {
|
|
818
|
-
const l = this.loc();
|
|
819
|
-
this.advance(); // consume 'tls'
|
|
820
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'tls'");
|
|
821
|
-
const config = {};
|
|
822
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
823
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected tls config key").value;
|
|
824
|
-
this.expect(TokenType.COLON, "Expected ':' after tls key");
|
|
825
|
-
const value = this.parseExpression();
|
|
826
|
-
config[key] = value;
|
|
827
|
-
this.match(TokenType.COMMA);
|
|
828
|
-
}
|
|
829
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close tls config");
|
|
830
|
-
return new AST.TlsDeclaration(config, l);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
parseCompressionConfig() {
|
|
834
|
-
const l = this.loc();
|
|
835
|
-
this.advance(); // consume 'compression'
|
|
836
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'compression'");
|
|
837
|
-
const config = {};
|
|
838
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
839
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected compression config key").value;
|
|
840
|
-
this.expect(TokenType.COLON, "Expected ':' after compression key");
|
|
841
|
-
const value = this.parseExpression();
|
|
842
|
-
config[key] = value;
|
|
843
|
-
this.match(TokenType.COMMA);
|
|
844
|
-
}
|
|
845
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close compression config");
|
|
846
|
-
return new AST.CompressionDeclaration(config, l);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
parseBackgroundJob() {
|
|
850
|
-
const l = this.loc();
|
|
851
|
-
this.advance(); // consume 'background'
|
|
852
|
-
this.expect(TokenType.FN, "Expected 'fn' after 'background'");
|
|
853
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected background job name").value;
|
|
854
|
-
this.expect(TokenType.LPAREN, "Expected '(' after background job name");
|
|
855
|
-
const params = this.parseParameterList();
|
|
856
|
-
this.expect(TokenType.RPAREN, "Expected ')' after background job parameters");
|
|
857
|
-
const body = this.parseBlock();
|
|
858
|
-
return new AST.BackgroundJobDeclaration(name, params, body, l);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
parseCacheConfig() {
|
|
862
|
-
const l = this.loc();
|
|
863
|
-
this.advance(); // consume 'cache'
|
|
864
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'cache'");
|
|
865
|
-
const config = {};
|
|
866
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
867
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected cache config key").value;
|
|
868
|
-
this.expect(TokenType.COLON, "Expected ':' after cache key");
|
|
869
|
-
const value = this.parseExpression();
|
|
870
|
-
config[key] = value;
|
|
871
|
-
this.match(TokenType.COMMA);
|
|
872
|
-
}
|
|
873
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close cache config");
|
|
874
|
-
return new AST.CacheDeclaration(config, l);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
parseSseDeclaration() {
|
|
878
|
-
const l = this.loc();
|
|
879
|
-
this.advance(); // consume 'sse'
|
|
880
|
-
const path = this.expect(TokenType.STRING, "Expected SSE endpoint path").value;
|
|
881
|
-
this.expect(TokenType.FN, "Expected 'fn' after SSE path");
|
|
882
|
-
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
883
|
-
const params = this.parseParameterList();
|
|
884
|
-
this.expect(TokenType.RPAREN, "Expected ')' after SSE parameters");
|
|
885
|
-
const body = this.parseBlock();
|
|
886
|
-
return new AST.SseDeclaration(path, params, body, l);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
parseModelDeclaration() {
|
|
890
|
-
const l = this.loc();
|
|
891
|
-
this.advance(); // consume 'model'
|
|
892
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected model/type name after 'model'").value;
|
|
893
|
-
let config = null;
|
|
894
|
-
if (this.check(TokenType.LBRACE)) {
|
|
895
|
-
this.advance(); // consume '{'
|
|
896
|
-
config = {};
|
|
897
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
898
|
-
const key = this.expect(TokenType.IDENTIFIER, "Expected model config key").value;
|
|
899
|
-
this.expect(TokenType.COLON, "Expected ':' after model config key");
|
|
900
|
-
const value = this.parseExpression();
|
|
901
|
-
config[key] = value;
|
|
902
|
-
this.match(TokenType.COMMA);
|
|
903
|
-
}
|
|
904
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close model config");
|
|
905
|
-
}
|
|
906
|
-
return new AST.ModelDeclaration(name, config, l);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
parseRoute() {
|
|
910
|
-
const l = this.loc();
|
|
911
|
-
this.expect(TokenType.ROUTE);
|
|
912
|
-
|
|
913
|
-
// HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS (as identifiers)
|
|
914
|
-
const methodTok = this.expect(TokenType.IDENTIFIER, "Expected HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)");
|
|
915
|
-
const method = methodTok.value.toUpperCase();
|
|
916
|
-
if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
917
|
-
this.error(`Invalid HTTP method: ${method}`);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const path = this.expect(TokenType.STRING, "Expected route path string");
|
|
921
|
-
|
|
922
|
-
// Optional decorators: route GET "/path" with auth, role("admin") => handler
|
|
923
|
-
let decorators = [];
|
|
924
|
-
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'with') {
|
|
925
|
-
this.advance(); // consume 'with'
|
|
926
|
-
// Parse comma-separated decorator list
|
|
927
|
-
do {
|
|
928
|
-
const decName = this.expect(TokenType.IDENTIFIER, "Expected decorator name").value;
|
|
929
|
-
let decArgs = [];
|
|
930
|
-
if (this.check(TokenType.LPAREN)) {
|
|
931
|
-
this.advance(); // (
|
|
932
|
-
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
933
|
-
decArgs.push(this.parseExpression());
|
|
934
|
-
if (!this.match(TokenType.COMMA)) break;
|
|
935
|
-
}
|
|
936
|
-
this.expect(TokenType.RPAREN, "Expected ')' after decorator arguments");
|
|
937
|
-
}
|
|
938
|
-
decorators.push({ name: decName, args: decArgs });
|
|
939
|
-
} while (this.match(TokenType.COMMA));
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
this.expect(TokenType.ARROW, "Expected '=>' after route path");
|
|
943
|
-
const handler = this.parseExpression();
|
|
944
|
-
|
|
945
|
-
return new AST.RouteDeclaration(method, path.value, handler, l, decorators);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// ─── Client-specific statements ───────────────────────────
|
|
949
|
-
|
|
950
|
-
parseClientStatement() {
|
|
951
|
-
if (this.check(TokenType.STATE)) return this.parseState();
|
|
952
|
-
if (this.check(TokenType.COMPUTED)) return this.parseComputed();
|
|
953
|
-
if (this.check(TokenType.EFFECT)) return this.parseEffect();
|
|
954
|
-
if (this.check(TokenType.COMPONENT)) return this.parseComponent();
|
|
955
|
-
if (this.check(TokenType.STORE)) return this.parseStore();
|
|
956
|
-
return this.parseStatement();
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
parseStore() {
|
|
960
|
-
const l = this.loc();
|
|
961
|
-
this.expect(TokenType.STORE);
|
|
962
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected store name").value;
|
|
963
|
-
this.expect(TokenType.LBRACE, "Expected '{' after store name");
|
|
964
|
-
|
|
965
|
-
const body = [];
|
|
966
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
967
|
-
if (this.check(TokenType.STATE)) {
|
|
968
|
-
body.push(this.parseState());
|
|
969
|
-
} else if (this.check(TokenType.COMPUTED)) {
|
|
970
|
-
body.push(this.parseComputed());
|
|
971
|
-
} else if (this.check(TokenType.FN) && this.peek(1).type === TokenType.IDENTIFIER) {
|
|
972
|
-
body.push(this.parseFunctionDeclaration());
|
|
973
|
-
} else {
|
|
974
|
-
this.error("Expected 'state', 'computed', or 'fn' inside store block");
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close store block");
|
|
978
|
-
|
|
979
|
-
return new AST.StoreDeclaration(name, body, l);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
parseState() {
|
|
983
|
-
const l = this.loc();
|
|
984
|
-
this.expect(TokenType.STATE);
|
|
985
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected state variable name").value;
|
|
986
|
-
|
|
987
|
-
let typeAnnotation = null;
|
|
988
|
-
if (this.match(TokenType.COLON)) {
|
|
989
|
-
typeAnnotation = this.parseTypeAnnotation();
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
this.expect(TokenType.ASSIGN, "Expected '=' in state declaration");
|
|
993
|
-
const value = this.parseExpression();
|
|
994
|
-
|
|
995
|
-
return new AST.StateDeclaration(name, typeAnnotation, value, l);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
parseComputed() {
|
|
999
|
-
const l = this.loc();
|
|
1000
|
-
this.expect(TokenType.COMPUTED);
|
|
1001
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected computed variable name").value;
|
|
1002
|
-
this.expect(TokenType.ASSIGN, "Expected '=' in computed declaration");
|
|
1003
|
-
const expr = this.parseExpression();
|
|
1004
|
-
|
|
1005
|
-
return new AST.ComputedDeclaration(name, expr, l);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
parseEffect() {
|
|
1009
|
-
const l = this.loc();
|
|
1010
|
-
this.expect(TokenType.EFFECT);
|
|
1011
|
-
const body = this.parseBlock();
|
|
1012
|
-
return new AST.EffectDeclaration(body, l);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
parseComponent() {
|
|
1016
|
-
const l = this.loc();
|
|
1017
|
-
this.expect(TokenType.COMPONENT);
|
|
1018
|
-
const name = this.expect(TokenType.IDENTIFIER, "Expected component name").value;
|
|
1019
|
-
|
|
1020
|
-
let params = [];
|
|
1021
|
-
if (this.match(TokenType.LPAREN)) {
|
|
1022
|
-
params = this.parseParameterList();
|
|
1023
|
-
this.expect(TokenType.RPAREN, "Expected ')' after component parameters");
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
this.expect(TokenType.LBRACE, "Expected '{' to open component body");
|
|
1027
|
-
const body = [];
|
|
1028
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
1029
|
-
if (this.check(TokenType.STYLE_BLOCK)) {
|
|
1030
|
-
const sl = this.loc();
|
|
1031
|
-
const css = this.current().value;
|
|
1032
|
-
this.advance();
|
|
1033
|
-
body.push(new AST.ComponentStyleBlock(css, sl));
|
|
1034
|
-
} else if (this.check(TokenType.LESS) && this._looksLikeJSX()) {
|
|
1035
|
-
body.push(this.parseJSXElement());
|
|
1036
|
-
} else if (this.check(TokenType.STATE)) {
|
|
1037
|
-
body.push(this.parseState());
|
|
1038
|
-
} else if (this.check(TokenType.COMPUTED)) {
|
|
1039
|
-
body.push(this.parseComputed());
|
|
1040
|
-
} else if (this.check(TokenType.EFFECT)) {
|
|
1041
|
-
body.push(this.parseEffect());
|
|
1042
|
-
} else if (this.check(TokenType.COMPONENT)) {
|
|
1043
|
-
body.push(this.parseComponent());
|
|
1044
|
-
} else {
|
|
1045
|
-
body.push(this.parseStatement());
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close component body");
|
|
1049
|
-
|
|
1050
|
-
return new AST.ComponentDeclaration(name, params, body, l);
|
|
419
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close shared block");
|
|
420
|
+
return new AST.SharedBlock(body, l, name);
|
|
1051
421
|
}
|
|
1052
422
|
|
|
1053
|
-
// ───
|
|
1054
|
-
|
|
1055
|
-
_collapseJSXWhitespace(text) {
|
|
1056
|
-
let result = text.replace(/\s+/g, ' ');
|
|
1057
|
-
if (result.trim() === '') return '';
|
|
1058
|
-
return result.trim();
|
|
1059
|
-
}
|
|
423
|
+
// ─── Data block ────────────────────────────────────────────
|
|
1060
424
|
|
|
1061
|
-
|
|
425
|
+
parseDataBlock() {
|
|
1062
426
|
const l = this.loc();
|
|
1063
|
-
this.
|
|
1064
|
-
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
this.advance(); // {
|
|
1074
|
-
this.advance(); // ...
|
|
1075
|
-
const expr = this.parseExpression();
|
|
1076
|
-
this.expect(TokenType.RBRACE, "Expected '}' after spread expression");
|
|
1077
|
-
attributes.push(new AST.JSXSpreadAttribute(expr, sl));
|
|
1078
|
-
} else {
|
|
1079
|
-
attributes.push(this.parseJSXAttribute());
|
|
427
|
+
this.advance(); // consume 'data'
|
|
428
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'data'");
|
|
429
|
+
const body = [];
|
|
430
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
431
|
+
try {
|
|
432
|
+
const stmt = this.parseDataStatement();
|
|
433
|
+
if (stmt) body.push(stmt);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
this.errors.push(e);
|
|
436
|
+
this._synchronizeBlock();
|
|
1080
437
|
}
|
|
1081
438
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (this.match(TokenType.SLASH)) {
|
|
1085
|
-
this.expect(TokenType.GREATER, "Expected '>' in self-closing tag");
|
|
1086
|
-
return new AST.JSXElement(tag, attributes, [], true, l);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
this.expect(TokenType.GREATER, "Expected '>'");
|
|
1090
|
-
|
|
1091
|
-
// Parse children
|
|
1092
|
-
const children = this.parseJSXChildren(tag);
|
|
1093
|
-
|
|
1094
|
-
return new AST.JSXElement(tag, attributes, children, false, l);
|
|
439
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close data block");
|
|
440
|
+
return new AST.DataBlock(body, l);
|
|
1095
441
|
}
|
|
1096
442
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
let name;
|
|
1101
|
-
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE) || this.check(TokenType.FOR) ||
|
|
1102
|
-
this.check(TokenType.IN) || this.check(TokenType.AS) || this.check(TokenType.EXPORT) ||
|
|
1103
|
-
this.check(TokenType.STATE) || this.check(TokenType.COMPUTED) || this.check(TokenType.ROUTE)) {
|
|
1104
|
-
name = this.advance().value;
|
|
1105
|
-
} else {
|
|
1106
|
-
this.error("Expected attribute name");
|
|
443
|
+
parseDataStatement() {
|
|
444
|
+
if (!this.check(TokenType.IDENTIFIER)) {
|
|
445
|
+
return this.parseStatement();
|
|
1107
446
|
}
|
|
1108
447
|
|
|
1109
|
-
|
|
1110
|
-
if (this.match(TokenType.COLON)) {
|
|
1111
|
-
let suffix;
|
|
1112
|
-
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.IN)) {
|
|
1113
|
-
suffix = this.advance().value;
|
|
1114
|
-
} else {
|
|
1115
|
-
suffix = this.expect(TokenType.IDENTIFIER, "Expected name after ':'").value;
|
|
1116
|
-
}
|
|
1117
|
-
name = `${name}:${suffix}`;
|
|
1118
|
-
}
|
|
448
|
+
const val = this.current().value;
|
|
1119
449
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
return
|
|
450
|
+
// source customers: Table<Customer> = read("customers.csv")
|
|
451
|
+
if (val === 'source') {
|
|
452
|
+
return this.parseSourceDeclaration();
|
|
1123
453
|
}
|
|
1124
454
|
|
|
1125
|
-
//
|
|
1126
|
-
if (
|
|
1127
|
-
|
|
1128
|
-
this.expect(TokenType.RBRACE, "Expected '}' after attribute expression");
|
|
1129
|
-
return new AST.JSXAttribute(name, expr, l);
|
|
455
|
+
// pipeline clean_customers = customers |> where(...)
|
|
456
|
+
if (val === 'pipeline') {
|
|
457
|
+
return this.parsePipelineDeclaration();
|
|
1130
458
|
}
|
|
1131
459
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return
|
|
460
|
+
// validate Customer { .email |> contains("@"), ... }
|
|
461
|
+
if (val === 'validate') {
|
|
462
|
+
return this.parseValidateBlock();
|
|
1135
463
|
}
|
|
1136
464
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const children = [];
|
|
1142
|
-
|
|
1143
|
-
while (!this.isAtEnd()) {
|
|
1144
|
-
// Closing tag: </tag>
|
|
1145
|
-
if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.SLASH) {
|
|
1146
|
-
this.advance(); // <
|
|
1147
|
-
this.advance(); // /
|
|
1148
|
-
const closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
|
|
1149
|
-
if (closeTag !== parentTag) {
|
|
1150
|
-
this.error(`Mismatched closing tag: expected </${parentTag}>, got </${closeTag}>`);
|
|
1151
|
-
}
|
|
1152
|
-
this.expect(TokenType.GREATER, "Expected '>' in closing tag");
|
|
1153
|
-
break;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// Nested element
|
|
1157
|
-
if (this.check(TokenType.LESS)) {
|
|
1158
|
-
children.push(this.parseJSXElement());
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// String literal as text
|
|
1163
|
-
if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
1164
|
-
const str = this.parseStringLiteral();
|
|
1165
|
-
children.push(new AST.JSXText(str, this.loc()));
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Unquoted JSX text
|
|
1170
|
-
if (this.check(TokenType.JSX_TEXT)) {
|
|
1171
|
-
const tok = this.advance();
|
|
1172
|
-
const text = this._collapseJSXWhitespace(tok.value);
|
|
1173
|
-
if (text.length > 0) {
|
|
1174
|
-
children.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
1175
|
-
}
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Expression in braces: {expr}
|
|
1180
|
-
if (this.check(TokenType.LBRACE)) {
|
|
1181
|
-
this.advance();
|
|
1182
|
-
const expr = this.parseExpression();
|
|
1183
|
-
this.expect(TokenType.RBRACE, "Expected '}' after JSX expression");
|
|
1184
|
-
children.push(new AST.JSXExpression(expr, this.loc()));
|
|
1185
|
-
continue;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// for loop inside JSX
|
|
1189
|
-
if (this.check(TokenType.FOR)) {
|
|
1190
|
-
children.push(this.parseJSXFor());
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// if inside JSX
|
|
1195
|
-
if (this.check(TokenType.IF)) {
|
|
1196
|
-
children.push(this.parseJSXIf());
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
break;
|
|
465
|
+
// refresh customers every 15.minutes
|
|
466
|
+
// refresh orders on_demand
|
|
467
|
+
if (val === 'refresh') {
|
|
468
|
+
return this.parseRefreshPolicy();
|
|
1201
469
|
}
|
|
1202
470
|
|
|
1203
|
-
return
|
|
471
|
+
return this.parseStatement();
|
|
1204
472
|
}
|
|
1205
473
|
|
|
1206
|
-
|
|
474
|
+
parseSourceDeclaration() {
|
|
1207
475
|
const l = this.loc();
|
|
1208
|
-
this.
|
|
1209
|
-
|
|
1210
|
-
// Support destructuring: for [i, item] in ..., for {name, age} in ...
|
|
1211
|
-
let variable;
|
|
1212
|
-
if (this.check(TokenType.LBRACKET)) {
|
|
1213
|
-
// Array destructuring: [a, b]
|
|
1214
|
-
this.advance(); // consume [
|
|
1215
|
-
const elements = [];
|
|
1216
|
-
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
1217
|
-
elements.push(this.expect(TokenType.IDENTIFIER, "Expected variable name in array pattern").value);
|
|
1218
|
-
if (!this.match(TokenType.COMMA)) break;
|
|
1219
|
-
}
|
|
1220
|
-
this.expect(TokenType.RBRACKET, "Expected ']' in destructuring pattern");
|
|
1221
|
-
variable = `[${elements.join(', ')}]`;
|
|
1222
|
-
} else if (this.check(TokenType.LBRACE)) {
|
|
1223
|
-
// Object destructuring: {name, age}
|
|
1224
|
-
this.advance(); // consume {
|
|
1225
|
-
const props = [];
|
|
1226
|
-
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
1227
|
-
props.push(this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value);
|
|
1228
|
-
if (!this.match(TokenType.COMMA)) break;
|
|
1229
|
-
}
|
|
1230
|
-
this.expect(TokenType.RBRACE, "Expected '}' in destructuring pattern");
|
|
1231
|
-
variable = `{${props.join(', ')}}`;
|
|
1232
|
-
} else {
|
|
1233
|
-
variable = this.expect(TokenType.IDENTIFIER, "Expected loop variable").value;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
this.expect(TokenType.IN, "Expected 'in' in for loop");
|
|
1237
|
-
const iterable = this.parseExpression();
|
|
476
|
+
this.advance(); // consume 'source'
|
|
477
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected source name").value;
|
|
1238
478
|
|
|
1239
|
-
// Optional
|
|
1240
|
-
let
|
|
1241
|
-
if (this.
|
|
1242
|
-
this.
|
|
1243
|
-
this.expect(TokenType.ASSIGN, "Expected '=' after 'key'");
|
|
1244
|
-
this.expect(TokenType.LBRACE, "Expected '{' after 'key='");
|
|
1245
|
-
keyExpr = this.parseExpression();
|
|
1246
|
-
this.expect(TokenType.RBRACE, "Expected '}' after key expression");
|
|
479
|
+
// Optional type annotation: source customers: Table<Customer>
|
|
480
|
+
let typeAnnotation = null;
|
|
481
|
+
if (this.match(TokenType.COLON)) {
|
|
482
|
+
typeAnnotation = this.parseTypeAnnotation();
|
|
1247
483
|
}
|
|
1248
484
|
|
|
1249
|
-
this.expect(TokenType.
|
|
485
|
+
this.expect(TokenType.ASSIGN, "Expected '=' after source name");
|
|
486
|
+
const expression = this.parseExpression();
|
|
1250
487
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (this.check(TokenType.LESS)) {
|
|
1254
|
-
body.push(this.parseJSXElement());
|
|
1255
|
-
} else if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
|
|
1256
|
-
body.push(new AST.JSXText(this.parseStringLiteral(), this.loc()));
|
|
1257
|
-
} else if (this.check(TokenType.JSX_TEXT)) {
|
|
1258
|
-
const tok = this.advance();
|
|
1259
|
-
const text = this._collapseJSXWhitespace(tok.value);
|
|
1260
|
-
if (text.length > 0) {
|
|
1261
|
-
body.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
1262
|
-
}
|
|
1263
|
-
} else if (this.check(TokenType.LBRACE)) {
|
|
1264
|
-
this.advance();
|
|
1265
|
-
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
1266
|
-
this.expect(TokenType.RBRACE);
|
|
1267
|
-
} else {
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close JSX for body");
|
|
488
|
+
return new AST.SourceDeclaration(name, typeAnnotation, expression, l);
|
|
489
|
+
}
|
|
1272
490
|
|
|
1273
|
-
|
|
491
|
+
parsePipelineDeclaration() {
|
|
492
|
+
const l = this.loc();
|
|
493
|
+
this.advance(); // consume 'pipeline'
|
|
494
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected pipeline name").value;
|
|
495
|
+
this.expect(TokenType.ASSIGN, "Expected '=' after pipeline name");
|
|
496
|
+
const expression = this.parseExpression();
|
|
497
|
+
return new AST.PipelineDeclaration(name, expression, l);
|
|
1274
498
|
}
|
|
1275
499
|
|
|
1276
|
-
|
|
1277
|
-
const
|
|
500
|
+
parseValidateBlock() {
|
|
501
|
+
const l = this.loc();
|
|
502
|
+
this.advance(); // consume 'validate'
|
|
503
|
+
const typeName = this.expect(TokenType.IDENTIFIER, "Expected type name after 'validate'").value;
|
|
504
|
+
this.expect(TokenType.LBRACE, "Expected '{' after validate type name");
|
|
505
|
+
|
|
506
|
+
const rules = [];
|
|
1278
507
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
body.push(new AST.JSXText(this.parseStringLiteral(), this.loc()));
|
|
1283
|
-
} else if (this.check(TokenType.JSX_TEXT)) {
|
|
1284
|
-
const tok = this.advance();
|
|
1285
|
-
const text = this._collapseJSXWhitespace(tok.value);
|
|
1286
|
-
if (text.length > 0) {
|
|
1287
|
-
body.push(new AST.JSXText(new AST.StringLiteral(text, this.loc()), this.loc()));
|
|
1288
|
-
}
|
|
1289
|
-
} else if (this.check(TokenType.LBRACE)) {
|
|
1290
|
-
this.advance();
|
|
1291
|
-
body.push(new AST.JSXExpression(this.parseExpression(), this.loc()));
|
|
1292
|
-
this.expect(TokenType.RBRACE);
|
|
1293
|
-
} else {
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
508
|
+
const rule = this.parseExpression();
|
|
509
|
+
rules.push(rule);
|
|
510
|
+
this.match(TokenType.COMMA); // optional comma separator
|
|
1296
511
|
}
|
|
1297
|
-
|
|
512
|
+
|
|
513
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close validate block");
|
|
514
|
+
return new AST.ValidateBlock(typeName, rules, l);
|
|
1298
515
|
}
|
|
1299
516
|
|
|
1300
|
-
|
|
517
|
+
parseRefreshPolicy() {
|
|
1301
518
|
const l = this.loc();
|
|
1302
|
-
this.
|
|
1303
|
-
const
|
|
1304
|
-
this.expect(TokenType.LBRACE, "Expected '{' in JSX if body");
|
|
1305
|
-
const consequent = this._parseJSXIfBody();
|
|
1306
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close JSX if body");
|
|
519
|
+
this.advance(); // consume 'refresh'
|
|
520
|
+
const sourceName = this.expect(TokenType.IDENTIFIER, "Expected source name after 'refresh'").value;
|
|
1307
521
|
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const elifCond = this.parseExpression();
|
|
1313
|
-
this.expect(TokenType.LBRACE, "Expected '{' in JSX elif body");
|
|
1314
|
-
const elifBody = this._parseJSXIfBody();
|
|
1315
|
-
this.expect(TokenType.RBRACE, "Expected '}' to close JSX elif body");
|
|
1316
|
-
alternates.push({ condition: elifCond, body: elifBody });
|
|
522
|
+
// refresh X every N.unit OR refresh X on_demand
|
|
523
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'on_demand') {
|
|
524
|
+
this.advance();
|
|
525
|
+
return new AST.RefreshPolicy(sourceName, 'on_demand', l);
|
|
1317
526
|
}
|
|
1318
527
|
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
this.advance();
|
|
1323
|
-
this.expect(TokenType.LBRACE);
|
|
1324
|
-
alternate = this._parseJSXIfBody();
|
|
1325
|
-
this.expect(TokenType.RBRACE);
|
|
528
|
+
// expect 'every'
|
|
529
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().value === 'every') {
|
|
530
|
+
this.advance(); // consume 'every'
|
|
1326
531
|
}
|
|
1327
532
|
|
|
1328
|
-
|
|
533
|
+
// Parse interval: N.unit (e.g., 15.minutes, 1.hour)
|
|
534
|
+
const value = this.expect(TokenType.NUMBER, "Expected interval value").value;
|
|
535
|
+
this.expect(TokenType.DOT, "Expected '.' after interval value");
|
|
536
|
+
const unit = this.expect(TokenType.IDENTIFIER, "Expected time unit (minutes, hours, seconds)").value;
|
|
537
|
+
|
|
538
|
+
return new AST.RefreshPolicy(sourceName, { value, unit }, l);
|
|
1329
539
|
}
|
|
1330
540
|
|
|
541
|
+
// Client-specific statements and JSX parsing are in client-parser.js (lazy-loaded)
|
|
542
|
+
|
|
1331
543
|
// ─── Statements ───────────────────────────────────────────
|
|
1332
544
|
|
|
1333
545
|
parseStatement() {
|
|
1334
546
|
// pub modifier: pub fn, pub type, pub x = ...
|
|
1335
547
|
if (this.check(TokenType.PUB)) return this.parsePubDeclaration();
|
|
548
|
+
if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FOR) {
|
|
549
|
+
this.advance(); // consume async
|
|
550
|
+
return this.parseForStatement(null, true);
|
|
551
|
+
}
|
|
1336
552
|
if (this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN) return this.parseAsyncFunctionDeclaration();
|
|
1337
553
|
if (this.check(TokenType.FN) && this.peek(1).type === TokenType.IDENTIFIER) return this.parseFunctionDeclaration();
|
|
1338
554
|
if (this.check(TokenType.TYPE)) return this.parseTypeDeclaration();
|
|
555
|
+
if (this.check(TokenType.MUT)) this.error("'mut' is not supported in Tova. Use 'var' for mutable variables");
|
|
1339
556
|
if (this.check(TokenType.VAR)) return this.parseVarDeclaration();
|
|
1340
557
|
if (this.check(TokenType.LET)) return this.parseLetDestructure();
|
|
1341
558
|
if (this.check(TokenType.IF)) return this.parseIfStatement();
|
|
1342
559
|
if (this.check(TokenType.FOR)) return this.parseForStatement();
|
|
1343
560
|
if (this.check(TokenType.WHILE)) return this.parseWhileStatement();
|
|
561
|
+
if (this.check(TokenType.LOOP)) return this.parseLoopStatement();
|
|
1344
562
|
if (this.check(TokenType.RETURN)) return this.parseReturnStatement();
|
|
1345
563
|
if (this.check(TokenType.IMPORT)) return this.parseImport();
|
|
1346
564
|
if (this.check(TokenType.MATCH)) return this.parseMatchAsStatement();
|
|
@@ -1352,8 +570,21 @@ export class Parser {
|
|
|
1352
570
|
if (this.check(TokenType.IMPL)) return this.parseImplDeclaration();
|
|
1353
571
|
if (this.check(TokenType.TRAIT)) return this.parseTraitDeclaration();
|
|
1354
572
|
if (this.check(TokenType.DEFER)) return this.parseDeferStatement();
|
|
573
|
+
if (this.check(TokenType.WITH)) return this.parseWithStatement();
|
|
1355
574
|
if (this.check(TokenType.EXTERN)) return this.parseExternDeclaration();
|
|
1356
575
|
|
|
576
|
+
// Labeled loops: name: for/while/loop
|
|
577
|
+
if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.COLON) {
|
|
578
|
+
const afterColon = this.peek(2).type;
|
|
579
|
+
if (afterColon === TokenType.FOR || afterColon === TokenType.WHILE || afterColon === TokenType.LOOP) {
|
|
580
|
+
const label = this.advance().value; // consume identifier
|
|
581
|
+
this.advance(); // consume colon
|
|
582
|
+
if (this.check(TokenType.FOR)) return this.parseForStatement(label);
|
|
583
|
+
if (this.check(TokenType.WHILE)) return this.parseWhileStatement(label);
|
|
584
|
+
if (this.check(TokenType.LOOP)) return this.parseLoopStatement(label);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
1357
588
|
return this.parseExpressionOrAssignment();
|
|
1358
589
|
}
|
|
1359
590
|
|
|
@@ -1449,6 +680,16 @@ export class Parser {
|
|
|
1449
680
|
return new AST.DeferStatement(body, l);
|
|
1450
681
|
}
|
|
1451
682
|
|
|
683
|
+
parseWithStatement() {
|
|
684
|
+
const l = this.loc();
|
|
685
|
+
this.expect(TokenType.WITH);
|
|
686
|
+
const expression = this.parseExpression();
|
|
687
|
+
this.expect(TokenType.AS, "Expected 'as' after with expression");
|
|
688
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected variable name after 'as'").value;
|
|
689
|
+
const body = this.parseBlock();
|
|
690
|
+
return new AST.WithStatement(expression, name, body, l);
|
|
691
|
+
}
|
|
692
|
+
|
|
1452
693
|
parseExternDeclaration() {
|
|
1453
694
|
const l = this.loc();
|
|
1454
695
|
this.expect(TokenType.EXTERN);
|
|
@@ -1472,6 +713,18 @@ export class Parser {
|
|
|
1472
713
|
const l = this.loc();
|
|
1473
714
|
this.expect(TokenType.FN);
|
|
1474
715
|
const name = this.expect(TokenType.IDENTIFIER, "Expected function name").value;
|
|
716
|
+
|
|
717
|
+
// Parse optional type parameters: fn name<T, U>(...)
|
|
718
|
+
let typeParams = [];
|
|
719
|
+
if (this.check(TokenType.LESS)) {
|
|
720
|
+
this.advance(); // consume <
|
|
721
|
+
while (!this.check(TokenType.GREATER) && !this.isAtEnd()) {
|
|
722
|
+
typeParams.push(this.expect(TokenType.IDENTIFIER, "Expected type parameter name").value);
|
|
723
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
724
|
+
}
|
|
725
|
+
this.expect(TokenType.GREATER, "Expected '>' after type parameters");
|
|
726
|
+
}
|
|
727
|
+
|
|
1475
728
|
this.expect(TokenType.LPAREN, "Expected '(' after function name");
|
|
1476
729
|
const params = this.parseParameterList();
|
|
1477
730
|
this.expect(TokenType.RPAREN, "Expected ')' after parameters");
|
|
@@ -1482,7 +735,7 @@ export class Parser {
|
|
|
1482
735
|
}
|
|
1483
736
|
|
|
1484
737
|
const body = this.parseBlock();
|
|
1485
|
-
return new AST.FunctionDeclaration(name, params, body, returnType, l);
|
|
738
|
+
return new AST.FunctionDeclaration(name, params, body, returnType, l, false, typeParams);
|
|
1486
739
|
}
|
|
1487
740
|
|
|
1488
741
|
parseAsyncFunctionDeclaration() {
|
|
@@ -1490,6 +743,18 @@ export class Parser {
|
|
|
1490
743
|
this.expect(TokenType.ASYNC);
|
|
1491
744
|
this.expect(TokenType.FN);
|
|
1492
745
|
const name = this.expect(TokenType.IDENTIFIER, "Expected function name").value;
|
|
746
|
+
|
|
747
|
+
// Parse optional type parameters: async fn name<T, U>(...)
|
|
748
|
+
let typeParams = [];
|
|
749
|
+
if (this.check(TokenType.LESS)) {
|
|
750
|
+
this.advance(); // consume <
|
|
751
|
+
while (!this.check(TokenType.GREATER) && !this.isAtEnd()) {
|
|
752
|
+
typeParams.push(this.expect(TokenType.IDENTIFIER, "Expected type parameter name").value);
|
|
753
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
754
|
+
}
|
|
755
|
+
this.expect(TokenType.GREATER, "Expected '>' after type parameters");
|
|
756
|
+
}
|
|
757
|
+
|
|
1493
758
|
this.expect(TokenType.LPAREN, "Expected '(' after function name");
|
|
1494
759
|
const params = this.parseParameterList();
|
|
1495
760
|
this.expect(TokenType.RPAREN, "Expected ')' after parameters");
|
|
@@ -1500,19 +765,29 @@ export class Parser {
|
|
|
1500
765
|
}
|
|
1501
766
|
|
|
1502
767
|
const body = this.parseBlock();
|
|
1503
|
-
return new AST.FunctionDeclaration(name, params, body, returnType, l, true);
|
|
768
|
+
return new AST.FunctionDeclaration(name, params, body, returnType, l, true, typeParams);
|
|
1504
769
|
}
|
|
1505
770
|
|
|
1506
771
|
parseBreakStatement() {
|
|
1507
772
|
const l = this.loc();
|
|
1508
773
|
this.expect(TokenType.BREAK);
|
|
1509
|
-
|
|
774
|
+
// Optional label: break outer
|
|
775
|
+
let label = null;
|
|
776
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().line === l.line) {
|
|
777
|
+
label = this.advance().value;
|
|
778
|
+
}
|
|
779
|
+
return new AST.BreakStatement(l, label);
|
|
1510
780
|
}
|
|
1511
781
|
|
|
1512
782
|
parseContinueStatement() {
|
|
1513
783
|
const l = this.loc();
|
|
1514
784
|
this.expect(TokenType.CONTINUE);
|
|
1515
|
-
|
|
785
|
+
// Optional label: continue outer
|
|
786
|
+
let label = null;
|
|
787
|
+
if (this.check(TokenType.IDENTIFIER) && this.current().line === l.line) {
|
|
788
|
+
label = this.advance().value;
|
|
789
|
+
}
|
|
790
|
+
return new AST.ContinueStatement(l, label);
|
|
1516
791
|
}
|
|
1517
792
|
|
|
1518
793
|
parseGuardStatement() {
|
|
@@ -1552,7 +827,7 @@ export class Parser {
|
|
|
1552
827
|
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
1553
828
|
const l = this.loc();
|
|
1554
829
|
|
|
1555
|
-
// Destructuring pattern parameter: {name, email} or [
|
|
830
|
+
// Destructuring pattern parameter: {name, email}: User or [head, ...tail]
|
|
1556
831
|
if (this.check(TokenType.LBRACE)) {
|
|
1557
832
|
this.advance();
|
|
1558
833
|
const properties = [];
|
|
@@ -1573,11 +848,22 @@ export class Parser {
|
|
|
1573
848
|
const pattern = new AST.ObjectPattern(properties, l);
|
|
1574
849
|
const param = new AST.Parameter(null, null, null, l);
|
|
1575
850
|
param.destructure = pattern;
|
|
851
|
+
// Optional type annotation after destructure: {name, age}: User
|
|
852
|
+
if (this.match(TokenType.COLON)) {
|
|
853
|
+
param.typeAnnotation = this.parseTypeAnnotation();
|
|
854
|
+
}
|
|
1576
855
|
params.push(param);
|
|
1577
856
|
} else if (this.check(TokenType.LBRACKET)) {
|
|
1578
857
|
this.advance();
|
|
1579
858
|
const elements = [];
|
|
1580
859
|
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
860
|
+
// Support spread in array destructure: [head, ...tail]
|
|
861
|
+
if (this.check(TokenType.SPREAD)) {
|
|
862
|
+
this.advance(); // consume ...
|
|
863
|
+
const restName = this.expect(TokenType.IDENTIFIER, "Expected identifier after '...'").value;
|
|
864
|
+
elements.push('...' + restName);
|
|
865
|
+
break; // rest must be last
|
|
866
|
+
}
|
|
1581
867
|
elements.push(this.expect(TokenType.IDENTIFIER, "Expected element name").value);
|
|
1582
868
|
if (!this.match(TokenType.COMMA)) break;
|
|
1583
869
|
}
|
|
@@ -1585,6 +871,10 @@ export class Parser {
|
|
|
1585
871
|
const pattern = new AST.ArrayPattern(elements, l);
|
|
1586
872
|
const param = new AST.Parameter(null, null, null, l);
|
|
1587
873
|
param.destructure = pattern;
|
|
874
|
+
// Optional type annotation after destructure: [head, ...tail]: [Int]
|
|
875
|
+
if (this.match(TokenType.COLON)) {
|
|
876
|
+
param.typeAnnotation = this.parseTypeAnnotation();
|
|
877
|
+
}
|
|
1588
878
|
params.push(param);
|
|
1589
879
|
} else {
|
|
1590
880
|
const name = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
|
|
@@ -1609,15 +899,32 @@ export class Parser {
|
|
|
1609
899
|
|
|
1610
900
|
parseTypeAnnotation() {
|
|
1611
901
|
const l = this.loc();
|
|
902
|
+
const first = this._parseSingleTypeAnnotation();
|
|
903
|
+
|
|
904
|
+
// Union types: Type | Type | Type
|
|
905
|
+
if (this.check(TokenType.BAR)) {
|
|
906
|
+
const members = [first];
|
|
907
|
+
while (this.match(TokenType.BAR)) {
|
|
908
|
+
members.push(this._parseSingleTypeAnnotation());
|
|
909
|
+
}
|
|
910
|
+
return new AST.UnionTypeAnnotation(members, l);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return first;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Parse a single type annotation without union (used as union member)
|
|
917
|
+
_parseSingleTypeAnnotation() {
|
|
918
|
+
const l = this.loc();
|
|
1612
919
|
|
|
1613
920
|
// [Type] — array type shorthand
|
|
1614
921
|
if (this.match(TokenType.LBRACKET)) {
|
|
1615
|
-
const elementType = this.
|
|
922
|
+
const elementType = this._parseSingleTypeAnnotation();
|
|
1616
923
|
this.expect(TokenType.RBRACKET, "Expected ']' in array type");
|
|
1617
924
|
return new AST.ArrayTypeAnnotation(elementType, l);
|
|
1618
925
|
}
|
|
1619
926
|
|
|
1620
|
-
// (Type, Type) — tuple type or
|
|
927
|
+
// (Type, Type) — tuple type or function type
|
|
1621
928
|
if (this.check(TokenType.LPAREN)) {
|
|
1622
929
|
this.advance();
|
|
1623
930
|
const types = [];
|
|
@@ -1626,7 +933,6 @@ export class Parser {
|
|
|
1626
933
|
if (!this.match(TokenType.COMMA)) break;
|
|
1627
934
|
}
|
|
1628
935
|
this.expect(TokenType.RPAREN, "Expected ')' in type annotation");
|
|
1629
|
-
// Check for -> to distinguish function type from tuple type
|
|
1630
936
|
if (this.match(TokenType.THIN_ARROW)) {
|
|
1631
937
|
const returnType = this.parseTypeAnnotation();
|
|
1632
938
|
return new AST.FunctionTypeAnnotation(types, returnType, l);
|
|
@@ -1636,7 +942,6 @@ export class Parser {
|
|
|
1636
942
|
|
|
1637
943
|
const name = this.expect(TokenType.IDENTIFIER, "Expected type name").value;
|
|
1638
944
|
|
|
1639
|
-
// Generics: Type<A, B>
|
|
1640
945
|
let typeParams = [];
|
|
1641
946
|
if (this.match(TokenType.LESS)) {
|
|
1642
947
|
do {
|
|
@@ -1689,7 +994,26 @@ export class Parser {
|
|
|
1689
994
|
return new AST.RefinementType(name, typeExpr, predicate, l);
|
|
1690
995
|
}
|
|
1691
996
|
|
|
1692
|
-
|
|
997
|
+
// Simple enum syntax: type Color = Red | Green | Blue
|
|
998
|
+
// Detect when the type expression is a union of bare identifiers (PascalCase, no type params)
|
|
999
|
+
// But NOT when any member is a known built-in type (that's a type alias, not an enum)
|
|
1000
|
+
if (typeExpr.type === 'UnionTypeAnnotation') {
|
|
1001
|
+
const builtinTypes = new Set(['String', 'Int', 'Float', 'Bool', 'List', 'Map', 'Set', 'Option', 'Result', 'Any', 'Nil', 'Void', 'Number', 'Array', 'Object', 'Promise', 'Tuple']);
|
|
1002
|
+
const isSimpleEnum = typeExpr.members.every(m =>
|
|
1003
|
+
m.type === 'TypeAnnotation' && m.typeParams.length === 0 && /^[A-Z]/.test(m.name)
|
|
1004
|
+
);
|
|
1005
|
+
const hasBuiltinType = typeExpr.members.some(m =>
|
|
1006
|
+
m.type === 'TypeAnnotation' && builtinTypes.has(m.name)
|
|
1007
|
+
);
|
|
1008
|
+
if (isSimpleEnum && !hasBuiltinType) {
|
|
1009
|
+
const variants = typeExpr.members.map(m =>
|
|
1010
|
+
new AST.TypeVariant(m.name, [], m.loc)
|
|
1011
|
+
);
|
|
1012
|
+
return new AST.TypeDeclaration(name, typeParams, variants, l);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return new AST.TypeAlias(name, typeParams, typeExpr, l);
|
|
1693
1017
|
}
|
|
1694
1018
|
|
|
1695
1019
|
this.expect(TokenType.LBRACE, "Expected '{' to open type body");
|
|
@@ -1773,6 +1097,9 @@ export class Parser {
|
|
|
1773
1097
|
} else if (this.check(TokenType.LPAREN)) {
|
|
1774
1098
|
// Tuple destructuring: let (a, b) = expr
|
|
1775
1099
|
pattern = this.parseTuplePattern();
|
|
1100
|
+
} else if (this.check(TokenType.IDENTIFIER)) {
|
|
1101
|
+
const name = this.current().value;
|
|
1102
|
+
this.error(`Use '${name} = value' for binding or 'var ${name} = value' for mutable. 'let' is only for destructuring: let {a, b} = obj`);
|
|
1776
1103
|
} else {
|
|
1777
1104
|
this.error("Expected '{', '[', or '(' after 'let' for destructuring");
|
|
1778
1105
|
}
|
|
@@ -1846,8 +1173,14 @@ export class Parser {
|
|
|
1846
1173
|
const consequent = this.parseBlock();
|
|
1847
1174
|
|
|
1848
1175
|
const alternates = [];
|
|
1849
|
-
while (this.check(TokenType.ELIF)
|
|
1850
|
-
|
|
1176
|
+
while (this.check(TokenType.ELIF) ||
|
|
1177
|
+
(this.check(TokenType.ELSE) && this.peek(1).type === TokenType.IF)) {
|
|
1178
|
+
if (this.check(TokenType.ELIF)) {
|
|
1179
|
+
this.advance();
|
|
1180
|
+
} else {
|
|
1181
|
+
this.advance(); // else
|
|
1182
|
+
this.advance(); // if
|
|
1183
|
+
}
|
|
1851
1184
|
const elifCond = this.parseExpression();
|
|
1852
1185
|
const elifBody = this.parseBlock();
|
|
1853
1186
|
alternates.push({ condition: elifCond, body: elifBody });
|
|
@@ -1861,7 +1194,7 @@ export class Parser {
|
|
|
1861
1194
|
return new AST.IfStatement(condition, consequent, alternates, elseBody, l);
|
|
1862
1195
|
}
|
|
1863
1196
|
|
|
1864
|
-
parseForStatement() {
|
|
1197
|
+
parseForStatement(label = null, isAsync = false) {
|
|
1865
1198
|
const l = this.loc();
|
|
1866
1199
|
this.expect(TokenType.FOR);
|
|
1867
1200
|
|
|
@@ -1899,6 +1232,13 @@ export class Parser {
|
|
|
1899
1232
|
|
|
1900
1233
|
this.expect(TokenType.IN, "Expected 'in' after for variable");
|
|
1901
1234
|
const iterable = this.parseExpression();
|
|
1235
|
+
|
|
1236
|
+
// Optional when guard: for user in users when user.active { ... }
|
|
1237
|
+
let guard = null;
|
|
1238
|
+
if (this.match(TokenType.WHEN)) {
|
|
1239
|
+
guard = this.parseExpression();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1902
1242
|
const body = this.parseBlock();
|
|
1903
1243
|
|
|
1904
1244
|
let elseBody = null;
|
|
@@ -1906,15 +1246,22 @@ export class Parser {
|
|
|
1906
1246
|
elseBody = this.parseBlock();
|
|
1907
1247
|
}
|
|
1908
1248
|
|
|
1909
|
-
return new AST.ForStatement(variable, iterable, body, elseBody, l);
|
|
1249
|
+
return new AST.ForStatement(variable, iterable, body, elseBody, l, guard, label, isAsync);
|
|
1910
1250
|
}
|
|
1911
1251
|
|
|
1912
|
-
parseWhileStatement() {
|
|
1252
|
+
parseWhileStatement(label = null) {
|
|
1913
1253
|
const l = this.loc();
|
|
1914
1254
|
this.expect(TokenType.WHILE);
|
|
1915
1255
|
const condition = this.parseExpression();
|
|
1916
1256
|
const body = this.parseBlock();
|
|
1917
|
-
return new AST.WhileStatement(condition, body, l);
|
|
1257
|
+
return new AST.WhileStatement(condition, body, l, label);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
parseLoopStatement(label = null) {
|
|
1261
|
+
const l = this.loc();
|
|
1262
|
+
this.expect(TokenType.LOOP);
|
|
1263
|
+
const body = this.parseBlock();
|
|
1264
|
+
return new AST.LoopStatement(body, label, l);
|
|
1918
1265
|
}
|
|
1919
1266
|
|
|
1920
1267
|
parseTryCatch() {
|
|
@@ -2049,6 +1396,23 @@ export class Parser {
|
|
|
2049
1396
|
const value = this.parseExpression();
|
|
2050
1397
|
return new AST.Assignment([expr], [value], l);
|
|
2051
1398
|
}
|
|
1399
|
+
// Destructuring without let: {name, age} = user or [a, b] = list
|
|
1400
|
+
if (expr.type === 'ObjectLiteral') {
|
|
1401
|
+
const pattern = new AST.ObjectPattern(
|
|
1402
|
+
expr.properties.map(p => ({ key: typeof p.key === 'string' ? p.key : p.key.name || p.key, value: typeof p.key === 'string' ? p.key : p.key.name || p.key })),
|
|
1403
|
+
expr.loc
|
|
1404
|
+
);
|
|
1405
|
+
const value = this.parseExpression();
|
|
1406
|
+
return new AST.LetDestructure(pattern, value, l);
|
|
1407
|
+
}
|
|
1408
|
+
if (expr.type === 'ArrayLiteral') {
|
|
1409
|
+
const pattern = new AST.ArrayPattern(
|
|
1410
|
+
expr.elements.map(e => e.type === 'Identifier' ? e.name : '_'),
|
|
1411
|
+
expr.loc
|
|
1412
|
+
);
|
|
1413
|
+
const value = this.parseExpression();
|
|
1414
|
+
return new AST.LetDestructure(pattern, value, l);
|
|
1415
|
+
}
|
|
2052
1416
|
this.error("Invalid assignment target");
|
|
2053
1417
|
}
|
|
2054
1418
|
|
|
@@ -2077,8 +1441,14 @@ export class Parser {
|
|
|
2077
1441
|
const consequent = this.parseBlock();
|
|
2078
1442
|
|
|
2079
1443
|
const alternates = [];
|
|
2080
|
-
while (this.check(TokenType.ELIF)
|
|
2081
|
-
|
|
1444
|
+
while (this.check(TokenType.ELIF) ||
|
|
1445
|
+
(this.check(TokenType.ELSE) && this.peek(1).type === TokenType.IF)) {
|
|
1446
|
+
if (this.check(TokenType.ELIF)) {
|
|
1447
|
+
this.advance();
|
|
1448
|
+
} else {
|
|
1449
|
+
this.advance(); // else
|
|
1450
|
+
this.advance(); // if
|
|
1451
|
+
}
|
|
2082
1452
|
const elifCond = this.parseExpression();
|
|
2083
1453
|
const elifBody = this.parseBlock();
|
|
2084
1454
|
alternates.push({ condition: elifCond, body: elifBody });
|
|
@@ -2209,6 +1579,19 @@ export class Parser {
|
|
|
2209
1579
|
parseMembership() {
|
|
2210
1580
|
let left = this.parseRange();
|
|
2211
1581
|
|
|
1582
|
+
// "is" / "is not" — type checking: value is String, value is not Nil
|
|
1583
|
+
if (this.check(TokenType.IS)) {
|
|
1584
|
+
const l = this.loc();
|
|
1585
|
+
this.advance(); // is
|
|
1586
|
+
let negated = false;
|
|
1587
|
+
if (this.check(TokenType.NOT)) {
|
|
1588
|
+
this.advance(); // not
|
|
1589
|
+
negated = true;
|
|
1590
|
+
}
|
|
1591
|
+
const typeName = this.expect(TokenType.IDENTIFIER, "Expected type name after 'is'").value;
|
|
1592
|
+
return new AST.IsExpression(left, typeName, negated, l);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
2212
1595
|
// "in" / "not in"
|
|
2213
1596
|
if (this.check(TokenType.NOT) && this.peek(1).type === TokenType.IN) {
|
|
2214
1597
|
const l = this.loc();
|
|
@@ -2331,6 +1714,12 @@ export class Parser {
|
|
|
2331
1714
|
if (this.check(TokenType.DOT)) {
|
|
2332
1715
|
const l = this.loc();
|
|
2333
1716
|
this.advance();
|
|
1717
|
+
// Tuple index access: t.0, t.1, etc.
|
|
1718
|
+
if (this.check(TokenType.NUMBER) && Number.isInteger(this.current().value) && this.current().value >= 0) {
|
|
1719
|
+
const idx = this.advance().value;
|
|
1720
|
+
expr = new AST.MemberExpression(expr, new AST.NumberLiteral(idx, l), true, l);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
2334
1723
|
const prop = this.expect(TokenType.IDENTIFIER, "Expected property name after '.'").value;
|
|
2335
1724
|
expr = new AST.MemberExpression(expr, prop, false, l);
|
|
2336
1725
|
continue;
|
|
@@ -2452,9 +1841,9 @@ export class Parser {
|
|
|
2452
1841
|
const name = this.advance().value;
|
|
2453
1842
|
this.advance(); // :
|
|
2454
1843
|
const value = this.parseExpression();
|
|
2455
|
-
args.push(new AST.NamedArgument(name, value, this.loc()));
|
|
1844
|
+
args.push(new AST.NamedArgument(name, this._maybeWrapItLambda(value), this.loc()));
|
|
2456
1845
|
} else {
|
|
2457
|
-
args.push(this.parseExpression());
|
|
1846
|
+
args.push(this._maybeWrapItLambda(this.parseExpression()));
|
|
2458
1847
|
}
|
|
2459
1848
|
if (!this.match(TokenType.COMMA)) break;
|
|
2460
1849
|
}
|
|
@@ -2566,8 +1955,8 @@ export class Parser {
|
|
|
2566
1955
|
// Identifier (or arrow lambda: x => expr)
|
|
2567
1956
|
if (this.check(TokenType.IDENTIFIER)) {
|
|
2568
1957
|
const name = this.advance().value;
|
|
2569
|
-
// Check for arrow lambda: x => expr
|
|
2570
|
-
if (this.check(TokenType.ARROW)) {
|
|
1958
|
+
// Check for arrow lambda: x => expr or x -> expr
|
|
1959
|
+
if (this.check(TokenType.ARROW) || this.check(TokenType.THIN_ARROW)) {
|
|
2571
1960
|
this.advance();
|
|
2572
1961
|
const body = this.parseExpression();
|
|
2573
1962
|
return new AST.LambdaExpression(
|
|
@@ -2951,10 +2340,10 @@ export class Parser {
|
|
|
2951
2340
|
|
|
2952
2341
|
this.expect(TokenType.LPAREN);
|
|
2953
2342
|
|
|
2954
|
-
// Empty parens: () => expr
|
|
2343
|
+
// Empty parens: () => expr or () -> expr
|
|
2955
2344
|
if (this.check(TokenType.RPAREN)) {
|
|
2956
2345
|
this.advance();
|
|
2957
|
-
if (this.check(TokenType.ARROW)) {
|
|
2346
|
+
if (this.check(TokenType.ARROW) || this.check(TokenType.THIN_ARROW)) {
|
|
2958
2347
|
this.advance();
|
|
2959
2348
|
const body = this.parseExpression();
|
|
2960
2349
|
return new AST.LambdaExpression([], body, l);
|
|
@@ -2997,14 +2386,18 @@ export class Parser {
|
|
|
2997
2386
|
|
|
2998
2387
|
if (isLambda && this.check(TokenType.RPAREN)) {
|
|
2999
2388
|
this.advance(); // )
|
|
3000
|
-
if (this.check(TokenType.ARROW)) {
|
|
2389
|
+
if (this.check(TokenType.ARROW) || this.check(TokenType.THIN_ARROW)) {
|
|
3001
2390
|
this.advance(); // =>
|
|
3002
2391
|
const body = this.check(TokenType.LBRACE) ? this.parseBlock() : this.parseExpression();
|
|
3003
2392
|
return new AST.LambdaExpression(params, body, l);
|
|
3004
2393
|
}
|
|
2394
|
+
// Helpful hint: user may have typed = instead of -> or =>
|
|
2395
|
+
if (this.check(TokenType.ASSIGN) || this.check(TokenType.EQUAL)) {
|
|
2396
|
+
this.error("Use '->' or '=>' for arrow functions: (x, y) -> expr");
|
|
2397
|
+
}
|
|
3005
2398
|
}
|
|
3006
2399
|
} catch (e) {
|
|
3007
|
-
//
|
|
2400
|
+
// Speculative parse failure — expected during backtracking, not a real error
|
|
3008
2401
|
}
|
|
3009
2402
|
|
|
3010
2403
|
// Backtrack and parse as parenthesized expression or tuple
|
|
@@ -3028,4 +2421,38 @@ export class Parser {
|
|
|
3028
2421
|
this.expect(TokenType.RPAREN, "Expected ')'");
|
|
3029
2422
|
return expr;
|
|
3030
2423
|
}
|
|
2424
|
+
|
|
2425
|
+
// ─── Implicit `it` parameter support ─────────────────────
|
|
2426
|
+
|
|
2427
|
+
_containsFreeIt(node) {
|
|
2428
|
+
if (!node) return false;
|
|
2429
|
+
if (node.type === 'Identifier' && node.name === 'it') return true;
|
|
2430
|
+
if (node.type === 'LambdaExpression' || node.type === 'FunctionDeclaration') return false;
|
|
2431
|
+
for (const key of Object.keys(node)) {
|
|
2432
|
+
if (key === 'loc' || key === 'type') continue;
|
|
2433
|
+
const val = node[key];
|
|
2434
|
+
if (Array.isArray(val)) {
|
|
2435
|
+
for (const item of val) {
|
|
2436
|
+
if (item && typeof item === 'object' && this._containsFreeIt(item)) return true;
|
|
2437
|
+
}
|
|
2438
|
+
} else if (val && typeof val === 'object' && val.type) {
|
|
2439
|
+
if (this._containsFreeIt(val)) return true;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
_maybeWrapItLambda(node) {
|
|
2446
|
+
if (node.type === 'Identifier' && node.name === 'it') return node;
|
|
2447
|
+
if (node.type === 'LambdaExpression') return node;
|
|
2448
|
+
if (node.type === 'FunctionDeclaration') return node;
|
|
2449
|
+
if (this._containsFreeIt(node)) {
|
|
2450
|
+
const loc = node.loc || this.loc();
|
|
2451
|
+
return new AST.LambdaExpression(
|
|
2452
|
+
[new AST.Parameter('it', null, null, loc)],
|
|
2453
|
+
node, loc
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
return node;
|
|
2457
|
+
}
|
|
3031
2458
|
}
|