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.
@@ -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.filter(t => t.type !== TokenType.NEWLINE && t.type !== TokenType.DOCSTRING && t.type !== TokenType.SEMICOLON);
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
- const idx = this.pos + offset;
44
- return idx < this.tokens.length ? this.tokens[idx] : this.tokens[this.tokens.length - 1];
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 = new AST.Program(body);
225
+ combined.partialAST = program;
226
+ if (this.errors.length >= maxErrors) {
227
+ combined.truncated = true;
228
+ }
182
229
  throw combined;
183
230
  }
184
- return new AST.Program(body);
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)) return this.parseServerBlock();
189
- if (this.check(TokenType.CLIENT)) return this.parseClientBlock();
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
- // ─── Full-stack blocks ────────────────────────────────────
229
-
230
- parseServerBlock() {
375
+ parseBenchBlock() {
231
376
  const l = this.loc();
232
- this.expect(TokenType.SERVER);
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 'server'");
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.parseServerStatement();
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 server block");
250
- return new AST.ServerBlock(body, l, name);
393
+ this.expect(TokenType.RBRACE, "Expected '}' to close bench block");
394
+ return new AST.BenchBlock(name, body, l);
251
395
  }
252
396
 
253
- parseClientBlock() {
254
- const l = this.loc();
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
- // ─── JSX-like parsing ─────────────────────────────────────
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
- parseJSXElement() {
425
+ parseDataBlock() {
1062
426
  const l = this.loc();
1063
- this.expect(TokenType.LESS, "Expected '<'");
1064
-
1065
- const tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
1066
-
1067
- // Parse attributes (including spread: {...expr})
1068
- const attributes = [];
1069
- while (!this.check(TokenType.GREATER) && !this.check(TokenType.SLASH) && !this.isAtEnd()) {
1070
- // Check for spread attribute: {...expr}
1071
- if (this.check(TokenType.LBRACE) && this.peek(1).type === TokenType.SPREAD) {
1072
- const sl = this.loc();
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
- // Self-closing tag: />
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
- parseJSXAttribute() {
1098
- const l = this.loc();
1099
- // Accept keywords as attribute names (type, class, for, etc. are valid HTML attributes)
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
- // Handle namespaced attributes: on:click, bind:value, class:active
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
- if (!this.match(TokenType.ASSIGN)) {
1121
- // Boolean attribute: <input disabled />
1122
- return new AST.JSXAttribute(name, new AST.BooleanLiteral(true, l), l);
450
+ // source customers: Table<Customer> = read("customers.csv")
451
+ if (val === 'source') {
452
+ return this.parseSourceDeclaration();
1123
453
  }
1124
454
 
1125
- // Value can be {expression} or "string"
1126
- if (this.match(TokenType.LBRACE)) {
1127
- const expr = this.parseExpression();
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
- if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
1133
- const val = this.parseStringLiteral();
1134
- return new AST.JSXAttribute(name, val, l);
460
+ // validate Customer { .email |> contains("@"), ... }
461
+ if (val === 'validate') {
462
+ return this.parseValidateBlock();
1135
463
  }
1136
464
 
1137
- this.error("Expected attribute value");
1138
- }
1139
-
1140
- parseJSXChildren(parentTag) {
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 children;
471
+ return this.parseStatement();
1204
472
  }
1205
473
 
1206
- parseJSXFor() {
474
+ parseSourceDeclaration() {
1207
475
  const l = this.loc();
1208
- this.expect(TokenType.FOR);
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 key expression: for item in items key={item.id} { ... }
1240
- let keyExpr = null;
1241
- if (this.check(TokenType.IDENTIFIER) && this.current().value === 'key') {
1242
- this.advance(); // consume 'key'
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.LBRACE, "Expected '{' in JSX for body");
485
+ this.expect(TokenType.ASSIGN, "Expected '=' after source name");
486
+ const expression = this.parseExpression();
1250
487
 
1251
- const body = [];
1252
- while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
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
- return new AST.JSXFor(variable, iterable, body, l, keyExpr);
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
- _parseJSXIfBody() {
1277
- const body = [];
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
- if (this.check(TokenType.LESS)) {
1280
- body.push(this.parseJSXElement());
1281
- } else if (this.check(TokenType.STRING) || this.check(TokenType.STRING_TEMPLATE)) {
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
- return body;
512
+
513
+ this.expect(TokenType.RBRACE, "Expected '}' to close validate block");
514
+ return new AST.ValidateBlock(typeName, rules, l);
1298
515
  }
1299
516
 
1300
- parseJSXIf() {
517
+ parseRefreshPolicy() {
1301
518
  const l = this.loc();
1302
- this.expect(TokenType.IF);
1303
- const condition = this.parseExpression();
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
- // Parse elif chains
1309
- const alternates = [];
1310
- while (this.check(TokenType.ELIF)) {
1311
- this.advance(); // consume 'elif'
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
- // Parse optional else
1320
- let alternate = null;
1321
- if (this.check(TokenType.ELSE)) {
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
- return new AST.JSXIf(condition, consequent, alternate, l, alternates);
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
- return new AST.BreakStatement(l);
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
- return new AST.ContinueStatement(l);
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 [a, b]
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.parseTypeAnnotation();
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 (Type, Type) -> ReturnType — function type
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
- return new AST.TypeAlias(name, typeExpr, l);
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
- this.advance();
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
- this.advance();
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
- // Not a lambda, backtrack
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
  }